Summary

A wide burst near 433 MHz looked unusual in SDR++. It was too broad for a typical narrowband voice channel and displayed repeated slanted structures in the waterfall. That observation led to a passive research pipeline:

BladeRF / SDR++ capture
  -> LoRa PHY identification
  -> GNU Radio demodulation
  -> payload CRC gate
  -> packet-preserving UDP
  -> Meshtastic protobuf decoder
  -> JSONL and SQLite persistence
  -> public, privacy-aware reporting

The goal of this note is to document the technical path from RF observation to structured Meshtastic packet analysis, while also showing that public RF telemetry can expose metadata, topology, and operational patterns.

Dataset scope and publication choices

This article treats Meshtastic traffic as public RF telemetry, but not as consent for permanent deanonymized publication. The published package therefore focuses on method, screenshots, aggregate statistics, scripts, and anonymized derived data.

Published here:

  • SDR++ and GNU Radio screenshots.
  • Signal, spectrum, spectrogram, and chirp-detail images.
  • Public aggregate charts.
  • An anonymized top-senders chart.
  • Flowgraph and decoder scripts.
  • Public CSV summaries derived from the capture.
  • A redacted example packet structure.

Intentionally not included:

  • Raw SQLite packet database.
  • Raw JSONL packet capture.
  • Full timestamped packet export.
  • Exact node IDs, MAC-like identifiers, callsigns, and node names.
  • Exact latitude/longitude records.
  • Full logs containing raw runtime details.

Capture summary

MetricResult
Accepted packets736
Collection duration3.81 hours
Unique sender IDs33
Identified nodes11
Position-reporting nodes28
Unresolved accepted packets0
Stored duplicates0
Packets marked via_mqtt683

No human text messages or alerts appeared in the strict collection. The traffic was mostly automated location, identity, and telemetry exchange.

1. Initial RF observation

The first waterfall showed repeated LoRa-like chirps in the 433 MHz range.

Initial SDR++ waterfall showing the unknown 433 MHz signal

The first recording also showed that the channel was not captured cleanly at the recording boundary.

Initial recording spectrum showing the signal placement problem

A full-recording spectrogram made the repeated chirp structure clearer.

Full recording spectrogram

The detailed view showed the characteristic slanted chirps.

LoRa chirp detail

Measured symbol timing pointed to LoRa with the following working parameters:

Bandwidth:        250 kHz
Spreading factor: 11
Coding rate:      4/5
Explicit header:  yes
Payload CRC:      yes
Sync word:        0x2B
Nominal centre:   433.125 MHz

The symbol period matched the expected relationship:

2^11 / 250000 = 0.008192 seconds

2. Capture configuration

The corrected SDR++ setup used a 2 MS/s baseband capture with the target channel offset from the hardware centre frequency.

SDR++ source settings

SDR++ capture-ready view

Sample rate:      2 MS/s
Hardware centre:  433.270452 MHz
Target channel:   433.125 MHz
Gain:             50, manual
Recording:        baseband IQ WAV, signed Int16

3. Offline GNU Radio demodulation

The first WAV flowgraph established the basic path from recorded IQ to LoRa demodulation.

Initial WAV flowgraph

The corrected WAV flowgraph used proper IQ handling, frequency translation, decimation, and LoRa receiver configuration.

Corrected WAV flowgraph

Stereo WAV
  -> Float to Complex
  -> Frequency Xlating FIR Filter
  -> LoRa Rx
firdes.low_pass(1.0, 2000000, 150000, 50000)
Frequency shift: -145452 Hz
Decimation:      2
Output rate:     1 MS/s
LoRa bandwidth:  250 kHz

The message output needed to be selected correctly so packet boundaries were preserved.

LoRa message output selection

4. Live BladeRF reception

The next step replaced the WAV source with a live Soapy BladeRF source and forwarded packet PDUs toward the decoder.

Live BladeRF UDP flowgraph

One useful failure was a complex-to-byte mismatch: raw IQ cannot be treated as packet bytes before LoRa demodulation.

Complex byte type error

The final version gated packets by LoRa payload CRC before forwarding them over UDP.

Final CRC-gated GNU Radio flowgraph

5. Problems encountered and fixes

FailureLesson
One WAV outputIQ WAVs need two channels mapped to real and imaginary.
Complex/byte type mismatchRaw IQ must pass through LoRa demodulation before a byte-oriented block.
Scheduler requested 8200 samples but buffer held 8191High-SF receivers may need a larger minimum output buffer.
GRC crashed on 0xffBinary payloads must not be treated as UTF-8 terminal text.
Socket PDU remained silentGNU Radio messages and PDUs are not automatically interchangeable.
Sync word constructor rejected 43The installed receiver API required [0x2B].
Random packets looked encryptedCRC-invalid bytes can resemble valid protobuf fields.
Nested protobuf crashed the decoderValidate both outer and application-specific messages.

6. Decoder and persistence

The decoder consumed packet-preserving UDP output, parsed Meshtastic protobuf structures, wrote JSONL, and persisted normalized records in SQLite.

conda activate meshdecode

python ~/Downloads/meshtastic_udp_decoder_v2.py   --audit-public-keys   --jsonl ~/meshtastic/capture.jsonl   --sqlite ~/meshtastic/capture.db   --compact   --suppress-exact-duplicates

The public package includes decoder scripts and the GNU Radio files, but does not include the raw database or raw JSONL capture.

7. What the network exchanged

Application traffic

ApplicationPacketsShare %
POSITION_APP62484.8
TELEMETRY_APP7910.7
NODEINFO_APP334.5

Application traffic chart

Origin flag

Origin flagPacketsShare %
via_mqtt = false537.2
via_mqtt = true68392.8

Origin flag chart

A packet marked via_mqtt was still received over LoRa by the SDR. The flag means the packet entered the wider system through an MQTT bridge earlier in its path.

Hop behaviour

Hops usedPacketsShare %
0162.2
191.2
239153.1
326536
4506.8
550.7

Hop distribution chart

Sender concentration

The public version uses anonymized sender aliases. The point is not who the nodes are; the point is that passive collection can reveal concentration, centrality, and repeated activity.

Sender concentration chart without raw node IDs

Anonymized top senders chart

Hardware distribution

Hardware distribution chart

Position source summary

Position sourceNodesPosition packetsDistinct coordinates
LOC_EXTERNAL161
LOC_INTERNAL51812
LOC_MANUAL2260022

The original position records contained exact coordinates. The public package includes only anonymized/rounded derived exports.

Telemetry metric summary

MetricCountMinimumMedianMaximum
device_metrics.air_util_tx720.01688890.7315567.76292
device_metrics.battery_level7241101101
device_metrics.channel_utilization7206.122524.9733
device_metrics.uptime_seconds72604.84884e+061.15663e+07
device_metrics.voltage72-0.0013.98154.819
environment_metrics.barometric_pressure71005.261005.871006.4
environment_metrics.relative_humidity737.754943.840856.7432
environment_metrics.temperature731.8834.1636.32

8. Pre-hardening versus strict capture

Before CRC gating and stricter semantic validation, corrupted RF frames could look like malformed or encrypted packets. After CRC gating, the accepted dataset contained no unresolved records.

MetricPre-hardening result
Raw records2974
Decoded plaintext2823
Unresolved151

Quality hardening comparison

9. Visibility and privacy risk

The exploitation risk of this kind of traffic may be low in many hobbyist scenarios. The bigger issue is visibility: passive reception at scale can turn local RF transmissions into a durable dataset.

Data pointVisibility risk
Node IDPersistent tracking across captures.
Long name / short namePossible attribution to a person, callsign, group, or location.
Position packetsPossible home, vehicle, hiking, event, or repeater location exposure.
Packet timestampsActivity and presence patterns.
Hop metadataMesh topology and relay visibility.
Hardware modelDevice fingerprinting.
MQTT flagEvidence that traffic crossed from a wider bridge into local RF.
Repeated sender countsIdentifies central, active, or high-value nodes.

In isolation, one packet may not reveal much. Over time, repeated passive collection can show which nodes are active, which nodes are central, whether positions are static or moving, and whether traffic is bridged through MQTT.

10. Public artifacts in this package

The package includes these public derived files:

The redacted example packet shows the structure without exposing raw identifiers or exact coordinates:

{
  "event": "packet",
  "from": "node_001",
  "to": "broadcast",
  "app": "POSITION_APP",
  "decode_mode": "plaintext",
  "key_assessment": "no-encryption",
  "via_mqtt": true,
  "hop_start": 7,
  "hop_limit": 5,
  "hops_used": 2,
  "payload_fields_observed": [
    "position.latitude",
    "position.longitude",
    "position.altitude",
    "position.location_source",
    "want_response"
  ],
  "location_detail": "exact coordinates omitted in public package",
  "packet_id": "redacted",
  "channel_hash": "redacted"
}

11. Useful SQLite queries

These are representative queries used during the analysis. They are included for reproducibility, but the raw database is intentionally not published.

SELECT app, COUNT(*) AS packets
FROM packets
GROUP BY app
ORDER BY packets DESC;
SELECT hops_used, COUNT(*) AS packets
FROM packets
GROUP BY hops_used
ORDER BY hops_used;
SELECT
    CASE via_mqtt
        WHEN 1 THEN 'MQTT-originated'
        ELSE 'RF-originated'
    END AS origin,
    COUNT(*) AS packets
FROM packets
GROUP BY via_mqtt;
SELECT
    received_at,
    sender,
    packet_id,
    channel_hash,
    decode_mode,
    json_extract(packet_json, '$.payload_hex') AS payload_hex
FROM packets
WHERE decode_mode = 'unresolved'
ORDER BY id DESC;

12. Practical mitigations for Meshtastic users

Users who care about privacy should consider:

  • Disabling position broadcasting unless needed.
  • Avoiding personally identifying node names.
  • Avoiding callsigns or real names unless intentionally public.
  • Understanding whether MQTT uplink/downlink is enabled.
  • Limiting precision of shared location data.
  • Using encryption settings appropriate to the use case.
  • Treating public channels as observable by anyone with RF equipment.

13. Next improvements

  1. Store local receive power, SNR, and carrier-frequency offset.
  2. Add capture-session metadata.
  3. Keep CRC-failure counters without forwarding invalid payloads.
  4. Build topology summaries from neighbour reports and alternate paths.
  5. Test controlled nodes with plaintext, default PSK, and random AES keys.
  6. Keep exact third-party identifiers and coordinates out of public reports.

Conclusion

The central lesson was not just identifying a LoRa signal. It was building a trustworthy boundary between RF errors and protocol facts:

capture broadly
  -> derive the PHY
  -> validate the LoRa frame
  -> preserve packet boundaries
  -> parse defensively
  -> store normalized data
  -> separate payload confidentiality from metadata exposure

The result is a practical SDR research workflow and a concrete demonstration that public RF telemetry can create meaningful visibility risk even when direct exploitation risk is low.