@agentdeck/bridge 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/fm-helper/agentdeck-fm-helper +0 -0
- package/dist/apme/collector.d.ts +10 -0
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +39 -10
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/dashboard-html.d.ts.map +1 -1
- package/dist/apme/dashboard-html.js +289 -0
- package/dist/apme/dashboard-html.js.map +1 -1
- package/dist/apme/graph.d.ts +48 -0
- package/dist/apme/graph.d.ts.map +1 -0
- package/dist/apme/graph.js +226 -0
- package/dist/apme/graph.js.map +1 -0
- package/dist/apme/http.d.ts.map +1 -1
- package/dist/apme/http.js +52 -0
- package/dist/apme/http.js.map +1 -1
- package/dist/apme/judge-detect.d.ts.map +1 -1
- package/dist/apme/judge-detect.js +26 -2
- package/dist/apme/judge-detect.js.map +1 -1
- package/dist/apme/store.d.ts +69 -1
- package/dist/apme/store.d.ts.map +1 -1
- package/dist/apme/store.js +238 -4
- package/dist/apme/store.js.map +1 -1
- package/dist/apme/types.d.ts +1 -1
- package/dist/apme/types.d.ts.map +1 -1
- package/dist/apme/types.js.map +1 -1
- package/dist/ble-sync-spawn.d.ts +25 -2
- package/dist/ble-sync-spawn.d.ts.map +1 -1
- package/dist/ble-sync-spawn.js +38 -2
- package/dist/ble-sync-spawn.js.map +1 -1
- package/dist/ble-sync-status.d.ts +77 -0
- package/dist/ble-sync-status.d.ts.map +1 -0
- package/dist/ble-sync-status.js +157 -0
- package/dist/ble-sync-status.js.map +1 -0
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +7 -1
- package/dist/bridge-core.js.map +1 -1
- package/dist/cli.js +0 -0
- package/dist/codex-rate-limits-live.d.ts +60 -0
- package/dist/codex-rate-limits-live.d.ts.map +1 -0
- package/dist/codex-rate-limits-live.js +282 -0
- package/dist/codex-rate-limits-live.js.map +1 -0
- package/dist/d200h/hid-protocol.d.ts +62 -0
- package/dist/d200h/hid-protocol.d.ts.map +1 -0
- package/dist/d200h/hid-protocol.js +124 -0
- package/dist/d200h/hid-protocol.js.map +1 -0
- package/dist/d200h/image-renderer.d.ts +24 -0
- package/dist/d200h/image-renderer.d.ts.map +1 -0
- package/dist/d200h/image-renderer.js +321 -0
- package/dist/d200h/image-renderer.js.map +1 -0
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +79 -0
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/hook-migration.d.ts +2 -0
- package/dist/hook-migration.d.ts.map +1 -0
- package/dist/hook-migration.js +116 -0
- package/dist/hook-migration.js.map +1 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +8 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -1
- package/dist/idotmatrix/idotmatrix-daemon-sync.js +19 -3
- package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
- package/dist/modules/d200h-module.d.ts +65 -0
- package/dist/modules/d200h-module.d.ts.map +1 -0
- package/dist/modules/d200h-module.js +495 -0
- package/dist/modules/d200h-module.js.map +1 -0
- package/dist/modules/d200h-renderer.d.ts +7 -0
- package/dist/modules/d200h-renderer.d.ts.map +1 -0
- package/dist/modules/d200h-renderer.js +321 -0
- package/dist/modules/d200h-renderer.js.map +1 -0
- package/dist/modules/idotmatrix-module.d.ts.map +1 -1
- package/dist/modules/idotmatrix-module.js +12 -1
- package/dist/modules/idotmatrix-module.js.map +1 -1
- package/dist/modules/timebox-module.d.ts.map +1 -1
- package/dist/modules/timebox-module.js +12 -1
- package/dist/modules/timebox-module.js.map +1 -1
- package/dist/modules/trmnl-module.d.ts +38 -0
- package/dist/modules/trmnl-module.d.ts.map +1 -0
- package/dist/modules/trmnl-module.js +111 -0
- package/dist/modules/trmnl-module.js.map +1 -0
- package/dist/timebox/timebox-daemon-sync.d.ts +8 -0
- package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -1
- package/dist/timebox/timebox-daemon-sync.js +29 -1
- package/dist/timebox/timebox-daemon-sync.js.map +1 -1
- package/dist/timeline-summarizer.d.ts +12 -2
- package/dist/timeline-summarizer.d.ts.map +1 -1
- package/dist/timeline-summarizer.js +66 -4
- package/dist/timeline-summarizer.js.map +1 -1
- package/dist/trmnl/byos-server.d.ts +39 -0
- package/dist/trmnl/byos-server.d.ts.map +1 -0
- package/dist/trmnl/byos-server.js +301 -0
- package/dist/trmnl/byos-server.js.map +1 -0
- package/dist/trmnl/frame-cache.d.ts +37 -0
- package/dist/trmnl/frame-cache.d.ts.map +1 -0
- package/dist/trmnl/frame-cache.js +157 -0
- package/dist/trmnl/frame-cache.js.map +1 -0
- package/dist/trmnl/image-renderer.d.ts +27 -0
- package/dist/trmnl/image-renderer.d.ts.map +1 -0
- package/dist/trmnl/image-renderer.js +235 -0
- package/dist/trmnl/image-renderer.js.map +1 -0
- package/dist/trmnl/trmnl-settings.d.ts +96 -0
- package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
- package/dist/trmnl/trmnl-settings.js +177 -0
- package/dist/trmnl/trmnl-settings.js.map +1 -0
- package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
- package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
- package/dist/trmnl/trmnl-telemetry.js +56 -0
- package/dist/trmnl/trmnl-telemetry.js.map +1 -0
- package/dist/whisper-server-manager.d.ts +19 -0
- package/dist/whisper-server-manager.d.ts.map +1 -0
- package/dist/whisper-server-manager.js +171 -0
- package/dist/whisper-server-manager.js.map +1 -0
- package/package.json +13 -14
- package/src/idotmatrix/sync.py +16 -1
- package/src/pysync/matrix_sync_common.py +83 -0
- package/src/timebox/sync_ble.py +16 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRMNL BYOS settings — reads/writes the `trmnl` block in
|
|
3
|
+
* ~/.agentdeck/settings.json, preserving all other keys (same pattern as
|
|
4
|
+
* pixoo-settings.ts).
|
|
5
|
+
*
|
|
6
|
+
* Shape:
|
|
7
|
+
* "trmnl": {
|
|
8
|
+
* "enabled": false, // force the module on even with no device yet
|
|
9
|
+
* "refreshRate": 180, // seconds between device polls
|
|
10
|
+
* "autoRegister": true, // auto-enroll a device that hits /api/setup
|
|
11
|
+
* "devices": [ { mac, apiKey, friendlyId, name? } ]
|
|
12
|
+
* }
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { homedir } from 'os';
|
|
17
|
+
import { randomBytes } from 'crypto';
|
|
18
|
+
// Resolve lazily so tests (and explicit pinning) can redirect via
|
|
19
|
+
// AGENTDECK_DATA_DIR, matching apme/settings.ts and esp32-serial.ts.
|
|
20
|
+
function settingsDir() {
|
|
21
|
+
return process.env.AGENTDECK_DATA_DIR || join(homedir(), '.agentdeck');
|
|
22
|
+
}
|
|
23
|
+
function settingsPath() {
|
|
24
|
+
return join(settingsDir(), 'settings.json');
|
|
25
|
+
}
|
|
26
|
+
export const TRMNL_DEFAULT_REFRESH = 180;
|
|
27
|
+
/**
|
|
28
|
+
* Cadence used only while an agent is AWAITING the user. NOT used for "working":
|
|
29
|
+
* a battery e-ink panel deep-sleeps between polls and full-flashes the screen on
|
|
30
|
+
* every refresh, so we only tighten the loop when the user actually needs to act.
|
|
31
|
+
*/
|
|
32
|
+
export const TRMNL_DEFAULT_REFRESH_ACTIVE = 60;
|
|
33
|
+
/** Floor on any cadence — too-frequent polls drain the panel battery + flash it. */
|
|
34
|
+
export const TRMNL_MIN_REFRESH = 30;
|
|
35
|
+
/**
|
|
36
|
+
* Seconds the firmware waits for the image download before giving up (its
|
|
37
|
+
* `image_url_timeout`). Generous so a slow/flaky WiFi link doesn't trip the
|
|
38
|
+
* device's "not responding" (WIFI_FAILED) screen. Firmware caps at ~65s, so we
|
|
39
|
+
* keep a healthy default (most "not responding" cycles are a lossy image GET
|
|
40
|
+
* timing out, not a server error — see docs/devices.md TRMNL section).
|
|
41
|
+
*/
|
|
42
|
+
export const TRMNL_DEFAULT_IMAGE_TIMEOUT = 50;
|
|
43
|
+
/** Hard cap the firmware honors (it clamps `image_url_timeout` ~65s internally). */
|
|
44
|
+
export const TRMNL_MAX_IMAGE_TIMEOUT = 65;
|
|
45
|
+
/**
|
|
46
|
+
* RSSI (dBm) at or below which the panel's WiFi link is treated as weak/lossy.
|
|
47
|
+
* A weak link is the dominant real-world cause of "not responding" (WIFI_FAILED):
|
|
48
|
+
* the image download drops packets and times out. RSSI is sent by the firmware on
|
|
49
|
+
* every poll, so we can react per-poll. (-78 ≈ marginal; below it, retransmits
|
|
50
|
+
* stack up and the default window can be too tight.)
|
|
51
|
+
*/
|
|
52
|
+
export const TRMNL_WEAK_RSSI_DBM = -78;
|
|
53
|
+
/** Image-download window served on a weak link — near the firmware cap. */
|
|
54
|
+
export const TRMNL_WEAK_LINK_IMAGE_TIMEOUT = 60;
|
|
55
|
+
/**
|
|
56
|
+
* Cadence for a poll. We only speed up for AWAITING (the user needs to act);
|
|
57
|
+
* "working" stays on the slow cadence because a deep-sleep e-ink panel can't be
|
|
58
|
+
* pushed and each wake full-flashes the screen + costs battery.
|
|
59
|
+
*/
|
|
60
|
+
export function effectiveRefreshRate(cfg, activity) {
|
|
61
|
+
return Math.max(TRMNL_MIN_REFRESH, activity.awaiting > 0 ? cfg.refreshActive : cfg.refreshRate);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Image-download timeout (seconds) for a single poll. Widens toward the firmware
|
|
65
|
+
* cap when the panel reports a weak WiFi signal, so a lossy image GET still has
|
|
66
|
+
* the maximum window to finish before the firmware shows "not responding"
|
|
67
|
+
* (WIFI_FAILED) — the #1 source of TRMNL flicker on a marginal link. A strong link
|
|
68
|
+
* keeps the lower default so a genuinely dead link doesn't hold the radio on
|
|
69
|
+
* (battery). RSSI absent (older firmware) ⇒ default; the default itself is already
|
|
70
|
+
* generous. Clamped to [5, firmware cap].
|
|
71
|
+
*/
|
|
72
|
+
export function effectiveImageTimeout(cfg, opts = {}) {
|
|
73
|
+
let t = cfg.imageUrlTimeout;
|
|
74
|
+
if (opts.rssi != null && Number.isFinite(opts.rssi) && opts.rssi <= TRMNL_WEAK_RSSI_DBM) {
|
|
75
|
+
t = Math.max(t, TRMNL_WEAK_LINK_IMAGE_TIMEOUT);
|
|
76
|
+
}
|
|
77
|
+
return Math.min(TRMNL_MAX_IMAGE_TIMEOUT, Math.max(5, Math.floor(t)));
|
|
78
|
+
}
|
|
79
|
+
function readSettings() {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(readFileSync(settingsPath(), 'utf-8'));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function writeSettings(settings) {
|
|
88
|
+
mkdirSync(settingsDir(), { recursive: true });
|
|
89
|
+
writeFileSync(settingsPath(), JSON.stringify(settings, null, 2) + '\n');
|
|
90
|
+
}
|
|
91
|
+
/** Load the trmnl config with defaults filled in. */
|
|
92
|
+
export function loadTrmnlConfig() {
|
|
93
|
+
const raw = (readSettings().trmnl ?? {});
|
|
94
|
+
const refreshRate = typeof raw.refreshRate === 'number' && raw.refreshRate >= 5 ? Math.floor(raw.refreshRate) : TRMNL_DEFAULT_REFRESH;
|
|
95
|
+
const refreshActive = typeof raw.refreshActive === 'number' && raw.refreshActive >= 5
|
|
96
|
+
? Math.floor(raw.refreshActive)
|
|
97
|
+
: TRMNL_DEFAULT_REFRESH_ACTIVE;
|
|
98
|
+
const imageUrlTimeout = typeof raw.imageUrlTimeout === 'number' && raw.imageUrlTimeout > 0
|
|
99
|
+
? Math.min(TRMNL_MAX_IMAGE_TIMEOUT, Math.floor(raw.imageUrlTimeout))
|
|
100
|
+
: TRMNL_DEFAULT_IMAGE_TIMEOUT;
|
|
101
|
+
return {
|
|
102
|
+
enabled: raw.enabled === true,
|
|
103
|
+
refreshRate,
|
|
104
|
+
refreshActive,
|
|
105
|
+
imageUrlTimeout,
|
|
106
|
+
autoRegister: raw.autoRegister !== false, // default on
|
|
107
|
+
devices: Array.isArray(raw.devices) ? raw.devices.filter((d) => d && typeof d.mac === 'string') : [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function saveTrmnlConfig(cfg) {
|
|
111
|
+
const settings = readSettings();
|
|
112
|
+
settings.trmnl = cfg;
|
|
113
|
+
writeSettings(settings);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Normalize a MAC to a stable identity key. A canonical 12-hex address becomes
|
|
117
|
+
* uppercase colon-separated pairs; any other id (some firmware/cloud setups
|
|
118
|
+
* report non-standard or punctuated identifiers) collapses to its bare uppercase
|
|
119
|
+
* hex digits. Returning the raw, untrimmed string for the odd case would orphan
|
|
120
|
+
* or duplicate a device whose punctuation varies between polls — so we always
|
|
121
|
+
* derive a deterministic key from the hex content.
|
|
122
|
+
*/
|
|
123
|
+
export function normalizeMac(mac) {
|
|
124
|
+
const hex = (mac || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
|
125
|
+
if (hex.length === 12)
|
|
126
|
+
return hex.match(/.{2}/g).join(':');
|
|
127
|
+
return hex || (mac || '').trim().toUpperCase();
|
|
128
|
+
}
|
|
129
|
+
/** True when two MAC spellings resolve to the same canonical identity. */
|
|
130
|
+
export function sameMac(a, b) {
|
|
131
|
+
return normalizeMac(a) === normalizeMac(b);
|
|
132
|
+
}
|
|
133
|
+
export function findDeviceByMac(mac) {
|
|
134
|
+
return loadTrmnlConfig().devices.find((d) => sameMac(d.mac, mac));
|
|
135
|
+
}
|
|
136
|
+
function genFriendlyId() {
|
|
137
|
+
// 6 chars from an unambiguous base32 alphabet (no 0/1/O/I).
|
|
138
|
+
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
139
|
+
const bytes = randomBytes(6);
|
|
140
|
+
let out = '';
|
|
141
|
+
for (let i = 0; i < 6; i++)
|
|
142
|
+
out += alphabet[bytes[i] % alphabet.length];
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Find or create a device record for the given MAC. Returns the device and
|
|
147
|
+
* whether it was newly created. Respects `autoRegister`: when off, a new MAC
|
|
148
|
+
* is NOT persisted and `created` is false with `device` undefined.
|
|
149
|
+
*/
|
|
150
|
+
export function ensureDevice(mac, name) {
|
|
151
|
+
const cfg = loadTrmnlConfig();
|
|
152
|
+
const norm = normalizeMac(mac);
|
|
153
|
+
const existing = cfg.devices.find((d) => sameMac(d.mac, mac));
|
|
154
|
+
if (existing)
|
|
155
|
+
return { device: existing, created: false };
|
|
156
|
+
if (!cfg.autoRegister)
|
|
157
|
+
return { created: false };
|
|
158
|
+
const device = {
|
|
159
|
+
mac: norm,
|
|
160
|
+
apiKey: randomBytes(16).toString('hex'),
|
|
161
|
+
friendlyId: genFriendlyId(),
|
|
162
|
+
name,
|
|
163
|
+
};
|
|
164
|
+
cfg.devices.push(device);
|
|
165
|
+
saveTrmnlConfig(cfg);
|
|
166
|
+
return { device, created: true };
|
|
167
|
+
}
|
|
168
|
+
export function removeDevice(mac) {
|
|
169
|
+
const cfg = loadTrmnlConfig();
|
|
170
|
+
const filtered = cfg.devices.filter((d) => !sameMac(d.mac, mac));
|
|
171
|
+
if (filtered.length === cfg.devices.length)
|
|
172
|
+
return false;
|
|
173
|
+
cfg.devices = filtered;
|
|
174
|
+
saveTrmnlConfig(cfg);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=trmnl-settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trmnl-settings.js","sourceRoot":"","sources":["../../src/trmnl/trmnl-settings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface DeviceTelemetry {
|
|
2
|
+
/** Normalized MAC identity (matches TrmnlDevice.mac). */
|
|
3
|
+
mac: string;
|
|
4
|
+
fwVersion: string;
|
|
5
|
+
batteryVoltage: number | null;
|
|
6
|
+
rssi: number | null;
|
|
7
|
+
width: number | null;
|
|
8
|
+
height: number | null;
|
|
9
|
+
/** Cadence the device says it's polling at (we still dictate it on response). */
|
|
10
|
+
refreshRate: number | null;
|
|
11
|
+
userAgent: string;
|
|
12
|
+
/** Epoch ms of the most recent poll. */
|
|
13
|
+
lastSeen: number;
|
|
14
|
+
}
|
|
15
|
+
export interface TelemetryInput {
|
|
16
|
+
fwVersion?: string;
|
|
17
|
+
batteryVoltage?: number | null;
|
|
18
|
+
rssi?: number | null;
|
|
19
|
+
width?: number | null;
|
|
20
|
+
height?: number | null;
|
|
21
|
+
refreshRate?: number | null;
|
|
22
|
+
userAgent?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Upsert the last-seen telemetry for a device. `now` is injected at the daemon
|
|
26
|
+
* boundary (defaults to wall clock) so layout/render code stays clock-free.
|
|
27
|
+
* Returns the previous `lastSeen` (epoch ms) so callers can detect poll gaps.
|
|
28
|
+
*/
|
|
29
|
+
export declare function recordTelemetry(mac: string, t: TelemetryInput, now?: number): number | null;
|
|
30
|
+
/** Snapshot of all known device telemetry, most-recently-seen first. */
|
|
31
|
+
export declare function getTelemetry(): DeviceTelemetry[];
|
|
32
|
+
/** Telemetry plus derived freshness — `secondsSinceSeen` and a `stale` flag. */
|
|
33
|
+
export interface DeviceHealth extends DeviceTelemetry {
|
|
34
|
+
secondsSinceSeen: number;
|
|
35
|
+
/** True when the panel hasn't polled in over 2× its expected cadence. */
|
|
36
|
+
stale: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Telemetry annotated with health. A panel that hasn't polled within 2× its
|
|
40
|
+
* cadence is flagged `stale` so the dashboard can distinguish "panel stuck /
|
|
41
|
+
* offline" from "nothing changed". `now`/`refreshRate` are injected at the daemon
|
|
42
|
+
* boundary so this stays clock- and config-free.
|
|
43
|
+
*/
|
|
44
|
+
export declare function getTelemetryHealth(refreshRate: number, now?: number): DeviceHealth[];
|
|
45
|
+
/** Test helper — clear the runtime map. */
|
|
46
|
+
export declare function _resetTelemetry(): void;
|
|
47
|
+
//# sourceMappingURL=trmnl-telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trmnl-telemetry.d.ts","sourceRoot":"","sources":["../../src/trmnl/trmnl-telemetry.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,iFAAiF;IACjF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,cAAc,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,GAAG,IAAI,CAgBvG;AAED,wEAAwE;AACxE,wBAAgB,YAAY,IAAI,eAAe,EAAE,CAEhD;AAED,gFAAgF;AAChF,MAAM,WAAW,YAAa,SAAQ,eAAe;IACnD,gBAAgB,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,YAAY,EAAE,CAMhG;AAED,2CAA2C;AAC3C,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRMNL device telemetry — a runtime-only, per-MAC snapshot of the headers a
|
|
3
|
+
* BYOS panel reports on each /api/setup or /api/display poll (firmware version,
|
|
4
|
+
* battery voltage, WiFi signal, panel resolution). This is NOT persisted to
|
|
5
|
+
* settings.json: polls arrive every ~180s and writing the settings file on each
|
|
6
|
+
* one would thrash it and race the user's manual edits. The daemon status
|
|
7
|
+
* snapshot reads this map for diagnostics; it's lost on daemon restart, which is
|
|
8
|
+
* fine — the next poll repopulates it.
|
|
9
|
+
*/
|
|
10
|
+
import { normalizeMac } from './trmnl-settings.js';
|
|
11
|
+
const telemetry = new Map();
|
|
12
|
+
/**
|
|
13
|
+
* Upsert the last-seen telemetry for a device. `now` is injected at the daemon
|
|
14
|
+
* boundary (defaults to wall clock) so layout/render code stays clock-free.
|
|
15
|
+
* Returns the previous `lastSeen` (epoch ms) so callers can detect poll gaps.
|
|
16
|
+
*/
|
|
17
|
+
export function recordTelemetry(mac, t, now = Date.now()) {
|
|
18
|
+
const key = normalizeMac(mac);
|
|
19
|
+
if (!key)
|
|
20
|
+
return null;
|
|
21
|
+
const prevLastSeen = telemetry.get(key)?.lastSeen ?? null;
|
|
22
|
+
telemetry.set(key, {
|
|
23
|
+
mac: key,
|
|
24
|
+
fwVersion: t.fwVersion ?? '',
|
|
25
|
+
batteryVoltage: t.batteryVoltage ?? null,
|
|
26
|
+
rssi: t.rssi ?? null,
|
|
27
|
+
width: t.width ?? null,
|
|
28
|
+
height: t.height ?? null,
|
|
29
|
+
refreshRate: t.refreshRate ?? null,
|
|
30
|
+
userAgent: t.userAgent ?? '',
|
|
31
|
+
lastSeen: now,
|
|
32
|
+
});
|
|
33
|
+
return prevLastSeen;
|
|
34
|
+
}
|
|
35
|
+
/** Snapshot of all known device telemetry, most-recently-seen first. */
|
|
36
|
+
export function getTelemetry() {
|
|
37
|
+
return [...telemetry.values()].sort((a, b) => b.lastSeen - a.lastSeen);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Telemetry annotated with health. A panel that hasn't polled within 2× its
|
|
41
|
+
* cadence is flagged `stale` so the dashboard can distinguish "panel stuck /
|
|
42
|
+
* offline" from "nothing changed". `now`/`refreshRate` are injected at the daemon
|
|
43
|
+
* boundary so this stays clock- and config-free.
|
|
44
|
+
*/
|
|
45
|
+
export function getTelemetryHealth(refreshRate, now = Date.now()) {
|
|
46
|
+
const staleAfterMs = Math.max(30, refreshRate) * 2 * 1000;
|
|
47
|
+
return getTelemetry().map((t) => {
|
|
48
|
+
const ageMs = now - t.lastSeen;
|
|
49
|
+
return { ...t, secondsSinceSeen: Math.round(ageMs / 1000), stale: ageMs > staleAfterMs };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Test helper — clear the runtime map. */
|
|
53
|
+
export function _resetTelemetry() {
|
|
54
|
+
telemetry.clear();
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=trmnl-telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trmnl-telemetry.js","sourceRoot":"","sources":["../../src/trmnl/trmnl-telemetry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AA2BnD,MAAM,SAAS,GAAG,IAAI,GAAG,EAA2B,CAAC;AAErD;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,CAAiB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IACtF,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,YAAY,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,QAAQ,IAAI,IAAI,CAAC;IAC1D,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE;QACjB,GAAG,EAAE,GAAG;QACR,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,EAAE;QAC5B,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,IAAI;QACxC,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,IAAI;QACpB,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI;QACtB,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,IAAI;QACxB,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,IAAI;QAClC,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,EAAE;QAC5B,QAAQ,EAAE,GAAG;KACd,CAAC,CAAC;IACH,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,YAAY;IAC1B,OAAO,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;AACzE,CAAC;AASD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IAC9E,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,WAAW,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAC1D,OAAO,YAAY,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC9B,MAAM,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC;QAC/B,OAAO,EAAE,GAAG,CAAC,EAAE,gBAAgB,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,YAAY,EAAE,CAAC;IAC3F,CAAC,CAAC,CAAC;AACL,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,eAAe;IAC7B,SAAS,CAAC,KAAK,EAAE,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whisper-server singleton manager.
|
|
3
|
+
*
|
|
4
|
+
* Fixed port 9100 — one server shared across all agentdeck sessions.
|
|
5
|
+
* Info file at ~/.agentdeck/whisper-server.json for discovery.
|
|
6
|
+
* Last session to exit kills the server.
|
|
7
|
+
*/
|
|
8
|
+
export declare const WHISPER_SERVER_PORT = 9100;
|
|
9
|
+
/**
|
|
10
|
+
* Ensure a whisper-server is running on the singleton port.
|
|
11
|
+
* Returns port on success, null if unavailable (binary missing, model missing, etc).
|
|
12
|
+
*/
|
|
13
|
+
export declare function ensureWhisperServer(serverBin: string, modelPath: string): Promise<number | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Release the whisper-server if no other sessions remain.
|
|
16
|
+
* Call AFTER deregisterSession() so the count is already decremented.
|
|
17
|
+
*/
|
|
18
|
+
export declare function releaseWhisperServer(): void;
|
|
19
|
+
//# sourceMappingURL=whisper-server-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whisper-server-manager.d.ts","sourceRoot":"","sources":["../src/whisper-server-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,eAAO,MAAM,mBAAmB,OAAO,CAAC;AA2DxC;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6ExB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAiB3C"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whisper-server singleton manager.
|
|
3
|
+
*
|
|
4
|
+
* Fixed port 9100 — one server shared across all agentdeck sessions.
|
|
5
|
+
* Info file at ~/.agentdeck/whisper-server.json for discovery.
|
|
6
|
+
* Last session to exit kills the server.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import { readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, existsSync, } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
import { listActive } from './session-registry.js';
|
|
14
|
+
import { debug } from './logger.js';
|
|
15
|
+
export const WHISPER_SERVER_PORT = 9100;
|
|
16
|
+
const INFO_DIR = join(homedir(), '.agentdeck');
|
|
17
|
+
const INFO_FILE = join(INFO_DIR, 'whisper-server.json');
|
|
18
|
+
function ensureDir() {
|
|
19
|
+
if (!existsSync(INFO_DIR)) {
|
|
20
|
+
mkdirSync(INFO_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function readInfo() {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(INFO_FILE, 'utf-8'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function writeInfo(info) {
|
|
32
|
+
ensureDir();
|
|
33
|
+
const tmp = join(INFO_DIR, `.whisper-server.${randomUUID()}.tmp`);
|
|
34
|
+
writeFileSync(tmp, JSON.stringify(info, null, 2), 'utf-8');
|
|
35
|
+
renameSync(tmp, INFO_FILE);
|
|
36
|
+
}
|
|
37
|
+
function removeInfo() {
|
|
38
|
+
try {
|
|
39
|
+
unlinkSync(INFO_FILE);
|
|
40
|
+
}
|
|
41
|
+
catch { /* already gone */ }
|
|
42
|
+
}
|
|
43
|
+
function isProcessAlive(pid) {
|
|
44
|
+
try {
|
|
45
|
+
process.kill(pid, 0);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function isServerResponding(port) {
|
|
53
|
+
try {
|
|
54
|
+
await fetch(`http://127.0.0.1:${port}`, { signal: AbortSignal.timeout(1000) });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function pollUntilReady(port, maxMs) {
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
while (Date.now() - start < maxMs) {
|
|
64
|
+
if (await isServerResponding(port))
|
|
65
|
+
return true;
|
|
66
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Ensure a whisper-server is running on the singleton port.
|
|
72
|
+
* Returns port on success, null if unavailable (binary missing, model missing, etc).
|
|
73
|
+
*/
|
|
74
|
+
export async function ensureWhisperServer(serverBin, modelPath) {
|
|
75
|
+
if (!existsSync(serverBin)) {
|
|
76
|
+
debug('WhisperMgr', `whisper-server not found at ${serverBin}, skipping`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
// 1. Check existing info file
|
|
80
|
+
const info = readInfo();
|
|
81
|
+
if (info) {
|
|
82
|
+
if (isProcessAlive(info.pid)) {
|
|
83
|
+
// PID alive — check if server is responding
|
|
84
|
+
if (await isServerResponding(info.port)) {
|
|
85
|
+
debug('WhisperMgr', `Reusing existing server (pid=${info.pid}, port=${info.port})`);
|
|
86
|
+
return info.port;
|
|
87
|
+
}
|
|
88
|
+
// PID alive but not responding — model may still be loading, poll 30s
|
|
89
|
+
debug('WhisperMgr', `Server pid=${info.pid} alive but not responding, waiting for model load...`);
|
|
90
|
+
if (await pollUntilReady(info.port, 30_000)) {
|
|
91
|
+
debug('WhisperMgr', `Server became ready (pid=${info.pid}, port=${info.port})`);
|
|
92
|
+
return info.port;
|
|
93
|
+
}
|
|
94
|
+
// Still not responding after 30s — stale
|
|
95
|
+
debug('WhisperMgr', `Server pid=${info.pid} never responded, treating as stale`);
|
|
96
|
+
}
|
|
97
|
+
// PID dead or stale — clean up
|
|
98
|
+
removeInfo();
|
|
99
|
+
}
|
|
100
|
+
// 2. Spawn new server (detached so it survives parent exit)
|
|
101
|
+
debug('WhisperMgr', `Starting whisper-server on port ${WHISPER_SERVER_PORT}...`);
|
|
102
|
+
const args = [
|
|
103
|
+
'--model', modelPath,
|
|
104
|
+
'--port', String(WHISPER_SERVER_PORT),
|
|
105
|
+
'--host', '127.0.0.1',
|
|
106
|
+
'-l', 'auto',
|
|
107
|
+
'--no-timestamps',
|
|
108
|
+
'--convert',
|
|
109
|
+
'--prompt', 'coding, programming, Claude, terminal, git, function, component, API',
|
|
110
|
+
];
|
|
111
|
+
const proc = spawn(serverBin, args, {
|
|
112
|
+
stdio: 'ignore',
|
|
113
|
+
detached: true,
|
|
114
|
+
});
|
|
115
|
+
proc.unref();
|
|
116
|
+
const pid = proc.pid;
|
|
117
|
+
if (!pid) {
|
|
118
|
+
debug('WhisperMgr', 'Failed to spawn whisper-server (no pid)');
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
// 3. Brief wait to detect immediate crash (e.g. port conflict)
|
|
122
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
123
|
+
if (!isProcessAlive(pid)) {
|
|
124
|
+
debug('WhisperMgr', `whisper-server died immediately (pid=${pid}), checking for existing server...`);
|
|
125
|
+
// Another process may have won the port race — re-check info file
|
|
126
|
+
const otherInfo = readInfo();
|
|
127
|
+
if (otherInfo && isProcessAlive(otherInfo.pid) && await isServerResponding(otherInfo.port)) {
|
|
128
|
+
debug('WhisperMgr', `Found winner server (pid=${otherInfo.pid}, port=${otherInfo.port})`);
|
|
129
|
+
return otherInfo.port;
|
|
130
|
+
}
|
|
131
|
+
removeInfo();
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
// 4. Write info file
|
|
135
|
+
writeInfo({ pid, port: WHISPER_SERVER_PORT, modelPath, startedAt: new Date().toISOString() });
|
|
136
|
+
// 5. Poll until ready (up to 30s for model loading)
|
|
137
|
+
if (await pollUntilReady(WHISPER_SERVER_PORT, 30_000)) {
|
|
138
|
+
debug('WhisperMgr', `whisper-server ready on port ${WHISPER_SERVER_PORT} (pid=${pid})`);
|
|
139
|
+
return WHISPER_SERVER_PORT;
|
|
140
|
+
}
|
|
141
|
+
debug('WhisperMgr', 'whisper-server failed to become ready within 30s');
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Release the whisper-server if no other sessions remain.
|
|
146
|
+
* Call AFTER deregisterSession() so the count is already decremented.
|
|
147
|
+
*/
|
|
148
|
+
export function releaseWhisperServer() {
|
|
149
|
+
const activeSessions = listActive();
|
|
150
|
+
if (activeSessions.length > 0) {
|
|
151
|
+
debug('WhisperMgr', `Keeping whisper-server alive (${activeSessions.length} session(s) remain)`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const info = readInfo();
|
|
155
|
+
if (!info)
|
|
156
|
+
return;
|
|
157
|
+
debug('WhisperMgr', `Killing whisper-server (pid=${info.pid})`);
|
|
158
|
+
try {
|
|
159
|
+
process.kill(info.pid, 'SIGTERM');
|
|
160
|
+
}
|
|
161
|
+
catch { /* already dead */ }
|
|
162
|
+
// Force kill after 3s
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
try {
|
|
165
|
+
process.kill(info.pid, 'SIGKILL');
|
|
166
|
+
}
|
|
167
|
+
catch { /* already dead */ }
|
|
168
|
+
}, 3000);
|
|
169
|
+
removeInfo();
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=whisper-server-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whisper-server-manager.js","sourceRoot":"","sources":["../src/whisper-server-manager.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentdeck/bridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,18 +24,7 @@
|
|
|
24
24
|
"bin": {
|
|
25
25
|
"agentdeck": "./dist/cli.js"
|
|
26
26
|
},
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "tsc",
|
|
29
|
-
"build:fm-helper": "node scripts/build-fm-helper.mjs",
|
|
30
|
-
"prepack": "node scripts/build-fm-helper.mjs",
|
|
31
|
-
"dev": "tsc --watch",
|
|
32
|
-
"clean": "rm -rf dist",
|
|
33
|
-
"typecheck": "tsc --noEmit",
|
|
34
|
-
"start": "node dist/cli.js claude"
|
|
35
|
-
},
|
|
36
27
|
"dependencies": {
|
|
37
|
-
"@agentdeck/hooks": "workspace:*",
|
|
38
|
-
"@agentdeck/shared": "workspace:*",
|
|
39
28
|
"@anthropic-ai/sdk": "^0.111.0",
|
|
40
29
|
"bonjour-service": "^1.3.0",
|
|
41
30
|
"commander": "^14.0.3",
|
|
@@ -43,7 +32,9 @@
|
|
|
43
32
|
"qrcode": "^1.5.4",
|
|
44
33
|
"sharp": "^0.34.5",
|
|
45
34
|
"strip-ansi": "^7.2.0",
|
|
46
|
-
"ws": "^8.20.0"
|
|
35
|
+
"ws": "^8.20.0",
|
|
36
|
+
"@agentdeck/hooks": "1.0.7",
|
|
37
|
+
"@agentdeck/shared": "1.0.7"
|
|
47
38
|
},
|
|
48
39
|
"optionalDependencies": {
|
|
49
40
|
"@picovoice/porcupine-node": "^4.0.2",
|
|
@@ -59,5 +50,13 @@
|
|
|
59
50
|
"@types/qrcode": "^1.5.6",
|
|
60
51
|
"@types/ws": "^8.18.1",
|
|
61
52
|
"typescript": "^6.0.3"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc",
|
|
56
|
+
"build:fm-helper": "node scripts/build-fm-helper.mjs",
|
|
57
|
+
"dev": "tsc --watch",
|
|
58
|
+
"clean": "rm -rf dist",
|
|
59
|
+
"typecheck": "tsc --noEmit",
|
|
60
|
+
"start": "node dist/cli.js claude"
|
|
62
61
|
}
|
|
63
|
-
}
|
|
62
|
+
}
|
package/src/idotmatrix/sync.py
CHANGED
|
@@ -21,6 +21,7 @@ from matrix_sync_common import ( # noqa: E402
|
|
|
21
21
|
DEFAULT_URL,
|
|
22
22
|
POLL_INTERVAL,
|
|
23
23
|
BRIDGE_GONE_EXIT_SEC,
|
|
24
|
+
StatusReporter,
|
|
24
25
|
fetch_display_state as _fetch_display_state_sync,
|
|
25
26
|
bridge_reachable as _bridge_reachable_sync,
|
|
26
27
|
resolve_display_brightness as _resolve_display_brightness_common,
|
|
@@ -124,7 +125,13 @@ async def run_sync(address: str, url: str, brightness: int = 100, boost: float =
|
|
|
124
125
|
print(f"AgentDeck Bridge API URL: {url}")
|
|
125
126
|
print(f"Initial Hardware Brightness: {brightness}%")
|
|
126
127
|
print(f"Software Brightness Boost: {boost}x")
|
|
127
|
-
|
|
128
|
+
|
|
129
|
+
# Machine-readable link state for the daemon supervisor — it spawns us but
|
|
130
|
+
# cannot see the BLE link, and we reconnect inside this loop rather than
|
|
131
|
+
# exiting, so process liveness tells it nothing about whether the panel is
|
|
132
|
+
# actually being driven.
|
|
133
|
+
status = StatusReporter()
|
|
134
|
+
|
|
128
135
|
manager = ConnectionManager()
|
|
129
136
|
|
|
130
137
|
idm_image = IdmImage()
|
|
@@ -187,8 +194,10 @@ async def run_sync(address: str, url: str, brightness: int = 100, boost: float =
|
|
|
187
194
|
# 1. Ensure bluetooth connection
|
|
188
195
|
if not connected:
|
|
189
196
|
print(f"Connecting to iDotMatrix ({address})...")
|
|
197
|
+
status.connecting()
|
|
190
198
|
await manager.connectByAddress(address)
|
|
191
199
|
print("Connected to Bluetooth device!")
|
|
200
|
+
status.connected()
|
|
192
201
|
|
|
193
202
|
# Settle: the panel drops commands sent too soon after the GATT
|
|
194
203
|
# link comes up. Without this the first setBrightness is lost on a
|
|
@@ -227,6 +236,7 @@ async def run_sync(address: str, url: str, brightness: int = 100, boost: float =
|
|
|
227
236
|
# bleak's is_connected reflects the real OS-level link state.
|
|
228
237
|
if manager.client is None or not manager.client.is_connected:
|
|
229
238
|
print("BLE link lost — reconnecting...")
|
|
239
|
+
status.disconnected("BLE link lost")
|
|
230
240
|
connected = False
|
|
231
241
|
last_hash = ""
|
|
232
242
|
await _interruptible_sleep(stop_event, 1.0)
|
|
@@ -274,6 +284,7 @@ async def run_sync(address: str, url: str, brightness: int = 100, boost: float =
|
|
|
274
284
|
last_brightness_assert = time.monotonic()
|
|
275
285
|
|
|
276
286
|
if display_dimmed:
|
|
287
|
+
status.streaming(dimmed=True)
|
|
277
288
|
await _interruptible_sleep(stop_event, POLL_INTERVAL)
|
|
278
289
|
continue
|
|
279
290
|
|
|
@@ -302,6 +313,7 @@ async def run_sync(address: str, url: str, brightness: int = 100, boost: float =
|
|
|
302
313
|
current_hash = hashlib.sha256(frame_data).hexdigest()
|
|
303
314
|
if current_hash == last_hash:
|
|
304
315
|
# Frame didn't change, skip BLE transmission to save battery and bandwidth
|
|
316
|
+
status.streaming()
|
|
305
317
|
await _interruptible_sleep(stop_event, POLL_INTERVAL)
|
|
306
318
|
continue
|
|
307
319
|
|
|
@@ -332,8 +344,10 @@ async def run_sync(address: str, url: str, brightness: int = 100, boost: float =
|
|
|
332
344
|
if res:
|
|
333
345
|
last_hash = current_hash
|
|
334
346
|
print("Frame uploaded successfully.")
|
|
347
|
+
status.frame_sent()
|
|
335
348
|
else:
|
|
336
349
|
print("Failed to upload frame (uploadUnprocessed returned False).")
|
|
350
|
+
status.disconnected("frame upload failed")
|
|
337
351
|
|
|
338
352
|
await _interruptible_sleep(stop_event, POLL_INTERVAL)
|
|
339
353
|
|
|
@@ -343,6 +357,7 @@ async def run_sync(address: str, url: str, brightness: int = 100, boost: float =
|
|
|
343
357
|
except Exception as e:
|
|
344
358
|
print(f"Error during loop: {e}")
|
|
345
359
|
print("Resetting bluetooth connection...")
|
|
360
|
+
status.failed(e)
|
|
346
361
|
connected = False
|
|
347
362
|
try:
|
|
348
363
|
await manager.disconnect()
|