JSON Schema

This schema is frozen. The visualiser depends on exact field names and types. Do not rename fields or change null behaviour. The firmware (data_logger.cpp) and the visualiser (/viz) both implement exactly this shape.


Full Schema

{
  "run_id": "run_001",
  "total_duration_s": 480.0,
  "sectors_completed": 4,
  "path": [
    { "t_s": 0.0, "x_mm": 0.0, "y_mm": 0.0 }
  ],
  "samples": [
    {
      "id": "W1",
      "type": "WATER",
      "sector": 1,
      "terrain": "Urban",
      "value_ntu": 42.3,
      "value_pct": null,
      "t_s": 45.2
    },
    {
      "id": "S1",
      "type": "SOIL",
      "sector": 1,
      "terrain": "Urban",
      "value_ntu": null,
      "value_pct": 68.5,
      "t_s": 78.1
    }
  ]
}

Field Reference

Top-Level

Field Type Description
run_id string Unique per run (run_<millis>) — a fresh document opens at every run start
total_duration_s number Run time in seconds (live during the run, frozen at End Run)
sectors_completed integer Count of distinct sectors with at least one sample
path array Position estimate, logged at 1 Hz while the run is live (capped at 480 points = 8 min)
samples array Measurement objects, max 12

Run clock: all timestamps count from run start (the first FWD / BWD / LEFT / RIGHT / SAMPLE_* command) — never from power-on. End Run freezes the document; the next drive/sample opens a fresh one, so back-to-back practice runs never bleed together.

Sample Object

Field Type Description
id string "W1""W4" for water, "S1""S8" for soil, numbered per type in collection order
type "WATER" or "SOIL" Sensor used
sector integer 1–4; 0 = unknown (no sector selected, or out of range 1–4)
terrain string From TERRAINS[] lookup by sector; "Unknown" for sector 0
value_ntu number or null NTU for WATER; null for SOIL
value_pct number or null Moisture % for SOIL; null for WATER
t_s number Seconds from run start

Path Point

Field Type Description
t_s number Seconds from run start
x_mm number Estimated X (origin = start position)
y_mm number Estimated Y

Path is a speed-model estimate (no encoders) — treat it as illustrative coverage, not ground truth. Samples carry no coordinates; sector labelling is the operator's job in RC mode.


Null Field Rules

Type value_ntu value_pct
WATER number null
SOIL null number

Both fields always present; the unused one is explicitly null, never omitted.


Terrain Names

Filled by firmware from the sector number via TERRAINS[] in data_logger.cpp:

Sector terrain
0 "Unknown"
1 "Urban"
2 "Field"
3 "Desert"
4 "Stone"

The full lookup is TERRAINS[5] = {"Unknown", "Urban", "Field", "Desert", "Stone"}.


Storage and Access

  • Every sample is also printed to serial with units in the [SAMPLE] S<n> <TYPE> <value> format — parsed by live_plot.py and rulebook-approved on its own. Never change this line's shape.
  • GET http://192.168.4.1/data returns the full run JSON at any time.
  • GET http://192.168.4.1/samples returns a lightweight samples-only feed (no 48KB path array) — polled every 2s by live_plot.py during the run.
  • End Run persists results.json to SPIFFS and freezes the document.
  • After a reboot, /data serves the saved results.json — judges can still pull data if the robot power-cycled.
  • live_plot.py additionally appends every sample to samples.csv on the laptop — a second approved output format for free.

To archive: on RobotAP, use the RC page ⬇ Download JSON (or open 192.168.4.1/data), save the JSON, back it up to two devices, then drag-drop into the visualiser.