pluto11 is a passive desktop application. It combines two different kinds of evidence without treating them as interchangeable:
- RF observations are measurements of energy received by an ADALM-Pluto.
- Network records are identities obtained from Windows WLAN metadata or successfully decoded management frames.
An RF peak is not an SSID, and an SSID overlay is not proof that every signal inside its advertised channel width came from that access point. Keeping those claims separate is the central design rule of the project.
Pluto / mock / replay capture
|
v
scanner backend -> CaptureResult -> analysis.measure -> ChannelMeasurement
| | |
| | +--> weighted channel/history aggregates
| +--> Welch PSD -> panorama assembler -> BandSpectrumEvent
|
+--> optional in-process OFDM decoder -> parsed management frames --+
|
Windows netsh scan ------------------------------------------------------+--> NetworkDatabase -> NetworksEvent
External decoder UDP frames ---------------------------------------------+
|
v
SurveyEngine event queue -> Tk pages / exports
SurveyEngine is the coordinator and the only component that communicates
with scanner backends. Worker threads produce immutable events; the Tk main
thread consumes them. Scanner workers must never touch Tk widgets.
| Area | Main modules | Responsibility |
|---|---|---|
| Application lifecycle | app.py, models.py, config.py, logging_setup.py |
Engine, events, configuration, CLI, source lifecycle. |
| Radio scheduling | channels.py, scheduler.py, scanner/ |
Channel plans, dwell policy, Pluto/mock/replay devices, capture persistence. |
| RF analysis | analysis/power.py, occupancy.py, bursts.py, spectrum.py, interference.py |
dBFS, short-time thresholds, burst periodicity, Welch spectra and conservative RF-pattern evidence for one capture. |
| Aggregation and panorama | analysis/aggregation.py, panorama.py |
Duration/sample-weighted channel summaries, time buckets, overlapping absolute-frequency sweeps. |
| Calibration | analysis/calibration.py, receiver_baseline.py |
dBFS-to-dBm calibration and separate terminated-input spectrum flattening. |
| Wi-Fi evidence | wifi/ |
Windows WLAN parsing, management-frame parsing, network merge/storage, UDP and IQ OFDM decode paths. |
| Presentation | gui/ |
Main window and individual pages; rendering only, never hardware ownership. |
| Import/export | export/ |
CSV, schema-versioned session JSON and replay-format IQ captures. |
| Optional GNU Radio bridge | gnuradio/ |
External decoder documentation and UDP bridge for a separately owned radio. |
ChannelMeasurement.busy_percent describes the fraction of usable time blocks
whose power inside a narrow cell at the nominal channel center exceeds the
selected threshold. The default cell is 4 MHz. Each capture remains 20 MHz,
but using its entire power for every 5 MHz-spaced 2.4 GHz channel would count
the same emitter repeatedly and flatten the channel chart. LO dither is
accounted for when mapping the cell to absolute RF frequency. In the UI and
documentation this is called RF activity. It can represent Wi-Fi traffic,
corrupted or weak Wi-Fi frames, Bluetooth, microwave leakage, proprietary
devices, receiver artifacts, or short measurement bursts.
It is not equivalent to a Wi-Fi chipset's Clear Channel Assessment counter, and it is not a count of access points. The default noise-relative threshold is useful for local comparisons but can move with the local noise estimate; use a fixed threshold and a controlled setup when comparing sessions.
Aggregates use observation duration when present, otherwise usable sample count for backward compatibility. Mean and noise power are converted to linear power before weighting and converted back to dBFS afterwards. A short capture therefore cannot outweigh a long capture merely because it generated one final percentage value.
The default 512-sample energy window represents 25.6 microseconds at the default 20 MS/s sample rate. The UI reports activity as a percentage of observed time and reports observation coverage separately. Unobserved wall-clock time is never counted as idle.
A 20 MHz Wi-Fi waveform can legitimately raise several neighboring center cells. That energy shape is not used to identify its primary Wi-Fi channel. Network channel identity is accepted only from advertised DS Parameter Set or HT Operation metadata, or from an operating-system Wi-Fi scan; the tuned Pluto channel is never substituted when a decoded frame omits that metadata.
RfEvidence may describe captured energy as quiet, narrowband, bursty,
bursty broadband, periodic broadband, continuous broadband, or mixed. It
uses short-time burst structure and frequency-domain shape. The label is an
engineering clue, not an emitter identity.
On 2.4 GHz, a sufficiently repeatable roughly 7--12.5 ms broadband pulse train may be described as microwave-like interference is possible, not proven. The wording is part of the data contract: it must not be shortened to "microwave detected." Other Wi-Fi or non-Wi-Fi sources and measurement artifacts can produce similar finite captures.
NetworkRecord is keyed by BSSID and can merge the following sources:
windows_wlan: the optional Windowsnetsh wlanscan;decoder: a successfully parsed raw management frame from the integrated decoder or an external UDP producer;merged: evidence from more than one source.
The database deliberately does not create a network from energy alone. Hidden networks remain hidden unless another valid frame or metadata source provides an SSID.
The built-in decoder works on finite IQ captures owned by the survey engine. It prefers GNU Radio/gr-ieee802-11 when installed and falls back to a native legacy 20 MHz OFDM decoder. Both are opportunistic, receive-only helpers; they do not turn an RF survey into a monitor-mode Wi-Fi adapter.
Current decoder limits include legacy OFDM only (6--54 Mb/s), valid-FCS frames only for identity creation, and no DSSS/CCK, HT/VHT/HE PHY decoding. Many 2.4 GHz compatibility beacons use DSSS, so an empty decoder result must not be interpreted as an empty channel. Decoder diagnostics distinguish inspected captures, preamble candidates, SIGNAL headers, unsupported rates, FCS failures and parsed frames.
Pluto sees one approximately 20 MHz slice at a time. A survey is sequential, not a simultaneous observation of an entire band.
For an RF panorama, the engine makes overlapping channel-centred captures and
cycles the LO through nominal, negative-dither and positive-dither positions.
The PanoramaAssembler maps their valid FFT bins to an absolute-frequency
grid, blends overlap in linear power with tapered weights, and publishes a
waterfall row only after every expected visit completes. Missing coverage is
represented as missing data, not quiet spectrum.
Time history is similarly bucketed. Blank cells mean that the channel was not observed in that time bucket. They must not be interpreted as zero RF activity or connected with a line to a later asynchronous sample.
There are two intentionally separate calibration systems:
- Signal calibration (
analysis/calibration.py) maps dBFS to estimated dBm using known signal measurements for a particular device, frequency, gain and antenna/cable setup. - 50-ohm receiver baseline (
analysis/receiver_baseline.py) flattens repeatable receiver shape, DC leakage and fixed spurs for exact LO/gain/ sample-rate settings. It is not an absolute power calibration and does not subtract real RF noise.
Neither profile should be extrapolated to another band, gain, sample rate, antenna path or unmeasured LO. The application leaves values in dBFS when a matching signal-calibration point is unavailable.
- A
DeviceWorkerowns operations on one scanner backend. SurveyEngineowns the workers, scheduler, source threads and event queue.- The integrated decoder consumes already captured IQ and never opens a second IIO context.
- A separate GNU Radio process must use a separate Pluto. Two independent processes must never open, stream from or retune the same Pluto URI.
netshand UDP are optional metadata sources. Their failure must not stop RF acquisition.
These ownership rules are functional requirements, not merely style: a second LO writer or continuous stream can corrupt both spectrum measurements and packet decoding.
- Session JSON uses an explicit schema version in
export/session_export.py. - Measurement exports preserve observation duration, burst periodicity and RF-pattern evidence so offline analysis can reproduce the UI's qualified claim.
- Replay directories use
meta.jsonplus orderedcapture_*.npycomplex64 arrays as defined byscanner/capture_format.py. - UDP accepts documented raw MAC, radiotap-plus-MAC or ASCII-hex datagrams;
see
../gnuradio/UDP_FORMAT.md.
When changing a persisted schema or event shape, add a compatibility test and document migration behaviour in the pull request.