Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

In this tutorial you’ll integrate Gumband into an existing Electron app. This Electron app has two modes: a digital signage and a simple video game:

At the beginning of the tutorial, the app will only be able to serve preset static content for the digital signage screen, but by the end it will be able to:

  • Dynamically set all text copy for the digital signage

  • Dynamically change the image shown for the digital signage

  • Toggle the screen between rendering the digital signage and the Button Clicker game

  • Set some configurations for the Button Clicker game

  • Capture usage data for the Button Clicker game

Table of Contents

Table of Contents

Setup

Clone the tutorial repo. Navigate into the gumband-sdk-tutorial directory and run npm install to install dependencies for the project. Run npm run start to start the electron app and serve the static digital signage content. You should see the following:

Installing and Initializing the Gumband SDK

Install the Gumband SDK as a new dependency: npm i @deeplocal/gumband-node-sdk.

Create a new exhibit in the Gumband UI and copy the exhibit ID and Authentication Token into a new .env file. You will use these two values to authenticate your exhibit with the Gumband Cloud.

./.env

Code Block
EXHIBIT_TOKEN=${alpha-numeric-string}
EXHIBIT_ID=${exhibit-id}

Create a new directory and file named ./gumband/gumband-service.ts, and initialize a Gumband SDK inside that new file:

./gumband/gumband-service.ts

Code Block
const { Gumband } = require("@deeplocal/gumband-node-sdk");

/**
 * A class that wraps the Gumband SDK and handles websocket messages 
 * that come from the Gumband Cloud.
 */
class GumbandService {
    /**
     * A reference to the Gumband SDK instance.
     */
    gumbandSDK;

    constructor() {
        this.gumbandSDK = new Gumband(
            process.env.EXHIBIT_TOKEN,
            process.env.EXHIBIT_ID,
            `${__dirname}/manifest.json`,
        );
    }
}

module.exports = { GumbandService };

Create a new manifest file at ./gumband/manifest.json, and add an empty manifest object inside:

./gumband/manifest.json

Code Block
{
    "manifest": {}
}

Initialize the GumbandService class in the electron app:

./electron-app/index.js

Code Block
const { app, BrowserWindow } = require('electron');
const { GumbandService } = require('../gumband/gumband-service');

let win;

const createWindow = () => {
  ...
}

app.whenReady().then(() => {
  createWindow();
  new GumbandService();
});

Now, when you run the electron app again (npm run start), your exhibit instance in the Gumband UI should come online:

Implementing an Operation Mode

Let’s say that the Operation Mode should cause the Electron app to show a blank screen.

First, listen for the OP_MODE_RECEIVED websocket event from the Gumband Cloud, so that when the Operation Mode is toggled in the Gumband UI, our exhibit will be notified.

./gumband/gumband-service.js

Code Block
const { Gumband, Sockets } = require("@deeplocal/gumband-node-sdk");

class GumbandService {
    gumbandSDK;
    /**
     * Whether the exhibit is in operation mode. Configured in the Gumband UI.
     */
    opMode;

    constructor() {
        this.gumbandSDK = new Gumband(
            process.env.EXHIBIT_TOKEN,
            process.env.EXHIBIT_ID,
            `${__dirname}/manifest.json`,
        );

        this.addSDKListeners();
    }

    /**
     * Add listeners on the Gumband SDK websocket connection to the Gumband Cloud.
     */
    addSDKListeners() {
        this.gumbandSDK.on(Sockets.OP_MODE_RECEIVED, (payload) => {
            this.opMode = payload.value;
            console.log(`opMode is ${this.opMode}`);
        });
    }
}

module.exports = { GumbandService };

Now, when the operation mode is toggled, you’ll see some log messages in the terminal:

opMode is true/false

Now we actually need to tell the Electron app to show a blank screen when the Operation Mode is false. The GumbandService can communicate with the Electron app through the Electron window object, so let’s pass that object to the GumbandService when it gets initialized and implement a communication layer between Gumband and Electron.

./electron-app/index.js

Code Block
let win;

const createWindow = () => {
  win = new BrowserWindow(...);

  ...
}

app.whenReady().then(() => {
  createWindow();
  // pass the window object to Gumband
  new GumbandService(win);
});

./gumband/gumband-service.js

Code Block
class GumbandService {
    /**
     * A reference to the window object of the Electron app frontend.
     */
    window;
    
    ...
    
    constructor(window) {
        //set reference to Electron window object
        this.window = window;
        ...
    }

    addSDKListeners() {
        this.gumbandSDK.on(Sockets.OP_MODE_RECEIVED, (payload) => {
            this.opMode = payload.value;
            this.setFrontendOperationMode();
        });
    }

    /**
     * Set the electron frontend app operation mode.
     */
    setFrontendOperationMode() {
        //The communication layer that expects a "operation-mode" string hasn't been 
        //defined in our Electron app yet. We'll do that next.
        this.window.webContents.send("fromGumband", { type: "operation-mode", value: this.opMode });
    }
}

./electron-app/main.js

Code Block
...

/**
 * Subscribes to events from the GumbandWrapper class.
 */
window.gb.receive("fromGumband", (data) => {
    const root = window.document.getElementById("root");
    switch(data.type) {
        case "operation-mode":
            if(!data.value) {
                clearChildren(root);
            } else {
                clearChildren(root);
                createDigitalSignagePage();
                addStaticCopy();
            }
            break;
    }
});

Now, you should see the Electron app show a blank screen when the Operation Mode is on, and you should see the digital signage when the Operation Mode is off:

You may have noticed that the Electron app always starts in the Digital Signage mode, even when the Gumband UI shows that the Operation Mode is off. This is because the GumbandService is only getting notified of the Operation Mode when it is changed, not when the app starts. Let’s add a listener for when the SDK connection to the Gumband Cloud is ready, and set the Electron app based on the existing Operation Mode.

./gumband/gumband-service.js

Code Block
...


    addSDKListeners() {
        //the Sockets.READY event includes the entire manifest object, which has the
        //operation mode
        this.gumbandSDK.on(Sockets.READY, async (manifest) => {
            this.opMode = manifest.opMode === "On";
            this.setFrontendOperationMode();
        });
        
        ...
    }

...

Now, the Electron app will display correctly when the app is first started.

Make the Digital Signage Copy Text Configurable

Now we would like the Digital Signage copy to be configurable through Gumband. For starters, we’ll need to add settings to the manifest for the settings we’ll need. Let’s put the header, subheader, and body settings in a SettingsGroup to differentiate them from the Game Mode settings that we’ll be adding later:

./gumband/manifest.json

Code Block
{
    "manifest": {
        "statuses": [],
        "controls": [],
        "settings": [
            {
                "id": "signage-group",
                "type": "SettingsGroup",
                "display": "Digital Signage Settings",
                "order": 0,
                "schema": [
                    {
                        "id": "header",
                        "type": "TextInput",
                        "display": "Header Copy",
                        "order": 0
                    },
                    {
                        "id": "subheader",
                        "type": "TextInput",
                        "display": "Subheader Copy",
                        "order": 1
                    },
                    {
                        "id": "body",
                        "type": "TextInput",
                        "display": "Body Copy (separate by | for new paragraph)",
                        "order": 2
                    }
                ]
            }
        ]
    }
}

Now, listen for the Sockets.SETTING_RECEIVED event and create a function to update the Electron app with text changes:

./gumband/gumband-service.js

Code Block
...

addSDKListeners() {
    this.gumbandSDK.on(Sockets.READY, async (manifest) => {
        ...
        //update the frontend from the current settings when the app starts
        this.updateFrontendFromSettings();
    });

    this.gumbandSDK.on(Sockets.OP_MODE_RECEIVED, (payload) => {
        ...
        //update the frontend from the current settings when the op mode changes
        this.updateFrontendFromSettings();
    });

    //update the frontend whenever a setting is changed
    this.gumbandSDK.on(Sockets.SETTING_RECEIVED, (payload) => {
        this.updateFrontendFromSettings();
    });
}

/**
  * Update the electron frontend app with the settings configured in Gumband.
  */
async updateFrontendFromSettings() {
    //don't update the frontend if the operation mode is off
    if(!this.opMode) {
        return;
    }
    
    const header = await this.getSettingValue("signage-group/header");
    const subheader = await this.getSettingValue("signage-group/subheader");
    const body = await this.getSettingValue("signage-group/body");

    //We allow multiple body paragraphs to be defined by using the pipe character as
    //a separator
    let bodyParagraphs = [];
    if(body) {
        bodyParagraphs = body.split('|');
    }
    
    //Now send these string values off to the Electron app.
    this.window.webContents.send("fromGumband", { type: "header", value: header });
    this.window.webContents.send("fromGumband", { type: "subheader", value: subheader });
    this.window.webContents.send("fromGumband", { type: "body", value: bodyParagraphs });
}

/**
  * A simple function to get a setting value based on the manifest ID. The manifest
  * ID is a combination of the setting ID and the setting group ID, both of 
  * which are defined in the manifest. You can also log out the manifest in the
  * Sockets.READY event callback.
  */
async getSettingValue(manifestId) {
    return (await this.gumbandSDK.getSetting(manifestId)).value;
}
...

./electron-app/main.js

Code Block
...

//The bare bones of the digital signage page is already scaffolded in the
//createDigitalSignagePage() function. So we just need to populate existing
//html elements with the copy text.

window.gb.receive("fromGumband", (data) => {
    const root = window.document.getElementById("root");
    switch(data.type) {
        ...
        case "header":
            const header = window.document.getElementsByClassName('header')[0];
            let headerContent = document.createElement('span');
            headerContent.innerText = data.value;
            header.appendChild(headerContent);
            break;
        case "subheader":
            const subheader = window.document.getElementsByClassName('subheader')[0];
            let subheaderContent = document.createElement('span');
            subheaderContent.innerText = data.value;
            subheader.appendChild(subheaderContent);
            break;
        case "body":
            const body = window.document.getElementsByClassName('body')[0];
            data.value.forEach(text => {
                let bodyParagraph = document.createElement('p');
                bodyParagraph.innerText = text;
                body.appendChild(bodyParagraph);
            });
            break;
    }
});

Also, delete the addStaticCopy function in the ./electron-app/main.js file, and all calls to it, since we don’t need to set static copy anymore.

Now, when you re-run the app, it should show a blank screen even when in Operation Mode. This is because it is pulling the text from the Gumband settings, and those settings are blank. If you fill them in, you will see the Electron app populate in real time.

Add Setting for Updating the Signage Image

Now let’s get the image for digital signage back and configurable. First, let’s upload a few images to Gumband. In the Gumband UI, navigate to the Files tab for your exhibit instance and upload the three images in the ./seed-images directory:

We’ll also want to tell the Gumband SDK to download any images that were uploaded here and to save them in a place accessible to the Electron app. To do this, we’ll just need to add some configuration to our Gumband SDK initialization:

./gumband/gumband-service.js

Code Block
class GumbandService {
    ...

    constructor(window) {
        ...
        this.gumbandSDK = new Gumband(
            ...
            {
                contentLocation: './electron-app/content'
            }
        );

        ...
    }

If you restart the app, you’ll notice a new directory at ./electron-app/content will all of the images. This list of files will update whenever new files are uploaded to the Gumband UI.

Next, similar to how we added the signage copy settings, we’ll follow a similar set of steps here: update the manifest with the new setting, listen for the setting change from the Gumband Cloud (we’re already doing this), and communicate any image change to the Electron app.

./gumband/manifest.json

Code Block
{
    "manifest": {
        "statuses": [],
        "controls": [],
        "settings": [
            {
                "id": "signage-group",
                "type": "SettingsGroup",
                "display": "Digital Signage Settings",
                "order": 0,
                "schema": [
                    ...
                    {
                        "id": "main-image",
                        "type": "FileSelection",
                        "display": "Image Asset",
                        "order": 3
                    }
                ]
            }
        ]
    }
  }

./gumband/gumband-service.js

Code Block
...

async updateFrontendFromSettings() {
    ...
    const image = await this.getSettingValue("signage-group/main-image");
    ...
    this.window.webContents.send("fromGumband", { type: "main-image", value: image });
}

...

./electron-app/main.js

Code Block
...

window.gb.receive("fromGumband", (data) => {
    const root = window.document.getElementById("root");
    switch(data.type) {
        ...
        case "main-image":
            const mainImage = window.document.getElementsByClassName('main-image')[0];
            let imageContent = document.createElement('img');
            if(data.value) {
                imageContent.src = `./content/${data.value}`;
            }
            mainImage.appendChild(imageContent);
            break;
    }
});

After restarting the app, you can now select any of the images you uploaded with your new setting in the Exhibit Settings tab of the Gumband UI. The selected image will show in the Electron app.

Toggle Between the Digital Signage and the Game

All the logic to build out the Button Clicker game in the frontend is already defined in the ./electron-app/main.js file, so all we need to do is call those functions based on a setting in Gumband. Let’s make this a Toggle setting called “Game Mode”; when it is true (toggled on) we’ll show the Button Clicker game, and when it is false we’ll show the Digital Signage. Once again, the changes required are in the manifest.json, the gumband-service.js, and the main.js. For the manifest, we’ll put the new setting outside the digital signage group of settings, since it is applicable to both the digital signage settings and the Button Clicker settings (to be added in a later step):

./gumband/manifest.json

Code Block
{
    "manifest": {
        "statuses": [],
        "controls": [],
        "settings": [
            {
                "id": "signage-group",
                "type": "SettingsGroup",
                "display": "Digital Signage Settings",
                "order": 0,
                "schema": [
                    ...
                ]
            },
            {
                "id": "game-mode",
                "type": "Toggle",
                "display": "Game Mode",
                "default": "",
                "order": 1
            }
        ]
    }
}

./gumband/gumband-service.js

Code Block
...

async updateFrontendFromSettings() {
    if(!this.opMode) {
        return;
    }

    const gameMode = this.convertToBoolean(await this.getSettingValue("game-mode"));
    const header = await this.getSettingValue("signage-group/header");
    const subheader = await this.getSettingValue("signage-group/subheader");
    const body = await this.getSettingValue("signage-group/body");
    const image = await this.getSettingValue("signage-group/main-image");

    let bodyParagraphs = [];
    if(body) {
        bodyParagraphs = body.split('|');
    }

    this.window.webContents.send("fromGumband", { type: "game-mode", value: gameMode });   
    if(!gameMode) {
        this.window.webContents.send("fromGumband", { type: "header", value: header });
        this.window.webContents.send("fromGumband", { type: "subheader", value: subheader });
        this.window.webContents.send("fromGumband", { type: "body", value: bodyParagraphs });
        this.window.webContents.send("fromGumband", { type: "main-image", value: image });
    }
}

/**
  * Needed because Gumband toggle settings return their boolean value as a string instead of a boolean.
  * @param {*} string a string that is "true" or "false"
  * @returns boolean
  */
convertToBoolean(string) {
    return string && string !== "false";
}

./electron-app/main.js

Code Block
window.gb.receive("fromGumband", (data) => {
    const root = window.document.getElementById("root");
    switch(data.type) {
        case "game-mode":
            clearChildren(root);
            if(data.value) {
                createGameMenu();
            } else {
                createDigitalSignagePage();
            }
            break;
        case "operation-mode":
            if(!data.value) {
                clearChildren(root);
            }
            break;
        ...
    }
});

Now when you toggle the new “Game Mode” setting on, you will see the app change to the Button Clicker game.

Configuring Button Clicker

Each Button Clicker game is 5 seconds long by default, and the length of time that the game summary screen shows is also 5 seconds (set by gameDuration and gameSummaryScreenDuration variables in the main.js file). But what if we want to configure these times without changing the source code? Let’s implement that:

./gumband/manifest.json

Code Block
{
    "manifest": {
        "statuses": [],
        "controls": [],
        "settings": [
            ...
            {
                "id": "game-group",
                "type": "SettingsGroup",
                "display": "Game Settings",
                "order": 2,
                "schema": [
                    {
                        "id": "game-duration",
                        "type": "IntegerInput",
                        "display": "Game Duration (seconds)",
                        "order": 0
                    },
                    {
                        "id": "game-summary-screen-duration",
                        "type": "IntegerInput",
                        "display": "Game Summary Screen Duration (seconds)",
                        "order": 1
                    }
                ]
            }
        ]
    }
  }

./gumband/gumband-service.js

Code Block
async updateFrontendFromSettings() {
    ...
    
    const gameDuration = await this.getSettingValue("game-group/game-duration");
    const gameSummaryScreenDuration = await this.getSettingValue("game-group/game-summary-screen-duration");

    ...
    
    if(!gameMode) {
        ...
    } else {
        this.window.webContents.send("fromGumband", { type: "game-duration", value: gameDuration });
        this.window.webContents.send("fromGumband", { type: "game-summary-screen-duration", value: gameSummaryScreenDuration });
        
        //Need to trigger the "game-mode" update at the end of the game duration updates. This is 
        //because the game duration updates don't trigger a re-render of the frontend, 
        //but the "game-mode" update does.
        this.window.webContents.send("fromGumband", { type: "game-mode", value: gameMode });
    }
}

./electron-app/main.js

Code Block
window.gb.receive("fromGumband", (data) => {
    const root = window.document.getElementById("root");
    switch(data.type) {
        case "game-duration":
            gameDuration = data.value;
            break;
        case "game-summary-screen-duration":
            gameSummaryScreenDuration = data.value;
            break;
        ...
    }
});

Track User Interaction with Reporting Events

We can configure many aspects of our app, but we can’t yet determine if anyone is actually interacting with it. Let’s track that with a reporting event that gets triggered every time someone finishes playing a game. There’s already a notification being sent from the Electron app whenever the game summary page is shown:

./electron-app/main.js

Code Block
function createEndGameScreen() {
    ...

    window.gb.send("fromElectron", { type: 'game-completed', value: targetCount });

    ...
}

So all we need to do now is to listen for that event in our gumband-service.js and send off a reporting event to the Gumband Cloud.

./gumband/gumband-service

Code Block
const { ipcMain } = require("electron");

...

constructor(window) {
    ...
    
    this.addElectronAppListeners();
}

/**
  * Add listeners for events sent from the Electron App.
  */
addElectronAppListeners() {
    ipcMain.on("fromElectron", async (event, data) => {
        if (data.type === "game-completed") {
            this.gumbandSDK.event.create("game-completed", { 
                "targets-clicked": data.value,
                "game-duration": await this.getSettingValue("game-group/game-duration")
            });
        }
    });
}

The next time you complete a game, you should now see a reporting event come into the Gumband UI under the Reports tab:

Make Exhibit Management Easier With Controls

Controls are custom, one-time events that can be triggered through the Gumband UI, and they must be configured through the manifest. Let’s add two controls: one for reloading the frontend in case it somehow gets into a weird state, and one for toggling the Game Mode just to make that a faster process.

First, we need to configure the controls by adding them to the manifest:

./gumband/manifest.json

Code Block
{
    "manifest": {
        "statuses": [],
        "controls": [
            {
              "id": "reload-frontend",
              "type": "Single",
              "display": "Reload Frontend",
              "order": 0
            },
            {
              "id": "toggle-game-mode",
              "type": "Single",
              "display": "Toggle Game Mode",
              "order": 1
            }
        ],
        "settings": [
            ...
        ]
    }
  }

When controls are triggered, the Gumband SDK will receive it as a Sockets.CONTROL_RECEIVED event, so let’s listen for that:

./gumband/gumband-service.js

Code Block
addSDKListeners() {
    ...
    
    //listen for the CONTROL_RECEIVED event
    this.gumbandSDK.on(Sockets.CONTROL_RECEIVED, async (payload) => {
        //when the control triggered is the "toggle-game-mode" control,
        //use the setSetting SDK function to toggle the "game-mode" setting
        //to the opposite of its current value
        if(payload.id === "toggle-game-mode") {
            this.gumbandSDK.setSetting(
                "game-mode", 
                !this.convertToBoolean(
                    (await this.getSettingValue("game-mode"))
                )
            );
        } else if (payload.id === "reload-frontend") {
            this.window.reload();
            setTimeout(() => {
                this.updateFrontendFromSettings();
            }, 100);
        }
    });
}

You should now be able to toggle the Game Mode with a single button click, and you can reload the frontend if needed. Test out the new controls in the Controls tab of the Gumband UI.

Add Health Statuses

Statuses are often used as a way to display the health state of your exhibit, but they can be used to show any value that can be represented as a string and that you’d like to have easily viewable. All statuses are displayed on the Overview tab of the Gumband UI to provide a quick glance at the state of your exhibit at any given time.

We’re going to add one status that shows what screen is currently being displayed: the standby screen, digital signage, or the game. The other status is going to be the date of the last time a user completed a game. Statuses are configured through the manifest, so we’ll start there:

./gumband/manifest.json

Code Block
{
    "manifest": {
        "statuses": [
            {
                "id": "screen-status",
                "type": "String",
                "display": "Screen is currently showing:",
                "order": 0
            },
            {
                "id": "last-game-played",
                "type": "String",
                "display": "Last game played:",
                "order": 1
            }
        ],
        "controls": [
            ...
        ],
        "settings": [
            ...
        ]
    }
}

Now we need to use the setStatus SDK function to update the “screen-status” any time the frontend can change to/from game mode and to/from operation mode.

./gumband/gumband-service.js

Code Block
...

setFrontendOperationMode() {
    this.window.webContents.send("fromGumband", { type: "operation-mode", value: this.opMode });
    if(!this.opMode) {
        this.gumbandSDK.setStatus("screen-status", "Standby Screen");
    }
}

async updateFrontendFromSettings() {
    ...
    
    if(!gameMode) {
        ...
        this.gumbandSDK.setStatus("screen-status", "Digital Signage");
    } else {
        ...
        this.gumbandSDK.setStatus("screen-status", "Game Screen");
    }
}

Then set the “last-game-played” status when a game has been completed:

./gumband/gumband-service.js

Code Block
addElectronAppListeners() {
    ipcMain.on("fromElectron", async (event, data) => {
        if (data.type === "game-completed") {
            ...
            this.gumbandSDK.setStatus("last-game-played", new Date().toString());
        }
    });
}

If you restart the app and complete a game, you will see your statuses update in real time.

Add Logging

There are 4 log levels you can log to in Gumband: info, debug, warning, and error. You can filter by these logs in the Gumband UI. For our app, let’s just add some info logs so we’ll have a record of when settings or the operation mode were changed, or when controls were triggered.

./gumband/gumband-service.js

Code Block
addSDKListeners() {
    ...
    
    this.gumbandSDK.on(Sockets.OP_MODE_RECEIVED, (payload) => {
        ...
        this.gumbandSDK.logger.info(`OP_MODE changed to: ${payload.value}`);
    });

    this.gumbandSDK.on(Sockets.SETTING_RECEIVED, (payload) => {
        this.gumbandSDK.logger.info(`${payload.id} setting updated to: ${payload.value}`);
        ...
    });

    this.gumbandSDK.on(Sockets.CONTROL_RECEIVED, async (payload) => {
        this.gumbandSDK.logger.info(`Control triggered: ${payload.id}`);
        ...
    });
}

Logging messages to Gumband is as easy as that. You can view the logs in the Logs tab in the Gumband UI:

System Diagram

Here is a system diagram of what we’ve implemented so far:

It shows the communication protocols used and the areas in which we’ve been implementing code.

Bonus

That concludes the main part of this tutorial. If you’re interested in implementing a few additional features (including a simple Gumband Hardware integration) press onward.

Upload Seed Images on Startup

It would be nice if we didn’t need to upload the seed images manually before we started our app the first time. What if this was just the development version, but for production there were 20 images to choose from? Let’s add a function that automatically uploads the seed images to Gumband with the uploadFile SDK function.

./gumband/gumband-service.js

Code Block
const fs = require('fs');
...

class GumbandService {

    ...

    addSDKListeners() {
        this.gumbandSDK.on(Sockets.READY, async (manifest) => {
            await this.addSeedImages();
            ...
        });
        
        ...
    }
    
    ...
    
    /**
      * Upload the default images included in the repo to the Gumband cloud if they aren't uploaded already.
      */
    async addSeedImages() {
        //get a list of all files already uploaded to Gumband.
        let currentRemoteFiles = (await this.gumbandSDK.content.getRemoteFileList()).files.map(file => file.file);
        
        //get a list of all files in the seed-images directory
        fs.readdir(`${__dirname}/../seed-images`, async (e, files) => {
            let fileUploadPromises = files.map((file) => {
            
                //only upload files that aren't already uploaded
                if(!currentRemoteFiles.find(currentFile => currentFile === file)) {
                    let stream = fs.createReadStream(`${__dirname}/../seed-images/${file}`)
                    return this.gumbandSDK.content.uploadFile(stream);
                };
            });
    
            //wait for all of the files to upload, then pull all the new files down to
            //the ./electron-app/content/ directory for our Electron app
            await Promise.all(fileUploadPromises);
            this.gumbandSDK.content.sync();
        });
    }
    
    ...
}

To test that this is working correctly, delete the ./electron-app/content directory. If you recall from the Add Setting for Updating the Signage Image section, this is the directory that we configured the Gumband SDK to place all of the files uploaded to the Gumband UI. You’ll also need to delete the images in the Gumband UI, so the Files tab shows no files:

Now, when you next run the app, the gumband-service.js should upload all of the files in the seed-images directory to the Gumband Cloud and the ./electron-app/content should get recreated with those files.

Hardware Integration

For this step, you’ll need a Gumband Hardware. We’ll be using one of the built-in buttons on the Hardware to toggle the Game Mode through the Gumband SDK.

If you haven’t already, follow the Hardware Getting Started Guide to set up the Gumband Hardware and your development environment. Create your new Hardware instance under the same Site in the Gumband UI as this tutorial exhibit. In this tutorial, the Site has been “Sandbox”, which you can see in the top left of each image of the Gumband UI. Creating the Hardware in the same Site as our exhibit instance will allow the Hardware and our app to communicate.

In the Arduino IDE, open the SendButtonPresses firmware example by navigating to File → Examples → Gumband API → SendButtonPresses, and flash it onto your Gumband Hardware. This firmware causes an event to be sent whenever the built-in button on the Gumband Hardware (next to the Ethernet port) is pressed.

Before we go any further, take a look at this updated system diagram:

Notice that the Gumband Hardware authenticates with the Gumband Cloud, then it receives back the “IP and port of the exhibit GBTT service”. The “exhibit GBTT service” is an MQTT message broker that runs on your exhibit locally through the Gumband SDK, and it is how the Gumband Hardware communicates with your exhibit. So we need to enable the GBTT service for our exhibit, then we need to tell the Gumband Cloud the IP and port of the service so the Cloud can pass that along to the hardware after it authenticates.

Starting the exhibit GBTT service is as simple as adding a two configurations to our Gumband SDK initialization.

.env

Code Block
...
EXHIBIT_GBTT_PORT=1884

./gumband/gumband-service.js

Code Block
class GumbandService {
    ...

    constructor(window) {
        ...
        this.gumbandSDK = new Gumband(
            process.env.EXHIBIT_TOKEN,
            process.env.EXHIBIT_ID,
            `${__dirname}/manifest.json`,
            {
                contentLocation: './electron-app/content',
                gbttEnabled: true,
                gbttPort: process.env.EXHIBIT_GBTT_PORT
            }
        );
        ...
    }

Now when you start the app, the Gumband SDK will start up the MQTT message broker at port 1884.

Next, run ifconfig in a terminal to find your local IP address and enter that and the GBTT port into the Exhibit Hardware tab of the Gumband UI:

To tell the Gumband Cloud that this exhibit is associated with your hardware, click “Connect hardware” and select the hardware you just added. Let’s test that we have it all set up correctly and that the Gumband Hardware can connect to our exhibit MQTT broker (the GBTT service). Connect the Gumband Hardware to power and to the same local network as your exhibit. If the LED on the hardware pulses green, that means it successfully authenticated with the Gumband Cloud, received the IP and port of your MQTT broker, and connected to it. For more details on the Gumband Hardware authentication and registration flow, check out the Gumband MQTT Reference Guide.

The last step is to listen for the event that the Gumband Hardware sends to our exhibit MQTT broker whenever the button is clicked:

./gumband/gumband-service.js

Code Block
addSDKListeners() {
    ...
    
    this.gumbandSDK.on(Sockets.HARDWARE_PROPERTY_RECEIVED, async (payload) => {
        if(payload.peripheral === "Button" && payload.value === 0) {
            this.gumbandSDK.setSetting(
                "game-mode", 
                !this.convertToBoolean(
                    (await this.getSettingValue("game-mode"))
                )
            );
        }
    });
}

Test it out by clicking the button on the Gumband Hardware. It should toggle the Game Mode setting.