@agentdeck/bridge 1.0.2 → 1.0.4

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 (175) hide show
  1. package/assets/fm-helper/agentdeck-fm-helper +0 -0
  2. package/dist/apme/collector.d.ts.map +1 -1
  3. package/dist/apme/collector.js +5 -2
  4. package/dist/apme/collector.js.map +1 -1
  5. package/dist/apme/runner.d.ts +11 -0
  6. package/dist/apme/runner.d.ts.map +1 -1
  7. package/dist/apme/runner.js +60 -4
  8. package/dist/apme/runner.js.map +1 -1
  9. package/dist/apme/settings.d.ts +10 -0
  10. package/dist/apme/settings.d.ts.map +1 -1
  11. package/dist/apme/settings.js +14 -0
  12. package/dist/apme/settings.js.map +1 -1
  13. package/dist/awaiting-overlay.d.ts +44 -10
  14. package/dist/awaiting-overlay.d.ts.map +1 -1
  15. package/dist/awaiting-overlay.js +96 -28
  16. package/dist/awaiting-overlay.js.map +1 -1
  17. package/dist/bridge-core.d.ts +10 -0
  18. package/dist/bridge-core.d.ts.map +1 -1
  19. package/dist/bridge-core.js +47 -12
  20. package/dist/bridge-core.js.map +1 -1
  21. package/dist/card-feed.d.ts +107 -0
  22. package/dist/card-feed.d.ts.map +1 -0
  23. package/dist/card-feed.js +277 -0
  24. package/dist/card-feed.js.map +1 -0
  25. package/dist/card-modules.d.ts +87 -0
  26. package/dist/card-modules.d.ts.map +1 -0
  27. package/dist/card-modules.js +179 -0
  28. package/dist/card-modules.js.map +1 -0
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +232 -69
  31. package/dist/cli.js.map +1 -1
  32. package/dist/daemon-server.d.ts.map +1 -1
  33. package/dist/daemon-server.js +1008 -47
  34. package/dist/daemon-server.js.map +1 -1
  35. package/dist/daemon-ws-client.d.ts +2 -1
  36. package/dist/daemon-ws-client.d.ts.map +1 -1
  37. package/dist/daemon-ws-client.js +2 -1
  38. package/dist/daemon-ws-client.js.map +1 -1
  39. package/dist/daemon.js +1 -1
  40. package/dist/device-photo.d.ts +86 -0
  41. package/dist/device-photo.d.ts.map +1 -0
  42. package/dist/device-photo.js +200 -0
  43. package/dist/device-photo.js.map +1 -0
  44. package/dist/device-voice-reply.d.ts +124 -0
  45. package/dist/device-voice-reply.d.ts.map +1 -0
  46. package/dist/device-voice-reply.js +236 -0
  47. package/dist/device-voice-reply.js.map +1 -0
  48. package/dist/device-voice.d.ts +56 -0
  49. package/dist/device-voice.d.ts.map +1 -0
  50. package/dist/device-voice.js +134 -0
  51. package/dist/device-voice.js.map +1 -0
  52. package/dist/esp32-serial.d.ts +40 -0
  53. package/dist/esp32-serial.d.ts.map +1 -1
  54. package/dist/esp32-serial.js +109 -13
  55. package/dist/esp32-serial.js.map +1 -1
  56. package/dist/foundation-models-helper.d.ts +30 -0
  57. package/dist/foundation-models-helper.d.ts.map +1 -1
  58. package/dist/foundation-models-helper.js +45 -0
  59. package/dist/foundation-models-helper.js.map +1 -1
  60. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -1
  61. package/dist/idotmatrix/idotmatrix-daemon-sync.js +5 -18
  62. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
  63. package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -1
  64. package/dist/idotmatrix/idotmatrix-discover.js +6 -14
  65. package/dist/idotmatrix/idotmatrix-discover.js.map +1 -1
  66. package/dist/index.js +2 -2
  67. package/dist/index.js.map +1 -1
  68. package/dist/mdns.d.ts +11 -0
  69. package/dist/mdns.d.ts.map +1 -1
  70. package/dist/mdns.js +13 -0
  71. package/dist/mdns.js.map +1 -1
  72. package/dist/observed-inject.d.ts +119 -0
  73. package/dist/observed-inject.d.ts.map +1 -0
  74. package/dist/observed-inject.js +405 -0
  75. package/dist/observed-inject.js.map +1 -0
  76. package/dist/passive-observer.d.ts +14 -0
  77. package/dist/passive-observer.d.ts.map +1 -1
  78. package/dist/passive-observer.js +35 -5
  79. package/dist/passive-observer.js.map +1 -1
  80. package/dist/peripheral-mapping.d.ts +65 -0
  81. package/dist/peripheral-mapping.d.ts.map +1 -0
  82. package/dist/peripheral-mapping.js +115 -0
  83. package/dist/peripheral-mapping.js.map +1 -0
  84. package/dist/python-ble-runtime.d.ts +62 -0
  85. package/dist/python-ble-runtime.d.ts.map +1 -0
  86. package/dist/python-ble-runtime.js +215 -0
  87. package/dist/python-ble-runtime.js.map +1 -0
  88. package/dist/session-aggregator.d.ts +4 -1
  89. package/dist/session-aggregator.d.ts.map +1 -1
  90. package/dist/session-aggregator.js +5 -5
  91. package/dist/session-aggregator.js.map +1 -1
  92. package/dist/session-id-resolve.d.ts +17 -0
  93. package/dist/session-id-resolve.d.ts.map +1 -0
  94. package/dist/session-id-resolve.js +35 -0
  95. package/dist/session-id-resolve.js.map +1 -0
  96. package/dist/session-transcript-timeline.d.ts +7 -0
  97. package/dist/session-transcript-timeline.d.ts.map +1 -1
  98. package/dist/session-transcript-timeline.js +16 -6
  99. package/dist/session-transcript-timeline.js.map +1 -1
  100. package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -1
  101. package/dist/timebox/timebox-daemon-sync.js +5 -20
  102. package/dist/timebox/timebox-daemon-sync.js.map +1 -1
  103. package/dist/timebox/timebox-discover.d.ts.map +1 -1
  104. package/dist/timebox/timebox-discover.js +6 -15
  105. package/dist/timebox/timebox-discover.js.map +1 -1
  106. package/dist/timeline-store.js +2 -2
  107. package/dist/timeline-store.js.map +1 -1
  108. package/dist/usage-event.d.ts.map +1 -1
  109. package/dist/usage-event.js +14 -7
  110. package/dist/usage-event.js.map +1 -1
  111. package/dist/voice.d.ts +13 -10
  112. package/dist/voice.d.ts.map +1 -1
  113. package/dist/voice.js +26 -272
  114. package/dist/voice.js.map +1 -1
  115. package/dist/ws-server.d.ts +14 -0
  116. package/dist/ws-server.d.ts.map +1 -1
  117. package/dist/ws-server.js +66 -1
  118. package/dist/ws-server.js.map +1 -1
  119. package/fm-helper/AgentDeckFMHelper.swift +303 -0
  120. package/package.json +8 -4
  121. package/python/requirements-ble.txt +5 -0
  122. package/src/idotmatrix/brightness.py +40 -0
  123. package/src/idotmatrix/scan.py +35 -0
  124. package/src/idotmatrix/sync.py +400 -0
  125. package/src/pysync/matrix_sync_common.py +89 -0
  126. package/src/timebox/scan_ble.py +52 -0
  127. package/src/timebox/sync_ble.py +394 -0
  128. package/dist/d200h/hid-protocol.d.ts +0 -62
  129. package/dist/d200h/hid-protocol.d.ts.map +0 -1
  130. package/dist/d200h/hid-protocol.js +0 -124
  131. package/dist/d200h/hid-protocol.js.map +0 -1
  132. package/dist/d200h/image-renderer.d.ts +0 -24
  133. package/dist/d200h/image-renderer.d.ts.map +0 -1
  134. package/dist/d200h/image-renderer.js +0 -321
  135. package/dist/d200h/image-renderer.js.map +0 -1
  136. package/dist/hook-migration.d.ts +0 -2
  137. package/dist/hook-migration.d.ts.map +0 -1
  138. package/dist/hook-migration.js +0 -104
  139. package/dist/hook-migration.js.map +0 -1
  140. package/dist/modules/d200h-module.d.ts +0 -65
  141. package/dist/modules/d200h-module.d.ts.map +0 -1
  142. package/dist/modules/d200h-module.js +0 -495
  143. package/dist/modules/d200h-module.js.map +0 -1
  144. package/dist/modules/d200h-renderer.d.ts +0 -7
  145. package/dist/modules/d200h-renderer.d.ts.map +0 -1
  146. package/dist/modules/d200h-renderer.js +0 -321
  147. package/dist/modules/d200h-renderer.js.map +0 -1
  148. package/dist/modules/trmnl-module.d.ts +0 -38
  149. package/dist/modules/trmnl-module.d.ts.map +0 -1
  150. package/dist/modules/trmnl-module.js +0 -111
  151. package/dist/modules/trmnl-module.js.map +0 -1
  152. package/dist/trmnl/byos-server.d.ts +0 -39
  153. package/dist/trmnl/byos-server.d.ts.map +0 -1
  154. package/dist/trmnl/byos-server.js +0 -301
  155. package/dist/trmnl/byos-server.js.map +0 -1
  156. package/dist/trmnl/frame-cache.d.ts +0 -37
  157. package/dist/trmnl/frame-cache.d.ts.map +0 -1
  158. package/dist/trmnl/frame-cache.js +0 -157
  159. package/dist/trmnl/frame-cache.js.map +0 -1
  160. package/dist/trmnl/image-renderer.d.ts +0 -27
  161. package/dist/trmnl/image-renderer.d.ts.map +0 -1
  162. package/dist/trmnl/image-renderer.js +0 -235
  163. package/dist/trmnl/image-renderer.js.map +0 -1
  164. package/dist/trmnl/trmnl-settings.d.ts +0 -96
  165. package/dist/trmnl/trmnl-settings.d.ts.map +0 -1
  166. package/dist/trmnl/trmnl-settings.js +0 -177
  167. package/dist/trmnl/trmnl-settings.js.map +0 -1
  168. package/dist/trmnl/trmnl-telemetry.d.ts +0 -47
  169. package/dist/trmnl/trmnl-telemetry.d.ts.map +0 -1
  170. package/dist/trmnl/trmnl-telemetry.js +0 -56
  171. package/dist/trmnl/trmnl-telemetry.js.map +0 -1
  172. package/dist/whisper-server-manager.d.ts +0 -19
  173. package/dist/whisper-server-manager.d.ts.map +0 -1
  174. package/dist/whisper-server-manager.js +0 -171
  175. package/dist/whisper-server-manager.js.map +0 -1
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env python3
2
+ """Sync AgentDeck frames to a Divoom Timebox Mini (BLE variant) over BLE GATT.
3
+
4
+ Some Timebox Mini revisions expose the 11x11 LED screen via BLE GATT using an
5
+ ISSC transparent-UART service (49535343-...), NOT Bluetooth Classic SPP. The
6
+ device appears in macOS as "TimeBox-mini-light" (a BLE peripheral) and shares
7
+ its BD_ADDR with the Classic audio endpoint under "TimeBox-mini-audio".
8
+
9
+ This writer builds the Divoom static-image protocol packet and tunnels it
10
+ through BLE GATT writes to the transparent-UART TX characteristic. Requires the
11
+ `bleak` package in the venv.
12
+ """
13
+
14
+ import argparse
15
+ import asyncio
16
+ import hashlib
17
+ import io
18
+ import os
19
+ import signal
20
+ import sys
21
+ import time
22
+ import urllib.request
23
+ from typing import Iterable
24
+
25
+ from PIL import Image as PilImage, ImageEnhance
26
+
27
+ # Shared HTTP/dim plumbing with the iDotMatrix client. We run from bridge/src/timebox/,
28
+ # so add the sibling pysync/ dir before importing the common module.
29
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "pysync"))
30
+ from matrix_sync_common import ( # noqa: E402
31
+ DEFAULT_URL,
32
+ POLL_INTERVAL,
33
+ BRIDGE_GONE_EXIT_SEC,
34
+ fetch_display_state,
35
+ bridge_reachable,
36
+ resolve_display_brightness as _resolve_display_brightness_common,
37
+ )
38
+
39
+ RECONNECT_DELAY = 3.0
40
+ # Force a re-push of the current frame at least this often even when the content
41
+ # is unchanged. A stateful write-without-response panel gives no delivery ACK, so
42
+ # a frame lost to an RF glitch or a brief dual-writer overlap (daemon restart)
43
+ # would otherwise stick until the content changes or the device is power-cycled.
44
+ # The heartbeat makes any such loss self-heal within a few seconds.
45
+ HEARTBEAT_SEC = 8.0
46
+
47
+ TIMEBOX_W = 11
48
+ TIMEBOX_H = 11
49
+ STATIC_IMAGE_CMD_LEN = 0x00BD
50
+
51
+ # ISSC transparent-UART service characteristics (discovered on Timebox Mini BLE)
52
+ WRITE_CHAR = "49535343-8841-43f4-a8d4-ecbe34729bb3" # write + write-without-response
53
+ CHUNK_SIZE = 20 # safe ATT payload for write-without-response
54
+
55
+
56
+ def clamp_nibble(value: int) -> int:
57
+ return max(0, min(15, value))
58
+
59
+
60
+ def escape_message(data: Iterable[int]) -> bytes:
61
+ out = bytearray()
62
+ out.append(0x01)
63
+ for b in data:
64
+ if b in (0x01, 0x02, 0x03):
65
+ out.append(0x03)
66
+ out.append(b + 0x03)
67
+ else:
68
+ out.append(b)
69
+ out.append(0x02)
70
+ return bytes(out)
71
+
72
+
73
+ def build_static_image_packet(image_bytes: bytes) -> bytes:
74
+ cmd = bytes([
75
+ STATIC_IMAGE_CMD_LEN & 0xFF,
76
+ (STATIC_IMAGE_CMD_LEN >> 8) & 0xFF,
77
+ 0x44,
78
+ 0x00,
79
+ 0x0A,
80
+ 0x0A,
81
+ 0x04,
82
+ ]) + image_bytes
83
+ if len(cmd) != STATIC_IMAGE_CMD_LEN:
84
+ raise ValueError(f"command length {len(cmd)} != {STATIC_IMAGE_CMD_LEN}")
85
+ checksum = sum(cmd) & 0xFFFF
86
+ return escape_message(cmd + bytes([checksum & 0xFF, (checksum >> 8) & 0xFF]))
87
+
88
+
89
+ def encode_image_bright(img: PilImage.Image, brightness: int, gamma: float, sat: float, contrast: float) -> bytes:
90
+ """Encode the native 11x11 micro frame to a 182-byte Timebox payload.
91
+
92
+ The source is `size=11&layout=micro` — a bold hand-authored creature glyph
93
+ already drawn at the device resolution with final, device-tuned colors. So the
94
+ pipeline is WYSIWYG by default (gamma/sat/contrast = 1.0): only the 0-100
95
+ software `brightness` dim is applied, then 4-bit quantization. The gamma/sat/
96
+ contrast args remain for manual tuning, but default to identity.
97
+ """
98
+ img = img.convert("RGB").resize((TIMEBOX_W, TIMEBOX_H), PilImage.Resampling.BOX)
99
+ if brightness <= 0:
100
+ return bytes(182)
101
+
102
+ if gamma != 1.0:
103
+ lut: list[int] = []
104
+ for _c in range(3):
105
+ lut.extend(min(255, int(255 * ((i / 255.0) ** gamma))) for i in range(256))
106
+ img = img.point(lut)
107
+ if sat != 1.0:
108
+ img = ImageEnhance.Color(img).enhance(sat)
109
+ if brightness != 100:
110
+ img = ImageEnhance.Brightness(img).enhance(brightness / 100.0)
111
+ if contrast != 1.0:
112
+ img = ImageEnhance.Contrast(img).enhance(contrast)
113
+
114
+ nibbles: list[int] = []
115
+ px = img.load()
116
+ for y in range(TIMEBOX_H):
117
+ for x in range(TIMEBOX_W):
118
+ r, g, b = px[x, y]
119
+ nibbles.extend([
120
+ clamp_nibble(round(r / 17)),
121
+ clamp_nibble(round(g / 17)),
122
+ clamp_nibble(round(b / 17)),
123
+ ])
124
+
125
+ out = bytearray()
126
+ it = iter(nibbles)
127
+ for low in it:
128
+ high = next(it, 0)
129
+ out.append(low | (high << 4))
130
+ if len(out) != 182:
131
+ raise ValueError(f"encoded Timebox image has {len(out)} bytes, expected 182")
132
+ return bytes(out)
133
+
134
+
135
+ def resolve_display_brightness(display_state, normal_brightness: int):
136
+ """Return (effective software brightness, dimmed, signature) for the host state.
137
+
138
+ The Timebox Mini has no hardware brightness command — brightness is baked into the
139
+ encoded frame by `encode_image_bright` (0 yields a blank frame). So the off floor is
140
+ 0 (a truly blank sleep frame) and the dim 'level' clamps down to 0 as well.
141
+ """
142
+ return _resolve_display_brightness_common(
143
+ display_state, normal_brightness, off_floor=0, level_floor=0
144
+ )
145
+
146
+
147
+ async def write_packet(client, packet: bytes) -> None:
148
+ """Chunked write-without-response to the transparent-UART TX characteristic."""
149
+ for i in range(0, len(packet), CHUNK_SIZE):
150
+ await client.write_gatt_char(WRITE_CHAR, packet[i:i + CHUNK_SIZE], response=False)
151
+
152
+
153
+ async def blank_panel(client) -> None:
154
+ """Push an all-black 11x11 farewell frame so the stateful LED panel doesn't
155
+ freeze on the last dashboard scene after we go away. The Timebox Mini has no
156
+ text resolution for an "OFFLINE" label (mirrors the Swift TimeboxModule, which
157
+ also blanks to 11x11 black; iDotMatrix/Pixoo can fit an OFFLINE glyph)."""
158
+ await write_packet(client, build_static_image_packet(bytes(182)))
159
+
160
+
161
+ async def push_micro_frame(client, url, brightness, gamma, sat, contrast, last_key, force=False) -> tuple[str, bool]:
162
+ """Fetch the native 11x11 micro frame and push it over BLE when its
163
+ content+brightness changed (or `force`). Returns (new dedup key, sent?).
164
+
165
+ `size=11&layout=micro` is a NATIVE 11x11 frame — a bold hand-authored creature
166
+ glyph on a status field, drawn pixel-for-pixel at the device resolution (no
167
+ downscale). The key mixes the source hash with `brightness` so a host display
168
+ sleep/wake (brightness change, same source frame) still forces a re-push.
169
+ """
170
+ frame_data = urllib.request.urlopen(
171
+ f"{url.rstrip('/')}/pixoo/frame?size=11&layout=micro", timeout=3.0
172
+ ).read()
173
+ key = f"{hashlib.sha256(frame_data).hexdigest()}|{brightness}"
174
+ if force or key != last_key:
175
+ img = PilImage.open(io.BytesIO(frame_data))
176
+ payload = encode_image_bright(img, brightness, gamma, sat, contrast)
177
+ await write_packet(client, build_static_image_packet(payload))
178
+ print(f"Frame sent ({key[:8]} @ {brightness}%)")
179
+ return key, True
180
+ return key, False
181
+
182
+
183
+ async def run(address: str, url: str, brightness: int, gamma: float, sat: float, contrast: float, once: bool = False) -> None:
184
+ print(f"Starting Timebox Mini BLE sync: {address} <- {url} brightness={brightness}% gamma={gamma}")
185
+ stop = asyncio.Event()
186
+ # Why we're stopping — decides the farewell. 'signal' = clean daemon shutdown
187
+ # (no successor → blank the panel). 'orphan' = parent died (a successor daemon
188
+ # may have taken over → don't clobber its frame). 'bridge_gone' = nobody home.
189
+ exit_reason = {"v": None}
190
+
191
+ def handle_stop(*_):
192
+ exit_reason["v"] = "signal"
193
+ stop.set()
194
+
195
+ loop = asyncio.get_running_loop()
196
+ for sig in (signal.SIGINT, signal.SIGTERM):
197
+ try:
198
+ loop.add_signal_handler(sig, handle_stop)
199
+ except (NotImplementedError, RuntimeError):
200
+ # Fall back to a classic handler that still requests a clean stop.
201
+ # The previous fallback installed a no-op handler that IGNORED
202
+ # SIGTERM — that stranded the process (the daemon's SIGTERM on
203
+ # shutdown did nothing) and left orphaned sync clients running for
204
+ # days, each holding the BLE link. Never ignore the signal.
205
+ signal.signal(sig, lambda *_: loop.call_soon_threadsafe(handle_stop))
206
+
207
+ # Imported lazily so the module is usable for encoding tests without bleak.
208
+ from bleak import BleakClient
209
+
210
+ last_bridge_ok = time.monotonic()
211
+
212
+ def should_exit() -> bool:
213
+ """True when we've been orphaned (parent daemon gone) or the bridge has
214
+ been unreachable long enough that nobody will ever stop us. A stateful
215
+ BLE panel can't detect a dropped link, so an orphan would otherwise loop
216
+ forever holding the single-central connection."""
217
+ if os.getppid() == 1:
218
+ print("Parent daemon gone (orphaned) — shutting down Timebox sync.")
219
+ exit_reason["v"] = "orphan"
220
+ return True
221
+ if time.monotonic() - last_bridge_ok > BRIDGE_GONE_EXIT_SEC:
222
+ print(f"Bridge unreachable >{int(BRIDGE_GONE_EXIT_SEC)}s — shutting down Timebox sync.")
223
+ exit_reason["v"] = "bridge_gone"
224
+ return True
225
+ return False
226
+
227
+ while not stop.is_set():
228
+ if should_exit():
229
+ stop.set()
230
+ break
231
+ link_lost = asyncio.Event()
232
+
233
+ def on_disconnect(_client):
234
+ # Fired by bleak when the GATT link drops (e.g. the Timebox is powered
235
+ # off). A stateful BLE panel driven by write-without-response means our
236
+ # writes can silently "succeed" over a dead link, so this callback — not
237
+ # a write error — is the authoritative "reconnect now" signal. Without it
238
+ # the inner loop would spin forever on a dead link and the outer reconnect
239
+ # path (below) would never run.
240
+ print("BLE peripheral disconnected — will reconnect.", file=sys.stderr)
241
+ loop.call_soon_threadsafe(link_lost.set)
242
+
243
+ try:
244
+ print(f"Connecting BLE {address}...")
245
+ async with BleakClient(address, timeout=15.0, disconnected_callback=on_disconnect) as client:
246
+ print(f"BLE connected (MTU={client.mtu_size})")
247
+
248
+ last_key = ""
249
+ last_sent_at = time.monotonic()
250
+ current_brightness = brightness
251
+ display_dimmed = False
252
+ last_display_signature = ""
253
+ # Honor the host display dim state already at connect time so a
254
+ # reconnect while the screen is asleep comes up dim/blank, not bright.
255
+ try:
256
+ current_brightness, display_dimmed, last_display_signature, _mode = \
257
+ resolve_display_brightness(fetch_display_state(url), brightness)
258
+ last_bridge_ok = time.monotonic()
259
+ except Exception:
260
+ current_brightness, display_dimmed, last_display_signature = brightness, False, ""
261
+
262
+ while not stop.is_set():
263
+ if should_exit():
264
+ stop.set()
265
+ break
266
+ # 0. The link dropped (peripheral powered off / out of range):
267
+ # leave the `async with` so the outer loop reconnects by
268
+ # address (macOS bleak addresses are stable UUIDs). Checked
269
+ # every iteration because write-without-response never errors
270
+ # on a dead link — only this flag / is_connected reveals it.
271
+ if link_lost.is_set() or not client.is_connected:
272
+ print("BLE link lost — reconnecting.", file=sys.stderr)
273
+ break
274
+ # 1. Apply host display sleep/wake. The daemon exposes the same
275
+ # display_state Pixoo/iDotMatrix/ESP32 receive; older session-
276
+ # only bridges omit it (keep the configured brightness).
277
+ transitioned = False
278
+ try:
279
+ eff_b, dimmed, sig, _mode = resolve_display_brightness(fetch_display_state(url), brightness)
280
+ last_bridge_ok = time.monotonic()
281
+ if sig != last_display_signature or eff_b != current_brightness:
282
+ current_brightness, display_dimmed, last_display_signature = eff_b, dimmed, sig
283
+ transitioned = True
284
+ print(f"Host display {'asleep' if dimmed else 'awake'} — brightness {eff_b}%")
285
+ except Exception:
286
+ pass
287
+
288
+ # 2. While the host display is asleep, push one frame at the dim
289
+ # brightness on the transition (0 => blank sleep frame), then
290
+ # pause polling to save BLE bandwidth (mirrors iDotMatrix).
291
+ if display_dimmed:
292
+ if transitioned or once:
293
+ try:
294
+ last_key, sent = await push_micro_frame(
295
+ client, url, current_brightness, gamma, sat, contrast, last_key, force=True
296
+ )
297
+ if sent:
298
+ last_sent_at = time.monotonic()
299
+ last_bridge_ok = time.monotonic()
300
+ except Exception as e:
301
+ print(f"Dim-frame send error: {e}", file=sys.stderr)
302
+ if link_lost.is_set() or not client.is_connected:
303
+ break
304
+ if once:
305
+ return
306
+ try:
307
+ await asyncio.wait_for(stop.wait(), timeout=POLL_INTERVAL)
308
+ except asyncio.TimeoutError:
309
+ pass
310
+ continue
311
+
312
+ # 3. Normal streaming — push when content/brightness changed, or
313
+ # force a heartbeat re-push so a frame lost to an RF glitch or a
314
+ # brief dual-writer overlap (daemon restart) self-heals instead
315
+ # of sticking until the content changes or a power-cycle.
316
+ force_beat = (time.monotonic() - last_sent_at) >= HEARTBEAT_SEC
317
+ try:
318
+ last_key, sent = await push_micro_frame(
319
+ client, url, current_brightness, gamma, sat, contrast, last_key, force=force_beat
320
+ )
321
+ if sent:
322
+ last_sent_at = time.monotonic()
323
+ last_bridge_ok = time.monotonic()
324
+ except Exception as e:
325
+ print(f"Frame fetch/send error: {e}", file=sys.stderr)
326
+ # A write error after the link dropped must escalate to a
327
+ # reconnect; a transient HTTP fetch error (bridge blip) must
328
+ # not — keep streaming and let should_exit() handle a truly
329
+ # gone bridge.
330
+ if link_lost.is_set() or not client.is_connected:
331
+ break
332
+ if once:
333
+ return
334
+ try:
335
+ await asyncio.wait_for(stop.wait(), timeout=POLL_INTERVAL)
336
+ except asyncio.TimeoutError:
337
+ pass
338
+
339
+ # Inner loop exited. If we're shutting down (SIGTERM, orphaned, or
340
+ # bridge gone) and the link is still up, blank the panel before the
341
+ # `async with` drops BLE — otherwise the stateful LED panel freezes
342
+ # on the last dashboard frame forever (parity with iDotMatrix's
343
+ # OFFLINE farewell and the Swift TimeboxModule's 11x11 black blank).
344
+ #
345
+ # EXCEPT when a successor daemon has already taken over: our parent
346
+ # died abruptly (orphan) but the bridge is answering again, so a new
347
+ # daemon restarted and is repainting the panel. Blanking here would
348
+ # clobber its fresh frame and the panel would sit blank until a
349
+ # power-cycle — the exact failure this guard prevents.
350
+ successor_took_over = exit_reason["v"] == "orphan" and bridge_reachable(url)
351
+ if successor_took_over:
352
+ print("Successor daemon detected — skipping farewell blank (it will repaint).")
353
+ if stop.is_set() and client.is_connected and not successor_took_over:
354
+ try:
355
+ await blank_panel(client)
356
+ # blank_panel writes WITHOUT response — the await returns once
357
+ # the packet is queued to the OS, not once it's transmitted.
358
+ # The `async with` below drops the BLE link immediately on
359
+ # exit; without this beat the queued blank never goes over the
360
+ # air and the panel freezes on its last dashboard frame.
361
+ await asyncio.sleep(0.5)
362
+ print("Shutting down — blanked Timebox panel.")
363
+ except Exception as e:
364
+ print(f"Farewell blank failed: {e}", file=sys.stderr)
365
+ except Exception as e:
366
+ print(f"BLE connection error: {e}", file=sys.stderr)
367
+ if once:
368
+ raise
369
+ try:
370
+ await asyncio.wait_for(stop.wait(), timeout=RECONNECT_DELAY)
371
+ except asyncio.TimeoutError:
372
+ pass
373
+
374
+
375
+ def main() -> None:
376
+ parser = argparse.ArgumentParser(description="AgentDeck Timebox Mini BLE sync")
377
+ parser.add_argument("--address", required=True, help="BLE address/UUID of TimeBox-mini-light")
378
+ parser.add_argument("--url", default=DEFAULT_URL, help=f"AgentDeck bridge URL (default: {DEFAULT_URL})")
379
+ parser.add_argument("--brightness", type=int, default=100, help="Software brightness 0-100")
380
+ parser.add_argument("--gamma", type=float, default=1.0, help="Gamma (lower=brighter midtones; 1.0=off)")
381
+ parser.add_argument("--sat", type=float, default=1.0, help="Saturation multiplier (1.0=off)")
382
+ parser.add_argument("--contrast", type=float, default=1.0, help="Contrast multiplier (1.0=off)")
383
+ parser.add_argument("--once", action="store_true", help="Send one frame and exit")
384
+ args = parser.parse_args()
385
+
386
+ if not (0 <= args.brightness <= 100):
387
+ print("Brightness must be between 0 and 100.", file=sys.stderr)
388
+ sys.exit(1)
389
+
390
+ asyncio.run(run(args.address, args.url, args.brightness, args.gamma, args.sat, args.contrast, args.once))
391
+
392
+
393
+ if __name__ == "__main__":
394
+ main()
@@ -1,62 +0,0 @@
1
- /**
2
- * D200H HID Protocol — D200-compatible 1024-byte packet protocol
3
- *
4
- * Packet structure:
5
- * [0-1] Header: 0x7C 0x7C
6
- * [2-3] Command: big-endian uint16
7
- * [4-7] Length: little-endian uint32
8
- * [8-1023] Payload (zero-padded)
9
- *
10
- * Image delivery: ZIP(manifest.json + icons/*.png) sent in chunked 1024-byte packets.
11
- * ZIP quirk: bytes at offset 1016, 2040, 3064... must NOT be 0x00 or 0x7C.
12
- */
13
- export declare const CMD: {
14
- readonly SET_BUTTONS: 1;
15
- readonly PARTIAL_UPDATE: 13;
16
- readonly SET_SMALL_WINDOW: 6;
17
- readonly SET_BRIGHTNESS: 10;
18
- readonly SET_LABEL_STYLE: 11;
19
- readonly IN_BUTTON: 257;
20
- readonly IN_DEVICE_INFO: 771;
21
- };
22
- export declare function buildPacket(command: number, payload: Buffer, totalLength?: number): Buffer;
23
- /**
24
- * Build all packets for a ZIP file transfer (SET_BUTTONS or PARTIAL_UPDATE).
25
- * First packet has the header; subsequent packets are raw 1024-byte chunks.
26
- */
27
- export declare function buildZipPackets(zipData: Buffer, command?: number): Buffer[];
28
- export declare function buildBrightnessPacket(brightness: number): Buffer;
29
- export declare function buildSmallWindowPacket(mode: number, cpu: number, mem: number, time: string, gpu: number): Buffer;
30
- export interface ButtonEvent {
31
- index: number;
32
- pressed: boolean;
33
- state: number;
34
- }
35
- export interface DeviceInfo {
36
- serialNumber: string;
37
- firmwareVersion: string;
38
- deviceType: string;
39
- hardwareVersion: string;
40
- }
41
- export type IncomingEvent = {
42
- type: 'button';
43
- data: ButtonEvent;
44
- } | {
45
- type: 'device_info';
46
- data: DeviceInfo;
47
- } | {
48
- type: 'unknown';
49
- command: number;
50
- raw: Buffer;
51
- };
52
- export declare function parseIncoming(data: Buffer): IncomingEvent | null;
53
- /**
54
- * Check if a ZIP has invalid bytes at 1024-byte chunk boundaries.
55
- * At offset 1016, 2040, 3064... the byte must not be 0x00 or 0x7C.
56
- */
57
- export declare function validateZipBoundaries(zipData: Buffer): boolean;
58
- /**
59
- * Generate a random dummy string to pad the ZIP until boundaries are valid.
60
- */
61
- export declare function generateDummyPadding(attempt: number): string;
62
- //# sourceMappingURL=hid-protocol.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hid-protocol.d.ts","sourceRoot":"","sources":["../../src/d200h/hid-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAaH,eAAO,MAAM,GAAG;;;;;;;;CAQN,CAAC;AAIX,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAY1F;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,MAAwB,GAAG,MAAM,EAAE,CAiB5F;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGhE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGhH;AAID,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAoChE;AAID;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO9D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D"}
@@ -1,124 +0,0 @@
1
- /**
2
- * D200H HID Protocol — D200-compatible 1024-byte packet protocol
3
- *
4
- * Packet structure:
5
- * [0-1] Header: 0x7C 0x7C
6
- * [2-3] Command: big-endian uint16
7
- * [4-7] Length: little-endian uint32
8
- * [8-1023] Payload (zero-padded)
9
- *
10
- * Image delivery: ZIP(manifest.json + icons/*.png) sent in chunked 1024-byte packets.
11
- * ZIP quirk: bytes at offset 1016, 2040, 3064... must NOT be 0x00 or 0x7C.
12
- */
13
- import { randomBytes } from 'crypto';
14
- const TAG = 'd200h-hid';
15
- const PACKET_SIZE = 1024;
16
- const HEADER = Buffer.from([0x7c, 0x7c]);
17
- const INVALID_BOUNDARY_BYTES = [0x00, 0x7c];
18
- // --- Commands ---
19
- export const CMD = {
20
- SET_BUTTONS: 0x0001,
21
- PARTIAL_UPDATE: 0x000d,
22
- SET_SMALL_WINDOW: 0x0006,
23
- SET_BRIGHTNESS: 0x000a,
24
- SET_LABEL_STYLE: 0x000b,
25
- IN_BUTTON: 0x0101,
26
- IN_DEVICE_INFO: 0x0303,
27
- };
28
- // --- Packet building ---
29
- export function buildPacket(command, payload, totalLength) {
30
- const pkt = Buffer.alloc(PACKET_SIZE, 0);
31
- // Header
32
- pkt[0] = 0x7c;
33
- pkt[1] = 0x7c;
34
- // Command (big-endian uint16)
35
- pkt.writeUInt16BE(command, 2);
36
- // Length (little-endian uint32) — total file size for multi-packet, else payload length
37
- pkt.writeUInt32LE(totalLength ?? payload.length, 4);
38
- // Payload
39
- payload.copy(pkt, 8, 0, Math.min(payload.length, PACKET_SIZE - 8));
40
- return pkt;
41
- }
42
- /**
43
- * Build all packets for a ZIP file transfer (SET_BUTTONS or PARTIAL_UPDATE).
44
- * First packet has the header; subsequent packets are raw 1024-byte chunks.
45
- */
46
- export function buildZipPackets(zipData, command = CMD.SET_BUTTONS) {
47
- const packets = [];
48
- const fileSize = zipData.length;
49
- // First packet: header(8) + first chunk
50
- const firstChunkSize = PACKET_SIZE - 8;
51
- const firstChunk = zipData.subarray(0, firstChunkSize);
52
- packets.push(buildPacket(command, firstChunk, fileSize));
53
- // Remaining chunks (raw, no header)
54
- for (let offset = firstChunkSize; offset < fileSize; offset += PACKET_SIZE) {
55
- const chunk = Buffer.alloc(PACKET_SIZE, 0);
56
- zipData.copy(chunk, 0, offset, Math.min(offset + PACKET_SIZE, fileSize));
57
- packets.push(chunk);
58
- }
59
- return packets;
60
- }
61
- export function buildBrightnessPacket(brightness) {
62
- const val = Math.max(0, Math.min(100, Math.round(brightness)));
63
- return buildPacket(CMD.SET_BRIGHTNESS, Buffer.from(String(val), 'utf-8'));
64
- }
65
- export function buildSmallWindowPacket(mode, cpu, mem, time, gpu) {
66
- const data = `${mode}|${cpu}|${mem}|${time}|${gpu}`;
67
- return buildPacket(CMD.SET_SMALL_WINDOW, Buffer.from(data, 'utf-8'));
68
- }
69
- export function parseIncoming(data) {
70
- if (data.length < 8)
71
- return null;
72
- if (data[0] !== 0x7c || data[1] !== 0x7c)
73
- return null;
74
- const command = data.readUInt16BE(2);
75
- if (command === CMD.IN_BUTTON && data.length >= 12) {
76
- return {
77
- type: 'button',
78
- data: {
79
- state: data[8],
80
- index: data[9],
81
- pressed: data[11] === 0x01,
82
- },
83
- };
84
- }
85
- if (command === CMD.IN_DEVICE_INFO) {
86
- try {
87
- const jsonStr = data.subarray(8).toString('ascii').split('\0')[0];
88
- const info = JSON.parse(jsonStr);
89
- return {
90
- type: 'device_info',
91
- data: {
92
- serialNumber: info.SerialNumber ?? '',
93
- firmwareVersion: info.Dversion ?? '',
94
- deviceType: info.DeviceType ?? '',
95
- hardwareVersion: info.HardwareVersion ?? '',
96
- },
97
- };
98
- }
99
- catch {
100
- return { type: 'unknown', command, raw: data };
101
- }
102
- }
103
- return { type: 'unknown', command, raw: data };
104
- }
105
- // --- ZIP validation (boundary byte workaround) ---
106
- /**
107
- * Check if a ZIP has invalid bytes at 1024-byte chunk boundaries.
108
- * At offset 1016, 2040, 3064... the byte must not be 0x00 or 0x7C.
109
- */
110
- export function validateZipBoundaries(zipData) {
111
- for (let i = 1016; i < zipData.length; i += PACKET_SIZE) {
112
- if (INVALID_BOUNDARY_BYTES.includes(zipData[i])) {
113
- return false;
114
- }
115
- }
116
- return true;
117
- }
118
- /**
119
- * Generate a random dummy string to pad the ZIP until boundaries are valid.
120
- */
121
- export function generateDummyPadding(attempt) {
122
- return 'AgentDeck ' + randomBytes(8 * (attempt + 1)).toString('hex');
123
- }
124
- //# sourceMappingURL=hid-protocol.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hid-protocol.js","sourceRoot":"","sources":["../../src/d200h/hid-protocol.ts"],"names":[],"mappings":"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"}
@@ -1,24 +0,0 @@
1
- /**
2
- * D200H Image Renderer — Renders AgentDeck state as 196×196 PNG key icons
3
- * using the shared SVG renderers (same visual output as Stream Deck plugin).
4
- *
5
- * Pipeline: state → shared SVG generators (144×144) → resvg rasterize (196×196 PNG) → ZIP
6
- *
7
- * Falls back to solid-color PNGs if resvg-js is not available.
8
- */
9
- import { parseState, buildButtonCommandMap } from '@agentdeck/shared';
10
- export { parseState, buildButtonCommandMap };
11
- export type { DashState, ButtonCommand, KeySlot } from '@agentdeck/shared';
12
- /** Initialize the renderer (call once at module start). */
13
- export declare function initRenderer(): Promise<void>;
14
- export declare function isResvgLoaded(): boolean;
15
- /**
16
- * Render the full AgentDeck dashboard as a ZIP ready for SET_BUTTONS.
17
- * Uses shared SVG renderers → resvg rasterization for SD+-quality output.
18
- */
19
- export declare function renderDashboardZip(stateEvt: any): Buffer;
20
- /**
21
- * Create a simple hash of the visual state for change detection.
22
- */
23
- export declare function stateHash(stateEvt: any): string;
24
- //# sourceMappingURL=image-renderer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"image-renderer.d.ts","sourceRoot":"","sources":["../../src/d200h/image-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,EAEL,UAAU,EACV,qBAAqB,EAEtB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,CAAC;AAC7C,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AA2D3E,2DAA2D;AAC3D,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAElD;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AA+LD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,GAAG,GAAG,MAAM,CAmExD;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,GAAG,GAAG,MAAM,CAI/C"}