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
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,17 @@ everything = reader.read_all() # {register_name: DataFrame}
```

Both paths are driven by a device schema. If you have only a `device.yml` and no
pre-generated package, `create_device` compiles it into a typed `Device` at runtime —
no code-generation step — which is exactly what `create_dataset_reader` does under
the hood:
pre-generated package, `create_device_module` compiles it into a module of register classes
at runtime — no code-generation step — which is exactly what `create_dataset_reader`
does under the hood:

```python
from pathlib import Path
from harp.device import create_device
from harp.device import create_device_module

Behavior = create_device(Path("device.yml").read_text())
AnalogData = Behavior.REGISTER_MAP[44] # registers are reached by address
behavior = create_device_module(Path("device.yml").read_text())
AnalogData = behavior.AnalogData # registers are reached by name...
assert behavior.REGISTER_MAP[44] is AnalogData # ...or by address
```

See the [Examples](https://harp-tech.org/pyharp/examples/) for the full walkthroughs,
Expand Down
2 changes: 1 addition & 1 deletion docs/api/device.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
---

::: harp.device.Device
::: harp.device.create_device
::: harp.device.create_device_module
::: harp.device.parse_device_schema
::: harp.device.ConverterContext
::: harp.device.HarpFramer
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
# Generating a Device from a Schema
# Generating Registers from a Schema

This example demonstrates how to turn a Harp `device.yml` into a typed
[`Device`](../../api/device.md) at runtime with `create_device`, without a
code-generation step. This is the quickest way to get started when you have only a
device's schema and no pre-generated package for it.
This example demonstrates how to turn a Harp `device.yml` into a module of register
classes at runtime with `create_device_module`, without a code-generation step. This is the
quickest way to get started when you have only a device's schema and no
pre-generated package for it.

The compiled device exposes its registers through `REGISTER_MAP` (keyed by address)
and carries the device's `__whoami__` identity. From there it works exactly like a
pre-generated device class — drive it over a transport to talk to hardware, or use
its register classes to decode recorded data.
A generated device package is a module: register classes at module level, with a
`REGISTER_MAP` beside them keyed by address. `create_device_module` builds that same shape
from a schema, so registers are reached the same way either way — by name
(`behavior.AnalogData`) or by address (`behavior.REGISTER_MAP[44]`). From there they
work exactly like a pre-generated package's: drive them over a transport with
[`Device`](../../api/device.md) to talk to hardware, or use them to decode recorded
data.

## When to use runtime generation

`create_device` trades statically generated device packages for schema-driven
`create_device_module` trades statically generated device packages for schema-driven
convenience. It's worth understanding what that buys you and what it costs.

**You gain:**

- **No build step.** A `device.yml` — even one you just pulled off a device —
becomes a working device in a single call. There's nothing to generate, install,
becomes a working module in a single call. There's nothing to generate, install,
or keep in sync with the schema.
- **Coverage for any device.** You don't need a published package for the device;
unreleased, custom, or one-off schemas work immediately.
Expand All @@ -27,9 +30,10 @@ convenience. It's worth understanding what that buys you and what it costs.

**You give up:**

- **Named, typed access.** Registers are reached by address (`REGISTER_MAP[44]`),
not as importable, autocompleting classes (`from harp_behavior import AnalogData`).
You lose editor discovery and static type checking of register names.
- **Static typing and autocomplete.** The names exist only once the module is built,
so an editor can't offer them and a type checker can't verify them. A generated
package is a real module on disk, so both work. The module also isn't in
`sys.modules`, so you bind it yourself rather than `import`-ing it.
- **Generator naming conventions.** Identifiers are kept verbatim from the yml
(`AnalogInput0`, `DIO0`) rather than the C# generator's snake_case fields and
`UPPER_SNAKE` enum members, so code written against a generated package won't line
Expand All @@ -40,14 +44,14 @@ convenience. It's worth understanding what that buys you and what it costs.
For shipped, widely-used devices a pre-generated package from the
[Harp C# generator](https://github.com/harp-tech/generators) remains the
authoritative choice — better editor support and static typing. Reach for
`create_device` when you want to go from a schema to working code with no
`create_device_module` when you want to go from a schema to working code with no
generation step.

!!! warning
Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port.

<!--codeinclude-->
```python
[](./create_device.py)
[](./create_device_module.py)
```
<!--/codeinclude-->
Original file line number Diff line number Diff line change
@@ -1,38 +1,47 @@
from pathlib import Path

from harp.data import parse_to_dataframe
from harp.device import create_device
from harp.device import Device, create_device_module
from harp.serial import open_serial_device

SERIAL_PORT = "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port)

# `create_device` compiles a Harp `device.yml` into a typed `Device` subclass at
# `create_device_module` compiles a Harp `device.yml` into a module of register classes at
# runtime — no code-generation step. This is the quickest way to work with a device
# when you don't have a pre-generated package for it: point it at the schema and you
# get the device's registers (keyed by address) plus its identity.
Behavior = create_device(Path("device.yml").read_text())

print("WhoAmI:", Behavior.__whoami__) # device identity, taken from the schema
AnalogData = Behavior.REGISTER_MAP[44] # registers are reached by address

# The generated device behaves like any other `Device` class. Talk to hardware over
# a transport — `read`/`write` take a register class:
with open_serial_device(Behavior, port=SERIAL_PORT) as device:
# get the same shape a generated package has, registers at module level beside a
# `REGISTER_MAP`.
behavior = create_device_module(Path("device.yml").read_text())

print("WhoAmI:", behavior.WHO_AM_I) # device identity, taken from the schema
AnalogData = behavior.AnalogData # registers are reached by name...
assert behavior.REGISTER_MAP[44] is AnalogData # ...or by address

# Registers are ordinary register classes, so they drive `read`/`write` on any
# `Device` over a transport. The schema carries no Python code, so there is no
# generated device class here: use `Device` itself.
with open_serial_device(Device, port=SERIAL_PORT) as device:
print("AnalogData:", device.read(AnalogData).parsed)

# ...or use the same register classes to decode a recorded binary dump into a
# pandas DataFrame (see the "Reading Data into a DataFrame" example for more):
df = parse_to_dataframe(AnalogData, "Behavior_44.bin")
print(df.head())

# To have the identity checked on connect, subclass `Device` with the schema's
# WhoAmI — the same one-liner a generated package ships:
#
# class Behavior(Device):
# __whoami__ = behavior.WHO_AM_I


# --- Custom interface types --------------------------------------------------
# A register with a custom `interfaceType` needs a converter so its field decodes
# to the right Python type. Pass it via `converters=`, keyed by "<Name>Converter":
#
# Behavior = create_device(yml_text, converters={"DataConverter": DataConverter()})
# behavior = create_device_module(yml_text, converters={"DataConverter": DataConverter()})
#
# An unresolved custom type raises `UnknownConverterError`; pass `strict=False` to
# decode it natively instead. `exclude_private=True` (the default) drops registers
# marked `private` in the schema. If you only want the parsed schema model rather
# than a device, `parse_device_schema(yml_text)` returns that directly.
# than a module, `parse_device_schema(yml_text)` returns that directly.
2 changes: 1 addition & 1 deletion docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This section contains some examples to help you get started with `harp`.

Working from a device schema:

- [Generating a Device from a Schema](./create_device/create_device.md) - compile a `device.yml` into a typed device at runtime with `create_device`.
- [Generating Registers from a Schema](./create_device_module/create_device_module.md) - compile a `device.yml` into a module of register classes at runtime with `create_device_module`.

Talking to a device:

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
from harp.device import Device, OperationControl, OperationControlPayload, OperationMode, WhoAmI
from harp.device import (
Device,
EnableFlag,
OperationControl,
OperationControlPayload,
OperationMode,
WhoAmI,
)
from harp.serial import open_serial_device

SERIAL_PORT = "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port)
Expand All @@ -11,7 +18,18 @@
control = device.read(OperationControl).parsed
print("operation_mode before:", control.operation_mode)

# Write the register, then read it back to confirm the change.
device.write(OperationControl, OperationControlPayload(operation_mode=OperationMode.ACTIVE))
# Write the register, then read it back to confirm the change. A struct payload
# is built whole, so every field is given a value.
device.write(
OperationControl,
OperationControlPayload(
operation_mode=OperationMode.ACTIVE,
dump_registers=False,
mute_replies=False,
visual_indicators=EnableFlag.ENABLED,
operation_led=EnableFlag.ENABLED,
heartbeat=EnableFlag.DISABLED,
),
)
control = device.read(OperationControl).parsed
print("operation_mode after:", control.operation_mode)
16 changes: 8 additions & 8 deletions docs/examples/read_dataset/read_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
# ┗ 📜 device.yml
#
# `create_dataset_reader` does the right thing: it finds `device.yml` inside the
# folder, builds the device that knows how to decode each register, and hands back
# a reader ready to go.
# folder, builds the module of register classes that knows how to decode each
# register, and hands back a reader ready to go.
reader = create_dataset_reader("session.harp")

# Read one register into a DataFrame — by register class (any register in the
Expand All @@ -34,13 +34,13 @@
absolute = reader.read(44, epoch=REFERENCE_EPOCH)
print(absolute.index[:3])

# --- Already have a device class? -------------------------------------------
# A pre-generated device package, or one you built yourself with `create_device`,
# can drive the reader directly — construct `DatasetReader(Device, folder)`:
# --- Already have a device module? -------------------------------------------
# A pre-generated device package, or one you built yourself with `create_device_module`,
# can drive the reader directly — construct `DatasetReader(module, folder)`:
#
# from harp.data import DatasetReader
# from harp.device import create_device
# from harp.device import create_device_module
# from pathlib import Path
#
# Behavior = create_device((Path("session.harp") / "device.yml").read_text())
# reader = DatasetReader(Behavior, "session.harp")
# behavior = create_device_module((Path("session.harp") / "device.yml").read_text())
# reader = DatasetReader(behavior, "session.harp")
10 changes: 6 additions & 4 deletions docs/examples/subscribing_to_events/subscribing_to_events.py
Comment thread
glopesdev marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import numpy as np
from harp.device import (
REGISTER_MAP,
Device,
EnableFlag,
OperationControl,
OperationControlPayload,
OperationMode,
Expand All @@ -12,7 +14,7 @@
SERIAL_PORT = "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port)


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


Expand All @@ -34,10 +36,10 @@ def print_any_event(msg: HarpMessage) -> None:
OperationControlPayload(
operation_mode=OperationMode.ACTIVE,
dump_registers=True,
heartbeat=True,
heartbeat=EnableFlag.ENABLED,
mute_replies=False,
operation_led=True,
visual_indicators=True,
operation_led=EnableFlag.ENABLED,
visual_indicators=EnableFlag.ENABLED,
),
)

Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ nav:
- Home: index.md
- Examples:
- examples/index.md
- Generating a Device from a Schema: examples/create_device/create_device.md
- Generating Registers from a Schema: examples/create_device_module/create_device_module.md
- Getting Device Info: examples/get_info/get_info.md
- Read and Write from Registers: examples/read_and_write_from_registers/read_and_write_from_registers.md
- Subscribing to Events: examples/subscribing_to_events/subscribing_to_events.md
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,14 @@ include = [
"src/packages/harp-device/src",
"src/packages/harp-serial/src",
"src/packages/harp-data/src",
"docs/examples",
"tests/conformance.py",
]
exclude = [
"**/node_modules",
"**/__pycache__",
"**/.*",
".venv",
"tests",
]
venvPath = "."
venv = ".venv"
Expand Down
10 changes: 5 additions & 5 deletions src/packages/harp-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ df = reader.read(44) # by address
everything = reader.read_all() # {register_name: DataFrame}
```

Already have a device class (e.g. a pre-generated package, or one built with
`create_device`)? Drive `DatasetReader` with it directly:
Already have a device module (e.g. a pre-generated package, or one built with
`create_device_module`)? Drive `DatasetReader` with it directly:

```python
from pathlib import Path
from harp.data import DatasetReader
from harp.device import create_device
from harp.device import create_device_module

Behavior = create_device((Path("session.harp") / "device.yml").read_text())
reader = DatasetReader(Behavior, "session.harp")
behavior = create_device_module((Path("session.harp") / "device.yml").read_text())
reader = DatasetReader(behavior, "session.harp")
```

Timestamps are auto-detected per register and placed on the DataFrame index
Expand Down
Loading