mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Add a BrightSign capability probe
Not a port — a way to answer on real hardware the questions that decide what a port looks like, instead of guessing them from documentation. The one that matters is persistence. ScreenTinker's device identity (deviceId, deviceToken, paired, serverUrl) lives in localStorage, and on BrightSign that behaves like sessionStorage: without a durable store every panel re-pairs on every boot and spawns a new device row. The registry is the alternative, so the probe reports whether it resolves and whether either store survives a power cycle. It runs LOCALLY first on purpose. That establishes whether the @brightsign/* modules resolve at all, separately from whether a remotely-served page can reach them — the question that decides between reusing the hosted web player and building a local shim that owns the registry and passes identity to an iframe via postMessage. Without that split a failure is ambiguous: an origin restriction and nodejs_enabled not taking look identical. Once local works, changing one line points it at a hosted copy and the delta is the answer. Also reports the web-platform features the player leans on — service workers and the Cache API for content caching, h264 in <video>, CSS clamp() for the directory-search keyboard — plus model, OS and Chromium version, since Series 4 is pinned to Chromium 87 and would give a misleadingly pessimistic result. SD-card ready: FAT32, both files at the root, empty card. Remote devtools on :2999. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f4595f017a
commit
41ebb02368
51
brightsign/README.md
Normal file
51
brightsign/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# ScreenTinker on BrightSign — capability probe
|
||||
|
||||
Not a port. This answers, on **real hardware**, the questions that decide what a port looks like —
|
||||
so the design isn't guessed from documentation.
|
||||
|
||||
## Run it
|
||||
|
||||
1. FAT32-format an SD card. It must be **empty** — a card with leftover data won't trigger a fresh
|
||||
provisioning cycle.
|
||||
2. Copy `autorun.brs` and `probe.html` to the **root**.
|
||||
3. Insert with the player powered off, then power on.
|
||||
4. Read the screen. Remote devtools are on `http://<player-ip>:2999` if you'd rather read it there.
|
||||
5. **Power-cycle and reload.** The reboot markers are the point — first run writes them, second run
|
||||
says which survived.
|
||||
|
||||
## What it answers, and why each matters
|
||||
|
||||
| check | why it decides something |
|
||||
|---|---|
|
||||
| which `@brightsign/*` modules resolve | `nodejs_enabled: true` injects them into the runtime. If injection is origin-independent, a **remotely-served** page gets them too — which is the whole cheap path. |
|
||||
| `registry` survives reboot | ScreenTinker's device identity (`deviceId`, `deviceToken`, `paired`, `serverUrl`) lives in `localStorage`, and on BrightSign that behaves like sessionStorage. Without a durable store every panel re-pairs on every boot and spawns a new device row. |
|
||||
| `localStorage` survives reboot | If it does on this OS build, the port gets dramatically simpler. Reports say it doesn't; worth confirming rather than inheriting a 2019 answer. |
|
||||
| serviceWorker / Cache API / indexedDB | The web player registers `/player/sw.js` for content caching. If unavailable, offline playback has to move to BrightSign's storage APIs — which is the "extra mile" work anyway. |
|
||||
| `<video>` + h264 | Whether HTML5 video is viable as a stopgap before wiring the native decode path. |
|
||||
| CSS `clamp()` | The directory-search keyboard scales with `clamp(…vh…)`. Chromium 87 (Series 4) is the risk. |
|
||||
| reach `screentinker.com/api/status` | Rules network/TLS out before blaming anything else. |
|
||||
|
||||
## Then: the actual question
|
||||
|
||||
The probe runs **locally** first to establish the baseline. Once `registry` resolves from
|
||||
`file:///`, change `url:` in `autorun.brs` to a hosted copy of `probe.html` and re-run.
|
||||
|
||||
- **Still resolves →** point the widget at the hosted player, swap identity persistence to the
|
||||
registry, done. Days, not weeks.
|
||||
- **Doesn't resolve →** a local shim page owns the registry and passes identity to the hosted
|
||||
player in an iframe via `postMessage`. The Chromium 110/120 notes say iframes now *require*
|
||||
`postMessage()` for BrightSign objects, which suggests this is the sanctioned pattern rather
|
||||
than a workaround.
|
||||
|
||||
## Model notes
|
||||
|
||||
Target **Series 6** (ships Chromium 120) or **Series 5** (upgradeable via the `html/widget_type`
|
||||
registry key). **Series 4 is pinned to Chromium 87** — a result from one would be misleadingly
|
||||
pessimistic.
|
||||
|
||||
## Scope
|
||||
|
||||
A URL wrapper is the on-ramp, not the destination. Doing this properly on BrightSign means the
|
||||
registry for identity, SD for offline media, and their native video path rather than `<video>`.
|
||||
ScreenTinker's existing multi-zone layouts, video walls and group sync map onto that platform's
|
||||
strengths unusually well — those are the parts worth showing off.
|
||||
33
brightsign/autorun.brs
Normal file
33
brightsign/autorun.brs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
' ScreenTinker — BrightSign capability probe
|
||||
'
|
||||
' Drop this plus probe.html on a FAT32 SD card (root, nothing else), insert, power on.
|
||||
' It opens the local probe page with Node.js enabled so the @brightsign/* modules are
|
||||
' injected, then the page reports what actually resolves on THIS model and OS build.
|
||||
'
|
||||
' Deliberately local-first: the whole point of the probe is to establish the baseline
|
||||
' (what a LOCAL page can reach) before testing whether a REMOTE page reaches the same.
|
||||
|
||||
Sub Main()
|
||||
videoMode = CreateObject("roVideoMode")
|
||||
r = CreateObject("roRectangle", 0, 0, videoMode.GetResX(), videoMode.GetResY())
|
||||
|
||||
config = {
|
||||
url: "file:///probe.html"
|
||||
nodejs_enabled: true ' REQUIRED for require("@brightsign/*")
|
||||
inspector_server: { port: 2999 } ' remote devtools: http://<player-ip>:2999
|
||||
storage_path: "SD:/"
|
||||
storage_quota: 1073741824
|
||||
javascript_enabled: true
|
||||
mouse_enabled: true
|
||||
}
|
||||
|
||||
html = CreateObject("roHtmlWidget", r, config)
|
||||
html.Show()
|
||||
|
||||
' Keep the script alive; the page does the work.
|
||||
msgPort = CreateObject("roMessagePort")
|
||||
html.SetPort(msgPort)
|
||||
while true
|
||||
msg = wait(0, msgPort)
|
||||
end while
|
||||
End Sub
|
||||
104
brightsign/probe.html
Normal file
104
brightsign/probe.html
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><title>ScreenTinker — BrightSign probe</title>
|
||||
<style>
|
||||
body{margin:0;background:#0f1420;color:#e6edf7;font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;padding:24px}
|
||||
h1{font-size:22px;margin:0 0 4px} .sub{opacity:.6;margin-bottom:18px}
|
||||
table{border-collapse:collapse;width:100%;max-width:1100px} td,th{padding:6px 10px;border-bottom:1px solid #223}
|
||||
th{text-align:left;opacity:.6;font-weight:600}
|
||||
.y{color:#3fb950;font-weight:700} .n{color:#f85149;font-weight:700} .w{color:#d29922;font-weight:700}
|
||||
.k{font-family:ui-monospace,Menlo,monospace;color:#9fc6ff}
|
||||
.box{background:#151b2b;border:1px solid #223;border-radius:8px;padding:14px;margin:14px 0;max-width:1100px}
|
||||
.big{font-size:17px}
|
||||
</style></head><body>
|
||||
<h1>ScreenTinker — BrightSign capability probe</h1>
|
||||
<div class="sub" id="sub">running…</div>
|
||||
|
||||
<div class="box big" id="verdict">…</div>
|
||||
|
||||
<div class="box">
|
||||
<b>Reboot test.</b> This page wrote a marker to both localStorage and the registry on first run.
|
||||
<b>Power-cycle the player and reload this page</b> — the table shows which survived.
|
||||
<div id="reboot" style="margin-top:8px"></div>
|
||||
</div>
|
||||
|
||||
<table id="t"><thead><tr><th style="width:280px">check</th><th style="width:90px">result</th><th>detail</th></tr></thead><tbody></tbody></table>
|
||||
|
||||
<script>
|
||||
const rows = [];
|
||||
const add = (name, ok, detail) => rows.push({ name, ok, detail: String(detail === undefined ? '' : detail).slice(0, 220) });
|
||||
const cls = v => v === true ? 'y' : v === false ? 'n' : 'w';
|
||||
const txt = v => v === true ? 'YES' : v === false ? 'NO' : String(v);
|
||||
|
||||
// ---- 1. which @brightsign/* modules resolve at all --------------------------------------
|
||||
// This is THE question: they are injected into the runtime by nodejs_enabled, so if the
|
||||
// injection is origin-independent a remotely-served page sees them too.
|
||||
const MODULES = ['registry','storage','system','systemtime','videoinput','videooutput',
|
||||
'networkconfiguration','networkdiagnostics','deviceinfo','filesystem',
|
||||
'messageport','controlport','audiooutput','screenshot','pointer'];
|
||||
const resolved = {};
|
||||
add('typeof require', typeof require !== 'undefined', typeof require);
|
||||
for (const m of MODULES) {
|
||||
try { const mod = require('@brightsign/' + m); resolved[m] = !!mod; add('@brightsign/' + m, !!mod, mod ? Object.keys(mod).slice(0,6).join(', ') : ''); }
|
||||
catch (e) { resolved[m] = false; add('@brightsign/' + m, false, e.message); }
|
||||
}
|
||||
|
||||
// ---- 2. what deviceinfo reports (model + OS decides everything else) --------------------
|
||||
let model = 'unknown', osv = 'unknown';
|
||||
try {
|
||||
const DeviceInfo = require('@brightsign/deviceinfo');
|
||||
const di = new DeviceInfo();
|
||||
model = di.model || di.getModel?.() || 'unknown';
|
||||
osv = di.OSVersion || di.version || di.getVersion?.() || 'unknown';
|
||||
add('device model', model, '');
|
||||
add('OS version', osv, '');
|
||||
add('chromium (UA)', (navigator.userAgent.match(/Chrome\/[\d.]+/) || ['n/a'])[0], navigator.userAgent.slice(0, 120));
|
||||
} catch (e) { add('deviceinfo read', false, e.message); }
|
||||
|
||||
// ---- 3. persistence: the thing that decides the whole port ------------------------------
|
||||
const MARK = 'st_probe_marker';
|
||||
let lsBefore = null, regBefore = null, reg = null;
|
||||
try { lsBefore = localStorage.getItem(MARK); } catch (e) { add('localStorage readable', false, e.message); }
|
||||
try {
|
||||
const Registry = require('@brightsign/registry');
|
||||
reg = new Registry();
|
||||
regBefore = reg.read ? reg.read('screentinker', MARK) : null;
|
||||
if (regBefore && regBefore.then) regBefore = '(async — see console)';
|
||||
} catch (e) { add('registry usable', false, e.message); }
|
||||
|
||||
const stamp = new Date().toISOString();
|
||||
try { if (lsBefore === null) localStorage.setItem(MARK, stamp); } catch (e) {}
|
||||
try { if (reg && !regBefore) (reg.write ? reg.write('screentinker', MARK, stamp) : null); } catch (e) {}
|
||||
|
||||
add('localStorage survived', lsBefore !== null ? true : 'first run — reboot & reload', lsBefore || stamp);
|
||||
add('registry survived', regBefore ? true : (reg ? 'first run — reboot & reload' : false), regBefore || (reg ? stamp : 'registry unavailable'));
|
||||
|
||||
// ---- 4. web-platform features the ScreenTinker player leans on --------------------------
|
||||
add('serviceWorker in navigator', 'serviceWorker' in navigator, '');
|
||||
add('caches (Cache API)', typeof caches !== 'undefined' && !!caches, '');
|
||||
add('indexedDB', typeof indexedDB !== 'undefined' && !!indexedDB, '');
|
||||
add('WebSocket', typeof WebSocket !== 'undefined', '');
|
||||
add('fetch', typeof fetch !== 'undefined', '');
|
||||
add('<video> element', !!document.createElement('video').canPlayType, 'h264=' + document.createElement('video').canPlayType('video/mp4; codecs="avc1.42E01E"'));
|
||||
add('CSS clamp()', CSS.supports?.('height', 'clamp(1px,1vh,2px)'), 'directory-search keyboard scaling depends on this');
|
||||
|
||||
// ---- 5. can it reach the ScreenTinker server at all -------------------------------------
|
||||
const ORIGIN = 'https://screentinker.com';
|
||||
fetch(ORIGIN + '/api/status', { cache: 'no-store' })
|
||||
.then(r => r.json()).then(d => { add('reach ' + ORIGIN, true, 'version ' + d.version); render(); })
|
||||
.catch(e => { add('reach ' + ORIGIN, false, e.message); render(); });
|
||||
|
||||
function render() {
|
||||
const tb = document.querySelector('#t tbody');
|
||||
tb.innerHTML = rows.map(r =>
|
||||
`<tr><td class="k">${r.name}</td><td class="${cls(r.ok)}">${txt(r.ok)}</td><td style="opacity:.75">${r.detail}</td></tr>`).join('');
|
||||
document.getElementById('sub').textContent = `${model} · BOS ${osv} · ${new Date().toLocaleString()}`;
|
||||
|
||||
const regOk = resolved.registry;
|
||||
document.getElementById('verdict').innerHTML = regOk
|
||||
? '<span class="y">registry resolves</span> — persistence has a home. Next question: does it still resolve when this page is served from a <b>remote https:// URL</b> instead of file:///? Change <span class="k">url:</span> in autorun.brs to the hosted probe and re-run.'
|
||||
: '<span class="n">registry did NOT resolve</span> — check <span class="k">nodejs_enabled: true</span> is set on the widget. If it is set and this still fails, a local shim page owning the registry + postMessage to an iframe is the fallback design.';
|
||||
document.getElementById('reboot').innerHTML =
|
||||
`localStorage marker: <span class="k">${lsBefore || '(just written)'}</span><br>registry marker: <span class="k">${regBefore || '(just written)'}</span>`;
|
||||
}
|
||||
render();
|
||||
</script></body></html>
|
||||
Loading…
Reference in a new issue