Add multi-SDR support to WeFax decoder (HackRF, LimeSDR, Airspy, SDRPlay)

Replace hardcoded rtl_fm with SDRFactory abstraction layer so WeFax works
with any supported SDR hardware, matching the pattern used by APRS and
other modes. RTL-SDR direct sampling flag preserved for HF reception.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-25 16:45:07 +00:00
parent 2202e3ed98
commit f3158cbb69
4 changed files with 295 additions and 269 deletions

View File

@@ -11,17 +11,18 @@ import queue
from flask import Blueprint, Response, jsonify, request, send_file
import app as app_module
from utils.logging import get_logger
from utils.sse import sse_stream_fanout
from utils.validation import validate_frequency
from utils.wefax import get_wefax_decoder
from utils.wefax_stations import (
WEFAX_USB_ALIGNMENT_OFFSET_KHZ,
get_current_broadcasts,
get_station,
load_stations,
resolve_tuning_frequency_khz,
)
from utils.logging import get_logger
from utils.sdr import SDRType
from utils.sse import sse_stream_fanout
from utils.validation import validate_frequency
from utils.wefax import get_wefax_decoder
from utils.wefax_stations import (
WEFAX_USB_ALIGNMENT_OFFSET_KHZ,
get_current_broadcasts,
get_station,
load_stations,
resolve_tuning_frequency_khz,
)
logger = get_logger('intercept.wefax')
@@ -34,29 +35,29 @@ _wefax_queue: queue.Queue = queue.Queue(maxsize=100)
wefax_active_device: int | None = None
def _progress_callback(data: dict) -> None:
"""Callback to queue progress updates for SSE stream."""
global wefax_active_device
try:
_wefax_queue.put_nowait(data)
except queue.Full:
try:
_wefax_queue.get_nowait()
_wefax_queue.put_nowait(data)
except queue.Empty:
pass
# Ensure manually claimed SDR devices are always released when a
# decode session ends on its own (complete/error/stopped).
if (
isinstance(data, dict)
and data.get('type') == 'wefax_progress'
and data.get('status') in ('complete', 'error', 'stopped')
and wefax_active_device is not None
):
app_module.release_sdr_device(wefax_active_device)
wefax_active_device = None
def _progress_callback(data: dict) -> None:
"""Callback to queue progress updates for SSE stream."""
global wefax_active_device
try:
_wefax_queue.put_nowait(data)
except queue.Full:
try:
_wefax_queue.get_nowait()
_wefax_queue.put_nowait(data)
except queue.Empty:
pass
# Ensure manually claimed SDR devices are always released when a
# decode session ends on its own (complete/error/stopped).
if (
isinstance(data, dict)
and data.get('type') == 'wefax_progress'
and data.get('status') in ('complete', 'error', 'stopped')
and wefax_active_device is not None
):
app_module.release_sdr_device(wefax_active_device)
wefax_active_device = None
@wefax_bp.route('/status')
@@ -75,16 +76,16 @@ def start_decoder():
"""Start WeFax decoder.
JSON body:
{
"frequency_khz": 4298,
"station": "NOJ",
"device": 0,
"gain": 40,
"ioc": 576,
"lpm": 120,
"direct_sampling": true,
"frequency_reference": "auto" // auto, carrier, or dial
}
{
"frequency_khz": 4298,
"station": "NOJ",
"device": 0,
"gain": 40,
"ioc": 576,
"lpm": 120,
"direct_sampling": true,
"frequency_reference": "auto" // auto, carrier, or dial
}
"""
decoder = get_wefax_decoder()
@@ -122,36 +123,42 @@ def start_decoder():
'message': f'Invalid frequency: {e}',
}), 400
station = str(data.get('station', '')).strip()
device_index = data.get('device', 0)
gain = float(data.get('gain', 40.0))
ioc = int(data.get('ioc', 576))
lpm = int(data.get('lpm', 120))
direct_sampling = bool(data.get('direct_sampling', True))
frequency_reference = str(data.get('frequency_reference', 'auto')).strip().lower()
if not frequency_reference:
frequency_reference = 'auto'
try:
tuned_frequency_khz, resolved_reference, usb_offset_applied = (
resolve_tuning_frequency_khz(
listed_frequency_khz=frequency_khz,
station_callsign=station,
frequency_reference=frequency_reference,
)
)
tuned_mhz = tuned_frequency_khz / 1000.0
validate_frequency(tuned_mhz, min_mhz=2.0, max_mhz=30.0)
except ValueError as e:
return jsonify({
'status': 'error',
'message': f'Invalid frequency settings: {e}',
}), 400
# Validate IOC and LPM
if ioc not in (288, 576):
return jsonify({
'status': 'error',
station = str(data.get('station', '')).strip()
device_index = data.get('device', 0)
gain = float(data.get('gain', 40.0))
ioc = int(data.get('ioc', 576))
lpm = int(data.get('lpm', 120))
direct_sampling = bool(data.get('direct_sampling', True))
frequency_reference = str(data.get('frequency_reference', 'auto')).strip().lower()
sdr_type_str = str(data.get('sdr_type', 'rtlsdr')).lower()
try:
sdr_type = SDRType(sdr_type_str)
except ValueError:
sdr_type = SDRType.RTL_SDR
if not frequency_reference:
frequency_reference = 'auto'
try:
tuned_frequency_khz, resolved_reference, usb_offset_applied = (
resolve_tuning_frequency_khz(
listed_frequency_khz=frequency_khz,
station_callsign=station,
frequency_reference=frequency_reference,
)
)
tuned_mhz = tuned_frequency_khz / 1000.0
validate_frequency(tuned_mhz, min_mhz=2.0, max_mhz=30.0)
except ValueError as e:
return jsonify({
'status': 'error',
'message': f'Invalid frequency settings: {e}',
}), 400
# Validate IOC and LPM
if ioc not in (288, 576):
return jsonify({
'status': 'error',
'message': 'IOC must be 288 or 576',
}), 400
@@ -172,34 +179,35 @@ def start_decoder():
'message': error,
}), 409
# Set callback and start
decoder.set_callback(_progress_callback)
success = decoder.start(
frequency_khz=tuned_frequency_khz,
station=station,
device_index=device_int,
gain=gain,
ioc=ioc,
lpm=lpm,
# Set callback and start
decoder.set_callback(_progress_callback)
success = decoder.start(
frequency_khz=tuned_frequency_khz,
station=station,
device_index=device_int,
gain=gain,
ioc=ioc,
lpm=lpm,
direct_sampling=direct_sampling,
sdr_type=sdr_type_str,
)
if success:
wefax_active_device = device_int
return jsonify({
'status': 'started',
'frequency_khz': frequency_khz,
'tuned_frequency_khz': tuned_frequency_khz,
'frequency_reference': resolved_reference,
'usb_offset_applied': usb_offset_applied,
'usb_offset_khz': (
WEFAX_USB_ALIGNMENT_OFFSET_KHZ if usb_offset_applied else 0.0
),
'station': station,
'ioc': ioc,
'lpm': lpm,
'device': device_int,
})
return jsonify({
'status': 'started',
'frequency_khz': frequency_khz,
'tuned_frequency_khz': tuned_frequency_khz,
'frequency_reference': resolved_reference,
'usb_offset_applied': usb_offset_applied,
'usb_offset_khz': (
WEFAX_USB_ALIGNMENT_OFFSET_KHZ if usb_offset_applied else 0.0
),
'station': station,
'ioc': ioc,
'lpm': lpm,
'device': device_int,
})
else:
app_module.release_sdr_device(device_int)
return jsonify({
@@ -322,16 +330,16 @@ def enable_schedule():
"""Enable auto-scheduling of WeFax broadcast captures.
JSON body:
{
"station": "NOJ",
"frequency_khz": 4298,
"device": 0,
"gain": 40,
"ioc": 576,
"lpm": 120,
"direct_sampling": true,
"frequency_reference": "auto" // auto, carrier, or dial
}
{
"station": "NOJ",
"frequency_khz": 4298,
"device": 0,
"gain": 40,
"ioc": 576,
"lpm": 120,
"direct_sampling": true,
"frequency_reference": "auto" // auto, carrier, or dial
}
Returns:
JSON with scheduler status.
@@ -365,61 +373,61 @@ def enable_schedule():
}), 400
device = int(data.get('device', 0))
gain = float(data.get('gain', 40.0))
ioc = int(data.get('ioc', 576))
lpm = int(data.get('lpm', 120))
direct_sampling = bool(data.get('direct_sampling', True))
frequency_reference = str(data.get('frequency_reference', 'auto')).strip().lower()
if not frequency_reference:
frequency_reference = 'auto'
try:
tuned_frequency_khz, resolved_reference, usb_offset_applied = (
resolve_tuning_frequency_khz(
listed_frequency_khz=frequency_khz,
station_callsign=station,
frequency_reference=frequency_reference,
)
)
tuned_mhz = tuned_frequency_khz / 1000.0
validate_frequency(tuned_mhz, min_mhz=2.0, max_mhz=30.0)
except ValueError as e:
return jsonify({
'status': 'error',
'message': f'Invalid frequency settings: {e}',
}), 400
scheduler = get_wefax_scheduler()
scheduler.set_callbacks(_progress_callback, _scheduler_event_callback)
try:
result = scheduler.enable(
station=station,
frequency_khz=tuned_frequency_khz,
device=device,
gain=gain,
ioc=ioc,
lpm=lpm,
direct_sampling=direct_sampling,
gain = float(data.get('gain', 40.0))
ioc = int(data.get('ioc', 576))
lpm = int(data.get('lpm', 120))
direct_sampling = bool(data.get('direct_sampling', True))
frequency_reference = str(data.get('frequency_reference', 'auto')).strip().lower()
if not frequency_reference:
frequency_reference = 'auto'
try:
tuned_frequency_khz, resolved_reference, usb_offset_applied = (
resolve_tuning_frequency_khz(
listed_frequency_khz=frequency_khz,
station_callsign=station,
frequency_reference=frequency_reference,
)
)
tuned_mhz = tuned_frequency_khz / 1000.0
validate_frequency(tuned_mhz, min_mhz=2.0, max_mhz=30.0)
except ValueError as e:
return jsonify({
'status': 'error',
'message': f'Invalid frequency settings: {e}',
}), 400
scheduler = get_wefax_scheduler()
scheduler.set_callbacks(_progress_callback, _scheduler_event_callback)
try:
result = scheduler.enable(
station=station,
frequency_khz=tuned_frequency_khz,
device=device,
gain=gain,
ioc=ioc,
lpm=lpm,
direct_sampling=direct_sampling,
)
except Exception:
logger.exception("Failed to enable WeFax scheduler")
return jsonify({
'status': 'error',
'message': 'Failed to enable scheduler',
}), 500
return jsonify({
'status': 'ok',
**result,
'frequency_khz': frequency_khz,
'tuned_frequency_khz': tuned_frequency_khz,
'frequency_reference': resolved_reference,
'usb_offset_applied': usb_offset_applied,
'usb_offset_khz': (
WEFAX_USB_ALIGNMENT_OFFSET_KHZ if usb_offset_applied else 0.0
),
})
return jsonify({
'status': 'error',
'message': 'Failed to enable scheduler',
}), 500
return jsonify({
'status': 'ok',
**result,
'frequency_khz': frequency_khz,
'tuned_frequency_khz': tuned_frequency_khz,
'frequency_reference': resolved_reference,
'usb_offset_applied': usb_offset_applied,
'usb_offset_khz': (
WEFAX_USB_ALIGNMENT_OFFSET_KHZ if usb_offset_applied else 0.0
),
})
@wefax_bp.route('/schedule/disable', methods=['POST'])

View File

@@ -148,20 +148,20 @@ var WeFax = (function () {
opt.textContent = f.khz + ' kHz — ' + f.description;
sel.appendChild(opt);
});
}
// ---- Start / Stop ----
function selectedFrequencyReference() {
var alignCheckbox = document.getElementById('wefaxAutoUsbAlign');
if (alignCheckbox && !alignCheckbox.checked) {
return 'dial';
}
return 'auto';
}
function start() {
if (state.running) return;
}
// ---- Start / Stop ----
function selectedFrequencyReference() {
var alignCheckbox = document.getElementById('wefaxAutoUsbAlign');
if (alignCheckbox && !alignCheckbox.checked) {
return 'dial';
}
return 'auto';
}
function start() {
if (state.running) return;
var freqSel = document.getElementById('wefaxFrequency');
var freqKhz = freqSel ? parseFloat(freqSel.value) : 0;
@@ -177,41 +177,42 @@ var WeFax = (function () {
var gainInput = document.getElementById('wefaxGain');
var dsCheckbox = document.getElementById('wefaxDirectSampling');
var deviceSel = document.getElementById('rtlDevice');
var device = deviceSel ? parseInt(deviceSel.value, 10) || 0 : 0;
var device = (typeof getSelectedDevice === 'function')
? parseInt(getSelectedDevice(), 10) || 0 : 0;
var body = {
frequency_khz: freqKhz,
station: station,
device: device,
gain: gainInput ? parseFloat(gainInput.value) || 40 : 40,
ioc: iocSel ? parseInt(iocSel.value, 10) : 576,
lpm: lpmSel ? parseInt(lpmSel.value, 10) : 120,
direct_sampling: dsCheckbox ? dsCheckbox.checked : true,
frequency_reference: selectedFrequencyReference(),
};
sdr_type: (typeof getSelectedSDRType === 'function') ? getSelectedSDRType() : 'rtlsdr',
gain: gainInput ? parseFloat(gainInput.value) || 40 : 40,
ioc: iocSel ? parseInt(iocSel.value, 10) : 576,
lpm: lpmSel ? parseInt(lpmSel.value, 10) : 120,
direct_sampling: dsCheckbox ? dsCheckbox.checked : true,
frequency_reference: selectedFrequencyReference(),
};
fetch('/wefax/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.status === 'started' || data.status === 'already_running') {
var tunedKhz = Number(data.tuned_frequency_khz);
if (isNaN(tunedKhz) || tunedKhz <= 0) tunedKhz = freqKhz;
state.running = true;
updateButtons(true);
if (data.usb_offset_applied) {
setStatus('Scanning ' + tunedKhz + ' kHz (USB aligned from ' + freqKhz + ' kHz)...');
} else {
setStatus('Scanning ' + tunedKhz + ' kHz...');
}
setStripFreq(tunedKhz);
connectSSE();
} else {
var errMsg = data.message || 'unknown error';
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.status === 'started' || data.status === 'already_running') {
var tunedKhz = Number(data.tuned_frequency_khz);
if (isNaN(tunedKhz) || tunedKhz <= 0) tunedKhz = freqKhz;
state.running = true;
updateButtons(true);
if (data.usb_offset_applied) {
setStatus('Scanning ' + tunedKhz + ' kHz (USB aligned from ' + freqKhz + ' kHz)...');
} else {
setStatus('Scanning ' + tunedKhz + ' kHz...');
}
setStripFreq(tunedKhz);
connectSSE();
} else {
var errMsg = data.message || 'unknown error';
setStatus('Error: ' + errMsg);
showStripError(errMsg);
}
@@ -342,25 +343,25 @@ var WeFax = (function () {
if (idleEl) idleEl.style.display = 'none';
}
// Image complete
if (data.status === 'complete' && data.image) {
scopeImageBurst = 1.0;
loadImages();
setStatus('Image decoded: ' + (data.line_count || '?') + ' lines');
}
if (data.status === 'complete') {
state.running = false;
updateButtons(false);
if (!state.schedulerEnabled) {
disconnectSSE();
}
}
if (data.status === 'error') {
state.running = false;
updateButtons(false);
showStripError(data.message || 'Decode error');
// Image complete
if (data.status === 'complete' && data.image) {
scopeImageBurst = 1.0;
loadImages();
setStatus('Image decoded: ' + (data.line_count || '?') + ' lines');
}
if (data.status === 'complete') {
state.running = false;
updateButtons(false);
if (!state.schedulerEnabled) {
disconnectSSE();
}
}
if (data.status === 'error') {
state.running = false;
updateButtons(false);
showStripError(data.message || 'Decode error');
}
if (data.status === 'stopped') {
@@ -1062,24 +1063,24 @@ var WeFax = (function () {
station: station,
frequency_khz: freqKhz,
device: device,
gain: gainInput ? parseFloat(gainInput.value) || 40 : 40,
ioc: iocSel ? parseInt(iocSel.value, 10) : 576,
lpm: lpmSel ? parseInt(lpmSel.value, 10) : 120,
direct_sampling: dsCheckbox ? dsCheckbox.checked : true,
frequency_reference: selectedFrequencyReference(),
}),
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.status === 'ok') {
var status = 'Auto-capture enabled — ' + (data.scheduled_count || 0) + ' broadcasts scheduled';
if (data.usb_offset_applied && !isNaN(Number(data.tuned_frequency_khz))) {
status += ' (tuning ' + Number(data.tuned_frequency_khz) + ' kHz)';
}
setStatus(status);
syncSchedulerCheckboxes(true);
state.schedulerEnabled = true;
connectSSE();
gain: gainInput ? parseFloat(gainInput.value) || 40 : 40,
ioc: iocSel ? parseInt(iocSel.value, 10) : 576,
lpm: lpmSel ? parseInt(lpmSel.value, 10) : 120,
direct_sampling: dsCheckbox ? dsCheckbox.checked : true,
frequency_reference: selectedFrequencyReference(),
}),
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.status === 'ok') {
var status = 'Auto-capture enabled — ' + (data.scheduled_count || 0) + ' broadcasts scheduled';
if (data.usb_offset_applied && !isNaN(Number(data.tuned_frequency_khz))) {
status += ' (tuning ' + Number(data.tuned_frequency_khz) + ' kHz)';
}
setStatus(status);
syncSchedulerCheckboxes(true);
state.schedulerEnabled = true;
connectSSE();
startSchedulerPoll();
} else {
setStatus('Scheduler error: ' + (data.message || 'unknown'));

View File

@@ -44,18 +44,18 @@
<label>Gain (dB)</label>
<input type="number" id="wefaxGain" value="40" step="1" min="0" max="50">
</div>
<div class="form-group" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="wefaxDirectSampling" checked>
<label for="wefaxDirectSampling" style="margin: 0; cursor: pointer;">Direct Sampling (Q-branch, required for HF)</label>
</div>
<div class="form-group" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="wefaxAutoUsbAlign" checked>
<label for="wefaxAutoUsbAlign" style="margin: 0; cursor: pointer;">Auto USB align listed carrier frequencies (-1.9 kHz)</label>
</div>
<p class="info-text" style="font-size: 11px; color: var(--text-dim); margin-top: -4px;">
Disable this if your source already provides USB dial frequencies.
</p>
</div>
<div class="form-group" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="wefaxDirectSampling" checked>
<label for="wefaxDirectSampling" style="margin: 0; cursor: pointer;">Direct Sampling (Q-branch, required for HF)</label>
</div>
<div class="form-group" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="wefaxAutoUsbAlign" checked>
<label for="wefaxAutoUsbAlign" style="margin: 0; cursor: pointer;">Auto USB align listed carrier frequencies (-1.9 kHz)</label>
</div>
<p class="info-text" style="font-size: 11px; color: var(--text-dim); margin-top: -4px;">
Disable this if your source already provides USB dial frequencies.
</p>
</div>
<div class="section">
<h3>Auto Capture</h3>
@@ -80,7 +80,7 @@
<div style="background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; margin-bottom: 10px;">
<strong style="color: #ffaa00; font-size: 12px;">Requirements</strong>
<ul style="margin: 6px 0 0 14px; padding: 0;">
<li><strong style="color: var(--text-primary);">SDR:</strong> RTL-SDR v3/v4 with direct sampling mode</li>
<li><strong style="color: var(--text-primary);">SDR:</strong> RTL-SDR (direct sampling), HackRF, LimeSDR, Airspy, or SDRPlay</li>
<li><strong style="color: var(--text-primary);">Antenna:</strong> Long wire (10m+), random wire, or dipole for target band</li>
<li><strong style="color: var(--text-primary);">Mode:</strong> USB (Upper Sideband) demodulation</li>
<li><strong style="color: var(--text-primary);">Signals:</strong> Moderate &mdash; HF propagation varies by time of day</li>

View File

@@ -1,10 +1,11 @@
"""WeFax (Weather Fax) decoder.
Decodes HF radiofax (weather fax) transmissions using RTL-SDR direct
sampling mode. The decoder implements the standard WeFax AM protocol:
Decodes HF radiofax (weather fax) transmissions using any supported SDR
(RTL-SDR, HackRF, LimeSDR, Airspy, SDRPlay) via the SDRFactory
abstraction layer. The decoder implements the standard WeFax AM protocol:
carrier 1900 Hz, deviation +/-400 Hz (black=1500, white=2300).
Pipeline: rtl_fm -M usb -E direct2 -> stdout PCM -> Python DSP state machine
Pipeline: rtl_fm/rx_fm -M usb -> stdout PCM -> Python DSP state machine
State machine: SCANNING -> PHASING -> RECEIVING -> COMPLETE
"""
@@ -30,6 +31,7 @@ import numpy as np
from utils.dependencies import get_tool_path
from utils.logging import get_logger
from utils.sdr import SDRFactory, SDRType
logger = get_logger('intercept.wefax')
@@ -262,17 +264,19 @@ class WeFaxDecoder:
ioc: int = DEFAULT_IOC,
lpm: int = DEFAULT_LPM,
direct_sampling: bool = True,
sdr_type: str = 'rtlsdr',
) -> bool:
"""Start WeFax decoder.
Args:
frequency_khz: Frequency in kHz (e.g. 4298 for NOJ).
station: Station callsign for metadata.
device_index: RTL-SDR device index.
device_index: SDR device index.
gain: Receiver gain in dB.
ioc: Index of Cooperation (576 or 288).
lpm: Lines per minute (120 or 60).
direct_sampling: Enable RTL-SDR direct sampling for HF.
sdr_type: SDR hardware type (rtlsdr, hackrf, limesdr, airspy, sdrplay).
Returns:
True if started successfully.
@@ -288,6 +292,7 @@ class WeFaxDecoder:
self._device_index = device_index
self._gain = gain
self._direct_sampling = direct_sampling
self._sdr_type = sdr_type
self._sample_rate = DEFAULT_SAMPLE_RATE
try:
@@ -312,27 +317,39 @@ class WeFaxDecoder:
return False
def _start_pipeline(self) -> None:
"""Start rtl_fm subprocess in USB mode for WeFax."""
rtl_fm_path = get_tool_path('rtl_fm')
if not rtl_fm_path:
raise RuntimeError('rtl_fm not found')
"""Start SDR FM demod subprocess in USB mode for WeFax."""
try:
sdr_type_enum = SDRType(self._sdr_type)
except ValueError:
sdr_type_enum = SDRType.RTL_SDR
freq_hz = int(self._frequency_khz * 1000)
# Validate that the required tool is available
if sdr_type_enum == SDRType.RTL_SDR:
if not get_tool_path('rtl_fm'):
raise RuntimeError('rtl_fm not found')
else:
if not get_tool_path('rx_fm'):
raise RuntimeError('rx_fm not found (required for non-RTL-SDR devices)')
rtl_cmd = [
rtl_fm_path,
'-d', str(self._device_index),
'-f', str(freq_hz),
'-M', 'usb',
'-s', str(self._sample_rate),
'-r', str(self._sample_rate),
'-g', str(self._gain),
]
sdr_device = SDRFactory.create_default_device(
sdr_type_enum, index=self._device_index)
builder = SDRFactory.get_builder(sdr_type_enum)
rtl_cmd = builder.build_fm_demod_command(
device=sdr_device,
frequency_mhz=self._frequency_khz / 1000.0,
sample_rate=self._sample_rate,
gain=self._gain,
modulation='usb',
)
if self._direct_sampling:
rtl_cmd.extend(['-E', 'direct2'])
rtl_cmd.append('-')
# RTL-SDR: append direct sampling flag for HF reception
if sdr_type_enum == SDRType.RTL_SDR and self._direct_sampling:
# Insert before trailing '-' stdout marker
if rtl_cmd and rtl_cmd[-1] == '-':
rtl_cmd.insert(-1, '-E')
rtl_cmd.insert(-1, 'direct2')
else:
rtl_cmd.extend(['-E', 'direct2', '-'])
logger.info(f"Starting rtl_fm: {' '.join(rtl_cmd)}")