Index: docs/SDD/Class_Overview.png =================================================================== diff -u -rf9eefe0a6092719f35d5a73fa43632c8c779e0df -r8b98ff34313ee5d3f5d49672d622162acfc755c9 Binary files differ Index: docs/SDD/Class_Overview.puml =================================================================== diff -u -rf9eefe0a6092719f35d5a73fa43632c8c779e0df -r8b98ff34313ee5d3f5d49672d622162acfc755c9 --- docs/SDD/Class_Overview.puml (.../Class_Overview.puml) (revision f9eefe0a6092719f35d5a73fa43632c8c779e0df) +++ docs/SDD/Class_Overview.puml (.../Class_Overview.puml) (revision 8b98ff34313ee5d3f5d49672d622162acfc755c9) @@ -6,28 +6,47 @@ skinparam class { BackgroundColor<> LightYellow BackgroundColor<> LightCyan - BackgroundColor<> #EEEEEE - BorderStyle<> dashed + BackgroundColor<> #E8F0E8 } package "CloudConnect (process)" { class CloudConnectController { - -_settings: QSettings -_canInterface: Can::CanInterface - -_canThread: QThread -_dispatcher: Can::MessageDispatcher - -_msgHandling: QHash - -_msgCache: QMap> - -_appServer: CloudConnectServer - -_txSequence: quint16 + -_canCache: QMap> + -_canRouting: QHash + -_appServerSocketPath: QString + -_appServer: QSharedPointer + -_mqttClient: MqttClient + -_topicPrefix: QString + -_deviceId: QString __ - +CloudConnectController(configPath, msgHandlingPath) + +CloudConnectController(parent) + +loadConfig(configPath): bool + +initThread(QThread&) + +startCan(): bool +listenForApp(): bool - -loadMsgHandling(msgHandlingPath) + +connectToCloud(): bool + -loadCanRouting(canRoutingPath): bool + -mqttTopic(Topic): QString __ - -onFrameReceive(QCanBusFrame) - -onMessageReceive(Can::Message) + -onCanFrameReceive(QCanBusFrame) <> + -onCanMessageReceive(Can::Message) <> + -onCloudStateChanged(ClientState) <> + -onCloudMessageStatusChanged(id, status, props) <> + -onCloudMessageReceived(QByteArray, QMqttTopicName) <> } + + enum "CloudConnectController::CanAction" as CanAction { + Drop + SendAlways + SendDelta + } + + class "CloudConnectController::CanRouting" as CanRouting { + +action: CanAction = Drop + +topic: CloudConnectFrame::Topic = NormalPriority + } } package "Comms (lib)" { @@ -37,7 +56,7 @@ -_txFrameCount: FrameCount -_erFrameCount: FrameCount __ - +init(thread: QThread&): bool + +init(): bool +quitDevice() +enableConsoleOut(bool) +status(): QString @@ -53,18 +72,47 @@ -onFrameWritten(qint64) <> } - class CloudConnectClient <> { + class MqttClient { + -_client: QMqttClient* + -_config: Config + -_sslConfig: QSslConfiguration + __ + +init(Config): bool + +open(): bool + +close() + +publish(topic, payload, msgId&, qos = 1): bool + +subscribe(topicFilter, qos = 1): bool + __ + +didStateChanged(ClientState) <> + +didMessageStatusChanged(id, status, props) <> + +didMessageReceived(QByteArray, QMqttTopicName) <> + __ + -onStateChanged() <> + } + + class "MqttClient::Config" as MqttConfig { + +endpoint: QString + +port: quint16 = 8883 + +certPath: QString + +keyPath: QString + +caPath: QString + +clientId: QString + +keepAliveSecs: quint16 = 30 + +cleanSession: bool = true + } + + class CloudConnectClient { -_socket: QLocalSocket -_reconnectTimer: QTimer -_socketPath: QString -_rxBuf: QByteArray -_rxMsg: CloudConnectFrame __ +init(socketPath, reconnectIntervalMs, thread): bool - +send(type, sequence, payload): bool + +send(topic, sequence, payload): bool +quit() <> __ - +didMessageReceive(Type, seq, payload) <> + +didMessageReceive(Topic, seq, payload) <> +didConnect() <> +didDisconnect() <> __ @@ -75,24 +123,17 @@ -onReconnectTimer() <> } - note bottom of CloudConnectClient - No call sites. The agent socket it served - was removed in Phase 1; the class and its - build entries in Comms remain. Delete or - repurpose — see AgentMigrationPlan.md §7. - end note - class CloudConnectServer { -_server: QLocalServer -_client: QLocalSocket* -_rxBuf: QByteArray -_rxMsg: CloudConnectFrame __ +listen(socketPath): bool - +send(type, sequence, payload): bool + +send(topic, sequence, payload): bool +isConnected(): bool __ - +didMessageReceive(Type, seq, payload) <> + +didMessageReceive(Topic, seq, payload) <> +didConnect() <> +didDisconnect() <> __ @@ -122,20 +163,20 @@ class CloudConnectFrame { -_headerBuf: QByteArray - -_rxType: Type + -_rxTopic: Topic -_rxSequence: quint16 -_rxPayloadLen: quint32 -_rxPayload: QByteArray __ - +{static} build(type, sequence, payload): QByteArray + +{static} build(topic, sequence, payload): QByteArray +read(bytes: QByteArray&): ReadState - +type(): Type + +topic(): Topic +sequence(): quint16 +payload(): QByteArray +reset() } - enum "CloudConnectFrame::Type" as Type { + enum "CloudConnectFrame::Topic" as Topic { HighPriority = 0x0001 NormalPriority = 0x0002 DeviceLogFile = 0x0003 @@ -151,33 +192,72 @@ } } -package "CANDumpPlayer (process)" { +package "MsgUtils (lib)" { + class "leahi (generated)" as MsgDefs <> { + +{static} msgIdString(MsgId): QString + +{static} msgIdToProtoName(msgId): std::string + +{static} canMessageToProtobufByteArray(when, msg, out): bool + __ + Generated from LeahiUnhandled.conf — + see CodeGenPipeline + } + + class "leahi::messages (protobuf)" as Proto <> { + Envelope + Header + <474 message types> + } +} + +package "Tools (processes)" { class "CANDumpPlayer\n(main)" <> { +can_interface: string +candump_file: string +--speed: double +--test: bool __ - Parses candump log and replays\nframes onto SocketCAN at a\nconfigurable speed multiplier + Replays a candump log onto\nSocketCAN at a configurable\nspeed multiplier } + + class "DCSsim\n(DCSSimController)" <> { + -_server: SslTcpServer + __ + Local MQTT broker simulator.\nTLS/mTLS via SslTcpServer,\nhand-rolled MqttPacket codec.\nStands in for AWS IoT Core. + } } +package "luis (process)" { + class "luis application" as LUIS <> { + -_ccClient: CloudConnectClient + } +} + ' --- composition --- CloudConnectController *-- CanInterface -CloudConnectController *-- CloudConnectServer CloudConnectController *-- MessageDispatcher +CloudConnectController *-- MqttClient +CloudConnectController o-- CloudConnectServer +' --- nested types --- +CloudConnectController +-- CanAction +CloudConnectController +-- CanRouting +MqttClient +-- MqttConfig +CloudConnectFrame +-- Topic +CloudConnectFrame +-- ReadState + ' --- internal composition --- MessageDispatcher *-- MessageBuilder +' --- external client --- +LUIS *-- CloudConnectClient +CloudConnectClient --> CloudConnectServer : connects via LocalSocket\n(CloudConnectFrame stream) + ' --- usage --- +CloudConnectController ..> MsgDefs : serialize / decode +MsgDefs ..> Proto : builds +CanRouting ..> Topic : uses CloudConnectServer ..> CloudConnectFrame : uses -CloudConnectClient ..> CloudConnectFrame : uses -' --- enum nesting --- -CloudConnectFrame +-- Type -CloudConnectFrame +-- ReadState - ' --- layout hints --- CanInterface -[hidden]- CloudConnectClient Index: docs/SDD/CodeGenPipeline.png =================================================================== diff -u -r5703cc9be0f77b0fb405d60767a76033d8f9d2cb -r8b98ff34313ee5d3f5d49672d622162acfc755c9 Binary files differ Index: docs/SDD/CodeGenPipeline.puml =================================================================== diff -u -rb915ccc1a72fcca21908c35140af7ac7df1f1f37 -r8b98ff34313ee5d3f5d49672d622162acfc755c9 --- docs/SDD/CodeGenPipeline.puml (.../CodeGenPipeline.puml) (revision b915ccc1a72fcca21908c35140af7ac7df1f1f37) +++ docs/SDD/CodeGenPipeline.puml (.../CodeGenPipeline.puml) (revision 8b98ff34313ee5d3f5d49672d622162acfc755c9) @@ -9,71 +9,60 @@ start :LeahiUnhandled.conf\n(CAN message definitions); -fork +|Build system| +if (build system) then (qmake) + :extra-compilers "proto", "msgdefs", "canrouting"\nin MsgUtils.pro / CloudConnect.pro; +else (cmake) + :generate_protobuf(),\ngenerate_msg_defs_cpp(),\ngenerate_can_routing_ini()\nin lib/MsgUtils/cmake; +endif +:invoke the generator scripts\n(GenerateProtobuf.py, GenerateMsgDefsCpp.py,\nGenerateCanRoutingIni.py)\nusing the shared Python venv; - ' ── Pipeline A: Protobuf schema ────────────────────────────── - |cmake| - :generate_protobuf()\ninvokes GenerateProtobuf.py; +|MsgUtils Python| +:MsgData.loadConf() parses .conf —\nmessage names, IDs,\nfield names and types; - |MsgUtils Python| - :MsgData loads .conf —\nparses message names, IDs,\nfield names and types; +fork + ' ── Pipeline A: Protobuf schema ────────────────────────────── |Jinja2 Templates| :MsgDefs_proto.jinja\nrenders LeahiMsgDefs.proto; |protoc| - :protoc compiles .proto\ninto C++ stubs\n(LeahiMsgDefs.pb.h / .pb.cc); + :protoc --cpp_out compiles .proto\ninto C++ stubs\n(LeahiMsgDefs.pb.h / .pb.cc); - |CMake build| - :pb stubs compiled\ninto MsgUtils library; + |Build| + :pb.h/cpp files compiled\ninto MsgUtils library; fork again ' ── Pipeline B: C++ message structs ───────────────────────── - |cmake| - :generate_msg_defs_cpp()\ninvokes GenerateMsgDefsCpp.py; - - |MsgUtils Python| - :MsgData loads .conf —\nparses message names, IDs,\nfield names and types; - |Jinja2 Templates| - :MsgDefs_h.jinja renders\nLeahiMsgDefs.h\n(typed structs + toProtobuf /\nfromQByteArray + msgIdString()); - :MsgDefs_cpp.jinja renders\nLeahiMsgDefs.cpp\n(method implementations +\ncanMessageToProtobufByteArray() +\nmsgIdString() switch body); + :MsgDefs_h.jinja renders\nLeahiMsgDefs.h\n(msg structs with fromQByteArray /\ntoQByteArray); + :MsgDefs_cpp.jinja renders\nLeahiMsgDefs.cpp; :MsgProtoUtils_h.jinja renders\nLeahiMsgProtoUtils.h; - :MsgProtoUtils_cpp.jinja renders\nLeahiMsgProtoUtils.cpp; + :MsgProtoUtils_cpp.jinja renders\nLeahiMsgProtoUtils.cpp\n(canMessageToProtobufByteArray()); - |CMake build| + |Build| :MsgDefs + MsgProtoUtils compiled\ninto MsgUtils library; fork again - ' ── Pipeline C: Message handling INI ──────────────────────── - |cmake| - :generate_msg_handling_ini()\ninvokes GenerateMsgHandlingIni.py; - + ' ── Pipeline C: CAN routing INI ───────────────────────────── |MsgUtils Python| - :MsgHandlingIni loads .conf —\nseeds each msgId with\naction=drop, topic=; - :loadIni() merges existing\nLeahiMsgHandling.ini —\npreserves hand-edited\naction + topic values; + :CanRoutingIni.loadConf() extends it —\nto add CAN message handling; + :loadIni() merges the existing\nLeahiCanRouting.ini —\nto preserve changes to original file; |Jinja2 Templates| - :MsgHandlingIni.jinja renders\nLeahiMsgHandling.ini\n(per-msgId: msg_id, action, topic); + :CanRoutingIni.jinja renders\nLeahiCanRouting.ini\n(msg_id, action, topic); end fork |Runtime| -:CloudConnectController::loadMsgHandling()\nreads LeahiMsgHandling.ini at startup; -:onMessageReceive() looks up msgId\nin _msgHandling → drop / send_always /\nsend_delta policy, MQTT topic; -:canMessageToProtobufByteArray()\nconverts CAN → protobuf bytes; -:msgIdString() used in log output; +:**CloudConnectController** +==== +* loadConfig(): reads CloudConnect.ini +* loadCanRouting(): reads LeahiCanRouting.ini +* onCanMessageReceive(): CAN msg → protobuf and routes msg +* canMessageToProtobufByteArray(): converts CAN → protobuf; stop -note right - Pipelines A and B are triggered by - the MsgUtils library cmake target; - Pipeline C is triggered by the - CloudConnect executable cmake target. - All three regenerate whenever - the .conf file changes. -end note - @enduml Index: docs/SDD/Comms_Overview.png =================================================================== diff -u -r6d2ef8c97f4bb34204e95811839b3995000c47c1 -r8b98ff34313ee5d3f5d49672d622162acfc755c9 Binary files differ Index: docs/SDD/Comms_Overview.puml =================================================================== diff -u -r6d2ef8c97f4bb34204e95811839b3995000c47c1 -r8b98ff34313ee5d3f5d49672d622162acfc755c9 --- docs/SDD/Comms_Overview.puml (.../Comms_Overview.puml) (revision 6d2ef8c97f4bb34204e95811839b3995000c47c1) +++ docs/SDD/Comms_Overview.puml (.../Comms_Overview.puml) (revision 8b98ff34313ee5d3f5d49672d622162acfc755c9) @@ -2,40 +2,49 @@ title Leahi Real-time CDT — Signal/Slot Wiring Overview -participant "Can::CanInterface" as CANI -participant "CloudConnectController" as LRC -participant "Can::MessageDispatcher" as MDISP -participant "CloudConnectServer" as RS -participant "Unix Domain\nSocket (app)" as UDS -participant "Luis application" as APP +participant "Can::CanInterface" as CAN +participant "Can::MessageDispatcher" as DISP +participant "CloudConnectController" as CC +participant "MqttClient" as MQTT +participant "AWS IoT Core" as AWS -== CAN frame → assembled message == +== CloudConnectController signals → slots == -CANI -> LRC : didFrameReceive(QCanBusFrame)\n→ onFrameReceive() -LRC -> MDISP : onFrameReceive(canId, payload) -MDISP -> LRC : didActionReceive(Can::Message)\n→ onMessageReceive() +note over CC + CanInterface::didFrameReceive → CloudConnectController::onCanFrameReceive + MessageDispatcher::didActionReceive → CloudConnectController::onCanMessageReceive + MqttClient::didStateChanged → CloudConnectController::onCloudStateChanged + MqttClient::didMessageStatusChanged → CloudConnectController::onCloudMessageStatusChanged + MqttClient::didMessageReceived → CloudConnectController::onCloudMessageReceived +end note -== Message handling & forwarding == +== CAN frame → Assembled CAN Message == -LRC -> LRC : look up policy in _msgHandling\n(Drop / SendAlways / SendDelta) -LRC -> RS : send(topic, sequence, protobuf payload) -RS -> UDS : write(CloudConnectFrame frame) -UDS -> APP : readyRead +CAN -> CC : didFrameReceive(QCanBusFrame)\n→ onCanFrameReceive() +CC -> DISP : onFrameReceive(canId, payload) +DISP -> CC : didActionReceive(Can::Message)\n→ onCanMessageReceive() -== Cloud delivery (planned — Phase 2/3) == +== CAN Message Routing & MQTT Publish == -note over LRC - The Connectivity Agent socket has been removed. - The spool/publish pipeline that replaces it - (MessageSpool → Router → MqttPublisher, with - SpoolDrain replaying on reconnect) is not yet - implemented — see docs/SDD/AgentMigrationPlan.md. -end note +CC -> CC : policy from LeahiCanRouting.ini\n(Drop / SendAlways / SendDelta) +CC -> CC : canMessageToProtobufByteArray()\n→ protobuf +CC -> MQTT : publish(mqttTopic(topic), payload, pubId, qos = 1) +MQTT -> AWS : PUBLISH (QoS 1, retain = false) -== Connection events == +== MQTT Publish ACK == -RS -> LRC : didConnect()\n→ (future) serial-number handshake -RS -> LRC : didDisconnect()\n→ (future) -RS -> LRC : didMessageReceive()\n→ (future inbound) +AWS -> MQTT : PUBACK +MQTT -> CC : didMessageStatusChanged(id, Acknowledged, props)\n→ onCloudMessageStatusChanged() +== MQTT Subscription == + +CC -> MQTT : subscribe(topic, QoS) +MQTT -> AWS : SUBSCRIBE (topic, requested QoS) +AWS -> MQTT : SUBACK + +== Inbound Subscription == + +AWS -> MQTT : PUBLISH on subscribed topic +MQTT -> CC : didMessageReceived(payload, topic)\n→ onCloudMessageReceived() + @enduml Index: docs/SDD/Seq_CanFrameToCloud.png =================================================================== diff -u Binary files differ Index: docs/SDD/Seq_CanFrameToCloud.puml =================================================================== diff -u --- docs/SDD/Seq_CanFrameToCloud.puml (revision 0) +++ docs/SDD/Seq_CanFrameToCloud.puml (revision 8b98ff34313ee5d3f5d49672d622162acfc755c9) @@ -0,0 +1,54 @@ +@startuml Seq_CanFrameToCloud + +title Real-time Data Transfer — CAN Frame to Cloud + +participant "CAN Bus\n(SocketCAN)" as CANBUS +participant "Can::CanInterface" as CAN +participant "Can::MessageDispatcher" as DISP +participant "CloudConnectController" as CC +participant "MqttClient" as MQTT +participant "AWS IoT Core\n(MQTT broker)" as AWS + +== CAN frame(s) → CAN message == + +CANBUS -> CAN : frame available on the device +CAN -> CC : didFrameReceive(QCanBusFrame)\n→ onCanFrameReceive() +CC -> DISP : onFrameReceive(canId, payload) +DISP -> DISP : MessageBuilder::buildMessage()\nreassembles multi-frame Can::Message +alt Message not yet Complete + DISP -> DISP : continue accumulating CAN frames +else Complete Message Received + DISP -> CC : didActionReceive(Can::Message)\n→ onCanMessageReceive() +end + +== Publish CAN Message == + +alt msgId not in CAN routing + CC -> CC : discard (no action defined) +else action == Drop + CC -> CC : discard +else action == SendDelta AND payload unchanged + CC -> CC : discard (no change) +else action == SendAlways OR SendDelta with new payload + + CC -> CC : leahi::canMessageToProtobufByteArray()\n→ serialized protobuf + + alt serialization succeeded + CC -> MQTT : publish(topic, payload, pubId, QoS) + + alt client accepted the publish + MQTT -> AWS : PUBLISH + + AWS --> MQTT : PUBACK + MQTT -> CC : didMessageStatusChanged(id, Acknowledged, props)\n→ onCloudMessageStatusChanged() + else client rejected the publish + CC -> CC : log warning, message lost + end + else serialization failed + CC -> CC : log warning, message dropped + end + + CC -> CC : update message cache for SendDelta check +end + +@enduml Index: docs/SDD/Seq_RealtimeDataTransfer.png =================================================================== diff -u -r6d2ef8c97f4bb34204e95811839b3995000c47c1 -r8b98ff34313ee5d3f5d49672d622162acfc755c9 Binary files differ Index: docs/SDD/Seq_RealtimeDataTransfer.puml =================================================================== diff -u -r6d2ef8c97f4bb34204e95811839b3995000c47c1 -r8b98ff34313ee5d3f5d49672d622162acfc755c9 --- docs/SDD/Seq_RealtimeDataTransfer.puml (.../Seq_RealtimeDataTransfer.puml) (revision 6d2ef8c97f4bb34204e95811839b3995000c47c1) +++ docs/SDD/Seq_RealtimeDataTransfer.puml (.../Seq_RealtimeDataTransfer.puml) (revision 8b98ff34313ee5d3f5d49672d622162acfc755c9) @@ -2,52 +2,90 @@ title Real-time Data Transfer — End-to-End Scenario -participant "CANDumpPlayer" as CDP participant "CAN Bus\n(SocketCAN)" as CANBUS -participant "CloudConnect\n(CloudConnectController)" as LRC -participant "CloudConnectServer" as RS -participant "Luis application" as APP -participant "Cloud pipeline\n(planned)" as PIPE +participant "CloudConnect\n(CloudConnectController)" as CC +participant "AWS IoT Core\n(MQTT broker)" as AWS -note over CDP - Replays a candump log onto SocketCAN - at a configurable speed multiplier. - See Seq_CANDumpPlayer for detail. +== Startup (main.cpp, on the controller thread) == + +CC -> CC : listenForApp()\nbind /tmp/cloudconnect.sock +CC -> AWS : connectToCloud() → MqttClient::open()\nconnectToHostEncrypted(), mTLS, MQTT 3.1.1, port 8883 +AWS --> CC : CONNACK +CC -> CC : onCloudStateChanged(Connected)\nreset CAN/MQTT stats counters +CC -> AWS : SUBSCRIBE {prefix}/{deviceId}/clinical (QoS 1) +CC -> CC : startCan() → CanInterface::init() + +note over CC + Order is deliberate: the app socket and the broker + connection come up before CAN, so frames arriving + before CONNACK are dropped rather than queued. end note -CDP -> CANBUS : writeFrame(can_id, payload) -CANBUS -> LRC : didFrameReceive(QCanBusFrame) +== Steady state: CAN frame → cloud == -LRC -> LRC : MessageDispatcher reassembles\nmulti-frame Can::Message\n(see Seq_CANToCloud for detail) +CANBUS -> CC : didFrameReceive(QCanBusFrame) -alt action == Drop (or msgId not in _msgHandling) - LRC -> LRC : discard +CC -> CC : MessageDispatcher::onFrameReceive(canId, payload)\nreassembles multi-frame Can::Message\n→ didActionReceive(Can::Message) + +CC -> CC : record CAN stats\n(count, inter-arrival, running average) + +alt msgId not in _canRouting + CC -> CC : discard (no action defined) +else action == Drop + CC -> CC : discard else action == SendDelta AND payload unchanged\n(cached data.chopped(1) == new data.chopped(1)) - LRC -> LRC : discard (no change) + CC -> CC : discard (no change) else action == SendAlways OR SendDelta with new payload - LRC -> LRC : canMessageToProtobufByteArray()\n→ serialised protobuf bytes + CC -> CC : leahi::canMessageToProtobufByteArray()\n→ serialised protobuf bytes - LRC -> RS : send(topic, seq++, protobuf payload) - RS -> APP : CloudConnectFrame frame over\n/tmp/cloudconnect.sock + alt serialisation succeeded + CC -> AWS : PUBLISH (QoS 1, retain=false)\ntopic {prefix}/{deviceId}/clinical - LRC -->> PIPE : route → spool → publish + alt publish accepted by the client + CC -> CC : _inflight[pubId] = { Sent, msgId, sequence, sentUs } + AWS --> CC : PUBACK + CC -> CC : onCloudMessageStatusChanged(Acknowledged)\nmark ACK, record publish→ack latency + else publish rejected + CC -> CC : log warning, message lost\n(no spool today) + end + else serialisation failed + CC -> CC : log warning, message dropped + end + + CC -> CC : cachedMsg = msg (delta cache updated) end -note over PIPE - NOT YET IMPLEMENTED. +== Echo path (loopback via own subscription) == - The Connectivity Agent process and its UDS have been - removed. The replacement in-process pipeline is: +AWS -> CC : PUBLISH on the subscribed topic\n→ didMessageReceived(payload, topic) - Router → resolve MQTT topic from Type, - build Envelope - MessageSpool → durable insert BEFORE publish - MqttPublisher → QoS 1 PUBLISH to AWS IoT Core - → PUBACK → MessageSpool::remove() - SpoolDrain → replay spooled messages on reconnect +CC -> CC : parse Envelope → Header (msgId, sequence, timestamp) +CC -> CC : match _inflight by (msgId, sequence)\nrecord publish→receive latency +CC -> CC : msgIdToProtoName() → DescriptorPool lookup\n→ DynamicMessageFactory → MessageToJsonString()\n→ log decoded message - Until this lands, nothing is sent to the cloud. - See docs/SDD/AgentMigrationPlan.md Phases 2-3. +note over CANBUS, AWS + The device both publishes to and subscribes to the same + topic. The echo is a round-trip latency measurement for + bring-up, not a cloud→device command channel. end note +note over CC + No persistence today. A message that cannot be published + is logged and lost; the spool-on-failure design + (AgentMigrationPlan.md section 4.6) is still outstanding. + + Bring-up shortcuts currently in CloudConnectController: + * unknown routing action defaults to SendAlways, not Drop + * mqttTopic() ignores the per-message Topic and always + returns the "clinical" suffix + * the delta cache's "received" flag is never set, so the + SendDelta suppression branch never fires +end note + +note over CC + CloudConnectServer still binds the app Unix domain socket + at startup, but no message is written to it — the CAN path + now publishes straight to AWS IoT Core. +end note + @enduml Index: docs/SDD/Seq_Startup.png =================================================================== diff -u Binary files differ Index: docs/SDD/Seq_Startup.puml =================================================================== diff -u --- docs/SDD/Seq_Startup.puml (revision 0) +++ docs/SDD/Seq_Startup.puml (revision 8b98ff34313ee5d3f5d49672d622162acfc755c9) @@ -0,0 +1,37 @@ +@startuml Seq_Startup + +title Real-time Data Transfer — Startup + +participant "CAN Bus\n(SocketCAN)" as CANBUS +participant "Can::CanInterface" as CAN +participant "Can::MessageDispatcher" as DISP +participant "CloudConnectController" as CC +participant "MqttClient" as MQTT +participant "AWS IoT Core\n(MQTT broker)" as AWS + +== CloudConnectController: signal → slot == + +note over CAN, MQTT + CanInterface::didFrameReceive → CloudConnectController::onCanFrameReceive + MessageDispatcher::didActionReceive → CloudConnectController::onCanMessageReceive + MqttClient::didStateChanged → CloudConnectController::onCloudStateChanged + MqttClient::didMessageStatusChanged → CloudConnectController::onCloudMessageStatusChanged + MqttClient::didMessageReceived → CloudConnectController::onCloudMessageReceived +end note + +== Configuration (on main thread) == + +CC -> CC : loadConfig(CloudConnect.ini) +CC -> CC : loadCanRouting(LeahiCanRouting.ini) → CAN mesg handling +CC -> MQTT : init(MqttClient::Config)\nbuild connection with cert/key/CA from CloudConnect.ini + +== Bring-up (on controller thread) == + +CC -> MQTT : open MQTT connection +CC -> CAN : open CAN connection +CAN -> CANBUS : open the SocketCAN device +MQTT -> AWS : CONNECT (create encrypted connection to host) +AWS --> MQTT : CONNACK +MQTT -> CC : didStateChanged(Connected) + +@enduml Index: docs/SDD/Seq_Subscription.png =================================================================== diff -u Binary files differ Index: docs/SDD/Seq_Subscription.puml =================================================================== diff -u --- docs/SDD/Seq_Subscription.puml (revision 0) +++ docs/SDD/Seq_Subscription.puml (revision 8b98ff34313ee5d3f5d49672d622162acfc755c9) @@ -0,0 +1,23 @@ +@startuml Seq_Subscription + +title Real-time Data Transfer — Subscription + +participant "CloudConnectController" as CC +participant "MqttClient" as MQTT +participant "AWS IoT Core (MQTT broker)" as AWS + +== Subscribe (on every new session) == + +MQTT -> CC : didStateChanged(Connected)\n→ onCloudStateChanged() +CC -> MQTT : subscribe(topic, QoS) +MQTT -> AWS : SUBSCRIBE (topic filter, requested QoS) +AWS --> MQTT : SUBACK + +== Subscription Delivery == + +AWS -> MQTT : PUBLISH on the subscribed topic +MQTT -> CC : didMessageReceived(payload, topic)\n→ onCloudMessageReceived() +CC -> CC : parse Envelope → Header\n(msgId, sequence, timestamp) +CC -> CC : convert serialized Protobuf payload → JSON for output + +@enduml Index: docs/SDD/SoftwareArchitecture.png =================================================================== diff -u -r6d2ef8c97f4bb34204e95811839b3995000c47c1 -r8b98ff34313ee5d3f5d49672d622162acfc755c9 Binary files differ Index: docs/SDD/SoftwareArchitecture.puml =================================================================== diff -u -r6d2ef8c97f4bb34204e95811839b3995000c47c1 -r8b98ff34313ee5d3f5d49672d622162acfc755c9 --- docs/SDD/SoftwareArchitecture.puml (.../SoftwareArchitecture.puml) (revision 6d2ef8c97f4bb34204e95811839b3995000c47c1) +++ docs/SDD/SoftwareArchitecture.puml (.../SoftwareArchitecture.puml) (revision 8b98ff34313ee5d3f5d49672d622162acfc755c9) @@ -5,89 +5,94 @@ skinparam component { BackgroundColor<> LightYellow BackgroundColor<> LightGray + BackgroundColor<> LightCyan BackgroundColor<> #E8E8F8 BorderStyle<> dashed + BackgroundColor<> #EEEEEE + BorderStyle<> dashed } title Leahi Real-time CDT — Component Architecture -note as N1 - The Connectivity Agent process and its Unix domain socket - have been removed; CloudConnect is becoming the single - conduit to the cloud (docs/SDD/AgentMigrationPlan.md). - Dashed = planned, not yet implemented. -end note - node "Leahi Device" { node "CloudConnect (process)" { - component "CloudConnectController" as LRC + component "CloudConnectController" as CC component "Can::CanInterface" as CAN - component "Cloud pipeline\n(MessageSpool, Router,\nMqttPublisher, SpoolDrain)" as PIPE <> + component "MqttClient\n(QMqttClient, TLS/mTLS)" as MQTT + component "MessageSpool + SpoolDrain\n(durability on publish failure)" as SPOOL <> } node "Comms (lib)" { - component "CloudConnectServer\n(local-socket server)" as RS + component "CloudConnectServer\n(local-socket server)" as RS <> component "Can::MessageDispatcher\n(per-CAN-id reassembly)" as MDISP component "Can::MessageBuilder\n(frame ↔ message)" as MB - component "CloudConnectFrame\n(framing + CRC)" as AM + component "CloudConnectFrame\n(framing + CRC)" as AM <> } node "MsgUtils (lib)" { - component "LeahiMsgDefs\n(generated C++)" as MD <> + component "LeahiMsgDefs\n(generated C++ + protobuf stubs)" as MD <> } interface "CAN Bus\n(SocketCAN)" as CANBUS - interface "Unix Domain Socket\n(/tmp/cloudconnect.sock)" as APPUDS + interface "Unix Domain Socket\n(/tmp/cloudconnect.sock)" as APPUDS <> } -node "Luis application" { - component "Application client" as APP +node "Test tools" { + component "CANDumpPlayer\n(replays candump log → SocketCAN)" as CDP <> + component "DCSsim\n(local MQTT broker, TLS/mTLS)" as DCS <> } -node "CANDumpPlayer (tool)" { - component "CAN dump replay\n(writeFrame → SocketCAN)" as CDP -} - cloud "AWS Cloud" as CLOUD <> { - component "AWS IoT Core\n(MQTT broker)" as IOTCORE <> - component "Amazon S3\n(log bundles)" as S3 <> + component "AWS IoT Core\n(MQTT broker, port 8883)" as IOTCORE <> + component "Amazon S3\n(log bundles)" as S3 <> } node "Build-time (scripts)" { component "MsgUtils Python\n(msgutils)" as PYUTILS <> - component "Jinja2 Templates\n(MsgDefs_*.jinja)" as JINJA <> + component "Jinja2 Templates\n(MsgDefs_*.jinja, CanRoutingIni.jinja)" as JINJA <> component "protoc" as PROTOC <> component "LeahiMsgDefs.proto\n(generated)" as PROTO <> component "LeahiMsgDefs.h/.cpp\n(generated)" as GENCPP <> component "Protobuf stubs\n(.pb.h/.pb.cc)" as PBSTUBS <> + component "LeahiCanRouting.ini\n(generated, hand-edits preserved)" as CANINI <> } +' --- runtime data flow --- CDP --> CANBUS : writeFrame (replay) CANBUS --> CAN -CAN --> LRC : didFrameReceive -LRC --> MDISP : onFrameReceive(canId, payload) +CAN --> CC : didFrameReceive +CC --> MDISP : onFrameReceive(canId, payload) MDISP --> MB : buildMessage() MB --> MDISP : Can::Message -MDISP --> LRC : didActionReceive(Can::Message) -LRC --> MD : canMessageToProtobufByteArray() -MD --> AM : protobuf bytes -LRC --> RS : send(Type, seq, payload) -RS --> AM : build() -RS --> APPUDS : write frame -APPUDS --> APP : readyRead +MDISP --> CC : didActionReceive(Can::Message) +CC --> MD : canMessageToProtobufByteArray() +MD --> CC : protobuf bytes +CC --> MQTT : publish(topic, payload, qos = 1) +MQTT --> IOTCORE : PUBLISH / SUBSCRIBE (mTLS 8883) +IOTCORE --> MQTT : PUBACK + echo PUBLISH +MQTT --> DCS : same client, broker swapped\nvia Cloud/ServerAddress in the INI -LRC ..> PIPE : route + spool (planned) -PIPE ..> IOTCORE : PUBLISH QoS 1 (mTLS 8883) -PIPE ..> S3 : multipart upload (HTTPS) +' --- present but idle --- +CC ..> RS : listen() only; send() has no call site +RS ..> AM : build() +RS ..> APPUDS : (no traffic) +' --- planned --- +CC ..> SPOOL : spool on publish failure (planned) +SPOOL ..> MQTT : replay on reconnect (planned) +CC ..> S3 : log bundle upload, HTTPS (planned) + +' --- build-time flow --- PYUTILS --> JINJA : renders JINJA --> PROTO : generates JINJA --> GENCPP : generates +JINJA --> CANINI : generates PROTO --> PROTOC : input PROTOC --> PBSTUBS : generates GENCPP ..> MD : compiled into PBSTUBS ..> MD : compiled into +CANINI ..> CC : loadCanRouting() at startup @enduml