Skip to content

Commit bdce1ef

Browse files
committed
PE-851: Add Example for Self-Updating Application in dev-cookbook
- Add new JS application that can self update and run on BrightSign players - Add optional Nodejs server that serves the "autorun.zip" file expected by the JS app
1 parent 3da13eb commit bdce1ef

File tree

9 files changed

+337
-0
lines changed

9 files changed

+337
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# bs-app-updater
2+
3+
A simple self-update mechanism for a JS application designed to run on BrightSign players.
4+
5+
## Overview
6+
7+
`bs-app-updater` is a lightweight utility that allows a JavaScript application running on BrightSign players to update itself by downloading and applying a new `autorun.zip` package. This enables remote updates and maintenance of deployed applications with minimal effort.
8+
9+
## Features
10+
- Checks for updates from a configurable server endpoint. See `SERVER_URL` in `index.ts`
11+
- Downloads and applies new `autorun.zip` files
12+
13+
## Getting Started
14+
15+
### Prerequisites
16+
- Node.js (v14 or later recommended)
17+
- npm (Node Package Manager)
18+
19+
### Installation
20+
1. Clone this repository or copy the code to your project directory.
21+
2. Install dependencies (if any):
22+
```sh
23+
npm install
24+
```
25+
26+
### Build
27+
If the project uses TypeScript or a build step, run:
28+
```sh
29+
npm run build
30+
```
31+
Otherwise, the code can be run directly with Node.js.
32+
33+
### Deploy and run on player
34+
1. Copy the `autorun.brs` file and `dist/index.js` files to the root of an SD card.
35+
- /storage/sd/autorun.brs
36+
- /storage/sd/index.js
37+
2. Insert the SD card into the BrightSign player.
38+
3. Boot up the player.
39+
40+
## Optional: Local Node.js Server for Testing
41+
42+
You can run a simple Node.js server locally to serve the `autorun.zip` file expected by the JS app. This is useful for development and testing.
43+
44+
### To start the server:
45+
1. Navigate to the `server` directory:
46+
```sh
47+
cd server
48+
```
49+
2. Install dependencies:
50+
```sh
51+
npm install
52+
```
53+
3. Build the app:
54+
```sh
55+
npm run build
56+
```
57+
4. Start the server:
58+
```sh
59+
npm start
60+
```
61+
5. The server will listen on port 7000 by default and has a single endpoint to serve the `autorun.zip` file:
62+
```
63+
http://localhost:7000/autorun.zip
64+
```
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
function main()
2+
3+
mp = CreateObject("roMessagePort")
4+
'Enable lDWS
5+
EnableLDWS()
6+
' Create Node JS Server
7+
node = createobject("roNodeJs", "SD:/index.js", { message_port:mp })
8+
9+
'Event Loop
10+
while true
11+
msg = wait(0,mp)
12+
print "msg received - type=";type(msg)
13+
14+
if type(msg) = "roNodeJsEvent" then
15+
print "msg: ";msg
16+
end if
17+
end while
18+
19+
end function
20+
21+
function EnableLDWS()
22+
registrySection = CreateObject("roRegistrySection", "networking")
23+
if type(registrySection) = "roRegistrySection" then
24+
registrySection.Write("http_server", "80")
25+
end if
26+
registrySection.Flush()
27+
end function
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import fetch from 'node-fetch';
2+
import decompress from 'decompress';
3+
import md5File from 'md5-file';
4+
import fs from 'fs';
5+
import path from 'path';
6+
7+
// @ts-ignore
8+
import { System } from '@brightsign/system';
9+
10+
// Configurable values
11+
const CHECK_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
12+
const SERVER_URL = 'http://localhost:7000/autorun.zip';
13+
const STORAGE_PATH = '/storage/sd';
14+
const TMP_PATH = '/storage/tmp';
15+
const extensionsToCheck = ['.brs', '.html', '.js', '.json'];
16+
17+
async function sleep(ms: number) {
18+
return new Promise(resolve => setTimeout(resolve, ms));
19+
}
20+
21+
async function doesFileExist(filePath: string): Promise<boolean> {
22+
try {
23+
await fs.promises.access(filePath, fs.constants.F_OK);
24+
return true;
25+
} catch {
26+
return false;
27+
}
28+
}
29+
30+
async function downloadAndUnzipFile(url: string, dest: string): Promise<boolean> {
31+
try {
32+
const res = await fetch(url);
33+
if (res.status === 200) {
34+
const buffer = Buffer.from(await res.arrayBuffer());
35+
await decompress(buffer, dest);
36+
console.log(`Downloaded and unzipped zip to ${dest}`);
37+
return true;
38+
} else {
39+
const err = await res.json();
40+
console.error(`Server error: ${JSON.stringify(err)}`);
41+
return false;
42+
}
43+
} catch (e) {
44+
console.error(`Download failed: ${e}`);
45+
return false;
46+
}
47+
}
48+
49+
async function backupFiles(storagePath: string): Promise<void> {
50+
// Read the storage path and back up autorun.brs as well as any HTML/JS files
51+
const filesToBackup = await findFiles(storagePath, extensionsToCheck);
52+
for (const file of filesToBackup) {
53+
const filename = file.replace(/^.*[\\/]/, '');
54+
const backupFile = path.join(TMP_PATH, filename);
55+
await fs.promises.copyFile(file, backupFile);
56+
console.log(`Backed up ${file} to ${backupFile}`);
57+
}
58+
}
59+
60+
async function restoreFiles(storagePath: string): Promise<void> {
61+
const filesToRestore = await findFiles(TMP_PATH, extensionsToCheck);
62+
for (const file of filesToRestore) {
63+
const filename = file.replace(/^.*[\\/]/, '');
64+
const originalFile = path.join(storagePath, filename);
65+
await fs.promises.copyFile(file, originalFile);
66+
console.log(`Restored ${file} to ${originalFile}`);
67+
}
68+
}
69+
70+
async function findFiles(dir: string, exts: string[]): Promise<string[]> {
71+
let results: string[] = [];
72+
const files = await fs.promises.readdir(dir);
73+
for (const file of files) {
74+
const fullPath = path.join(dir, file);
75+
const stat = await fs.promises.stat(fullPath);
76+
if (stat.isDirectory()) {
77+
results = results.concat(await findFiles(fullPath, exts));
78+
} else if (exts.some(ext => file.endsWith(ext))) {
79+
results.push(fullPath);
80+
}
81+
}
82+
return results;
83+
}
84+
85+
async function checksum(filePath: string): Promise<string | null> {
86+
try {
87+
return await md5File(filePath);
88+
} catch {
89+
return null;
90+
}
91+
}
92+
93+
async function reboot() {
94+
try {
95+
console.log('Rebooting device...');
96+
new System().reboot();
97+
} catch (e: any) {
98+
console.error('Failed to reboot:', e.message);
99+
}
100+
}
101+
102+
async function processUpdate() {
103+
await backupFiles(STORAGE_PATH);
104+
105+
const downloaded = await downloadAndUnzipFile(SERVER_URL, STORAGE_PATH);
106+
if (!downloaded) return;
107+
108+
// Find autorun.brs in storage
109+
const autorunPath = path.join(STORAGE_PATH, 'autorun.brs');
110+
if (!(await doesFileExist(autorunPath))) {
111+
console.error('No autorun.brs script found after unzip. Restoring backup and rebooting.');
112+
await restoreFiles(STORAGE_PATH);
113+
await reboot();
114+
return;
115+
}
116+
117+
// Check BRS/HTML/JS files and
118+
// reboot only if any file has changed.
119+
const tmpFiles = await findFiles(TMP_PATH, extensionsToCheck);
120+
for (const file of tmpFiles) {
121+
const filename = file.replace(/^.*[\\/]/, '');
122+
const newFile = path.join(STORAGE_PATH, filename);
123+
if (await doesFileExist(newFile)) {
124+
const oldSum = await checksum(file);
125+
const newSum = await checksum(newFile);
126+
if (newSum !== oldSum) {
127+
console.log(`${file} changed. Rebooting.`);
128+
await reboot();
129+
return;
130+
}
131+
}
132+
}
133+
}
134+
135+
async function main() {
136+
while (true) {
137+
try {
138+
await processUpdate();
139+
} catch (e) {
140+
console.error('Error in update loop:', e);
141+
}
142+
await sleep(CHECK_INTERVAL_MS);
143+
}
144+
}
145+
146+
main();
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "bs-app-updater",
3+
"version": "1.0.0",
4+
"main": "dist/index.js",
5+
"scripts": {
6+
"build": "webpack",
7+
"start": "node dist/index.js"
8+
},
9+
"dependencies": {
10+
"@types/decompress": "^4.2.7",
11+
"@types/node-fetch": "^2.6.12",
12+
"decompress": "^4.2.1",
13+
"md5-file": "^5.0.0",
14+
"node-fetch": "2.7.0"
15+
},
16+
"devDependencies": {
17+
"ts-loader": "^9.5.2",
18+
"typescript": "^5.0.0",
19+
"webpack": "^5.0.0",
20+
"webpack-cli": "^5.0.0"
21+
}
22+
}
919 KB
Binary file not shown.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const express = require('express');
2+
const fs = require('fs');
3+
const path = require('path');
4+
5+
const app = express();
6+
const PORT = process.env.PORT || 7000;
7+
8+
// Example route to deliver autorun.zip
9+
app.get('/autorun.zip', (req, res) => {
10+
const zipPath = path.join(__dirname, 'autorun.zip');
11+
fs.access(zipPath, fs.constants.F_OK, (err) => {
12+
if (err) {
13+
return res.status(404).json({ error: 'autorun.zip not found' });
14+
}
15+
res.sendFile(zipPath, (err) => {
16+
if (err) {
17+
res.status(500).json({ error: 'Failed to send file' });
18+
}
19+
});
20+
});
21+
});
22+
23+
// Catch-all error handler
24+
app.use((err, req, res, next) => {
25+
res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
26+
});
27+
28+
app.listen(PORT, () => {
29+
console.log(`Server running on port ${PORT}`);
30+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "bs-autorun-zip-server",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"start": "node index.js"
7+
},
8+
"dependencies": {
9+
"express": "^4.18.2"
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "commonjs",
5+
"outDir": "dist",
6+
"strict": true,
7+
"esModuleInterop": true,
8+
"skipLibCheck": true
9+
},
10+
"include": ["index.ts"]
11+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const path = require('path');
2+
3+
module.exports = {
4+
entry: './index.ts',
5+
target: 'node',
6+
output: {
7+
filename: 'index.js',
8+
path: path.resolve(__dirname, 'dist'),
9+
},
10+
mode: 'development', // or 'production'
11+
devtool: false,
12+
resolve: {
13+
extensions: ['.ts', '.js'],
14+
},
15+
module: {
16+
rules: [
17+
{
18+
test: /\.ts$/,
19+
use: 'ts-loader',
20+
},
21+
],
22+
},
23+
externals: {
24+
'@brightsign/system': 'commonjs @brightsign/system',
25+
},
26+
};

0 commit comments

Comments
 (0)