diff --git a/.gitignore b/.gitignore index d13f157..c94f6f5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,9 @@ Debug Release x64 DAPNETGateway +GitVersion.h *.o +*.d *.opendb *.bak *.obj diff --git a/Conf.cpp b/Conf.cpp index d6efd84..315080a 100644 --- a/Conf.cpp +++ b/Conf.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018,2020,2025 by Jonathan Naylor G4KLX + * Copyright (C) 2018,2020,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 @@ -31,6 +31,7 @@ enum class SECTION { NONE, GENERAL, LOG, + MQTT, DAPNET }; @@ -46,10 +47,14 @@ 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_mqttAuthEnabled(false), +m_mqttUsername(), +m_mqttPassword(), m_dapnetAddress(), m_dapnetPort(0U), m_dapnetAuthKey(), @@ -81,6 +86,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 @@ -151,16 +158,25 @@ 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 (::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; @@ -236,24 +252,44 @@ 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; +} + +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 diff --git a/Conf.h b/Conf.h index 4faf12d..1f8ec6b 100644 --- a/Conf.h +++ b/Conf.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 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 @@ -25,61 +25,72 @@ 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; + bool getMQTTAuthEnabled() const; + std::string getMQTTUsername() const; + std::string getMQTTPassword() 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; + bool m_mqttAuthEnabled; + std::string m_mqttUsername; + std::string m_mqttPassword; + + 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 2488607..313ac76 100644 --- a/DAPNETGateway.cpp +++ b/DAPNETGateway.cpp @@ -1,5 +1,5 @@ /* -* Copyright (C) 2018,2020,2024,2025 by Jonathan Naylor G4KLX +* 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 @@ -16,15 +16,18 @@ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ +#include "MQTTConnection.h" #include "DAPNETGateway.h" #include "StopWatch.h" #include "Version.h" #include "Thread.h" #include "Timer.h" + +#include "REGEX.h" +#include "Utils.h" #include "Log.h" #include "GitVersion.h" -#include "REGEX.h" #include #if defined(_WIN32) || defined(_WIN64) @@ -43,6 +46,9 @@ const char* DEFAULT_INI_FILE = "DAPNETGateway.ini"; const char* DEFAULT_INI_FILE = "/etc/DAPNETGateway.ini"; #endif +// In Log.cpp +extern CMQTTConnection* m_mqtt; + static bool m_killed = false; static int m_signal = 0; @@ -86,6 +92,7 @@ const unsigned char FUNCTIONAL_ALPHANUMERIC = 3U; const unsigned int MAX_TIME_TO_HOLD_TIME_MESSAGES = 15000U; // 15s + int main(int argc, char** argv) { const char* iniFile = DEFAULT_INI_FILE; @@ -236,16 +243,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); @@ -254,6 +251,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(), m_conf.getMQTTAuthEnabled(), m_conf.getMQTTUsername(), m_conf.getMQTTPassword(), 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(); @@ -282,24 +289,26 @@ int CDAPNETGateway::run() ret = m_dapnetNetwork->open(); if (!ret) { m_pocsagNetwork->close(); + delete m_pocsagNetwork; delete m_dapnetNetwork; ::LogError("Cannot open the DAPNET network port"); + writeJSONLink("failed", "socket"); return 1; } - LogMessage("Starting DAPNETGateway-%s", VERSION); - ret = m_dapnetNetwork->login(); if (!ret) { m_pocsagNetwork->close(); m_dapnetNetwork->close(); + delete m_pocsagNetwork; delete m_dapnetNetwork; ::LogError("Cannot login to the DAPNET network"); + writeJSONLink("failed", "socket"); return 1; } @@ -320,6 +329,10 @@ int CDAPNETGateway::run() if (m_regexWhitelist->load()) regexWhitelist = m_regexWhitelist->get(); + LogInfo("DAPNETGateway-%s is starting", VERSION); + LogInfo("Built %s %s (GitID #%.7s)", __TIME__, __DATE__, gitversion); + + writeJSONStatus("DAPNETGateway is starting"); while (!m_killed) { unsigned char buffer[200U]; @@ -347,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) { @@ -430,6 +445,10 @@ int CDAPNETGateway::run() CThread::sleep(10U); } + LogInfo("DAPNETGateway is stopping"); + writeJSONStatus("DAPNETGateway is stopping"); + writeJSONLink("unlinked", ""); + m_pocsagNetwork->close(); delete m_pocsagNetwork; @@ -621,3 +640,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, false); +} + diff --git a/DAPNETGateway.h b/DAPNETGateway.h index d3dbb17..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 @@ -61,6 +61,7 @@ private: void loadSchedule(); bool sendMessage(CPOCSAGMessage* message) const; + void writeJSONStatus(const std::string& status); }; #endif diff --git a/DAPNETGateway.ini b/DAPNETGateway.ini index 1610a0d..6fccbbb 100644 --- a/DAPNETGateway.ini +++ b/DAPNETGateway.ini @@ -13,10 +13,16 @@ 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 +Auth=0 +Username=mmdvm +Password=mmdvm +Name=dapnet-gateway [DAPNET] Address=dapnet.afu.rwth-aachen.de diff --git a/DAPNETGateway.vcxproj b/DAPNETGateway.vcxproj index 0a39333..cd87f6f 100644 --- a/DAPNETGateway.vcxproj +++ b/DAPNETGateway.vcxproj @@ -23,6 +23,7 @@ + @@ -39,6 +40,7 @@ + @@ -60,26 +62,26 @@ Application true - v142 + v143 Unicode Application false - v142 + v143 true Unicode Application true - v142 + v143 Unicode Application false - v142 + v143 true Unicode @@ -121,11 +123,13 @@ true HAVE_LOG_H;_CRT_SECURE_NO_WARNINGS true + C:\Program Files\mosquitto\devel;C:\Program Files Console true - ws2_32.lib;%(AdditionalDependencies) + ws2_32.lib;mosquitto.lib;%(AdditionalDependencies) + C:\Program Files\mosquitto\devel prebuild.cmd @@ -139,11 +143,13 @@ true HAVE_LOG_H;_CRT_SECURE_NO_WARNINGS true + C:\Program Files\mosquitto\devel;C:\Program Files Console true - ws2_32.lib;%(AdditionalDependencies) + ws2_32.lib;mosquitto.lib;%(AdditionalDependencies) + C:\Program Files\mosquitto\devel prebuild.cmd @@ -159,13 +165,15 @@ true HAVE_LOG_H;_CRT_SECURE_NO_WARNINGS true + C:\Program Files\mosquitto\devel;C:\Program Files Console true true true - ws2_32.lib;%(AdditionalDependencies) + ws2_32.lib;mosquitto.lib;%(AdditionalDependencies) + C:\Program Files\mosquitto\devel prebuild.cmd @@ -181,13 +189,15 @@ true HAVE_LOG_H;_CRT_SECURE_NO_WARNINGS true + C:\Program Files\mosquitto\devel;C:\Program Files Console true true true - ws2_32.lib;%(AdditionalDependencies) + ws2_32.lib;mosquitto.lib;%(AdditionalDependencies) + C:\Program Files\mosquitto\devel prebuild.cmd diff --git a/DAPNETGateway.vcxproj.filters b/DAPNETGateway.vcxproj.filters index 442cacc..e3841f4 100644 --- a/DAPNETGateway.vcxproj.filters +++ b/DAPNETGateway.vcxproj.filters @@ -53,6 +53,9 @@ Header Files + + Header Files + @@ -94,5 +97,8 @@ Source Files + + Source Files + \ No newline at end of file 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 9aa2f96..6e2751f 100644 --- a/Log.cpp +++ b/Log.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2020,2025 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2020,2022,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 @@ -17,6 +17,8 @@ */ #include "Log.h" +#include "MQTTConnection.h" +#include "Utils.h" #if defined(_WIN32) || defined(_WIN64) #include @@ -32,117 +34,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 = nullptr; -static FILE* m_fpLog = nullptr; -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 != nullptr) - return true; - } else { - if (m_fpLog != nullptr) - ::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")) != nullptr) { - 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 != nullptr) - 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")) != nullptr) { - 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 != nullptr) - ::fclose(m_fpLog); + if (m_mqtt != nullptr) { + m_mqtt->close(); + delete m_mqtt; + m_mqtt = nullptr; + } } void Log(unsigned int level, const char* fmt, ...) @@ -171,22 +83,38 @@ 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 != nullptr && 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, bool retain) +{ + if (m_mqtt != nullptr) { + nlohmann::json top; + + top[topLevel] = json; + + 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 ae95b60..f4e012f 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,15 @@ 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, 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 new file mode 100644 index 0000000..6277a4b --- /dev/null +++ b/MQTTConnection.cpp @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2022,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 + * 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 + +#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), +m_port(port), +m_name(name), +m_authEnabled(authEnabled), +m_username(username), +m_password(password), +m_subs(subs), +m_keepalive(keepalive), +m_qos(qos), +m_mosq(nullptr), +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() +{ + char name[50U]; +#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); + + m_mosq = ::mosquitto_new(name, true, this); + if (m_mosq == nullptr){ + ::fprintf(stderr, "MQTT Error newing: Out of memory.\n"); + 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); + ::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 = nullptr; + ::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 = nullptr; + ::fprintf(stderr, "MQTT Error loop starting: %s\n", ::mosquitto_strerror(rc)); + return false; + } + + return true; +} + +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), retain); +} + +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(), retain); +} + +bool CMQTTConnection::publish(const char* topic, const unsigned char* data, unsigned int len, bool retain) +{ + assert(topic != nullptr); + assert(data != nullptr); + + if (!m_connected) + return false; + + if (::strchr(topic, '/') == nullptr) { + 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), 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), retain); + 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 != nullptr) { + ::mosquitto_disconnect(m_mosq); + ::mosquitto_loop_stop(m_mosq, true); + ::mosquitto_destroy(m_mosq); + m_mosq = nullptr; + } +} + +void CMQTTConnection::onConnect(mosquitto* mosq, void* obj, int rc) +{ + assert(mosq != nullptr); + assert(obj != nullptr); + + ::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, nullptr, 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, nullptr, 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 != nullptr); + assert(obj != nullptr); + assert(grantedQOS != nullptr); + + 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 != nullptr); + assert(obj != nullptr); + assert(message != nullptr); + + 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 != nullptr); + assert(obj != nullptr); + + ::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..86d8c2e --- /dev/null +++ b/MQTTConnection.h @@ -0,0 +1,66 @@ +/* + * 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 + * 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 class MQTT_QOS : int { + AT_MODE_ONCE = 0, + AT_LEAST_ONCE = 1, + EXACTLY_ONCE = 2 +}; + +class CMQTTConnection { +public: + 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(); + + 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(); + +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; + 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 ff6d6dc..9768912 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,21 @@ 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 -MMD -MD -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 +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 @@ -20,7 +23,6 @@ DAPNETGateway.o: GitVersion.h FORCE FORCE: - install: install -m 755 DAPNETGateway /usr/local/bin/ 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. diff --git a/TCPSocket.cpp b/TCPSocket.cpp index bf52b35..b971629 100644 --- a/TCPSocket.cpp +++ b/TCPSocket.cpp @@ -145,7 +145,7 @@ int CTCPSocket::read(unsigned char* buffer, unsigned int length, unsigned int se tv.tv_sec = secs; tv.tv_usec = msecs * 1000; - int ret = ::select(m_fd + 1, &readFds, nullptr, nullptr, &tv); + int ret = ::select(int(m_fd) + 1, &readFds, nullptr, nullptr, &tv); if (ret < 0) { #if defined(_WIN32) || defined(_WIN64) LogError("Error returned from TCP client select, err=%d", ::GetLastError()); diff --git a/Utils.cpp b/Utils.cpp index 4461eee..31a42fd 100644 --- a/Utils.cpp +++ b/Utils.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009,2014,2015,2016,2025 Jonathan Naylor, G4KLX + * Copyright (C) 2009,2014,2015,2016,2023,2025 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,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 != nullptr); @@ -70,77 +77,24 @@ 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) +std::string CUtils::createTimestamp() { - assert(bits != nullptr); + char buffer[100U]; - dump(2U, title, bits, length); +#if defined(_WIN32) || defined(_WIN64) + SYSTEMTIME st; + ::GetSystemTime(&st); + + ::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-%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; } -void CUtils::dump(int level, const std::string& title, const bool* bits, unsigned int length) -{ - assert(bits != nullptr); - - 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 != nullptr); - - 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 != nullptr); - - 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 != nullptr); - - 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 != nullptr); - - 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..17c1a35 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,14 +21,7 @@ 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); + static std::string createTimestamp(); private: }; diff --git a/Version.h b/Version.h index 4ef4b10..3c678f8 100644 --- a/Version.h +++ b/Version.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018,2020,2024,2025 by Jonathan Naylor G4KLX + * Copyright (C) 2018,2020,2023,2024,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 @@ -19,6 +19,6 @@ #if !defined(VERSION_H) #define VERSION_H -const char* VERSION = "20250607"; +const char* VERSION = "20260214"; #endif diff --git a/schema.json b/schema.json new file mode 100644 index 0000000..bf9d0e3 --- /dev/null +++ b/schema.json @@ -0,0 +1,23 @@ +{ + "$defs": { + "timestamp": {"type": "string"}, + "action": {"type": "string", "enum": ["linking", "unlinked", "failed"]}, + "reason": {"type": "string", "enum": ["auth", "socket", "lost"]} + }, + + "status": { + "type": "object", + "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"] + } +} +