Python library for communicating with OpenDisplay BLE e-paper displays.
pip install py-opendisplayfrom opendisplay import OpenDisplayDevice
from PIL import Image
async with OpenDisplayDevice(mac_address="AA:BB:CC:DD:EE:FF") as device:
image = Image.open("photo.jpg")
await device.upload_image(image)from opendisplay import OpenDisplayDevice, discover_devices
from PIL import Image
# List available devices
devices = await discover_devices()
print(devices) # {"OpenDisplay-A123": "AA:BB:CC:DD:EE:FF", ...}
# Connect using name
async with OpenDisplayDevice(device_name="OpenDisplay-A123") as device:
image = Image.open("photo.jpg")
await device.upload_image(image)Images are automatically resized to match the display dimensions. A warning is logged if resizing occurs:
- WARNING:opendisplay.device:Resizing image from 1920x1080 to 296x128
For best results, resize images to the exact display dimensions before uploading.
E-paper displays have limited color palettes, requiring dithering to convert full-color images. py-opendisplay supports 9 dithering algorithms with different quality/speed tradeoffs:
none- Direct palette mapping without dithering (fastest, lowest quality)ordered- Bayer/ordered dithering using pattern matrix (fast, visible patterns)burkes- Burkes error diffusion (default, good balance)floyd-steinberg- Floyd-Steinberg error diffusion (most popular, widely used)sierra-lite- Sierra Lite (fast, simple 3-neighbor algorithm)sierra- Sierra-2-4A (balanced quality and performance)atkinson- Atkinson (designed for early Macs, artistic look)stucki- Stucki (high quality, wide error distribution)jarvis-judice-ninke- Jarvis-Judice-Ninke (highest quality, smooth gradients)
from opendisplay import OpenDisplayDevice, RefreshMode, DitherMode
from PIL import Image
async with OpenDisplayDevice(mac_address="AA:BB:CC:DD:EE:FF") as device:
image = Image.open("photo.jpg")
# Use Floyd-Steinberg dithering
await device.upload_image(
image,
dither_mode=DitherMode.FLOYD_STEINBERG,
refresh_mode=RefreshMode.FULL
)To preview how different dithering algorithms will look on your e-paper display, use the img2lcd.com online tool. Upload your image and compare the visual results before choosing an algorithm.
Quality vs Speed Tradeoff:
| Category | Algorithms |
|---|---|
| Fastest / Lowest Cost | none, ordered, sierra-lite |
| Best Cost-to-Quality | floyd-steinberg, burkes, sierra |
| Heavy / Rarely Worth It | stucki, jarvis-judice-ninke |
| Stylized / High Contrast | atkinson |
Control how the display updates when uploading images:
from opendisplay import RefreshMode
await device.upload_image(
image,
refresh_mode=RefreshMode.FULL # Options: FULL, FAST, PARTIAL, PARTIAL2
)| Mode | Description |
|---|---|
RefreshMode.FULL |
Full display refresh (default). Cleanest image quality; eliminates ghosting; slower (~5–15 seconds). |
RefreshMode.FAST |
Fast partial refresh. Quicker updates; may show slight ghosting. |
RefreshMode.PARTIAL |
Partial refresh mode 2. |
RefreshMode.PARTIAL2 |
Partial refresh mode 3. |
Note: Partial refresh support varies by display hardware. Check device capabilities for supported modes.
Query the complete device configuration including hardware specs, sensors, and capabilities:
async with OpenDisplayDevice(mac_address="AA:BB:CC:DD:EE:FF") as device:
# Automatic interrogation on first connect
config = device.config
print(f"IC Type: {config.system.ic_type_enum.name}")
print(f"Displays: {len(config.displays)}")
print(f"Sensors: {len(config.sensors)}")Skip interrogation if the device info is already cached:
# Provide cached config to skip interrogation
device = OpenDisplayDevice(mac_address="AA:BB:CC:DD:EE:FF", config=cached_config)
# Or provide minimal capabilities
capabilities = DeviceCapabilities(296, 128, ColorScheme.BWR, 0)
device = OpenDisplayDevice(mac_address="AA:BB:CC:DD:EE:FF", capabilities=capabilities)Read the device firmware version including git commit SHA:
async with OpenDisplayDevice(mac_address="AA:BB:CC:DD:EE:FF") as device:
fw = await device.read_firmware_version()
print(f"Firmware: {fw['major']}.{fw['minor']}")
print(f"Git SHA: {fw['sha']}")
# Example output:
# Firmware: 0.65
# Git SHA: e63ae32447a83f3b64f3146999060ca1e906bf15Access detailed device configuration:
async with OpenDisplayDevice(mac_address="AA:BB:CC:DD:EE:FF") as device:
# Display configuration
display = device.config.displays[0]
print(f"Panel IC: {display.panel_ic_type}")
print(f"Rotation: {display.rotation}°")
print(f"Supports ZIP: {display.supports_zip}")
print(f"Supports Direct Write: {display.supports_direct_write}")
# System configuration
system = device.config.system
print(f"IC Type: {system.ic_type_enum.name}")
print(f"Has external power pin: {system.has_pwr_pin}")
# Power configuration
if device.config.power:
power = device.config.power
print(f"Battery: {power.battery_mah}mAh")
print(f"Power mode: {power.power_mode_enum.name}")Parse real-time sensor data from BLE advertisements:
from opendisplay import parse_advertisement
# Parse manufacturer data from BLE advertisement
adv_data = parse_advertisement(manufacturer_data)
print(f"Battery: {adv_data.battery_mv}mV")
print(f"Temperature: {adv_data.temperature_c}°C")
print(f"Loop counter: {adv_data.loop_counter}")List all nearby OpenDisplay devices:
from opendisplay import discover_devices
# Scan for 10 seconds
devices = await discover_devices(timeout=10.0)
for name, mac in devices.items():
print(f"{name}: {mac}")
# Output:
# OpenDisplayA123: AA:BB:CC:DD:EE:FF
# OpenDisplayB456: 11:22:33:44:55:66uv sync --all-extras
uv run pytest