Metadata-Version: 2.4
Name: tesserae-opendisplay
Version: 0.1.0
Summary: Bridge that pushes Tesserae-rendered frames to OpenDisplay BLE e-paper tags.
Author: Kayden D'Mello
License: AGPL-3.0-or-later
Project-URL: Homepage, https://github.com/dmellok/tesserae-opendisplay
Project-URL: Tesserae, https://github.com/dmellok/tesserae
Project-URL: OpenDisplay, https://opendisplay.org/
Keywords: tesserae,opendisplay,e-ink,e-paper,ble,bluetooth
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests<3,>=2.31
Requires-Dist: pillow<12,>=10.0
Requires-Dist: py-opendisplay<6,>=5
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: ruff<0.16,>=0.15; extra == "dev"
Dynamic: license-file

# tesserae-opendisplay

A bridge that pushes [Tesserae](https://github.com/dmellok/tesserae)-rendered dashboards to [OpenDisplay](https://opendisplay.org/) BLE e-paper tags.

Tesserae renders your dashboards on the server; OpenDisplay tags are driven over Bluetooth LE. This bridge sits between them: it polls Tesserae's frame endpoint for each tag over the network, and when a frame changes it pushes the image to the tag over BLE with the [`py-opendisplay`](https://pypi.org/project/py-opendisplay/) SDK, which dithers for whatever panel the tag advertises. **One bridge drives many tags**, and you can run several bridges (one per BLE cluster) against the same Tesserae server.

## Requirements

- A running Tesserae server reachable over the network.
- A machine near your tags with a Bluetooth LE adapter (a Pi, a mini PC, an HA box) to run the bridge on.
- Python 3.11+.

## Install

```bash
pip install tesserae-opendisplay
```

## Find your tags

```bash
tesserae-opendisplay discover
```

This scans for tags in range and prints a `[[tags]]` stub for each, including the panel size it reads off the tag, so you can paste it straight into your config.

## Configure

Create `config.toml`:

```toml
server_url = "http://tesserae.local:8000"
poll_interval_s = 300        # fallback cadence; the server can override per tag

[[tags]]
device_id = "kitchen"         # the Tesserae device id (you choose it)
ble = "AA:BB:CC:DD:EE:FF"      # tag BLE MAC (or a device name to auto-scan)
width = 800                   # the tag's panel, from `discover`
height = 480
pairing_code = "AB12CD"       # optional: a Tesserae pairing code to register this tag once
# token = "..."               # or a device token you already hold
# rotate = 0                  # optional 0 / 90 / 180 / 270
# fit = "contain"             # optional contain / cover / stretch / crop
# dither = "burkes"           # optional dither mode
# refresh = "full"            # optional full / partial

[[tags]]
device_id = "hall"
ble = "OpenDisplay-A1"
width = 400
height = 300
pairing_code = "AB12CD"
```

Generate a pairing code in Tesserae under **Settings → Devices**. The bridge registers each tag with that code on first run, then caches the token in `config.toml.state.json` so later runs skip registration. After a tag is registered it shows up in Tesserae like any other device: bind a dashboard, add it to a rotation, schedule it.

## Run

```bash
tesserae-opendisplay --config config.toml
```

Each cycle the bridge asks Tesserae for every tag's frame (sending the last ETag, so unchanged frames cost nothing), and only pushes over BLE when a frame actually changed. BLE uploads happen one tag at a time (Bluetooth is serial), so a change to many tags refreshes them in sequence. Run it under systemd / an init supervisor for an always-on setup, or `--once` for a single pass (a cron-driven refresh).

## Scaling to many tags

- **One bridge** cycles its tags sequentially. Fine for a wall that updates on an e-paper cadence.
- **Multiple bridges**, each on its own BLE adapter near its cluster of tags, all point at the same `server_url`. That's how you cover more tags or more rooms: shard the tags across bridges by location.

## How it fits together

```
Tesserae server  ──HTTP /frame──►  bridge (this)  ──BLE upload──►  OpenDisplay tag
   renders                          polls + pushes                  displays
```

Tesserae renders each tag's dashboard as a full-colour PNG (the `opendisplay` device kind uses the `pi_png` renderer); the bridge hands that PNG to the OpenDisplay SDK, which does the panel-specific dithering. Nothing about the tag's geometry lives on the server beyond the panel size you register it with.

## License

AGPL-3.0-or-later.
