Subsystem Search

substation.scanner

The API reference for RadioScanner.

RadioScanner

class RadioScanner(
    config_path: str | pathlib.Path | None = None,
    band_name: str = 'pmr',
    device_type: str = 'rtlsdr',
    device_index: int = 0,
    config: typing.Any | None = None,
    clock: VirtualClock | None = None,
    device_kwargs: dict | None = None,
)

Scan one band with one receiver: detect each transmission on its radio channels, and record it where the band records.

Build one from a loaded configuration, register a handler for each event you want with on(), or with the simpler add_state_callback() and add_recording_callback(), then await scan():

scanner = substation.scanner.RadioScanner(
        config=substation.config.load_config(),
        band_name="amateur_2m",
        device_type="rtlsdr",
)
scanner.on("channel_state", handler)
await scanner.scan()

The band, the receiver and every setting come from the configuration. The scanner's own attributes are its working state while it scans, not settings to change.

When the configuration's osc settings turn sending on, the scanner also sends its events as OSC messages, as OscEventSender describes, with no handler to register. A script that attaches its own OscEventSender to the same destination as well sends each message twice.

Set up a scanner for one band and one receiver. Nothing is opened until scan() runs.

Parameters

Raises

Members: add_recording_callback, add_state_callback, emit, off, on, scan

RadioScanner.on

RadioScanner.on(event: str, handler: typing.Callable) -> None

Subscribe a handler to one of the scanner's events.

The handler is called with the event's values as keyword arguments:

Every value is a plain Python type (bool, int, float, str, or None), so a payload can go straight to json.dumps.

When they fire:

ctcss_hz and dcs_code on channel_state carry a tone detected when a radio channel turns ON, and are otherwise None; an OFF always carries None. On recording_saved they carry whatever tone was detected during the activation, or None. Tone detection has not yet been thoroughly tested with real radios.

Handlers may be plain functions or coroutine functions. A coroutine function always runs on the scanner's event loop, and the end of a scan waits for it. A plain function runs on the event loop for channel_state and recording_started, and on the thread that emits the event for the others: the processing thread for noise_floor and channel_snr. See emit() for the details.

Parameters

RadioScanner.off

RadioScanner.off(event: str, handler: typing.Callable) -> None

Unsubscribe a handler from an event. A handler that was not subscribed is ignored.

Parameters

RadioScanner.emit

RadioScanner.emit(
    event: str,
    loop: asyncio.AbstractEventLoop | None = None,
    **kwargs: typing.Any,
) -> None

Fire all handlers registered for an event. The scanner calls this itself; a program embedding it subscribes with on().

How each handler is called, by its kind and whether loop is given:

A handler's exception is caught wherever the handler runs, and logged at WARNING with its traceback the first time that handler fails, then at DEBUG. A failing handler never stops the other handlers firing.

Parameters

RadioScanner.add_state_callback

RadioScanner.add_state_callback(callback: typing.Callable) -> None

Call a function each time a radio channel turns ON or OFF: the channel_state event, in a simpler form.

The function receives band_name, channel_index, is_active and snr_db as positional arguments. For the radio channel's frequency or a detected tone, subscribe to channel_state with on() instead.

Parameters

RadioScanner.add_recording_callback

RadioScanner.add_recording_callback(callback: typing.Callable) -> None

Call a function each time a recording is saved: the recording_saved event, in a simpler form. It is not called for a recording the noise checks discard.

The function receives band_name, channel_index and file_path as positional arguments. For the radio channel's frequency or a detected tone, subscribe to recording_saved with on() instead.

Parameters

RadioScanner.scan

async RadioScanner.scan() -> None

Scan until cancelled, or until an IQ file has been played to its end.

It opens the receiver, streams IQ samples from it in the background, and processes each slice on a worker thread, so the event loop stays free for the program embedding it.

Returns when an IQ file has been played to its end, or when the scan is cancelled, as Ctrl+C cancels it. Anything that stops the scan early is raised once the recordings and the device have been closed: an error opening or configuring the device, an error processing a slice, or a live device that stops streaming, which raises the error it failed with, or RuntimeError when it gave none.