Build Real-Time TV Dashboards with Salesforce Data, Platform Events & a Raspberry Pi – Part 3 – Raspberry Pi Setup

This blog post is Part 2 of the blog post series on Building Real-Time TV Dashboards with Salesforce Data, Platform Events & a Raspberry Pi.

Let’s get started with the Raspberry Pi setup.

The idea is to have the Dashboard running on the Pi. This can be done in various different ways but for the sake for this project we will be hosting and running the dashboard on the Raspberry Pi.
We will be building the Dashboard using the LWC OSS (Lightning Web Components Open Source) Framework.
 Pro Tip (Optional but totally worth trying out): Setup SSH and VNC on the Pi for super easy remote development and remote control.

Step 0: Install the OS (Skip this step if you already have the OS installed)

I am using Raspbian OS on my Pi and I set it up using NOOBS – New Out Of the Box Software and it makes the installation very easy.
Here is a comprehensive step by step guid on installing the OS using NOOBS – https://www.raspberrypi.org/help/noobs-setup/2/

Step 1: Disable the Screensaver

This is an important step because by default the PI’s screen keeps blanking out every 15 mins. As we will be having a standalone realtime dashboard running on the PI, we need to have our screensaver disabled.
Easiest way to get this done is by installing xscreensaver.
In the Terminal, run the following command to install it. This might take a few minutes
$ sudo apt-get install xscreensaver
Once installed, go to Preferences option in the main desktop menu. You should find the screen saver application. Launch it and search for the option to disable it completely.

Step 2: Install Node.js

Run the following commands in the Terminal
  1. Update your system package list
    sudo apt-get update
  2. Upgrade all your installed packages to their latest version
    sudo apt-get dist-upgrade
  3. Download the latest version of Node.js
    curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
  4. Install it
    sudo apt-get install -y nodejs
  5. To check if the installation was successful, run the following command and it should return the version number of Node.js
    node -v

Step 3: Create LWC App

The Dashboard Interface is an LWC App. So, the goal is to have an LWC App running on the Raspberry Pi and it is totally up to you on how and where you build the LWC app.
As long as there is an LWC App to be run on the Pi, you can either develop it on the Pi or develop it somewhere else(like your local machine) and migrate it to the Pi.
For example, I developed it on my local machine and pushed it to the Pi via version control(bitbucket) so that I could maintain a backup and also sync code changes between my system and the pi.
Feel free choose which ever way you are comfortable with and make sure you have Node.js installed as it is required to develop and run LWC Apps.
3.1 Create tv-dashboard LWC App
  1. On the command line/terminal, run command:
    npx create-lwc-app tv-dashboard
  2. When prompted about the application details, fill in the details as following
    • Package name for npm: Press Enter to accept the default
    • Description: Type the app description or press Enter to accept the default
    • Author: Type your name or press Enter to accept the default
    • Version: Press Enter to accept the default
    • License: Press Enter to accept the default
    • Who is the GitHub owner of the repository ( https://github.com/OWNER/repo): Type in your GitHub owner name or leave it blank and press Enter
    • What is the GitHub name of the repository ( https://github.com/owner/REPO): Type in your GitHub repo name or Press Enter to accept the default
    • Select a package manager: Use your arrow keys to select npm press Enter
    • Use TypeScript or JavaScript: Use your arrow keys to select JavaScript press Enter
    • Use custom Express server configuration: Enter y to install the server and press Enter
Once the application is created, you will see the following message:
🎉  Created conference-app in /YOUR/FILE/STRUCTURE/tv-dashboard. Checkout the `scripts` section of your `package.json` to get started.
3.2 Install A Few Packages
  1. Open command line/terminal
  2. Navigate to the directory containing the tv-dashboard application
    cd tv-dashboard
  3. Run the following command:
    npm install jsforce dotenv socket.io chart.js @salesforce-ux/design-system
But, what are these applications and why do we need them?
  • jsforce – JavaScript library to interact with Salesforce Orgs and APIs
  • dotenv – To store our Salesforce credentials as environment variables in a .env file and reference them in or app
  • socket.io – JavaScript library that enables real-time bidirectional event-based communication. We are using this to communicate our platform events from the LWC Server to LWC Frontend(charts). Works in a pub-sub model.
  • chart.js – JavaScript library to create simple and beautiful HTML5 charts using canvas elements
  • @salesforce-ux/design-system – Salesforce Lightning Design System (SLDS) – CSS framework to make our app beautiful
3.3 Create the .env file and declare our salesforce credentials as environment variables
  1. Open the tv-dashboard application folder
  2. Create a file with .env (with a leading period) as file name. Make sure that you are creating this folder in the root folder i.e., tv-dashboard
  3. Add the following the content into the .env file
    .env
    SF_LOGIN_URL=https://login.salesforce.com 
    SF_USERNAME=YOUR_USERNAME
    SF_PASSWORD=YOUR_PASSWORD
    SF_TOKEN=YOUR_SECURITY_TOKEN

  4. Make sure to update the above placeholder values with your data
    SF_USERNAME: Your Salesforce Org’s username.
    SF_PASSWORD: Your Salesforce Org’s password.
    SF_TOKEN: Your Salesforce Org’s security token.
3.4 Update lwc-services.config.js to use the Lightning Design System SLDS
  1. In the tv-dashboard application folder, open the scr folder
  2. Open the file lwc-services.config.js
  3. Add the following in the resources section of your config
    {
    	from: 'node_modules/@salesforce-ux/design-system/assets',
    	to: 'dist/resources/assets'
    }

3.5 Add LWC Express Server Code
  1. In the tv-dashboard application folder, open folder scr > server
  2. Open the file index.js
  3. Add the following code at the beginning of the file (before the line module.exports)
    src/server/index.js
    // eslint-disable-next-line no-undef
    require('dotenv').config();
    
    const { exec } = require('child_process');
    
    const app = require('express')();
    const server = require('http').Server(app);
    const io = require('socket.io')(server);
    const jsforce = require('jsforce');
    
    const PORT = 3003;
    const CDC_DATA_CHANNEL = '/data/ChangeEvents';
    const CHATTER_CHANNEL = '/event/Chatter_To_TV_Dashboard__e';
    
    const {
        SF_USERNAME,
        SF_PASSWORD,
        SF_TOKEN,
        SF_LOGIN_URL,
        npm_lifecycle_event
    } = process.env;
    
    // Check for required Salesforce Credentials
    if (!(SF_USERNAME && SF_PASSWORD && SF_TOKEN && SF_LOGIN_URL)) {
        console.error(
            'Cannot start app: missing mandatory configuration. Check your .env file.'
        );
        process.exit(-1);
    }
    
    // Connect to Salesforce
    const conn = new jsforce.Connection({
        loginUrl: SF_LOGIN_URL
    });
    
    conn.login(SF_USERNAME, SF_PASSWORD + SF_TOKEN, err => {
        if (err) {
            console.error(err);
            process.exit(-1);
        }
    
        console.log('SF Logged In!');
    
        // Subscribe to Change Data Capture Events
        console.log('subscribing to CDC channel: ' + CDC_DATA_CHANNEL);
        conn.streaming.topic(CDC_DATA_CHANNEL).subscribe(data => {
            const { event, payload } = data;
            const { entityName, changeType } = payload.ChangeEventHeader;
            console.log(
                `cdc event received [${event.replayId}]: ${entityName}:${changeType}`
            );
            //Publish Socket Event with the CDC events data to be received by the client(LWC Front-end)
            io.emit(`cdc`, payload);
        });
    
        // Subscribe to custom Platform Event for Chatter Announcements
        console.log('subscribing to chatter channel: ' + CHATTER_CHANNEL);
        conn.streaming.topic(CHATTER_CHANNEL).subscribe(data => {
            console.log('chatter announcement Event >>> ', data);
            //Publish Socket Event with the Custom Platform Events data to be received by the client(LWC Front-end)
            io.emit(`chatterAnnouncement`, data);
        });
    });
    
    // Log when a client connects to socket server
    io.on('connection', socket => {
        console.log(`client connected: ${socket.id}`);
    });
    
    //Start Socket.io Server
    //server.listen(PORT, () => console.log(`Running server on port ${PORT}`));
    
    // Start backend server
    server.listen(PORT, openDashboard);
    
    function openDashboard() {
        console.log(`Running socket server on port ${PORT}`);
        if (npm_lifecycle_event === 'serve') {
            console.log('Launching Dashboard!!');
            exec(
                'chromium-browser --noerrdialogs --kiosk  http://0.0.0.0:3002 --incognito --disable-translate'
            );
        }
    }
3.6 EDIT Client INDEX.html to add SLDS Stylesheet
  1. In the tv-dashboard application folder, open folder scr > client
  2. Open the file index.html
  3. Replace the file content with the following code
    /src/client/index.html
    <!DOCTYPE html>
    <html lang="en">
    
    	<head>
    		<meta charset="utf-8" />
    		<title>TV Dashboard</title>
    		<link rel="stylesheet" type="text/css" href="/resources/assets/styles/salesforce-lightning-design-system.css" />
    		<meta name="viewport" content="width=device-width,initial-scale=1" />
    		<link rel="shortcut icon" href="/resources/favicon.ico" />
    	</head>
    
    	<body>
    		<my-app></my-app>
    	</body>
    
    </html>
3.7 Edit Client INDEX.js to use Synthetic Shadow DOM for the app and components to be able to use SLDS
  1. In the tv-dashboard application folder, open folder scr > client
  2. Open the file index.js
  3. Add the following code at the beginning of the file content
    import '@lwc/synthetic-shadow';
3.8 Create  LWC Chart Component for visualising Opportunity Count By their Stage
  1. In the tv-dashboard application folder, open the folder src > client > modules > my
  2. Create a folder named opportunitiesByStage
  3. Inside opportunitiesByStage folder, create a file opportunitiesByStage.html with the following content
    /src/client/modules/my/opportunitiesByStage.html
    <template>
        <article class="slds-card">
            <div class="slds-card__header slds-grid">
                <header class="slds-media slds-media_center slds-has-flexi-truncate">
                    <!-- <div class="slds-media__figure">
                        <span class="slds-icon_container slds-icon-standard-opportunity" title="opportunity">
                            <svg class="slds-icon slds-icon_small" aria-hidden="true">
                                <use xlink:href="resources/assets/icons/standard-sprite/svg/symbols.svg#opportunity"></use>
                            </svg>
                            <span class="slds-assistive-text">opportunity</span>
                        </span>
                    </div> -->
                    <div class="slds-media__body">
                        <h2 class="slds-card__header-title">
                            <p class="slds-truncate slds-text-heading_small" title="Opportunities By Stage">
                                <span>Opportunities By Stage</span>
                            </p>
                        </h2>
                    </div>
                </header>
            </div>
            <div class="slds-card__body slds-card__body_inner">
                <!--Chart.js uses HTML canvas element to create charts so here is one-->
                <canvas class="chart" lwc:dom="manual"></canvas>
            </div>
        </article>
    </template>

  4. Inside opportunitiesByStage folder, create a file opportunitiesByStage.js with the following content
    /src/client/modules/my/opportunitiesByStage.js
    import { LightningElement, api, track } from 'lwc';
    
    export default class opportunitiesByStage extends LightningElement {
        @api sobject = '';
        @api socket;
    
        @track socketInitialized = false;
        @track chartInitialized = false;
    
        chart;
    
        //object to keep track of the number of opportunities per stage
        chartData = {};
    
        chartConfig = {
            type: 'doughnut',
            data: {
                datasets: [
                    {
                        data: [],
                        backgroundColor: [
                            '#3296ED',
                            '#9D53F2',
                            '#E287B2',
                            '#26ABA4',
                            '#77B9F2',
                            '#C398F5',
                            '#4ED4CD'
                        ]
                    }
                ],
                labels: []
            },
            options: {
                responsive: true,
                elements: {
                    arc: {
                        borderWidth: 0
                    }
                },
                legend: {
                    position: 'right',
                    labels: {
                        usePointStyle: true
                    }
                },
                animation: {
                    animateScale: true,
                    animateRotate: true
                }
            }
        };
    
        async renderedCallback() {
            if (!this.socketInitialized && this.socket) {
                this.initializeSocket();
            }
            if (!this.chartInitialized && this.socketInitialized) {
                await this.initializeChart();
            }
        }
    
        initializeSocket() {
            //bind the onSocketEvent method to the 'cdc' socket event to update the chart with new incoming data
            this.socket.on('cdc', this.onSocketEvent.bind(this));
            this.socketInitialized = true;
        }
    
        //initialize chart with chart.js
        async initializeChart() {
            await require('chart.js');
            const ctx = this.template
                .querySelector('canvas.chart')
                .getContext('2d');
            this.chart = new window.Chart(ctx, this.chartConfig);
            this.chartInitialized = true;
        }
    
        onSocketEvent(data) {
            const { changeType, entityName } = data.ChangeEventHeader;
    
            // check to make sure the change event is for the configured sobject and the record event is CREATE
            if (
                this.sobject.toLowerCase() !== entityName.toLowerCase() ||
                changeType !== 'CREATE'
            ) {
                return;
            }
    
            //update the chartData to increment the corresponding opportunity stage counter
            this.chartData[data.StageName] =
                this.chartData[data.StageName] + 1 || 1;
    
            //sort chartData in descending order
            let sortable = Object.entries(this.chartData);
            sortable.sort(function(a, b) {
                return b[1] - a[1];
            });
    
            //update chartData with sorted data
            this.chartData = Object.fromEntries(sortable);
    
            //add the updated data to the chart object
            this.chart.data.labels = Object.keys(this.chartData);
            this.chart.data.datasets[0].data = Object.values(this.chartData);
    
            //update the chart to reflect latest data
            this.chart.update();
        }
    }
3.9 Create an LWC Component to view Chatter Announcements
  1. In the tv-dashboard application folder, open the folder src > client > modules > my
  2. Create a folder named chatterAnnouncement
  3. Inside chatterAnnouncement folder, create a file chatterAnnouncement.html with the following content
    /src/client/modules/my/chatterAnnouncement.html
    <template>
        <article class="slds-card">
            <div class="slds-card__header slds-grid">
                <header class="slds-media slds-media_center slds-has-flexi-truncate">
                    <div class="slds-media__figure">
                        <span class="slds-icon_container slds-icon-standard-announcement" title="announcement">
                            <svg class="slds-icon slds-icon_small" aria-hidden="true">
                                <use xlink:href="resources/assets/icons/standard-sprite/svg/symbols.svg#announcement"></use>
                            </svg>
                            <span class="slds-assistive-text">announcement</span>
                        </span>
                    </div>
                    <div class="slds-media__body">
                        <h2 class="slds-card__header-title">
                            <p class="slds-truncate slds-text-heading_small" title="Chatter Announcements">
                                <span>Chatter Announcements</span>
                            </p>
                        </h2>
                    </div>
                </header>
            </div>
            <div class="slds-card__body slds-card__body_inner">
                <div class="slds-text-heading_large">{announcementMessage}</div>
                <div class="slds-text-color_weak slds-m-top_xx-small">{dateTime}</div>
            </div>
        </article>
    </template>
  4. Inside chatterAnnouncement folder, create a file chatterAnnouncement.html with the following content
    /src/client/modules/my/chatterAnnouncement.js
    import { LightningElement, api, track } from 'lwc';
    
    export default class chatterAnnouncement extends LightningElement {
        @api socket;
    
        @track socketInitialized = false;
    
        @track time;
        @track date;
        @track dateTime;
    
        @track announcementMessage;
    
        async renderedCallback() {
            if (!this.socketInitialized && this.socket) {
                this.initializeSocket();
            }
        }
    
        initializeSocket() {
            //binding onSocketEvent method to the socket event to update the component to show the latest anouncement
            this.socket.on('chatterAnnouncement', this.onSocketEvent.bind(this));
            this.socketInitialized = true;
        }
    
        onSocketEvent(data) {
            const { payload } = data;
    
            console.log(`chatter announcement message ${data.payload.Message__c}`);
            //update the announcementMessage property with the latest announcement message
            this.announcementMessage = payload.Message__c;
            //update the date and time on the UI
            this.setDateAndTime();
        }
    
        setDateAndTime() {
            const today = new Date();
            let hour = today.getHours();
            let min = today.getMinutes();
            let sec = today.getSeconds();
            const ap = hour < 12 ? 'AM' : 'PM';
            hour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
            hour = this.formatNumber(hour);
            min = this.formatNumber(min);
            sec = this.formatNumber(sec);
            this.time = `${hour}:${min} ${ap}`;
    
            const months = [
                'January',
                'February',
                'March',
                'April',
                'May',
                'June',
                'July',
                'August',
                'September',
                'October',
                'November',
                'December'
            ];
            const days = [
                'Sunday',
                'Monday',
                'Tuesday',
                'Wednesday',
                'Thursday',
                'Friday',
                'Saturday'
            ];
            const curWeekDay = days[today.getDay()];
            const curDay = today.getDate();
            const curMonth = months[today.getMonth()];
            const curYear = today.getFullYear();
    
            this.date = `${curWeekDay}, ${curDay} ${curMonth}, ${curYear}`;
            this.dateTime = `${this.date} • ${this.time}`;
        }
    
        formatNumber(num) {
            return num < 10 ? '0' + num : num;
        }
    }
3.10 Edit the App Component to add the Chart and the Chatter Announcements Components and also subscribe to our Socket.io server
  1. In the tv-dashboard application folder, open the folder src > client > modules > my > app
  2. Open the file app.html and replace its content with the following
    /src/client/modules/my/app.html
    <template>
        <div class="slds-page-header">
            <div class="slds-page-header__row">
                <div class="slds-page-header__col-title">
                    <div class="slds-media">
                        <div class="slds-media__figure">
                            <span class="slds-icon_container slds-icon-standard-dashboard" title="dashboard">
                                <svg class="slds-icon slds-page-header__icon" aria-hidden="true">
                                    <use xlink:href="/resources/assets/icons/standard-sprite/svg/symbols.svg#dashboard">
                                    </use>
                                </svg>
                                <span class="slds-assistive-text">dashboard</span>
                            </span>
                        </div>
                        <div class="slds-media__body">
                            <div class="slds-page-header__name">
                                <div class="slds-page-header__name-title">
                                    <h1>
                                        <span class="slds-page-header__title slds-truncate"
                                            title="Executive Dashboard">Executive Dashboard</span>
                                    </h1>
                                </div>
                            </div>
                            <p class="slds-page-header__name-meta">Sales</p>
                        </div>
                    </div>
                </div>
            </div>
    
        </div>
    
        <template if:true={socketReady}>
            <div class="slds-grid">
                <div class="slds-col slds-size_6-of-12 slds-p-around_x-small">
                    <my-opportunities-by-stage sobject="Opportunity" socket={socket}></my-opportunities-by-stage>
                </div>
                <div class="slds-col slds-size_6-of-12 slds-p-around_x-small">
                    <my-chatter-announcement socket={socket}>
                    </my-chatter-announcement>
                </div>
            </div>
        </template>
    </template>

  3. Open the file app.js and replace its content with the following code to initialise and subscribe to the backend socket server
    /src/client/modules/my/app.js
    import { LightningElement, track } from 'lwc';
    
    export default class App extends LightningElement {
        @track socket;
        @track socketReady = false;
    
        connectedCallback() {
            this.openSocket();
        }
    
        disconnectedCallback() {
            this.closeSocket();
        }
    
        async openSocket() {
            //subscribe to socket events that are broadcasted by our dashboard server app
            const io = await require('socket.io-client');
            this.socket = io('http://0.0.0.0:3003');
            this.socket.on('connect', () => {
                console.log('socket connected!');
                this.socketReady = true;
            });
        }
    
        async closeSocket() {
            this.socket.close();
            this.socket = null;
        }
    }

Step 4: Run The Dashboard

We run the dashboard on chromium in kiosk mode, this helps show the in dashboard full screen.
You can run the dashboard using the following command
npm run build && npm run serve

If you are developing, making code changes and would like for them to reflecting in real time, use the command
npm run watch
Note: Only the npm run serve command launches the Dashboard automatically. When using the npm run watch command, you would need manually view the app in the browser.
Once you run the dashboard, start creating some Opportunities and Chatter Announcements in your org and you will see the Dashboard update in real-time!
TV Dashboard Preview GIF

Step 5 (Optional): Auto Launch the Dashboard on Raspberry Pi Startup

5.1 Change the LWC Dashboard App Folder Permissions
This wouldn’t be necessary if you are created everything on the pi. But when I used Git to sync code, I came across a the EACCESS Permission error and this following terminal command took care of the error.
sudo chown -R pi:pi ABSOLUTE_PATH_TO_LWC_APP_FOLDER
Make sure to replace the ABSOLUTE_PATH_TO_LWC_APP_FOLDER with the actual absolute path of your Dashboard LWC App folder.
To get the absolute path, right click your Dashboard LWC App folder and Select Copy Path(s)
5.2 Create a executable shell script file
  1. Open the tv-dashboard folder
  2. Create a file with name run.sh and add the below as its content

    run.sh
    #!/bin/bash
    cd ABSOLUTE_PATH_TO_LWC_APP_FOLDER
    npm run build && npm run serve
  3. Make sure to replace the ABSOLUTE_PATH_TO_LWC_APP_FOLDER with the actual absolute path of your Dashboard LWC App folder.
    To get the absolute path, right click your Dashboard LWC App and Select Copy Path(s)
5.3 Edit the Raspberry Pi Autostart Script
  1. Open Terminal
  2. Run the following command
    sudo nano /etc/xdg/lxsession/LXDE-pi/autostart
  3. Add the following line to the file
    @lxterminal --command "ABSOLUTE_PATH_TO_SHELL_SCRIPT_FILE"
  4. Make sure to replace the ABSOLUTE_PATH_TO_SHELL_SCRIPT_FILE with the actual absolute path of the shell script created(in the previous step) inside the Dashboard LWC App folder.
    To get the absolute path, right click the shell script file that you created(in the pervious step) inside the Dashboard LWC App and Select Copy Path(s)
  5. Hit Control+S to Save
  6. Then Control+X to Exit the editor

And… that is it! We learnt how to go about Build Real-Time TV Dashboards with Salesforce Data, Platform Events & a Raspberry Pi. 🎉
We’ve scratched the surface with just 2 components and here is the tv-dashforce GitHub Project that not only contains the components created in this blog post but also different chart components along with Twitter Live Stream, Clock, Weather and Holidays.
Make sure to check it out! GitHub Project- https://github.com/Minerva18/tv-dashforce

This can also be done in another way where, we can host the Dashboard LWC App on the cloud like Heroku/AWS/Google etc. and use the URL to show the Dashboard LWC app via chromium kiosk mode on the Raspberry Pi.

Raspberry Pi Resources

Leave a Reply

Your email address will not be published.