Skip to content

Commit 8be09da

Browse files
committed
implement mn parser
1 parent 001c718 commit 8be09da

1 file changed

Lines changed: 151 additions & 41 deletions

File tree

signalduino/parser/mn.py

Lines changed: 151 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@
33
from __future__ import annotations
44

55
import logging
6+
import re
67
from typing import Any, Dict, Iterable
78

89
from sd_protocols import SDProtocols
910

1011
from ..exceptions import SignalduinoParserError
1112
from ..types import DecodedMessage, RawFrame
12-
from .base import ensure_message_type
13+
from .base import ensure_message_type, calc_rssi
14+
15+
# Regex to match MN messages: MN;D=...;R=...;A=...
16+
# Supports optional Y prefix in data, and optional R/A fields
17+
MN_PATTERN = re.compile(r"^MN;D=(Y?)([0-9A-F]+);(?:R=([0-9]+);)?(?:A=(-?[0-9]{1,3});)?$")
1318

1419

1520
class MNParser:
1621
"""
17-
Parses informational (MN) messages. If an rfmode is set, it attempts
18-
to demodulate the message; otherwise, it just logs it.
22+
Parses informational (MN) messages.
23+
Iterates over available MN protocols to find a match based on rfmode, length, and regex.
1924
"""
2025

2126
def __init__(self, protocols: SDProtocols, logger: logging.Logger, rfmode: str | None = None):
@@ -31,48 +36,153 @@ def parse(self, frame: RawFrame) -> Iterable[DecodedMessage]:
3136
self.logger.debug("Not an MN message: %s", e)
3237
return
3338

34-
msg_data = self._parse_to_dict(frame.line)
35-
36-
# If no rfmode is set, just log the message
37-
if not self.rfmode:
38-
self.logger.info("Received firmware message: %s", frame.line)
39-
return
40-
41-
if "D" not in msg_data:
42-
self.logger.debug("Ignoring MN message without data (D): %s", frame.line)
39+
match = MN_PATTERN.match(frame.line)
40+
if not match:
41+
self.logger.debug("MN message format mismatch: %s", frame.line)
4342
return
4443

45-
msg_data["data"] = msg_data["D"]
46-
msg_data["rfmode"] = self.rfmode
47-
48-
try:
49-
demodulated_list = self.protocols.demodulate(msg_data, "MN")
50-
except Exception:
51-
self.logger.exception("Error during MN demodulation for line: %s", frame.line)
52-
return
53-
54-
for decoded in demodulated_list:
55-
if not isinstance(decoded, dict) or "protocol_id" not in decoded:
56-
self.logger.warning("Invalid result from demodulator: %s", decoded)
44+
# Extract groups
45+
# Group 1: 'Y' or ''
46+
# Group 2: Hex Data
47+
# Group 3: RSSI (optional)
48+
# Group 4: AFC (optional)
49+
50+
# raw_data should not contain the 'Y' prefix if present
51+
raw_data = match.group(2)
52+
53+
rssi = None
54+
if match.group(3):
55+
try:
56+
rssi = calc_rssi(int(match.group(3)))
57+
except ValueError:
58+
pass
59+
60+
freq_afc = None
61+
if match.group(4):
62+
try:
63+
# AFC calculation formula from Perl:
64+
# round((26000000 / 16384 * freqafc / 1000), 0)
65+
raw_afc = int(match.group(4))
66+
freq_afc = round((26000000 / 16384 * raw_afc / 1000), 0)
67+
except ValueError:
68+
pass
69+
70+
# Prepare common message data for methods
71+
msg_data = {
72+
"raw_data": raw_data,
73+
"data": raw_data, # Alias
74+
"rssi": rssi,
75+
"freq_afc": freq_afc,
76+
"rfmode": self.rfmode
77+
}
78+
79+
# Iterate over all MN protocols (those having 'modulation' property)
80+
mn_ids = self.protocols.get_keys('modulation')
81+
82+
for pid in mn_ids:
83+
# 1. Check rfmode
84+
proto_rfmode = self.protocols.check_property(pid, 'rfmode', None)
85+
if not proto_rfmode:
86+
self.logger.debug("MN Parse: Protocol %s has no rfmode defined", pid)
87+
continue
88+
89+
# Perl implementation checks if rfmode is active in some way, but here we just check if it matches
90+
# or if we have generic processing. For now, we assume if the protocol has an rfmode, we check it.
91+
92+
# 2. Check Length
93+
# Note: raw_data is hex string here. LengthInRange in Perl checks char length of this string.
94+
# length_in_range in SDProtocols expects length.
95+
rcode, rtxt = self.protocols.length_in_range(pid, len(raw_data))
96+
if not rcode:
97+
self.logger.debug("MN Parse: Protocol %s length check failed: %s", pid, rtxt)
5798
continue
5899

100+
# 3. Regex Match
101+
match_regex = self.protocols.check_property(pid, 'regexMatch', None)
102+
modulation = self.protocols.check_property(pid, 'modulation', None)
103+
proto_name = self.protocols.get_property(pid, 'name')
104+
105+
if match_regex:
106+
if re.search(match_regex, raw_data):
107+
self.logger.debug("MN Parse: Found %s Protocol id %s -> %s with match", modulation, pid, proto_name)
108+
else:
109+
self.logger.debug("MN Parse: %s Protocol id %s -> %s msg %s not match %s", modulation, pid, proto_name, raw_data, match_regex)
110+
continue
111+
else:
112+
self.logger.debug("MN Parse: Found %s Protocol id %s -> %s (no regex)", modulation, pid, proto_name)
113+
114+
# 4. Method Execution
115+
method_name_full = self.protocols.get_property(pid, 'method')
116+
117+
# Default result is just raw_data if no method
118+
decoded_payload = raw_data
119+
120+
if method_name_full:
121+
method_name = method_name_full.split('.')[-1]
122+
method = getattr(self.protocols, method_name, None)
123+
124+
if method and callable(method):
125+
try:
126+
# We assume the method takes (self.protocols, raw_data) or similar.
127+
# Based on Perl: $method->($hash->{protocolObject},$rawData)
128+
# Based on existing Python structure it might be method(msg_data, 'MN')
129+
# But since we are in the parser and have direct access, let's try to be robust.
130+
# If the method was ported from Perl 1:1, it might expect (protocols, raw_data).
131+
# If it was adapted for Python SDProtocols style, it might expect (msg_data, 'MN').
132+
133+
# Let's inspect existing methods? No can do easily.
134+
# We assume adaptation to: method(msg_data, 'MN') -> list of dicts OR (decoded, error)
135+
# OR method(protocols_obj, raw_data) -> (decoded_data, error_msg)
136+
137+
# Given `demodulate_mn` implementation in `sd_protocols.py`:
138+
# It calls method_func(msg_data, msg_type)
139+
140+
# So we pass:
141+
msg_data_with_id = msg_data.copy()
142+
msg_data_with_id['protocol_id'] = pid
143+
144+
# We try to support both signatures or assume one.
145+
# Let's assume the `demodulate_mn` signature is the standard for Python port.
146+
result = method(msg_data_with_id, "MN")
147+
148+
# Result handling depends on what the method returns.
149+
# Perl returns array: (decoded_data, error_msg)
150+
# Python `demodulate_mn` expects list of dicts.
151+
152+
if isinstance(result, list) and result and isinstance(result[0], dict):
153+
# Looks like demodulate_mn style result
154+
decoded_payload = result[0].get('payload', raw_data)
155+
elif isinstance(result, tuple):
156+
# Maybe (decoded, error)
157+
if len(result) > 1 and result[1] and "missing module" in str(result[1]):
158+
self.logger.warning("MN Parse: Error method %s", result[1])
159+
continue
160+
decoded_payload = result[0]
161+
else:
162+
# Fallback
163+
decoded_payload = str(result)
164+
165+
except Exception as e:
166+
self.logger.exception("Error executing method %s for protocol %s: %s", method_name, pid, e)
167+
continue
168+
else:
169+
self.logger.warning("MN Parse: Method %s not found for protocol %s", method_name, pid)
170+
continue
171+
172+
# 5. Construct Final Message
173+
preamble = self.protocols.check_property(pid, 'preamble', '')
174+
final_payload = f"{preamble}{decoded_payload}"
175+
176+
self.logger.info("MN Parse: Decoded matched MN Protocol id %s dmsg=%s", pid, final_payload)
177+
59178
yield DecodedMessage(
60-
protocol_id=str(decoded["protocol_id"]),
61-
payload=str(decoded.get("payload", "")),
179+
protocol_id=str(pid),
180+
payload=final_payload,
62181
raw=frame,
63-
metadata=decoded.get("meta", {}),
182+
metadata={
183+
"rssi": rssi,
184+
"freq_afc": freq_afc,
185+
"modulation": modulation,
186+
"rfmode": proto_rfmode
187+
},
64188
)
65-
66-
def _parse_to_dict(self, line: str) -> Dict[str, Any]:
67-
"""Splits a semicolon-separated line into a dictionary."""
68-
msg_data: Dict[str, Any] = {}
69-
parts = line.split(";")
70-
for part in parts:
71-
if not part:
72-
continue
73-
if "=" in part:
74-
key, value = part.split("=", 1)
75-
msg_data[key] = value
76-
else:
77-
msg_data[part] = ""
78-
return msg_data

0 commit comments

Comments
 (0)