Skip to Content
Technical Articles
Author's profile photo Ferry Djaja

Adding LED Display as an Threshold Indicator in SAP Analytics Cloud

In this blog, we are going to add the LED display as an threshold indicator in SAP Analytics Cloud. If the certain threshold in the bar chart is reached, the LED will on otherwise it will turn off.

Components

  • Chrome browser with Web Bluetooth enabled.
    Open the new tab and run this command to check if your Chrome browser supports the web Bluetooth: chrome://bluetooth-internals/#devices
    I am using version 78.0.3904.108 (Official Build) (64-bit) for this demo.
  • BBC micro:bit.

Architecture

  • Host HTML page
    It consists of the web Bluetooth functions to communicate with micro:bit to on and off the LED. For simplicity we are just using the built-in LED matrix in micro:bit. If you want you can also use external LEDs.
  • SAP Analytics Application embedded via iFrame
    An Analytics App to check the threshold in the bar chart and send the signal to host HTML page to on and off the LED.

Steps to Build

Build SAC, Analytics Application

  • Create a simple bar chart that consist of Product ID and Volume with threshold information as shown in the below picture.
  • Add a Timer, Timer_1 to retrieve the result set from the bar chart in order to get the Volume from each Product every 10 seconds.
    If the volume reached the certain threshold, in this example is 100, execute the postMessage to send the signal “met” to the parent host to on the LED. Otherwise send the signal “nmet” to off the LED.

    var resultSet = Chart_1.getDataSource().getResultSet();
    
    console.log(resultSet);
    var rawvalue = "";
    var productID = "";
    var threshold_met = false;
    
    for(var i=0; i<resultSet.length; i++) {
    	rawvalue = resultSet[i]['@MeasureDimension'].rawValue;
    	productID =  resultSet[i]['Product'].id;
    
    	if(ConvertUtils.stringToInteger(rawvalue) >= threshold) {
    		console.log("met threshold");
    		threshold_met = true;			
    	}
    }
    
    if (threshold_met) {
    	Application.postMessage(PostMessageReceiver.Parent, "met", "*");
    } else {
    	Application.postMessage(PostMessageReceiver.Parent, "nmet", "*");
    }
    
    Timer_1.start(10);​
  • Add a Timer, Timer_2 to refresh the chart every 10 seconds.

    Application.refreshData();
    Timer_2.start(10);​
  • On onInitialization(), insert the following scripts to start the timer_1 and timer_2.

Build Host HTML Page

Create the host HTML app and run it as a server (NodeJS for example). Replace iframeSource with SAC Analytics App URL.

<html>
<head>
    <title>SAP Analytics Cloud</title>
    <meta name="description" content="SAP Analytics Cloud">
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-capable" content="yes">
    
    <script src="./web/bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
    <!-- Polymer components -->
    <link rel="import" href="./web/bower_components/paper-progress/paper-progress.html">
    <link rel="import" href="./web/bower_components/paper-button/paper-button.html">
    <link rel="import" href="./web/bower_components/iron-icons/iron-icons.html">
    <link rel="import" href="./web/bower_components/iron-icons/image-icons.html">
    <link rel="import" href="./web/bower_components/paper-icon-button/paper-icon-button.html">
    <link rel="import" href="./web/bower_components/paper-card/paper-card.html">
    <link rel="import" href="./web/bower_components/paper-dialog/paper-dialog.html">
    <link rel="import" href="./web/bower_components/paper-toggle-button/paper-toggle-button.html">
    <link rel="import" href="./web/bower_components/iron-flex-layout/iron-flex-layout.html">

    <link rel="import" href="./web/bower_components/paper-dialog/paper-dialog.html">
    <link rel="import" href="./web/bower_components/paper-button/paper-button.html">
    <link rel="import" href="./web/bower_components/paper-input/paper-input.html">

    <link rel="import" href="./web/bower_components/paper-styles/color.html">
    <link rel="stylesheet" href="./web/bower_components/paper-styles/demo.css">

    <style is="custom-style">
        paper-progress {
            width: 100%;
        }
        
        paper-progress.blue {
            paper-progress-active-color: var(--paper-light-blue-500);
            paper-progress-secondary-color: var(--paper-light-blue-100);
        }
        
        paper-toggle-button.blue {
            --paper-toggle-button-checked-bar-color: var(--paper-light-blue-500);
            --paper-toggle-button-checked-button-color: var(--paper-light-blue-500);
            --paper-toggle-button-checked-ink-color: var(--paper-light-blue-500);
            --paper-toggle-button-unchecked-bar-color: var(--paper-light-blue-900);
            --paper-toggle-button-unchecked-button-color: var(--paper-light-blue-900);
            --paper-toggle-button-unchecked-ink-color: var(--paper-light-blue-900);
        }
        
        paper-button {
            display: block;
            width: 100px;
            height: 40px;
            min-width: 0em;
            margin: 0.2em 0.2em;
        }
        
        paper-button.blue {
            color: var(--paper-light-blue-500);
            paper-button-flat-focus-color: var(--paper-light-blue-50);
        }
        
        #cards {
            margin-left: auto;
            margin-right: auto;
            max-width: 300px;
        }
        
        paper-card {
            --paper-card-header-text: {
                font-size: 10px;
            }
            ;
            margin-bottom: 1px;
            margin-top: 1px;
            width: 100%;
        }
        
        .flex {
            @apply(--layout-horizontal);
        }
        
        paper-button.indigo {
            background-color: var(--paper-indigo-500);
            color: white;
            --paper-button-raised-keyboard-focus: {
                background-color: var(--paper-pink-a200) !important;
                color: white !important;
            }
            ;
        }
    </style>
</head>

<body unresolved>
    <div id="cards">
        <paper-card heading="SAP Analytics Cloud Demo">
            <div class="card-content">
                <paper-toggle-button class="blue" id="connect">Connect</paper-toggle-button>
                <paper-progress id="progress" indeterminate></paper-progress>
            </div>
        </paper-card>
        <paper-dialog id="no-bluetooth">
            <h2>No Web Bluetooth</h2>
            <p>The Web Bluetooth API is missing. Please enable it at chrome://flags/#enable-web-bluetooth and try again.</p>
        </paper-dialog>
        <paper-dialog id="errorDialog">
            <h2>Error</h2>
            <p>Could not connect to bluetooth device!</p>
        </paper-dialog>
    </div>
    <br>
    <script>
        'use strict';
        document.addEventListener('WebComponentsReady', () => {
            let connectToggle = document.querySelector('#connect');
            let progress = document.querySelector('#progress');
            let dialog = document.querySelector('#errorDialog');
            let gattServer;
            let commandService;
            let writeCharacteristic;
            let writeClientReqCharacteristic;
            let writeClientEvtCharacteristic;
            let busy = false;
            let commandQueue = [];
            progress.hidden = true;

            function bindEvent(element, eventName, eventHandler) {
                if (element.addEventListener) {
                    element.addEventListener(eventName, eventHandler, false);
                } else if (element.attachEvent) {
                    element.attachEvent('on' + eventName, eventHandler);
                }
            }

            var iframeSource = '__REPLACE_WITH_SAC_ANALYTIC_APP_URL_';
            // Create the iframe
            var iframe = document.createElement('iframe');
            iframe.setAttribute('src', iframeSource);
            iframe.setAttribute('id', 'the_iframe');
            iframe.setAttribute('frameborder', 0);
            iframe.setAttribute('style', 'overflow: hidden; height: 100%; width: 100%; position: inherit;');
            document.body.appendChild(iframe);

            // Listen to message from child window
            bindEvent(window, 'message', function(e) {
                if (e.data !== "embed:ready" && !e.data.event_type) {
                    console.log({
                        message: e.data
                    });
                    if (e.data === "met") {
                        send_key("8");
                    } else {
                        send_key("");
                    }

                }
            });

            /**
             * Connect to command characteristic.
             */
            connectToggle.addEventListener('click', () => {
                if (gattServer != null && gattServer.connected) {
                    if (gattServer.disconnect) {
                        console.log('Disconnecting...');
                        gattServer.disconnect();
                    }
                    resetVariables();
                } else {
                    console.log('Connecting...');
                    connectToggle.checked = true;
                    progress.hidden = false;
                    if (writeCharacteristic == null) {
                        navigator.bluetooth.requestDevice({
                                filters: [{
                                    namePrefix: 'BBC micro:bit',
                                }],
                                optionalServices: ['e95d93af-251d-470a-a062-fa1922dfa9a8'] //EVENTSERVICE_SERVICE_UUID
                            })
                            .then(device => {
                                console.log('Connecting to GATT Server...');
                                return device.gatt.connect();
                            })
                            .then(server => {
                                console.log('> Found GATT server');
                                gattServer = server;
                                //console.log(gattServer);
                                // Get command service
                                return gattServer.getPrimaryService('e95d93af-251d-470a-a062-fa1922dfa9a8'); //EVENTSERVICE_SERVICE_UUID
                            })
                            .then(service => {
                                console.log('> Found command service');
                                commandService = service;
                                return commandService.getCharacteristic('e95d9775-251d-470a-a062-fa1922dfa9a8'); //MICROBITEVENT_CHARACTERISTIC_UUID
                            })
                            .then(characteristic => {
                                console.log('> Found write characteristic');
                                return characteristic.startNotifications().then(_ => {
                                    console.log('> Notifications started');
                                    characteristic.addEventListener('characteristicvaluechanged',
                                        handleCharacteristicValueChanged);
                                });
                            })
                            .then(service => {
                                return commandService.getCharacteristic('e95d5404-251d-470a-a062-fa1922dfa9a8'); //CLIENTEVENT_CHARACTERISTIC_UUID
                            })
                            .then(ClientEvtCharacteristic => {
                                console.log('> Found Client Event ClientEvtCharacteristic');
                                writeClientEvtCharacteristic = ClientEvtCharacteristic;
                                return commandService.getCharacteristic('e95d23c4-251d-470a-a062-fa1922dfa9a8'); //CLIENTREQUIREMENTS_CHARACTERISTIC_UUID
                            })
                            .then(ClientReqCharacteristic => {
                                console.log('> Found Client Requirement characteristic');
                                writeClientReqCharacteristic = ClientReqCharacteristic;
                                initEvent();
                                progress.hidden = true;
                            })

                        .catch(handleError);
                    } else {
                        progress.hidden = true;
                    }
                }
            });

            function send_key(cmd) {
                console.log(cmd);
                let bytes = []; // char codes
                let sbyte;
                let cmd1;
                let code;
                for (let i = 0; i < cmd.length; ++i) {
                    code = cmd.charCodeAt(i);
                    bytes = bytes.concat([code]);
                }
                cmd = new Uint16Array([0x22B8, new Uint8Array(bytes)]);
                sendCommand(cmd);
            }

            function sendCommand(cmd) {
                if (writeClientEvtCharacteristic) {
                    // Handle one command at a time
                    if (busy) {
                        // Queue commands
                        commandQueue.push(cmd);
                        return Promise.resolve();
                    }
                    busy = true;

                    return writeClientEvtCharacteristic.writeValue(cmd).then(() => {
                        busy = false;
                        // Get next command from queue
                        let nextCommand = commandQueue.shift();
                        if (nextCommand) {
                            sendCommand(nextCommand);
                        }
                    });
                } else {
                    return Promise.resolve();
                }
            }
            /**
             * Check if browser supports Web Bluetooth API.
             */
            if (navigator.bluetooth == undefined) {
                document.getElementById("no-bluetooth").style.display = "block";
                document.getElementById("no-bluetooth").open();
            }
            /**
             * Reset the app variable states.
             */
            function resetVariables() {
                busy = false;
                progress.hidden = true;
                gattServer = null;
                commandService = null;
                writeCharacteristic = null;
                writeClientReqCharacteristic = null;
                writeClientEvtCharacteristic = null;
                connectToggle.checked = false;
            }
            /**
             * API error handler.
             */
            function handleError(error) {
                console.log(error);
                resetVariables();
                dialog.open();
            }

            function initEvent() {
                let cmdPinAd = new Uint16Array([0x22B8, 0x00]);
                sendEvent(cmdPinAd).then(() => {
                    console.log('Event set.');
                }).catch(handleError);
            }

            function sendEvent(cmd) {
                if (writeClientReqCharacteristic) {
                    // Handle one command at a time
                    if (busy) {
                        // Queue commands
                        commandQueueP.push(cmd);
                        return Promise.resolve();
                    }
                    busy = true;
                    return writeClientReqCharacteristic.writeValue(cmd).then(() => {
                        busy = false;
                        // Get next command from queue
                        let nextCommand = commandQueue.shift();
                        if (nextCommand) {
                            sendEvent(nextCommand);
                        }
                    });
                } else {
                    return Promise.resolve();
                }
            }

            function handleCharacteristicValueChanged(event) {
                var value = event.target.value;

                let a = [];
                // Convert raw data bytes to hex values just for the sake of showing something.
                // In the "real" world, you'd use data.getUint8, data.getUint16 or even
                // TextDecoder to process raw data bytes.
                for (let i = 0; i < value.byteLength; i++) {
                    a.push('0x' + ('00' + value.getUint8(i).toString(16)).slice(-2));
                }
                console.log('> ' + a.join(' '));
            }
        });
    </script>
</body>
</html>

Run the App

  • Flash the firmware to micro:bit by dragging the microbit-samples-combined.hex file onto it.
  • Switch on the micro:bit and you should see the scrolling message “DC”.
  • Open the host HTML page app URL and enable the Bluetooth on your computer.
  • Click Connect button and select the available micro:bit and click Pair button.
  • If the connection is established, you will see the screenshot as shown below.
  • Check the LED on micro:bit.

Think other possibilities with this approach to control any external modules from SAP Analytics Cloud via web Bluetooth. Let me know if you have any comments/questions. Thanks.

Assigned Tags

      2 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Beat Andreas Honegger
      Beat Andreas Honegger

      Hello Ferry

      Cool Idea and nice Blog - Thanks.

      Do you now it there also the possibility to receive some count from a HW?

      As an example - count visitors from a sensor?

      Thanks

      Beat

      Author's profile photo Ferry Djaja
      Ferry Djaja
      Blog Post Author

      Hi Beat,

       

      Sorry for the late late reply. Yes technically is possible. But I suggest using MQTT or similar technology.

       

      Best,

      Ferry