Index: LeahiRt/CMakeLists.txt =================================================================== diff -u -rac303b902c681a25ff0d910dd56ab309669e381f -r8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52 --- LeahiRt/CMakeLists.txt (.../CMakeLists.txt) (revision ac303b902c681a25ff0d910dd56ab309669e381f) +++ LeahiRt/CMakeLists.txt (.../CMakeLists.txt) (revision 8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52) @@ -16,6 +16,8 @@ find_package(Comms HINTS ${CMAKE_CURRENT_SOURCE_DIR}/../lib/Comms REQUIRED) find_package(MsgUtils HINTS ${CMAKE_CURRENT_SOURCE_DIR}/../lib/MsgUtils REQUIRED) +set(LEAHI_MSG_CONF ${CMAKE_CURRENT_SOURCE_DIR}/../data/LeahiUnhandled.conf) + set(INCLUDES LeahiRtController.h ) @@ -25,6 +27,8 @@ main.cpp ) +generate_message_ini(LEAHI_MSG_CONF ${CMAKE_CURRENT_SOURCE_DIR}/config/LeahiRtMessage.ini generate_message_ini) + add_executable(${PROJECT_NAME}) target_sources(${PROJECT_NAME} PRIVATE ${INCLUDES} ${SRCS}) Index: lib/MsgUtils/cmake/MsgUtils.cmake =================================================================== diff -u -r402926738e7394ee2d3dc7add2e6d755f06a289d -r8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52 --- lib/MsgUtils/cmake/MsgUtils.cmake (.../MsgUtils.cmake) (revision 402926738e7394ee2d3dc7add2e6d755f06a289d) +++ lib/MsgUtils/cmake/MsgUtils.cmake (.../MsgUtils.cmake) (revision 8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52) @@ -57,6 +57,34 @@ ) endfunction() +# \brief This function custom command to generate/update a message handling INI. +# \param[in] _input_confs list of message conf files to use to generate the INI +# \param[in] _output_ini path to the INI file to create or update +# \param[in] _target_name name of the custom target to add +function(generate_message_ini _input_confs _output_ini _target_name) + find_package(Python3 COMPONENTS Interpreter Development REQUIRED) + + # cmake >= 3.20: cmake_path(ABSOLUTE_PATH _output_ini) + get_filename_component(_output_ini ${_output_ini} ABSOLUTE) # cmake < 3.20 + + # generate a pretty string containing a list of the absolute paths of all conf files + foreach(_path ${${_input_confs}}) + # cmake >= 3.20: cmake_path(ABSOLUTE_PATH _path) + get_filename_component(_path ${_path} ABSOLUTE) + list(APPEND _files ${_path}) + endforeach() + list(JOIN _files ", " _file_list) + + add_custom_target(${_target_name} + COMMAND + ${PROJECT_PYTHON} ${MSGUTILS_SCRIPTS_DIR}/GenerateMsgIni.py + ${${_input_confs}} + ${_output_ini} + COMMENT "Generating/updating LeahiRt message INI ${_output_ini} from input ${_file_list}" + VERBATIM + ) +endfunction() + # \brief This function adds a custom command to generate a Protobuf file from inputted message conf files # \param[in] _input_confs list of message conf files to use to generate the header # \param[in] _protobuf_filename name of the generated protobuf file Index: lib/MsgUtils/include/MessageDispatcher.h =================================================================== diff -u --- lib/MsgUtils/include/MessageDispatcher.h (revision 0) +++ lib/MsgUtils/include/MessageDispatcher.h (revision 8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52) @@ -0,0 +1,202 @@ +/*! + * + * Copyright (c) 2020-2026 Diality Inc. - All Rights Reserved. + * \copyright + * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN + * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. + * + * \file MessageDispatcher.h + * \author (last) Stephen Quong + * \date (last) 12-Jun-2026 + * \author (original) Behrouz NematiPour + * \date (original) 26-Aug-2020 + * + */ +// SQ Ported from luis application/sources/canbus/MessageDispatcher.h. +// SQ Only the inbound per-CAN-id frame reassembly path is kept active; the +// SQ GUI/action/acknowledge machinery is commented out (not deleted) so the +// SQ original luis structure stays recognizable and re-enabling stays trivial. +#pragma once + +// Qt +#include +#include + +// Project +// #include "main.h" // Doxygen : do not remove // SQ commented out: luis singleton/logging infra +#include "MessageBuilder.h" +// #include "MessageInterpreter.h" // SQ commented out: GUI message interpretation + +// define +// #define _MessageDispatcher Can::MessageDispatcher::I() // SQ commented out: singleton accessor not used here + +// forward declarations +// class tst_canbus; // SQ commented out: luis test friends +// class tst_acknow; +// class tst_messaging; + +// since this class is the interface between GUI and Can +// it needs to use Gui namespace otherwise it makes code hard to read. +// using namespace Gui; // SQ commented out: no Gui namespace in this project +namespace Can { +/*! + * \brief The MessageDispatcher class \n + * \details Message Dispatcher is the class which is the mediator between CanBus Frames \n + * and Application Messages. \n + * The massages and frames need to be interpreted form/to hex \n + * and also need to be split into frames or constructor from frames to be a message. \n + * ---------------------------------------------------------------------------------------- \n + * \n + * Interpreter : message [ toHex , fromHex] \n + * \n + * Builder : message [ toFrame , fromFrame ] \n + * \n + * Dispatcher : signal->Handler( .. frame .. ) \n + * \n + * Handler : signal->Dispatcher( .. frame .. ) \n + * \n + * ---------------------------------------------------------------------------------------- \n + * \n + * *** UI <-> AppController <-> Dispatcher <-> Handler <-> HD *** \n + * \n + * ---------------------------------------------------------------------------------------- \n + * \n + * UI -> message -> \n + * AppController { \n + * signal->Dispatcher ( .. message .. ) \n + * } \n + * \n + * Dispatcher { \n + * .. \n + * messageList[ch][frameList] += Builder.toFrame ( Interpreter.toHex ( message ) ) \n + * .. \n + * signal->Handler( .. frame .. ) \n + * } \n + * \n + * ---------------------------------------------------------------------------------------- \n + * \n + * HD -> frame -> \n + * Handler { \n + * signal->Dispatcher ( .. frame .. ) \n + * } \n + * \n + * Dispatcher { \n + * messageList[ch][frameList] += Interpreter.fromHex( frame ) \n + * isComplete => Builder.fromFrame( frameList ) : signal->AppController \n + * } \n + * \n + * ---------------------------------------------------------------------------------------- \n + */ +class MessageDispatcher : public QObject +{ + Q_OBJECT + + // Singleton + // SINGLETON(MessageDispatcher) // SQ commented out: not a singleton here, owned by LeahiRtController + + // friends + // friend class ::tst_canbus; // SQ commented out: luis test friends + // friend class ::tst_acknow; + // friend class ::tst_messaging; + + QHash _messageList; // SQ Can_Id -> CanId (this project's enum name) + + Sequence _txSequence = 0; + Sequence _rxSequence = 0; + + MessageBuilder _builder; + // MessageInterpreter _interpreter; // SQ commented out: GUI message interpretation + + // QThread *_thread = nullptr; // SQ commented out: thread owned by LeahiRtController + // bool _init = false; // SQ commented out: init()/thread mgmt removed + + // SQ commented out: list of transmit(request) messages that require AckBack -- GUI/action specific. +#if 0 + // List of the transmit(request) only, messages which require acknowledge back(AckBack). + QList _needsAcknow { + GuiActionType::ID_TDCheckIn , + // ... (full luis list of ~60 GuiActionType ids) ... + GuiActionType::ID_ResetHDInServiceModeReq , + }; +#endif + +// public slots: // SQ commented out: init()/quit() thread lifecycle (controller-managed) +// bool init(); +// bool init(QThread &vThread); +// void quit(); + +public: + explicit MessageDispatcher(QObject *parent = nullptr); // SQ replaced singleton with plain ctor + + // void enableConsoleOut(bool vEnable) { _builder.enableConsoleOut(vEnable); } // SQ commented out: console-out passthrough + +private: + // void initConnections(); // SQ commented out: wiring now done in LeahiRtController + + // void initThread(QThread &vThread); // SQ commented out: thread mgmt removed + // void quitThread(); + + // SQ commented out: outbound action/frame transmit + acknowledge path (GUI/action specific) +#if 0 + void actionTransmit (GuiActionType vActionId, const QVariantList &vData, Sequence vSequence = 0, Can_Id vCanId = Can::Can_Id::eChlid_UI_TD); + void framesTransmit (Can_Id vCan_Id, const FrameList &vFrameList); + + bool needsAcknow (GuiActionType vActionId); + bool needsAcknow (Can_Id vCan_Id); +#endif + + bool buildMessage (CanId vCanId, const QByteArray &vPayload); // SQ Can_Id -> CanId + // bool interpretMessage(const Message &vMessage); // SQ commented out: GUI interpretation + + // SQ commented out: tx/rx sequence counters -- rxCount() kept, txCount() unused here + // Sequence txCount(); // SQ commented out: no outbound path here + Sequence rxCount(); + + // SQ commented out: acknowledge check/transmit (GUI/action specific, needs Message::actionId) +#if 0 + bool checkAcknowReceived(const Message &vMessage, const QString &vSrcText); + bool checkAcknowTransmit(const Message &vMessage, const QString &vSrcText); +#endif + +signals: + // SQ commented out: GUI action-level receive signal (needs GuiActionType) + // void didActionReceive (GuiActionType vAction , const QVariantList &vData); + + /*! + * \brief didActionReceive + * \details Emitted when a complete message has been reassembled from CAN frame(s). + * \param vMessage - the completed message // SQ documented param (luis left it blank) + */ + void didActionReceive (const Message &vMessage); // SQ kept: repurposed as the message-complete signal + + // SQ commented out: acknowledge + outbound-transmit signals (GUI/action specific) +#if 0 + void didAcknowReceive (Sequence vSequence); + void didAcknowTransmit(Can_Id vCan_Id, Sequence vSequence, const FrameList &vFrameList); + void didFrameTransmit (Can_Id vCan_Id, const QByteArray &vPayload); + void didFailedTransmit(Sequence vSequence); +#endif + +public slots: // SQ was private slots in luis; public so controller can connect/forward + // A Frame has been received from CanInterface + void onFrameReceive (CanId vCanId, const QByteArray &vPayload); // SQ Can_Id -> CanId + + // SQ commented out: outbound transmit + action/settings slots (GUI/action specific) +#if 0 + void onFramesTransmit (Can_Id vCan_Id, Sequence vSequence, const FrameList &vFrameList); + void onFailedTransmit ( Sequence vSequence); + + // An Action has been requested to be transmitted. + void onActionTransmit (GuiActionType vActionId, const QVariantList &vData); + + void onSettingsDone (); + + // ---- Signal/Slots + ADJUST_TRANSMT_MODEL_BRIDGE_DEFINITIONS_NOEMIT + ACTION_RECEIVE_MODEL_BRIDGE_DEFINITIONS + + ACTION_RECEIVE_PRIVATE_SLOT(UIPostFinalResultHDRequestData) +#endif + +}; +} Index: scripts/MsgUtils/GenerateMsgIni.py =================================================================== diff -u --- scripts/MsgUtils/GenerateMsgIni.py (revision 0) +++ scripts/MsgUtils/GenerateMsgIni.py (revision 8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52) @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import argparse +import os +import sys +from msgutils import MsgIni + +def main(): + parser = argparse.ArgumentParser( + description='Tool for generating/maintaining the per-message routing INI from the inputted message conf file(s)' + ) + parser.add_argument('conf', nargs='+') + parser.add_argument('output', help='path to the message INI to create or update') + + args = parser.parse_args() + if len(sys.argv) < 3: + parser.print_help() + else: + msg_ini = MsgIni() + try: + for conf in args.conf: + msg_ini.loadConf(conf, clear=False) + output_dir = os.path.dirname(os.path.abspath(args.output)) + os.makedirs(output_dir, exist_ok=True) + msg_ini.merge_ini(args.output) + except Exception as e: + print('Error: %s' % e) + sys.exit(1) + +if __name__ == "__main__": # calling main function + main() Index: scripts/MsgUtils/msgutils/MsgIni.py =================================================================== diff -u --- scripts/MsgUtils/msgutils/MsgIni.py (revision 0) +++ scripts/MsgUtils/msgutils/MsgIni.py (revision 8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52) @@ -0,0 +1,118 @@ +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)") Index: scripts/MsgUtils/msgutils/__init__.py =================================================================== diff -u -rbde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b -r8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52 --- scripts/MsgUtils/msgutils/__init__.py (.../__init__.py) (revision bde1243eb2ff6af2868f6c6ad0cb4f5760aaf68b) +++ scripts/MsgUtils/msgutils/__init__.py (.../__init__.py) (revision 8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52) @@ -1,3 +1,4 @@ from .MsgData import MsgData from .MsgCpp import MsgCpp from .MsgProtobuf import MsgProtobuf +from .MsgIni import MsgIni Index: scripts/MsgUtils/pyproject.toml =================================================================== diff -u -r088513e6ea7bad08b4fb7862127c726eabad18fd -r8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52 --- scripts/MsgUtils/pyproject.toml (.../pyproject.toml) (revision 088513e6ea7bad08b4fb7862127c726eabad18fd) +++ scripts/MsgUtils/pyproject.toml (.../pyproject.toml) (revision 8165b9eb5d23253e5d38b6ffde9d1a21cb1cbc52) @@ -12,6 +12,7 @@ [project.scripts] GenerateMsgDefsCpp = "GenerateMsgDefsCpp:main" GenerateProtobuf = "GenerateProtobuf:main" +GenerateMsgIni = "GenerateMsgIni:main" [tool.setuptools.packages.find] exclude = ["test*"]