33from __future__ import annotations
44
55import logging
6+ import re
67from typing import Any , Dict , Iterable
78
89from sd_protocols import SDProtocols
910
1011from ..exceptions import SignalduinoParserError
1112from ..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
1520class 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