mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 17:18:11 -07:00
Fix APRS stop/start not repopulating stations
- Make stopAprs() async and await backend stop completion before re-enabling the Start button, preventing race where a late stop request kills newly started processes - Add cache-buster param to EventSource URL to prevent browser SSE connection reuse between stop/start cycles - Capture aprs_active_device locally in stream_aprs_output so the old thread's finally block doesn't release a device claimed by a new session Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+241
-238
@@ -14,14 +14,14 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from subprocess import PIPE, STDOUT
|
from subprocess import PIPE, STDOUT
|
||||||
from typing import Any, Generator, Optional
|
from typing import Any, Generator, Optional
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request, Response
|
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
|
||||||
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
|
||||||
from utils.constants import (
|
from utils.constants import (
|
||||||
@@ -94,7 +94,7 @@ def find_rtl_power() -> Optional[str]:
|
|||||||
DIREWOLF_CONFIG_PATH = os.path.join(tempfile.gettempdir(), 'intercept_direwolf.conf')
|
DIREWOLF_CONFIG_PATH = os.path.join(tempfile.gettempdir(), 'intercept_direwolf.conf')
|
||||||
|
|
||||||
|
|
||||||
def create_direwolf_config() -> str:
|
def create_direwolf_config() -> str:
|
||||||
"""Create a minimal direwolf config for receive-only operation."""
|
"""Create a minimal direwolf config for receive-only operation."""
|
||||||
config = """# Minimal direwolf config for INTERCEPT (receive-only)
|
config = """# Minimal direwolf config for INTERCEPT (receive-only)
|
||||||
# Audio input is handled via stdin
|
# Audio input is handled via stdin
|
||||||
@@ -104,32 +104,32 @@ CHANNEL 0
|
|||||||
MYCALL N0CALL
|
MYCALL N0CALL
|
||||||
MODEM 1200
|
MODEM 1200
|
||||||
"""
|
"""
|
||||||
with open(DIREWOLF_CONFIG_PATH, 'w') as f:
|
with open(DIREWOLF_CONFIG_PATH, 'w') as f:
|
||||||
f.write(config)
|
f.write(config)
|
||||||
return DIREWOLF_CONFIG_PATH
|
return DIREWOLF_CONFIG_PATH
|
||||||
|
|
||||||
|
|
||||||
def normalize_aprs_output_line(line: str) -> str:
|
def normalize_aprs_output_line(line: str) -> str:
|
||||||
"""Normalize a decoder output line to raw APRS packet format.
|
"""Normalize a decoder output line to raw APRS packet format.
|
||||||
|
|
||||||
Handles common decoder prefixes:
|
Handles common decoder prefixes:
|
||||||
- multimon-ng: ``AFSK1200: ...``
|
- multimon-ng: ``AFSK1200: ...``
|
||||||
- direwolf tags: ``[0.4] ...``, ``[0L] ...``, etc.
|
- direwolf tags: ``[0.4] ...``, ``[0L] ...``, etc.
|
||||||
"""
|
"""
|
||||||
if not line:
|
if not line:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
normalized = line.strip()
|
normalized = line.strip()
|
||||||
if normalized.startswith('AFSK1200:'):
|
if normalized.startswith('AFSK1200:'):
|
||||||
normalized = normalized[9:].strip()
|
normalized = normalized[9:].strip()
|
||||||
|
|
||||||
# Strip one or more leading bracket tags emitted by decoders.
|
# Strip one or more leading bracket tags emitted by decoders.
|
||||||
# Examples: [0.4], [0L], [NONE]
|
# Examples: [0.4], [0L], [NONE]
|
||||||
normalized = re.sub(r'^(?:\[[^\]]+\]\s*)+', '', normalized)
|
normalized = re.sub(r'^(?:\[[^\]]+\]\s*)+', '', normalized)
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
def parse_aprs_packet(raw_packet: str) -> Optional[dict]:
|
def parse_aprs_packet(raw_packet: str) -> Optional[dict]:
|
||||||
"""Parse APRS packet into structured data.
|
"""Parse APRS packet into structured data.
|
||||||
|
|
||||||
Supports all major APRS packet types:
|
Supports all major APRS packet types:
|
||||||
@@ -143,19 +143,19 @@ def parse_aprs_packet(raw_packet: str) -> Optional[dict]:
|
|||||||
- Third-party traffic
|
- Third-party traffic
|
||||||
- Raw GPS/NMEA data
|
- Raw GPS/NMEA data
|
||||||
- User-defined formats
|
- User-defined formats
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
raw_packet = normalize_aprs_output_line(raw_packet)
|
raw_packet = normalize_aprs_output_line(raw_packet)
|
||||||
if not raw_packet:
|
if not raw_packet:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Basic APRS packet format: CALLSIGN>PATH:DATA
|
# Basic APRS packet format: CALLSIGN>PATH:DATA
|
||||||
# Example: N0CALL-9>APRS,TCPIP*:@092345z4903.50N/07201.75W_090/000g005t077
|
# Example: N0CALL-9>APRS,TCPIP*:@092345z4903.50N/07201.75W_090/000g005t077
|
||||||
|
|
||||||
# Source callsigns can include tactical suffixes like "/1" on some stations.
|
# Source callsigns can include tactical suffixes like "/1" on some stations.
|
||||||
match = re.match(r'^([A-Z0-9/\-]+)>([^:]+):(.+)$', raw_packet, re.IGNORECASE)
|
match = re.match(r'^([A-Z0-9/\-]+)>([^:]+):(.+)$', raw_packet, re.IGNORECASE)
|
||||||
if not match:
|
if not match:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
callsign = match.group(1).upper()
|
callsign = match.group(1).upper()
|
||||||
path = match.group(2)
|
path = match.group(2)
|
||||||
@@ -418,18 +418,18 @@ def parse_aprs_packet(raw_packet: str) -> Optional[dict]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def parse_position(data: str) -> Optional[dict]:
|
def parse_position(data: str) -> Optional[dict]:
|
||||||
"""Parse APRS position data."""
|
"""Parse APRS position data."""
|
||||||
try:
|
try:
|
||||||
# Format: DDMM.mmN/DDDMM.mmW (or similar with symbols)
|
# Format: DDMM.mmN/DDDMM.mmW (or similar with symbols)
|
||||||
# Example: 4903.50N/07201.75W
|
# Example: 4903.50N/07201.75W
|
||||||
|
|
||||||
pos_match = re.match(
|
|
||||||
r'^(\d{2})(\d{2}\.\d+)([NS])(.)(\d{3})(\d{2}\.\d+)([EW])(.)?',
|
|
||||||
data
|
|
||||||
)
|
|
||||||
|
|
||||||
if pos_match:
|
pos_match = re.match(
|
||||||
|
r'^(\d{2})(\d{2}\.\d+)([NS])(.)(\d{3})(\d{2}\.\d+)([EW])(.)?',
|
||||||
|
data
|
||||||
|
)
|
||||||
|
|
||||||
|
if pos_match:
|
||||||
lat_deg = int(pos_match.group(1))
|
lat_deg = int(pos_match.group(1))
|
||||||
lat_min = float(pos_match.group(2))
|
lat_min = float(pos_match.group(2))
|
||||||
lat_dir = pos_match.group(3)
|
lat_dir = pos_match.group(3)
|
||||||
@@ -467,113 +467,113 @@ def parse_position(data: str) -> Optional[dict]:
|
|||||||
if alt_match:
|
if alt_match:
|
||||||
result['altitude'] = int(alt_match.group(1)) # feet
|
result['altitude'] = int(alt_match.group(1)) # feet
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Legacy/no-decimal variant occasionally seen in degraded decodes:
|
# Legacy/no-decimal variant occasionally seen in degraded decodes:
|
||||||
# DDMMN/DDDMMW (symbol chars still present between/after coords).
|
# DDMMN/DDDMMW (symbol chars still present between/after coords).
|
||||||
nodot_match = re.match(
|
nodot_match = re.match(
|
||||||
r'^(\d{2})(\d{2})([NS])(.)(\d{3})(\d{2})([EW])(.)?',
|
r'^(\d{2})(\d{2})([NS])(.)(\d{3})(\d{2})([EW])(.)?',
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
if nodot_match:
|
if nodot_match:
|
||||||
lat_deg = int(nodot_match.group(1))
|
lat_deg = int(nodot_match.group(1))
|
||||||
lat_min = float(nodot_match.group(2))
|
lat_min = float(nodot_match.group(2))
|
||||||
lat_dir = nodot_match.group(3)
|
lat_dir = nodot_match.group(3)
|
||||||
symbol_table = nodot_match.group(4)
|
symbol_table = nodot_match.group(4)
|
||||||
lon_deg = int(nodot_match.group(5))
|
lon_deg = int(nodot_match.group(5))
|
||||||
lon_min = float(nodot_match.group(6))
|
lon_min = float(nodot_match.group(6))
|
||||||
lon_dir = nodot_match.group(7)
|
lon_dir = nodot_match.group(7)
|
||||||
symbol_code = nodot_match.group(8) or ''
|
symbol_code = nodot_match.group(8) or ''
|
||||||
|
|
||||||
lat = lat_deg + lat_min / 60.0
|
lat = lat_deg + lat_min / 60.0
|
||||||
if lat_dir == 'S':
|
if lat_dir == 'S':
|
||||||
lat = -lat
|
lat = -lat
|
||||||
|
|
||||||
lon = lon_deg + lon_min / 60.0
|
lon = lon_deg + lon_min / 60.0
|
||||||
if lon_dir == 'W':
|
if lon_dir == 'W':
|
||||||
lon = -lon
|
lon = -lon
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'lat': round(lat, 6),
|
'lat': round(lat, 6),
|
||||||
'lon': round(lon, 6),
|
'lon': round(lon, 6),
|
||||||
'symbol': symbol_table + symbol_code,
|
'symbol': symbol_table + symbol_code,
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining = data[13:] if len(data) > 13 else ''
|
remaining = data[13:] if len(data) > 13 else ''
|
||||||
|
|
||||||
cs_match = re.search(r'(\d{3})/(\d{3})', remaining)
|
cs_match = re.search(r'(\d{3})/(\d{3})', remaining)
|
||||||
if cs_match:
|
if cs_match:
|
||||||
result['course'] = int(cs_match.group(1))
|
result['course'] = int(cs_match.group(1))
|
||||||
result['speed'] = int(cs_match.group(2))
|
result['speed'] = int(cs_match.group(2))
|
||||||
|
|
||||||
alt_match = re.search(r'/A=(-?\d+)', remaining)
|
alt_match = re.search(r'/A=(-?\d+)', remaining)
|
||||||
if alt_match:
|
if alt_match:
|
||||||
result['altitude'] = int(alt_match.group(1))
|
result['altitude'] = int(alt_match.group(1))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Fallback: tolerate APRS ambiguity spaces in minute fields.
|
# Fallback: tolerate APRS ambiguity spaces in minute fields.
|
||||||
# Example: 4903. N/07201. W
|
# Example: 4903. N/07201. W
|
||||||
if len(data) >= 18:
|
if len(data) >= 18:
|
||||||
lat_field = data[0:7]
|
lat_field = data[0:7]
|
||||||
lat_dir = data[7]
|
lat_dir = data[7]
|
||||||
symbol_table = data[8] if len(data) > 8 else ''
|
symbol_table = data[8] if len(data) > 8 else ''
|
||||||
lon_field = data[9:17] if len(data) >= 17 else ''
|
lon_field = data[9:17] if len(data) >= 17 else ''
|
||||||
lon_dir = data[17] if len(data) > 17 else ''
|
lon_dir = data[17] if len(data) > 17 else ''
|
||||||
symbol_code = data[18] if len(data) > 18 else ''
|
symbol_code = data[18] if len(data) > 18 else ''
|
||||||
|
|
||||||
if (
|
if (
|
||||||
len(lat_field) == 7
|
len(lat_field) == 7
|
||||||
and len(lon_field) == 8
|
and len(lon_field) == 8
|
||||||
and lat_dir in ('N', 'S')
|
and lat_dir in ('N', 'S')
|
||||||
and lon_dir in ('E', 'W')
|
and lon_dir in ('E', 'W')
|
||||||
):
|
):
|
||||||
lat_deg_txt = lat_field[:2]
|
lat_deg_txt = lat_field[:2]
|
||||||
lat_min_txt = lat_field[2:].replace(' ', '0')
|
lat_min_txt = lat_field[2:].replace(' ', '0')
|
||||||
lon_deg_txt = lon_field[:3]
|
lon_deg_txt = lon_field[:3]
|
||||||
lon_min_txt = lon_field[3:].replace(' ', '0')
|
lon_min_txt = lon_field[3:].replace(' ', '0')
|
||||||
|
|
||||||
if (
|
if (
|
||||||
lat_deg_txt.isdigit()
|
lat_deg_txt.isdigit()
|
||||||
and lon_deg_txt.isdigit()
|
and lon_deg_txt.isdigit()
|
||||||
and re.match(r'^\d{2}\.\d+$', lat_min_txt)
|
and re.match(r'^\d{2}\.\d+$', lat_min_txt)
|
||||||
and re.match(r'^\d{2}\.\d+$', lon_min_txt)
|
and re.match(r'^\d{2}\.\d+$', lon_min_txt)
|
||||||
):
|
):
|
||||||
lat_deg = int(lat_deg_txt)
|
lat_deg = int(lat_deg_txt)
|
||||||
lon_deg = int(lon_deg_txt)
|
lon_deg = int(lon_deg_txt)
|
||||||
lat_min = float(lat_min_txt)
|
lat_min = float(lat_min_txt)
|
||||||
lon_min = float(lon_min_txt)
|
lon_min = float(lon_min_txt)
|
||||||
|
|
||||||
lat = lat_deg + lat_min / 60.0
|
lat = lat_deg + lat_min / 60.0
|
||||||
if lat_dir == 'S':
|
if lat_dir == 'S':
|
||||||
lat = -lat
|
lat = -lat
|
||||||
|
|
||||||
lon = lon_deg + lon_min / 60.0
|
lon = lon_deg + lon_min / 60.0
|
||||||
if lon_dir == 'W':
|
if lon_dir == 'W':
|
||||||
lon = -lon
|
lon = -lon
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'lat': round(lat, 6),
|
'lat': round(lat, 6),
|
||||||
'lon': round(lon, 6),
|
'lon': round(lon, 6),
|
||||||
'symbol': symbol_table + symbol_code,
|
'symbol': symbol_table + symbol_code,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keep same extension parsing behavior as primary branch.
|
# Keep same extension parsing behavior as primary branch.
|
||||||
remaining = data[19:] if len(data) > 19 else ''
|
remaining = data[19:] if len(data) > 19 else ''
|
||||||
|
|
||||||
cs_match = re.search(r'(\d{3})/(\d{3})', remaining)
|
cs_match = re.search(r'(\d{3})/(\d{3})', remaining)
|
||||||
if cs_match:
|
if cs_match:
|
||||||
result['course'] = int(cs_match.group(1))
|
result['course'] = int(cs_match.group(1))
|
||||||
result['speed'] = int(cs_match.group(2))
|
result['speed'] = int(cs_match.group(2))
|
||||||
|
|
||||||
alt_match = re.search(r'/A=(-?\d+)', remaining)
|
alt_match = re.search(r'/A=(-?\d+)', remaining)
|
||||||
if alt_match:
|
if alt_match:
|
||||||
result['altitude'] = int(alt_match.group(1))
|
result['altitude'] = int(alt_match.group(1))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Failed to parse position: {e}")
|
logger.debug(f"Failed to parse position: {e}")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1449,7 +1449,11 @@ def stream_aprs_output(rtl_process: subprocess.Popen, decoder_process: subproces
|
|||||||
- type='meter': Audio level meter readings (rate-limited)
|
- type='meter': Audio level meter readings (rate-limited)
|
||||||
"""
|
"""
|
||||||
global aprs_packet_count, aprs_station_count, aprs_last_packet_time, aprs_stations
|
global aprs_packet_count, aprs_station_count, aprs_last_packet_time, aprs_stations
|
||||||
global _last_meter_time, _last_meter_level
|
global _last_meter_time, _last_meter_level, aprs_active_device
|
||||||
|
|
||||||
|
# Capture the device claimed by THIS session so the finally block only
|
||||||
|
# releases our own device, not one claimed by a subsequent start.
|
||||||
|
my_device = aprs_active_device
|
||||||
|
|
||||||
# Reset meter state
|
# Reset meter state
|
||||||
_last_meter_time = 0.0
|
_last_meter_time = 0.0
|
||||||
@@ -1476,12 +1480,12 @@ def stream_aprs_output(rtl_process: subprocess.Popen, decoder_process: subproces
|
|||||||
app_module.aprs_queue.put(meter_msg)
|
app_module.aprs_queue.put(meter_msg)
|
||||||
continue # Audio level lines are not packets
|
continue # Audio level lines are not packets
|
||||||
|
|
||||||
# Normalize decoder prefixes (multimon/direwolf) before parsing.
|
# Normalize decoder prefixes (multimon/direwolf) before parsing.
|
||||||
line = normalize_aprs_output_line(line)
|
line = normalize_aprs_output_line(line)
|
||||||
|
|
||||||
# Skip non-packet lines (APRS format: CALL>PATH:DATA)
|
# Skip non-packet lines (APRS format: CALL>PATH:DATA)
|
||||||
if '>' not in line or ':' not in line:
|
if '>' not in line or ':' not in line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
packet = parse_aprs_packet(line)
|
packet = parse_aprs_packet(line)
|
||||||
if packet:
|
if packet:
|
||||||
@@ -1493,29 +1497,29 @@ def stream_aprs_output(rtl_process: subprocess.Popen, decoder_process: subproces
|
|||||||
if callsign and callsign not in aprs_stations:
|
if callsign and callsign not in aprs_stations:
|
||||||
aprs_station_count += 1
|
aprs_station_count += 1
|
||||||
|
|
||||||
# Update station data, preserving last known coordinates when
|
# Update station data, preserving last known coordinates when
|
||||||
# packets do not contain position fields.
|
# packets do not contain position fields.
|
||||||
if callsign:
|
if callsign:
|
||||||
existing = aprs_stations.get(callsign, {})
|
existing = aprs_stations.get(callsign, {})
|
||||||
packet_lat = packet.get('lat')
|
packet_lat = packet.get('lat')
|
||||||
packet_lon = packet.get('lon')
|
packet_lon = packet.get('lon')
|
||||||
aprs_stations[callsign] = {
|
aprs_stations[callsign] = {
|
||||||
'callsign': callsign,
|
'callsign': callsign,
|
||||||
'lat': packet_lat if packet_lat is not None else existing.get('lat'),
|
'lat': packet_lat if packet_lat is not None else existing.get('lat'),
|
||||||
'lon': packet_lon if packet_lon is not None else existing.get('lon'),
|
'lon': packet_lon if packet_lon is not None else existing.get('lon'),
|
||||||
'symbol': packet.get('symbol') or existing.get('symbol'),
|
'symbol': packet.get('symbol') or existing.get('symbol'),
|
||||||
'last_seen': packet.get('timestamp'),
|
'last_seen': packet.get('timestamp'),
|
||||||
'packet_type': packet.get('packet_type'),
|
'packet_type': packet.get('packet_type'),
|
||||||
}
|
}
|
||||||
# Geofence check
|
# Geofence check
|
||||||
_aprs_lat = packet_lat
|
_aprs_lat = packet_lat
|
||||||
_aprs_lon = packet_lon
|
_aprs_lon = packet_lon
|
||||||
if _aprs_lat is not None and _aprs_lon is not None:
|
if _aprs_lat is not None and _aprs_lon is not None:
|
||||||
try:
|
try:
|
||||||
from utils.geofence import get_geofence_manager
|
from utils.geofence import get_geofence_manager
|
||||||
for _gf_evt in get_geofence_manager().check_position(
|
for _gf_evt in get_geofence_manager().check_position(
|
||||||
callsign, 'aprs_station', _aprs_lat, _aprs_lon,
|
callsign, 'aprs_station', _aprs_lat, _aprs_lon,
|
||||||
{'callsign': callsign}
|
{'callsign': callsign}
|
||||||
):
|
):
|
||||||
process_event('aprs', _gf_evt, 'geofence')
|
process_event('aprs', _gf_evt, 'geofence')
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1543,7 +1547,6 @@ def stream_aprs_output(rtl_process: subprocess.Popen, decoder_process: subproces
|
|||||||
logger.error(f"APRS stream error: {e}")
|
logger.error(f"APRS stream error: {e}")
|
||||||
app_module.aprs_queue.put({'type': 'error', 'message': str(e)})
|
app_module.aprs_queue.put({'type': 'error', 'message': str(e)})
|
||||||
finally:
|
finally:
|
||||||
global aprs_active_device
|
|
||||||
app_module.aprs_queue.put({'type': 'status', 'status': 'stopped'})
|
app_module.aprs_queue.put({'type': 'status', 'status': 'stopped'})
|
||||||
# Cleanup processes
|
# Cleanup processes
|
||||||
for proc in [rtl_process, decoder_process]:
|
for proc in [rtl_process, decoder_process]:
|
||||||
@@ -1555,9 +1558,9 @@ def stream_aprs_output(rtl_process: subprocess.Popen, decoder_process: subproces
|
|||||||
proc.kill()
|
proc.kill()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# Release SDR device
|
# Release SDR device — only if it's still ours (not reclaimed by a new start)
|
||||||
if aprs_active_device is not None:
|
if my_device is not None and aprs_active_device == my_device:
|
||||||
app_module.release_sdr_device(aprs_active_device)
|
app_module.release_sdr_device(my_device)
|
||||||
aprs_active_device = None
|
aprs_active_device = None
|
||||||
|
|
||||||
|
|
||||||
@@ -1596,31 +1599,31 @@ def aprs_status() -> Response:
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@aprs_bp.route('/stations')
|
@aprs_bp.route('/stations')
|
||||||
def get_stations() -> Response:
|
def get_stations() -> Response:
|
||||||
"""Get all tracked APRS stations."""
|
"""Get all tracked APRS stations."""
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'stations': list(aprs_stations.values()),
|
'stations': list(aprs_stations.values()),
|
||||||
'count': len(aprs_stations)
|
'count': len(aprs_stations)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@aprs_bp.route('/data')
|
@aprs_bp.route('/data')
|
||||||
def aprs_data() -> Response:
|
def aprs_data() -> Response:
|
||||||
"""Get APRS data snapshot for remote controller polling compatibility."""
|
"""Get APRS data snapshot for remote controller polling compatibility."""
|
||||||
running = False
|
running = False
|
||||||
if app_module.aprs_process:
|
if app_module.aprs_process:
|
||||||
running = app_module.aprs_process.poll() is None
|
running = app_module.aprs_process.poll() is None
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'running': running,
|
'running': running,
|
||||||
'stations': list(aprs_stations.values()),
|
'stations': list(aprs_stations.values()),
|
||||||
'count': len(aprs_stations),
|
'count': len(aprs_stations),
|
||||||
'packet_count': aprs_packet_count,
|
'packet_count': aprs_packet_count,
|
||||||
'station_count': aprs_station_count,
|
'station_count': aprs_station_count,
|
||||||
'last_packet_time': aprs_last_packet_time,
|
'last_packet_time': aprs_last_packet_time,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@aprs_bp.route('/start', methods=['POST'])
|
@aprs_bp.route('/start', methods=['POST'])
|
||||||
@@ -1908,25 +1911,25 @@ def stop_aprs() -> Response:
|
|||||||
return jsonify({'status': 'stopped'})
|
return jsonify({'status': 'stopped'})
|
||||||
|
|
||||||
|
|
||||||
@aprs_bp.route('/stream')
|
@aprs_bp.route('/stream')
|
||||||
def stream_aprs() -> Response:
|
def stream_aprs() -> Response:
|
||||||
"""SSE stream for APRS packets."""
|
"""SSE stream for APRS packets."""
|
||||||
def _on_msg(msg: dict[str, Any]) -> None:
|
def _on_msg(msg: dict[str, Any]) -> None:
|
||||||
process_event('aprs', msg, msg.get('type'))
|
process_event('aprs', msg, msg.get('type'))
|
||||||
|
|
||||||
response = Response(
|
response = Response(
|
||||||
sse_stream_fanout(
|
sse_stream_fanout(
|
||||||
source_queue=app_module.aprs_queue,
|
source_queue=app_module.aprs_queue,
|
||||||
channel_key='aprs',
|
channel_key='aprs',
|
||||||
timeout=SSE_QUEUE_TIMEOUT,
|
timeout=SSE_QUEUE_TIMEOUT,
|
||||||
keepalive_interval=SSE_KEEPALIVE_INTERVAL,
|
keepalive_interval=SSE_KEEPALIVE_INTERVAL,
|
||||||
on_message=_on_msg,
|
on_message=_on_msg,
|
||||||
),
|
),
|
||||||
mimetype='text/event-stream',
|
mimetype='text/event-stream',
|
||||||
)
|
)
|
||||||
response.headers['Cache-Control'] = 'no-cache'
|
response.headers['Cache-Control'] = 'no-cache'
|
||||||
response.headers['X-Accel-Buffering'] = 'no'
|
response.headers['X-Accel-Buffering'] = 'no'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@aprs_bp.route('/frequencies')
|
@aprs_bp.route('/frequencies')
|
||||||
|
|||||||
@@ -9687,7 +9687,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopAprs() {
|
async function stopAprs() {
|
||||||
const isAgentMode = aprsCurrentAgent !== null;
|
const isAgentMode = aprsCurrentAgent !== null;
|
||||||
const endpoint = isAgentMode
|
const endpoint = isAgentMode
|
||||||
? `/controller/agents/${aprsCurrentAgent}/aprs/stop`
|
? `/controller/agents/${aprsCurrentAgent}/aprs/stop`
|
||||||
@@ -9697,9 +9697,8 @@
|
|||||||
isAprsRunning = false;
|
isAprsRunning = false;
|
||||||
aprsCurrentAgent = null;
|
aprsCurrentAgent = null;
|
||||||
resetAprsAgentStationTracking();
|
resetAprsAgentStationTracking();
|
||||||
document.getElementById('aprsStripStartBtn').style.display = 'inline-block';
|
|
||||||
document.getElementById('aprsStripStopBtn').style.display = 'none';
|
document.getElementById('aprsStripStopBtn').style.display = 'none';
|
||||||
document.getElementById('aprsMapStatus').textContent = 'STANDBY';
|
document.getElementById('aprsMapStatus').textContent = 'STOPPING';
|
||||||
document.getElementById('aprsMapStatus').style.color = '';
|
document.getElementById('aprsMapStatus').style.color = '';
|
||||||
updateAprsStatus('standby');
|
updateAprsStatus('standby');
|
||||||
document.getElementById('aprsStripFreq').textContent = '--';
|
document.getElementById('aprsStripFreq').textContent = '--';
|
||||||
@@ -9722,7 +9721,9 @@
|
|||||||
aprsPollTimer = null;
|
aprsPollTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return postStopRequest(endpoint, timeoutMs);
|
await postStopRequest(endpoint, timeoutMs);
|
||||||
|
document.getElementById('aprsStripStartBtn').style.display = 'inline-block';
|
||||||
|
document.getElementById('aprsMapStatus').textContent = 'STANDBY';
|
||||||
}
|
}
|
||||||
|
|
||||||
function startAprsStream(isAgentMode = false) {
|
function startAprsStream(isAgentMode = false) {
|
||||||
@@ -9730,7 +9731,7 @@
|
|||||||
|
|
||||||
// Use different stream endpoint for agent mode
|
// Use different stream endpoint for agent mode
|
||||||
const streamUrl = isAgentMode ? '/controller/stream/all' : '/aprs/stream';
|
const streamUrl = isAgentMode ? '/controller/stream/all' : '/aprs/stream';
|
||||||
aprsEventSource = new EventSource(streamUrl);
|
aprsEventSource = new EventSource(streamUrl + (streamUrl.includes('?') ? '&' : '?') + 't=' + Date.now());
|
||||||
|
|
||||||
aprsEventSource.onmessage = function (e) {
|
aprsEventSource.onmessage = function (e) {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
|
|||||||
Reference in New Issue
Block a user