Pressing `l` during a scan locked a frequency out for that run only, so the
same birdie had to be locked out again on every later one. It now writes
back to the settings file the run started from -- only that one key, since
a scan's config also holds whatever was passed on the command line for this
run and saving all of it would quietly make those permanent. The file is
read, its lock-outs replaced, the rest left as it was. `save_lockouts`
turns it off for anyone who would rather their config were never touched.
Lock-outs were also single frequencies only. They are now a list of
frequencies and spans -- "162.55M, 450M-455M, 88M to 108M" -- which is what
a pager band or a noisy stretch of spectrum actually is. A point is still
widened by the lock-out width; a span is taken exactly as written, because
whoever typed it already said how wide it is.
The scanner matched lock-outs by rounding a frequency into a bucket of the
lock-out width, which cannot express a span and was never exact at the
edges. It now holds intervals and tests them directly.
Settings files that predate this hold a bare number per lock-out, and still
mean the same thing: Lockout.coerce takes numbers, strings, pairs and dicts,
so old profiles load untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every setting was checked by setting all of them to something other than
their default, saving, loading, and comparing: all 54 survived, ranges kept
every field of their own, and the default settings file behaved the same as
a named profile. Those checks are now tests rather than a one-off, along
with a check that no setting is saved but never read, and that a profile
written by an older version still loads.
Two gaps were real.
The plain display could only be asked for per invocation. It was a
hand-written flag rather than an entry in the settings table, so unlike
`quiet` -- which does the same sort of thing -- it could not be saved,
leaving anyone who runs over ssh to pass --plain every time. It is a
setting now, which gives it a menu entry and --no-plain for free.
Gain displayed as "28.5 dB" but refused to parse "28.5 dB" back, because it
was the one numeric setting that called float() instead of the unit-aware
parser every other one uses. The menu shows a value as the example of how
to type it, so what it shows has to be something it accepts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sweeps any set of frequency ranges, records what it finds, and works out
what kind of signal it was.
- Frequency ranges entered by hand or picked from a 135-entry US band plan,
including whole-band and all-CW sweeps that resolve the demodulator per
segment.
- Detection calibrated against the peak-hold detector's own noise statistics,
so the threshold means real margin over static rather than over the floor.
- A content gate: captures are kept only if they carry voice, decodable CW,
or an identified digital keying scheme. Speech is recognised by a pitch
track that drifts, which static cannot imitate.
- Identification of NFM/WFM/AM/SSB, CW with Morse decoded to text, P25, DMR,
NXDN, D-STAR, POCSAG, FLEX, ACARS, AIS, APRS, n-FSK and n-PSK.
- Gapless streaming capture, with the signal path fast enough to keep up in
real time, so recordings play back at the right speed.
- Optional one-file-per-frequency recording with spoken timestamps, and
speech-to-text transcription.
- Menus and command line generated from one settings table, so neither can
offer something the other cannot; settings persist in ~/.config.
367 tests, run against synthetic signals, a built-in receiver simulator, and
real hardware.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>