← LORA Tunnel beta installer · User guide

Release 0.2.2-beta.1 · Public beta · 794956a35501

Native session format, schema 1

The primary record is /sessions/LT-xxxxxxxx.jsonl on microSD. Each complete line is one UTF-8 JSON object. Preserve these native session files even after export or upload: they retain observations and raw radio bytes that a mapping CSV cannot represent. The formal structural contract is schemas/observation-v1.schema.json (JSON Schema draft 2020-12).

Common fields and time

Field Meaning
schema Integer format version, currently 1.
type session_start, session_end, wifi, ble, or lora.
capture_mode wardriver or recon. Optional for older schema-1 files, whose missing mode is treated as wardriver by exporters.
session Eight lowercase hex digits, generated per session. Filename collisions are rejected; this is not a device identifier or globally guaranteed UUID.
seq Serialization sequence beginning at 1 in this session. A gap can indicate a serialization/drop condition; no gap does not prove every radio packet was received.
monotonic_ms ESP32 monotonic time in milliseconds since boot. It can order observations without a GNSS clock, but does not supply UTC or bridge reboots.
utc Latest sufficiently fresh GNSS sentence time, YYYY-MM-DDTHH:MM:SSZ, otherwise null. No boot-time-derived or invented UTC.
utc_age_ms Age of the GNSS time sentence in milliseconds, otherwise null. This differs from position-fix age.
timestamp_basis Current writer: gnss_latest_sentence. This is not a hardware timestamp of the received packet.
gps Receiver fix snapshot at observation submission, described below.

Current firmware only emits UTC when date and time sentences are valid and at most 1500 ms old. It uses the last sentence's UTC rather than extrapolating a wall clock between sentences. Multiple observations can share a second. Wi-Fi scan results also contain wifi.timestamp_basis:scan_result_delivery: several APs in a scan can share a delivery time and GPS snapshot. This does not claim each beacon arrived at precisely that timestamp/location. utc_age_ms and the top-level timestamp basis are optional in the schema for earlier v1 fixtures; new firmware writes them explicitly.

gps.valid is true only for a currently accepted receiver fix, no older than 10000 ms; recent invalid RMC/GGA status invalidates it. gps.age_ms records position age, or null if unavailable. When invalid, lat, lon and alt_m are null. When valid, latitude/longitude are real in-range receiver coordinates; altitude can still be null. hdop is dimensionless and may be null. sats is the available satellite count; zero also represents unavailable/stale satellite information in this milestone. GNSS altitude/HDOP/satellite sentence ages are not separately retained yet. No current field is a guaranteed metre accuracy.

The optional extension gps.accuracy_m means an actual receiver-provided horizontal accuracy in metres. Current firmware does not emit it. Exporters must never convert HDOP directly to that field or fabricate zero accuracy.

Session markers and counters

Start/end markers add firmware, board and counts. Counters include ingress drops, log drops, storage errors, radio errors, UI skips and maximum observed SD flush duration in milliseconds. Counter values are cumulative since boot; they are not reset just because a new session starts. Compare marker snapshots to calculate a session delta. Missing session_end means orderly closure was not recorded; keep complete earlier lines and treat the tail as interrupted.

The live written implementation counter means records serialized into the write buffer, not guaranteed durable records. A completed write/flush is stronger evidence, and physical power-loss tests are still required. Drop counters cannot count packets that the radio never received during another channel/dwell or a contention interval. Storage failure can itself prevent final counters from reaching the file; capture visible diagnostics where practical.

Wi-Fi observation

wifi contains BSSID, SSID when available, channel, MHz frequency, RSSI in dBm, security classification and the scan-result time basis. This build passively scans 2.4 GHz channels 1–11; schema 1 permits up to channel 14 for future regional configuration. Wi-Fi and BLE scanning are deliberately scheduled in turns.

Recon captures raw 802.11 frames separately from AP scans. Such records have wifi.wifi_frame:true, tx_address, and raw_frame_hex, with no bssid. The transmitter can be a client or NAN peer; it must not become an invented AP identity. At most 1024 bytes of a frame are retained after removing its FCS. original_len, captured_len, frame_truncated, and fcs_present:false describe that boundary. timestamp_basis:packet_callback distinguishes these records from scan-result deliveries. The WiGLE exporter excludes raw Wi-Fi frames even when an input record has a missing or incorrect capture mode.

Wi-Fi ssid is a bounded display-safe UTF-8 string, up to the 32-byte SSID limit; invalid bytes are replaced by ? and ssid_sanitized marks that change. ssid_hex retains the original bytes returned by the ESP32 scan SDK before text sanitization, up to 32 bytes. This is not a complete raw-beacon capture: an embedded NUL can limit the SDK C-string result, so any suffix beyond it remains unknown. An empty SSID means hidden/unavailable, not a fabricated name. The WiGLE exporter recovers exact valid UTF-8 from ssid_hex when sanitization was reported; if those bytes are missing, malformed or not valid UTF-8, it leaves the exported SSID blank and counts that outcome. It never exports a replacement ? as though it were an observed name. security reflects the ESP32 scan classification; it is not a complete negotiated-security audit. Preserve these limitations when describing recon evidence or exporting names.

BLE observation

ble contains address, address type, RSSI, bounded advertised name, raw payload_hex, payload_truncated, and optional manufacturer ID or malformed advertisement flag. Address types are public, random, public_identity, random_identity or unknown, following the scanner's reported type. A random address is an observation key, not proof of a persistent physical identity.

The raw payload retains at most 512 bytes, represented as at most 1024 hex characters. payload_truncated:true means more bytes were presented than kept. original_len and captured_len retain those sizes. The optional advertising_len preserves NimBLE's ADV/SCAN_RSP boundary within the captured bytes; zero means no separate boundary is available. Current scans are passive, but any concatenated input must be parsed as separate segments. Zero padding ends only its own segment, and an AD element cannot consume the next segment. Earlier schema-1 captures limited payloads to 255 bytes and remain valid. The name is limited to 39 bytes plus terminator. Valid UTF-8 is preserved; name_sanitized marks invalid-byte replacement in the display-safe string. The CSV exporter leaves a sanitized BLE name blank, with a count, while the native raw advertising bytes remain available for later name/service decoding. Service/manufacturer structures can be interpreted later from those raw bytes. The current convenience manufacturer_id takes the little-endian ID from a valid manufacturer AD element; if multiple such elements exist, the last parsed one wins. Raw payload remains authoritative. An invalid AD length or segment boundary sets malformed_ad and omits the convenience manufacturer ID without reading beyond the buffer. This milestone does not flatten all services into separate fields or persist a complete extended-advertisement stream beyond the bound.

Recon evidence

Wardriver sessions retain observations for ordinary inventory/export. Recon sessions retain matched observations in enabled categories with their evidence; they are not complete Wi-Fi/BLE inventories. recon contains category, confidence, label, vendor, evidence, and payload_valid. Confidence names describe signature strength, not measured probabilities. Vendor matches and spoofable identifiers do not establish a product, operator, purpose, or recording state. Local rules and category/confidence settings control matches, display and alerts. Offline WiGLE and WDGWars mesh exporters exclude Recon and unknown modes.

When actually decoded, recon.remote_id keeps a self-reported broadcast ID, id_type, aircraft type, and whichever aircraft/operator coordinates, altitude and speed are available in that observation. It records source:self_reported_broadcast and authenticated:false; no broadcast authentication has been established. Operator coordinates include the reported operator_location_type and do not automatically mean a current pilot position. Missing fields are omitted, not filled from earlier packets or receiver GPS. Receiver location remains in the separate top-level gps object.

A parsed recon.ibeacon contains UUID hex, major/minor and measured power. These values describe a common proximity-beacon format, not a permanent physical identity or evidence of malicious activity. Native raw bytes remain available for later interpretation.

LoRa reception and candidate classification

lora contains the fixed receive profile: MHz frequency, kHz bandwidth, spreading factor, coding-rate denominator, sync word, preamble symbols, profile name and LNA setting. It also stores RSSI, SNR, packet length and raw received bytes in raw_hex, bounded to 255 bytes. original_len and truncated retain any difference between supplied and retained lengths. LoRa RSSI is stored as an integer, so fractional driver RSSI resolution is not preserved; SNR remains floating point. The current band is 902–928 MHz. The receiver can listen to only one modem/frequency profile at a time. Radio settings come from the session's fixed configuration; changes while capturing/hopping are not implemented.

radio_read_status:ok records successful driver delivery. crc_status:not_reported is deliberate. A successful RadioLib read is not an independently retained CRC-observation flag. payload_status:not_decoded means no payload parsing/decryption was performed; it does not claim all traffic is encrypted. Unknown payload bytes remain useful and are preserved.

classification is unknown or meshtastic_header_candidate in this build. A candidate adds candidate_header with eight-digit from/to/packet IDs, channel hash, hop-limit/start, MQTT/ACK flags and raw next-hop/relay bytes. These are a plausible interpretation of a packet's bytes, not verified source identity, authenticated protocol classification, decoded position or proof that the original sender is near the receiver. A header alone cannot justify a human-readable node name, message or WDGWars mesh record. relay_fields_available indicates the parser has a nonzero hop-start value; do not turn an unavailable field into a zero-hop/direct claim.

Future decoded mesh extensions may use a top-level mesh object. The schema allows that extension but does not infer it from candidate_header or validate it as an authenticated sender. The experimental offline exporter has separate strict eligibility rules in docs/EXPORTS.md. Any advertised node position belongs in that extension with its source explicitly labelled; it must never overwrite the receiver's gps object.

Summaries, bounds and interrupted logs

The UI maintains 64 shared RAM summary slots with least-recently-seen replacement. Wi-Fi/BLE summaries use kind/address/type; LoRa summaries use a clearly marked candidate sender or packet fingerprint. A summary eviction discards only that RAM view. Observation logging is requested before summary update, so eviction does not erase historical observations already captured in the native log. Summary totals are bounded sightings, not a census of unique physical devices or a full session database.

The current pipeline uses a 48-observation capture queue and 32-observation logger queue, both with explicitly allocated PSRAM payload storage. The observation's raw array has 1024 bytes; acquisition/serialization bounds are Wi-Fi 1024, BLE 512 and LoRa 255 bytes. The logger has an 8192-byte static JSON document arena, 8192-byte SD write buffer, and 12288-byte writer stack (ESP-IDF specifies task stack sizes in bytes). Missing queue memory makes initialization fail explicitly. Oversized/overflowing JSON records increment the log-drop counter rather than overflowing the write buffer. Writer flushes occur approximately every two seconds, on buffer pressure, and at orderly stop. Queue overflow, malformed records and write errors have visible counters. SD writes share a mutex with TFT operations; slow-card effects require hardware measurement. These limits bound application allocations but do not prove total framework heap use or watchdog behavior under load.

A JSONL parser should process complete lines independently. Ignore/report a truncated final line without throwing away valid earlier records. Never append a fresh session over an old session file. The Python exporter uses a separate 64 KiB maximum input-line limit so it can reject corrupt/untrusted oversized records without unbounded memory growth; it streams the file and reports skips.

Schema validation checks structure, not all semantics. In particular, compare LoRa byte length to length, Wi-Fi channel to frequency, timestamp freshness, and protocol decode evidence in code. A syntactically valid example is not a real observation. Files marked synthetic:true are test fixtures and must not be uploaded.

SD configuration

schemas/config-v1.schema.json describes /lora-tunnel/config.json. Implemented sections include lora, schedule, recon, ui.buttons_enabled, and credentials. A missing config uses this user's confirmed United States / 915 MHz defaults with US LongFast reception enabled. An existing invalid config without a usable backup disables LoRa and reports the problem. A valid previous configuration may be recovered from config.bak; saving credentials uses a validated temporary file before promotion and preserves a usable backup.

Both config and the separate /lora-tunnel/detection_rules.json input have a 16 KiB file cap and fixed 16 KiB JSON arena. Parsed rule count is limited to 64; rule types support exact case-insensitive name match, Wi-Fi SSID prefix, OUI prefix, BLE service16 and manufacturer identifiers. BLE identifier rules respect advertising boundaries and require valid AD structure. OUI matching excludes randomized BLE and locally assigned addresses. Vendor/name evidence is not proof of a device's purpose. Rule labels/matches are bounded to 47 bytes. The rule file is separate from credentials.

Credential buffers hold at most 127 UTF-8 bytes each. JSON Schema maxLength counts characters, so firmware byte limits remain authoritative for non-ASCII input. Config strings longer than firmware buffers can be truncated on load; keep them within their byte limits. Plaintext SD credentials are masked in UI and excluded from logs/source control. Storing them does not mean firmware uploads or auto-upload are implemented. This milestone has no upload scheduler or network submission control; additional unrecognized config fields are forward-compatible data, not active features.