Skip to content

feat(transport): add WiFi/LAN (TCP + TLS-PSK) transport - #147

Merged
g4bri3lDev merged 3 commits into
OpenDisplay:mainfrom
davelee98:feat/wifi-transport-PR
Jul 30, 2026
Merged

feat(transport): add WiFi/LAN (TCP + TLS-PSK) transport#147
g4bri3lDev merged 3 commits into
OpenDisplay:mainfrom
davelee98:feat/wifi-transport-PR

Conversation

@davelee98

Copy link
Copy Markdown
Contributor

Summary

Adds an IP transport alongside BLE so OpenDisplay displays can be driven over WiFi/LAN.

  • transport/ip.py — TCP + TLS-PSK connection, framing per the wire protocol
  • transport/base.py / connection.py — transport abstraction and selection
  • discovery_ip.py — zeroconf-based IP discovery (new optional wifi extra)
  • device.py / cli.py — host/port/tls/psk addressing and transport selection
  • DATA-ack timeout handling on the compressed path; TLS-PSK derived from the device master key

⚠️ Breaking: minimum Python is now 3.13

TLS-PSK requires ssl.set_psk_client_callback, added in Python 3.13. This bumps requires-python from >=3.11 to >=3.13, dropping 3.11 and 3.12 support. CI matrix updated accordingly (3.13 floor + 3.14 non-blocking); ruff/mypy targets moved to 3.13; AGENTS.md updated. Flagging for maintainer sign-off since it narrows the advertised support range.

Notes

  • Version/CHANGELOG intentionally left to release-please (feat: commit drives the bump).
  • uv.lock regenerated: adds zeroconf + ifaddr; drops tomli (was a python_version < "3.11" marker dep, now unreachable).

Testing

  • uv run pytest — 886 passed, on both 3.13 and 3.14
  • uv run prek run --all-files — ruff, ruff-format, mypy (strict), pylint all pass

Add an IP transport alongside BLE so displays can be driven over WiFi/LAN.

- transport/ip.py: TCP + TLS-PSK connection, framing per the wire protocol
- transport/base.py, connection.py: transport abstraction and selection
- discovery_ip.py: zeroconf-based IP discovery (new optional `wifi` extra)
- device.py/cli.py: host/port/tls/psk addressing and transport selection
- DATA-ack timeout handling on the compressed path; TLS-PSK derived from
  the device master key

Requires Python >= 3.13 (TLS-PSK needs ssl.set_psk_client_callback).
CI matrix updated to 3.13 (floor) + 3.14 (non-blocking).
@davelee98
davelee98 requested a review from g4bri3lDev as a code owner July 25, 2026 13:34
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.26950% with 50 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/opendisplay/cli.py 0.00% 47 Missing ⚠️
src/opendisplay/device.py 94.44% 2 Missing ⚠️
src/opendisplay/transport/connection.py 75.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Patch coverage on OpenDisplay#147 was 59.58% against a 79.05% target, failing
codecov/patch. Brings transport/ip.py and discovery_ip.py to 100%.

- test_transport_ip.py: _build_ssl_context() verified against a real
  loopback TLS-PSK server (handshake, framed round-trip, wrong-PSK
  rejection), asserting the "opendisplay" identity reaches the wire;
  skipped on OpenSSL builds without PSK ciphersuites. Adds the
  connect/disconnect/write/read error branches.
- test_discovery_ip.py (new): zeroconf stubbed at the lazily-imported
  module attributes. Covers resolution skips, the name-suffix fallback,
  teardown on error, and the TXT decode helpers.

cli.py's new lines stay uncovered: it is 0% repo-wide on main and needs
CLI test scaffolding that does not exist yet.
@g4bri3lDev

Copy link
Copy Markdown
Member

scan --lan never finds anything, because _on_change in discovery_ip.py names its first two params _zeroconf/_service_type while zeroconf's Signal.fire() dispatches by keyword — every event raises TypeError: got an unexpected keyword argument 'zeroconf', which the browser swallows. Dropping the underscores fixes it; the suite misses it because _FakeBrowser (test_discovery_ip.py:54) calls the handler positionally rather than by keyword like real zeroconf does.

…hes with

`_on_change` declared its first two parameters as `_zeroconf` / `_service_type`,
but zeroconf's Signal.fire() invokes handlers with KEYWORD arguments
(zeroconf=, service_type=, name=, state_change=). Every service-state-change
event therefore raised

    TypeError: _on_change() got an unexpected keyword argument 'zeroconf'

which the browser swallows, so `scan --lan` reported "No WiFi OpenDisplay
devices found" while the device was on the network advertising itself.
Reproduced 100% of the time with zeroconf 0.150.0.

The underscore prefix reads as the usual unused-argument convention, but these
names are part of the callback contract; only positional-only parameters are
safe to rename.

_FakeBrowser called the handler positionally, so the fake and the callback
shared the same wrong assumption and agreed with each other all the way to
green. Dispatch by keyword in the fake so it matches real zeroconf: reverting
the discovery_ip.py half now fails 9 tests instead of 0.

Verified on hardware (reTerminal E1003, ESP32-S3): `scan --lan` finds the
device first try.
@g4bri3lDev
g4bri3lDev merged commit 64a8b02 into OpenDisplay:main Jul 30, 2026
4 checks passed
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