"""RTLAMR utility meter monitoring routes.""" from __future__ import annotations import contextlib import json import queue import subprocess import threading import time from datetime import datetime from flask import Blueprint, Response, jsonify, request import app as app_module from utils.event_pipeline import process_event from utils.logging import sensor_logger as logger from utils.process import register_process, unregister_process from utils.responses import api_error from utils.sse import sse_stream_fanout from utils.validation import validate_device_index, validate_frequency, validate_gain, validate_ppm rtlamr_bp = Blueprint('rtlamr', __name__) # Store rtl_tcp process separately rtl_tcp_process = None rtl_tcp_lock = threading.Lock() # Track which device is being used rtlamr_active_device: int | None = None rtlamr_active_sdr_type: str = 'rtlsdr' def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None: """Stream rtlamr JSON output to queue.""" try: app_module.rtlamr_queue.put({'type': 'status', 'text': 'started'}) for line in iter(process.stdout.readline, b''): line = line.decode('utf-8', errors='replace').strip() if not line: continue try: # rtlamr outputs JSON objects, one per line data = json.loads(line) data['type'] = 'rtlamr' app_module.rtlamr_queue.put(data) # Log if enabled if app_module.logging_enabled: try: with open(app_module.log_file_path, 'a') as f: timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') f.write(f"{timestamp} | RTLAMR | {json.dumps(data)}\n") except Exception: pass except json.JSONDecodeError: # Not JSON, send as raw app_module.rtlamr_queue.put({'type': 'raw', 'text': line}) except Exception as e: app_module.rtlamr_queue.put({'type': 'error', 'text': str(e)}) finally: global rtl_tcp_process, rtlamr_active_device, rtlamr_active_sdr_type # Ensure rtlamr process is terminated try: process.terminate() process.wait(timeout=2) except Exception: with contextlib.suppress(Exception): process.kill() unregister_process(process) # Kill companion rtl_tcp process with rtl_tcp_lock: if rtl_tcp_process: try: rtl_tcp_process.terminate() rtl_tcp_process.wait(timeout=2) except Exception: with contextlib.suppress(Exception): rtl_tcp_process.kill() unregister_process(rtl_tcp_process) rtl_tcp_process = None app_module.rtlamr_queue.put({'type': 'status', 'text': 'stopped'}) with app_module.rtlamr_lock: app_module.rtlamr_process = None # Release SDR device if rtlamr_active_device is not None: app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type) rtlamr_active_device = None @rtlamr_bp.route('/start_rtlamr', methods=['POST']) def start_rtlamr() -> Response: global rtl_tcp_process, rtlamr_active_device, rtlamr_active_sdr_type with app_module.rtlamr_lock: if app_module.rtlamr_process: return api_error('RTLAMR already running', 409) data = request.json or {} sdr_type_str = data.get('sdr_type', 'rtlsdr') if sdr_type_str != 'rtlsdr': return api_error(f'{sdr_type_str.replace("_", " ").title()} is not yet supported for this mode. Please use an RTL-SDR device.', 400) # Validate inputs try: freq = validate_frequency(data.get('frequency', '912.0')) gain = validate_gain(data.get('gain', '0')) ppm = validate_ppm(data.get('ppm', '0')) device = validate_device_index(data.get('device', '0')) except ValueError as e: return api_error(str(e), 400) # Check if device is available device_int = int(device) error = app_module.claim_sdr_device(device_int, 'rtlamr', sdr_type_str) if error: return api_error(error, 409, error_type='DEVICE_BUSY') rtlamr_active_device = device_int rtlamr_active_sdr_type = sdr_type_str # Clear queue while not app_module.rtlamr_queue.empty(): try: app_module.rtlamr_queue.get_nowait() except queue.Empty: break # Get message type (default to scm) msgtype = data.get('msgtype', 'scm') output_format = data.get('format', 'json') # Start rtl_tcp first rtl_tcp_just_started = False rtl_tcp_cmd_str = '' with rtl_tcp_lock: if not rtl_tcp_process: logger.info("Starting rtl_tcp server...") try: rtl_tcp_cmd = ['rtl_tcp', '-a', '0.0.0.0'] # Add device index if not 0 if device and device != '0': rtl_tcp_cmd.extend(['-d', str(device)]) # Add gain if not auto if gain and gain != '0': rtl_tcp_cmd.extend(['-g', str(gain)]) # Add PPM correction if not 0 if ppm and ppm != '0': rtl_tcp_cmd.extend(['-p', str(ppm)]) rtl_tcp_process = subprocess.Popen( rtl_tcp_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) register_process(rtl_tcp_process) rtl_tcp_just_started = True rtl_tcp_cmd_str = ' '.join(rtl_tcp_cmd) except Exception as e: logger.error(f"Failed to start rtl_tcp: {e}") # Release SDR device on rtl_tcp failure if rtlamr_active_device is not None: app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type) rtlamr_active_device = None return api_error(f'Failed to start rtl_tcp: {e}', 500) # Wait for rtl_tcp to start outside lock if rtl_tcp_just_started: time.sleep(3) logger.info(f"rtl_tcp started: {rtl_tcp_cmd_str}") app_module.rtlamr_queue.put({'type': 'info', 'text': f'rtl_tcp: {rtl_tcp_cmd_str}'}) # Build rtlamr command cmd = [ 'rtlamr', '-server=127.0.0.1:1234', f'-msgtype={msgtype}', f'-format={output_format}', f'-centerfreq={int(float(freq) * 1e6)}' ] # Add filter options if provided filterid = data.get('filterid') if filterid: cmd.append(f'-filterid={filterid}') filtertype = data.get('filtertype') if filtertype: cmd.append(f'-filtertype={filtertype}') # Unique messages only if data.get('unique', True): cmd.append('-unique=true') full_cmd = ' '.join(cmd) logger.info(f"Running: {full_cmd}") try: app_module.rtlamr_process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) register_process(app_module.rtlamr_process) # Start output thread thread = threading.Thread(target=stream_rtlamr_output, args=(app_module.rtlamr_process,)) thread.daemon = True thread.start() # Monitor stderr def monitor_stderr(): for line in app_module.rtlamr_process.stderr: err = line.decode('utf-8', errors='replace').strip() if err: logger.debug(f"[rtlamr] {err}") app_module.rtlamr_queue.put({'type': 'info', 'text': f'[rtlamr] {err}'}) stderr_thread = threading.Thread(target=monitor_stderr) stderr_thread.daemon = True stderr_thread.start() app_module.rtlamr_queue.put({'type': 'info', 'text': f'Command: {full_cmd}'}) return jsonify({'status': 'started', 'command': full_cmd}) except FileNotFoundError: # If rtlamr fails, clean up rtl_tcp and release device with rtl_tcp_lock: if rtl_tcp_process: rtl_tcp_process.terminate() rtl_tcp_process.wait(timeout=2) rtl_tcp_process = None if rtlamr_active_device is not None: app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type) rtlamr_active_device = None return api_error('rtlamr not found. Install from https://github.com/bemasher/rtlamr') except Exception as e: # If rtlamr fails, clean up rtl_tcp and release device with rtl_tcp_lock: if rtl_tcp_process: rtl_tcp_process.terminate() rtl_tcp_process.wait(timeout=2) rtl_tcp_process = None if rtlamr_active_device is not None: app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type) rtlamr_active_device = None return api_error(str(e)) @rtlamr_bp.route('/stop_rtlamr', methods=['POST']) def stop_rtlamr() -> Response: global rtl_tcp_process, rtlamr_active_device, rtlamr_active_sdr_type # Grab process refs inside locks, clear state, then terminate outside rtlamr_proc = None with app_module.rtlamr_lock: if app_module.rtlamr_process: rtlamr_proc = app_module.rtlamr_process app_module.rtlamr_process = None if rtlamr_proc: rtlamr_proc.terminate() try: rtlamr_proc.wait(timeout=2) except subprocess.TimeoutExpired: rtlamr_proc.kill() # Also stop rtl_tcp tcp_proc = None with rtl_tcp_lock: if rtl_tcp_process: tcp_proc = rtl_tcp_process rtl_tcp_process = None if tcp_proc: tcp_proc.terminate() try: tcp_proc.wait(timeout=2) except subprocess.TimeoutExpired: tcp_proc.kill() logger.info("rtl_tcp stopped") # Release device from registry if rtlamr_active_device is not None: app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type) rtlamr_active_device = None return jsonify({'status': 'stopped'}) @rtlamr_bp.route('/stream_rtlamr') def stream_rtlamr() -> Response: def _on_msg(msg: dict[str, Any]) -> None: process_event('rtlamr', msg, msg.get('type')) response = Response( sse_stream_fanout( source_queue=app_module.rtlamr_queue, channel_key='rtlamr', timeout=1.0, keepalive_interval=30.0, on_message=_on_msg, ), mimetype='text/event-stream', ) response.headers['Cache-Control'] = 'no-cache' response.headers['X-Accel-Buffering'] = 'no' response.headers['Connection'] = 'keep-alive' return response