feat: host-side helpers so callers stop reimplementing the protocol - #158
Closed
g4bri3lDev wants to merge 3 commits into
Closed
feat: host-side helpers so callers stop reimplementing the protocol#158g4bri3lDev wants to merge 3 commits into
g4bri3lDev wants to merge 3 commits into
Conversation
Hosts driving OpenDisplay devices have had to reimplement pieces of the protocol the library already knew: packing an LED colour, sizing an NDEF record, formatting a firmware version to match its release tag, parsing a stored encryption key, and working out when a sleeping device is reachable. Every reimplementation is somewhere a host can drift from the firmware. All additive; no existing signature changes meaning. - pack_led_color / unpack_led_color / ms_to_loop_delay_units / ms_to_inter_delay_units, plus LedFlashStep.from_rgb() and .rgb so callers can work in RGB and milliseconds instead of the packed wire encoding - build_nfc_payload(), the same payload write_nfc_* sends, so a record can be validated before a connection is spent on it - format_firmware_version(), the single definition of how a version maps to a release tag. A host that formats it differently from the tag it compares against shows a permanently pending update - supports_ble_ota_install(), deliberately narrower than "an asset exists": it answers whether a host should offer the install over a Bluetooth proxy, which nRF Legacy DFU cannot survive - parse_encryption_key(), accepting a key however it was pasted, raising a typed InvalidEncryptionKeyError - BinaryInputs.enabled_button_ids and DisplayConfig.canvas_size() - SleepModel: the post-wake window and whether a device has gone dark again, with host-side slack passed in rather than assumed - prepare_image(compress=None) derives compression from the device config Internal call sites now use these too, so the library no longer keeps its own copies of the NFC payload assembly or the compression capability check. Verified against firmware upstream/main: the 10 s default wake window is DEFAULT_IDLE_HOLD_MS in src/main.h, and the sleep-entry condition is the one in platformIdle(). The window is re-armed by activity and held open by a button wake, so probably_asleep() is documented as a prediction that errs towards asleep rather than a fact.
`opendisplay info` always printed three version parts, so a device on the 1.6 release showed as 1.6.0 and never matched the tag it came from. It now formats through the library, so the CLI, a host's device registry and the GitHub tag all agree on what a device is running. The --json output is unchanged: it still reports separate major, minor and patch fields, which is a machine-readable contract rather than a rendering. --key now goes through the library's parser as well. It accepts everything it accepted before, including colon- and space-separated hex, and reports a malformed key with the same wording every other host will use.
OpenDisplayDevice took encryption_key as bytes. A hex string, which is how every host actually stores a key, was accepted silently and then failed inside authenticate() with "TypeError: key must be bytes-like" - after the connection had already been made. On a deep-sleeping device that cost a whole wake window to learn the key was the wrong type, and the error named neither the parameter nor the fix. encryption_key (and authenticate()) now take the 16 raw bytes or 32 hex characters, in any capitalization and with optional colon or space separators. The value is normalized and validated in the constructor, so a malformed key raises InvalidEncryptionKeyError before a single frame is sent and can never be mistaken for a device refusing a good key. parse_encryption_key() widens the same way, so a host holding either form can pass it straight through instead of branching. Raw bytes of the wrong length are now rejected too, rather than reaching AES and failing there.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hosts driving OpenDisplay devices have had to reimplement pieces of the protocol this library already knew: packing an LED colour, sizing an NDEF record, formatting a firmware version to match its release tag, parsing a stored encryption key, and working out when a sleeping device is reachable. Each reimplementation is somewhere a host can drift from the firmware — and the library had drifted from itself, which is what prompted two of these commits.
New API
All additive; no existing signature changes meaning.
pack_led_color/unpack_led_color/ms_to_loop_delay_units/ms_to_inter_delay_units, plusLedFlashStep.from_rgb()and.rgbbuild_nfc_payload()— the same payloadwrite_nfc_*sends, so a record can be validated before a connection is spent on itformat_firmware_version()— the single definition of how a version maps to a release tagsupports_ble_ota_install()— deliberately narrower than "an asset exists": it answers whether a host should offer the install over a Bluetooth proxy, which nRF Legacy DFU cannot surviveparse_encryption_key()and a typedInvalidEncryptionKeyErrorBinaryInputs.enabled_button_ids,DisplayConfig.canvas_size(),DisplayConfig.supports_compressionSleepModel— the post-wake window and whether a device has gone dark again, with host-side slack passed in rather than assumedprepare_image(compress=None)derives compression from the device configTwo user-visible changes
opendisplay infoalways printed three version parts, showing1.6.0for a device whose release tag is1.6. It now formats through the library, so the CLI, a host's device registry and the tag all agree.--jsonkeeps its separate major/minor/patch fields, which is a machine-readable contract rather than a rendering.encryption_keyaccepts hex as well as raw bytes, and a malformed key now raisesInvalidEncryptionKeyErrorat construction instead ofTypeError: key must be bytes-likeinsideauthenticate()— after the connection had already been made. On a deep-sleeping device that cost a whole wake window to learn the key was the wrong type. Raw bytes of the wrong length are rejected up front too, rather than reaching AES and failing there.Internal call sites now use these
The library no longer keeps its own copies of the NFC payload assembly, the compression capability check, the firmware version format, or the key parser.
cli.py's_parse_hex_keyis gone; it accepted colon- and space-separated hex, soparse_encryption_keyaccepts those too and nothing regresses.Firmware verification
The 10 s default wake window and the sleep-entry condition were checked against firmware
upstream/mainrather than taken from comments:DEFAULT_IDLE_HOLD_MSinsrc/main.h, and the gate inplatformIdle()(src/main.cpp). That also surfaced a nuance worth encoding — the window is measured from the last activity and re-armed by it, and a button wake holds it open formin_wake_time_secondsregardless. Soprobably_asleep()is documented as a prediction that errs towards asleep, not a fact.Verification
uv run pytest— 1150 passed (was 1071).uv run prek run --all-files— ruff, ruff-format, mypy strict and pylint all pass, idempotent on re-run.