Skip to content

Feature/update vocab, updating vocab to version 0.3.0 - #96

Merged
egekorkan merged 22 commits into
eclipse-thingweb:mainfrom
Hao-Siemens:feature/update-vocab
Aug 26, 2026
Merged

Feature/update vocab, updating vocab to version 0.3.0#96
egekorkan merged 22 commits into
eclipse-thingweb:mainfrom
Hao-Siemens:feature/update-vocab

Conversation

@Hao-Siemens

@Hao-Siemens Hao-Siemens commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Version 0.3.0 moved decoded values from properties to events and renamed or
withdrew the terms below. Also the corresponding toolchain, documentation, and examples are updated. And the vocab is simplied to reuse existing terms from TD and schema.

Removed Replacement Why
properties events uplinks are pushed, not polled
lorav:type lorav:wireType shadowed the data schema's type
lorav:offset lorav:addend read as a byte position next to lorav:byteOffset
lorav:length lorav:byteLength ambiguous between bytes and elements
lorav:presenceField, lorav:presenceBit lorav:presentWhen: { field, bit } halves were separable but meaningless apart
lorav:switchField, lorav:switchValue lorav:presentWhen: { field, value } same condition, one spelling
lorav:var lorav:alias it exports a reference name; it is not a condition
lorav:ref, lorav:polynomial, lorav:compute, lorav:guard, lorav:transform lorav:derived: { … } one concept, five top-level terms
lorav:validRange data.minimum / data.maximum TD core already says this
lorav:enum data.oneOf with const + title TD core already says this
lorav:unece data.unit unit already carries UN/CEFACT codes
lorav:brand, lorav:model schema:brand, schema:model schema.org already says this
lorav:hardwareVersion, lorav:softwareVersion version.model, version.instance TD core already says this
lorav:endDeviceId the Thing's id or title TD core already says this

Hao-Siemens and others added 7 commits August 10, 2026 23:19
…is now expressed as a form-level big/little string instead of a boolean, updated the vocabulary and related documentation with the investigation how often are the various terms used in the device decoding
…actor

Records the payload schemas emitted for the curated examples, the decoded values for every test vector, and the reference-device catalog coverage plus a per-device round-trip digest.

Extracts convert_catalog() from generate_device_tds so the snapshot builder shares the catalog walk instead of duplicating it.
@Hao-Siemens Hao-Siemens mentioned this pull request Aug 13, 2026
6 tasks
@egekorkan

egekorkan commented Aug 17, 2026

Copy link
Copy Markdown
Member

Let's use this repository going forward as the reference. I am putting my feedback based on the first comment of this PR and not the git diff.

Removed Replacement Why Ege Comment
properties events uplinks are pushed, not polled None
lorav:type lorav:wireType shadowed the data schema's type while I understand, all other bindings also have just type. @wiresio also a point for data mapping
lorav:offset lorav:addend read as a byte position next to lorav:byteOffset I find the word very weird but I agree that offset is confusing here
lorav:length lorav:byteLength ambiguous between bytes and elements None
lorav:presenceField, lorav:presenceBit lorav:presentWhen: { field, bit } halves were separable but meaningless apart None
lorav:switchField, lorav:switchValue lorav:presentWhen: { field, value } same condition, one spelling None
lorav:var lorav:alias it exports a reference name; it is not a condition Agree with the problem but find alias more like a nickname
lorav:ref, lorav:polynomial, lorav:compute, lorav:guard, lorav:transform lorav:derived: { … } one concept, five top-level terms Not sure if I understand in this high level
lorav:validRange data.minimum / data.maximum TD core already says this agree
lorav:enum data.oneOf with const + title TD core already says this See bacnet and profinet binding on why this is not a perfect solution. @wiresio FYI
lorav:unece data.unit unit already carries UN/CEFACT codes agree
lorav:brand, lorav:model schema:brand, schema:model schema.org already says this schema:manufacturer and schema:mpn is preferred
lorav:hardwareVersion, lorav:softwareVersion version.model, version.instance TD core already says this schema.org should be used. Those in TD core are about the TD or TM version, not device or its SW/HW version
lorav:endDeviceId the Thing's id or title TD core already says this

Comment thread python/lorawan/README.md Outdated
For a `ports` layout, don't forget to pass the frame port:

`--fport` is required for a `ports` layout, because the frame port selects which
layout to apply. Other layouts ignore it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
layout to apply. Other layouts ignore it.
layout to apply; without it the interpreter cannot select the right field set Other layouts ignore it.

Reviewer feedback on eclipse-thingweb#96: this repository is the reference for the LoRaWAN
binding going forward, so the six curated `*.td.json` + `*.vectors.json` pairs
belong here, reviewed like any other source file. They were previously ignored
and mirrored in from eclipse-thingweb/examples on demand, which meant the files
the test suite ran against were whatever happened to be on disk.

`sync-examples` is removed rather than kept: it copies upstream over the local
files, so leaving it would let a stale mirror silently overwrite the canonical
copies. The mirror direction is now outbound.

The examples-missing skip guard in test_golden goes with it - the examples can
no longer be absent, and a guard that never fires only hides the day it should.
Reviewer feedback on eclipse-thingweb#96, in three parts.

schema:brand / schema:model become schema:manufacturer / schema:mpn.
schema:brand ranges over Brand and schema:model over ProductModel, so both
invite a nested object where a device repository carries a plain identifier.
schema:manufacturer and schema:mpn (manufacturer part number) are what a
catalogue entry actually holds.

The hardware and firmware revisions move off TD core's version object onto
schema:version and schema:softwareVersion. TD core's version/model and
version/instance version the *Thing Description*: they say which revision of
the document you are holding, not which firmware the device is running. A TD
can be revised without the device changing at all, which is exactly the moment
you would want to read the firmware version and could not.

No lorav: term changes; all four names were already withdrawn in 0.3.0, so
this only retargets what REMOVED_TERMS points at and what the migration script
emits.

Also from review:

* README: say why --fport is required for a ports layout (the frame port
  selects which layout applies, so without it the interpreter cannot tell
  which field set the bytes belong to) rather than restating that it is.
* README: document lorav:derived with two worked examples from the Decentlab
  catalogue -- a ref + polynomial fit and a guarded compute -- and a table of
  what each of the five keys means, since "this value is computed rather than
  read from the wire" was not readable from the one-line mention it had.
The AM102 example was written from the vendor codec rather than from a
device, and its payloads were plausible-looking rather than observed. The
EM300-TH shares that codec byte-for-byte, so swapping the example costs
nothing in coverage and buys uplinks that were actually received on the
air, on fPort 85:

  0367F800046862        -> temperature 24.8, humidity 49.0
  0175640367F900046862  -> battery 100, temperature 24.9, humidity 49.0

The synthetic negative-temperature vector is kept because no captured
frame covers the sign-extension path, and that path is the one most
likely to regress.

Renames em300-zld to milesight-em300-zld so both Milesight examples are
vendor-qualified and sort together, and fixes two long-standing defects
in its vectors file: the description was copy-pasted from the AM102, and
both cases were named "nominal_reading", which meant the golden snapshot
silently recorded only the second one. Naming them apart restores the
first case to the snapshot.

Also finishes 17fe113 in the examples: four of them still carried device
hardware and firmware revisions in TD core's "version" object, which that
commit's README now says versions the *document*. Moved to schema:version
and schema:softwareVersion so the examples agree with the prose.
Categorical readings were emitted as a data schema of "oneOf" entries
pairing "const" with "title", chosen so the binding would not have to
mint a term for something TD core looked able to express. Reviewing it
against the WoT BACnet and Modbus bindings showed the reasoning was
right but the placement was wrong, and measuring it showed the result
was broken: all 21 such schemas across the examples are unsatisfiable.

    {"type": "string", "oneOf": [{"const": 0, "title": "normal"}]}

rejects the decoded "normal" and the raw 0 alike. "const" held the wire
integer inside the schema of the decoded value, so the schema described
neither end, and "title" -- a display label with a multi-language
"titles" sibling -- was carrying machine-readable data, so rewording or
translating a Thing Description silently changed what its payloads
decoded to. Nothing caught this because the decode tests compared
decoded values only against the vectors, never against the TD.

Split it the way BACnet does. The values a reading may take stay in the
data schema as a plain TD core "enum"; which wire encoding yields which
of them moves to the form as lorav:valueMap, a list of {wireValue,
value} pairs. That correspondence is a fact about the transfer, which
TD core has no vocabulary for and a protocol binding exists to state --
the categorical counterpart of lorav:wireType. BACnet reaches the same
split with bacv:hasValueMap / bacv:hasProtocolVal / bacv:hasLogicalVal,
Modbus with modv:type. lorav:enum therefore becomes a rename rather
than a withdrawal, and the migration script now splits an 0.2.x table
in two, retyping the event to match the labels it now carries.

tests/test_decode.py validates every vector's decoded value against its
own event's data schema, closing the gap that let this through.

No behaviour changes: every payload schema, lookup table and decoded
value across the 157 catalog devices and 7 curated examples is
byte-identical. The only golden additions belong to the new example.

Also adds the MClimate Multipurpose Button example, which exercises
affine raw-byte scaling (lorav:multiplier with lorav:addend) and a
lorav:bitmask status bit against the official TTN codec.
The thermistor status bit is set when the thermistor is *absent*, which the
example expressed as "lorav:multiplier": -1 with "lorav:addend": 1 -- arithmetic
that turns 1 into 0 without ever saying that 1 meant "disconnected". The
polarity survived only in the prose description, and the decoded value was a
bare 0 or 1 that a consumer still had to interpret.

A lorav:valueMap says it directly: the data schema offers "connected" /
"disconnected" and the form maps the bit onto them.

pressEvent is deliberately left as a raw integer, which surfaced a limitation
worth recording. The MultiTech interpreter applies a lookup positionally --
'if 0 <= value < len(lookup)' -- so it reads the table as a list indexed by the
wire value rather than as a mapping. The button numbers its press codes 1..3, so
that table has length 3 and wire value 3 falls outside the guard, decoding to
the bare integer 3 rather than "triple". Three RadioBridge rbs30x events in the
generated catalog have the same shape.

This is pinned by a test rather than asserted away, so a future submodule bump
that fixes the interpreter will fail it and tell us the gap has closed. The same
test records that the failure is at least detectable: the leaked wire value does
not validate against its own data schema, which is precisely the check the
withdrawn oneOf/const spelling could never have provided.

Also replaces a tautological test added with lorav:valueMap, which validated a
literal against a literal and would have passed no matter what the converter
did, with one that asserts the converter leaves the data schema alone.
…ed tier

The bit says whether the thermistor is connected, so the honest decoded value
is true/false rather than the string pair it was given. TD core's "boolean"
already enumerates its own value space, so the data schema needs no 'enum'
beside it -- which shows that a value map does not depend on one, and the
'value' of an entry may be any JSON type. Generalised the wording in vocab.py,
the form schema and the ontology accordingly; each had said the mapped value
must appear in the data schema's 'enum'.

Booleans had not appeared in a test vector before, and Python's True == 1 meant
test_decode_matches_expected would have accepted a decoder that returned the raw
bit instead of mapping it. Compares booleans by identity, before the numeric
branches.

Corrects the Tier column for lorav:valueMap from "common" to "rare". The column
is defined as frequency across the bundled device catalog, and a census puts the
term in 4 of 157 devices (2.5%) -- the same share as lorav:derived, which is
listed as rare. The figure was carried over rather than measured when the term
was added. Every other row in the table was checked against the same census and
is accurate.

Also trims the two README passages added with the term, which restated the
argument the surrounding section had already made.
The two Milesight examples describe the same wire format with the same tag
fields, yet one said "ctv" and the other "tlv". Nothing in the converter reads
the difference -- both fall into the same branch -- so the two names produced
byte-identical schemas and the disagreement was noise a reader had to resolve.

'tlv' wins on weight of usage: all 85 tagged devices in the generated catalog
carry it, and 'ctv' now appears in no document at all. The golden snapshot is
unchanged by this commit, which is the evidence that the rename decides only
what the file is called.

The same example was also the only one in the repository with a form href other
than "uplink" -- it used "ctv", repeating the layout name in a slot that holds a
resource locator relative to the Thing's base. The other 28 curated forms and
all 1623 generated ones use "uplink".

Adds a test pinning ctv and tlv to the same output. The alias is invisible in
the corpus now, so a reader can reasonably assume it does something; the test
says it does not, and would fail if a later change gave one name behaviour the
other lacks. vocab.py and the README record the same, including the reason the
distinction does not arise: the names differ over whether a length travels with
each value, and the width comes from the wire type either way.
@Hao-Siemens

Hao-Siemens commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Ready to be merged.

Breaking revision of the vocab. (There will still be small iterations on the vocab in the future)

addressing the comments @egekorkan from above

Was Now
lorav:brand, lorav:model schema:manufacturer, schema:mpn
lorav:hardwareVersion, lorav:softwareVersion schema:version, schema:softwareVersion

One term added: lorav:valueMap

  • lorav:enumlorav:valueMap.
    The fix follows the way the WoT BACnet binding does. The data schema says which values the reading may take, using TD core's enum; the form says which byte on the wire produces which of them:
"leakage_status": {
  "data": { "type": "string", "enum": ["normal", "leak"] },
  "forms": [{
    "lorav:wireType": "u8",
    "lorav:valueMap": [
      { "wireValue": 0, "value": "normal" },
      { "wireValue": 1, "value": "leak" }
    ]
  }]
}

Add the example TD back to the examples folder
examples/adeunis-comfort2.td.json, examples/milesight-em300-th.td.json, examples/milesight-em300-zld.td.json , examples/dragino-lht65n.td.json ,examples/netvox-r718a.td.json, examples/generic-lorawan11.td.json.

A new example TD is added: examples/mclimate-mc-button.td.json

More information is in the README under /python/lorawan

The sentence named the layout that needs the flag but not what goes wrong
without it, which is the part a reader hits at the command line.
@Hao-Siemens
Hao-Siemens marked this pull request as ready for review August 26, 2026 12:09
Comment thread README.md Outdated
@egekorkan
egekorkan merged commit a20a672 into eclipse-thingweb:main Aug 26, 2026
1 check passed
@egekorkan

Copy link
Copy Markdown
Member

This is looking good now. There are some small things to update in the examples but I will do a bigger pass through everything later on. We need this PR in so that the rest of the PRs can be incorporated

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.

2 participants