Skip to content

Parameterize HarpMessage by its payload type - #30

Merged
glopesdev merged 3 commits into
mainfrom
parameterize-harp-message
Aug 19, 2026
Merged

Parameterize HarpMessage by its payload type#30
glopesdev merged 3 commits into
mainfrom
parameterize-harp-message

Conversation

@glopesdev

@glopesdev glopesdev commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

HarpMessage is now generic over its payload type and ParsedHarpMessage is retired. A frame off the wire is a HarpMessage[Any], and decoding it with a register yields a HarpMessage[P], so read, write and subscribe all deliver one type carrying the frame and the decoded value together. That is what keeps the timestamp, the message type and the error flag reachable from whatever a read returns, instead of having to be recovered separately.

payload names the typed contract

payload is now the decoded value and payload_bytes is the frame bytes, inverting the previous split where payload was the bytes and parsed was the value.

Two things support reading payload as the typed contract rather than the encoding. Bonsai.Harp already uses it that way through its GetPayload() family, and Harp's own schema uses payloadSpec and payloadMember to describe a structured contract rather than bytes. parsed named how the value was obtained rather than what it is. payload_bytes also pairs with the existing bytes property, whole frame against the payload portion of it, which the alternative did not.

One generic class rather than a subclass

Python generics are erased, so HarpMessage[Any] expresses the untyped case on the same class and no second type is needed. A subclass would have needed a name for something with no role in the protocol, since a typed message is not specifically a reply and not specifically an event.

The cost this accepts is that HarpMessage[Any].payload is Any, so an unsafe access on the raw path from parse or subscribe_all is not caught statically. Reading payload before a register has decoded a message raises rather than falling back to the bytes, and has_payload reports which state a message is in, so an explicit check is available where the static type cannot help.

Decoding is bound to the message it came from

The new msg.decode(register) method is the only way to attach a typed payload. It derives the payload from the frame in the same call, so the two cannot disagree. Decoding also checks what parse does not. A U32 frame read through a U16 register, and a four-byte payload read through a scalar register, both raise instead of silently returning the leading bytes. The address is deliberately not checked, since the payload type and byte count decide whether these bytes can be read as this payload at all, while the address says which register the device meant. That keeps a frame decodable by any register describing the same layout.

This also closes a hole that was open in two places. ParsedHarpMessage could be constructed with any parsed value, and ParsedHarpMessage.from_message(msg, parsed) would wrap any message with any value, so nothing checked that a payload came from the bytes beside it.

Note

typing-extensions rises to >=4.14 since Sentinel arrives in 4.14 and three modules on main already import it.

HarpMessage is now generic over the type of its payload, and the
ParsedHarpMessage subclass is removed. A message read from the wire is a
HarpMessage[Any] and decoding it with a register yields a
HarpMessage[P], so read, write and subscribe all deliver one type
carrying both the frame and the decoded value.

payload is now that decoded value, replacing parsed, and the byte view
it displaces becomes raw_payload. Reading payload before a register has
decoded the message raises rather than falling back to the bytes, and
has_payload reports which state a message is in. with_payload attaches a
decoded value to a copy, so decoding never mutates a frame the dispatch
loop has already handed elsewhere.
@glopesdev
glopesdev requested a review from bruno-f-cruz August 19, 2026 09:44
@glopesdev glopesdev added the feature New planned feature label Aug 19, 2026
Comment thread src/packages/harp-protocol/src/harp/protocol/_message.py Outdated
Comment thread src/packages/harp-protocol/src/harp/protocol/_message.py Outdated
Comment thread src/packages/harp-protocol/src/harp/protocol/_message.py Outdated
The undecoded payload marker is a typing_extensions Sentinel rather
than a bare object, so _payload is typed as P | _UNDECODED and
narrows on the identity check instead of being hidden behind Any.
payload_bytes replaces raw_payload, pairing with the existing bytes
property that returns the whole frame.

typing-extensions rises to 4.14, the version that introduces
Sentinel, which three modules already import under the previous
lower bound.
HarpMessage.decode takes anything satisfying the new PayloadDecoder
protocol, which every register does, and returns a copy carrying the
decoded payload. The payload is derived from the frame in the same
call, so the two cannot disagree.

Decoding checks the payload type and the byte count, which parse does
not, so a U32 frame read through a U16 register and a 4-byte payload
read through a scalar register both raise instead of returning the
leading bytes. The address is not checked, so a frame can be decoded
by any register describing the same layout. The protocol is
structural, keeping registers out of the message imports.
@bruno-f-cruz
bruno-f-cruz self-requested a review August 19, 2026 20:19
@glopesdev
glopesdev merged commit 5b8e437 into main Aug 19, 2026
13 checks passed
@glopesdev
glopesdev deleted the parameterize-harp-message branch August 19, 2026 23:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New planned feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants