Skip to content

Add device interface specification - #215

Open
bruno-f-cruz wants to merge 5 commits into
mainfrom
feat-add-device-interface-spec
Open

Add device interface specification#215
bruno-f-cruz wants to merge 5 commits into
mainfrom
feat-add-device-interface-spec

Conversation

@bruno-f-cruz

@bruno-f-cruz bruno-f-cruz commented Apr 25, 2026

Copy link
Copy Markdown
Member

This is a first stab at describing the device interface, device.yml specification.
It provides a high-level description on how the document is organized, validated and used by downstream tooling. I also provides a few examples.

I think we could add more detail in the following points:

@bruno-f-cruz
bruno-f-cruz requested review from a team, banchan86 and glopesdev April 25, 2026 18:05
@bruno-f-cruz

bruno-f-cruz commented Apr 26, 2026

Copy link
Copy Markdown
Member Author

We should also address #214
One idea is that scalar registers should NEVER specify length. If length is specied (even if 0) the register interface must always return a array type

Documention on heterogenous registers here(harp-tech/generators#87)

@bruno-f-cruz
bruno-f-cruz force-pushed the feat-add-device-interface-spec branch from 3e6f4a9 to ce8a1f6 Compare June 21, 2026 12:12

@banchan86 banchan86 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good to me! I thought it was well written and clear with the accompanying examples. Just minor suggestions for clarity and one stumbling block regarding the bitmasks.

Comment thread DeviceInterface.md

## Document Structure Overview

A device interface file is a YAML document whose schema is defined by `./schema/device.json`. The root level of the document contains metadata fields describing the device and a `registers` map that declares all application registers. Optional `bitMasks` and `groupMasks` sections define named masks that can be referenced by registers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the first line duplicates/contradicts the "Schema Reference" at the end of the file, which states that it's defined by a set of 3 files (device.json, core.json, registers.json). I think it might help to bring the "Schema Reference" section with the table up here and delete this line, as I also find that section is a little out of place at the end of the article.

I also realize the term device.yml is never used anywhere, while core.yml is mentioned. I feel like it can be brought in here.

Suggested change
A device interface file is a YAML document whose schema is defined by `./schema/device.json`. The root level of the document contains metadata fields describing the device and a `registers` map that declares all application registers. Optional `bitMasks` and `groupMasks` sections define named masks that can be referenced by registers.
The root level of the device interface file (`device.yml`) contains metadata fields describing the device and a `registers` map that declares all application registers. Optional `bitMasks` and `groupMasks` sections define named masks that can be referenced by registers.

Comment thread DeviceInterface.md
A YAML language-server schema directive SHOULD be placed at the top of the file to enable editor validation and autocompletion:

```yaml
# yaml-language-server: $schema=https://harp-tech.org/draft-02/schema/device.json

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# yaml-language-server: $schema=https://harp-tech.org/draft-02/schema/device.json
%YAML 1.1
---
# yaml-language-server: $schema=https://harp-tech.org/draft-03/schema/device.json

Add yaml version directive and update the schema directive link which is now on draft-03

Comment thread DeviceInterface.md

The file MUST declare all four metadata fields (`device`, `whoAmI`, `firmwareVersion`, `hardwareTargets`) and the `registers` map. The `bitMasks` and `groupMasks` sections are OPTIONAL and SHOULD be included when registers reference named masks.

A YAML language-server schema directive SHOULD be placed at the top of the file to enable editor validation and autocompletion:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A YAML language-server schema directive SHOULD be placed at the top of the file to enable editor validation and autocompletion:
A YAML version and language-server schema directive SHOULD be placed at the top of the file to enable editor validation and autocompletion:

Include the YAML version directive here?

Comment thread DeviceInterface.md

## Core Registers

Every Harp device also exposes a set of core registers (addresses 0–31) that are common to all devices. These registers are defined in a separate file (`core.yml`) and follow the same YAML schema described in this document. Core registers handle device identification, timestamping, operation mode control, and other shared functionality. See [Device Registers and Operation](Device.md#core-registers) for the full specification of core register behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Every Harp device also exposes a set of core registers (addresses 0–31) that are common to all devices. These registers are defined in a separate file (`core.yml`) and follow the same YAML schema described in this document. Core registers handle device identification, timestamping, operation mode control, and other shared functionality. See [Device Registers and Operation](Device.md#core-registers) for the full specification of core register behavior.
Every Harp device also exposes a set of core registers (addresses 0–31) defined in a separate [`core.yml`](./schema/core.yml) file that follows the same YAML schema described in this document. Core registers handle device identification, timestamping, operation mode control, and other shared functionality. As these registers are common to all devices, `core.yml` does not need to be included alongside `device.yml`. See [Device Registers and Operation](Device.md#core-registers) for the full specification of core register behavior.

I feel like one thing that is not obvious to people is whether or not they should include core.yml in the device repository, so I suggested edits here, and added link to schema/core.yml.

Comment thread DeviceInterface.md

A register that specifies only `Event` as its access mode is implicitly read-only from the Controller's perspective — the Controller cannot write to it, and the device pushes data when events occur.

When multiple access modes are needed, they SHOULD be expressed as a YAML sequence (1–3 unique values):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
When multiple access modes are needed, they SHOULD be expressed as a YAML sequence (1–3 unique values):
When multiple access modes are needed, they MUST be expressed as a YAML sequence (1–3 unique values):

Is there another way of expressing multiple access modes? If not, should it be a MUST?

Comment thread DeviceInterface.md Outdated
| `ushort` | 16-bit unsigned integer. |
| `float` | Floating-point value. |
| `string` | Text, typically over a span of byte elements. When using interfaceType, the `length` parameter MUST be defined |
| `HarpVersion` | Custom type composed from multiple elements that describes a SemVer-like version. When using this `interfaceType`, the `length` MUST be equal to 3 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `HarpVersion` | Custom type composed from multiple elements that describes a SemVer-like version. When using this `interfaceType`, the `length` MUST be equal to 3 |
| `HarpVersion` | Custom type composed from multiple elements that describes a [semantic version](https://semver.org/). When using this `interfaceType`, the `length` MUST be equal to 3 |

Changed text and added link, similar to what was done in Device.MD

Comment thread DeviceInterface.md Outdated
A member is located within the payload using two **independent** coordinates, either or both of which MAY be present:

- `offset` (with optional `length`) selects *which element(s)* of the payload array the member occupies.
- `mask` selects *which bits* within a payload word the member occupies.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So one thing that really tripped me up is I am not sure when one should define the bitmask in the payloadSpec and when one should define it in the "bitMask" section of the device.yml. Are these reasons correct?

Define bitmask in payloadSpec:

  • When combining offset and bitmask to locate a member?
  • When separating heterogenous type members with different-sized bitfields?

Define bitmask in bitMasks section:

  • When defining the same type members, with equal-size bitfields?

I also think I may have the wrong mental model and might be completely off about the distinction between the two 😆 A note about the differences between the two might help other confused readers like me.

Comment thread DeviceInterface.md Outdated
Comment on lines +324 to +325
| `int` | Signed integer. |
| `uint` | Unsigned integer. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `int` | Signed integer. |
| `uint` | Unsigned integer. |
| `int` | 32-bit signed integer. |
| `uint` | 32-bit unsigned integer. |

Comment thread DeviceInterface.md
Comment on lines +531 to +545
### Private registers

Registers that are not meant to be exposed in high-level interfaces (e.g. code-generated classes) can be marked with `visibility: private`. This is commonly used for reserved registers that fill gaps in the address space to maintain contiguity.

```yaml
Reserved0: &reserved
address: 33
type: U8
access: Read
description: Reserved for future use
visibility: private
Reserved1:
<<: *reserved
address: 84
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I feel like this section might be better placed in the walkthrough of the properties in the "Registers" section. The example will have to be changed to get rid of the yaml anchors and aliases.

Comment thread DeviceInterface.md
Comment on lines +547 to +557
## Schema Reference

The device interface YAML format is formally defined by a set of JSON Schema files:

| Schema | Description |
| :--------------- | :---------------------------------------------------------------------------------------------------------- |
| `device.json` | Top-level schema for device interface files. Requires device metadata and references `core.json`. |
| `core.json` | Schema for core device properties. Requires `protocolVersion` and references `registers.json`. |
| `registers.json` | Defines the structure of registers, bit masks, group masks, payload members, and all associated properties. |

Editors supporting the [YAML Language Server](https://github.com/redhat-developer/yaml-language-server) can validate device interface files by adding the schema directive shown in the [Document Structure Overview](#document-structure-overview).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned there is some overlap with this and the document overview section so might be better to move this up there.

@glopesdev

Copy link
Copy Markdown
Collaborator

While fixing a client defect I measured what the three generator targets actually do with length, and found two things the specification could settle. Both are cheap to write down and neither needs a schema change.

An absent length and length: 1 are different declarations, and the specification does not say so. registers.json gives length a minimum of 1 and leaves it optional, so absence carries meaning that is written nowhere. Both generator targets read absence as a single value and any declared length as an array, so length: 1 produces ushort[] in C# and an array register in Python, while omitting it produces ushort and a scalar. That is a reasonable rule and worth stating, because a client that defaults the field to 1 instead of leaving it absent silently produces the wrong type. That is exactly the bug we just fixed in the Python client.

A payloadSpec member offset must fall inside the register payload, and nothing enforces it. A register that declares no length is one element, so a member at offset: 1 is out of bounds. The C# generator does not reject that schema, it crashes with IndexOutOfRangeException, and the Python target emits a payload spanning two elements. A sentence saying member offsets are bounded by the register length, with absence meaning one element, would make that a validation error rather than a generator crash.

One related observation, which may belong in the interface types section rather than here. A member narrower than the register element, for example an interfaceType: bool member of a U16 register, leaves the payload extent ambiguous. C# reads a whole ushort and the Python client currently reads one byte, which cannot be right on the wire since a U16 payload is always a multiple of two bytes. If the specification says a register payload is always a whole number of elements of its declared type, both targets have a rule to follow and the disagreement becomes a bug in one of them rather than an open question.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants