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 +887 -0
- package/dist/Objects-Zntmn07s.cjs +1 -0
- package/dist/Objects-ulh5Kfet.js +1043 -0
- package/dist/main.cjs +1 -1
- package/dist/main.js +641 -287
- package/dist/objects.cjs +1 -1
- package/dist/objects.js +1 -1
- package/dist/types/Types.d.ts +99 -0
- package/dist/types/ble/BluetoothProvider.d.ts +9 -0
- package/dist/types/ble/Mesh.d.ts +14 -9
- package/dist/types/ble/MeshWriter.d.ts +8 -0
- package/dist/types/ble/MockBluetooth.d.ts +25 -0
- package/dist/types/ble/Settings.d.ts +4 -0
- package/dist/types/composable/Debug.d.ts +26 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/standee/Loader.d.ts +5 -0
- package/package.json +1 -1
- package/dist/Objects-C2BH61j-.js +0 -941
- package/dist/Objects-WdcC281c.cjs +0 -1
- package/readme.md +0 -30
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
|
+
```
|