mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
feat: add rtl_tcp remote SDR support to aprs, morse, and dsc routes
Closes #164. Only pager and sensor routes supported rtl_tcp connections. Now aprs, morse, and dsc routes follow the same pattern: extract rtl_tcp_host/port from the request, skip local device claiming for remote connections, and use SDRFactory.create_network_device(). DSC also refactored from manual rtl_fm command building to use SDRFactory's builder abstraction. Frontend wired up for all three modes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,13 @@ from flask import Blueprint, jsonify, request, Response
|
|||||||
|
|
||||||
import app as app_module
|
import app as app_module
|
||||||
from utils.logging import sensor_logger as logger
|
from utils.logging import sensor_logger as logger
|
||||||
from utils.validation import validate_device_index, validate_gain, validate_ppm
|
from utils.validation import (
|
||||||
|
validate_device_index,
|
||||||
|
validate_gain,
|
||||||
|
validate_ppm,
|
||||||
|
validate_rtl_tcp_host,
|
||||||
|
validate_rtl_tcp_port,
|
||||||
|
)
|
||||||
from utils.sse import sse_stream_fanout
|
from utils.sse import sse_stream_fanout
|
||||||
from utils.event_pipeline import process_event
|
from utils.event_pipeline import process_event
|
||||||
from utils.sdr import SDRFactory, SDRType
|
from utils.sdr import SDRFactory, SDRType
|
||||||
@@ -1689,6 +1695,10 @@ def start_aprs() -> Response:
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
# Check for rtl_tcp (remote SDR) connection
|
||||||
|
rtl_tcp_host = data.get('rtl_tcp_host')
|
||||||
|
rtl_tcp_port = data.get('rtl_tcp_port', 1234)
|
||||||
|
|
||||||
sdr_type_str = str(data.get('sdr_type', 'rtlsdr')).lower()
|
sdr_type_str = str(data.get('sdr_type', 'rtlsdr')).lower()
|
||||||
try:
|
try:
|
||||||
sdr_type = SDRType(sdr_type_str)
|
sdr_type = SDRType(sdr_type_str)
|
||||||
@@ -1708,16 +1718,17 @@ def start_aprs() -> Response:
|
|||||||
'message': f'rx_fm not found. Install SoapySDR tools for {sdr_type.value}.'
|
'message': f'rx_fm not found. Install SoapySDR tools for {sdr_type.value}.'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# Reserve SDR device to prevent conflicts with other modes
|
# Reserve SDR device to prevent conflicts (skip for remote rtl_tcp)
|
||||||
error = app_module.claim_sdr_device(device, 'aprs', sdr_type_str)
|
if not rtl_tcp_host:
|
||||||
if error:
|
error = app_module.claim_sdr_device(device, 'aprs', sdr_type_str)
|
||||||
return jsonify({
|
if error:
|
||||||
'status': 'error',
|
return jsonify({
|
||||||
'error_type': 'DEVICE_BUSY',
|
'status': 'error',
|
||||||
'message': error
|
'error_type': 'DEVICE_BUSY',
|
||||||
}), 409
|
'message': error
|
||||||
aprs_active_device = device
|
}), 409
|
||||||
aprs_active_sdr_type = sdr_type_str
|
aprs_active_device = device
|
||||||
|
aprs_active_sdr_type = sdr_type_str
|
||||||
|
|
||||||
# Get frequency for region
|
# Get frequency for region
|
||||||
region = data.get('region', 'north_america')
|
region = data.get('region', 'north_america')
|
||||||
@@ -1741,8 +1752,17 @@ def start_aprs() -> Response:
|
|||||||
|
|
||||||
# Build FM demod command for APRS (AFSK1200 @ 22050 Hz) via SDR abstraction.
|
# Build FM demod command for APRS (AFSK1200 @ 22050 Hz) via SDR abstraction.
|
||||||
try:
|
try:
|
||||||
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
if rtl_tcp_host:
|
||||||
builder = SDRFactory.get_builder(sdr_type)
|
try:
|
||||||
|
rtl_tcp_host = validate_rtl_tcp_host(rtl_tcp_host)
|
||||||
|
rtl_tcp_port = validate_rtl_tcp_port(rtl_tcp_port)
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
sdr_device = SDRFactory.create_network_device(rtl_tcp_host, rtl_tcp_port)
|
||||||
|
logger.info(f"Using remote SDR: rtl_tcp://{rtl_tcp_host}:{rtl_tcp_port}")
|
||||||
|
else:
|
||||||
|
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
||||||
|
builder = SDRFactory.get_builder(sdr_device.sdr_type)
|
||||||
rtl_cmd = builder.build_fm_demod_command(
|
rtl_cmd = builder.build_fm_demod_command(
|
||||||
device=sdr_device,
|
device=sdr_device,
|
||||||
frequency_mhz=float(frequency),
|
frequency_mhz=float(frequency),
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ from utils.database import (
|
|||||||
from utils.dsc.parser import parse_dsc_message
|
from utils.dsc.parser import parse_dsc_message
|
||||||
from utils.sse import sse_stream_fanout
|
from utils.sse import sse_stream_fanout
|
||||||
from utils.event_pipeline import process_event
|
from utils.event_pipeline import process_event
|
||||||
from utils.validation import validate_device_index, validate_gain
|
from utils.validation import (
|
||||||
|
validate_device_index,
|
||||||
|
validate_gain,
|
||||||
|
validate_rtl_tcp_host,
|
||||||
|
validate_rtl_tcp_port,
|
||||||
|
)
|
||||||
from utils.sdr import SDRFactory, SDRType
|
from utils.sdr import SDRFactory, SDRType
|
||||||
from utils.dependencies import get_tool_path
|
from utils.dependencies import get_tool_path
|
||||||
from utils.process import register_process, unregister_process
|
from utils.process import register_process, unregister_process
|
||||||
@@ -336,19 +341,29 @@ def start_decoding() -> Response:
|
|||||||
# Get SDR type from request
|
# Get SDR type from request
|
||||||
sdr_type_str = data.get('sdr_type', 'rtlsdr')
|
sdr_type_str = data.get('sdr_type', 'rtlsdr')
|
||||||
|
|
||||||
# Check if device is available using centralized registry
|
# Check for rtl_tcp (remote SDR) connection
|
||||||
global dsc_active_device, dsc_active_sdr_type
|
rtl_tcp_host = data.get('rtl_tcp_host')
|
||||||
device_int = int(device)
|
rtl_tcp_port = data.get('rtl_tcp_port', 1234)
|
||||||
error = app_module.claim_sdr_device(device_int, 'dsc', sdr_type_str)
|
|
||||||
if error:
|
|
||||||
return jsonify({
|
|
||||||
'status': 'error',
|
|
||||||
'error_type': 'DEVICE_BUSY',
|
|
||||||
'message': error
|
|
||||||
}), 409
|
|
||||||
|
|
||||||
dsc_active_device = device_int
|
try:
|
||||||
dsc_active_sdr_type = sdr_type_str
|
sdr_type = SDRType(sdr_type_str)
|
||||||
|
except ValueError:
|
||||||
|
sdr_type = SDRType.RTL_SDR
|
||||||
|
|
||||||
|
# Check if device is available using centralized registry (skip for remote rtl_tcp)
|
||||||
|
global dsc_active_device, dsc_active_sdr_type
|
||||||
|
if not rtl_tcp_host:
|
||||||
|
device_int = int(device)
|
||||||
|
error = app_module.claim_sdr_device(device_int, 'dsc', sdr_type_str)
|
||||||
|
if error:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error_type': 'DEVICE_BUSY',
|
||||||
|
'message': error
|
||||||
|
}), 409
|
||||||
|
|
||||||
|
dsc_active_device = device_int
|
||||||
|
dsc_active_sdr_type = sdr_type_str
|
||||||
|
|
||||||
# Clear queue
|
# Clear queue
|
||||||
while not app_module.dsc_queue.empty():
|
while not app_module.dsc_queue.empty():
|
||||||
@@ -357,22 +372,32 @@ def start_decoding() -> Response:
|
|||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Build rtl_fm command
|
# Build rtl_fm command via SDR abstraction layer
|
||||||
rtl_fm_path = tools['rtl_fm']['path']
|
|
||||||
decoder_path = tools['dsc_decoder']['path']
|
decoder_path = tools['dsc_decoder']['path']
|
||||||
|
|
||||||
# rtl_fm command for DSC decoding
|
if rtl_tcp_host:
|
||||||
# DSC uses narrow FM at 156.525 MHz with 48kHz sample rate
|
try:
|
||||||
rtl_cmd = [
|
rtl_tcp_host = validate_rtl_tcp_host(rtl_tcp_host)
|
||||||
rtl_fm_path,
|
rtl_tcp_port = validate_rtl_tcp_port(rtl_tcp_port)
|
||||||
'-f', f'{DSC_VHF_FREQUENCY_MHZ}M',
|
except ValueError as e:
|
||||||
'-s', str(DSC_SAMPLE_RATE),
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
'-d', str(device),
|
sdr_device = SDRFactory.create_network_device(rtl_tcp_host, rtl_tcp_port)
|
||||||
'-g', str(gain),
|
logger.info(f"Using remote SDR: rtl_tcp://{rtl_tcp_host}:{rtl_tcp_port}")
|
||||||
'-M', 'fm', # FM demodulation
|
else:
|
||||||
'-l', '0', # No squelch for DSC
|
sdr_device = SDRFactory.create_default_device(sdr_type, index=int(device))
|
||||||
'-E', 'dc' # DC blocking filter
|
|
||||||
]
|
builder = SDRFactory.get_builder(sdr_device.sdr_type)
|
||||||
|
rtl_cmd = list(builder.build_fm_demod_command(
|
||||||
|
device=sdr_device,
|
||||||
|
frequency_mhz=DSC_VHF_FREQUENCY_MHZ,
|
||||||
|
sample_rate=DSC_SAMPLE_RATE,
|
||||||
|
gain=float(gain) if gain and str(gain) != '0' else None,
|
||||||
|
modulation='fm',
|
||||||
|
squelch=0,
|
||||||
|
))
|
||||||
|
# Ensure trailing '-' for stdin piping and add DC blocking filter
|
||||||
|
if rtl_cmd and rtl_cmd[-1] == '-':
|
||||||
|
rtl_cmd = rtl_cmd[:-1] + ['-E', 'dc', '-']
|
||||||
|
|
||||||
# Decoder command
|
# Decoder command
|
||||||
decoder_cmd = [decoder_path]
|
decoder_cmd = [decoder_path]
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ from utils.validation import (
|
|||||||
validate_frequency,
|
validate_frequency,
|
||||||
validate_gain,
|
validate_gain,
|
||||||
validate_ppm,
|
validate_ppm,
|
||||||
|
validate_rtl_tcp_host,
|
||||||
|
validate_rtl_tcp_port,
|
||||||
)
|
)
|
||||||
|
|
||||||
morse_bp = Blueprint('morse', __name__)
|
morse_bp = Blueprint('morse', __name__)
|
||||||
@@ -279,6 +281,10 @@ def start_morse() -> Response:
|
|||||||
|
|
||||||
sdr_type_str = data.get('sdr_type', 'rtlsdr')
|
sdr_type_str = data.get('sdr_type', 'rtlsdr')
|
||||||
|
|
||||||
|
# Check for rtl_tcp (remote SDR) connection
|
||||||
|
rtl_tcp_host = data.get('rtl_tcp_host')
|
||||||
|
rtl_tcp_port = data.get('rtl_tcp_port', 1234)
|
||||||
|
|
||||||
with app_module.morse_lock:
|
with app_module.morse_lock:
|
||||||
if morse_state in {MORSE_STARTING, MORSE_RUNNING, MORSE_STOPPING}:
|
if morse_state in {MORSE_STARTING, MORSE_RUNNING, MORSE_STOPPING}:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -287,17 +293,19 @@ def start_morse() -> Response:
|
|||||||
'state': morse_state,
|
'state': morse_state,
|
||||||
}), 409
|
}), 409
|
||||||
|
|
||||||
device_int = int(device)
|
# Reserve SDR device (skip for remote rtl_tcp)
|
||||||
error = app_module.claim_sdr_device(device_int, 'morse', sdr_type_str)
|
if not rtl_tcp_host:
|
||||||
if error:
|
device_int = int(device)
|
||||||
return jsonify({
|
error = app_module.claim_sdr_device(device_int, 'morse', sdr_type_str)
|
||||||
'status': 'error',
|
if error:
|
||||||
'error_type': 'DEVICE_BUSY',
|
return jsonify({
|
||||||
'message': error,
|
'status': 'error',
|
||||||
}), 409
|
'error_type': 'DEVICE_BUSY',
|
||||||
|
'message': error,
|
||||||
|
}), 409
|
||||||
|
|
||||||
morse_active_device = device_int
|
morse_active_device = device_int
|
||||||
morse_active_sdr_type = sdr_type_str
|
morse_active_sdr_type = sdr_type_str
|
||||||
morse_last_error = ''
|
morse_last_error = ''
|
||||||
morse_session_id += 1
|
morse_session_id += 1
|
||||||
|
|
||||||
@@ -320,23 +328,35 @@ def start_morse() -> Response:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
sdr_type = SDRType.RTL_SDR
|
sdr_type = SDRType.RTL_SDR
|
||||||
|
|
||||||
|
# Create network or local SDR device
|
||||||
|
network_sdr_device = None
|
||||||
|
if rtl_tcp_host:
|
||||||
|
try:
|
||||||
|
rtl_tcp_host = validate_rtl_tcp_host(rtl_tcp_host)
|
||||||
|
rtl_tcp_port = validate_rtl_tcp_port(rtl_tcp_port)
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
network_sdr_device = SDRFactory.create_network_device(rtl_tcp_host, rtl_tcp_port)
|
||||||
|
logger.info(f"Using remote SDR: rtl_tcp://{rtl_tcp_host}:{rtl_tcp_port}")
|
||||||
|
|
||||||
requested_device_index = int(device)
|
requested_device_index = int(device)
|
||||||
active_device_index = requested_device_index
|
active_device_index = requested_device_index
|
||||||
builder = SDRFactory.get_builder(sdr_type)
|
builder = SDRFactory.get_builder(network_sdr_device.sdr_type if network_sdr_device else sdr_type)
|
||||||
|
|
||||||
device_catalog: dict[int, dict[str, str]] = {}
|
device_catalog: dict[int, dict[str, str]] = {}
|
||||||
candidate_device_indices: list[int] = [requested_device_index]
|
candidate_device_indices: list[int] = [requested_device_index]
|
||||||
with contextlib.suppress(Exception):
|
if not network_sdr_device:
|
||||||
detected_devices = SDRFactory.detect_devices()
|
with contextlib.suppress(Exception):
|
||||||
same_type_devices = [d for d in detected_devices if d.sdr_type == sdr_type]
|
detected_devices = SDRFactory.detect_devices()
|
||||||
for d in same_type_devices:
|
same_type_devices = [d for d in detected_devices if d.sdr_type == sdr_type]
|
||||||
device_catalog[d.index] = {
|
for d in same_type_devices:
|
||||||
'name': str(d.name or f'SDR {d.index}'),
|
device_catalog[d.index] = {
|
||||||
'serial': str(d.serial or 'Unknown'),
|
'name': str(d.name or f'SDR {d.index}'),
|
||||||
}
|
'serial': str(d.serial or 'Unknown'),
|
||||||
for d in sorted(same_type_devices, key=lambda dev: dev.index):
|
}
|
||||||
if d.index not in candidate_device_indices:
|
for d in sorted(same_type_devices, key=lambda dev: dev.index):
|
||||||
candidate_device_indices.append(d.index)
|
if d.index not in candidate_device_indices:
|
||||||
|
candidate_device_indices.append(d.index)
|
||||||
|
|
||||||
def _device_label(device_index: int) -> str:
|
def _device_label(device_index: int) -> str:
|
||||||
meta = device_catalog.get(device_index, {})
|
meta = device_catalog.get(device_index, {})
|
||||||
@@ -350,7 +370,7 @@ def start_morse() -> Response:
|
|||||||
tuned_frequency_mhz = max(0.5, float(freq))
|
tuned_frequency_mhz = max(0.5, float(freq))
|
||||||
else:
|
else:
|
||||||
tuned_frequency_mhz = max(0.5, float(freq) - (float(tone_freq) / 1_000_000.0))
|
tuned_frequency_mhz = max(0.5, float(freq) - (float(tone_freq) / 1_000_000.0))
|
||||||
sdr_device = SDRFactory.create_default_device(sdr_type, index=device_index)
|
sdr_device = network_sdr_device or SDRFactory.create_default_device(sdr_type, index=device_index)
|
||||||
fm_kwargs: dict[str, Any] = {
|
fm_kwargs: dict[str, Any] = {
|
||||||
'device': sdr_device,
|
'device': sdr_device,
|
||||||
'frequency_mhz': tuned_frequency_mhz,
|
'frequency_mhz': tuned_frequency_mhz,
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ var MorseMode = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function collectConfig() {
|
function collectConfig() {
|
||||||
return {
|
var config = {
|
||||||
frequency: (el('morseFrequency') && el('morseFrequency').value) || '14.060',
|
frequency: (el('morseFrequency') && el('morseFrequency').value) || '14.060',
|
||||||
gain: (el('morseGain') && el('morseGain').value) || '40',
|
gain: (el('morseGain') && el('morseGain').value) || '40',
|
||||||
ppm: (el('morsePPM') && el('morsePPM').value) || '0',
|
ppm: (el('morsePPM') && el('morsePPM').value) || '0',
|
||||||
@@ -117,6 +117,17 @@ var MorseMode = (function () {
|
|||||||
wpm: (el('morseWpm') && el('morseWpm').value) || '15',
|
wpm: (el('morseWpm') && el('morseWpm').value) || '15',
|
||||||
wpm_lock: !!(el('morseWpmLock') && el('morseWpmLock').checked),
|
wpm_lock: !!(el('morseWpmLock') && el('morseWpmLock').checked),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add rtl_tcp params if using remote SDR
|
||||||
|
if (typeof getRemoteSDRConfig === 'function') {
|
||||||
|
var remoteConfig = getRemoteSDRConfig();
|
||||||
|
if (remoteConfig) {
|
||||||
|
config.rtl_tcp_host = remoteConfig.host;
|
||||||
|
config.rtl_tcp_port = remoteConfig.port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistSettings() {
|
function persistSettings() {
|
||||||
|
|||||||
@@ -1179,6 +1179,19 @@
|
|||||||
const isAgentMode = typeof aisCurrentAgent !== 'undefined' && aisCurrentAgent !== 'local';
|
const isAgentMode = typeof aisCurrentAgent !== 'undefined' && aisCurrentAgent !== 'local';
|
||||||
dscCurrentAgent = isAgentMode ? aisCurrentAgent : null;
|
dscCurrentAgent = isAgentMode ? aisCurrentAgent : null;
|
||||||
|
|
||||||
|
// Check for remote SDR (only for local mode)
|
||||||
|
const remoteConfig = (!isAgentMode && typeof getRemoteSDRConfig === 'function')
|
||||||
|
? getRemoteSDRConfig() : null;
|
||||||
|
if (remoteConfig === false) return; // Validation failed
|
||||||
|
|
||||||
|
const requestBody = { device, gain };
|
||||||
|
|
||||||
|
// Add rtl_tcp params if using remote SDR
|
||||||
|
if (remoteConfig) {
|
||||||
|
requestBody.rtl_tcp_host = remoteConfig.host;
|
||||||
|
requestBody.rtl_tcp_port = remoteConfig.port;
|
||||||
|
}
|
||||||
|
|
||||||
// Determine endpoint based on agent mode
|
// Determine endpoint based on agent mode
|
||||||
const endpoint = isAgentMode
|
const endpoint = isAgentMode
|
||||||
? `/controller/agents/${aisCurrentAgent}/dsc/start`
|
? `/controller/agents/${aisCurrentAgent}/dsc/start`
|
||||||
@@ -1187,7 +1200,7 @@
|
|||||||
fetch(endpoint, {
|
fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ device, gain })
|
body: JSON.stringify(requestBody)
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|||||||
@@ -9797,6 +9797,10 @@
|
|||||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
aprsCurrentAgent = isAgentMode ? currentAgent : null;
|
aprsCurrentAgent = isAgentMode ? currentAgent : null;
|
||||||
|
|
||||||
|
// Check for remote SDR (only for local mode)
|
||||||
|
const remoteConfig = isAgentMode ? null : getRemoteSDRConfig();
|
||||||
|
if (remoteConfig === false) return; // Validation failed
|
||||||
|
|
||||||
// Build request body
|
// Build request body
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
region,
|
region,
|
||||||
@@ -9805,6 +9809,12 @@
|
|||||||
sdr_type: sdrType
|
sdr_type: sdrType
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add rtl_tcp params if using remote SDR
|
||||||
|
if (remoteConfig) {
|
||||||
|
requestBody.rtl_tcp_host = remoteConfig.host;
|
||||||
|
requestBody.rtl_tcp_port = remoteConfig.port;
|
||||||
|
}
|
||||||
|
|
||||||
// Add custom frequency if selected
|
// Add custom frequency if selected
|
||||||
if (region === 'custom') {
|
if (region === 'custom') {
|
||||||
const customFreq = document.getElementById('aprsStripCustomFreq').value;
|
const customFreq = document.getElementById('aprsStripCustomFreq').value;
|
||||||
|
|||||||
Reference in New Issue
Block a user