@agentdeck/bridge 1.0.12 → 1.0.13

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.
Files changed (58) hide show
  1. package/assets/fm-helper/agentdeck-fm-helper +0 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +34 -19
  4. package/dist/cli.js.map +1 -1
  5. package/dist/d200h/hid-protocol.d.ts +62 -0
  6. package/dist/d200h/hid-protocol.d.ts.map +1 -0
  7. package/dist/d200h/hid-protocol.js +124 -0
  8. package/dist/d200h/hid-protocol.js.map +1 -0
  9. package/dist/d200h/image-renderer.d.ts +24 -0
  10. package/dist/d200h/image-renderer.d.ts.map +1 -0
  11. package/dist/d200h/image-renderer.js +321 -0
  12. package/dist/d200h/image-renderer.js.map +1 -0
  13. package/dist/daemon-server.d.ts.map +1 -1
  14. package/dist/daemon-server.js +47 -1
  15. package/dist/daemon-server.js.map +1 -1
  16. package/dist/daemon.js +1 -1
  17. package/dist/hook-migration.d.ts +2 -0
  18. package/dist/hook-migration.d.ts.map +1 -0
  19. package/dist/hook-migration.js +116 -0
  20. package/dist/hook-migration.js.map +1 -0
  21. package/dist/modules/d200h-module.d.ts +65 -0
  22. package/dist/modules/d200h-module.d.ts.map +1 -0
  23. package/dist/modules/d200h-module.js +495 -0
  24. package/dist/modules/d200h-module.js.map +1 -0
  25. package/dist/modules/d200h-renderer.d.ts +7 -0
  26. package/dist/modules/d200h-renderer.d.ts.map +1 -0
  27. package/dist/modules/d200h-renderer.js +321 -0
  28. package/dist/modules/d200h-renderer.js.map +1 -0
  29. package/dist/modules/trmnl-module.d.ts +38 -0
  30. package/dist/modules/trmnl-module.d.ts.map +1 -0
  31. package/dist/modules/trmnl-module.js +111 -0
  32. package/dist/modules/trmnl-module.js.map +1 -0
  33. package/dist/trmnl/byos-server.d.ts +39 -0
  34. package/dist/trmnl/byos-server.d.ts.map +1 -0
  35. package/dist/trmnl/byos-server.js +301 -0
  36. package/dist/trmnl/byos-server.js.map +1 -0
  37. package/dist/trmnl/frame-cache.d.ts +37 -0
  38. package/dist/trmnl/frame-cache.d.ts.map +1 -0
  39. package/dist/trmnl/frame-cache.js +157 -0
  40. package/dist/trmnl/frame-cache.js.map +1 -0
  41. package/dist/trmnl/image-renderer.d.ts +27 -0
  42. package/dist/trmnl/image-renderer.d.ts.map +1 -0
  43. package/dist/trmnl/image-renderer.js +235 -0
  44. package/dist/trmnl/image-renderer.js.map +1 -0
  45. package/dist/trmnl/trmnl-settings.d.ts +96 -0
  46. package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
  47. package/dist/trmnl/trmnl-settings.js +177 -0
  48. package/dist/trmnl/trmnl-settings.js.map +1 -0
  49. package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
  50. package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
  51. package/dist/trmnl/trmnl-telemetry.js +56 -0
  52. package/dist/trmnl/trmnl-telemetry.js.map +1 -0
  53. package/dist/whisper-server-manager.d.ts +19 -0
  54. package/dist/whisper-server-manager.d.ts.map +1 -0
  55. package/dist/whisper-server-manager.js +171 -0
  56. package/dist/whisper-server-manager.js.map +1 -0
  57. package/fm-helper/AgentDeckFMHelper.swift +111 -10
  58. package/package.json +3 -3
@@ -0,0 +1,235 @@
1
+ /**
2
+ * TRMNL image renderer — rasterizes the shared 800×480 dashboard SVG to a
3
+ * 1-bit (black/white) grayscale PNG sized for the TRMNL 7.5" e-ink panel.
4
+ *
5
+ * Pipeline: shared `renderTrmnlDashboard` SVG → resvg rasterize (800×480 RGBA)
6
+ * → luminance threshold → packed 1-bit PNG (color type 0, bit depth 1) encoded
7
+ * with Node's built-in `zlib` (no new dependency). A 1-bit PNG of mostly-white
8
+ * line art deflates to a few KB, comfortably under TRMNL's 90 KB download cap.
9
+ *
10
+ * Falls back to a blank 1-bit PNG when resvg-js is unavailable (matches the
11
+ * D200H renderer's optional-dependency pattern).
12
+ */
13
+ import { deflateSync } from 'zlib';
14
+ import { createHash } from 'crypto';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { dirname, join } from 'node:path';
17
+ import { existsSync } from 'node:fs';
18
+ import { renderTrmnlDashboard, TRMNL_WIDTH, TRMNL_HEIGHT } from '@agentdeck/shared';
19
+ import { debug, logTagged } from '../logger.js';
20
+ const TAG = 'trmnl-render';
21
+ // --- Font supply for resvg (same trap/fix as the D200H renderer) ---
22
+ // resvg drops every <text> element unless given a font to shape glyphs with.
23
+ // Load the bundled OFL faces via `fontFiles` for crisp Latin/mono, AND enable
24
+ // `loadSystemFonts` so resvg can FALL BACK to the OS's CJK faces — session goals
25
+ // are user prompts, often Korean/Chinese, which the Latin-only bundled fonts
26
+ // render as boxes. Unlike the 14-tile-per-frame D200H renderer, TRMNL renders one
27
+ // frame only on real state change, so the per-`new Resvg()` system-font scan is an
28
+ // acceptable cost here.
29
+ const FONT_OPTS = (() => {
30
+ try {
31
+ // bridge/{src,dist}/trmnl/image-renderer.{ts,js} → bridge/assets/fonts
32
+ const here = dirname(fileURLToPath(import.meta.url));
33
+ const fontsDir = join(here, '..', '..', 'assets', 'fonts');
34
+ const files = [
35
+ 'IBMPlexSans-Regular.ttf',
36
+ 'IBMPlexSans-Bold.ttf',
37
+ 'JetBrainsMono-Regular.ttf',
38
+ 'JetBrainsMono-Bold.ttf',
39
+ ]
40
+ .map((n) => join(fontsDir, n))
41
+ .filter((p) => existsSync(p));
42
+ if (files.length > 0) {
43
+ return { fontFiles: files, loadSystemFonts: true, defaultFontFamily: 'IBM Plex Sans' };
44
+ }
45
+ debug(TAG, `bundled fonts not found under ${fontsDir} — falling back to system fonts`);
46
+ }
47
+ catch (err) {
48
+ debug(TAG, `font path resolution failed (${err}) — falling back to system fonts`);
49
+ }
50
+ return { loadSystemFonts: true, defaultFontFamily: 'Helvetica Neue' };
51
+ })();
52
+ let Resvg = null;
53
+ let resvgLoaded = false;
54
+ async function loadResvg() {
55
+ if (resvgLoaded)
56
+ return Resvg;
57
+ resvgLoaded = true;
58
+ try {
59
+ const mod = await import('@resvg/resvg-js');
60
+ Resvg = mod.Resvg ?? mod.default?.Resvg;
61
+ debug(TAG, 'resvg-js loaded — SVG rendering enabled');
62
+ return Resvg;
63
+ }
64
+ catch {
65
+ debug(TAG, 'resvg-js not available — falling back to blank 1-bit PNG');
66
+ return null;
67
+ }
68
+ }
69
+ /** Initialize the renderer (loads resvg-js if available). Call once at start. */
70
+ export async function initTrmnlRenderer() {
71
+ await loadResvg();
72
+ }
73
+ export function isTrmnlResvgLoaded() {
74
+ return Resvg !== null;
75
+ }
76
+ // --- PNG (1-bit grayscale) encoding ---
77
+ const crcTable = new Uint32Array(256);
78
+ for (let n = 0; n < 256; n++) {
79
+ let c = n;
80
+ for (let k = 0; k < 8; k++)
81
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
82
+ crcTable[n] = c >>> 0;
83
+ }
84
+ function crc32(buf) {
85
+ let crc = 0xffffffff;
86
+ for (let i = 0; i < buf.length; i++)
87
+ crc = crcTable[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
88
+ return (crc ^ 0xffffffff) >>> 0;
89
+ }
90
+ function pngChunk(type, data) {
91
+ const typeBytes = Buffer.from(type, 'ascii');
92
+ const len = Buffer.alloc(4);
93
+ len.writeUInt32BE(data.length, 0);
94
+ const crc = Buffer.alloc(4);
95
+ crc.writeUInt32BE(crc32(Buffer.concat([typeBytes, data])), 0);
96
+ return Buffer.concat([len, typeBytes, data, crc]);
97
+ }
98
+ const PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
99
+ /**
100
+ * Encode a 1-bit grayscale PNG from a packed bitmap.
101
+ * `bits` holds one bit per pixel, MSB-first, `bytesPerRow` bytes per scanline,
102
+ * where 1 = white and 0 = black (PNG grayscale convention).
103
+ */
104
+ function encode1BitPng(bits, width, height, bytesPerRow) {
105
+ const ihdr = Buffer.alloc(13);
106
+ ihdr.writeUInt32BE(width, 0);
107
+ ihdr.writeUInt32BE(height, 4);
108
+ ihdr[8] = 1; // bit depth
109
+ ihdr[9] = 0; // color type: grayscale
110
+ ihdr[10] = 0; // compression
111
+ ihdr[11] = 0; // filter
112
+ ihdr[12] = 0; // interlace
113
+ // Raw image data: each scanline prefixed with filter byte 0 (None).
114
+ const raw = Buffer.alloc((bytesPerRow + 1) * height);
115
+ for (let y = 0; y < height; y++) {
116
+ const dst = y * (bytesPerRow + 1);
117
+ raw[dst] = 0; // filter: None
118
+ for (let x = 0; x < bytesPerRow; x++)
119
+ raw[dst + 1 + x] = bits[y * bytesPerRow + x];
120
+ }
121
+ const idat = deflateSync(raw, { level: 9 });
122
+ return Buffer.concat([
123
+ PNG_SIG,
124
+ pngChunk('IHDR', ihdr),
125
+ pngChunk('IDAT', idat),
126
+ pngChunk('IEND', Buffer.alloc(0)),
127
+ ]);
128
+ }
129
+ /**
130
+ * Box-downsample a supersampled RGBA buffer (`ss`× the target size) to a packed
131
+ * 1-bit bitmap (1 = white, 0 = black). Averaging each ss×ss block of luma BEFORE
132
+ * thresholding is what sharpens text on a 1-bit panel: a hard threshold of a
133
+ * 1×-rendered glyph drops/jaggies thin strokes (notably Hangul), whereas the
134
+ * averaged edge lands on the majority-coverage boundary, so the same glyph reads
135
+ * crisp. `ss === 1` degrades to a plain threshold.
136
+ */
137
+ function rgbaToPacked1Bit(pixels, srcWidth, srcHeight, ss = 1) {
138
+ const width = Math.floor(srcWidth / ss);
139
+ const height = Math.floor(srcHeight / ss);
140
+ const bytesPerRow = (width + 7) >> 3;
141
+ const bits = new Uint8Array(bytesPerRow * height);
142
+ bits.fill(0xff); // default white
143
+ const norm = 1 / (ss * ss);
144
+ for (let y = 0; y < height; y++) {
145
+ for (let x = 0; x < width; x++) {
146
+ let sum = 0;
147
+ for (let dy = 0; dy < ss; dy++) {
148
+ const sy = Math.min(y * ss + dy, srcHeight - 1);
149
+ const rowBase = sy * srcWidth;
150
+ for (let dx = 0; dx < ss; dx++) {
151
+ const sx = Math.min(x * ss + dx, srcWidth - 1);
152
+ const p = (rowBase + sx) * 4;
153
+ // Rec.601 luma; transparent pixels (alpha 0) read as white since our SVG
154
+ // paints a full white background, so alpha is effectively always 255.
155
+ sum += (pixels[p] * 299 + pixels[p + 1] * 587 + pixels[p + 2] * 114) / 1000;
156
+ }
157
+ }
158
+ if (sum * norm < 128) {
159
+ // black pixel → clear the bit
160
+ bits[y * bytesPerRow + (x >> 3)] &= ~(0x80 >> (x & 7));
161
+ }
162
+ }
163
+ }
164
+ return { bits, bytesPerRow, width, height };
165
+ }
166
+ /** A fully-white 1-bit PNG (fallback when resvg is unavailable). */
167
+ function blankPng(width, height) {
168
+ const bytesPerRow = (width + 7) >> 3;
169
+ const bits = new Uint8Array(bytesPerRow * height).fill(0xff);
170
+ return encode1BitPng(bits, width, height, bytesPerRow);
171
+ }
172
+ function hashBuffer(buf) {
173
+ return createHash('sha1').update(buf).digest('hex').slice(0, 16);
174
+ }
175
+ /**
176
+ * Render the AgentDeck dashboard for the given broadcast state into a 1-bit PNG.
177
+ * Accepts a raw state event or a pre-parsed DashState (passed through to the
178
+ * shared layout). `now` may be supplied for deterministic tests.
179
+ */
180
+ export function renderTrmnlFrame(stateEvt, now, size) {
181
+ const width = size?.width && size.width > 0 ? Math.round(size.width) : TRMNL_WIDTH;
182
+ const height = size?.height && size.height > 0 ? Math.round(size.height) : TRMNL_HEIGHT;
183
+ let buffer;
184
+ let degraded = false;
185
+ if (Resvg) {
186
+ try {
187
+ const svg = renderTrmnlDashboard(stateEvt, { ...(now ? { now } : {}), width, height });
188
+ // Supersample so the 1-bit threshold has sub-pixel edge info (crisper text,
189
+ // esp. Hangul). Cap the rasterized width so large panels don't blow up the
190
+ // per-frame render: 800→3×, ~1200→2×, ≥2400→1×.
191
+ const ss = Math.max(1, Math.min(3, Math.floor(2400 / width)));
192
+ const resvg = new Resvg(svg, {
193
+ fitTo: { mode: 'width', value: width * ss },
194
+ font: FONT_OPTS,
195
+ background: '#ffffff',
196
+ });
197
+ const rendered = resvg.render();
198
+ const packed = rgbaToPacked1Bit(rendered.pixels, rendered.width, rendered.height, ss);
199
+ buffer = encode1BitPng(packed.bits, packed.width, packed.height, packed.bytesPerRow);
200
+ }
201
+ catch (err) {
202
+ // Always visible (not debug-gated): a failed render used to silently ship
203
+ // a blank white frame to the panel — the classic "dashboard disappeared"
204
+ // report with zero trace. Rate-limited per error message.
205
+ noteRenderFailure(String(err));
206
+ buffer = blankPng(width, height);
207
+ degraded = true;
208
+ }
209
+ }
210
+ else {
211
+ buffer = blankPng(width, height);
212
+ degraded = true;
213
+ }
214
+ return {
215
+ buffer,
216
+ contentHash: hashBuffer(buffer),
217
+ width,
218
+ height,
219
+ contentType: 'image/png',
220
+ ...(degraded ? { degraded: true } : {}),
221
+ };
222
+ }
223
+ // Rate-limit render-failure logging so a hot broadcast loop can't spam stderr:
224
+ // each distinct error message logs at most once per minute.
225
+ const RENDER_FAIL_LOG_INTERVAL_MS = 60_000;
226
+ const lastRenderFailLogAt = new Map();
227
+ function noteRenderFailure(message) {
228
+ const now = Date.now();
229
+ const last = lastRenderFailLogAt.get(message) ?? 0;
230
+ if (now - last < RENDER_FAIL_LOG_INTERVAL_MS)
231
+ return;
232
+ lastRenderFailLogAt.set(message, now);
233
+ logTagged(TAG, `render failed (frame cache keeps the last good frame): ${message}`);
234
+ }
235
+ //# sourceMappingURL=image-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-renderer.js","sourceRoot":"","sources":["../../src/trmnl/image-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,GAAG,GAAG,cAAc,CAAC;AAE3B,sEAAsE;AACtE,6EAA6E;AAC7E,8EAA8E;AAC9E,iFAAiF;AACjF,6EAA6E;AAC7E,kFAAkF;AAClF,mFAAmF;AACnF,wBAAwB;AACxB,MAAM,SAAS,GAAkF,CAAC,GAAG,EAAE;IACrG,IAAI,CAAC;QACH,uEAAuE;QACvE,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAG;YACZ,yBAAyB;YACzB,sBAAsB;YACtB,2BAA2B;YAC3B,wBAAwB;SACzB;aACE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;aAC7B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,iBAAiB,EAAE,eAAe,EAAE,CAAC;QACzF,CAAC;QACD,KAAK,CAAC,GAAG,EAAE,iCAAiC,QAAQ,iCAAiC,CAAC,CAAC;IACzF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,GAAG,EAAE,gCAAgC,GAAG,kCAAkC,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC;AACxE,CAAC,CAAC,EAAE,CAAC;AAUL,IAAI,KAAK,GAAsB,IAAI,CAAC;AACpC,IAAI,WAAW,GAAG,KAAK,CAAC;AAExB,KAAK,UAAU,SAAS;IACtB,IAAI,WAAW;QAAE,OAAO,KAAK,CAAC;IAC9B,WAAW,GAAG,IAAI,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;QAC5C,KAAK,GAAI,GAAW,CAAC,KAAK,IAAK,GAAW,CAAC,OAAO,EAAE,KAAK,CAAC;QAC1D,KAAK,CAAC,GAAG,EAAE,yCAAyC,CAAC,CAAC;QACtD,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,KAAK,CAAC,GAAG,EAAE,0DAA0D,CAAC,CAAC;QACvE,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,MAAM,SAAS,EAAE,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,KAAK,KAAK,IAAI,CAAC;AACxB,CAAC;AAED,yCAAyC;AAEzC,MAAM,QAAQ,GAAG,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC;AACtC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;IAC7B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACzE,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACxB,CAAC;AACD,SAAS,KAAK,CAAC,GAAW;IACxB,IAAI,GAAG,GAAG,UAAU,CAAC;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,GAAG,GAAG,QAAQ,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;IACzF,OAAO,CAAC,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;AAClC,CAAC;AACD,SAAS,QAAQ,CAAC,IAAY,EAAE,IAAY;IAC1C,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC7C,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5B,GAAG,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAClC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5B,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9D,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AAE/D;;;;GAIG;AACH,SAAS,aAAa,CAAC,IAAgB,EAAE,KAAa,EAAE,MAAc,EAAE,WAAmB;IACzF,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC9B,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC7B,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC9B,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY;IACzB,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,wBAAwB;IACrC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc;IAC5B,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;IACvB,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY;IAE1B,oEAAoE;IACpE,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;IACrD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;QAClC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,eAAe;QAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC,EAAE;YAAE,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC;IACrF,CAAC;IAED,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5C,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,OAAO;QACP,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;KAClC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,gBAAgB,CACvB,MAA2B,EAC3B,QAAgB,EAChB,SAAiB,EACjB,EAAE,GAAG,CAAC;IAEN,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,EAAE,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,EAAE,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,WAAW,GAAG,MAAM,CAAC,CAAC;IAClD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAgB;IACjC,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/B,IAAI,GAAG,GAAG,CAAC,CAAC;YACZ,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;gBAC/B,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC;gBAChD,MAAM,OAAO,GAAG,EAAE,GAAG,QAAQ,CAAC;gBAC9B,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;oBAC/B,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC;oBAC/C,MAAM,CAAC,GAAG,CAAC,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;oBAC7B,yEAAyE;oBACzE,sEAAsE;oBACtE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC;gBAC9E,CAAC;YACH,CAAC;YACD,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,EAAE,CAAC;gBACrB,8BAA8B;gBAC9B,IAAI,CAAC,CAAC,GAAG,WAAW,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACzD,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC9C,CAAC;AAED,oEAAoE;AACpE,SAAS,QAAQ,CAAC,KAAa,EAAE,MAAc;IAC7C,MAAM,WAAW,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,WAAW,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7D,OAAO,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;AACzD,CAAC;AAiBD,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACnE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAa,EACb,GAAU,EACV,IAAwC;IAExC,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;IACnF,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;IACxF,IAAI,MAAc,CAAC;IACnB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,oBAAoB,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;YACvF,4EAA4E;YAC5E,2EAA2E;YAC3E,gDAAgD;YAChD,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YAC9D,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE;gBAC3B,KAAK,EAAE,EAAE,IAAI,EAAE,OAAgB,EAAE,KAAK,EAAE,KAAK,GAAG,EAAE,EAAE;gBACpD,IAAI,EAAE,SAAS;gBACf,UAAU,EAAE,SAAS;aACtB,CAAC,CAAC;YACH,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YACtF,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;QACvF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,0EAA0E;YAC1E,yEAAyE;YACzE,0DAA0D;YAC1D,iBAAiB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAC/B,MAAM,GAAG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACjC,QAAQ,GAAG,IAAI,CAAC;QAClB,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACjC,QAAQ,GAAG,IAAI,CAAC;IAClB,CAAC;IACD,OAAO;QACL,MAAM;QACN,WAAW,EAAE,UAAU,CAAC,MAAM,CAAC;QAC/B,KAAK;QACL,MAAM;QACN,WAAW,EAAE,WAAW;QACxB,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxC,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,4DAA4D;AAC5D,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAC3C,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEtD,SAAS,iBAAiB,CAAC,OAAe;IACxC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,IAAI,GAAG,mBAAmB,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,IAAI,GAAG,GAAG,IAAI,GAAG,2BAA2B;QAAE,OAAO;IACrD,mBAAmB,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACtC,SAAS,CAAC,GAAG,EAAE,0DAA0D,OAAO,EAAE,CAAC,CAAC;AACtF,CAAC"}
@@ -0,0 +1,96 @@
1
+ export declare const TRMNL_DEFAULT_REFRESH = 180;
2
+ /**
3
+ * Cadence used only while an agent is AWAITING the user. NOT used for "working":
4
+ * a battery e-ink panel deep-sleeps between polls and full-flashes the screen on
5
+ * every refresh, so we only tighten the loop when the user actually needs to act.
6
+ */
7
+ export declare const TRMNL_DEFAULT_REFRESH_ACTIVE = 60;
8
+ /** Floor on any cadence — too-frequent polls drain the panel battery + flash it. */
9
+ export declare const TRMNL_MIN_REFRESH = 30;
10
+ /**
11
+ * Seconds the firmware waits for the image download before giving up (its
12
+ * `image_url_timeout`). Generous so a slow/flaky WiFi link doesn't trip the
13
+ * device's "not responding" (WIFI_FAILED) screen. Firmware caps at ~65s, so we
14
+ * keep a healthy default (most "not responding" cycles are a lossy image GET
15
+ * timing out, not a server error — see docs/devices.md TRMNL section).
16
+ */
17
+ export declare const TRMNL_DEFAULT_IMAGE_TIMEOUT = 50;
18
+ /** Hard cap the firmware honors (it clamps `image_url_timeout` ~65s internally). */
19
+ export declare const TRMNL_MAX_IMAGE_TIMEOUT = 65;
20
+ /**
21
+ * RSSI (dBm) at or below which the panel's WiFi link is treated as weak/lossy.
22
+ * A weak link is the dominant real-world cause of "not responding" (WIFI_FAILED):
23
+ * the image download drops packets and times out. RSSI is sent by the firmware on
24
+ * every poll, so we can react per-poll. (-78 ≈ marginal; below it, retransmits
25
+ * stack up and the default window can be too tight.)
26
+ */
27
+ export declare const TRMNL_WEAK_RSSI_DBM = -78;
28
+ /** Image-download window served on a weak link — near the firmware cap. */
29
+ export declare const TRMNL_WEAK_LINK_IMAGE_TIMEOUT = 60;
30
+ export interface TrmnlDevice {
31
+ /** Normalized (uppercase, colon-separated) MAC address — the device identity. */
32
+ mac: string;
33
+ /** Secret issued at /api/setup, presented back as the Access-Token header. */
34
+ apiKey: string;
35
+ /** Short human-friendly id shown on the device + in logs. */
36
+ friendlyId: string;
37
+ /** Optional user label. */
38
+ name?: string;
39
+ }
40
+ export interface TrmnlConfig {
41
+ enabled: boolean;
42
+ /** Idle/working cadence (seconds) — the slow, battery-friendly default. */
43
+ refreshRate: number;
44
+ /** Cadence (seconds) while a session is AWAITING the user. */
45
+ refreshActive: number;
46
+ /** Image-download timeout (seconds) handed to the firmware as image_url_timeout. */
47
+ imageUrlTimeout: number;
48
+ autoRegister: boolean;
49
+ devices: TrmnlDevice[];
50
+ }
51
+ /**
52
+ * Cadence for a poll. We only speed up for AWAITING (the user needs to act);
53
+ * "working" stays on the slow cadence because a deep-sleep e-ink panel can't be
54
+ * pushed and each wake full-flashes the screen + costs battery.
55
+ */
56
+ export declare function effectiveRefreshRate(cfg: TrmnlConfig, activity: {
57
+ awaiting: number;
58
+ working: number;
59
+ }): number;
60
+ /**
61
+ * Image-download timeout (seconds) for a single poll. Widens toward the firmware
62
+ * cap when the panel reports a weak WiFi signal, so a lossy image GET still has
63
+ * the maximum window to finish before the firmware shows "not responding"
64
+ * (WIFI_FAILED) — the #1 source of TRMNL flicker on a marginal link. A strong link
65
+ * keeps the lower default so a genuinely dead link doesn't hold the radio on
66
+ * (battery). RSSI absent (older firmware) ⇒ default; the default itself is already
67
+ * generous. Clamped to [5, firmware cap].
68
+ */
69
+ export declare function effectiveImageTimeout(cfg: TrmnlConfig, opts?: {
70
+ rssi?: number | null;
71
+ }): number;
72
+ /** Load the trmnl config with defaults filled in. */
73
+ export declare function loadTrmnlConfig(): TrmnlConfig;
74
+ /**
75
+ * Normalize a MAC to a stable identity key. A canonical 12-hex address becomes
76
+ * uppercase colon-separated pairs; any other id (some firmware/cloud setups
77
+ * report non-standard or punctuated identifiers) collapses to its bare uppercase
78
+ * hex digits. Returning the raw, untrimmed string for the odd case would orphan
79
+ * or duplicate a device whose punctuation varies between polls — so we always
80
+ * derive a deterministic key from the hex content.
81
+ */
82
+ export declare function normalizeMac(mac: string): string;
83
+ /** True when two MAC spellings resolve to the same canonical identity. */
84
+ export declare function sameMac(a: string, b: string): boolean;
85
+ export declare function findDeviceByMac(mac: string): TrmnlDevice | undefined;
86
+ /**
87
+ * Find or create a device record for the given MAC. Returns the device and
88
+ * whether it was newly created. Respects `autoRegister`: when off, a new MAC
89
+ * is NOT persisted and `created` is false with `device` undefined.
90
+ */
91
+ export declare function ensureDevice(mac: string, name?: string): {
92
+ device?: TrmnlDevice;
93
+ created: boolean;
94
+ };
95
+ export declare function removeDevice(mac: string): boolean;
96
+ //# sourceMappingURL=trmnl-settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trmnl-settings.d.ts","sourceRoot":"","sources":["../../src/trmnl/trmnl-settings.ts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAC/C,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAC9C,oFAAoF;AACpF,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,2EAA2E;AAC3E,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,2BAA2B;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB,oFAAoF;IACpF,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC9C,MAAM,CAER;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAAG,MAAM,CAMnG;AAeD,qDAAqD;AACrD,wBAAgB,eAAe,IAAI,WAAW,CAoB7C;AAQD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIhD;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEpE;AAWD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG;IAAE,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAgBnG;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOjD"}
@@ -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"}