Export overview · 2.0.1 (54)
Export format
Formats and standards
Export overview checked against app 2.0.1 (build 54), delivery protocols and the TXT V6 recording dictionary.
The overview below matches the 2.0.1 export registry. The detailed TXT dictionary further down was based on 1.5.2 with later additions and is not a complete list of all 2.0.1 payloads. The TXT V6 format identifier is unchanged.
Set for paper: no colour fills, no navigation. The same dialog saves it as a PDF.
Formats and standards · 2.0.1
Three recording containers and six conversions. Conversions select part of the data; use tar.gz to archive the full measurement with media.
| Format | File | Standard / schema | Content and limits |
|---|---|---|---|
| TXT | .txt | Signal Collector TXT V6 · UTF-8 · TSV | Full sample rows and session metadata. Photos and audio are separate files. |
| GZIP | .txt.gz | GZIP + TXT V6 | The same text recording, compressed; use the archive to include media. |
| Archive | .tar.gz | TAR + GZIP + TXT V6 | Recording in data.txt with photos and voice notes in media/. |
| CSV | .zip | CSV · RFC 4180 escaping · ZIP | One CSV per source plus header.txt. No attached media. |
| KML | .kml | OGC KML 2.2 | Map track and survey points. Points without a preceding location fix are skipped. |
| OpenCelliD | .csv | OpenCelliD upload CSV | Cell observations paired with location, without a header row. Incomplete observations are skipped. |
| MLS | .json | MLS Geosubmit v2 | Location, cells and Wi-Fi for compatible services such as BeaconDB. SSIDs are omitted; _nomap networks are excluded. |
| RINEX | .rnx | RINEX 3.04 observations | Requires gnss_raw. Observation data only, no navigation file; unsupported or invalid observations are skipped. |
| Protocol | .html | HTML · PDF via system printing | Summary report with embedded photos; audio is represented by duration. PDF is produced by the print dialog. |
Delivery protocols
Delivery determines where data goes; the format determines the contents of the file.
- HTTP over local Wi-Fi: preview, downloads and live JSON / NDJSON / CSV output.
- MQTT: publish selected live measurements to a configured broker.
- S3-compatible storage, WebDAV and SFTP: delivery of exported files.
File structure
The export is a UTF‑8 text file. Depending on the estimated size it is stored as .txt or packed into GZIP. It has three parts: a first line naming the format, a header block of key=value pairs, a blank line, and then a tab-separated table of samples.
A measurement that carries photos or voice notes is exported as a tar.gz archive instead, because gzip compresses a single stream and cannot hold a folder. The text file inside is exactly the same one, only named data.txt:
signal-collector-42/
data.txt
media/p7-1786439642118.jpg
media/p9-1786439701244.m4a
SIGNAL_COLLECTOR_TXT_V6
session_id=42
session_status=COMPLETED
…
sources=accelerometer,collector_heartbeat,gnss_raw,location,wifi
mark_media_count=2
mark_media_dir=media
timestamp_iso_utc epoch_ms elapsed_realtime_ns session_id sequence_number source payload
2026-08-10T09:14:02.118Z 1786439642118 918273645000 42 1 location record_type=fix;provider=gps;latitude=50.087;…
Samples are ordered by timestampEpochMs and, on a tie, by insertion order. A row never contains a raw tab or line break — see escaping.
Session header
It describes the whole measurement run: when it started, how it ended and how many samples passed through each stage. The header is written by MeasurementRepository.exportLatest from a closed session.
| Key | Type | Description |
|---|---|---|
SIGNAL_COLLECTOR_TXT_V6 | text | The first line of the file, not a key=value pair. The format version; it goes up whenever the structure changes. Files exported by earlier versions of the app carry SIGNAL_COLLECTOR_TXT_V5 and have no chunk_* keys and no session_record_count; in those, record_count is the count for the whole measurement and the file is always whole. They stay readable exactly as they are — nothing rewrites them. |
mark_media_count | long | How many photos and voice notes belong to the measurement. In a plain TXT it is a warning that the file is not the whole measurement; in an archive it must match the contents of media/. |
mark_media_dir | text | The folder the media sit in inside the archive; always media. |
session_id | long | ID of the session in the local database. The same value appears in the session_id column of every row. |
session_status | enum | COMPLETED ordinary Stop · FAILED the write or the start failed · INTERRUPTED the process died and the session was finished off by the next app start. |
end_reason | enum | USER_STOP · WRITER_ERROR · START_ERROR · PROCESS_RECOVERY. Empty only for records from versions that did not have the field yet. |
started_at | ISO‑8601 | Creation of the session in UTC, that is, the moment Start was first pressed. Resuming does not overwrite it. |
ended_at | ISO‑8601 | End of the session, that is, the last Stop. Never earlier than the last confirmed write, even when the callback arrived while the collectors were shutting down. |
running_ms | long | Pure collection time: the sum of the running stretches without the pauses between a Stop and the next Start. For a session with a pause it is smaller than ended_at − started_at; the length of each pause can be derived from the events of the session source. |
last_record_at | ISO‑8601 | Time of the last sample in the file (MAX(timestampEpochMs)). |
last_persisted_at | ISO‑8601 | Time of the last sample the writer actually confirmed into the database. |
tail_gap_ms | long | ended_at − last_persisted_at, at least 0. How much time at the end of the session was left without data — units to hundreds of ms on a healthy run. |
device | text | A nested manufacturer;model;device;android;sdk, escaped the same way as a payload. |
chunk_index | int | Which part of the measurement this file is, counted from 1. Together with chunk_count=1 it means the whole measurement is in this single file. |
chunk_count | int | How many parts the measurement was split into. The same pair is repeated in the file name as -03of07 — the only way a recipient can tell that a part is missing. |
chunk_reason | enum | user the boundary was set by the operator at a pause · size the export cut the file at its size limit. Empty when nothing was split, which is what a single-file export writes. |
chunk_label | text | Optional name the operator gave the part, for example 2nd floor. Escaped like a payload. Empty when the part has no name. |
chunk_label_part | int | Filled only when a named part had to be cut further by size; it numbers the pieces sharing that one name. Empty otherwise. |
chunk_started_at | ISO‑8601 | Time of the first sample in this file. |
chunk_ended_at | ISO‑8601 | Time of the last sample in this file. Equal to last_record_at as long as nothing is split. |
record_count | long | Number of sample rows in this file. |
session_record_count | long | Number of sample rows in the whole measurement, across all its parts. Equal to record_count as long as nothing is split; the two part company the moment it is. Check a file against record_count, never against this one. |
accepted_samples | long | How many samples the write queue accepted. A collector hands its sample over and carries on; it never waits for the disk. |
written_samples | long | How many samples the writer confirmed into the database. The difference against accepted_samples is the queue that did not get through in time. |
dropped_samples | long | How many samples the queue refused because it was full (capacity 20,000). The sample is lost, but it is counted — silence in the data cannot be mistaken for loss. |
distance_m | double | Distance walked, derived from GPS fixes, to one decimal place. It is not measured by summing neighbouring fixes but by anchor moves — a phone lying on a desk therefore walks not one metre. |
distance_max_accuracy_m | float | Fix accuracy threshold for distance (20.0 m). A worse fix does not contribute to the distance at all. |
writer_error | text | Type and message of the exception that brought the write down. Empty = no error. |
sources | list | Alphabetical list of the sources that actually wrote something in this session — not a list of ticked switches. It includes collector_heartbeat, haptic, session and mqtt as well. |
TSV columns
Seven tab-separated columns. The first six are the same for every source; the seventh is the payload of that particular source.
| Column | Type | Description |
|---|---|---|
timestamp_iso_utc | ISO‑8601 | A readable transcription of epoch_ms in UTC. A derived column, no extra information. |
epoch_ms | long | System time when the sample was accepted into the queue. It allows comparison with other devices, but it can jump when the phone's clock is reset. |
elapsed_realtime_ns | long | Monotonic time since the phone booted; it keeps running during sleep. This is the time axis for measuring intervals. Most sources take it straight from the Android API (fix, scan, cell, BLE, GNSS clock), so it matches the moment of measurement, not the moment the callback arrived. |
sequence_number | long | Row order in the file, from 1. It is assigned at export time; it is not a database ID. |
source | enum | Source of the sample — one of the values described below, or collector_heartbeat. |
payload | text | key=value pairs separated by semicolons. The record_type key says which payload shape follows. A missing value is written as empty — the key stays in the row. |
Escaping
Payloads such as raw_scan_result, NMEA sentences or exception text routinely contain characters that would break the structure of the file. Escaping is therefore always applied, even when a particular value holds no problematic character.
| Character | Written as | Where |
|---|---|---|
\ | \\ | payload, header and TSV columns |
; | \; | inside a payload and the header — otherwise it would look like a pair separator |
= | \= | inside a payload and the header — otherwise it would look like a key/value separator |
| tab | \t | the source and payload columns, values in the header |
| CR / LF | \r / \n | everywhere |
A parser therefore splits the row on tabs first, then the payload on unescaped semicolons, then each pair on the first unescaped equals sign — and unescapes the value last.
Shared payload suffixes
Some keys do not come from the source itself but are attached to the sample along the way. That is how even an accelerometer sample carries the last known location without knowing anything about location.
Location with every sample
Added to every sample for which the last location is known. If a source already carries a location of its own, it is not attached again. For internet_quality it is always attached — empty if need be, so the data shows that no location was available.
| Key | Type | Description |
|---|---|---|
last_location | bool | Whether a location was available. |
last_latitude | double | Latitude of the last fix. |
last_longitude | double | Longitude of the last fix. |
last_altitude | double | Altitude, only when the fix carried one. |
last_location_accuracy | float | Reported horizontal accuracy of the fix, in metres. |
location_age_ms | long | Age of the fix relative to this sample. Without it there would be no telling whether the location is one second or five minutes old. |
Network context with internet mapping
Attached only to internet_quality samples, tying the measured transfer to a specific network. The set of keys differs by transport.
| Key | Transport | Description |
|---|---|---|
ssid, bssid, rssi_dbm, rssi, frequency_mhz, frequency, channel, wifi_standard, rx_link_speed_mbps, tx_link_speed_mbps | WIFI | A snapshot of the connected Wi‑Fi at the moment of the sample. Unknown values are left empty; Android placeholder values (RSSI −127, frequency −1) never reach the export. |
operator, mcc, mnc, plmn, technology, cell_id, nci, pci, tac, bands, band, earfcn, nrarfcn, rsrp, rsrq, sinr, ss_rsrp, ss_rsrq, ss_sinr | CELLULAR | A snapshot of the serving cell. On 5G, cell_id is filled in from nci and rsrp/rsrq/sinr from ss_*, so the series can be compared across technologies. band is now computed from the channel number for every technology (LTE, NR, WCDMA, GSM), not just the system's own band list, which Android only reports for LTE/NR — see mobile_network. |
network_context_age_ms | both | Age of the network snapshot relative to the sample. |
network_handle, network_request_transport, network_request_internet, network_has_internet, network_validated, interface_name, subscription_id | both | Proof of which Android Network the transfer actually went through. Without it, samples could not be told apart when Wi‑Fi and cellular are measured at the same time. |
Sensors
source: accelerometer · gyroscope · magnetometer · barometer · light · proximity · rotation_vector
All seven sensors share the same payload shape. Values are taken exactly as SensorEvent returns them, with no filtering and no conversion. elapsed_realtime_ns is the time of the sensor event, not the time of delivery.
| Key | Type | Description |
|---|---|---|
v0, v1, v2, … | float | Axes by sensor type: accelerometer m/s² (x, y, z) · gyroscope rad/s · magnetometer µT · barometer hPa (v0 only) · light lx · proximity cm · rotation vector quaternion (v0–v3, and possibly v4 as the accuracy in radians). |
accuracy | int | Reported sensor accuracy 0–3 (0 = unreliable, 3 = high). |
sensor | text | Name of the specific hardware sensor as the device reports it. |
temperature
source: temperature · record_type: none, the payload always has the same shape
None of these is the temperature of the air around you. Every thermometer here sits inside the phone, which heats itself — the more so the longer it measures — so all of them read above the room, and the difference between them grows during a measurement. Outdoor or room temperature comes only from an external sensor over BLE (bluetooth_ble).
The phone exposes several thermometers and the app writes every one of them, each as its own row, distinguished by probe. A phone rarely carries the standard ambient sensor; what it does carry are thermometers inside chips, put there to compensate their own drift.
| Key | Type | Description |
|---|---|---|
v0 | float | Temperature in °C as the probe reports it, unfiltered. |
probe | text | What the thermometer measures: ambient the standard ambient sensor, the only one meant for the surroundings and still inside a warm phone · pressure_sensor the thermometer in the barometer chip · gyroscope the thermometer in the gyroscope, a service reading of a chip that heats with use · battery the battery, a large slow mass that shows how the whole instrument warmed up · other a vendor thermometer this version does not recognise. |
primary | bool | Present on the one probe the app treats as the leading one, in the order ambient, barometer, gyroscope. The live trace on screen and the value cell on the home screen are drawn from this row alone, so the line stays one quantity. |
sensor | text | Name of the hardware sensor as the device reports it, or battery. |
accuracy | int | Reported sensor accuracy 0–3. Battery rows do not have it. |
charging | bool | Battery rows only: was the phone charging. Charging warms it, which is otherwise the unexplained rise in the curve. |
level_pct | int | Battery rows only: charge level in percent, or empty when the device does not report it. |
noise
source: noise · record_type: none, the payload always has the same shape
Only the loudness of a block of samples is computed from the microphone. No audio is stored or sent anywhere; audio cannot be reconstructed from the stored values.
| Key | Type | Description |
|---|---|---|
rms | double | Root mean square of the block, in units of 16-bit PCM. |
dbfs | double | Loudness in dBFS relative to full scale, floored at −120. It is a relative level, not calibrated dB SPL. |
sample_rate_hz | int | The actual sample rate at which the microphone could be opened. |
samples | int | Number of PCM samples in this block. |
location
source: location · record_type: fix · registration
record_type=fix
| Key | Type | Description |
|---|---|---|
provider | enum | gps or network. Both providers run at once when enabled. |
latitude / longitude | double | Position in degrees (WGS‑84). |
altitude | double | Height above the ellipsoid in metres, only when the fix carries it. |
speed_mps | float | Speed in m/s. |
bearing_deg | float | Bearing of travel in degrees. |
horizontal_accuracy_m | float | Radius of 68 % confidence in metres. Above 20 m the fix does not count towards distance_m. |
vertical_accuracy_m | float | Uncertainty of the altitude. |
speed_accuracy_mps | float | Uncertainty of the speed. |
bearing_accuracy_deg | float | Uncertainty of the bearing. |
location_time_epoch_ms | long | Time of the fix according to the location provider — it need not match the row's epoch_ms. |
location_elapsed_realtime_ns | long | Monotonic time of the fix; the row's elapsed_realtime_ns derives from it. |
is_mock | bool | A mocked location. It does not count towards the distance. |
raw_location | raw | toString() of the Location object, in case some new attribute is missing in the future. |
record_type=registration
| Key | Type | Description |
|---|---|---|
callback | text | Always LOCATION_UPDATES. |
provider | enum | Which provider the registration concerns. |
provider_enabled | bool | Whether the provider was enabled in the system. |
status | enum | SUCCESS · PROVIDER_DISABLED · FAILED. It tells a disabled GPS apart from a refused registration. |
registered | bool | Whether the subscription to fixes was actually created. |
error / message | text | Class and message of the exception if the registration failed. |
gnss_satellite
source: gnss_satellite · record_type: satellite · summary · event · registration · callback_error
On every change of satellite status, one row per satellite is written plus a single summary. The summary is precomputed deliberately — otherwise everyone processing the data would have to compute it.
record_type=satellite
| Key | Type | Description |
|---|---|---|
constellation | enum | GPS · GALILEO · GLONASS · BEIDOU · QZSS · NAVIC · SBAS · UNKNOWN. |
svid | int | Identifier of the satellite within its constellation. |
cn0_db_hz | float | Carrier-to-noise density in dB‑Hz. The main indicator of satellite signal strength. |
baseband_cn0_db_hz | float | C/N0 measured at baseband, when the chip reports it. |
elevation_deg / azimuth_deg | float | Position of the satellite in the sky. |
carrier_frequency_hz | float | Carrier frequency of the signal — it tells L1 from L5. |
used_in_fix | bool | Whether the satellite was used in computing the position. |
has_ephemeris / has_almanac | bool | Whether the receiver holds ephemeris / almanac data for the satellite. |
record_type=summary
| Key | Type | Description |
|---|---|---|
satellites_visible / satellites_used | int | How many satellites the receiver sees and how many it used for the fix. |
fix_use_ratio | double | Ratio of used to visible. |
cn0_avg_db_hz / cn0_max_db_hz | double | Mean and best C/N0 across all visible satellites. |
used_cn0_avg_db_hz | double | Mean C/N0 of the satellites used in the fix only. |
ephemeris_count / almanac_count | int | How many satellites hold ephemeris / almanac data. |
gps_count, galileo_count, glonass_count, beidou_count, qzss_count, navic_count, sbas_count, unknown_count | int | Counts of visible satellites per constellation. |
record_type=event / registration / callback_error
| Key | Type | Description |
|---|---|---|
event | enum | GNSS_STARTED · GNSS_STOPPED · GNSS_FIRST_FIX. |
ttff_ms | int | Time to first fix — only with GNSS_FIRST_FIX. |
callback | text | GNSS_STATUS for a registration. |
status, registered, error, message | mixed | Result of the registration, the same shape as with location. |
gnss_raw
source: gnss_raw · record_type: clock · measurement · agc · capabilities · registration · status · callback_error
Raw GNSS measurements for post-processing. One clock record plus as many measurement rows as the chip tracked satellites in that epoch.
record_type=clock
| Key | Type | Description |
|---|---|---|
measurement_count | int | How many measurement rows belong to this epoch. |
full_tracking | bool | Whether the chip runs in full tracking mode (Android 14+). |
time_nanos | long | Hardware time of the receiver in nanoseconds. |
time_uncertainty_nanos | double | Uncertainty of the hardware time. |
full_bias_nanos / bias_nanos | long / double | Coarse and fine offset of the receiver clock against GPS time. |
bias_uncertainty_nanos | double | Uncertainty of the clock offset. |
drift_nanos_per_second | double | Drift of the receiver clock. |
drift_uncertainty_nanos_per_second | double | Uncertainty of the drift. |
leap_second | int | Current number of leap seconds. |
hardware_clock_discontinuity_count | int | Number of clock jumps. A change in the value means phase must not be carried across continuously. |
clock_elapsed_realtime_ns | long | Tie of the receiver clock to the system monotonic time. |
clock_elapsed_realtime_uncertainty_ns | double | Uncertainty of that tie. |
reference_constellation, reference_carrier_frequency_hz, reference_code_type | mixed | Reference signal for inter-system biases. |
raw_clock | raw | toString() of the GnssClock object. |
record_type=measurement
| Key | Type | Description |
|---|---|---|
constellation / svid | enum / int | Constellation and satellite number. |
code_type | text | Type of the signal code, for example C, L, Q. |
state | int | Bit mask of the tracking state. It determines whether received_sv_time_ns is unambiguous. |
received_sv_time_ns | long | Received satellite time — the basis for pseudorange. |
received_sv_time_uncertainty_ns | long | Uncertainty of the received time. |
time_offset_ns | double | Offset of the measurement against the time in clock. |
cn0_db_hz / baseband_cn0_db_hz / snr_db | double | Signal strength and signal-to-noise ratio. |
carrier_frequency_hz | float | Carrier frequency; it tells L1 from L5. |
carrier_cycles, carrier_phase, carrier_phase_uncertainty | long / double | Carrier phase measurements. |
pseudorange_rate_mps | double | Rate of change of the pseudorange in m/s (Doppler measurement). |
pseudorange_rate_uncertainty_mps | double | Uncertainty of the same. |
accumulated_delta_range_m | double | Accumulated range change from the carrier phase. |
accumulated_delta_range_uncertainty_m | double | Uncertainty of the accumulated change. |
accumulated_delta_range_state | int | State of the accumulation; it reports a lost phase (cycle slip). |
multipath_indicator | int | Indicator of signal reflection, when the chip provides one. |
agc_level_db | double | Automatic gain control level — sensitive to interference in the band. |
full_inter_signal_bias_ns, full_inter_signal_bias_uncertainty_ns, satellite_inter_signal_bias_ns, satellite_inter_signal_bias_uncertainty_ns | double | Biases between signals and between satellites. |
raw_measurement | raw | toString() of the GnssMeasurement object. |
record_type=agc / capabilities / status
| Key | Type | Description |
|---|---|---|
constellation, carrier_frequency_hz, level_db, raw_agc | mixed | AGC per band (Android 13+). |
hardware_model, hardware_year, capabilities | text / int | Model and year of the GNSS chip and the capabilities it reports. It explains why certain fields are always missing on a given phone. |
status / status_name | int / enum | READY · LOCATION_DISABLED · NOT_SUPPORTED · UNKNOWN. |
gnss_nmea
source: gnss_nmea · record_type: nmea · registration
| Key | Type | Description |
|---|---|---|
nmea_timestamp_epoch_ms | long | Time of the sentence according to the system. |
sentence | text | The whole NMEA sentence without trailing whitespace. The asterisk and the checksum are kept; any separators are escaped. |
gnss_antenna
source: gnss_antenna · record_type: antenna_info · registration
| Key | Type | Description |
|---|---|---|
index | int | Position of the antenna in the list the phone reports. |
carrier_frequency_mhz | double | The band the values relate to. |
phase_center_offset | raw | Offset of the antenna phase centre, including uncertainties. |
phase_center_variation_corrections | raw | Map of phase centre corrections by direction. |
signal_gain_corrections | raw | Map of gain corrections by direction. |
raw_antenna_info | raw | toString() of the GnssAntennaInfo object. |
wifi
source: wifi · record_type: scan · access_point · connection · mlo_link · status
Every fresh scan writes one row per visible access point plus one row about the phone's own connection. Android throttles scans and scanResults then keeps returning the same last result, which is why every scan attempt has its own record_type=scan row and every AP carries its own scan_timestamp_us.
record_type=scan
Written on every scan attempt, even when no new data comes of it. Without it, an old result returned again and again would look like regular sampling at the configured interval, and a coverage map could not tell where measuring actually happened. access_point rows are written only for the FRESH state.
| Key | Type | Description |
|---|---|---|
status | enum | FRESH a new scan · THROTTLED the system refused startScan() · UNCHANGED the same scan had already been processed by the system callback. The distinction matters: UNCHANGED also grows on a phone that does no throttling at all, where reading it as throttling would be wrong. |
trigger | enum | PERIODIC_POLL the collector's own tick · SCAN_RESULTS_CALLBACK a scan triggered by the system or another app, which the collector took over instead of waiting for its next tick. |
start_scan_requested | bool | Return value of startScan(). Empty for trigger=SCAN_RESULTS_CALLBACK, where the collector did not ask for a scan. |
scan_timestamp_us | long | Time of the newest result in the scan, in microseconds since boot. This is exactly what reveals that it is the same scan as last time. |
scan_age_ms | long | Age of the result relative to this row. Units to hundreds of ms for FRESH; it grows for a repeated scan. |
ap_count | int | How many access points the scan returned. |
fresh_scans / throttled_scans / unchanged_scans | long | Cumulative counters for the running stretch. After a resume within a session they start from zero again, so the ratio is always computed within a single stretch. |
record_type=access_point
| Key | Type | Description |
|---|---|---|
ssid / bssid | text | Network name and MAC address of the access point. |
rssi_dbm | int | Signal strength in dBm. |
frequency_mhz / channel | int | Frequency and the channel derived from it. |
channel_width | int | Channel width as the ScanResult constant (0 = 20 MHz, 1 = 40, 2 = 80, …). |
center_freq_0_mhz / center_freq_1_mhz | int | Centre frequencies of the segments on wide channels. |
capabilities | text | Capability and security string as the scan reports it. |
security_types | list | Numeric security types (Android 13+). |
wifi_standard | enum | LEGACY · 802.11n · 802.11ac · 802.11ax · 802.11ad · 802.11be. An unknown standard is left empty, not UNKNOWN. |
scan_timestamp_us | long | When the AP was last seen, in microseconds since boot. It exposes an old scan returned again. |
passpoint, operator_friendly_name, venue_name | bool / text | Passpoint metadata, when the network reports it. |
raw_scan_result | raw | toString() of the ScanResult object. |
record_type=connection
| Key | Type | Description |
|---|---|---|
ssid / bssid | text | Identification of the connected network. Empty when the system hides it for permission reasons. |
ssid_available / bssid_available | bool | It tells “the network has no name” from “the system did not give us the name”. |
rssi_dbm, frequency_mhz, channel | int | Signal and channel of the current connection. |
link_speed_mbps, rx_link_speed_mbps, tx_link_speed_mbps | int | Negotiated link rate. This is not measured throughput — that is measured by internet_quality. |
max_rx_link_speed_mbps / max_tx_link_speed_mbps | int | The maximum the connection supports. |
wifi_standard | enum | Standard of the current connection. |
security_type | int | Security type of the connected network. |
passpoint_fqdn / passpoint_provider | text | Passpoint provider, if the phone connected through one. |
ap_mld_mac / ap_mlo_link_id | text / int | Wi‑Fi 7 multi-link: the MLD address and the ID of the active link. |
raw_wifi_info | raw | toString() of the WifiInfo object. |
record_type=mlo_link
| Key | Type | Description |
|---|---|---|
mlo_kind | enum | scan_affiliated · connection_affiliated · connection_associated — where the link came from. |
link_id | int | ID of the link within the MLD. |
ap_mac / sta_mac | text | Addresses of both ends of the link. |
band, channel, frequency_mhz | int | Band and channel of the link. |
rssi_dbm, rx_link_speed_mbps, tx_link_speed_mbps | int | Signal and rate of the link (Android 14+). |
state | enum | ACTIVE · IDLE · UNASSOCIATED · INVALID. |
raw_mlo_link | raw | toString() of the MloLink object. |
record_type=status
A single state, status=SCAN_CALLBACK_REGISTRATION_FAILED with the keys error and message. It means the subscription to system scans could not be registered — the data will then hold only the scans the collector requested, that is, trigger=PERIODIC_POLL.
wifi_rtt
source: wifi_rtt · record_type: capabilities · ranging_result · status
Ranging to access points that support FTM (802.11mc / 802.11az). Such APs are rare; when none is in range, status=NO_RESPONDERS is written so that it is clear the measurement ran and simply had nothing to measure.
| Key | Type | Description |
|---|---|---|
available / characteristics | bool / text | Availability of RTT and the characteristics the chip reports (record_type=capabilities). |
status | int / enum | For a result, a numeric code (0 = success); for a status, UNAVAILABLE · NO_RESPONDERS · FAILURE · ERROR. |
mac_address | text | Address of the access point being ranged. |
distance_mm / distance_std_dev_mm | int | Distance and its standard deviation in millimetres. Filled in only for a successful measurement. |
rssi_dbm | int | Signal at the moment of measurement. |
attempted_measurements / successful_measurements | int | How many individual measurements were attempted and how many succeeded. |
ranging_timestamp_ms | long | Time of the measurement according to the chip; the row's elapsed_realtime_ns derives from it. |
measurement_frequency_mhz / measurement_bandwidth | int | Channel and bandwidth of the measurement (Android 14+). |
is_80211mc / is_80211az_ntb | bool | Which FTM variant the measurement used. |
lci_hex / lcr_hex | hex | Reported location and civic address of the access point (Android 15+). |
min_ntb_interval_us / max_ntb_interval_us | long | Range of intervals for 802.11az non-trigger-based ranging. |
ranging_authenticated / ranging_frame_protected | bool | Security of the ranging (Android 16+). |
code, error, message | int / text | Reason for failure on status records. |
raw_ranging_result | raw | toString() of the RangingResult object. |
wifi_throughput
source: wifi_throughput · record_type: wifi_throughput · status
One scan gives the signal of every network around at once; throughput does not — the phone is only ever associated with a single AP. The source therefore visits the networks listed in advance one by one through a WifiNetworkSpecifier and measures download, upload and HTTPS RTT on each. The switch applies to this app only; the rest of the phone's traffic stays on the original network.
Measure standing still: while walking, each network in the cycle would be measured from a different place and the numbers could not be compared. On the first connection to each network Android demands consent through a modal system dialog that cannot be suppressed — a walk therefore starts by clearing those dialogs at the first point.
record_type=wifi_throughput
One row per network per cycle, even when the network was never reached — otherwise the data could not tell a network that was not there from a network that was forgotten.
| Key | Type | Description |
|---|---|---|
status | enum | SUCCESS both transfers went through · PARTIAL at least one ended in an error · NOT_VISIBLE the network was not in the last scan, so no attempt was made · CONNECT_FAILED the connection did not succeed within connect_timeout_s. |
cycle / target_index | int | Which round of the walk, and the position of the network in the configured list. The pair identifies the row uniquely. |
requested_ssid / secured | text / bool | The network as entered in the settings, and whether it has a password. The password itself is never written into the data. |
connect_ms | double | Time from the start of the attempt to an available network, that is, association and DHCP. Without it a slow network could not be told from slow switching. Zero for NOT_VISIBLE. |
total_ms | double | The whole handling of the network, including both transfers and the RTT probes. |
has_internet | bool | Whether the network reports NET_CAPABILITY_INTERNET. A network without internet is still measured and ends with a transfer error — as a result, not as unavailability. |
ssid, bssid, rssi_dbm, frequency_mhz, channel, wifi_standard, rx_link_speed_mbps, tx_link_speed_mbps | text / int | Snapshot of the connection at the moment of measurement: which AP the phone actually joined and at what negotiated rate. Filled in only for a successful connection. |
latency_ms_median / latency_ok / latency_errors | double / int / list | Median HTTPS RTT, the number of probes that came back and the exception types of those that did not. This is not an ICMP ping. |
download_mbps / upload_mbps | double | Throughput of the transfer. On an error it stays empty even when part of the bytes moved — half a transfer is not a measured rate. |
download_bytes / upload_bytes | long | How many bytes actually moved; on an error, up to the moment of failure. |
download_ms / upload_ms | double | Duration of the transfer, including establishing the connection. |
download_error / upload_error | text | Name of the exception class, for example SocketTimeoutException. A refused HTTP status code arrives here as IllegalStateException. Empty on success. |
server_mode / server | text | Mode and host of the test endpoint, the same as with internet_quality. |
record_type=status
A single state, status=NO_TARGETS with an explanatory message. The source is switched on but the list of networks is empty, so it never starts — and the export shows that instead of staying silent.
mobile_network
source: mobile_network · record_type: cell · signal_strength · event · cell_bearing_estimate
For record_type=cell the set of keys is always the same and always complete — fields a given technology does not have are left empty. The columns of the table therefore never shift about depending on whether the phone is currently on LTE or 5G.
Keys shared by every cell record
| Key | Type | Description |
|---|---|---|
scan_mode | enum | PASSIVE_CELL_SCAN ordinary subscription · ACTIVE_NETWORK_SCAN the experimental active scan. |
trigger | enum | TELEPHONY_CALLBACK · GET_ALL_CELL_INFO · REQUEST_CELL_INFO_UPDATE · ACTIVE_NETWORK_SCAN. It tells a requested snapshot from a spontaneous change. |
technology | enum | NR · LTE · WCDMA · TDSCDMA · GSM · CDMA. |
registered | bool | Whether the phone is registered to this cell. |
connection_status | enum | PRIMARY_SERVING · SECONDARY_SERVING · NONE · UNKNOWN. |
subscription_id / sim_slot | int | Which SIM the cell belongs to. With two SIMs, two independent sets of records run. |
cell_timestamp_ms | long | Time of the snapshot according to the modem; the row's elapsed_realtime_ns derives from it. |
dbm, asu, level | int | Signal strength in dBm, in ASU and as a 0–4 level for the UI. |
mcc, mnc, plmn | text | Country code, network code and the two joined. |
operator | text | Operator name, or the PLMN when there is no name. UNKNOWN for a cell with no identity. |
operator_alpha_long / operator_alpha_short | text | Long and short network name according to the modem. |
additional_plmns | list | Further PLMNs the cell broadcasts (network sharing). |
raw_cell_info, raw_cell_identity, raw_signal_strength | raw | toString() of three modem objects, as a fallback for fields that have no key of their own here yet. |
Keys by technology
| Key | Technology | Description |
|---|---|---|
cell_id | LTE, WCDMA, GSM, TDSCDMA, CDMA | Cell identifier (ci on LTE, cid on the others, base station ID on CDMA). |
nci | NR | The 48-bit 5G NR Cell Identity. |
pci | LTE, NR | Physical Cell ID. |
tac | LTE, NR | Tracking Area Code. |
lac | WCDMA, GSM, TDSCDMA | Location Area Code. |
psc | WCDMA, GSM, TDSCDMA | Primary Scrambling Code; on TDSCDMA it is filled from cpid. |
bsic | GSM | Base Station Identity Code. |
earfcn / nrarfcn / uarfcn / arfcn | LTE / NR / WCDMA, TDSCDMA / GSM | Channel number by technology. |
bands | LTE, NR | Bands the cell operates in, as reported by Android. |
bandwidth | LTE | Bandwidth in kHz. |
band / frequency_mhz | LTE, NR, WCDMA, GSM | Band and downlink centre frequency computed from the channel number above using the 3GPP band tables for the technology, stored next to it — never in place of it. Empty when the channel number is missing or falls outside the built tables (CDMA and TDSCDMA have none); when it falls in a range shared by more than one band at the same frequency, every matching band is listed, joined by /. |
rssi | LTE, GSM | Total received power. |
rsrp / rsrq | LTE | Reference signal power and quality. |
rssnr | LTE | Signal-to-noise ratio. |
sinr | LTE, NR | A unified key for comparison across technologies: on LTE it copies rssnr, on NR ss_sinr. |
ss_rsrp, ss_rsrq, ss_sinr | NR | Measurements on the synchronisation signal. |
csi_rsrp, csi_rsrq, csi_sinr | NR | Measurements on CSI‑RS. |
cqi / cqi_table_index | LTE, NR | Channel quality indicator; on NR it is a list of values. |
timing_advance / timing_advance_us | LTE, GSM / NR | Timing advance — a coarse hint of the distance to the transmitter. |
ec_no | WCDMA | Chip energy to noise ratio. |
rscp | TDSCDMA | Received Signal Code Power. |
bit_error_rate | GSM | Error rate. |
network_id, system_id, basestation_latitude, basestation_longitude, cdma_dbm, cdma_ecio, evdo_dbm, evdo_ecio, evdo_snr | CDMA | Fields of the old CDMA networks; they stay empty in Europe. |
record_type=signal_strength
| Key | Type | Description |
|---|---|---|
technology | text | Android's measurement class, for example CellSignalStrengthLte. |
dbm, asu, level | int | Signal strength in the three usual units. |
subscription_id / sim_slot | int | Which SIM the measurement concerns. |
raw_signal_strength | raw | toString() of the measurement. |
record_type=event
| Key | Type | Description |
|---|---|---|
event | enum | CELL_LIST_CHANGED · SIGNAL_STRENGTH_CHANGED · SERVICE_STATE_CHANGED · DISPLAY_INFO_CHANGED · DATA_CONNECTION_CHANGED · SERVING_CELL_CHANGED · CELL_INFO_UPDATE_ERROR · RADIO_API_ERROR · ACTIVE_NETWORK_SCAN_*. |
before / after | raw | State before and after the change, so the transition can be reconstructed. |
before_technology, before_cell_id, before_pci, after_technology, after_cell_id, after_pci | mixed | The serving-cell transition broken out — this is a handover. |
operation | text | Which modem call failed on RADIO_API_ERROR. |
has_carrier_privileges / modify_phone_state_granted | bool | Why the system allowed or refused the active scan. |
result_count | int | Number of cells returned by the active scan. |
error_code, error, exception, message | int / text | The exact reason for the refusal or the error. |
manufacturer, model, android, sdk | text / int | Device and system on active scan records — its availability depends heavily on the phone. |
record_type=cell_bearing_estimate
A rough, walk-based estimate of where the serving cell of the active data SIM lies: a weighted centroid of the phone's own positions, weighted toward the stronger readings. Written only after at least five usable fixes spread over at least 30 m since the last change of serving cell, and again only when the rounded bearing or the distance step changes. Always an estimate (method=own_walk), never a fix — terrain, buildings and multipath can throw it off by a wide margin.
| Key | Type | Meaning |
|---|---|---|
technology, cell_id, pci | text / long / int | Identity of the serving cell the estimate belongs to — the same triple that drives SERVING_CELL_CHANGED. Empty when the system does not report the value. |
sample_count | int | How many fixes went into the estimate since the last serving-cell change. |
spread_m | double | Diagonal of the bounding box of those fixes, in metres — how far the phone was carried meanwhile. |
estimated_latitude / estimated_longitude | double | The weighted centroid — the estimated tower position, raw value. |
from_latitude / from_longitude | double | The fix the bearing and distance were computed from (the latest usable one). Carried explicitly so live output (MQTT) has a position too; the usual last_* suffix is added on top in the file. |
bearing_deg | double | Bearing from that fix to the centroid, clockwise from north, rounded to the nearest 5° (0–355). |
distance_m | double | Distance from that fix to the centroid, rounded up to a coarse step (50, 100, 150, 200, 300, 500, 750, 1000 … 30000 m). |
method | text | own_walk — derived from the phone's own walk, not from any cell database. |
sim_info
source: sim_info · record_type: summary · subscription
Which cards were in the phone while measuring. mobile_network carries the identity of the transmitter but not of whoever was looking at it — and with two cards, or in roaming, that is a crucial difference. It is written once when the measurement starts and after that only when the system reports a change of subscriptions; cards do not change as you walk.
Neither ICCID nor EID is recorded. They are permanent subscription identifiers that outlive even a change of phone, whereas the rest of this section describes the network the measurement ran in. Since Android 10 the system would not hand them to an app anyway.
record_type=summary
| Key | Type | Description |
|---|---|---|
slot_count | int | How many subscriptions the phone can hold at once. |
active_count | int | How many of them are active. Zero means a phone with no card, not a read error. |
esim_count | int | How many of the active ones are eSIM profiles. |
euicc_supported | bool | Whether the phone has an eSIM chip, regardless of any profile on it. |
record_type=subscription
| Key | Type | Description |
|---|---|---|
subscription_id | int | Local subscription number. It changes when the card is re-registered, so it does not identify the phone; it serves to join rows within a single measurement. |
slot_index | int | The slot the card sits in. Numbered from zero. |
carrier_name / display_name | text | Operator according to the card, and the name the user gave the subscription. |
mcc / mnc | text | Country and network code. It joins the card to the cell records in mobile_network. |
country_iso | text | Country of the subscription, ISO code. |
embedded | bool | true for an eSIM profile, false for a physical card. |
roaming | bool | Whether the subscription is currently roaming. |
default_data, default_voice, default_sms | bool | Which card carries data, calls and messages. For analysing a measurement, default_data matters most — its network is the one that shows up in internet_quality. |
network_context
source: network_context · record_type: capabilities · link_properties · event
The operating system's view of the networks: what the phone claims about a network before anything is transferred over it. Written at the start, periodically, and on every change.
record_type=capabilities
| Key | Type | Description |
|---|---|---|
trigger | enum | INITIAL · PERIODIC · CALLBACK. |
network_handle | long | Network identifier. It joins the record to a specific internet_quality sample. |
transports | list | CELLULAR, WIFI, VPN, ETHERNET, BLUETOOTH, THREAD, SATELLITE… |
capabilities | list | Numeric constants of the network capabilities. |
internet / validated | bool | Whether the network promises internet, and whether the system verified that it really works. |
captive_portal | bool | The network requires a sign-in through a portal. |
metered, roaming, congested, suspended | bool | State of the network. Stored in the positive sense, so metered=true means a metered network. |
downstream_kbps / upstream_kbps | int | The system's estimate of link capacity — an estimate, not a measurement. |
signal_strength | int | Signal strength of the network, where the system states it. |
owner_uid, network_specifier, transport_info | mixed | Supplementary identification of the network; on Wi‑Fi it contains WifiInfo. |
raw_capabilities | raw | toString() of the NetworkCapabilities object. |
record_type=link_properties
| Key | Type | Description |
|---|---|---|
interface_name | text | Interface name, for example wlan0 or rmnet_data0. |
link_addresses | list | IP addresses of the interface. |
dns_servers / domains | list / text | DNS servers and search domains. |
routes | list | Routing entries, separated by a pipe. |
mtu | int | Maximum frame size. |
private_dns_active / private_dns_server | bool / text | State of private DNS. |
dhcp_server, nat64_prefix, http_proxy, wake_on_lan | mixed | Other properties of the link. |
raw_link_properties | raw | toString() of the LinkProperties object. |
record_type=event
| Key | Type | Description |
|---|---|---|
event | enum | AVAILABLE · LOSING · LOST · BLOCKED_CHANGED. |
max_ms_to_live | int | How much time the system gives the network on LOSING. |
blocked | bool | Whether the app's traffic on this network is blocked. |
internet_quality
source: internet_quality · record_type: network_quality_sample · network_capacity_probe · network_capacity_probe_started · speed_test_result · speed_test_event
Real HTTPS transfers bound to a specific Android Network, not an estimate of link capacity. The one-second sample is small on purpose: the goal is a continuous map of quality while walking, not a peak rate. That is why every record also carries measurement_semantics=SMALL_TRANSFER_QUALITY_NOT_MAX_SPEED.
Every sample additionally carries the shared suffixes — the location and the network snapshot.
record_type=network_quality_sample / network_capacity_probe
| Key | Type | Description |
|---|---|---|
timestamp / timestamp_epoch_ms | long | Start of the sample. Two keys with the same value, for backward compatibility with older scripts. |
scheduled_at_epoch_ms / started_at_epoch_ms | long | When the sample was due to start and when it did. |
schedule_delay_ms | long | Delay against the plan. It grows when the link is slow or the system throttles the app. |
missed_intervals | long | How many whole intervals were skipped. The per-second sample axis writes a row even when measuring was impossible, so this only grows after the process was genuinely put to sleep. |
status | enum | Measured: SUCCESS · PARTIAL · TIMEOUT (everything failed, and only by running out of time) · ERROR. Not measured: BUSY (a burst or a manual test held the transport) · NETWORK_LOST (there was nothing to measure on). The last two carry empty measurement columns and do not count towards the error rate. |
busy_reason | enum | Who held the transport: QUALITY_SAMPLE · CAPACITY_PROBE · SPEED_TEST. Only with status=BUSY. |
measurement_semantics | enum | Always SMALL_TRANSFER_QUALITY_NOT_MAX_SPEED. A safeguard against reading the values as a speed test. |
server_mode / server | text | Mode and host of the test endpoint. |
network_binding | enum | Always ANDROID_NETWORK — the transfer demonstrably went over the selected network, not the default one. |
transport_target | enum | What the user chose: AUTO · WIFI · CELLULAR · BOTH. |
transport | enum | What the sample actually went over: WIFI · CELLULAR · ETHERNET · BLUETOOTH · VPN · OTHER · NONE. |
download_requested_bytes / upload_requested_bytes | long | How many bytes were meant to be transferred. |
download_bytes / upload_bytes | long | How many actually were. On an error, these are the bytes moved up to the moment of failure. |
download_duration_ms / upload_duration_ms | double | Duration of the transfer, including establishing the connection. |
short_download_mbps / short_upload_mbps | double | Throughput of the one-second sample. A small transfer, so it is a lower bound — not the maximum of the link. |
burst_download_mbps / burst_upload_mbps | double | The same for the capacity burst, which runs once per configured interval and is considerably larger. |
latency_probe_count | int | How many RTT probes were meant to be sent. |
successful_probes / failed_probes | int | How many got through and how many failed. |
rtt_min_ms, rtt_avg_ms, rtt_max_ms | double | Response time of the HTTPS probes. |
jitter_ms | double | Mean absolute change of RTT between neighbouring probes. |
failed_requests / timeout_count | int | How many requests failed in total and how many of those timed out. |
probe_duration_ms | double | How long the whole sample took. On a slow link it is longer than the interval — the following seconds then carry status=BUSY. |
errors / error | text | List of errors in the form phase:type, or a one-word reason (NO_ACTIVE_NETWORK, TARGET_NETWORK_UNAVAILABLE). |
record_type=network_capacity_probe_started
Written the moment a burst begins. The burst holds the transport lock for the whole transfer, so during that time rows with status=BUSY arrive instead of measured samples — this marker says from when and why. Its keys are a subset of the previous table plus transfer_deadline_ms (the latest the transfer may take).
record_type=speed_test_result
A manual test started by the button, not part of continuous mapping. It transfers megabytes, not kilobytes.
| Key | Type | Description |
|---|---|---|
trigger | text | MANUAL for a test from the UI. |
status | enum | SUCCESS · PARTIAL · ERROR. |
transport | enum | WIFI · MOBILE. Choosing BOTH writes two separate results. |
download_mbps / upload_mbps | double | Throughput of the large transfer. |
rtt_median_ms / rtt_p95_ms | double | Median and 95th percentile of the response time — a manual test sends more probes than a sample. |
packet_loss_pct | double | Share of probes that never came back. |
download_error, upload_error, latency_errors | text | Errors of the individual phases of the test. |
record_type=speed_test_event
| Key | Type | Description |
|---|---|---|
event | enum | INTERNET_SPEED_TEST_STARTED · _COMPLETE · _ERROR. |
target | enum | The chosen target of the test, including BOTH. |
status, trigger, error, message | text | State and any error of the test run. |
bluetooth_classic
source: bluetooth_classic · record_type: discovery_result · paired_device · event · registration · status
| Key | Type | Description |
|---|---|---|
discovery_mode | enum | CLASSIC_DISCOVERY found by a scan · BONDED_CACHE listed from the paired devices. |
scan_cycle_id | long | Number of the scan cycle. It ties a find to a specific scan window. |
result_index | int | Which find it is within this cycle. |
name / alias | text | Device name and user alias, where available. |
address | text | MAC address of the device. |
bond_state | int | 10 not paired · 11 pairing · 12 paired. |
device_type | int | 1 Classic · 2 LE · 3 Dual. |
bluetooth_class | text | Device class (type and services). |
uuids | list | Services offered. |
rssi_dbm | int | Signal at the moment of the find. |
paired | bool | Only on paired_device, always true. |
event / status | enum | CLASSIC_DISCOVERY with the state REQUESTED · STARTED · FINISHED · REJECTED · ERROR. |
found_devices | int | How many devices the cycle found — on FINISHED this is its result. |
receiver_flag | enum | RECEIVER_EXPORTED or IMPLICIT. Discovery broadcasts come from the system, so with a non-exported receiver the scan looks dead in the data — which is why the flag used is recorded. |
callback, registered, error, message | mixed | Result of registering the receiver. |
status=ADAPTER_DISABLED means Bluetooth is switched off. Without that report the source would look enabled, just without a single find.
bluetooth_ble
source: bluetooth_ble · record_type: advertisement · status
The complete advertising payload, raw bytes included. The scan runs in low-latency mode and does not filter, so in a busy environment it is the most prolific source of rows.
| Key | Type | Description |
|---|---|---|
callback_type | int | Type of the scanner callback (all matches, first match, match lost). |
name, alias, address | text | Identification of the device. |
address_type | int | Public or random address (Android 15+). It explains why an address changes over time. |
bond_state / device_type | int | The same meaning as with bluetooth_classic. |
rssi_dbm | int | Signal of the received packet. |
tx_power / record_tx_power | int | Transmit power according to the scanner and according to the advertising content. Together with RSSI it gives a coarse distance estimate. |
connectable / legacy | bool | Whether it can be connected to; whether it is the old advertising format. |
data_status | int | Whether the payload is complete or continues in another packet. |
primary_phy / secondary_phy | int | Physical layer (1M, 2M, coded). |
advertising_sid | int | ID of the advertising set in extended advertising. |
periodic_advertising_interval | int | Interval of periodic advertising, where the device uses it. |
advertise_flags / advertised_name | int / text | Flags and name straight from the advertising packet. |
service_uuids / service_solicitation_uuids | list | Services offered and services solicited. |
manufacturer_data | hex | Manufacturer data as 0xIDID:hex, comma separated. The manufacturer ID allows the device to be typed. |
service_data | hex | Service data as uuid:hex. |
advertising_data | hex | The whole map of AD types as 0xTT:hex (Android 13+). |
sensor_protocol | enum | The advertising format recognised in the bytes above: bthome_v2, ruuvi_rawv2, ruuvi_rawv1, atc1441, pvvx, eddystone_tlm. Absent for a device that only announces itself. |
sensor_encrypted | bool | The payload is encrypted and the reading cannot be recovered without the pairing key. The device is recorded, its values are not invented. |
sensor_truncated | bool | Reading stopped at an object of unknown length. Everything before it is certain; nothing behind it was guessed. |
sensor_temperature_c, sensor_humidity_pct, sensor_pressure_hpa, sensor_co2_ppm, sensor_pm25_ugm3, sensor_voltage_v, sensor_battery_pct, sensor_battery_mv … | number | Readings decoded from the advertisement of a nearby sensor. The unit is part of the key and always SI; a value the sensor flagged as invalid is left out rather than written as a number. |
raw_bytes_hex | hex | The raw advertising packet. It allows anything the other keys do not cover to be derived. |
raw_scan_record / raw_scan_result | raw | toString() of both objects. |
status / error_code | enum / int | SCAN_FAILED with an error code, or ADAPTER_DISABLED. |
collector_heartbeat
source: collector_heartbeat · record_type: collector_heartbeat · every 5 s
The heartbeat is not a source of data about the surroundings but about the measurement itself. It passes through the same queue and the same writer as every other sample, so a finished file shows which module was really running when, and whether writing ever stalled. Silence in the data therefore cannot be mistaken for a fault.
| Key | Type | Description |
|---|---|---|
active_collectors | list | Classes of the running collectors, alphabetically. |
active_sources | list | The sources that were actually registered at start — not the ones ticked. |
writer_status | enum | OK · RETRYING · DRAINING · DRAINED · FAILED · CANCELLED · STOPPED. |
queue_size | long | How many samples are waiting in the queue right now. A value that keeps growing means the write is falling behind. |
accepted_samples, written_samples, dropped_samples | long | Running counters, the same meaning as in the header. |
writer_error | text | The first write error, if there was one. |
haptic
source: haptic · record_type: haptic
A vibration is a mechanical impulse and the phone is recording the accelerometer and the gyroscope at the same time, so every buzz writes itself into its own data as a shake that did not come from walking. This record says when it happened, so a window can be cut around it in the sensor stream. What gets written is the loss and return of the default network and the point marker; the alarm for a failed write has no record, because by then the writer is no longer alive — that one is carried by end_reason=WRITER_ERROR, and no sensor data is produced beyond that moment.
| Key | Type | Description |
|---|---|---|
signal | enum | TRANSPORT_LOST the default network disappeared and nothing replaced it within two seconds · TRANSPORT_RESTORED the network is back · SELECT the operator marked a point, and a survey_point belongs to the same instant. |
pattern | text | Rhythm of the signal: thud, tick-tick or tick. |
nominal_duration_ms | long | Nominal length of the envelope, not a measured one. For a composed effect the decay is up to the system, so the window to cut should be taken with room to spare. |
composed | bool | true = the phone supports VibrationEffect.Composition primitives and this was a tap; false = a substitute waveform with the same rhythm. |
delivered | bool | false means the system refused the vibration — so the shake is not in the data. |
session
source: session · record_type: session_started · session_paused · session_resumed · survey_point · mark_media · export
Start resumes into the last measurement that was not reset, so one session can consist of several separate stretches. These records say where a stretch began and where it ended — without them a pause in the data could not be told from a hole in coverage or from frozen collection. A pause is deliberately two separate events rather than one record with a length: a record with a length would have to wait for the end of the pause, and if the system killed the app meanwhile, it would never be written at all.
| Key | Type | Description |
|---|---|---|
session_id | long | The session the stretch belongs to. It does not change on a resume — which is how it shows this is a continuation, not a new measurement. |
segment_started_at_epoch_ms | long | Start of the stretch. On session_paused, the start of the one just ended. |
session_started_at_epoch_ms | long | The first Start of the whole session; on session_started and session_resumed. |
paused_at_epoch_ms | long | The moment of the Stop; only on session_paused. |
paused_since_epoch_ms | long | When the pause being resumed from began; only on session_resumed. The difference against segment_started_at_epoch_ms is the length of the pause. |
segment_running_ms / running_ms_total | long | Length of the finished stretch and the sum of all so far; only on session_paused. The sum matches running_ms in the header. |
running_ms_before | long | Time accumulated before this stretch. Zero on session_started. |
distance_m / distance_m_before | double | Distance at the end of the stretch, and at the beginning of the resumed one. Ground covered during a pause does not count towards the distance — the anchor is discarded on resume. |
accepted_samples_before | long | How many samples the session had before the resume; only on session_resumed. |
end_reason / status | enum | Why and in what state the stretch ended; only on session_paused. The same values as end_reason and session_status in the header. |
previous_status / previous_writer_error | text | State and error of the previous stretch; only on session_resumed. A resume puts the session row back into the running state and overwrites its status and error, so this is the only place where a failed write remains traceable even after a successful finish. |
record_type=survey_point
A marker of the place the measurement is standing at. Indoors GNSS gives no position, so this is the only thing in the data that ties measured values to a point on a floor plan: the operator stops, taps Mark point, waits a moment and walks on. The marker stands on its own in the timeline and point_index is not added to the other records — values are attributed to a point by a time window after the marker, not by a key in every row.
| Key | Type | Description |
|---|---|---|
session_id | long | The session the point belongs to. |
point_index | int | Number of the point, from 1, within the running stretch. After a resume it starts from one again — points from two stretches are told apart by running_ms_total or by the preceding session_resumed event. |
label | text | Optional label from the field above the button. A point left blank stays a sequence number, which is enough for a walk along a numbered floor plan. |
marked_at_epoch_ms | long | System time of the tap. The row also carries the usual elapsed_realtime_ns, which is what the window is cut by. |
running_ms_total | long | Collection time accumulated at the moment of the marker, including previous stretches and excluding pauses. |
distance_m | double | Distance walked at the moment of the marker. The difference against the previous point says how many metres the operator really covered between them. |
media | enum | photo or audio when a photo or a voice note belongs to the point; empty otherwise. The file itself arrives as a separate mark_media record — for a voice note only after it ends, which can be a minute later. |
The tap also buzzes, so every point has a haptic row with the same timestamp — the shake in the sensors is therefore not unexplained.
record_type=mark_media
A photo or a voice note taken at a survey point. The file is not in the TXT — it travels in the same archive, under media/ next to data.txt, and this record is what ties it to a point. A measurement carrying media is therefore exported as a single tar.gz: gzip on its own compresses one stream and cannot hold a folder.
| Key | Type | Description |
|---|---|---|
session_id | long | The session the file belongs to. |
point_index | int | The point the file belongs to; the same number as on its survey_point row. |
kind | enum | photo (JPEG, up to 2,560 px on the long side) · audio (AAC in MPEG-4, mono 44.1 kHz, at most ten minutes). |
file | text | Path inside the archive, always media/…. The name carries the point number and the moment the file was stored, so it stays unique even when a point has both a photo and a recording. |
bytes | long | Size of the file. Media go into the archive uncompressed — gzip has nothing to take off a JPEG — so this is what they add to the export. |
sha256 | text | Hash of the file contents, computed when the marker was written. It is what proves a received photo belongs to this point of this measurement and was not touched on the way. |
label | text | Label of the point, copied here so the file can be read on its own. |
duration_ms | long | Length of the recording; only on kind=audio. |
width_px / height_px | int | Dimensions of the photo with the EXIF rotation already applied; only on kind=photo. |
record_type=export
The export writes itself into the measurement it came from: when, in which format, which way it left and how much it carried. It is written only after the file exists, so an exported file never contains the record of its own export — only the next one does. That is the order of events, not a gap. The app reads the last of these rows before a Reset to say whether the measurement has left the phone.
| Key | Type | Description |
|---|---|---|
session_id | long | The session that was exported. |
exported_at_epoch_ms | long | System time when the file was finished; the same value as the row's timestamp. |
format | enum | txt · gzip · archive (tar.gz with media) · the id of a conversion, such as csv_zip or kml. |
via | enum | file (saved through the system file picker) · share (handed to another app) · print (protocol sent to the print dialog) · delivery (uploaded from the delivery queue). |
destination | enum | Only on via=delivery: s3 · webdav · sftp. Empty otherwise — the app does not know where a shared file went. |
records | long | How many rows the export carried. |
bytes | long | Size of the file when the app had it in hand (share, delivery); empty for a file written straight into a location chosen in the system picker. |
A voice note is timed to its end, while the point it belongs to was written when the operator tapped — that is where they stood. The gap between the two rows is the length of the recording.
mqtt
source: mqtt · record_type: mqtt_connected · mqtt_disconnected · mqtt_failed
Breaks in the live MQTT output, written into the measurement as their own source. When the connection to the broker drops, the live stream falls silent — and without this record, whoever looks at the finished measurement cannot tell whether nothing happened out there or the broker just went quiet. A failed connection attempt is not written on its own: an hour on a route with no signal would otherwise leave the export with dozens of rows about nothing. Only a connection that had stood and then broke, and the final give-up once retries are exhausted, get a row.
Like collector_heartbeat, haptic and session, this is a record of the app's own operation, not a measured quantity — it is deliberately left out of the source picker and does not drive the live pulse on screen.
No broker address, client identity or password ever reaches the export through this source — only the name of an enum case leaves the connection layer. A rejected password shows up as reason=CONNECTION_REJECTED;reason_code=BAD_CREDENTIALS, never as the credentials themselves or the exception text that named them.
record_type=mqtt_connected
| Key | Type | Description |
|---|---|---|
reconnect | bool | false for the first connection of the measurement, true when it follows one that broke. |
offline_ms | long | Length of the silence that preceded this connection. Empty on the first connection, since nothing came before it. |
record_type=mqtt_disconnected
A connection that had stood broke. The client will retry.
| Key | Type | Description |
|---|---|---|
reason | enum | NOT_CONFIGURED · NOT_CONNECTED · TOPIC_INVALID · TOPIC_TOO_LONG · PAYLOAD_TOO_LONG · CONNECTION_REJECTED · TLS_FAILED · FINGERPRINT_UNKNOWN · FINGERPRINT_CHANGED · CONNECTION_FAILED · SERVER_NO_RESPONSE · KEEP_ALIVE_LOST · SUBSCRIPTION_REJECTED · UNEXPECTED_PACKET · MALFORMED_PACKET · SEND_FAILED. Just the name of the case, never the underlying exception text, a topic, or a certificate fingerprint. |
reason_code | enum | Filled only for two of the reasons above. For NOT_CONFIGURED, which prerequisite is missing: MISSING_SERVER · INVALID_PORT · ENCRYPTION_REQUIRED · MISSING_CLIENT_ID · MISSING_IDENTITY · PASSWORD_WITHOUT_USERNAME · INVALID_KEEP_ALIVE. For CONNECTION_REJECTED, the broker's CONNACK code: UNSUPPORTED_PROTOCOL_VERSION · CLIENT_ID_REJECTED · SERVER_UNAVAILABLE · BAD_CREDENTIALS · NOT_AUTHORIZED · UNKNOWN. Empty for every other reason. |
connected_ms | long | How long the connection stood before it broke, in ms. |
published_messages | long | How many messages went out over that connection before it broke. |
record_type=mqtt_failed
Retries are exhausted. From here on the silence is no longer temporary — the response to it lives in settings, not in another attempt.
| Key | Type | Description |
|---|---|---|
reason | enum | Same set of cases as on mqtt_disconnected, above. |
reason_code | enum | Same meaning as on mqtt_disconnected, above. |
connected_ms | long | How long a connection stood before this last attempt gave up, in ms. Empty when no connection was ever open during the attempt. |
published_messages | long | How many messages went out before the attempt gave up. |
Measurement parameters
Every source can be switched on individually before the start, and for some of them the pace can be set as well. Values outside the range are pulled back to the limits — otherwise interval_s=0, say, would turn the periodic loop into a tight loop.
| Source | Parameter | Default · range |
|---|---|---|
| sensors (7×) | interval_ms | 50 ms · 10–1,000 |
noise | sample_rate_hz | 8,000 Hz · 8,000–48,000 |
location | interval_ms | 1,000 ms · 100–60,000 |
gnss_raw | interval_ms | 1,000 ms · 100–60,000 |
wifi, wifi_rtt, mobile_network, network_context | interval_s | 30 s · 5–300 |
bluetooth_classic, bluetooth_ble | scan_s / pause_s | 15 s · 5–60 / 45 s · 5–300 |
internet_quality | transport_target | Wi‑Fi + cellular data · AUTO, WIFI, CELLULAR, BOTH |
sample_interval_ms | 1,000 ms · 500–5,000 | |
download_kib / upload_kib | 64 KiB · 1–1,024 / 8 KiB · 1–256 | |
latency_probe_count | 3 × · 2–3 | |
capacity_interval_s | 30 s · 10–300 | |
capacity_download_kib | 1,024 KiB · 64–10,240 | |
capacity_upload_kib | 128 KiB · 8–2,048 | |
download_mb (manual test) | 10 MB · 1–100 | |
upload_mb (manual test) | 5 MB · 1–50 | |
| Internal limits: 3 s request timeout, 5 s ceiling for a short transfer, 15 s ceiling for a burst, 60 s ceiling for a manual test. | ||
wifi_throughput | cycle_interval_s | 60 s · 10–600 |
connect_timeout_s | 30 s · 5–120 | |
download_kib | 1,024 KiB · 64–10,240 | |
upload_kib | 128 KiB · 8–2,048 | |
latency_probe_count | 3 × · 1–5 | |
| Internal limit: 20 s timeout for a single HTTPS request. The list of measured networks is not a parameter — it is entered separately in the source card, and the passwords are stored in the app preferences in readable form. | ||
Storage in the phone
The data sits in a local Room / SQLite database, schema version 6. It is never sent anywhere; exporting to a file is up to the user.
Table sensor_samples
| Column | Type | Description |
|---|---|---|
id | long | Primary key. It determines the order of samples that share a timestamp. |
sessionId | long | Reference to the session; deleting the session deletes the samples too. |
source | text | Source of the sample. |
timestampEpochMs | long | Exported as epoch_ms. |
elapsedRealtimeNanos | long | Exported as elapsed_realtime_ns. |
payload | text | Payload of the source, including the attached suffixes. |
Table measurement_sessions
One row per measurement run. The columns match the fields in the export header; on top of that, acceptedSamples, writtenSamples, droppedSamples and lastPersistedAtEpochMs are updated as the run proceeds, in the same transaction as the insert of a batch of samples. If the process dies, the session stays in the RUNNING state and the next app start closes it as INTERRUPTED with end_reason=PROCESS_RECOVERY — the partial record stays exportable.