py-atc-ble-oepl
Python library for interacting with ATC BLE firmware over Bluetooth Low Energy.
Installation
uv add py-atc-ble-oepl
For the CLI:
uv add "py-atc-ble-oepl[cli]"
Or run the CLI directly without installing into a project:
uvx --from "py-atc-ble-oepl[cli]" atc-ble scan
CLI
atc-ble scan [--timeout 30] [--json]
atc-ble info --device ADDR [--timeout 60] [--json]
atc-ble led --device ADDR [--duration 5]
atc-ble upload --device ADDR IMAGE [--dither-mode burkes] [--fit contain] [--rotate 0] [--no-compress]
Scan for nearby devices:
$ atc-ble scan
┌──────────────────────────────────────┬───────────────┬──────────┐
│ Address │ Name │ RSSI │
├──────────────────────────────────────┼───────────────┼──────────┤
│ 5F4CEF52-A1CD-E2EE-011F-F27129B8D4A9 │ ATC_911943 │ -61 dBm │
└──────────────────────────────────────┴───────────────┴──────────┘
Show device info:
$ atc-ble info --device 5F4CEF52-A1CD-E2EE-011F-F27129B8D4A9
5F4CEF52-A1CD-E2EE-011F-F27129B8D4A9
├── Display
│ ├── Resolution 184 × 384
│ └── Color BWY
└── Hardware
├── OEPL type 0x0060
├── Screen type 2 (350 HS BWY UC Inverted)
└── ...
Flash the LED to identify a device physically:
$ atc-ble led --device 5F4CEF52-A1CD-E2EE-011F-F27129B8D4A9
LED done → ATC_911943
Upload an image:
$ atc-ble upload --device 5F4CEF52-... photo.jpg
Upload complete.
ATC tags advertise infrequently — the default --timeout for device commands is 60 s. On macOS the address is a UUID, not a MAC address.
Python API
Discover devices
from py_atc_ble_oepl import discover_atc_devices
devices = await discover_atc_devices(timeout=30.0)
for d in devices:
print(f"{d.name} {d.mac_address} {d.rssi} dBm")
Upload an image
from py_atc_ble_oepl import ATCDevice
async with ATCDevice("AA:BB:CC:DD:EE:FF") as device:
success = await device.upload_image("photo.jpg")
upload_image accepts a file path (str), raw bytes, or a PIL Image. It automatically:
- queries device capabilities (dimensions, color scheme)
- resizes and fits the image
- dithers to the display's color palette (MONO / BWR / BWY / BWRY)
- compresses and uploads over BLE
Image options
from py_atc_ble_oepl import ATCDevice, FitMode, Rotation
from epaper_dithering import DitherMode
async with ATCDevice("AA:BB:CC:DD:EE:FF") as device:
await device.upload_image(
"photo.jpg",
dither_mode=DitherMode.BURKES, # default
fit=FitMode.COVER, # STRETCH / CONTAIN / COVER / CROP
rotate=Rotation.ROTATE_90, # 0 / 90 / 180 / 270
compress=True, # default
)
Read device info
from py_atc_ble_oepl import ATCDevice
async with ATCDevice("AA:BB:CC:DD:EE:FF") as device:
caps = device._capabilities # DeviceCapabilities
cfg = device.device_config # DeviceConfig (full hardware settings)
print(f"{caps.width}x{caps.height} {caps.color_scheme}")
print(f"OEPL type 0x{cfg.hw_type:04X} screen_type {cfg.screen_type}")
Pass a discovered device to avoid re-scanning
On macOS bleak can only connect to a device it has already seen during a scan. Passing the BLEDevice object from discovery skips a second scan:
from py_atc_ble_oepl import discover_atc_devices, ATCDevice
devices = await discover_atc_devices(timeout=30.0)
if devices:
async with ATCDevice(devices[0].mac_address, ble_device=devices[0].device) as device:
await device.upload_image("photo.jpg")
Orientation
device.width and device.height are the size the panel displays, which
is what you draw for. They are a quarter turn from the buffer the tag is
actually sent, and upload_image rotates your image into that buffer for you.
The turn is a property of the hardware, not of any one model: a block written
at buffer pixel (0,0) appears at the physical top right, with the buffer's X
axis running physically downward. That was measured on four tags covering both
wh_inverted_ble values, two resolutions, both panel orientations and both
colour schemes.
It matters because getting it wrong is invisible from the code: the buffer is
the right size either way, so nothing fails and the tag renders a full,
deliberate-looking image that is simply sideways. If you are comparing against
ATC_BLE_OEPL_Image_Upload.html, note it leaves this to the person drawing on
the canvas, so a straight port of its pixel loop is a quarter turn out.
API reference
ATCDevice
ATCDevice(mac_address, ble_device=None, auto_interrogate=True, connection_timeout=60.0)
| Method / Property | Description |
|---|---|
async interrogate() |
Query device capabilities (called automatically on connect) |
async flash_led(duration=5.0) |
Flash the LED for duration seconds to identify the device |
async upload_image(image, ...) |
Upload and display an image |
width, height |
Display dimensions in pixels (None before interrogation) |
color_scheme |
ColorScheme enum value (None before interrogation) |
device_config |
DeviceConfig dataclass with full hardware settings |
discover_atc_devices(timeout=30.0)
Scans for ATC BLE devices (manufacturer ID 0x1337). Returns list[DiscoveredDevice].
DeviceCapabilities
| Field | Type | Description |
|---|---|---|
width |
int |
Display width in pixels |
height |
int |
Display height in pixels |
color_scheme |
int |
0=MONO, 1=BWR, 2=BWY, 3=BWRY |
DeviceConfig
Full hardware configuration from the 0011 dynamic config read. Key fields:
| Field | Type | Description |
|---|---|---|
hw_type |
int |
OEPL tag type (hex) |
screen_type |
int |
ATC screen driver type (1–47) |
screen_w, screen_h |
int |
Physical display dimensions |
screen_colors |
int |
Color count |
black_invert, second_color_invert |
bool |
Color plane polarity |
epd_pinout, led_pinout, nfc_pinout, flash_pinout |
dataclass or None |
GPIO pin assignments |
Acknowledgements
This library is based on the work of Aaron (atc1441) - creator of the ATC BLE firmware and the original web uploader for ATC BLE e-paper tags. The BLE protocol implemented here is derived entirely from that web uploader.
Development
git clone https://github.com/OpenDisplay-org/py-atc-ble-oepl.git
cd py-atc-ble-oepl
uv sync --all-extras
uv run pytest tests/ -v
uv run ruff check .
uv run mypy src/py_atc_ble_oepl
Metadata
Release files for py-atc-ble-oepl 0.5.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| py_atc_ble_oepl-0.5.2.tar.gz | 107.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| py_atc_ble_oepl-0.5.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 147.2 kB
Release files / py_atc_ble_oepl-0.5.2.tar.gz
| Download URL | py_atc_ble_oepl-0.5.2.tar.gz |
|---|---|
| Size | 107.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c38bae8aaf51c631fb39ddbaef6217d81210b0a87adfdaa7931139ebae56660e
|
|
BLAKE2b-256 checksum How to use checksums |
3f95070932cbd11384420322d9b807dc617e182e58e2d66b3ef615ac4476ddad
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.
Transparency logRelease files / py_atc_ble_oepl-0.5.2-py3-none-any.whl
| Download URL | py_atc_ble_oepl-0.5.2-py3-none-any.whl |
|---|---|
| Size | 39.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
21f4c1b50e48bb93f1400771c221e6ffc977935b66d6eb124e4ec658990133f6
|
|
BLAKE2b-256 checksum How to use checksums |
bccb9deddb825f63c1739ab60661504868fc99844e95d1e164b1ace96ef0747c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.
Transparency log