SOL Radio Observatory
Status: operational, receive-only
Primary hardware: HackRF
Current profile: all 22 FRS channels from one fixed IQ capture
Center frequency: FRS Channel 1, 462.5625 MHz
IQ sample rate: 12 MS/s
Raw capture span: 456.5625-468.5625 MHz
Decoded FRS span: 462.5500-467.7125 MHz
SOL Radio Observatory is the receive-only RF observation layer for the Sol-37
system. It continuously acquires one wide HackRF IQ stream, renders a live
waterfall, demodulates all 22 FRS channels in parallel, records detected
activity, produces ensemble transcription and descriptive audio metadata, and
promotes sufficiently meaningful observations into the existing emergency
incident and notification workflow.
Emergency monitoring is a policy consumer of the observatory. It is not the
definition of the observatory itself.

Design Principles
- Receive only. This project does not transmit RF.
- One owner for the SDR. Every HackRF session is acquired through
radio_session.py.
- Observe before interpreting. RF activity is persisted before
transcription, inference, alerting, or household playback.
- Evidence survives failed inference. Recordings, RF metadata, candidate
transcripts, and deterministic classifications are retained even if a model
is unavailable.
- Live monitoring precedes classification. Selected demodulated audio can
be heard while the same event moves through transcription and policy.
- Emergency behavior is bounded. A single trusted PTT press is alert-worthy
but does not create a notification cascade. Repeated presses, Morse-like
signaling, directed voice, and SOS evidence escalate in stages.
- Cleanup means quarantine, not deletion. Diagnostic false positives stay
available in the complete archive but are excluded from live feeds and worker
queues.
Signal Path
text
HackRF
|
| 12 MS/s complex IQ centered at 462.5625 MHz
v
radio_session.py ownership boundary
|
+--> 4096-bin / 18-row-per-second waterfall
|
+--> 22 simultaneous FRS NFM channel paths
|
+--> adaptive RF noise-floor gate
+--> carrier and PTT pulse observations
+--> selected-channel live audio mixer
+--> per-observation WAV recording
+--> Morse timing analysis
+--> ensemble transcription
+--> descriptive audio when no speech is supported
+--> local LLM transcript reconciliation and priority inference
+--> incident policy, indicator, phone reminders, and optional Nest acknowledgment
The 12 MHz acquisition span is wider than the 5.1625 MHz FRS allocation and is
centered exactly on Channel 1. No retuning or scanner dwell is required to
monitor any FRS channel.
FRS Channel Map
| Channel | Frequency (MHz) | Channel | Frequency (MHz) |
| ---: | ---: | ---: | ---: |
| 1 | 462.5625 | 12 | 467.6625 |
| 2 | 462.5875 | 13 | 467.6875 |
| 3 | 462.6125 | 14 | 467.7125 |
| 4 | 462.6375 | 15 | 462.5500 |
| 5 | 462.6625 | 16 | 462.5750 |
| 6 | 462.6875 | 17 | 462.6000 |
| 7 | 462.7125 | 18 | 462.6250 |
| 8 | 467.5625 | 19 | 462.6500 |
| 9 | 467.5875 | 20 | 462.6750 |
| 10 | 467.6125 | 21 | 462.7000 |
| 11 | 467.6375 | 22 | 462.7250 |
The lower FRS block contains Channels 15, 1, 16, 2, 17, 3, 18, 4, 19, 5, 20,
6, 21, 7, and 22 from 462.5500 through 462.7250 MHz. The upper block contains
Channels 8-14 from 467.5625 through 467.7125 MHz.
Layered Data Model
Observation Layer
Every qualified signal starts as an observation inobservations.sqlite3. An observation records:
- channel and exact center frequency
- start, end, duration, and signal level
- modulation and source
- recording path
- carrier/PTT events
- Morse timing result
- candidate and recompiled transcripts
- descriptive audio
- classification, interest level, and lifecycle state
- an optional link to a promoted incident
Observation records are neutral RF facts. A carrier can remain passive,
become interesting, or be promoted into an incident.
Incident Layer
Promoted observations are represented in incidents.sqlite3. The incident
store also owns:
- burst evidence
- deterministic and model inference runs
- notification attempts
- persistent reminder schedules
- acknowledgment and dismissal state
- household playback requests
Policy Layer
Policy combines trusted-channel context, PTT pulse sequences, supported speech,
Morse decoding, deterministic emergency phrases, and local model output. Model
output may raise priority but cannot lower a deterministic emergency floor.
Alert Policy
| Evidence | Local indicator | Phone behavior | Repeat policy |
| --- | --- | --- | --- |
| Non-trusted short carrier/noise | No immediate prompt | None until stronger evidence | None |
| One trusted Channel 1 PTT press | Immediate incident prompt | One staged incident update after the pulse sequence closes | None |
| Multiple trusted PTT presses | Immediate incident prompt | Bounded updates | Limited, maximum 3 |
| Morse-like sequence | Immediate incident prompt | Bounded updates with decoded pattern | Limited unless emergency |
| Decoded SOS | Critical incident prompt | Immediate emergency update | Rapid, acknowledgment-bound |
| Supported voice | Incident prompt according to inference | Informational, directed, or emergency update | Policy-dependent |
This distinction prevents a single PTT click from starting an unbounded alert
storm while preserving it as a meaningful incident. Multiple short/long presses
are retained as a sequence so PTT signaling can develop into Morse evidence.
RF Detection and Noise Rejection
Broad multichannel decoding can make one common noise rise appear in many
channel filters at once. The receiver now applies a per-cycle adaptive gate:
text
effective threshold = max(configured threshold, measured noise floor + 5 dB)
Only channels above that effective threshold are opened. This supplements the
configured squelch and prevents ambient levels around -75 dB from generating
simultaneous events across the FRS map. Real nearby PTT fixtures around -42 dB
remain well above the gate.
Quarantined classifications are excluded from:
- the live Spectrum Activity feed
- transcription and inference queues
- current alert selection
- notification scheduling
They remain visible through the complete archive API and local archive window.
Audio and Transcription
Live Audio
The receiver creates demodulated audio before emergency classification. The
status indicator can:
- listen to every currently active channel
- listen only to selected channels/frequencies
- stop live monitoring
- play recent recordings
The current persisted selection is Channels 1 and 20. Selection changes affect
audio playback, not RF observation: all 22 channels continue to be decoded.
Ensemble Transcription
Each retained recording can be processed through multiple local variants and
backends. Candidate outputs are stored independently. The inference worker
compares the evidence and produces a recompiled transcript rather than treating
one recognizer as authoritative.
When supported speech is absent, the pipeline emits descriptive audio such as
carrier-only, noise burst, music-like audio, or unintelligible voice instead of
inventing spoken words. Candidate evidence is preserved when reprocessing
cannot access an old audio file.

Waterfall
The live waterfall is generated directly from the active emergency receiver,
not from a second SDR process. It currently publishes:
- 12 MHz RF span
- 4096 frequency bins
- 18 rows per second
- full-width RF frequency axis
- exact Channel 1 center marker
- overlays for all 22 FRS frequencies
- separate lower and upper FRS block strips
The browser stream is demand-driven. Loading the Radio Observatory page opens
the MJPEG waterfall transport; leaving it idle allows the browser bridge to
stand down without stopping the receiver or observation pipeline.
Project Components
| Component | Responsibility |
| --- | --- |
| radio_emergency_receiver.py | Wide IQ acquisition, channelization, NFM audio, adaptive gate, recordings, waterfall |
| radio_session.py | Exclusive HackRF ownership and health tracking |
| radio_observatory.py | Observation ledger and observation events |
| radio_morse.py | PTT pulse timing and Morse sequence decoding |
| radio_emergency.py | Incident store, promotion, push integration, schedules |
| radio_emergency_policy.py | Deterministic alert and severity floors |
| radio_emergency_transcriber.py | Candidate transcription and descriptive audio worker |
| radio_emergency_inference.py | Local LLM reconciliation and priority inference |
| radio_emergency_scheduler.py | Persistent bounded reminder delivery |
| radio_metadata_backfill.py | Archive metadata repair and ensemble reprocessing |
| radio_site_api.py | Local API, browser waterfall/audio bridge, dashboards |
| radio_emergency_indicator.py | XFCE status menu, alerts, live monitor, archive launchers |
| radio_transmission_archive.py | Complete observations, recordings, and transcript browser |
| radio_emergency_panel.py | Local incident and receiver control panel |
The source files live under /home/david/random/bin. Runtime configuration is
under /home/david/.config/radio, and state is under/home/david/.local/state/radio.
Services
The normal user-service topology is:
text
sol37-radio-api.service
radio-emergency-indicator.service
sol-radio-emergency-transcriber.service
sol-radio-emergency-inference.service
sol-radio-emergency-scheduler.service
orb-loop.service
ha-tv-indicator.service
orb-radio-confirmation.service is an on-demand unit. The wideband receiver is
started and stopped through the existing API and remains the primary HackRF
owner while active.
Local and Site Interfaces
- Local API:
http://127.0.0.1:8902 - Sol-37 page:
https://sol.system42.one/programs/radio.html - Public read-only snapshot:
/api/radio/status - Public demand-driven waterfall:
/api/radio/stream/waterfall.mjpeg - Public demand-driven audio:
/api/radio/stream/audio.mp3 - Local incident panel:
radio_emergency_panel.py - Local transmission archive:
radio_transmission_archive.py - XFCE status menu:
radio_emergency_indicator.py
Primary local API operations:
text
GET /emergency/status
GET /emergency/config
PUT /emergency/config
GET /emergency/incidents
GET /emergency/live-monitor-selection
PUT /emergency/live-monitor-selection
POST /emergency/receiver/start
POST /emergency/receiver/stop
GET /observatory/status
GET /observatory/observations
GET /observatory/observations/{id}
Operating Procedures
Inspect ownership and services:
bash
python3 /home/david/random/bin/radio_session.py status
systemctl --user --no-pager --full status \
sol37-radio-api.service \
radio-emergency-indicator.service \
sol-radio-emergency-transcriber.service \
sol-radio-emergency-inference.service \
sol-radio-emergency-scheduler.service
curl -fsS http://127.0.0.1:8902/emergency/status | python3 -m json.tool
Start all-channel receive:
bash
curl -fsS -X POST \
-H 'Content-Type: application/json' \
-d '{"channel":1,"all_channels":true,"threshold":450.0}' \
http://127.0.0.1:8902/emergency/receiver/start
Stop receive:
bash
curl -fsS -X POST http://127.0.0.1:8902/emergency/receiver/stop
Force a one-worker archive reprocessing pass:
bash
nice -n 10 python3 /home/david/random/bin/radio_metadata_backfill.py \
--force --workers 1
By default, the backfill skips quarantined diagnostic classifications. Use--include-quarantined only for an explicit forensic review.
Archive Recovery, July 23, 2026
A multichannel validation run generated a deterministic false-positive window
from 2026-07-23T04:20:00Z through 04:39:00Z. The repair:
- retained every raw database row, event, and recording
- quarantined 1,816 incident rows
- quarantined 1,818 observation rows
- stopped all notification schedules
- excluded quarantined rows from workers and live feeds
- reprocessed 63 retained incidents and 55 retained observations
- populated primary/display metadata and candidate evidence for every retained
incident
- produced 28 recompiled spoken transcripts and 51 descriptive-audio results
No records were deleted. Pre-cleanup database copies are stored under:
text
/home/david/.local/state/radio/backups/archive-cleanup-20260722-234836/
One synthetic integration-test incident references the intentionally absentradio-confirmation-test.wav; its operator-confirmed transcript and synthetic
provenance remain preserved.
Verification
Run after changing receiver, policy, API, or worker code:
bash
python3 -m py_compile /home/david/random/bin/radio_emergency*.py \
/home/david/random/bin/radio_site_api.py \
/home/david/random/bin/radio_morse.py \
/home/david/random/bin/radio_metadata_backfill.py
/home/david/.venvs/radio/bin/python \
/home/david/random/bin/test_radio_emergency.py
python3 /home/david/random/bin/radio_session.py status
Hardware validation must be finite unless the managed receiver is intentionally
being left active. The native Soapy/HackRF graph has an observed teardown race
inside libHackRFSupport.so; the receiver therefore releases its managed
session token and uses a scoped immediate process exit after native graph
shutdown. Four consecutive finite hardware runs exited cleanly after this
containment was added.
Current Boundaries
- CTCSS/DCS identity matching is not yet authoritative.
- Call-alert and Roger-beep templates still require calibrated captures.
- Privacy codes are squelch filters, not encryption.
- No emergency-services call automation exists.
- The system does not transmit RF.
- Full overnight soak and watchdog reporting remain future validation work.