"""GPS dongle routes for USB GPS device support.""" from __future__ import annotations import queue import threading import time from typing import Generator from flask import Blueprint, jsonify, request, Response from utils.logging import get_logger from utils.sse import format_sse from utils.gps import ( detect_gps_devices, is_serial_available, get_gps_reader, start_gps, stop_gps, get_current_position, GPSPosition, ) logger = get_logger('intercept.gps') gps_bp = Blueprint('gps', __name__, url_prefix='/gps') # Queue for SSE position updates _gps_queue: queue.Queue = queue.Queue(maxsize=100) def _position_callback(position: GPSPosition) -> None: """Callback to queue position updates for SSE stream.""" try: _gps_queue.put_nowait(position.to_dict()) except queue.Full: # Discard oldest if queue is full try: _gps_queue.get_nowait() _gps_queue.put_nowait(position.to_dict()) except queue.Empty: pass @gps_bp.route('/available') def check_gps_available(): """Check if GPS dongle support is available.""" return jsonify({ 'available': is_serial_available(), 'message': None if is_serial_available() else 'pyserial not installed - run: pip install pyserial' }) @gps_bp.route('/devices') def list_gps_devices(): """List available GPS serial devices.""" if not is_serial_available(): return jsonify({ 'status': 'error', 'message': 'pyserial not installed' }), 503 devices = detect_gps_devices() return jsonify({ 'status': 'ok', 'devices': devices }) @gps_bp.route('/start', methods=['POST']) def start_gps_reader(): """Start GPS reader on specified device.""" if not is_serial_available(): return jsonify({ 'status': 'error', 'message': 'pyserial not installed' }), 503 # Check if already running reader = get_gps_reader() if reader and reader.is_running: return jsonify({ 'status': 'error', 'message': 'GPS reader already running' }), 409 data = request.json or {} device_path = data.get('device') baudrate = data.get('baudrate', 9600) if not device_path: return jsonify({ 'status': 'error', 'message': 'Device path required' }), 400 # Validate baudrate valid_baudrates = [4800, 9600, 19200, 38400, 57600, 115200] if baudrate not in valid_baudrates: return jsonify({ 'status': 'error', 'message': f'Invalid baudrate. Valid options: {valid_baudrates}' }), 400 # Clear the queue while not _gps_queue.empty(): try: _gps_queue.get_nowait() except queue.Empty: break # Start the GPS reader success = start_gps(device_path, baudrate) if success: # Register callback for SSE streaming reader = get_gps_reader() if reader: reader.add_callback(_position_callback) return jsonify({ 'status': 'started', 'device': device_path, 'baudrate': baudrate }) else: reader = get_gps_reader() error = reader.error if reader else 'Unknown error' return jsonify({ 'status': 'error', 'message': f'Failed to start GPS reader: {error}' }), 500 @gps_bp.route('/stop', methods=['POST']) def stop_gps_reader(): """Stop GPS reader.""" reader = get_gps_reader() if reader: reader.remove_callback(_position_callback) stop_gps() return jsonify({'status': 'stopped'}) @gps_bp.route('/status') def get_gps_status(): """Get current GPS reader status.""" reader = get_gps_reader() if not reader: return jsonify({ 'running': False, 'device': None, 'position': None, 'error': None, 'message': 'GPS reader not started' }) position = reader.position return jsonify({ 'running': reader.is_running, 'device': reader.device_path, 'position': position.to_dict() if position else None, 'last_update': reader.last_update.isoformat() if reader.last_update else None, 'error': reader.error, 'message': 'Waiting for GPS fix - ensure GPS has clear view of sky' if reader.is_running and not position else None }) @gps_bp.route('/position') def get_position(): """Get current GPS position.""" position = get_current_position() if position: return jsonify({ 'status': 'ok', 'position': position.to_dict() }) else: reader = get_gps_reader() if not reader or not reader.is_running: return jsonify({ 'status': 'error', 'message': 'GPS reader not running' }), 400 else: return jsonify({ 'status': 'waiting', 'message': 'Waiting for GPS fix - ensure GPS has clear view of sky' }) @gps_bp.route('/debug') def debug_gps(): """Debug endpoint showing GPS reader state.""" reader = get_gps_reader() if not reader: return jsonify({ 'reader': None, 'message': 'No GPS reader initialized' }) position = reader.position return jsonify({ 'running': reader.is_running, 'device': reader.device_path, 'baudrate': reader.baudrate, 'has_position': position is not None, 'position': position.to_dict() if position else None, 'last_update': reader.last_update.isoformat() if reader.last_update else None, 'error': reader.error, 'callbacks_registered': len(reader._callbacks), }) @gps_bp.route('/stream') def stream_gps(): """SSE stream of GPS position updates.""" def generate() -> Generator[str, None, None]: last_keepalive = time.time() keepalive_interval = 30.0 while True: try: position = _gps_queue.get(timeout=1) last_keepalive = time.time() yield format_sse({'type': 'position', **position}) except queue.Empty: now = time.time() if now - last_keepalive >= keepalive_interval: yield format_sse({'type': 'keepalive'}) last_keepalive = now response = Response(generate(), mimetype='text/event-stream') response.headers['Cache-Control'] = 'no-cache' response.headers['X-Accel-Buffering'] = 'no' response.headers['Connection'] = 'keep-alive' return response