mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-29 09:23:16 -06:00
Self-contained examples for the PiP overlay API (POST /api/pip), each with a CSP-safe query-param overlay (external JS), config.example.json, zero runtime deps, an offline test, and a README: - PIP-Announce-Broadcast manual one-shot message to a screen/group - PIP-Weather-Widget Open-Meteo current conditions (keyless) - PIP-Air-Quality Open-Meteo US AQI widget (keyless) - PIP-Crypto-Ticker CoinGecko price strip (keyless) - PIP-News-Ticker scrolling RSS/Atom headlines - PIP-Room-Status-Calendar ICS-driven Available/Busy room sign - PIP-Event-Countdown client-side countdown, auto-clears at zero - PIP-Welcome-Board rotating welcome/birthday cards from CSV - PIP-Fundraiser-Thermometer goal-progress bar from local/URL JSON - PIP-QR-Rotator rotating QR codes, encoded client-side - PIP-Incident-Webhook event-driven: red on firing, clear on resolved Also includes the CAP-AU (NSW RFS) and US NWS/NOAA emergency-alert monitors that push expiry-aware PiP overlays. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
3.9 KiB
JavaScript
73 lines
3.9 KiB
JavaScript
'use strict';
|
|
|
|
// Offline test. No network, no player. Covers:
|
|
// - qr.js pure helpers (entry validation, overlay-uri build/round-trip, rotation wrap)
|
|
// - the embedded QR encoder's Reed-Solomon core, checked against the published QR
|
|
// generator polynomials (degree 7 and 10) — this catches GF(256) math errors without
|
|
// needing a QR decoder — plus structural invariants of a generated matrix.
|
|
const { validateEntries, overlayUri, nextIndex } = require('./qr');
|
|
const QR = require('./qr-overlay');
|
|
|
|
let ok = true;
|
|
function check(name, cond) { console.log(`${cond ? '•' : '✗'} ${name}`); if (!cond) ok = false; }
|
|
|
|
// ---- qr.js pure helpers ----
|
|
const v = validateEntries([
|
|
{ label: 'A', data: 'https://x.test/1' },
|
|
{ label: 'B', data: ' ' }, // blank -> rejected
|
|
{ data: 'WIFI:T:WPA;S:Net;P:pw;;' }, // no label -> ok, label defaults to ''
|
|
{ label: 'C' }, // no data -> rejected
|
|
]);
|
|
check('validateEntries keeps the 2 valid entries', v.entries.length === 2);
|
|
check('validateEntries reports the 2 bad entries', v.errors.length === 2);
|
|
check('validateEntries defaults missing label to ""', v.entries[1].label === '');
|
|
check('validateEntries non-array -> error', validateEntries('nope').errors.length === 1);
|
|
|
|
const entry = { label: 'Guest Wi-Fi & More', data: 'WIFI:T:WPA;S:Lobby Guest;P:p@ss=1;;' };
|
|
const uri = overlayUri('https://s.example.com/qr-overlay.html', entry);
|
|
const back = new URLSearchParams(uri.split('?')[1]);
|
|
check('overlayUri round-trips data exactly', back.get('data') === entry.data);
|
|
check('overlayUri round-trips label exactly', back.get('label') === entry.label);
|
|
check('overlayUri encodes (no raw spaces/&/;)', !/[ &;]/.test(uri.split('?')[1].replace(/&data=|&label=/, '')));
|
|
check('overlayUri joins with & when base already has ?',
|
|
overlayUri('https://s/x?a=1', { data: 'd' }).includes('?a=1&'));
|
|
|
|
check('nextIndex wraps around', nextIndex(2, 3) === 0 && nextIndex(0, 3) === 1 && nextIndex(1, 3) === 2);
|
|
check('nextIndex guards empty list', nextIndex(0, 0) === 0);
|
|
|
|
// ---- Reed-Solomon core vs published QR generator polynomials ----
|
|
// Build GF(256) exp/log tables from the encoder's own multiply, then convert the computed
|
|
// divisor coefficients back to alpha-exponent form to compare with the spec's values.
|
|
const exp = new Array(256), log = new Array(256);
|
|
exp[0] = 1;
|
|
for (let i = 1; i < 256; i++) exp[i] = QR.rsMul(exp[i - 1], 2);
|
|
for (let i = 0; i < 255; i++) log[exp[i]] = i;
|
|
|
|
function toAlpha(coeffs) { return coeffs.map((c) => log[c]); }
|
|
|
|
// Published non-leading generator-polynomial exponents (Thonky / ISO 18004 Annex A).
|
|
const GEN7 = [87, 229, 146, 149, 238, 102, 21];
|
|
const GEN10 = [251, 67, 46, 61, 118, 70, 64, 94, 32, 45];
|
|
const d7 = toAlpha(QR.rsDivisor(7));
|
|
const d10 = toAlpha(QR.rsDivisor(10));
|
|
check('RS generator poly (deg 7) matches spec', JSON.stringify(d7) === JSON.stringify(GEN7));
|
|
check('RS generator poly (deg 10) matches spec', JSON.stringify(d10) === JSON.stringify(GEN10));
|
|
|
|
// ---- encoder structural invariants ----
|
|
const tiny = QR.encodeBytes(QR.utf8Bytes('hi'), 'M'); // tiny -> version 1
|
|
check('tiny payload -> 21x21 (version 1)', tiny.size === 21 && tiny.modules.length === 21);
|
|
// finder patterns: dark outer ring at the three corners, white separator beside them.
|
|
check('top-left finder corner dark', tiny.modules[0][0] === true);
|
|
check('top-left separator light', tiny.modules[0][7] === false);
|
|
check('top-left finder centre dark', tiny.modules[3][3] === true);
|
|
check('top-right finder present', tiny.modules[0][tiny.size - 1] === true);
|
|
check('bottom-left finder present', tiny.modules[tiny.size - 1][0] === true);
|
|
// timing pattern alternates along row/col 6
|
|
check('timing pattern alternates', tiny.modules[6][8] !== tiny.modules[6][9]);
|
|
|
|
const url = QR.encodeBytes(QR.utf8Bytes('https://example.com/menu'), 'M');
|
|
check('longer URL bumps the version (size > 21)', url.size > 21 && (url.size - 17) % 4 === 0);
|
|
|
|
console.log('\nRESULT:', ok ? 'PASS ✅' : 'FAIL ❌');
|
|
process.exit(ok ? 0 : 1);
|