mirror of
https://github.com/smittix/intercept.git
synced 2026-07-28 18:48:10 -07:00
Fix APRS parser for direwolf bracket-prefixed frames
This commit is contained in:
+16
-5
@@ -2860,11 +2860,22 @@ class ModeManager:
|
|||||||
pass
|
pass
|
||||||
logger.info("APRS reader stopped")
|
logger.info("APRS reader stopped")
|
||||||
|
|
||||||
def _parse_aprs_packet(self, line: str) -> dict | None:
|
def _parse_aprs_packet(self, line: str) -> dict | None:
|
||||||
"""Parse APRS packet from direwolf or multimon-ng."""
|
"""Parse APRS packet from direwolf or multimon-ng."""
|
||||||
match = re.match(r'([A-Z0-9-]+)>([^:]+):(.+)', line)
|
if not line:
|
||||||
if not match:
|
return None
|
||||||
return None
|
|
||||||
|
# Normalize common decoder prefixes before parsing.
|
||||||
|
# multimon-ng: "AFSK1200: ..."
|
||||||
|
# direwolf: "[0.4] ...", "[0L] ..."
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith('AFSK1200:'):
|
||||||
|
line = line[9:].strip()
|
||||||
|
line = re.sub(r'^(?:\[[^\]]+\]\s*)+', '', line)
|
||||||
|
|
||||||
|
match = re.match(r'([A-Z0-9-]+)>([^:]+):(.+)', line)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
|
||||||
callsign = match.group(1)
|
callsign = match.group(1)
|
||||||
path = match.group(2)
|
path = match.group(2)
|
||||||
|
|||||||
+44
-25
@@ -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,12 +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 parse_aprs_packet(raw_packet: str) -> Optional[dict]:
|
def normalize_aprs_output_line(line: str) -> str:
|
||||||
|
"""Normalize a decoder output line to raw APRS packet format.
|
||||||
|
|
||||||
|
Handles common decoder prefixes:
|
||||||
|
- multimon-ng: ``AFSK1200: ...``
|
||||||
|
- direwolf tags: ``[0.4] ...``, ``[0L] ...``, etc.
|
||||||
|
"""
|
||||||
|
if not line:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
normalized = line.strip()
|
||||||
|
if normalized.startswith('AFSK1200:'):
|
||||||
|
normalized = normalized[9:].strip()
|
||||||
|
|
||||||
|
# Strip one or more leading bracket tags emitted by decoders.
|
||||||
|
# Examples: [0.4], [0L], [NONE]
|
||||||
|
normalized = re.sub(r'^(?:\[[^\]]+\]\s*)+', '', normalized)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
@@ -123,13 +143,17 @@ 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:
|
||||||
# Basic APRS packet format: CALLSIGN>PATH:DATA
|
raw_packet = normalize_aprs_output_line(raw_packet)
|
||||||
# Example: N0CALL-9>APRS,TCPIP*:@092345z4903.50N/07201.75W_090/000g005t077
|
if not raw_packet:
|
||||||
|
return None
|
||||||
match = re.match(r'^([A-Z0-9-]+)>([^:]+):(.+)$', raw_packet, re.IGNORECASE)
|
|
||||||
if not match:
|
# Basic APRS packet format: CALLSIGN>PATH:DATA
|
||||||
|
# Example: N0CALL-9>APRS,TCPIP*:@092345z4903.50N/07201.75W_090/000g005t077
|
||||||
|
|
||||||
|
match = re.match(r'^([A-Z0-9-]+)>([^:]+):(.+)$', raw_packet, re.IGNORECASE)
|
||||||
|
if not match:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
callsign = match.group(1).upper()
|
callsign = match.group(1).upper()
|
||||||
@@ -1348,17 +1372,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
|
||||||
|
|
||||||
# multimon-ng prefixes decoded packets with "AFSK1200: "
|
# Normalize decoder prefixes (multimon/direwolf) before parsing.
|
||||||
if line.startswith('AFSK1200:'):
|
line = normalize_aprs_output_line(line)
|
||||||
line = line[9:].strip()
|
|
||||||
|
# Skip non-packet lines (APRS format: CALL>PATH:DATA)
|
||||||
# direwolf often prefixes packets with "[0.4] " or similar audio level indicator
|
if '>' not in line or ':' not in line:
|
||||||
# Strip any leading bracket prefix like "[0.4] " before parsing
|
continue
|
||||||
line = re.sub(r'^\[\d+\.\d+\]\s*', '', line)
|
|
||||||
|
|
||||||
# Skip non-packet lines (APRS format: CALL>PATH:DATA)
|
|
||||||
if '>' not in line or ':' not in line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
packet = parse_aprs_packet(line)
|
packet = parse_aprs_packet(line)
|
||||||
if packet:
|
if packet:
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""APRS packet parser regression tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from routes.aprs import parse_aprs_packet
|
||||||
|
|
||||||
|
|
||||||
|
_BASE_PACKET = "N0CALL-9>APRS,TCPIP*:@092345z4903.50N/07201.75W_090/000g005t077"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"line",
|
||||||
|
[
|
||||||
|
_BASE_PACKET,
|
||||||
|
f"[0.4] {_BASE_PACKET}",
|
||||||
|
f"[0L] {_BASE_PACKET}",
|
||||||
|
f"AFSK1200: {_BASE_PACKET}",
|
||||||
|
f"AFSK1200: [0L] {_BASE_PACKET}",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parse_aprs_packet_accepts_decoder_prefix_variants(line: str) -> None:
|
||||||
|
packet = parse_aprs_packet(line)
|
||||||
|
assert packet is not None
|
||||||
|
assert packet["callsign"] == "N0CALL-9"
|
||||||
|
assert packet["type"] == "aprs"
|
||||||
Reference in New Issue
Block a user