@opendisplay/opendisplay 1.0.0 → 1.1.0
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 +441 -0
- package/dist/index.cjs +39 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +39 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
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
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Web Bluetooth Integration**: Control OpenDisplay devices directly from the browser
|
|
10
|
+
- **Complete Protocol Support**: Image upload, device configuration, firmware management
|
|
11
|
+
- **Automatic Image Processing**: Built-in dithering, encoding, and compression
|
|
12
|
+
- **Type-Safe**: Full TypeScript support with exported types
|
|
13
|
+
- **Device Discovery**: Browser-native device picker
|
|
14
|
+
## Browser Compatibility
|
|
15
|
+
|
|
16
|
+
Web Bluetooth API is required. Supported browsers:
|
|
17
|
+
|
|
18
|
+
- ✅ Chrome/Edge 56+ (Desktop & Android)
|
|
19
|
+
- ✅ Opera 43+ (Desktop & Android)
|
|
20
|
+
- ✅ Samsung Internet 6.0+
|
|
21
|
+
- ❌ Firefox (no Web Bluetooth support)
|
|
22
|
+
- ❌ Safari (no Web Bluetooth support)
|
|
23
|
+
|
|
24
|
+
**Note**: HTTPS or localhost is required for Web Bluetooth API.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
### NPM/Bun/Yarn
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @opendisplay/opendisplay
|
|
32
|
+
# or
|
|
33
|
+
bun add @opendisplay/opendisplay
|
|
34
|
+
# or
|
|
35
|
+
yarn add @opendisplay/opendisplay
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### CDN (for quick prototyping)
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<script type="module">
|
|
42
|
+
import { OpenDisplayDevice } from 'https://esm.sh/@opendisplay/opendisplay@1.0.0';
|
|
43
|
+
// Your code here
|
|
44
|
+
</script>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { OpenDisplayDevice, DitherMode, RefreshMode } from '@opendisplay/opendisplay';
|
|
51
|
+
|
|
52
|
+
// Create device instance
|
|
53
|
+
const device = new OpenDisplayDevice();
|
|
54
|
+
|
|
55
|
+
// Connect to device (shows browser picker)
|
|
56
|
+
await device.connect();
|
|
57
|
+
|
|
58
|
+
// Device is auto-interrogated on first connect
|
|
59
|
+
console.log(`Connected to ${device.width}x${device.height} display`);
|
|
60
|
+
|
|
61
|
+
// Load image from canvas
|
|
62
|
+
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
|
|
63
|
+
const ctx = canvas.getContext('2d')!;
|
|
64
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
65
|
+
|
|
66
|
+
// Upload image
|
|
67
|
+
await device.uploadImage(imageData, {
|
|
68
|
+
refreshMode: RefreshMode.FULL,
|
|
69
|
+
ditherMode: DitherMode.FLOYD_STEINBERG,
|
|
70
|
+
compress: true
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Disconnect when done
|
|
74
|
+
await device.disconnect();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## API Documentation
|
|
78
|
+
|
|
79
|
+
### OpenDisplayDevice
|
|
80
|
+
|
|
81
|
+
Main class for interacting with OpenDisplay devices.
|
|
82
|
+
|
|
83
|
+
#### Constructor
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
new OpenDisplayDevice(options?: {
|
|
87
|
+
config?: GlobalConfig;
|
|
88
|
+
capabilities?: DeviceCapabilities;
|
|
89
|
+
device?: BluetoothDevice;
|
|
90
|
+
namePrefix?: string;
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Options:**
|
|
95
|
+
- `config`: Cached device configuration to skip interrogation
|
|
96
|
+
- `capabilities`: Minimal device info (width, height, color scheme) to skip interrogation
|
|
97
|
+
- `device`: Pre-selected BluetoothDevice instance
|
|
98
|
+
- `namePrefix`: Device name filter for picker (e.g., "OpenDisplay")
|
|
99
|
+
|
|
100
|
+
#### Methods
|
|
101
|
+
|
|
102
|
+
##### `connect(options?: BLEConnectionOptions): Promise<void>`
|
|
103
|
+
|
|
104
|
+
Connect to an OpenDisplay device. Shows browser's Bluetooth device picker if no device was provided in constructor.
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
await device.connect();
|
|
108
|
+
// or with name filter
|
|
109
|
+
await device.connect({ namePrefix: 'OpenDisplay' });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Automatically interrogates device on first connect unless config/capabilities were provided.
|
|
113
|
+
|
|
114
|
+
##### `disconnect(): Promise<void>`
|
|
115
|
+
|
|
116
|
+
Disconnect from the device.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
await device.disconnect();
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
##### `uploadImage(imageData: ImageData, options?): Promise<void>`
|
|
123
|
+
|
|
124
|
+
Upload image to device display. Handles resizing, dithering, encoding, and compression automatically.
|
|
125
|
+
|
|
126
|
+
**Parameters:**
|
|
127
|
+
- `imageData`: Image as ImageData from canvas
|
|
128
|
+
- `options.refreshMode`: Display refresh mode (default: `RefreshMode.FULL`)
|
|
129
|
+
- `options.ditherMode`: Dithering algorithm (default: `DitherMode.BURKES`)
|
|
130
|
+
- `options.compress`: Enable zlib compression (default: `true`)
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// From canvas
|
|
134
|
+
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
|
|
135
|
+
const ctx = canvas.getContext('2d')!;
|
|
136
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
137
|
+
|
|
138
|
+
await device.uploadImage(imageData, {
|
|
139
|
+
refreshMode: RefreshMode.FULL,
|
|
140
|
+
ditherMode: DitherMode.FLOYD_STEINBERG,
|
|
141
|
+
compress: true
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Supported Refresh Modes:**
|
|
146
|
+
- `RefreshMode.FULL` - Full refresh (recommended, ~15s)
|
|
147
|
+
- `RefreshMode.FAST` - Fast refresh if supported (~2s, may have ghosting)
|
|
148
|
+
- `RefreshMode.PARTIAL` - Partial refresh if supported
|
|
149
|
+
|
|
150
|
+
**Supported Dither Modes** (from [@opendisplay/epaper-dithering](https://www.npmjs.com/package/@opendisplay/epaper-dithering)):
|
|
151
|
+
- `DitherMode.FLOYD_STEINBERG` - Classic error diffusion (recommended)
|
|
152
|
+
- `DitherMode.BURKES` - Burkes error diffusion
|
|
153
|
+
- `DitherMode.SIERRA` - Sierra error diffusion
|
|
154
|
+
- `DitherMode.ATKINSON` - Atkinson dithering (HyperCard style)
|
|
155
|
+
- `DitherMode.STUCKI` - Stucki error diffusion
|
|
156
|
+
- `DitherMode.JARVIS` - Jarvis-Judice-Ninke
|
|
157
|
+
- `DitherMode.SIMPLE_2D` - Fast 2D error diffusion
|
|
158
|
+
- `DitherMode.ORDERED_BAYER_2` - 2x2 Bayer ordered dithering
|
|
159
|
+
- `DitherMode.ORDERED_BAYER_4` - 4x4 Bayer ordered dithering
|
|
160
|
+
|
|
161
|
+
##### `interrogate(): Promise<GlobalConfig>`
|
|
162
|
+
|
|
163
|
+
Read complete device configuration from device. Automatically called on first connect unless config/capabilities were provided.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const config = await device.interrogate();
|
|
167
|
+
console.log(`Device has ${config.displays.length} display(s)`);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
##### `readFirmwareVersion(): Promise<FirmwareVersion>`
|
|
171
|
+
|
|
172
|
+
Read firmware version from device.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const fw = await device.readFirmwareVersion();
|
|
176
|
+
console.log(`Firmware v${fw.major}.${fw.minor} (${fw.sha})`);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
##### `writeConfig(config: GlobalConfig): Promise<void>`
|
|
180
|
+
|
|
181
|
+
Write configuration to device. Device must be rebooted for changes to take effect.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// Read current config
|
|
185
|
+
const config = device.config!;
|
|
186
|
+
|
|
187
|
+
// Modify config
|
|
188
|
+
config.displays[0].rotation = 1;
|
|
189
|
+
|
|
190
|
+
// Write back to device
|
|
191
|
+
await device.writeConfig(config);
|
|
192
|
+
|
|
193
|
+
// Reboot to apply changes
|
|
194
|
+
await device.reboot();
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
##### `reboot(): Promise<void>`
|
|
198
|
+
|
|
199
|
+
Reboot the device. Connection will drop as device resets.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
await device.reboot();
|
|
203
|
+
// Device will disconnect automatically
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### Properties
|
|
207
|
+
|
|
208
|
+
##### `isConnected: boolean`
|
|
209
|
+
|
|
210
|
+
Check if currently connected to a device.
|
|
211
|
+
|
|
212
|
+
##### `width: number`
|
|
213
|
+
|
|
214
|
+
Display width in pixels (throws if not interrogated).
|
|
215
|
+
|
|
216
|
+
##### `height: number`
|
|
217
|
+
|
|
218
|
+
Display height in pixels (throws if not interrogated).
|
|
219
|
+
|
|
220
|
+
##### `colorScheme: ColorScheme`
|
|
221
|
+
|
|
222
|
+
Display color scheme (throws if not interrogated).
|
|
223
|
+
|
|
224
|
+
Possible values:
|
|
225
|
+
- `ColorScheme.MONO` - Black and white
|
|
226
|
+
- `ColorScheme.BWR` - Black, white, red
|
|
227
|
+
- `ColorScheme.BWY` - Black, white, yellow
|
|
228
|
+
- `ColorScheme.BWRY` - Black, white, red, yellow (4-color)
|
|
229
|
+
- `ColorScheme.BWGBRY` - Black, white, green, blue, red, yellow (6-color Spectra)
|
|
230
|
+
- `ColorScheme.GRAYSCALE_4` - 4-level grayscale
|
|
231
|
+
|
|
232
|
+
##### `rotation: number`
|
|
233
|
+
|
|
234
|
+
Display rotation steps (by 90 degrees) (0, 1, 2, 3).
|
|
235
|
+
|
|
236
|
+
##### `config: GlobalConfig | null`
|
|
237
|
+
|
|
238
|
+
Full device configuration (null if not interrogated).
|
|
239
|
+
|
|
240
|
+
##### `capabilities: DeviceCapabilities | null`
|
|
241
|
+
|
|
242
|
+
Minimal device info (width, height, colorScheme, rotation).
|
|
243
|
+
|
|
244
|
+
### Discovery
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { discoverDevices } from '@opendisplay/opendisplay';
|
|
248
|
+
|
|
249
|
+
// Show device picker
|
|
250
|
+
const device = await discoverDevices();
|
|
251
|
+
// or with name filter
|
|
252
|
+
const device = await discoverDevices('OD');
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Types
|
|
256
|
+
|
|
257
|
+
All types are exported for TypeScript users:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import type {
|
|
261
|
+
GlobalConfig,
|
|
262
|
+
DisplayConfig,
|
|
263
|
+
DeviceCapabilities,
|
|
264
|
+
FirmwareVersion,
|
|
265
|
+
AdvertisementData
|
|
266
|
+
} from '@opendisplay/opendisplay';
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Usage Examples
|
|
270
|
+
|
|
271
|
+
### Basic Image Upload
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { OpenDisplayDevice } from '@opendisplay/opendisplay';
|
|
275
|
+
|
|
276
|
+
const device = new OpenDisplayDevice();
|
|
277
|
+
await device.connect();
|
|
278
|
+
|
|
279
|
+
// Create canvas with image
|
|
280
|
+
const canvas = document.createElement('canvas');
|
|
281
|
+
canvas.width = device.width;
|
|
282
|
+
canvas.height = device.height;
|
|
283
|
+
const ctx = canvas.getContext('2d')!;
|
|
284
|
+
|
|
285
|
+
// Draw something
|
|
286
|
+
ctx.fillStyle = 'white';
|
|
287
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
288
|
+
ctx.fillStyle = 'black';
|
|
289
|
+
ctx.font = '48px Arial';
|
|
290
|
+
ctx.fillText('Hello OpenDisplay!', 50, 100);
|
|
291
|
+
|
|
292
|
+
// Upload to device
|
|
293
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
294
|
+
await device.uploadImage(imageData);
|
|
295
|
+
|
|
296
|
+
await device.disconnect();
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Upload Image from File
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// HTML: <input type="file" id="imageInput" accept="image/*">
|
|
303
|
+
|
|
304
|
+
const input = document.getElementById('imageInput') as HTMLInputElement;
|
|
305
|
+
input.addEventListener('change', async (e) => {
|
|
306
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
307
|
+
if (!file) return;
|
|
308
|
+
|
|
309
|
+
// Load image
|
|
310
|
+
const img = new Image();
|
|
311
|
+
img.src = URL.createObjectURL(file);
|
|
312
|
+
await img.decode();
|
|
313
|
+
|
|
314
|
+
// Convert to ImageData
|
|
315
|
+
const canvas = document.createElement('canvas');
|
|
316
|
+
canvas.width = img.width;
|
|
317
|
+
canvas.height = img.height;
|
|
318
|
+
const ctx = canvas.getContext('2d')!;
|
|
319
|
+
ctx.drawImage(img, 0, 0);
|
|
320
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
321
|
+
|
|
322
|
+
// Upload
|
|
323
|
+
await device.uploadImage(imageData);
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Skip Interrogation with Cached Config
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { OpenDisplayDevice } from '@opendisplay/opendisplay';
|
|
331
|
+
|
|
332
|
+
// First connection - interrogate and cache
|
|
333
|
+
const device = new OpenDisplayDevice();
|
|
334
|
+
await device.connect();
|
|
335
|
+
const cachedConfig = device.config!;
|
|
336
|
+
|
|
337
|
+
// Store config in localStorage
|
|
338
|
+
localStorage.setItem('deviceConfig', JSON.stringify(cachedConfig));
|
|
339
|
+
|
|
340
|
+
// Later - reuse cached config
|
|
341
|
+
const savedConfig = JSON.parse(localStorage.getItem('deviceConfig')!);
|
|
342
|
+
const fastDevice = new OpenDisplayDevice({ config: savedConfig });
|
|
343
|
+
await fastDevice.connect();
|
|
344
|
+
// No interrogation needed!
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Skip Interrogation with Minimal Capabilities
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
import { OpenDisplayDevice, ColorScheme } from '@opendisplay/opendisplay';
|
|
351
|
+
|
|
352
|
+
// If you know your device specs
|
|
353
|
+
const device = new OpenDisplayDevice({
|
|
354
|
+
capabilities: {
|
|
355
|
+
width: 296,
|
|
356
|
+
height: 128,
|
|
357
|
+
colorScheme: ColorScheme.BWR,
|
|
358
|
+
rotation: 0
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await device.connect();
|
|
363
|
+
// Fast connection - no interrogation!
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Read and Modify Device Configuration
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
const device = new OpenDisplayDevice();
|
|
370
|
+
await device.connect();
|
|
371
|
+
|
|
372
|
+
// Read config
|
|
373
|
+
const config = await device.interrogate();
|
|
374
|
+
console.log(`Display: ${config.displays[0].pixelWidth}x${config.displays[0].pixelHeight}`);
|
|
375
|
+
console.log(`Battery: ${config.power?.batteryCapacityMah}mAh`);
|
|
376
|
+
|
|
377
|
+
// Modify rotation
|
|
378
|
+
config.displays[0].rotation = 180;
|
|
379
|
+
|
|
380
|
+
// Write back
|
|
381
|
+
await device.writeConfig(config);
|
|
382
|
+
await device.reboot(); // Reboot to apply
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Error Handling
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import {
|
|
389
|
+
OpenDisplayDevice,
|
|
390
|
+
BLEConnectionError,
|
|
391
|
+
BLETimeoutError,
|
|
392
|
+
ProtocolError
|
|
393
|
+
} from '@opendisplay/opendisplay';
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const device = new OpenDisplayDevice();
|
|
397
|
+
await device.connect();
|
|
398
|
+
await device.uploadImage(imageData);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (error instanceof BLEConnectionError) {
|
|
401
|
+
console.error('Failed to connect:', error.message);
|
|
402
|
+
} else if (error instanceof BLETimeoutError) {
|
|
403
|
+
console.error('Operation timed out:', error.message);
|
|
404
|
+
} else if (error instanceof ProtocolError) {
|
|
405
|
+
console.error('Protocol error:', error.message);
|
|
406
|
+
} else {
|
|
407
|
+
console.error('Unexpected error:', error);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Architecture
|
|
413
|
+
|
|
414
|
+
This library mirrors the architecture of [py-opendisplay](https://github.com/OpenDisplay-org/py-opendisplay):
|
|
415
|
+
|
|
416
|
+
- **Protocol Layer**: Command builders, response parsers, TLV config handling
|
|
417
|
+
- **Transport Layer**: Web Bluetooth wrapper with notification queue
|
|
418
|
+
- **Encoding Layer**: Image encoding, compression, bitplane handling
|
|
419
|
+
- **Models Layer**: TypeScript interfaces for all data structures
|
|
420
|
+
- **Public API**: `OpenDisplayDevice` class and helper functions
|
|
421
|
+
|
|
422
|
+
## Development
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
# Install dependencies
|
|
426
|
+
npm install
|
|
427
|
+
|
|
428
|
+
# Build library
|
|
429
|
+
npm run build
|
|
430
|
+
|
|
431
|
+
# Type check
|
|
432
|
+
npm run type-check
|
|
433
|
+
|
|
434
|
+
# Lint
|
|
435
|
+
npm run lint
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Related Packages
|
|
439
|
+
|
|
440
|
+
- [@opendisplay/epaper-dithering](https://www.npmjs.com/package/@opendisplay/epaper-dithering) - Dithering algorithms for e-paper displays
|
|
441
|
+
- [py-opendisplay](https://github.com/OpenDisplay-org/py-opendisplay) - Python version
|
package/dist/index.cjs
CHANGED
|
@@ -1716,7 +1716,10 @@ var OpenDisplayDevice = class _OpenDisplayDevice {
|
|
|
1716
1716
|
* await device.uploadImage(imageData, {
|
|
1717
1717
|
* refreshMode: RefreshMode.FULL,
|
|
1718
1718
|
* ditherMode: DitherMode.BURKES,
|
|
1719
|
-
* compress: true
|
|
1719
|
+
* compress: true,
|
|
1720
|
+
* onProgress: (current, total, stage) => {
|
|
1721
|
+
* console.log(`${stage}: ${current}/${total} (${Math.floor(current/total*100)}%)`);
|
|
1722
|
+
* }
|
|
1720
1723
|
* });
|
|
1721
1724
|
* ```
|
|
1722
1725
|
*/
|
|
@@ -1726,9 +1729,12 @@ var OpenDisplayDevice = class _OpenDisplayDevice {
|
|
|
1726
1729
|
const refreshMode = options.refreshMode ?? 0 /* FULL */;
|
|
1727
1730
|
const ditherMode = options.ditherMode ?? import_epaper_dithering4.DitherMode.BURKES;
|
|
1728
1731
|
const compress = options.compress ?? true;
|
|
1732
|
+
const onProgress = options.onProgress;
|
|
1733
|
+
const onStatusChange = options.onStatusChange;
|
|
1729
1734
|
console.log(
|
|
1730
1735
|
`Uploading image (${this.width}x${this.height}, ${import_epaper_dithering4.ColorScheme[this.colorScheme]})`
|
|
1731
1736
|
);
|
|
1737
|
+
onStatusChange?.("Preparing image...");
|
|
1732
1738
|
const encodedData = prepareImageForUpload(
|
|
1733
1739
|
imageData,
|
|
1734
1740
|
this.width,
|
|
@@ -1738,27 +1744,44 @@ var OpenDisplayDevice = class _OpenDisplayDevice {
|
|
|
1738
1744
|
);
|
|
1739
1745
|
let compressedData = null;
|
|
1740
1746
|
if (compress) {
|
|
1747
|
+
onStatusChange?.("Compressing...");
|
|
1741
1748
|
compressedData = compressImageData(encodedData, 6);
|
|
1742
1749
|
if (compressedData.length < MAX_COMPRESSED_SIZE) {
|
|
1743
1750
|
console.log(`Using compressed upload protocol (size: ${compressedData.length} bytes)`);
|
|
1751
|
+
onStatusChange?.("Uploading...");
|
|
1744
1752
|
await this.executeUpload({
|
|
1745
1753
|
imageData: encodedData,
|
|
1746
1754
|
refreshMode,
|
|
1747
1755
|
useCompression: true,
|
|
1748
1756
|
compressedData,
|
|
1749
|
-
uncompressedSize: encodedData.length
|
|
1757
|
+
uncompressedSize: encodedData.length,
|
|
1758
|
+
onProgress,
|
|
1759
|
+
onStatusChange
|
|
1750
1760
|
});
|
|
1751
1761
|
} else {
|
|
1752
1762
|
console.log(
|
|
1753
1763
|
`Compressed size exceeds ${MAX_COMPRESSED_SIZE} bytes, using uncompressed protocol`
|
|
1754
1764
|
);
|
|
1755
|
-
|
|
1765
|
+
onStatusChange?.("Uploading...");
|
|
1766
|
+
await this.executeUpload({
|
|
1767
|
+
imageData: encodedData,
|
|
1768
|
+
refreshMode,
|
|
1769
|
+
onProgress,
|
|
1770
|
+
onStatusChange
|
|
1771
|
+
});
|
|
1756
1772
|
}
|
|
1757
1773
|
} else {
|
|
1758
1774
|
console.log("Compression disabled, using uncompressed protocol");
|
|
1759
|
-
|
|
1775
|
+
onStatusChange?.("Uploading...");
|
|
1776
|
+
await this.executeUpload({
|
|
1777
|
+
imageData: encodedData,
|
|
1778
|
+
refreshMode,
|
|
1779
|
+
onProgress,
|
|
1780
|
+
onStatusChange
|
|
1781
|
+
});
|
|
1760
1782
|
}
|
|
1761
1783
|
console.log("Image upload complete");
|
|
1784
|
+
onStatusChange?.("Upload complete!");
|
|
1762
1785
|
}
|
|
1763
1786
|
/**
|
|
1764
1787
|
* Execute image upload using compressed or uncompressed protocol.
|
|
@@ -1769,7 +1792,9 @@ var OpenDisplayDevice = class _OpenDisplayDevice {
|
|
|
1769
1792
|
refreshMode,
|
|
1770
1793
|
useCompression = false,
|
|
1771
1794
|
compressedData,
|
|
1772
|
-
uncompressedSize
|
|
1795
|
+
uncompressedSize,
|
|
1796
|
+
onProgress,
|
|
1797
|
+
onStatusChange
|
|
1773
1798
|
} = params;
|
|
1774
1799
|
let startCmd;
|
|
1775
1800
|
let remainingCompressed = null;
|
|
@@ -1788,11 +1813,12 @@ var OpenDisplayDevice = class _OpenDisplayDevice {
|
|
|
1788
1813
|
validateAckResponse(response, 112 /* DIRECT_WRITE_START */);
|
|
1789
1814
|
let autoCompleted = false;
|
|
1790
1815
|
if (useCompression && remainingCompressed && remainingCompressed.length > 0) {
|
|
1791
|
-
autoCompleted = await this.sendDataChunks(remainingCompressed);
|
|
1816
|
+
autoCompleted = await this.sendDataChunks(remainingCompressed, onProgress, onStatusChange);
|
|
1792
1817
|
} else if (!useCompression) {
|
|
1793
|
-
autoCompleted = await this.sendDataChunks(imageData);
|
|
1818
|
+
autoCompleted = await this.sendDataChunks(imageData, onProgress, onStatusChange);
|
|
1794
1819
|
}
|
|
1795
1820
|
if (!autoCompleted) {
|
|
1821
|
+
onStatusChange?.("Refreshing display...");
|
|
1796
1822
|
const endCmd = buildDirectWriteEndCommand(refreshMode);
|
|
1797
1823
|
await this.connection.writeCommand(endCmd);
|
|
1798
1824
|
response = await this.connection.readResponse(
|
|
@@ -1810,11 +1836,13 @@ var OpenDisplayDevice = class _OpenDisplayDevice {
|
|
|
1810
1836
|
* - Progress logging
|
|
1811
1837
|
*
|
|
1812
1838
|
* @param imageData - Data to send in chunks
|
|
1839
|
+
* @param onProgress - Optional progress callback (current bytes, total bytes, stage)
|
|
1840
|
+
* @param onStatusChange - Optional status message callback
|
|
1813
1841
|
* @returns True if device auto-completed (sent 0x0072 END early), false if all chunks sent normally
|
|
1814
1842
|
* @throws {ProtocolError} If unexpected response received
|
|
1815
1843
|
* @throws {BLETimeoutError} If no response within timeout
|
|
1816
1844
|
*/
|
|
1817
|
-
async sendDataChunks(imageData) {
|
|
1845
|
+
async sendDataChunks(imageData, onProgress, onStatusChange) {
|
|
1818
1846
|
let bytesSent = 0;
|
|
1819
1847
|
let chunksSent = 0;
|
|
1820
1848
|
while (bytesSent < imageData.length) {
|
|
@@ -1835,6 +1863,7 @@ var OpenDisplayDevice = class _OpenDisplayDevice {
|
|
|
1835
1863
|
console.log(
|
|
1836
1864
|
`No response after chunk ${chunksSent} (${(bytesSent / imageData.length * 100).toFixed(1)}%), waiting for device refresh...`
|
|
1837
1865
|
);
|
|
1866
|
+
onStatusChange?.("Refreshing display...");
|
|
1838
1867
|
response = await this.connection.readResponse(
|
|
1839
1868
|
_OpenDisplayDevice.TIMEOUT_REFRESH
|
|
1840
1869
|
);
|
|
@@ -1844,10 +1873,12 @@ var OpenDisplayDevice = class _OpenDisplayDevice {
|
|
|
1844
1873
|
}
|
|
1845
1874
|
const [command, isAck] = checkResponseType(response);
|
|
1846
1875
|
if (command === 113 /* DIRECT_WRITE_DATA */) {
|
|
1876
|
+
onProgress?.(bytesSent, imageData.length, "upload");
|
|
1847
1877
|
} else if (command === 114 /* DIRECT_WRITE_END */) {
|
|
1848
1878
|
console.log(
|
|
1849
1879
|
`Received END response after chunk ${chunksSent} - device auto-completed`
|
|
1850
1880
|
);
|
|
1881
|
+
onProgress?.(imageData.length, imageData.length, "upload");
|
|
1851
1882
|
return true;
|
|
1852
1883
|
} else {
|
|
1853
1884
|
throw new ProtocolError(
|