Compare commits

..

20 commits
v1.0 ... master

Author SHA1 Message Date
Jonathan Naylor
b869053a80
Merge pull request #40 from W0CHP/master
Publish structured link/connectivity status
2026-08-11 11:30:57 +01:00
Chipster
5aafb6c983
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.
2026-08-10 17:37:55 -05:00
Jonathan Naylor
552754628f create timestamps in rfc3339 format 2026-03-23 13:00:13 +00:00
Jonathan Naylor
8abb83120c Merge branch 'master' of github.com:g4klx/DAPNETGateway 2026-03-11 17:10:55 +00:00
Jonathan Naylor
fbba1d449e Improve the build process. 2026-03-11 17:10:09 +00:00
Jonathan Naylor
c97ce090a2
Merge pull request #39 from MW0MWZ/fix/mqtt-client-id
Fix MQTT client ID collision on 32-bit ARM userland
2026-03-08 15:21:33 +00:00
Andy Taylor
edac3b873a 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.
2026-03-08 14:16:32 +00:00
Andy Taylor
3b99af8afc 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).
2026-03-08 12:51:35 +00:00
Jonathan Naylor
5cf7887c18 Update README.md 2026-02-14 18:19:54 +00:00
Jonathan Naylor
1cb140bc29 Merge branch 'master' into mqtt 2026-02-14 16:39:05 +00:00
Jonathan Naylor
d1bd0b549a Add MQTT authentication. 2025-03-26 15:11:42 +00:00
Jonathan Naylor
3aee1f0ed8 Merge branch 'master' into mqtt 2025-03-18 15:13:07 +00:00
Jonathan Naylor
2a13d8b4d1 Merge branch 'master' into mqtt 2024-08-31 20:10:29 +01:00
Jonathan Naylor
496c22f4c2 Merge branch 'master' into mqtt 2024-08-28 17:54:53 +01:00
Jonathan Naylor
98aa2e877d Merge branch 'master' into mqtt 2024-01-29 16:38:39 +00:00
Jonathan Naylor
c28edb9c4f Bump the version date. 2023-07-11 17:10:41 +01:00
Jonathan Naylor
0eda05e064 Exit the main loop correctly. 2023-07-11 13:43:13 +01:00
Jonathan Naylor
97db2e9b45 Add basic JSON logging. 2023-07-11 11:25:39 +01:00
Jonathan Naylor
f0a271ae53 Upgrade the version and signal handling. 2023-07-07 18:41:30 +01:00
Jonathan Naylor
e677d6a954 Convert the gateway to log via MQTT. 2023-07-07 17:57:36 +01:00
20 changed files with 609 additions and 297 deletions

2
.gitignore vendored
View file

@ -2,7 +2,9 @@ Debug
Release
x64
DAPNETGateway
GitVersion.h
*.o
*.d
*.opendb
*.bak
*.obj

View file

@ -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

103
Conf.h
View file

@ -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<unsigned int> getWhiteList() const;
std::vector<unsigned int> 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<unsigned int> getWhiteList() const;
std::vector<unsigned int> 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<unsigned int> m_whiteList;
std::vector<unsigned int> m_blackList;
std::string m_callsign;
std::vector<unsigned int> m_whiteList;
std::vector<unsigned int> 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

View file

@ -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 <regex>
#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<std::pair<std::string, void (*)(const unsigned char*, unsigned int)>> 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);
}

View file

@ -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

View file

@ -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

View file

@ -23,6 +23,7 @@
<ClInclude Include="DAPNETGateway.h" />
<ClInclude Include="DAPNETNetwork.h" />
<ClInclude Include="Log.h" />
<ClInclude Include="MQTTConnection.h" />
<ClInclude Include="POCSAGMessage.h" />
<ClInclude Include="POCSAGNetwork.h" />
<ClInclude Include="REGEX.h" />
@ -39,6 +40,7 @@
<ClCompile Include="DAPNETGateway.cpp" />
<ClCompile Include="DAPNETNetwork.cpp" />
<ClCompile Include="Log.cpp" />
<ClCompile Include="MQTTConnection.cpp" />
<ClCompile Include="POCSAGMessage.cpp" />
<ClCompile Include="POCSAGNetwork.cpp" />
<ClCompile Include="REGEX.cpp" />
@ -60,26 +62,26 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
@ -121,11 +123,13 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>HAVE_LOG_H;_CRT_SECURE_NO_WARNINGS</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>C:\Program Files\mosquitto\devel;C:\Program Files</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>ws2_32.lib;mosquitto.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>C:\Program Files\mosquitto\devel</AdditionalLibraryDirectories>
</Link>
<PreBuildEvent>
<Command>prebuild.cmd</Command>
@ -139,11 +143,13 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>HAVE_LOG_H;_CRT_SECURE_NO_WARNINGS</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>C:\Program Files\mosquitto\devel;C:\Program Files</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>ws2_32.lib;mosquitto.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>C:\Program Files\mosquitto\devel</AdditionalLibraryDirectories>
</Link>
<PreBuildEvent>
<Command>prebuild.cmd</Command>
@ -159,13 +165,15 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>HAVE_LOG_H;_CRT_SECURE_NO_WARNINGS</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>C:\Program Files\mosquitto\devel;C:\Program Files</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>ws2_32.lib;mosquitto.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>C:\Program Files\mosquitto\devel</AdditionalLibraryDirectories>
</Link>
<PreBuildEvent>
<Command>prebuild.cmd</Command>
@ -181,13 +189,15 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>HAVE_LOG_H;_CRT_SECURE_NO_WARNINGS</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>C:\Program Files\mosquitto\devel;C:\Program Files</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>ws2_32.lib;mosquitto.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>C:\Program Files\mosquitto\devel</AdditionalLibraryDirectories>
</Link>
<PreBuildEvent>
<Command>prebuild.cmd</Command>

View file

@ -53,6 +53,9 @@
<ClInclude Include="REGEX.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MQTTConnection.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Conf.cpp">
@ -94,5 +97,8 @@
<ClCompile Include="REGEX.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MQTTConnection.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

View file

@ -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)

148
Log.cpp
View file

@ -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 <Windows.h>
@ -32,117 +34,27 @@
#include <cassert>
#include <cstring>
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);
}

14
Log.h
View file

@ -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 <string>
#include <nlohmann/json.hpp>
#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

232
MQTTConnection.cpp Normal file
View file

@ -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 <cassert>
#include <cstdio>
#include <cstring>
#if defined(_WIN32) || defined(_WIN64)
#include <process.h>
#else
#include <unistd.h>
#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<std::pair<std::string, void (*)(const unsigned char*, unsigned int)>>& 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<int>(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<int>(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<CMQTTConnection*>(obj);
p->m_connected = true;
for (std::vector<std::pair<std::string, void (*)(const unsigned char*, unsigned int)>>::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<int>(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<int>(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<CMQTTConnection*>(obj);
for (std::vector<std::pair<std::string, void (*)(const unsigned char*, unsigned int)>>::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<CMQTTConnection*>(obj);
p->m_connected = false;
}

66
MQTTConnection.h Normal file
View file

@ -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 <mosquitto.h>
#include <vector>
#include <string>
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<std::pair<std::string, void (*)(const unsigned char*, unsigned int)>>& 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<std::pair<std::string, void (*)(const unsigned char*, unsigned int)>> 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

View file

@ -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/

View file

@ -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.

View file

@ -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());

View file

@ -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 <cstdio>
#include <cassert>
#if defined(_WIN32) || defined(_WIN64)
#include <Windows.h>
#else
#include <sys/time.h>
#include <unistd.h>
#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;
}

11
Utils.h
View file

@ -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:
};

View file

@ -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

23
schema.json Normal file
View file

@ -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"]
}
}