From e677d6a9545ace654deba7006bd418ffdca343e7 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Fri, 7 Jul 2023 17:57:36 +0100 Subject: [PATCH 01/12] Convert the gateway to log via MQTT. --- Conf.cpp | 62 +++++++------ Conf.h | 97 +++++++++++---------- DAPNETGateway.cpp | 26 +++--- DAPNETGateway.h | 1 - DAPNETGateway.ini | 11 ++- Log.cpp | 135 ++++++----------------------- Log.h | 8 +- MQTTConnection.cpp | 211 +++++++++++++++++++++++++++++++++++++++++++++ MQTTConnection.h | 63 ++++++++++++++ Makefile | 7 +- Utils.cpp | 76 +--------------- Utils.h | 11 +-- Version.h | 4 +- 13 files changed, 423 insertions(+), 289 deletions(-) create mode 100644 MQTTConnection.cpp create mode 100644 MQTTConnection.h diff --git a/Conf.cpp b/Conf.cpp index 77b7e5b..7c51223 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018,2020 by Jonathan Naylor G4KLX + * Copyright (C) 2018,2020,2023 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,10 +27,11 @@ const int BUFFER_SIZE = 500; enum SECTION { - SECTION_NONE, - SECTION_GENERAL, - SECTION_LOG, - SECTION_DAPNET + SECTION_NONE, + SECTION_GENERAL, + SECTION_LOG, + SECTION_MQTT, + SECTION_DAPNET }; CConf::CConf(const std::string& file) : @@ -45,10 +46,11 @@ m_myAddress(), m_myPort(0U), m_daemon(false), m_logDisplayLevel(0U), -m_logFileLevel(0U), -m_logFilePath(), -m_logFileRoot(), -m_logFileRotate(true), +m_logMQTTLevel(0U), +m_mqttAddress("127.0.0.1"), +m_mqttPort(1883U), +m_mqttKeepalive(60U), +m_mqttName("dapnet-gateway"), m_dapnetAddress(), m_dapnetPort(0U), m_dapnetAuthKey(), @@ -80,6 +82,8 @@ bool CConf::read() section = SECTION_GENERAL; else if (::strncmp(buffer, "[Log]", 5U) == 0) section = SECTION_LOG; + else if (::strncmp(buffer, "[MQTT]", 6U) == 0) + section = SECTION_MQTT; else if (::strncmp(buffer, "[DAPNET]", 8U) == 0) section = SECTION_DAPNET; else @@ -153,16 +157,19 @@ bool CConf::read() else if (::strcmp(key, "Daemon") == 0) m_daemon = ::atoi(value) == 1; } else if (section == SECTION_LOG) { - if (::strcmp(key, "FilePath") == 0) - m_logFilePath = value; - else if (::strcmp(key, "FileRoot") == 0) - m_logFileRoot = value; - else if (::strcmp(key, "FileLevel") == 0) - m_logFileLevel = (unsigned int)::atoi(value); + if (::strcmp(key, "MQTTLevel") == 0) + m_logMQTTLevel = (unsigned int)::atoi(value); else if (::strcmp(key, "DisplayLevel") == 0) m_logDisplayLevel = (unsigned int)::atoi(value); - else if (::strcmp(key, "FileRotate") == 0) - m_logFileRotate = ::atoi(value) == 1; + } else if (section == SECTION_MQTT) { + if (::strcmp(key, "Address") == 0) + m_mqttAddress = value; + else if (::strcmp(key, "Port") == 0) + m_mqttPort = (unsigned short)::atoi(value); + else if (::strcmp(key, "Keepalive") == 0) + m_mqttKeepalive = (unsigned int)::atoi(value); + else if (::strcmp(key, "Name") == 0) + m_mqttName = value; } else if (section == SECTION_DAPNET) { if (::strcmp(key, "Address") == 0) m_dapnetAddress = value; @@ -239,24 +246,29 @@ unsigned int CConf::getLogDisplayLevel() const return m_logDisplayLevel; } -unsigned int CConf::getLogFileLevel() const +unsigned int CConf::getLogMQTTLevel() const { - return m_logFileLevel; + return m_logMQTTLevel; } -std::string CConf::getLogFilePath() const +std::string CConf::getMQTTAddress() const { - return m_logFilePath; + return m_mqttAddress; } -std::string CConf::getLogFileRoot() const +unsigned short CConf::getMQTTPort() const { - return m_logFileRoot; + return m_mqttPort; } -bool CConf::getLogFileRotate() const +unsigned int CConf::getMQTTKeepalive() const { - return m_logFileRotate; + return m_mqttKeepalive; +} + +std::string CConf::getMQTTName() const +{ + return m_mqttName; } std::string CConf::getDAPNETAddress() const diff --git a/Conf.h b/Conf.h index 4faf12d..c2cc26c 100644 --- a/Conf.h +++ b/Conf.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 by Jonathan Naylor G4KLX + * Copyright (C) 2018,2023 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,61 +25,66 @@ class CConf { public: - CConf(const std::string& file); - ~CConf(); + CConf(const std::string& file); + ~CConf(); - bool read(); + bool read(); - // The General section - std::string getCallsign() const; - std::vector getWhiteList() const; - std::vector getBlackList() const; - std::string getblacklistRegexfile() const; - std::string getwhitelistRegexfile() const; - std::string getRptAddress() const; - unsigned short getRptPort() const; - std::string getMyAddress() const; - unsigned short getMyPort() const; - bool getDaemon() const; + // The General section + std::string getCallsign() const; + std::vector getWhiteList() const; + std::vector getBlackList() const; + std::string getblacklistRegexfile() const; + std::string getwhitelistRegexfile() const; + std::string getRptAddress() const; + unsigned short getRptPort() const; + std::string getMyAddress() const; + unsigned short getMyPort() const; + bool getDaemon() const; - // The Log section - unsigned int getLogDisplayLevel() const; - unsigned int getLogFileLevel() const; - std::string getLogFilePath() const; - std::string getLogFileRoot() const; - bool getLogFileRotate() const; + // The Log section + unsigned int getLogDisplayLevel() const; + unsigned int getLogMQTTLevel() const; - // The DAPNET section - std::string getDAPNETAddress() const; - unsigned short getDAPNETPort() const; - std::string getDAPNETAuthKey() const; - bool getDAPNETDebug() const; + // The MQTT section + std::string getMQTTAddress() const; + unsigned short getMQTTPort() const; + unsigned int getMQTTKeepalive() const; + std::string getMQTTName() const; + + // The DAPNET section + std::string getDAPNETAddress() const; + unsigned short getDAPNETPort() const; + std::string getDAPNETAuthKey() const; + bool getDAPNETDebug() const; private: - std::string m_file; + std::string m_file; - std::string m_callsign; - std::vector m_whiteList; - std::vector m_blackList; + std::string m_callsign; + std::vector m_whiteList; + std::vector m_blackList; - std::string m_blacklistRegexfile; - std::string m_whitelistRegexfile; - std::string m_rptAddress; - unsigned short m_rptPort; - std::string m_myAddress; - unsigned short m_myPort; - bool m_daemon; + std::string m_blacklistRegexfile; + std::string m_whitelistRegexfile; + std::string m_rptAddress; + unsigned short m_rptPort; + std::string m_myAddress; + unsigned short m_myPort; + bool m_daemon; - unsigned int m_logDisplayLevel; - unsigned int m_logFileLevel; - std::string m_logFilePath; - std::string m_logFileRoot; - bool m_logFileRotate; + unsigned int m_logDisplayLevel; + unsigned int m_logMQTTLevel; - std::string m_dapnetAddress; - unsigned short m_dapnetPort; - std::string m_dapnetAuthKey; - bool m_dapnetDebug; + std::string m_mqttAddress; + unsigned short m_mqttPort; + unsigned int m_mqttKeepalive; + std::string m_mqttName; + + std::string m_dapnetAddress; + unsigned short m_dapnetPort; + std::string m_dapnetAuthKey; + bool m_dapnetDebug; }; #endif diff --git a/DAPNETGateway.cpp b/DAPNETGateway.cpp index 834c5ba..196b0f1 100644 --- a/DAPNETGateway.cpp +++ b/DAPNETGateway.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2018,2020 by Jonathan Naylor G4KLX +* Copyright (C) 2018,2020,2023 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,6 +16,7 @@ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ +#include "MQTTConnection.h" #include "DAPNETGateway.h" #include "StopWatch.h" #include "Version.h" @@ -41,6 +42,9 @@ const char* DEFAULT_INI_FILE = "DAPNETGateway.ini"; const char* DEFAULT_INI_FILE = "/etc/DAPNETGateway.ini"; #endif +// In Log.cpp +extern CMQTTConnection* m_mqtt; + #include #include @@ -192,16 +196,6 @@ int CDAPNETGateway::run() } #endif -#if !defined(_WIN32) && !defined(_WIN64) - ret = ::LogInitialise(m_daemon, m_conf.getLogFilePath(), m_conf.getLogFileRoot(), m_conf.getLogFileLevel(), m_conf.getLogDisplayLevel(), m_conf.getLogFileRotate()); -#else - ret = ::LogInitialise(false, m_conf.getLogFilePath(), m_conf.getLogFileRoot(), m_conf.getLogFileLevel(), m_conf.getLogDisplayLevel(), m_conf.getLogFileRotate()); -#endif - if (!ret) { - ::fprintf(stderr, "DAPNETGateway: unable to open the log file\n"); - return 1; - } - #if !defined(_WIN32) && !defined(_WIN64) if (m_daemon) { ::close(STDIN_FILENO); @@ -210,6 +204,16 @@ int CDAPNETGateway::run() } #endif + ::LogInitialise(m_conf.getLogDisplayLevel(), m_conf.getLogMQTTLevel()); + + std::vector> subscriptions; + m_mqtt = new CMQTTConnection(m_conf.getMQTTAddress(), m_conf.getMQTTPort(), m_conf.getMQTTName(), subscriptions, m_conf.getMQTTKeepalive()); + ret = m_mqtt->open(); + if (!ret) { + delete m_mqtt; + return -1; + } + bool debug = m_conf.getDAPNETDebug(); std::string rptAddress = m_conf.getRptAddress(); diff --git a/DAPNETGateway.h b/DAPNETGateway.h index d3dbb17..28ed57a 100644 --- a/DAPNETGateway.h +++ b/DAPNETGateway.h @@ -60,7 +60,6 @@ private: unsigned int calculateCodewords(const CPOCSAGMessage* message) const; void loadSchedule(); bool sendMessage(CPOCSAGMessage* message) const; - }; #endif diff --git a/DAPNETGateway.ini b/DAPNETGateway.ini index 1610a0d..cd3e282 100644 --- a/DAPNETGateway.ini +++ b/DAPNETGateway.ini @@ -13,10 +13,13 @@ Daemon=0 [Log] # Logging levels, 0=No logging DisplayLevel=1 -FileLevel=1 -FilePath=. -FileRoot=DAPNETGateway -FileRotate=1 +MQTTLevel=1 + +[MQTT] +Address=127.0.0.1 +Port=1883 +Keepalive=60 +Name=dapnet-gateway [DAPNET] Address=dapnet.afu.rwth-aachen.de diff --git a/Log.cpp b/Log.cpp index 752601e..4537544 100644 --- a/Log.cpp +++ b/Log.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2020 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2020,2022,2023 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,6 +17,7 @@ */ #include "Log.h" +#include "MQTTConnection.h" #if defined(_WIN32) || defined(_WIN64) #include @@ -32,117 +33,27 @@ #include #include -static unsigned int m_fileLevel = 2U; -static std::string m_filePath; -static std::string m_fileRoot; -static bool m_fileRotate = true; +CMQTTConnection* m_mqtt = NULL; -static FILE* m_fpLog = NULL; -static bool m_daemon = false; +static unsigned int m_mqttLevel = 2U; static unsigned int m_displayLevel = 2U; -static struct tm m_tm; - static char LEVELS[] = " DMIWEF"; -static bool logOpenRotate() +void LogInitialise(unsigned int displayLevel, unsigned int mqttLevel) { - bool status = false; - - if (m_fileLevel == 0U) - return true; - - time_t now; - ::time(&now); - - struct tm* tm = ::gmtime(&now); - - if (tm->tm_mday == m_tm.tm_mday && tm->tm_mon == m_tm.tm_mon && tm->tm_year == m_tm.tm_year) { - if (m_fpLog != NULL) - return true; - } else { - if (m_fpLog != NULL) - ::fclose(m_fpLog); - } - - char filename[200U]; -#if defined(_WIN32) || defined(_WIN64) - ::sprintf(filename, "%s\\%s-%04d-%02d-%02d.log", m_filePath.c_str(), m_fileRoot.c_str(), tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); -#else - ::sprintf(filename, "%s/%s-%04d-%02d-%02d.log", m_filePath.c_str(), m_fileRoot.c_str(), tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); -#endif - - if ((m_fpLog = ::fopen(filename, "a+t")) != NULL) { - status = true; - -#if !defined(_WIN32) && !defined(_WIN64) - if (m_daemon) - dup2(fileno(m_fpLog), fileno(stderr)); -#endif - } - - m_tm = *tm; - - return status; -} - -static bool logOpenNoRotate() -{ - bool status = false; - - if (m_fileLevel == 0U) - return true; - - if (m_fpLog != NULL) - return true; - - char filename[200U]; -#if defined(_WIN32) || defined(_WIN64) - ::sprintf(filename, "%s\\%s.log", m_filePath.c_str(), m_fileRoot.c_str()); -#else - ::sprintf(filename, "%s/%s.log", m_filePath.c_str(), m_fileRoot.c_str()); -#endif - - if ((m_fpLog = ::fopen(filename, "a+t")) != NULL) { - status = true; - -#if !defined(_WIN32) && !defined(_WIN64) - if (m_daemon) - dup2(fileno(m_fpLog), fileno(stderr)); -#endif - } - - return status; -} - -bool LogOpen() -{ - if (m_fileRotate) - return logOpenRotate(); - else - return logOpenNoRotate(); -} - -bool LogInitialise(bool daemon, const std::string& filePath, const std::string& fileRoot, unsigned int fileLevel, unsigned int displayLevel, bool rotate) -{ - m_filePath = filePath; - m_fileRoot = fileRoot; - m_fileLevel = fileLevel; + m_mqttLevel = mqttLevel; m_displayLevel = displayLevel; - m_daemon = daemon; - m_fileRotate = rotate; - - if (m_daemon) - m_displayLevel = 0U; - - return ::LogOpen(); } void LogFinalise() { - if (m_fpLog != NULL) - ::fclose(m_fpLog); + if (m_mqtt != NULL) { + m_mqtt->close(); + delete m_mqtt; + m_mqtt = NULL; + } } void Log(unsigned int level, const char* fmt, ...) @@ -171,22 +82,26 @@ void Log(unsigned int level, const char* fmt, ...) va_end(vl); - if (level >= m_fileLevel && m_fileLevel != 0U) { - bool ret = ::LogOpen(); - if (!ret) - return; - - ::fprintf(m_fpLog, "%s\n", buffer); - ::fflush(m_fpLog); - } + if (m_mqtt != NULL && level >= m_mqttLevel && m_mqttLevel != 0U) + m_mqtt->publish("log", buffer); if (level >= m_displayLevel && m_displayLevel != 0U) { ::fprintf(stdout, "%s\n", buffer); ::fflush(stdout); } - if (level == 6U) { // Fatal - ::fclose(m_fpLog); + if (level == 6U) // Fatal exit(1); +} + +void WriteJSON(const std::string& topLevel, nlohmann::json& json) +{ + if (m_mqtt != NULL) { + nlohmann::json top; + + top[topLevel] = json; + + m_mqtt->publish("json", top.dump()); } } + diff --git a/Log.h b/Log.h index ae95b60..7392164 100644 --- a/Log.h +++ b/Log.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2020 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2020,2022,2023 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,6 +21,8 @@ #include +#include + #define LogDebug(fmt, ...) Log(1U, fmt, ##__VA_ARGS__) #define LogMessage(fmt, ...) Log(2U, fmt, ##__VA_ARGS__) #define LogInfo(fmt, ...) Log(3U, fmt, ##__VA_ARGS__) @@ -30,7 +32,9 @@ extern void Log(unsigned int level, const char* fmt, ...); -extern bool LogInitialise(bool daemon, const std::string& filePath, const std::string& fileRoot, unsigned int fileLevel, unsigned int displayLevel, bool rotate); +extern void LogInitialise(unsigned int displayLevel, unsigned int mqttLevel); extern void LogFinalise(); +extern void WriteJSON(const std::string& topLevel, nlohmann::json& json); + #endif diff --git a/MQTTConnection.cpp b/MQTTConnection.cpp new file mode 100644 index 0000000..fa951e5 --- /dev/null +++ b/MQTTConnection.cpp @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2022,2023 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "MQTTConnection.h" + +#include +#include +#include + + +CMQTTConnection::CMQTTConnection(const std::string& host, unsigned short port, const std::string& name, const std::vector>& subs, unsigned int keepalive, MQTT_QOS qos) : +m_host(host), +m_port(port), +m_name(name), +m_subs(subs), +m_keepalive(keepalive), +m_qos(qos), +m_mosq(NULL), +m_connected(false) +{ + assert(!host.empty()); + assert(port > 0U); + assert(!name.empty()); + assert(keepalive >= 5U); + + ::mosquitto_lib_init(); +} + +CMQTTConnection::~CMQTTConnection() +{ + ::mosquitto_lib_cleanup(); +} + +bool CMQTTConnection::open() +{ + m_mosq = ::mosquitto_new(m_name.c_str(), true, this); + if (m_mosq == NULL){ + ::fprintf(stderr, "MQTT Error newing: Out of memory.\n"); + return false; + } + + ::mosquitto_connect_callback_set(m_mosq, onConnect); + ::mosquitto_subscribe_callback_set(m_mosq, onSubscribe); + ::mosquitto_message_callback_set(m_mosq, onMessage); + ::mosquitto_disconnect_callback_set(m_mosq, onDisconnect); + + int rc = ::mosquitto_connect(m_mosq, m_host.c_str(), m_port, m_keepalive); + if (rc != MOSQ_ERR_SUCCESS) { + ::mosquitto_destroy(m_mosq); + m_mosq = NULL; + ::fprintf(stderr, "MQTT Error connecting: %s\n", ::mosquitto_strerror(rc)); + return false; + } + + rc = ::mosquitto_loop_start(m_mosq); + if (rc != MOSQ_ERR_SUCCESS) { + ::mosquitto_disconnect(m_mosq); + ::mosquitto_destroy(m_mosq); + m_mosq = NULL; + ::fprintf(stderr, "MQTT Error loop starting: %s\n", ::mosquitto_strerror(rc)); + return false; + } + + return true; +} + +bool CMQTTConnection::publish(const char* topic, const char* text) +{ + assert(topic != NULL); + assert(text != NULL); + + return publish(topic, (unsigned char*)text, ::strlen(text)); +} + +bool CMQTTConnection::publish(const char* topic, const std::string& text) +{ + assert(topic != NULL); + + return publish(topic, (unsigned char*)text.c_str(), text.size()); +} + +bool CMQTTConnection::publish(const char* topic, const unsigned char* data, unsigned int len) +{ + assert(topic != NULL); + assert(data != NULL); + + if (!m_connected) + return false; + + if (::strchr(topic, '/') == NULL) { + char topicEx[100U]; + ::sprintf(topicEx, "%s/%s", m_name.c_str(), topic); + + int rc = ::mosquitto_publish(m_mosq, NULL, topicEx, len, data, static_cast(m_qos), false); + if (rc != MOSQ_ERR_SUCCESS) { + ::fprintf(stderr, "MQTT Error publishing: %s\n", ::mosquitto_strerror(rc)); + return false; + } + } else { + int rc = ::mosquitto_publish(m_mosq, NULL, topic, len, data, static_cast(m_qos), false); + if (rc != MOSQ_ERR_SUCCESS) { + ::fprintf(stderr, "MQTT Error publishing: %s\n", ::mosquitto_strerror(rc)); + return false; + } + } + + return true; +} + +void CMQTTConnection::close() +{ + if (m_mosq != NULL) { + ::mosquitto_disconnect(m_mosq); + ::mosquitto_destroy(m_mosq); + m_mosq = NULL; + } +} + +void CMQTTConnection::onConnect(mosquitto* mosq, void* obj, int rc) +{ + assert(mosq != NULL); + assert(obj != NULL); + + ::fprintf(stdout, "MQTT: on_connect: %s\n", ::mosquitto_connack_string(rc)); + if (rc != 0) { + ::mosquitto_disconnect(mosq); + return; + } + + CMQTTConnection* p = static_cast(obj); + p->m_connected = true; + + for (std::vector>::const_iterator it = p->m_subs.cbegin(); it != p->m_subs.cend(); ++it) { + std::string topic = (*it).first; + + if (topic.find_first_of('/') == std::string::npos) { + char topicEx[100U]; + ::sprintf(topicEx, "%s/%s", p->m_name.c_str(), topic.c_str()); + + rc = ::mosquitto_subscribe(mosq, NULL, topicEx, static_cast(p->m_qos)); + if (rc != MOSQ_ERR_SUCCESS) { + ::fprintf(stderr, "MQTT: error subscribing to %s - %s\n", topicEx, ::mosquitto_strerror(rc)); + ::mosquitto_disconnect(mosq); + } + } else { + rc = ::mosquitto_subscribe(mosq, NULL, topic.c_str(), static_cast(p->m_qos)); + if (rc != MOSQ_ERR_SUCCESS) { + ::fprintf(stderr, "MQTT: error subscribing to %s - %s\n", topic.c_str(), ::mosquitto_strerror(rc)); + ::mosquitto_disconnect(mosq); + } + } + } +} + +void CMQTTConnection::onSubscribe(mosquitto* mosq, void* obj, int mid, int qosCount, const int* grantedQOS) +{ + assert(mosq != NULL); + assert(obj != NULL); + assert(grantedQOS != NULL); + + for (int i = 0; i < qosCount; i++) + ::fprintf(stdout, "MQTT: on_subscribe: %d:%d\n", i, grantedQOS[i]); +} + +void CMQTTConnection::onMessage(mosquitto* mosq, void* obj, const mosquitto_message* message) +{ + assert(mosq != NULL); + assert(obj != NULL); + assert(message != NULL); + + CMQTTConnection* p = static_cast(obj); + + for (std::vector>::const_iterator it = p->m_subs.cbegin(); it != p->m_subs.cend(); ++it) { + std::string topic = (*it).first; + + char topicEx[100U]; + ::sprintf(topicEx, "%s/%s", p->m_name.c_str(), topic.c_str()); + + if (::strcmp(topicEx, message->topic) == 0) { + (*it).second((unsigned char*)message->payload, message->payloadlen); + break; + } + } +} + +void CMQTTConnection::onDisconnect(mosquitto* mosq, void* obj, int rc) +{ + assert(mosq != NULL); + assert(obj != NULL); + + ::fprintf(stdout, "MQTT: on_disconnect: %s\n", ::mosquitto_reason_string(rc)); + + CMQTTConnection* p = static_cast(obj); + p->m_connected = false; +} + diff --git a/MQTTConnection.h b/MQTTConnection.h new file mode 100644 index 0000000..8fc98ce --- /dev/null +++ b/MQTTConnection.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022,2023 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(MQTTPUBLISHER_H) +#define MQTTPUBLISHER_H + +#include + +#include +#include + +enum MQTT_QOS { + MQTT_QOS_AT_MODE_ONCE = 0U, + MQTT_QOS_AT_LEAST_ONCE = 1U, + MQTT_QOS_EXACTLY_ONCE = 2U +}; + +class CMQTTConnection { +public: + CMQTTConnection(const std::string& host, unsigned short port, const std::string& name, const std::vector>& subs, unsigned int keepalive, MQTT_QOS qos = MQTT_QOS_EXACTLY_ONCE); + ~CMQTTConnection(); + + 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); + + void close(); + +private: + std::string m_host; + unsigned short m_port; + std::string m_name; + std::vector> m_subs; + unsigned int m_keepalive; + MQTT_QOS m_qos; + mosquitto* m_mosq; + bool m_connected; + + static void onConnect(mosquitto* mosq, void* obj, int rc); + static void onSubscribe(mosquitto* mosq, void* obj, int mid, int qosCount, const int* grantedQOS); + static void onMessage(mosquitto* mosq, void* obj, const mosquitto_message* message); + static void onDisconnect(mosquitto* mosq, void* obj, int rc); +}; + +#endif + diff --git a/Makefile b/Makefile index a789596..e311d90 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,11 @@ CC = cc CXX = c++ -CFLAGS = -g -O3 -Wall -DHAVE_LOG_H -std=c++0x -pthread -LIBS = -lm -lpthread +CFLAGS = -g -O3 -Wall -std=c++0x -pthread +LIBS = -lm -lpthread -lmosquitto LDFLAGS = -g -OBJECTS = Conf.o DAPNETGateway.o DAPNETNetwork.o Log.o POCSAGMessage.o POCSAGNetwork.o StopWatch.o TCPSocket.o Thread.o Timer.o UDPSocket.o Utils.o REGEX.o +OBJECTS = Conf.o DAPNETGateway.o DAPNETNetwork.o Log.o MQTTConnection.o POCSAGMessage.o POCSAGNetwork.o REGEX.o StopWatch.o \ + TCPSocket.o Thread.o Timer.o UDPSocket.o Utils.o all: DAPNETGateway diff --git a/Utils.cpp b/Utils.cpp index 49ded13..7276d60 100644 --- a/Utils.cpp +++ b/Utils.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009,2014,2015,2016 Jonathan Naylor, G4KLX + * Copyright (C) 2009,2014,2015,2016,2023 Jonathan Naylor, G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -70,77 +70,3 @@ void CUtils::dump(int level, const std::string& title, const unsigned char* data } } -void CUtils::dump(const std::string& title, const bool* bits, unsigned int length) -{ - assert(bits != NULL); - - dump(2U, title, bits, length); -} - -void CUtils::dump(int level, const std::string& title, const bool* bits, unsigned int length) -{ - assert(bits != NULL); - - unsigned char bytes[100U]; - unsigned int nBytes = 0U; - for (unsigned int n = 0U; n < length; n += 8U, nBytes++) - bitsToByteBE(bits + n, bytes[nBytes]); - - dump(level, title, bytes, nBytes); -} - -void CUtils::byteToBitsBE(unsigned char byte, bool* bits) -{ - assert(bits != NULL); - - bits[0U] = (byte & 0x80U) == 0x80U; - bits[1U] = (byte & 0x40U) == 0x40U; - bits[2U] = (byte & 0x20U) == 0x20U; - bits[3U] = (byte & 0x10U) == 0x10U; - bits[4U] = (byte & 0x08U) == 0x08U; - bits[5U] = (byte & 0x04U) == 0x04U; - bits[6U] = (byte & 0x02U) == 0x02U; - bits[7U] = (byte & 0x01U) == 0x01U; -} - -void CUtils::byteToBitsLE(unsigned char byte, bool* bits) -{ - assert(bits != NULL); - - bits[0U] = (byte & 0x01U) == 0x01U; - bits[1U] = (byte & 0x02U) == 0x02U; - bits[2U] = (byte & 0x04U) == 0x04U; - bits[3U] = (byte & 0x08U) == 0x08U; - bits[4U] = (byte & 0x10U) == 0x10U; - bits[5U] = (byte & 0x20U) == 0x20U; - bits[6U] = (byte & 0x40U) == 0x40U; - bits[7U] = (byte & 0x80U) == 0x80U; -} - -void CUtils::bitsToByteBE(const bool* bits, unsigned char& byte) -{ - assert(bits != NULL); - - byte = bits[0U] ? 0x80U : 0x00U; - byte |= bits[1U] ? 0x40U : 0x00U; - byte |= bits[2U] ? 0x20U : 0x00U; - byte |= bits[3U] ? 0x10U : 0x00U; - byte |= bits[4U] ? 0x08U : 0x00U; - byte |= bits[5U] ? 0x04U : 0x00U; - byte |= bits[6U] ? 0x02U : 0x00U; - byte |= bits[7U] ? 0x01U : 0x00U; -} - -void CUtils::bitsToByteLE(const bool* bits, unsigned char& byte) -{ - assert(bits != NULL); - - byte = bits[0U] ? 0x01U : 0x00U; - byte |= bits[1U] ? 0x02U : 0x00U; - byte |= bits[2U] ? 0x04U : 0x00U; - byte |= bits[3U] ? 0x08U : 0x00U; - byte |= bits[4U] ? 0x10U : 0x00U; - byte |= bits[5U] ? 0x20U : 0x00U; - byte |= bits[6U] ? 0x40U : 0x00U; - byte |= bits[7U] ? 0x80U : 0x00U; -} diff --git a/Utils.h b/Utils.h index ade28c0..752d4dc 100644 --- a/Utils.h +++ b/Utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009,2014,2015 by Jonathan Naylor, G4KLX + * Copyright (C) 2009,2014,2015,2023 by Jonathan Naylor, G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,15 +21,6 @@ public: static void dump(const std::string& title, const unsigned char* data, unsigned int length); static void dump(int level, const std::string& title, const unsigned char* data, unsigned int length); - static void dump(const std::string& title, const bool* bits, unsigned int length); - static void dump(int level, const std::string& title, const bool* bits, unsigned int length); - - static void byteToBitsBE(unsigned char byte, bool* bits); - static void byteToBitsLE(unsigned char byte, bool* bits); - - static void bitsToByteBE(const bool* bits, unsigned char& byte); - static void bitsToByteLE(const bool* bits, unsigned char& byte); - private: }; diff --git a/Version.h b/Version.h index e41719c..acbd087 100644 --- a/Version.h +++ b/Version.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 by Jonathan Naylor G4KLX + * Copyright (C) 2018,2020,2023 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20201031"; +const char* VERSION = "20230707"; #endif From f0a271ae5371c7549bee20028cef5cbdc25c2c3f Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Fri, 7 Jul 2023 18:41:30 +0100 Subject: [PATCH 02/12] Upgrade the version and signal handling. --- .gitignore | 1 + DAPNETGateway.cpp | 47 +++++++++++++++++++++++++++++++++++++++++------ Makefile | 15 +++++++++++++++ 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 3cadb85..90a428d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ Debug Release x64 DAPNETGateway +GitVersion.h *.o *.opendb *.bak diff --git a/DAPNETGateway.cpp b/DAPNETGateway.cpp index 196b0f1..f6e1af4 100644 --- a/DAPNETGateway.cpp +++ b/DAPNETGateway.cpp @@ -24,6 +24,8 @@ #include "Timer.h" #include "Log.h" #include "REGEX.h" +#include "GitVersion.h" + #include #if defined(_WIN32) || defined(_WIN64) @@ -77,6 +79,17 @@ const unsigned char FUNCTIONAL_ALPHANUMERIC = 3U; const unsigned int MAX_TIME_TO_HOLD_TIME_MESSAGES = 15000U; // 15s +static bool m_killed = false; +static int m_signal = 0; + +#if !defined(_WIN32) && !defined(_WIN64) +static void sigHandler(int signum) +{ + m_killed = true; + m_signal = signum; +} +#endif + int main(int argc, char** argv) { const char* iniFile = DEFAULT_INI_FILE; @@ -84,7 +97,7 @@ int main(int argc, char** argv) for (int currentArg = 1; currentArg < argc; ++currentArg) { std::string arg = argv[currentArg]; if ((arg == "-v") || (arg == "--version")) { - ::fprintf(stdout, "DAPNETGateway version %s\n", VERSION); + ::fprintf(stdout, "DAPNETGateway version %s git #%.7s\n", VERSION, gitversion); return 0; } else if (arg.substr(0, 1) == "-") { ::fprintf(stderr, "Usage: DAPNETGateway [-v|--version] [filename]\n"); @@ -95,11 +108,33 @@ int main(int argc, char** argv) } } - CDAPNETGateway* gateway = new CDAPNETGateway(std::string(iniFile)); +#if !defined(_WIN32) && !defined(_WIN64) + ::signal(SIGINT, sigHandler); + ::signal(SIGTERM, sigHandler); + ::signal(SIGHUP, sigHandler); +#endif - int ret = gateway->run(); + int ret = 0; - delete gateway; + do { + m_signal = 0; + + CDAPNETGateway* gateway = new CDAPNETGateway(std::string(iniFile)); + ret = gateway->run(); + delete gateway; + + if (m_signal == 2) + ::LogInfo("DAPNETGateway-%s exited on receipt of SIGINT", VERSION); + + if (m_signal == 15) + ::LogInfo("DAPNETGateway-%s exited on receipt of SIGTERM", VERSION); + + if (m_signal == 1) + ::LogInfo("DAPNETGateway-%s restarted on receipt of SIGHUP", VERSION); + + } while (m_signal == 1); + + ::LogFinalise(); return ret; } @@ -255,8 +290,6 @@ int CDAPNETGateway::run() return 1; } - LogMessage("Starting DAPNETGateway-%s", VERSION); - ret = m_dapnetNetwork->login(); if (!ret) { m_pocsagNetwork->close(); @@ -286,6 +319,8 @@ int CDAPNETGateway::run() if (m_regexWhitelist->load()) regexWhitelist = m_regexWhitelist->get(); + LogMessage("DAPNETGateway-%s is starting", VERSION); + LogMessage("Built %s %s (GitID #%.7s)", __TIME__, __DATE__, gitversion); for (;;) { unsigned char buffer[200U]; diff --git a/Makefile b/Makefile index e311d90..1c66938 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,23 @@ DAPNETGateway: $(OBJECTS) %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< +DAPNETGateway.o: GitVersion.h FORCE + +.PHONY: GitVersion.h + +FORCE: + install: install -m 755 DAPNETGateway /usr/local/bin/ clean: $(RM) DAPNETGateway *.o *.d *.bak *~ + +# Export the current git version if the index file exists, else 000... +GitVersion.h: +ifneq ("$(wildcard .git/index)","") + echo "const char *gitversion = \"$(shell git rev-parse HEAD)\";" > $@ +else + echo "const char *gitversion = \"0000000000000000000000000000000000000000\";" > $@ +endif + From 97db2e9b456477874b8f96d8af68cbb8e2da6b7b Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 11 Jul 2023 11:25:39 +0100 Subject: [PATCH 03/12] Add basic JSON logging. --- DAPNETGateway.cpp | 35 ++++++++++++++++++++--------------- DAPNETGateway.h | 4 +++- Utils.cpp | 28 ++++++++++++++++++++++++++++ Utils.h | 2 ++ schema.json | 13 +++++++++++++ 5 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 schema.json diff --git a/DAPNETGateway.cpp b/DAPNETGateway.cpp index f6e1af4..07140bc 100644 --- a/DAPNETGateway.cpp +++ b/DAPNETGateway.cpp @@ -22,8 +22,9 @@ #include "Version.h" #include "Thread.h" #include "Timer.h" -#include "Log.h" #include "REGEX.h" +#include "Utils.h" +#include "Log.h" #include "GitVersion.h" #include @@ -260,8 +261,6 @@ int CDAPNETGateway::run() ret = m_pocsagNetwork->open(); if (!ret) { ::LogError("Cannot open the repeater network port"); - ::LogFinalise(); - return 1; } @@ -272,8 +271,6 @@ int CDAPNETGateway::run() if (dapnetAuthKey.length() == 0 || dapnetAuthKey == "TOPSECRET") { ::LogError("AuthKey not set or invalid"); - ::LogFinalise(); - return 1; } @@ -283,10 +280,7 @@ int CDAPNETGateway::run() m_pocsagNetwork->close(); delete m_pocsagNetwork; delete m_dapnetNetwork; - ::LogError("Cannot open the DAPNET network port"); - ::LogFinalise(); - return 1; } @@ -296,10 +290,7 @@ int CDAPNETGateway::run() m_dapnetNetwork->close(); delete m_pocsagNetwork; delete m_dapnetNetwork; - ::LogError("Cannot login to the DAPNET network"); - ::LogFinalise(); - return 1; } @@ -319,8 +310,10 @@ int CDAPNETGateway::run() if (m_regexWhitelist->load()) regexWhitelist = m_regexWhitelist->get(); - LogMessage("DAPNETGateway-%s is starting", VERSION); - LogMessage("Built %s %s (GitID #%.7s)", __TIME__, __DATE__, gitversion); + LogInfo("DAPNETGateway-%s is starting", VERSION); + LogInfo("Built %s %s (GitID #%.7s)", __TIME__, __DATE__, gitversion); + + writeJSONStatus("DAPNETGateway is starting"); for (;;) { unsigned char buffer[200U]; @@ -431,14 +424,15 @@ int CDAPNETGateway::run() CThread::sleep(10U); } + LogInfo("DAPNETGateway is stopping"); + writeJSONStatus("DAPNETGateway is stopping"); + m_pocsagNetwork->close(); delete m_pocsagNetwork; m_dapnetNetwork->close(); delete m_dapnetNetwork; - ::LogFinalise(); - return 0; } @@ -624,3 +618,14 @@ bool CDAPNETGateway::sendMessage(CPOCSAGMessage* message) const return true; } } + +void CDAPNETGateway::writeJSONStatus(const std::string& status) +{ + nlohmann::json json; + + json["timestamp"] = CUtils::createTimestamp(); + json["message"] = status; + + WriteJSON("status", json); +} + diff --git a/DAPNETGateway.h b/DAPNETGateway.h index 28ed57a..55d16fd 100644 --- a/DAPNETGateway.h +++ b/DAPNETGateway.h @@ -1,5 +1,5 @@ /* -* Copyright (C) 2018 by Jonathan Naylor G4KLX +* Copyright (C) 2018,2023 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -60,6 +60,8 @@ private: unsigned int calculateCodewords(const CPOCSAGMessage* message) const; void loadSchedule(); bool sendMessage(CPOCSAGMessage* message) const; + + void writeJSONStatus(const std::string& status); }; #endif diff --git a/Utils.cpp b/Utils.cpp index 7276d60..bbecbfd 100644 --- a/Utils.cpp +++ b/Utils.cpp @@ -17,6 +17,13 @@ #include #include +#if defined(_WIN32) || defined(_WIN64) +#include +#else +#include +#include +#endif + void CUtils::dump(const std::string& title, const unsigned char* data, unsigned int length) { assert(data != NULL); @@ -70,3 +77,24 @@ void CUtils::dump(int level, const std::string& title, const unsigned char* data } } +std::string CUtils::createTimestamp() +{ + char buffer[100U]; + +#if defined(_WIN32) || defined(_WIN64) + SYSTEMTIME st; + ::GetSystemTime(&st); + + ::sprintf(buffer, "%04u-%02u-%02u %02u:%02u:%02u.%03u", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); +#else + struct timeval now; + ::gettimeofday(&now, NULL); + + struct tm* tm = ::gmtime(&now.tv_sec); + + ::sprintf(buffer, "%04d-%02d-%02d %02d:%02d:%02d.%03lld", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, now.tv_usec / 1000LL); +#endif + + return buffer; +} + diff --git a/Utils.h b/Utils.h index 752d4dc..17c1a35 100644 --- a/Utils.h +++ b/Utils.h @@ -21,6 +21,8 @@ public: static void dump(const std::string& title, const unsigned char* data, unsigned int length); static void dump(int level, const std::string& title, const unsigned char* data, unsigned int length); + static std::string createTimestamp(); + private: }; diff --git a/schema.json b/schema.json new file mode 100644 index 0000000..58be79f --- /dev/null +++ b/schema.json @@ -0,0 +1,13 @@ +{ + "$defs": { + "timestamp": {"type": "string"} + }, + + "status": { + "type": "object", + "timestamp": {"$ref": "#/$defs/timestamp"}, + "message": {"type": "string"}, + "required": ["timestamp", "message"] + } +} + From 0eda05e06449e8fa0e4166fdf0ef659f16f44915 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 11 Jul 2023 13:43:13 +0100 Subject: [PATCH 04/12] Exit the main loop correctly. --- DAPNETGateway.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DAPNETGateway.cpp b/DAPNETGateway.cpp index 07140bc..933bcfe 100644 --- a/DAPNETGateway.cpp +++ b/DAPNETGateway.cpp @@ -118,6 +118,7 @@ int main(int argc, char** argv) int ret = 0; do { + m_killed = false; m_signal = 0; CDAPNETGateway* gateway = new CDAPNETGateway(std::string(iniFile)); @@ -315,7 +316,7 @@ int CDAPNETGateway::run() writeJSONStatus("DAPNETGateway is starting"); - for (;;) { + while (!m_killed) { unsigned char buffer[200U]; if (m_pocsagNetwork->read(buffer) > 0U) { From c28edb9c4f40872ccfbcaaf5f98cd85f44cc9fbd Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Tue, 11 Jul 2023 17:10:41 +0100 Subject: [PATCH 05/12] Bump the version date. --- Version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version.h b/Version.h index acbd087..6a077a5 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20230707"; +const char* VERSION = "20230711"; #endif From d1bd0b549ac51f6bc3098188b1bb9fb1b5258836 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Wed, 26 Mar 2025 15:11:42 +0000 Subject: [PATCH 06/12] Add MQTT authentication. --- Conf.cpp | 24 ++++++++++++++++++++++++ Conf.h | 8 +++++++- DAPNETGateway.cpp | 8 ++------ DAPNETGateway.ini | 3 +++ MQTTConnection.cpp | 8 +++++++- MQTTConnection.h | 5 ++++- Version.h | 2 +- 7 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Conf.cpp b/Conf.cpp index a164587..d087d49 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -51,6 +51,9 @@ m_mqttAddress("127.0.0.1"), m_mqttPort(1883U), m_mqttKeepalive(60U), m_mqttName("dapnet-gateway"), +m_mqttAuthEnabled(false), +m_mqttUsername(), +m_mqttPassword(), m_dapnetAddress(), m_dapnetPort(0U), m_dapnetAuthKey(), @@ -167,6 +170,12 @@ bool CConf::read() m_mqttKeepalive = (unsigned int)::atoi(value); else if (::strcmp(key, "Name") == 0) m_mqttName = value; + else if (::strcmp(key, "Auth") == 0) + m_mqttAuthEnabled = ::atoi(value) == 1; + else if (::strcmp(key, "Username") == 0) + m_mqttUsername = value; + else if (::strcmp(key, "Password") == 0) + m_mqttPassword = value; } else if (section == SECTION::DAPNET) { if (::strcmp(key, "Address") == 0) m_dapnetAddress = value; @@ -267,6 +276,21 @@ std::string CConf::getMQTTName() const return m_mqttName; } +bool CConf::getMQTTAuthEnabled() const +{ + return m_mqttAuthEnabled; +} + +std::string CConf::getMQTTUsername() const +{ + return m_mqttUsername; +} + +std::string CConf::getMQTTPassword() const +{ + return m_mqttPassword; +} + std::string CConf::getDAPNETAddress() const { return m_dapnetAddress; diff --git a/Conf.h b/Conf.h index c2cc26c..1f8ec6b 100644 --- a/Conf.h +++ b/Conf.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018,2023 by Jonathan Naylor G4KLX + * Copyright (C) 2018,2023,2025 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -51,6 +51,9 @@ public: unsigned short getMQTTPort() const; unsigned int getMQTTKeepalive() const; std::string getMQTTName() const; + bool getMQTTAuthEnabled() const; + std::string getMQTTUsername() const; + std::string getMQTTPassword() const; // The DAPNET section std::string getDAPNETAddress() const; @@ -80,6 +83,9 @@ private: unsigned short m_mqttPort; unsigned int m_mqttKeepalive; std::string m_mqttName; + bool m_mqttAuthEnabled; + std::string m_mqttUsername; + std::string m_mqttPassword; std::string m_dapnetAddress; unsigned short m_dapnetPort; diff --git a/DAPNETGateway.cpp b/DAPNETGateway.cpp index b0b5dea..6118d84 100644 --- a/DAPNETGateway.cpp +++ b/DAPNETGateway.cpp @@ -1,9 +1,5 @@ /* -<<<<<<< HEAD -* Copyright (C) 2018,2020,2023,2024 by Jonathan Naylor G4KLX -======= -* Copyright (C) 2018,2020,2024,2025 by Jonathan Naylor G4KLX ->>>>>>> master +* Copyright (C) 2018,2020,2023,2024,2025 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -258,7 +254,7 @@ int CDAPNETGateway::run() ::LogInitialise(m_conf.getLogDisplayLevel(), m_conf.getLogMQTTLevel()); std::vector> subscriptions; - m_mqtt = new CMQTTConnection(m_conf.getMQTTAddress(), m_conf.getMQTTPort(), m_conf.getMQTTName(), subscriptions, m_conf.getMQTTKeepalive()); + m_mqtt = new CMQTTConnection(m_conf.getMQTTAddress(), m_conf.getMQTTPort(), m_conf.getMQTTName(), m_conf.getMQTTAuthEnabled(), m_conf.getMQTTUsername(), m_conf.getMQTTPassword(), subscriptions, m_conf.getMQTTKeepalive()); ret = m_mqtt->open(); if (!ret) { delete m_mqtt; diff --git a/DAPNETGateway.ini b/DAPNETGateway.ini index cd3e282..6fccbbb 100644 --- a/DAPNETGateway.ini +++ b/DAPNETGateway.ini @@ -19,6 +19,9 @@ MQTTLevel=1 Address=127.0.0.1 Port=1883 Keepalive=60 +Auth=0 +Username=mmdvm +Password=mmdvm Name=dapnet-gateway [DAPNET] diff --git a/MQTTConnection.cpp b/MQTTConnection.cpp index 333fbbe..465b5c2 100644 --- a/MQTTConnection.cpp +++ b/MQTTConnection.cpp @@ -23,10 +23,13 @@ #include #include -CMQTTConnection::CMQTTConnection(const std::string& host, unsigned short port, const std::string& name, const std::vector>& subs, unsigned int keepalive, MQTT_QOS qos) : +CMQTTConnection::CMQTTConnection(const std::string& host, unsigned short port, const std::string& name, const bool authEnabled, const std::string& username, const std::string& password, const std::vector>& subs, unsigned int keepalive, MQTT_QOS qos) : m_host(host), m_port(port), m_name(name), +m_authEnabled(authEnabled), +m_username(username), +m_password(password), m_subs(subs), m_keepalive(keepalive), m_qos(qos), @@ -59,6 +62,9 @@ bool CMQTTConnection::open() return false; } + if (m_authEnabled) + ::mosquitto_username_pw_set(m_mosq, m_username.c_str(), m_password.c_str()); + ::mosquitto_connect_callback_set(m_mosq, onConnect); ::mosquitto_subscribe_callback_set(m_mosq, onSubscribe); ::mosquitto_message_callback_set(m_mosq, onMessage); diff --git a/MQTTConnection.h b/MQTTConnection.h index 3d9ccfb..2c695fe 100644 --- a/MQTTConnection.h +++ b/MQTTConnection.h @@ -32,7 +32,7 @@ enum class MQTT_QOS : int { class CMQTTConnection { public: - CMQTTConnection(const std::string& host, unsigned short port, const std::string& name, const std::vector>& subs, unsigned int keepalive, MQTT_QOS qos = MQTT_QOS::EXACTLY_ONCE); + CMQTTConnection(const std::string& host, unsigned short port, const std::string& name, const bool authEnabled, const std::string& username, const std::string& password, const std::vector>& subs, unsigned int keepalive, MQTT_QOS qos = MQTT_QOS::EXACTLY_ONCE); ~CMQTTConnection(); bool open(); @@ -47,6 +47,9 @@ private: std::string m_host; unsigned short m_port; std::string m_name; + bool m_authEnabled; + std::string m_username; + std::string m_password; std::vector> m_subs; unsigned int m_keepalive; MQTT_QOS m_qos; diff --git a/Version.h b/Version.h index 3752c6e..d055aad 100644 --- a/Version.h +++ b/Version.h @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20250318"; +const char* VERSION = "20250326"; #endif From 5cf7887c18f3d9bac74e7389710911a72f649e55 Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Sat, 14 Feb 2026 18:19:54 +0000 Subject: [PATCH 07/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1beab61..729f864 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ The DAPNET Gateway links the MMDVM Host to the DAPNET network to receive paging The messages are buffered by the gateway when the host is busy with other modes, and then sends them to the host in order once the host becomes idle. -They build on 32-bit and 64-bit Linux as well as on Windows using Visual Studio 2017 on x86 and x64. +They build on 32-bit and 64-bit Linux as well as on Windows using Visual Studio 2022 on x86 and x64. This software is licenced under the GPL v2 and is primarily intended for amateur and educational use. From 3b99af8afc01f0a125cf65e5264f35d4dab72224 Mon Sep 17 00:00:00 2001 From: Andy Taylor Date: Sun, 8 Mar 2026 12:51:35 +0000 Subject: [PATCH 08/12] Fix MQTT client ID collision on ARM 32-bit userland with 64-bit kernel The MQTT client ID was generated using sprintf with %ld and time(nullptr). On platforms with 32-bit userland but 64-bit kernel (such as Raspberry Pi OS and some custom Alpine Linux builds), time_t is a 64-bit long long but %ld only reads 32 bits. Since the upper 32 bits of the current Unix timestamp are zero, this always produces a client ID ending in .0, causing collisions when multiple instances or restarts occur. Replace time()-based client IDs with PID-based IDs using getpid(), which is always a portable 32-bit value and unique per process. Platform-guarded for Windows (_getpid) and POSIX (getpid). --- MQTTConnection.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/MQTTConnection.cpp b/MQTTConnection.cpp index 465b5c2..77cbea6 100644 --- a/MQTTConnection.cpp +++ b/MQTTConnection.cpp @@ -21,7 +21,12 @@ #include #include #include -#include + +#if defined(_WIN32) || defined(_WIN64) +#include +#else +#include +#endif CMQTTConnection::CMQTTConnection(const std::string& host, unsigned short port, const std::string& name, const bool authEnabled, const std::string& username, const std::string& password, const std::vector>& subs, unsigned int keepalive, MQTT_QOS qos) : m_host(host), @@ -52,7 +57,11 @@ CMQTTConnection::~CMQTTConnection() bool CMQTTConnection::open() { char name[50U]; - ::sprintf(name, "DAPNETGateway.%ld", ::time(nullptr)); +#if defined(_WIN32) || defined(_WIN64) + ::sprintf(name, "DAPNETGateway.%u", (unsigned)::_getpid()); +#else + ::sprintf(name, "DAPNETGateway.%u", (unsigned)::getpid()); +#endif ::fprintf(stdout, "DAPNETGateway (%s) connecting to MQTT as %s\n", m_name.c_str(), name); From edac3b873a3d9a3ba656d8b4fb5096ab5007eb1b Mon Sep 17 00:00:00 2001 From: Andy Taylor Date: Sun, 8 Mar 2026 14:16:32 +0000 Subject: [PATCH 09/12] Add mosquitto_loop_stop before mosquitto_destroy in close() The background network thread started by mosquitto_loop_start() was not being stopped before mosquitto_destroy(), which can cause a use-after-free if the thread is still running when the mosquitto structure is freed. --- MQTTConnection.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/MQTTConnection.cpp b/MQTTConnection.cpp index 77cbea6..bb23703 100644 --- a/MQTTConnection.cpp +++ b/MQTTConnection.cpp @@ -146,6 +146,7 @@ void CMQTTConnection::close() { if (m_mosq != nullptr) { ::mosquitto_disconnect(m_mosq); + ::mosquitto_loop_stop(m_mosq, true); ::mosquitto_destroy(m_mosq); m_mosq = nullptr; } From fbba1d449e3dc48d078c6d229d6548f979dc57ae Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Wed, 11 Mar 2026 17:10:09 +0000 Subject: [PATCH 10/12] Improve the build process. --- .gitignore | 1 + MQTTConnection.h | 8 ++++---- Makefile | 12 +++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index a3b50b1..c94f6f5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ x64 DAPNETGateway GitVersion.h *.o +*.d *.opendb *.bak *.obj diff --git a/MQTTConnection.h b/MQTTConnection.h index 2c695fe..2521460 100644 --- a/MQTTConnection.h +++ b/MQTTConnection.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022,2023,2025 by Jonathan Naylor G4KLX + * Copyright (C) 2022,2023,2025,2026 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,9 +25,9 @@ #include enum class MQTT_QOS : int { - AT_MODE_ONCE = 0U, - AT_LEAST_ONCE = 1U, - EXACTLY_ONCE = 2U + AT_MODE_ONCE = 0, + AT_LEAST_ONCE = 1, + EXACTLY_ONCE = 2 }; class CMQTTConnection { diff --git a/Makefile b/Makefile index 0c21f44..9768912 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,21 @@ CC = cc CXX = c++ -CFLAGS = -g -O3 -Wall -std=c++0x -pthread +CFLAGS = -g -O3 -Wall -std=c++0x -MMD -MD -pthread LIBS = -lm -lpthread -lmosquitto LDFLAGS = -g -OBJECTS = Conf.o DAPNETGateway.o DAPNETNetwork.o Log.o MQTTConnection.o POCSAGMessage.o POCSAGNetwork.o REGEX.o StopWatch.o \ - TCPSocket.o Thread.o Timer.o UDPSocket.o Utils.o +SRCS = $(wildcard *.cpp) +OBJS = $(SRCS:.cpp=.o) +DEPS = $(SRCS:.cpp=.d) all: DAPNETGateway -DAPNETGateway: GitVersion.h $(OBJECTS) - $(CXX) $(OBJECTS) $(CFLAGS) $(LIBS) -o DAPNETGateway +DAPNETGateway: GitVersion.h $(OBJS) + $(CXX) $(OBJS) $(CFLAGS) $(LIBS) -o DAPNETGateway %.o: %.cpp $(CXX) $(CFLAGS) -c -o $@ $< +-include $(DEPS) DAPNETGateway.o: GitVersion.h FORCE From 552754628f29ebdba0ea9bfd4710c998a42cd17c Mon Sep 17 00:00:00 2001 From: Jonathan Naylor Date: Mon, 23 Mar 2026 13:00:13 +0000 Subject: [PATCH 11/12] create timestamps in rfc3339 format --- Utils.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Utils.cpp b/Utils.cpp index a71832e..31a42fd 100644 --- a/Utils.cpp +++ b/Utils.cpp @@ -85,14 +85,14 @@ std::string CUtils::createTimestamp() SYSTEMTIME st; ::GetSystemTime(&st); - ::sprintf(buffer, "%04u-%02u-%02u %02u:%02u:%02u.%03u", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); + ::sprintf(buffer, "%04u-%02u-%02uT%02u:%02u:%02u.%03uZ", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); #else struct timeval now; ::gettimeofday(&now, nullptr); struct tm* tm = ::gmtime(&now.tv_sec); - ::sprintf(buffer, "%04d-%02d-%02d %02d:%02d:%02d.%03lld", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, now.tv_usec / 1000LL); + ::sprintf(buffer, "%04d-%02d-%02dT%02d:%02d:%02d.%03lldZ", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, now.tv_usec / 1000LL); #endif return buffer; From 5aafb6c9831dbcfb8f0b6660c39ac767db4b619a Mon Sep 17 00:00:00 2001 From: Chipster Date: Mon, 10 Aug 2026 17:37:55 -0500 Subject: [PATCH 12/12] 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"] } }