mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
BrightSign: report IPv6, the attached display and the active video mode
Follow-on from the Node-stdlib work, guided by BrightSign's own dev-cookbook
rather than by guessing at module names.
IPv6 costs nothing extra — it comes from the same os.networkInterfaces() call
the v4 address does. The column, the API field and the dashboard card have all
existed since 1.9.29 and no player has ever filled them; the card is written to
appear ONLY when set, precisely so the overwhelmingly v4 fleet does not pay
screen space for an empty row. fe80:: is skipped for the same reason 169.254 is
— a link-local address is scoped to one interface and cannot be dialled from a
laptop across the office. A ULA is kept, because that one is reachable.
The attached display and video mode are new columns, and they answer the first
question anyone asks about a dark sign: which panel is that, and is the player
outputting at all. screen_width/height could not answer it — they are what the
PAGE believes it has, i.e. the widget's own geometry. Our XT245 drives a CX101
at 1920x1200@60 while the page reports its own canvas.
Per telemetry row rather than on `devices`, because a display can be swapped,
unplugged or renegotiated without the player re-registering.
MULTI-OUTPUT: the output is chosen by screen number, not hard-coded. A
dual-output player registers ONE DEVICE ROW PER OUTPUT (?screen=N →
output_index), so each row must report its own panel — otherwise a box driving
a lobby TV and a menu board shows the lobby TV twice. Both naming forms are
tried: probed on hardware, "hdmi" and "HDMI-1" both resolve to output 1, while
a second output that does not exist fails cleanly ("hdmi2" throws from the
constructor, "HDMI-2" rejects), so a single-output player reports nothing
rather than inventing a screen. That case has its own test.
Dashboard: two cards, shown only when the player reports them, like every other
card in that block.
Verified end to end on the real XT245 (FW 9.1.93.2) — attached_display=CX101,
video_mode=1920x1200@60, alongside local_ip 192.168.1.46, 119616 MB disk,
3656 MB RAM and live CPU.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bvjey4FNam49MN7ybjcq6A
This commit is contained in:
parent
def30e6d39
commit
08b3d7d404
|
|
@ -47,6 +47,8 @@
|
||||||
*/
|
*/
|
||||||
var VideoModeConfigClass = tryRequire('@brightsign/videomodeconfiguration');
|
var VideoModeConfigClass = tryRequire('@brightsign/videomodeconfiguration');
|
||||||
var CecClass = tryRequire('@brightsign/cec');
|
var CecClass = tryRequire('@brightsign/cec');
|
||||||
|
// Reads the attached display's EDID. Read-only; the mode setter is videomodeconfiguration.
|
||||||
|
var VideoOutputClass = tryRequire('@brightsign/videooutput');
|
||||||
/*
|
/*
|
||||||
* Node's standard library, present because the widget is created with nodejs_enabled. Used for
|
* Node's standard library, present because the widget is created with nodejs_enabled. Used for
|
||||||
* the LAN address (see refreshTelemetry) exactly as BrightSign's own dev-cookbook templates do.
|
* the LAN address (see refreshTelemetry) exactly as BrightSign's own dev-cookbook templates do.
|
||||||
|
|
@ -918,21 +920,95 @@
|
||||||
try {
|
try {
|
||||||
var ifaces = osModule.networkInterfaces() || {};
|
var ifaces = osModule.networkInterfaces() || {};
|
||||||
var names = Object.keys(ifaces);
|
var names = Object.keys(ifaces);
|
||||||
for (var ni = 0; ni < names.length && !telemetry.local_ip; ni++) {
|
for (var ni = 0; ni < names.length; ni++) {
|
||||||
var addrs = ifaces[names[ni]] || [];
|
var addrs = ifaces[names[ni]] || [];
|
||||||
for (var ai = 0; ai < addrs.length; ai++) {
|
for (var ai = 0; ai < addrs.length; ai++) {
|
||||||
var a = addrs[ai];
|
var a = addrs[ai];
|
||||||
if (!a || a.internal) continue;
|
if (!a || a.internal) continue;
|
||||||
if (a.family !== 'IPv4' && a.family !== 4) continue;
|
|
||||||
var ip = String(a.address || '');
|
var ip = String(a.address || '');
|
||||||
if (!ip || ip.indexOf('169.254.') === 0) continue;
|
if (!ip) continue;
|
||||||
telemetry.local_ip = ip;
|
var isV4 = (a.family === 'IPv4' || a.family === 4);
|
||||||
break;
|
var isV6 = (a.family === 'IPv6' || a.family === 6);
|
||||||
|
if (isV4 && !telemetry.local_ip && ip.indexOf('169.254.') !== 0) telemetry.local_ip = ip;
|
||||||
|
/*
|
||||||
|
* The v6 column has existed as long as the v4 one and has never held anything, on any
|
||||||
|
* player. The dashboard is already built for it — it renders a second card ONLY when
|
||||||
|
* this is set, precisely so the overwhelmingly v4 fleet does not pay screen space for
|
||||||
|
* an empty row.
|
||||||
|
*
|
||||||
|
* fe80:: is skipped for the same reason 169.254 is: a link-local address is scoped to
|
||||||
|
* one interface and cannot be dialled from a laptop across the office, so reporting it
|
||||||
|
* would send someone somewhere they cannot go. A ULA (fd00::/8) is kept — that IS
|
||||||
|
* reachable on the site network, which is the question this field answers.
|
||||||
|
*/
|
||||||
|
if (isV6 && !telemetry.local_ip6 && ip.toLowerCase().indexOf('fe80') !== 0) {
|
||||||
|
// Node appends a zone id to link-locals ("fe80::1%eth0"); strip any that survives.
|
||||||
|
var pct = ip.indexOf('%');
|
||||||
|
telemetry.local_ip6 = pct === -1 ? ip : ip.slice(0, pct);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) { /* no networking yet, or a firmware without it — stay silent */ }
|
} catch (e) { /* no networking yet, or a firmware without it — stay silent */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* WHICH SCREEN IS PLUGGED IN, and what the output is actually driving.
|
||||||
|
*
|
||||||
|
* The first question about a dark sign is "which panel is that?", and until now the dashboard
|
||||||
|
* could not answer it: screen_width/height are what the PAGE believes it has, which is the
|
||||||
|
* widget's own geometry, not what the hardware negotiated with the display.
|
||||||
|
*
|
||||||
|
* The output is chosen by SCREEN NUMBER, because a dual-output player registers one device
|
||||||
|
* row per output (?screen=N, see output_index) and each row must report its OWN panel — a box
|
||||||
|
* driving a lobby TV and a menu board would otherwise show the lobby TV twice.
|
||||||
|
*
|
||||||
|
* Both names are tried. Probed on an XT245 (FW 9.1.93.2): "hdmi" and "HDMI-1" both resolve to
|
||||||
|
* output 1 and answer with the same monitor, while a second output that does not exist fails
|
||||||
|
* cleanly — "hdmi2" throws from the constructor and "HDMI-2" rejects. So a single-output
|
||||||
|
* player simply reports nothing here rather than inventing a screen.
|
||||||
|
*/
|
||||||
|
if (VideoOutputClass) {
|
||||||
|
var wantScreen = screenNumber();
|
||||||
|
var outNames = ['HDMI-' + wantScreen];
|
||||||
|
if (wantScreen === 1) outNames.push('hdmi');
|
||||||
|
for (var oi = 0; oi < outNames.length; oi++) {
|
||||||
|
try {
|
||||||
|
var vo = new VideoOutputClass(outNames[oi]);
|
||||||
|
if (!vo || typeof vo.getEdidIdentity !== 'function') continue;
|
||||||
|
var edid = vo.getEdidIdentity();
|
||||||
|
if (edid && typeof edid.then === 'function') {
|
||||||
|
edid.then(function (e) {
|
||||||
|
var mn = e && (e.monitorName || e.monitor_name);
|
||||||
|
if (typeof mn === 'string' && mn.trim()) telemetry.attached_display = mn.trim();
|
||||||
|
}, function () { /* no display on this output */ });
|
||||||
|
}
|
||||||
|
} catch (e) { /* no such output on this model */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The mode the output is negotiated to, which is not the same as the widget's size. Reported
|
||||||
|
* as WxH@Hz so it reads the way an installer would say it out loud. Our XT245 answers
|
||||||
|
* 1920x1200@60 — the panel's native mode, while the page reports its own 1920x1080 canvas.
|
||||||
|
*/
|
||||||
|
if (VideoModeConfigClass) {
|
||||||
|
try {
|
||||||
|
var vmc = new VideoModeConfigClass();
|
||||||
|
if (vmc && typeof vmc.getActiveMode === 'function') {
|
||||||
|
var mode = vmc.getActiveMode();
|
||||||
|
if (mode && typeof mode.then === 'function') {
|
||||||
|
mode.then(function (m) {
|
||||||
|
if (!m) return;
|
||||||
|
var w = m.graphicsPlaneWidth || m.width;
|
||||||
|
var h = m.graphicsPlaneHeight || m.height;
|
||||||
|
var f = m.frequency || m.refreshRate;
|
||||||
|
if (w && h) telemetry.video_mode = w + 'x' + h + (f ? '@' + f : '');
|
||||||
|
}, function () { /* mode not readable on this firmware */ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* older OS without the call */ }
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Memory, load and REAL uptime — all from the same Node standard library the address above
|
* Memory, load and REAL uptime — all from the same Node standard library the address above
|
||||||
* came from, and all previously NULL on every BrightSign in the fleet.
|
* came from, and all previously NULL on every BrightSign in the fleet.
|
||||||
|
|
|
||||||
|
|
@ -488,6 +488,8 @@ export default {
|
||||||
'device.info.os_version': 'OS Version',
|
'device.info.os_version': 'OS Version',
|
||||||
'device.info.serial': 'Serial',
|
'device.info.serial': 'Serial',
|
||||||
'device.info.temperature': 'Temperature',
|
'device.info.temperature': 'Temperature',
|
||||||
|
'device.info.attached_display': 'Attached display',
|
||||||
|
'device.info.video_mode': 'Video mode',
|
||||||
'device.info.output_n': '(output {n})',
|
'device.info.output_n': '(output {n})',
|
||||||
'device.info.player_type': 'Player Type',
|
'device.info.player_type': 'Player Type',
|
||||||
'device.info.web_player': 'Web Player',
|
'device.info.web_player': 'Web Player',
|
||||||
|
|
|
||||||
|
|
@ -565,6 +565,20 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
<div class="info-card-label">${t('device.info.temperature')}</div>
|
<div class="info-card-label">${t('device.info.temperature')}</div>
|
||||||
<div class="info-card-value small" id="telTemp">${latestTelemetry.temperature_c}°C</div>
|
<div class="info-card-value small" id="telTemp">${latestTelemetry.temperature_c}°C</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
<!-- The physical panel, from its EDID, and the mode the output is negotiated to. Shown
|
||||||
|
only when the player reports them, like every other card here: a family that cannot
|
||||||
|
read its own output must not grow an empty row. On a dual-output player each device
|
||||||
|
row is one output, so this is THAT output's screen — not the box's first. -->
|
||||||
|
${latestTelemetry.attached_display ? `
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-card-label">${t('device.info.attached_display')}</div>
|
||||||
|
<div class="info-card-value small" id="telDisplay">${esc(latestTelemetry.attached_display)}</div>
|
||||||
|
</div>` : ''}
|
||||||
|
${latestTelemetry.video_mode ? `
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-card-label">${t('device.info.video_mode')}</div>
|
||||||
|
<div class="info-card-value small" id="telVideoMode">${esc(latestTelemetry.video_mode)}</div>
|
||||||
|
</div>` : ''}
|
||||||
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-card-label">${t('device.info.wifi')}</div>
|
<div class="info-card-label">${t('device.info.wifi')}</div>
|
||||||
|
|
|
||||||
|
|
@ -388,6 +388,16 @@ const migrations = [
|
||||||
// dual-stack panel genuinely has both and an operator may need either — collapsing them would
|
// dual-stack panel genuinely has both and an operator may need either — collapsing them would
|
||||||
// make the field mean "whichever we happened to enumerate first".
|
// make the field mean "whichever we happened to enumerate first".
|
||||||
"ALTER TABLE device_telemetry ADD COLUMN local_ip6 TEXT",
|
"ALTER TABLE device_telemetry ADD COLUMN local_ip6 TEXT",
|
||||||
|
// What is physically PLUGGED IN, read from the display's EDID, and the mode actually being
|
||||||
|
// driven. A signage operator's first question about a dark screen is which panel it is and
|
||||||
|
// whether the player is outputting at all — the dashboard could say neither, and
|
||||||
|
// screen_width/height are what the PAGE thinks it has, not what the hardware negotiated.
|
||||||
|
//
|
||||||
|
// Per-telemetry-row rather than on `devices` because a display can be swapped, unplugged or
|
||||||
|
// renegotiated without the player re-registering, and because a dual-output player registers ONE
|
||||||
|
// ROW PER OUTPUT (see output_index) — each row must carry its own screen, not the box's first.
|
||||||
|
"ALTER TABLE device_telemetry ADD COLUMN attached_display TEXT",
|
||||||
|
"ALTER TABLE device_telemetry ADD COLUMN video_mode TEXT",
|
||||||
// Panel temperature in Celsius. REAL because the sensor reports fractions, and nullable because
|
// Panel temperature in Celsius. REAL because the sensor reports fractions, and nullable because
|
||||||
// only some hardware exposes one — Android and the browser players send nothing and must keep
|
// only some hardware exposes one — Android and the browser players send nothing and must keep
|
||||||
// reading as "no sensor" rather than "0 degrees", which is why every read site treats null as
|
// reading as "no sensor" rather than "0 degrees", which is why every read site treats null as
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ router.get('/', (req, res) => {
|
||||||
const devices = db.prepare(`
|
const devices = db.prepare(`
|
||||||
SELECT d.*,
|
SELECT d.*,
|
||||||
t.battery_level, t.battery_charging, t.storage_free_mb, t.storage_total_mb,
|
t.battery_level, t.battery_charging, t.storage_free_mb, t.storage_total_mb,
|
||||||
t.ram_free_mb, t.ram_total_mb, t.wifi_ssid, t.wifi_rssi, t.uptime_seconds, t.local_ip, t.local_ip6,
|
t.ram_free_mb, t.ram_total_mb, t.wifi_ssid, t.wifi_rssi, t.uptime_seconds, t.local_ip, t.local_ip6, t.attached_display, t.video_mode,
|
||||||
t.cpu_usage,
|
t.cpu_usage,
|
||||||
s.filepath as screenshot_path, s.captured_at as screenshot_at,
|
s.filepath as screenshot_path, s.captured_at as screenshot_at,
|
||||||
u.email as owner_email, u.name as owner_name
|
u.email as owner_email, u.name as owner_name
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const path = require('node:path');
|
||||||
const SRC = fs.readFileSync(path.join(__dirname, '..', '..', 'brightsign', 'st-bridge.js'), 'utf8');
|
const SRC = fs.readFileSync(path.join(__dirname, '..', '..', 'brightsign', 'st-bridge.js'), 'utf8');
|
||||||
|
|
||||||
/** Load the bridge into a fake window. `mods` present => pretend we are on a BrightSign. */
|
/** Load the bridge into a fake window. `mods` present => pretend we are on a BrightSign. */
|
||||||
function load({ search = '', mods = null, ua = 'Mozilla/5.0 Chrome/150', seed = {}, storageEstimate = null, temperature = null, os = null, fs = null } = {}) {
|
function load({ search = '', mods = null, ua = 'Mozilla/5.0 Chrome/150', seed = {}, storageEstimate = null, temperature = null, os = null, fs = null, edid = null, activeMode = null } = {}) {
|
||||||
const posted = [];
|
const posted = [];
|
||||||
const registryStore = new Map(Object.entries(seed));
|
const registryStore = new Map(Object.entries(seed));
|
||||||
const cec = { sent: [] };
|
const cec = { sent: [] };
|
||||||
|
|
@ -70,6 +70,23 @@ function load({ search = '', mods = null, ua = 'Mozilla/5.0 Chrome/150', seed =
|
||||||
// Not an @brightsign module, so it is answered before the platform ones.
|
// Not an @brightsign module, so it is answered before the platform ones.
|
||||||
if (name === 'os') { if (!os) throw new Error("Cannot find module 'os'"); return os; }
|
if (name === 'os') { if (!os) throw new Error("Cannot find module 'os'"); return os; }
|
||||||
if (name === 'fs') { if (!fs) throw new Error("Cannot find module 'fs'"); return fs; }
|
if (name === 'fs') { if (!fs) throw new Error("Cannot find module 'fs'"); return fs; }
|
||||||
|
// The attached panel's EDID, per OUTPUT. `edid` maps an output name to a monitor name;
|
||||||
|
// anything not in it behaves like a real player asked for an output it does not have —
|
||||||
|
// "hdmi2" throws from the constructor, "HDMI-2" rejects. Both observed on an XT245.
|
||||||
|
if (name === '@brightsign/videooutput') {
|
||||||
|
if (!edid) throw new Error('no videooutput');
|
||||||
|
return function (outputName) {
|
||||||
|
if (!(outputName in edid)) {
|
||||||
|
if (/^hdmi\d/.test(outputName)) throw new Error('no such output');
|
||||||
|
return { getEdidIdentity: () => Promise.reject(new Error('Output not connected')) };
|
||||||
|
}
|
||||||
|
return { getEdidIdentity: () => Promise.resolve({ monitorName: edid[outputName] }) };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (name === '@brightsign/videomodeconfiguration') {
|
||||||
|
if (!activeMode) throw new Error('no videomodeconfiguration');
|
||||||
|
return function () { return { getActiveMode: () => Promise.resolve(activeMode) }; };
|
||||||
|
}
|
||||||
if (name === '@brightsign/messageport') {
|
if (name === '@brightsign/messageport') {
|
||||||
return function () {
|
return function () {
|
||||||
return {
|
return {
|
||||||
|
|
@ -557,3 +574,69 @@ test('a firmware without statfs degrades instead of throwing', () => {
|
||||||
assert.doesNotThrow(() => api.refreshTelemetry());
|
assert.doesNotThrow(() => api.refreshTelemetry());
|
||||||
assert.equal(api.telemetrySnapshot().local_ip, '192.168.1.46', 'and the rest still reports');
|
assert.equal(api.telemetrySnapshot().local_ip, '192.168.1.46', 'and the rest still reports');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Which screen is plugged in, and what the output is driving.
|
||||||
|
//
|
||||||
|
// screen_width/height are what the PAGE believes it has — the widget's own geometry. They say
|
||||||
|
// nothing about the panel. Our XT245 drives a CX101 at 1920x1200@60 while the page reports its own
|
||||||
|
// canvas, so an operator asking "which display is that and is it even outputting?" had no answer.
|
||||||
|
//
|
||||||
|
// The output is chosen by SCREEN NUMBER because a dual-output player registers one device row per
|
||||||
|
// output (?screen=N → output_index), and each row must report its own panel.
|
||||||
|
|
||||||
|
const flush = () => new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
|
test('the attached display is read from EDID', async () => {
|
||||||
|
const { api } = load({ mods: true, os: OS_STUB, edid: { 'HDMI-1': 'CX101' } });
|
||||||
|
api.refreshTelemetry();
|
||||||
|
await flush();
|
||||||
|
assert.equal(api.telemetrySnapshot().attached_display, 'CX101');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MULTI-SCREEN: each output reports its OWN panel, not the box\'s first', async () => {
|
||||||
|
// The bug this prevents: a player driving a lobby TV and a menu board showing the lobby TV twice.
|
||||||
|
const wiring = { edid: { 'HDMI-1': 'Lobby-55', 'HDMI-2': 'MenuBoard-32' } };
|
||||||
|
const one = load({ mods: true, os: OS_STUB, search: '?screen=1', ...wiring });
|
||||||
|
const two = load({ mods: true, os: OS_STUB, search: '?screen=2', ...wiring });
|
||||||
|
one.api.refreshTelemetry();
|
||||||
|
two.api.refreshTelemetry();
|
||||||
|
await flush();
|
||||||
|
assert.equal(one.api.telemetrySnapshot().attached_display, 'Lobby-55');
|
||||||
|
assert.equal(two.api.telemetrySnapshot().attached_display, 'MenuBoard-32');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a single-output player reports nothing rather than inventing a second screen', async () => {
|
||||||
|
// Verified on hardware: "hdmi2" throws from the constructor and "HDMI-2" rejects.
|
||||||
|
const { api } = load({ mods: true, os: OS_STUB, search: '?screen=2', edid: { 'HDMI-1': 'CX101' } });
|
||||||
|
assert.doesNotThrow(() => api.refreshTelemetry());
|
||||||
|
await flush();
|
||||||
|
assert.equal(api.telemetrySnapshot().attached_display, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screen 1 also accepts the lowercase name the vendor cookbook uses', async () => {
|
||||||
|
const { api } = load({ mods: true, os: OS_STUB, edid: { hdmi: 'CX101' } });
|
||||||
|
api.refreshTelemetry();
|
||||||
|
await flush();
|
||||||
|
assert.equal(api.telemetrySnapshot().attached_display, 'CX101');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the active mode is reported as WxH@Hz, the way an installer says it', async () => {
|
||||||
|
const { api } = load({
|
||||||
|
mods: true, os: OS_STUB,
|
||||||
|
activeMode: { graphicsPlaneWidth: 1920, graphicsPlaneHeight: 1200, frequency: 60 },
|
||||||
|
});
|
||||||
|
api.refreshTelemetry();
|
||||||
|
await flush();
|
||||||
|
assert.equal(api.telemetrySnapshot().video_mode, '1920x1200@60');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a firmware with neither module degrades quietly', async () => {
|
||||||
|
const { api } = load({ mods: true, os: OS_STUB });
|
||||||
|
assert.doesNotThrow(() => api.refreshTelemetry());
|
||||||
|
await flush();
|
||||||
|
const t = api.telemetrySnapshot();
|
||||||
|
assert.equal(t.attached_display, undefined);
|
||||||
|
assert.equal(t.video_mode, undefined);
|
||||||
|
assert.equal(t.local_ip, '192.168.1.46', 'and everything else still reports');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -238,3 +238,16 @@ test('a browser tab gains nothing — it measures none of this', () => {
|
||||||
assert.equal(has(html, 'telRam'), false);
|
assert.equal(has(html, 'telRam'), false);
|
||||||
assert.equal(has(html, 'telCpu'), false);
|
assert.equal(has(html, 'telCpu'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the attached display and video mode get cards when reported', () => {
|
||||||
|
const html = renderWith(BS_WITH_DATA, { ...REAL_TELEMETRY, attached_display: 'CX101', video_mode: '1920x1200@60' });
|
||||||
|
assert.ok(has(html, 'telDisplay'), 'the panel EDID card');
|
||||||
|
assert.ok(has(html, 'telVideoMode'), 'the negotiated mode card');
|
||||||
|
assert.ok(html.includes('CX101'), 'and the monitor name itself');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a player that cannot read its output grows no empty rows', () => {
|
||||||
|
const html = renderWith(BS_WITH_DATA, REAL_TELEMETRY);
|
||||||
|
assert.equal(has(html, 'telDisplay'), false);
|
||||||
|
assert.equal(has(html, 'telVideoMode'), false);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1202,8 +1202,9 @@ module.exports = function setupDeviceSocket(io) {
|
||||||
if (telemetry && deviceExists(device_id)) {
|
if (telemetry && deviceExists(device_id)) {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO device_telemetry (device_id, battery_level, battery_charging, storage_free_mb, storage_total_mb,
|
INSERT INTO device_telemetry (device_id, battery_level, battery_charging, storage_free_mb, storage_total_mb,
|
||||||
ram_free_mb, ram_total_mb, cpu_usage, wifi_ssid, wifi_rssi, uptime_seconds, local_ip, local_ip6, temperature_c)
|
ram_free_mb, ram_total_mb, cpu_usage, wifi_ssid, wifi_rssi, uptime_seconds, local_ip, local_ip6, temperature_c,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
attached_display, video_mode)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(
|
`).run(
|
||||||
device_id,
|
device_id,
|
||||||
telemetry.battery_level ?? null,
|
telemetry.battery_level ?? null,
|
||||||
|
|
@ -1225,7 +1226,12 @@ module.exports = function setupDeviceSocket(io) {
|
||||||
// Only a finite number is a reading. A panel with no sensor sends nothing, and NaN or
|
// Only a finite number is a reading. A panel with no sensor sends nothing, and NaN or
|
||||||
// Infinity from a flaky one must land as "no reading" rather than poisoning the column.
|
// Infinity from a flaky one must land as "no reading" rather than poisoning the column.
|
||||||
typeof telemetry.temperature_c === 'number' && Number.isFinite(telemetry.temperature_c)
|
typeof telemetry.temperature_c === 'number' && Number.isFinite(telemetry.temperature_c)
|
||||||
? telemetry.temperature_c : null
|
? telemetry.temperature_c : null,
|
||||||
|
// Free text from the panel's EDID and the mode the output is driving. Trimmed and
|
||||||
|
// bounded like the address fields above: this is a string the DISPLAY chose, not one
|
||||||
|
// we control, and a monitor with a silly name must not be able to grow the row.
|
||||||
|
typeof telemetry.attached_display === 'string' ? telemetry.attached_display.trim().slice(0, 64) || null : null,
|
||||||
|
typeof telemetry.video_mode === 'string' ? telemetry.video_mode.trim().slice(0, 32) || null : null
|
||||||
);
|
);
|
||||||
pruneTelemetry(device_id);
|
pruneTelemetry(device_id);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue