From ce854ff2d8528d0971659db52bb886e488c0ffe6 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Tue, 4 Aug 2026 21:07:47 -0500 Subject: [PATCH] Wire the BrightSign bridge into the web player MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bridge and the host existed but nothing loaded them. Now the player does. restartPlayer() replaces every location.reload() call site. On BrightSign a page-initiated reload does not reliably bring the roHtmlWidget back, so the page asks the host to rebuild it and only falls back to reload() when no host is there to take the request. That covers the deploy path, the operator refresh, the service-worker activation and the manual reset. Identity now round-trips through the registry, which outlives localStorage on this platform: getConfig() adopts a registry identity when local storage comes back empty, instead of re-pairing and spawning a second row for a panel that is already provisioned. The operator reset clears the registry too — otherwise it would clear localStorage, get the same identity straight back on the next boot, and reset nothing. Registration reports platform 'brightsign' rather than "Chrome 120", which is what sync-backend.js resolves native-vs-ours from, plus model, OS, serial and which output this widget paints. Dual output needed a collision fix: autorun.brs gives the second HDMI output its own widget, and both widgets share an origin, a registry and one SD storage_path. Un-namespaced, output 2 would read output 1's config, install salt and device id and the two would collapse into a single device row. Storage keys and registry keys are now suffixed per output; screen 1 keeps the bare names so nothing existing moves. The bridge is served from its single source so the copy the player loads can never skew from the one on the SD card next to autorun.brs, and it is served to every player rather than gated on a user agent — a panel reporting an unexpected UA would otherwise silently lose restart-instead-of-reload. Two test harnesses extract player functions and run them in an isolated scope, so they now supply SCREEN_SUFFIX; one gained a case proving two outputs of one player get distinct identities. 927 pass. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL --- brightsign/autorun.brs | 6 + brightsign/st-bridge.js | 51 ++++- server/player/index.html | 104 +++++++++-- server/server.js | 13 ++ server/test/brightsign-bridge.test.js | 174 ++++++++++++++++++ .../fingerprint-identity-collision.test.js | 17 +- server/test/player-identity-reset.test.js | 2 + 7 files changed, 345 insertions(+), 22 deletions(-) create mode 100644 server/test/brightsign-bridge.test.js diff --git a/brightsign/autorun.brs b/brightsign/autorun.brs index 7eb2937..0082e0b 100644 --- a/brightsign/autorun.brs +++ b/brightsign/autorun.brs @@ -174,6 +174,12 @@ Sub Main() else if m.type = "identity" then ' Pairing completed in the page — persist it where a reboot can find it. + ' clear:true is the operator reset; the registry must forget the display or + ' the next boot re-adopts it and the reset silently does nothing. + if m.clear = true then + SaveRegistry("device_id", "") + cfg.device_id = "" + end if if m.device_id <> invalid then SaveRegistry("device_id", m.device_id) cfg.device_id = m.device_id diff --git a/brightsign/st-bridge.js b/brightsign/st-bridge.js index 541b367..026c1ca 100644 --- a/brightsign/st-bridge.js +++ b/brightsign/st-bridge.js @@ -69,17 +69,37 @@ try { registry = new RegistryClass(); } catch (e) { registry = null; } } - function regRead(key, fallback) { + function screenNumber() { + try { + var m = new RegExp('[?&]screen=([^&]*)').exec(global.location.search || ''); + var n = m ? parseInt(decodeURIComponent(m[1]), 10) : 1; + return (isNaN(n) || n < 1) ? 1 : n; + } catch (e) { return 1; } + } + + /* + * Registry keys are namespaced per output. On a dual-output player autorun.brs runs TWO + * widgets against the same registry, the same SD storage_path and the same origin — so an + * un-namespaced "device_id" would have both outputs adopt one identity and collapse into a + * single device row. Screen 1 keeps the bare key so existing single-output panels are + * unaffected. + */ + function key(name) { + var s = screenNumber(); + return s > 1 ? name + '_s' + s : name; + } + + function regRead(name, fallback) { if (!registry) return fallback; try { - var v = registry.read('screentinker', key); + var v = registry.read('screentinker', key(name)); return (v === undefined || v === null || v === '') ? fallback : v; } catch (e) { return fallback; } } - function regWrite(key, value) { + function regWrite(name, value) { if (!registry) return false; - try { registry.write('screentinker', key, String(value)); return true; } catch (e) { return false; } + try { registry.write('screentinker', key(name), String(value)); return true; } catch (e) { return false; } } var deviceInfo = null; @@ -132,9 +152,16 @@ }, /* Which physical output this widget is painting. 1 unless autorun.brs made a second one. */ - screen: function () { - var n = parseInt(qs('screen') || '1', 10); - return (isNaN(n) || n < 1) ? 1 : n; + screen: screenNumber, + + /* + * Suffix callers should append to any per-display storage key. Two widgets on one player + * share an origin and therefore share localStorage, so the config, playlist cache and + * install salt all need separating or the second output silently becomes the first. + */ + storageSuffix: function () { + var s = screenNumber(); + return s > 1 ? '_s' + s : ''; }, /* @@ -154,6 +181,16 @@ post({ type: 'identity', device_id: deviceId || null, server_url: serverUrl || null }); }, + /* + * Forget this display. Required for the operator reset to mean anything: the registry + * outlives localStorage, so clearing local storage alone would leave the panel re-adopting + * the same identity on its next boot — a reset that resets nothing. + */ + clearIdentity: function () { + regWrite('device_id', ''); + return post({ type: 'identity', clear: true }); + }, + /* * THE reload replacement. Never call location.reload() on this platform. * Returns false if there is no host, so the caller can decide whether reloading in place diff --git a/server/player/index.html b/server/player/index.html index e00a0f8..4c941be 100644 --- a/server/player/index.html +++ b/server/player/index.html @@ -116,6 +116,10 @@ few-ms delay vs the inline trap is fine since errors before this loads are already captured in __debugLog by the inline trap above. --> + + ScreenTinker Player