From 5aafb6c9831dbcfb8f0b6660c39ac767db4b619a Mon Sep 17 00:00:00 2001 From: Chipster Date: Mon, 10 Aug 2026 17:37:55 -0500 Subject: [PATCH] Publish structured link/connectivity status DAPNETGateway had no MQTT link status at all, and no retain support in its MQTT layer. Adds a link Kind (linking/unlinked/failed) and publishes it from the places connectivity already changes state but was previously only logged: successful login, a rejected auth key (detected asynchronously from the server's own protocol response, not the initial TCP connect), a dropped connection triggering the existing reconnect loop, a failed startup socket open, and clean shutdown. --- DAPNETGateway.cpp | 9 +++++++-- DAPNETNetwork.cpp | 2 ++ Log.cpp | 17 +++++++++++++++-- Log.h | 8 +++++++- MQTTConnection.cpp | 14 +++++++------- MQTTConnection.h | 6 +++--- schema.json | 12 +++++++++++- 7 files changed, 52 insertions(+), 16 deletions(-) diff --git a/DAPNETGateway.cpp b/DAPNETGateway.cpp index 6118d84..313ac76 100644 --- a/DAPNETGateway.cpp +++ b/DAPNETGateway.cpp @@ -294,6 +294,7 @@ int CDAPNETGateway::run() delete m_dapnetNetwork; ::LogError("Cannot open the DAPNET network port"); + writeJSONLink("failed", "socket"); return 1; } @@ -307,6 +308,7 @@ int CDAPNETGateway::run() delete m_dapnetNetwork; ::LogError("Cannot login to the DAPNET network"); + writeJSONLink("failed", "socket"); return 1; } @@ -358,8 +360,10 @@ int CDAPNETGateway::run() } bool ok = m_dapnetNetwork->read(); - if (!ok) + if (!ok) { + writeJSONLink("unlinked", "lost"); recover(); + } CPOCSAGMessage* message = m_dapnetNetwork->readMessage(); if (message != nullptr) { @@ -443,6 +447,7 @@ int CDAPNETGateway::run() LogInfo("DAPNETGateway is stopping"); writeJSONStatus("DAPNETGateway is stopping"); + writeJSONLink("unlinked", ""); m_pocsagNetwork->close(); delete m_pocsagNetwork; @@ -643,6 +648,6 @@ void CDAPNETGateway::writeJSONStatus(const std::string& status) json["timestamp"] = CUtils::createTimestamp(); json["message"] = status; - WriteJSON("status", json); + WriteJSON("status", json, false); } diff --git a/DAPNETNetwork.cpp b/DAPNETNetwork.cpp index de535ca..40661eb 100644 --- a/DAPNETNetwork.cpp +++ b/DAPNETNetwork.cpp @@ -100,6 +100,7 @@ bool CDAPNETNetwork::read() if (!m_loggedIn) { m_loggedIn = true; LogMessage("Logged into the DAPNET network"); + writeJSONLink("linking", ""); } // Time synchronisation char* p = ::strchr((char*)buffer, '\n'); @@ -271,6 +272,7 @@ bool CDAPNETNetwork::parseFailedLogin(unsigned char* data) assert(p != nullptr); LogMessage("Login failed: %s", p); + writeJSONLink("failed", "auth"); CThread::sleep(BACKOFF[m_failCount]); if (m_failCount < 9) diff --git a/Log.cpp b/Log.cpp index 222d18e..6e2751f 100644 --- a/Log.cpp +++ b/Log.cpp @@ -18,6 +18,7 @@ #include "Log.h" #include "MQTTConnection.h" +#include "Utils.h" #if defined(_WIN32) || defined(_WIN64) #include @@ -94,14 +95,26 @@ void Log(unsigned int level, const char* fmt, ...) exit(1); } -void WriteJSON(const std::string& topLevel, nlohmann::json& json) +void WriteJSON(const std::string& topLevel, nlohmann::json& json, bool retain) { if (m_mqtt != nullptr) { nlohmann::json top; top[topLevel] = json; - m_mqtt->publish("json", top.dump()); + m_mqtt->publish("json", top.dump(), retain); } } +void writeJSONLink(const std::string& action, const std::string& reason) +{ + nlohmann::json json; + + json["timestamp"] = CUtils::createTimestamp(); + json["action"] = action; + if (!reason.empty()) + json["reason"] = reason; + + WriteJSON("link", json, true); +} + diff --git a/Log.h b/Log.h index 7392164..f4e012f 100644 --- a/Log.h +++ b/Log.h @@ -35,6 +35,12 @@ extern void Log(unsigned int level, const char* fmt, ...); extern void LogInitialise(unsigned int displayLevel, unsigned int mqttLevel); extern void LogFinalise(); -extern void WriteJSON(const std::string& topLevel, nlohmann::json& json); +extern void WriteJSON(const std::string& topLevel, nlohmann::json& json, bool retain); + +// Publishes a "link" Kind directly, for connectivity events (DAPNET login +// success/failure/loss) that CDAPNETNetwork detects but has no WriteJSON +// access of its own (it isn't CDAPNETGateway) -- same free-function shape +// as APRSGateway's writeJSONLink. +extern void writeJSONLink(const std::string& action, const std::string& reason); #endif diff --git a/MQTTConnection.cpp b/MQTTConnection.cpp index bb23703..6277a4b 100644 --- a/MQTTConnection.cpp +++ b/MQTTConnection.cpp @@ -99,22 +99,22 @@ bool CMQTTConnection::open() return true; } -bool CMQTTConnection::publish(const char* topic, const char* text) +bool CMQTTConnection::publish(const char* topic, const char* text, bool retain) { assert(topic != nullptr); assert(text != nullptr); - return publish(topic, (unsigned char*)text, (unsigned int)::strlen(text)); + return publish(topic, (unsigned char*)text, (unsigned int)::strlen(text), retain); } -bool CMQTTConnection::publish(const char* topic, const std::string& text) +bool CMQTTConnection::publish(const char* topic, const std::string& text, bool retain) { assert(topic != nullptr); - return publish(topic, (unsigned char*)text.c_str(), (unsigned int)text.size()); + return publish(topic, (unsigned char*)text.c_str(), (unsigned int)text.size(), retain); } -bool CMQTTConnection::publish(const char* topic, const unsigned char* data, unsigned int len) +bool CMQTTConnection::publish(const char* topic, const unsigned char* data, unsigned int len, bool retain) { assert(topic != nullptr); assert(data != nullptr); @@ -126,13 +126,13 @@ bool CMQTTConnection::publish(const char* topic, const unsigned char* data, unsi char topicEx[100U]; ::sprintf(topicEx, "%s/%s", m_name.c_str(), topic); - int rc = ::mosquitto_publish(m_mosq, nullptr, topicEx, len, data, static_cast(m_qos), false); + int rc = ::mosquitto_publish(m_mosq, nullptr, topicEx, len, data, static_cast(m_qos), retain); if (rc != MOSQ_ERR_SUCCESS) { ::fprintf(stderr, "MQTT Error publishing: %s\n", ::mosquitto_strerror(rc)); return false; } } else { - int rc = ::mosquitto_publish(m_mosq, nullptr, topic, len, data, static_cast(m_qos), false); + int rc = ::mosquitto_publish(m_mosq, nullptr, topic, len, data, static_cast(m_qos), retain); if (rc != MOSQ_ERR_SUCCESS) { ::fprintf(stderr, "MQTT Error publishing: %s\n", ::mosquitto_strerror(rc)); return false; diff --git a/MQTTConnection.h b/MQTTConnection.h index 2521460..86d8c2e 100644 --- a/MQTTConnection.h +++ b/MQTTConnection.h @@ -37,9 +37,9 @@ public: bool open(); - bool publish(const char* topic, const char* text); - bool publish(const char* topic, const std::string& text); - bool publish(const char* topic, const unsigned char* data, unsigned int len); + bool publish(const char* topic, const char* text, bool retain = false); + bool publish(const char* topic, const std::string& text, bool retain = false); + bool publish(const char* topic, const unsigned char* data, unsigned int len, bool retain = false); void close(); diff --git a/schema.json b/schema.json index 58be79f..bf9d0e3 100644 --- a/schema.json +++ b/schema.json @@ -1,6 +1,8 @@ { "$defs": { - "timestamp": {"type": "string"} + "timestamp": {"type": "string"}, + "action": {"type": "string", "enum": ["linking", "unlinked", "failed"]}, + "reason": {"type": "string", "enum": ["auth", "socket", "lost"]} }, "status": { @@ -8,6 +10,14 @@ "timestamp": {"$ref": "#/$defs/timestamp"}, "message": {"type": "string"}, "required": ["timestamp", "message"] + }, + + "link": { + "type": "object", + "timestamp": {"$ref": "#/$defs/timestamp"}, + "action": {"$ref": "#/$defs/action"}, + "reason": {"$ref": "#/$defs/reason"}, + "required": ["timestamp", "action"] } }