Metadata-Version: 2.4
Name: ust-format-checker
Version: 0.1.1
Summary: Checks that xDOS firmware keeps its output format backward compatible
Author: Universal Scientific Technologies
License-Expression: GPL-3.0-or-later
Project-URL: Repository, https://github.com/UniversalScientificTechnologies/DOSPORTAL
Project-URL: Issues, https://github.com/UniversalScientificTechnologies/DOSPORTAL/issues
Keywords: xdos,dosimetry,firmware,simavr,platformio,backward-compatibility
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Embedded Systems
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML>=6
Dynamic: license-file

# UST Format Checker

Checks that xDOS firmware keeps its output format backward compatible by running the
firmware in [simavr](https://github.com/buserror/simavr) against a deterministic scenario
and inspecting what it prints on UART0.

Plan and requirements: [PLAN.md](docs/PLAN.md). Per-message rules: [CHECKS.md](docs/CHECKS.md).
Where docs, parser and firmware disagree: [DOC_DEVIATIONS.md](docs/DOC_DEVIATIONS.md).

Status: phases F1-F3 done (checks, report, CLI, board and scenarios as data, PlatformIO hook),
driven against [AIRDOS03_USTDFF](https://github.com/UniversalScientificTechnologies/AIRDOS03/tree/AIRDOS03B/fw/AIRDOS03_USTDFF).

## Installing

```sh
pip install "ust-format-checker @ git+https://github.com/UniversalScientificTechnologies/DOSPORTAL@<commit>#subdirectory=backend/packages/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).

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](messages.yaml) - the message schema: fields, types, ranges, which rules apply.
- [schema.py](schema.py), [checks.py](checks.py), [rules.py](rules.py) - schema loading and layer 1 checks.
- [compat.py](compat.py) - layer 2: append-only comparison against a golden capture.
- [parser_layer.py](parser_layer.py) - layer 3: what `packages.parsing` reads out of the same log.
- [stimulus_checks.py](stimulus_checks.py) - the value the simulation injected has to be the value that comes out.
- [regressions/](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.
- [report.py](report.py), [cli.py](cli.py) - rustc-style report and the `xdos-check` entry point.
- [components/](components) - what a component is and which runner directive drives it. Shared by every device.
- 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](scenario.py), [build.py](build.py) - board + scenario -> runner directives + the stimulus to check against.
- [simulator/runner.c](simulator/runner.c) - loads the firmware ELF into simavr, attaches component models, replays the directives, prints UART0.
- [docker/](docker) - a reproducible toolchain (simavr, PlatformIO) for CI and for reproducing
  a finding on someone else's machine. Day-to-day development does not need it.

## Checking a captured log

```sh
cd backend
python -m packages.ust_format_checker <log>                        # schema + parser layer
python -m packages.ust_format_checker <log> --golden <reference>   # + compatibility layer
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.

The reference for the compatibility layer is a plain captured log, so a format change shows up
as a readable diff in the device repository. Recording one after a deliberate format change:

```sh
python -m packages.ust_format_checker <log> --golden xdos/golden/basic.txt --accept
```

Tests:

```sh
python -m pytest packages/ust_format_checker -q
```

## Checking a firmware ELF

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

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

`--cache` skips the run when the ELF has not changed. A missing simulator is reported and exits
0, so a firmware build never fails because the checker could not run.

## 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:

```sh
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 \
    --golden $A/xdos/golden/basic.txt --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 a second:

```sh
python -m pytest packages/ust_format_checker -q
```

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:

```yaml
# 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:

```yaml
name: basic
stop_blocks: 2
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>` | adds to a 32-bit counter in the firmware's RAM; the symbol is resolved from the ELF |

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