Lights On Granger Lane logo
  • Home 
  • Rules for Viewing 
  • About the Show 
  1.   Posts
  1. Home
  2. Posts
  3. Light Show On-Demand via the Web

Light Show On-Demand via the Web

Posted on November 22, 2025  (Last modified on November 23, 2025) • 11 min read • 2,287 words
Technical  
Technical  
Share via
Lights On Granger Lane
Link copied to clipboard

On this page
Background   The Goal   Prerequisites   The Three-Part Workflow   The Client (the Browser and GPS)   The Gatekeeper (The Cloudflare Worker)   The Controller (Node-RED)   Implementation Tips   Connecting the Worker to your Node-RED (or FPP)   Website Javascript (Client-side)   Cloudflare Worker (The Gatekeeper Logic)   Node-RED Configuration (Command Routing)   The Feedback Loop: Node-RED to Browser Toast   Browser Displays the Toast Message   Conclusion  
Light Show On-Demand via the Web

Background  

Out of respect for my neighbors, I only want flashy music sequences to play when a visitor is actually outside my house watching. Up until this year, I’ve relied on a system I built where visitors can trigger the show by sending a text message to a phone number I display on my garage door matrix.

The next step in evolution was to allow visitors to choose a specific sequence to play. This would require them to be presented with a numbered list to choose from, then text that sequence number. Then I thought, since they’re on my website anyway, can I just build the ability to play a song from the web page directly? Turns out I can. And I did.

Note that I am aware of Remote Falcon  . I hear it’s really good as a near plug-and-play solution. In my case, though, I already use other tools for all of my show automations. Integrating Remote Falcon with all of that while not conflicting with my existing SMS/text-based on-demand feature would likely have been harder than just building it myself.

This guide walks through the architecture of a custom, secure, and reliable system that uses GPS and serverless code to make that happen.

The Goal  

The objective is to create a public web interface where a visitor can tap a button to play a specific sequence on your FPP, but only after their phone has verified they are within a specific geographic boundary (a “geo-fence”) around your display. This prevents someone from stumbling onto your website and maliciously triggering your show from across the country or otherwise playing when no one is there to watch.

Prerequisites  

To replicate this setup, you need access and familiarity with these core components:

  • FPP: Running your holiday sequences.
  • A website: A way to host a simple web page (e.g., GitHub Pages, Netify or any basic shared host will do).
  • Serverless Compute: A free Cloudflare  account to host a Worker to act as a secure API endpoint. This ensures website visitors never see your home IP address.
  • Command Broker (Optional): While you could script everything in your Cloudflare Worker and have it talk directly to your FPP, I recommend something like Node-RED  . I already use it for all of my home and light show automation, integrated with HomeAssistant  , so there was nothing new for me to install here.
  • Location Data: The exact latitude and longitude of your light show (from which to create the geo-fence).

The Three-Part Workflow  

This system is built on a chain of three distinct, yet connected, steps: the Client, the Gatekeeper, and the Controller.

The Client (the Browser and GPS)  

The client’s job is to gather and submit location data. This is done using a simple JavaScript function on your website:

  • Action: When the visitor clicks the “Play” button, the JavaScript immediately calls the browser’s built-in Geolocation API (navigator.geolocation.getCurrentPosition).
  • Payload: Once the GPS coordinates are retrieved, the JavaScript packages them into a JSON payload along with the requested song number (item_id).
  • Submission: This payload is sent via a secure POST request to the Cloudflare Worker URL.

The Gatekeeper (The Cloudflare Worker)  

The Worker acts as the single, publicly-exposed, and secured entry point.

  • Function: This small piece of JavaScript code performs the crucial geo-fencing calculation and decision-making.
  • Authorization Check: The Worker uses the Haversine formula to calculate the distance between the user’s coordinates and your home coordinates. If the distance is greater than the pre-set limit (e.g., 0.3 miles), the Worker returns a 403 Forbidden response.
  • Command Forwarding: If the check passes, the Worker constructs the final command string (e.g., message=go7 to play sequence #7) and sends a non-geo-fenced request to the next component (the Controller).

The Controller (Node-RED)  

The Controller handles all the complex rules and logic before executing the final command.

  • Input: Node-RED receives the command string (e.g., go0 or go7) from the Worker.
  • Business Logic: This is where you implement all your time-based and status checks:
    • Is it after sunset? (using a within-time-switch)
    • Is the FPP system accepting on-demand commands? (requires a way to track a switch state, like HomeAssistant)
    • Is another sequence already playing? (requires checking FPP’s status via the API)
  • Final Execution: Once all conditions are met, Node-RED uses an HTTP Request or an MQTT Out node to send the final, appropriate command (via API or MQTT) directly to FPP to start the selected sequence.

As I mentioned before, you could bypass using Node-RED and instead just code any business logic directly into your Cloudflare Worker, and let the Worker communicate directly with your FPP instance.

Implementation Tips  

Connecting the Worker to your Node-RED (or FPP)  

One of the reasons to use a Cloudflare worker is to prevent exposing your home IP address or internal API endpoints to the public in your web page source. Instead, the browser only talks to the Worker, and the Worker is configured to reject any traffic not coming from your web page. Only the Worker talks to your home.

I use Nginx as a reverse proxy for this to receive the incoming request and forward to Node-RED. The details of configuring Nginx, punching holes in your firewall, optionally setting up DNS, etc, are beyond the scope of the post.

However, once you have all that, setting up Node-RED is as easy as creating an HTTP IN node with an obscure, not guessable URL to use as your endpoint in your Worker code (below).

Website Javascript (Client-side)  

The front-end code is responsible for two key tasks: retrieving the user’s GPS coordinates and handling the command structure for playing a sequence.

Geo-Location and Payload Construction  

This JavaScript function requests the location and packages the data. Note that in my show, I also allow visitors to play a short set of random sequences, accomplished by sending item_id: '0'. That exception is handled in the business logic in Node-RED. This sends the data to your Worker and waits for a response.

// Function attached to your 'Play Song' or 'Play Random' button
function sendCommand(musicItemID) {
    if (!navigator.geolocation) {
        showToast("Geolocation not supported.", true);
        return;
    }

    navigator.geolocation.getCurrentPosition(async function(position) {
        // Payload sent to the Cloudflare Worker
        const payload = {
            // Sends '0' for randomize, or '7' for song #7, etc.
            item_id: musicItemID, 
            user_lat: position.coords.latitude,
            user_lon: position.coords.longitude
        };

        const response = await fetch('YOUR_WORKER_URL', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });
    
        // Handle Worker response (200 success or 403 denied)
        const message = await response.text();
        showToast(message, !response.ok);
    });
}

Example usage on a button click:

document.getElementById('btn-random').onclick = () => sendCommand('0'); 

document.getElementById('btn-song-7').onclick = () => sendCommand('7');

Cloudflare Worker (The Gatekeeper Logic)  

This snippet shows the two most critical checks within your Worker: the geo-fencing calculation and the denial response.

The Haversine Formula (Geo-Fencing)  

Use this function to calculate the distance in miles based on the user’s location and your home coordinates (HOME_LAT/HOME_LON):

const HOME_LAT = 41.9484424; // YOUR HOME LATITUDE
const HOME_LON = -87.6579076; // YOUR HOME LONGITUDE
const MAX_DISTANCE_MILES = 0.3; // Geo-fence radius
const EARTH_RADIUS_MILES = 3958.8; 

const toRad = (angle) => angle * (Math.PI / 180);

function calculateDistance(lat1, lon1, lat2, lon2) {
    const dLat = toRad(lat2 - lat1);
    const dLon = toRad(lon2 - lon1);
    const a = 
        Math.sin(dLat / 2) * Math.sin(dLat / 2) +
        Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return EARTH_RADIUS_MILES * c; // Distance in miles
}

Yes, the coordinates above are Wrigley Field. My tribute to The Blues Brothers  .

The Geo-Fence Enforcement  

This logic runs inside the Worker’s fetch handler:

// Worker receives userLat and userLon from the payload...

const distance = calculateDistance(HOME_LAT, HOME_LON, userLat, userLon);

if (distance > MAX_DISTANCE_MILES) {
    // DENIAL: This immediately sends the 403 Forbidden response back to the browser.
    return new Response('Access denied. You must be near the display to trigger a song.', { 
        status: 403, 
        headers: corsHeaders 
    });
}

// If allowed, construct the final command for Node-RED
const messageValue = `go${musicItemID}`; 
// ... forward command to Node-RED endpoint ...

Node-RED Configuration (Command Routing)  

With the HTTP IN node in-place and receiving data from your Worker, I convert the incoming JSON to a Javascript object, then send to a switch node. In the switch node, I handle these 3 cases:

  • message == go0 → Exact match. Routes to a Node-RED flow that randomizes all the sequences in an FPP playlist, then plays the first set (5 or 6 sequences).
  • messsage matches regex ^go([1-9]\d*)$ → This regex matches ‘go’ followed by a non-zero digit, intentionally excluding go0 and catching all valid sequence numbers (eg, go1, go7).
  • otherwise → Catches everything else that doesn’t match.

The Feedback Loop: Node-RED to Browser Toast  

Once the command passes all the checks (Geo-fence, Daylight, FPP Status), the system needs to send a confirmation message back to the user’s browser at the same time it starts playing the sequence. This is a three-step process handled by the Controller and the Gatekeeper.

Node-RED Responds to the Worker  

Node-RED’s job is simple: after triggering FPP, it sends a successful status code and a simple confirmation message.

  • Action: At the end of every successful path (Randomize Flow, Specific Song Flow), the Node-RED flow terminates in an HTTP Response node.
  • Response Code: Set the Status Code to 200.
  • Body: Send a success message in the response body. (eg, “Starting. Tune to 87.9FM and enjoy!”)

And since I’m also checking to confirm it’s nighttime, nothing else is playing, etc, Node-RED sends appropriate response messages for those cases too.

The Worker Passes the Status to the Browser  

The Cloudflare Worker receives the 200 Success or another message from Node-RED and simply passes that status and message back to the browser. The only other responses the Worker generates are the security denial (403 Forbidden).

  • Action: The worker captures the response.status and response.text() from the Node-RED API call and uses it to construct the final response to the user’s browser.

Browser Displays the Toast Message  

The client-side JavaScript receives the response and displays it using the custom showToast function:

  • Action: The browser checks the HTTP status code. If it’s a 200, it’s a success. If it’s anything else (like the Worker’s 403 or a Node-RED 500), it’s treated as an error.
  • Display: The showToast function is called with the message and the error status.

The core logic in your web page that handles the Worker’s response looks like this:

// Located in your button's click handler, after the fetch call:

const response = await fetch(endpoint, { /* ... */ });
const text = await response.text();
                                
// Check if the response was successful (HTTP status code 200-299)
const isError = !response.ok; 

// showToast handles styling: Green for Success (isError=false), Red for Error (isError=true)
showToast(text, isError);

This structure ensures the user instantly gets clear feedback, whether it’s a successful start message, an unsuccessful “Come back after sunset”, from Node-RED, or a clear failure message (e.g., “Access denied.” from the Worker, or “Location access denied.” from the browser itself).

Toast Notification CSS  

This CSS defines the look and positioning of the toast. You should place this within <style> tags in the <head> of your page or in a linked stylesheet.

<style>
.toast-container {
    /* Positions the toast in the bottom center of the viewport */
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1000;
    pointer-events: none; /* Allows clicks to pass through container */
}

.toast-message {
    /* Style for the actual toast box */
    padding: 10px 20px;
    border-radius: 6px;
    color: white;
    background-color: rgba(0, 0, 0, 0.75);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
    
    /* Animation and initial state */
    opacity: 0;
    transition: opacity 0.5s, transform 0.5s;
    transform: translateY(20px);
    pointer-events: auto; /* Re-enables clicks on the toast itself */
    max-width: 90vw;
    text-align: center;
    margin-top: 10px;
}

.toast-message.show {
    opacity: 1;
    transform: translateY(0);
}

.toast-message.error {
    background-color: rgba(200, 0, 0, 0.85); /* Red background for errors */
}

.toast-message.success {
    background-color: rgba(0, 150, 0, 0.85); /* Green background for success */
}
</style>

<div class="toast-container"></div>

JavaScript showToast Function  

This function handles creating the toast element, applying the correct styling (success or error), displaying it, and automatically removing it after 5 seconds.

// Function to display the toast message
function showToast(message, isError = false) {
    const container = document.querySelector('.toast-container');
    const toast = document.createElement('div');
    
    // Set content and class for styling
    toast.textContent = message;
    toast.classList.add('toast-message');
    toast.classList.add(isError ? 'error' : 'success');
    
    container.appendChild(toast);

    // Force reflow/repaint before showing the transition
    void toast.offsetWidth;

    // Show the toast
    toast.classList.add('show');

    // Hide and remove after 5 seconds (5000ms)
    setTimeout(() => {
        toast.classList.remove('show');
        // Wait for the transition to finish before removing from DOM
        setTimeout(() => {
            toast.remove();
        }, 500); 
    }, 5000);
}

Conclusion  

At this time of this writing, you can see this in action on the currently active 2025 Thanksgiving Light Show. However, since I didn’t build in any determination of what season/show/playlist it should play from, I’ll move this functionality to the Christmas Show page when it goes live. After Christmas, I’ll disable it until next season. Also, since you’re likely not standing in front of my house, all you’re going to see are the pop-up toast messages stating you’re out of range.

The above is a simplified version of my implementation. I retained the SMS/Text functionality, which is set up to construct the same payload from an SMS/text message as received from the web. I then inject both payloads, regardless of source, into the same business logic flow, apply the same tests, respond with the same messages and route to the next actions (play single, play random, etc).

I hope this has been helpful to someone, or at least interesting.

 Light Show On-Demand via SMS/Text
On this page:
Background   The Goal   Prerequisites   The Three-Part Workflow   The Client (the Browser and GPS)   The Gatekeeper (The Cloudflare Worker)   The Controller (Node-RED)   Implementation Tips   Connecting the Worker to your Node-RED (or FPP)   Website Javascript (Client-side)   Cloudflare Worker (The Gatekeeper Logic)   Node-RED Configuration (Command Routing)   The Feedback Loop: Node-RED to Browser Toast   Browser Displays the Toast Message   Conclusion  

Connect with me on Bluesky or subcribe on Youtube...

   
Copyright © 2025 Lights on Granger. All rights reserved. |
Lights On Granger Lane
Code copied to clipboard