@opendisplay/opendisplay 1.0.0 → 1.0.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 +455 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# @opendisplay/opendisplay
|
|
2
|
+
|
|
3
|
+
TypeScript library for OpenDisplay BLE e-paper displays using Web Bluetooth API. Control your OpenDisplay devices directly from the browser.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@opendisplay/opendisplay)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Web Bluetooth Integration**: Control OpenDisplay devices directly from the browser
|
|
11
|
+
- **Complete Protocol Support**: Image upload, device configuration, firmware management
|
|
12
|
+
- **Automatic Image Processing**: Built-in dithering, encoding, and compression
|
|
13
|
+
- **Type-Safe**: Full TypeScript support with exported types
|
|
14
|
+
- **Device Discovery**: Browser-native device picker
|
|
15
|
+
- **Protocol Compliance**: Byte-for-byte compatible with [py-opendisplay](https://github.com/OpenDisplay-org/py-opendisplay)
|
|
16
|
+
|
|
17
|
+
## Browser Compatibility
|
|
18
|
+
|
|
19
|
+
Web Bluetooth API is required. Supported browsers:
|
|
20
|
+
|
|
21
|
+
- ✅ Chrome/Edge 56+ (Desktop & Android)
|
|
22
|
+
- ✅ Opera 43+ (Desktop & Android)
|
|
23
|
+
- ✅ Samsung Internet 6.0+
|
|
24
|
+
- ❌ Firefox (no Web Bluetooth support)
|
|
25
|
+
- ❌ Safari (no Web Bluetooth support)
|
|
26
|
+
|
|
27
|
+
**Note**: HTTPS or localhost is required for Web Bluetooth API.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
### NPM/Bun/Yarn
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @opendisplay/opendisplay
|
|
35
|
+
# or
|
|
36
|
+
bun add @opendisplay/opendisplay
|
|
37
|
+
# or
|
|
38
|
+
yarn add @opendisplay/opendisplay
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### CDN (for quick prototyping)
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<script type="module">
|
|
45
|
+
import { OpenDisplayDevice } from 'https://esm.sh/@opendisplay/opendisplay@1.0.0';
|
|
46
|
+
// Your code here
|
|
47
|
+
</script>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { OpenDisplayDevice, DitherMode, RefreshMode } from '@opendisplay/opendisplay';
|
|
54
|
+
|
|
55
|
+
// Create device instance
|
|
56
|
+
const device = new OpenDisplayDevice();
|
|
57
|
+
|
|
58
|
+
// Connect to device (shows browser picker)
|
|
59
|
+
await device.connect();
|
|
60
|
+
|
|
61
|
+
// Device is auto-interrogated on first connect
|
|
62
|
+
console.log(`Connected to ${device.width}x${device.height} display`);
|
|
63
|
+
|
|
64
|
+
// Load image from canvas
|
|
65
|
+
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
|
|
66
|
+
const ctx = canvas.getContext('2d')!;
|
|
67
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
68
|
+
|
|
69
|
+
// Upload image
|
|
70
|
+
await device.uploadImage(imageData, {
|
|
71
|
+
refreshMode: RefreshMode.FULL,
|
|
72
|
+
ditherMode: DitherMode.FLOYD_STEINBERG,
|
|
73
|
+
compress: true
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Disconnect when done
|
|
77
|
+
await device.disconnect();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Documentation
|
|
81
|
+
|
|
82
|
+
### OpenDisplayDevice
|
|
83
|
+
|
|
84
|
+
Main class for interacting with OpenDisplay devices.
|
|
85
|
+
|
|
86
|
+
#### Constructor
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
new OpenDisplayDevice(options?: {
|
|
90
|
+
config?: GlobalConfig;
|
|
91
|
+
capabilities?: DeviceCapabilities;
|
|
92
|
+
device?: BluetoothDevice;
|
|
93
|
+
namePrefix?: string;
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Options:**
|
|
98
|
+
- `config`: Cached device configuration to skip interrogation
|
|
99
|
+
- `capabilities`: Minimal device info (width, height, color scheme) to skip interrogation
|
|
100
|
+
- `device`: Pre-selected BluetoothDevice instance
|
|
101
|
+
- `namePrefix`: Device name filter for picker (e.g., "OpenDisplay")
|
|
102
|
+
|
|
103
|
+
#### Methods
|
|
104
|
+
|
|
105
|
+
##### `connect(options?: BLEConnectionOptions): Promise<void>`
|
|
106
|
+
|
|
107
|
+
Connect to an OpenDisplay device. Shows browser's Bluetooth device picker if no device was provided in constructor.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
await device.connect();
|
|
111
|
+
// or with name filter
|
|
112
|
+
await device.connect({ namePrefix: 'OpenDisplay' });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Automatically interrogates device on first connect unless config/capabilities were provided.
|
|
116
|
+
|
|
117
|
+
##### `disconnect(): Promise<void>`
|
|
118
|
+
|
|
119
|
+
Disconnect from the device.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
await device.disconnect();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
##### `uploadImage(imageData: ImageData, options?): Promise<void>`
|
|
126
|
+
|
|
127
|
+
Upload image to device display. Handles resizing, dithering, encoding, and compression automatically.
|
|
128
|
+
|
|
129
|
+
**Parameters:**
|
|
130
|
+
- `imageData`: Image as ImageData from canvas
|
|
131
|
+
- `options.refreshMode`: Display refresh mode (default: `RefreshMode.FULL`)
|
|
132
|
+
- `options.ditherMode`: Dithering algorithm (default: `DitherMode.BURKES`)
|
|
133
|
+
- `options.compress`: Enable zlib compression (default: `true`)
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// From canvas
|
|
137
|
+
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
|
|
138
|
+
const ctx = canvas.getContext('2d')!;
|
|
139
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
140
|
+
|
|
141
|
+
await device.uploadImage(imageData, {
|
|
142
|
+
refreshMode: RefreshMode.FULL,
|
|
143
|
+
ditherMode: DitherMode.FLOYD_STEINBERG,
|
|
144
|
+
compress: true
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Supported Refresh Modes:**
|
|
149
|
+
- `RefreshMode.FULL` - Full refresh (recommended, ~15s)
|
|
150
|
+
- `RefreshMode.FAST` - Fast refresh if supported (~2s, may have ghosting)
|
|
151
|
+
- `RefreshMode.PARTIAL` - Partial refresh if supported
|
|
152
|
+
|
|
153
|
+
**Supported Dither Modes** (from [@opendisplay/epaper-dithering](https://www.npmjs.com/package/@opendisplay/epaper-dithering)):
|
|
154
|
+
- `DitherMode.FLOYD_STEINBERG` - Classic error diffusion (recommended)
|
|
155
|
+
- `DitherMode.BURKES` - Burkes error diffusion
|
|
156
|
+
- `DitherMode.SIERRA` - Sierra error diffusion
|
|
157
|
+
- `DitherMode.ATKINSON` - Atkinson dithering (HyperCard style)
|
|
158
|
+
- `DitherMode.STUCKI` - Stucki error diffusion
|
|
159
|
+
- `DitherMode.JARVIS` - Jarvis-Judice-Ninke
|
|
160
|
+
- `DitherMode.SIMPLE_2D` - Fast 2D error diffusion
|
|
161
|
+
- `DitherMode.ORDERED_BAYER_2` - 2x2 Bayer ordered dithering
|
|
162
|
+
- `DitherMode.ORDERED_BAYER_4` - 4x4 Bayer ordered dithering
|
|
163
|
+
|
|
164
|
+
##### `interrogate(): Promise<GlobalConfig>`
|
|
165
|
+
|
|
166
|
+
Read complete device configuration from device. Automatically called on first connect unless config/capabilities were provided.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const config = await device.interrogate();
|
|
170
|
+
console.log(`Device has ${config.displays.length} display(s)`);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
##### `readFirmwareVersion(): Promise<FirmwareVersion>`
|
|
174
|
+
|
|
175
|
+
Read firmware version from device.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const fw = await device.readFirmwareVersion();
|
|
179
|
+
console.log(`Firmware v${fw.major}.${fw.minor} (${fw.sha})`);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
##### `writeConfig(config: GlobalConfig): Promise<void>`
|
|
183
|
+
|
|
184
|
+
Write configuration to device. Device must be rebooted for changes to take effect.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// Read current config
|
|
188
|
+
const config = device.config!;
|
|
189
|
+
|
|
190
|
+
// Modify config
|
|
191
|
+
config.displays[0].rotation = 180;
|
|
192
|
+
|
|
193
|
+
// Write back to device
|
|
194
|
+
await device.writeConfig(config);
|
|
195
|
+
|
|
196
|
+
// Reboot to apply changes
|
|
197
|
+
await device.reboot();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
##### `reboot(): Promise<void>`
|
|
201
|
+
|
|
202
|
+
Reboot the device. Connection will drop as device resets.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
await device.reboot();
|
|
206
|
+
// Device will disconnect automatically
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### Properties
|
|
210
|
+
|
|
211
|
+
##### `isConnected: boolean`
|
|
212
|
+
|
|
213
|
+
Check if currently connected to a device.
|
|
214
|
+
|
|
215
|
+
##### `width: number`
|
|
216
|
+
|
|
217
|
+
Display width in pixels (throws if not interrogated).
|
|
218
|
+
|
|
219
|
+
##### `height: number`
|
|
220
|
+
|
|
221
|
+
Display height in pixels (throws if not interrogated).
|
|
222
|
+
|
|
223
|
+
##### `colorScheme: ColorScheme`
|
|
224
|
+
|
|
225
|
+
Display color scheme (throws if not interrogated).
|
|
226
|
+
|
|
227
|
+
Possible values:
|
|
228
|
+
- `ColorScheme.MONO` - Black and white
|
|
229
|
+
- `ColorScheme.BWR` - Black, white, red
|
|
230
|
+
- `ColorScheme.BWY` - Black, white, yellow
|
|
231
|
+
- `ColorScheme.BWRY` - Black, white, red, yellow (4-color)
|
|
232
|
+
- `ColorScheme.BWGBRY` - Black, white, green, blue, red, yellow (6-color Spectra)
|
|
233
|
+
- `ColorScheme.GRAYSCALE_4` - 4-level grayscale
|
|
234
|
+
|
|
235
|
+
##### `rotation: number`
|
|
236
|
+
|
|
237
|
+
Display rotation in degrees (0, 90, 180, 270).
|
|
238
|
+
|
|
239
|
+
##### `config: GlobalConfig | null`
|
|
240
|
+
|
|
241
|
+
Full device configuration (null if not interrogated).
|
|
242
|
+
|
|
243
|
+
##### `capabilities: DeviceCapabilities | null`
|
|
244
|
+
|
|
245
|
+
Minimal device info (width, height, colorScheme, rotation).
|
|
246
|
+
|
|
247
|
+
### Discovery
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { discoverDevices } from '@opendisplay/opendisplay';
|
|
251
|
+
|
|
252
|
+
// Show device picker
|
|
253
|
+
const device = await discoverDevices();
|
|
254
|
+
// or with name filter
|
|
255
|
+
const device = await discoverDevices('OpenDisplay');
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Types
|
|
259
|
+
|
|
260
|
+
All types are exported for TypeScript users:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import type {
|
|
264
|
+
GlobalConfig,
|
|
265
|
+
DisplayConfig,
|
|
266
|
+
DeviceCapabilities,
|
|
267
|
+
FirmwareVersion,
|
|
268
|
+
AdvertisementData
|
|
269
|
+
} from '@opendisplay/opendisplay';
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Usage Examples
|
|
273
|
+
|
|
274
|
+
### Basic Image Upload
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { OpenDisplayDevice } from '@opendisplay/opendisplay';
|
|
278
|
+
|
|
279
|
+
const device = new OpenDisplayDevice();
|
|
280
|
+
await device.connect();
|
|
281
|
+
|
|
282
|
+
// Create canvas with image
|
|
283
|
+
const canvas = document.createElement('canvas');
|
|
284
|
+
canvas.width = device.width;
|
|
285
|
+
canvas.height = device.height;
|
|
286
|
+
const ctx = canvas.getContext('2d')!;
|
|
287
|
+
|
|
288
|
+
// Draw something
|
|
289
|
+
ctx.fillStyle = 'white';
|
|
290
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
291
|
+
ctx.fillStyle = 'black';
|
|
292
|
+
ctx.font = '48px Arial';
|
|
293
|
+
ctx.fillText('Hello OpenDisplay!', 50, 100);
|
|
294
|
+
|
|
295
|
+
// Upload to device
|
|
296
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
297
|
+
await device.uploadImage(imageData);
|
|
298
|
+
|
|
299
|
+
await device.disconnect();
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Upload Image from File
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// HTML: <input type="file" id="imageInput" accept="image/*">
|
|
306
|
+
|
|
307
|
+
const input = document.getElementById('imageInput') as HTMLInputElement;
|
|
308
|
+
input.addEventListener('change', async (e) => {
|
|
309
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
310
|
+
if (!file) return;
|
|
311
|
+
|
|
312
|
+
// Load image
|
|
313
|
+
const img = new Image();
|
|
314
|
+
img.src = URL.createObjectURL(file);
|
|
315
|
+
await img.decode();
|
|
316
|
+
|
|
317
|
+
// Convert to ImageData
|
|
318
|
+
const canvas = document.createElement('canvas');
|
|
319
|
+
canvas.width = img.width;
|
|
320
|
+
canvas.height = img.height;
|
|
321
|
+
const ctx = canvas.getContext('2d')!;
|
|
322
|
+
ctx.drawImage(img, 0, 0);
|
|
323
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
324
|
+
|
|
325
|
+
// Upload
|
|
326
|
+
await device.uploadImage(imageData);
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Skip Interrogation with Cached Config
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { OpenDisplayDevice } from '@opendisplay/opendisplay';
|
|
334
|
+
|
|
335
|
+
// First connection - interrogate and cache
|
|
336
|
+
const device = new OpenDisplayDevice();
|
|
337
|
+
await device.connect();
|
|
338
|
+
const cachedConfig = device.config!;
|
|
339
|
+
|
|
340
|
+
// Store config in localStorage
|
|
341
|
+
localStorage.setItem('deviceConfig', JSON.stringify(cachedConfig));
|
|
342
|
+
|
|
343
|
+
// Later - reuse cached config
|
|
344
|
+
const savedConfig = JSON.parse(localStorage.getItem('deviceConfig')!);
|
|
345
|
+
const fastDevice = new OpenDisplayDevice({ config: savedConfig });
|
|
346
|
+
await fastDevice.connect();
|
|
347
|
+
// No interrogation needed!
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Skip Interrogation with Minimal Capabilities
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { OpenDisplayDevice, ColorScheme } from '@opendisplay/opendisplay';
|
|
354
|
+
|
|
355
|
+
// If you know your device specs
|
|
356
|
+
const device = new OpenDisplayDevice({
|
|
357
|
+
capabilities: {
|
|
358
|
+
width: 296,
|
|
359
|
+
height: 128,
|
|
360
|
+
colorScheme: ColorScheme.BWR,
|
|
361
|
+
rotation: 0
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
await device.connect();
|
|
366
|
+
// Fast connection - no interrogation!
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Read and Modify Device Configuration
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
const device = new OpenDisplayDevice();
|
|
373
|
+
await device.connect();
|
|
374
|
+
|
|
375
|
+
// Read config
|
|
376
|
+
const config = await device.interrogate();
|
|
377
|
+
console.log(`Display: ${config.displays[0].pixelWidth}x${config.displays[0].pixelHeight}`);
|
|
378
|
+
console.log(`Battery: ${config.power?.batteryCapacityMah}mAh`);
|
|
379
|
+
|
|
380
|
+
// Modify rotation
|
|
381
|
+
config.displays[0].rotation = 180;
|
|
382
|
+
|
|
383
|
+
// Write back
|
|
384
|
+
await device.writeConfig(config);
|
|
385
|
+
await device.reboot(); // Reboot to apply
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Error Handling
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
import {
|
|
392
|
+
OpenDisplayDevice,
|
|
393
|
+
BLEConnectionError,
|
|
394
|
+
BLETimeoutError,
|
|
395
|
+
ProtocolError
|
|
396
|
+
} from '@opendisplay/opendisplay';
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const device = new OpenDisplayDevice();
|
|
400
|
+
await device.connect();
|
|
401
|
+
await device.uploadImage(imageData);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error instanceof BLEConnectionError) {
|
|
404
|
+
console.error('Failed to connect:', error.message);
|
|
405
|
+
} else if (error instanceof BLETimeoutError) {
|
|
406
|
+
console.error('Operation timed out:', error.message);
|
|
407
|
+
} else if (error instanceof ProtocolError) {
|
|
408
|
+
console.error('Protocol error:', error.message);
|
|
409
|
+
} else {
|
|
410
|
+
console.error('Unexpected error:', error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Architecture
|
|
416
|
+
|
|
417
|
+
This library mirrors the architecture of [py-opendisplay](https://github.com/OpenDisplay-org/py-opendisplay):
|
|
418
|
+
|
|
419
|
+
- **Protocol Layer**: Command builders, response parsers, TLV config handling
|
|
420
|
+
- **Transport Layer**: Web Bluetooth wrapper with notification queue
|
|
421
|
+
- **Encoding Layer**: Image encoding, compression, bitplane handling
|
|
422
|
+
- **Models Layer**: TypeScript interfaces for all data structures
|
|
423
|
+
- **Public API**: `OpenDisplayDevice` class and helper functions
|
|
424
|
+
|
|
425
|
+
## Development
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
# Install dependencies
|
|
429
|
+
npm install
|
|
430
|
+
|
|
431
|
+
# Build library
|
|
432
|
+
npm run build
|
|
433
|
+
|
|
434
|
+
# Type check
|
|
435
|
+
npm run type-check
|
|
436
|
+
|
|
437
|
+
# Lint
|
|
438
|
+
npm run lint
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Related Packages
|
|
442
|
+
|
|
443
|
+
- [@opendisplay/epaper-dithering](https://www.npmjs.com/package/@opendisplay/epaper-dithering) - Dithering algorithms for e-paper displays
|
|
444
|
+
- [py-opendisplay](https://github.com/OpenDisplay-org/py-opendisplay) - Python version for desktop/server applications
|
|
445
|
+
|
|
446
|
+
## License
|
|
447
|
+
|
|
448
|
+
MIT
|
|
449
|
+
|
|
450
|
+
## Links
|
|
451
|
+
|
|
452
|
+
- [npm Package](https://www.npmjs.com/package/@opendisplay/opendisplay)
|
|
453
|
+
- [GitHub Repository](https://github.com/OpenDisplay-org/opendisplay-js)
|
|
454
|
+
- [OpenDisplay Website](https://opendisplay.io)
|
|
455
|
+
- [Python Library](https://github.com/OpenDisplay-org/py-opendisplay)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendisplay/opendisplay",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "TypeScript library for OpenDisplay BLE e-paper displays (Web Bluetooth)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"web-bluetooth"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@opendisplay/epaper-dithering": "^2.1.
|
|
38
|
+
"@opendisplay/epaper-dithering": "^2.1.2",
|
|
39
39
|
"pako": "^2.1.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|