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=abc1...23
EXHIBIT_ID=1

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

gumband/manifest.js

Code Block
languagejs
const manifest = {
    manifest: {}
}

module.exports = { manifest };

Later, this manifest will tell Gumband what configurations your exhibit has, but right now it’s empty.

Now we need to initialize the Gumband SDK class, and pass it our exhibit id, token, and manifest so it can connect to Gumband. Create a new directory and file named ./gumband/gumband-service.js, and initialize a Gumband SDK inside that new file:

gumband/gumband-service.js

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

/**
 * 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,
            manifest,
        );
    }
}

module.exports = { GumbandService };

Initialize the GumbandService class in the electron app:

electron-app/index.js

Code Block
languagejs
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:

Make the Digital Signage Copy Text Configurable

To start, let’s make the Digital Signage copy configurable through Gumband. We’ll need to add settings to the manifest that will represent the different copy text we’d like to be able to configure. 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 (SettingsGroups simply change how the settings are organized in the Gumband UI):

gumband/manifest.js

Code Block
languagejs
const SIGNAGE_GROUP_ID = "signage-group";
const SIGNAGE_HEADER_ID = "header";
const SIGNAGE_SUBHEADER_ID = "subheader";
const SIGNAGE_BODY_ID = "body";

const manifest = {
    manifest: {
        statuses: [],
        controls: [],
        settings: [
            {
                id: SIGNAGE_GROUP_ID,
                type: "SettingsGroup",
                display: "Digital Signage Settings",
                order: 0,
                schema: [
                    {
                        id: SIGNAGE_HEADER_ID,
                        type: "TextInput",
                        display: "Header Copy",
                        order: 0
                    },
                    {
                        id: SIGNAGE_SUBHEADER_ID,
                        type: "TextInput",
                        display: "Subheader Copy",
                        order: 1
                    },
                    {
                        id: SIGNAGE_BODY_ID,
                        type: "TextInput",
                        display: "Body Copy (separate by | for new paragraph)",
                        order: 2
                    }
                ]
            }
        ]
    }
}

module.exports = { 
    manifest,
    SIGNAGE_GROUP_ID,
    SIGNAGE_HEADER_ID,
    SIGNAGE_SUBHEADER_ID,
    SIGNAGE_BODY_ID,
};

If you restart the app again, this new manifest will be submitted to Gumband you’ll see them settings in the Gumband UI:

When these settings are changed in the UI, our app will receive a Sockets.SETTING_RECEIVED websocket event. Let’s listen for that event and create a function to update the Electron app with text changes:

gumband/gumband-service.js

Code Block
languagejs
const { Gumband, Sockets } = require("@deeplocal/gumband-node-sdk");
const { manifest, SIGNAGE_GROUP_ID, SIGNAGE_HEADER_ID, SIGNAGE_SUBHEADER_ID, SIGNAGE_BODY_ID } = require("./manifest");

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

    constructor(window) {
        this.window = window;
        this.gumbandSDK = new Gumband(
            process.env.EXHIBIT_TOKEN,
            process.env.EXHIBIT_ID,
            manifest,
        );

        this.addSDKListeners();
    }

    addSDKListeners() {
        this.gumbandSDK.on(Sockets.READY, async (manifest) => {
            //update the frontend from the current settings when the app starts
            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() {
        const header = await this.getSettingValue(`${SIGNAGE_GROUP_ID}/${SIGNAGE_HEADER_ID}`);
        const subheader = await this.getSettingValue(`${SIGNAGE_GROUP_ID}/${SIGNAGE_SUBHEADER_ID}`);
        const body = await this.getSettingValue(`${SIGNAGE_GROUP_ID}/${SIGNAGE_BODY_ID}`);
    
        //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: SIGNAGE_HEADER_ID, value: header });
        this.window.webContents.send("fromGumband", { type: SIGNAGE_SUBHEADER_ID, value: subheader });
        this.window.webContents.send("fromGumband", { type: SIGNAGE_BODY_ID, 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;
    }
}

module.exports = { GumbandService };

If you noticed, we also needed to pass a new window object into the constructor, so the GumbandService can communicate with the Electron app. Let’s pass that window object into the GumbandService instantiation:

electron-app/index.js

Code Block
languagejs
...

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

Now we listen for content events coming from the GumbandService to the Electron App:

electron-app/main.js

Code Block
languagejs
...

//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;
            clearChildren(header);
            header.appendChild(headerContent);
            break;
        case "subheader":
            const subheader = window.document.getElementsByClassName('subheader')[0];
            let subheaderContent = document.createElement('span');
            subheaderContent.innerText = data.value;
            clearChildren(subheader);
            subheader.appendChild(subheaderContent);
            break;
        case "body":
            const body = window.document.getElementsByClassName('body')[0];
            clearChildren(body);
            data.value.forEach(text => {
                let bodyParagraph = document.createElement('p');
                bodyParagraph.innerText = text;
                body.appendChild(bodyParagraph);
            });
    }
});

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. This is because it is pulling the text from the Gumband settings, and those settings are blank. If you fill them in and click “Save”, you will see the Electron app populate in real time.

Image RemovedImage Added

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
languagejs
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. We also want these files to update whenever new files are uploaded to the Gumband UI. Let’s implement that by listening for the Sockets.FILE_UPLOADED event, which is fired whenever a new file is uploaded to your exhibit instance in Gumband:

gumband/gumband-service.js

Code Block
addSDKListeners() {
    ...
    
    this.gumbandSDK.on(Sockets.FILE_UPLOADED, async (manifest) => {
        //The content.sync method will pull all new files from the Gumband Cloud and save them
        //to the electron-app/content directory whenever a new file is uploaded.
        this.gumbandSDK.content.sync();
    });
}

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

gumband/manifest.js

Code Block
languagejs
...
const SIGNAGE_MAIN_IMAGE_ID = "main-image";

const manifest = {
    manifest: {
        statuses: [],
        controls: [],
        settings: [
            {
                id: SIGNAGE_GROUP_ID,
                type: "SettingsGroup",
                display: "Digital Signage Settings",
                order: 0,
                schema: [
                    ...
                    {
                        id: SIGNAGE_MAIN_IMAGE_ID,
                        type: "FileSelection",
                        display: "Image Asset",
                        order: 3
                    }
                ]
            }
        ]
    }
}

module.exports = { 
    ...
    SIGNAGE_MAIN_IMAGE_ID,
};

gumband/gumband-service.js

Code Block
languagejs
//import SIGNAGE_MAIN_IMAGE_ID

...

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

...

electron-app/main.js

Code Block
languagejs
...

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];
            clearChildren(mainImage);
            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.js, 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
languagejs
...

const GAME_MODE_ID = "game-mode";

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

module.exports = { 
    ...
    GAME_MODE_ID,
};

gumband/gumband-service.js

Code Block
languagejs
...

async updateFrontendFromSettings() {
    const gameMode = await this.getSettingValue(GAME_MODE_ID);
    ...

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

    this.window.webContents.send("fromGumband", { type: GAME_MODE_ID, value: gameMode });   
    if(!gameMode) {
        this.window.webContents.send("fromGumband", { type: SIGNAGE_HEADER_ID, value: header });
        this.window.webContents.send("fromGumband", { type: SIGNAGE_SUBHEADER_ID, value: subheader });
        this.window.webContents.send("fromGumband", { type: SIGNAGE_BODY_ID, value: bodyParagraphs });
        this.window.webContents.send("fromGumband", { type: SIGNAGE_MAIN_IMAGE_ID, value: image });
    }
}

electron-app/main.js

Code Block
languagejs
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;
        ...
    }
});

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.js

Code Block
languagejs
const GAME_GROUP_ID = "game-group";
const GAME_DURATION_ID = "game-duration";
const GAME_SUMMARY_SCREEN_DURATION_ID = "game-summary-screen-duration";

const manifest = {
    manifest: {
        statuses: [],
        controls: [],
        settings: [
            ...
            {
                id: GAME_GROUP_ID,
                type: "SettingsGroup",
                display: "Game Settings",
                order: 2,
                schema: [
                    {
                        id: GAME_DURATION_ID,
                        type: "IntegerInput",
                        display: "Game Duration (seconds)",
                        default: "5",
                        order: 0
                    },
                    {
                        id: GAME_SUMMARY_SCREEN_DURATION_ID,
                        type: "IntegerInput",
                        display: "Game Summary Screen Duration (seconds)",
                        default: "5"
                        order: 1
                    }
                ]
            }
        ]
    }
}

module.exports = { 
    ...
    GAME_GROUP_ID,
    GAME_DURATION_ID,
    GAME_SUMMARY_SCREEN_DURATION_ID,
};

gumband/gumband-service.js

Code Block
languagejs
//import GAME_GROUP_ID, GAME_DURATION_ID, and GAME_SUMMARY_SCREEN_DURATION_ID

async updateFrontendFromSettings() {
    ...
    
    const gameDuration = await this.getSettingValue(`${GAME_GROUP_ID}/${GAME_DURATION_ID}`);
    const gameSummaryScreenDuration = await this.getSettingValue(`${GAME_GROUP_ID}/${GAME_SUMMARY_SCREEN_DURATION_ID}`);

    ...
    
    if(!gameMode) {
        ...
    } else {
        this.window.webContents.send("fromGumband", { type: GAME_DURATION_ID, value: gameDuration });
        this.window.webContents.send("fromGumband", { type: GAME_SUMMARY_SCREEN_DURATION_ID, 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_ID, value: gameMode });
    }
}

electron-app/main.js

Code Block
languagejs
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;
        ...
    }
});

Measuring Interaction Time

When measuring the engagement of your exhibit we use have two basic concepts to understand: “events” and “interactions”.

Events are singular moments in time when something happens on the exhibit, typically due to a user action like a button push. An interaction is simply a collection of events that occurred within a specific window of time.

Getting started with Gumband interaction metrics is as simple as using the metrics module to create events.

Lets emit a new type of event from the electron app anytime a target is hit by adding a single line to createNewTarget.js.

Code Block
languagejs
// gumband-sdk-tutorial/electron-app/main.js

... 
/**
 * Creates a new target and renders it in a random location on the screen.
 */
function createNewTarget() {
    ...
    target.onclick = () => {
        targetCount++;
        // 👇 emit an event from electron to the gumband service
        window.gb.send("fromElectron", { type: 'target-hit' });
        createNewTarget();
    };
    ...
}

Next we handle this new data type event within the gumband-service.js by calling the metrics.create(...) method

Code Block
languagejs
// gumband-sdk-tutorial/gumband/gumband-service.js

const { ipcMain } = require("electron");
...

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

    /**
     * Add listeners for events sent from the Electron App.
     */
    addElectronAppListeners(event, data) {
      if (data.type === 'target-hit') {
          this.gumbandSDK.metrics.create('target-hit')
      }
    }
  }

Once you begin emitting events in this manner the Gumband backend internally begins an interaction and will continue to group any incoming events into that particular interaction. If no events have been emitted within 60 seconds then the interaction will timeout and be considered ended.

For our game scenario, if we play a singular game (and hit at least 1 target) we can expect that interaction to last about 1 minute. However if we play several games one after another (hitting at least 1 target each time to trigger the metrics.create call) then we can expect that interaction to last longer than 1 minute because events continued to come in, preventing the Gumband backend from reaching it’s timeout period.

If you want to calibrate the timeout duration you can check out: How To Configure the Interaction Timeout

Make Exhibit Management Easier With Controls

Controls are custom, one-time events that can be triggered through the Gumband UI, which 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.js

Code Block
languagejs
const RELOAD_FRONTEND_CONTROL = "reload-frontend";
const TOGGLE_GAME_MODE_CONTROL = "toggle-game-mode";
...

const manifest = {
    manifest: {
        statuses: [],
        controls: [
            {
                id: RELOAD_FRONTEND_CONTROL,
                type: "Single",
                display: "Reload Frontend",
                order: 0
              },
              {
                id: TOGGLE_GAME_MODE_CONTROL,
                type: "Single",
                display: "Toggle Game Mode",
                order: 1
              }
        ],
        settings: [
            ...
        ]
    }
}

module.exports = { 
    RELOAD_FRONTEND_CONTROL,
    TOGGLE_GAME_MODE_CONTROL,
    ...
};

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

gumband/gumband-service.js

Code Block
languagejs
//import TOGGLE_GAME_MODE_CONTROL and RELOAD_FRONTEND_CONTROL

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_CONTROL) {
            this.gumbandSDK.setSetting(
                GAME_MODE_ID, 
                !(await this.getSettingValue(GAME_MODE_ID))
            );
        } else if (payload.id === RELOAD_FRONTEND_CONTROL) {
            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 Status 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 digital signage, or the game. Statuses are configured through the manifest, so we’ll start there:

gumband/manifest.js

Code Block
languagejs
const SCREEN_STATUS = "screen-status";
...

const manifest = {
    manifest: {
        statuses: [
            {
                id: SCREEN_STATUS,
                type: "String",
                display: "Screen is currently showing:",
                order: 0
            }
        ],
        controls: [
            ...
        ],
        settings: [
            ...
        ]
    }
}

module.exports = { 
    SCREEN_STATUS,
    ...
};

Now we need to use the setStatus SDK function to update the “screen-status” any time the frontend can change modes.

gumband/gumband-service.js

Code Block
languagejs
//import SCREEN_STATUS
...

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

If you restart the app, 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 were changed or when controls were triggered.

gumband/gumband-service.js

Code Block
languagejs
addSDKListeners() {
    ...

    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:

Gumband SDK Tutorial (2).png

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
languagejs
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 directory should get recreated with those files.

Custom Email Notifications

Let’s say our exhibit has a screen that should be serviced every few months, and we want to have an email notification sent out whenever it needs attention. We can accomplish this with custom email notifications:

.env

Code Block
...
# Epoch time (in seconds) for August 15, 2023
INSTALL_DATE=1692121575

# 6 months in seconds
MAINTENANCE_INTERVAL=15770000

gumband/gumband-service.js

Code Block
languagejs
const ONE_DAY_IN_MILLISECONDS = 86400000;

...

class GumbandService {
    ...
    
    /**
     * The installDate in Epoch time (seconds)
     */
    installDate;
    /**
     * The time interval that should elapse between maintenances in seconds.
     */
    maintenanceInterval;
    /**
     * The number of maintenance reminders that have been triggered since the install date.
     */
    maintenanceNotificationsTriggered;
    
    constructor(window) {
        this.installDate = parseInt(process.env.INSTALL_DATE);
        this.maintenanceInterval = parseInt(process.env.MAINTENANCE_INTERVAL);
        ...
    }
    
    addSDKListeners() {
        this.gumbandSDK.on(Sockets.READY, async (manifest) => {
            ...
            this.initializeMaintenanceReminders();
        });
        
        ...
        
    }
    
    ...
    
    /**
     * Initializes a time interval that runs every day to check if a new maintenance reminder should be sent.
     */
    initializeMaintenanceReminders() {
        this.maintenanceNotificationsTriggered = this.getMaintenanceIntervalsSinceInstall();
        setInterval(async () => {
            if(this.maintenanceNotificationsTriggered < this.getMaintenanceIntervalsSinceInstall()) {
                //------------------------------------------------------------------------
                //This is the line that actually triggers the email notification. 
                //Everything else is just logic to get it to send at the right time.
                this.gumbandSDK.notifications.email('This is a maintenance reminder');
                //------------------------------------------------------------------------
                
                this.maintenanceNotificationsTriggered++;
            }
        }, ONE_DAY_IN_MILLISECONDS);
    }

    /**
     * Returns the number of maintenances that should have occurred since initial installation.
     * @returns The number of maintenances that should have occurred since install
     */
    getMaintenanceIntervalsSinceInstall() {
        return Math.floor((Math.floor(new Date().getTime() / 1000) - this.installDate) / this.maintenanceInterval);
    }
}

Going forward, any user subscribed to the “Custom Exhibit” email notification in the Gumband UI will receive a maintenance reminder every 6 months.

Hardware Integration

For this step, you’ll need a Gumband Hardware. We’ll be implementing two integrations with the Gumband Hardware:

  1. When one of the built-in buttons on the Gumband Hardware is pressed, the Game Mode setting will be toggled through the Gumband SDK.

  2. When a user is playing the “Button Clicker” game, and they click on a button target, the LED on the Gumband Hardware will pulse on/off.

Connecting Your Hardware to the SDK

If you haven’t already, follow the Hardware Getting Started Guide to set up the Gumband Hardware and your development environment.

In the Arduino IDE, open the RemoteLEDandButton firmware example by navigating to File → Examples → Gumband SDK Examples → RemoteLEDandButton, 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. Note that this diagram also includes the “Pulse LED” event that is part of a later step:

Gumband SDK Tutorial with Hardware (2).png

Notice that the Gumband Hardware is communicating with our Gumband Service through an MQTT broker that the SDK owns (it is also an option to use your own third party MQTT broker). So to establish this communication we need to:

  1. Enable the built-in SDK MQTT broker and configure it with a port.

  2. Tell the hardware what the MQTT broker IP and Port are.

Starting the built-in MQTT broker is as simple as adding one configuration to our Gumband SDK initialization.

gumband/gumband-service.js

Code Block
languagejs
class GumbandService {
    ...

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

Now when you start the app, the Gumband SDK will start up the MQTT message broker at the default port, 1883. See the API Reference documentation for more information on configuring the MQTT broker.

Next, run ifconfig in a terminal to find your local IP address and configure the Gumband Hardware with this address and the MQTT port. You can do this with the Arduino IDE Serial Monitor with the write app address and write app port commands:

arduino-gif.gif

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.

Receiving a Hardware Event

Now, let’s make the Game Mode setting toggle whenever the built-in button on the Gumband Hardware is pressed. All we need to do is listen for the Sockets.HARDWARE_PROPERTY_RECEIVED event and then toggle the Game Mode setting:

gumband/gumband-service.js

Code Block
languagejs
addSDKListeners() {
    ...
    
    this.gumbandSDK.on(Sockets.HARDWARE_PROPERTY_RECEIVED, async (payload) => {
        if(payload.property === "Button/Press" && !payload.value[0]) {
            this.gumbandSDK.setSetting(
                GAME_MODE_ID, 
                !(await this.getSettingValue(GAME_MODE_ID))
            );
        }
    });
}

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

Writing a Property Value to the Hardware

Let’s simulate a user interaction that causes a physical change in our exhibit by making the built-in LED on the Gumband Hardware blink when the user clicks a button target in the Button Clicker game. To do this, we’ll need to:

  1. Listen for the Sockets.HARDWARE_FULLY_REGISTERED event in the gumband-service.js so that we can get the ID of the hardware. We will need this for the next step.

  2. Listen for the Electron “target-hit” event in the gumband-service.js and dispatch MQTT messages that will toggle the hardware LED on and off.

gumband/gumband-service.js

Code Block
...

/**
 * The length of time in milliseconds that the Hardware LED should blink on when a button in Button Click is clicked.
 * You could even make this into a new, configurable setting!
 */
const HARDWARE_LED_BLINK_TIME = 25;

...

class GumbandService {
    ...
    /**
      * The ID of the Gumband Hardware.
      */
    hardwareId;
    
    addSDKListeners() {
        ...
        
        this.gumbandSDK.on(Sockets.HARDWARE_FULLY_REGISTERED, (payload) => {
            this.hardwareId = payload.id;
        });
    }
    
    addElectronAppListeners() {
        ipcMain.on("fromElectron", async (event, data) => {
            if (data.type === 'target-hit') {
                //send an MQTT message through the Gumband SDK (goes through the SDK GBTT service)
                //that turns the LED on.
                this.gumbandSDK.hardware.setProperty(this.hardwareId, 'LED/Toggle', 1);
                setTimeout(() => {
                    //send an MQTT message that turns the LED off.
                    this.gumbandSDK.hardware.setProperty(this.hardwareId, 'LED/Toggle', 0);
                }, HARDWARE_LED_BLINK_TIME);
                
                this.gumbandSDK.metrics.create('target-hit');
            }
        });
    }
    
    ...
    
}
Panel
panelIconId2139
panelIcon:information_source:
panelIconTextℹ️
bgColor#DEEBFF

One thing to keep in mind is that the property path hardcoded in lines 31 and 34 above (“LED/Toggle”) will change based on the firmware you’ve flashed onto the Gumband Hardware. You can see all of the properties and peripherals on the hardware by logging out the payload of the Sockets.HARDWARE_FULLY_REGISTERED event.

Test this out by playing the Button Clicker game. An LED on the Gumband Hardware should flash every time you click on one of the button targets.

Congratulations on making it the whole way through this Tutorial! Now that you have a general understanding of Gumband, check out the Quick Start guide for any reminders about the syntax usage of the SDK, and see the API Reference doc to look through the entire SDK API.