diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed4371f --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# General +.env +.envrc +*~ + +# Build artifacts +bin/ +target/ +*.jar +*.zip +*.war + +# Downloaded JRE and packaging staging directory (re-created by make download-jre / make package) +workshop/04-build/java/hello-extension/install/ + +# Java +*.class +*.log +.classpath +.project +.settings/ +.idea/ +*.iml + +# Node (if any tooling added) +node_modules/ +dist/ + +# macOS +.DS_Store + +# Editor +.vscode/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..859ff2f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,454 @@ +# BrightSign Extension Workshop — Project CLAUDE.md + +## What This Project Is + +A hands-on, instructor-led workshop (AWS catalog style) that teaches development teams how +to build, package, deploy, and iterate on BrightSign player extensions using the +[extension-template](https://github.com/brightsign/extension-template). + +**Primary goal:** Reduce extension development time from days to hours by showing teams +the complete, repeatable workflow — from blank repo to running extension on a live player. +The "Hello BrightSign" example program is a teaching vehicle, not the point. The point +is the template, the workflow, and the packaging and deployment pipeline that teams take +home and apply to whatever they are actually building. + +**Current milestone:** Working Java workshop deliverable in one week. +**Planned future variants:** Go, C++ (swap Module 4 only; everything else is shared). + +This repo covers the **extension half** of the system. A companion HTML repo (separate +repo, same workshop) covers the BrightSign HTML app that runs on the player and consumes +the extension. See "Companion HTML App" section below. + +--- + +## Key Reference Repositories + +- **Extension template:** https://github.com/brightsign/extension-template — the scaffold + every participant clones. The workshop teaches this template, not a custom project. +- **NPU gaze extension:** https://github.com/brightsign/brightsign-npu-gaze-extension — + a production-grade extension using the same template. Use it to show what the template + scales to; never reference its domain-specific complexity (NPU, camera, gaze). +- **Simple gaze HTML app:** https://github.com/brightsign/simple-gaze-detection-html — + the structural model for the companion HTML repo (BrightScript bootstrap, webpack + bundle, Makefile pattern, SD card deploy pipeline). + +--- + +## Prerequisites and Development Environment + +### Workshop Participant Prerequisites + +| Category | Requirement | +|---|---| +| Hardware | BrightSign player (LS424, XD1034, or later with extension support) | +| Hardware | Development workstation: macOS, Windows, or Linux | +| Hardware | Ethernet cable + power for the player | +| Hardware | SD card (for Module 9 HTML app deployment) | +| Network | Travel router (GL.iNet or equivalent) bridging venue WiFi to local LAN; players on Ethernet switch; workstations on router WiFi | +| Skills | Comfort with a terminal / command line | +| Skills | Basic familiarity with the workshop language (Java for the Java variant, etc.) | + +Participants do NOT need to install any tools on their workstations if they use the +development container (see below). The container is the recommended path for +instructor-led workshops because it eliminates tool version conflicts and OS differences. + +### Development Container (Recommended) + +A pre-built container image is published to the GitHub Container Registry at: +``` +ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest +``` + +The container includes all tools needed for the full workshop: +- JDK 11 + Maven 3.6 +- Go 1.21 (for future Go module) +- Node 14.x + npm (for HTML app module) +- Git, curl, unzip, jq, ssh, scp +- squashfs-tools (mksquashfs — required for Module 5 packaging) +- wget, python3 (for JSON pretty-printing in curl exercises) + +The Dockerfile for this image lives at `docker/Dockerfile` in this repo. + +#### Running the Container on macOS + +Prerequisites: Docker Desktop for Mac (or OrbStack). + +``` +$ docker pull ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest +$ docker run -it --rm \ + -v "$HOME/workshop:/workspace" \ + -p 8080:8080 \ + ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest +``` + +- `-v "$HOME/workshop:/workspace"` mounts a local folder so work persists after the + container exits. Replace `$HOME/workshop` with any path you prefer. +- `-p 8080:8080` forwards port 8080 so the local smoke test (`curl localhost:8080`) works + from outside the container. +- All workshop commands are run inside this container shell. + +> **Note for Apple Silicon (M1/M2/M3):** The container is built for `linux/amd64`. Docker +> Desktop runs it under Rosetta 2 emulation automatically. Add `--platform linux/amd64` +> explicitly if you see a platform warning. + +#### Running the Container on Windows + +Prerequisites: Docker Desktop for Windows with WSL2 backend enabled. + +Open a PowerShell or Windows Terminal prompt: + +```powershell +docker pull ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest +docker run -it --rm ` + -v "${env:USERPROFILE}\workshop:/workspace" ` + -p 8080:8080 ` + ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest +``` + +- Use backtick `` ` `` for line continuation in PowerShell. +- The workshop directory is created at `%USERPROFILE%\workshop` (e.g., + `C:\Users\YourName\workshop`). +- scp/ssh to the player work from inside the container — no Windows SSH client needed. + +> **Note:** If Docker Desktop is not available, participants can use WSL2 directly with +> Ubuntu and install tools manually (see Manual Install fallback in the facilitator guide). + +#### Cloning the Workshop Repo Inside the Container + +Once inside the container: + +``` +$ cd /workspace +$ git clone https://github.com/BrightSign-Playground/bs-extension-workshop +$ cd bs-extension-workshop +``` + +### Manual Tool Install (Fallback — No Container) + +If the container is unavailable, participants can install tools directly. Minimum set: + +| Tool | Version | Install | +|---|---|---| +| JDK | 11+ | https://adoptium.net | +| Maven | 3.6+ | https://maven.apache.org | +| Node.js | 14.x | https://nodejs.org (select 14.x LTS) | +| Git | any recent | OS package manager | +| curl | any | OS package manager | +| unzip | any | OS package manager | +| squashfs-tools | any | `sudo apt install squashfs-tools` (Linux only) | + +> **Warning:** squashfs-tools is Linux-only. On macOS/Windows without the container, +> Module 5 packaging must be done inside the container or a Linux VM. This is the primary +> reason the container is strongly preferred. + +--- + +## Workshop Philosophy + +- **The example program is a teaching prop, not the deliverable.** "Hello BrightSign" is + intentionally trivial. What participants take home is the template workflow: any team + can slot their own binary — Java, Go, C++, Rust, anything — into the same packaging + and deployment pipeline without understanding the internals of the template. +- **The binary section is the only swappable part.** Module 4 (Build the Extension + Binary) is language-specific. Every other module is shared, identical across languages. + This is the core structural decision. Do not leak language-specific content elsewhere. +- **Two repos, one workshop.** The extension repo and the HTML app repo are developed + and deployed independently, but taught together. Teams leave knowing how to build both + halves of a real extension-driven application. +- **AWS workshop conventions.** Number every participant action step. Use callout blocks + (Note / Warning / Tip). Every module has a stated learning objective. Participants type + real commands and see real output. +- **Instructor-led with self-paced fallback.** Every step is complete enough to follow + alone. Add facilitator guidance in `` HTML comments for things + that benefit from live demonstration. + +--- + +## Target Extension: "Hello BrightSign" + +A deliberately minimal extension that exercises the full lifecycle: + +- Starts an HTTP server on port 8080. +- `GET /` → returns JSON: `{ "message": "Hello from BrightSign!", "uptime_seconds": N }`. +- Writes one line to `/tmp/hello-extension.log` on startup. +- Runs until shutdown signal; exits cleanly. + +This covers the three primitives every real extension uses: network, filesystem, +lifecycle. Nothing else. The moment participants try to add features, they are ahead of +the workshop — which is the right problem to have. + +--- + +## Companion HTML App (Separate Repo) + +A standalone BrightSign HTML application that runs on the same player and interacts with +the extension. It lives in its own repository: + +**https://github.com/BrightSign-Playground/bs-extension-workshop-html-app** + +This repo contains no reference to it beyond this CLAUDE.md and the Module 9 README. +Participants clone it independently during Module 9. + +### Structural model: simple-gaze-detection-html + +Follow the structure and conventions from +https://github.com/brightsign/simple-gaze-detection-html exactly: + +| Pattern | How we apply it | +|---|---| +| `src/autorun.brs` — BrightScript bootstrap | Same: load HTML widget, enable SSH/inspector | +| `src/index.html` — HTML template | Same: minimal markup, load `bundle.js` | +| `src/index.js` — application logic | Fetch from extension HTTP endpoint instead of polling `/tmp/` | +| Webpack + Babel bundle | Same toolchain | +| `make prep / build / publish / clean` | Same Makefile targets | +| Deploy to `sd/` → SD card | Same deployment path | + +### Interaction contract between extension and HTML app + +The extension exposes one endpoint that the HTML app calls: + +``` +GET http://localhost:8080/ +→ { "message": "Hello from BrightSign!", "uptime_seconds": N } +``` + +The HTML app polls this endpoint once per second, renders the message and uptime on +screen. This demonstrates the full communication path: +extension process (port 8080) ↔ BrightSign JS runtime ↔ HTML UI. + +### HTML app layout + +``` +bs-extension-workshop-html-app/ ← separate repo +├── src/ +│ ├── autorun.brs # BrightScript bootstrap +│ ├── index.html # UI template +│ └── index.js # fetch loop, DOM update +├── webpack.config.js +├── package.json +├── Makefile # prep / build / publish / clean +└── README.md +``` + +Webpack config targets `node` (BrightSign JS runtime), externalizes `@brightsign/*` +packages. Node 14.x. Same as simple-gaze-detection-html. + +--- + +## Workshop Modules (Canonical Structure) + +Target duration: **3.5 hours** including short breaks. This is a half-day session. + +### Module 0 — Introduction (15 min) +Learning objective: Understand what BrightSign extensions are and why teams build them. +- What is a BrightSign extension? Where does it run? What problem does it solve? +- System architecture diagram: player OS ↔ extension (port 8080) ↔ HTML app (port 2999). +- Demo the finished product: facilitator shows JSON response from `curl` and the HTML + app rendering it live on the player display. +- What the template gives you: packaging, manifest, deploy pipeline — ready to use. +- What we will build today (and what we will NOT build — scope is intentionally small). + +### Module 1 — Environment Setup (30 min) +Learning objective: Verify all tools are present and the player is reachable. +- Workstation: JDK 11+, Maven 3.6+, Node 14, Git, curl, unzip. +- Player: power on, find IP, enable dev mode (Local Extensions + Insecure Content Loading). +- Verify connectivity: `ping `, `curl http://:8008`. +- Clone the extension template: `git clone https://github.com/brightsign/extension-template`. + +### Module 2 — Understand the Template (20 min) +Learning objective: Know what every file in the extension template does and why. +- Walk every file in the cloned template. +- `manifest.json` deep-dive: every field, what breaks if wrong. +- How the player finds, installs, starts, and stops an extension. +- Show npu-gaze-extension: same template, bigger program. Same workflow scales. + +### Module 3 — Understand the Player API (15 min) +Learning objective: Control extensions via the BrightSign REST API. +- Port 8008 is the player control plane. Port 8080 is the extension's own data plane. +- Live `curl` demos: list extensions, start, stop, get logs. +- These same `curl` commands work for any extension — this API does not change. + +### Module 4 — Build the Extension Binary [MODULAR — SWAP PER LANGUAGE] (45 min) +Learning objective: Produce a binary that satisfies the extension contract. + +All language variants live in `workshop/04-build/` with per-language subdirectories. +Swap this module for the customer's language. The output contract is always the same: +one binary (or JAR) plus any supporting files, which Module 5 packages. + +#### 04-build/java/ +- Create Maven project. Implement `HelloExtension.java`: embedded HTTP server + (`com.sun.net.httpserver`), uptime counter, startup log write. +- `mvn clean package` → fat JAR with all dependencies. +- Local smoke test: `java -jar target/hello-extension.jar` → `curl localhost:8080`. + +#### 04-build/go/ (future) +- `go mod init`, `net/http`, cross-compile for BrightSign ARM target. +- Same local smoke test. + +#### 04-build/cpp/ (future) +- CMake + cpp-httplib, cross-compile toolchain. +- Same local smoke test. + +### Module 5 — Package the Extension (15 min) +Learning objective: Produce a valid extension ZIP from any binary. +- Create ZIP: `manifest.json` at root, JAR/binary alongside it. +- Validate ZIP structure before upload (common failure modes: wrong `mainClass` path, + missing dependencies, JAR not self-contained). +- This step is language-agnostic. The packaging workflow is identical for every variant. + +### Module 6 — Deploy to the Player (20 min) +Learning objective: Install and start the extension on a live player. +- Upload ZIP via player web UI — walk every click with screenshots. +- Alternatively: `curl` upload for command-line preference. +- Install, start, confirm status shows "Running". + +### Module 7 — Verify It Works (15 min) +Learning objective: Confirm end-to-end behavior from two angles. +- `curl http://:8080/` — see JSON response. +- View extension logs via player web UI. +- (Optional) SSH into player, `cat /tmp/hello-extension.log`. + +### Module 8 — Iterate: Change and Redeploy (20 min) +Learning objective: Execute the full change → rebuild → redeploy cycle. +- Modify message string. Stop → rebuild → repackage → upload → install → start. +- This is the workflow teams will run daily. Muscle memory matters. + +### Module 9 — The HTML App (30 min) +Learning objective: Build and deploy the HTML app that consumes the extension. +- Clone https://github.com/BrightSign-Playground/bs-extension-workshop-html-app +- `make prep && make build && make publish`. +- Walk `src/autorun.brs`, `src/index.html`, `src/index.js` — same pattern as + simple-gaze-detection-html. +- Deploy `sd/` contents to SD card. Insert, boot, watch the UI pull from the extension. +- Discussion: what teams can build in this HTML layer — dashboards, kiosks, anything. + +### Module 10 — Production Hardening Overview (15 min) +Learning objective: Know what changes before shipping. +- Extension signing: why, tools, process — conceptual, no hands-on keys. +- Secure mode vs. dev mode: what is disabled, what is required. +- What to never ship (debug logging, hardcoded credentials, unsigned code). +- Pointer to BrightSign signing documentation. + +### Cleanup +- Stop and uninstall the extension. +- Restore player dev mode settings if needed. +- Pointer to BrightSign developer portal, extension-template, community. + +--- + +## Repository Layout + +``` +/ +├── CLAUDE.md ← this file +├── docs/ +│ ├── PRD.md ← original PRD (do not delete) +│ └── DESIGN.md ← master design doc (create before building) +├── docker/ +│ └── Dockerfile ← workshop dev container (published to GHCR) +├── workshop/ +│ ├── 00-introduction/ +│ │ └── README.md +│ ├── 01-environment-setup/ +│ │ └── README.md +│ ├── 02-understand-template/ +│ │ └── README.md +│ ├── 03-player-api/ +│ │ └── README.md +│ ├── 04-build/ ← language-specific module +│ │ ├── README.md ← Java (full), Go + C++ (stubs) +│ │ └── java/ +│ │ └── hello-extension/ ← Maven project + bsext_init +│ ├── 05-package/ +│ │ └── README.md +│ ├── 06-deploy/ +│ │ └── README.md +│ ├── 07-verify/ +│ │ └── README.md +│ ├── 08-iterate/ +│ │ └── README.md +│ ├── 09-html-app/ +│ │ └── README.md +│ ├── 10-production/ +│ │ └── README.md +│ └── cleanup/ +│ └── README.md +├── facilitator-guide/ +│ └── README.md ← timing, demo prep, FAQ, common failures +└── Makefile +``` + +--- + +## Writing Style Rules for Workshop Content + +- **Numbered action steps.** Every participant action is a numbered list item beginning + with a verb: "1. Open a terminal." "2. Run the following command:" +- **Commands in fenced code blocks** with shell prompt: `$ mvn clean package`. +- **Expected output** shown immediately after every command that produces output. +- **Callout blocks** (blockquote format): + - `> **Note:** ...` — helpful context that does not block progress. + - `> **Warning:** ...` — common mistake, destructive action, or known gotcha. + - `> **Tip:** ...` — optional shortcut or exploration for fast finishers. +- **Module header format:** Duration · Learning Objectives · Prerequisites. +- No filler. No praise. No "In this section we will...". Start with the first action. + +--- + +## Makefile Targets (Minimum, Both Repos) + +Extension repo: +``` +build # build the hello-extension JAR/binary +package # produce the deployable ZIP +test-local # run locally and curl-verify +clean # remove build artifacts +help # list targets (default) +``` + +HTML app repo (mirrors simple-gaze-detection-html): +``` +prep # npm install +build # webpack bundle +publish # copy dist/ + autorun.brs to sd/ +clean # remove build artifacts +help # list targets (default) +``` + +--- + +## What NOT to Do + +- Do not make the example extension clever. Simplicity is the feature. +- Do not skip the local smoke test in Module 4. Participants must see the binary work + before touching the player. +- Do not put language-specific content outside `04-build-/`. Every other + module must be identical across all language variants. +- Do not reference npu-gaze-extension domain complexity (NPU, camera, gaze) as + something participants should understand. It is a packaging structure reference only. +- Do not write a full-day workshop. 3.5 hours with breaks. Half day. +- Do not invent a custom HTML app structure. Follow simple-gaze-detection-html exactly + — same Makefile targets, same file layout, same webpack config shape. + +--- + +## Current Status + +- [x] Module 0: Introduction +- [x] Module 1: Environment Setup +- [x] Module 2: Understand Template +- [x] Module 3: Player API +- [x] Module 4 (Java): Build Extension + Maven project +- [x] Module 5: Package +- [x] Module 6: Deploy +- [x] Module 7: Verify +- [x] Module 8: Iterate +- [x] Module 9: HTML App (module README written; html-app submodule registered) +- [x] Module 10: Production +- [x] Facilitator Guide +- [x] Makefile (extension repo) +- [x] docker/Dockerfile — dev container for GHCR +- [ ] .github/workflows/docker-publish.yml — not yet created +- [ ] Module 1 README update — add container launch instructions (macOS + Windows) +- [x] HTML app — lives at https://github.com/BrightSign-Playground/bs-extension-workshop-html-app (separate repo, no submodule) +- [x] Java bsext_init — bundles Eclipse Temurin 11 JRE for linux/aarch64; no system Java required on player diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9b08cf3 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +EXTENSION_DIR := workshop/04-build/java/hello-extension +INSTALL_DIR := $(EXTENSION_DIR)/install +COMMON_SCRIPTS := ../extension-template/examples/common-scripts +JAR_NAME := hello-extension-1.0.0.jar +EXTENSION_NAME := hello_extension + +.PHONY: help build download-jre package test-local clean + +help: ## Print available targets + @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*##"}; {printf " %-14s %s\n", $$1, $$2}' + +build: ## Build the extension JAR + $(MAKE) -C $(EXTENSION_DIR) build + +download-jre: ## Download Temurin JRE 11 for linux/aarch64 (required for packaging) + $(MAKE) -C $(EXTENSION_DIR) download-jre + +package: download-jre ## Build JAR, bundle JRE, and produce the deployable extension ZIP + @if [ ! -d "$(COMMON_SCRIPTS)" ]; then \ + echo ""; \ + echo "ERROR: common-scripts not found at $(COMMON_SCRIPTS)"; \ + echo " Run: git clone https://github.com/brightsign/extension-template"; \ + echo " alongside this repository (one level up), then retry."; \ + echo ""; \ + exit 1; \ + fi + $(MAKE) -C $(EXTENSION_DIR) package + +test-local: build ## Build, run the extension locally, verify the HTTP endpoint, then stop + $(MAKE) -C $(EXTENSION_DIR) test-local + +clean: ## Remove build artifacts, install directory, and ZIP files + $(MAKE) -C $(EXTENSION_DIR) clean diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..007d3a4 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,58 @@ +FROM debian:bookworm-slim + +LABEL org.opencontainers.image.source=https://github.com/BrightSign-Playground/bs-extension-workshop +LABEL org.opencontainers.image.description="BrightSign Extension Workshop — development environment" +LABEL org.opencontainers.image.licenses=Apache-2.0 + +# System packages: base tools + squashfs-tools (required for Module 5 packaging) +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + jq \ + lvm2 \ + maven \ + openssh-client \ + openjdk-11-jdk \ + python3 \ + squashfs-tools \ + unzip \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# Node.js 14.x (required for HTML app module — matches BrightSign player runtime) +RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +# Go 1.21 (required for future Go extension module) +ARG GO_VERSION=1.21.13 +ARG TARGETARCH=amd64 +RUN wget -q https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz \ + && tar -C /usr/local -xzf go${GO_VERSION}.linux-${TARGETARCH}.tar.gz \ + && rm go${GO_VERSION}.linux-${TARGETARCH}.tar.gz + +ENV PATH="/usr/local/go/bin:${PATH}" +ENV GOPATH="/go" +ENV PATH="${GOPATH}/bin:${PATH}" + +# Maven local repo pre-warm: pull the shade plugin and java 11 settings so +# the first `mvn clean package` inside the workshop does not require internet. +# This runs a dummy build that forces download of the shade plugin. +RUN mkdir -p /tmp/mvn-warmup/src/main/java/warmup && \ + echo '4.0.0warmupwarmup1.01111org.apache.maven.pluginsmaven-shade-plugin3.5.1packageshade' \ + > /tmp/mvn-warmup/pom.xml && \ + echo 'package warmup; public class Main { public static void main(String[] a){} }' \ + > /tmp/mvn-warmup/src/main/java/warmup/Main.java && \ + mvn -f /tmp/mvn-warmup/pom.xml clean package -q 2>/dev/null || true && \ + rm -rf /tmp/mvn-warmup + +# Non-root user for workshop work +RUN useradd -m -s /bin/bash workshop +USER workshop +WORKDIR /workspace + +# Verify tools +RUN java -version && mvn -version && node --version && npm --version && go version && mksquashfs -version 2>&1 | head -1 + +CMD ["/bin/bash"] diff --git a/facilitator-guide/README.md b/facilitator-guide/README.md new file mode 100644 index 0000000..2360a00 --- /dev/null +++ b/facilitator-guide/README.md @@ -0,0 +1,152 @@ +# Facilitator Guide + +This guide is for BrightSign engineers running the workshop. Participants do not need to read this. + +--- + +## Pre-Workshop Setup (Day Before) + +### Travel Router and Network (Set Up at the Venue) + +The travel router bridges the venue internet to the local workshop LAN. Players connect via Ethernet through a switch; participants connect via the router's WiFi AP. + +**Equipment checklist:** +- [ ] Travel router (GL.iNet [GL-MT3000](https://www.amazon.com/dp/B09N72FMH5) recommended, or equivalent) +- [ ] Ethernet switch (one port per player + one uplink to router) +- [ ] Ethernet cables: one per player from switch, one from router LAN port to switch uplink + +**Router configuration:** +- [ ] Set WAN mode to **WiFi Repeater / Extender** — connect to the venue WiFi as the WAN uplink +- [ ] Confirm the router's LAN subnet does not conflict with the venue network (change if needed, e.g. `192.168.8.0/24`) +- [ ] Set DHCP reservations: one static IP per player MAC address +- [ ] Confirm internet access through the router before the session: `curl https://api.github.com` + +**Player cabling:** +- [ ] Each player connected via Ethernet to the switch +- [ ] Switch uplink connected to the router LAN port + +**Verification:** +- [ ] From a workstation on the router WiFi: ping each player IP +- [ ] From a workstation: `curl http:///api/v1/info` returns JSON +- [ ] From a workstation: internet access works (GitHub, Docker Hub reachable) + +**Labels and handouts:** +- [ ] IP address label printed and affixed to each player — participants use this in Module 1.2 +- [ ] SSID and password written on a card or slide to hand out in Module 1.1 + +> **If you cannot pre-assign IPs:** participants boot their player without an SD card and the player shows its IP on screen. Add ~5 minutes to Module 1.2. + +### Players — Insecuring (Critical — Must Be Done Before the Workshop) + +Insecuring a player is **irreversible**. Do this only on dedicated development units. + +For each participant player: +- [ ] Connect serial cable (115200 baud, 8N1) +- [ ] Remove SD card, hold SVC button, apply power, press Ctrl-C at bootloader countdown +- [ ] At bootloader prompt: `disable_secure_boot` (or `setenv SECURE_CHECKS 0` + `saveenv` as fallback) +- [ ] Reboot, then at BrightSign shell: `script debug on` + `reboot` +- [ ] Boot with the development `autorun.brs` on SD card (see Module 1.3 in the workshop for the script) +- [ ] After player displays "Setup complete", remove SD card and reboot +- [ ] Verify: SSH in, type `exit` twice — second `exit` should drop to `#` Linux shell (not reboot) +- [ ] Affix IP label to player + +Reference: [BrightSign dev environment setup docs](https://github.com/BrightDevelopers/technical-documentation/blob/main/howto-articles/01-setting-up-development-environment.md) + +### Facilitator Demo Player + +- [ ] Separate pre-insecured player with the finished extension and HTML app already deployed +- [ ] Connected to the travel router switch; verify the extension responds on port 8080 before the session +- [ ] Used for the Module 0 demo — show `curl` output and the HTML app on screen before participants start +- [ ] Do not use it for participant exercises + +### Workstations (Container Path — Recommended) + +- [ ] Docker Desktop installed and running (macOS/Windows) or Docker Engine (Linux) +- [ ] Container image pre-pulled to avoid download time during Module 1: + ``` + docker pull ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest + ``` +- [ ] Test the container run command on each workstation OS before the day +- [ ] `$HOME/workshop` (macOS/Linux) or `%USERPROFILE%\workshop` (Windows) directory exists as the volume mount point +- [ ] Network access to GitHub (for `git clone` inside the container), OR pre-cloned repos staged in the volume mount directory + +### Workstations (Manual Install Fallback) + +If Docker is unavailable, ensure these are installed on the workstation: +- [ ] JDK 11+, Maven 3.6+, Node 14.x, Git, curl, unzip +- [ ] `squashfs-tools` (Linux only — this is the hard blocker on macOS/Windows without container) +- [ ] On macOS/Windows without squashfs-tools: pre-run Module 5 packaging for participants as a fallback, provide the ZIP directly + +--- + +## Timing Guide + +| Module | Expected | Watch for | +|---|---|---| +| 0 — Introduction | 15 min | Demo can run long; cut Q&A if needed | +| 1 — Environment Setup | 30 min | Travel router connectivity is the most common blocker; verify all players are reachable before starting | +| 2 — Understand Template | 20 min | Often sparks questions; 5-minute buffer built in | +| 3 — Player API | 15 min | Network issues here signal problems ahead; fix before proceeding | +| Break | 10 min | After Module 3 | +| 4 — Build Java Extension | 45 min | Maven download on slow networks; pre-cache `.m2` or use a local mirror | +| 5 — Package | 15 min | Usually smooth | +| 6 — Deploy | 20 min | Most common failure: wrong `manifest.json` | +| 7 — Verify | 15 min | Usually fast | +| Break | 10 min | After Module 7 | +| 8 — Iterate | 20 min | Let fast finishers add a `/health` endpoint | +| 9 — HTML App | 30 min | Node/webpack issues on Windows; have a Linux fallback ready | +| 10 — Production | 15 min | Conceptual; keep it crisp | +| Cleanup | 5 min | | +| **Total** | **~3.5 hours** | | + +--- + +## Common Failures and Fixes + +### Module 1: Player not reachable + +- Check that the cable is in the correct Ethernet port. +- The player may still be booting — wait 90 seconds and retry. +- Run `arp -a` to scan the network for the player's MAC address. + +### Module 1: `curl` to port 8008 returns nothing + +- Dev mode may not be enabled yet — enable it manually for that participant via the player's web UI. +- A firewall on the workstation may be blocking outbound connections; check Windows Defender rules. + +### Module 4: Maven download hangs + +- Use a local `.m2` mirror if one is available on the network. +- Pre-stage the fat JAR on a USB drive as a fallback so participants can skip the build and continue. + +### Module 6: Extension fails to start after install + +- Most common cause: wrong `mainClass` in `manifest.json` — verify the exact fully-qualified `package.ClassName`. +- Second most common cause: the JAR is not a fat JAR — the shaded JAR with all dependencies bundled is required. +- Pull the extension logs to diagnose: + + ```bash + curl http://$PLAYER_IP:8008/api/v1/extensions/hello-extension/logs + ``` + +### Module 9: webpack fails on Windows + +- Use WSL2 or a Linux VM — the simple-gaze-detection-html repo has known issues on macOS and Windows. +- A pre-built `sd/` directory is available on USB as a fallback so participants can continue to Module 10. + +--- + +## Fast Finisher Extensions + +Optional exercises for participants who finish ahead of schedule. + +**Finished Module 4 early:** + +- Add a `GET /health` endpoint that returns `{ "status": "ok" }`. +- Add a request counter to the JSON response body. +- Return the player hostname in the JSON response. + +**Finished Module 8 early:** + +- Use the `curl`-based deploy flow instead of the web UI to redeploy the extension. +- Explore what other endpoints port 8008 exposes on the player. diff --git a/workshop/00-introduction/README.md b/workshop/00-introduction/README.md new file mode 100644 index 0000000..a77c955 --- /dev/null +++ b/workshop/00-introduction/README.md @@ -0,0 +1,117 @@ + + +# Module 0: Introduction + +**Duration:** 15 minutes +**Learning Objectives:** +- Explain what a BrightSign extension is and how it runs on the player +- Describe the three-process architecture: extension, HTML app, and Control API +- Identify what the extension-template provides and why it accelerates development +- State what you will build and deploy by the end of this workshop + +**Prerequisites:** None — this is the first module. + +--- + +## 1. What Is a BrightSign Extension? + +A BrightSign extension is a **separate process** that runs alongside BrightSign OS on the player. It is not a plugin or a scripting hook — it is a standalone executable that the OS starts and manages. + +Key properties: + +- Runs as its own process on the player hardware +- Exposes HTTP endpoints on **port 8080** that other processes (and remote clients) can call +- Has full access to hardware, the filesystem, and the network — anything the player supports +- Is **language-agnostic**: Java, Go, C++, or any language that produces a runnable binary + +> **Note:** The extension HTTP server runs on port 8080 by convention. BrightSign OS does not enforce this — your extension can bind any available port — but the workshop and the template use 8080 throughout. + +--- + +## 2. Why Teams Build Extensions + +| Problem | Extension solution | +|---|---| +| BrightSign OS does not expose a capability natively | Implement it yourself and expose it over HTTP | +| Custom compute needed (AI inference, sensor fusion, proprietary protocols) | Run the compute on the player; no external server required | +| Round-trip latency to a backend server is unacceptable | Co-locate business logic with the display device | + +The extension model lets you treat the BrightSign player as a general-purpose Linux compute node that also drives a display. + +--- + +## 3. System Architecture + +```mermaid +graph TB + subgraph player["BrightSign Player"] + ext["Extension Process\nport 8080\nyour code"] + html["HTML App\ndisplay layer\nBrightScript + JS"] + api["Control API\nport 8008"] + ext -->|"HTTP GET /"| html + end + ws["Your Workstation\ncurl / CI / management tool"] -->|"install · start · stop"| api +``` + +**How the pieces interact:** + +1. You upload a packaged extension to the player via the **Control API (port 8008)**. +2. The Control API installs and starts the extension process. +3. The **extension process** binds port 8080 and begins serving HTTP. +4. The **HTML app** running in the display layer fetches data from `http://localhost:8080` and renders it on screen. + +--- + +## 4. What the Extension Template Gives You + +The `extension-template` repository in this workshop provides: + +- **Packaging structure** — `manifest.json`, compiled binary, and a ZIP archive in the layout BrightSign OS expects +- **Deploy pipeline** — a scripted sequence: upload → install → start → verify +- **Consistent pattern** — the template works for any language; teams replace the binary with their own without changing the packaging or deploy steps + +> **Tip:** The template's core value is the workflow, not the code. Once you understand how packaging and deployment work, swapping in a real extension (AI inference engine, sensor reader, etc.) is a matter of replacing the binary and updating `manifest.json`. + +--- + +## 5. What We Will Build Today + +By the end of this workshop you will have deployed a fully functional extension to a live BrightSign player. + +**The extension ("Hello BrightSign"):** +- HTTP server on port 8080 +- Single endpoint `GET /` returning JSON with server uptime +- Intentionally trivial — the workflow is the point, not the logic + +**The HTML app:** +- Polls the extension endpoint every few seconds +- Displays the JSON response on screen + +**The full cycle:** +- Package → deploy → verify with `curl` → see output on the display + +--- + +## 6. Architecture of the Finished Product + +```mermaid +graph TB + subgraph player["BrightSign Player"] + ext["hello-brightsign\nport 8080\nGET / → {message, uptime_seconds}"] + html["index.html\nrenders message and uptime on screen"] + ext -->|"fetch every 1s"| html + end + ws["Your Workstation"] -.->|"curl http://player-ip:8080/"| ext +``` + +--- + +## 7. What This Workshop Is Not + +> **Note:** This is not a Java, Go, or C++ tutorial. The extension binary is intentionally trivial — a minimal HTTP server that returns a static JSON response. The purpose of this workshop is to teach the **packaging and deployment workflow**, not application development. Once you know the workflow, the language and complexity of your actual extension are separate concerns. + +--- + +## Next Step + +Proceed to **[Module 1: Environment Setup](../01-environment-setup/README.md)** to install the required tools and verify connectivity to your player. diff --git a/workshop/01-environment-setup/README.md b/workshop/01-environment-setup/README.md new file mode 100644 index 0000000..4183e23 --- /dev/null +++ b/workshop/01-environment-setup/README.md @@ -0,0 +1,343 @@ +# Module 1: Environment Setup + +**Duration:** 30 minutes +**Learning Objectives:** +- Connect your workstation to the workshop network +- Locate your player's IP address and verify connectivity +- Confirm the player is configured for extension development +- Launch the workshop development container +- Clone the workshop materials +- Create your own extension repository from the template on GitHub + +**Prerequisites:** Module 0 complete. Docker Desktop installed (macOS/Windows) or Docker Engine (Linux). GitHub account (personal or work) — needed in section 1.6. + +--- + +## 1.1 Workshop Network Setup + + + +The WL provides a travel router that bridges the venue internet connection to a local workshop network. The network topology is: + +```mermaid +graph TD + inet["Facility WiFi / Internet"] + inet -->|WAN uplink via WiFi| router["Travel Router\nGL.iNet or equivalent"] + router -->|LAN port| switch["Ethernet Switch"] + switch --> p1["BrightSign Player 1"] + switch --> p2["BrightSign Player 2"] + switch --> pn["BrightSign Player N..."] + router -->|WiFi| wp1["Your Workstation"] + router -->|WiFi| wp2["Other Workstations"] +``` + +Your workstation connects to the travel router over **WiFi**. This gives you: +- Access to the internet (for `git clone`, `docker pull`, etc.) +- Direct access to the BrightSign players on the wired LAN + +The players connect to the travel router via Ethernet through a switch — not WiFi. This ensures stable, low-latency connections to the players during deployment. + +1. On your workstation, connect to the workshop WiFi: + - **SSID:** provided by your WL + - **Password:** provided by your WL + +2. Confirm your workstation received an IP on the workshop subnet: + + macOS / Linux: + ``` + $ ip addr show # or: ifconfig + ``` + Windows: + ``` + > ipconfig + ``` + Expected: an address in the travel router's subnet (e.g. `192.168.8.x`). + +3. Confirm you have internet access: + ``` + $ curl -s https://api.github.com | python3 -m json.tool | head -5 + ``` + Expected: JSON from GitHub API. If this fails, ask your WL — the router's WAN uplink may need attention. + +--- + +## 1.2 Find Your Player's IP Address + +Each player at your bench has a label showing its IP address. This IP is a static DHCP reservation set up by the WL — it will not change during the workshop. + +> **If there is no label on your player**, boot it without an SD card inserted. The player displays its IP address on the connected display during boot. + +1. Note the IP address from the label (or from the display). + +2. Set an environment variable — this is used in every module from here on: + ``` + $ export PLAYER_IP= + ``` + + > **Tip:** Save this in a file so it survives a container restart: + > ``` + > $ echo "export PLAYER_IP=$PLAYER_IP" >> /workspace/.envrc + > ``` + +3. Verify the player is reachable: + ``` + $ ping -c 3 $PLAYER_IP + ``` + Expected: + ``` + 3 packets transmitted, 3 received, 0% packet loss + ``` + +4. Verify the BrightSign API responds: + ``` + $ curl -s http://$PLAYER_IP/api/v1/info | python3 -m json.tool + ``` + Expected: JSON with player model, firmware version, serial number. + + > **Note:** The DWS runs on port 80 (not 8008). The WL has already configured the + > player to run the DWS with no authentication required. + +--- + +## 1.3 Understand the Player's Development Configuration + + + +The WL has already prepared each player for extension development. This section explains what was done so you understand the state of the hardware you are working with. + +### What "Insecuring" Means + +BrightSign players ship with secure boot enabled. Secure boot prevents unsigned code — including native OS extensions — from running. To develop and deploy unsigned extensions, secure boot must be permanently disabled. This is a one-way, irreversible operation called "insecuring" the player. + +> **Warning:** Insecuring a player cannot be undone by factory reset, OS update, or any other means. The players provided for this workshop are dedicated development units. Never insecure a production player. + +The insecuring process required: +1. Connecting a serial cable (115200 baud, 8N1) and interrupting the bootloader at startup using the SVC button + Ctrl-C. +2. Running `disable_secure_boot` at the bootloader prompt (or `setenv SECURE_CHECKS 0` + `saveenv` as a fallback). +3. Enabling the BrightScript debugger via `script debug on` at the BrightSign shell. + +### What the Development autorun.brs Does + +After insecuring, the WL booted each player with this `autorun.brs` on an SD card: + +```brightscript +Sub Main() + regB = CreateObject("roRegistrySection", "brightscript") + regB.Write("debug", "1") + regB.Flush() + + reg = CreateObject("roRegistrySection", "networking") + reg.Write("bbhf", "on") + reg.Write("dwse", "yes") + reg.Write("curl_debug", "1") + reg.Write("prometheus-node-exporter-port", "9100") + reg.Write("ssh", "22") + reg.Write("telnet_log_level", "7") + reg.Flush() + + CreateObject("roNetworkConfiguration", 0).SetupDWS({port: "80", open: "none"}) + + n = CreateObject("roNetworkConfiguration", 0) + n.SetLoginPassword("none") + n.Apply() + + ShowMessage("Setup complete -- manually reboot the player to apply settings") + sleep(50000) +End Sub +``` + +This one-time setup wrote the following registry keys and then the player was rebooted **without the SD card**. After that reboot, the player has: + +| Feature | Value | +|---|---| +| Local DWS | `http:///` — no login required | +| SSH | port 22 — no password required | +| BrightScript debug | enabled | +| curl verbose logging | enabled | + +> **Warning:** This configuration has no authentication. It is only safe on the isolated workshop travel router network. Do not expose these players to a corporate or public network. + +### Verify the Player is Ready + +1. Open a browser on your workstation and navigate to: + ``` + http:/// + ``` + Expected: the BrightSign Diagnostic Web Server (DWS) home page loads with no login prompt. + +2. Verify SSH from inside the container: + ``` + $ ssh brightsign@$PLAYER_IP + ``` + Expected: shell prompt with no password required. Type `exit` to leave. + + > **Note:** `exit` at the BrightSign shell (`BrightSign>`) reboots the player on a secure device. On these insecured players, `exit` drops to a Linux root shell (`#`). Type `exit` again to reboot. + +3. Verify the DWS API: + ``` + $ curl -s http://$PLAYER_IP/api/v1/info | python3 -m json.tool + ``` + Expected: JSON response with `model`, `firmwareVersion`, `serialNumber`. + +If any of these fail, ask your WL before proceeding. + +--- + +## 1.4 Start the Development Container + +The workshop uses a pre-built container that includes all required tools: JDK 11, Maven, +Node 14, Go, Git, curl, squashfs-tools, and more. This eliminates tool installation and +version conflicts across macOS, Windows, and Linux. + +> **Note:** If your WL has confirmed that tools are pre-installed on your +> workstation, skip to section 1.5. + +### macOS + +1. Open Terminal. + +2. Pull the container image: + ``` + $ docker pull ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest + ``` + +3. Start the container: + ``` + $ docker run -it --rm \ + -v "$HOME/workshop:/workspace" \ + -p 8080:8080 \ + ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest + ``` + + You are now at a shell prompt inside the container. All subsequent commands in this + workshop are run here unless stated otherwise. + + > **Note for Apple Silicon (M1/M2/M3):** If you see a platform warning, add + > `--platform linux/amd64` to the `docker run` command. + +4. Verify tools: + ``` + $ java -version && mvn -version && node --version && mksquashfs -version 2>&1 | head -1 + ``` + Expected: version lines for each tool, no errors. + +### Windows + +1. Open PowerShell or Windows Terminal. + +2. Pull the container image: + ```powershell + docker pull ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest + ``` + +3. Start the container: + ```powershell + docker run -it --rm ` + -v "${env:USERPROFILE}\workshop:/workspace" ` + -p 8080:8080 ` + ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest + ``` + +4. Verify tools: + ``` + $ java -version && mvn -version && node --version && mksquashfs -version 2>&1 | head -1 + ``` + Expected: version lines for each tool, no errors. + + > **Warning:** Use PowerShell or Windows Terminal — not `cmd.exe`. The volume mount syntax does not work in `cmd.exe`. + +### Linux + +1. Open a terminal and start the container: + ``` + $ docker run -it --rm \ + -v "$HOME/workshop:/workspace" \ + -p 8080:8080 \ + ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest + ``` + +2. Verify tools as above. + +--- + +## 1.5 Clone the Workshop Repo + +Inside the container: + +1. Navigate to workspace: + ``` + $ cd /workspace + ``` + +2. Clone: + ``` + $ git clone https://github.com/BrightSign-Playground/bs-extension-workshop + $ cd bs-extension-workshop + ``` + +3. Verify: + ``` + $ ls workshop/ + ``` + Expected: numbered module directories (00 through cleanup). + +--- + +## 1.6 Create Your Extension Repo from the Template + + + +Rather than cloning the extension template directly, you will create your own repository from it. This gives you a repo you own, can push to, and take home after the workshop. + +> **Prerequisite:** You need a GitHub account (personal or work). If you do not have one, create a free account at https://github.com now. + +### On GitHub (in your browser) + +1. Navigate to the extension template: + ``` + https://github.com/brightsign/extension-template + ``` + +2. Click the **"Use this template"** button (green, near the top right). + +3. Select **"Create a new repository"**. + +4. Fill in the form: + - **Owner:** your GitHub username or organization + - **Repository name:** choose a name for your extension project (e.g. `my-brightsign-extension`, `acme-player-ext`, or whatever fits your use case) + - **Visibility:** Public or Private — your choice + - Leave "Include all branches" unchecked + +5. Click **"Create repository"**. + + GitHub creates a new repo in your account with the full template structure already in place. + +### Back in the container + +6. Clone your new repo: + ``` + $ cd /workspace + $ git clone https://github.com// + $ cd + ``` + +7. Verify contents: + ``` + $ find . -type f | sort + ``` + Expected: files under `examples/`, `common-scripts/`, `docs/`. + +8. Set your Git identity inside the container (required to commit): + ``` + $ git config user.email "you@example.com" + $ git config user.name "Your Name" + ``` + +> **Note:** This is your extension repo. You will build your Hello BrightSign extension +> here in Module 4 and push your changes back to GitHub at the end of the workshop. +> The template files in `examples/` are reference material — Module 2 walks through them. + +--- + +You now have a connected insecured player, a running container, the workshop materials cloned, and your own extension repo ready. +Proceed to **[Module 2](../02-understand-template/README.md)**. diff --git a/workshop/02-understand-template/README.md b/workshop/02-understand-template/README.md new file mode 100644 index 0000000..38ddab1 --- /dev/null +++ b/workshop/02-understand-template/README.md @@ -0,0 +1,198 @@ + + +# Module 2: Understanding the Extension Template + +**Duration:** 20 minutes +**Learning Objectives:** +- Understand the structure and purpose of every file in the extension template +- Understand how bsext_init controls the extension lifecycle +- Know what the packaging scripts produce and why +- See how the same structure scales from hello world to production + +**Prerequisites:** Module 1 complete. Extension template cloned. + +--- + +## 2.1 Template Structure + +From inside the cloned `extension-template` directory, list every file: + +``` +$ find . -type f | sort +``` + +Expected output: +``` +./CLAUDE.md +./Dockerfile +./LICENSE.txt +./README.md +./docs/Serial-Connection.md +./docs/Un-Secure-Player.md +./examples/common-scripts/make-extension-lvm +./examples/common-scripts/make-extension-ubi +./examples/common-scripts/pkg-dev.sh +./examples/hello_world-go-extension/bsext_init +./examples/hello_world-go-extension/main.go +./examples/hello_world-ts-extension/bsext_init +./examples/hello_world-ts-extension/package.json +./examples/hello_world-ts-extension/src/index.ts +./examples/hello_world-ts-extension/webpack.config.js +./examples/time_publisher-cpp-extension/bsext_init +./examples/time_publisher-cpp-extension/CMakeLists.txt +./examples/time_publisher-cpp-extension/src/main.cpp +``` + +There is no build system at the root level. Each example in `examples/` is self-contained. The only shared code is the packaging scripts in `examples/common-scripts/`. + +--- + +## 2.2 The bsext_init Script — The Most Important File + +`bsext_init` is a SysV-style init script. It is the entry point for your extension. The player uses it to start, stop, and restart your extension process. Every extension — regardless of language — must have one. + +Read the Go example's init script: + +``` +$ cat examples/hello_world-go-extension/bsext_init +``` + +Walk through each part: + +**`DAEMON_NAME`** +The extension's unique identifier. Rules: 3–31 characters, lowercase letters, numbers, and underscores only, must start with a letter. This is how the player tracks your process. It must match the directory name your extension installs to on the player. + +**`run_extension()`** +Sets environment variables (such as `PORT`) and launches your binary as a background daemon using `start-stop-daemon`. The `--make-pidfile` flag writes a PID file so the player can send signals to your process later. + +**`do_start()`** +Reads the player's registry to check whether the autostart flag is set for this extension. If it is, calls `run_extension`. If not, exits without starting the process. + +**`do_stop()`** +Reads the PID file written by `start-stop-daemon` and sends `SIGTERM` to the process. After a grace period, sends `SIGKILL` if the process has not exited. + +**`start | stop | restart | run` arguments** +Standard SysV init interface. The player calls `bsext_init start` to start your extension, `bsext_init stop` to stop it. The `run` argument is for manual testing — it runs the binary in the foreground. + +> **Note:** There is no `manifest.json`. The `bsext_init` script IS the extension definition. The `DAEMON_NAME` value in this file must match the directory name your extension installs to on the player filesystem. + +> **Warning:** `DAEMON_NAME` must be globally unique if you plan to submit for production signing. Use an organization-specific prefix — `acme_hello`, not `hello`. BrightSign's signing process will reject names that conflict with existing signed extensions. + +--- + +## 2.3 The Packaging Scripts + +Read the packaging script: + +``` +$ cat examples/common-scripts/pkg-dev.sh +``` + +What this script does, in order: + +1. Takes your `install/` directory as input along with a filesystem type (`lvm` or `ubi`) and your extension name. +2. Calls either `make-extension-lvm` or `make-extension-ubi` depending on your target player's flash storage type. +3. Creates a squashfs read-only filesystem image from the contents of `install/`. +4. Generates an installation shell script named `ext__install-lvm.sh` (or `-ubi.sh`). +5. Bundles the squashfs image and the install script into a timestamped ZIP file: `-YYYYMMDD-HHMMSS.zip`. + +> **Note:** Extensions are squashfs read-only filesystems, not ZIP files you extract manually. The squashfs image is mounted by the player at runtime as a read-only volume. Your extension binary cannot write to its own directory. Write temporary data to `/dev/shm` (RAM) or `/var/volatile` depending on your player firmware version. + +--- + +## 2.4 What Goes in install/ + +The `install/` directory is the root of your extension's filesystem. Treat it as `/` for your extension's mount point. + +Required contents: +- Your compiled binary (or entry script, or JAR file) +- `bsext_init` — copied from your project root + +Optional contents: +- Configuration files your binary reads at startup +- Supporting shared libraries (`.so` files for C++ extensions) +- Static assets your extension reads at runtime (certificates, model files, etc.) + +Do not include build artifacts, source files, or development tools. The squashfs image is size-constrained and is mounted read-only. + +--- + +## 2.5 Walk a Complete Example: Go + +List the Go example directory: + +``` +$ ls examples/hello_world-go-extension/ +``` + +Expected output: +``` +bsext_init main.go +``` + +Read `main.go`. It does three things: +- Reads `PORT` from the environment (defaults to `5010` if not set) +- Sends a short UDP broadcast message once per second +- Registers handlers for `SIGINT` and `SIGTERM` and shuts down cleanly when either signal arrives + +Cross-compile the binary for BrightSign's ARM64 processor: + +``` +$ GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o hello_world_go main.go +``` + +> **Note:** BrightSign players run ARM64 Linux. Your development machine is almost certainly x86-64. `CGO_ENABLED=0` produces a fully static binary with no libc dependency — the safest approach for cross-compilation. Other languages require a cross-compiler toolchain or a Docker container with the BrightSign SDK. + +Package the extension: + +``` +$ mkdir -p install +$ cp hello_world_go bsext_init install/ +$ ../common-scripts/pkg-dev.sh install lvm hello_world_go +``` + +Expected output: +``` +Creating squashfs image... +Generating install script... +hello_world_go-20240315-143022.zip +``` + +That ZIP file is your deployable extension artifact. Upload it to the player via the Control API. + +--- + +## 2.6 The Same Pattern for Every Language + +The template works for any language that can produce a runnable binary. The packaging and deployment steps do not change. Only the build step changes. + +| Language | Binary produced | Cross-compile method | bsext_init starts it | +|---|---|---|---| +| TypeScript | `index.js` (webpack bundle) | n/a — uses the player's built-in Node.js runtime | `node ./index.js` | +| Go | static binary | `GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build` | `./hello_world_go` | +| C++ | dynamically linked binary | Docker container with BrightSign SDK cross-compiler | `LD_LIBRARY_PATH=./lib ./time_publisher` | +| Java | fat JAR | n/a — JVM-portable bytecode | `java -jar hello-extension.jar` | + +> **Note:** Java requires a JVM present on the player at runtime. Verify with your facilitator whether the target player model has a JVM pre-installed. If not, a bundled JRE can be placed inside `install/` and referenced by a relative path in `bsext_init`, but this significantly increases extension size and is unusual in practice. + +--- + +## 2.7 The NPU Gaze Extension — What This Scales To + +The template you just studied is the same structure used in production extensions. For reference, see the BrightSign NPU Gaze Extension: + +``` +https://github.com/brightsign/brightsign-npu-gaze-extension +``` + +It is a substantially larger binary: it runs a neural processing pipeline, streams camera frames through an NPU inference engine, and publishes gaze coordinates over HTTP. The `bsext_init` structure is identical. The packaging scripts are identical. The deployment workflow is identical. + +> **Note:** Do not study the gaze extension's internals during this workshop. The point is that the template does not change regardless of what the extension does. Your team replaces the binary and updates `DAEMON_NAME` in `bsext_init`. Everything else is the same workflow you just learned. + +--- + +## What's Next + +In Module 4 you will write your own `bsext_init`, compile your own binary, run `pkg-dev.sh`, and deploy the resulting ZIP to your player. The steps are exactly what you walked through in sections 2.5 above. + +Proceed to **[Module 3: The BrightSign Player API](../03-player-api/README.md)**. diff --git a/workshop/03-player-api/README.md b/workshop/03-player-api/README.md new file mode 100644 index 0000000..3a21245 --- /dev/null +++ b/workshop/03-player-api/README.md @@ -0,0 +1,148 @@ + + +# Module 3: The BrightSign Player API + +**Duration:** 15 minutes +**Learning Objectives:** +- Distinguish between the control API port and the extension server port +- Query player info, extension list, and system status via curl +- Execute the full extension lifecycle (install, start, stop, uninstall) using API calls + +**Prerequisites:** Module 2 complete. `PLAYER_IP` environment variable set. + +--- + +## 3.1 Two Ports, Two Purposes + +The player exposes two HTTP interfaces. They serve different roles and you will use both throughout the rest of the workshop. + +```mermaid +graph LR + ws["Your Workstation"] + + subgraph player["BrightSign Player"] + api["port 8008\nControl API\ninstall · start · stop · logs\nplayer info · system status"] + ext["port 8080\nExtension Server\nyour HTTP endpoints\n(only active when extension runs)"] + end + + ws -->|"manage"| api + ws -.->|"verify / test"| ext + html["HTML App\non player"] -->|"fetch data"| ext +``` + +Requests to `:8008` go to BrightSign firmware. Requests to `:8080` go to your code. + +--- + +## 3.2 Query the Player + +Run these commands against your player. Confirm each response before moving to the next. + +1. Get player info: + ``` + $ curl -s http://$PLAYER_IP:8008/api/v1/info | python3 -m json.tool + ``` + Expected output: + ```json + { + "model": "XT1144", + "firmwareVersion": "9.x.x", + "serialNumber": "..." + } + ``` + +2. List installed extensions: + ``` + $ curl -s http://$PLAYER_IP:8008/api/v1/extensions | python3 -m json.tool + ``` + Expected output (no extensions installed yet): + ```json + [] + ``` + +3. Get system status: + ``` + $ curl -s http://$PLAYER_IP:8008/api/v1/system/status | python3 -m json.tool + ``` + Expected output: + ```json + { + "uptime": 12345, + "temperature": "...", + "storage": {...} + } + ``` + +> **Note:** `python3 -m json.tool` pretty-prints JSON. It is available on every Python 3 installation with no extra packages. + +> **Tip:** If you prefer `jq`, install it with your system package manager (`apt install jq`, `brew install jq`, etc.) and substitute `| jq .` anywhere you see `| python3 -m json.tool`. + +--- + +## 3.3 Extension Lifecycle via API + +These are the six operations you will use repeatedly in Modules 6, 7, and 8. Run through them now against the demo player so the pattern is familiar before you use your own extension. + +**Install** — POST a ZIP file to the extensions endpoint: +``` +$ curl -X POST http://$PLAYER_IP:8008/api/v1/extensions \ + -F "file=@hello-extension.zip" +``` +Expected output: +```json +{"status": "installed", "name": "hello-extension"} +``` + +**Start** — start a named extension: +``` +$ curl -X POST http://$PLAYER_IP:8008/api/v1/extensions/hello-extension/start +``` +Expected output: +```json +{"status": "running"} +``` + +**Get status** — query the current state of an extension: +``` +$ curl -s http://$PLAYER_IP:8008/api/v1/extensions/hello-extension | python3 -m json.tool +``` +Expected output: +```json +{ + "name": "hello-extension", + "status": "running", + "pid": 1234 +} +``` + +**Stop** — stop a running extension: +``` +$ curl -X POST http://$PLAYER_IP:8008/api/v1/extensions/hello-extension/stop +``` +Expected output: +```json +{"status": "stopped"} +``` + +**Uninstall** — remove the extension from the player: +``` +$ curl -X DELETE http://$PLAYER_IP:8008/api/v1/extensions/hello-extension +``` +Expected output: +```json +{"status": "uninstalled"} +``` + +**Get logs** — retrieve stdout/stderr from the extension process: +``` +$ curl -s http://$PLAYER_IP:8008/api/v1/extensions/hello-extension/logs +``` +Expected output: plain text lines from your extension's output. + +> **Note:** These exact commands are used in Modules 6, 7, and 8. Bookmark this module or keep this terminal window open. + +--- + +## 3.4 Key Takeaway + +The player API does not change based on what language your extension is written in or what your extension does. The packaging and deployment workflow covered in Modules 5 through 8 is identical for every extension. This is what the template from Module 1 standardizes — it handles the packaging so you only write application code. diff --git a/workshop/04-build/README.md b/workshop/04-build/README.md new file mode 100644 index 0000000..8b27e89 --- /dev/null +++ b/workshop/04-build/README.md @@ -0,0 +1,283 @@ + + +# Module 4: Build the Extension + +**Duration:** 45 minutes +**Learning Objectives:** +- Build a binary that satisfies the extension contract +- Write a `bsext_init` script to manage the process lifecycle +- Verify the extension works locally before deploying to the player + +**Prerequisites:** Modules 1–3 complete. Development container running. + +Your WL will direct you to the section for your session: + +- [Java](#java) — JDK 11, Maven, fat JAR +- [Go](#go) — coming soon +- [C++](#cpp) — coming soon + +--- + +## The Extension Contract + +Every extension — regardless of language — must satisfy this contract. Module 5 and all +subsequent modules depend on it exactly as stated here. + +1. A binary (or JAR) that starts an HTTP server on **port 8080**. +2. `GET /` returns `{"message":"Hello from BrightSign!","uptime_seconds":N}` with `Content-Type: application/json`. +3. Writes one line to `/tmp/hello-extension.log` on startup. +4. Handles SIGTERM and SIGINT and exits cleanly. +5. A `bsext_init` script that the player OS uses to start and stop the process. + +The contract does not care what language produces the binary. Module 5 is identical for +Java, Go, and C++ — it packages whatever is in `install/`. + +--- + + +## Java + +**Language:** Java 11+ (Maven) + +> **Note:** BrightSign players do not have a system Java installation. The JRE is bundled +> inside the squashfs image. The Makefile handles the download automatically. + +### 4.1 Create the Project + +1. Copy the Java starter project into your extension repo: + ``` + $ cd /workspace/ + $ cp -r /workspace/bs-extension-workshop/workshop/04-build/java/hello-extension/. . + ``` + +2. Verify the structure: + ``` + $ find . -not -path './.git/*' -type f | sort + ``` + Expected: + ``` + ./Makefile + ./bsext_init + ./pom.xml + ./src/main/java/com/brightsign/workshop/HelloExtension.java + ``` + +### 4.2 Walk pom.xml + +Open `pom.xml` and note three things: + +**Coordinates and Java version:** +```xml +com.brightsign.workshop +hello-extension +1.0.0 + + 11 + 11 + +``` + +**maven-shade-plugin:** +```xml + + org.apache.maven.plugins + maven-shade-plugin + ... + + + + com.brightsign.workshop.HelloExtension + + + + +``` +The shade plugin bundles all dependencies into a single JAR and sets `Main-Class` in the +manifest. The player has no Maven, no classpath, no internet access. The JAR must be +completely self-contained. + +**No external dependencies:** +There is no `` block. `com.sun.net.httpserver` is part of the JDK 11+ +standard library — no external HTTP framework needed. + +### 4.3 Walk HelloExtension.java + +Open `src/main/java/com/brightsign/workshop/HelloExtension.java`. Walk through each part +before building. + +**Named constants:** +```java +private static final int HTTP_PORT = 8080; +private static final String LOG_PATH = "/tmp/hello-extension.log"; +``` + +**Startup log write:** +The first thing `main` does is write one line to `/tmp/hello-extension.log`. `/tmp` is a +writable RAM disk on every player. The extension's own directory is read-only. + +**HttpServer setup:** +```java +HttpServer server = HttpServer.create(new InetSocketAddress(HTTP_PORT), 0); +server.createContext("/", exchange -> handleRoot(exchange, startMillis)); +server.start(); +``` +`com.sun.net.httpserver.HttpServer` is part of the JDK — no external HTTP library. + +**Request handler:** +```java +long uptimeSeconds = (System.currentTimeMillis() - startMillis) / 1000; +String body = "{\"message\":\"Hello from BrightSign!\",\"uptime_seconds\":" + uptimeSeconds + "}"; +``` +JSON built by hand — intentional. Zero dependencies, readable output. + +**Shutdown hook:** +```java +Runtime.getRuntime().addShutdownHook(new Thread(() -> shutDown(server))); +``` +The JVM calls shutdown hooks on SIGTERM and SIGINT. This satisfies contract item 4 +without any native signal handling. + +### 4.4 Walk bsext_init + +Open `bsext_init` at the project root. + +**DAEMON_NAME:** +```sh +DAEMON_NAME="hello_extension" +``` +Lowercase with underscores. Becomes the PID file path and appears in log messages. + +**Bundled JRE path:** +```sh +EXTENSION_DIR="/var/volatile/bsext/${DAEMON_NAME}" +JAVA_BIN="${EXTENSION_DIR}/jre/bin/java" +JAR_PATH="${EXTENSION_DIR}/hello-extension-1.0.0.jar" +``` +The extension mounts read-only at `/var/volatile/bsext/hello_extension/`. The `jre/` +directory inside is the Temurin JRE bundled during packaging. + +**exec in run_extension:** +```sh +run_extension() { + exec "${JAVA_BIN}" -jar "${JAR_PATH}" +} +``` +`exec` replaces the shell with the JVM. Signals go directly to the JVM and trigger its +shutdown hook. No shell wrapper in the process tree. + +**run (foreground mode):** +```sh +run) + run_extension + ;; +``` +Use `bsext_init run` when SSH'd into the player to see output directly. Ctrl-C to stop. + +### 4.5 Download the Bundled JRE + +The Makefile downloads Eclipse Temurin 11 JRE for `linux/aarch64` into `install/jre/`. +Run this once — it skips the download if `install/jre/` already exists. + +``` +$ make download-jre +``` +Expected: +``` +Downloading Eclipse Temurin JRE 11 for linux/aarch64... +JRE installed at install/jre +``` + +> **Note:** `install/jre/` is in `.gitignore` — do not commit it. It is re-downloaded by +> `make download-jre` on a fresh checkout. + +> **Note:** The JRE is `linux/aarch64` — it only runs on the BrightSign player (ARM64). +> Use the system `java` for the local smoke test in the next section. + +### 4.6 Build + +``` +$ make build +``` +Expected: +``` +[INFO] BUILD SUCCESS +``` + +Verify the fat JAR: +``` +$ ls -lh target/hello-extension-1.0.0.jar +``` +Expected: file present, larger than 1 KB. + +> **Note:** The JAR must be self-contained. If you add a dependency later, the shade +> plugin bundles it automatically. + +### 4.7 Local Smoke Test + +Verify the extension satisfies the contract on your workstation before touching the +player. This catches most problems before they become harder to debug on remote hardware. + +1. Start the extension: + ``` + $ java -jar target/hello-extension-1.0.0.jar & + ``` + +2. Test the endpoint: + ``` + $ curl -s http://localhost:8080/ | python3 -m json.tool + ``` + Expected: + ```json + { + "message": "Hello from BrightSign!", + "uptime_seconds": 2 + } + ``` + +3. Check the startup log: + ``` + $ cat /tmp/hello-extension.log + ``` + Expected: one line with an ISO timestamp. + +4. Stop the process: + ``` + $ kill %1 + ``` + +> **Warning:** If curl returns `Connection refused`, port 8080 is not bound. Check for +> another process: `lsof -i :8080` + +> **Tip:** `make test-local` runs steps 1–4 automatically. + +### 4.8 What You Have + +| Path | Purpose | +|---|---| +| `target/hello-extension-1.0.0.jar` | Self-contained extension JAR | +| `bsext_init` | Init script the player OS uses to start and stop the extension | +| `install/jre/` | Eclipse Temurin JRE 11 for linux/aarch64 — bundled with the extension | + +Module 5 is **identical regardless of language.** It copies whatever is in `install/` +into the squashfs image. + +Proceed to **[Module 5: Package the Extension](../05-package/README.md)**. + +--- + + +## Go + +> **Coming soon.** This section will cover building the Hello BrightSign extension in Go, +> cross-compiling for `linux/arm64`, and writing a `bsext_init` for a static binary. +> The packaging and deployment steps (Modules 5–10) are identical to the Java variant. + +--- + + +## C++ + +> **Coming soon.** This section will cover building the Hello BrightSign extension in +> C++ using CMake and the BrightSign SDK Docker container, cross-compiling for +> `linux/arm64`. The packaging and deployment steps (Modules 5–10) are identical to the +> Java variant. diff --git a/workshop/04-build/java/hello-extension/Makefile b/workshop/04-build/java/hello-extension/Makefile new file mode 100644 index 0000000..ac19674 --- /dev/null +++ b/workshop/04-build/java/hello-extension/Makefile @@ -0,0 +1,64 @@ +.DEFAULT_GOAL := help + +EXTENSION_NAME := hello_extension +JAR_NAME := hello-extension-1.0.0.jar +JAR_PATH := target/$(JAR_NAME) +INSTALL_DIR := install +JRE_DIR := $(INSTALL_DIR)/jre +COMMON_SCRIPTS := ../common-scripts + +# Eclipse Temurin 11 JRE for BrightSign (ARM64 Linux). +# This JRE is bundled inside the squashfs image — the player has no system Java. +JRE_ARCH := aarch64 +JRE_VERSION := 11 +JRE_URL := https://api.adoptium.net/v3/binary/latest/$(JRE_VERSION)/ga/linux/$(JRE_ARCH)/jre/hotspot/normal/eclipse + +.PHONY: help build download-jre package test-local clean + +help: ## Print available targets + @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*##"}; {printf " %-14s %s\n", $$1, $$2}' + +build: ## Build the fat JAR (mvn clean package) + mvn clean package -q + +download-jre: ## Download Temurin JRE 11 for linux/aarch64 into install/jre/ + @if [ -d "$(JRE_DIR)" ]; then \ + echo "JRE already present at $(JRE_DIR) — skipping download."; \ + else \ + echo "Downloading Eclipse Temurin JRE $(JRE_VERSION) for linux/$(JRE_ARCH)..."; \ + mkdir -p $(JRE_DIR); \ + wget -q --show-progress -O /tmp/jre.tar.gz "$(JRE_URL)" || \ + { echo "ERROR: JRE download failed. Check network access."; exit 1; }; \ + tar -xzf /tmp/jre.tar.gz -C $(JRE_DIR) --strip-components=1; \ + rm /tmp/jre.tar.gz; \ + echo "JRE installed at $(JRE_DIR)"; \ + fi + +package: build download-jre ## Package extension into a deployable squashfs ZIP + @if [ ! -d "$(COMMON_SCRIPTS)" ]; then \ + echo "ERROR: $(COMMON_SCRIPTS) not found."; \ + echo "Clone the extension template first:"; \ + echo " git clone https://github.com/brightsign/extension-template"; \ + echo "Then ensure common-scripts is accessible at $(COMMON_SCRIPTS)"; \ + exit 1; \ + fi + mkdir -p $(INSTALL_DIR) + cp $(JAR_PATH) $(INSTALL_DIR)/$(JAR_NAME) + cp bsext_init $(INSTALL_DIR)/bsext_init + $(COMMON_SCRIPTS)/pkg-dev.sh $(INSTALL_DIR) lvm $(EXTENSION_NAME) + +test-local: build ## Run the extension locally and verify the HTTP endpoint + @echo "Starting extension locally..."; \ + java -jar $(JAR_PATH) & EXT_PID=$$!; \ + sleep 2; \ + echo "Testing endpoint..."; \ + curl -sf http://localhost:8080/ | python3 -m json.tool; \ + CURL_STATUS=$$?; \ + echo "Stopping extension..."; \ + kill $$EXT_PID 2>/dev/null; \ + exit $$CURL_STATUS + +clean: ## Remove build artifacts, install/, and generated ZIPs + mvn clean -q + rm -rf $(INSTALL_DIR) *.zip diff --git a/workshop/04-build/java/hello-extension/bsext_init b/workshop/04-build/java/hello-extension/bsext_init new file mode 100755 index 0000000..4c5ca61 --- /dev/null +++ b/workshop/04-build/java/hello-extension/bsext_init @@ -0,0 +1,81 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: hello_extension +# Required-Start: $network +# Required-Stop: $network +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Hello BrightSign extension +### END INIT INFO + +DAEMON_NAME="hello_extension" + +# The extension is mounted read-only at EXTENSION_DIR. +# The JRE is bundled inside the squashfs image so no system Java is required. +EXTENSION_DIR="/var/volatile/bsext/${DAEMON_NAME}" +JAVA_BIN="${EXTENSION_DIR}/jre/bin/java" +JAR_PATH="${EXTENSION_DIR}/hello-extension-1.0.0.jar" +PID_FILE="/var/run/${DAEMON_NAME}.pid" +LOG_FILE="/tmp/${DAEMON_NAME}.log" + +run_extension() { + # exec replaces this shell with the JVM process. + # Signals (SIGTERM/SIGINT) go directly to the JVM and trigger its shutdown hook. + exec "${JAVA_BIN}" -jar "${JAR_PATH}" +} + +do_start() { + if [ -f "${PID_FILE}" ]; then + PID=$(cat "${PID_FILE}") + if kill -0 "${PID}" 2>/dev/null; then + echo "${DAEMON_NAME} is already running (pid ${PID})" + return 0 + fi + rm -f "${PID_FILE}" + fi + + start-stop-daemon --start \ + --background \ + --make-pidfile \ + --pidfile "${PID_FILE}" \ + --exec /bin/sh -- -c "exec ${JAVA_BIN} -jar ${JAR_PATH} >> ${LOG_FILE} 2>&1" + + echo "started ${DAEMON_NAME}" +} + +do_stop() { + if [ ! -f "${PID_FILE}" ]; then + echo "${DAEMON_NAME} is not running" + return 0 + fi + + PID=$(cat "${PID_FILE}") + if kill -TERM "${PID}" 2>/dev/null; then + echo "stopped ${DAEMON_NAME} (pid ${PID})" + else + echo "failed to stop ${DAEMON_NAME} (pid ${PID})" + fi + rm -f "${PID_FILE}" +} + +case "$1" in + start) + do_start + ;; + stop) + do_stop + ;; + restart) + do_stop + sleep 1 + do_start + ;; + run) + # Run in the foreground — use this when SSH'd into the player to see output directly. + run_extension + ;; + *) + echo "Usage: $0 {start|stop|restart|run}" + exit 1 + ;; +esac diff --git a/workshop/04-build/java/hello-extension/pom.xml b/workshop/04-build/java/hello-extension/pom.xml new file mode 100644 index 0000000..abea9cf --- /dev/null +++ b/workshop/04-build/java/hello-extension/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + com.brightsign.workshop + hello-extension + 1.0.0 + jar + + + 11 + 11 + UTF-8 + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + com.brightsign.workshop.HelloExtension + + + + false + + + + + + + diff --git a/workshop/04-build/java/hello-extension/src/main/java/com/brightsign/workshop/HelloExtension.java b/workshop/04-build/java/hello-extension/src/main/java/com/brightsign/workshop/HelloExtension.java new file mode 100644 index 0000000..3e715ac --- /dev/null +++ b/workshop/04-build/java/hello-extension/src/main/java/com/brightsign/workshop/HelloExtension.java @@ -0,0 +1,69 @@ +package com.brightsign.workshop; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.Instant; +import java.time.format.DateTimeFormatter; + +public class HelloExtension { + + private static final int HTTP_PORT = 8080; + private static final String LOG_PATH = "/tmp/hello-extension.log"; + private static final String CONTENT_TYPE_JSON = "application/json"; + private static final int HTTP_OK = 200; + + public static void main(String[] args) throws IOException { + long startMillis = System.currentTimeMillis(); + + writeStartupLog(); + + HttpServer server = HttpServer.create(new InetSocketAddress(HTTP_PORT), 0); + server.createContext("/", exchange -> handleRoot(exchange, startMillis)); + server.start(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> shutDown(server))); + } + + private static void handleRoot(HttpExchange exchange, long startMillis) throws IOException { + long uptimeSeconds = (System.currentTimeMillis() - startMillis) / 1000; + String body = "{\"message\":\"Hello from BrightSign!\",\"uptime_seconds\":" + uptimeSeconds + "}"; + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + + exchange.getResponseHeaders().set("Content-Type", CONTENT_TYPE_JSON); + exchange.sendResponseHeaders(HTTP_OK, bytes.length); + + try (OutputStream out = exchange.getResponseBody()) { + out.write(bytes); + } + } + + private static void writeStartupLog() { + String timestamp = DateTimeFormatter.ISO_INSTANT.format(Instant.now()); + String line = "hello-extension started at " + timestamp + System.lineSeparator(); + try { + Files.write( + Paths.get(LOG_PATH), + line.getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, + StandardOpenOption.APPEND + ); + } catch (IOException e) { + // /tmp is writable on the player; log write failure is non-fatal but worth surfacing + System.err.println("failed to write startup log: " + e.getMessage()); + } + } + + private static void shutDown(HttpServer server) { + System.err.println("hello-extension stopping"); + // Delay of 0 means stop immediately, rejecting queued requests + server.stop(0); + } +} diff --git a/workshop/05-package/README.md b/workshop/05-package/README.md new file mode 100644 index 0000000..7d4d7cf --- /dev/null +++ b/workshop/05-package/README.md @@ -0,0 +1,139 @@ + + +# Module 5: Package the Extension + +**Duration:** 15 minutes + +**Learning Objectives:** +- Produce a deployable extension package from any binary +- Understand what squashfs packaging means for extensions +- Validate the package before deploying + +**Prerequisites:** Module 4 complete. Binary/JAR and bsext_init in hand. + +--- + +## 5.1 What Packaging Produces + +The extension is not deployed as a raw JAR or binary. The packaging step produces three artifacts: + +1. A squashfs read-only filesystem image — your binary and bsext_init, compressed into a single file +2. An installation shell script that writes the squashfs image to an LVM logical volume on the player +3. A ZIP containing both, for transfer to the player + +The `common-scripts/` tools in the extension-template handle all of this. You do not write squashfs commands or LVM commands by hand. + +The player mounts the squashfs volume at boot. Your binary runs from that mount point. The filesystem is read-only at runtime; write to `/var/volatile/` for any runtime state. + +> **Note (Java variant):** The Java extension package includes a bundled JRE (~45MB extracted). The resulting squashfs image will be approximately 50–60MB — well under the 100MB limit. The packaging process is the same; the only difference is the size of `install/`. + +--- + +## 5.2 Copy the Packaging Scripts + +The `common-scripts/` directory lives in the cloned extension-template. Copy it to your workshop directory: + +``` +$ cp -r ~/extension-template/examples/common-scripts ~/workshop/ +``` + +Verify the scripts are present: + +``` +$ ls ~/workshop/common-scripts/ +``` + +Expected output: + +``` +make-extension-lvm make-extension-ubi pkg-dev.sh +``` + +> **Note:** `make-extension-lvm` targets eMMC-based players (Series 5 and later). `make-extension-ubi` targets NAND-based players. This workshop uses `lvm`. The `pkg-dev.sh` script wraps the correct one based on the argument you pass. + +--- + +## 5.3 Create the install/ Directory + +The packaging script reads from an `install/` directory you provide. Everything in that directory becomes the squashfs image. + +``` +$ cd ~/workshop/hello-extension +$ mkdir -p install +$ cp target/hello-extension-1.0.0.jar install/ +$ cp bsext_init install/ +$ ls install/ +``` + +Expected output: + +``` +bsext_init hello-extension-1.0.0.jar +``` + +> **Warning:** Do not place files in `install/` that should not be on the player. The squashfs image is a snapshot of that directory. Credentials, test configs, and build artifacts have no place there. + +--- + +## 5.4 Run the Packaging Script + +Run `pkg-dev.sh` from inside your extension directory, passing: the install directory, the packaging type (`lvm`), and the extension name: + +``` +$ ../common-scripts/pkg-dev.sh install lvm hello_extension +``` + +Expected output: + +``` +Parallel mksquashfs: Using 4 processors +Creating 4.0 filesystem on ext_hello_extension.squashfs, block size 131072. +[=======================================] 2/2 100% + +hello_extension-20260321-143022.zip +``` + +Verify the ZIP and its contents: + +``` +$ ls -lh hello_extension-*.zip +$ unzip -l hello_extension-*.zip +``` + +Expected ZIP contents: + +``` + Length Date Time Name +--------- ---------- ----- ---- + 131072 2026-03-21 14:30 ext_hello_extension.squashfs + 2048 2026-03-21 14:30 ext_hello_extension_install-lvm.sh +--------- ------- + 133120 2 files +``` + +> **Note:** `squashfs-tools` must be installed on your workstation to provide `mksquashfs`. Install it with `sudo apt install squashfs-tools` on Debian/Ubuntu, or run the packaging step inside Docker if your workstation does not have it. + +> **Warning:** The `DAEMON_NAME` variable inside `bsext_init` must match the extension name argument you pass to `pkg-dev.sh`. If they differ, the extension will not start on the player. Check now: `grep DAEMON_NAME install/bsext_init` + +--- + +## 5.5 Inspect the Install Script + +Look at the generated install script before transferring it to the player: + +``` +$ unzip -p hello_extension-*.zip ext_hello_extension_install-lvm.sh | head -40 +``` + +The script performs these steps at install time: + +- Verifies the SHA256 checksum of the squashfs image +- Creates an LVM logical volume named `hello_extension` +- Writes the squashfs image directly to the logical volume +- At next boot, the player mounts the volume at `/var/volatile/bsext/hello_extension/` + +> **Note:** You never edit this script. It is generated by `make-extension-lvm`. If you rebuild the squashfs image, always regenerate the ZIP with `pkg-dev.sh` — the checksum embedded in the install script must match the squashfs image it accompanies. + +--- + +**Next:** [Module 6 — Deploy to the Player](../06-deploy/README.md) diff --git a/workshop/06-deploy/README.md b/workshop/06-deploy/README.md new file mode 100644 index 0000000..e444d48 --- /dev/null +++ b/workshop/06-deploy/README.md @@ -0,0 +1,123 @@ + + +# Module 6: Deploy to the Player + +**Duration:** 20 minutes + +**Learning Objectives:** +- Transfer and install the extension package on a live player +- Start the extension and confirm it is running + +**Prerequisites:** Module 5 complete. Extension ZIP in `~/workshop/hello-extension/`. + +--- + +## 6.1 Confirm Your Environment + +Your `PLAYER_IP` variable was set in Module 1. Confirm it is still set: + +``` +$ echo $PLAYER_IP +``` + +Expected: an IP address on the local network (e.g., `192.168.1.42`). + +> **Warning:** If `PLAYER_IP` is empty, return to Module 1 and set it before continuing. Every command in this module depends on it. + +Confirm the extension ZIP is present: + +``` +$ ls ~/workshop/hello-extension/hello_extension-*.zip +``` + +Note the filename. You will reference it by the glob pattern `hello_extension-*.zip` throughout this module. + +--- + +## 6.2 Copy the ZIP to the Player + +``` +$ scp ~/workshop/hello-extension/hello_extension-*.zip admin@$PLAYER_IP:/usr/local/ +``` + +Enter the SSH password when prompted. The default password is shown on the facilitator's screen or on the player's front display. + +Expected: a progress bar that completes without error. + +> **Warning:** If `scp` fails with "Connection refused", SSH is not enabled on the player. Ask your facilitator to enable SSH via the BrightSign Network or the player's local web interface before continuing. + +--- + +## 6.3 SSH into the Player + +``` +$ ssh admin@$PLAYER_IP +``` + +You are now on the player's BusyBox Linux shell. The prompt will look like: + +``` +BrightSign:/# +``` + +All commands in sections 6.4 and 6.5 run on the player, not your workstation. + +--- + +## 6.4 Install the Extension + +Change to `/usr/local/`, unzip the package, and run the install script: + +``` +# cd /usr/local +# ls hello_extension-*.zip +# unzip hello_extension-TIMESTAMP.zip +# bash ext_hello_extension_install-lvm.sh +``` + +Replace `TIMESTAMP` with the actual timestamp in the filename from the previous `ls`. + +Expected output from the install script: + +``` +Verifying checksum... OK +Creating logical volume hello_extension... +Writing squashfs image... +Installation complete. Reboot to activate. +``` + +> **Warning:** If the checksum verification step prints `FAILED`, the ZIP was corrupted during transfer. Exit the SSH session, re-run the `scp` command from section 6.2, unzip again, and retry the install script. + +--- + +## 6.5 Reboot the Player + +``` +# reboot +``` + +Exit the SSH session. Wait 60–90 seconds for the player to complete its boot sequence and initialize the BrightSign runtime APIs. + +> **Note:** The 60–90 second wait is not arbitrary. BrightSign runtime APIs must initialize before `bsext_init` signals the extension to start. The `do_start()` function in `bsext_init` checks an autostart registry key; this check runs only after the runtime is ready. On the first boot after install, the extension starts automatically. + +--- + +## 6.6 Verify the Extension is Running + +After the player has rebooted, reconnect and check the process list: + +``` +$ ssh admin@$PLAYER_IP +# ps aux | grep hello_extension +``` + +Expected: a Java process running `hello-extension-1.0.0.jar` appears in the output. + +> **Tip:** If the process is not in the list, check the player syslog for messages from the init script: +> ``` +> # logread | grep hello_extension +> ``` + +--- + +**Next:** [Module 7 — Verify the Extension](../07-verify/README.md) diff --git a/workshop/07-verify/README.md b/workshop/07-verify/README.md new file mode 100644 index 0000000..8cb9f00 --- /dev/null +++ b/workshop/07-verify/README.md @@ -0,0 +1,98 @@ +# Module 7: Verify the Extension + +**Duration:** 15 minutes + +**Learning Objectives:** +- Confirm the extension is responding on port 8080 +- Read extension logs from the player +- Know the two verification paths: network and SSH + +**Prerequisites:** Module 6 complete. Extension installed and player rebooted. + +--- + +## 7.1 Test the HTTP Endpoint + +Run this from your workstation, not from inside the player SSH session: + +``` +$ curl -s http://$PLAYER_IP:8080/ | python3 -m json.tool +``` + +Expected output: + +```json +{ + "message": "Hello from BrightSign!", + "uptime_seconds": 47 +} +``` + +If you see this response, the extension is running and reachable from the network. + +> **Warning:** If `curl` times out or returns a connection error, the extension process may have failed to start. Skip to section 7.3 for the debug path before spending time on other steps. + +--- + +## 7.2 Read the Startup Log + +``` +$ ssh admin@$PLAYER_IP "cat /tmp/hello-extension.log" +``` + +Expected: one line containing an ISO 8601 timestamp from when the extension process started. + +> **Note:** `/tmp` is a RAM disk on BrightSign players. Its contents are lost on every reboot. If you need logs to survive a reboot, write them to `/var/volatile/` instead. `/var/volatile/` persists across reboots but is cleared on factory reset. + +--- + +## 7.3 Read Extension Process Logs (Debug Path) + +If the extension is not responding to HTTP requests, work through these steps in order. + +**Step 1.** Check whether the process is running: + +``` +$ ssh admin@$PLAYER_IP "ps aux | grep java" +``` + +**Step 2.** Check the syslog for messages from the init script: + +``` +$ ssh admin@$PLAYER_IP "logread | grep hello_extension" +``` + +**Step 3.** Check whether another process has claimed port 8080: + +``` +$ ssh admin@$PLAYER_IP "netstat -tlnp | grep 8080" +``` + +**Step 4.** Run the extension in the foreground to see its output directly: + +``` +$ ssh admin@$PLAYER_IP +# /var/volatile/bsext/hello_extension/bsext_init run +``` + +Watch the output. Press Ctrl+C to stop. + +> **Note:** `bsext_init run` launches the extension process attached to the terminal instead of daemonizing it. Any stdout/stderr from your extension appears here. This is the fastest way to see startup errors. + +--- + +## 7.4 Confirm the Uptime Counter + +Wait 60 seconds, then curl the endpoint again: + +``` +$ curl -s http://$PLAYER_IP:8080/ | python3 -m json.tool +``` + +Expected: `uptime_seconds` is higher than the value you saw in section 7.1. + +If `uptime_seconds` is increasing, the extension is alive, its internal state is updating, and the HTTP server is responding to each request independently. This is your baseline for confirming the extension is healthy after every future redeploy. + +--- + +**Next:** [Module 8 — Iterate: Change and Redeploy](../08-iterate/README.md) diff --git a/workshop/08-iterate/README.md b/workshop/08-iterate/README.md new file mode 100644 index 0000000..d7e0d0f --- /dev/null +++ b/workshop/08-iterate/README.md @@ -0,0 +1,177 @@ + + +# Module 8: Iterate — Change and Redeploy + +**Duration:** 20 minutes + +**Learning Objectives:** +- Execute the full change → rebuild → repackage → redeploy cycle +- Know the stop and uninstall steps required before reinstalling + +**Prerequisites:** Module 7 complete. Extension running on player. + +--- + +## 8.1 Make a Code Change + +On your workstation, open `~/workshop/hello-extension/src/main/java/com/example/HelloExtension.java`. + +Change the message constant: + +From: + +```java +private static final String MESSAGE = "Hello from BrightSign!"; +``` + +To: + +```java +private static final String MESSAGE = "Hello from BrightSign! (v2)"; +``` + +Save the file. + +> **Tip:** If you finish early: add a `/health` endpoint that returns `{"status":"ok"}`, and add a `request_count` field to the root response that increments on each request. + +--- + +## 8.2 Rebuild + +``` +$ cd ~/workshop/hello-extension +$ mvn clean package -q +``` + +Expected: no output (the `-q` flag suppresses Maven's progress lines). A non-zero exit code means a compile error — fix it before continuing. + +--- + +## 8.3 Repackage + +Remove the old artifacts, copy the new JAR into `install/`, and produce a new ZIP: + +``` +$ rm -f install/hello-extension-*.jar +$ cp target/hello-extension-1.0.0.jar install/ +$ rm -f hello_extension-*.zip +$ ../common-scripts/pkg-dev.sh install lvm hello_extension +``` + +The new ZIP will have a later timestamp in its filename. Confirm: + +``` +$ ls -lh hello_extension-*.zip +``` + +--- + +## 8.4 Stop and Uninstall the Old Extension + +> **Warning:** You must stop the running extension and remove the old LVM volume before installing a new version. The install script creates the LVM volume by name. If a volume with that name already exists, the script fails. Do not skip this step. + +SSH into the player: + +``` +$ ssh admin@$PLAYER_IP +``` + +Stop the extension process: + +``` +# /var/volatile/bsext/hello_extension/bsext_init stop +``` + +Remove the LVM volume: + +``` +# /usr/local/ext_hello_extension_install-lvm.sh uninstall +``` + +Expected: the install script confirms the volume was removed. You can now log out or leave the session open for the next step. + +> **Note:** The install script accepts an `uninstall` argument that reverses the installation: it unmounts the squashfs volume and removes the LVM logical volume. The `bsext_init stop` call before uninstall is required because the volume cannot be removed while it is mounted. + +--- + +## 8.5 Copy and Reinstall + +From your workstation, transfer the new ZIP: + +``` +$ scp hello_extension-*.zip admin@$PLAYER_IP:/usr/local/ +``` + +On the player, unzip and install: + +``` +# cd /usr/local +# unzip hello_extension-NEWTIMESTAMP.zip +# bash ext_hello_extension_install-lvm.sh +``` + +Replace `NEWTIMESTAMP` with the timestamp in the new ZIP filename. + +Expected install output: + +``` +Verifying checksum... OK +Creating logical volume hello_extension... +Writing squashfs image... +Installation complete. Reboot to activate. +``` + +Reboot: + +``` +# reboot +``` + +Wait 60–90 seconds. + +--- + +## 8.6 Verify the Change + +After reboot: + +``` +$ curl -s http://$PLAYER_IP:8080/ | python3 -m json.tool +``` + +Expected: + +```json +{ + "message": "Hello from BrightSign! (v2)", + "uptime_seconds": 12 +} +``` + +The updated message confirms the new squashfs image is mounted and running. + +--- + +## 8.7 The Deploy Loop + +This is the complete cycle for every change. Write it down. Steps 5–10 are identical regardless of language or framework. + +```mermaid +flowchart TD + A["1. Edit source code"] --> B["2. Rebuild\nmvn clean package"] + B --> C["3. Update install/\ncopy new JAR or binary"] + C --> D["4. Repackage\npkg-dev.sh → new ZIP"] + D --> E["5. scp ZIP to player"] + E --> F["6. SSH: bsext_init stop"] + F --> G["7. SSH: uninstall old version"] + G --> H["8. SSH: unzip + install"] + H --> I["9. SSH: reboot"] + I --> J["10. curl to verify"] + J -.->|"make another change"| A +``` + +> **Note:** Steps 2–4 change per language: Go uses `go build`, C++ uses `make`, and so on. Steps 5–10 are the same for every extension. The squashfs packaging and LVM deployment mechanism does not care what runtime is inside the image. + +--- + +**Workshop complete.** If you are continuing to the production module, see [Module 10 — Production Considerations](../10-production/README.md). diff --git a/workshop/09-html-app/README.md b/workshop/09-html-app/README.md new file mode 100644 index 0000000..1967fb9 --- /dev/null +++ b/workshop/09-html-app/README.md @@ -0,0 +1,243 @@ + + +# Module 9: The HTML App + +**Duration:** 30 minutes +**Learning Objectives:** +- Build a BrightSign HTML application that communicates with the extension +- Understand how autorun.brs bootstraps the HTML layer +- Deploy the HTML app via SD card + +**Prerequisites:** Module 8 complete. Extension running on player. Node 14.x available (pre-installed in the workshop container). + +--- + +## 9.1 How the HTML App Fits In + +The extension you built in earlier modules exposes an HTTP API on port 8080. The HTML app is a separate piece of content that runs alongside the extension on the same player. It polls that API and renders the result on screen. + +Updated architecture: + +```mermaid +graph TB + subgraph player["BrightSign Player"] + ext["Extension Process\nport 8080\nJava · Go · C++\n\nGET / → {message, uptime_seconds}"] + subgraph html["HTML App — this module"] + brs["autorun.brs\nBrightScript bootstrap"] + bundle["dist/index.html\n+ dist/bundle.js"] + brs --> bundle + end + ext -->|"fetch every 1s"| bundle + end +``` + +The HTML app lives in its own repository, separate from the extension repo. + +--- + +## 9.2 Clone the HTML App + +``` +$ cd /workspace +$ git clone https://github.com/BrightSign-Playground/bs-extension-workshop-html-app +$ cd bs-extension-workshop-html-app +``` + +Verify contents: +``` +$ ls +``` +Expected: `src/`, `package.json`, `webpack.config.js`, `Makefile`, `README.md` + +--- + +## 9.3 Project Structure + +``` +$ find . -type f | sort +``` + +Expected output: + +``` +./Makefile +./package.json +./webpack.config.js +./src/autorun.brs +./src/index.html +./src/index.js +``` + +Walk each file: + +- `autorun.brs`: BrightScript entry point. Creates a message port, creates an HTML widget, loads `dist/index.html` from the SD card, enables SSH and inspector. +- `src/index.html`: Minimal HTML shell. Loads `bundle.js` and calls `window.main()`. +- `src/index.js`: The fetch loop — polls `localhost:8080` every second, updates DOM elements. +- `webpack.config.js`: `target: node`, externalizes `@brightsign/*` packages. +- `Makefile`: `prep` / `build` / `publish` / `clean` targets. + +> **Note:** `target: node` in `webpack.config.js` is correct and intentional. BrightSign's JavaScript runtime is Node-compatible, not a browser. This setting allows use of Node APIs and correctly externalizes BrightSign platform packages that are provided by the runtime, not bundled. + +--- + +## 9.4 Walk src/index.js + +The core polling loop (read through this — do not type it): + +```javascript +const EXTENSION_URL = 'http://localhost:8080/'; +const POLL_INTERVAL_MS = 1000; + +async function poll() { + try { + const response = await fetch(EXTENSION_URL); + const data = await response.json(); + document.getElementById('message').textContent = data.message; + document.getElementById('uptime').textContent = `Uptime: ${data.uptime_seconds}s`; + } catch (e) { + document.getElementById('message').textContent = 'Extension not responding'; + } +} + +window.main = function() { + setInterval(poll, POLL_INTERVAL_MS); + poll(); +}; +``` + +> **Note:** `fetch` is available in the BrightSign JS runtime. No polyfill needed. + +`window.main` is the entry point called by `index.html` after the page loads. Starting the interval there — rather than at module load time — ensures the DOM is ready before the first poll fires. + +--- + +## 9.5 Walk src/autorun.brs + +The BrightScript bootstrap structure: + +```brightscript +Sub Main() + msgPort = CreateObject("roMessagePort") + html = CreateObject("roHtmlWidget", ...) + html.LoadURL("file:///sd:/dist/index.html") + ' event loop + While True + msg = Wait(0, msgPort) + End While +End Sub +``` + +> **Note:** `autorun.brs` is the entry point for ALL BrightSign content — not just HTML apps. The player runs this file automatically when it boots with an SD card present. The `roHtmlWidget` object launches the Chromium-based renderer and loads the specified URL into it. + +The `Wait(0, msgPort)` call blocks indefinitely, keeping the BrightScript process alive and the HTML widget running. + +--- + +## 9.6 Build + +**Step 1 — Install dependencies:** + +``` +$ make prep +``` + +Expected output: npm install completes without errors. + +**Step 2 — Build the bundle:** + +``` +$ make build +``` + +Expected output: webpack produces `dist/bundle.js` and copies `dist/index.html`. + +**Step 3 — Verify the output:** + +``` +$ ls dist/ +``` + +Expected output: + +``` +bundle.js index.html +``` + +> **Warning:** If webpack exits with an error about `Cannot find module`, run `make prep` again. A missing `node_modules` directory is the most common cause. + +--- + +## 9.7 Publish to SD + +``` +$ make publish +``` + +Expected output: creates `sd/` directory with the following layout: + +``` +sd/ +├── autorun.brs +└── dist/ + ├── bundle.js + └── index.html +``` + +The `publish` target copies `autorun.brs` to the SD root and places the built assets under `sd/dist/`. This mirrors the path that `autorun.brs` loads: `file:///sd:/dist/index.html`. + +--- + +## 9.8 Deploy to SD Card + +1. Insert the SD card into your workstation. + +2. Copy `sd/` contents to the SD card root: + + ``` + $ cp -r sd/* /media/$USER/BRIGHTSIGN/ + ``` + + > **Note:** The mount path varies by OS. On macOS it is typically `/Volumes/BRIGHTSIGN`. Use your file manager to confirm the correct mount point. + +3. Eject the SD card safely before removing it: + + ``` + $ sync && sudo eject /media/$USER/BRIGHTSIGN + ``` + +4. Insert the SD card into the player's SD slot. + +5. Reboot the player. + +> **Note:** The player runs `autorun.brs` from the SD card root on boot. If both an SD card and internal storage contain `autorun.brs`, the SD card takes priority. + +--- + +## 9.9 Verify the HTML App + +After the player reboots, the display should show the message and uptime returned by the extension. + +**Step 1 — Confirm the extension is responding:** + +``` +$ curl -s http://$PLAYER_IP:8080/ | python3 -m json.tool +``` + +Expected output: + +```json +{ + "message": "Hello from BrightSign Extension", + "uptime_seconds": 42 +} +``` + +**Step 2 — Watch the display.** + +The uptime counter on screen should increment every second. If the display shows `Extension not responding`, the extension process is not running — check Module 7 to redeploy it. + +> **Tip:** Open the BrightSign inspector in a browser at `http://$PLAYER_IP:2999` to debug JavaScript errors in the HTML app. The console tab shows output from `console.log` calls and any uncaught exceptions. + +--- + +**Next:** [Module 10 — Production Considerations](../10-production/README.md) diff --git a/workshop/10-production/README.md b/workshop/10-production/README.md new file mode 100644 index 0000000..e3e15a3 --- /dev/null +++ b/workshop/10-production/README.md @@ -0,0 +1,73 @@ + + +# Module 10: Production Hardening + +**Duration:** 15 minutes + +**Learning Objectives:** +- Know the differences between dev mode and production mode +- Understand the extension signing process conceptually +- Know what must change before a production deployment + +**Prerequisites:** Module 9 complete. + +--- + +## 10.1 Dev Mode vs. Production Mode + +| Setting | Development | Production | +|---|---|---| +| Local Extensions | Enabled | Disabled | +| Unsigned extensions | Allowed | Rejected | +| Insecure Content Loading | Enabled | Disabled | +| Remote Debugging | Enabled | Disabled | +| SSH | Enabled | Disabled | +| Extension Signing | Not required | Required | + +> **Warning:** All extensions deployed in this workshop use dev mode settings. Never ship a player with these settings to a real installation. + +--- + +## 10.2 Extension Signing Overview + +Signing is handled outside the normal build process. No hands-on steps are required here — understand the flow conceptually before you need it. + +1. BrightSign signs extensions with a private key. +2. Players in production mode verify the signature before installing — unsigned ZIPs are rejected at install time. +3. The signing workflow: + 1. Build your extension ZIP as normal (same `mvn package` + `zip` steps from Module 5). + 2. Submit the ZIP to the BrightSign signing portal, or use your own certificate if your organization is authorized. + 3. Receive a signed ZIP back. + 4. Deploy the signed ZIP to production players using the same install API from Module 6. +4. Certificate management rules: + - Private keys are never stored in version control. + - Use environment variables in CI/CD pipelines for key paths. + - Rotate certificates per BrightSign security guidelines. + +> **Note:** The signing portal URL and certificate authorization process are covered in BrightSign's internal developer documentation. Ask your BrightSign contact for access if you do not have it. + +--- + +## 10.3 Production Deployment Checklist + +Run through this checklist before deploying any extension to a production player. + +- [ ] No hardcoded credentials in extension code +- [ ] Debug logging disabled or removed +- [ ] All external HTTP calls use HTTPS +- [ ] Extension ZIP signed with valid BrightSign certificate +- [ ] Tested on a player in secure/production mode before rollout +- [ ] File I/O uses only `/tmp` and the extension's own directory +- [ ] Extension handles shutdown signal gracefully (no zombie processes) +- [ ] ZIP size under 100 MB + +> **Tip:** Add this checklist as a pull request template in your extension repository so it runs on every release. + +--- + +## 10.4 Where to Go Next + +- [BrightSign Developer Documentation](https://brightsign.atlassian.net/wiki/spaces/DOC/overview) — official reference for the player API, manifest fields, and signing +- [extension-template](https://github.com/brightsign/extension-template) — the starter repo used in this workshop; keep it as your baseline for new extensions +- [brightsign-npu-gaze-extension](https://github.com/brightsign/brightsign-npu-gaze-extension) — a more complex real-world example using hardware inference and an HTML frontend +- BrightSign Developer Community — ask your facilitator for the current forum or Slack channel link diff --git a/workshop/cleanup/README.md b/workshop/cleanup/README.md new file mode 100644 index 0000000..526df7a --- /dev/null +++ b/workshop/cleanup/README.md @@ -0,0 +1,58 @@ +# Cleanup + +**Duration:** 5 minutes + +--- + +## Stop and Remove the Extension + +1. Stop the extension: + + ```bash + curl -X POST http://$PLAYER_IP:8008/api/v1/extensions/hello-extension/stop + ``` + +2. Uninstall the extension: + + ```bash + curl -X DELETE http://$PLAYER_IP:8008/api/v1/extensions/hello-extension + ``` + +3. Verify the extension is gone: + + ```bash + curl -s http://$PLAYER_IP:8008/api/v1/extensions + ``` + + Expected response: + + ```json + [] + ``` + +--- + +## Restore Player Settings + +If the player is shared or will be used in a demo, restore its settings before handing it back. + +1. Open `http://$PLAYER_IP` in a browser. +2. Navigate to **Settings → Developer Options**. +3. Disable **Local Extensions**. +4. Disable **Insecure Content Loading**. +5. Click **Save** / **Apply**. + +> **Note:** Skip this step if the facilitator has told you to leave the player in dev mode for the next group. + +--- + +## What You Built + +Over the course of this workshop you: + +- Built a working BrightSign extension that runs a real HTTP server inside a sandboxed Java process on the player. +- Walked the complete workflow: build → package → deploy → verify → iterate. +- Built an HTML app that communicates with the extension over the player's local network. +- Learned the production hardening steps required before any extension ships. + +The [extension-template](https://github.com/brightsign/extension-template) repo is your starting point for any future extension. The same workflow you practiced here applies to any program you package into it.