Index: scripts/MsgUtils/msgutils/MsgIni.py =================================================================== diff -u -r8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52 -r9d547159d805f3a0b0f2f6a11f4e34feb5117b65 --- scripts/MsgUtils/msgutils/MsgIni.py (.../MsgIni.py) (revision 8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52) +++ scripts/MsgUtils/msgutils/MsgIni.py (.../MsgIni.py) (revision 9d547159d805f3a0b0f2f6a11f4e34feb5117b65) @@ -1,118 +1,85 @@ -import re +import configparser import sys from pathlib import Path +from jinja2 import Environment, FileSystemLoader 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: +# \details One section per message keyed by hex msgId (e.g. [0x0100]), carrying: # 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. +# Call loadConf() to load message definitions, then loadIni() to merge in existing +# handling/topic values, then write_ini() to write the result. class MsgIni(MsgData): DEFAULT_HANDLING = 'drop' - VALID_HANDLING = ('send_always', 'send_delta', '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 = {} + # \brief Load a .conf file and cache the data, enriching each entry with default INI fields. + # \param[in] filename Filename of the .conf to load. + # \param[in] clear If true, clear any previously loaded data before processing. + # \return none + def loadConf(self, filename, clear=True): + super().loadConf(filename, clear) + for msg in self.data.values(): + msg['handling'] = self.DEFAULT_HANDLING + msg['topic'] = '' + + + # \brief Merge handling/topic values from an existing INI into the loaded message data. + # \details Only handling and topic are read; msg_id is always regenerated from the conf. + # Sections in the INI that are no longer in the conf are ignored (and warned if + # they carried non-default values). Unknown handling values are reset to drop. + # \param[in] filename Path to the existing INI file; no-op if the file does not exist. + # \return none + def loadIni(self, filename): 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 + return + cfg = configparser.ConfigParser(inline_comment_prefixes=(';',)) + cfg.read(path, encoding='utf-8') + # just use self.data.keys() + conf_keys = set(MsgData.value_to_hex_string(v) for v in self.data.keys()) - # \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" - ) + for section in cfg.sections(): + try: + msg_id_value = int(section, 16) + except ValueError: + print(f"WARNING: could not convert section message ID {section} to valid message ID, skipping") + continue + if msg_id_value not in self.data.keys(): + handling = cfg.get(section, 'handling', fallback=self.DEFAULT_HANDLING).strip() + topic = cfg.get(section, 'topic', fallback='').strip() + if handling != self.DEFAULT_HANDLING or topic: + print(f"WARNING: {MsgData.value_to_hex_string(msg_id_value)} no longer in Unhandled.conf, " + f"dropping section with handling={handling} topic={topic or ''}") + continue - - # \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) + handling = cfg.get(section, 'handling', fallback=self.DEFAULT_HANDLING).strip() 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) + print(f"WARNING: {MsgData.value_to_hex_string(msg_id_value)} has invalid handling \"{handling}\", " + f"resetting to {self.DEFAULT_HANDLING}") 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) + self.data[msg_id_value]['handling'] = handling + self.data[msg_id_value]['topic'] = cfg.get(section, 'topic', fallback='').strip() - 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" - ) + + # \brief Write the loaded message data to the INI file. + # \param[in] filename Path to the INI file to create or update. + # \return none + def write_ini(self, filename): + env = Environment(loader=FileSystemLoader(f'{Path(__file__).parent.absolute()}/templates'), + keep_trailing_newline=True) + template = env.get_template('MsgIni.jinja') + render = template.render({'msg_ini': self}) 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)") + out_file.write(render) + print(f"Wrote message INI {filename} with {len(self.data)} messages")