Convert to use of MQTT for command, APRS, and dynamic TG control.

This commit is contained in:
Jonathan Naylor
2023-07-07 11:14:34 +01:00
parent a28aa7c549
commit 04146faa81
15 changed files with 634 additions and 553 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2010-2014,2016,2017,2018,2020 by Jonathan Naylor G4KLX
* Copyright (C) 2010-2014,2016,2017,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 "APRSWriter.h"
#include "DMRDefines.h"
#include "Log.h"
@@ -25,7 +26,10 @@
#include <cstring>
#include <cmath>
CAPRSWriter::CAPRSWriter(const std::string& callsign, const std::string& suffix, const std::string& address, unsigned short port, bool debug) :
// In Log.cpp
extern CMQTTConnection* m_mqtt;
CAPRSWriter::CAPRSWriter(const std::string& callsign, const std::string& suffix, bool debug) :
m_idTimer(1000U),
m_callsign(callsign),
m_debug(debug),
@@ -35,22 +39,14 @@ m_latitude(0.0F),
m_longitude(0.0F),
m_height(0),
m_desc(),
m_symbol(),
m_aprsAddr(),
m_aprsLen(0U),
m_aprsSocket()
m_symbol()
{
assert(!callsign.empty());
assert(!address.empty());
assert(port > 0U);
if (!suffix.empty()) {
m_callsign.append("-");
m_callsign.append(suffix.substr(0U, 1U));
}
if (CUDPSocket::lookup(address, port, m_aprsAddr, m_aprsLen) != 0)
m_aprsLen = 0U;
}
CAPRSWriter::~CAPRSWriter()
@@ -75,17 +71,6 @@ void CAPRSWriter::setLocation(float latitude, float longitude, int height)
bool CAPRSWriter::open()
{
if (m_aprsLen == 0U) {
LogError("Could not lookup the address of the APRS-IS server");
return false;
}
bool ret = m_aprsSocket.open(m_aprsAddr);
if (!ret)
return false;
LogMessage("Opened connection to the APRS Gateway");
m_idTimer.setTimeout(60U);
m_idTimer.start();
@@ -104,7 +89,6 @@ void CAPRSWriter::clock(unsigned int ms)
void CAPRSWriter::close()
{
m_aprsSocket.close();
}
void CAPRSWriter::sendIdFrame()
@@ -172,5 +156,6 @@ void CAPRSWriter::sendIdFrame()
if (m_debug)
LogDebug("APRS ==> %s", output);
m_aprsSocket.write((unsigned char*)output, (unsigned int)::strlen(output), m_aprsAddr, m_aprsLen);
m_mqtt->publish("aprs-gateway/aprs", output);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2010,2011,2012,2016,2017,2018,2020 by Jonathan Naylor G4KLX
* Copyright (C) 2010,2011,2012,2016,2017,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,7 +19,6 @@
#ifndef APRSWriter_H
#define APRSWriter_H
#include "UDPSocket.h"
#include "Timer.h"
#include <string>
@@ -39,7 +38,7 @@
class CAPRSWriter {
public:
CAPRSWriter(const std::string& callsign, const std::string& suffix, const std::string& address, unsigned short port, bool debug);
CAPRSWriter(const std::string& callsign, const std::string& suffix, bool debug);
~CAPRSWriter();
bool open();
@@ -63,9 +62,6 @@ private:
int m_height;
std::string m_desc;
std::string m_symbol;
sockaddr_storage m_aprsAddr;
unsigned int m_aprsLen;
CUDPSocket m_aprsSocket;
void sendIdFrame();
};

134
Conf.cpp
View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2015-2020 by Jonathan Naylor G4KLX
* Copyright (C) 2015-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
@@ -40,8 +40,9 @@ enum SECTION {
SECTION_XLX_NETWORK,
SECTION_GPSD,
SECTION_APRS,
SECTION_MQTT,
SECTION_DYNAMIC_TG_CONTROL,
SECTION_REMOTE_CONTROL
SECTION_REMOTE_COMMANDS
};
CConf::CConf(const std::string& file) :
@@ -59,10 +60,7 @@ m_voiceEnabled(true),
m_voiceLanguage("en_GB"),
m_voiceDirectory(),
m_logDisplayLevel(0U),
m_logFileLevel(0U),
m_logFilePath(),
m_logFileRoot(),
m_logFileRotate(true),
m_logMQTTLevel(0U),
m_infoLatitude(0.0F),
m_infoLongitude(0.0F),
m_infoHeight(0),
@@ -178,16 +176,15 @@ m_gpsdEnabled(false),
m_gpsdAddress(),
m_gpsdPort(),
m_aprsEnabled(false),
m_aprsAddress(),
m_aprsPort(0U),
m_aprsSuffix(),
m_aprsDescription(),
m_aprsSymbol(),
m_mqttAddress("127.0.0.1"),
m_mqttPort(1883U),
m_mqttKeepalive(60U),
m_mqttName("dmr-gateway"),
m_dynamicTGControlEnabled(false),
m_dynamicTGControlPort(3769U),
m_remoteControlEnabled(false),
m_remoteControlAddress("127.0.0.1"),
m_remoteControlPort(0U)
m_remoteCommandsEnabled(false)
{
}
@@ -235,10 +232,12 @@ bool CConf::read()
section = SECTION_GPSD;
else if (::strncmp(buffer, "[APRS]", 6U) == 0)
section = SECTION_APRS;
else if (::strncmp(buffer, "[MQTT]", 6U) == 0)
section = SECTION_MQTT;
else if (::strncmp(buffer, "[Dynamic TG Control]", 20U) == 0)
section = SECTION_DYNAMIC_TG_CONTROL;
else if (::strncmp(buffer, "[Remote Control]", 16U) == 0)
section = SECTION_REMOTE_CONTROL;
else if (::strncmp(buffer, "[Remote Commands]", 17U) == 0)
section = SECTION_REMOTE_COMMANDS;
else
section = SECTION_NONE;
@@ -292,16 +291,10 @@ bool CConf::read()
else if (::strcmp(key, "Debug") == 0)
m_debug = ::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_VOICE) {
if (::strcmp(key, "Enabled") == 0)
m_voiceEnabled = ::atoi(value) == 1;
@@ -353,8 +346,7 @@ bool CConf::read()
*p++ = '0';
m_xlxNetworkStartup = std::string(buffer);
}
else if (::strcmp(key, "Relink") == 0)
} else if (::strcmp(key, "Relink") == 0)
m_xlxNetworkRelink = (unsigned int)::atoi(value);
else if (::strcmp(key, "Debug") == 0)
m_xlxNetworkDebug = ::atoi(value) == 1;
@@ -982,28 +974,27 @@ bool CConf::read()
} else if (section == SECTION_APRS) {
if (::strcmp(key, "Enable") == 0)
m_aprsEnabled = ::atoi(value) == 1;
else if (::strcmp(key, "Address") == 0)
m_aprsAddress = value;
else if (::strcmp(key, "Port") == 0)
m_aprsPort = (unsigned short)::atoi(value);
else if (::strcmp(key, "Suffix") == 0)
m_aprsSuffix = value;
else if (::strcmp(key, "Description") == 0)
m_aprsDescription = value;
else if (::strcmp(key, "Symbol") == 0)
m_aprsSymbol = value;
} 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_DYNAMIC_TG_CONTROL) {
if (::strcmp(key, "Enabled") == 0)
m_dynamicTGControlEnabled = ::atoi(value) == 1;
else if (::strcmp(key, "Port") == 0)
m_dynamicTGControlPort = (unsigned short)::atoi(value);
} else if (section == SECTION_REMOTE_CONTROL) {
if (::strcmp(key, "Enable") == 0)
m_remoteControlEnabled = ::atoi(value) == 1;
else if (::strcmp(key, "Address") == 0)
m_remoteControlAddress = value;
else if (::strcmp(key, "Port") == 0)
m_remoteControlPort = (unsigned short)::atoi(value);
m_dynamicTGControlEnabled = ::atoi(value) == 1;
} else if (section == SECTION_REMOTE_COMMANDS) {
if (::strcmp(key, "Enable") == 0)
m_remoteCommandsEnabled = ::atoi(value) == 1;
}
}
@@ -1062,24 +1053,9 @@ unsigned int CConf::getLogDisplayLevel() const
return m_logDisplayLevel;
}
unsigned int CConf::getLogFileLevel() const
unsigned int CConf::getLogMQTTLevel() const
{
return m_logFileLevel;
}
std::string CConf::getLogFilePath() const
{
return m_logFilePath;
}
std::string CConf::getLogFileRoot() const
{
return m_logFileRoot;
}
bool CConf::getLogFileRotate() const
{
return m_logFileRotate;
return m_logMQTTLevel;
}
bool CConf::getVoiceEnabled() const
@@ -1685,16 +1661,6 @@ bool CConf::getAPRSEnabled() const
return m_aprsEnabled;
}
std::string CConf::getAPRSAddress() const
{
return m_aprsAddress;
}
unsigned short CConf::getAPRSPort() const
{
return m_aprsPort;
}
std::string CConf::getAPRSSuffix() const
{
return m_aprsSuffix;
@@ -1710,27 +1676,33 @@ std::string CConf::getAPRSSymbol() const
return m_aprsSymbol;
}
std::string CConf::getMQTTAddress() const
{
return m_mqttAddress;
}
unsigned short CConf::getMQTTPort() const
{
return m_mqttPort;
}
unsigned int CConf::getMQTTKeepalive() const
{
return m_mqttKeepalive;
}
std::string CConf::getMQTTName() const
{
return m_mqttName;
}
bool CConf::getDynamicTGControlEnabled() const
{
return m_dynamicTGControlEnabled;
}
unsigned short CConf::getDynamicTGControlPort() const
bool CConf::getRemoteCommandsEnabled() const
{
return m_dynamicTGControlPort;
return m_remoteCommandsEnabled;
}
bool CConf::getRemoteControlEnabled() const
{
return m_remoteControlEnabled;
}
std::string CConf::getRemoteControlAddress() const
{
return m_remoteControlAddress;
}
unsigned short CConf::getRemoteControlPort() const
{
return m_remoteControlPort;
}

41
Conf.h
View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2015,2016,2017,2019,2020 by Jonathan Naylor G4KLX
* Copyright (C) 2015,2016,2017,2019,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
@@ -90,10 +90,7 @@ public:
// The Log section
unsigned int getLogDisplayLevel() const;
unsigned int getLogFileLevel() const;
std::string getLogFilePath() const;
std::string getLogFileRoot() const;
bool getLogFileRotate() const;
unsigned int getLogMQTTLevel() const;
// The Voice section
bool getVoiceEnabled() const;
@@ -232,20 +229,21 @@ public:
// The APRS section
bool getAPRSEnabled() const;
std::string getAPRSAddress() const;
unsigned short getAPRSPort() const;
std::string getAPRSSuffix() const;
std::string getAPRSDescription() const;
std::string getAPRSSymbol() const;
// The MQTT section
std::string getMQTTAddress() const;
unsigned short getMQTTPort() const;
unsigned int getMQTTKeepalive() const;
std::string getMQTTName() const;
// The Dynamic TG Control section
bool getDynamicTGControlEnabled() const;
unsigned short getDynamicTGControlPort() const;
// The Remote Control section
bool getRemoteControlEnabled() const;
std::string getRemoteControlAddress() const;
unsigned short getRemoteControlPort() const;
// The Remote Commands section
bool getRemoteCommandsEnabled() const;
private:
std::string m_file;
@@ -264,10 +262,7 @@ private:
std::string m_voiceDirectory;
unsigned int m_logDisplayLevel;
unsigned int m_logFileLevel;
std::string m_logFilePath;
std::string m_logFileRoot;
bool m_logFileRotate;
unsigned int m_logMQTTLevel;
float m_infoLatitude;
float m_infoLongitude;
@@ -392,18 +387,18 @@ private:
std::string m_gpsdPort;
bool m_aprsEnabled;
std::string m_aprsAddress;
unsigned short m_aprsPort;
std::string m_aprsSuffix;
std::string m_aprsDescription;
std::string m_aprsSymbol;
bool m_dynamicTGControlEnabled;
unsigned short m_dynamicTGControlPort;
std::string m_mqttAddress;
unsigned short m_mqttPort;
unsigned int m_mqttKeepalive;
std::string m_mqttName;
bool m_remoteControlEnabled;
std::string m_remoteControlAddress;
unsigned short m_remoteControlPort;
bool m_dynamicTGControlEnabled;
bool m_remoteCommandsEnabled;
};
#endif

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2015-2021 by Jonathan Naylor G4KLX
* Copyright (C) 2015-2021,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 "RewriteType.h"
#include "DMRSlotType.h"
#include "RewriteSrc.h"
@@ -67,10 +68,15 @@ static void sigHandler(int signum)
}
#endif
// In Log.cpp
extern CMQTTConnection* m_mqtt;
static CDMRGateway* gateway = NULL;
const char* HEADER1 = "This software is for use on amateur radio networks only,";
const char* HEADER2 = "it is to be used for educational purposes only. Its use on";
const char* HEADER3 = "commercial networks is strictly prohibited.";
const char* HEADER4 = "Copyright(C) 2017-2022 by Jonathan Naylor, G4KLX and others";
const char* HEADER4 = "Copyright(C) 2017-2023 by Jonathan Naylor, G4KLX and others";
int main(int argc, char** argv)
{
@@ -102,10 +108,9 @@ int main(int argc, char** argv)
do {
m_signal = 0;
CDMRGateway* host = new CDMRGateway(std::string(iniFile));
ret = host->run();
delete host;
gateway = new CDMRGateway(std::string(iniFile));
ret = gateway->run();
delete gateway;
if (m_signal == 2)
::LogInfo("DMRGateway-%s exited on receipt of SIGINT", VERSION);
@@ -181,7 +186,6 @@ m_dmr4Passalls(),
m_dmr5Passalls(),
m_dynVoices(),
m_dynRF(),
m_socket(NULL),
m_writer(NULL),
m_callsign(),
m_txFrequency(0U),
@@ -343,16 +347,6 @@ int CDMRGateway::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, "DMRGateway: unable to open the log file\n");
return 1;
}
#if !defined(_WIN32) && !defined(_WIN64)
if (m_daemon) {
::close(STDIN_FILENO);
@@ -360,6 +354,20 @@ int CDMRGateway::run()
::close(STDERR_FILENO);
}
#endif
::LogInitialise(m_conf.getLogDisplayLevel(), m_conf.getLogMQTTLevel());
std::vector<std::pair<std::string, void (*)(const unsigned char*, unsigned int)>> subscriptions;
if (m_conf.getDynamicTGControlEnabled())
subscriptions.push_back(std::make_pair("dynamic", CDMRGateway::onDynamic));
if (m_conf.getRemoteCommandsEnabled())
subscriptions.push_back(std::make_pair("command", CDMRGateway::onCommand));
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;
}
m_network1Enabled = m_conf.getDMRNetwork1Enabled();
m_network2Enabled = m_conf.getDMRNetwork2Enabled();
@@ -448,24 +456,9 @@ int CDMRGateway::run()
}
}
bool remoteControlEnabled = m_conf.getRemoteControlEnabled();
if (remoteControlEnabled) {
std::string address = m_conf.getRemoteControlAddress();
unsigned short port = m_conf.getRemoteControlPort();
LogInfo("Remote Control Parameters");
LogInfo(" Address: %s", address.c_str());
LogInfo(" Port: %hu", port);
m_remoteControl = new CRemoteControl(this, address, port);
ret = m_remoteControl->open();
if (!ret) {
LogInfo("Failed to open Remove Control Socket");
delete m_remoteControl;
m_remoteControl = NULL;
}
}
bool remoteCommandsEnabled = m_conf.getRemoteCommandsEnabled();
if (remoteCommandsEnabled)
m_remoteControl = new CRemoteControl(this);
if (m_network1Enabled && m_conf.getDMRNetwork1Enabled()) {
ret = createDMRNetwork1();
@@ -497,12 +490,6 @@ int CDMRGateway::run()
return 1;
}
if (m_conf.getDynamicTGControlEnabled()) {
bool ret = createDynamicTGControl();
if (!ret)
return 1;
}
createAPRS();
unsigned int rfTimeout = m_conf.getRFTimeout();
@@ -1222,11 +1209,6 @@ int CDMRGateway::run()
m_repeater->write(data);
}
if (m_socket != NULL)
processDynamicTGControl();
remoteControl();
unsigned int ms = stopWatch.elapsed();
stopWatch.start();
@@ -1286,10 +1268,7 @@ int CDMRGateway::run()
m_repeater->close();
delete m_repeater;
if (m_remoteControl != NULL) {
m_remoteControl->close();
delete m_remoteControl;
}
#if defined(USE_GPSD)
if (m_gpsd != NULL) {
@@ -1333,11 +1312,6 @@ int CDMRGateway::run()
delete m_xlxNetwork;
}
if (m_socket != NULL) {
m_socket->close();
delete m_socket;
}
delete timer[1U];
delete timer[2U];
@@ -2288,22 +2262,6 @@ bool CDMRGateway::createXLXNetwork()
return true;
}
bool CDMRGateway::createDynamicTGControl()
{
unsigned short port = m_conf.getDynamicTGControlPort();
m_socket = new CUDPSocket(port);
bool ret = m_socket->open();
if (!ret) {
delete m_socket;
m_socket = NULL;
return false;
}
return true;
}
bool CDMRGateway::linkXLX(const std::string &number)
{
CReflector* reflector = m_xlxReflectors->find(number);
@@ -2520,12 +2478,10 @@ void CDMRGateway::createAPRS()
if (!m_conf.getAPRSEnabled())
return;
std::string address = m_conf.getAPRSAddress();
unsigned short port = m_conf.getAPRSPort();
std::string suffix = m_conf.getAPRSSuffix();
bool debug = m_conf.getDebug();
m_writer = new CAPRSWriter(m_callsign, suffix, address, port, debug);
m_writer = new CAPRSWriter(m_callsign, suffix, debug);
std::string desc = m_conf.getAPRSDescription();
std::string symbol = m_conf.getAPRSSymbol();
@@ -2551,42 +2507,38 @@ void CDMRGateway::createAPRS()
#endif
}
void CDMRGateway::processDynamicTGControl()
void CDMRGateway::processDynamicTGControl(const std::string& command)
{
unsigned char buffer[100U];
sockaddr_storage address;
unsigned int addrlen;
int len = m_socket->read(buffer, 100U, address, addrlen);
if (len <= 0)
return;
std::vector<std::string> args;
buffer[len] = '\0';
std::stringstream tokeniser(command);
if (::memcmp(buffer + 0U, "DynTG", 5U) == 0) {
char* pSlot = ::strtok((char*)(buffer + 5U), ", \r\n");
char* pTG = ::strtok(NULL, ", \r\n");
// Parse the original command into a vector of strings.
std::string token;
while (std::getline(tokeniser, token, ' '))
args.push_back(token);
if (pSlot == NULL || pTG == NULL) {
if (args.at(0U) == "DynTG") {
if (args.size() < 3) {
LogWarning("Malformed dynamic TG control message");
return;
}
unsigned int slot = (unsigned int)::atoi(pSlot);
unsigned int tg = (unsigned int)::atoi(pTG);
unsigned int slot = (unsigned int)std::stoi(args.at(1U));
unsigned int tg = (unsigned int)std::stoi(args.at(2U));
for (std::vector<CRewriteDynTGRF*>::iterator it = m_dynRF.begin(); it != m_dynRF.end(); ++it)
(*it)->tgChange(slot, tg);
} else {
LogWarning("Unknown dynamic TG control message: %s", buffer);
LogWarning("Unknown dynamic TG control message: %s", command.c_str());
}
}
void CDMRGateway::remoteControl()
void CDMRGateway::remoteControl(const std::string& commandStr)
{
if (m_remoteControl == NULL)
return;
assert(m_remoteControl != NULL);
REMOTE_COMMAND command = m_remoteControl->getCommand();
REMOTE_COMMAND command = m_remoteControl->processCommand(commandStr);
switch (command) {
case RCD_ENABLE_NETWORK1:
processEnableCommand(m_dmrNetwork1, "DMR Network 1", m_network1Enabled, true);
@@ -2689,3 +2641,18 @@ void CDMRGateway::buildNetworkHostNetworkString(std::string &str, const std::str
str += name + ":\""+ ((network == NULL) ? "NONE" : ((host.length() > 0) ? host : "NONE")) + "\"";
}
}
void CDMRGateway::onDynamic(const unsigned char* message, unsigned int length)
{
assert(gateway != NULL);
gateway->processDynamicTGControl(std::string((char*)message, length));
}
void CDMRGateway::onCommand(const unsigned char* message, unsigned int length)
{
assert(gateway != NULL);
gateway->remoteControl(std::string((char*)message, length));
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2015,2016,2017,2019,2020 by Jonathan Naylor G4KLX
* Copyright (C) 2015,2016,2017,2019,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
@@ -117,7 +117,6 @@ private:
std::vector<CRewrite*> m_dmr5Passalls;
std::vector<CDynVoice*> m_dynVoices;
std::vector<CRewriteDynTGRF*> m_dynRF;
CUDPSocket* m_socket;
CAPRSWriter* m_writer;
std::string m_callsign;
unsigned int m_txFrequency;
@@ -140,7 +139,6 @@ private:
bool createDMRNetwork4();
bool createDMRNetwork5();
bool createXLXNetwork();
bool createDynamicTGControl();
bool linkXLX(const std::string &number);
void unlinkXLX();
@@ -153,11 +151,14 @@ private:
void processRadioPosition();
void processTalkerAlias();
void createAPRS();
void processDynamicTGControl();
void remoteControl();
void processDynamicTGControl(const std::string& command);
void remoteControl(const std::string& command);
void processEnableCommand(CDMRNetwork* network, const std::string& name, bool& mode, bool enabled);
void buildNetworkStatusNetworkString(std::string &str, const std::string& name, CDMRNetwork* network, bool enabled);
void buildNetworkHostNetworkString(std::string &str, const std::string& name, CDMRNetwork* network);
static void onCommand(const unsigned char* message, unsigned int length);
static void onDynamic(const unsigned char* message, unsigned int length);
};
#endif

View File

@@ -13,10 +13,7 @@ Debug=0
[Log]
# Logging levels, 0=No logging
DisplayLevel=1
FileLevel=1
FilePath=.
FileRoot=DMRGateway
FileRotate=1
MQTTLevel=1
[Voice]
Enabled=1
@@ -145,17 +142,19 @@ Port=2947
[APRS]
Enable=0
Address=127.0.0.1
Port=8673
Description=APRS Description
Suffix=3
# Symbol="/r"
[Dynamic TG Control]
Enabled=1
Port=3769
[Remote Control]
Enable=0
[MQTT]
Address=127.0.0.1
Port=7643
Port=1883
Keepalive=60
Name=dmr-gateway
[Dynamic TG Control]
Enable=1
[Remote Commands]
Enable=0

135
Log.cpp
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
@@ -17,6 +17,7 @@
*/
#include "Log.h"
#include "MQTTConnection.h"
#if defined(_WIN32) || defined(_WIN64)
#include <Windows.h>
@@ -32,117 +33,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 = 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());
}
}

8
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,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

211
MQTTConnection.cpp Normal file
View File

@@ -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 <cassert>
#include <cstdio>
#include <cstring>
CMQTTConnection::CMQTTConnection(const std::string& host, unsigned short port, const std::string& name, 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_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<int>(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<int>(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<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, NULL, 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, NULL, 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 != 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<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 != NULL);
assert(obj != NULL);
::fprintf(stdout, "MQTT: on_disconnect: %s\n", ::mosquitto_reason_string(rc));
CMQTTConnection* p = static_cast<CMQTTConnection*>(obj);
p->m_connected = false;
}

63
MQTTConnection.h Normal file
View File

@@ -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 <mosquitto.h>
#include <vector>
#include <string>
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<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 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<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

@@ -2,19 +2,19 @@ CC = cc
CXX = c++
# Use the following CFLAGS and LIBS if you don't want to use gpsd.
CFLAGS = -g -O3 -Wall -DHAVE_LOG_H -std=c++0x -pthread
LIBS = -lpthread
CFLAGS = -g -O3 -Wall -std=c++0x -pthread
LIBS = -lpthread -lmosquitto
# Use the following CFLAGS and LIBS if you do want to use gpsd.
#CFLAGS = -g -O3 -Wall -DHAVE_LOG_H -DUSE_GPSD -std=c++0x -pthread
#LIBS = -lpthread -lgps
#CFLAGS = -g -O3 -Wall -DUSE_GPSD -std=c++0x -pthread
#LIBS = -lpthread -lgps -lmosquitto
LDFLAGS = -g
OBJECTS = APRSWriter.o BPTC19696.o Conf.o CRC.o DMRCSBK.o DMRData.o DMRDataHeader.o DMREmbeddedData.o DMREMB.o DMRFullLC.o DMRGateway.o \
DMRLC.o DMRNetwork.o DMRSlotType.o DynVoice.o Golay2087.o GPSD.o Hamming.o Log.o MMDVMNetwork.o PassAllPC.o PassAllTG.o \
QR1676.o Reflectors.o RemoteControl.o Rewrite.o RewriteDstId.o RewriteDynTGNet.o RewriteDynTGRF.o RewritePC.o RewriteSrc.o RewriteSrcId.o \
RewriteTG.o RewriteType.o RS129.o SHA256.o StopWatch.o Sync.o Thread.o Timer.o UDPSocket.o Utils.o XLXVoice.o
DMRLC.o DMRNetwork.o DMRSlotType.o DynVoice.o Golay2087.o GPSD.o Hamming.o Log.o MMDVMNetwork.o MQTTConnection.o PassAllPC.o \
PassAllTG.o QR1676.o Reflectors.o RemoteControl.o Rewrite.o RewriteDstId.o RewriteDynTGNet.o RewriteDynTGRF.o RewritePC.o \
RewriteSrc.o RewriteSrcId.o RewriteTG.o RewriteType.o RS129.o SHA256.o StopWatch.o Sync.o Thread.o Timer.o UDPSocket.o Utils.o XLXVoice.o
all: DMRGateway

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019,2021 by Jonathan Naylor G4KLX
* Copyright (C) 2019,2021,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 "RemoteControl.h"
#include "Log.h"
#include "DMRGateway.h"
@@ -30,48 +31,34 @@ const unsigned int DISABLE_ARGS = 2U;
const unsigned int BUFFER_LENGTH = 100U;
CRemoteControl::CRemoteControl(CDMRGateway* host, const std::string address, unsigned short port) :
// In Log.cpp
extern CMQTTConnection* m_mqtt;
CRemoteControl::CRemoteControl(CDMRGateway* host) :
m_host(host),
m_socket(address, port),
m_command(RCD_NONE),
m_args()
{
assert(port > 0U);
}
CRemoteControl::~CRemoteControl()
{
}
bool CRemoteControl::open()
{
return m_socket.open();
}
REMOTE_COMMAND CRemoteControl::getCommand()
REMOTE_COMMAND CRemoteControl::processCommand(const std::string& command)
{
m_command = RCD_NONE;
m_args.clear();
char command[BUFFER_LENGTH];
char buffer[BUFFER_LENGTH * 2];
std::string replyStr = "OK";
sockaddr_storage address;
unsigned int addrlen;
int ret = m_socket.read((unsigned char*)buffer, BUFFER_LENGTH, address, addrlen);
if (ret > 0) {
buffer[ret] = '\0';
// Make a copy of the original command for logging.
::strcpy(command, buffer);
std::stringstream tokeniser(command);
// Parse the original command into a vector of strings.
char* b = buffer;
char* p = NULL;
while ((p = ::strtok(b, " ")) != NULL) {
b = NULL;
m_args.push_back(std::string(p));
}
std::string token;
while (std::getline(tokeniser, token, ' '))
m_args.push_back(token);
if (m_args.at(0U) == "enable" && m_args.size() >= ENABLE_ARGS) {
if (m_args.at(1U) == "net1")
m_command = RCD_ENABLE_NETWORK1;
@@ -103,28 +90,26 @@ REMOTE_COMMAND CRemoteControl::getCommand()
else
replyStr = "KO";
} else if (m_args.at(0U) == "status") {
if (m_host != NULL) {
if (m_host != NULL)
m_host->buildNetworkStatusString(replyStr);
}
else {
else
replyStr = "KO";
}
m_command = RCD_CONNECTION_STATUS;
} else if (m_args.at(0U) == "hosts") {
if (m_host != NULL) {
if (m_host != NULL)
m_host->buildNetworkHostsString(replyStr);
}
else {
else
replyStr = "KO";
}
m_command = RCD_CONFIG_HOSTS;
} else {
replyStr = "KO";
}
::snprintf(buffer, BUFFER_LENGTH * 2, "%s remote command of \"%s\" received", ((m_command == RCD_NONE) ? "Invalid" : "Valid"), command);
char buffer[200U];
::snprintf(buffer, 200, "%s remote command of \"%s\" received", ((m_command == RCD_NONE) ? "Invalid" : "Valid"), command.c_str());
if (m_command == RCD_NONE) {
m_args.clear();
LogWarning(buffer);
@@ -134,8 +119,7 @@ REMOTE_COMMAND CRemoteControl::getCommand()
#endif
}
m_socket.write((unsigned char*)replyStr.c_str(), (unsigned int)replyStr.length(), address, addrlen);
}
m_mqtt->publish("response", replyStr);
return m_command;
}
@@ -171,7 +155,3 @@ int CRemoteControl::getArgInt(unsigned int n) const
return ::atoi(getArgString(n).c_str());
}
void CRemoteControl::close()
{
m_socket.close();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019,2021 by Jonathan Naylor G4KLX
* Copyright (C) 2019,2021,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,8 +19,6 @@
#ifndef RemoteControl_H
#define RemoteControl_H
#include "UDPSocket.h"
#include <vector>
#include <string>
@@ -47,12 +45,10 @@ class CDMRNetwork;
class CRemoteControl {
public:
CRemoteControl(CDMRGateway* host, const std::string address, unsigned short port);
CRemoteControl(CDMRGateway* host);
~CRemoteControl();
bool open();
REMOTE_COMMAND getCommand();
REMOTE_COMMAND processCommand(const std::string& command);
unsigned int getArgCount() const;
@@ -60,11 +56,8 @@ public:
unsigned int getArgUInt(unsigned int n) const;
signed int getArgInt(unsigned int n) const;
void close();
private:
CDMRGateway* m_host;
CUDPSocket m_socket;
REMOTE_COMMAND m_command;
std::vector<std::string> m_args;
};

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2015-2021 by Jonathan Naylor G4KLX
* Copyright (C) 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
@@ -19,6 +19,6 @@
#if !defined(VERSION_H)
#define VERSION_H
const char* VERSION = "20230212";
const char* VERSION = "20230707";
#endif