Skip to main content

UST Format Checker

Checks that xDOS firmware keeps its output format backward compatible and in step with the documentation, by running the firmware in simavr against a deterministic scenario and inspecting what it prints on the UART.

Plan and requirements: PLAN.md. Per-message rules: CHECKS.md. Where docs, parser and firmware disagree: DOC_DEVIATIONS.md.

The checker only knows format version 2.1. Every capture has to name its own format with $DATAFORMAT; without it the check fails. Older data is still readable by DOSPORTAL, but firmware is only released as 2.1.

Installing

pip install ust-format-checker

Needs simavr to run a firmware (apt install simavr libsimavr-dev libelf-dev gcc). Installed commands: xdos-check (a capture), xdos-check-firmware (an ELF, end to end), xdos-build (board + scenario -> runner directives).

In a device repository you normally do not install it by hand - the PlatformIO hook pins a version in xdos/checker.txt and builds its own venv from it.

Installed standalone, layer 3 is skipped and says so: packages.parsing lives in DOSPORTAL and is not a package of its own yet. Run from a DOSPORTAL checkout to get it.

Layout

  • messages.yaml - the message schema: fields, types, ranges, which rules apply.
  • components/ - what a component is, which runner directive drives it and which messages it makes the device produce.
  • schema.py, checks.py, rules.py - schema loading and layer 1 checks.
  • lines.py - a capture as bytes and lines ending at \n; nothing is stripped or decoded away before the checks see it.
  • structure.py - rules about the file itself, checked on the bytes: allowed characters, line endings, line length.
  • requirements.yaml, requirements.py - how each requirement of the format specification is verified. A check names the IDs it verifies where it creates the finding, a test with @pytest.mark.req(...); test_requirements_matrix.py keeps the three in step.
  • matrix.py - prints the verification matrix as markdown (python -m ust_format_checker.matrix), for comparing with the documentation or keeping with a CI run.
  • emitted.py - a message a component on the board produces must come out.
  • parser_layer.py - layer 3: what packages.parsing reads out of the same log.
  • stimulus_checks.py - the value the simulation injected has to be the value that comes out.
  • findings.py, report.py, cli.py - findings, the rustc-style report and the xdos-check entry point.
  • simulate.py - compiles the runner and runs it; no container in the loop.
  • firmware.py - the whole chain in one command, plus the verdict cache.
  • platformio.py - what the device-repository hook calls; holds every path convention.
  • pytest_plugin.py - the capture fixture, so a device repository needs no conftest.
  • regressions/ - real captures carrying format bugs the checker must keep catching. Only genuine captures belong here; a hand-written one shares its blind spots with the rule it guards, so that case goes in a unit test.
  • Which component sits at which address or pin, and what the simulation injects, belong to the device repository as xdos/board.yaml and xdos/scenarios/*.yaml. A device with no scenario of its own gets one generated from its board.
  • scenario.py, build_scenario.py - board + scenario -> runner directives + the stimulus to check against.
  • simulator/runner.c - loads the firmware ELF into simavr, attaches component models, replays the directives, prints the UART.

Checking a captured log

cd backend
python -m packages.ust_format_checker <log>                        # schema + parser layer
python -m packages.ust_format_checker <log> --board <board.yaml>   # + messages the board must produce
python -m packages.ust_format_checker <log> --stimulus <json>      # + injected values (from xdos-build)
python -m packages.ust_format_checker <log> --no-parser            # schema only
python -m packages.ust_format_checker <log> --warnings-as-errors   # CI profile

Exit code 1 means the log failed. Layer 3 (packages.parsing) never produces an error on its own: the parser can be wrong just as easily as the firmware, so its findings are warnings that say so.

With --board, a message that a component on the board produces and the capture does not contain is an error. Without it the checker cannot tell "this device has no such sensor" from "the firmware stopped printing it", and has to treat both as the same piece of information.

Checking a firmware ELF

The whole chain in one command - this is what the PlatformIO hook in a device repository calls:

xdos-check-firmware firmware.elf --board xdos/board.yaml --scenario xdos/scenarios/basic.yaml \
    --cache .pio/build/<env>/xdos/last.json

--cache remembers the verdict against a fingerprint of the ELF, the board, the scenario and the checker itself: when none of them changed, the run is skipped but the finding is printed again, so it cannot quietly disappear after the first build. A new checker version or an edited board.yaml always runs the check again. A missing simulator is reported and exits 0, so a firmware build never fails because the checker could not run.

The simulation also reports any I2C address the firmware talked to that board.yaml does not know (UNMODELLED_DEVICE). Without it an incomplete board model makes the check silently test less than it appears to.

In a device repository

The hook and the path conventions live in platformio.py, so changing any of them is a version bump rather than a pull request against every device repository:

python -m ust_format_checker.platformio <project_dir> <build_dir> <elf>        # local, warns
python -m ust_format_checker.platformio --ci <project_dir> <build_dir> <elf>   # CI, blocks

--ci turns warnings into failures and drops the verdict cache. Under GitHub Actions the findings are also emitted as annotations, on stderr so they stay out of the report itself.

Tests of the device's own behaviour need nothing but a test file - the package registers as a pytest plugin and supplies the capture fixture:

def test_events_only_appear_inside_a_block(capture):
    assert len(capture.messages("$START")) == len(capture.messages("$STOP"))

Working on the checker

Linux, natively, against an ELF PlatformIO has already built. No container in the loop - the run takes under a second, so the edit-run cycle stays tight:

sudo apt install simavr libsimavr-dev libelf-dev gcc   # once
cd backend
A=~/AIRDOS03/fw/AIRDOS03_USTDFF

python -m packages.ust_format_checker.firmware $A/.pio/build/TFUNIPAYLOAD01_uart/firmware.elf \
    --board $A/xdos/board.yaml --scenario $A/xdos/scenarios/basic.yaml \
    --work-dir /tmp/xdos-dev --no-parser

--work-dir keeps what the run produced: capture.txt, the compiled run.scenario and run.stimulus.json. Reading capture.txt is usually the fastest way to understand a finding.

Most of the test suite needs none of this and runs in under two seconds. The -c /dev/null is there because the DOSPORTAL checkout carries a pytest.ini full of Django plugins this package does not use:

python -m pytest packages/ust_format_checker -q -c /dev/null

To see the checker the way a firmware developer does, run pio run in the device repository with XDOS_CHECKER_PATH pointing at this checkout - see that repository's xdos/README.md.

Board and scenario

A device is described by which components it carries:

# xdos/board.yaml
device: AIRDOS03B
mcu: atmega1284
f_cpu: 8000000
millis_symbol: timer0_millis     # the counter that paces a measurement block
seconds_symbol: rtc_seconds      # the device clock, moved forward along with it
parts:
  - {component: i2c_eeprom, addr: 0x5B, params: {offset: "0800", data: "0011...EEFF"}}
  - {component: sht31, addr: 0x45}
  - {component: spi_adc_pulse, pins: {conv: PB0, reset: PC2}}
  - {component: gnss, pins: {pps: PD4}}

A scenario says what to inject. Values may be rand(min, max) within the component's range; the seed comes from the scenario name, so runs stay reproducible:

name: basic
stop_blocks: 4
sensors:
  sht31:
    temp_c: rand(-40, 85)
    humidity: rand(0, 100)
events:
  at: 1.2
  channels: [0, 12, 12, 40, 63, 64, 100, 500, 1023]
gnss:
  fix_at: 1.0
  unix: 1789560000

Write a rand(min, max) on its own line, never inside a {...} flow mapping: the comma inside the call splits the mapping, so {temp_c: rand(-40, 85)} silently becomes temp_c: 'rand(-40' plus a key named 85). The scenario is rejected with an error that says so.

Skipping the idle wait between measurement blocks means moving millis_symbol forward. A device whose clock runs off something else - AIRDOS03 counts 1PPS pulses - would then have the two drift apart, and the time in the output could not be compared with anything. That is what seconds_symbol is for: both counters move together, so $STOP times stay meaningful and the run still finishes in under a second. Without it the time checks are skipped.

xdos-build turns the two into runner directives plus a stimulus file, and --stimulus then checks that every injected event, sensor reading and GNSS time comes back out of the firmware. A device with no scenario of its own gets one generated from its board.

Runner directives

The compiled intermediate the simulator reads, one directive per line, times in simulated seconds:

Directive Meaning
i2c_memory <addr> <reg_bytes> <offset> <hex> I2C memory (EEPROM) with preset bytes
sht31 <addr> <temp_c> <humidity> SHT31 returning a fixed reading
adc <conv_port> <conv_pin> <reset_port> <reset_pin> USTSIPIN ADC: CONV high until DRESET, value over SPI
adc_pulse <t> <value> one detected pulse
pps <port> <pin> <t> <count> 1PPS pulses every second
uart1_line <t> <text> text line sent to UART1 (GNSS)
warp_u32 <symbol|0xaddr> <t> <add> [period] [count] adds to a 32-bit counter in the firmware's RAM; the symbol is resolved from the ELF

warp_u32 reschedules itself when period and count are given, which is how one directive covers a whole run: simavr has only 64 timer slots and the peripherals have to fit in too, so one warp per step would break the simulation.

Generated from YAML, so the only reason to read them is debugging a simulation.

Release files for ust-format-checker 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ust-format-checker 0.2.0
File Size Uploaded
ust_format_checker-0.2.0.tar.gz 105.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ust-format-checker 0.2.0
File Interpreter ABI Platform
ust_format_checker-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 177.0 kB

Release files / ust_format_checker-0.2.0.tar.gz

Download URL ust_format_checker-0.2.0.tar.gz
Size 105.9 kB
Tags Source
SHA-256 checksum
How to use checksums
bd639bf5fdf060f4d4c80323596d8aa7a37e5343101f760275fef95f70e53a97
BLAKE2b-256 checksum
How to use checksums
d7ad371d57865214be038162b0019e208419b9dac766ecd5124132e8f1b6c8b2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / ust_format_checker-0.2.0-py3-none-any.whl

Download URL ust_format_checker-0.2.0-py3-none-any.whl
Size 71.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4d1a22ea7339825938325ddd5d831f8941d98f0418ca8b5b42e90c926166497f
BLAKE2b-256 checksum
How to use checksums
eefdd8a4ae112901f3e5edcf28d5233ec31e349a0e79f976f1a5db129c767387
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page