"""RTLAMR utility meter monitoring routes.""" from __future__ import annotations import json import queue import subprocess import threading import time from datetime import datetime from typing import Generator from flask import Blueprint, jsonify, request, Response import app as app_module from utils.logging import sensor_logger as logger from utils.validation import ( validate_frequency, validate_device_index, validate_gain, validate_ppm ) from utils.sse import sse_stream_fanout from utils.event_pipeline import process_event from utils.process import safe_terminate, register_process, unregister_process 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: try: process.kill() except Exception: pass 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: try: rtl_tcp_process.kill() except Exception: pass 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 jsonify({'status': 'error', 'message': 'RTLAMR already running'}), 409 data = request.json or {} sdr_type_str = data.get('sdr_type', 'rtlsdr') if sdr_type_str != 'rtlsdr': return jsonify({ 'status': 'error', 'message': 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 jsonify({'status': 'error', 'message': 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 jsonify({ 'status': 'error', 'error_type': 'DEVICE_BUSY', 'message': error }), 409 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 jsonify({'status': 'error', 'message': 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 jsonify({'status': 'error', 'message': '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 jsonify({'status': 'error', 'message': 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