MeshSonar: Drive format

Drive format

The open upload format MeshSonar uses, and the endpoints that accept it. Any server can implement it.

The upload format for MeshSonar, the open-source MeshCore wardriving app, and the matching CMAT endpoints. Any server can accept it; CMAT's implementation is src/lib/drive/ (validator: validate.ts, endpoints: handlers.ts).

The same JSON is what MeshSonar's Export writes on the phone, so a saved drive file can be uploaded later unchanged.

Principles

  • Never guess. A value the phone or radio couldn't measure is null. Out-of-range values are rejected, not clamped.
  • "Heard nothing" is data. A tx ping with "echoes": [] means pinged here, no repeater answered.
  • Retries are harmless. Every ping carries a UUID made on the phone. Re-sending a ping the server already has is counted as a duplicate and stored once.
  • Nothing secret in the app. Each driver gets their own key; the server stores only its SHA-256.

Upload body

{
  "format": "meshsonar.drive.v1",                 // required, exact
  "app": { "name": "MeshSonar", "version": "0.1.0", "platform": "ios" },
  "session": {
    "id": "uuid",                                 // required; made on the phone when the drive starts
    "started_at": "2026-10-15T21:00:00Z",         // required, ISO 8601
    "ended_at": null,                             // null while driving; must not be before started_at
    "label": "Westerville loop",                  // optional, ≤ 80 chars
    "channel": "#wardriving",                     // channel the pings were sent on, ≤ 40 chars
    "radio": { "freq_mhz": 910.525, "bw_khz": 62.5, "sf": 7, "cr": 5, "tx_dbm": 27.8 },
    "antenna": { "type": "external", "gain_dbi": 5.8, "height_m": 1.8 },
    "companion": { "public_key": "hex", "name": "KE4BFI-v4", "firmware": "1.9" }
  },
  "pings": [                                      // 0 to 5,000 per request; send more in further requests
    {
      "id": "uuid",                               // required
      "ts": "2026-10-15T21:02:11.384Z",           // required, ISO 8601; not more than a day in the future
      "lat": 40.128091, "lon": -82.977705,        // required (0,0 rejected)
      "gps_accuracy_m": 4.0, "speed_mps": 13.2,   // optional
      "kind": "tx",                               // "tx" (we pinged) or "rx" (overheard); default "tx"
      "noise_floor_dbm": -94,                     // optional
      "echoes": [                                 // required list; [] = heard nothing; ≤ 64
        { "repeater": "df0136", "snr_db": 12.75, "rssi_dbm": -71, "hops": 1, "path": ["df0136"], "delay_ms": 840 }
      ],
      "neighbor": null                            // two-way exchange when the firmware reports one:
                                                  // { "repeater", "local_snr_db", "local_rssi_dbm", "remote_snr_db" }
    }
  ]
}

Field rules

FieldRule
repeater, path[], companion.public_key, neighbor.repeaterHex key or prefix, whole bytes (even length, 2–64 chars). 0x and case are normalised.
snr_db, local_snr_db, remote_snr_db−40 … +40 dB
rssi_dbm, local_rssi_dbm, noise_floor_dbm−160 … 0 dBm
hopsInteger 0 … 64. 1 = the repeater was heard directly; only those measure a phone↔repeater link.
pathList of ≤ 64 hex keys, or null
delay_msInteger 0 … 600,000
gps_accuracy_m0 … 10,000
speed_mps0 … 150
radio.freq_mhz / bw_khz / sf / cr / tx_dbm100–3000 / 1–1000 / integer 5–12 / integer 5–8 / −10…40
antenna.gain_dbi / height_m−10…20 / 0…300
Text fieldsControl characters removed; trimmed; capped as noted

A ping with any invalid echo is rejected whole. Dropping only the bad echo could turn a ping that heard something into a false "heard nothing".

Problems with the whole upload (wrong format, bad session, more than 5,000 pings) fail the request with HTTP 400. Problems with individual pings don't: good pings are stored and the bad ones are listed.

Endpoints (CMAT: https://<host>/api/cmh/<name>)

EndpointAuthRequestResponse
driver-requestnonePOST {name, callsign?, email?}201 {driverId, key, status, display} — the key is shown once; store it on the device
driver-statusx-driver-keyGET{status, display}
drive-uploadx-driver-key (approved)POST upload body{sessionId, stored, duplicate, rejected, rejections[≤20]: {index, id, reason}}
drive-repeatersx-driver-key (approved)GET{repeaters: [{key, name, lat, lon}], at} — for offline repeater names
driver-adminx-admin-keyGET{mode, drivers[]}
POST {action: "approve" | "reject" | "revoke", driverId}{driverId, status}
POST {action: "mode", mode: "approval" | "open"}{mode}
drive-dataCMAT access code (x-access-token)GET{sessions[≤500], cells[], totals, cell} — community view, see below

Driver flow

  1. MeshSonar calls driver-request with the driver's name (and callsign/email if given) and stores the returned key.
  2. Approval mode (current): status is pending until the admin approves it. MeshSonar polls driver-status. Open mode: status is approved immediately.
  3. Approved drivers upload. Revoked or rejected keys get 403 {error, status}; unknown keys get 401.

Status changes: pending → approved | rejected, approved → revoked, rejected | revoked → approved.

Status codes

CodeMeaning
400Upload-level problem; error says what
401Missing or unknown driver key / admin key
403Driver key exists but isn't approved (status included)
409Session id belongs to another driver, or a status change that isn't allowed
429Too many key requests waiting for approval

Display names and privacy

The public handle (display) is the callsign, or "Driver N" without one. Real names and emails are visible to the admin only. Public views hide the first and last 500 m of each drive (the driver's trim_m) and any privacy zones the driver sets; the model still uses those points, and a driver's own export is never trimmed.

drive-data (CMAT community view)

Source: src/lib/drive/drive-read.ts (TypeScript, used by the dev mock and tests) and public.meshsonar_drive_read (drizzle/migrations/0004_drive_read.sql, used in production). The two give identical output.

  • sessions: newest first, at most 500 — {id, display, startedAt, endedAt, label, pings, heard, heardPct, antennaType, txDbm}. display is the public handle, or "Anonymous" when the driver opted out. pings/heard count tx pings.
  • cells: ~220 m squares (0.002° lat × 0.0025° lon) — {lat, lon (centre), pings, heard, bestSnrDb, repeaters[≤5]}. Only tx pings; bestSnrDb and repeaters (most pings first) use direct echoes only (hops 1 or null). Left out per session: pings within trim_m of the start or end, measured along the path and also in a straight line from the first/last point (so GPS jitter while parked can't reveal a driveway), and pings inside any privacy zone.
  • totals: {sessions, drivers, pings, heard} over every visible session.
  • Only approved or revoked drivers are included. Raw tracks are never returned.

Account and public endpoints

All at https://<host>/api/cmh/<name>. Errors are {error} with 400, 401, 403, 404, 409 or 429 (with Retry-After).

EndpointAuthRequest and response
site-confignoneGET → {viewerGate, driverRegistration, appLinks}
public-coveragenoneGET [?since=ISO] → {cellDeg, cells[], totals, asOf}: measured squares, drive ends trimmed, flagged drives excluded
public-statsnoneGET → {drivers, drives, pings, heardPct, km, repeatersHeard, lastDriveAt}
leaderboardnoneGET ?period=30d|all → {period, rows[]}: opt-in drivers, callsign only
me-driver-keyx-user-jwt (signed in)POST {callsign?} → {driver, key}: a new driver key linked to your account, shown once
me-driver-revokex-user-jwt (signed in)POST {driverId} → {ok: true}
me-drivesx-user-jwt (signed in)GET → {drives[]}: your own drives, untrimmed
CMAT — CMH MeshCore Analysis Tool: analysis and planning for central Ohio’s MeshCore networkMethod and data