Skip to content

feat: PIPE_WRITE sliding-window client (0x0080-0x0082) + partial-region refresh#128

Open
davelee98 wants to merge 9 commits into
OpenDisplay:mainfrom
davelee98:feat/pipe-partial
Open

feat: PIPE_WRITE sliding-window client (0x0080-0x0082) + partial-region refresh#128
davelee98 wants to merge 9 commits into
OpenDisplay:mainfrom
davelee98:feat/pipe-partial

Conversation

@davelee98

Copy link
Copy Markdown
Contributor

Summary

Adds a PIPE_WRITE sliding-window client (protocol 0x0080–0x0082) for image transfer over BLE, modeled on QUIC-style selective repeat, plus partial-region refresh layered on the same window.

PIPE_WRITE full-frame (0x0080–0x0082)

  • Sliding-window sender with a 33-deep reorder queue and QUIC-style selective-repeat (SACK) recovery.
  • Transfer parameters (blocks_per_ack, max_queue_size) are negotiated via the 0x0080 START handshake, which stays authoritative once the gate passes.
  • Hard-gated on the device config advertising transmission_modes bit 0x10 (supports_pipe_write): a device whose config lacks the bit never receives a 0x0080 probe and stays on the legacy path. Previously the bit was advisory and the probe alone decided.
  • With the config gate in place, the START wait is a normal command timeout (TIMEOUT_PIPE_START, 30 s) rather than a 2 s discovery probe. Silence still falls back to legacy and is cached per connection.

Partial-region refresh over the window

  • 0x0080 flags bit1 PIPE_FLAG_PARTIAL + 12-byte LE geometry; ACK flags bit1 confirms.
  • Small mono partial updates now get the pipe's throughput/robustness and the e-paper partial waveform, with the legacy 0x76 path retained as fallback.
  • New START NACK codes: 0x05 (etag mismatch) / 0x06 (partial unsupported) / 0x07 (rect invalid) drive the fallback ladder.
  • No new capability bit — eligibility is inferred from existing supports_pipe_write + partial_update_support. Works unchanged under an encrypted session.

Robustness fixes

  • Sliding-window sender and END_ACK waiter now bound pathological ACK streams that never make progress (previously loops without a progress guarantee, reachable only with buggy/hostile firmware).
  • BLE stale-GATT-cache connect recovery is bounded and broadened.
  • The pipe-partial fallback no longer masks auth/integrity errors.

Testing

  • uv run pytest -q green; new suites tests/unit/test_pipe_write.py (769 lines) and tests/unit/test_pipe_write_sender.py (648 lines), plus expanded partial/clear-cache/upload-flow coverage.
  • ⚠️ Hardware validation on-device is still pending — verified by mocked unit tests only so far.

Notes for reviewers

  • Contains 7.12.0.devN version bumps that were used to force HA client reinstalls during development; squash/adjust as you prefer for release.
  • FINDINGS.md is an internal performance/correctness analysis doc — happy to drop it from this PR if it doesn't belong upstream.

🤖 Generated with Claude Code

davelee98 and others added 7 commits July 11, 2026 08:30
…ctive repeat

Adds the client side of the windowed image-transfer protocol. A single
0x0080 probe negotiates window ("max queue size", tokens in flight) and
ack cadence ("blocks per ack") and doubles as the capability check: old
firmware ignores the unknown opcode and the client falls back to the
untouched legacy 0x70/0x71/0x72 flow after a 2 s timeout, cached per
connection. New constructor kwargs blocks_per_ack=8 / max_queue_size=16
(<=1 disables pipe mode).

_send_pipe_chunks implements QUIC-style selective repeat: span-based
token rule (next_to_send - window_base < W), 1-byte rolling seq, ACK
mask expansion with mod-256 rollover, retransmit of exactly the missing
chunks re-paced per new ACK (rewind fallback when the receiver doesn't
buffer out-of-order), PTO probes, MAX_RETX/MAX_PTO abort budgets, and a
short tail-flush probe so sub-cadence tails don't stall 5 s. Uncompressed
transfers complete via the firmware's unsolicited END ack (no explicit
END, etag reported uncommitted) mirroring the legacy auto-finish
contract; compressed transfers send explicit END with etag. Fully
functional under session encryption: seq rides as the first CCM
plaintext byte, every transmission (incl. retransmits) gets a fresh
nonce, and W<=32 keeps the replay window safe.

Transport gains write_command(drain_stale=False) so in-stream writes
don't drain pending ACK notifications. 87 new unit tests cover
negotiation, windowing, loss recovery, encryption, and legacy
regression; legacy upload-flow tests pin max_queue_size=1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ient

The pipe (sliding-window) client shares version 7.11.2 with feat/ble-speed,
so HA/pip treat the git-branch requirement as already satisfied and never
reinstall from this branch — leaving HA on the pipe-less code. Bump to a
0.dev prerelease so the requirement is unambiguously unsatisfied and a
fresh install pulls this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Small mono partial updates now ride the pipe (0x0080 flags bit1
PIPE_FLAG_PARTIAL + 12-byte LE geometry; ACK flags bit1 confirms),
gaining the window's throughput/robustness and the e-paper partial
waveform. Legacy 0x76 retained as fallback via new START NACK codes
0x05 (etag mismatch) / 0x06 (partial unsupported) / 0x07 (rect invalid).

PIPE_WRITE (full and partial) is now hard-gated on the config's
transmission_modes bit 0x10 (supports_pipe_write); ungated devices never
see a 0x0080 probe. With the gate, the START wait becomes a normal
30s command timeout (TIMEOUT_PIPE_START) rather than a 2s discovery probe.

Hardening: sender and END_ACK waiter now bound pathological ACK streams
that never make progress. Works unchanged under an encrypted session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ient

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The partial-refresh upload wrapped _run_pipe_upload in a broad
`except ProtocolError` that downgraded any failure to a silent
full-upload fallback. AuthenticationError and IntegrityCheckError are
ProtocolError subclasses, so an auth/session failure (which must surface
so callers can trigger reauth) or a decrypt-integrity rejection was
swallowed. Re-raise those two families before the fallback; genuine
protocol NACKs still fall back to a full upload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously only a missing-service error triggered a single cache-clear
retry, so an ESPHome proxy serving a stale CCCD handle (ATT "Invalid
handle" during notify setup) bypassed recovery and could loop
unbounded. Recovery now covers handle-level mismatches too, is capped
at MAX_CACHE_RETRIES attempts, and fails fast without retrying on
device-absent errors.
@davelee98 davelee98 requested a review from g4bri3lDev as a code owner July 12, 2026 00:03
@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.26087% with 31 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/opendisplay/device.py 91.00% 27 Missing ⚠️
src/opendisplay/protocol/responses.py 97.43% 2 Missing ⚠️
src/opendisplay/transport/connection.py 92.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

- ruff-format the pipe-write source and tests
- drop unused total_size param from _run_pipe_upload (and call sites/tests)
- remove superfluous parens after not; scope inline pylint disables for the
  device class attribute count and the selective-repeat sender complexity

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread pyproject.toml
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.

2 participants