Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ command_exists () {
# Workaround for Windows 10, Git Bash, and Yarn
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi
fi
68 changes: 68 additions & 0 deletions examples/bs-self-updater-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# bs-self-updater

A simple self-updating application written using TypeScript and designed to run on BrightSign players.

## Overview

`bs-app-updater` is a lightweight utility that allows an 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.

## Features

- Calls a configurable server endpoint to download the `autorun.zip` application to run on the player.
- See `SERVER_URL` in `index.ts`.
- Unzips the downloaded package and executes the `autorun.brs` file from the unzipped package.
- Makes periodic calls to the server to check for any updates to the `autorun.zip` file.

## Getting Started

### Prerequisites

- Node.js (v18 or later recommended)
- Yarn (for package management)

### Installation

1. Clone this repository or copy the code to your project directory.
2. Install dependencies (if any):
```sh
yarn install
```

### Build

Run the following command to compile the TypeScript code into JavaScript:

```sh
yarn build
```

### Deploy and run on player

1. Copy the `autorun.brs` file and `dist/index.js` files to the root of an SD card.
- /storage/sd/autorun.brs
- /storage/sd/index.js
2. Insert the SD card into the BrightSign player.
3. Boot up the player.

## Optional: Local Node.js Server for Testing

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.

### To start the server:

1. Navigate to the `server` directory:
```sh
cd server
```
2. Install dependencies:
```sh
yarn install
```
3. Start the server:
```sh
yarn start
```
4. The server will listen on port 7000 by default and has a single endpoint to serve the `autorun.zip` file:
```
http://localhost:7000/autorun.zip
```
29 changes: 29 additions & 0 deletions examples/bs-self-updater-example/autorun.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
function main()

mp = CreateObject("roMessagePort")

'Enable Local DWS
EnableLDWS()

' Create Node JS Server
node = createobject("roNodeJs", "SD:/index.js", { message_port:mp })

'Event Loop
while true
msg = wait(0,mp)
print "msg received - type=";type(msg)

if type(msg) = "roNodeJsEvent" then
print "msg: ";msg
end if
end while

end function

function EnableLDWS()
registrySection = CreateObject("roRegistrySection", "networking")
if type(registrySection) = "roRegistrySection" then
registrySection.Write("http_server", "80")
end if
registrySection.Flush()
end function
147 changes: 147 additions & 0 deletions examples/bs-self-updater-example/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import fetch from "node-fetch";
import decompress from "decompress";
import md5File from "md5-file";
import fs from "fs";
import path from "path";

// @ts-ignore
import { System } from "@brightsign/system";

// Configurable values
const CHECK_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
const SERVER_URL = "http://localhost:7000/autorun.zip"; // Update to your server URL
const STORAGE_PATH = "/storage/sd";
const TMP_PATH = "/storage/tmp";
const extensionsToCheck = [".brs", ".html", ".js", ".json"];

async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function doesFileExist(filePath: string): Promise<boolean> {
try {
await fs.promises.access(filePath, fs.constants.F_OK);
return true;
} catch {
return false;
}
}

async function downloadAndUnzipFile(
url: string,
dest: string
): Promise<boolean> {
try {
const res = await fetch(url);
if (res.status === 200) {
const buffer = Buffer.from(await res.arrayBuffer());
await decompress(buffer, dest);
console.log(`Downloaded zip and unzipped contents to ${dest}`);
return true;
} else {
const err = await res.json();
console.error(`Server error: ${JSON.stringify(err)}`);
return false;
}
} catch (e) {
console.error(`Download failed: ${e}`);
return false;
}
}

async function backupFiles(storagePath: string): Promise<void> {
const filesToBackup = await findFiles(storagePath, extensionsToCheck);
for (const file of filesToBackup) {
const filename = file.replace(/^.*[\\/]/, "");
const backupFile = path.join(TMP_PATH, filename);
await fs.promises.copyFile(file, backupFile);
console.log(`Backed up ${file} to ${backupFile}`);
}
}

async function restoreFiles(storagePath: string): Promise<void> {
const filesToRestore = await findFiles(TMP_PATH, extensionsToCheck);
for (const file of filesToRestore) {
const filename = file.replace(/^.*[\\/]/, "");
const originalFile = path.join(storagePath, filename);
await fs.promises.copyFile(file, originalFile);
console.log(`Restored ${file} to ${originalFile}`);
}
}

async function findFiles(dir: string, exts: string[]): Promise<string[]> {
let results: string[] = [];
const files = await fs.promises.readdir(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = await fs.promises.stat(fullPath);
if (stat.isDirectory()) {
results = results.concat(await findFiles(fullPath, exts));
} else if (exts.some((ext) => file.endsWith(ext))) {
results.push(fullPath);
}
}
return results;
}

async function checksum(filePath: string): Promise<string | null> {
try {
return await md5File(filePath);
} catch {
return null;
}
}

async function reboot() {
try {
console.log("Rebooting device...");
new System().reboot();
} catch (e: any) {
console.error("Failed to reboot:", e.message);
}
}

async function processUpdate() {
await backupFiles(STORAGE_PATH);

const downloaded = await downloadAndUnzipFile(SERVER_URL, STORAGE_PATH);
if (!downloaded) return;

const autorunPath = path.join(STORAGE_PATH, "autorun.brs");
if (!(await doesFileExist(autorunPath))) {
console.error(
"No autorun.brs script found after unzip. Restoring backup and rebooting."
);
await restoreFiles(STORAGE_PATH);
await reboot();
return;
}

const tmpFiles = await findFiles(TMP_PATH, extensionsToCheck);
for (const file of tmpFiles) {
const filename = file.replace(/^.*[\\/]/, "");
const newFile = path.join(STORAGE_PATH, filename);
if (await doesFileExist(newFile)) {
const oldSum = await checksum(file);
const newSum = await checksum(newFile);
if (newSum !== oldSum) {
console.log(`${file} changed. Rebooting.`);
await reboot();
return;
}
}
}
}

async function main() {
while (true) {
try {
await processUpdate();
} catch (e) {
console.error("Error in update loop:", e);
}
await sleep(CHECK_INTERVAL_MS);
}
}

main();
24 changes: 24 additions & 0 deletions examples/bs-self-updater-example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "bs-self-updater",
"description": "A simple self-updater example using TypeScript to run on BrightSign players.",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"format": "prettier . --write --config ../../.prettierrc.js --cache --cache-location=../../prettiercache && yarn lint --fix",
"lint": "eslint --no-error-on-unmatched-pattern --config ../../.eslintrc template/src/**/*.{js,jsx}",
"build": "webpack"
},
"dependencies": {
"@types/decompress": "^4.2.7",
"@types/node-fetch": "^2.6.12",
"decompress": "^4.2.1",
"md5-file": "^5.0.0",
"node-fetch": "2.7.0"
},
"devDependencies": {
"ts-loader": "^9.5.2",
"typescript": "^5.0.0",
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0"
}
}
Binary file not shown.
31 changes: 31 additions & 0 deletions examples/bs-self-updater-example/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const express = require("express");
const fs = require("fs");
const path = require("path");

const app = express();
const PORT = process.env.PORT || 7000;

app.get("/autorun.zip", (req, res) => {
const zipPath = path.join(__dirname, "autorun.zip");
fs.access(zipPath, fs.constants.F_OK, (err) => {
if (err) {
return res.status(404).json({ error: "autorun.zip not found" });
}
res.sendFile(zipPath, (err) => {
if (err) {
res.status(500).json({ error: "Failed to send file" });
}
});
});
});

// Catch-all error handler
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
error: err.message || "Internal Server Error",
});
});

app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
13 changes: 13 additions & 0 deletions examples/bs-self-updater-example/server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "bs-autorun-zip-server",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"format": "prettier . --write --config ../../../.prettierrc.js --cache --cache-location=../../../prettiercache && yarn lint --fix",
"lint": "eslint --no-error-on-unmatched-pattern --config ../../../.eslintrc template/src/**/*.{js,jsx}",
"start": "node index.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
11 changes: 11 additions & 0 deletions examples/bs-self-updater-example/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["index.ts"]
}
26 changes: 26 additions & 0 deletions examples/bs-self-updater-example/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const path = require("path");

module.exports = {
entry: "./index.ts",
target: "node",
output: {
filename: "index.js",
path: path.resolve(__dirname, "dist"),
},
mode: "development",
devtool: false,
resolve: {
extensions: [".ts", ".js"],
},
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader",
},
],
},
externals: {
"@brightsign/system": "commonjs @brightsign/system",
},
};
Loading