Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 30 Next »

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

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

EXHIBIT_TOKEN=abc1...23
EXHIBIT_ID=1

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

gumband/gumband-service.js

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

{
    "manifest": {}
}

Initialize the GumbandService class in the electron app:

electron-app/index.js

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

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

let win;

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

  ...
}

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

gumband/gumband-service.js

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

...

/**
 * 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

...


    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

{
    "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

...

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

...

//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

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

{
    "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

...

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

...

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

{
    "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

...

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

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

{
    "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

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

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

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

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

{
    "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

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

{
    "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

...

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

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

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

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.

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

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

# 6 months in seconds
MAINTENANCE_INTERVAL=15770000

gumband/gumband-service.js

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, the LED on the Gumband Hardware will pulse brighter.

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

...
EXHIBIT_GBTT_PORT=1884

gumband/gumband-service.js

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.

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

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 next to the ethernet port on the Gumband Hardware. It should toggle the Game Mode setting.

Writing a Property Value to the Hardware

TODO - the example firmware this will work with this isn’t published to the public repo for the Gumband Hardware yet. For now, I’ve added a copy of the firmware in the accordion below. This file needs to be flashed to the Gumband Hardware for this step to work

 Example Firmware
/*
 * Gumband Remote Button/LED Example
 * 
 * Demonstrates how to control hardware using an Exhibit Application
 *   - sets up a property to turn the on-board LED on/off.
 *   - sets up a property that updates when on-board button is pressed/released.
 */

// Create the Gumband Properties
GumbandProp button_prop = gumbandCreate("Button", "Press", gmbnd_bool);
GumbandProp led_toggle_prop = gumbandCreate("LED", "Toggle", gmbnd_bool);

// Local variable that keeps track of the button state
static bool button_currently_pressed = false;

// Define our LED/Toggle property write callback
void led_toggle_callback(uint16_t length, void* data)
{
    // Cast our received value to our intended value (we can ignore length since we are expecting one value)
    gmbnd_bool_t toggle_val = GUMBAND_BOOL(data);

    // Turn the on-board LED on or off using the toggle value
    if(toggle_val == 1) {
      gumbandLedOn();
    }
    else {
      gumbandLedOff();
    }
}

void setup()
{
  // Attach the callback to executes when something is written to the LED/Toggle property
  gumbandSetWriteCallback(led_toggle_prop, led_toggle_callback);
}

void loop()
{
  // If the on-board button is pressed
  if(gumbandButtonPressed() && button_currently_pressed == false) {
    
    // Tell the exhibit application the button has been pressed
    gumbandPublish(button_prop, true);

    button_currently_pressed = true;
  }
  else if(!gumbandButtonPressed() && button_currently_pressed == true)
  {
    // Tell the exhibit application the button has been released
    gumbandPublish(button_prop, false);

    button_currently_pressed = false;
  }
}

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

  1. Send an event from the Electron app to the gumband-service.js whenever the user clicks on a button in the Button Clicker game.

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

  3. Listen for the Electron button click event in the gumband-service.js and toggle the hardware LED on and off.

electron-app/main.js

function createNewTarget() {
    window.gb.send("fromElectron", { type: 'button-clicked' });
    ...
}

gumband/gumband-service.js

...

/**
 * The length of time in milliseconds that the Hardware LED should blink on when a button in Button Click is clicked.
 */
const HARDWARE_LED_BLINK_TIME = 25;

...

class GumbandService {
    ...
    /**
      * The ID of the Gumband Hardware.
      */
    hardwareId;
    
    addSDKListeners() {
        ...
        
        this.gumbandSDK.on(Sockets.HARDWARE_ONLINE, (payload) => {
            this.hardwareId = payload.hardwareId;
        });
    }
    
    addElectronAppListeners() {
        ipcMain.on("fromElectron", async (event, data) => {
            if (data.type === "game-completed") {
                ...
            } else if (data.type === 'button-clicked') {
                this.gumbandSDK.hardware.set(`${this.hardwareId}/LED/Toggle`, 1);
                setTimeout(() => {
                    this.gumbandSDK.hardware.set(`${this.hardwareId}/LED/Toggle`, 0);
                }, HARDWARE_LED_BLINK_TIME);
            }
        });
    }
    
    ...
    
}

One thing to keep in mind is that the property name hardcoded in lines 30 and 32 in gumband-service.js above 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_ONLINE event:

this.gumbandSDK.on(Sockets.HARDWARE_ONLINE, (payload) => {

console.log(payload);

});

for the firmware we’re using for this tutorial, this would log out:

{ hardwareId: ${whatever-your-hardware-id-is}, name: 'SDK Button LED Example', peripherals: { Button: { Press: [Object] }, LED: { Toggle: [Object] } } }

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.

  • No labels