Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ from harp.device import behavior, core

# Use "COMx" on Windows, "/dev/ttyUSBx" on Linux.
with serial.open_device(behavior, port="COM3") as device:
print(device.read(core.WhoAmI).parsed) # a common register
print(device.read(behavior.AnalogData).parsed) # a device register
print(device.read(core.WhoAmI).payload) # a common register
print(device.read(behavior.AnalogData).payload) # a device register
device.write(
core.OperationControl,
core.OperationControlPayload(operation_mode=core.OperationMode.ACTIVE),
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/create_device_module/create_device_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
# any `Device` over a transport. Passing the module itself validates the device
# identity on open, against its `WHO_AM_I`, which a value of `0` skips.
with serial.open_device(behavior, port=SERIAL_PORT) as device:
print("AnalogData:", device.read(AnalogData).parsed)
print("AnalogData:", device.read(AnalogData).payload)

# The same register classes also decode a recorded binary dump into a pandas
# DataFrame. See the "Reading Data into a DataFrame" example for more.
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/get_info/get_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
# check, so this works against any device. The connection closes on exit.
with serial.open_device(port=SERIAL_PORT) as device:
# Identify the device.
print("WhoAmI:", device.read(core.WhoAmI).parsed)
print("WhoAmI:", device.read(core.WhoAmI).payload)

# Dump every core register.
for address, register in sorted(core.REGISTER_MAP.items()):
reply = device.read(register)
print(f"{register.__name__:24s} (addr {address:2d}) = {reply.parsed}")
print(f"{register.__name__:24s} (addr {address:2d}) = {reply.payload}")
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

with serial.open_device(client.Device, port=SERIAL_PORT) as device:
# Read a scalar register.
print("WhoAmI:", device.read(core.WhoAmI).parsed)
print("WhoAmI:", device.read(core.WhoAmI).payload)

# Read a structured register and inspect a field.
control = device.read(core.OperationControl).parsed
control = device.read(core.OperationControl).payload
print("operation_mode before:", control.operation_mode)

# Write the register, then read it back to confirm the change. A struct payload
Expand All @@ -24,5 +24,5 @@
heartbeat=core.EnableFlag.DISABLED,
),
)
control = device.read(core.OperationControl).parsed
control = device.read(core.OperationControl).payload
print("operation_mode after:", control.operation_mode)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This example demonstrates how to react to messages pushed by the device, e.g. unsolicited `Event` messages, without polling, using two subscription styles:

- `device.subscribe(register, handler)`, where the handler receives a typed, parsed `ParsedHarpMessage` for a single register.
- `device.subscribe(register, handler)`, where the handler receives a `HarpMessage` typed by the payload of a single register.
- `device.subscribe_all(handler)`, a catch-all handler that receives the raw `HarpMessage` for every register.

Handlers run on a dedicated event thread, so they never block `read()` or `write()`. Both methods return a `Subscription`. Call `.unsubscribe()`, or use it as a context manager, to stop receiving events.
Expand Down
10 changes: 5 additions & 5 deletions docs/examples/subscribing_to_events/subscribing_to_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@

from harp import serial
from harp.device import client, core
from harp.protocol import HarpMessage, ParsedHarpMessage
from harp.protocol import HarpMessage

SERIAL_PORT = "/dev/ttyUSB0" # or "COMx" in Windows, where "x" is the serial port number


def print_timestamp(msg: ParsedHarpMessage[np.uint32]) -> None:
print(f"[timestamp] {msg.timestamp:.6f} {msg.parsed}")
def print_timestamp(msg: HarpMessage[np.uint32]) -> None:
print(f"[timestamp] {msg.timestamp:.6f} {msg.payload}")


def print_any_event(msg: HarpMessage) -> None:
register = core.REGISTER_MAP.get(msg.address, None)
value = register.parse(msg) if register is not None else msg.payload.hex()
value = register.parse(msg) if register is not None else msg.payload_bytes.hex()
print(f"[{msg.address}] {msg.timestamp:.6f} {msg.message_type.name:<5s} {value}")


with serial.open_device(client.Device, port=SERIAL_PORT) as device:
# Subscribe to a single, typed register: the handler receives a parsed payload.
# Subscribe to a single register: the handler receives a message typed by its payload.
timestamp_subscription = device.subscribe(core.TimestampSeconds, print_timestamp)

# Subscribe to every register at once: the handler receives the raw message.
Expand Down
2 changes: 1 addition & 1 deletion src/packages/harp-device/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A `Device` operates over a transport. `read` and `write` take a register class:
from harp.device import core

# `device` is a Device opened over some transport, see harp-serial
who = device.read(core.WhoAmI).parsed # -> np.uint16
who = device.read(core.WhoAmI).payload # -> np.uint16
device.write(core.OperationControl, payload) # write a register
```

Expand Down
25 changes: 12 additions & 13 deletions src/packages/harp-device/src/harp/device/client/_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import threading

from harp.protocol import HarpMessage, MessageType
from harp.protocol._message import ParsedHarpMessage
from harp.protocol._register import RegisterBase

from harp.device.schema import DeviceModuleLike
Expand All @@ -23,8 +22,8 @@

_logger = logging.getLogger(__name__)

EventHandler = Callable[[ParsedHarpMessage[P]], None]
"""A callback receiving a typed, parsed event for a specific register."""
EventHandler = Callable[[HarpMessage[P]], None]
"""A callback receiving a message typed by the payload of a specific register."""

MessageTypeFilter = MessageType | Iterable[MessageType]
"""Message types a subscription reacts to, as a single type or an iterable."""
Expand Down Expand Up @@ -164,7 +163,7 @@ def _validate_whoami(self) -> None:
expected = module.WHO_AM_I
if expected == 0x0:
return
actual = int(self.read(WhoAmI).parsed)
actual = int(self.read(WhoAmI).payload)
if actual != expected:
raise RuntimeError(
f"WhoAmI mismatch: {module.DEVICE_NAME} expects 0x{expected:04x} "
Expand Down Expand Up @@ -204,12 +203,12 @@ def read(
*,
timestamp: float | None = None,
port: int = 255,
) -> ParsedHarpMessage[P]:
) -> HarpMessage[P]:
# Note: ty can't correctly infer the return type, and this is a known issue:
# https://github.com/astral-sh/ty/issues/623
frame = register.format(message_type=MessageType.Read, timestamp=timestamp, port=port)
msg = self._request(register.address, frame)
return ParsedHarpMessage.from_message(msg, register.parse(msg))
return msg.decode(register)

def write(
self,
Expand All @@ -218,12 +217,12 @@ def write(
*,
timestamp: float | None = None,
port: int = 255,
) -> ParsedHarpMessage[P]:
) -> HarpMessage[P]:
frame = register.format(
value, message_type=MessageType.Write, timestamp=timestamp, port=port
)
msg = self._request(register.address, frame)
return ParsedHarpMessage.from_message(msg, register.parse(msg))
return msg.decode(register)

# ------------------------------------------------------------------
# Events
Expand All @@ -236,8 +235,8 @@ def subscribe(
*,
message_types: MessageTypeFilter = MessageType.Event,
) -> Subscription:
"""Call ``handler`` with a typed, parsed :class:`ParsedHarpMessage` each
time the device emits a message for ``register``.
"""Call ``handler`` with a :class:`~harp.protocol.HarpMessage` typed by the
payload of ``register``, each time the device emits a message for it.

By default only unsolicited ``Event`` messages are delivered. Pass
``message_types`` (a :class:`MessageType` or an iterable of them) to also
Expand Down Expand Up @@ -310,14 +309,14 @@ def _deliver_event(self, msg: HarpMessage) -> None:
matching = [s for s in subs if msg.message_type in s._message_types]
if matching and register is not None:
try:
parsed = ParsedHarpMessage.from_message(msg, register.parse(msg))
typed = msg.decode(register)
except Exception:
_logger.exception(
"Failed to parse %r for address 0x%02x", msg.message_type, msg.address
)
else:
for sub in matching:
self._safe_call(sub._handler, parsed)
self._safe_call(sub._handler, typed)

for sub in catch_all:
if msg.message_type in sub._message_types:
Expand Down Expand Up @@ -372,7 +371,7 @@ def _request(self, address: int, frame: bytes) -> HarpMessage:
if msg.has_error and self.raise_on_error:
raise RuntimeError(
f"Device returned error for register address {address} "
f"(0x{address:02x}). Payload: {msg.payload.hex()}"
f"(0x{address:02x}). Payload: {msg.payload_bytes.hex()}"
)
return msg
except queue.Empty as exc:
Expand Down
2 changes: 1 addition & 1 deletion src/packages/harp-protocol/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ keywords = ['python', 'harp']
requires-python = ">=3.11"
dependencies = [
"numpy>=1.24",
"typing-extensions>=4.0",
"typing-extensions>=4.14",
]

[build-system]
Expand Down
3 changes: 1 addition & 2 deletions src/packages/harp-protocol/src/harp/protocol/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ._message import HarpMessage, HarpParseError, ParsedHarpMessage
from ._message import HarpMessage, HarpParseError
from ._message_type import MessageType
from ._payload_converters import (
BoolConverter,
Expand Down Expand Up @@ -74,7 +74,6 @@
"encode_payload_type",
# Message
"HarpMessage",
"ParsedHarpMessage",
"HarpParseError",
# Converters
"Converter",
Expand Down
Loading