Skip to content

Offload image prepare to executor, keep BLE transfer on the loop#54

Open
balloob wants to merge 1 commit into
OpenDisplay:feat/clean-portfrom
balloob:fix/offload-prepare-image
Open

Offload image prepare to executor, keep BLE transfer on the loop#54
balloob wants to merge 1 commit into
OpenDisplay:feat/clean-portfrom
balloob:fix/offload-prepare-image

Conversation

@balloob

@balloob balloob commented Jul 5, 2026

Copy link
Copy Markdown

Problem

_async_send_image (the shared upload path behind both the upload_image and drawcustom services) awaited device.upload_image(...). Inside the library that call runs _prepare_image synchronously on the event loop: rotate + fit + dither + encode + zlib on a full frame. So every upload/drawcustom blocked the Home Assistant event loop for the entire CPU stage before any BLE I/O even started.

Fix

py-opendisplay 7.9.0 exposes the split deliberately:

  • prepare_image(image, config=..., ...) — module-level, does the heavy CPU work (rotate/fit/dither/encode/compress) from the device config only, no BLE connection.
  • device.upload_prepared_image(prepared, refresh_mode=...) — sends the pre-computed data over BLE.

_async_send_image now runs prepare_image in the executor via hass.async_add_executor_job (passing the same dither_mode/tone/fit/rotate options previously handed to upload_image), then does only upload_prepared_image on the loop inside _async_connect_and_run. The heavy CPU work is off the loop; only the BLE transfer stays on it.

compress is gated on display.supports_zip, mirroring upload_image() which passes compress and supports_compression into _prepare_image. When the panel has no zip support, prepare_image() returns compressed_data=None and upload_prepared_image() falls back to the uncompressed protocol — identical behavior to before, just moved off-loop.

Interaction with the per-MAC ble_lock (#53)

This is written so the two compose correctly. #53 wraps the whole _async_connect_and_run connection lifetime (async with entry.runtime_data.ble_lock, OpenDisplayDevice(...)) in the BLE lock. By moving the prepare_image executor call before _async_connect_and_run (before the connection is opened), the CPU work runs outside the lock, and the lock is held only around the upload_prepared_image BLE transfer — exactly the desired scoping (don't hold the per-tag BLE lock while burning CPU on dithering). #53 is not yet merged into feat/clean-port; this branch is based on the current feat/clean-port state and does not touch the lock itself, so it merges cleanly in either order.

Testing

  • python -m py_compile custom_components/opendisplay/services.py passes.
  • Verified the v7.9.0 API by signature: prepare_image is a module-level function exported from opendisplay, deriving capabilities and panel_ic_type from config when not supplied (equivalent to a config-constructed device that skips interrogation); upload_prepared_image(prepared_data, refresh_mode=..., compress=True, ...) accepts the (uncompressed, compressed_or_None, processed_image) tuple prepare_image returns. Only args that exist at the pinned v7.9.0 are used.
  • Reasoned end-to-end: the dither/encode/zlib CPU stage now runs in an executor thread; the BLE transfer stays on the loop.

🤖 Generated with Claude Code

https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR

_async_send_image awaited device.upload_image(), which runs the library's
_prepare_image synchronously on the event loop: rotate + fit + dither +
encode + zlib on a full frame. Every upload_image/drawcustom call blocked
the HA event loop for the whole CPU stage before any BLE I/O started.

py-opendisplay 7.9.0 exposes the split deliberately: prepare_image() is the
heavy CPU work and needs only the device config (no BLE), and
device.upload_prepared_image() does the BLE transfer. Run prepare_image() in
the executor via async_add_executor_job and pass the same
dither/tone/fit/rotate options; then upload_prepared_image() on the loop.

compress is gated on display.supports_zip to mirror upload_image() (which
passes `compress and supports_compression` into _prepare_image); when the
panel has no zip support prepare_image() returns compressed_data=None and
upload_prepared_image() falls back to the uncompressed protocol, unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
@balloob balloob requested a review from g4bri3lDev as a code owner July 5, 2026 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant