From d07f4145845dc26f501d7e1c9a0ee863602c2830 Mon Sep 17 00:00:00 2001 From: Greg Herlein Date: Sat, 21 Mar 2026 06:49:59 -0700 Subject: [PATCH 1/8] interim commit --- .gitignore | 30 +++++ CLAUDE.md | 334 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 364 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a24bf8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# General +.env +.envrc +*~ + +# Build artifacts +bin/ +target/ +*.jar +*.zip +*.war + +# 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..4b0f31b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,334 @@ +# 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). + +--- + +## 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 is developed in a sibling repo (name TBD, e.g. +`bs-workshop-html-app`). + +### 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-workshop-html-app/ +├── 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. + +Each language variant lives in `workshop/04-build-/`. 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 the HTML app repo. `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) +├── workshop/ +│ ├── 00-introduction/ +│ │ └── README.md +│ ├── 01-environment-setup/ +│ │ └── README.md +│ ├── 02-understand-template/ +│ │ └── README.md +│ ├── 03-player-api/ +│ │ └── README.md +│ ├── 04-build-java/ ← first language variant +│ │ ├── README.md +│ │ └── hello-extension/ ← Maven project +│ ├── 04-build-go/ ← future +│ ├── 04-build-cpp/ ← future +│ ├── 05-package/ +│ │ └── README.md +│ ├── 06-deploy/ +│ │ └── README.md +│ ├── 07-verify/ +│ │ └── README.md +│ ├── 08-iterate/ +│ │ └── README.md +│ ├── 09-html-app/ +│ │ └── README.md ← points to companion HTML repo +│ ├── 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 + +- [ ] Module 0: Introduction +- [ ] Module 1: Environment Setup +- [ ] Module 2: Understand Template +- [ ] Module 3: Player API +- [ ] Module 4 (Java): Build Extension + Maven project +- [ ] Module 5: Package +- [ ] Module 6: Deploy +- [ ] Module 7: Verify +- [ ] Module 8: Iterate +- [ ] Module 9: HTML App + companion repo scaffold +- [ ] Module 10: Production +- [ ] Facilitator Guide +- [ ] Makefile (extension repo) From e920dce3ade02b1d55332c6292d67aa86719d8c5 Mon Sep 17 00:00:00 2001 From: Greg Herlein Date: Sat, 21 Mar 2026 07:10:58 -0700 Subject: [PATCH 2/8] interim commit --- .gitmodules | 3 + CLAUDE.md | 181 +++++++++++-- Makefile | 47 ++++ docker/Dockerfile | 58 ++++ facilitator-guide/README.md | 113 ++++++++ workshop/00-introduction/README.md | 142 ++++++++++ workshop/01-environment-setup/README.md | 218 ++++++++++++++++ workshop/02-understand-template/README.md | 198 ++++++++++++++ workshop/03-player-api/README.md | 146 +++++++++++ workshop/04-build-java/README.md | 241 +++++++++++++++++ .../04-build-java/hello-extension/Makefile | 37 +++ .../04-build-java/hello-extension/bsext_init | 75 ++++++ .../04-build-java/hello-extension/pom.xml | 46 ++++ .../brightsign/workshop/HelloExtension.java | 69 +++++ workshop/05-package/README.md | 137 ++++++++++ workshop/06-deploy/README.md | 123 +++++++++ workshop/07-verify/README.md | 98 +++++++ workshop/08-iterate/README.md | 176 +++++++++++++ workshop/09-html-app/README.md | 247 ++++++++++++++++++ workshop/10-production/README.md | 73 ++++++ workshop/cleanup/README.md | 58 ++++ 21 files changed, 2462 insertions(+), 24 deletions(-) create mode 100644 .gitmodules create mode 100644 Makefile create mode 100644 docker/Dockerfile create mode 100644 facilitator-guide/README.md create mode 100644 workshop/00-introduction/README.md create mode 100644 workshop/01-environment-setup/README.md create mode 100644 workshop/02-understand-template/README.md create mode 100644 workshop/03-player-api/README.md create mode 100644 workshop/04-build-java/README.md create mode 100644 workshop/04-build-java/hello-extension/Makefile create mode 100755 workshop/04-build-java/hello-extension/bsext_init create mode 100644 workshop/04-build-java/hello-extension/pom.xml create mode 100644 workshop/04-build-java/hello-extension/src/main/java/com/brightsign/workshop/HelloExtension.java create mode 100644 workshop/05-package/README.md create mode 100644 workshop/06-deploy/README.md create mode 100644 workshop/07-verify/README.md create mode 100644 workshop/08-iterate/README.md create mode 100644 workshop/09-html-app/README.md create mode 100644 workshop/10-production/README.md create mode 100644 workshop/cleanup/README.md diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7db39bc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "workshop/html-app"] + path = workshop/html-app + url = https://github.com/BrightSign-Playground/bs-extension-workshop-html-app diff --git a/CLAUDE.md b/CLAUDE.md index 4b0f31b..1762553 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,122 @@ the extension. See "Companion HTML App" section below. --- +## 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 | Wired network with player and workstation on same subnet | +| 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 --recurse-submodules https://github.com/BrightSign-Playground/bs-extension-workshop +$ cd bs-extension-workshop +``` + +`--recurse-submodules` initializes `workshop/html-app/` automatically. + +If you cloned without the flag: +``` +$ git submodule update --init --recursive +``` + +### 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 @@ -70,11 +186,20 @@ the workshop — which is the right problem to have. --- -## Companion HTML App (Separate Repo) +## Companion HTML App (Git Submodule) A standalone BrightSign HTML application that runs on the same player and interacts with -the extension. It is developed in a sibling repo (name TBD, e.g. -`bs-workshop-html-app`). +the extension. It lives as a git submodule in `workshop/html-app/`, pointing to: +https://github.com/BrightSign-Playground/bs-extension-workshop-html-app + +Active development happens in the submodule. After cloning this repo, initialize it: +``` +git submodule update --init --recursive +``` + +Changes to the HTML app are committed and pushed inside `workshop/html-app/` against the +submodule's own repo. Changes to the submodule pointer (version bump) are committed in +this repo. ### Structural model: simple-gaze-detection-html @@ -106,14 +231,14 @@ extension process (port 8080) ↔ BrightSign JS runtime ↔ HTML UI. ### HTML app layout ``` -bs-workshop-html-app/ +workshop/html-app/ ← git submodule ├── src/ -│ ├── autorun.brs # BrightScript bootstrap -│ ├── index.html # UI template -│ └── index.js # fetch loop, DOM update +│ ├── autorun.brs # BrightScript bootstrap +│ ├── index.html # UI template +│ └── index.js # fetch loop, DOM update ├── webpack.config.js ├── package.json -├── Makefile # prep / build / publish / clean +├── Makefile # prep / build / publish / clean └── README.md ``` @@ -227,9 +352,12 @@ Learning objective: Know what changes before shipping. ``` / ├── CLAUDE.md ← this file +├── .gitmodules ← declares workshop/html-app submodule ├── 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 @@ -239,9 +367,9 @@ Learning objective: Know what changes before shipping. │ │ └── README.md │ ├── 03-player-api/ │ │ └── README.md -│ ├── 04-build-java/ ← first language variant +│ ├── 04-build-java/ ← Java language variant │ │ ├── README.md -│ │ └── hello-extension/ ← Maven project +│ │ └── hello-extension/ ← Maven project + bsext_init │ ├── 04-build-go/ ← future │ ├── 04-build-cpp/ ← future │ ├── 05-package/ @@ -253,7 +381,8 @@ Learning objective: Know what changes before shipping. │ ├── 08-iterate/ │ │ └── README.md │ ├── 09-html-app/ -│ │ └── README.md ← points to companion HTML repo +│ │ └── README.md +│ ├── html-app/ ← git submodule (bs-extension-workshop-html-app) │ ├── 10-production/ │ │ └── README.md │ └── cleanup/ @@ -319,16 +448,20 @@ help # list targets (default) ## Current Status -- [ ] Module 0: Introduction -- [ ] Module 1: Environment Setup -- [ ] Module 2: Understand Template -- [ ] Module 3: Player API -- [ ] Module 4 (Java): Build Extension + Maven project -- [ ] Module 5: Package -- [ ] Module 6: Deploy -- [ ] Module 7: Verify -- [ ] Module 8: Iterate -- [ ] Module 9: HTML App + companion repo scaffold -- [ ] Module 10: Production -- [ ] Facilitator Guide -- [ ] Makefile (extension repo) +- [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) +- [ ] docker/Dockerfile — dev container for GHCR +- [ ] Module 1 README update — add container launch instructions (macOS + Windows) +- [ ] workshop/html-app submodule — active development (separate repo) +- [ ] Verify Java bsext_init JVM path against actual player model diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0b0dcce --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +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 package test-local clean + +## Print available targets +help: + @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*##"}; {printf " %-14s %s\n", $$1, $$2}' + +## Build the extension JAR +build: + cd $(EXTENSION_DIR) && mvn clean package -q + +## Build and produce the deployable extension ZIP +package: build + @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 + mkdir -p $(INSTALL_DIR) + cp $(EXTENSION_DIR)/target/$(JAR_NAME) $(INSTALL_DIR)/ + cp $(EXTENSION_DIR)/bsext_init $(INSTALL_DIR)/ + cd $(EXTENSION_DIR) && ../../$(COMMON_SCRIPTS)/pkg-dev.sh install lvm $(EXTENSION_NAME) + +## Build, run locally, curl the endpoint, then stop +test-local: build + java -jar $(EXTENSION_DIR)/target/$(JAR_NAME) & \ + BSX_PID=$$!; \ + sleep 2; \ + curl -sf http://localhost:8080/ ; \ + CURL_EXIT=$$?; \ + kill $$BSX_PID 2>/dev/null; \ + exit $$CURL_EXIT + +## Remove build artifacts, install directory, and ZIP files +clean: + cd $(EXTENSION_DIR) && mvn clean -q + rm -rf $(INSTALL_DIR) + rm -f $(EXTENSION_DIR)/$(EXTENSION_NAME)-*.zip 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..4854c07 --- /dev/null +++ b/facilitator-guide/README.md @@ -0,0 +1,113 @@ +# Facilitator Guide + +This guide is for BrightSign engineers running the workshop. Participants do not need to read this. + +--- + +## Pre-Workshop Setup (Day Before) + +### Players + +- [ ] One player per participant (or per pair for larger groups) +- [ ] All players on the same network as participant workstations +- [ ] Dev mode pre-enabled on all players (saves ~5 minutes per participant in Module 1) +- [ ] IP address list printed or displayed on a shared screen (one row per player) +- [ ] SSH enabled (optional but useful for Module 7 log inspection) + +### Facilitator Demo Player + +- [ ] Separate player with the finished extension and HTML app already deployed +- [ ] Used for the Module 0 demo — show it working before participants start +- [ ] Keep this player isolated; 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 | Windows JDK issues eat time; have a pre-configured VM ready | +| 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..fcdd0e5 --- /dev/null +++ b/workshop/00-introduction/README.md @@ -0,0 +1,142 @@ + + +# 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 + +``` +┌─────────────────────────────────────────────────┐ +│ BrightSign Player │ +│ │ +│ ┌─────────────────────┐ │ +│ │ Extension Process │ ← your code │ +│ │ port 8080 │ │ +│ └──────────┬──────────┘ │ +│ │ HTTP GET / │ +│ ┌──────────▼──────────┐ │ +│ │ HTML App │ (BrightSign display │ +│ │ (display layer) │ layer / BrightScript)│ +│ └─────────────────────┘ │ +│ │ +│ ┌─────────────────────┐ │ +│ │ BrightSign Control │ ← install / start / │ +│ │ API port 8008 │ stop extensions │ +│ └─────────────────────┘ │ +└─────────────────────────────────────────────────┘ + ▲ + │ curl / CI pipeline / management tool + (your workstation) +``` + +**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 + +``` +┌─────────────────────────────────────────────────┐ +│ BrightSign Player │ +│ │ +│ ┌─────────────────────────────────┐ │ +│ │ hello-brightsign (extension) │ │ +│ │ │ │ +│ │ GET / → { "uptime": "42s" } │ │ +│ │ port 8080 │ │ +│ └───────────────┬─────────────────┘ │ +│ │ fetch every 5s │ +│ ┌───────────────▼─────────────────┐ │ +│ │ index.html (HTML app) │ │ +│ │ renders uptime on screen │ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────────────────┘ + +Verification from workstation: + curl http://:8080/ + → {"uptime":"42s"} +``` + +--- + +## 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..f100710 --- /dev/null +++ b/workshop/01-environment-setup/README.md @@ -0,0 +1,218 @@ +# Module 1: Environment Setup + +**Duration:** 30 minutes +**Learning Objectives:** +- Launch the workshop development container (or verify manual tool installs) +- Connect to your BrightSign player and confirm network access +- Enable developer mode on the player +- Clone the extension template repository + +**Prerequisites:** Module 0 complete. Docker Desktop installed (macOS/Windows) or Docker Engine (Linux). + +--- + +## 1.1 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 facilitator has confirmed that tools are pre-installed on your +> workstation, skip to section 1.2. + +### 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. The container runs under + > Rosetta 2 emulation automatically. + +4. Verify tools inside the container: + ``` + $ 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 + ``` + + You are now at a shell prompt inside the container. All workshop commands run here. + +4. Verify tools: + ``` + $ java -version && mvn -version && node --version && mksquashfs -version 2>&1 | head -1 + ``` + Expected: version lines for each tool, no errors. + +> **Warning:** Do not use `cmd.exe`. Use PowerShell or Windows Terminal. The volume +> mount syntax above does not work in `cmd.exe`. + +### Linux + +1. Open a terminal. + +2. Pull and start: + ``` + $ docker run -it --rm \ + -v "$HOME/workshop:/workspace" \ + -p 8080:8080 \ + ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest + ``` + +3. Verify tools as above. + +--- + +## 1.2 Clone the Workshop Repo + +Inside the container (or on your workstation if not using the container): + +1. Navigate to the workspace directory: + ``` + $ cd /workspace + ``` + +2. Clone with submodules: + ``` + $ git clone --recurse-submodules \ + https://github.com/BrightSign-Playground/bs-extension-workshop + $ cd bs-extension-workshop + ``` + + `--recurse-submodules` initializes `workshop/html-app/` (the companion HTML app) automatically. + +3. Verify: + ``` + $ ls workshop/ + ``` + Expected: numbered module directories plus `html-app/`. + +--- + +## 1.3 Player Setup + +Each participant has a BrightSign player at their bench. + +1. Connect the power cable to the player. + +2. Connect an Ethernet cable from the player to the workshop network switch. + +3. Wait for the player to complete its boot sequence. The LED on the front panel turns solid (non-blinking) when ready. + +4. Find your player's IP address: + - **Display method:** If no content is loaded, the player shows its IP on screen during boot. + - **DHCP table method:** Check your router or network switch DHCP lease table for the player's MAC address. + - **Facilitator list:** Use the IP assigned to your bench in the workshop network map. + +5. Set an environment variable (used in every module from here on): + ``` + $ export PLAYER_IP= + ``` + + > **Tip:** Add this to your container session's history so you can re-run it if you + > restart the container. If using a persistent `/workspace` volume, save it in a + > `.envrc` file: `echo "export PLAYER_IP=" >> /workspace/.envrc` + +6. Verify network connectivity from inside the container: + ``` + $ ping -c 3 $PLAYER_IP + ``` + Expected: + ``` + 3 packets transmitted, 3 received, 0% packet loss + ``` + +7. Verify the BrightSign API is reachable: + ``` + $ curl -s http://$PLAYER_IP:8008/api/v1/info | python3 -m json.tool + ``` + Expected: JSON with player model, firmware version, serial number. + +--- + +## 1.4 Enable Developer Mode on the Player + +> **Warning:** Developer mode disables signature verification. Never apply these settings +> to a player in a public or production installation. + +1. Open a browser on your workstation (not inside the container) and navigate to: + ``` + http:// + ``` + +2. Log in to the player web interface. Default credentials are on the player's display or + in the facilitator guide. + +3. Navigate to **Settings** → **Developer Options**. + +4. Enable **Local Extensions**. + +5. Enable **Insecure Content Loading**. + +6. Click **Save** (or **Apply**). + +7. The player may reboot. Wait for the LED to return to solid, then re-verify: + ``` + $ curl -s http://$PLAYER_IP:8008/api/v1/info | python3 -m json.tool + ``` + Expected: same JSON response as step 1.3.7. + +--- + +## 1.5 Clone the Extension Template + +1. Inside the container, navigate to workspace: + ``` + $ cd /workspace + ``` + +2. Clone the template (separate from the workshop repo): + ``` + $ git clone https://github.com/brightsign/extension-template + $ cd extension-template + ``` + +3. Verify contents: + ``` + $ find . -type f | sort + ``` + Expected: tree of files including `examples/`, `common-scripts/`, `docs/`. + +> **Note:** You are cloning this template to study its structure. In Module 4 you will +> build your own extension. Do not modify these template files. + +--- + +You now have a running container, a connected player in developer mode, and the template +cloned. Proceed to **Module 2**. 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..dcafafe --- /dev/null +++ b/workshop/03-player-api/README.md @@ -0,0 +1,146 @@ + + +# 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. + +``` +BrightSign Player +├── :8008 BrightSign Control API +│ Install / start / stop / uninstall extensions +│ Query player info, system status, logs +│ Manage content +│ +└── :8080 Your Extension's Server + Whatever HTTP interface your extension exposes + Only active when your extension is running +``` + +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-java/README.md b/workshop/04-build-java/README.md new file mode 100644 index 0000000..559ed93 --- /dev/null +++ b/workshop/04-build-java/README.md @@ -0,0 +1,241 @@ + + +# Module 4: Build the Extension — Java + +**Duration:** 45 minutes +**Language:** Java 11+ (Maven) +**Learning Objectives:** +- Build a self-contained JAR that satisfies the extension contract +- Write a bsext_init script for a Java extension +- Verify the extension works locally before deploying to the player + +**Prerequisites:** Modules 1–3 complete. JDK 11+ and Maven 3.6+ verified. + +--- + +## 4.1 The Extension Contract + +Every extension — regardless of language — must satisfy this contract. Module 5 (packaging) 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 the build step places in `install/`. + +--- + +## 4.2 Create the Project + +1. Open a terminal and create a working directory: + ``` + $ mkdir -p ~/workshop/hello-extension + $ cd ~/workshop/hello-extension + ``` + +2. Copy the project files from the workshop materials: + ``` + $ cp -r /path/to/workshop/04-build-java/hello-extension/. . + ``` + +3. Verify the structure: + ``` + $ find . -type f | sort + ``` + Expected output: + ``` + ./Makefile + ./bsext_init + ./pom.xml + ./src/main/java/com/brightsign/workshop/HelloExtension.java + ``` + +### pom.xml walkthrough + +Open `pom.xml` and note three things: + +**Coordinates and Java version:** +```xml +com.brightsign.workshop +hello-extension +1.0.0 + + + 11 + 11 + +``` +Java 11 source and target. The player runs a JVM — confirm the exact version with your facilitator. + +**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 local classpath, and 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. We do not need an external HTTP framework. + +--- + +## 4.3 Implement HelloExtension.java + +The source file is at `src/main/java/com/brightsign/workshop/HelloExtension.java`. Walk through each part before building. + +**Named constants — no magic numbers:** +```java +private static final int HTTP_PORT = 8080; +private static final String LOG_PATH = "/tmp/hello-extension.log"; +``` +Both values are referenced by the extension contract. Naming them makes the contract visible in the code. + +**Startup log write:** +The first thing `main` does (before binding the port) is write one line to `/tmp/hello-extension.log`. The path `/tmp` is used because the extension's own directory on the player is read-only. `/tmp` is a writable RAM disk available on every player. + +**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. The `startMillis` timestamp captured at the top of `main` is passed through to the handler for uptime calculation. + +**Request handler:** +```java +long uptimeSeconds = (System.currentTimeMillis() - startMillis) / 1000; +String body = "{\"message\":\"Hello from BrightSign!\",\"uptime_seconds\":" + uptimeSeconds + "}"; +``` +The JSON is built by hand — no JSON library. This is intentional: zero dependencies, zero classpath complexity, and the output is short enough that string concatenation is readable. + +**Shutdown hook:** +```java +Runtime.getRuntime().addShutdownHook(new Thread(() -> shutDown(server))); +``` +The JVM calls shutdown hooks in response to SIGTERM and SIGINT. The hook logs `"hello-extension stopping"` and calls `server.stop(0)`. This satisfies contract item 4 without any native signal handling. + +--- + +## 4.4 Write the bsext_init Script + +The player OS manages extensions using SysV-style init scripts. The `bsext_init` file is at the project root (not inside `src/`). + +**DAEMON_NAME:** +```sh +DAEMON_NAME="hello_extension" +``` +Must be lowercase with underscores. This name becomes the PID file path and appears in log messages. Dashes are not safe in all shell contexts — use underscores. + +**run_extension function:** +```sh +run_extension() { + exec "${JAVA_BIN}" -jar "${JAR_PATH}" +} +``` +`exec` replaces the shell process with the JVM process. This means the PID in the PID file is the JVM's PID, not a shell wrapper. Signals sent to that PID go directly to the JVM and trigger the shutdown hook. + +> **Warning:** The path to `java` must match the JVM location on the player. `/usr/bin/java` is the default in `bsext_init`. Verify the correct path with your facilitator before deploying. An incorrect path produces a cryptic "file not found" error that does not mention Java. + +**run target (foreground mode):** +```sh +run) + run_extension + ;; +``` +The `run` action runs the extension in the foreground with its output going directly to the terminal. Use this when SSH'd into the player to diagnose startup problems. + +--- + +## 4.5 Build + +1. From your project directory, run: + ``` + $ mvn clean package + ``` + Expected output: + ``` + [INFO] BUILD SUCCESS + ``` + +2. Verify the fat JAR was created: + ``` + $ ls -lh target/hello-extension-1.0.0.jar + ``` + Expected: a file larger than 1 KB. Even with no dependencies the shade plugin produces a JAR with a complete manifest and the class files bundled. + +> **Note:** The JAR must be self-contained. If you add a dependency in the future, the shade plugin bundles it. The player has no Maven, no classpath configuration, and no internet access — the JAR is the entire runtime. + +> **Tip:** Run `make build` instead of `mvn clean package` directly. Both do the same thing; the Makefile wraps `mvn clean package -q` to suppress verbose output. + +--- + +## 4.6 Local Smoke Test + +Verify the extension satisfies the contract on your workstation before touching the player. This step catches the majority of problems before they become harder to debug on remote hardware. + +1. Start the extension in the background: + ``` + $ java -jar target/hello-extension-1.0.0.jar & + ``` + +2. Wait two seconds for the HTTP server to bind, then send a request: + ``` + $ curl -s http://localhost:8080/ | python3 -m json.tool + ``` + Expected output: + ```json + { + "message": "Hello from BrightSign!", + "uptime_seconds": 2 + } + ``` + +3. Check the startup log: + ``` + $ cat /tmp/hello-extension.log + ``` + Expected: one line similar to: + ``` + hello-extension started at 2026-03-21T10:15:30.123Z + ``` + +4. Stop the local process: + ``` + $ kill %1 + ``` + The shutdown hook will log `hello-extension stopping` to stderr before the JVM exits. + +> **Warning:** If curl returns `Connection refused`, the HttpServer failed to bind port 8080. Check whether another process is already using that port: +> ``` +> $ lsof -i :8080 +> ``` +> Stop the conflicting process, then retry. + +> **Tip:** `make test-local` runs steps 1–4 automatically. It starts the extension, waits 3 seconds, curls the endpoint, and kills the process. + +--- + +## 4.7 What You Have + +After completing this module you have two files that Module 5 needs: + +| File | Purpose | +|---|---| +| `target/hello-extension-1.0.0.jar` | The self-contained extension binary | +| `bsext_init` | The init script the player OS uses to start and stop the extension | + +Module 5 is **identical regardless of language**. It picks up these two files from `install/`, adds `manifest.json`, and produces the ZIP that the player accepts. The packaging and deployment workflow does not change based on what is inside the JAR. + +Proceed to **[Module 5: Package the Extension](../../05-package/README.md)**. diff --git a/workshop/04-build-java/hello-extension/Makefile b/workshop/04-build-java/hello-extension/Makefile new file mode 100644 index 0000000..fb26e9f --- /dev/null +++ b/workshop/04-build-java/hello-extension/Makefile @@ -0,0 +1,37 @@ +.DEFAULT_GOAL := help + +INSTALL_DIR := install +JAR_NAME := hello-extension-1.0.0.jar +JAR_PATH := target/$(JAR_NAME) + +.PHONY: help build package test-local clean + +help: + @echo "Targets:" + @echo " build - build the fat JAR (mvn clean package)" + @echo " package - copy artifacts into install/ and create extension package" + @echo " test-local - run the extension locally and verify the HTTP endpoint" + @echo " clean - remove build artifacts, install/, and *.zip" + +build: + mvn clean package -q + +package: build + mkdir -p $(INSTALL_DIR) + cp $(JAR_PATH) $(INSTALL_DIR)/$(JAR_NAME) + cp bsext_init $(INSTALL_DIR)/bsext_init + ../../common-scripts/pkg-dev.sh install lvm hello_extension + +test-local: build + @echo "Starting extension in background..." ; \ + java -jar $(JAR_PATH) & EXT_PID=$$! ; \ + sleep 3 ; \ + echo "Sending test request..." ; \ + curl -sf http://localhost:8080/ | python3 -m json.tool ; \ + echo "Stopping extension..." ; \ + kill $$EXT_PID 2>/dev/null ; \ + true + +clean: + 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..789b5ed --- /dev/null +++ b/workshop/04-build-java/hello-extension/bsext_init @@ -0,0 +1,75 @@ +#!/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" + +# Adjust JAVA_BIN to the absolute path of the java executable on the player. +# Verify the correct path with your facilitator before deploying. +JAVA_BIN="/usr/bin/java" + +JAR_PATH="/var/volatile/bsext/${DAEMON_NAME}/hello-extension-1.0.0.jar" +PID_FILE="/var/run/${DAEMON_NAME}.pid" +LOG_FILE="/tmp/${DAEMON_NAME}.log" + +run_extension() { + exec "${JAVA_BIN}" -jar "${JAR_PATH}" +} + +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 + + run_extension >> "${LOG_FILE}" 2>&1 & + echo $! > "${PID_FILE}" + echo "started ${DAEMON_NAME} (pid $(cat "${PID_FILE}"))" +} + +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) + start + ;; + stop) + stop + ;; + restart) + stop + sleep 1 + start + ;; + run) + # Run in the foreground for interactive debugging on the player + 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..9e7fa9d --- /dev/null +++ b/workshop/05-package/README.md @@ -0,0 +1,137 @@ + + +# 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. + +--- + +## 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..64a9781 --- /dev/null +++ b/workshop/08-iterate/README.md @@ -0,0 +1,176 @@ + + +# 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. + +``` +1. Edit source code +2. mvn clean package (rebuild) +3. Update install/ directory (copy new JAR/binary) +4. pkg-dev.sh → new ZIP (repackage) +5. scp ZIP to player +6. SSH: bsext_init stop +7. SSH: install script uninstall +8. SSH: unzip + install new version +9. SSH: reboot +10. curl to verify +``` + +> **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..a1ae65a --- /dev/null +++ b/workshop/09-html-app/README.md @@ -0,0 +1,247 @@ + + +# 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: + +``` +BrightSign Player +├── Extension process (port 8080) ← your Java/Go/C++ binary +│ └── GET / → {"message":..., "uptime_seconds":N} +│ +└── HTML app (BrightScript + JS) ← this module + ├── autorun.brs — bootstrap, loads HTML widget + ├── dist/bundle.js — webpack bundle of src/index.js + └── dist/index.html + └── polls http://localhost:8080/ every 1000ms + → displays message and uptime on screen +``` + +The HTML app lives in the `workshop/html-app/` directory of this repo as a **git submodule**. It was initialized when you cloned with `--recurse-submodules` in Module 1. + +--- + +## 9.2 Navigate to the HTML App + +``` +$ cd /workspace/bs-extension-workshop/workshop/html-app +``` + +Verify it is populated: +``` +$ ls +``` +Expected: `src/`, `package.json`, `webpack.config.js`, `Makefile`, `README.md` + +> **Note:** If the directory is empty, the submodule was not initialized. Run from the +> repo root: +> ``` +> $ git submodule update --init --recursive +> ``` + +--- + +## 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. From 4a90a8d35b729748b59cacae44eef38c248a8c66 Mon Sep 17 00:00:00 2001 From: Greg Herlein Date: Sat, 21 Mar 2026 07:31:11 -0700 Subject: [PATCH 3/8] interim commit --- .github/workflows/docker-publish.yml | 70 +++++++++++++++++ .gitignore | 3 + CLAUDE.md | 7 +- Makefile | 39 ++++------ workshop/04-build-java/README.md | 67 ++++++++++++---- .../04-build-java/hello-extension/Makefile | 77 +++++++++++++------ .../04-build-java/hello-extension/bsext_init | 36 +++++---- workshop/05-package/README.md | 2 + workshop/html-app/.gitignore | 6 ++ workshop/html-app/Makefile | 27 +++++++ workshop/html-app/README.md | 76 ++++++++++++++++++ workshop/html-app/package.json | 22 ++++++ workshop/html-app/src/autorun.brs | 30 ++++++++ workshop/html-app/src/index.html | 71 +++++++++++++++++ workshop/html-app/src/index.js | 41 ++++++++++ workshop/html-app/webpack.config.js | 50 ++++++++++++ 16 files changed, 541 insertions(+), 83 deletions(-) create mode 100644 .github/workflows/docker-publish.yml create mode 100644 workshop/html-app/.gitignore create mode 100644 workshop/html-app/Makefile create mode 100644 workshop/html-app/README.md create mode 100644 workshop/html-app/package.json create mode 100644 workshop/html-app/src/autorun.brs create mode 100644 workshop/html-app/src/index.html create mode 100644 workshop/html-app/src/index.js create mode 100644 workshop/html-app/webpack.config.js diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..178b0fc --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,70 @@ +name: Publish Dev Container + +on: + push: + branches: + - main + paths: + - 'docker/Dockerfile' + - '.github/workflows/docker-publish.yml' + tags: + - 'v*' + pull_request: + branches: + - main + paths: + - 'docker/Dockerfile' + - '.github/workflows/docker-publish.yml' + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: brightsign-playground/bs-extension-workshop-devenv + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # latest on every push to main + type=raw,value=latest,enable={{is_default_branch}} + # semver tags: v1.2.3 → 1.2.3, 1.2, 1 + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + # short SHA for traceability + type=sha,prefix=sha-,format=short + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: docker/ + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 2a24bf8..b383672 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ target/ *.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 diff --git a/CLAUDE.md b/CLAUDE.md index 1762553..2f27634 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -461,7 +461,8 @@ help # list targets (default) - [x] Module 10: Production - [x] Facilitator Guide - [x] Makefile (extension repo) -- [ ] docker/Dockerfile — dev container for GHCR +- [x] docker/Dockerfile — dev container for GHCR +- [x] .github/workflows/docker-publish.yml — builds and pushes on merge to main and version tags - [ ] Module 1 README update — add container launch instructions (macOS + Windows) -- [ ] workshop/html-app submodule — active development (separate repo) -- [ ] Verify Java bsext_init JVM path against actual player model +- [x] workshop/html-app submodule — content built (autorun.brs, index.html, index.js, webpack, Makefile) +- [x] Java bsext_init — bundles Eclipse Temurin 11 JRE for linux/aarch64; no system Java required on player diff --git a/Makefile b/Makefile index 0b0dcce..6280c79 100644 --- a/Makefile +++ b/Makefile @@ -4,19 +4,19 @@ COMMON_SCRIPTS := ../extension-template/examples/common-scripts JAR_NAME := hello-extension-1.0.0.jar EXTENSION_NAME := hello_extension -.PHONY: help build package test-local clean +.PHONY: help build download-jre package test-local clean -## Print available targets -help: +help: ## Print available targets @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \ awk 'BEGIN {FS = ":.*##"}; {printf " %-14s %s\n", $$1, $$2}' -## Build the extension JAR -build: - cd $(EXTENSION_DIR) && mvn clean package -q +build: ## Build the extension JAR + $(MAKE) -C $(EXTENSION_DIR) build -## Build and produce the deployable extension ZIP -package: 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)"; \ @@ -25,23 +25,10 @@ package: build echo ""; \ exit 1; \ fi - mkdir -p $(INSTALL_DIR) - cp $(EXTENSION_DIR)/target/$(JAR_NAME) $(INSTALL_DIR)/ - cp $(EXTENSION_DIR)/bsext_init $(INSTALL_DIR)/ - cd $(EXTENSION_DIR) && ../../$(COMMON_SCRIPTS)/pkg-dev.sh install lvm $(EXTENSION_NAME) + $(MAKE) -C $(EXTENSION_DIR) package -## Build, run locally, curl the endpoint, then stop -test-local: build - java -jar $(EXTENSION_DIR)/target/$(JAR_NAME) & \ - BSX_PID=$$!; \ - sleep 2; \ - curl -sf http://localhost:8080/ ; \ - CURL_EXIT=$$?; \ - kill $$BSX_PID 2>/dev/null; \ - exit $$CURL_EXIT +test-local: build ## Build, run the extension locally, verify the HTTP endpoint, then stop + $(MAKE) -C $(EXTENSION_DIR) test-local -## Remove build artifacts, install directory, and ZIP files -clean: - cd $(EXTENSION_DIR) && mvn clean -q - rm -rf $(INSTALL_DIR) - rm -f $(EXTENSION_DIR)/$(EXTENSION_NAME)-*.zip +clean: ## Remove build artifacts, install directory, and ZIP files + $(MAKE) -C $(EXTENSION_DIR) clean diff --git a/workshop/04-build-java/README.md b/workshop/04-build-java/README.md index 559ed93..df76be4 100644 --- a/workshop/04-build-java/README.md +++ b/workshop/04-build-java/README.md @@ -9,7 +9,7 @@ - Write a bsext_init script for a Java extension - Verify the extension works locally before deploying to the player -**Prerequisites:** Modules 1–3 complete. JDK 11+ and Maven 3.6+ verified. +**Prerequisites:** Modules 1–3 complete. JDK 11+ and Maven 3.6+ verified (pre-installed in the workshop container). --- @@ -128,7 +128,39 @@ The JVM calls shutdown hooks in response to SIGTERM and SIGINT. The hook logs `" --- -## 4.4 Write the bsext_init Script +## 4.4 Download the Bundled JRE + +BrightSign players do not have a system Java installation. The JRE must be bundled inside the extension's squashfs image alongside the JAR. + +The Makefile downloads Eclipse Temurin 11 JRE for `linux/aarch64` from Adoptium and places it in `install/jre/`. This only needs to happen once — the Makefile skips the download if `install/jre/` already exists. + +1. Download the JRE: + ``` + $ make download-jre + ``` + Expected output: + ``` + Downloading Eclipse Temurin JRE 11 for linux/aarch64... + JRE installed at install/jre + ``` + This downloads ~45MB and extracts to `install/jre/`. The download requires internet access. + +2. Verify: + ``` + $ install/jre/bin/java -version + ``` + Expected: + ``` + openjdk version "11.x.x" ... + ``` + +> **Note:** `install/jre/` is excluded from version control via `.gitignore`. It is re-downloaded by `make download-jre` on a fresh checkout. Do not commit it. + +> **Note:** The JRE is `linux/aarch64` — it only runs on ARM64 hardware (the BrightSign player). Running `install/jre/bin/java` on an x86 workstation will fail. Use the system `java` command for the local smoke test in section 4.6. + +--- + +## 4.5 Write the bsext_init Script The player OS manages extensions using SysV-style init scripts. The `bsext_init` file is at the project root (not inside `src/`). @@ -138,15 +170,21 @@ DAEMON_NAME="hello_extension" ``` Must be lowercase with underscores. This name becomes the PID file path and appears in log messages. Dashes are not safe in all shell contexts — use underscores. +**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 is mounted read-only at `/var/volatile/bsext/hello_extension/`. The `jre/` directory inside that mount is the Temurin JRE bundled during packaging. + **run_extension function:** ```sh run_extension() { exec "${JAVA_BIN}" -jar "${JAR_PATH}" } ``` -`exec` replaces the shell process with the JVM process. This means the PID in the PID file is the JVM's PID, not a shell wrapper. Signals sent to that PID go directly to the JVM and trigger the shutdown hook. - -> **Warning:** The path to `java` must match the JVM location on the player. `/usr/bin/java` is the default in `bsext_init`. Verify the correct path with your facilitator before deploying. An incorrect path produces a cryptic "file not found" error that does not mention Java. +`exec` replaces the shell with the JVM process. Signals go directly to the JVM and trigger the shutdown hook. No shell wrapper remains in the process tree. **run target (foreground mode):** ```sh @@ -154,11 +192,11 @@ run) run_extension ;; ``` -The `run` action runs the extension in the foreground with its output going directly to the terminal. Use this when SSH'd into the player to diagnose startup problems. +Runs the extension in the foreground with output going directly to the terminal. Use this when SSH'd into the player to diagnose startup problems. --- -## 4.5 Build +## 4.6 Build 1. From your project directory, run: ``` @@ -181,7 +219,7 @@ The `run` action runs the extension in the foreground with its output going dire --- -## 4.6 Local Smoke Test +## 4.7 Local Smoke Test Verify the extension satisfies the contract on your workstation before touching the player. This step catches the majority of problems before they become harder to debug on remote hardware. @@ -223,19 +261,20 @@ Verify the extension satisfies the contract on your workstation before touching > ``` > Stop the conflicting process, then retry. -> **Tip:** `make test-local` runs steps 1–4 automatically. It starts the extension, waits 3 seconds, curls the endpoint, and kills the process. +> **Tip:** `make test-local` runs steps 1–4 automatically. It starts the extension, waits 2 seconds, curls the endpoint, and kills the process. Note: this uses the system `java` command, not the bundled ARM64 JRE — that is correct for local testing. --- -## 4.7 What You Have +## 4.8 What You Have -After completing this module you have two files that Module 5 needs: +After completing this module you have everything Module 5 needs: -| File | Purpose | +| Path | Purpose | |---|---| -| `target/hello-extension-1.0.0.jar` | The self-contained extension binary | +| `target/hello-extension-1.0.0.jar` | The self-contained extension JAR | | `bsext_init` | The 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 picks up these two files from `install/`, adds `manifest.json`, and produces the ZIP that the player accepts. The packaging and deployment workflow does not change based on what is inside the JAR. +Module 5 is **identical regardless of language**. It copies whatever is in `install/` into the squashfs image. The packaging and deployment workflow does not change based on what language produced the binary. Proceed to **[Module 5: Package the Extension](../../05-package/README.md)**. diff --git a/workshop/04-build-java/hello-extension/Makefile b/workshop/04-build-java/hello-extension/Makefile index fb26e9f..a9ca7b2 100644 --- a/workshop/04-build-java/hello-extension/Makefile +++ b/workshop/04-build-java/hello-extension/Makefile @@ -1,37 +1,64 @@ .DEFAULT_GOAL := help -INSTALL_DIR := install -JAR_NAME := hello-extension-1.0.0.jar -JAR_PATH := target/$(JAR_NAME) +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 -.PHONY: help build package test-local clean +# 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 -help: - @echo "Targets:" - @echo " build - build the fat JAR (mvn clean package)" - @echo " package - copy artifacts into install/ and create extension package" - @echo " test-local - run the extension locally and verify the HTTP endpoint" - @echo " clean - remove build artifacts, install/, and *.zip" +.PHONY: help build download-jre package test-local clean -build: +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 -package: build +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 lvm hello_extension - -test-local: build - @echo "Starting extension in background..." ; \ - java -jar $(JAR_PATH) & EXT_PID=$$! ; \ - sleep 3 ; \ - echo "Sending test request..." ; \ - curl -sf http://localhost:8080/ | python3 -m json.tool ; \ - echo "Stopping extension..." ; \ - kill $$EXT_PID 2>/dev/null ; \ - true - -clean: + $(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 index 789b5ed..4c5ca61 100755 --- a/workshop/04-build-java/hello-extension/bsext_init +++ b/workshop/04-build-java/hello-extension/bsext_init @@ -10,19 +10,21 @@ DAEMON_NAME="hello_extension" -# Adjust JAVA_BIN to the absolute path of the java executable on the player. -# Verify the correct path with your facilitator before deploying. -JAVA_BIN="/usr/bin/java" - -JAR_PATH="/var/volatile/bsext/${DAEMON_NAME}/hello-extension-1.0.0.jar" +# 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}" } -start() { +do_start() { if [ -f "${PID_FILE}" ]; then PID=$(cat "${PID_FILE}") if kill -0 "${PID}" 2>/dev/null; then @@ -32,12 +34,16 @@ start() { rm -f "${PID_FILE}" fi - run_extension >> "${LOG_FILE}" 2>&1 & - echo $! > "${PID_FILE}" - echo "started ${DAEMON_NAME} (pid $(cat "${PID_FILE}"))" + 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}" } -stop() { +do_stop() { if [ ! -f "${PID_FILE}" ]; then echo "${DAEMON_NAME} is not running" return 0 @@ -54,18 +60,18 @@ stop() { case "$1" in start) - start + do_start ;; stop) - stop + do_stop ;; restart) - stop + do_stop sleep 1 - start + do_start ;; run) - # Run in the foreground for interactive debugging on the player + # Run in the foreground — use this when SSH'd into the player to see output directly. run_extension ;; *) diff --git a/workshop/05-package/README.md b/workshop/05-package/README.md index 9e7fa9d..7d4d7cf 100644 --- a/workshop/05-package/README.md +++ b/workshop/05-package/README.md @@ -25,6 +25,8 @@ The `common-scripts/` tools in the extension-template handle all of this. You do 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 diff --git a/workshop/html-app/.gitignore b/workshop/html-app/.gitignore new file mode 100644 index 0000000..c6c8e12 --- /dev/null +++ b/workshop/html-app/.gitignore @@ -0,0 +1,6 @@ +.env +.envrc +*~ +node_modules/ +dist/ +sd/ diff --git a/workshop/html-app/Makefile b/workshop/html-app/Makefile new file mode 100644 index 0000000..a5c6000 --- /dev/null +++ b/workshop/html-app/Makefile @@ -0,0 +1,27 @@ +.DEFAULT_GOAL := help + +SD_DIR := sd +DIST_DIR := dist + +.PHONY: help prep build publish clean + +help: ## Print available targets + @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*##"}; {printf " %-12s %s\n", $$1, $$2}' + +prep: ## Install Node dependencies (npm install) + npm install + +build: ## Bundle src/ into dist/ via webpack + npm run build + +publish: build ## Copy dist/ and autorun.brs into sd/ for SD card deployment + mkdir -p $(SD_DIR)/dist + cp -r $(DIST_DIR)/. $(SD_DIR)/dist/ + cp src/autorun.brs $(SD_DIR)/autorun.brs + @echo "" + @echo "SD card contents ready at $(SD_DIR)/" + @echo "Copy to your SD card root: cp -r $(SD_DIR)/. /path/to/sdcard/" + +clean: ## Remove dist/, sd/, and node_modules/ + rm -rf $(DIST_DIR) $(SD_DIR) node_modules diff --git a/workshop/html-app/README.md b/workshop/html-app/README.md new file mode 100644 index 0000000..5f89262 --- /dev/null +++ b/workshop/html-app/README.md @@ -0,0 +1,76 @@ +# BrightSign Extension Workshop — HTML App + +BrightSign HTML application used in the extension workshop. Runs on a BrightSign player +alongside the Hello extension and displays its output on screen. + +This repo is used as a git submodule in +[bs-extension-workshop](https://github.com/BrightSign-Playground/bs-extension-workshop) +at `workshop/html-app/`. + +--- + +## What It Does + +Polls `http://localhost:8080/` (the Hello extension) every second and renders: +- The message string from the extension +- The extension's uptime in seconds + +If the extension is not running, it shows "Extension not responding" and retries. + +--- + +## Prerequisites + +- Node.js 14.x and npm (pre-installed in the workshop dev container) +- A BrightSign player with the Hello extension running +- An SD card for deployment + +--- + +## Build + +``` +$ make prep # npm install +$ make build # webpack → dist/ +$ make publish # copy dist/ + autorun.brs → sd/ +``` + +--- + +## Deploy + +Copy the contents of `sd/` to the root of an SD card: + +``` +$ cp -r sd/. /path/to/sdcard/ +``` + +Insert the SD card into the player and reboot. The player runs `autorun.brs` on boot, +which loads `dist/index.html` as a full-screen HTML widget. + +--- + +## Project Structure + +``` +src/ +├── autorun.brs BrightScript bootstrap: creates HTML widget, enables SSH + inspector +├── index.html UI template: message display, uptime counter +└── index.js Fetch loop: polls extension HTTP API, updates DOM +webpack.config.js Target: node (BrightSign JS runtime). Externalizes @brightsign/* packages. +package.json +Makefile prep / build / publish / clean +``` + +--- + +## Debugging + +Open Chrome DevTools and navigate to `http://:2999` to inspect the running +HTML app. SSH is also enabled on port 22. + +--- + +## License + +Apache 2.0 diff --git a/workshop/html-app/package.json b/workshop/html-app/package.json new file mode 100644 index 0000000..09bd7e9 --- /dev/null +++ b/workshop/html-app/package.json @@ -0,0 +1,22 @@ +{ + "name": "bs-extension-workshop-html-app", + "version": "1.0.0", + "description": "BrightSign HTML app for the extension workshop — polls the Hello extension and displays its output", + "private": true, + "engines": { + "node": ">=14.0.0" + }, + "scripts": { + "build": "webpack --mode=development", + "build:prod": "webpack --mode=production", + "clean": "rm -rf dist sd node_modules" + }, + "devDependencies": { + "@babel/core": "^7.23.0", + "@babel/preset-env": "^7.23.0", + "babel-loader": "^9.1.3", + "html-webpack-plugin": "^5.5.3", + "webpack": "^5.88.0", + "webpack-cli": "^5.1.4" + } +} diff --git a/workshop/html-app/src/autorun.brs b/workshop/html-app/src/autorun.brs new file mode 100644 index 0000000..3799f2b --- /dev/null +++ b/workshop/html-app/src/autorun.brs @@ -0,0 +1,30 @@ +' BrightSign HTML app bootstrap for the extension workshop. +' Loads the webpack-bundled HTML app from the SD card and displays it full-screen. + +Sub Main() + msgPort = CreateObject("roMessagePort") + + ' Full-screen rectangle at 1080p. + rect = { x: 0, y: 0, w: 1920, h: 1080 } + + config = CreateObject("roAssociativeArray") + config.port = msgPort + + html = CreateObject("roHtmlWidget", rect, config) + html.SetPort(msgPort) + + ' Enable SSH for debugging. + shell = CreateObject("roShell") + shell.EnableSSH(22) + + ' Enable the JavaScript inspector (open http://:2999 in Chrome DevTools). + html.EnableJavaScriptInspector(2999) + + ' Load the bundled app from the SD card root. + html.LoadURL("file:///sd:/dist/index.html") + + ' Event loop — keep the process alive. + While True + msg = Wait(0, msgPort) + End While +End Sub diff --git a/workshop/html-app/src/index.html b/workshop/html-app/src/index.html new file mode 100644 index 0000000..ea76bd5 --- /dev/null +++ b/workshop/html-app/src/index.html @@ -0,0 +1,71 @@ + + + + + + BrightSign Extension Workshop + + + + +
Connecting to extension…
+
+
● waiting
+ + + + diff --git a/workshop/html-app/src/index.js b/workshop/html-app/src/index.js new file mode 100644 index 0000000..841e968 --- /dev/null +++ b/workshop/html-app/src/index.js @@ -0,0 +1,41 @@ +'use strict'; + +const EXTENSION_URL = 'http://localhost:8080/'; +const POLL_INTERVAL_MS = 1000; + +function updateUI(message, uptimeSeconds) { + document.getElementById('message').textContent = message; + document.getElementById('uptime').textContent = 'Uptime: ' + uptimeSeconds + 's'; + + const status = document.getElementById('status'); + status.textContent = '● connected'; + status.className = ''; +} + +function showError() { + document.getElementById('message').textContent = 'Extension not responding'; + document.getElementById('uptime').textContent = ''; + + const status = document.getElementById('status'); + status.textContent = '● waiting for extension'; + status.className = 'error'; +} + +async function poll() { + try { + const response = await fetch(EXTENSION_URL); + if (!response.ok) { + showError(); + return; + } + const data = await response.json(); + updateUI(data.message, data.uptime_seconds); + } catch (e) { + showError(); + } +} + +window.main = function main() { + poll(); + setInterval(poll, POLL_INTERVAL_MS); +}; diff --git a/workshop/html-app/webpack.config.js b/workshop/html-app/webpack.config.js new file mode 100644 index 0000000..0e74ddd --- /dev/null +++ b/workshop/html-app/webpack.config.js @@ -0,0 +1,50 @@ +'use strict'; + +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = { + // BrightSign's JavaScript runtime is Node-compatible, not a browser. + // Target 'node' gives access to Node APIs (fs, net, etc.) without polyfills. + target: 'node', + + entry: './src/index.js', + + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'bundle.js', + }, + + // @brightsign/* packages are provided by the player's runtime — do not bundle them. + externals: { + '@brightsign/deviceinfo': 'commonjs @brightsign/deviceinfo', + '@brightsign/registry': 'commonjs @brightsign/registry', + '@brightsign/videooutput': 'commonjs @brightsign/videooutput', + }, + + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: [ + ['@babel/preset-env', { targets: { node: '14' } }] + ], + }, + }, + }, + ], + }, + + plugins: [ + new HtmlWebpackPlugin({ + template: './src/index.html', + inject: false, + }), + ], + + devtool: 'eval-source-map', +}; From 596f3625f06dcb531451eeef1a0b3236775f4334 Mon Sep 17 00:00:00 2001 From: Greg Herlein Date: Sat, 21 Mar 2026 11:23:15 -0700 Subject: [PATCH 4/8] interim commit --- .gitmodules | 3 - CLAUDE.md | 41 +++++-------- workshop/00-introduction/README.md | 59 ++++++------------- workshop/01-environment-setup/README.md | 9 +-- workshop/03-player-api/README.md | 22 +++---- workshop/08-iterate/README.md | 23 ++++---- workshop/09-html-app/README.md | 38 ++++++------- workshop/html-app/.gitignore | 6 -- workshop/html-app/Makefile | 27 --------- workshop/html-app/README.md | 76 ------------------------- workshop/html-app/package.json | 22 ------- workshop/html-app/src/autorun.brs | 30 ---------- workshop/html-app/src/index.html | 71 ----------------------- workshop/html-app/src/index.js | 41 ------------- workshop/html-app/webpack.config.js | 50 ---------------- 15 files changed, 75 insertions(+), 443 deletions(-) delete mode 100644 .gitmodules delete mode 100644 workshop/html-app/.gitignore delete mode 100644 workshop/html-app/Makefile delete mode 100644 workshop/html-app/README.md delete mode 100644 workshop/html-app/package.json delete mode 100644 workshop/html-app/src/autorun.brs delete mode 100644 workshop/html-app/src/index.html delete mode 100644 workshop/html-app/src/index.js delete mode 100644 workshop/html-app/webpack.config.js diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 7db39bc..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "workshop/html-app"] - path = workshop/html-app - url = https://github.com/BrightSign-Playground/bs-extension-workshop-html-app diff --git a/CLAUDE.md b/CLAUDE.md index 2f27634..df3f095 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,17 +119,10 @@ Once inside the container: ``` $ cd /workspace -$ git clone --recurse-submodules https://github.com/BrightSign-Playground/bs-extension-workshop +$ git clone https://github.com/BrightSign-Playground/bs-extension-workshop $ cd bs-extension-workshop ``` -`--recurse-submodules` initializes `workshop/html-app/` automatically. - -If you cloned without the flag: -``` -$ git submodule update --init --recursive -``` - ### Manual Tool Install (Fallback — No Container) If the container is unavailable, participants can install tools directly. Minimum set: @@ -186,20 +179,15 @@ the workshop — which is the right problem to have. --- -## Companion HTML App (Git Submodule) +## Companion HTML App (Separate Repo) A standalone BrightSign HTML application that runs on the same player and interacts with -the extension. It lives as a git submodule in `workshop/html-app/`, pointing to: -https://github.com/BrightSign-Playground/bs-extension-workshop-html-app +the extension. It lives in its own repository: -Active development happens in the submodule. After cloning this repo, initialize it: -``` -git submodule update --init --recursive -``` +**https://github.com/BrightSign-Playground/bs-extension-workshop-html-app** -Changes to the HTML app are committed and pushed inside `workshop/html-app/` against the -submodule's own repo. Changes to the submodule pointer (version bump) are committed in -this repo. +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 @@ -231,14 +219,14 @@ extension process (port 8080) ↔ BrightSign JS runtime ↔ HTML UI. ### HTML app layout ``` -workshop/html-app/ ← git submodule +bs-extension-workshop-html-app/ ← separate repo ├── src/ -│ ├── autorun.brs # BrightScript bootstrap -│ ├── index.html # UI template -│ └── index.js # fetch loop, DOM update +│ ├── autorun.brs # BrightScript bootstrap +│ ├── index.html # UI template +│ └── index.js # fetch loop, DOM update ├── webpack.config.js ├── package.json -├── Makefile # prep / build / publish / clean +├── Makefile # prep / build / publish / clean └── README.md ``` @@ -327,7 +315,8 @@ Learning objective: Execute the full change → rebuild → redeploy cycle. ### Module 9 — The HTML App (30 min) Learning objective: Build and deploy the HTML app that consumes the extension. -- Clone the HTML app repo. `make prep && make build && make publish`. +- 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. @@ -352,7 +341,6 @@ Learning objective: Know what changes before shipping. ``` / ├── CLAUDE.md ← this file -├── .gitmodules ← declares workshop/html-app submodule ├── docs/ │ ├── PRD.md ← original PRD (do not delete) │ └── DESIGN.md ← master design doc (create before building) @@ -382,7 +370,6 @@ Learning objective: Know what changes before shipping. │ │ └── README.md │ ├── 09-html-app/ │ │ └── README.md -│ ├── html-app/ ← git submodule (bs-extension-workshop-html-app) │ ├── 10-production/ │ │ └── README.md │ └── cleanup/ @@ -464,5 +451,5 @@ help # list targets (default) - [x] docker/Dockerfile — dev container for GHCR - [x] .github/workflows/docker-publish.yml — builds and pushes on merge to main and version tags - [ ] Module 1 README update — add container launch instructions (macOS + Windows) -- [x] workshop/html-app submodule — content built (autorun.brs, index.html, index.js, webpack, Makefile) +- [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/workshop/00-introduction/README.md b/workshop/00-introduction/README.md index fcdd0e5..a77c955 100644 --- a/workshop/00-introduction/README.md +++ b/workshop/00-introduction/README.md @@ -42,28 +42,15 @@ The extension model lets you treat the BrightSign player as a general-purpose Li ## 3. System Architecture -``` -┌─────────────────────────────────────────────────┐ -│ BrightSign Player │ -│ │ -│ ┌─────────────────────┐ │ -│ │ Extension Process │ ← your code │ -│ │ port 8080 │ │ -│ └──────────┬──────────┘ │ -│ │ HTTP GET / │ -│ ┌──────────▼──────────┐ │ -│ │ HTML App │ (BrightSign display │ -│ │ (display layer) │ layer / BrightScript)│ -│ └─────────────────────┘ │ -│ │ -│ ┌─────────────────────┐ │ -│ │ BrightSign Control │ ← install / start / │ -│ │ API port 8008 │ stop extensions │ -│ └─────────────────────┘ │ -└─────────────────────────────────────────────────┘ - ▲ - │ curl / CI pipeline / management tool - (your workstation) +```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:** @@ -107,26 +94,14 @@ By the end of this workshop you will have deployed a fully functional extension ## 6. Architecture of the Finished Product -``` -┌─────────────────────────────────────────────────┐ -│ BrightSign Player │ -│ │ -│ ┌─────────────────────────────────┐ │ -│ │ hello-brightsign (extension) │ │ -│ │ │ │ -│ │ GET / → { "uptime": "42s" } │ │ -│ │ port 8080 │ │ -│ └───────────────┬─────────────────┘ │ -│ │ fetch every 5s │ -│ ┌───────────────▼─────────────────┐ │ -│ │ index.html (HTML app) │ │ -│ │ renders uptime on screen │ │ -│ └─────────────────────────────────┘ │ -└─────────────────────────────────────────────────┘ - -Verification from workstation: - curl http://:8080/ - → {"uptime":"42s"} +```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 ``` --- diff --git a/workshop/01-environment-setup/README.md b/workshop/01-environment-setup/README.md index f100710..853be03 100644 --- a/workshop/01-environment-setup/README.md +++ b/workshop/01-environment-setup/README.md @@ -103,20 +103,17 @@ Inside the container (or on your workstation if not using the container): $ cd /workspace ``` -2. Clone with submodules: +2. Clone: ``` - $ git clone --recurse-submodules \ - https://github.com/BrightSign-Playground/bs-extension-workshop + $ git clone https://github.com/BrightSign-Playground/bs-extension-workshop $ cd bs-extension-workshop ``` - `--recurse-submodules` initializes `workshop/html-app/` (the companion HTML app) automatically. - 3. Verify: ``` $ ls workshop/ ``` - Expected: numbered module directories plus `html-app/`. + Expected: numbered module directories (00 through cleanup). --- diff --git a/workshop/03-player-api/README.md b/workshop/03-player-api/README.md index dcafafe..3a21245 100644 --- a/workshop/03-player-api/README.md +++ b/workshop/03-player-api/README.md @@ -16,16 +16,18 @@ The player exposes two HTTP interfaces. They serve different roles and you will use both throughout the rest of the workshop. -``` -BrightSign Player -├── :8008 BrightSign Control API -│ Install / start / stop / uninstall extensions -│ Query player info, system status, logs -│ Manage content -│ -└── :8080 Your Extension's Server - Whatever HTTP interface your extension exposes - Only active when your extension is running +```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. diff --git a/workshop/08-iterate/README.md b/workshop/08-iterate/README.md index 64a9781..d7e0d0f 100644 --- a/workshop/08-iterate/README.md +++ b/workshop/08-iterate/README.md @@ -156,17 +156,18 @@ The updated message confirms the new squashfs image is mounted and running. This is the complete cycle for every change. Write it down. Steps 5–10 are identical regardless of language or framework. -``` -1. Edit source code -2. mvn clean package (rebuild) -3. Update install/ directory (copy new JAR/binary) -4. pkg-dev.sh → new ZIP (repackage) -5. scp ZIP to player -6. SSH: bsext_init stop -7. SSH: install script uninstall -8. SSH: unzip + install new version -9. SSH: reboot -10. curl to verify +```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. diff --git a/workshop/09-html-app/README.md b/workshop/09-html-app/README.md index a1ae65a..1967fb9 100644 --- a/workshop/09-html-app/README.md +++ b/workshop/09-html-app/README.md @@ -18,41 +18,37 @@ The extension you built in earlier modules exposes an HTTP API on port 8080. The Updated architecture: -``` -BrightSign Player -├── Extension process (port 8080) ← your Java/Go/C++ binary -│ └── GET / → {"message":..., "uptime_seconds":N} -│ -└── HTML app (BrightScript + JS) ← this module - ├── autorun.brs — bootstrap, loads HTML widget - ├── dist/bundle.js — webpack bundle of src/index.js - └── dist/index.html - └── polls http://localhost:8080/ every 1000ms - → displays message and uptime on screen +```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 the `workshop/html-app/` directory of this repo as a **git submodule**. It was initialized when you cloned with `--recurse-submodules` in Module 1. +The HTML app lives in its own repository, separate from the extension repo. --- -## 9.2 Navigate to the HTML App +## 9.2 Clone the HTML App ``` -$ cd /workspace/bs-extension-workshop/workshop/html-app +$ cd /workspace +$ git clone https://github.com/BrightSign-Playground/bs-extension-workshop-html-app +$ cd bs-extension-workshop-html-app ``` -Verify it is populated: +Verify contents: ``` $ ls ``` Expected: `src/`, `package.json`, `webpack.config.js`, `Makefile`, `README.md` -> **Note:** If the directory is empty, the submodule was not initialized. Run from the -> repo root: -> ``` -> $ git submodule update --init --recursive -> ``` - --- ## 9.3 Project Structure diff --git a/workshop/html-app/.gitignore b/workshop/html-app/.gitignore deleted file mode 100644 index c6c8e12..0000000 --- a/workshop/html-app/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.env -.envrc -*~ -node_modules/ -dist/ -sd/ diff --git a/workshop/html-app/Makefile b/workshop/html-app/Makefile deleted file mode 100644 index a5c6000..0000000 --- a/workshop/html-app/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -.DEFAULT_GOAL := help - -SD_DIR := sd -DIST_DIR := dist - -.PHONY: help prep build publish clean - -help: ## Print available targets - @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \ - awk 'BEGIN {FS = ":.*##"}; {printf " %-12s %s\n", $$1, $$2}' - -prep: ## Install Node dependencies (npm install) - npm install - -build: ## Bundle src/ into dist/ via webpack - npm run build - -publish: build ## Copy dist/ and autorun.brs into sd/ for SD card deployment - mkdir -p $(SD_DIR)/dist - cp -r $(DIST_DIR)/. $(SD_DIR)/dist/ - cp src/autorun.brs $(SD_DIR)/autorun.brs - @echo "" - @echo "SD card contents ready at $(SD_DIR)/" - @echo "Copy to your SD card root: cp -r $(SD_DIR)/. /path/to/sdcard/" - -clean: ## Remove dist/, sd/, and node_modules/ - rm -rf $(DIST_DIR) $(SD_DIR) node_modules diff --git a/workshop/html-app/README.md b/workshop/html-app/README.md deleted file mode 100644 index 5f89262..0000000 --- a/workshop/html-app/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# BrightSign Extension Workshop — HTML App - -BrightSign HTML application used in the extension workshop. Runs on a BrightSign player -alongside the Hello extension and displays its output on screen. - -This repo is used as a git submodule in -[bs-extension-workshop](https://github.com/BrightSign-Playground/bs-extension-workshop) -at `workshop/html-app/`. - ---- - -## What It Does - -Polls `http://localhost:8080/` (the Hello extension) every second and renders: -- The message string from the extension -- The extension's uptime in seconds - -If the extension is not running, it shows "Extension not responding" and retries. - ---- - -## Prerequisites - -- Node.js 14.x and npm (pre-installed in the workshop dev container) -- A BrightSign player with the Hello extension running -- An SD card for deployment - ---- - -## Build - -``` -$ make prep # npm install -$ make build # webpack → dist/ -$ make publish # copy dist/ + autorun.brs → sd/ -``` - ---- - -## Deploy - -Copy the contents of `sd/` to the root of an SD card: - -``` -$ cp -r sd/. /path/to/sdcard/ -``` - -Insert the SD card into the player and reboot. The player runs `autorun.brs` on boot, -which loads `dist/index.html` as a full-screen HTML widget. - ---- - -## Project Structure - -``` -src/ -├── autorun.brs BrightScript bootstrap: creates HTML widget, enables SSH + inspector -├── index.html UI template: message display, uptime counter -└── index.js Fetch loop: polls extension HTTP API, updates DOM -webpack.config.js Target: node (BrightSign JS runtime). Externalizes @brightsign/* packages. -package.json -Makefile prep / build / publish / clean -``` - ---- - -## Debugging - -Open Chrome DevTools and navigate to `http://:2999` to inspect the running -HTML app. SSH is also enabled on port 22. - ---- - -## License - -Apache 2.0 diff --git a/workshop/html-app/package.json b/workshop/html-app/package.json deleted file mode 100644 index 09bd7e9..0000000 --- a/workshop/html-app/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "bs-extension-workshop-html-app", - "version": "1.0.0", - "description": "BrightSign HTML app for the extension workshop — polls the Hello extension and displays its output", - "private": true, - "engines": { - "node": ">=14.0.0" - }, - "scripts": { - "build": "webpack --mode=development", - "build:prod": "webpack --mode=production", - "clean": "rm -rf dist sd node_modules" - }, - "devDependencies": { - "@babel/core": "^7.23.0", - "@babel/preset-env": "^7.23.0", - "babel-loader": "^9.1.3", - "html-webpack-plugin": "^5.5.3", - "webpack": "^5.88.0", - "webpack-cli": "^5.1.4" - } -} diff --git a/workshop/html-app/src/autorun.brs b/workshop/html-app/src/autorun.brs deleted file mode 100644 index 3799f2b..0000000 --- a/workshop/html-app/src/autorun.brs +++ /dev/null @@ -1,30 +0,0 @@ -' BrightSign HTML app bootstrap for the extension workshop. -' Loads the webpack-bundled HTML app from the SD card and displays it full-screen. - -Sub Main() - msgPort = CreateObject("roMessagePort") - - ' Full-screen rectangle at 1080p. - rect = { x: 0, y: 0, w: 1920, h: 1080 } - - config = CreateObject("roAssociativeArray") - config.port = msgPort - - html = CreateObject("roHtmlWidget", rect, config) - html.SetPort(msgPort) - - ' Enable SSH for debugging. - shell = CreateObject("roShell") - shell.EnableSSH(22) - - ' Enable the JavaScript inspector (open http://:2999 in Chrome DevTools). - html.EnableJavaScriptInspector(2999) - - ' Load the bundled app from the SD card root. - html.LoadURL("file:///sd:/dist/index.html") - - ' Event loop — keep the process alive. - While True - msg = Wait(0, msgPort) - End While -End Sub diff --git a/workshop/html-app/src/index.html b/workshop/html-app/src/index.html deleted file mode 100644 index ea76bd5..0000000 --- a/workshop/html-app/src/index.html +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - BrightSign Extension Workshop - - - - -
Connecting to extension…
-
-
● waiting
- - - - diff --git a/workshop/html-app/src/index.js b/workshop/html-app/src/index.js deleted file mode 100644 index 841e968..0000000 --- a/workshop/html-app/src/index.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -const EXTENSION_URL = 'http://localhost:8080/'; -const POLL_INTERVAL_MS = 1000; - -function updateUI(message, uptimeSeconds) { - document.getElementById('message').textContent = message; - document.getElementById('uptime').textContent = 'Uptime: ' + uptimeSeconds + 's'; - - const status = document.getElementById('status'); - status.textContent = '● connected'; - status.className = ''; -} - -function showError() { - document.getElementById('message').textContent = 'Extension not responding'; - document.getElementById('uptime').textContent = ''; - - const status = document.getElementById('status'); - status.textContent = '● waiting for extension'; - status.className = 'error'; -} - -async function poll() { - try { - const response = await fetch(EXTENSION_URL); - if (!response.ok) { - showError(); - return; - } - const data = await response.json(); - updateUI(data.message, data.uptime_seconds); - } catch (e) { - showError(); - } -} - -window.main = function main() { - poll(); - setInterval(poll, POLL_INTERVAL_MS); -}; diff --git a/workshop/html-app/webpack.config.js b/workshop/html-app/webpack.config.js deleted file mode 100644 index 0e74ddd..0000000 --- a/workshop/html-app/webpack.config.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -const path = require('path'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); - -module.exports = { - // BrightSign's JavaScript runtime is Node-compatible, not a browser. - // Target 'node' gives access to Node APIs (fs, net, etc.) without polyfills. - target: 'node', - - entry: './src/index.js', - - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'bundle.js', - }, - - // @brightsign/* packages are provided by the player's runtime — do not bundle them. - externals: { - '@brightsign/deviceinfo': 'commonjs @brightsign/deviceinfo', - '@brightsign/registry': 'commonjs @brightsign/registry', - '@brightsign/videooutput': 'commonjs @brightsign/videooutput', - }, - - module: { - rules: [ - { - test: /\.js$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader', - options: { - presets: [ - ['@babel/preset-env', { targets: { node: '14' } }] - ], - }, - }, - }, - ], - }, - - plugins: [ - new HtmlWebpackPlugin({ - template: './src/index.html', - inject: false, - }), - ], - - devtool: 'eval-source-map', -}; From 11083a00f92a8dd441ac1cc9fb697b353fd3b24e Mon Sep 17 00:00:00 2001 From: Greg Herlein Date: Sun, 22 Mar 2026 06:43:29 -0700 Subject: [PATCH 5/8] interim commit --- facilitator-guide/README.md | 38 +++- workshop/01-environment-setup/README.md | 276 +++++++++++++++--------- 2 files changed, 202 insertions(+), 112 deletions(-) diff --git a/facilitator-guide/README.md b/facilitator-guide/README.md index 4854c07..3614924 100644 --- a/facilitator-guide/README.md +++ b/facilitator-guide/README.md @@ -6,13 +6,37 @@ This guide is for BrightSign engineers running the workshop. Participants do not ## Pre-Workshop Setup (Day Before) -### Players +### Travel Router (Network) -- [ ] One player per participant (or per pair for larger groups) -- [ ] All players on the same network as participant workstations -- [ ] Dev mode pre-enabled on all players (saves ~5 minutes per participant in Module 1) -- [ ] IP address list printed or displayed on a shared screen (one row per player) -- [ ] SSH enabled (optional but useful for Module 7 log inspection) +- [ ] GL.iNet travel router (or equivalent) configured and tested +- [ ] SSID and password written on a card to hand to each participant +- [ ] Router DHCP reservations set: one static IP per player MAC address +- [ ] IP address label printed and affixed to each player — participants use this in Module 1.2 +- [ ] Confirm workstation and players can all reach each other on the router subnet + +> **If you cannot pre-assign IPs:** participants boot without an SD card and the player displays its IP on screen. Add ~5 minutes to Module 1. + +### 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 +- [ ] Used for the Module 0 demo — show it working before participants start +- [ ] Connected to the travel router; verify extension responds on port 8080 before the session ### Facilitator Demo Player @@ -45,7 +69,7 @@ If Docker is unavailable, ensure these are installed on the workstation: | Module | Expected | Watch for | |---|---|---| | 0 — Introduction | 15 min | Demo can run long; cut Q&A if needed | -| 1 — Environment Setup | 30 min | Windows JDK issues eat time; have a pre-configured VM ready | +| 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 | diff --git a/workshop/01-environment-setup/README.md b/workshop/01-environment-setup/README.md index 853be03..8f20d4c 100644 --- a/workshop/01-environment-setup/README.md +++ b/workshop/01-environment-setup/README.md @@ -2,23 +2,170 @@ **Duration:** 30 minutes **Learning Objectives:** -- Launch the workshop development container (or verify manual tool installs) -- Connect to your BrightSign player and confirm network access -- Enable developer mode on the player -- Clone the extension template repository +- 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 and extension template repositories **Prerequisites:** Module 0 complete. Docker Desktop installed (macOS/Windows) or Docker Engine (Linux). --- -## 1.1 Start the Development Container +## 1.1 Workshop Network Setup + + + +The Workshop Leader (WL) provides a travel router (GL.iNet or equivalent) that creates an isolated local network for all players and workstations. **Do not use venue WiFi for player communication** — it is slower and may block the ports used by the player. + +1. On your workstation, connect to the workshop WiFi: + - **SSID:** provided by your WL + - **Password:** provided by your WL + +2. Confirm your workstation has an IP on the workshop subnet: + + macOS / Linux: + ``` + $ ip addr show # or: ifconfig + ``` + Windows: + ``` + > ipconfig + ``` + Expected: an address in the same subnet as the players (e.g. `192.168.8.x`). + +--- + +## 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 facilitator has confirmed that tools are pre-installed on your -> workstation, skip to section 1.2. +> **Note:** If your WL has confirmed that tools are pre-installed on your +> workstation, skip to section 1.5. ### macOS @@ -41,10 +188,9 @@ version conflicts across macOS, Windows, and Linux. 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. The container runs under - > Rosetta 2 emulation automatically. + > `--platform linux/amd64` to the `docker run` command. -4. Verify tools inside the container: +4. Verify tools: ``` $ java -version && mvn -version && node --version && mksquashfs -version 2>&1 | head -1 ``` @@ -67,22 +213,17 @@ version conflicts across macOS, Windows, and Linux. ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest ``` - You are now at a shell prompt inside the container. All workshop commands run here. - 4. Verify tools: ``` $ java -version && mvn -version && node --version && mksquashfs -version 2>&1 | head -1 ``` Expected: version lines for each tool, no errors. -> **Warning:** Do not use `cmd.exe`. Use PowerShell or Windows Terminal. The volume -> mount syntax above does not work in `cmd.exe`. + > **Warning:** Use PowerShell or Windows Terminal — not `cmd.exe`. The volume mount syntax does not work in `cmd.exe`. ### Linux -1. Open a terminal. - -2. Pull and start: +1. Open a terminal and start the container: ``` $ docker run -it --rm \ -v "$HOME/workshop:/workspace" \ @@ -90,15 +231,15 @@ version conflicts across macOS, Windows, and Linux. ghcr.io/brightsign-playground/bs-extension-workshop-devenv:latest ``` -3. Verify tools as above. +2. Verify tools as above. --- -## 1.2 Clone the Workshop Repo +## 1.5 Clone the Workshop Repo -Inside the container (or on your workstation if not using the container): +Inside the container: -1. Navigate to the workspace directory: +1. Navigate to workspace: ``` $ cd /workspace ``` @@ -117,99 +258,24 @@ Inside the container (or on your workstation if not using the container): --- -## 1.3 Player Setup - -Each participant has a BrightSign player at their bench. - -1. Connect the power cable to the player. - -2. Connect an Ethernet cable from the player to the workshop network switch. - -3. Wait for the player to complete its boot sequence. The LED on the front panel turns solid (non-blinking) when ready. - -4. Find your player's IP address: - - **Display method:** If no content is loaded, the player shows its IP on screen during boot. - - **DHCP table method:** Check your router or network switch DHCP lease table for the player's MAC address. - - **Facilitator list:** Use the IP assigned to your bench in the workshop network map. - -5. Set an environment variable (used in every module from here on): - ``` - $ export PLAYER_IP= - ``` - - > **Tip:** Add this to your container session's history so you can re-run it if you - > restart the container. If using a persistent `/workspace` volume, save it in a - > `.envrc` file: `echo "export PLAYER_IP=" >> /workspace/.envrc` - -6. Verify network connectivity from inside the container: - ``` - $ ping -c 3 $PLAYER_IP - ``` - Expected: - ``` - 3 packets transmitted, 3 received, 0% packet loss - ``` - -7. Verify the BrightSign API is reachable: - ``` - $ curl -s http://$PLAYER_IP:8008/api/v1/info | python3 -m json.tool - ``` - Expected: JSON with player model, firmware version, serial number. - ---- - -## 1.4 Enable Developer Mode on the Player - -> **Warning:** Developer mode disables signature verification. Never apply these settings -> to a player in a public or production installation. - -1. Open a browser on your workstation (not inside the container) and navigate to: - ``` - http:// - ``` - -2. Log in to the player web interface. Default credentials are on the player's display or - in the facilitator guide. - -3. Navigate to **Settings** → **Developer Options**. - -4. Enable **Local Extensions**. - -5. Enable **Insecure Content Loading**. - -6. Click **Save** (or **Apply**). - -7. The player may reboot. Wait for the LED to return to solid, then re-verify: - ``` - $ curl -s http://$PLAYER_IP:8008/api/v1/info | python3 -m json.tool - ``` - Expected: same JSON response as step 1.3.7. - ---- - -## 1.5 Clone the Extension Template +## 1.6 Clone the Extension Template -1. Inside the container, navigate to workspace: +1. From the workspace directory, clone the template alongside the workshop repo: ``` $ cd /workspace - ``` - -2. Clone the template (separate from the workshop repo): - ``` $ git clone https://github.com/brightsign/extension-template - $ cd extension-template ``` -3. Verify contents: +2. Verify contents: ``` - $ find . -type f | sort + $ find extension-template -type f | sort ``` - Expected: tree of files including `examples/`, `common-scripts/`, `docs/`. + Expected: files under `examples/`, `common-scripts/`, `docs/`. -> **Note:** You are cloning this template to study its structure. In Module 4 you will -> build your own extension. Do not modify these template files. + > **Note:** You are cloning this template to study its structure. In Module 4 you will + > build your own extension. Do not modify these template files. --- -You now have a running container, a connected player in developer mode, and the template -cloned. Proceed to **Module 2**. +You now have a connected, insecured player, a running container, and both repos cloned. +Proceed to **[Module 2](../02-understand-template/README.md)**. From 02f38e316aed3e39cd1f1653d7e412f5318bc120 Mon Sep 17 00:00:00 2001 From: Greg Herlein Date: Sun, 22 Mar 2026 07:00:13 -0700 Subject: [PATCH 6/8] interim commit --- CLAUDE.md | 2 +- facilitator-guide/README.md | 43 ++++++++---- workshop/01-environment-setup/README.md | 90 +++++++++++++++++++++---- 3 files changed, 106 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index df3f095..e85de76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ the extension. See "Companion HTML App" section below. | Hardware | Development workstation: macOS, Windows, or Linux | | Hardware | Ethernet cable + power for the player | | Hardware | SD card (for Module 9 HTML app deployment) | -| Network | Wired network with player and workstation on same subnet | +| 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.) | diff --git a/facilitator-guide/README.md b/facilitator-guide/README.md index 3614924..2360a00 100644 --- a/facilitator-guide/README.md +++ b/facilitator-guide/README.md @@ -6,15 +6,35 @@ This guide is for BrightSign engineers running the workshop. Participants do not ## Pre-Workshop Setup (Day Before) -### Travel Router (Network) +### Travel Router and Network (Set Up at the Venue) -- [ ] GL.iNet travel router (or equivalent) configured and tested -- [ ] SSID and password written on a card to hand to each participant -- [ ] Router DHCP reservations set: one static IP per player MAC address +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 -- [ ] Confirm workstation and players can all reach each other on the router subnet +- [ ] SSID and password written on a card or slide to hand out in Module 1.1 -> **If you cannot pre-assign IPs:** participants boot without an SD card and the player displays its IP on screen. Add ~5 minutes to Module 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) @@ -35,14 +55,9 @@ Reference: [BrightSign dev environment setup docs](https://github.com/BrightDeve ### Facilitator Demo Player - [ ] Separate pre-insecured player with the finished extension and HTML app already deployed -- [ ] Used for the Module 0 demo — show it working before participants start -- [ ] Connected to the travel router; verify extension responds on port 8080 before the session - -### Facilitator Demo Player - -- [ ] Separate player with the finished extension and HTML app already deployed -- [ ] Used for the Module 0 demo — show it working before participants start -- [ ] Keep this player isolated; do not use it for participant exercises +- [ ] 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) diff --git a/workshop/01-environment-setup/README.md b/workshop/01-environment-setup/README.md index 8f20d4c..4183e23 100644 --- a/workshop/01-environment-setup/README.md +++ b/workshop/01-environment-setup/README.md @@ -6,23 +6,42 @@ - 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 and extension template repositories +- 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). +**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 Workshop Leader (WL) provides a travel router (GL.iNet or equivalent) that creates an isolated local network for all players and workstations. **Do not use venue WiFi for player communication** — it is slower and may block the ports used by the player. +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 has an IP on the workshop subnet: +2. Confirm your workstation received an IP on the workshop subnet: macOS / Linux: ``` @@ -32,7 +51,13 @@ The Workshop Leader (WL) provides a travel router (GL.iNet or equivalent) that c ``` > ipconfig ``` - Expected: an address in the same subnet as the players (e.g. `192.168.8.x`). + 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. --- @@ -258,24 +283,61 @@ Inside the container: --- -## 1.6 Clone the Extension Template +## 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. -1. From the workspace directory, clone the template alongside the workshop repo: +> **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/brightsign/extension-template + $ git clone https://github.com// + $ cd ``` -2. Verify contents: +7. Verify contents: ``` - $ find extension-template -type f | sort + $ find . -type f | sort ``` Expected: files under `examples/`, `common-scripts/`, `docs/`. - > **Note:** You are cloning this template to study its structure. In Module 4 you will - > build your own extension. Do not modify these template files. +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, and both repos cloned. +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)**. From a19cb2bfbba9f0467ca68f8d8ec9947822e62b64 Mon Sep 17 00:00:00 2001 From: Greg Herlein Date: Sun, 22 Mar 2026 07:08:40 -0700 Subject: [PATCH 7/8] interim commit --- .gitignore | 2 +- CLAUDE.md | 21 +- Makefile | 2 +- workshop/04-build-java/README.md | 280 ----------------- workshop/04-build/README.md | 283 ++++++++++++++++++ .../java}/hello-extension/Makefile | 2 +- .../java}/hello-extension/bsext_init | 0 .../java}/hello-extension/pom.xml | 0 .../brightsign/workshop/HelloExtension.java | 0 9 files changed, 296 insertions(+), 294 deletions(-) delete mode 100644 workshop/04-build-java/README.md create mode 100644 workshop/04-build/README.md rename workshop/{04-build-java => 04-build/java}/hello-extension/Makefile (98%) rename workshop/{04-build-java => 04-build/java}/hello-extension/bsext_init (100%) rename workshop/{04-build-java => 04-build/java}/hello-extension/pom.xml (100%) rename workshop/{04-build-java => 04-build/java}/hello-extension/src/main/java/com/brightsign/workshop/HelloExtension.java (100%) diff --git a/.gitignore b/.gitignore index b383672..ed4371f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ target/ *.war # Downloaded JRE and packaging staging directory (re-created by make download-jre / make package) -workshop/04-build-java/hello-extension/install/ +workshop/04-build/java/hello-extension/install/ # Java *.class diff --git a/CLAUDE.md b/CLAUDE.md index e85de76..6ca6833 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -271,21 +271,21 @@ Learning objective: Control extensions via the BrightSign REST API. ### Module 4 — Build the Extension Binary [MODULAR — SWAP PER LANGUAGE] (45 min) Learning objective: Produce a binary that satisfies the extension contract. -Each language variant lives in `workshop/04-build-/`. 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. +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/ +#### 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) +#### 04-build/go/ (future) - `go mod init`, `net/http`, cross-compile for BrightSign ARM target. - Same local smoke test. -#### 04-build-cpp/ (future) +#### 04-build/cpp/ (future) - CMake + cpp-httplib, cross-compile toolchain. - Same local smoke test. @@ -355,11 +355,10 @@ Learning objective: Know what changes before shipping. │ │ └── README.md │ ├── 03-player-api/ │ │ └── README.md -│ ├── 04-build-java/ ← Java language variant -│ │ ├── README.md -│ │ └── hello-extension/ ← Maven project + bsext_init -│ ├── 04-build-go/ ← future -│ ├── 04-build-cpp/ ← future +│ ├── 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/ diff --git a/Makefile b/Makefile index 6280c79..9b08cf3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -EXTENSION_DIR := workshop/04-build-java/hello-extension +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 diff --git a/workshop/04-build-java/README.md b/workshop/04-build-java/README.md deleted file mode 100644 index df76be4..0000000 --- a/workshop/04-build-java/README.md +++ /dev/null @@ -1,280 +0,0 @@ - - -# Module 4: Build the Extension — Java - -**Duration:** 45 minutes -**Language:** Java 11+ (Maven) -**Learning Objectives:** -- Build a self-contained JAR that satisfies the extension contract -- Write a bsext_init script for a Java extension -- Verify the extension works locally before deploying to the player - -**Prerequisites:** Modules 1–3 complete. JDK 11+ and Maven 3.6+ verified (pre-installed in the workshop container). - ---- - -## 4.1 The Extension Contract - -Every extension — regardless of language — must satisfy this contract. Module 5 (packaging) 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 the build step places in `install/`. - ---- - -## 4.2 Create the Project - -1. Open a terminal and create a working directory: - ``` - $ mkdir -p ~/workshop/hello-extension - $ cd ~/workshop/hello-extension - ``` - -2. Copy the project files from the workshop materials: - ``` - $ cp -r /path/to/workshop/04-build-java/hello-extension/. . - ``` - -3. Verify the structure: - ``` - $ find . -type f | sort - ``` - Expected output: - ``` - ./Makefile - ./bsext_init - ./pom.xml - ./src/main/java/com/brightsign/workshop/HelloExtension.java - ``` - -### pom.xml walkthrough - -Open `pom.xml` and note three things: - -**Coordinates and Java version:** -```xml -com.brightsign.workshop -hello-extension -1.0.0 - - - 11 - 11 - -``` -Java 11 source and target. The player runs a JVM — confirm the exact version with your facilitator. - -**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 local classpath, and 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. We do not need an external HTTP framework. - ---- - -## 4.3 Implement HelloExtension.java - -The source file is at `src/main/java/com/brightsign/workshop/HelloExtension.java`. Walk through each part before building. - -**Named constants — no magic numbers:** -```java -private static final int HTTP_PORT = 8080; -private static final String LOG_PATH = "/tmp/hello-extension.log"; -``` -Both values are referenced by the extension contract. Naming them makes the contract visible in the code. - -**Startup log write:** -The first thing `main` does (before binding the port) is write one line to `/tmp/hello-extension.log`. The path `/tmp` is used because the extension's own directory on the player is read-only. `/tmp` is a writable RAM disk available on every player. - -**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. The `startMillis` timestamp captured at the top of `main` is passed through to the handler for uptime calculation. - -**Request handler:** -```java -long uptimeSeconds = (System.currentTimeMillis() - startMillis) / 1000; -String body = "{\"message\":\"Hello from BrightSign!\",\"uptime_seconds\":" + uptimeSeconds + "}"; -``` -The JSON is built by hand — no JSON library. This is intentional: zero dependencies, zero classpath complexity, and the output is short enough that string concatenation is readable. - -**Shutdown hook:** -```java -Runtime.getRuntime().addShutdownHook(new Thread(() -> shutDown(server))); -``` -The JVM calls shutdown hooks in response to SIGTERM and SIGINT. The hook logs `"hello-extension stopping"` and calls `server.stop(0)`. This satisfies contract item 4 without any native signal handling. - ---- - -## 4.4 Download the Bundled JRE - -BrightSign players do not have a system Java installation. The JRE must be bundled inside the extension's squashfs image alongside the JAR. - -The Makefile downloads Eclipse Temurin 11 JRE for `linux/aarch64` from Adoptium and places it in `install/jre/`. This only needs to happen once — the Makefile skips the download if `install/jre/` already exists. - -1. Download the JRE: - ``` - $ make download-jre - ``` - Expected output: - ``` - Downloading Eclipse Temurin JRE 11 for linux/aarch64... - JRE installed at install/jre - ``` - This downloads ~45MB and extracts to `install/jre/`. The download requires internet access. - -2. Verify: - ``` - $ install/jre/bin/java -version - ``` - Expected: - ``` - openjdk version "11.x.x" ... - ``` - -> **Note:** `install/jre/` is excluded from version control via `.gitignore`. It is re-downloaded by `make download-jre` on a fresh checkout. Do not commit it. - -> **Note:** The JRE is `linux/aarch64` — it only runs on ARM64 hardware (the BrightSign player). Running `install/jre/bin/java` on an x86 workstation will fail. Use the system `java` command for the local smoke test in section 4.6. - ---- - -## 4.5 Write the bsext_init Script - -The player OS manages extensions using SysV-style init scripts. The `bsext_init` file is at the project root (not inside `src/`). - -**DAEMON_NAME:** -```sh -DAEMON_NAME="hello_extension" -``` -Must be lowercase with underscores. This name becomes the PID file path and appears in log messages. Dashes are not safe in all shell contexts — use underscores. - -**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 is mounted read-only at `/var/volatile/bsext/hello_extension/`. The `jre/` directory inside that mount is the Temurin JRE bundled during packaging. - -**run_extension function:** -```sh -run_extension() { - exec "${JAVA_BIN}" -jar "${JAR_PATH}" -} -``` -`exec` replaces the shell with the JVM process. Signals go directly to the JVM and trigger the shutdown hook. No shell wrapper remains in the process tree. - -**run target (foreground mode):** -```sh -run) - run_extension - ;; -``` -Runs the extension in the foreground with output going directly to the terminal. Use this when SSH'd into the player to diagnose startup problems. - ---- - -## 4.6 Build - -1. From your project directory, run: - ``` - $ mvn clean package - ``` - Expected output: - ``` - [INFO] BUILD SUCCESS - ``` - -2. Verify the fat JAR was created: - ``` - $ ls -lh target/hello-extension-1.0.0.jar - ``` - Expected: a file larger than 1 KB. Even with no dependencies the shade plugin produces a JAR with a complete manifest and the class files bundled. - -> **Note:** The JAR must be self-contained. If you add a dependency in the future, the shade plugin bundles it. The player has no Maven, no classpath configuration, and no internet access — the JAR is the entire runtime. - -> **Tip:** Run `make build` instead of `mvn clean package` directly. Both do the same thing; the Makefile wraps `mvn clean package -q` to suppress verbose output. - ---- - -## 4.7 Local Smoke Test - -Verify the extension satisfies the contract on your workstation before touching the player. This step catches the majority of problems before they become harder to debug on remote hardware. - -1. Start the extension in the background: - ``` - $ java -jar target/hello-extension-1.0.0.jar & - ``` - -2. Wait two seconds for the HTTP server to bind, then send a request: - ``` - $ curl -s http://localhost:8080/ | python3 -m json.tool - ``` - Expected output: - ```json - { - "message": "Hello from BrightSign!", - "uptime_seconds": 2 - } - ``` - -3. Check the startup log: - ``` - $ cat /tmp/hello-extension.log - ``` - Expected: one line similar to: - ``` - hello-extension started at 2026-03-21T10:15:30.123Z - ``` - -4. Stop the local process: - ``` - $ kill %1 - ``` - The shutdown hook will log `hello-extension stopping` to stderr before the JVM exits. - -> **Warning:** If curl returns `Connection refused`, the HttpServer failed to bind port 8080. Check whether another process is already using that port: -> ``` -> $ lsof -i :8080 -> ``` -> Stop the conflicting process, then retry. - -> **Tip:** `make test-local` runs steps 1–4 automatically. It starts the extension, waits 2 seconds, curls the endpoint, and kills the process. Note: this uses the system `java` command, not the bundled ARM64 JRE — that is correct for local testing. - ---- - -## 4.8 What You Have - -After completing this module you have everything Module 5 needs: - -| Path | Purpose | -|---|---| -| `target/hello-extension-1.0.0.jar` | The self-contained extension JAR | -| `bsext_init` | The 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. The packaging and deployment workflow does not change based on what language produced the binary. - -Proceed to **[Module 5: Package the Extension](../../05-package/README.md)**. 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 similarity index 98% rename from workshop/04-build-java/hello-extension/Makefile rename to workshop/04-build/java/hello-extension/Makefile index a9ca7b2..ac19674 100644 --- a/workshop/04-build-java/hello-extension/Makefile +++ b/workshop/04-build/java/hello-extension/Makefile @@ -5,7 +5,7 @@ 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 +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. diff --git a/workshop/04-build-java/hello-extension/bsext_init b/workshop/04-build/java/hello-extension/bsext_init similarity index 100% rename from workshop/04-build-java/hello-extension/bsext_init rename to workshop/04-build/java/hello-extension/bsext_init diff --git a/workshop/04-build-java/hello-extension/pom.xml b/workshop/04-build/java/hello-extension/pom.xml similarity index 100% rename from workshop/04-build-java/hello-extension/pom.xml rename to workshop/04-build/java/hello-extension/pom.xml 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 similarity index 100% rename from workshop/04-build-java/hello-extension/src/main/java/com/brightsign/workshop/HelloExtension.java rename to workshop/04-build/java/hello-extension/src/main/java/com/brightsign/workshop/HelloExtension.java From 85adf57bc52cda760afe5ea402db0f28db65705c Mon Sep 17 00:00:00 2001 From: Greg Herlein Date: Sun, 22 Mar 2026 07:59:59 -0700 Subject: [PATCH 8/8] interim commit --- .github/workflows/docker-publish.yml | 70 ---------------------------- CLAUDE.md | 2 +- 2 files changed, 1 insertion(+), 71 deletions(-) delete mode 100644 .github/workflows/docker-publish.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index 178b0fc..0000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Publish Dev Container - -on: - push: - branches: - - main - paths: - - 'docker/Dockerfile' - - '.github/workflows/docker-publish.yml' - tags: - - 'v*' - pull_request: - branches: - - main - paths: - - 'docker/Dockerfile' - - '.github/workflows/docker-publish.yml' - workflow_dispatch: - -env: - REGISTRY: ghcr.io - IMAGE_NAME: brightsign-playground/bs-extension-workshop-devenv - -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Log in to GHCR - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - # latest on every push to main - type=raw,value=latest,enable={{is_default_branch}} - # semver tags: v1.2.3 → 1.2.3, 1.2, 1 - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - # short SHA for traceability - type=sha,prefix=sha-,format=short - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: docker/ - platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/CLAUDE.md b/CLAUDE.md index 6ca6833..859ff2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -448,7 +448,7 @@ help # list targets (default) - [x] Facilitator Guide - [x] Makefile (extension repo) - [x] docker/Dockerfile — dev container for GHCR -- [x] .github/workflows/docker-publish.yml — builds and pushes on merge to main and version tags +- [ ] .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