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

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

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

  • Part 1 – The Intro

  • Part 2 – Salesforce Setup

  • \> Part 3 – Raspberry Pi Setup

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 real-time dashboard running on the PI, we need to have our screensaver disabled.

The 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 the 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 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 ( 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 ( 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

     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 initialize 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!

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

For the ease of readability & structure I’ve divided this tutorial into a series of blog posts. This is the first one in the series and here are the links to the other two:

Raspberry Pi Resources

💡
Built with ❤️ using Salesforce Platform Events, Lightning Web Components Open Source, Lightning Design System, JSforce, Socket.io, Chart.js, Gauge.js, CountUp.js, Node Tweet Stream.