nsd-ble 0.3.3 → 0.4.1

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/README.md ADDED
@@ -0,0 +1,887 @@
1
+ # nsd-ble
2
+
3
+ A TypeScript library for communicating with NotSoDoom Bluetooth standees via the Web Bluetooth API. Connect to one standee and communicate with the entire mesh network.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install nsd-ble
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { useMesh, useStandees } from "nsd-ble";
15
+
16
+ const mesh = useMesh();
17
+ const { standees, onChange } = useStandees();
18
+
19
+ // connect() must be called from a user gesture (e.g. button click)
20
+ document.getElementById("connect").addEventListener("click", async () => {
21
+ await mesh.connect();
22
+ });
23
+
24
+ // React to standees joining the mesh
25
+ onChange.attach((list) => {
26
+ console.log(
27
+ "Standees:",
28
+ list.map((s) => s.name()),
29
+ );
30
+ });
31
+ ```
32
+
33
+ ## Requirements
34
+
35
+ - A browser that supports the [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API) (Chrome, Edge, Opera)
36
+ - `connect()` must be called from a user-initiated event (click, tap, etc.) due to Web Bluetooth security requirements
37
+
38
+ ## Imports
39
+
40
+ The package exposes four entry points:
41
+
42
+ ```typescript
43
+ // Core composables
44
+ import {
45
+ useMesh,
46
+ useStandees,
47
+ useGraphics,
48
+ useLoader,
49
+ useDisplay,
50
+ useStandeeElements,
51
+ useActions,
52
+ useButtons,
53
+ useConverter,
54
+ useBinary,
55
+ useImage,
56
+ useUtils,
57
+ useDebug,
58
+ useMockBluetooth,
59
+ } from "nsd-ble";
60
+
61
+ // Display elements
62
+ import { Rectangle, Image, Text, DisplayElement } from "nsd-ble/elements";
63
+
64
+ // Object types
65
+ import { Standee, ButtonResult, Buffer } from "nsd-ble/objects";
66
+
67
+ // Enums
68
+ import {
69
+ Font,
70
+ Button,
71
+ ActionType,
72
+ ElementData,
73
+ ActionCondition,
74
+ ActionDataType,
75
+ HandshakeType,
76
+ DataType,
77
+ FloodTypes,
78
+ Data,
79
+ } from "nsd-ble/enums";
80
+ ```
81
+
82
+ ## Core Concepts
83
+
84
+ ### Mesh Network
85
+
86
+ Standees form a Bluetooth mesh network. You only need to connect to a single standee — messages are automatically flooded to all reachable nodes.
87
+
88
+ ### Composable Pattern
89
+
90
+ All functionality is accessed through `useX()` composable functions. Some composables are global (`useMesh`, `useDisplay`) while others are scoped to a specific standee (`useGraphics(standee)`, `useLoader(standee)`).
91
+
92
+ ### Buffer System
93
+
94
+ Most operations return a `Buffer` object. Call `.send()` on it to transmit the data over BLE:
95
+
96
+ ```typescript
97
+ const buffer = useDisplay().draw(standee);
98
+ await buffer.send();
99
+ ```
100
+
101
+ Buffers can also be chained with `.then()`:
102
+
103
+ ```typescript
104
+ useDisplay()
105
+ .clear(standee)
106
+ .then(() => {
107
+ console.log("Screen cleared");
108
+ })
109
+ .send();
110
+ ```
111
+
112
+ ## API Reference
113
+
114
+ ### useMesh()
115
+
116
+ Main entry point. Manages the BLE connection and mesh-level events. Auto-reconnect with exponential backoff is enabled automatically after the first successful connection.
117
+
118
+ ```typescript
119
+ const {
120
+ connect, // () => Promise<void> — opens the BLE device picker
121
+ reconnect, // () => Promise<void> — reconnects to the last device
122
+ onConnect, // SyncEvent<void> — fires when connected
123
+ onDisconnect, // SyncEvent<void> — fires when disconnected
124
+ onReconnecting, // SyncEvent<ReconnectStatus> — auto-reconnect attempt in progress
125
+ onReconnectFailed, // SyncEvent<void> — all auto-reconnect retries exhausted
126
+ onNodes, // SyncEvent<Map<number, number[]>> — all discovered nodes
127
+ onNode, // SyncEvent<Map<number, number[]>> — new node joined
128
+ onNodeLost, // SyncEvent<number> — node left the mesh
129
+ onPlusButton, // SyncEvent<ButtonResult>
130
+ onMinButton, // SyncEvent<ButtonResult>
131
+ onEitherButton, // SyncEvent<ButtonResult>
132
+ onMessageDone, // SyncEvent<number> — message transmission complete
133
+ } = useMesh();
134
+ ```
135
+
136
+ Auto-reconnect uses exponential backoff starting at 2 seconds, doubling each attempt up to 30 seconds, with a maximum of 5 retries. Subscribe to `onReconnecting` to track reconnect attempts:
137
+
138
+ ```typescript
139
+ mesh.onReconnecting.attach((status) => {
140
+ // status.attempt — current attempt number
141
+ // status.maxRetries — maximum retries (5)
142
+ // status.delay — delay before this attempt in ms
143
+ console.log(`Reconnecting: attempt ${status.attempt}/${status.maxRetries}`);
144
+ });
145
+
146
+ mesh.onReconnectFailed.attach(() => {
147
+ console.log("All reconnect attempts failed");
148
+ });
149
+ ```
150
+
151
+ ### useStandees()
152
+
153
+ Tracks discovered standees as a reactive list.
154
+
155
+ ```typescript
156
+ const {
157
+ standees, // Standee[] — current list of standees
158
+ onChange, // SyncEvent<Standee[]> — fires when the list changes
159
+ } = useStandees();
160
+ ```
161
+
162
+ ### useGraphics(standee)
163
+
164
+ Upload images to a standee's memory. Images are stored by numeric ID (0-255) and can later be referenced by display elements.
165
+
166
+ ```typescript
167
+ const { upload, clear } = useGraphics(standee);
168
+
169
+ // Convert an image to bytes and upload it as ID 0
170
+ const bytes = await useConverter().imageToBytes("/images/icon.png");
171
+ await upload(0, bytes).send();
172
+
173
+ // Clear all uploaded graphics
174
+ await clear().send();
175
+ ```
176
+
177
+ ### useLoader(standee)
178
+
179
+ Sends multiple buffers in sequence with progress tracking. Useful for uploading a batch of images.
180
+
181
+ ```typescript
182
+ const loader = useLoader(standee).load(buffers);
183
+
184
+ loader.onProgress.attach((progress) => {
185
+ // progress.current — buffers completed so far
186
+ // progress.total — total buffers
187
+ // progress.percentage — 0 to 1
188
+ console.log(`${Math.round(progress.percentage * 100)}%`);
189
+ });
190
+
191
+ loader.onError.attach((error) => {
192
+ // error.buffer — index of the buffer that failed
193
+ // error.error — the Error object
194
+ console.error(`Buffer ${error.buffer} failed:`, error.error);
195
+ });
196
+
197
+ await loader.send();
198
+ ```
199
+
200
+ ### useStandeeElements(standee)
201
+
202
+ Add and remove display elements on a standee. Elements are auto-assigned an ID.
203
+
204
+ ```typescript
205
+ const { add, remove } = useStandeeElements(standee);
206
+
207
+ const rect = add(new Rectangle(0, 0, 128, 64));
208
+ const text = add(new Text(10, 30, "Hello", Font.u8g2_font_5x7_mf));
209
+ const img = add(new Image(80, 10, 32, 32, 0)); // references uploaded image ID 0
210
+
211
+ remove(rect.id);
212
+ ```
213
+
214
+ ### useDisplay()
215
+
216
+ Draw elements to the standee's screen or clear it.
217
+
218
+ ```typescript
219
+ const { draw, clear } = useDisplay();
220
+
221
+ // Draw all dirty elements
222
+ await draw(standee).send();
223
+
224
+ // Clear the screen
225
+ await clear(standee).send();
226
+ ```
227
+
228
+ Only elements marked as `dirty` are redrawn. Modifying any element property (e.g. `rect.x = 50`) automatically marks it dirty.
229
+
230
+ ### useActions() and useButtons(standee)
231
+
232
+ Define interactive behavior triggered by the standee's physical buttons.
233
+
234
+ ```typescript
235
+ const actions = useActions();
236
+ const { setButton } = useButtons(standee);
237
+
238
+ // Create a target: which element property to affect
239
+ const target = actions.target(textElement.id, ElementData.X);
240
+
241
+ // Assign actions to the plus button
242
+ await setButton(Button.PLUS, [actions.increment(target)]).send();
243
+ ```
244
+
245
+ Available actions:
246
+
247
+ | Action | Description |
248
+ | ------------------------------------------------------ | ---------------------------------- |
249
+ | `increment(target)` | Increment a property value |
250
+ | `decrement(target)` | Decrement a property value |
251
+ | `show(elementId)` | Show an element |
252
+ | `hide(elementId)` | Hide an element |
253
+ | `set(target, value)` | Set a property to a specific value |
254
+ | `broadcast(target?)` | Broadcast a value across the mesh |
255
+ | `stop()` | Break the action chain |
256
+ | `timer(ms, resetable, onComplete)` | Run actions after a delay |
257
+ | `condition(left, op, right, actions)` | Conditional action execution |
258
+ | `map(value, fromLow, fromHigh, toLow, toHigh, target)` | Map a value between ranges |
259
+
260
+ The `condition` action supports comparing targets, numbers, and strings. The `op` parameter uses `ActionCondition` values:
261
+
262
+ ```typescript
263
+ // Show an element when HP equals 0
264
+ const hp = actions.target(label.id, ElementData.TEXT);
265
+ actions.condition(hp, ActionCondition.EQUAL, 0, [actions.show(gameOverElement.id)]);
266
+ ```
267
+
268
+ ### useConverter()
269
+
270
+ Converts images to the monochrome byte format the standees expect.
271
+
272
+ ```typescript
273
+ const { imageToBytes } = useConverter();
274
+ const bytes = await imageToBytes("/path/to/image.png");
275
+ ```
276
+
277
+ ### useBinary()
278
+
279
+ Low-level binary utilities for pixel data conversion.
280
+
281
+ ```typescript
282
+ const { pixelsToBytes, bitswap, flattenUint8Arrays } = useBinary();
283
+
284
+ // Convert pixel array to monochrome byte format
285
+ const bytes = pixelsToBytes(pixels, width);
286
+
287
+ // Concatenate multiple Uint8Arrays into one
288
+ const combined = flattenUint8Arrays([array1, array2]);
289
+ ```
290
+
291
+ ### useImage()
292
+
293
+ Low-level image processing utilities used internally by `useConverter()`.
294
+
295
+ ```typescript
296
+ const { createImage, getImageData, imageDataToPixels } = useImage();
297
+
298
+ // Load an image from URL
299
+ const image = await createImage("/path/to/image.png");
300
+
301
+ // Get raw ImageData from an HTMLImageElement
302
+ const imageData = getImageData(image);
303
+
304
+ // Convert ImageData to monochrome pixel array
305
+ const pixels = imageDataToPixels(imageData);
306
+ // { width, height, pixels: number[] } — pixels are 0 (black) or 1 (white)
307
+ ```
308
+
309
+ ### useUtils()
310
+
311
+ General-purpose utility functions.
312
+
313
+ ```typescript
314
+ const { map } = useUtils();
315
+
316
+ // Map a value from one range to another
317
+ const mapped = map(value, fromLow, fromHigh, toLow, toHigh);
318
+ ```
319
+
320
+ ### useDebug()
321
+
322
+ Message inspector and debug mode. Logs decoded packet contents, retransmission events, and message timing in a structured format. Emits events for building debug UIs.
323
+
324
+ ```typescript
325
+ const debug = useDebug();
326
+
327
+ // Enable debug logging (no overhead when disabled)
328
+ debug.enable();
329
+
330
+ // Check if debug mode is active
331
+ debug.isEnabled(); // true
332
+
333
+ // Subscribe to all debug events in real-time
334
+ debug.onDebugEvent.attach((entry) => {
335
+ console.log(`[${entry.type}]`, entry.data);
336
+ });
337
+
338
+ // Track message latency
339
+ debug.onTimingUpdate.attach((timing) => {
340
+ if (timing.latencyMs !== null) {
341
+ console.log(`Cache ${timing.cacheId}: ${timing.latencyMs}ms`);
342
+ }
343
+ });
344
+
345
+ // Read the full event log
346
+ const log = debug.getLog();
347
+
348
+ // Get timing records for all messages
349
+ const timings = debug.getTimings();
350
+
351
+ // Get timing for a specific cache ID
352
+ const timing = debug.getTimingForCache(cacheId);
353
+
354
+ // Decode a raw BLE packet into a structured object
355
+ const decoded = debug.decodePacket(rawPacket);
356
+ // { cacheId, targetId, ttl, position, dataSize, typeName, typeValue, payloadBytes, raw }
357
+
358
+ // Control
359
+ debug.disable(); // Pause logging
360
+ debug.clear(); // Clear log and timing data
361
+ debug.setMaxLogSize(1000); // Adjust log buffer size (default: 500)
362
+ ```
363
+
364
+ Event types logged: `message_queued`, `packet_sent`, `retransmit`, `timeout`, `message_complete`, `message_done`, `received_packets`, `node_discovered`, `node_lost`, `connected`, `disconnected`.
365
+
366
+ ### useMockBluetooth()
367
+
368
+ Offline simulator that replaces the real BLE layer with an in-memory mock. Enables development without physical standees and makes integration testing possible.
369
+
370
+ ```typescript
371
+ const mock = useMockBluetooth();
372
+
373
+ // Activate the mock before connecting
374
+ mock.activate({
375
+ standees: [
376
+ { id: 1, neighbors: [2, 3] },
377
+ { id: 2, neighbors: [1] },
378
+ { id: 3, neighbors: [1] },
379
+ ],
380
+ responseDelay: 10, // ms before MESSAGE_DONE response (default: 10)
381
+ dropRate: 0, // 0-1 packet drop probability (default: 0)
382
+ partialDelivery: false, // simulate DATA_FEEDBACK for incomplete messages (default: false)
383
+ });
384
+
385
+ // Now connect normally — all BLE calls go through the mock
386
+ await useMesh().connect();
387
+ // onNodes fires with the configured standee topology
388
+
389
+ // Simulate button presses from virtual standees
390
+ mock.pressButton(1, "plus"); // triggers onPlusButton
391
+ mock.pressButton(2, "minus", "data"); // triggers onMinButton with text data
392
+ mock.pressButton(3, "either"); // triggers onEitherButton
393
+
394
+ // Simulate mesh topology changes
395
+ mock.simulateNodeJoin({ id: 4, neighbors: [1] }); // triggers onNode
396
+ mock.simulateNodeLost(3); // triggers onNodeLost
397
+
398
+ // Simulate disconnection
399
+ mock.disconnect(); // triggers onDisconnect
400
+
401
+ // Control reconnect behavior
402
+ mock.reconnectable(false); // makes reconnect() reject
403
+ mock.reconnectable(true); // restore (default)
404
+
405
+ // Update config at runtime
406
+ mock.updateConfig({ responseDelay: 50, dropRate: 0.2 });
407
+
408
+ // Check state
409
+ mock.isActive(); // true
410
+ mock.getConfig(); // current config
411
+
412
+ // Restore real BLE
413
+ mock.deactivate();
414
+ ```
415
+
416
+ When `writeCharacteristic` is called (via `Buffer.send()`), the mock tracks incoming packets and automatically fires `MESSAGE_DONE` through the flood characteristic once all packets for a message are received. With `partialDelivery: true`, it sends `DATA_FEEDBACK` after a batch of writes, triggering the retransmission flow.
417
+
418
+ ## Display Elements
419
+
420
+ All elements extend `DisplayElement` and share common properties:
421
+
422
+ | Property | Type | Description |
423
+ | ------------ | ------- | ----------------------------------------------------- |
424
+ | `id` | number | Auto-assigned by `useStandeeElements().add()` |
425
+ | `x` | number | X position |
426
+ | `y` | number | Y position |
427
+ | `show` | boolean | Visibility (default: `true`) |
428
+ | `colorBlack` | boolean | Draw in black when `true`, white when `false` |
429
+ | `dirty` | boolean | Automatically set to `true` when any property changes |
430
+
431
+ Setting any property automatically marks the element as `dirty`, so it will be included in the next `draw()` call.
432
+
433
+ ### Rectangle
434
+
435
+ ```typescript
436
+ new Rectangle(x, y, width, height, radius?, fill?, show?, colorBlack?)
437
+ ```
438
+
439
+ | Property | Type | Default | Description |
440
+ | -------- | ------- | ------- | ------------------------------- |
441
+ | `width` | number | | Rectangle width |
442
+ | `height` | number | | Rectangle height |
443
+ | `radius` | number | `0` | Corner radius for rounded edges |
444
+ | `fill` | boolean | `false` | Fill the rectangle when `true` |
445
+
446
+ ### Text
447
+
448
+ ```typescript
449
+ new Text(x, y, text, font, center?, show?, colorBlack?)
450
+ ```
451
+
452
+ | Property | Type | Default | Description |
453
+ | -------- | ------- | ------- | ---------------------------------------- |
454
+ | `text` | string | | The text content |
455
+ | `font` | number | | Font from the `Font` enum |
456
+ | `center` | boolean | `false` | Center the text at the given coordinates |
457
+
458
+ Available fonts (from `Font` enum):
459
+
460
+ - `u8g2_font_5x7_mf` — small
461
+ - `u8g2_font_crox2c_tr` — medium
462
+ - `u8g2_font_10x20_tn` — numeric
463
+ - `u8g2_font_inr33_t_cyrillic` — large
464
+
465
+ ### Image
466
+
467
+ ```typescript
468
+ new Image(x, y, width, height, data, show?, colorBlack?)
469
+ ```
470
+
471
+ | Property | Type | Description |
472
+ | -------- | ------ | ------------------------------------------------- |
473
+ | `width` | number | Image width |
474
+ | `height` | number | Image height |
475
+ | `data` | number | Image ID (0-255) matching a previously uploaded graphic |
476
+
477
+ ## Objects
478
+
479
+ ### Standee
480
+
481
+ Represents a discovered standee in the mesh network.
482
+
483
+ | Property | Type | Description |
484
+ | ------------ | ----------------- | ---------------------------------------- |
485
+ | `id` | number | Unique standee identifier |
486
+ | `neighbors` | number[] | IDs of neighboring standees in the mesh |
487
+ | `elements` | DisplayElement[] | Display elements assigned to this standee|
488
+ | `onPlus` | SyncEvent\<void\> | Fires when the plus button is pressed |
489
+ | `onMin` | SyncEvent\<void\> | Fires when the minus button is pressed |
490
+
491
+ | Method | Returns | Description |
492
+ | -------- | ------- | -------------------------------------------- |
493
+ | `name()` | string | Hex-formatted ID (e.g. `"0a"` for id `10`) |
494
+
495
+ ### ButtonResult
496
+
497
+ Returned by button press events (`onPlusButton`, `onMinButton`, `onEitherButton`).
498
+
499
+ | Property | Type | Description |
500
+ | -------- | ------ | ------------------------------- |
501
+ | `id` | number | ID of the standee that was pressed |
502
+ | `data` | string | Optional data sent with the press |
503
+
504
+ ### Buffer
505
+
506
+ Wraps BLE message data for transmission. Created internally by composable methods.
507
+
508
+ | Property | Type | Description |
509
+ | --------- | ------------ | --------------------------------------- |
510
+ | `cache` | number | Auto-generated cache ID for tracking |
511
+ | `type` | DataType | The message type |
512
+ | `id` | number | Target standee ID |
513
+ | `packets` | Uint8Array[] | The message split into BLE-sized packets|
514
+
515
+ | Method | Returns | Description |
516
+ | ------------------------- | -------------- | ------------------------------------ |
517
+ | `send()` | Promise\<void\>| Transmit the buffer over BLE |
518
+ | `then(callback)` | Buffer | Chain a callback for after send completes |
519
+
520
+ ## Enums
521
+
522
+ ### Core Enums
523
+
524
+ #### Font
525
+
526
+ | Value | ID | Description |
527
+ | ----------------------------- | -- | ----------- |
528
+ | `u8g2_font_5x7_mf` | 0 | Small |
529
+ | `u8g2_font_inr33_t_cyrillic` | 1 | Large |
530
+ | `u8g2_font_10x20_tn` | 2 | Numeric |
531
+ | `u8g2_font_crox2c_tr` | 3 | Medium |
532
+
533
+ #### Button
534
+
535
+ | Value | ID | Description |
536
+ | ------- | -- | ------------- |
537
+ | `MINUS` | 0 | Minus button |
538
+ | `PLUS` | 1 | Plus button |
539
+ | `EITHER`| 2 | Either button |
540
+
541
+ #### ActionType
542
+
543
+ | Value | ID | Description |
544
+ | ----------- | -- | -------------------- |
545
+ | `INCREMENT` | 0 | Increment a value |
546
+ | `DECREMENT` | 1 | Decrement a value |
547
+ | `SHOW` | 2 | Show an element |
548
+ | `HIDE` | 3 | Hide an element |
549
+ | `SET` | 4 | Set a value |
550
+ | `BROADCAST` | 5 | Broadcast to mesh |
551
+ | `TIMER` | 6 | Delayed action |
552
+ | `CONDITION` | 7 | Conditional action |
553
+ | `BREAK` | 8 | Break action chain |
554
+ | `MAP` | 9 | Map value to range |
555
+
556
+ #### ElementData
557
+
558
+ Target properties for actions.
559
+
560
+ | Value | ID | Description |
561
+ | -------- | -- | ---------------- |
562
+ | `TYPE` | 0 | Element type |
563
+ | `ID` | 1 | Element ID |
564
+ | `SHOW` | 2 | Visibility |
565
+ | `X` | 3 | X position |
566
+ | `Y` | 4 | Y position |
567
+ | `WIDTH` | 5 | Width |
568
+ | `HEIGHT` | 6 | Height |
569
+ | `RADIUS` | 7 | Corner radius |
570
+ | `FILL` | 8 | Fill mode |
571
+ | `DATA` | 9 | Data/image ID |
572
+ | `FONT` | 10 | Font |
573
+
574
+ #### ActionCondition
575
+
576
+ Comparison operators for `condition()` actions.
577
+
578
+ | Value | ID | Description |
579
+ | --------------- | -- | ---------------- |
580
+ | `EQUAL` | 0 | Equal to |
581
+ | `GREATER` | 1 | Greater than |
582
+ | `LESS` | 2 | Less than |
583
+ | `GREATER_EQUAL` | 3 | Greater or equal |
584
+ | `LESS_EQUAL` | 4 | Less or equal |
585
+
586
+ #### ActionDataType
587
+
588
+ Value types used in `set()` and `condition()` actions.
589
+
590
+ | Value | ID | Description |
591
+ | --------- | -- | --------------------- |
592
+ | `STRING` | 0 | String value |
593
+ | `NUMBER` | 1 | Numeric value |
594
+ | `ELEMENT` | 2 | Element property ref |
595
+
596
+ ### Protocol Enums
597
+
598
+ These enums are used internally by the BLE protocol layer but are exported for advanced use cases and debugging.
599
+
600
+ #### DataType
601
+
602
+ Message types sent over BLE.
603
+
604
+ | Value | ID | Description |
605
+ | ---------------- | -- | ------------------------ |
606
+ | `UPLOAD` | 0 | Image upload |
607
+ | `DRAW_IMAGE` | 1 | Draw an image element |
608
+ | `DRAW_RECTANGLE` | 2 | Draw a rectangle element |
609
+ | `BUTTON_PRESS` | 3 | Button press event |
610
+ | `DRAW_TEXT` | 4 | Draw a text element |
611
+ | `ALL` | 5 | Draw all elements |
612
+ | `CLEAR_GRAPHICS` | 6 | Clear uploaded graphics |
613
+ | `ACTIONS` | 7 | Button action assignment |
614
+ | `CLEAR_SCREEN` | 8 | Clear the screen |
615
+ | `LOADER` | 10 | Loader header |
616
+
617
+ #### FloodTypes
618
+
619
+ Flood message types for mesh-level communication.
620
+
621
+ | Value | ID | Description |
622
+ | ---------------- | -- | ---------------------------------- |
623
+ | `NODES` | 0 | Node discovery |
624
+ | `LOST_NODE` | 1 | Node left the mesh |
625
+ | `MESSAGE_DONE` | 2 | Message fully received by target |
626
+ | `DATA_FEEDBACK` | 3 | Partial delivery feedback |
627
+
628
+ #### HandshakeType
629
+
630
+ Handshake protocol types for initial connection.
631
+
632
+ | Value | ID | Description |
633
+ | ------------ | -- | --------------------- |
634
+ | `FLOOD_NODES`| 2 | Flood node discovery |
635
+ | `STEP_ONE` | 4 | Handshake step one |
636
+ | `STEP_TWO` | 5 | Handshake step two |
637
+
638
+ #### Data
639
+
640
+ Byte positions within a BLE packet. Useful for manual packet inspection with `useDebug().decodePacket()`.
641
+
642
+ | Value | Byte Position | Description |
643
+ | ----------- | ------------- | --------------------------------- |
644
+ | `ID` | 0 | Target standee ID |
645
+ | `CACHE` | 1 | Cache ID for message tracking |
646
+ | `TTL` | 2 | Time to live (mesh hops) |
647
+ | `POSITION` | 3-4 | Packet position (uint16 LE) |
648
+ | `DATA_SIZE` | 5-6 | Total data size (uint16 LE) |
649
+ | `TYPE` | 7 | Message type (DataType) |
650
+ | `DATA` | 8+ | Payload data |
651
+
652
+ ## TypeScript Types
653
+
654
+ The library exports the following type definitions for use in your application:
655
+
656
+ ### Connection & Reconnect
657
+
658
+ ```typescript
659
+ interface ReconnectStatus {
660
+ attempt: number; // Current attempt number
661
+ maxRetries: number; // Maximum retries allowed
662
+ delay: number; // Delay before this attempt in ms
663
+ }
664
+ ```
665
+
666
+ ### Debug Types
667
+
668
+ ```typescript
669
+ interface DebugLogEntry {
670
+ type: DebugEventType;
671
+ timestamp: number;
672
+ data: Record<string, unknown>;
673
+ }
674
+
675
+ type DebugEventType =
676
+ | "packet_sent" | "message_queued" | "retransmit" | "timeout"
677
+ | "message_complete" | "message_done" | "received_packets"
678
+ | "node_discovered" | "node_lost" | "connected" | "disconnected";
679
+
680
+ interface DecodedPacket {
681
+ cacheId: number;
682
+ targetId: number;
683
+ ttl: number;
684
+ position: number;
685
+ dataSize: number;
686
+ typeName: string;
687
+ typeValue: number;
688
+ payloadBytes: number;
689
+ raw: number[];
690
+ }
691
+
692
+ interface MessageTimingRecord {
693
+ cacheId: number;
694
+ queuedAt: number;
695
+ firstPacketAt: number | null;
696
+ completedAt: number | null;
697
+ latencyMs: number | null;
698
+ retransmitCount: number;
699
+ timeoutCount: number;
700
+ packetsSent: number;
701
+ totalPackets: number;
702
+ }
703
+ ```
704
+
705
+ ### MeshWriter Events
706
+
707
+ ```typescript
708
+ interface PacketSentEvent {
709
+ cacheId: number;
710
+ position: number;
711
+ dataSize: number;
712
+ packetBytes: number;
713
+ batchIndex: number;
714
+ timestamp: number;
715
+ }
716
+
717
+ interface MessageQueuedEvent {
718
+ cacheId: number;
719
+ type: number;
720
+ targetId: number;
721
+ packetCount: number;
722
+ totalBytes: number;
723
+ timestamp: number;
724
+ }
725
+
726
+ interface RetransmitEvent {
727
+ cacheId: number;
728
+ packetCount: number;
729
+ reason: "feedback" | "timeout";
730
+ timestamp: number;
731
+ }
732
+
733
+ interface TimeoutEvent {
734
+ cacheId: number;
735
+ retryCount: number;
736
+ maxRetries: number;
737
+ willRetry: boolean;
738
+ timestamp: number;
739
+ }
740
+
741
+ interface MessageCompleteEvent {
742
+ cacheId: number;
743
+ timestamp: number;
744
+ }
745
+ ```
746
+
747
+ ### Loader Types
748
+
749
+ ```typescript
750
+ interface LoaderProgress {
751
+ current: number; // Buffers completed so far
752
+ total: number; // Total buffers
753
+ percentage: number; // 0 to 1
754
+ }
755
+
756
+ interface LoaderError {
757
+ buffer: number; // Index of the buffer that failed
758
+ error: Error; // The Error object
759
+ }
760
+ ```
761
+
762
+ ### Mock Types
763
+
764
+ ```typescript
765
+ interface VirtualStandee {
766
+ id: number;
767
+ neighbors: number[];
768
+ }
769
+
770
+ interface MockMeshConfig {
771
+ standees: VirtualStandee[];
772
+ responseDelay?: number; // Default: 10
773
+ dropRate?: number; // Default: 0
774
+ partialDelivery?: boolean; // Default: false
775
+ }
776
+ ```
777
+
778
+ ### Image Types
779
+
780
+ ```typescript
781
+ type ImagePixels = {
782
+ width: number;
783
+ height: number;
784
+ pixels: number[]; // 0 (black) or 1 (white)
785
+ }
786
+ ```
787
+
788
+ ## Configuration
789
+
790
+ The BLE protocol uses these internal constants (via `useBluetoothSettings()`):
791
+
792
+ | Constant | Value | Description |
793
+ | ----------------------- | ------ | ------------------------------------------------- |
794
+ | `PACKET_SIZE` | 13 | Max payload bytes per BLE packet |
795
+ | `BATCH_SIZE` | 20 | Packets sent per batch before waiting for ack |
796
+ | `ACK_TIMEOUT` | 5000 | Milliseconds to wait for acknowledgment |
797
+ | `MAX_RETRIES` | 3 | Retransmission attempts before giving up |
798
+ | `TTL` | 10 | Time to live (max mesh hops) |
799
+ | `RECONNECT_DELAY` | 2000 | Base delay for auto-reconnect (doubles each try) |
800
+ | `RECONNECT_MAX_RETRIES` | 5 | Maximum auto-reconnect attempts |
801
+
802
+ ## Events
803
+
804
+ The library uses [`ts-events`](https://github.com/rogierschouten/ts-events) for event handling:
805
+
806
+ ```typescript
807
+ // Subscribe
808
+ mesh.onConnect.attach(() => console.log("Connected"));
809
+
810
+ // Unsubscribe
811
+ const handler = () => console.log("Connected");
812
+ mesh.onConnect.attach(handler);
813
+ mesh.onConnect.detach(handler);
814
+ ```
815
+
816
+ ## Full Example
817
+
818
+ A complete example that connects to the mesh, uploads images, creates a UI, and configures button interaction:
819
+
820
+ ```typescript
821
+ import {
822
+ useMesh,
823
+ useStandees,
824
+ useStandeeElements,
825
+ useDisplay,
826
+ useGraphics,
827
+ useLoader,
828
+ useConverter,
829
+ useActions,
830
+ useButtons,
831
+ } from "nsd-ble";
832
+ import { Rectangle, Text, Image } from "nsd-ble/elements";
833
+ import { Font, Button, ElementData } from "nsd-ble/enums";
834
+
835
+ const mesh = useMesh();
836
+ const { standees, onChange } = useStandees();
837
+ const { draw, clear } = useDisplay();
838
+
839
+ // Connect on button click
840
+ document.getElementById("connect").addEventListener("click", () => {
841
+ mesh.connect();
842
+ });
843
+
844
+ onChange.attach(async (list) => {
845
+ const standee = list[0];
846
+
847
+ // 1. Upload images
848
+ const images = ["/img/icon-a.png", "/img/icon-b.png"];
849
+ const buffers = await Promise.all(
850
+ images.map(async (path, i) => {
851
+ const bytes = await useConverter().imageToBytes(path);
852
+ return useGraphics(standee).upload(i, bytes);
853
+ }),
854
+ );
855
+
856
+ const loader = useLoader(standee).load(buffers);
857
+ loader.onProgress.attach((p) => {
858
+ console.log(`Uploading: ${Math.round(p.percentage * 100)}%`);
859
+ });
860
+ await loader.send();
861
+
862
+ // 2. Build the display
863
+ const { add } = useStandeeElements(standee);
864
+ const bg = add(new Rectangle(0, 0, 128, 64, 0, true));
865
+ const icon = add(new Image(10, 16, 32, 32, 0));
866
+ const label = add(new Text(64, 40, "HP: 10", Font.u8g2_font_5x7_mf, true));
867
+
868
+ await draw(standee).send();
869
+
870
+ // 3. Wire up buttons
871
+ const actions = useActions();
872
+ const hpTarget = actions.target(label.id, ElementData.TEXT);
873
+
874
+ await useButtons(standee)
875
+ .setButton(Button.PLUS, [actions.increment(hpTarget)])
876
+ .send();
877
+
878
+ await useButtons(standee)
879
+ .setButton(Button.MINUS, [actions.decrement(hpTarget)])
880
+ .send();
881
+ });
882
+
883
+ // Listen for button presses from the app side
884
+ mesh.onPlusButton.attach((result) => {
885
+ console.log(`Plus pressed on standee ${result.id}`);
886
+ });
887
+ ```