Index: CloudConnect/CMakeLists.txt =================================================================== diff -u -r51e99f2578e0901d9da91a4cb60d1b8858cfe971 -raaebfee335c74b0250864a6dce0555f866adadea --- CloudConnect/CMakeLists.txt (.../CMakeLists.txt) (revision 51e99f2578e0901d9da91a4cb60d1b8858cfe971) +++ CloudConnect/CMakeLists.txt (.../CMakeLists.txt) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -27,11 +27,11 @@ main.cpp ) -generate_msg_handling_ini(LEAHI_MSG_CONF ${CMAKE_CURRENT_SOURCE_DIR}/config/LeahiMsgHandling.ini generate_msg_handling_ini) +generate_can_routing_ini(LEAHI_MSG_CONF ${CMAKE_CURRENT_SOURCE_DIR}/config/LeahiCanRouting.ini generate_can_routing_ini) add_executable(${PROJECT_NAME}) -add_dependencies(${PROJECT_NAME} generate_msg_handling_ini) +add_dependencies(${PROJECT_NAME} generate_can_routing_ini) target_sources(${PROJECT_NAME} PRIVATE ${INCLUDES} ${SRCS}) Index: CloudConnect/CloudConnect.pro =================================================================== diff -u -r51e99f2578e0901d9da91a4cb60d1b8858cfe971 -raaebfee335c74b0250864a6dce0555f866adadea --- CloudConnect/CloudConnect.pro (.../CloudConnect.pro) (revision 51e99f2578e0901d9da91a4cb60d1b8858cfe971) +++ CloudConnect/CloudConnect.pro (.../CloudConnect.pro) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -1,7 +1,7 @@ TEMPLATE = app TARGET = CloudConnect CONFIG += c++17 moc -QT += core network +QT += core network mqtt QT -= gui QMAKE_CXXFLAGS += -Wall -Werror -Wextra @@ -14,22 +14,22 @@ LEAHI_MSG_CONF = $$PWD/../data/LeahiUnhandled.conf # Generate/update the Message-handling INI -MSG_HANDLING_INI = $$PWD/config/LeahiMsgHandling.ini +CAN_ROUTING_INI = $$PWD/config/LeahiCanRouting.ini -gen_ini.target = $$MSG_HANDLING_INI +gen_ini.target = $$CAN_ROUTING_INI gen_ini.depends = \ $$LEAHI_MSG_CONF \ + $$MSGUTILS_SCRIPTS_DIR/GenerateCanRoutingIni.py \ $$MSGUTILS_SCRIPTS_DIR/msgutils/MsgData.py \ - $$MSGUTILS_SCRIPTS_DIR/msgutils/MsgHandlingIni.py \ - $$MSGUTILS_SCRIPTS_DIR/GenerateMsgHandlingIni.py \ - $$MSGUTILS_SCRIPTS_DIR/msgutils/templates/MsgHandlingIni.jinja + $$MSGUTILS_SCRIPTS_DIR/msgutils/CanRoutingIni.py \ + $$MSGUTILS_SCRIPTS_DIR/msgutils/templates/CanRoutingIni.jinja gen_ini.commands = \ $$shell_quote($$PROJECT_PYTHON) \ - $$MSGUTILS_SCRIPTS_DIR/GenerateMsgHandlingIni.py \ + $$MSGUTILS_SCRIPTS_DIR/GenerateCanRoutingIni.py \ $$LEAHI_MSG_CONF \ - $$MSG_HANDLING_INI + $$CAN_ROUTING_INI QMAKE_EXTRA_TARGETS += gen_ini -PRE_TARGETDEPS += $$MSG_HANDLING_INI +PRE_TARGETDEPS += $$CAN_ROUTING_INI HEADERS = \ CloudConnectController.h Index: CloudConnect/CloudConnectController.cpp =================================================================== diff -u -r51e99f2578e0901d9da91a4cb60d1b8858cfe971 -raaebfee335c74b0250864a6dce0555f866adadea --- CloudConnect/CloudConnectController.cpp (.../CloudConnectController.cpp) (revision 51e99f2578e0901d9da91a4cb60d1b8858cfe971) +++ CloudConnect/CloudConnectController.cpp (.../CloudConnectController.cpp) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -10,48 +10,103 @@ * \date (original) 24-May-2026 * */ +#include +#include +#include + #include #include #include +#include +#include +#include #include #include "CloudConnectController.h" +#include "LeahiMsgDefs.pb.h" #include "LeahiMsgProtoUtils.h" -#include // TODO: temporary for capture protobuf -#include // TODO: temporary for capture protobuf -#include // TODO: temporary for capture protobuf +Q_LOGGING_CATEGORY(logConfig, "config") +Q_LOGGING_CATEGORY(logProto, "proto") +Q_LOGGING_CATEGORY(logCanRouting, "can.routing") +Q_LOGGING_CATEGORY(logMqtt, "mqtt") +Q_LOGGING_CATEGORY(logStats, "mqtt.stats") /*! * \brief CloudConnectController::CloudConnectController * \details Constructor. Wires the frame→dispatcher→completed-message pipeline. * The CAN device itself is started by startCan() once this object is * on its worker thread. * \param configPath - path to the settings file - * \param msgHandlingPath - path to the message handling INI + * \param canRoutingPath - path to the CAN message routing INI * \param parent - optional QObject parent * \note Must be constructed on the main thread, but later moved to a worker thread. * _canInterface is parented to this object so it migrates with it. */ -CloudConnectController::CloudConnectController(const QString &configPath, const QString &msgHandlingPath, - QObject *parent) : +CloudConnectController::CloudConnectController(QObject *parent) : QObject(parent), - _settings(configPath, QSettings::IniFormat), _canInterface(this), _dispatcher(this), _mqttClient(this) { - loadMsgHandling(msgHandlingPath); - - connect(&_canInterface, &Can::CanInterface::didFrameReceive, this, &CloudConnectController::onFrameReceive); - connect(&_dispatcher, &Can::MessageDispatcher::didActionReceive, this, &CloudConnectController::onMessageReceive); + connect(&_canInterface, &Can::CanInterface::didFrameReceive, this, &CloudConnectController::onCanFrameReceive); + connect(&_dispatcher, &Can::MessageDispatcher::didActionReceive, this, &CloudConnectController::onCanMessageReceive); + connect(&_mqttClient, &MqttClient::didStateChanged, this, &CloudConnectController::onCloudStateChanged); + connect(&_mqttClient, &MqttClient::didMessageStatusChanged, this, &CloudConnectController::onCloudMessageStatusChanged); + connect(&_mqttClient, &MqttClient::didMessageReceived, this, &CloudConnectController::onCloudMessageReceived); } /*! * \brief CloudConnectController::~CloudConnectController */ CloudConnectController::~CloudConnectController() = default; +bool CloudConnectController::loadConfig(const QString &configPath) +{ + static auto resolvePath = [](const QString &path) -> QString { + const QFileInfo info(path); + return info.isAbsolute() + ? path + : QString("%1/%2").arg(QCoreApplication::applicationDirPath(), path); + }; + + if (!QFileInfo::exists(configPath)) { + qCCritical(logConfig).noquote() << QString("config file %1 does not exist").arg(configPath); + return false; + } + + QSettings config = QSettings(configPath, QSettings::IniFormat); + if (config.status() != QSettings::NoError) { + qCCritical(logConfig).noquote() << QString("could not read config file %1").arg(configPath); + return false; + } + + // NOTE: do not try to prefix keys in the General (i.e. "General/") or beginGroup("General") or + // QSettings will look for a group in the INI file called "%General", "General" is a special group. + if (!loadCanRouting(resolvePath(config.value(QStringLiteral("CanRoutingConfig")).toString()))) { + return false; + } + + _appServerSocketPath = config.value("App/SocketName", "/tmp/cloudconnect.sock").toString(); + + config.beginGroup(QStringLiteral("Cloud")); + MqttClient::Config mqttConfig; + mqttConfig.endpoint = config.value(QStringLiteral("ServerAddress"), QStringLiteral("localhost")).toString(); + mqttConfig.port = config.value(QStringLiteral("Port"), 8883).toInt(); + mqttConfig.clientId = config.value(QStringLiteral("ClientId")).toString(); + mqttConfig.caPath = resolvePath(config.value(QStringLiteral("CaFile")).toString()); + mqttConfig.certPath = resolvePath(config.value(QStringLiteral("CertFile")).toString()); + mqttConfig.keyPath = resolvePath(config.value(QStringLiteral("KeyFile")).toString()); + _deviceId = config.value(QStringLiteral("SerialNumber")).toString(); + _topicPrefix = config.value(QStringLiteral("TopicPrefix")).toString(); + config.endGroup(); + if (!_mqttClient.init(mqttConfig)) { + return false; + } + + return true; +} + /*! * \brief CloudConnectController::initThread * \details Moves this object and its children onto a worker thread and starts it. @@ -61,7 +116,7 @@ void CloudConnectController::initThread(QThread &thread) { Q_ASSERT_X(QThread::currentThread() == qApp->thread(), __func__, - "CloudConnectController initialization must be done in Main Thread"); + "CloudConnectController initialization must be done in Main Thread"); thread.setObjectName(QString("%1_Thread").arg(metaObject()->className())); moveToThread(&thread); @@ -81,7 +136,7 @@ bool CloudConnectController::startCan() { Q_ASSERT_X(QThread::currentThread() == thread(), __func__, - "startCan() must run on the controller thread"); + "startCan() must run on the controller thread"); return _canInterface.init(); } @@ -97,14 +152,13 @@ bool CloudConnectController::listenForApp() { Q_ASSERT_X(QThread::currentThread() == thread(), __func__, - "listenForApp() must run on the controller thread"); + "listenForApp() must run on the controller thread"); // use Q_ASSERT only during object creation or thread move if (_appServer == nullptr) { _appServer = QSharedPointer::create(this); } - const QString appSocketPath = _settings.value("Socket/AppSocketName", "/tmp/cloudconnect.sock").toString(); - return _appServer->listen(appSocketPath); + return _appServer->listen(_appServerSocketPath); } /*! @@ -117,44 +171,23 @@ bool CloudConnectController::connectToCloud() { Q_ASSERT_X(QThread::currentThread() == thread(), __func__, - "connectToCloud() must run on the controller thread"); + "connectToCloud() must run on the controller thread"); - _mqttEnabled = _settings.value("Mqtt/Enabled", false).toBool(); - if (!_mqttEnabled) { - qInfo().noquote() << "CloudConnect: MQTT disabled, forwarding to the app socket only"; - return true; - } - - _topicPrefix = _settings.value("Mqtt/TopicPrefix", QStringLiteral("diality/v1/devices")).toString(); - _deviceId = _settings.value("Mqtt/DeviceId", QStringLiteral("test_device")).toString(); - - MqttClient::Config config; - config.endpoint = _settings.value("Mqtt/ServerAddress", QStringLiteral("127.0.0.1")).toString(); - config.port = static_cast(_settings.value("Mqtt/Port", 1883).toUInt()); - config.clientId = _deviceId; - config.caPath = _settings.value("Mqtt/CaFile").toString(); - config.certPath = _settings.value("Mqtt/CertFile").toString(); - config.keyPath = _settings.value("Mqtt/KeyFile").toString(); - - if (!_mqttClient.init(config)) { - qCritical().noquote() << "CloudConnect: could not initialise the MQTT interface"; - return false; - } - return _mqttClient.open(); } /*! - * \brief CloudConnectController::loadMsgHandling - * \details Parses message handling INI and populates _msgHandling. - * \param msgHandlingPath - path to the message handling INI file + * \brief CloudConnectController::loadCanRouting + * \details Parses CAN message routing INI and populates _canRouting. + * \param canRoutingPath - path to the CAN message routing INI file + * \return true if CanRouting config successfully loads, otherwise false */ -void CloudConnectController::loadMsgHandling(const QString &msgHandlingPath) +bool CloudConnectController::loadCanRouting(const QString &canRoutingPath) { - static const QHash actionMap = { - { QStringLiteral("SendAlways"), MsgAction::SendAlways }, - { QStringLiteral("SendDelta"), MsgAction::SendDelta }, - { QStringLiteral("Drop"), MsgAction::Drop }, + static const QHash actionMap = { + { QStringLiteral("SendAlways"), CanAction::SendAlways }, + { QStringLiteral("SendDelta"), CanAction::SendDelta }, + { QStringLiteral("Drop"), CanAction::Drop }, }; static const QHash topicMap = { { QStringLiteral("HighPriority"), CloudConnectFrame::Topic::HighPriority }, @@ -164,121 +197,133 @@ { QStringLiteral("CloudSyncLogFile"), CloudConnectFrame::Topic::CloudSyncLogFile }, }; - if (! QFile::exists(msgHandlingPath)) { - qWarning().noquote() << "CloudConnect: handling INI" << msgHandlingPath << "does not exist"; - return; + if (!QFile::exists(canRoutingPath)) { + qCWarning(logCanRouting).noquote() << QString("CAN mesg routing config file %1 does not exist").arg(canRoutingPath); + return false; } - QSettings msgHandlingIni(msgHandlingPath, QSettings::IniFormat); - if (msgHandlingIni.status() != QSettings::NoError) { - qWarning().noquote() << "CloudConnect: could not read message handling INI" << msgHandlingPath << "— all messages will be dropped"; - return; + QSettings canRoutingConfig(canRoutingPath, QSettings::IniFormat); + if (canRoutingConfig.status() != QSettings::NoError) { + qCWarning(logCanRouting).noquote() << QString("could not read CAN message routing config file %1").arg(canRoutingPath); + return false; } int msgCount = 0; - for (const QString &group : msgHandlingIni.childGroups()) { + for (const QString &group : canRoutingConfig.childGroups()) { bool ok = false; const Can::MsgId msgId = static_cast(group.toUInt(&ok, 16)); if (!ok) { - qWarning().noquote() << QString("CloudConnect: could not convert group \"%1\" to MsgId").arg(group); + qCWarning(logCanRouting).noquote() << QString("could not convert group \"%1\" to MsgId in CAN mesg routing config file %2") + .arg(group, canRoutingPath); continue; } - msgHandlingIni.beginGroup(group); - const QString msgIdStr = msgHandlingIni.value(QStringLiteral("msg_id")).toString(); - const QString actionStr = msgHandlingIni.value(QStringLiteral("action")).toString().trimmed(); - const QString topicStr = msgHandlingIni.value(QStringLiteral("topic")).toString().trimmed(); - qInfo().noquote() << QString("%1 (0x%2): action=%3, topic=%4") - .arg(msgIdStr, QString("%1").arg(quint16(msgId), 4, 16, QChar('0')).toUpper(), actionStr, topicStr); - msgHandlingIni.endGroup(); + canRoutingConfig.beginGroup(group); + const QString msgIdStr = canRoutingConfig.value(QStringLiteral("msg_id")).toString(); + const QString actionStr = canRoutingConfig.value(QStringLiteral("action")).toString().trimmed(); + const QString topicStr = canRoutingConfig.value(QStringLiteral("topic")).toString().trimmed(); + // qCInfo(logCanRouting).noquote() << QString("%1 (0x%2): action=%3, topic=%4") + // .arg(msgIdStr, QString("%1").arg(quint16(msgId), 4, 16, QChar('0')).toUpper(), actionStr, topicStr); + canRoutingConfig.endGroup(); - MsgHandling msgHandling; - msgHandling.action = actionMap.value(actionStr, MsgAction::Drop); - msgHandling.topic = topicMap.value(topicStr, CloudConnectFrame::Topic::NormalPriority); + CanRouting canRouting; + // canRouting.action = actionMap.value(actionStr, CanAction::Drop); + canRouting.action = actionMap.value(actionStr, CanAction::SendAlways); + canRouting.topic = topicMap.value(topicStr, CloudConnectFrame::Topic::NormalPriority); if (actionStr.length() > 0 && !actionMap.contains(actionStr)) { - qWarning().noquote() << QString("CloudConnect: unknown message action \"%1\" for msgId=0x%2 — defaulting to Drop") - .arg(actionStr).arg(QString("%1").arg(quint16(msgId), 4, 16, QChar('0').toUpper())); + qCWarning(logCanRouting).noquote() << QString("unknown message action \"%1\" for msgId=0x%2 — defaulting to Drop") + .arg(actionStr, QString("%1").arg(quint16(msgId), 4, 16, QChar('0').toUpper())); } if (topicStr.length() > 0 && !topicMap.contains(topicStr)) { - qWarning().noquote() << QString("CloudConnect: unknown message topic \"%1\" for msgId=0x%2 — defaulting to NormalPriority") - .arg(topicStr).arg(QString("%1").arg(quint16(msgId), 4, 16, QChar('0').toUpper())); + qCWarning(logCanRouting).noquote() << QString("unknown message topic \"%1\" for msgId=0x%2 — defaulting to NormalPriority") + .arg(topicStr, QString("%1").arg(quint16(msgId), 4, 16, QChar('0').toUpper())); } - _msgHandling.insert(msgId, msgHandling); + _canRouting.insert(msgId, canRouting); msgCount++; } - qInfo().noquote() << QString("CloudConnect: loaded message handling %1 (%2 entries)").arg(msgHandlingPath).arg(msgCount); + qCInfo(logCanRouting).noquote() << QString("loaded CAN message routing %1 (%2 entries)").arg(canRoutingPath).arg(msgCount); + return true; } /*! - * \brief CloudConnectController::onFrameReceive + * \brief CloudConnectController::onCanFrameReceive * \details Unpacks a CAN frame and sends it to the dispatcher to reassemble the entire CAN message. * \param frame - the received CAN frame */ -void CloudConnectController::onFrameReceive(const QCanBusFrame frame) +void CloudConnectController::onCanFrameReceive(const QCanBusFrame &frame) { _dispatcher.onFrameReceive(Can::CanId(frame.frameId()), frame.payload()); } /*! - * \brief CloudConnectController::onMessageReceive - * \details Applies the message handling policy from MsgHandling.ini. + * \brief CloudConnectController::onCanMessageReceive + * \details Applies the CAN mesg routing policy from CanRouting.ini. * \param msg - the reassembled message */ -void CloudConnectController::onMessageReceive(const Can::Message &msg) +void CloudConnectController::onCanMessageReceive(const Can::Message &msg) { - const auto it = _msgHandling.constFind(msg.msgId); - if (it == _msgHandling.constEnd()) { - qInfo().noquote() << QString("CloudConnect: no action defined for %1 (0x%2), dropping") + const auto it = _canRouting.constFind(msg.msgId); + if (it == _canRouting.constEnd()) { + qCInfo(logCanRouting).noquote() << QString("no action defined for %1 (0x%2), dropping") .arg(leahi::msgIdString(leahi::MsgId(msg.msgId))).arg(msg.msgId, 4, 16, QChar('0')); return; } - if (it->action == MsgAction::Drop) { - qInfo().noquote() << QString("CloudConnect: %1 (0x%2) dropped") + if (it->action == CanAction::Drop) { + qCInfo(logCanRouting).noquote() << QString("%1 (0x%2) dropped") .arg(leahi::msgIdString(leahi::MsgId(msg.msgId))).arg(msg.msgId, 4, 16, QChar('0')); return; } - auto &[received, cachedMsg] = _msgCache[msg.msgId]; - if (it->action == MsgAction::SendDelta && received && - cachedMsg.data.chopped(1) == msg.data.chopped(1)) { - qInfo().noquote() << QString("CloudConnect: %1 (0x%2) did not changed from previous, dropping") + auto &[received, cachedMsg] = _canCache[msg.msgId]; + if (it->action == CanAction::SendDelta && received && + cachedMsg.data.chopped(1) == msg.data.chopped(1)) { // move chops to payload sending + qCInfo(logCanRouting).noquote() << QString("%1 (0x%2) did not changed from previous, dropping") .arg(leahi::msgIdString(leahi::MsgId(msg.msgId))).arg(msg.msgId, 4, 16, QChar('0')); return; } - const QByteArray payload = leahi::canMessageToProtobufByteArray( - QDateTime::currentDateTime(), - QStringLiteral("test_device"), - msg); + QByteArray payload; + if (leahi::canMessageToProtobufByteArray(QDateTime::currentDateTime(), msg, payload)) { + // Read the token out before it is advanced: the value handed to publish() is + // what the delivery ledger reports, so logging the post-increment value made + // every trace-back off by one. + qint32 msgId; + if (_mqttClient.publish(mqttTopic(it->topic), payload, msgId)) { + // Logs the token so an unacknowledged publish reported by the MQTT delivery + // ledger can be traced back to the message it carried. + qCWarning(logMqtt).noquote() << QString("%1 (0x%2) published (msgId=%3)") + .arg(leahi::msgIdString(leahi::MsgId(msg.msgId))).arg(msg.msgId, 4, 16, QChar('0')).arg(msgId); + } + else { + // TODO: spool the message if publish fails + qCWarning(logMqtt).noquote() << QString("could not publish %1 (0x%2), message lost") + .arg(leahi::msgIdString(leahi::MsgId(msg.msgId))).arg(msg.msgId, 4, 16, QChar('0')); + } - // TODO: temporary for capture protobuf - // captureProtobuf(payload); - - const bool published = _mqttEnabled && _mqttClient.publish(mqttTopic(it->topic), payload); - if (_mqttEnabled && !published) { - qWarning().noquote() << QString("CloudConnect: could not publish %1 (0x%2), message lost") - .arg(leahi::msgIdString(leahi::MsgId(msg.msgId))).arg(msg.msgId, 4, 16, QChar('0')); + // NOTE: this records that the client ACCEPTED the message, not that AWS + // acknowledged it. Only the PUBACK counted in the MqttClient ledger + // means delivered. + // received = published; } else { - qWarning().noquote() << QString("CloudConnect: %1 (0x%2) published") + qCWarning(logProto).noquote() << QString("could not serialize %1 (0x%2) message to protobuf, message dropped") .arg(leahi::msgIdString(leahi::MsgId(msg.msgId))).arg(msg.msgId, 4, 16, QChar('0')); } // Cache the message only after it has been successfully sent. // Note: saving before successfully send may prevent a message of with that msgId from being // sent until a delta message is received if SendDelta is specified for the msgId. - received = published; cachedMsg = msg; } /*! * \brief CloudConnectController::mqttTopic * \details Builds the MQTT topic for a message class. - * \param topic - message class from the message handling INI + * \param topic - message class from the CAN message routing INI * \return Topic of the form {prefix}/{deviceId}/{suffix}. */ QString CloudConnectController::mqttTopic(CloudConnectFrame::Topic topic) const @@ -291,98 +336,135 @@ { CloudConnectFrame::Topic::CloudSyncLogFile, QStringLiteral("cs_log") }, }; - return QString("%1/%2/%3").arg(_topicPrefix, _deviceId, - suffixes.value(topic, QStringLiteral("normal"))); + // return suffixes.value(topic, QStringLiteral("normal")); + Q_UNUSED(topic) + return QString("%1/%2/%3").arg(_topicPrefix, _deviceId, "clinical"); } /*! - * \brief CloudConnectController::captureProtobuf - * \details Appends one varint-delimited protobuf record to the .ser capture file and - * the equivalent json to .json capture file. - * \note TODO: temporary for capture protobuf + * \brief CloudConnectController::onCloudStateChanged + * \details Re-establishes the echo subscriptions on every new session, and + * retires anything still awaiting an echo when one ends. + * \param connected - true once the broker has accepted the session + * \note Subscribing here rather than in connectToCloud() is required, not + * stylistic: open() only starts the attempt, and a SUBSCRIBE sent + * before the CONNACK is rejected by the client. */ -void CloudConnectController::captureProtobuf(const QByteArray &payload) +void CloudConnectController::onCloudStateChanged(QMqttClient::ClientState state) { - static auto encodeVarint = [](quint32 value) -> QByteArray { - QByteArray out; - do { - quint8 byte = value & 0x7F; - value >>= 7; - if (value != 0) { - byte |= 0x80; // more bytes follow - } - out.append(static_cast(byte)); - } while (value != 0); - return out; - }; + switch (state) { + case QMqttClient::Disconnected: + // clear any pending messages + // expireInFlight(true); + break; + case QMqttClient::Connecting: + break; + case QMqttClient::Connected: + (void)_mqttClient.subscribe(mqttTopic(CloudConnectFrame::Topic::NormalPriority)); + break; + default: + break; + } +} - if (!_captureSerFile) { - QString serPath = _settings.value("Capture/SerFile").toString(); - if (!serPath.isEmpty()) { - // Serialized protobuf streams carry the .ser extension. - if (!serPath.endsWith(QStringLiteral(".ser"), Qt::CaseInsensitive)) { - serPath += QStringLiteral(".ser"); - } - - _captureSerFile = std::make_unique(serPath); - if (_captureSerFile->open(QIODevice::WriteOnly | QIODevice::Append)) { - qWarning().noquote() << "CloudConnect: protobuf capture ser writing to" << serPath; - } - else { - qCritical().noquote() << "CloudConnect: cannot open capture ser file" << serPath; - _captureSerFile.reset(); - } - } +/*! + * \brief CloudConnectController::onCloudMessageStatusChanged + * \details Correlates one message delivered by the broker back to the publish + * that produced it and credits that msgId. + * \param msgId - message identifier + * \param status - new message status + * \param properties - additional properties specified by the server/broker + */ +void CloudConnectController::onCloudMessageStatusChanged(qint32 id, QMqtt::MessageStatus status, + const QMqttMessageStatusProperties &properties) +{ + Q_UNUSED(id) // SQ + Q_UNUSED(properties) // SQ + switch (status) { + case QMqtt::MessageStatus::Unknown: + qCInfo(logMqtt).noquote() << "message status changed to Unknown"; // SQ + break; + case QMqtt::MessageStatus::Published: + qCInfo(logMqtt).noquote() << "message status changed to Published"; // SQ + break; + case QMqtt::MessageStatus::Acknowledged: + qCInfo(logMqtt).noquote() << "message status changed to Acknowledged"; // SQ + break; + case QMqtt::MessageStatus::Received: + qCInfo(logMqtt).noquote() << "message status changed to Received"; // SQ + break; + case QMqtt::MessageStatus::Released: + qCInfo(logMqtt).noquote() << "message status changed to Released"; // SQ + break; + case QMqtt::MessageStatus::Completed: + qCInfo(logMqtt).noquote() << "message status changed to Completed"; // SQ + break; + default: + break; } +} - if (_captureSerFile) { - qDebug().noquote() << QString("wrote to capture.ser file (sizes: length=%1, payload=%2)") - .arg(_captureSerFile->write(encodeVarint(static_cast(payload.size())))) - .arg(_captureSerFile->write(payload)); - _captureSerFile->flush(); +/*! + * \brief CloudConnectController::onCloudMessageReceived + * \details Correlates one message delivered by the broker back to the publish + * that produced it and credits that msgId. + * \param topic - topic the message was delivered on + * \param payload - serialised protobuf message + * \note The msgId is read from the message itself: every Leahi message carries + * Header at field 1, and Envelope exists to parse exactly that much + * without knowing the concrete message type. + */ +void CloudConnectController::onCloudMessageReceived(const QString &topic, const QByteArray &payload) +{ + qCInfo(logMqtt).noquote() << QString("received message with topic=%1").arg(topic); // SQ + + // TODO: filter by topic + + leahi::messages::Envelope envelope; + if (!envelope.ParseFromArray(payload.constData(), payload.size()) || !envelope.has_header()) { + // Unknown fields parse cleanly in proto3, so a foreign payload usually + // lands here as a header-less Envelope rather than as a parse failure. + qCWarning(logMqtt).noquote() << QString("received mesg (size=%1) for topic %2 do not contain a valid protobuf payload, ignoring") + .arg(payload.size()).arg(topic); + return; } - if (!_captureJsonFile) { - QString jsonPath = _settings.value("Capture/JsonFile").toString(); - if (!jsonPath.isEmpty()) { - // Serialized protobuf streams carry the .ser extension. - if (!jsonPath.endsWith(QStringLiteral(".json"), Qt::CaseInsensitive)) { - jsonPath += QStringLiteral(".json"); - } + const leahi::messages::Header &header = envelope.header(); + const Can::MsgId msgId = Can::MsgId(header.msgid()); - _captureJsonFile = std::make_unique(jsonPath); - if (_captureJsonFile->open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { - qWarning().noquote() << "CloudConnect: protobuf capture json writing to" << jsonPath; - } - else { - qCritical().noquote() << "CloudConnect: cannot open capture json file" << jsonPath; - _captureJsonFile.reset(); - } - } + // No serial here: Header carries only timestamp/sequence/msgId, and the device + // identity travels in the topic (logged by handlePublishPacket) rather than the + // payload. Printing the timestamp instead also proves updateHeader populated it. + const qint64 headerMsecs = header.timestamp().seconds() * 1000 + header.timestamp().nanos() / 1000000; + qCInfo(logProto).noquote() << QString("header: msgId=0x%1 (%2), seq=%3, timestamp=%4") + .arg(QString("%1").arg(msgId, 4, 16, QChar('0')).toUpper(), leahi::msgIdString(static_cast(msgId))) + .arg(header.sequence()) + .arg(QDateTime::fromMSecsSinceEpoch(headerMsecs).toString(Qt::ISODateWithMs)); + + const std::string &typeName = leahi::msgIdToProtoName(msgId); + if (typeName.empty()) { + qCWarning(logProto).noquote() << QString("no protobuf mapping for message with msgId=0x%1, ignoring").arg(msgId, 4, 16, QChar('0')); + return; } - if (_captureJsonFile) { - leahi::messages::Envelope envelope; - if (envelope.ParseFromArray(payload.constData(), payload.size())) { - const leahi::messages::Header &header = envelope.header(); - const google::protobuf::Descriptor *desc = - google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName( - leahi::msgIdToProtoName(static_cast(header.msgid()))); - if (desc) { - google::protobuf::DynamicMessageFactory factory; - std::unique_ptr body(factory.GetPrototype(desc)->New()); - if (body->ParseFromArray(payload.constData(), payload.size())) { - std::string json; - google::protobuf::util::JsonPrintOptions opts; - opts.add_whitespace = true; - opts.always_print_primitive_fields = true; - google::protobuf::util::MessageToJsonString(*body, &json, opts); - QString jsonStr = QString::fromStdString(json); - qDebug().noquote() << QString("wrote to capture.json file (size: json=%2)") - .arg(_captureJsonFile->write(jsonStr.toLatin1())); - _captureJsonFile->flush(); - } - } - } + const google::protobuf::Descriptor *descriptor = + google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName(typeName); + if (descriptor == nullptr) { + qCWarning(logProto).noquote() << QString("no descriptor for protobuf message %1, ignoring").arg(QString::fromStdString(typeName)); + return; } + + google::protobuf::DynamicMessageFactory factory; + std::unique_ptr body(factory.GetPrototype(descriptor)->New()); + if (!body->ParseFromArray(payload.constData(), static_cast(payload.size()))) { + qCWarning(logProto).noquote() << QString("could not parse protobuf message %1, ignoring").arg(QString::fromStdString(typeName)); + return; + } + + std::string json; + google::protobuf::util::JsonPrintOptions options; + options.add_whitespace = true; + options.always_print_primitive_fields = true; + google::protobuf::util::MessageToJsonString(*body, &json, options); + qCDebug(logProto).noquote() << QString::fromStdString(typeName) << ":" << Qt::endl << QString::fromStdString(json); } Index: CloudConnect/CloudConnectController.h =================================================================== diff -u -r51e99f2578e0901d9da91a4cb60d1b8858cfe971 -raaebfee335c74b0250864a6dce0555f866adadea --- CloudConnect/CloudConnectController.h (.../CloudConnectController.h) (revision 51e99f2578e0901d9da91a4cb60d1b8858cfe971) +++ CloudConnect/CloudConnectController.h (.../CloudConnectController.h) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -13,21 +13,16 @@ #pragma once #include -#include -#include // TODO: temporary for protobuf capture #include #include #include -#include #include #include #include -#include -#include -#include "CloudConnectFrame.h" #include "CanInterface.h" #include "CanMessage.h" +#include "CloudConnectFrame.h" #include "CloudConnectServer.h" #include "MessageDispatcher.h" #include "MqttClient.h" @@ -42,53 +37,55 @@ Q_OBJECT public: - explicit CloudConnectController(const QString &configPath, const QString &msgHandlingPath, - QObject *parent = nullptr); + explicit CloudConnectController(QObject *parent = nullptr); ~CloudConnectController(); + bool loadConfig(const QString &configPath); bool startCan(); bool listenForApp(); bool connectToCloud(); void initThread(QThread &thread); private: /*! - * \brief Send message handling policy for a CAN message. + * \brief CAN message routing actions */ - enum class MsgAction { + enum class CanAction { Drop, SendAlways, SendDelta, }; /*! - * \brief Message handling entry loaded from message handling INI. + * \brief CAN message routing entry loaded from CAN handling INI. */ - struct MsgHandling { - MsgAction action = MsgAction::Drop; + struct CanRouting { + CanAction action = CanAction::Drop; CloudConnectFrame::Topic topic = CloudConnectFrame::Topic::NormalPriority; }; - void loadMsgHandling(const QString &msgHandlingPath); + bool loadCanRouting(const QString &canRoutingPath); QString mqttTopic(CloudConnectFrame::Topic topic) const; - void captureProtobuf(const QByteArray &payload); // TODO: temporary for protobuf capture - QSettings _settings; Can::CanInterface _canInterface; Can::MessageDispatcher _dispatcher; - QMap> _msgCache; - QHash _msgHandling; + QMap> _canCache; + QHash _canRouting; + QString _appServerSocketPath; QSharedPointer _appServer; MqttClient _mqttClient; QString _topicPrefix; // TODO: define in INI? QString _deviceId; // TODO: this needs to be sent from Leahi app or retrieved from somewhere - bool _mqttEnabled = false; - // TODO: temporary for protobuf capture - std::unique_ptr _captureSerFile; - std::unique_ptr _captureJsonFile; + QString _serialNumber; + QString _deviceCertPath; + QString _deviceKeyPath; private Q_SLOTS: - void onFrameReceive(const QCanBusFrame frame); - void onMessageReceive(const Can::Message &msg); + void onCanFrameReceive(const QCanBusFrame &frame); + void onCanMessageReceive(const Can::Message &msg); + void onCloudStateChanged(QMqttClient::ClientState state); + void onCloudMessageStatusChanged(qint32 id, QMqtt::MessageStatus status, + const QMqttMessageStatusProperties &properties); + void onCloudMessageReceived(const QString &topic, const QByteArray &payload); }; Index: CloudConnect/config/CloudConnect.ini =================================================================== diff -u -r51e99f2578e0901d9da91a4cb60d1b8858cfe971 -raaebfee335c74b0250864a6dce0555f866adadea --- CloudConnect/config/CloudConnect.ini (.../CloudConnect.ini) (revision 51e99f2578e0901d9da91a4cb60d1b8858cfe971) +++ CloudConnect/config/CloudConnect.ini (.../CloudConnect.ini) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -1,17 +1,17 @@ -[Socket] -AppSocketName=/tmp/cloudconnect.sock +[General] +CanRoutingConfig=config/LeahiCanRouting.ini -[Mqtt] -ServerAddress=127.0.0.1 +[App] +SocketName=/tmp/cloudconnect.sock + +[Cloud] +; ServerAddress=127.0.0.1 +ServerAddress=a30gn2ua3sx6wx-ats.iot.us-east-1.amazonaws.com Port=8883 TopicPrefix=diality/v1/devices -; TODO: DeviceId should be sent by Leahi app or retrieved from somewhere else -DeviceId=test_device -CaFile= -CertFile= -KeyFile= - -; TODO: temporary for protobuf capture -[Capture] -SerFile=/home/leahi/Public/leahi-realtime-cdt/cloudconnect_protobuf.ser -JsonFile=/home/leahi/Public/leahi-realtime-cdt/cloudconnect_protobuf.json +ClientId=diality-test_device +; TODO: SerialNumber should be sent by Leahi app or retrieved from somewhere else +SerialNumber=diality-test_device +CaFile=config/ssl/certs/AmazonRootCA1.pem +CertFile=config/ssl/certs/device.crt +KeyFile=config/ssl/private/device.key Index: CloudConnect/config/LeahiCanRouting.ini =================================================================== diff -u --- CloudConnect/config/LeahiCanRouting.ini (revision 0) +++ CloudConnect/config/LeahiCanRouting.ini (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -0,0 +1,1429 @@ +; Group format: +; [ ] +; msg_id = +; action = (default: Drop) +; topic = (default: NormalPriority) + +[0x0000] +msg_id = MSG_ID_UNUSED + +[0x0080] +msg_id = MSG_ID_TESTER_LOGIN_REQUEST + +[0x00A0] +msg_id = MSG_ID_DD_TESTER_LOGIN_REQUEST + +[0x00B0] +msg_id = MSG_ID_FP_TESTER_LOGIN_REQUEST + +[0x0100] +msg_id = MSG_ID_ALARM_STATUS_DATA + +[0x0180] +msg_id = MSG_ID_TD_SOFTWARE_RESET_REQUEST + +[0x01A0] +msg_id = MSG_ID_DD_SOFTWARE_RESET_REQUEST + +[0x01B0] +msg_id = MSG_ID_FP_SOFTWARE_RESET_REQUEST + +[0x0200] +msg_id = MSG_ID_ALARM_TRIGGERED + +[0x0280] +msg_id = MSG_ID_TD_SEND_TEST_CONFIGURATION + +[0x02A0] +msg_id = MSG_ID_DD_SEND_TEST_CONFIGURATION + +[0x02B0] +msg_id = MSG_ID_FP_SEND_TEST_CONFIGURATION + +[0x0300] +msg_id = MSG_ID_ALARM_CLEARED + +[0x0380] +msg_id = MSG_ID_TD_BUBBLE_OVERRIDE_REQUEST + +[0x03A0] +msg_id = MSG_ID_DD_VALVE_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x03B0] +msg_id = MSG_ID_FP_VALVE_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x0400] +msg_id = MSG_ID_ALARM_CONDITION_CLEARED + +[0x0480] +msg_id = MSG_ID_TD_VOLTAGE_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x04A0] +msg_id = MSG_ID_DD_VALVE_STATE_OVERRIDE_REQUEST + +[0x04B0] +msg_id = MSG_ID_FP_VALVE_CMD_STATE_OVERRIDE_REQUEST + +[0x0500] +msg_id = MSG_ID_USER_ALARM_SILENCE_REQUEST + +[0x0580] +msg_id = MSG_ID_TD_VOLTAGE_OVERRIDE_REQUEST + +[0x05A0] +msg_id = MSG_ID_DD_VALVE_SENSED_STATE_OVERRIDE_REQUEST + +[0x05B0] +msg_id = MSG_ID_FP_VALVE_SENSED_STATE_OVERRIDE_REQUEST + +[0x0600] +msg_id = MSG_ID_UI_ALARM_USER_ACTION_REQUEST + +[0x0680] +msg_id = MSG_ID_TD_BUBBLE_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x06A0] +msg_id = MSG_ID_DD_PRESSURE_SENSOR_READINGS_OVERRIDE_REQUEST + +[0x06B0] +msg_id = MSG_ID_FP_FLUID_PUMP_SET_PWM_REQUEST + +[0x0700] +msg_id = MSG_ID_TD_ALARM_INFORMATION_DATA + +[0x0780] +msg_id = MSG_ID_TD_PRESSURE_OVERRIDE_REQUEST + +[0x07A0] +msg_id = MSG_ID_DD_PRESSURE_SENSOR_TEMPERATURE_OVERRIDE_REQUEST + +[0x07B0] +msg_id = MSG_ID_FP_FLUID_PUMP_READ_PWM_OVERRIDE_REQUEST + +[0x0800] +msg_id = MSG_ID_DD_ALARM_INFO_DATA + +[0x0880] +msg_id = MSG_ID_TD_AIR_PUMP_SET_STATE_REQUEST + +[0x08A0] +msg_id = MSG_ID_DD_PRESSURE_SENSOR_READ_COUNTER_OVERRIDE_REQUEST + +[0x08B0] +msg_id = MSG_ID_FP_FLUID_PUMP_SPEED_OVERRIDE_REQUEST + +[0x0900] +msg_id = MSG_ID_UI_ACTIVE_ALARMS_LIST_REQUEST + +[0x0980] +msg_id = MSG_ID_TD_AIR_PUMP_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x09A0] +msg_id = MSG_ID_DD_PRESSURE_SENSOR_ERROR_COUNTER_OVERRIDE_REQUEST + +[0x09B0] +msg_id = MSG_ID_FP_RO_PUMP_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x0A00] +msg_id = MSG_ID_TD_ACTIVE_ALARMS_LIST_REQUEST_RESPONSE + +[0x0A80] +msg_id = MSG_ID_TD_SWITCHES_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x0AA0] +msg_id = MSG_ID_DD_PRESSURE_SENSOR_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x0AB0] +msg_id = MSG_ID_FP_PRESSURE_OVERRIDE_REQUEST + +[0x0B00] +msg_id = MSG_ID_UI_SET_ALARM_AUDIO_VOLUME_LEVEL_CMD_REQUEST + +[0x0B80] +msg_id = MSG_ID_TD_SWITCH_STATE_OVERRIDE_REQUEST + +[0x0BA0] +msg_id = MSG_ID_DD_PRESSURE_SENSOR_FILTER_READINGS_OVERRIDE_REQUEST + +[0x0BB0] +msg_id = MSG_ID_FP_PRESSURE_TEMP_OVERRIDE_REQUEST + +[0x0C00] +msg_id = MSG_ID_TD_ALARM_AUDIO_VOLUME_SET_RESPONSE + +[0x0C80] +msg_id = MSG_ID_TD_OFF_BUTTON_OVERRIDE_REQUEST + +[0x0CA0] +msg_id = MSG_ID_DD_PRESSURE_SENSOR_FILTER_TEMPERATURE_OVERRIDE_REQUEST + +[0x0CB0] +msg_id = MSG_ID_FP_PRESSURE_SENSOR_FILTER_READINGS_OVERRIDE_REQUEST + +[0x0D00] +msg_id = MSG_ID_FW_VERSIONS_REQUEST + +[0x0D80] +msg_id = MSG_ID_TD_STOP_BUTTON_OVERRIDE_REQUEST + +[0x0DA0] +msg_id = MSG_ID_DD_CONDUCTIVITY_SENSOR_READINGS_OVERRIDE_REQUEST + +[0x0DB0] +msg_id = MSG_ID_FP_PRESSURE_SENSOR_FILTER_TEMPERATURE_OVERRIDE_REQUEST + +[0x0E00] +msg_id = MSG_ID_TD_VERSION_RESPONSE + +[0x0E80] +msg_id = MSG_ID_TD_ALARM_LAMP_PATTERN_OVERRIDE_REQUEST + +[0x0EA0] +msg_id = MSG_ID_DD_CONDUCTIVITY_SENSOR_TEMPERATURE_OVERRIDE_REQUEST + +[0x0EB0] +msg_id = MSG_ID_FP_PRESSURE_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x0F00] +msg_id = MSG_ID_DD_VERSION_RESPONSE + +[0x0F80] +msg_id = MSG_ID_TD_ALARM_AUDIO_LEVEL_OVERRIDE_REQUEST + +[0x0FA0] +msg_id = MSG_ID_DD_CONDUCTIVITY_SENSOR_CONDUCTIVITY_READ_COUNTER_OVERRIDE_REQUEST + +[0x0FB0] +msg_id = MSG_ID_FP_LEVEL_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x1000] +msg_id = MSG_ID_UI_CHECK_IN + +[0x1080] +msg_id = MSG_ID_TD_ALARM_AUDIO_CURRENT_HG_OVERRIDE_REQUEST + +[0x10A0] +msg_id = MSG_ID_DD_CONDUCTIVITY_SENSOR_CONDUCTIVITY_ERROR_COUNTER_OVERRIDE_REQUEST + +[0x10B0] +msg_id = MSG_ID_FP_FLOATER_LEVEL_OVERRIDE_REQUEST + +[0x1100] +msg_id = MSG_ID_TD_BLOOD_PUMP_DATA + +[0x1180] +msg_id = MSG_ID_TD_ALARM_AUDIO_CURRENT_LG_OVERRIDE_REQUEST + +[0x11A0] +msg_id = MSG_ID_DD_CONDUCTIVITY_SENSOR_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x11B0] +msg_id = MSG_ID_FP_FLOWS_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x1200] +msg_id = MSG_ID_TD_OP_MODE_DATA + +[0x1280] +msg_id = MSG_ID_TD_BACKUP_ALARM_AUDIO_CURRENT_OVERRIDE_REQUEST + +[0x12A0] +msg_id = MSG_ID_DD_CONCENTRATE_PUMP_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x12B0] +msg_id = MSG_ID_FP_FLOW_RATE_OVERRIDE_REQUEST + +[0x1300] +msg_id = MSG_ID_DD_OP_MODE_DATA + +[0x1380] +msg_id = MSG_ID_TD_PRESSURE_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x13A0] +msg_id = MSG_ID_DD_CONCENTRATE_PUMP_TARGET_SPEED_OVERRIDE_REQUEST + +[0x13B0] +msg_id = MSG_ID_FP_FLOW_TEMP_OVERRIDE_REQUEST + +[0x1400] +msg_id = MSG_ID_DD_COMMAND_RESPONSE + +[0x1480] +msg_id = MSG_ID_TD_AIR_TRAP_LEVEL_OVERRIDE_REQUEST + +[0x14A0] +msg_id = MSG_ID_DD_CONCENTRATE_PUMP_MEASURED_SPEED_OVERRIDE_REQUEST + +[0x14B0] +msg_id = MSG_ID_FP_CONDUCTIVITY_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x1500] +msg_id = MSG_ID_TD_UI_VERSION_INFO_REQUEST + +[0x1580] +msg_id = MSG_ID_TD_AIR_TRAP_LEVEL_RAW_OVERRIDE_REQUEST + +[0x15A0] +msg_id = MSG_ID_DD_CONCENTRATE_PUMP_PARKED_OVERRIDE_REQUEST + +[0x15B0] +msg_id = MSG_ID_FP_CONDUCTIVITY_SENSOR_READINGS_OVERRIDE_REQUEST + +[0x1600] +msg_id = MSG_ID_UI_VERSION_INFO_RESPONSE + +[0x1680] +msg_id = MSG_ID_TD_AIR_TRAP_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x16A0] +msg_id = MSG_ID_DD_CONCENTRATE_PUMP_PARK_FAULT_OVERRIDE_REQUEST + +[0x16B0] +msg_id = MSG_ID_FP_CONDUCTIVITY_SENSOR_TEMPERATURE_OVERRIDE_REQUEST + +[0x1700] +msg_id = MSG_ID_TD_EVENT + +[0x1780] +msg_id = MSG_ID_TD_3_WAY_VALVE_SET_STATE_REQUEST + +[0x17A0] +msg_id = MSG_ID_DD_CONCENTRATE_PUMP_PARK_REQUEST_OVERRIDE_REQUEST + +[0x17B0] +msg_id = MSG_ID_FP_CONDUCTIVITY_SENSOR_CONDUCTIVITY_READ_COUNT_OVERRIDE_REQUEST + +[0x1800] +msg_id = MSG_ID_DD_EVENT + +[0x1880] +msg_id = MSG_ID_TD_ROTARY_PINCH_VALVE_SET_POS_REQUEST + +[0x18A0] +msg_id = MSG_ID_DD_TEMPERATURE_SENSOR_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x18B0] +msg_id = MSG_ID_FP_CONDUCTIVITY_SENSOR_CONDUCTIVITY_ERROR_COUNT_OVERRIDE_REQUEST + +[0x1900] +msg_id = MSG_ID_TD_DD_ALARMS_REQUEST + +[0x1980] +msg_id = MSG_ID_TD_ROTARY_PINCH_VALVE_STATUS_OVERRIDE_REQUEST + +[0x19A0] +msg_id = MSG_ID_DD_TEMPERATURE_SENSOR_MEASURED_TEMPERATURE_OVERRIDE_REQUEST + +[0x19B0] +msg_id = MSG_ID_FP_TEMPERATURE_OVERRIDE_REQUEST + +[0x1A00] +msg_id = MSG_ID_UI_TD_RESET_IN_SERVICE_MODE_REQUEST + +[0x1A80] +msg_id = MSG_ID_TD_ROTARY_PINCH_VALVE_POSITION_OVERRIDE_REQUEST + +[0x1AA0] +msg_id = MSG_ID_DD_TEMPERATURE_SENSOR_READ_COUNTER_OVERRIDE_REQUEST + +[0x1AB0] +msg_id = MSG_ID_FP_FILTERED_FLOW_RATE_OVERRIDE_REQUEST + +[0x1B00] +msg_id = MSG_ID_DD_VALVES_STATES_DATA + +[0x1B80] +msg_id = MSG_ID_TD_VALVES_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x1BA0] +msg_id = MSG_ID_DD_TEMPERATURE_SENSOR_FILTERED_TEMP_OVERRIDE_REQUEST + +[0x1BB0] +msg_id = MSG_ID_FP_FILTERED_FLOW_TEMP_OVERRIDE_REQUEST + +[0x1C00] +msg_id = MSG_ID_DD_PRESSURES_DATA + +[0x1C80] +msg_id = MSG_ID_TD_ALARM_STATUS_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x1CA0] +msg_id = MSG_ID_DD_SET_OPERATION_SUB_MODE_OVERRIDE_REQUEST + +[0x1CB0] +msg_id = MSG_ID_FP_PRE_GEN_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x1D00] +msg_id = MSG_ID_TD_VOLTAGES_DATA + +[0x1D80] +msg_id = MSG_ID_TD_ALARM_INFO_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x1DA0] +msg_id = MSG_ID_DD_DIALYSATE_PUMPS_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x1DB0] +msg_id = MSG_ID_FP_SET_OPERATION_MODE_REQUEST + +[0x1E00] +msg_id = MSG_ID_TD_BUBBLES_DATA + +[0x1E80] +msg_id = MSG_ID_TD_ALARM_START_TIME_OVERRIDE_REQUEST + +[0x1EA0] +msg_id = MSG_ID_DD_DIALYSATE_PUMPS_TARGET_SPEED_OVERRIDE_REQUEST + +[0x1EB0] +msg_id = MSG_ID_FP_OPERATION_MODE_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x1F00] +msg_id = MSG_ID_DD_CONDUCTIVITY_DATA + +[0x1F80] +msg_id = MSG_ID_TD_ALARM_CLEAR_ALL_ALARMS_REQUEST + +[0x1FA0] +msg_id = MSG_ID_DD_DIALYSATE_PUMPS_MEASURED_SPEED_OVERRIDE_REQUEST + +[0x1FB0] +msg_id = MSG_ID_FP_TEMPERATURE_SENSOR_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x2000] +msg_id = MSG_ID_TD_AIR_PUMP_DATA + +[0x2080] +msg_id = MSG_ID_TD_WATCHDOG_OVERRIDE_REQUEST + +[0x20A0] +msg_id = MSG_ID_DD_DIALYSATE_PUMPS_TARGET_PRESSURE_OVERRIDE_REQUEST + +[0x20B0] +msg_id = MSG_ID_FP_RO_PUMP_TARGET_PRESSURE_OVERRIDE_REQUEST + +[0x2100] +msg_id = MSG_ID_TD_SWITCHES_DATA + +[0x2180] +msg_id = MSG_ID_TD_ALARM_STATE_OVERRIDE_REQUEST + +[0x21A0] +msg_id = MSG_ID_DD_DIALYSATE_PUMPS_MEASURED_CURRENT_OVERRIDE_REQUEST + +[0x21B0] +msg_id = MSG_ID_FP_RO_PUMP_TARGET_FLOW_OVERRIDE_REQUEST + +[0x2200] +msg_id = MSG_ID_POWER_OFF_WARNING + +[0x2280] +msg_id = MSG_ID_TD_SAFETY_SHUTDOWN_OVERRIDE_REQUEST + +[0x22A0] +msg_id = MSG_ID_DD_DIALYSATE_PUMPS_MEASURED_DIRECTION_OVERRIDE_REQUEST + +[0x22B0] +msg_id = MSG_ID_FP_RO_PUMP_TARGET_PWM_OVERRIDE_REQUEST + +[0x2300] +msg_id = MSG_ID_OFF_BUTTON_PRESS_REQUEST + +[0x2380] +msg_id = MSG_ID_TD_PINCH_VALVE_SET_POSITION_REQUEST + +[0x23A0] +msg_id = MSG_ID_DD_HEATERS_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x23B0] +msg_id = MSG_ID_FP_BOOST_PUMP_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x2400] +msg_id = MSG_ID_TD_PRESSURE_DATA + +[0x2480] +msg_id = MSG_ID_TD_PINCH_VALVE_HOME_REQUEST + +[0x24A0] +msg_id = MSG_ID_DD_HEATERS_DUTY_CYCLE_OVERRIDE_REQUEST + +[0x24B0] +msg_id = MSG_ID_FP_BOOST_PUMP_TARGET_PRESSURE_OVERRIDE_REQUEST + +[0x2500] +msg_id = MSG_ID_DD_CONCENTRATE_PUMP_DATA + +[0x2580] +msg_id = MSG_ID_TD_BLOOD_PUMP_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x25A0] +msg_id = MSG_ID_DD_LEVELS_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x25B0] +msg_id = MSG_ID_FP_BOOST_PUMP_TARGET_FLOW_OVERRIDE_REQUEST + +[0x2600] +msg_id = MSG_ID_DD_TEMPERATURE_DATA + +[0x2680] +msg_id = MSG_ID_TD_BLOOD_PUMP_SET_FLOW_RATE_REQUEST + +[0x26A0] +msg_id = MSG_ID_DD_LEVELS_STATUS_OVERRIDE_REQUEST + +[0x26B0] +msg_id = MSG_ID_FP_BOOST_PUMP_TARGET_PWM_OVERRIDE_REQUEST + +[0x2700] +msg_id = MSG_ID_DIALYSATE_PUMPS_DATA + +[0x2780] +msg_id = MSG_ID_TD_BLOOD_PUMP_SET_SPEED_REQUEST + +[0x27B0] +msg_id = MSG_ID_FP_BOOST_PUMP_STOP_REQUEST + +[0x2800] +msg_id = MSG_ID_DD_HEATERS_DATA + +[0x2880] +msg_id = MSG_ID_TD_BLOOD_PUMP_MEASURED_FLOW_RATE_OVERRIDE_REQUEST + +[0x28A0] +msg_id = MSG_ID_DD_OP_MODE_STATUS_OVERRIDE_REQUEST + +[0x28B0] +msg_id = MSG_ID_FP_RO_PUMP_STOP_REQUEST + +[0x2900] +msg_id = MSG_ID_DD_LEVEL_DATA + +[0x2980] +msg_id = MSG_ID_TD_BLOOD_PUMP_MEASURED_MOTOR_SPEED_OVERRIDE_REQUEST + +[0x29A0] +msg_id = MSG_ID_DD_SET_OPERATION_MODE_OVERRIDE_REQUEST + +[0x29B0] +msg_id = MSG_ID_FP_SAFETY_SHUTDOWN_OVERRIDE_REQUEST + +[0x2A00] +msg_id = MSG_ID_TD_AIR_TRAP_DATA + +[0x2A80] +msg_id = MSG_ID_TD_BLOOD_PUMP_MEASURED_ROTOR_SPEED_OVERRIDE_REQUEST + +[0x2AA0] +msg_id = MSG_ID_DD_UF_DATA_PUBLISH_OVERRIDE_REQUEST + +[0x2AB0] +msg_id = MSG_ID_FP_PERMEATE_TANK_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x2B00] +msg_id = MSG_ID_TD_VALVES_DATA + +[0x2B80] +msg_id = MSG_ID_TD_BLOOD_PUMP_ROTOR_COUNT_OVERRIDE_REQUEST + +[0x2BA0] +msg_id = MSG_ID_DD_DIALYSATE_PUMPS_START_STOP_OVERRIDE_REQUEST + +[0x2BB0] +msg_id = MSG_ID_FP_ALARM_STATE_OVERRIDE_REQUEST + +[0x2C00] +msg_id = MSG_ID_FP_EVENT + +[0x2C80] +msg_id = MSG_ID_TD_TMP_PRESSURE_OVERRIDE_REQUEST + +[0x2CA0] +msg_id = MSG_ID_DD_GEND_MODE_DATA_PUBLISH_OVERRIDE_REQUEST + +[0x2CB0] +msg_id = MSG_ID_FP_ALARM_CLEAR_ALL_ALARMS_REQUEST + +[0x2D00] +msg_id = MSG_ID_FP_ALARM_INFO_DATA + +[0x2D80] +msg_id = MSG_ID_TD_REQ_CURRENT_TREATMENT_PARAMETERS + +[0x2DA0] +msg_id = MSG_ID_DD_CONCENTRATE_PUMPS_START_STOP_OVERRIDE_REQUEST + +[0x2DB0] +msg_id = MSG_ID_FP_SET_TEST_CONFIGURATION + +[0x2E00] +msg_id = MSG_ID_DD_BAL_CHAMBER_DATA + +[0x2E80] +msg_id = MSG_ID_TD_RSP_CURRENT_TREATMENT_PARAMETERS + +[0x2EA0] +msg_id = MSG_ID_DD_HEATERS_START_STOP_OVERRIDE_REQUEST + +[0x2EB0] +msg_id = MSG_ID_FP_GET_TEST_CONFIGURATION + +[0x2F00] +msg_id = MSG_ID_DD_GEN_DIALYSATE_MODE_DATA + +[0x2F80] +msg_id = MSG_ID_TD_SET_TREATMENT_PARAMETER + +[0x2FA0] +msg_id = MSG_ID_DD_VALVES_OPEN_CLOSE_STATE_OVERRIDE_REQUEST + +[0x2FB0] +msg_id = MSG_ID_FP_RESET_ALL_TEST_CONFIGURATIONS + +[0x3000] +msg_id = MSG_ID_DD_GEN_DIALYSATE_REQUEST_DATA + +[0x3080] +msg_id = MSG_ID_TD_OP_MODE_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x30B0] +msg_id = MSG_ID_FP_INLET_PRES_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x3100] +msg_id = MSG_ID_FP_VALVES_STATES_DATA + +[0x3180] +msg_id = MSG_ID_TD_OP_MODE_OVERRIDE_REQUEST + +[0x31A0] +msg_id = MSG_ID_DD_BAL_CHAMBER_DATA_PUBLISH_OVERRIDE_REQUEST + +[0x31B0] +msg_id = MSG_ID_FP_INLET_PRES_CHECK_TIME_OVERRIDE_REQUEST + +[0x3200] +msg_id = MSG_ID_FP_RO_PUMP_DATA + +[0x3280] +msg_id = MSG_ID_TD_EJECTOR_MOTOR_SET_SPEED_REQUEST + +[0x32A0] +msg_id = MSG_ID_DD_BAL_CHAMBER_SWITCH_FREQ_OVERRIDE_REQUEST + +[0x32B0] +msg_id = MSG_ID_FP_FILTERED_COND_SENSOR_READINGS_OVERRIDE_REQUEST + +[0x3300] +msg_id = MSG_ID_FP_OP_MODE_DATA + +[0x3380] +msg_id = MSG_ID_TD_EJECTOR_COMMAND + +[0x33A0] +msg_id = MSG_ID_DD_DIAL_DELIVERY_IN_PROGRESS_OVERRIDE_REQUEST + +[0x33B0] +msg_id = MSG_ID_FP_FILTERED_COND_SENSOR_TEMPERATURE_OVERRIDE_REQUEST + +[0x3400] +msg_id = MSG_ID_FP_PRESSURES_DATA + +[0x3480] +msg_id = MSG_ID_TD_EJECTOR_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x34A0] +msg_id = MSG_ID_DD_DIAL_DELIVERY_GOOD_TO_DELIVER_OVERRIDE_REQUEST + +[0x34B0] +msg_id = MSG_ID_FP_SET_START_STOP_OVERRIDE_REQUEST + +[0x3500] +msg_id = MSG_ID_FP_LEVEL_DATA + +[0x3580] +msg_id = MSG_ID_TD_SET_AIR_TRAP_CONTROL + +[0x35A0] +msg_id = MSG_ID_DD_HEATERS_TARGET_TEMPERATURE_OVERRIDE_REQUEST + +[0x35B0] +msg_id = MSG_ID_FP_RO_REJECTION_RATIO_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x3600] +msg_id = MSG_ID_FP_FLOW_DATA + +[0x3680] +msg_id = MSG_ID_TD_HOME_BLOOD_PUMP + +[0x36A0] +msg_id = MSG_ID_DD_BC_VALVE_STATES_OVERRIDE_REQUEST + +[0x36B0] +msg_id = MSG_ID_FP_RO_FILTERED_REJECTION_RATIO_OVERRIDE_REQUEST + +[0x3700] +msg_id = MSG_ID_FP_CONDUCTIVITY_DATA + +[0x3780] +msg_id = MSG_ID_TD_BLOOD_FLOW_STROKE_VOLUME_OVERRIDE_REQUEST + +[0x37A0] +msg_id = MSG_ID_DD_BC_SWITCH_ONLY_START_STOP_OVERRIDE_REQUEST + +[0x37B0] +msg_id = MSG_ID_FP_RO_GET_CALCULATED_DUTY_CYCLE_REQUEST + +[0x3800] +msg_id = MSG_ID_AVAILABLE_6 + +[0x3880] +msg_id = MSG_ID_TD_BLOOD_FLOW_WEAR_A_TERM_OVERRIDE_REQUEST + +[0x38A0] +msg_id = MSG_ID_DD_HYD_CHAMBER_TARGET_TEMP_OVERRIDE_REQUEST + +[0x38B0] +msg_id = MSG_ID_FP_RO_CALCULATED_DUTY_CYCLE_RESPONSE + +[0x3900] +msg_id = MSG_ID_FP_TEMPERATURE_DATA + +[0x3980] +msg_id = MSG_ID_TD_BLOOD_FLOW_WEAR_B_TERM_OVERRIDE_REQUEST + +[0x39A0] +msg_id = MSG_ID_DD_ACID_DOSING_VOLUME_OVERRIDE_REQUEST + +[0x39B0] +msg_id = MSG_ID_FP_FLUSH_FILTER_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x3A00] +msg_id = MSG_ID_FP_HEATER_DATA + +[0x3A80] +msg_id = MSG_ID_TD_SET_TEST_CONFIGURATION + +[0x3AA0] +msg_id = MSG_ID_DD_BICARB_DOSING_VOLUME_OVERRIDE_REQUEST + +[0x3AB0] +msg_id = MSG_ID_FP_FLUSH_FILTER_TIMER_OVERRIDE_REQUEST + +[0x3B00] +msg_id = MSG_ID_TD_TREATMENT_TIME_DATA + +[0x3B80] +msg_id = MSG_ID_TD_GET_TEST_CONFIGURATION + +[0x3BA0] +msg_id = MSG_ID_DD_GEND_EXEC_STATE_OVERRIDE_REQUEST + +[0x3BB0] +msg_id = MSG_ID_FP_FLUSH_PERMEATE_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x3C00] +msg_id = MSG_ID_TD_TREATMENT_STATE_DATA + +[0x3C80] +msg_id = MSG_ID_TD_RESET_ALL_TEST_CONFIGURATIONS + +[0x3CA0] +msg_id = MSG_ID_DD_HEATERS_PWM_PERIOD_OVERIDE_REQUEST + +[0x3CB0] +msg_id = MSG_ID_FP_FLUSH_PERMEATE_TIMER_OVERRIDE_REQUEST + +[0x3D00] +msg_id = MSG_ID_TD_FLUID_BOLUS_DATA + +[0x3D80] +msg_id = MSG_ID_TD_AIR_PUMP_POWER_RAISE_OVERRIDE_REQUEST + +[0x3DA0] +msg_id = MSG_ID_DD_PRE_GEND_MODE_DATA_PUBLISH_OVERRIDE_REQUEST + +[0x3DB0] +msg_id = MSG_ID_FP_FLUSH_PERMEATE_ALARM_TIMER_OVERRIDE_REQUEST + +[0x3E00] +msg_id = MSG_ID_TD_ULTRAFILTRATION_DATA + +[0x3E80] +msg_id = MSG_ID_TD_AIR_PUMP_POWER_LOWER_OVERRIDE_REQUEST + +[0x3EA0] +msg_id = MSG_ID_DD_POST_GEND_MODE_DATA_PUBLISH_OVERRIDE_REQUEST + +[0x3EB0] +msg_id = MSG_ID_FP_FLUSH_CONCENTRATE_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x3F00] +msg_id = MSG_ID_UI_TREATMENT_PARAMS_TO_VALIDATE + +[0x3F80] +msg_id = MSG_ID_TD_HARD_STOP_BLOOD_PUMP + +[0x3FA0] +msg_id = MSG_ID_DD_SEND_BLOOD_LEAK_EMB_MODE_RESPONSE + +[0x3FB0] +msg_id = MSG_ID_FP_FLUSH_CONCENTRATE_TIMER_OVERRIDE_REQUEST + +[0x4000] +msg_id = MSG_ID_TD_RESP_TREATMENT_PARAMS_TO_VALIDATE + +[0x4080] +msg_id = MSG_ID_TD_BARO_MFG_CRC_OVERRIDE + +[0x40A0] +msg_id = MSG_ID_DD_SPENT_CHAMB_FILL_DATA_PUBLISH_OVERRIDE_REQUEST + +[0x40B0] +msg_id = MSG_ID_FP_DEF_FLUSH_FILTER_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x4100] +msg_id = MSG_ID_UI_TREATMENT_UF_VOLUME_VALIDATE_REQUEST + +[0x4180] +msg_id = MSG_ID_TD_BARO_PRESSURE_OVERRIDE + +[0x41A0] +msg_id = MSG_ID_DD_AVAILABLE_TO_USE_4 + +[0x41B0] +msg_id = MSG_ID_FP_DEF_FLUSH_FILTER_TIMER_OVERRIDE_REQUEST + +[0x4200] +msg_id = MSG_ID_TD_TREATMENT_UF_VOLUME_VALIDATE_RESPONSE + +[0x4280] +msg_id = MSG_ID_TD_TEMPERATURE_OVERRIDE + +[0x42A0] +msg_id = MSG_ID_DD_SAFETY_SHUTDOWN_OVERRIDE_REQUEST + +[0x42B0] +msg_id = MSG_ID_FP_DEF_PRE_GEN_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x4300] +msg_id = MSG_ID_TD_TREATMENT_PARAM_RANGES + +[0x4380] +msg_id = MSG_ID_TD_TEMPERATURE_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x43A0] +msg_id = MSG_ID_DD_SET_TEST_CONFIGURATION + +[0x43B0] +msg_id = MSG_ID_FP_DEF_GEN_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x4400] +msg_id = MSG_ID_TD_VALIDATED_TREATMENT_PARAMS + +[0x4480] +msg_id = MSG_ID_TD_EJECTOR_OPT_SENSOR_OVERRIDE_REQUEST + +[0x44A0] +msg_id = MSG_ID_DD_GET_TEST_CONFIGURATION + +[0x44B0] +msg_id = MSG_ID_FP_DEF_STATUS_REQUEST + +[0x4500] +msg_id = MSG_ID_UI_INITIATE_TREATMENT_WORKFLOW + +[0x4580] +msg_id = MSG_ID_TD_BLOOD_PRIME_VOLUME_OVERRIDE + +[0x45A0] +msg_id = MSG_ID_DD_RESET_ALL_TEST_CONFIGURATIONS + +[0x45B0] +msg_id = MSG_ID_FP_DEF_STATUS_RESPONSE + +[0x4600] +msg_id = MSG_ID_TD_RESP_INITIATE_TREATMENT_WORKFLOW + +[0x4680] +msg_id = MSG_ID_TD_BLOOD_PRIME_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x46A0] +msg_id = MSG_ID_AVAILABLE_1 + +[0x46B0] +msg_id = MSG_ID_FP_SET_OPERATION_SUB_MODE_REQUEST + +[0x4700] +msg_id = MSG_ID_UI_UF_PAUSE_RESUME_REQUEST + +[0x4780] +msg_id = MSG_ID_TD_ENABLE_VENOUS_BUBBLE_ALARM + +[0x47A0] +msg_id = MSG_ID_DD_BLOOD_LEAK_DATA_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x47B0] +msg_id = MSG_ID_FP_CONDUCTIVITY_SENSOR_RESISTANCE_OVERRIDE_REQUEST + +[0x4800] +msg_id = MSG_ID_TD_UF_PAUSE_RESUME_RESPONSE + +[0x4880] +msg_id = MSG_ID_TD_SYRINGE_PUMP_OPERATION_REQUEST + +[0x48A0] +msg_id = MSG_ID_DD_BLOOD_LEAK_STATUS_OVERRIDE_REQUEST + +[0x48B0] +msg_id = MSG_ID_FP_SET_RECOVERY_VALVES_REQUEST + +[0x4900] +msg_id = MSG_ID_FP_GEN_WATER_MODE_DATA + +[0x4980] +msg_id = MSG_ID_HD_SYRINGE_PUMP_PUBLISH_INTERVAL_OVERRIDE + +[0x49A0] +msg_id = MSG_ID_DD_BLOOD_LEAK_SET_TO_EMBEDDED_MODE_REQUEST + +[0x49B0] +msg_id = MSG_ID_FP_BOOST_PUMP_INSTALL_STATUS_REQUEST + +[0x4A00] +msg_id = MSG_ID_DD_PRE_GEN_DIALYSATE_STATE_DATA + +[0x4AA0] +msg_id = MSG_ID_DD_BLOOD_LEAK_SET_EMBEDDED_MODE_CMD_REQUEST + +[0x4AB0] +msg_id = MSG_ID_FP_BOOST_PUMP_INSTALL_STATUS_RESPONSE + +[0x4B00] +msg_id = MSG_ID_DD_POST_GEN_DIALYSATE_STATE_DATA + +[0x4BA0] +msg_id = MSG_ID_DD_BLOOD_LEAK_EMBEDDED_MODE_INFO_OVERRIDE_REQUEST + +[0x4C00] +msg_id = MSG_ID_DD_PRE_GEN_DIALYSATE_REQUEST_DATA + +[0x4CA0] +msg_id = MSG_ID_DD_BLOOD_LEAK_INTENSITY_MOVING_AVERAGE_OVERRIDE_REQUEST + +[0x4CB0] +msg_id = MSG_ID_FP_CONDUCTIVITY_SENSOR_VERSION_RESPONSE + +[0x4D00] +msg_id = MSG_ID_FP_PRE_GEN_WATER_MODE_DATA + +[0x4DA0] +msg_id = MSG_ID_DD_BLOOD_LEAK_ZEROING_INTERVAL_IN_MS_OVERRIDE_REQUEST + +[0x4E00] +msg_id = MSG_ID_TD_EJECTOR_DATA + +[0x4EA0] +msg_id = MSG_ID_DD_BLOOD_LEAK_ZERO_REQUEST + +[0x4EB0] +msg_id = MSG_ID_FP_CONDUCTIVITY_SENSOR_CAL_RESPONSE + +[0x4F00] +msg_id = MSG_ID_TD_TREATMENT_SET_POINTS + +[0x4FA0] +msg_id = MSG_ID_DD_FILTERED_COND_SENSOR_READINGS_OVERRIDE_REQUEST + +[0x5000] +msg_id = MSG_ID_FP_BOOST_PUMP_DATA + +[0x5080] +msg_id = MSG_ID_TD_SYRINGE_PUMP_RATE_OVERRIDE_REQUEST + +[0x50A0] +msg_id = MSG_ID_DD_FILTERED_COND_SENSOR_TEMPERATURE_OVERRIDE_REQUEST + +[0x5100] +msg_id = MSG_ID_TD_SERIAL_RESPONSE + +[0x5180] +msg_id = MSG_ID_TD_SYRINGE_PUMP_FORCE_OVERRIDE_REQUEST + +[0x51A0] +msg_id = MSG_ID_DD_VOLTAGE_DATA_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x5200] +msg_id = MSG_ID_DD_SERIAL_RESPONSE + +[0x5280] +msg_id = MSG_ID_TD_SYRINGE_PUMP_HOME_OVERRIDE_REQUEST + +[0x52A0] +msg_id = MSG_ID_DD_MONITORED_VOLTAGE_OVERRIDE_REQUEST + +[0x5300] +msg_id = MSG_ID_TD_TEMPERATURE_DATA + +[0x5380] +msg_id = MSG_ID_TD_SYRINGE_PUMP_POSITION_OVERRIDE_REQUEST + +[0x53A0] +msg_id = MSG_ID_DD_RINSE_PUMP_DATA_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x5400] +msg_id = MSG_ID_TD_BATTERY_DATA + +[0x5480] +msg_id = MSG_ID_TD_SYRINGE_PUMP_VOLUME_OVERRIDE_REQUEST + +[0x54A0] +msg_id = MSG_ID_DD_RINSE_PUMP_PWM_PERCENT_OVERRIDE_REQUEST + +[0x5500] +msg_id = MSG_ID_UI_PATIENT_DISCONNECT_CONFIRM_REQUEST + +[0x5580] +msg_id = MSG_ID_TD_SYRINGE_PUMP_STATUS_OVERRIDE_REQUEST + +[0x55A0] +msg_id = MSG_ID_DD_RINSE_PUMP_TURN_ON_OFF_REQUEST + +[0x5600] +msg_id = MSG_ID_TD_PATIENT_DISCONNECT_CONFIRM_RESPONSE + +[0x5680] +msg_id = MSG_ID_TD_SYRINGE_PUMP_ENCODER_STATUS_OVERRIDE_REQUEST + +[0x56A0] +msg_id = MSG_ID_DD_DRY_BICART_DATA_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x5700] +msg_id = MSG_ID_FP_CONCENTRATE_FLUSH_DATA + +[0x5780] +msg_id = MSG_ID_TD_SYRINGE_PUMP_ADC_DAC_STATUS_OVERRIDE_REQUEST + +[0x57A0] +msg_id = MSG_ID_DD_DRY_BICART_FILL_CYCLE_MAX_OVERRIDE_REQUEST + +[0x5800] +msg_id = MSG_ID_FP_GENP_DEF_DATA + +[0x5880] +msg_id = MSG_ID_TD_SYRINGE_PUMP_ADC_READ_COUNTER_OVERRIDE_REQUEST + +[0x58A0] +msg_id = MSG_ID_DD_DRY_BICART_FILL_REQUEST_OVERRIDE_REQUEST + +[0x5900] +msg_id = MSG_ID_FP_PRE_GEN_DEF_DATA + +[0x5980] +msg_id = MSG_ID_TD_HEPARIN_BOLUS_TARGET_RATE_OVERRIDE_REQUEST + +[0x59A0] +msg_id = MSG_ID_DD_BICARB_CHAMBER_FILL_REQUEST_OVERRIDE_REQUEST + +[0x5A00] +msg_id = MSG_ID_FP_VERSION_RESPONSE + +[0x5AA0] +msg_id = MSG_ID_DD_BICART_DRAIN_REQUEST_OVERRIDE_REQUEST + +[0x5B00] +msg_id = MSG_ID_TD_TREATMENT_PAUSED_TIMER_DATA + +[0x5BA0] +msg_id = MSG_ID_DD_BICART_CARTRIDGE_SELECT_OVERRIDE_REQUEST + +[0x5C00] +msg_id = MSG_ID_DD_UF_DATA + +[0x5CA0] +msg_id = MSG_ID_DD_SET_CONDUCTIVITY_MODEL_REQUEST + +[0x5D00] +msg_id = MSG_ID_FP_PERMEATE_TANK_DATA + +[0x5DA0] +msg_id = MSG_ID_DD_CONDUCTIVITY_SENSOR_RESISTANCE_OVERRIDE_REQUEST + +[0x5E00] +msg_id = MSG_ID_DD_SPENT_CHAMBER_FILL_DATA + +[0x5F00] +msg_id = MSG_ID_UI_FLUID_BOLUS_REQUEST + +[0x5FA0] +msg_id = MSG_ID_DD_CONDUCTIVITY_SENSOR_VERSION_RESPONSE + +[0x6000] +msg_id = MSG_ID_TD_FLUID_BOLUS_RESPONSE + +[0x6080] +msg_id = MSG_ID_TD_SYRINGE_PUMP_FORCE_SENSOR_CALIBRATION_REQUEST + +[0x60A0] +msg_id = MSG_ID_DD_BICARB_MIX_VOL_KP_GAIN_COEFF_OVERRIDE_REQUEST + +[0x6100] +msg_id = MSG_ID_DD_BLOOD_LEAK_DATA + +[0x6180] +msg_id = MSG_ID_TD_GET_ALARM_PROPERTIES_REQUEST + +[0x61A0] +msg_id = MSG_ID_DD_BICARB_MIX_VOL_KI_GAIN_COEFF_OVERRIDE_REQUEST + +[0x6200] +msg_id = MSG_ID_FP_INLET_PRESSURE_CHECK_DATA + +[0x6280] +msg_id = MSG_ID_TD_ALARM_PROPERTIES_RESPONSE + +[0x62A0] +msg_id = MSG_ID_DD_ACID_MIX_VOL_KP_GAIN_COEFF_OVERRIDE_REQUEST + +[0x6300] +msg_id = MSG_ID_UI_BLOOD_PRESSURE_REQUEST + +[0x63A0] +msg_id = MSG_ID_DD_ACID_MIX_VOL_KI_GAIN_COEFF_OVERRIDE_REQUEST + +[0x6400] +msg_id = MSG_ID_TD_BLOOD_PRESSURE_READING + +[0x64A0] +msg_id = MSG_ID_DD_ACID_MIX_VOL_OVERRIDE_REQUEST + +[0x6500] +msg_id = MSG_ID_TD_BLOOD_PRESSURE_DATA + +[0x65A0] +msg_id = MSG_ID_DD_BICARB_MIX_VOL_OVERRIDE_REQUEST + +[0x6600] +msg_id = MSG_ID_UI_ULTRAFILTRATION_CHANGE_CONFIRM_REQUEST + +[0x66A0] +msg_id = MSG_ID_DD_BICARB_TARGET_CONDUCTIVITY_OVERRIDE_REQUEST + +[0x6700] +msg_id = MSG_ID_TD_ULTRAFILTRATION_CHANGE_CONFIRM_RESPONSE + +[0x67A0] +msg_id = MSG_ID_DD_BICARB_DELTA_CONDUCTIVITY_OVERRIDE_REQUEST + +[0x6800] +msg_id = MSG_ID_DD_VOLTAGES_DATA + +[0x68A0] +msg_id = MSG_ID_DD_DIALYSATE_TARGET_CONDUCTIVITY_OVERRIDE_REQUEST + +[0x6900] +msg_id = MSG_ID_DD_RINSE_PUMP_DATA + +[0x69A0] +msg_id = MSG_ID_DD_DIALYSATE_DELTA_CONDUCTIVITY_OVERRIDE_REQUEST + +[0x6A00] +msg_id = MSG_ID_TD_TREATMENT_LOG_ALARM_EVENT + +[0x6AA0] +msg_id = MSG_ID_DD_BICART_UPPER_PRESSURE_OVERRIDE_REQUEST + +[0x6B00] +msg_id = MSG_ID_TD_TREATMENT_LOG_EVENT + +[0x6BA0] +msg_id = MSG_ID_DD_BICART_LOWER_PRESSURE_OVERRIDE_REQUEST + +[0x6C00] +msg_id = MSG_ID_TD_DATE_AND_TIME_REQUEST + +[0x6CA0] +msg_id = MSG_ID_DD_FLOATER_LEVEL_OVERRIDE_REQUEST + +[0x6D00] +msg_id = MSG_ID_TD_DATE_AND_TIME_RESPONSE + +[0x6DA0] +msg_id = MSG_ID_DD_SUBSTITUTION_PUMP_START_STOP_OVERRIDE_REQUEST + +[0x6E00] +msg_id = MSG_ID_DD_DATE_AND_TIME_REQUEST + +[0x6EA0] +msg_id = MSG_ID_DD_SUBSTITUTION_PUMP_BROADCAST_INTERVAL_OVERRIDE_REQUEST + +[0x6F00] +msg_id = MSG_ID_DD_DATE_AND_TIME_RESPONSE + +[0x6FA0] +msg_id = MSG_ID_DD_SUBSTITUTION_PUMP_TARGET_RATE_OVERRIDE_REQUEST + +[0x7000] +msg_id = MSG_ID_DD_DRY_BICART_DATA + +[0x7100] +msg_id = MSG_ID_FP_RO_REJECTION_RATIO_DATA + +[0x71A0] +msg_id = MSG_ID_DD_CONDUCTIVITY_SENSOR_CAL_RESPONSE + +[0x7200] +msg_id = MSG_ID_UI_PRESSURE_LIMITS_CHANGE_REQUEST + +[0x72A0] +msg_id = MSG_ID_DD_MIXING_CONTROL_DATA + +[0x7300] +msg_id = MSG_ID_TD_PRESSURE_LIMITS_CHANGE_RESPONSE + +[0x73A0] +msg_id = MSG_ID_DD_MIXING_CONTROL_DATA_PUBLISH_INTERVAL_OVERRIDE_REQUEST + +[0x7400] +msg_id = MSG_ID_UI_BOLUS_VOLUME_CHANGE_REQUEST + +[0x74A0] +msg_id = MSG_ID_DD_BICART_DEPRESSURISE_REQUEST_OVERRIDE_REQUEST + +[0x7500] +msg_id = MSG_ID_TD_BOLUS_VOLUME_CHANGE_RESPONSE + +[0x75A0] +msg_id = MSG_ID_DD_TREATMENT_PARAMS_OVERRIDE_REQUEST + +[0x7600] +msg_id = MSG_ID_UI_DURATION_VALIDATE_REQUEST + +[0x7700] +msg_id = MSG_ID_TD_DURATION_VALIDATE_RESPONSE + +[0x7800] +msg_id = MSG_ID_UI_DURATION_CONFIRM_REQUEST + +[0x7900] +msg_id = MSG_ID_TD_DURATION_CONFIRM_RESPONSE + +[0x7A00] +msg_id = MSG_ID_UI_TREATMENT_SET_POINTS_CHANGE_REQUEST + +[0x7B00] +msg_id = MSG_ID_TD_TREATMENT_SET_POINTS_CHANGE_RESPONSE + +[0x7C00] +msg_id = MSG_ID_UI_TREATMENT_SET_POINT_BLOOD_FLOW_CHANGE_REQUEST + +[0x7D00] +msg_id = MSG_ID_TD_TREATMENT_SET_POINT_BLOOD_FLOW_CHANGE_RESPONSE + +[0x7E00] +msg_id = MSG_ID_UI_TREATMENT_SET_POINT_DIALYSATE_FLOW_CHANGE_REQUEST + +[0x7F00] +msg_id = MSG_ID_TD_TREATMENT_SET_POINT_DIALYSATE_FLOW_CHANGE_RESPONSE + +[0x8000] +msg_id = MSG_ID_UI_TREATMENT_SET_POINT_DIALYSATE_TEMP_CHANGE_REQUEST + +[0x8100] +msg_id = MSG_ID_TD_TREATMENT_SET_POINT_DIALYSATE_TEMP_CHANGE_RESPONSE + +[0x8200] +msg_id = MSG_ID_TD_INSTITUTIONAL_RECORD_REQUEST + +[0x8300] +msg_id = MSG_ID_TD_INSTITUTIONAL_RECORD_RESPONSE + +[0x8400] +msg_id = MSG_ID_TD_ADJUST_INSTITUTIONAL_RECORD_REQUEST + +[0x8500] +msg_id = MSG_ID_TD_ADJUST_INSTITUTIONAL_RECORD_RESPONSE + +[0x8600] +msg_id = MSG_ID_TD_ADVANCED_INSTITUTIONAL_RECORD_REQUEST + +[0x8700] +msg_id = MSG_ID_TD_ADVANCED_INSTITUTIONAL_RECORD_RESPONSE + +[0x8800] +msg_id = MSG_ID_TD_ADVANCED_ADJUST_INSTITUTIONAL_RECORD_REQUEST + +[0x8900] +msg_id = MSG_ID_TD_ADVANCED_ADJUST_INSTITUTIONAL_RECORD_RESPONSE + +[0x8A00] +msg_id = MSG_ID_TD_HEPARIN_REQUEST + +[0x8B00] +msg_id = MSG_ID_TD_HEPARIN_RESPONSE + +[0x8C00] +msg_id = MSG_ID_TD_HEPARIN_DATA + +[0x8D00] +msg_id = MSG_ID_TD_END_TREATMENT_REQUEST + +[0x8E00] +msg_id = MSG_ID_TD_END_TREATMENT_RESPONSE + +[0x8F00] +msg_id = MSG_ID_TD_RINSEBACK_PROGRESS + +[0x9000] +msg_id = MSG_ID_UI_RINSEBACK_CMD_REQUEST + +[0x9100] +msg_id = MSG_ID_TD_RINSEBACK_CMD_RESPONSE + +[0x9200] +msg_id = MSG_ID_UI_ADJUST_DISPOSABLES_CONFIRM_REQUEST + +[0x9300] +msg_id = MSG_ID_TD_ADJUST_DISPOSABLES_CONFIRM_RESPONSE + +[0x9400] +msg_id = MSG_ID_UI_ADJUST_DISPOSABLES_REMOVAL_CONFIRM_REQUEST + +[0x9500] +msg_id = MSG_ID_TD_ADJUST_DISPOSABLES_REMOVAL_CONFIRM_RESPONSE + +[0x9600] +msg_id = MSG_ID_FP_FILTER_FLUSH_DEF_DATA + +[0x9700] +msg_id = MSG_ID_TD_BLOOD_PRIME_PROGRESS_DATA + +[0x9800] +msg_id = MSG_ID_UI_BLOOD_PRIME_CMD_REQUEST + +[0x9900] +msg_id = MSG_ID_TD_BLOOD_PRIME_CMD_RESPONSE + +[0x9A00] +msg_id = MSG_ID_TD_ISOLATED_UF_DATA + +[0x9B00] +msg_id = MSG_ID_UI_ISOLATED_UF_DURATION_CHANGE_REQUEST + +[0x9C00] +msg_id = MSG_ID_TD_ISOLATED_UF_DURATION_CHANGE_RESPONSE + +[0x9D00] +msg_id = MSG_ID_UI_ISOLATED_UF_VOLUME_GOAL_CHANGE_REQUEST + +[0x9E00] +msg_id = MSG_ID_TD_ISOLATED_UF_VOLUME_GOAL_CHANGE_RESPONSE + +[0x9F00] +msg_id = MSG_ID_UI_ISOLATED_UF_CONFIRM_REQUEST + +[0xA000] +msg_id = MSG_ID_TD_ISOLATED_UF_CONFIRM_RESPONSE + +[0xA100] +msg_id = MSG_ID_UI_ADJUST_START_TREATMENT_REQUEST + +[0xA200] +msg_id = MSG_ID_TD_ADJUST_START_TREATMENT_RESPONSE + +[0xA300] +msg_id = MSG_ID_UI_WATER_SAMPLE_RESULT_REQUEST + +[0xA400] +msg_id = MSG_ID_UI_PRESSURE_LIMIT_WIDEN_REQUEST + +[0xA500] +msg_id = MSG_ID_TD_PRESSURE_LIMIT_WIDEN_RESPONSE + +[0xA600] +msg_id = MSG_ID_UI_RECIRCULATE_REQUEST + +[0xA700] +msg_id = MSG_ID_TD_RECIRCULATE_RESPONSE + +[0xA800] +msg_id = MSG_ID_TD_RECIRCULATE_DATA + +[0xA900] +msg_id = MSG_ID_UI_ADJUST_TREATMENT_LOGS_REQUEST + +[0xAA00] +msg_id = MSG_ID_TD_ADJUST_TREATMENT_LOGS_RESPONSE + +[0xAB00] +msg_id = MSG_ID_TD_WATER_SAMPLE_RESULT_RESPONSE + +[0xAC00] +msg_id = MSG_ID_TD_WATER_SAMPLE_DATA + +[0xAD00] +msg_id = MSG_ID_TD_TREATMENT_LOG_AVERAGE_DATA + +[0xAE00] +msg_id = MSG_ID_TD_DRY_SELF_TEST_PROGRESS_DATA + +[0xAF00] +msg_id = MSG_ID_TD_TUBE_SET_AUTHENTICATION_REQUEST + +[0xB000] +msg_id = MSG_ID_TD_TUBE_SET_AUTHENTICATION_ACK_RESPONSE + +[0xB100] +msg_id = MSG_ID_TD_SYRINGE_PUMP_DATA + +[0xB200] +msg_id = MSG_ID_TD_HEPARIN_PAUSE_RESUME_RESPONSE + +[0xB300] +msg_id = MSG_ID_FFU_SIGNAL_TD_UPDATE_AVAILABLE + +[0xB400] +msg_id = MSG_ID_FFU_SIGNAL_DD_UPDATE_AVAILABLE + +[0xB500] +msg_id = MSG_ID_TD_UI_GENERIC_CONFIRMATION_REQUEST + +[0xB600] +msg_id = MSG_ID_UI_GENERIC_CONFIRMATION_RESULT_RESPONSE + +[0xB700] +msg_id = MSG_ID_AVAILABLE_B7 + +[0xB800] +msg_id = MSG_ID_UI_VITALS_ADJUSTMENT_REQUEST + +[0xB900] +msg_id = MSG_ID_TD_VITALS_ADJUSTMENT_RESPONSE + +[0xD600] +msg_id = MSG_ID_UI_SETUP_TUBING_SET_CONNECTIONS_CONFIRM_REQUEST + +[0xD700] +msg_id = MSG_ID_TD_SETUP_TUBING_SET_CONNECTIONS_CONFIRM_RESPONSE + +[0xDB00] +msg_id = MSG_ID_DD_SUBSTITUTION_PUMP_DATA + +[0xDC00] +msg_id = MSG_ID_DD_CONDUCTIVITY_SENSOR_RESISTANCE_DATA + +[0xDD00] +msg_id = MSG_ID_FP_FILTER_FLUSH_DATA + +[0xDE00] +msg_id = MSG_ID_FP_PERMEATE_FLUSH_DATA + +[0xF1FF] +msg_id = MSG_ID_TD_DEBUG_EVENT + +[0xF2FF] +msg_id = MSG_ID_DD_DEBUG_EVENT + +[0xF3FF] +msg_id = MSG_ID_FP_DEBUG_EVENT + +[0xFFFF] +msg_id = MSG_ID_ACK_MESSAGE_THAT_REQUIRES_ACK +action = Drop + Fisheye: Tag aaebfee335c74b0250864a6dce0555f866adadea refers to a dead (removed) revision in file `CloudConnect/config/LeahiMsgHandling.ini'. Fisheye: No comparison available. Pass `N' to diff? Index: CloudConnect/main.cpp =================================================================== diff -u -r59b4c22f45a1d098064a886452769204e90cfb4b -raaebfee335c74b0250864a6dce0555f866adadea --- CloudConnect/main.cpp (.../main.cpp) (revision 59b4c22f45a1d098064a886452769204e90cfb4b) +++ CloudConnect/main.cpp (.../main.cpp) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -58,18 +58,16 @@ QCommandLineOption configOption( {"c", "config"}, "Path to the configuration INI file.", "config", - QDir(app.applicationDirPath()).filePath("/home/leahi/Public/leahi-realtime-cdt/CloudConnect/config/CloudConnect.ini") + QDir(app.applicationDirPath()).filePath("config/CloudConnect.ini") ); - QCommandLineOption msgHandlingOption( - {"m", "msg_handling"}, "Path to the message handling INI file.", "msg_handling", - QDir(app.applicationDirPath()).filePath("/home/leahi/Public/leahi-realtime-cdt/CloudConnect/config/LeahiMsgHandling.ini") - ); parser.addOption(configOption); - parser.addOption(msgHandlingOption); parser.process(app); QThread controllerThread; - CloudConnectController ccController(parser.value(configOption), parser.value(msgHandlingOption)); + CloudConnectController ccController; + if (!ccController.loadConfig(parser.value(configOption))) { + return 1; + } // Bind on the controller thread: QLocalServer's socket engine belongs to the // thread that calls listen(), and accepted sockets are parented under it. Index: leahi-realtime-cdt.pro =================================================================== diff -u -r59b4c22f45a1d098064a886452769204e90cfb4b -raaebfee335c74b0250864a6dce0555f866adadea --- leahi-realtime-cdt.pro (.../leahi-realtime-cdt.pro) (revision 59b4c22f45a1d098064a886452769204e90cfb4b) +++ leahi-realtime-cdt.pro (.../leahi-realtime-cdt.pro) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -4,9 +4,8 @@ SUBDIRS += \ Comms \ MsgUtils \ - CANDumpPlayer \ - DCSsim \ - CloudConnect + CloudConnect \ + CANDumpPlayer Comms.subdir = lib/Comms MsgUtils.subdir = lib/MsgUtils Index: lib/Comms/Comms.pro =================================================================== diff -u -r51e99f2578e0901d9da91a4cb60d1b8858cfe971 -raaebfee335c74b0250864a6dce0555f866adadea --- lib/Comms/Comms.pro (.../Comms.pro) (revision 51e99f2578e0901d9da91a4cb60d1b8858cfe971) +++ lib/Comms/Comms.pro (.../Comms.pro) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -3,7 +3,7 @@ TEMPLATE = lib TARGET = Comms CONFIG += shared c++17 moc -QT += core mqtt network serialbus +QT += core network serialbus mqtt QMAKE_CXXFLAGS += -Wall -Werror -Wextra Index: lib/Comms/include/CanInterface.h =================================================================== diff -u -r59b4c22f45a1d098064a886452769204e90cfb4b -raaebfee335c74b0250864a6dce0555f866adadea --- lib/Comms/include/CanInterface.h (.../CanInterface.h) (revision 59b4c22f45a1d098064a886452769204e90cfb4b) +++ lib/Comms/include/CanInterface.h (.../CanInterface.h) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -63,7 +63,7 @@ * \details This signal will be emitted when a frame has been received * \param vFrame - The Frame which has been received */ - void didFrameReceive(const QCanBusFrame vFrame); + void didFrameReceive(const QCanBusFrame &vFrame); /*! * \brief didFrameTransmit Index: lib/Comms/include/MqttClient.h =================================================================== diff -u -r51e99f2578e0901d9da91a4cb60d1b8858cfe971 -raaebfee335c74b0250864a6dce0555f866adadea --- lib/Comms/include/MqttClient.h (.../MqttClient.h) (revision 51e99f2578e0901d9da91a4cb60d1b8858cfe971) +++ lib/Comms/include/MqttClient.h (.../MqttClient.h) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -17,25 +17,14 @@ #include #include #include +#include +#include class QMqttClient; /*! - * \brief Publishes messages to the cloud over MQTT. - * \details Wraps QtMqtt's QMqttClient for a QoS 1 publishing session. - * - * The lifecycle is init() -> open() -> close(). init() only validates - * the configuration and builds the TLS configuration from it; the - * QMqttClient itself is created by open() and destroyed by close(), - * or by an unsolicited disconnect. A close()/open() pair is therefore - * a full reset, which is what allows the session to be reopened with - * rotated credentials. - * \note Connecting is asynchronous: open() only starts the attempt, and a - * true return does not mean the broker accepted the session. - * \note Must live on the thread that calls open(): the client's socket - * belongs to the thread that constructs it. - * \note The connection is always TLS. init() fails unless keyPath names a - * readable private key. + * \brief Encrypted client MQTT connection to a broker for message sending and receiving. + * Lifecycle: init() -> open() -> close(). */ class MqttClient : public QObject { @@ -62,19 +51,37 @@ ~MqttClient() override; bool init(const Config &config); - -public Q_SLOTS: bool open(); void close(); - bool publish(const QString &topic, const QByteArray &payload, qint64 messageId = -1); + bool publish(const QString &topic, const QByteArray &payload, qint32 &msgId, quint8 qos=1); + bool subscribe(const QString &topicFilter, quint8 qos=1); -private: +Q_SIGNALS: + /*! + * \brief Emitted when the state of the session changes (i.e. connect, disconnect) + * \note Subscriptions belong to the QMqttClient that gets destroyed on disconnect, + * so re-subscription is required on every new connection. + * \note Not emitted for a caller-initiated close() + */ + void didStateChanged(QMqttClient::ClientState state); + + /*! + * \brief Emitted for status change of every sent message. + */ + void didMessageStatusChanged(quint32 id, QMqtt::MessageStatus status, + const QMqttMessageStatusProperties &properties); + + /*! + * \brief Emitted for every message the broker delivers to this client. + * \details Covers all active subscriptions; the receiver demultiplexes by topic. + */ + void didMessageReceived(const QString &topic, const QByteArray &payload); + +private Q_SLOTS: void onStateChanged(); - void onMessageSent(qint32 id); - void failPending(); +private: QMqttClient *_client = nullptr; Config _config; QSslConfiguration _sslConfig; - QHash _pending; }; Index: lib/Comms/include/types.h =================================================================== diff -u -ra5781739bcbe58c754aff8861561495624bc5b67 -raaebfee335c74b0250864a6dce0555f866adadea --- lib/Comms/include/types.h (.../types.h) (revision a5781739bcbe58c754aff8861561495624bc5b67) +++ lib/Comms/include/types.h (.../types.h) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -17,6 +17,7 @@ #include #include #include +#include // stl #include @@ -25,6 +26,8 @@ #include "format.h" // #include "Logger.h" +Q_DECLARE_LOGGING_CATEGORY(logTypes) + // defines // #define GetValue(vData, vIndex, vValue ) Types::getValue<>(vData, vIndex, vValue, QT_STRINGIFY(vValue)) // #define GetBits( vData, vIndex, vFlags, vLen) Types::getBits( vData, vIndex, vFlags, vLen ) @@ -155,14 +158,12 @@ int size = sizeof(T); int end = vStartIndex + size; if (vData.length() < end) { - Q_UNUSED(vValueName) - // LOG_DEBUG(QString("Not enough data from position %1 to the length of %2 to get data of type '%3' in buffer %4 %5") - // .arg(vStartIndex) - // .arg(size) - // .arg(typeid(T).name()) - // .arg(Format::toHexString(vData)) - // .arg(vValueName.isEmpty() ? "" : QString("for value %1").arg(vValueName)) - // ); + qCWarning(logTypes).noquote() << QString("Not enough data from position %1 to the length of %2 to get data of type '%3' in buffer %4 %5") + .arg(vStartIndex) + .arg(size) + .arg(typeid(T).name()) + .arg(Format::toHexString(vData)) + .arg(vValueName.isEmpty() ? "" : QString("for value %1").arg(vValueName)); return false; } int i = 0; Index: lib/Comms/src/MqttClient.cpp =================================================================== diff -u -r51e99f2578e0901d9da91a4cb60d1b8858cfe971 -raaebfee335c74b0250864a6dce0555f866adadea --- lib/Comms/src/MqttClient.cpp (.../MqttClient.cpp) (revision 51e99f2578e0901d9da91a4cb60d1b8858cfe971) +++ lib/Comms/src/MqttClient.cpp (.../MqttClient.cpp) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -10,12 +10,16 @@ * \date (original) 30-Jul-2026 * */ +#include + #include #include #include #include #include #include +#include +#include #include #include "MqttClient.h" @@ -44,13 +48,14 @@ /*! * \brief MqttClient::init * \details Validates the configuration and builds the TLS configuration from - * the certificate paths it names. No client is created and no - * connection is attempted here; that is open()'s job. - * \param config - parameters for the session, retained for the next open() - * \return true if the configuration is usable, otherwise false + * the certificate and key paths. Connection to server is not attempted + * until open() is called. + * \note This function does nothing if a client connection already exists. * \note keyPath is required. caPath and certPath are optional: without * caPath the system CA set verifies the broker, and without certPath * the session is server-authenticated only rather than mTLS. + * \param config - parameters for the session, retained for the next open() + * \return true if the configuration is usable, otherwise false */ bool MqttClient::init(const Config &config) { @@ -60,39 +65,54 @@ } QSslConfiguration ssl = QSslConfiguration::defaultConfiguration(); - if (!config.caPath.isEmpty()) { + if (QFile::exists(config.caPath)) { const QList cas = QSslCertificate::fromPath(config.caPath); if (cas.isEmpty()) { qCCritical(logMqtt).noquote() << QString("%1 is not a CA certificate").arg(config.caPath); return false; } ssl.setCaCertificates(cas); } + else { + qCCritical(logMqtt).noquote() << QString("CA certificate file %1 does not exists").arg(config.caPath); + return false; + } - if (!config.certPath.isEmpty()) { + if (QFile::exists(config.certPath)) { const QList certs = QSslCertificate::fromPath(config.certPath); if (certs.isEmpty()) { qCCritical(logMqtt).noquote() << QString("%1 is not a certificate").arg(config.certPath); return false; } ssl.setLocalCertificate(certs.first()); } - - QFile keyFile(config.keyPath); - if (!keyFile.open(QIODevice::ReadOnly)) { - qCCritical(logMqtt).noquote() << QString("could not read private key %1").arg(config.keyPath); + else { + qCCritical(logMqtt).noquote() << QString("certificate file %1 does not exists").arg(config.certPath); return false; } - const QByteArray pem = keyFile.readAll(); - QSslKey key(pem, QSsl::Rsa, QSsl::Pem); - if (key.isNull()) { - key = QSslKey(pem, QSsl::Ec, QSsl::Pem); + + if (QFile::exists(config.keyPath)) { + QFile keyFile(config.keyPath); + if (!keyFile.open(QIODevice::ReadOnly)) { + qCCritical(logMqtt).noquote() << QString("could not read private key %1").arg(config.keyPath); + return false; + } + const QByteArray pem = keyFile.readAll(); + keyFile.close(); + QSslKey key(pem, QSsl::Rsa, QSsl::Pem); + if (key.isNull()) { + key = QSslKey(pem, QSsl::Ec, QSsl::Pem); + } + if (key.isNull()) { + qCCritical(logMqtt).noquote() << QString("private key %1 is not a PEM, RSA, or EC key").arg(config.keyPath); + return false; + } + ssl.setPrivateKey(key); } - if (key.isNull()) { - qCCritical(logMqtt).noquote() << QString("private key %1 is not a PEM, RSA, or EC key").arg(config.keyPath); + else { + qCCritical(logMqtt).noquote() << QString("key file %1 does not exist").arg(config.keyPath); return false; } - ssl.setPrivateKey(key); _sslConfig = ssl; _config = config; @@ -107,8 +127,6 @@ * \return true if the attempt was started * \note Must be called on this object's thread; the client's socket belongs * to the thread that constructs it. - * \note The attempt is asynchronous, so a true return only means the client - * accepted the request. The outcome arrives via onStateChanged(). */ bool MqttClient::open() { @@ -126,7 +144,13 @@ _client->setCleanSession(_config.cleanSession); connect(_client, &QMqttClient::stateChanged, this, &MqttClient::onStateChanged); - connect(_client, &QMqttClient::messageSent, this, &MqttClient::onMessageSent); + connect(_client, &QMqttClient::messageStatusChanged, this, &MqttClient::didMessageStatusChanged); + // Fires for every delivery on this connection, whichever subscription matched. + connect(_client, &QMqttClient::messageReceived, this, + [this](const QByteArray &message, const QMqttTopicName &topic) { + Q_EMIT didMessageReceived(topic.name(), message); + } + ); connect(_client, &QMqttClient::errorChanged, this, [](QMqttClient::ClientError error) { if (error != QMqttClient::NoError) { @@ -136,8 +160,9 @@ ); qCInfo(logMqtt).noquote() << QString("attempting to connect to %1:%2 as %3") - .arg(_config.endpoint).arg(_config.port).arg(_config.clientId); + .arg(_config.endpoint).arg(_config.port).arg(_config.clientId); _client->connectToHostEncrypted(_sslConfig); + return true; } @@ -157,7 +182,7 @@ QMqttClient *client = _client; _client = nullptr; - failPending(); + // stop any signals coming from client before modifying it client->disconnect(this); // flush to force the socket to send the Disconnect so bytesToWrite hopefully @@ -187,32 +212,76 @@ /*! * \brief MqttClient::publish * \details Hands one message to the client for QoS 1 delivery. - * \param topic - fully resolved MQTT topic - * \param payload - serialised message bytes - * \param messageId - caller token echoed back on acknowledgement, or -1 for none + * \param[in] topic - fully resolved MQTT topic + * \param[in] payload - serialised message bytes + * \param[out] msgId - identifier assigned by QMqttClient::publish to track message + * \param[in] qos - QoS level (default=1) * \return true if the client accepted the message. * \note Return status does NOT indicate delivery, only that the client * accepted the message for sending. The PUBACK that confirms delivery * arrives later in onMessageSent(). */ -bool MqttClient::publish(const QString &topic, const QByteArray &payload, qint64 messageId) +bool MqttClient::publish(const QString &topic, const QByteArray &payload, qint32 &msgId, quint8 qos) { if (_client == nullptr || _client->state() != QMqttClient::Connected) { + msgId = -1; return false; } - const qint32 id = _client->publish(QMqttTopicName(topic), payload, 1, false); - if (id == -1) { - qCWarning(logMqtt).noquote() << QString("client rejected publishing to topic %1").arg(topic); + msgId = _client->publish(QMqttTopicName(topic), payload, qos, false); + if (msgId != -1) { + // qCDebug(logMqtt).noquote() << QString("publish accepted (msgId=%1): topic=%2, qos=%3") + // .arg(msgId).arg(topic).arg(qos); + return true; + } + else { + qCWarning(logMqtt).noquote() << QString("client rejected publish: topic=%1, qos=%2") + .arg(topic).arg(qos); return false; } +} - _pending.insert(id, messageId); - // TODO: determine if messages waiting on a PUBACK need to be re-sent - // set a timeout interval to indicate when a message needs to be - // resent and have a timer periodically check _pending for messages - // that have met that threshold +/*! + * \brief MqttClient::subscribe + * \details Subscribes this session to a topic filter. Deliveries arrive on + * didMessageReceived(), which is not per-subscription: it carries the + * topic so the receiver can demultiplex. + * \param topicFilter - MQTT topic filter, wildcards allowed + * \param qos - requested maximum QoS for deliveries on this filter + * \return true if the client accepted the request. + * \note Return status does NOT mean the subscription is active. The SUBACK + * arrives later; until it does, the broker may drop matching messages. + * The granted QoS can also be lower than requested, which is not an error. + * \note Subscriptions live on the QMqttClient and die with it, so they must be + * re-established after every reconnect (see didConnectionChange()). + */ +bool MqttClient::subscribe(const QString &topicFilter, quint8 qos) +{ + if (_client == nullptr || _client->state() != QMqttClient::Connected) { + qCWarning(logMqtt).noquote() << QString("cannot subscribe to %1 while disconnected").arg(topicFilter); + return false; + } + QMqttSubscription *subscription = _client->subscribe(QMqttTopicFilter(topicFilter), qos); + if (subscription == nullptr) { + qCWarning(logMqtt).noquote() << QString("client rejected subscribing to topic %1").arg(topicFilter); + return false; + } + + // A subscription AWS IoT refuses (a filter outside the policy's iot:Receive or + // iot:Subscribe resources) fails here rather than at publish time, and without + // this log the only symptom is that no message ever arrives. + connect(subscription, &QMqttSubscription::stateChanged, this, + [topicFilter](QMqttSubscription::SubscriptionState state) { + if (state == QMqttSubscription::Subscribed) { + qCInfo(logMqtt).noquote() << QString("subscribed to %1").arg(topicFilter); + } + else if (state == QMqttSubscription::Error) { + qCWarning(logMqtt).noquote() << QString("subscription to topic %1 failed").arg(topicFilter); + } + } + ); + return true; } @@ -226,52 +295,24 @@ if (_client != nullptr) { switch (_client->state()) { case QMqttClient::Disconnected: - // NOTE: Receiving state change to Disconnected here means that connection - // was not done by user of this class calling close() and is most - // likely caused by externally factors, like the server side closing - // the connection. + // NOTE: close() disconnects all signals from _client so further signals, + // like stateChanged, will no longer be received qCInfo(logMqtt).noquote() << "client disconnected"; - failPending(); _client->deleteLater(); _client = nullptr; // TODO: reconnect on disconnect - // set a flag reconnect flag indicating if connection should try to - // be re-established + Q_EMIT didStateChanged(QMqttClient::Disconnected); break; case QMqttClient::Connecting: qCInfo(logMqtt).noquote() << "client connecting"; + Q_EMIT didStateChanged(_client->state()); break; case QMqttClient::Connected: qCInfo(logMqtt).noquote() << "client connected"; + // Subscriptions did not survive the previous client, so this is where + // a receiver re-establishes them. + Q_EMIT didStateChanged(_client->state()); break; } } } - -/*! - * \brief MqttClient::onMessageSent - * \details Clears one QoS 1 publish from _pending on PUBACK. - * \param id - QMqttClient's identifier for the acknowledged publish - */ -void MqttClient::onMessageSent(qint32 id) -{ - if (!_pending.contains(id)) { - qCWarning(logMqtt).noquote() << QString("received PUBACK for an unknown published message with id %1").arg(id); - return; - } - _pending.take(id); -} - -/*! - * \brief MqttClient::failPending - * \details Abandons every publish still awaiting a PUBACK. - */ -void MqttClient::failPending() -{ - if (_pending.isEmpty()) { - return; - } - - qCWarning(logMqtt).noquote() << QString("Failing %1 unacknowledged publishes").arg(_pending.size()); - _pending.clear(); -} Index: lib/Comms/src/format.cpp =================================================================== diff -u -ra5781739bcbe58c754aff8861561495624bc5b67 -raaebfee335c74b0250864a6dce0555f866adadea --- lib/Comms/src/format.cpp (.../format.cpp) (revision a5781739bcbe58c754aff8861561495624bc5b67) +++ lib/Comms/src/format.cpp (.../format.cpp) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -100,7 +100,11 @@ case QMetaType::QVariantList: // list { QVariantList list = vData.toList(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) for(const auto &item: std::as_const(list)) { +#else + for(const auto &item: qAsConst(list)) { +#endif mData += fromVariant(item); } return mData; Index: lib/Comms/src/types.cpp =================================================================== diff -u -ra5781739bcbe58c754aff8861561495624bc5b67 -raaebfee335c74b0250864a6dce0555f866adadea --- lib/Comms/src/types.cpp (.../types.cpp) (revision a5781739bcbe58c754aff8861561495624bc5b67) +++ lib/Comms/src/types.cpp (.../types.cpp) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -18,6 +18,8 @@ // Project +Q_LOGGING_CATEGORY(logTypes, "comms.types") + /*! * \brief Types::floatCompare * \details compares two floats with a tolerance. @@ -46,8 +48,14 @@ QByteArray data = vData.mid(vStartIndex, vLen); quint8 bytes = vLen / 8; vStartIndex = vLen % 8 ? bytes + 1 : bytes ; //CEILING DIVISION TO ROUND UP TO LATEST BIT - if ( data.length() * 8 < vLen ) + if ( data.length() * 8 < vLen ) { + qCWarning(logTypes).noquote() + << QString("not enough data to read %1 bit(s): buffer holds only %2 bit(s) [%3]") + .arg(vLen) + .arg(data.length() * 8) + .arg(Format::toHexString(data)); return false; + } vFlags = QBitArray::fromBits(data, vLen); return true; } Index: lib/MsgUtils/CMakeLists.txt =================================================================== diff -u -r4dccc470aedf6f68a0ad73c1101f8a96188cbdb1 -raaebfee335c74b0250864a6dce0555f866adadea --- lib/MsgUtils/CMakeLists.txt (.../CMakeLists.txt) (revision 4dccc470aedf6f68a0ad73c1101f8a96188cbdb1) +++ lib/MsgUtils/CMakeLists.txt (.../CMakeLists.txt) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -94,6 +94,9 @@ target_include_directories(${PROJECT_NAME} PUBLIC $ +) + +target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${Protobuf_INCLUDE_DIR} ) Index: lib/MsgUtils/cmake/MsgUtils.cmake =================================================================== diff -u -r4dccc470aedf6f68a0ad73c1101f8a96188cbdb1 -raaebfee335c74b0250864a6dce0555f866adadea --- lib/MsgUtils/cmake/MsgUtils.cmake (.../MsgUtils.cmake) (revision 4dccc470aedf6f68a0ad73c1101f8a96188cbdb1) +++ lib/MsgUtils/cmake/MsgUtils.cmake (.../MsgUtils.cmake) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -57,11 +57,11 @@ ) endfunction() -# \brief This function adds a custom command to generate/update a message handling INI. +# \brief This function adds a custom command to generate/update a CAN routing 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_msg_handling_ini _input_confs _output_ini _target_name) +function(generate_can_routing_ini _input_confs _output_ini _target_name) find_package(Python3 COMPONENTS Interpreter Development REQUIRED) # cmake >= 3.20: cmake_path(ABSOLUTE_PATH _output_ini) @@ -77,15 +77,15 @@ add_custom_command( DEPENDS + ${MSGUTILS_SCRIPTS_DIR}/GenerateCanRoutingIni.py ${MSGUTILS_SCRIPTS_DIR}/msgutils/MsgData.py - ${MSGUTILS_SCRIPTS_DIR}/msgutils/MsgHandlingIni.py - ${MSGUTILS_SCRIPTS_DIR}/GenerateMsgHandlingIni.py - ${MSGUTILS_SCRIPTS_DIR}/msgutils/templates/MsgHandlingIni.jinja + ${MSGUTILS_SCRIPTS_DIR}/msgutils/CanRoutingIni.py + ${MSGUTILS_SCRIPTS_DIR}/msgutils/templates/CanRoutingIni.jinja ${${_input_confs}} OUTPUT ${_output_ini} COMMAND - ${PROJECT_PYTHON} ${MSGUTILS_SCRIPTS_DIR}/GenerateMsgHandlingIni.py + ${PROJECT_PYTHON} ${MSGUTILS_SCRIPTS_DIR}/GenerateCanRoutingIni.py ${${_input_confs}} ${_output_ini} COMMENT "Generating/updating message INI ${_output_ini} from input ${_file_list}" Index: scripts/MsgUtils/GenerateCanRoutingIni.py =================================================================== diff -u --- scripts/MsgUtils/GenerateCanRoutingIni.py (revision 0) +++ scripts/MsgUtils/GenerateCanRoutingIni.py (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import argparse +import os +import sys +from msgutils import CanRoutingIni + +def main(): + parser = argparse.ArgumentParser( + description='Tool for generating/updating the CAN message routing INI from the inputted message conf file(s)' + ) + parser.add_argument('conf', nargs='+') + parser.add_argument('output', help='path to the CAN message routing INI to create or update') + + args = parser.parse_args() + if len(sys.argv) < 3: + parser.print_help() + else: + can_ini = CanRoutingIni() + try: + for conf in args.conf: + can_ini.loadConf(conf, clear=False) + can_ini.loadIni(args.output) + output_dir = os.path.dirname(os.path.abspath(args.output)) + os.makedirs(output_dir, exist_ok=True) + can_ini.write_ini(args.output) + except Exception as e: + print('Error: %s' % e) + sys.exit(1) + +if __name__ == "__main__": # calling main function + main() Fisheye: Tag aaebfee335c74b0250864a6dce0555f866adadea refers to a dead (removed) revision in file `scripts/MsgUtils/GenerateMsgHandlingIni.py'. Fisheye: No comparison available. Pass `N' to diff? Index: scripts/MsgUtils/msgutils/CanRoutingIni.py =================================================================== diff -u --- scripts/MsgUtils/msgutils/CanRoutingIni.py (revision 0) +++ scripts/MsgUtils/msgutils/CanRoutingIni.py (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -0,0 +1,94 @@ +import configparser +import sys +from pathlib import Path +from jinja2 import Environment, FileSystemLoader +from .MsgData import MsgData + + +# \brief Generates and maintains the CAN message routing INI from loaded message data. +# \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. +# action - send_always | send_delta | drop; defaults to drop, preserved across runs. +# topic - MQTT topic; empty by default, preserved across runs. +# Call loadConf() to load message definitions, then loadIni() to merge in existing +# action/topic values, then write_ini() to write the result. +class CanRoutingIni(MsgData): + DEFAULT_ACTION = 'Drop' + VALID_ACTIONS = ('SendAlways', 'SendDelta', 'Drop') + DEFAULT_TOPIC = 'NormalPriority' + VALID_TOPICS = ('HighPriority', 'NormalPriority', 'DeviceLogFile', + 'TreatmentLogFile', 'CloudSyncLogFile') + + # \brief Initializer + def __init__(self): + super().__init__() + + + # \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['action'] = '' + msg['topic'] = '' + + + # \brief Merge action/topic values from an existing INI into the loaded message data. + # \details Only action 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 action 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 + 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()) + + 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(): + action = cfg.get(section, 'action', fallback='').strip() + topic = cfg.get(section, 'topic', fallback='').strip() + if action or topic: + print(f"WARNING: {MsgData.value_to_hex_string(msg_id_value)} no longer in Unhandled.conf, " + f"dropping section with action={action or ''} topic={topic or ''}") + continue + + action = cfg.get(section, 'action', fallback='').strip() + if len(action) and action not in self.VALID_ACTIONS: + print(f"WARNING: {MsgData.value_to_hex_string(msg_id_value)} has invalid action \"{action}\", " + f"resetting to {self.DEFAULT_ACTION}") + action = '' + + topic = cfg.get(section, 'topic', fallback='').strip() + if len(topic) and topic not in self.VALID_TOPICS: + print(f"WARNING: {MsgData.value_to_hex_string(msg_id_value)} has invalid topic \"{topic}\", " + f"resetting to {self.DEFAULT_TOPIC}") + topic = '' + + self.data[msg_id_value]['action'] = action + self.data[msg_id_value]['topic'] = topic + + + # \brief Write the loaded message routing 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('CanRoutingIni.jinja') + render = template.render({'msg_ini': self}) + with open(filename, mode='w', encoding='utf-8', newline='\n') as out_file: + out_file.write(render) + print(f"Wrote CAN message routing INI {filename} with {len(self.data)} messages") Fisheye: Tag aaebfee335c74b0250864a6dce0555f866adadea refers to a dead (removed) revision in file `scripts/MsgUtils/msgutils/MsgHandlingIni.py'. Fisheye: No comparison available. Pass `N' to diff? Index: scripts/MsgUtils/msgutils/__init__.py =================================================================== diff -u -r64243101dff61b5c1a40b96ef33080236999acf6 -raaebfee335c74b0250864a6dce0555f866adadea --- scripts/MsgUtils/msgutils/__init__.py (.../__init__.py) (revision 64243101dff61b5c1a40b96ef33080236999acf6) +++ scripts/MsgUtils/msgutils/__init__.py (.../__init__.py) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -1,4 +1,4 @@ from .MsgData import MsgData from .MsgCpp import MsgCpp from .MsgProtobuf import MsgProtobuf -from .MsgHandlingIni import MsgHandlingIni +from .CanRoutingIni import CanRoutingIni Index: scripts/MsgUtils/msgutils/templates/CanRoutingIni.jinja =================================================================== diff -u --- scripts/MsgUtils/msgutils/templates/CanRoutingIni.jinja (revision 0) +++ scripts/MsgUtils/msgutils/templates/CanRoutingIni.jinja (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -0,0 +1,25 @@ +; Group format: +; [ ] +; msg_id = +{%- set config = namespace(actions = []) %} +{%- for action in msg_ini.VALID_ACTIONS %} +{%- set config.actions = config.actions + [ action ] %} +{%- endfor %} +; action = <{{ config.actions | join("|") }}> (default: {{ msg_ini.DEFAULT_ACTION }}) +{%- set config = namespace(topics = []) %} +{%- for topic in msg_ini.VALID_TOPICS %} +{%- set config.topics = config.topics + [ topic ] %} +{%- endfor %} +; topic = <{{ config.topics | join("|") }}> (default: {{ msg_ini.DEFAULT_TOPIC }}) + +{% for msg in msg_ini.data.values() -%} +[{{ msg.msg_id_hex_string }}] +msg_id = {{ msg.msg_id }} +{%- if msg.action | length %} +action = {{ msg.action }} +{%- endif %} +{%- if msg.topic | length %} +topic = {{ msg.topic }} +{%- endif %} + +{% endfor -%} Index: scripts/MsgUtils/msgutils/templates/MsgDefs_proto.jinja =================================================================== diff -u -r53d80d0edc1c441132b13f387f5f13570a7aafd2 -raaebfee335c74b0250864a6dce0555f866adadea --- scripts/MsgUtils/msgutils/templates/MsgDefs_proto.jinja (.../MsgDefs_proto.jinja) (revision 53d80d0edc1c441132b13f387f5f13570a7aafd2) +++ scripts/MsgUtils/msgutils/templates/MsgDefs_proto.jinja (.../MsgDefs_proto.jinja) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -9,10 +9,9 @@ // Header is required as the first field (i.e. 'Header header = 1;') of every message. message Header { - string deviceSerialNum = 1; - google.protobuf.Timestamp timestamp = 2; + google.protobuf.Timestamp timestamp = 1; + int32 sequence = 2; uint32 msgId = 3; - int32 sequence = 4; } // Envelope is a minimal wrapper used to extract the Header from serialized messages without knowing the Fisheye: Tag aaebfee335c74b0250864a6dce0555f866adadea refers to a dead (removed) revision in file `scripts/MsgUtils/msgutils/templates/MsgHandlingIni.jinja'. Fisheye: No comparison available. Pass `N' to diff? Index: scripts/MsgUtils/msgutils/templates/MsgProtoUtils_cpp.jinja =================================================================== diff -u -r59b4c22f45a1d098064a886452769204e90cfb4b -raaebfee335c74b0250864a6dce0555f866adadea --- scripts/MsgUtils/msgutils/templates/MsgProtoUtils_cpp.jinja (.../MsgProtoUtils_cpp.jinja) (revision 59b4c22f45a1d098064a886452769204e90cfb4b) +++ scripts/MsgUtils/msgutils/templates/MsgProtoUtils_cpp.jinja (.../MsgProtoUtils_cpp.jinja) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -12,18 +12,17 @@ {%- endif %} // Populate the protobuf Header (field 1) shared by every typed message. -static void updateHeader(messages::Header *header, const QDateTime ×tamp, const QString &deviceSerialNum, quint16 msgId, qint16 sequence) +static void updateHeader(messages::Header *header, const QDateTime ×tamp, qint16 sequence, quint16 msgId) { if (header) { const auto msecs = timestamp.toMSecsSinceEpoch(); - header->set_deviceserialnum(deviceSerialNum.toStdString()); auto proto_timestamp = header->mutable_timestamp(); if (proto_timestamp) { proto_timestamp->set_seconds(msecs / 1000); proto_timestamp->set_nanos((msecs % 1000) * 1000000); } - header->set_msgid(msgId); header->set_sequence(sequence); + header->set_msgid(msgId); } } {%- for (msg_id_value, msg) in msg_cpp.data.items() %} @@ -32,12 +31,13 @@ // {{ msg['msg_id'] }} ({{ msg['msg_id_hex_string'] }}) // payload: {{ msg_cpp.field_list(msg_id_value) | join(", ") }} // serializeProto: msg struct -> QByteArray of serialized protobuf data (header populated from params) -QByteArray serializeProto([[maybe_unused]] const {{ msg['msg_name'] }}Payload &src, const QDateTime ×tamp, const QString &deviceSerialNum, quint16 msgId, qint16 sequence) +bool serializeProto([[maybe_unused]] const {{ msg['msg_name'] }}Payload &src, const QDateTime ×tamp, qint16 sequence, quint16 msgId, QByteArray &dst) { {%- if has_union %} qDebug().noquote() << "WARNING: MsgId={{ msg['msg_name'] }} contains union/oneof field(s); protobuf serialization is partial"; {%- endif %} messages::{{ msg['msg_name'] }} proto; + updateHeader(proto.mutable_header(), timestamp, sequence, msgId); {%- for field in msg['payload'] %} {%- if field['type'] != "union" %} // {{ field['type'] ~ "-" ~ field['name'] }} @@ -46,10 +46,15 @@ // TODO: {{ field['type'] ~ "-" ~ field['name'] }} {%- endif %} {%- endfor %} - updateHeader(proto.mutable_header(), timestamp, deviceSerialNum, msgId, sequence); std::string out; - (void)proto.SerializeToString(&out); - return QByteArray(out.data(), static_cast(out.size())); + if (proto.SerializeToString(&out)) { + dst = QByteArray::fromStdString(out); + return true; + } + else { + dst = QByteArray(); + return false; + } } // {{ msg['msg_id'] }} ({{ msg['msg_id_hex_string'] }}) @@ -61,39 +66,44 @@ qDebug().noquote() << "WARNING: MsgId={{ msg['msg_name'] }} contains union/oneof field(s); protobuf deserialization is partial"; {%- endif %} messages::{{ msg['msg_name'] }} proto; - if (proto.ParseFromArray(bytes.constData(), bytes.size()) == false) { - qDebug().noquote() << "ERROR: could not parse protobuf for MsgId={{ msg['msg_name'] }}"; - return false; - } + if (proto.ParseFromArray(bytes.constData(), bytes.size())) { {%- for field in msg['payload'] %} {%- if field['type'] != "union" %} - // {{ field['type'] ~ "-" ~ field['name'] }} - dst.{{ field['name'] }}.value = proto.{{ field['name'].lower() }}(); + // {{ field['type'] ~ "-" ~ field['name'] }} + dst.{{ field['name'] }}.value = proto.{{ field['name'].lower() }}(); {%- else %} - // TODO: {{ field['type'] ~ "-" ~ field['name'] }} + // TODO: {{ field['type'] ~ "-" ~ field['name'] }} {%- endif %} {%- endfor %} - return true; + return true; + } + else { + return false; + } } {%- endfor %} -QByteArray canMessageToProtobufByteArray(const QDateTime ×tamp, const QString &deviceSerialNum, const Can::Message &msg) +bool canMessageToProtobufByteArray(const QDateTime ×tamp, const Can::Message &msg, QByteArray &dst) { switch (msg.msgId) { {%- for (msg_id_value, msg) in msg_cpp.data.items() %} case {{ msg['msg_id'] }}: { {{ msg['msg_name'] }}Payload payload; - if (payload.fromQByteArray(msg.data) == false) { - qDebug().noquote() << "ERROR: could not convert CAN message with MsgId={{ msg['msg_name'] }} to struct"; + if (payload.fromQByteArray(msg.data)) { + return serializeProto(payload, timestamp, msg.sequence, msg.msgId, dst); } - return serializeProto(payload, timestamp, deviceSerialNum, msg.msgId, msg.sequence); + else { + dst = QByteArray(); + return false; + } } {%- endfor %} default: qDebug().noquote() << QString("WARNING: MsgId=0x%1 not handled").arg(msg.msgId, 4, 16, QChar('0')); + dst = QByteArray(); + return false; break; } - return QByteArray(); } // Maps a msgId to its fully-qualified protobuf message name for descriptor-pool lookup. Index: scripts/MsgUtils/msgutils/templates/MsgProtoUtils_h.jinja =================================================================== diff -u -rf9c6b488aa4135e8cd47ccd3fdc6c3ae1cd831aa -raaebfee335c74b0250864a6dce0555f866adadea --- scripts/MsgUtils/msgutils/templates/MsgProtoUtils_h.jinja (.../MsgProtoUtils_h.jinja) (revision f9c6b488aa4135e8cd47ccd3fdc6c3ae1cd831aa) +++ scripts/MsgUtils/msgutils/templates/MsgProtoUtils_h.jinja (.../MsgProtoUtils_h.jinja) (revision aaebfee335c74b0250864a6dce0555f866adadea) @@ -21,11 +21,11 @@ // payload: {{ msg_cpp.field_list(msg_id_value) | join(", ") }} // serializeProto: msg struct -> QByteArray of serialized protobuf data (header populated from params) // deserializeProto: QByteArray of serialized protobuf data -> msg struct (false on parse failure) -QByteArray serializeProto(const {{ msg['msg_name'] }}Payload &src, const QDateTime ×tamp, const QString &deviceSerialNum, quint16 msgId, qint16 sequence); +bool serializeProto(const {{ msg['msg_name'] }}Payload &src, const QDateTime ×tamp, qint16 sequence, quint16 msgId, QByteArray &dst); bool deserializeProto(const QByteArray &bytes, {{ msg['msg_name'] }}Payload &dst); {%- endfor %} -QByteArray canMessageToProtobufByteArray(const QDateTime ×tamp, const QString &deviceSerialNumber, const Can::Message &msg); +bool canMessageToProtobufByteArray(const QDateTime ×tamp, const Can::Message &msg, QByteArray &dst); // Maps a msgId to its fully-qualified protobuf message name (e.g. "{{ cpp_namespace if cpp_namespace else 'messages' }}.messages.AlarmStatusData"). // Returns an empty string if the msgId is unknown. Intended for descriptor-pool lookup