import re import sys from pathlib import Path from .MsgData import MsgData # \brief Generates and maintains the per-message LeahiRt routing INI from loaded message data. # \details One section per message, keyed by hex msgId (e.g. [0x0100]). Each section carries: # msg_id - the MSG_ID_* reference name, regenerated from the conf on every run. # handling - send_always | send_delta | drop; defaults to drop, preserved across runs. # topic - MQTT topic; empty by default, preserved across runs. # merge_ini() rewrites the INI from the loaded data while preserving the hand-edited # handling/topic values of sections that still exist in the conf. class MsgIni(MsgData): DEFAULT_HANDLING = 'drop' VALID_HANDLING = ('send_always', 'send_delta', 'drop') # match a section header line: [0x0100] _section_regex = re.compile(r'^\[\s*(0[xX][0-9a-fA-F]+)\s*\]\s*$') # match a "key = value ; comment" line, capturing key and value (comment stripped) _key_regex = re.compile(r'^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([^;]*?)\s*(?:;.*)?$') # \brief Initializer def __init__(self): super().__init__() # \brief Parse an existing INI into an ordered { hex_key: {handling, topic} } mapping. # \details Only handling/topic are read back; msg_id is always regenerated from the conf, # so its prior value is ignored. Unknown keys are dropped on the next write. # \param[in] filename Path to the existing INI file. # \return dict keyed by lowercase hex msgId string -> {'handling': str, 'topic': str} def parse_ini(self, filename): existing = {} path = Path(filename) if not path.is_file(): return existing current = None with open(path, mode='r', encoding='utf-8') as in_file: for line in in_file: section = self._section_regex.match(line) if section: current = section.group(1).lower() existing[current] = {'handling': self.DEFAULT_HANDLING, 'topic': ''} continue if current is None: continue key = self._key_regex.match(line) if not key: continue name, value = key.group(1).lower(), key.group(2).strip() if name in ('handling', 'topic'): existing[current][name] = value return existing # \brief Format a single message section as INI text. # \param[in] hex_key Lowercase hex msgId string (e.g. "0x0100"). # \param[in] msg_id MSG_ID_* reference name regenerated from the conf. # \param[in] handling Preserved handling value. # \param[in] topic Preserved MQTT topic value. # \return INI section text including a trailing blank line def _format_section(self, hex_key, msg_id, handling, topic): return ( f"[{hex_key}]\n" f"msg_id = {msg_id} ; reference, regenerated from the conf\n" f"handling = {handling} ; send_always | send_delta | drop\n" f"topic = {topic} ; MQTT topic\n" f"\n" ) # \brief Merge the loaded conf data into the INI at filename and write it back. # \details Adds sections for new messages (handling=drop, empty topic), removes sections # whose msgId is no longer in the conf, preserves existing handling/topic for # surviving messages, and regenerates msg_id from the conf. Sections are written # in ascending msgId order. A dropped section that still held a non-default # handling or a non-empty topic is reported to stderr so accidental conf # deletions are visible. # \param[in] filename Path to the INI file to create or update. # \return none def merge_ini(self, filename): existing = self.parse_ini(filename) conf_keys = set() sections = [] for (msg_id_value, msg) in self.data.items(): hex_key = MsgData.value_to_hex_string(msg_id_value).lower() conf_keys.add(hex_key) prior = existing.get(hex_key, {}) handling = prior.get('handling', self.DEFAULT_HANDLING) if handling not in self.VALID_HANDLING: print(f"WARNING: {hex_key} has invalid handling \"{handling}\", " f"resetting to {self.DEFAULT_HANDLING}", file=sys.stderr) handling = self.DEFAULT_HANDLING topic = prior.get('topic', '') sections.append(self._format_section(hex_key, msg['msg_id'], handling, topic)) # report sections being removed that carried hand-set values for hex_key, values in existing.items(): if hex_key in conf_keys: continue if values.get('handling', self.DEFAULT_HANDLING) != self.DEFAULT_HANDLING or values.get('topic', ''): print(f"WARNING: {hex_key} no longer in conf — dropping section with " f"handling={values.get('handling', self.DEFAULT_HANDLING)} " f"topic={values.get('topic', '') or ''}", file=sys.stderr) header = ( "; LeahiRt per-message routing configuration.\n" "; Generated by GenerateMsgIni.py from LeahiUnhandled.conf.\n" "; msg_id is regenerated on every run; handling and topic are preserved across runs.\n" "\n" ) with open(filename, mode='w', encoding='utf-8', newline='\n') as out_file: out_file.write(header) out_file.write(''.join(sections)) print(f"Wrote LeahiRt message INI {filename} with {len(sections)} messages " f"({len(existing)} previously)")