Skip to content

Case study · Payload decoders

Four defects in a vendor's payload decoder, fixed upstream in a day

A LoRaWAN sensor arrives with a decoder written by its manufacturer, and the station believes whatever that file returns. We read the published decoder for an air-quality and CO sensor family, found four ways it hands a building system something it cannot act on, and sent them to the manufacturer. All four are fixed in the manufacturer's own public repository, and the commits are linked at the bottom of this page.

  • LoRaWAN
  • Payload decoding
  • Accepted upstream
  • Four findings
  • Public commits
  • decoder-check

The systems involved

What had to talk to what

The decoderOne JavaScript file per product family, published by the manufacturer and run as-is by whichever network server receives the frames. It is the only thing that knows what the bytes mean, and nothing downstream can check its work.
What consumes its outputThe network server hands the decoded object upwards. In this trade that is a station, where each field becomes a point and the field's type decides whether that point is numeric, a string, or never updates at all.
What we read it withdecoder-check, the free tool on this site: it runs a vendor decoder in a sandbox with no network, no filesystem and no timers, and prints what a station would actually receive frame by frame.

The difficulty

What made them hard to connect

Nothing in the chain validates the decoder. The network server runs the file and the station takes the object. A byte value nobody mapped returns undefined, which is not an error anywhere: the field is absent, the point never updates, and there is no log line to find. Three functions in the published file ended that way - the product type, the message type and the radio region - and each one is an input an integrator cannot control.

The same key changed type between frames. A measurement arrived as {"value":-30,"unit":"°C"} in a normal frame and as the string "Error" in a fault frame. One point cannot be both: the import makes a numeric point, and the string arrives as a fault or as nothing. A BMS needs one type per field for the lifetime of the integration.

One function was declared twice. In the version published on 17 September, decoderAir+.js declared function humidity at line 53 and again at line 175. JavaScript keeps the later one silently, so the code a reviewer reads first is not the code that runs. That is worse than a wrong conversion, because reviewing the file carefully is what produces the wrong answer.

Enumerations arrived as English display strings. Readable in a test console, and useless to a control system: the ordinal is discarded, so a station cannot compare, alarm on or trend the value without parsing prose it did not choose.

The work

What we built

A reading, not a bug report

Four findings, each with the line, the byte that triggers it, and what a station receives instead - produced by running the vendor's own file in a sandbox rather than by inference. The mail was four numbered items and a frame per item.

The manufacturer chose the fix

They took all four, said no pull request was needed, and published their own answer: a defaulted value on every enumeration, a second decoder with one numeric type per field for building-system use, and an MIT licence added to each product family folder the same day.

A rule, not only a patch

The day after, a CHANGELOG.md appeared in the repository carrying the rule we had argued for: an output field renamed or removed, or a value or type changed, is a major version, announced before publication and marked as an output change. That is the part that protects the next integration rather than this one.

Now

What it does today

The published decoder has one humidity function, a value returned for every unmapped code, a numeric-typed variant beside it for building systems, an MIT licence and a versioning rule for output changes. Our name is nowhere in the repository, which is the point: the fix sits upstream of everyone who buys the sensor, including whoever reads this page.

Findings sent4
Findings acted on4
<code>function humidity</code> declarations in the published file2 before, 1 after
Enumerations returning nothing for an unmapped byte3 before, 0 after
Our post to the first fix commitsame day, 28 September 2026
Versioning rule for output changesnone before, published the next day
Pull requests from us0 - the manufacturer wrote the code
Invoicednothing, and nothing was offered

The stack

What it is made of

The fileJavaScript, as the manufacturer publishes it, one per family
Read withdecoder-check: node:vm, no network, no filesystem
Frames readproduct status, CO alarm status, real time, and the configuration frame
Where the fix livesthe manufacturer's public repository, MIT licensed

Who did what

The reading is ours. The decoder is not ours, the fix is not ours and the repository is not ours: we sent four findings and the manufacturer wrote the code, chose the shape of the numeric variant and published the versioning rule. Nothing here was commissioned, paid for or sponsored in either direction, and we hold no relationship with the manufacturer beyond that exchange. They are not a client and this page does not imply one.

Check it

The evidence, not a description of it

Every link here opens the thing itself — a repository, a running demo, a note with the method in it.

Next step

Tell us the version, the hardware, and what it has to do.

You will get a written scope and a fixed price against it. If the honest answer is that you do not need us, you will get that instead.