@opendisplay/opendisplay
v1.2.0
Published
TypeScript library for OpenDisplay BLE e-paper displays (Web Bluetooth)
Maintainers
Readme
@opendisplay/opendisplay
TypeScript library for OpenDisplay BLE e-paper displays using Web Bluetooth API. Control your OpenDisplay devices directly from the browser.
Features
- Web Bluetooth Integration: Control OpenDisplay devices directly from the browser
- Complete Protocol Support: Image upload, device configuration, firmware management
- Automatic Image Processing: Built-in dithering, encoding, and compression
- Type-Safe: Full TypeScript support with exported types
- Device Discovery: Browser-native device picker
Browser Compatibility
Web Bluetooth API is required. Supported browsers:
- ✅ Chrome/Edge 56+ (Desktop & Android)
- ✅ Opera 43+ (Desktop & Android)
- ✅ Samsung Internet 6.0+
- ❌ Firefox (no Web Bluetooth support)
- ❌ Safari (no Web Bluetooth support)
Note: HTTPS or localhost is required for Web Bluetooth API.
Installation
NPM/Bun/Yarn
npm install @opendisplay/opendisplay
# or
bun add @opendisplay/opendisplay
# or
yarn add @opendisplay/opendisplayCDN (for quick prototyping)
<script type="module">
import { OpenDisplayDevice } from 'https://esm.sh/@opendisplay/[email protected]';
// Your code here
</script>Quick Start
import { OpenDisplayDevice, DitherMode, RefreshMode } from '@opendisplay/opendisplay';
// Create device instance
const device = new OpenDisplayDevice();
// Connect to device (shows browser picker)
await device.connect();
// Device is auto-interrogated on first connect
console.log(`Connected to ${device.width}x${device.height} display`);
// Load image from canvas
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Upload image
await device.uploadImage(imageData, {
refreshMode: RefreshMode.FULL,
ditherMode: DitherMode.FLOYD_STEINBERG,
compress: true
});
// Disconnect when done
await device.disconnect();API Documentation
OpenDisplayDevice
Main class for interacting with OpenDisplay devices.
Constructor
new OpenDisplayDevice(options?: {
config?: GlobalConfig;
capabilities?: DeviceCapabilities;
device?: BluetoothDevice;
namePrefix?: string;
})Options:
config: Cached device configuration to skip interrogationcapabilities: Minimal device info (width, height, color scheme) to skip interrogationdevice: Pre-selected BluetoothDevice instancenamePrefix: Device name filter for picker (e.g., "OpenDisplay")
Methods
connect(options?: BLEConnectionOptions): Promise<void>
Connect to an OpenDisplay device. Shows browser's Bluetooth device picker if no device was provided in constructor.
await device.connect();
// or with name filter
await device.connect({ namePrefix: 'OpenDisplay' });Automatically interrogates device on first connect unless config/capabilities were provided.
disconnect(): Promise<void>
Disconnect from the device.
await device.disconnect();uploadImage(imageData: ImageData, options?): Promise<void>
Upload image to device display. Handles resizing, dithering, encoding, and compression automatically.
Parameters:
imageData: Image as ImageData from canvasoptions.refreshMode: Display refresh mode (default:RefreshMode.FULL)options.ditherMode: Dithering algorithm (default:DitherMode.BURKES)options.compress: Enable zlib compression (default:true)
// From canvas
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
await device.uploadImage(imageData, {
refreshMode: RefreshMode.FULL,
ditherMode: DitherMode.FLOYD_STEINBERG,
compress: true
});Upload Progress & Callbacks
The uploadImage() method supports several optional callbacks for tracking progress and timing:
await device.uploadImage(imageData, {
refreshMode: RefreshMode.FULL,
ditherMode: DitherMode.BURKES,
compress: true,
// Called during data transfer with progress updates
onProgress: (current, total, stage) => {
const percent = Math.floor((current / total) * 100);
console.log(`${stage}: ${percent}% (${current}/${total} bytes)`);
},
// Called at various stages with status messages
onStatusChange: (message) => {
console.log(`Status: ${message}`);
// Example messages:
// - "Preparing image..."
// - "Compressing..."
// - "Uploading..."
// - "Upload complete (X.Xs), refreshing display..."
// - "Refresh complete (Y.Ys)"
},
// Called when upload is complete (before display refresh)
onUploadComplete: (uploadTimeSeconds) => {
console.log(`Upload took ${uploadTimeSeconds.toFixed(1)}s`);
},
// Called when entire operation completes (upload + refresh)
onComplete: (uploadTime, refreshTime, totalTime) => {
console.log(`Complete! Upload: ${uploadTime.toFixed(1)}s, Refresh: ${refreshTime.toFixed(1)}s, Total: ${totalTime.toFixed(1)}s`);
}
});Callback Reference:
| Callback | Parameters | When Called | Use Case |
|----------|-----------|-------------|----------|
| onProgress | (current, total, stage) | After each data chunk sent | Progress bars, upload percentage |
| onStatusChange | (message) | At each operation stage | Status messages, user feedback |
| onUploadComplete | (uploadTimeSeconds) | Upload done, before refresh | Track upload performance |
| onComplete | (uploadTime, refreshTime, totalTime) | After refresh completes | Final timing summary |
Supported Refresh Modes:
RefreshMode.FULL- Full refresh (recommended, ~15s)RefreshMode.FAST- Fast refresh if supported (~2s, may have ghosting)RefreshMode.PARTIAL- Partial refresh if supported
Supported Dither Modes (from @opendisplay/epaper-dithering):
DitherMode.FLOYD_STEINBERG- Classic error diffusion (recommended)DitherMode.BURKES- Burkes error diffusionDitherMode.SIERRA- Sierra error diffusionDitherMode.ATKINSON- Atkinson dithering (HyperCard style)DitherMode.STUCKI- Stucki error diffusionDitherMode.JARVIS- Jarvis-Judice-NinkeDitherMode.SIMPLE_2D- Fast 2D error diffusionDitherMode.ORDERED_BAYER_2- 2x2 Bayer ordered ditheringDitherMode.ORDERED_BAYER_4- 4x4 Bayer ordered dithering
interrogate(): Promise<GlobalConfig>
Read complete device configuration from device. Automatically called on first connect unless config/capabilities were provided.
const config = await device.interrogate();
console.log(`Device has ${config.displays.length} display(s)`);readFirmwareVersion(): Promise<FirmwareVersion>
Read firmware version from device.
const fw = await device.readFirmwareVersion();
console.log(`Firmware v${fw.major}.${fw.minor} (${fw.sha})`);writeConfig(config: GlobalConfig): Promise<void>
Write configuration to device. Device must be rebooted for changes to take effect.
// Read current config
const config = device.config!;
// Modify config
config.displays[0].rotation = 1;
// Write back to device
await device.writeConfig(config);
// Reboot to apply changes
await device.reboot();reboot(): Promise<void>
Reboot the device. Connection will drop as device resets.
await device.reboot();
// Device will disconnect automaticallyProperties
isConnected: boolean
Check if currently connected to a device.
width: number
Display width in pixels (throws if not interrogated).
height: number
Display height in pixels (throws if not interrogated).
colorScheme: ColorScheme
Display color scheme (throws if not interrogated).
Possible values:
ColorScheme.MONO- Black and whiteColorScheme.BWR- Black, white, redColorScheme.BWY- Black, white, yellowColorScheme.BWRY- Black, white, red, yellow (4-color)ColorScheme.BWGBRY- Black, white, green, blue, red, yellow (6-color Spectra)ColorScheme.GRAYSCALE_4- 4-level grayscale
rotation: number
Display rotation steps (by 90 degrees) (0, 1, 2, 3).
config: GlobalConfig | null
Full device configuration (null if not interrogated).
capabilities: DeviceCapabilities | null
Minimal device info (width, height, colorScheme, rotation).
Discovery
import { discoverDevices } from '@opendisplay/opendisplay';
// Show device picker
const device = await discoverDevices();
// or with name filter
const device = await discoverDevices('OD');Types
All types are exported for TypeScript users:
import type {
GlobalConfig,
DisplayConfig,
DeviceCapabilities,
FirmwareVersion,
AdvertisementData
} from '@opendisplay/opendisplay';Usage Examples
Basic Image Upload
import { OpenDisplayDevice } from '@opendisplay/opendisplay';
const device = new OpenDisplayDevice();
await device.connect();
// Create canvas with image
const canvas = document.createElement('canvas');
canvas.width = device.width;
canvas.height = device.height;
const ctx = canvas.getContext('2d')!;
// Draw something
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black';
ctx.font = '48px Arial';
ctx.fillText('Hello OpenDisplay!', 50, 100);
// Upload to device
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
await device.uploadImage(imageData);
await device.disconnect();Upload Image from File
// HTML: <input type="file" id="imageInput" accept="image/*">
const input = document.getElementById('imageInput') as HTMLInputElement;
input.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
// Load image
const img = new Image();
img.src = URL.createObjectURL(file);
await img.decode();
// Convert to ImageData
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Upload
await device.uploadImage(imageData);
});Skip Interrogation with Cached Config
import { OpenDisplayDevice } from '@opendisplay/opendisplay';
// First connection - interrogate and cache
const device = new OpenDisplayDevice();
await device.connect();
const cachedConfig = device.config!;
// Store config in localStorage
localStorage.setItem('deviceConfig', JSON.stringify(cachedConfig));
// Later - reuse cached config
const savedConfig = JSON.parse(localStorage.getItem('deviceConfig')!);
const fastDevice = new OpenDisplayDevice({ config: savedConfig });
await fastDevice.connect();
// No interrogation needed!Skip Interrogation with Minimal Capabilities
import { OpenDisplayDevice, ColorScheme } from '@opendisplay/opendisplay';
// If you know your device specs
const device = new OpenDisplayDevice({
capabilities: {
width: 296,
height: 128,
colorScheme: ColorScheme.BWR,
rotation: 0
}
});
await device.connect();
// Fast connection - no interrogation!Read and Modify Device Configuration
const device = new OpenDisplayDevice();
await device.connect();
// Read config
const config = await device.interrogate();
console.log(`Display: ${config.displays[0].pixelWidth}x${config.displays[0].pixelHeight}`);
console.log(`Battery: ${config.power?.batteryCapacityMah}mAh`);
// Modify rotation
config.displays[0].rotation = 180;
// Write back
await device.writeConfig(config);
await device.reboot(); // Reboot to applyError Handling
import {
OpenDisplayDevice,
BLEConnectionError,
BLETimeoutError,
ProtocolError
} from '@opendisplay/opendisplay';
try {
const device = new OpenDisplayDevice();
await device.connect();
await device.uploadImage(imageData);
} catch (error) {
if (error instanceof BLEConnectionError) {
console.error('Failed to connect:', error.message);
} else if (error instanceof BLETimeoutError) {
console.error('Operation timed out:', error.message);
} else if (error instanceof ProtocolError) {
console.error('Protocol error:', error.message);
} else {
console.error('Unexpected error:', error);
}
}Architecture
This library mirrors the architecture of py-opendisplay:
- Protocol Layer: Command builders, response parsers, TLV config handling
- Transport Layer: Web Bluetooth wrapper with notification queue
- Encoding Layer: Image encoding, compression, bitplane handling
- Models Layer: TypeScript interfaces for all data structures
- Public API:
OpenDisplayDeviceclass and helper functions
Development
# Install dependencies
npm install
# Build library
npm run build
# Type check
npm run type-check
# Lint
npm run lintRelated Packages
- @opendisplay/epaper-dithering - Dithering algorithms for e-paper displays
- py-opendisplay - Python version
