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
On the command line/terminal, run command:
npx create-lwc-app tv-dashboard
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
Open command line/terminal
Navigate to the directory containing the tv-dashboard application
cd tv-dashboard
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
Open the
tv-dashboard
application folderCreate 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
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
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
In the
tv-dashboard
application folder, open the scr folderOpen the file lwc-services.config.js
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
In the
tv-dashboard
application folder, open folder scr > serverOpen the file index.js
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
In the
tv-dashboard
application folder, open folder scr > clientOpen the file index.html
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
In the
tv-dashboard
application folder, open folder scr > clientOpen the file index.js
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
In the
tv-dashboard
application folder, open the folder src > client > modules > myCreate a folder named opportunitiesByStage
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>
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
In the
tv-dashboard
application folder, open the folder src > client > modules > myCreate a folder named chatterAnnouncement
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>
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
In the
tv-dashboard
application folder, open the folder src > client > modules > my > appOpen 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>
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
Open the tv-dashboard folder
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
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
Open Terminal
Run the following command
sudo nano /etc/xdg/lxsession/LXDE-pi/autostart
Add the following line to the file
@lxterminal --command "ABSOLUTE_PATH_TO_SHELL_SCRIPT_FILE"
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)
Hit Control+S to Save
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:
Part 3 – Raspberry Pi Setup (you are on this now)
Raspberry Pi Resources
What is a Raspberry Pi?
Raspberry Pi Documentation and Setup Guides
Installing OS
https://www.raspberrypi.org/documentation/installation/noobs.md
SSH Setup Guide
https://www.raspberrypi.org/documentation/remote-access/ssh/
VNC Setup Guide
https://www.raspberrypi.org/documentation/remote-access/vnc/
Screensaver
https://www.raspberrypi.org/documentation/configuration/screensaver.md
Securing your Raspberry Pi
https://www.raspberrypi.org/documentation/configuration/security.md
LED warning flash codes
https://www.raspberrypi.org/documentation/configuration/led_blink_warnings.md