mirror of
https://github.com/smittix/intercept.git
synced 2026-07-24 16:58:12 -07:00
Parse dsd-fme JSON output for DMR events
This commit is contained in:
+91
-2
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
import queue
|
import queue
|
||||||
import re
|
import re
|
||||||
import select
|
import select
|
||||||
@@ -114,6 +115,78 @@ def find_ffmpeg() -> str | None:
|
|||||||
return shutil.which('ffmpeg')
|
return shutil.which('ffmpeg')
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_int(value) -> int | None:
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_dsd_json(payload: dict, ts: str) -> dict | None:
|
||||||
|
"""Parse JSON output lines from dsd-fme into events."""
|
||||||
|
event_type = str(payload.get('type') or payload.get('event') or payload.get('msg') or payload.get('kind') or '').lower()
|
||||||
|
nested = payload.get('data') if isinstance(payload.get('data'), dict) else {}
|
||||||
|
|
||||||
|
def first_of(keys):
|
||||||
|
for obj in (payload, nested):
|
||||||
|
for key in keys:
|
||||||
|
if key in obj and obj[key] is not None:
|
||||||
|
return obj[key]
|
||||||
|
return None
|
||||||
|
|
||||||
|
talkgroup = _coerce_int(first_of([
|
||||||
|
'tg', 'tgt', 'talkgroup', 'talk_group', 'tgid',
|
||||||
|
'group', 'group_id', 'groupId', 'dst', 'dest',
|
||||||
|
'destination', 'target'
|
||||||
|
]))
|
||||||
|
source = _coerce_int(first_of([
|
||||||
|
'src', 'source', 'src_id', 'source_id', 'sourceId',
|
||||||
|
'uid', 'unit', 'radio', 'rid', 'radio_id', 'radioId'
|
||||||
|
]))
|
||||||
|
slot = _coerce_int(first_of(['slot', 'timeslot', 'time_slot', 'ts']))
|
||||||
|
nac = first_of(['nac'])
|
||||||
|
protocol = first_of(['protocol', 'mode', 'system', 'sys', 'network'])
|
||||||
|
|
||||||
|
if talkgroup is not None and source is not None:
|
||||||
|
event = {
|
||||||
|
'type': 'call',
|
||||||
|
'talkgroup': talkgroup,
|
||||||
|
'source_id': source,
|
||||||
|
'timestamp': ts,
|
||||||
|
}
|
||||||
|
if slot is not None:
|
||||||
|
event['slot'] = slot
|
||||||
|
if protocol:
|
||||||
|
event['protocol'] = str(protocol)
|
||||||
|
return event
|
||||||
|
|
||||||
|
if nac is not None:
|
||||||
|
return {'type': 'nac', 'nac': str(nac), 'timestamp': ts}
|
||||||
|
|
||||||
|
if 'sync' in event_type:
|
||||||
|
return {
|
||||||
|
'type': 'sync',
|
||||||
|
'protocol': str(protocol or event_type),
|
||||||
|
'timestamp': ts,
|
||||||
|
}
|
||||||
|
|
||||||
|
voice_flag = first_of(['voice', 'voice_frame', 'voiceFrame'])
|
||||||
|
if 'voice' in event_type or voice_flag is True:
|
||||||
|
event = {
|
||||||
|
'type': 'voice',
|
||||||
|
'detail': str(first_of(['detail', 'text']) or event_type or 'voice'),
|
||||||
|
'timestamp': ts,
|
||||||
|
}
|
||||||
|
if slot is not None:
|
||||||
|
event['slot'] = slot
|
||||||
|
return event
|
||||||
|
|
||||||
|
if protocol:
|
||||||
|
return {'type': 'sync', 'protocol': str(protocol), 'timestamp': ts}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def parse_dsd_output(line: str) -> dict | None:
|
def parse_dsd_output(line: str) -> dict | None:
|
||||||
"""Parse a line of DSD stderr output into a structured event.
|
"""Parse a line of DSD stderr output into a structured event.
|
||||||
|
|
||||||
@@ -124,6 +197,24 @@ def parse_dsd_output(line: str) -> dict | None:
|
|||||||
if not line:
|
if not line:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
ts = datetime.now().strftime('%H:%M:%S')
|
||||||
|
|
||||||
|
# If dsd-fme is emitting JSON (via -J), parse it first.
|
||||||
|
if line.startswith('{') and line.endswith('}'):
|
||||||
|
try:
|
||||||
|
payload = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
payload = None
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
parsed = _parse_dsd_json(payload, ts)
|
||||||
|
if parsed:
|
||||||
|
return parsed
|
||||||
|
return {
|
||||||
|
'type': 'raw',
|
||||||
|
'text': line[:200],
|
||||||
|
'timestamp': ts,
|
||||||
|
}
|
||||||
|
|
||||||
# Skip DSD/dsd-fme startup banner lines (ASCII art, version info, etc.)
|
# Skip DSD/dsd-fme startup banner lines (ASCII art, version info, etc.)
|
||||||
# Only filter lines that are purely decorative — dsd-fme uses box-drawing
|
# Only filter lines that are purely decorative — dsd-fme uses box-drawing
|
||||||
# characters (│, ─) as column separators in DATA lines, so we must not
|
# characters (│, ─) as column separators in DATA lines, so we must not
|
||||||
@@ -134,8 +225,6 @@ def parse_dsd_output(line: str) -> dict | None:
|
|||||||
if re.match(r'^\s*(Build Version|MBElib|CODEC2|Audio (Out|In)|Decoding )', line):
|
if re.match(r'^\s*(Build Version|MBElib|CODEC2|Audio (Out|In)|Decoding )', line):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
ts = datetime.now().strftime('%H:%M:%S')
|
|
||||||
|
|
||||||
# Sync detection: "Sync: +DMR (data)" or "Sync: +P25 Phase 1"
|
# Sync detection: "Sync: +DMR (data)" or "Sync: +P25 Phase 1"
|
||||||
sync_match = re.match(r'Sync:\s*\+?(\S+.*)', line)
|
sync_match = re.match(r'Sync:\s*\+?(\S+.*)', line)
|
||||||
if sync_match:
|
if sync_match:
|
||||||
|
|||||||
Reference in New Issue
Block a user