A couple of FPP matrix display scripts…

Our show has a large matrix display above our carport/driveway that displays general show information, along with our “Tune To” FM frequency to listen to the music, and song information. Usually in order to do this- the information needs to be built into the musical and lighting sequences. Sequencing to music for these shows is tedious and time-consuming enough, without having to deal with what text to display in each one. Another problem is if you need to change any of the text- you have to re-build and re-render every sequence that uses it.

There are ways to set up static displays (with no information changes, like just “Tune to: 99.9FM”) using Pixel Overlay Models and Effects, but the problem there is they can’t display changing information, such as song title and artist, and if you have to update the text- you have to edit the effect.

I wanted a way to completely automate this sign, so we don’t have to deal with it at all in our sequences. I want it to display our show and schedule information when nothing else is playing, and our tune-to and sequence/music information when something is. Well, FPP (Falcon Player) has a great REST API that makes this fairly easy. It can be polled for status, playlist and sequence information, as well as any available song tag information.

Show Info Matrix

So, my first PHP script does just that. It’s pretty well documented, so I’m going to leave it at that. Obviously if you do your own show and want to use it- you will need to edit it accordingly. I’ll admit it’s a bit old-school (no objects here), but it works.

Note that in the code below- IP addresses and some display information have been modified. These scripts will be updated frequently as I modify them for our 2021 show.

<?php
/*** 
 * info-matrix.php
 * by Wolf I. Butler
 * v. 1.4, Last Updated: 10/05/2021
 * (Added Static display code.)
 * 
 * This script extracts and displays song information from a running FPP sequence.
 * It also displays in-show information (such as a welcome message and tune-to info.)
 * If there isn't anything playing, it displays show schedule and any other information
 * from a file.
 *  
 * This must be run on an FPP install that will be playing sequences with media
 * as it needs to know what media files are playing. The display host needs to have a matrix
 * configured with a pixel overlay model, but otherwise does not need to be running any
 * sequences. It can be in Player or Remote mode, as long as FPPD is running.
 *  
 * This script should be run in the background using UserCallbackHook.sh which can be found
 * in the FPP Script Repository. Put it in the boot section so it only runs once on startup.
 *
 * THIS SCRIPT RUNS A CONTINUIOUS LOOP! 
 * YOU MUST END IT WITH ' &' SO IT RUNS IN THE BACKGROUND, OR IT WILL PREVENT FPP FROM BOOTING!!!
 * 
 * Like this:
 *    boot)		
 *      # put your commands here
 *      /bin/php /home/fpp/media/scripts/info-matrix.php &
 *      ;;
 * 
 * If you need to kill it for any reason, the PID will be in: /tmp/info-matrix.lock
 * 
 * This script uses code from "PixelOverlay-ScrollingText.php"
 * From the FPP Script Repository. The author was not attributed in that source.
 * 
 * License: CC0 1.0 Universal: This work has been marked as dedicated to the public domain.
 * 
 * This script is provided "AS IS". Developer makes no warranties, express or implied, 
 * and hereby disclaims all implied warranties, including any warranty of merchantability 
 * and warranty of fitness for a particular purpose.
 * 
*/

//*** You must edit the following sections... ***

//PREROLL plays first, and might contain a welcome message.
//TUNE should be your tune-to information. It plays after PREROLL.
//Song information, if available, plays after TUNE.
//POSTROLL plays after the above. I use this for reminder/courtesy info.
//GAP is what to put between each element. It can just be spaces, or something like "... " or " - " or " * * * ".
define ( "PREROLL", 'Welcome to our light show!' );
define ( "TUNE", 'Tune To: 99.9 FM' );
define ( "POSTROLL", 'Please do not block driveways.' );
define ( "GAP", "  -  " );

//The following is used to display the text in FPP:
$host  = "192.168.200.20";  # Host/ip of the FPP instance with the matrix. This can be localhost, an IP address, or a resolvable host name.
$name  = "LED+Panels";      # Pixel Overlay Model Name. Verify name in FPP! Use "+" for any spaces. URL Encode any other special chars.
$color = "RAND";            # Text Color (#FF0000) (also names like 'red', 'blue', etc.). Set to RAND for random color for each message.
$font  = "Helvetica";       # Font Name
$size  = 14;                # Font size
$pos   = "R2L";             # Position/Scroll: 'Center', 'L2R', 'R2L', 'T2B', 'B2T'
$pps   = 32;                # Pixels Per Second to Scroll
$antiAlias = true;          # Anti-Alias the text

//Block mode. You should leave this at 1 unless you need to enable advanced functionality.
//Set to 2 for Transparent mode, or 3 for Transparent RGB
$blockMode = 1;

//Sleep time. This is a loop delay in seconds to prevent this script from overloading FPP.
//It should always be at least 1 second, although it should be set higher if you notice performance
//or display issues (including the wrong information for a sequence). Default is 5 seconds.
$sleepTime = 5; //Seconds

//Idle file. This file contains show information played when a sequence isn't running.
//For example, it can contain show schedule information.
//Separate lines are split using GAP specified above.
//***This file needs to be uploaded to the "Uploads" folder in FPP's file manager.***
//It is read on every non-sequence loop iteration, so you can change it on-the-fly if you need to.
$idleFile = "info-matrix.txt";

//We have some "Static" light displays before and after our actual musical show. These emulate "old school"
//christmas lights, or just include patterns that we don't need to display any other information about.
//In these cases- we want to display the show information in the "idleFile" (above).
//In order to do this- I am prefixing them with the text below.
$staticPrefix = "Static_";

/*
* If a song has no MP3 tags, the script looks in the Uploads folder for a TXT
* file with the same name as the MP3. This file should contain three lines of text:
* Title
* Artist
* Album
* 
* For example: Little Drummer Boy Live.txt :
* Little Drummer Boy
* for King & Country
* Live Performance
* 
* If you don't have specific information, leave that line blank (with a CR/LF).
* If there is no .TXT file, the name of the MP3 is displayed as the Title.
*/

/*
 ************************************************************************************
 * That's it. Do not edit anything below unless you REALLY know what you are doing. *
 ************************************************************************************
*/

//Check to see if this script is already running in another process. Exit if it is.
$lockFile = sys_get_temp_dir() . '/info-matrix.lock';
if ( is_file ( $lockFile ) ) {
    $pid = file_get_contents($lockFile);
    if (posix_getsid($pid) === false) {
        file_put_contents($lockFile, getmypid()); // create lockfile
    } 
    else {
        echo "\nAnother instance of this script is already running!\nAborting this process...\n";
        exit;
    }
}
else file_put_contents($lockFile, getmypid()); // create lockfile

# Function to post the data to the REST API
function do_put ( $url, $data ) {

    $data = json_encode($data);     //The API uses JSON.
    //Initiate cURL.
    $ch = curl_init($url);
    //Tell cURL that we want to send a PUT request.
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
    
    //Attach our encoded JSON string to the PUT fields.
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    
    //Set the content type to application/json
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    //Set timeouts to reasonable values:
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    //Execute the request
    if ( $curlResult = curl_exec($ch) ) {
        $arrReturn = json_decode ( $curlResult, TRUE );
        return $arrReturn;
    }
    return FALSE;
}

//Get data from API
function do_get ( $url ) {
    //Initiate cURL.
    $ch = curl_init($url);

    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE);
    
    //Set timeouts to reasonable values:
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);

    //Execute the request
    if ( $curlResult = curl_exec($ch) ) {
        $arrReturn = json_decode ( $curlResult, TRUE );
        return $arrReturn;
    }
    return FALSE;
}

//Load idle text display from file...
function idle_text () {
    global $idleFile;
    $info = "/home/fpp/media/upload/$idleFile";
    if ( is_file ( $info ) ) {
        $arrInfo = file ( $info );
        $outText = null;
        $gapFlag = FALSE;
        foreach ( $arrInfo as $value ) {
            if ( $gapFlag ) $outText .= GAP;
            else $gapFlag = TRUE;
            $outText .= trim ( $value );
        }
    }
    else $outText = PREROLL . GAP . POSTROLL;   //If no file, display what we can.
    return $outText;
}

//Play text to matrix
function play_text ( $host, $name, $outText, $blockMode, $arrDispConfig, $resetDisplay ) {
    global $timer, $displayTime;

    if ( $resetDisplay ) {
        //Attempt to clear the display.
        do_get ( "http://$host/api/overlays/model/$name/clear" );
        do_put ( "http://$host/api/overlays/model/$name/state", array( 'state' => 0 ) );
    }
    else {
        //Don't do anything if there is an active effect.
        $arrStatus = do_get ( "http://$host/api/overlays/model/$name" );
        if ( $arrStatus['effectRunning'] ) return FALSE;
    }

    $data = array( 'State' => $blockMode );
    do_put ( "http://$host/api/overlays/model/$name/state", $data );

    //Randomize colors if RAND was used.
    if ( $arrDispConfig['Color'] == 'RAND' ) {
        
        $total = 0;
        while ( $total < 255 ) {
            //Setting minimum color brightness (~33%)
            $rd = rand ( 0, 255 );
            $gr = rand ( 0, 255 ); 
            $bl = rand ( 0, 255 );
            $total = $rd + $gr + $bl;
        }

        $rd = strval ( dechex ( $rd ) );
        $gr = strval ( dechex ( $gr ) ); 
        $bl = strval ( dechex ( $bl ) );

        //Pad for color codes
        if ( strlen ( $rd ) < 2 ) $rd = '0' . $rd;
        if ( strlen ( $gr ) < 2 ) $gr = '0' . $gr;
        if ( strlen ( $bl ) < 2 ) $bl = '0' . $bl;
        $arrDispConfig['Color'] = '#' . $rd . $gr . $bl;
    }
    $arrDispConfig['Message'] = $outText;
    if ( $timer ) $displayTime = time() - $timer;
    $timer = time();    //Start a new timer.
    return do_put ( "http://$host/api/overlays/model/$name/text", $arrDispConfig );
}

//Init:
$lastFile = null;
$timer = FALSE;
$displayTime = FALSE;

$arrDispConfig = array(
    'Color' => $color,
    'Font' => $font,
    'FontSize' => $size,
    'AntiAlias' => $antiAlias,
    'Position' => $pos,
    'PixelsPerSecond' => $pps
);

//Loop continuously
while (TRUE) {

    $resetDisplay = FALSE;
    $fppStatus = do_get( "http://localhost/api/fppd/status" );

    if ( $fppStatus['fppd'] ) {
        //fpp is running and playing a sequence.
        
        //Bypass sequence information if a "Static" sequence is playing.
        if ( isset ( $staticPrefix ) ) {
            $staticLen = strlen ( $staticPrefix );
            $seq = $fppStatus['current_sequence'];
            if ( substr ( $seq, 0, $staticLen ) == $staticPrefix ) {
                $outText = idle_text();
                play_text ( $host, $name, $outText, $blockMode, $arrDispConfig, $resetDisplay );
                sleep ($sleepTime);
                continue;
            }
        };

        //Display song information if possible.
        $mp3 = $fppStatus['current_song'];
        $mediaFile = '/home/fpp/media/music/' . $mp3;
        $timeRemaining = intval ( $fppStatus['seconds_remaining'] );

        if ( $displayTime ) {
            if ( $timeRemaining < $displayTime ) {
                if ( $timeRemaining <= $sleepTime ) {
                    //Don't do anything. Song will be done before sleepTime is over.
                    sleep (1);
                    continue;
                }
                //Not enough time to complete full text display. Just display Preroll and Tune.
                $outText = PREROLL . GAP . TUNE;
                play_text ( $host, $name, $outText, $blockMode, $arrDispConfig, $resetDisplay );
                sleep ($sleepTime);
                continue;
            }
        }

        if ( $lastFile != $mediaFile ) {
            //File change. Try to get new meta data.
            $songTitle = FALSE;
            $songArtist = FALSE;
            $songAlbum = FALSE;
            $lastFile = $mediaFile;
            
            $mp3URL = rawurlencode ( $mp3 );
            $arrMeta = do_get ( "http://localhost/api/media/$mp3URL/meta" );
            $arrMediaInfo = $arrMeta['format']['tags'];

            if ( $songTitle = $arrMediaInfo['title'] ) {
                if ( $songArtist = $arrMediaInfo['artist'] ) {
                    $songAlbum = $arrMediaInfo['album'];
                }
                elseif ( $songArtist = $arrMediaInfo['album_artist'] ) {
                    $songAlbum = $arrMediaInfo['album'];
                }
            }

            if ( ! $songTitle ) {
                $arrMP3 = explode ( ".", $mp3 );
                $songTitle = $arrMP3[0];    //Default to MP3 name.

                //Assuming no MP3 tags found. Try to retrieve song information text file.
                $info = "/home/fpp/media/upload/$songTitle.txt";
                if ( is_file ( $info ) ) {
                    $arrInfo = file ( $info );
                    foreach ( $arrInfo as $index => $value ) {
                        //Doing this way to limit errors if the file isn't formatted correctly.
                        if ($index == 0) $songTitle = trim ( $value );
                        if ($index == 1) $songArtist = trim ( $value );
                        if ($index == 2) $songAlbum = trim ( $value );
                        if ($index == 3) break;
                    }
                }
            }

            $outText = PREROLL . GAP;
            $outText .= TUNE . GAP;
            if ( $songTitle ) {
                $outText .= 'Now Playing: ' . $songTitle ;
                if ( $songArtist ) $outText .= ", By: " . $songArtist ;
                if ( $songAlbum ) $outText .= ", Album: " . $songAlbum ;
                $outText .= GAP;
            }
            $outText .= POSTROLL;
            $resetDisplay = TRUE;   //Display new media information immediately when available.
        }

    }
    else {
        //fpp is not playing a sequnce. Display general information from file.
        $outText = idle_text();
    }

    //Display outText:
    if ( $outText ) play_text ( $host, $name, $outText, $blockMode, $arrDispConfig, $resetDisplay );
    sleep ($sleepTime);
}
?>

So, in a nutshell, what this does is gather show and sequence information from the FPP install it is running on, and then it pushes text out to an “Info Matrix” running FPP. If there is no show information available, it displays schedule and any other information loaded into a file. The matrix itself needs to be set up as a Pixel Overlay Model. Generally one called “LED Panels” is created automatically when you configure a P5 or P10 panel matrix in FPP.

So with this running- song and other information is displayed automatically, with no sequencing or other interactions needed. It just does its thing!

Show Status Matrix

Based on the above, and because I had some extra hardware and panels from another display I decided not to use this year, I decided that I would also like a matrix that displays the status of the show “at a glance” in my office. In this case, the information is pushed out to a much-smaller matrix hanging above a window inside. This tells me if FPP is running on the show-runner system, and also checks the system power, FM transmitter power, the status of the Info Matrix (above), and shows scheduler and/or playlist and song information.

Here is that script:

<?php
/*** 
 * show-status.php
 * by Wolf I. Butler
 * v. 1.0, Last Updated: 10/03/2021
 * 
 * This script displays the status of my show. It is based on info-matrix.php.
 * 
 * This should be run on the master/show-runner FPP intall.
 * 
 * The display host can be any machine with a matrix configured with a pixel overlay model,
 * but otherwise does not need to be running any sequences. It can be in Player or Remote mode, 
 * as long as FPPD is running.
 * 
 * This script should be run in the background using UserCallbackHook.sh which can be found
 * in the FPP Script Repository. Put it in the boot section so it only runs once on startup.
 *
 * THIS SCRIPT RUNS A CONTINUIOUS LOOP! 
 * YOU MUST END IT WITH ' &' SO IT RUNS IN THE BACKGROUND, OR IT WILL PREVENT FPP FROM BOOTING!!!
 * 
 * Like this:
 *    boot)		
 *      # put your commands here
 *      /bin/php /home/fpp/media/scripts/show-status.php &
 *      ;;
 * 
 * If you need to kill it for any reason, the PID will be in: /tmp/show-status.lock
 * 
 * This script uses code from "PixelOverlay-ScrollingText.php"
 * From the FPP Script Repository. The author was not attributed in that source.
 * 
 * License: CC0 1.0 Universal: This work has been marked as dedicated to the public domain.
 * 
 * This script is provided ‚Äč"AS IS". Developer makes no warranties, express or implied, 
 * and hereby disclaims all implied warranties, including any warranty of merchantability 
 * and warranty of fitness for a particular purpose.
 * 
*/

//*** You must edit the following sections... ***

define ( 'GAP', '. . . ');

//The following is used to display the text in FPP:
$host  = "192.168.200.60";  # Host/ip of the FPP instance with the matrix. This can be localhost, an IP address, or a resolvable host name.
$name  = "LED+Panels";      # Pixel Overlay Model Name. Verify name in FPP! Use "+" for any spaces. URL Encode any other special chars.
$color = "#00AA00";         # Color code (#00FF00), value (Green), or "RAND" for random.
$font  = "DejaVuSans";      # Font Name. RAND will randomize fonts.
$size  = 14;                # Font size
$pos   = "R2L";             # Position/Scroll: 'Center', 'L2R', 'R2L', 'T2B', 'B2T'
$pps   = 48;                # Pixels Per Second to Scroll
$antiAlias = TRUE;          # Anti-Alias the text

//This is the host name/IP of the Info Matrix. If used- the script will see if it appears to be online.
//Set to FALSE if you aren't running one.
$info = "192.168.200.50";
$infoOverlay = "LED+Panels";

//Block mode. You should leave this at 1 unless you need to enable advanced functionality.
//Set to 2 for Transparent mode, or 3 for Transparent RGB
$blockMode = 1;

//Sleep time. This is a loop delay in seconds to prevent this script from overloading FPP.
//It should always be at least 1 second, although it should be set higher if you notice performance
//or display issues (including the wrong information for a sequence). Default is 5 seconds.
$sleepTime = 5; //Seconds

//Power controller and Overlays used for GPIOs. Set $power to FALSE if not using.
$power = "192.168.200.30";
$powerOverlays = array ( "Pwr_L", "Pwr_R" );
$powerNames = array ( "PWR_L", "PWR_R" );

//FM Transmitter power. Set to FALSE if not using.
$FM = "localhost";
$fmOverlay = "FM_Pwr";


/*
 ************************************************************************************
 * That's it. Do not edit anything below unless you REALLY know what you are doing. *
 ************************************************************************************
*/

//Check to see if this script is already running in another process. Exit if it is.
$lockFile = sys_get_temp_dir() . '/show-status.lock';
if ( is_file ( $lockFile ) ) {
    $pid = file_get_contents($lockFile);
    if (posix_getsid($pid) === false) {
        file_put_contents($lockFile, getmypid()); // create lockfile
    } 
    else {
        echo "\nAnother instance of this script is already running!\nAborting this process...\n";
        exit;
    }
}
else file_put_contents($lockFile, getmypid()); // create lockfile

# Function to post the data to the REST API
function do_put ( $url, $data ) {

    $data = json_encode ( $data );
    //Initiate cURL.
    $ch = curl_init($url);
    //Tell cURL that we want to send a PUT request.
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
    
    //Attach our encoded JSON string to the PUT fields.
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    
    //Set the content type to application/json
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    //Set timeouts to reasonable values:
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    //Execute the request
    $curlResult = curl_exec($ch);
    
    curl_close($ch);

    return $curlResult;
}

//Get data from API
function do_get ( $url ) {
    //Initiate cURL.
    $ch = curl_init($url);

    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE);
    
    //Set timeouts to reasonable values:
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);

    if ( $curlResult = curl_exec($ch) ) {
        $curlJSON = json_decode ( $curlResult, TRUE );
        return $curlJSON;
    }
    return FALSE;
}

//Check to see if the model is currently active.
function is_active ( $host, $name ) {
    $arrStatus = do_get ( "http://$host/api/overlays/model/$name" );
    if ( $arrStatus['isActive'] ) return TRUE;
    else return FALSE;
}

//Play text to matrix
function play_text ( $host, $name, $outText, $blockMode, $arrDispConfig, $resetDisplay ) {
    global $lastText, $fontList;
    if ( $lastText != $outText ) $resetDisplay = TRUE;  //A change occurred. Reset.
    $lastText = $outText;
    if ( $resetDisplay ) {
        do_get ( "http://$host/api/overlays/model/$name/clear" );
        do_put ( "http://$host/api/overlays/model/$name/state", array( 'state' => 0 ) );
    }
    else {
        //Don't do anything if there is an active effect.
        $arrStatus = do_get ( "http://$host/api/overlays/model/$name" );
        if ( $arrStatus['effectRunning'] ) return FALSE;
    }

    //Initialize display.
    $data = array( 'State' => $blockMode );
    do_put("http://$host/api/overlays/model/$name/state", $data ); 

    if ( $arrDispConfig['Font'] == 'RAND' ) {
        //Pick a random font and display it on the console (Temporary)
        $x = array_rand( $fontList );
        $font = $fontList[$x];
        $arrDispConfig['Font'] = $font;
        echo "\nRandom font: $font";
    }
    
    if ( $arrDispConfig['Color'] == 'RAND' ) {
        
        $total = 0;
        while ( $total < 400 ) {    //Setting for minimum brightness.
            //Set these for a dimmer or brighter display.
            $rd = rand ( 0, 150 );  //Never want all red, since that indicates an error.
            $gr = rand ( 0, 255 ); 
            $bl = rand ( 0, 255 );
            $total = $rd + $gr + $bl;
        }

        $rd = strval ( dechex ( $rd ) );
        $gr = strval ( dechex ( $gr ) ); 
        $bl = strval ( dechex ( $bl ) );

        //Pad for color codes
        if ( strlen ( $rd ) < 2 ) $rd = '0' . $rd;
        if ( strlen ( $gr ) < 2 ) $gr = '0' . $gr;
        if ( strlen ( $bl ) < 2 ) $bl = '0' . $bl;
        $arrDispConfig['Color'] = '#' . $rd . $gr . $bl;
    }

    $arrDispConfig['Message'] = $outText;
    return do_put("http://$host/api/overlays/model/$name/text",  $arrDispConfig ); 
}

function info_matrix_running ( $info ) {
    //Check to see if the Info-Matrix is likely running. Not 100% accurate.
    //Can't just check the overlay model, since there are brief times when it may not be running, between updates.
    //First check to see if FPP is running.
    if ( do_get ( "http://$info/api/fppd/status" ) ) {
        //Check to see if there is a script lockfile for info-matrix.php. Only works for localhost.
        $lockFile = sys_get_temp_dir() . '/info-matrix.lock';
        if ( is_file ( $lockFile ) ) {
            $pid = file_get_contents($lockFile);
            if (posix_getsid($pid) === FALSE) return FALSE;
            else return TRUE;
        }
    }
    return FALSE;
}

//Init:
$online = null;
$lastStatus = null;
$lastSequence = null;
$lastText = null;

$fontList = do_get ( "http://$host/api/overlays/fonts" );

$arrDispConfig = array(
    'Color' => $color,
    'Font' => $font,
    'FontSize' => $size,
    'AntiAlias' => $antiAlias,
    'Position' => $pos,
    'PixelsPerSecond' => $pps
);

//Loop continuously
while (TRUE) {

    $fppStatus = do_get( "http://localhost/api/fppd/status" );

    if ( $fppStatus['fppd'] ) {
        //fpp is running.
        $online = TRUE;
        $arrDispConfig['Color'] = $color;    //Just in case it was overridden (below).

        if ( $lastStatus == $online ) $resetDisplay = FALSE;
        else $resetDisplay = TRUE;
        $lastStatus = $online;

        $outText = 'Lights are ON' . GAP;
        
        if ( $power ) {
            $commaFlag = FALSE;
            foreach ( $powerOverlays as $pwrIndex => $overlay ) {
                if ( $commaFlag ) $outText .= ', ';
                else $commaFlag = TRUE;
                if ( is_active ( $power, $overlay ) ) $outText .= $powerNames[$pwrIndex] . ": ON";
                else $outText .= $powerNames[$pwrIndex] . ": OFF";
            }
            $outText .= GAP;
        }

        if ( $FM ) {
            if ( is_active ( $FM, $fmOverlay ) ) $outText .= "FM: ON" . GAP;
            else $outText .= "FM: OFF" . GAP;
        }

        if ( $info ) {
            if ( info_matrix_running( $info ) ) $outText .= "Info: ON" . GAP;
            else $outText .= "Info: OFF" . GAP;
        }

        $playlist = $fppStatus['current_playlist']['playlist'];
        if ( $playlist ) {
            $outText .= "Playlist: \"$playlist\",";

            if ( $sequence = $fppStatus['current_sequence'] ) {
                if ( $lastSequence != $sequence ) $resetDisplay = TRUE;
                $lastSequence = $sequence;

                $outText .= " Sequence: $sequence";
                $outText .= ", Song: " . $fppStatus['current_song'] . GAP;
            }
            else $outText .= GAP;
        }
        elseif ( $fppStatus['next_playlist']['start_time'] ) {
            $outText .= "Scheduled Playlist: \"" . $fppStatus['next_playlist']['playlist'];;
            $outText .= "\" will run: " . $fppStatus['next_playlist']['start_time'];
        }
        else $outText .= "Nothing playing or scheduled" . GAP;
    }
    else
    {
        //FPP is not running!
        $online = FALSE;
        if ( $lastStatus == $online ) $resetDisplay = FALSE;
        else $resetDisplay = TRUE;
        $lastStatus = $online;

        $arrDispConfig['Color'] = '#990000';    //Override- Red
        $outText = "Lights are OFFLINE!!!";
    }
    play_text ( $host, $name, $outText, $blockMode, $arrDispConfig, $resetDisplay );
    sleep ($sleepTime);
}
?>

Here is a video of it in action:

As noted in the script comments, either or both should be run on-boot, preferably on the master/show-runner FPP computer, using the UserCallbackHook.sh script, which can be found in FPP’s script repository. Once set up, you can pretty much forget about them- they will just do their thing in the background.

If you are interested in how I am controlling and monitoring power, check out More Power! which shows my main power controller build. I’m using a similar relay system for powering the FM transmitter. Both run off Raspberry Pi GPIO “Outputs” in FPP, which can be set and monitored remotely using Pixel Overlay Models.

So, for example, I can use GPIO 4 on a Raspberry Pi to control a power relay. I then use FPP to set up a GPIO “Output” for that GPIO, and set up a single channel Pixel Overlay Model for it. Then- I can toggle that pixel overlay on and off, which toggles the power relay on and off. I can also read the state of the pixel overlay model, and display it.

I’m planning to add my projector power to the mix once I dig it out of storage and get it reconfigured for this year’s show. I’ll update the above once that happens.