Merge branch 'mqtt' into json_hosts

This commit is contained in:
Jonathan Naylor 2026-02-15 17:25:14 +00:00
commit 385677bfda
24 changed files with 1103 additions and 823 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2010-2014,2016,2017,2018,2020,2025 by Jonathan Naylor G4KLX
* Copyright (C) 2010-2014,2016,2017,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
@ -17,6 +17,7 @@
*/
#include "APRSWriter.h"
#include "MQTTConnection.h"
#include "Log.h"
#include <cstdio>
@ -24,7 +25,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),
@ -34,10 +38,7 @@ m_latitude(0.0F),
m_longitude(0.0F),
m_height(0),
m_desc(),
m_symbol(),
m_aprsAddr(),
m_aprsAddrLen(0U),
m_aprsSocket()
m_symbol()
#if defined(USE_GPSD)
,m_gpsdEnabled(false),
m_gpsdAddress(),
@ -46,16 +47,11 @@ m_gpsdData()
#endif
{
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_aprsAddrLen) != 0)
m_aprsAddrLen = 0U;
}
CAPRSWriter::~CAPRSWriter()
@ -91,11 +87,6 @@ void CAPRSWriter::setGPSDLocation(const std::string& address, const std::string&
bool CAPRSWriter::open()
{
if (m_aprsAddrLen == 0U) {
LogError("Unable to resolve the address of the APRS Gateway");
return false;
}
#if defined(USE_GPSD)
if (m_gpsdEnabled) {
int ret = ::gps_open(m_gpsdAddress.c_str(), m_gpsdPort.c_str(), &m_gpsdData);
@ -109,12 +100,6 @@ bool CAPRSWriter::open()
LogMessage("Connected to GPSD");
}
#endif
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();
@ -128,7 +113,7 @@ void CAPRSWriter::write(const char* data)
if (m_debug)
LogDebug("APRS ==> %s", data);
m_aprsSocket.write((unsigned char*)data, (unsigned int)::strlen(data), m_aprsAddr, m_aprsAddrLen);
m_mqtt->publish("aprs-gateway/aprs", data);
}
void CAPRSWriter::clock(unsigned int ms)
@ -156,8 +141,6 @@ void CAPRSWriter::clock(unsigned int ms)
void CAPRSWriter::close()
{
m_aprsSocket.close();
#if defined(USE_GPSD)
if (m_gpsdEnabled) {
::gps_stream(&m_gpsdData, WATCH_DISABLE, nullptr);
@ -228,9 +211,6 @@ void CAPRSWriter::sendIdFrameFixed()
lon, (m_longitude < 0.0F) ? 'W' : 'E', symbol[1],
float(m_height) * 3.28F, band, desc);
if (m_debug)
LogDebug("APRS ==> %s", output);
write(output);
}
@ -336,9 +316,6 @@ void CAPRSWriter::sendIdFrameMobile()
::sprintf(output + ::strlen(output), "%s %s\r\n", band, desc);
if (m_debug)
LogDebug("APRS ==> %s", output);
write(output);
}
#endif

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>
@ -42,7 +41,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();
@ -70,9 +69,6 @@ private:
int m_height;
std::string m_desc;
std::string m_symbol;
sockaddr_storage m_aprsAddr;
unsigned int m_aprsAddrLen;
CUDPSocket m_aprsSocket;
#if defined(USE_GPSD)
bool m_gpsdEnabled;
std::string m_gpsdAddress;

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015,2016,2017,2018,2020,2025 by Jonathan Naylor G4KLX
* Copyright (C) 2015,2016,2017,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
@ -34,6 +34,7 @@ enum class SECTION {
VOICE,
LOG,
APRS,
MQTT,
NETWORK,
GPSD,
REMOTE_COMMANDS
@ -63,16 +64,18 @@ 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_aprsEnabled(false),
m_aprsAddress("127.0.0.1"),
m_aprsPort(8673U),
m_aprsSuffix(),
m_aprsDescription(),
m_aprsSymbol(),
m_mqttAddress("127.0.0.1"),
m_mqttPort(1883U),
m_mqttKeepalive(60U),
m_mqttName("nxdn-gateway"),
m_mqttAuthEnabled(false),
m_mqttUsername(),
m_mqttPassword(),
m_networkPort(0U),
m_networkHosts1(),
m_networkHosts2(),
@ -88,8 +91,7 @@ m_networkDebug(false),
m_gpsdEnabled(false),
m_gpsdAddress(),
m_gpsdPort(),
m_remoteCommandsEnabled(false),
m_remoteCommandsPort(6075U)
m_remoteCommandsEnabled(false)
{
}
@ -99,193 +101,198 @@ CConf::~CConf()
bool CConf::read()
{
FILE* fp = ::fopen(m_file.c_str(), "rt");
if (fp == nullptr) {
::fprintf(stderr, "Couldn't open the .ini file - %s\n", m_file.c_str());
return false;
}
FILE* fp = ::fopen(m_file.c_str(), "rt");
if (fp == nullptr) {
::fprintf(stderr, "Couldn't open the .ini file - %s\n", m_file.c_str());
return false;
}
SECTION section = SECTION::NONE;
char buffer[BUFFER_SIZE];
while (::fgets(buffer, BUFFER_SIZE, fp) != nullptr) {
if (buffer[0U] == '#')
continue;
char buffer[BUFFER_SIZE];
while (::fgets(buffer, BUFFER_SIZE, fp) != nullptr) {
if (buffer[0U] == '#')
continue;
if (buffer[0U] == '[') {
if (::strncmp(buffer, "[General]", 9U) == 0)
section = SECTION::GENERAL;
else if (::strncmp(buffer, "[Info]", 6U) == 0)
section = SECTION::INFO;
else if (::strncmp(buffer, "[Id Lookup]", 11U) == 0)
section = SECTION::ID_LOOKUP;
else if (::strncmp(buffer, "[Voice]", 7U) == 0)
section = SECTION::VOICE;
else if (::strncmp(buffer, "[Log]", 5U) == 0)
section = SECTION::LOG;
else if (::strncmp(buffer, "[APRS]", 6U) == 0)
section = SECTION::APRS;
else if (::strncmp(buffer, "[Network]", 9U) == 0)
section = SECTION::NETWORK;
else if (::strncmp(buffer, "[GPSD]", 6U) == 0)
section = SECTION::GPSD;
else if (::strncmp(buffer, "[Remote Commands]", 17U) == 0)
section = SECTION::REMOTE_COMMANDS;
else
section = SECTION::NONE;
if (buffer[0U] == '[') {
if (::strncmp(buffer, "[General]", 9U) == 0)
section = SECTION::GENERAL;
else if (::strncmp(buffer, "[Info]", 6U) == 0)
section = SECTION::INFO;
else if (::strncmp(buffer, "[Id Lookup]", 11U) == 0)
section = SECTION::ID_LOOKUP;
else if (::strncmp(buffer, "[Voice]", 7U) == 0)
section = SECTION::VOICE;
else if (::strncmp(buffer, "[Log]", 5U) == 0)
section = SECTION::LOG;
else if (::strncmp(buffer, "[APRS]", 6U) == 0)
section = SECTION::APRS;
else if (::strncmp(buffer, "[MQTT]", 6U) == 0)
section = SECTION::MQTT;
else if (::strncmp(buffer, "[Network]", 9U) == 0)
section = SECTION::NETWORK;
else if (::strncmp(buffer, "[GPSD]", 6U) == 0)
section = SECTION::GPSD;
else if (::strncmp(buffer, "[Remote Commands]", 17U) == 0)
section = SECTION::REMOTE_COMMANDS;
else
section = SECTION::NONE;
continue;
}
continue;
}
char* key = ::strtok(buffer, " \t=\r\n");
if (key == nullptr)
continue;
char* key = ::strtok(buffer, " \t=\r\n");
if (key == nullptr)
continue;
char* value = ::strtok(nullptr, "\r\n");
if (value == nullptr)
continue;
char* value = ::strtok(nullptr, "\r\n");
if (value == nullptr)
continue;
// Remove quotes from the value
size_t len = ::strlen(value);
if (len > 1U && *value == '"' && value[len - 1U] == '"') {
value[len - 1U] = '\0';
value++;
} else {
char *p;
// Remove quotes from the value
size_t len = ::strlen(value);
if (len > 1U && *value == '"' && value[len - 1U] == '"') {
value[len - 1U] = '\0';
value++;
} else {
char *p;
// if value is not quoted, remove after # (to make comment)
if ((p = strchr(value, '#')) != nullptr)
*p = '\0';
// if value is not quoted, remove after # (to make comment)
if ((p = strchr(value, '#')) != nullptr)
*p = '\0';
// remove trailing tab/space
for (p = value + strlen(value) - 1U; p >= value && (*p == '\t' || *p == ' '); p--)
*p = '\0';
}
// remove trailing tab/space
for (p = value + strlen(value) - 1U; p >= value && (*p == '\t' || *p == ' '); p--)
*p = '\0';
}
if (section == SECTION::GENERAL) {
if (::strcmp(key, "Callsign") == 0) {
// Convert the callsign to upper case
for (unsigned int i = 0U; value[i] != 0; i++)
value[i] = ::toupper(value[i]);
m_callsign = value;
} else if (::strcmp(key, "Suffix") == 0) {
// Convert the callsign to upper case
for (unsigned int i = 0U; value[i] != 0; i++)
value[i] = ::toupper(value[i]);
m_suffix = value;
} else if (::strcmp(key, "RptProtocol") == 0)
m_rptProtocol = value;
else if (::strcmp(key, "RptAddress") == 0)
m_rptAddress = value;
else if (::strcmp(key, "RptPort") == 0)
m_rptPort = (unsigned short)::atoi(value);
else if (::strcmp(key, "LocalPort") == 0)
m_myPort = (unsigned short)::atoi(value);
else if (::strcmp(key, "Debug") == 0)
m_debug = ::atoi(value) == 1;
else if (::strcmp(key, "Daemon") == 0)
m_daemon = ::atoi(value) == 1;
} else if (section == SECTION::INFO) {
if (::strcmp(key, "TXFrequency") == 0)
m_txFrequency = (unsigned int)::atoi(value);
else if (::strcmp(key, "RXFrequency") == 0)
m_rxFrequency = (unsigned int)::atoi(value);
else if (::strcmp(key, "Power") == 0)
m_power = (unsigned int)::atoi(value);
else if (::strcmp(key, "Latitude") == 0)
m_latitude = float(::atof(value));
else if (::strcmp(key, "Longitude") == 0)
m_longitude = float(::atof(value));
else if (::strcmp(key, "Height") == 0)
m_height = ::atoi(value);
else if (::strcmp(key, "Name") == 0)
m_name = value;
else if (::strcmp(key, "Description") == 0)
m_description = value;
} else if (section == SECTION::ID_LOOKUP) {
if (::strcmp(key, "Name") == 0)
m_lookupName = value;
else if (::strcmp(key, "Time") == 0)
m_lookupTime = (unsigned int)::atoi(value);
} else if (section == SECTION::VOICE) {
if (::strcmp(key, "Enabled") == 0)
m_voiceEnabled = ::atoi(value) == 1;
else if (::strcmp(key, "Language") == 0)
m_voiceLanguage = value;
else if (::strcmp(key, "Directory") == 0)
m_voiceDirectory = value;
} 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);
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::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::NETWORK) {
if (::strcmp(key, "Port") == 0)
m_networkPort = (unsigned short)::atoi(value);
else if (::strcmp(key, "HostsFile1") == 0)
m_networkHosts1 = value;
else if (::strcmp(key, "HostsFile2") == 0)
m_networkHosts2 = value;
else if (::strcmp(key, "ReloadTime") == 0)
m_networkReloadTime = (unsigned int)::atoi(value);
else if (::strcmp(key, "ParrotAddress") == 0)
m_networkParrotAddress = value;
else if (::strcmp(key, "ParrotPort") == 0)
m_networkParrotPort = (unsigned short)::atoi(value);
else if (::strcmp(key, "NXDN2DMRAddress") == 0)
m_networkNXDN2DMRAddress = value;
else if (::strcmp(key, "NXDN2DMRPort") == 0)
m_networkNXDN2DMRPort = (unsigned short)::atoi(value);
else if (::strcmp(key, "Static") == 0) {
char* p = ::strtok(value, ",\r\n");
while (p != nullptr) {
unsigned short tg = (unsigned short)::atoi(p);
m_networkStatic.push_back(tg);
p = ::strtok(nullptr, ",\r\n");
}
} else if (::strcmp(key, "RFHangTime") == 0)
m_networkRFHangTime = (unsigned int)::atoi(value);
else if (::strcmp(key, "NetHangTime") == 0)
m_networkNetHangTime = (unsigned int)::atoi(value);
else if (::strcmp(key, "Debug") == 0)
m_networkDebug = ::atoi(value) == 1;
} else if (section == SECTION::GPSD) {
if (::strcmp(key, "Enable") == 0)
m_gpsdEnabled = ::atoi(value) == 1;
else if (::strcmp(key, "Address") == 0)
m_gpsdAddress = value;
else if (::strcmp(key, "Port") == 0)
m_gpsdPort = value;
} else if (section == SECTION::REMOTE_COMMANDS) {
if (::strcmp(key, "Enable") == 0)
m_remoteCommandsEnabled = ::atoi(value) == 1;
else if (::strcmp(key, "Port") == 0)
m_remoteCommandsPort = (unsigned short)::atoi(value);
}
}
if (section == SECTION::GENERAL) {
if (::strcmp(key, "Callsign") == 0) {
// Convert the callsign to upper case
for (unsigned int i = 0U; value[i] != 0; i++)
value[i] = ::toupper(value[i]);
m_callsign = value;
} else if (::strcmp(key, "Suffix") == 0) {
// Convert the callsign to upper case
for (unsigned int i = 0U; value[i] != 0; i++)
value[i] = ::toupper(value[i]);
m_suffix = value;
} else if (::strcmp(key, "RptProtocol") == 0)
m_rptProtocol = value;
else if (::strcmp(key, "RptAddress") == 0)
m_rptAddress = value;
else if (::strcmp(key, "RptPort") == 0)
m_rptPort = (unsigned short)::atoi(value);
else if (::strcmp(key, "LocalPort") == 0)
m_myPort = (unsigned short)::atoi(value);
else if (::strcmp(key, "Debug") == 0)
m_debug = ::atoi(value) == 1;
else if (::strcmp(key, "Daemon") == 0)
m_daemon = ::atoi(value) == 1;
} else if (section == SECTION::INFO) {
if (::strcmp(key, "TXFrequency") == 0)
m_txFrequency = (unsigned int)::atoi(value);
else if (::strcmp(key, "RXFrequency") == 0)
m_rxFrequency = (unsigned int)::atoi(value);
else if (::strcmp(key, "Power") == 0)
m_power = (unsigned int)::atoi(value);
else if (::strcmp(key, "Latitude") == 0)
m_latitude = float(::atof(value));
else if (::strcmp(key, "Longitude") == 0)
m_longitude = float(::atof(value));
else if (::strcmp(key, "Height") == 0)
m_height = ::atoi(value);
else if (::strcmp(key, "Name") == 0)
m_name = value;
else if (::strcmp(key, "Description") == 0)
m_description = value;
} else if (section == SECTION::ID_LOOKUP) {
if (::strcmp(key, "Name") == 0)
m_lookupName = value;
else if (::strcmp(key, "Time") == 0)
m_lookupTime = (unsigned int)::atoi(value);
} else if (section == SECTION::VOICE) {
if (::strcmp(key, "Enabled") == 0)
m_voiceEnabled = ::atoi(value) == 1;
else if (::strcmp(key, "Language") == 0)
m_voiceLanguage = value;
else if (::strcmp(key, "Directory") == 0)
m_voiceDirectory = value;
} else if (section == SECTION::LOG) {
if (::strcmp(key, "DisplayLevel") == 0)
m_logDisplayLevel = (unsigned int)::atoi(value);
else if (::strcmp(key, "MQTTLevel") == 0)
m_logMQTTLevel = (unsigned int)::atoi(value);
} else if (section == SECTION::APRS) {
if (::strcmp(key, "Enable") == 0)
m_aprsEnabled = ::atoi(value) == 1;
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 (::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::NETWORK) {
if (::strcmp(key, "Port") == 0)
m_networkPort = (unsigned short)::atoi(value);
else if (::strcmp(key, "HostsFile1") == 0)
m_networkHosts1 = value;
else if (::strcmp(key, "HostsFile2") == 0)
m_networkHosts2 = value;
else if (::strcmp(key, "ReloadTime") == 0)
m_networkReloadTime = (unsigned int)::atoi(value);
else if (::strcmp(key, "ParrotAddress") == 0)
m_networkParrotAddress = value;
else if (::strcmp(key, "ParrotPort") == 0)
m_networkParrotPort = (unsigned short)::atoi(value);
else if (::strcmp(key, "NXDN2DMRAddress") == 0)
m_networkNXDN2DMRAddress = value;
else if (::strcmp(key, "NXDN2DMRPort") == 0)
m_networkNXDN2DMRPort = (unsigned short)::atoi(value);
else if (::strcmp(key, "Static") == 0) {
char* p = ::strtok(value, ",\r\n");
while (p != nullptr) {
unsigned short tg = (unsigned short)::atoi(p);
m_networkStatic.push_back(tg);
p = ::strtok(nullptr, ",\r\n");
}
} else if (::strcmp(key, "RFHangTime") == 0)
m_networkRFHangTime = (unsigned int)::atoi(value);
else if (::strcmp(key, "NetHangTime") == 0)
m_networkNetHangTime = (unsigned int)::atoi(value);
else if (::strcmp(key, "Debug") == 0)
m_networkDebug = ::atoi(value) == 1;
} else if (section == SECTION::GPSD) {
if (::strcmp(key, "Enable") == 0)
m_gpsdEnabled = ::atoi(value) == 1;
else if (::strcmp(key, "Address") == 0)
m_gpsdAddress = value;
else if (::strcmp(key, "Port") == 0)
m_gpsdPort = value;
} else if (section == SECTION::REMOTE_COMMANDS) {
if (::strcmp(key, "Enable") == 0)
m_remoteCommandsEnabled = ::atoi(value) == 1;
}
}
::fclose(fp);
::fclose(fp);
return true;
return true;
}
std::string CConf::getCallsign() const
@ -398,16 +405,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;
@ -423,29 +420,49 @@ 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::getMQTTAuthEnabled() const
{
return m_mqttAuthEnabled;
}
std::string CConf::getMQTTUsername() const
{
return m_mqttUsername;
}
std::string CConf::getMQTTPassword() const
{
return m_mqttPassword;
}
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;
}
unsigned short CConf::getNetworkPort() const
@ -528,7 +545,3 @@ bool CConf::getRemoteCommandsEnabled() const
return m_remoteCommandsEnabled;
}
unsigned short CConf::getRemoteCommandsPort() const
{
return m_remoteCommandsPort;
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015,2016,2017,2018,2020 by Jonathan Naylor G4KLX
* Copyright (C) 2015,2016,2017,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
@ -25,137 +25,143 @@
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::string getSuffix() const;
std::string getRptProtocol() const;
std::string getRptAddress() const;
unsigned short getRptPort() const;
unsigned short getMyPort() const;
bool getDebug() const;
bool getDaemon() const;
// The General section
std::string getCallsign() const;
std::string getSuffix() const;
std::string getRptProtocol() const;
std::string getRptAddress() const;
// The Info section
unsigned int getRxFrequency() const;
unsigned int getTxFrequency() const;
unsigned int getPower() const;
float getLatitude() const;
float getLongitude() const;
int getHeight() const;
std::string getName() const;
std::string getDescription() const;
unsigned short getRptPort() const;
unsigned short getMyPort() const;
bool getDebug() const;
bool getDaemon() const;
// The Id Lookup section
std::string getLookupName() const;
unsigned int getLookupTime() const;
// The Info section
unsigned int getRxFrequency() const;
unsigned int getTxFrequency() const;
unsigned int getPower() const;
float getLatitude() const;
float getLongitude() const;
int getHeight() const;
std::string getName() const;
std::string getDescription() const;
// The Voice section
bool getVoiceEnabled() const;
std::string getVoiceLanguage() const;
std::string getVoiceDirectory() const;
// The Id Lookup section
std::string getLookupName() const;
unsigned int getLookupTime() const;
// 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 Voice section
bool getVoiceEnabled() const;
std::string getVoiceLanguage() const;
std::string getVoiceDirectory() const;
// The Log section
unsigned int getLogDisplayLevel() const;
unsigned int getLogFileLevel() const;
std::string getLogFilePath() const;
std::string getLogFileRoot() const;
bool getLogFileRotate() const;
// The APRS section
bool getAPRSEnabled() const;
std::string getAPRSSuffix() const;
std::string getAPRSDescription() const;
std::string getAPRSSymbol() const;
// The Network section
unsigned short getNetworkPort() const;
std::string getNetworkHosts1() const;
std::string getNetworkHosts2() const;
unsigned int getNetworkReloadTime() const;
std::string getNetworkParrotAddress() const;
unsigned short getNetworkParrotPort() const;
std::string getNetworkNXDN2DMRAddress() const;
unsigned short getNetworkNXDN2DMRPort() const;
std::vector<unsigned short> getNetworkStatic() const;
unsigned int getNetworkRFHangTime() const;
unsigned int getNetworkNetHangTime() const;
bool getNetworkDebug() const;
// The Log section
unsigned int getLogDisplayLevel() const;
unsigned int getLogMQTTLevel() const;
// The GPSD section
bool getGPSDEnabled() const;
std::string getGPSDAddress() const;
std::string getGPSDPort() 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 Remote Commands section
bool getRemoteCommandsEnabled() const;
unsigned short getRemoteCommandsPort() const;
// The Network section
unsigned short getNetworkPort() const;
std::string getNetworkHosts1() const;
std::string getNetworkHosts2() const;
unsigned int getNetworkReloadTime() const;
std::string getNetworkParrotAddress() const;
unsigned short getNetworkParrotPort() const;
std::string getNetworkNXDN2DMRAddress() const;
unsigned short getNetworkNXDN2DMRPort() const;
std::vector<unsigned short> getNetworkStatic() const;
unsigned int getNetworkRFHangTime() const;
unsigned int getNetworkNetHangTime() const;
bool getNetworkDebug() const;
// The GPSD section
bool getGPSDEnabled() const;
std::string getGPSDAddress() const;
std::string getGPSDPort() const;
// The Remote Commands section
bool getRemoteCommandsEnabled() const;
private:
std::string m_file;
std::string m_callsign;
std::string m_suffix;
std::string m_rptProtocol;
std::string m_rptAddress;
unsigned short m_rptPort;
unsigned short m_myPort;
bool m_debug;
bool m_daemon;
std::string m_file;
std::string m_callsign;
std::string m_suffix;
std::string m_rptProtocol;
std::string m_rptAddress;
unsigned short m_rptPort;
unsigned short m_myPort;
bool m_debug;
bool m_daemon;
unsigned int m_rxFrequency;
unsigned int m_txFrequency;
unsigned int m_power;
float m_latitude;
float m_longitude;
int m_height;
std::string m_name;
std::string m_description;
unsigned int m_rxFrequency;
unsigned int m_txFrequency;
unsigned int m_power;
float m_latitude;
float m_longitude;
int m_height;
std::string m_name;
std::string m_description;
std::string m_lookupName;
unsigned int m_lookupTime;
std::string m_lookupName;
unsigned int m_lookupTime;
bool m_voiceEnabled;
std::string m_voiceLanguage;
std::string m_voiceDirectory;
bool m_voiceEnabled;
std::string m_voiceLanguage;
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_logDisplayLevel;
unsigned int m_logMQTTLevel;
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_aprsEnabled;
std::string m_aprsSuffix;
std::string m_aprsDescription;
std::string m_aprsSymbol;
unsigned short m_networkPort;
std::string m_networkHosts1;
std::string m_networkHosts2;
unsigned int m_networkReloadTime;
std::string m_networkParrotAddress;
unsigned short m_networkParrotPort;
std::string m_networkNXDN2DMRAddress;
unsigned short m_networkNXDN2DMRPort;
std::vector<unsigned short> m_networkStatic;
unsigned int m_networkRFHangTime;
unsigned int m_networkNetHangTime;
bool m_networkDebug;
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;
bool m_gpsdEnabled;
std::string m_gpsdAddress;
std::string m_gpsdPort;
unsigned short m_networkPort;
std::string m_networkHosts1;
std::string m_networkHosts2;
unsigned int m_networkReloadTime;
std::string m_networkParrotAddress;
unsigned short m_networkParrotPort;
std::string m_networkNXDN2DMRAddress;
unsigned short m_networkNXDN2DMRPort;
std::vector<unsigned short> m_networkStatic;
unsigned int m_networkRFHangTime;
unsigned int m_networkNetHangTime;
bool m_networkDebug;
bool m_remoteCommandsEnabled;
unsigned short m_remoteCommandsPort;
bool m_gpsdEnabled;
std::string m_gpsdAddress;
std::string m_gpsdPort;
bool m_remoteCommandsEnabled;
};
#endif

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,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 = 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 +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 != 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)
{
if (m_mqtt != nullptr) {
nlohmann::json top;
top[topLevel] = json;
m_mqtt->publish("json", top.dump());
}
}

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

View file

@ -0,0 +1,222 @@
/*
* 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>
#include <ctime>
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];
::sprintf(name, "NXDNGateway.%ld", ::time(nullptr));
::fprintf(stdout, "NXDNGateway (%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)
{
assert(topic != nullptr);
assert(text != nullptr);
return publish(topic, (unsigned char*)text, (unsigned int)::strlen(text));
}
bool CMQTTConnection::publish(const char* topic, const std::string& text)
{
assert(topic != nullptr);
return publish(topic, (unsigned char*)text.c_str(), (unsigned int)text.size());
}
bool CMQTTConnection::publish(const char* topic, const unsigned char* data, unsigned int len)
{
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), 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, nullptr, 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 != nullptr) {
::mosquitto_disconnect(m_mosq);
::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;
}

View file

@ -0,0 +1,66 @@
/*
* 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.
*/
#if !defined(MQTTPUBLISHER_H)
#define MQTTPUBLISHER_H
#include <mosquitto.h>
#include <vector>
#include <string>
enum class MQTT_QOS : int {
AT_MODE_ONCE = 0U,
AT_LEAST_ONCE = 1U,
EXACTLY_ONCE = 2U
};
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 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;
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

@ -2,17 +2,17 @@ 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 Conf.o GPSHandler.o IcomNetwork.o KenwoodNetwork.o Log.o Mutex.o NXDNCRC.o NXDNGateway.o NXDNLookup.o NXDNNetwork.o \
Reflectors.o RptNetwork.o StopWatch.o Thread.o Timer.o UDPSocket.o Utils.o Voice.o
OBJECTS = APRSWriter.o Conf.o GPSHandler.o IcomNetwork.o KenwoodNetwork.o Log.o Mutex.o MQTTConnection.o NXDNCRC.o NXDNGateway.o \
NXDNLookup.o NXDNNetwork.o Reflectors.o RptNetwork.o StopWatch.o Thread.o Timer.o UDPSocket.o Utils.o Voice.o
all: NXDNGateway
@ -28,7 +28,6 @@ NXDNGateway.o: GitVersion.h FORCE
FORCE:
install:
install -m 755 NXDNGateway /usr/local/bin/

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2016,2017,2018,2020,2024,2025 by Jonathan Naylor G4KLX
* Copyright (C) 2016,2017,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
@ -17,17 +17,15 @@
*/
#include "KenwoodNetwork.h"
#include "MQTTConnection.h"
#include "IcomNetwork.h"
#include "NXDNNetwork.h"
#include "NXDNGateway.h"
#include "RptNetwork.h"
#include "NXDNLookup.h"
#include "Reflectors.h"
#include "GPSHandler.h"
#include "StopWatch.h"
#include "Version.h"
#include "Thread.h"
#include "Timer.h"
#include "Utils.h"
#include "Log.h"
#include "GitVersion.h"
@ -59,6 +57,11 @@ const char* DEFAULT_INI_FILE = "/etc/NXDNGateway.ini";
#include <ctime>
#include <cstring>
// In Log.cpp
extern CMQTTConnection* m_mqtt;
static CNXDNGateway* gateway = nullptr;
static bool m_killed = false;
static int m_signal = 0;
@ -76,13 +79,6 @@ const unsigned char NXDN_TYPE_TX_REL = 0x08U;
const unsigned short NXDN_VOICE_ID = 9999U;
class CStaticTG {
public:
unsigned short m_tg;
sockaddr_storage m_addr;
unsigned int m_addrLen;
};
int main(int argc, char** argv)
{
const char* iniFile = DEFAULT_INI_FILE;
@ -101,7 +97,6 @@ int main(int argc, char** argv)
}
}
#if !defined(_WIN32) && !defined(_WIN64)
::signal(SIGINT, sigHandler);
::signal(SIGTERM, sigHandler);
@ -114,7 +109,7 @@ int main(int argc, char** argv)
m_signal = 0;
m_killed = false;
CNXDNGateway* gateway = new CNXDNGateway(std::string(iniFile));
gateway = new CNXDNGateway(std::string(iniFile));
ret = gateway->run();
delete gateway;
@ -146,7 +141,16 @@ CNXDNGateway::CNXDNGateway(const std::string& file) :
m_conf(file),
m_writer(nullptr),
m_gps(nullptr),
m_voice(nullptr)
m_voice(nullptr),
m_remoteNetwork(nullptr),
m_currentTG(0U),
m_currentAddrLen(0U),
m_currentAddr(),
m_currentIsStatic(false),
m_hangTimer(1000U),
m_rfHangTime(0U),
m_reflectors(nullptr),
m_staticTGs()
{
CUDPSocket::startup();
}
@ -220,16 +224,6 @@ int CNXDNGateway::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, "NXDNGateway: unable to open the log file\n");
return 1;
}
#if !defined(_WIN32) && !defined(_WIN64)
if (m_daemon) {
::close(STDIN_FILENO);
@ -237,6 +231,16 @@ int CNXDNGateway::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.getRemoteCommandsEnabled())
subscriptions.push_back(std::make_pair("command", CNXDNGateway::onCommand));
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)
return 1;
createGPS();
@ -249,42 +253,33 @@ int CNXDNGateway::run()
localNetwork = new CIcomNetwork(m_conf.getMyPort(), m_conf.getRptAddress(), m_conf.getRptPort(), m_conf.getDebug());
ret = localNetwork->open();
if (!ret)
return 1;
CNXDNNetwork remoteNetwork(m_conf.getNetworkPort(), m_conf.getCallsign(), m_conf.getNetworkDebug());
ret = remoteNetwork.open();
if (!ret) {
delete localNetwork;
return 1;
}
m_remoteNetwork = new CNXDNNetwork(m_conf.getNetworkPort(), m_conf.getCallsign(), m_conf.getNetworkDebug());
ret = m_remoteNetwork->open();
if (!ret) {
delete m_remoteNetwork;
localNetwork->close();
delete localNetwork;
return 1;
}
CUDPSocket* remoteSocket = nullptr;
if (m_conf.getRemoteCommandsEnabled()) {
remoteSocket = new CUDPSocket(m_conf.getRemoteCommandsPort());
ret = remoteSocket->open();
if (!ret) {
delete remoteSocket;
remoteSocket = nullptr;
}
}
CReflectors reflectors(m_conf.getNetworkHosts1(), m_conf.getNetworkHosts2(), m_conf.getNetworkReloadTime());
m_reflectors = new CReflectors(m_conf.getNetworkHosts1(), m_conf.getNetworkHosts2(), m_conf.getNetworkReloadTime());
if (m_conf.getNetworkParrotPort() > 0U)
reflectors.setParrot(m_conf.getNetworkParrotAddress(), m_conf.getNetworkParrotPort());
m_reflectors->setParrot(m_conf.getNetworkParrotAddress(), m_conf.getNetworkParrotPort());
if (m_conf.getNetworkNXDN2DMRPort() > 0U)
reflectors.setNXDN2DMR(m_conf.getNetworkNXDN2DMRAddress(), m_conf.getNetworkNXDN2DMRPort());
reflectors.load();
m_reflectors->setNXDN2DMR(m_conf.getNetworkNXDN2DMRAddress(), m_conf.getNetworkNXDN2DMRPort());
m_reflectors->load();
CNXDNLookup* lookup = new CNXDNLookup(m_conf.getLookupName(), m_conf.getLookupTime());
lookup->read();
unsigned int rfHangTime = m_conf.getNetworkRFHangTime();
m_rfHangTime = m_conf.getNetworkRFHangTime();
unsigned int netHangTime = m_conf.getNetworkNetHangTime();
CTimer hangTimer(1000U);
CTimer pollTimer(1000U, 5U);
pollTimer.start();
@ -300,28 +295,32 @@ int CNXDNGateway::run()
}
}
LogMessage("Starting NXDNGateway-%s", VERSION);
LogInfo("NXDNGateway-%s is starting", VERSION);
LogInfo("Built %s %s (GitID #%.7s)", __TIME__, __DATE__, gitversion);
writeJSONStatus("NXDNGateway is starting");
unsigned short srcId = 0U;
unsigned short dstTG = 0U;
bool grp = false;
bool currentIsStatic = false;
CNXDNReflector currentTG;
std::vector<unsigned short> staticIds = m_conf.getNetworkStatic();
std::vector<CNXDNReflector> staticTGs;
for (const auto& it : staticIds) {
CNXDNReflector* reflector = reflectors.find(it);
for (std::vector<unsigned short>::const_iterator it = staticIds.cbegin(); it != staticIds.cend(); ++it) {
CNXDNReflector* reflector = m_reflectors->find(*it);
if (reflector != nullptr) {
staticTGs.push_back(*reflector);
CStaticTG staticTG;
staticTG.m_tg = *it;
staticTG.m_addr = reflector->m_addr;
staticTG.m_addrLen = reflector->m_addrLen;
m_staticTGs.push_back(staticTG);
remoteNetwork.writePoll(*reflector);
remoteNetwork.writePoll(*reflector);
remoteNetwork.writePoll(*reflector);
m_remoteNetwork->writePoll(staticTG.m_addr, staticTG.m_addrLen, staticTG.m_tg);
m_remoteNetwork->writePoll(staticTG.m_addr, staticTG.m_addrLen, staticTG.m_tg);
m_remoteNetwork->writePoll(staticTG.m_addr, staticTG.m_addrLen, staticTG.m_tg);
LogMessage("Statically linked to reflector %u", it);
LogMessage("Statically linked to reflector %u", staticTG.m_tg);
writeJSONLinking("startup", staticTG.m_tg);
}
}
@ -331,10 +330,10 @@ int CNXDNGateway::run()
unsigned int addrLen;
// From the reflector to the MMDVM
unsigned int len = remoteNetwork.readData(buffer, 200U, addr, addrLen);
unsigned int len = m_remoteNetwork->readData(buffer, 200U, addr, addrLen);
while (len > 0U) {
// If we're linked and it's from the right place, send it on
if (currentTG.isUsed() && CNXDNNetwork::match(addr, currentTG)) {
if (m_currentAddrLen > 0U && CUDPSocket::match(m_currentAddr, addr)) {
// Don't pass reflector control data through to the MMDVM
if (::memcmp(buffer, "NXDND", 5U) == 0) {
unsigned short dstTG = 0U;
@ -343,28 +342,32 @@ int CNXDNGateway::run()
bool grp = (buffer[9U] & 0x01U) == 0x01U;
if (grp && currentTG.m_id == dstTG) {
if (grp && m_currentTG == dstTG)
localNetwork->write(buffer + 10U, len - 10U);
if (grp && m_currentTG == dstTG) {
if (!isVoiceBusy())
localNetwork->write(buffer + 10U, len - 10U);
}
hangTimer.start();
m_hangTimer.start();
}
} else if (currentTG.isEmpty()) {
} else if (m_currentTG == 0U) {
bool poll = false;
// We weren't really connected yet, but we got a reply from a poll, or some data
if ((::memcmp(buffer, "NXDND", 5U) == 0) || (poll = (::memcmp(buffer, "NXDNP", 5U) == 0))) {
// Find the static TG that this audio data/poll belongs to
for (const auto& it : staticTGs) {
if (CNXDNNetwork::match(addr, it)) {
currentTG = it;
for (std::vector<CStaticTG>::const_iterator it = m_staticTGs.cbegin(); it != m_staticTGs.cend(); ++it) {
if (CUDPSocket::match(addr, (*it).m_addr)) {
m_currentTG = (*it).m_tg;
break;
}
}
if (currentTG.isUsed()) {
currentIsStatic = true;
if (m_currentTG > 0U) {
m_currentAddr = addr;
m_currentAddrLen = addrLen;
m_currentIsStatic = true;
unsigned short dstTG = 0U;
dstTG |= (buffer[7U] << 8) & 0xFF00U;
@ -372,20 +375,22 @@ int CNXDNGateway::run()
bool grp = (buffer[9U] & 0x01U) == 0x01U;
if (grp && (currentTG.m_id == dstTG) && !poll) {
if (grp && m_currentTG == dstTG && !poll) {
if (!isVoiceBusy())
localNetwork->write(buffer + 10U, len - 10U);
}
LogMessage("Switched to reflector %u due to network activity", currentTG.m_id);
writeJSONLinking("network", m_currentTG);
hangTimer.setTimeout(netHangTime);
hangTimer.start();
LogMessage("Switched to reflector %u due to network activity", m_currentTG);
m_hangTimer.setTimeout(netHangTime);
m_hangTimer.start();
}
}
}
len = remoteNetwork.readData(buffer, 200U, addr, addrLen);
len = m_remoteNetwork->readData(buffer, 200U, addr, addrLen);
}
// From the MMDVM to the reflector or control data
@ -401,64 +406,72 @@ int CNXDNGateway::run()
dstTG = (buffer[10U] << 8) & 0xFF00U;
dstTG |= (buffer[11U] << 0) & 0x00FFU;
if (dstTG != currentTG.m_id) {
if (currentTG.isUsed()) {
if (dstTG != m_currentTG) {
if (m_currentAddrLen > 0U) {
std::string callsign = lookup->find(srcId);
LogMessage("Unlinking from reflector %u by %s", currentTG.m_id, callsign.c_str());
if (!currentIsStatic) {
remoteNetwork.writeUnlink(currentTG);
remoteNetwork.writeUnlink(currentTG);
remoteNetwork.writeUnlink(currentTG);
writeJSONUnlinked("user");
LogMessage("Unlinking from reflector %u by %s", m_currentTG, callsign.c_str());
if (!m_currentIsStatic) {
m_remoteNetwork->writeUnlink(m_currentAddr, m_currentAddrLen, m_currentTG);
m_remoteNetwork->writeUnlink(m_currentAddr, m_currentAddrLen, m_currentTG);
m_remoteNetwork->writeUnlink(m_currentAddr, m_currentAddrLen, m_currentTG);
}
hangTimer.stop();
m_hangTimer.stop();
}
CNXDNReflector found;
for (const auto& it : staticTGs) {
if (dstTG == it.m_id) {
found = it;
const CStaticTG* found = nullptr;
for (std::vector<CStaticTG>::const_iterator it = m_staticTGs.cbegin(); it != m_staticTGs.cend(); ++it) {
if (dstTG == (*it).m_tg) {
found = &(*it);
break;
}
}
if (found.isEmpty()) {
CNXDNReflector* refl = reflectors.find(dstTG);
if (found == nullptr) {
CNXDNReflector* refl = m_reflectors->find(dstTG);
if (refl != nullptr) {
currentTG = *refl;
currentIsStatic = false;
m_currentTG = dstTG;
m_currentAddr = refl->m_addr;
m_currentAddrLen = refl->m_addrLen;
m_currentIsStatic = false;
} else {
currentTG.reset();
currentIsStatic = false;
m_currentTG = dstTG;
m_currentAddrLen = 0U;
m_currentIsStatic = false;
}
} else {
currentTG = found;
currentIsStatic = true;
m_currentTG = found->m_tg;
m_currentAddr = found->m_addr;
m_currentAddrLen = found->m_addrLen;
m_currentIsStatic = true;
}
// Link to the new reflector
if (currentTG.isUsed()) {
if (m_currentAddrLen > 0U) {
std::string callsign = lookup->find(srcId);
LogMessage("Switched to reflector %u due to RF activity from %s", currentTG.m_id, callsign.c_str());
LogMessage("Switched to reflector %u due to RF activity from %s", m_currentTG, callsign.c_str());
writeJSONLinking("user", m_currentTG);
if (!currentIsStatic) {
remoteNetwork.writePoll(currentTG);
remoteNetwork.writePoll(currentTG);
remoteNetwork.writePoll(currentTG);
if (!m_currentIsStatic) {
m_remoteNetwork->writePoll(m_currentAddr, m_currentAddrLen, m_currentTG);
m_remoteNetwork->writePoll(m_currentAddr, m_currentAddrLen, m_currentTG);
m_remoteNetwork->writePoll(m_currentAddr, m_currentAddrLen, m_currentTG);
}
hangTimer.setTimeout(rfHangTime);
hangTimer.start();
m_hangTimer.setTimeout(m_rfHangTime);
m_hangTimer.start();
} else {
hangTimer.stop();
m_hangTimer.stop();
}
if (m_voice != nullptr) {
if (currentTG.isEmpty())
if (m_currentAddrLen == 0U)
m_voice->unlinked();
else
m_voice->linkedTo(currentTG.m_id);
m_voice->linkedTo(m_currentTG);
}
}
@ -493,9 +506,9 @@ int CNXDNGateway::run()
}
// If we're linked and we have a network, send it on
if (currentTG.isUsed()) {
remoteNetwork.writeData(buffer, len, srcId, dstTG, grp, currentTG);
hangTimer.start();
if (m_currentAddrLen > 0U) {
m_remoteNetwork->writeData(buffer, len, srcId, dstTG, grp, m_currentAddr, m_currentAddrLen);
m_hangTimer.start();
}
len = localNetwork->read(buffer);
@ -507,140 +520,48 @@ int CNXDNGateway::run()
localNetwork->write(buffer, length);
}
if (remoteSocket != nullptr) {
sockaddr_storage addr;
unsigned int addrLen;
int res = remoteSocket->read(buffer, 200U, addr, addrLen);
if (res > 0) {
buffer[res] = '\0';
if (::memcmp(buffer + 0U, "TalkGroup", 9U) == 0) {
unsigned int tg = ((strlen((char*)buffer + 0U) > 10) ? (unsigned int)::atoi((char*)(buffer + 10U)) : 9999);
if (tg != currentTG.m_id) {
if (currentTG.isUsed()) {
LogMessage("Unlinked from reflector %u by remote command", currentTG.m_id);
if (!currentIsStatic) {
remoteNetwork.writeUnlink(currentTG);
remoteNetwork.writeUnlink(currentTG);
remoteNetwork.writeUnlink(currentTG);
}
hangTimer.stop();
}
CNXDNReflector found;
for (const auto& it : staticTGs) {
if (tg == it.m_id) {
found = it;
break;
}
}
if (found.isEmpty()) {
CNXDNReflector* refl = reflectors.find(tg);
if (refl != nullptr) {
currentTG = *refl;
currentIsStatic = false;
} else {
currentTG.reset();
currentIsStatic = false;
}
} else {
currentTG = found;
currentIsStatic = true;
}
// Link to the new reflector
if (currentTG.isUsed()) {
LogMessage("Switched to reflector %u by remote command", currentTG.m_id);
if (!currentIsStatic) {
remoteNetwork.writePoll(currentTG);
remoteNetwork.writePoll(currentTG);
remoteNetwork.writePoll(currentTG);
}
hangTimer.setTimeout(rfHangTime);
hangTimer.start();
} else {
hangTimer.stop();
}
if (m_voice != nullptr) {
if (currentTG.isEmpty())
m_voice->unlinked();
else
m_voice->linkedTo(currentTG.m_id);
}
}
} else if (::memcmp(buffer + 0U, "status", 6U) == 0) {
std::string state = std::string("nxdn:") + (currentTG.isUsed() ? "conn" : "disc");
remoteSocket->write((unsigned char*)state.c_str(), (unsigned int)state.length(), addr, addrLen);
} else if (::memcmp(buffer + 0U, "host", 4U) == 0) {
std::string host = "nxdn:\"NONE\"";
if (currentTG.isUsed()) {
if (remoteNetwork.hasIPv6() && currentTG.hasIPv6()) {
char buffer[100U];
if (::getnameinfo((struct sockaddr*)&currentTG.IPv6.m_addr, currentTG.IPv6.m_addrLen, buffer, sizeof(buffer), 0, 0, NI_NUMERICHOST | NI_NUMERICSERV) == 0)
host = "p25:\"" + std::string(buffer) + "\"";
}
else if (remoteNetwork.hasIPv4() && currentTG.hasIPv4()) {
char buffer[100U];
if (::getnameinfo((struct sockaddr*)&currentTG.IPv4.m_addr, currentTG.IPv4.m_addrLen, buffer, sizeof(buffer), 0, 0, NI_NUMERICHOST | NI_NUMERICSERV) == 0)
host = "p25:\"" + std::string(buffer) + "\"";
}
}
remoteSocket->write((unsigned char*)host.c_str(), (unsigned int)host.length(), addr, addrLen);
} else {
CUtils::dump("Invalid remote command received", buffer, res);
}
}
}
unsigned int ms = stopWatch.elapsed();
stopWatch.start();
reflectors.clock(ms);
m_reflectors->clock(ms);
localNetwork->clock(ms);
if (m_voice != nullptr)
m_voice->clock(ms);
hangTimer.clock(ms);
if (hangTimer.isRunning() && hangTimer.hasExpired()) {
if (currentTG.isUsed()) {
LogMessage("Unlinking from %u due to inactivity", currentTG.m_id);
m_hangTimer.clock(ms);
if (m_hangTimer.isRunning() && m_hangTimer.hasExpired()) {
if (m_currentAddrLen > 0U) {
LogMessage("Unlinking from %u due to inactivity", m_currentTG);
writeJSONUnlinked("timer");
if (!currentIsStatic) {
remoteNetwork.writeUnlink(currentTG);
remoteNetwork.writeUnlink(currentTG);
remoteNetwork.writeUnlink(currentTG);
if (!m_currentIsStatic) {
m_remoteNetwork->writeUnlink(m_currentAddr, m_currentAddrLen, m_currentTG);
m_remoteNetwork->writeUnlink(m_currentAddr, m_currentAddrLen, m_currentTG);
m_remoteNetwork->writeUnlink(m_currentAddr, m_currentAddrLen, m_currentTG);
}
if (m_voice != nullptr)
m_voice->unlinked();
currentTG.reset();
m_currentAddrLen = 0U;
hangTimer.stop();
m_hangTimer.stop();
}
currentTG.reset();
m_currentTG = 0U;
}
pollTimer.clock(ms);
if (pollTimer.isRunning() && pollTimer.hasExpired()) {
// Poll the static TGs
for (const auto& it : staticTGs)
remoteNetwork.writePoll(it);
for (std::vector<CStaticTG>::const_iterator it = m_staticTGs.cbegin(); it != m_staticTGs.cend(); ++it)
m_remoteNetwork->writePoll((*it).m_addr, (*it).m_addrLen, (*it).m_tg);
// Poll the dynamic TG
if (!currentIsStatic && currentTG.isUsed())
remoteNetwork.writePoll(currentTG);
if (!m_currentIsStatic && m_currentAddrLen > 0U)
m_remoteNetwork->writePoll(m_currentAddr, m_currentAddrLen, m_currentTG);
pollTimer.start();
}
@ -652,17 +573,16 @@ int CNXDNGateway::run()
CThread::sleep(5U);
}
LogInfo("NXDNGateway is stopping");
writeJSONStatus("NXDNGateway is stopping");
delete m_voice;
localNetwork->close();
delete localNetwork;
if (remoteSocket != nullptr) {
remoteSocket->close();
delete remoteSocket;
}
remoteNetwork.close();
m_remoteNetwork->close();
delete m_remoteNetwork;
lookup->stop();
@ -682,12 +602,10 @@ void CNXDNGateway::createGPS()
std::string callsign = m_conf.getCallsign();
std::string rptSuffix = m_conf.getSuffix();
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(callsign, rptSuffix, address, port, debug);
m_writer = new CAPRSWriter(callsign, rptSuffix, debug);
unsigned int txFrequency = m_conf.getTxFrequency();
unsigned int rxFrequency = m_conf.getRxFrequency();
@ -720,6 +638,98 @@ void CNXDNGateway::createGPS()
m_gps = new CGPSHandler(callsign, suffix, m_writer);
}
void CNXDNGateway::writeCommand(const std::string& command)
{
if (command.substr(0, 9) == "TalkGroup") {
unsigned int tg = 9999U;
if (command.length() > 10)
tg = (unsigned int)std::stoi(command.substr(10));
if (tg != m_currentTG) {
if (m_currentAddrLen > 0U) {
LogMessage("Unlinked from reflector %u by remote command", m_currentTG);
writeJSONUnlinked("remote");
if (!m_currentIsStatic) {
m_remoteNetwork->writeUnlink(m_currentAddr, m_currentAddrLen, m_currentTG);
m_remoteNetwork->writeUnlink(m_currentAddr, m_currentAddrLen, m_currentTG);
m_remoteNetwork->writeUnlink(m_currentAddr, m_currentAddrLen, m_currentTG);
}
m_hangTimer.stop();
}
const CStaticTG* found = nullptr;
for (std::vector<CStaticTG>::const_iterator it = m_staticTGs.cbegin(); it != m_staticTGs.cend(); ++it) {
if (tg == (*it).m_tg) {
found = &(*it);
break;
}
}
if (found == nullptr) {
CNXDNReflector* refl = m_reflectors->find(tg);
if (refl != nullptr) {
m_currentTG = tg;
m_currentAddr = refl->m_addr;
m_currentAddrLen = refl->m_addrLen;
m_currentIsStatic = false;
} else {
m_currentTG = tg;
m_currentAddrLen = 0U;
m_currentIsStatic = false;
}
} else {
m_currentTG = found->m_tg;
m_currentAddr = found->m_addr;
m_currentAddrLen = found->m_addrLen;
m_currentIsStatic = true;
}
// Link to the new reflector
if (m_currentAddrLen > 0U) {
LogMessage("Switched to reflector %u by remote command", m_currentTG);
writeJSONLinking("remote", m_currentTG);
if (!m_currentIsStatic) {
m_remoteNetwork->writePoll(m_currentAddr, m_currentAddrLen, m_currentTG);
m_remoteNetwork->writePoll(m_currentAddr, m_currentAddrLen, m_currentTG);
m_remoteNetwork->writePoll(m_currentAddr, m_currentAddrLen, m_currentTG);
}
m_hangTimer.setTimeout(m_rfHangTime);
m_hangTimer.start();
} else {
m_hangTimer.stop();
}
if (m_voice != nullptr) {
if (m_currentAddrLen == 0U)
m_voice->unlinked();
else
m_voice->linkedTo(m_currentTG);
}
}
} else if (command.substr(0, 6) == "status") {
std::string state = std::string("nxdn:") + ((m_currentAddrLen > 0) ? "conn" : "disc");
m_mqtt->publish("response", state);
} else if (command.substr(0, 4) == "host") {
std::string ref;
if (m_currentAddrLen > 0) {
char buffer[INET6_ADDRSTRLEN];
if (::getnameinfo((struct sockaddr*)&m_currentAddr, m_currentAddrLen, buffer, sizeof(buffer), 0, 0, NI_NUMERICHOST | NI_NUMERICSERV) == 0) {
ref = std::string(buffer);
}
}
std::string host = std::string("nxdn:\"") + ((ref.length() == 0) ? "NONE" : ref) + "\"";
m_mqtt->publish("response", host);
} else {
CUtils::dump("Invalid remote command received", (unsigned char*)command.c_str(), (unsigned int)command.length());
}
}
bool CNXDNGateway::isVoiceBusy() const
{
if (m_voice == nullptr)
@ -727,3 +737,55 @@ bool CNXDNGateway::isVoiceBusy() const
return m_voice->isBusy();
}
void CNXDNGateway::writeJSONStatus(const std::string& status)
{
nlohmann::json json;
json["timestamp"] = CUtils::createTimestamp();
json["message"] = status;
WriteJSON("status", json);
}
void CNXDNGateway::writeJSONLinking(const std::string& reason, unsigned short tg)
{
nlohmann::json json;
json["timestamp"] = CUtils::createTimestamp();
json["action"] = "linking";
json["reason"] = reason;
json["talkgroup"] = int(tg);
WriteJSON("link", json);
}
void CNXDNGateway::writeJSONUnlinked(const std::string& reason)
{
nlohmann::json json;
json["timestamp"] = CUtils::createTimestamp();
json["action"] = "unlinked";
json["reason"] = reason;
WriteJSON("link", json);
}
void CNXDNGateway::writeJSONRelinking(unsigned short tg)
{
nlohmann::json json;
json["timestamp"] = CUtils::createTimestamp();
json["action"] = "relinking";
json["talkgroup"] = int(tg);
WriteJSON("link", json);
}
void CNXDNGateway::onCommand(const unsigned char* command, unsigned int length)
{
assert(gateway != nullptr);
assert(command != nullptr);
gateway->writeCommand(std::string((char*)command, length));
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2016,2018,2024 by Jonathan Naylor G4KLX
* Copyright (C) 2016,2018,2023,2024 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,10 @@
#if !defined(NXDNGateway_H)
#define NXDNGateway_H
#include "NXDNNetwork.h"
#include "APRSWriter.h"
#include "GPSHandler.h"
#include "Reflectors.h"
#include "Voice.h"
#include "Timer.h"
#include "Conf.h"
@ -41,6 +43,13 @@
#include <winsock.h>
#endif
class CStaticTG {
public:
unsigned short m_tg;
sockaddr_storage m_addr;
unsigned int m_addrLen;
};
class CNXDNGateway
{
public:
@ -50,14 +59,33 @@ public:
int run();
private:
CConf m_conf;
CAPRSWriter* m_writer;
CGPSHandler* m_gps;
CVoice* m_voice;
CConf m_conf;
CAPRSWriter* m_writer;
CGPSHandler* m_gps;
CVoice* m_voice;
CNXDNNetwork* m_remoteNetwork;
unsigned short m_currentTG;
unsigned int m_currentAddrLen;
sockaddr_storage m_currentAddr;
bool m_currentIsStatic;
CTimer m_hangTimer;
unsigned int m_rfHangTime;
CReflectors* m_reflectors;
std::vector<CStaticTG> m_staticTGs;
void createGPS();
bool isVoiceBusy() const;
void writeJSONStatus(const std::string& status);
void writeJSONLinking(const std::string& reason, unsigned short tg);
void writeJSONUnlinked(const std::string& reason);
void writeJSONRelinking(unsigned short tg);
void writeCommand(const std::string& command);
static void onCommand(const unsigned char* command, unsigned int length);
};
#endif

View file

@ -36,8 +36,6 @@ Directory=./Audio
[APRS]
Enable=0
Address=127.0.0.1
Port=8673
Suffix=N
Description=APRS Description
# Symbol="/r"
@ -49,10 +47,16 @@ Time=24
[Log]
# Logging levels, 0=No logging
DisplayLevel=1
FileLevel=1
FilePath=.
FileRoot=NXDNGateway
FileRotate=1
MQTTLevel=1
[MQTT]
Address=127.0.0.1
Port=1883
Keepalive=60
Auth=0
Username=mmdvm
Password=mmdvm
Name=nxdn-gateway
[Network]
Port=14050
@ -75,4 +79,4 @@ Port=2947
[Remote Commands]
Enable=0
Port=6075

View file

@ -28,26 +28,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>
@ -88,12 +88,13 @@
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>HAVE_LOG_H;UDP_SOCKET_MAX=2;WIN32;_DEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>C:\Program Files</AdditionalIncludeDirectories>
<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>
@ -106,12 +107,13 @@
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>HAVE_LOG_H;UDP_SOCKET_MAX=2;_DEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>C:\Program Files</AdditionalIncludeDirectories>
<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>
@ -126,14 +128,15 @@
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>HAVE_LOG_H;UDP_SOCKET_MAX=2;WIN32;NDEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>C:\Program Files</AdditionalIncludeDirectories>
<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>
@ -148,14 +151,15 @@
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>HAVE_LOG_H;UDP_SOCKET_MAX=2;NDEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>C:\Program Files</AdditionalIncludeDirectories>
<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>
@ -168,6 +172,7 @@
<ClInclude Include="IcomNetwork.h" />
<ClInclude Include="KenwoodNetwork.h" />
<ClInclude Include="Log.h" />
<ClInclude Include="MQTTConnection.h" />
<ClInclude Include="Mutex.h" />
<ClInclude Include="NXDNCRC.h" />
<ClInclude Include="NXDNGateway.h" />
@ -191,6 +196,7 @@
<ClCompile Include="IcomNetwork.cpp" />
<ClCompile Include="KenwoodNetwork.cpp" />
<ClCompile Include="Log.cpp" />
<ClCompile Include="MQTTConnection.cpp" />
<ClCompile Include="Mutex.cpp" />
<ClCompile Include="NXDNCRC.cpp" />
<ClCompile Include="NXDNGateway.cpp" />

View file

@ -74,6 +74,9 @@
<ClInclude Include="KenwoodNetwork.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MQTTConnection.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Conf.cpp">
@ -133,5 +136,8 @@
<ClCompile Include="KenwoodNetwork.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MQTTConnection.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

View file

@ -20,7 +20,6 @@
#
###############################################################################
#
# Full path to the NXDNHosts JSON file
#
NXDNHOSTS=/path/to/NXDNHosts.json

View file

@ -94,7 +94,7 @@ int CUDPSocket::lookup(const std::string& hostname, unsigned short port, sockadd
/* Port is always digits, no needs to lookup service */
hints.ai_flags |= AI_NUMERICSERV;
int err = ::getaddrinfo(hostname.empty() ? nullptr : hostname.c_str(), portstr.c_str(), &hints, &res);
int err = ::getaddrinfo(hostname.empty() ? NULL : hostname.c_str(), portstr.c_str(), &hints, &res);
if (err != 0) {
sockaddr_in* paddr = (sockaddr_in*)&addr;
::memset(paddr, 0x00U, address_length = sizeof(sockaddr_in));

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-%02u %02u:%02u:%02u.%03u", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
#else
struct timeval now;
::gettimeofday(&now, nullptr);
struct tm* tm = ::gmtime(&now.tv_sec);
::sprintf(buffer, "%04d-%02d-%02d %02d:%02d:%02d.%03lld", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, now.tv_usec / 1000LL);
#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;
}

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) 2015-2018,2020,2023,2024,2025 by Jonathan Naylor G4KLX
* Copyright (C) 2015-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 = "20250506";
const char* VERSION = "20260215";
#endif

25
NXDNGateway/schema.json Normal file
View file

@ -0,0 +1,25 @@
{
"$defs": {
"timestamp": {"type": "string"},
"talkgroup": {"type": "integer"},
"action": {"type": "string", "enum": ["linking", "unlinked", "failed", "relinking"]},
"reason": {"type": "string", "enum": ["user", "network", "timer", "remote", "startup"]}
},
"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"},
"talkgroup": {"$ref": "#/defs/talkgroup"},
"required": ["timestamp", "action"]
}
}

View file

@ -28,26 +28,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>

View file

@ -94,7 +94,7 @@ int CUDPSocket::lookup(const std::string& hostname, unsigned short port, sockadd
/* Port is always digits, no needs to lookup service */
hints.ai_flags |= AI_NUMERICSERV;
int err = ::getaddrinfo(hostname.empty() ? nullptr : hostname.c_str(), portstr.c_str(), &hints, &res);
int err = ::getaddrinfo(hostname.empty() ? NULL : hostname.c_str(), portstr.c_str(), &hints, &res);
if (err != 0) {
sockaddr_in* paddr = (sockaddr_in*)&addr;
::memset(paddr, 0x00U, address_length = sizeof(sockaddr_in));

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015,2016,2018,2024,2025 by Jonathan Naylor G4KLX
* Copyright (C) 2015,2016,2018,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 = "20250319";
const char* VERSION = "20260215";
#endif

View file

@ -15,8 +15,13 @@ line. The Parrot takes the UDP port number to listen on as an argument.
The MMDVM .ini file should have the IP address and port number of the client in
the [NXDN Network] settings.
The file that contains the information about the reachable reflectors is held in
the NXDNHosts.txt file that should be donwloaded from the DVRef.com web site. A
script to do this under Linux is included. This is handled automatically in WPSD
and Pi-Star.
These programs build on 32-bit and 64-bit Linux as well as on Windows using
Visual Studio 2019 on x86 and x64.
Visual Studio 2022 on x86 and x64.
This software is licenced under the GPL v2 and is primarily intended for amateur and
educational use.