From 6eb88515a4c2d99b8ed9fc7ee55b40e6a53e6023 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 18 Aug 2026 11:35:38 +0100 Subject: [PATCH 1/2] Generate the core register set harp.device.core is now the generated Python interface for the core register metadata, installed verbatim as the package __init__.py, replacing a stale copy of that output and the two modules that wrapped it. The module docstring is generated from a description field in the metadata. The register set, enum members, masks and docstrings are unchanged, so the module still publishes the same 24 names and the same 15 register address map. Runtime emission is verified against the package itself and against core.yml vendored from the generator, so the expected_core copy and the common.yml fixture are both removed, and register layout is now compared for every core register. Requires the matching Python target change in harp-tech/generators. --- .../src/harp/device/core/__init__.py | 293 +++++++++++++++--- .../src/harp/device/core/_register_map.py | 48 --- .../src/harp/device/core/_registers.py | 210 ------------- tests/assets/{common.yml => core.yml} | 131 +++++--- tests/conftest.py | 6 +- tests/device/expected_core.py | 276 ----------------- tests/device/test_emit.py | 16 +- tests/device/test_schema.py | 31 +- 8 files changed, 375 insertions(+), 636 deletions(-) delete mode 100644 src/packages/harp-device/src/harp/device/core/_register_map.py delete mode 100644 src/packages/harp-device/src/harp/device/core/_registers.py rename tests/assets/{common.yml => core.yml} (56%) delete mode 100644 tests/device/expected_core.py diff --git a/src/packages/harp-device/src/harp/device/core/__init__.py b/src/packages/harp-device/src/harp/device/core/__init__.py index 7e3dee2..a0b4ab3 100644 --- a/src/packages/harp-device/src/harp/device/core/__init__.py +++ b/src/packages/harp-device/src/harp/device/core/__init__.py @@ -1,34 +1,37 @@ +# This file was automatically generated and should not be edited directly. +# To make changes, edit the device metadata and regenerate the interface. + """The core register set every Harp device carries, and its address space.""" -from ._register_map import REGISTER_MAP -from ._registers import ( - AssemblyVersion, - ClockConfiguration, - ClockConfigurationFlags, - ClockConfigurationPayload, - CoreVersionHigh, - CoreVersionLow, - DeviceName, - DeviceNamePayload, - EnableFlag, - FirmwareVersionHigh, - FirmwareVersionLow, - HardwareVersionHigh, - HardwareVersionLow, - OperationControl, - OperationControlPayload, - OperationMode, - ResetDevice, - ResetDevicePayload, - ResetFlags, - SerialNumber, - TimestampMicroseconds, - TimestampSeconds, - WhoAmI, +import enum +from typing import Any, ClassVar + +import numpy as np +from harp.protocol import ( + AnonymousPayload, + BitMask, + BoolConverter, + Field, + GroupMask, + PayloadType, + RegisterBase, + RegisterU16, + RegisterU32, + RegisterU8, + StringConverter, + StructPayload, ) + __all__ = [ - "REGISTER_MAP", + "ResetFlags", + "ClockConfigurationFlags", + "OperationMode", + "EnableFlag", + "OperationControlPayload", + "ResetDevicePayload", + "DeviceNamePayload", + "ClockConfigurationPayload", "WhoAmI", "HardwareVersionHigh", "HardwareVersionLow", @@ -40,16 +43,236 @@ "TimestampSeconds", "TimestampMicroseconds", "OperationControl", - "OperationControlPayload", - "OperationMode", "ResetDevice", - "ResetDevicePayload", - "ResetFlags", "DeviceName", - "DeviceNamePayload", - "EnableFlag", - "ClockConfiguration", - "ClockConfigurationFlags", - "ClockConfigurationPayload", "SerialNumber", + "ClockConfiguration", + "REGISTER_MAP", ] + + +class ResetFlags(enum.IntFlag): + """Specifies the behavior of the non-volatile registers when resetting the device.""" + + RESTORE_DEFAULT = 0x1 + """The device will boot with all the registers reset to their default factory values.""" + + RESTORE_EEPROM = 0x2 + """The device will boot and restore all the registers to the values stored in non-volatile memory.""" + + SAVE = 0x4 + """The device will boot and save all the current register values to non-volatile memory.""" + + RESTORE_NAME = 0x8 + """The device will boot with the default device name.""" + + UPDATE_FIRMWARE = 0x20 + """The device will enter firmware update mode.""" + + BOOT_FROM_DEFAULT = 0x40 + """Specifies that the device has booted from default factory values.""" + + BOOT_FROM_EEPROM = 0x80 + """Specifies that the device has booted from non-volatile values stored in EEPROM.""" + + +class ClockConfigurationFlags(enum.IntFlag): + """Specifies configuration flags for the device synchronization clock.""" + + CLOCK_REPEATER = 0x1 + """The device will repeat the clock synchronization signal to the clock output connector, if available.""" + + CLOCK_GENERATOR = 0x2 + """The device resets and generates the clock synchronization signal on the clock output connector, if available.""" + + REPEATER_CAPABILITY = 0x8 + """Specifies the device has the capability to repeat the clock synchronization signal to the clock output connector.""" + + GENERATOR_CAPABILITY = 0x10 + """Specifies the device has the capability to generate the clock synchronization signal to the clock output connector.""" + + CLOCK_UNLOCK = 0x40 + """The device will unlock the timestamp register counter and will accept commands to set new timestamp values.""" + + CLOCK_LOCK = 0x80 + """The device will lock the timestamp register counter and will not accept commands to set new timestamp values.""" + + +class OperationMode(enum.IntEnum): + """Specifies the operation mode of the device.""" + + STANDBY = 0 + """Disable all event reporting on the device.""" + + ACTIVE = 1 + """Event detection is enabled. Only enabled events are reported by the device.""" + + SPEED = 3 + """The device enters speed mode.""" + + +class EnableFlag(enum.IntEnum): + """Specifies whether a specific register flag is enabled or disabled.""" + + DISABLED = 0 + """Specifies that the flag is disabled.""" + + ENABLED = 1 + """Specifies that the flag is enabled.""" + + +class OperationControlPayload(StructPayload[np.uint8]): + """Represents the payload of the OperationControl register.""" + + operation_mode: OperationMode = GroupMask(enum=OperationMode, mask=0x3) + """Specifies the operation mode of the device.""" + + dump_registers: bool = Field(BoolConverter(), mask=0x8) + """Specifies whether the device should report the content of all registers on initialization.""" + + mute_replies: bool = Field(BoolConverter(), mask=0x10) + """Specifies whether the replies to all commands will be muted, i.e. not sent by the device.""" + + visual_indicators: EnableFlag = GroupMask(enum=EnableFlag, mask=0x20) + """Specifies the state of all visual indicators on the device.""" + + operation_led: EnableFlag = GroupMask(enum=EnableFlag, mask=0x40) + """Specifies whether the device state LED should report the operation mode of the device.""" + + heartbeat: EnableFlag = GroupMask(enum=EnableFlag, mask=0x80) + """Specifies whether the device should report the content of the seconds register each second.""" + + +class ResetDevicePayload(AnonymousPayload[np.uint8]): + """Represents the payload of the ResetDevice register.""" + + __value__: ResetFlags = BitMask(enum=ResetFlags) + + +class DeviceNamePayload(AnonymousPayload[np.uint8]): + """Represents the payload of the DeviceName register.""" + + __value__: str = Field(StringConverter(25)) + + +class ClockConfigurationPayload(AnonymousPayload[np.uint8]): + """Represents the payload of the ClockConfiguration register.""" + + __value__: ClockConfigurationFlags = BitMask(enum=ClockConfigurationFlags) + + +class WhoAmI(RegisterU16): + """Specifies the identity class of the device.""" + + address: ClassVar[int] = 0 + + +class HardwareVersionHigh(RegisterU8): + """Specifies the major hardware version of the device.""" + + address: ClassVar[int] = 1 + + +class HardwareVersionLow(RegisterU8): + """Specifies the minor hardware version of the device.""" + + address: ClassVar[int] = 2 + + +class AssemblyVersion(RegisterU8): + """Specifies the version of the assembled components in the device.""" + + address: ClassVar[int] = 3 + + +class CoreVersionHigh(RegisterU8): + """Specifies the major version of the Harp core implemented by the device.""" + + address: ClassVar[int] = 4 + + +class CoreVersionLow(RegisterU8): + """Specifies the minor version of the Harp core implemented by the device.""" + + address: ClassVar[int] = 5 + + +class FirmwareVersionHigh(RegisterU8): + """Specifies the major version of the Harp core implemented by the device.""" + + address: ClassVar[int] = 6 + + +class FirmwareVersionLow(RegisterU8): + """Specifies the minor version of the Harp core implemented by the device.""" + + address: ClassVar[int] = 7 + + +class TimestampSeconds(RegisterU32): + """Stores the integral part of the system timestamp, in seconds.""" + + address: ClassVar[int] = 8 + + +class TimestampMicroseconds(RegisterU16): + """Stores the fractional part of the system timestamp, in microseconds.""" + + address: ClassVar[int] = 9 + + +class OperationControl(RegisterBase[OperationControlPayload]): + """Stores the configuration mode of the device.""" + + address: ClassVar[int] = 10 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = OperationControlPayload + + +class ResetDevice(RegisterBase[ResetFlags]): + """Resets the device and saves non-volatile registers.""" + + address: ClassVar[int] = 11 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = ResetDevicePayload + + +class DeviceName(RegisterBase[str]): + """Stores the user-specified device name.""" + + address: ClassVar[int] = 12 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = DeviceNamePayload + + +class SerialNumber(RegisterU16): + """Specifies the unique serial number of the device.""" + + address: ClassVar[int] = 13 + + +class ClockConfiguration(RegisterBase[ClockConfigurationFlags]): + """Specifies the configuration for the device synchronization clock.""" + + address: ClassVar[int] = 14 + payload_type: ClassVar[PayloadType] = PayloadType.U8 + payload_class = ClockConfigurationPayload + + +REGISTER_MAP: dict[int, type[RegisterBase[Any]]] = { + 0: WhoAmI, + 1: HardwareVersionHigh, + 2: HardwareVersionLow, + 3: AssemblyVersion, + 4: CoreVersionHigh, + 5: CoreVersionLow, + 6: FirmwareVersionHigh, + 7: FirmwareVersionLow, + 8: TimestampSeconds, + 9: TimestampMicroseconds, + 10: OperationControl, + 11: ResetDevice, + 12: DeviceName, + 13: SerialNumber, + 14: ClockConfiguration, +} diff --git a/src/packages/harp-device/src/harp/device/core/_register_map.py b/src/packages/harp-device/src/harp/device/core/_register_map.py deleted file mode 100644 index 560bd83..0000000 --- a/src/packages/harp-device/src/harp/device/core/_register_map.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Address to register-class map for the core Harp registers. - -Downstream device packages spread this into their own map:: - - from harp.device.core import REGISTER_MAP as _CORE_REGISTER_MAP - - REGISTER_MAP = {**_CORE_REGISTER_MAP, 32: DigitalInputState, ...} -""" - -from typing import Any - -from harp.protocol import RegisterBase - -from ._registers import ( - AssemblyVersion, - ClockConfiguration, - CoreVersionHigh, - CoreVersionLow, - DeviceName, - FirmwareVersionHigh, - FirmwareVersionLow, - HardwareVersionHigh, - HardwareVersionLow, - OperationControl, - ResetDevice, - SerialNumber, - TimestampMicroseconds, - TimestampSeconds, - WhoAmI, -) - -REGISTER_MAP: dict[int, type[RegisterBase[Any]]] = { - 0: WhoAmI, - 1: HardwareVersionHigh, - 2: HardwareVersionLow, - 3: AssemblyVersion, - 4: CoreVersionHigh, - 5: CoreVersionLow, - 6: FirmwareVersionHigh, - 7: FirmwareVersionLow, - 8: TimestampSeconds, - 9: TimestampMicroseconds, - 10: OperationControl, - 11: ResetDevice, - 12: DeviceName, - 13: SerialNumber, - 14: ClockConfiguration, -} diff --git a/src/packages/harp-device/src/harp/device/core/_registers.py b/src/packages/harp-device/src/harp/device/core/_registers.py deleted file mode 100644 index b489da6..0000000 --- a/src/packages/harp-device/src/harp/device/core/_registers.py +++ /dev/null @@ -1,210 +0,0 @@ -# This file was automatically generated and should not be edited directly. -# To make changes, edit the device metadata and regenerate the interface. - -import enum -from typing import ClassVar - -import numpy as np -from harp.protocol import ( - AnonymousPayload, - BitMask, - BoolConverter, - Field, - GroupMask, - PayloadType, - RegisterBase, - RegisterU16, - RegisterU32, - RegisterU8, - StringConverter, - StructPayload, -) - - -class ResetFlags(enum.IntFlag): - """Specifies the behavior of the non-volatile registers when resetting the device.""" - - RESTORE_DEFAULT = 0x1 - """The device will boot with all the registers reset to their default factory values.""" - RESTORE_EEPROM = 0x2 - """The device will boot and restore all the registers to the values stored in non-volatile memory.""" - SAVE = 0x4 - """The device will boot and save all the current register values to non-volatile memory.""" - RESTORE_NAME = 0x8 - """The device will boot with the default device name.""" - UPDATE_FIRMWARE = 0x20 - """The device will enter firmware update mode.""" - BOOT_FROM_DEFAULT = 0x40 - """Specifies that the device has booted from default factory values.""" - BOOT_FROM_EEPROM = 0x80 - """Specifies that the device has booted from non-volatile values stored in EEPROM.""" - - -class ClockConfigurationFlags(enum.IntFlag): - """Specifies configuration flags for the device synchronization clock.""" - - CLOCK_REPEATER = 0x1 - """The device will repeat the clock synchronization signal to the clock output connector, if available.""" - CLOCK_GENERATOR = 0x2 - """The device resets and generates the clock synchronization signal on the clock output connector, if available.""" - REPEATER_CAPABILITY = 0x8 - """Specifies the device has the capability to repeat the clock synchronization signal to the clock output connector.""" - GENERATOR_CAPABILITY = 0x10 - """Specifies the device has the capability to generate the clock synchronization signal to the clock output connector.""" - CLOCK_UNLOCK = 0x40 - """The device will unlock the timestamp register counter and will accept commands to set new timestamp values.""" - CLOCK_LOCK = 0x80 - """The device will lock the timestamp register counter and will not accept commands to set new timestamp values.""" - - -class OperationMode(enum.IntEnum): - """Specifies the operation mode of the device.""" - - STANDBY = 0 - """Disable all event reporting on the device.""" - ACTIVE = 1 - """Event detection is enabled. Only enabled events are reported by the device.""" - SPEED = 3 - """The device enters speed mode.""" - - -class EnableFlag(enum.IntEnum): - """Specifies whether a specific register flag is enabled or disabled.""" - - DISABLED = 0 - """Specifies that the flag is disabled.""" - ENABLED = 1 - """Specifies that the flag is enabled.""" - - -class OperationControlPayload(StructPayload[np.uint8]): - """Represents the payload of the OperationControl register.""" - - operation_mode: OperationMode = GroupMask(enum=OperationMode, mask=0x3) - """Specifies the operation mode of the device.""" - dump_registers: bool = Field(BoolConverter(), mask=0x8) - """Specifies whether the device should report the content of all registers on initialization.""" - mute_replies: bool = Field(BoolConverter(), mask=0x10) - """Specifies whether the replies to all commands will be muted, i.e. not sent by the device.""" - visual_indicators: EnableFlag = GroupMask(enum=EnableFlag, mask=0x20) - """Specifies the state of all visual indicators on the device.""" - operation_led: EnableFlag = GroupMask(enum=EnableFlag, mask=0x40) - """Specifies whether the device state LED should report the operation mode of the device.""" - heartbeat: EnableFlag = GroupMask(enum=EnableFlag, mask=0x80) - """Specifies whether the device should report the content of the seconds register each second.""" - - -class ResetDevicePayload(AnonymousPayload[np.uint8]): - """Represents the payload of the ResetDevice register.""" - - __value__: ResetFlags = BitMask(enum=ResetFlags) - - -class DeviceNamePayload(AnonymousPayload[np.uint8]): - """Represents the payload of the DeviceName register.""" - - __value__: str = Field(StringConverter(25)) - - -class ClockConfigurationPayload(AnonymousPayload[np.uint8]): - """Represents the payload of the ClockConfiguration register.""" - - __value__: ClockConfigurationFlags = BitMask(enum=ClockConfigurationFlags) - - -class WhoAmI(RegisterU16): - """Specifies the identity class of the device.""" - - address: ClassVar[int] = 0 - - -class HardwareVersionHigh(RegisterU8): - """Specifies the major hardware version of the device.""" - - address: ClassVar[int] = 1 - - -class HardwareVersionLow(RegisterU8): - """Specifies the minor hardware version of the device.""" - - address: ClassVar[int] = 2 - - -class AssemblyVersion(RegisterU8): - """Specifies the version of the assembled components in the device.""" - - address: ClassVar[int] = 3 - - -class CoreVersionHigh(RegisterU8): - """Specifies the major version of the Harp core implemented by the device.""" - - address: ClassVar[int] = 4 - - -class CoreVersionLow(RegisterU8): - """Specifies the minor version of the Harp core implemented by the device.""" - - address: ClassVar[int] = 5 - - -class FirmwareVersionHigh(RegisterU8): - """Specifies the major version of the Harp core implemented by the device.""" - - address: ClassVar[int] = 6 - - -class FirmwareVersionLow(RegisterU8): - """Specifies the minor version of the Harp core implemented by the device.""" - - address: ClassVar[int] = 7 - - -class TimestampSeconds(RegisterU32): - """Stores the integral part of the system timestamp, in seconds.""" - - address: ClassVar[int] = 8 - - -class TimestampMicroseconds(RegisterU16): - """Stores the fractional part of the system timestamp, in microseconds.""" - - address: ClassVar[int] = 9 - - -class OperationControl(RegisterBase[OperationControlPayload]): - """Stores the configuration mode of the device.""" - - address: ClassVar[int] = 10 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = OperationControlPayload - - -class ResetDevice(RegisterBase[ResetFlags]): - """Resets the device and saves non-volatile registers.""" - - address: ClassVar[int] = 11 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = ResetDevicePayload - - -class DeviceName(RegisterBase[str]): - """Stores the user-specified device name.""" - - address: ClassVar[int] = 12 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = DeviceNamePayload - - -class SerialNumber(RegisterU16): - """Specifies the unique serial number of the device.""" - - address: ClassVar[int] = 13 - - -class ClockConfiguration(RegisterBase[ClockConfigurationFlags]): - """Specifies the configuration for the device synchronization clock.""" - - address: ClassVar[int] = 14 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = ClockConfigurationPayload diff --git a/tests/assets/common.yml b/tests/assets/core.yml similarity index 56% rename from tests/assets/common.yml rename to tests/assets/core.yml index f32a34f..2610747 100644 --- a/tests/assets/common.yml +++ b/tests/assets/core.yml @@ -1,4 +1,6 @@ -# yaml-language-server: $schema=registers.json +# yaml-language-server: $schema=https://harp-tech.org/draft-02/schema/registers.json +description: The core register set every Harp device carries, and its address space. +device: Tests registers: WhoAmI: address: 0 @@ -44,14 +46,14 @@ registers: address: 8 type: U32 access: [Read, Write, Event] - description: Stores the integral part of the system timestamp, in seconds. volatile: true + description: Stores the integral part of the system timestamp, in seconds. TimestampMicroseconds: address: 9 type: U16 access: Read - description: Stores the fractional part of the system timestamp, in microseconds. volatile: true + description: Stores the fractional part of the system timestamp, in microseconds. OperationControl: address: 10 type: U8 @@ -59,29 +61,29 @@ registers: description: Stores the configuration mode of the device. payloadSpec: OperationMode: - description: Specifies the operation mode of the device. - maskType: OperationMode mask: 0x3 + maskType: OperationMode + description: Specifies the operation mode of the device. DumpRegisters: - description: Specifies whether the device should report the content of all registers on initialization. - interfaceType: bool mask: 0x8 - MuteReplies: - description: Specifies whether the replies to all commands will be muted, i.e. not sent by the device. interfaceType: bool + description: Specifies whether the device should report the content of all registers on initialization. + MuteReplies: mask: 0x10 + interfaceType: bool + description: Specifies whether the replies to all commands will be muted, i.e. not sent by the device. VisualIndicators: - description: Specifies the state of all visual indicators on the device. - maskType: LedState mask: 0x20 + maskType: EnableFlag + description: Specifies the state of all visual indicators on the device. OperationLed: - description: Specifies whether the device state LED should report the operation mode of the device. - maskType: LedState mask: 0x40 - Heartbeat: - description: Specifies whether the device should report the content of the seconds register each second. maskType: EnableFlag + description: Specifies whether the device state LED should report the operation mode of the device. + Heartbeat: mask: 0x80 + maskType: EnableFlag + description: Specifies whether the device should report the content of the seconds register each second. ResetDevice: address: 11 type: U8 @@ -93,6 +95,7 @@ registers: type: U8 length: 25 access: Write + interfaceType: string description: Stores the user-specified device name. SerialNumber: address: 13 @@ -105,41 +108,77 @@ registers: access: Write maskType: ClockConfigurationFlags description: Specifies the configuration for the device synchronization clock. -groupMasks: - OperationMode: - description: Specifies the operation mode of the device. - values: - Standby: {value: 0, description: Disable all event reporting on the device.} - Active: {value: 1, description: Event detection is enabled. Only enabled events are reported by the device.} - Speed: {value: 3, description: The device enters speed mode.} - EnableFlag: - description: Specifies whether a specific register flag is enabled or disabled. - values: - Disabled: {value: 0, description: Specifies that the flag is disabled.} - Enabled: {value: 1, description: Specifies that the flag is enabled.} - LedState: - description: Specifies the state of an LED on the device. - values: - Off: {value: 0, description: Specifies that the LED is off.} - On: {value: 1, description: Specifies that the LED is on.} bitMasks: ResetFlags: description: Specifies the behavior of the non-volatile registers when resetting the device. bits: - None: {value: 0, description: All reset flags are cleared.} - RestoreDefault: {value: 0x1, description: The device will boot with all the registers reset to their default factory values.} - RestoreEeprom: {value: 0x2, description: The device will boot and restore all the registers to the values stored in non-volatile memory.} - Save: {value: 0x4, description: The device will boot and save all the current register values to non-volatile memory.} - RestoreName: {value: 0x8, description: The device will boot with the default device name.} - BootFromDefault: {value: 0x40, description: Specifies that the device has booted from default factory values.} - BootFromEeprom: {value: 0x80, description: Specifies that the device has booted from non-volatile values stored in EEPROM.} + None: + value: 0x0 + description: All reset flags are cleared. + RestoreDefault: + value: 0x1 + description: The device will boot with all the registers reset to their default factory values. + RestoreEeprom: + value: 0x2 + description: The device will boot and restore all the registers to the values stored in non-volatile memory. + Save: + value: 0x4 + description: The device will boot and save all the current register values to non-volatile memory. + RestoreName: + value: 0x8 + description: The device will boot with the default device name. + UpdateFirmware: + value: 0x20 + description: The device will enter firmware update mode. + BootFromDefault: + value: 0x40 + description: Specifies that the device has booted from default factory values. + BootFromEeprom: + value: 0x80 + description: Specifies that the device has booted from non-volatile values stored in EEPROM. ClockConfigurationFlags: description: Specifies configuration flags for the device synchronization clock. bits: - None: {value: 0, description: All clock configuration flags are cleared.} - ClockRepeater: {value: 0x1, description: "The device will repeat the clock synchronization signal to the clock output connector, if available."} - ClockGenerator: {value: 0x2, description: "The device resets and generates the clock synchronization signal on the clock output connector, if available."} - RepeaterCapability: {value: 0x8, description: Specifies the device has the capability to repeat the clock synchronization signal to the clock output connector.} - GeneratorCapability: {value: 0x10, description: Specifies the device has the capability to generate the clock synchronization signal to the clock output connector.} - ClockUnlock: {value: 0x40, description: The device will unlock the timestamp register counter and will accept commands to set new timestamp values.} - ClockLock: {value: 0x80, description: The device will lock the timestamp register counter and will not accept commands to set new timestamp values.} \ No newline at end of file + None: + value: 0x0 + description: All clock configuration flags are cleared. + ClockRepeater: + value: 0x1 + description: The device will repeat the clock synchronization signal to the clock output connector, if available. + ClockGenerator: + value: 0x2 + description: The device resets and generates the clock synchronization signal on the clock output connector, if available. + RepeaterCapability: + value: 0x8 + description: Specifies the device has the capability to repeat the clock synchronization signal to the clock output connector. + GeneratorCapability: + value: 0x10 + description: Specifies the device has the capability to generate the clock synchronization signal to the clock output connector. + ClockUnlock: + value: 0x40 + description: The device will unlock the timestamp register counter and will accept commands to set new timestamp values. + ClockLock: + value: 0x80 + description: The device will lock the timestamp register counter and will not accept commands to set new timestamp values. +groupMasks: + OperationMode: + description: Specifies the operation mode of the device. + values: + Standby: + value: 0x0 + description: Disable all event reporting on the device. + Active: + value: 0x1 + description: Event detection is enabled. Only enabled events are reported by the device. + Speed: + value: 0x3 + description: The device enters speed mode. + EnableFlag: + description: Specifies whether a specific register flag is enabled or disabled. + values: + Disabled: + value: 0x0 + description: Specifies that the flag is disabled. + Enabled: + value: 0x1 + description: Specifies that the flag is enabled. diff --git a/tests/conftest.py b/tests/conftest.py index ed31099..9a9f45b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,6 +22,6 @@ def device_yml() -> str: @pytest.fixture(scope="session") -def common_yml() -> str: - """The Harp common (core) register set ``common.yml`` as text.""" - return (ASSETS / "common.yml").read_text() +def core_yml() -> str: + """The generators core metadata ``core.yml`` as text.""" + return (ASSETS / "core.yml").read_text() diff --git a/tests/device/expected_core.py b/tests/device/expected_core.py deleted file mode 100644 index 39970f4..0000000 --- a/tests/device/expected_core.py +++ /dev/null @@ -1,276 +0,0 @@ -# This file was automatically generated and should not be edited directly. -# To make changes, edit the device metadata and regenerate the interface. - -import enum -from typing import Any, ClassVar - -import numpy as np -from harp.protocol import ( - AnonymousPayload, - BitMask, - BoolConverter, - Field, - GroupMask, - PayloadType, - RegisterBase, - RegisterU16, - RegisterU32, - RegisterU8, - StringConverter, - StructPayload, -) - - -__all__ = [ - "ResetFlags", - "ClockConfigurationFlags", - "OperationMode", - "EnableFlag", - "OperationControlPayload", - "ResetDevicePayload", - "DeviceNamePayload", - "ClockConfigurationPayload", - "WhoAmI", - "HardwareVersionHigh", - "HardwareVersionLow", - "AssemblyVersion", - "CoreVersionHigh", - "CoreVersionLow", - "FirmwareVersionHigh", - "FirmwareVersionLow", - "TimestampSeconds", - "TimestampMicroseconds", - "OperationControl", - "ResetDevice", - "DeviceName", - "SerialNumber", - "ClockConfiguration", - "REGISTER_MAP", -] - - -class ResetFlags(enum.IntFlag): - """Specifies the behavior of the non-volatile registers when resetting the device.""" - - RESTORE_DEFAULT = 0x1 - """The device will boot with all the registers reset to their default factory values.""" - - RESTORE_EEPROM = 0x2 - """The device will boot and restore all the registers to the values stored in non-volatile memory.""" - - SAVE = 0x4 - """The device will boot and save all the current register values to non-volatile memory.""" - - RESTORE_NAME = 0x8 - """The device will boot with the default device name.""" - - UPDATE_FIRMWARE = 0x20 - """The device will enter firmware update mode.""" - - BOOT_FROM_DEFAULT = 0x40 - """Specifies that the device has booted from default factory values.""" - - BOOT_FROM_EEPROM = 0x80 - """Specifies that the device has booted from non-volatile values stored in EEPROM.""" - - -class ClockConfigurationFlags(enum.IntFlag): - """Specifies configuration flags for the device synchronization clock.""" - - CLOCK_REPEATER = 0x1 - """The device will repeat the clock synchronization signal to the clock output connector, if available.""" - - CLOCK_GENERATOR = 0x2 - """The device resets and generates the clock synchronization signal on the clock output connector, if available.""" - - REPEATER_CAPABILITY = 0x8 - """Specifies the device has the capability to repeat the clock synchronization signal to the clock output connector.""" - - GENERATOR_CAPABILITY = 0x10 - """Specifies the device has the capability to generate the clock synchronization signal to the clock output connector.""" - - CLOCK_UNLOCK = 0x40 - """The device will unlock the timestamp register counter and will accept commands to set new timestamp values.""" - - CLOCK_LOCK = 0x80 - """The device will lock the timestamp register counter and will not accept commands to set new timestamp values.""" - - -class OperationMode(enum.IntEnum): - """Specifies the operation mode of the device.""" - - STANDBY = 0 - """Disable all event reporting on the device.""" - - ACTIVE = 1 - """Event detection is enabled. Only enabled events are reported by the device.""" - - SPEED = 3 - """The device enters speed mode.""" - - -class EnableFlag(enum.IntEnum): - """Specifies whether a specific register flag is enabled or disabled.""" - - DISABLED = 0 - """Specifies that the flag is disabled.""" - - ENABLED = 1 - """Specifies that the flag is enabled.""" - - -class OperationControlPayload(StructPayload[np.uint8]): - """Represents the payload of the OperationControl register.""" - - operation_mode: OperationMode = GroupMask(enum=OperationMode, mask=0x3) - """Specifies the operation mode of the device.""" - - dump_registers: bool = Field(BoolConverter(), mask=0x8) - """Specifies whether the device should report the content of all registers on initialization.""" - - mute_replies: bool = Field(BoolConverter(), mask=0x10) - """Specifies whether the replies to all commands will be muted, i.e. not sent by the device.""" - - visual_indicators: EnableFlag = GroupMask(enum=EnableFlag, mask=0x20) - """Specifies the state of all visual indicators on the device.""" - - operation_led: EnableFlag = GroupMask(enum=EnableFlag, mask=0x40) - """Specifies whether the device state LED should report the operation mode of the device.""" - - heartbeat: EnableFlag = GroupMask(enum=EnableFlag, mask=0x80) - """Specifies whether the device should report the content of the seconds register each second.""" - - -class ResetDevicePayload(AnonymousPayload[np.uint8]): - """Represents the payload of the ResetDevice register.""" - - __value__: ResetFlags = BitMask(enum=ResetFlags) - - -class DeviceNamePayload(AnonymousPayload[np.uint8]): - """Represents the payload of the DeviceName register.""" - - __value__: str = Field(StringConverter(25)) - - -class ClockConfigurationPayload(AnonymousPayload[np.uint8]): - """Represents the payload of the ClockConfiguration register.""" - - __value__: ClockConfigurationFlags = BitMask(enum=ClockConfigurationFlags) - - -class WhoAmI(RegisterU16): - """Specifies the identity class of the device.""" - - address: ClassVar[int] = 0 - - -class HardwareVersionHigh(RegisterU8): - """Specifies the major hardware version of the device.""" - - address: ClassVar[int] = 1 - - -class HardwareVersionLow(RegisterU8): - """Specifies the minor hardware version of the device.""" - - address: ClassVar[int] = 2 - - -class AssemblyVersion(RegisterU8): - """Specifies the version of the assembled components in the device.""" - - address: ClassVar[int] = 3 - - -class CoreVersionHigh(RegisterU8): - """Specifies the major version of the Harp core implemented by the device.""" - - address: ClassVar[int] = 4 - - -class CoreVersionLow(RegisterU8): - """Specifies the minor version of the Harp core implemented by the device.""" - - address: ClassVar[int] = 5 - - -class FirmwareVersionHigh(RegisterU8): - """Specifies the major version of the Harp core implemented by the device.""" - - address: ClassVar[int] = 6 - - -class FirmwareVersionLow(RegisterU8): - """Specifies the minor version of the Harp core implemented by the device.""" - - address: ClassVar[int] = 7 - - -class TimestampSeconds(RegisterU32): - """Stores the integral part of the system timestamp, in seconds.""" - - address: ClassVar[int] = 8 - - -class TimestampMicroseconds(RegisterU16): - """Stores the fractional part of the system timestamp, in microseconds.""" - - address: ClassVar[int] = 9 - - -class OperationControl(RegisterBase[OperationControlPayload]): - """Stores the configuration mode of the device.""" - - address: ClassVar[int] = 10 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = OperationControlPayload - - -class ResetDevice(RegisterBase[ResetFlags]): - """Resets the device and saves non-volatile registers.""" - - address: ClassVar[int] = 11 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = ResetDevicePayload - - -class DeviceName(RegisterBase[str]): - """Stores the user-specified device name.""" - - address: ClassVar[int] = 12 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = DeviceNamePayload - - -class SerialNumber(RegisterU16): - """Specifies the unique serial number of the device.""" - - address: ClassVar[int] = 13 - - -class ClockConfiguration(RegisterBase[ClockConfigurationFlags]): - """Specifies the configuration for the device synchronization clock.""" - - address: ClassVar[int] = 14 - payload_type: ClassVar[PayloadType] = PayloadType.U8 - payload_class = ClockConfigurationPayload - - -REGISTER_MAP: dict[int, type[RegisterBase[Any]]] = { - 0: WhoAmI, - 1: HardwareVersionHigh, - 2: HardwareVersionLow, - 3: AssemblyVersion, - 4: CoreVersionHigh, - 5: CoreVersionLow, - 6: FirmwareVersionHigh, - 7: FirmwareVersionLow, - 8: TimestampSeconds, - 9: TimestampMicroseconds, - 10: OperationControl, - 11: ResetDevice, - 12: DeviceName, - 13: SerialNumber, - 14: ClockConfiguration, -} diff --git a/tests/device/test_emit.py b/tests/device/test_emit.py index eebab9a..cc1f588 100644 --- a/tests/device/test_emit.py +++ b/tests/device/test_emit.py @@ -13,7 +13,7 @@ create_registers, ) -from . import expected_core, expected_device +from . import expected_device from .converters import DataConverter CONVERTERS = {"DataConverter": DataConverter()} @@ -137,17 +137,19 @@ def test_enum_names_match_generator_for_every_enum(device_registers): # --------------------------------------------------------------------------- -# Core reference output, from protocol common.yml +# Core registers: the emitter and the generated package, from the same core.yml # --------------------------------------------------------------------------- def _core_expected(): - return {cls.__name__: cls for cls in expected_core.REGISTER_MAP.values()} + # harp.device.core is the generated Python interface, so it is the reference for + # what the emitter should build from the same core registers. + return {cls.__name__: cls for cls in core.REGISTER_MAP.values()} @pytest.mark.parametrize("name", sorted(_core_expected())) -def test_core_register_structural(name, common_yml): - emitted = create_registers(common_yml)[name] +def test_core_register_structural(name, core_yml): + emitted = create_registers(core_yml)[name] expected = _core_expected()[name] assert emitted.address == expected.address assert emitted.payload_type == expected.payload_type @@ -155,10 +157,6 @@ def test_core_register_structural(name, common_yml): emitted.payload_class.payload_dtype.itemsize == expected.payload_class.payload_dtype.itemsize ) - if name == "DeviceName": - # Generator enriches DeviceName to interfaceType: string, while the - # common.yml of the protocol does not, so only the layout size matches here. - return assert _layout(emitted.payload_class.payload_dtype) == _layout( expected.payload_class.payload_dtype ) diff --git a/tests/device/test_schema.py b/tests/device/test_schema.py index ab74d8d..f15cafb 100644 --- a/tests/device/test_schema.py +++ b/tests/device/test_schema.py @@ -58,22 +58,35 @@ def test_parse_bytes_decodes_as_utf8_regardless_of_locale(): ) -def test_parse_common_registers(common_yml): - c = parse_device_schema(common_yml) - assert c.device is None +def test_reserved_word_mask_keys_stay_strings(): + # Off, On, Yes and No are booleans in YAML 1.1, so a 1.1 parser keys these values by + # True and False. + schema = ( + "registers:\n" + " Indicators: {address: 32, type: U8, access: Write, maskType: LedState}\n" + "groupMasks:\n" + " LedState:\n" + " values:\n" + " Off: 0\n" + " On: 1\n" + ) + m = parse_device_schema(schema) + assert {k: int(v) for k, v in m.groupMasks["LedState"].values.items()} == {"Off": 0, "On": 1} + + +def test_parse_core_registers(core_yml): + c = parse_device_schema(core_yml) assert "WhoAmI" in c.registers - # Off/On group-mask keys must stay strings (YAML 1.1 would coerce to bool). - assert {k: int(v) for k, v in c.groupMasks["LedState"].values.items()} == {"Off": 0, "On": 1} # 'None' bit name stays a string, not YAML null. assert "None" in c.bitMasks["ResetFlags"].bits -def test_bool_values_preserved(common_yml): - c = parse_device_schema(common_yml) +def test_bool_values_preserved(core_yml): + c = parse_device_schema(core_yml) assert c.registers["TimestampSeconds"].volatile is True -def test_access_list_and_scalar(common_yml): - c = parse_device_schema(common_yml) +def test_access_list_and_scalar(core_yml): + c = parse_device_schema(core_yml) # TimestampSeconds has a list access [Read, Write, Event]; WhoAmI a scalar. assert isinstance(c.registers["TimestampSeconds"].access, list) From a94775398d0337331a83af2bfeed57153d3296ae Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 18 Aug 2026 14:44:31 +0100 Subject: [PATCH 2/2] Read the module docstring from the schema Registers gains a description field, matching the top-level property in the generator object model, and create_device_module uses it as the module docstring. --- .../src/harp/device/schema/_model.py | 3 +++ .../src/harp/device/schema/_module.py | 5 +++-- tests/device/test_create_device_module.py | 21 ++++++++++++++++++- tests/device/test_schema.py | 2 ++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/packages/harp-device/src/harp/device/schema/_model.py b/src/packages/harp-device/src/harp/device/schema/_model.py index 14d6271..5e6ac67 100644 --- a/src/packages/harp-device/src/harp/device/schema/_model.py +++ b/src/packages/harp-device/src/harp/device/schema/_model.py @@ -211,6 +211,9 @@ class Register(BaseModel): class Registers(BaseModel): """A bare register collection, a header-less ``device.yml`` fragment.""" + description: Optional[str] = Field( + None, description="A summary description of the register interface." + ) registers: Dict[str, Register] = Field( ..., description="The collection of registers implementing the device function." ) diff --git a/src/packages/harp-device/src/harp/device/schema/_module.py b/src/packages/harp-device/src/harp/device/schema/_module.py index 259136f..5a7170c 100644 --- a/src/packages/harp-device/src/harp/device/schema/_module.py +++ b/src/packages/harp-device/src/harp/device/schema/_module.py @@ -84,7 +84,8 @@ def create_device_module( name, so :class:`~harp.data.DatasetReader` matches files by it; * ``__name__``, the same name, falling back to ``"Device"`` so the module is never anonymous. This names the module rather than the device, and is not part of what - a device module promises. + a device module promises; + * ``__doc__``, the optional ``description`` of the schema. Because the names come from the schema at runtime they don't autocomplete, and each resolves as ``Any`` rather than its own type. A generated device package is @@ -112,7 +113,7 @@ def create_device_module( for declaration in contents.values(): declaration.__module__ = module_name - module = DeviceModule(module_name, f"Harp registers for {module_name}, from a schema.") + module = DeviceModule(module_name, device.description) vars(module).update( contents, DEVICE_NAME=device_name, diff --git a/tests/device/test_create_device_module.py b/tests/device/test_create_device_module.py index ae8c39f..ccab57c 100644 --- a/tests/device/test_create_device_module.py +++ b/tests/device/test_create_device_module.py @@ -5,7 +5,12 @@ import pytest from harp.device.core import REGISTER_MAP as CORE_REGISTER_MAP from harp.device.core import WhoAmI -from harp.device.schema import DeviceModule, DeviceModuleLike, create_device_module +from harp.device.schema import ( + DeviceModule, + DeviceModuleLike, + create_device_module, + parse_device_schema, +) from harp.protocol import RegisterBase from . import expected_device @@ -48,6 +53,20 @@ def test_whoami_from_schema(): assert mod.WHO_AM_I == 1216 +def test_docstring_from_schema(core_yml): + # The docstring is the schema description rather than composed from the module name, + # so a runtime module reads the same as the generated package for the same metadata. + doc = create_device_module(core_yml).__doc__ + assert doc == parse_device_schema(core_yml).description + assert doc + + +def test_docstring_absent_when_undeclared(test_module): + # device.yml declares no description, and a generated package carries no module + # docstring either, so both paths agree instead of one composing a name. + assert test_module.__doc__ is None + + def test_registers_are_reachable_by_name(test_module): assert test_module.AnalogData.address == 33 assert test_module.EncoderMode.address == 103 diff --git a/tests/device/test_schema.py b/tests/device/test_schema.py index f15cafb..bfda793 100644 --- a/tests/device/test_schema.py +++ b/tests/device/test_schema.py @@ -10,6 +10,7 @@ def test_parse_full_device(device_yml): assert isinstance(m, DeviceModel) assert m.device == "Tests" assert m.whoAmI is None # this application-device metadata omits whoAmI + assert m.description is None # and omits a top-level description assert "AnalogData" in m.registers ad = m.registers["AnalogData"] assert ad.type is PayloadType.Float @@ -77,6 +78,7 @@ def test_reserved_word_mask_keys_stay_strings(): def test_parse_core_registers(core_yml): c = parse_device_schema(core_yml) assert "WhoAmI" in c.registers + assert c.description # the core metadata declares a top-level description # 'None' bit name stays a string, not YAML null. assert "None" in c.bitMasks["ResetFlags"].bits