From a2a7ac8fecfc339b199567c992c61743e678260b Mon Sep 17 00:00:00 2001 From: Smittix Date: Mon, 9 Feb 2026 11:41:32 +0000 Subject: [PATCH] Fix banner filter eating dsd-fme data lines and add event log capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The box-drawing character filter was dropping ANY line containing │ or ─, including dsd-fme data lines that use these as column separators (e.g. "DMR BS │ Slot 1 │ TG: 12345 │ SRC: 67890"). Now only filters lines that are purely decorative (no alphanumeric content). Also adds -J /dev/stderr so dsd-fme writes its event log to stderr where we capture it, and debug logging of raw stderr lines. Co-Authored-By: Claude Opus 4.6 --- routes/dmr.py | 13 ++++++++++--- tests/test_dmr.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/routes/dmr.py b/routes/dmr.py index 6924b37..5eb1622 100644 --- a/routes/dmr.py +++ b/routes/dmr.py @@ -113,8 +113,11 @@ def parse_dsd_output(line: str) -> dict | None: return None # Skip DSD/dsd-fme startup banner lines (ASCII art, version info, etc.) - # These contain box-drawing characters or are pure decoration. - if re.search(r'[╔╗╚╝║═██▀▄╗╝╩╦╠╣╬│┤├┘└┐┌─┼█▓▒░]', line): + # Only filter lines that are purely decorative — dsd-fme uses box-drawing + # characters (│, ─) as column separators in DATA lines, so we must not + # discard lines that also contain alphanumeric content. + stripped_of_box = re.sub(r'[╔╗╚╝║═██▀▄╗╝╩╦╠╣╬│┤├┘└┐┌─┼█▓▒░\s]', '', line) + if not stripped_of_box: return None if re.match(r'^\s*(Build Version|MBElib|CODEC2|Audio (Out|In)|Decoding )', line): return None @@ -136,7 +139,7 @@ def parse_dsd_output(line: str) -> dict | None: # dsd-fme: "TG: 12345, Src: 67890" or "Talkgroup: 12345, Source: 67890" # "TGT: 12345 | SRC: 67890" (pipe-delimited variant) tg_match = re.search( - r'(?:TGT?|Talkgroup)[:\s]+(\d+)[,|\s]+(?:Src|Source|SRC)[:\s]+(\d+)', line, re.IGNORECASE + r'(?:TGT?|Talkgroup)[:\s]+(\d+)[,|│\s]+(?:Src|Source|SRC)[:\s]+(\d+)', line, re.IGNORECASE ) if tg_match: result = { @@ -242,6 +245,7 @@ def stream_dsd_output(rtl_process: subprocess.Popen, dsd_process: subprocess.Pop if not text: continue + logger.debug("DSD raw: %s", text) parsed = parse_dsd_output(text) if parsed: _queue_put(parsed) @@ -389,6 +393,9 @@ def start_dmr() -> Response: if is_fme: dsd_cmd.extend(_DSD_FME_PROTOCOL_FLAGS.get(protocol, [])) dsd_cmd.extend(_DSD_FME_MODULATION.get(protocol, [])) + # Event log to stderr so we capture TG/Source/Voice data that + # dsd-fme may not output on stderr by default. + dsd_cmd.extend(['-J', '/dev/stderr']) # Relax CRC checks for marginal signals — lets more frames # through at the cost of occasional decode errors. if data.get('relaxCrc', False): diff --git a/tests/test_dmr.py b/tests/test_dmr.py index e5b15b4..d245663 100644 --- a/tests/test_dmr.py +++ b/tests/test_dmr.py @@ -108,6 +108,23 @@ def test_parse_unrecognized(): assert result['text'] == 'some random text' +def test_parse_banner_filtered(): + """Pure box-drawing lines (banners) should be filtered.""" + assert parse_dsd_output('╔══════════════╗') is None + assert parse_dsd_output('║ ║') is None + assert parse_dsd_output('╚══════════════╝') is None + assert parse_dsd_output('───────────────') is None + + +def test_parse_box_drawing_with_data_not_filtered(): + """Lines with box-drawing separators AND data should NOT be filtered.""" + result = parse_dsd_output('DMR BS │ Slot 1 │ TG: 12345 │ SRC: 67890') + assert result is not None + assert result['type'] == 'call' + assert result['talkgroup'] == 12345 + assert result['source_id'] == 67890 + + def test_dsd_fme_flags_differ_from_classic(): """dsd-fme remapped several flags; tables must NOT be identical.""" assert _DSD_FME_PROTOCOL_FLAGS != _DSD_PROTOCOL_FLAGS