diff --git a/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 0000000..864d47a --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,349 @@ +# DStarRepeater Configuration Reference + +> **Note:** The config parser treats any line beginning with `#` as a +> comment, and blank lines are skipped. Use `dstarrepeater.example` as +> your starting point for a clean config. + +**Config file location:** `/etc/dstarrepeater` +(or `/etc/dstarrepeater_` when using the `-name` command line option) + +**Format:** `key=value` — one setting per line, no spaces around the `=`. +Boolean values use `0` for off/false and `1` for on/true. + +--- + +## Callsign Settings + +| Setting | Description | +|---------|-------------| +| `callsign=GB3IN C` | Repeater callsign, padded to 8 characters. 7-character base callsign + 1-character module suffix (A, B, C, D, or E). | +| `gateway=` | Gateway callsign, padded to 8 characters (e.g. `GB3IN G`). Leave empty if not linked to a gateway. | +| `mode=0` | Operating mode — see [Mode Values](#mode-values) below. | +| `ack=1` | Acknowledgement type — see [Ack Values](#ack-values) below. Forced to `0` in Gateway, TX Only, RX Only, and TX and RX modes. | +| `restriction=0` | `1` = only whitelisted callsigns accepted. | +| `rpt1Validation=1` | `1` = only accept headers addressed to this repeater. Forced to `1` in Gateway mode. | +| `dtmfBlanking=1` | `1` = mute DTMF tones from the audio stream. | +| `errorReply=1` | `1` = reply with an error message for invalid commands. | + +### Mode Values + +| Value | Mode | Description | +|:-----:|------|-------------| +| `0` | Duplex | Full duplex repeater (simultaneous TX and RX) | +| `1` | Simplex | Simplex repeater | +| `2` | Gateway | Gateway mode (ack forced off, RPT1 validation forced on) | +| `3` | TX Only | Transmit only (ack forced off) | +| `4` | RX Only | Receive only (ack forced off) | +| `5` | TX and RX | Sequential transmit then receive | + +### Ack Values + +| Value | Type | Description | +|:-----:|------|-------------| +| `0` | Off | No acknowledgement | +| `1` | BER | Respond with Bit Error Rate | +| `2` | Status | Respond with status message | + +--- + +## Network Settings + +| Setting | Description | +|---------|-------------| +| `gatewayAddress=127.0.0.1` | IP address of the [ircDDBGateway](https://github.com/g4klx/ircDDBGateway) or [DStarGateway](https://github.com/g4klx/DStarGateway). | +| `gatewayPort=20010` | UDP port the gateway is listening on. | +| `localAddress=127.0.0.1` | Local IP address to bind for gateway communication. | +| `localPort=20011` | Local UDP port for gateway communication. | +| `networkName=` | Network name identifier for multi-repeater setups. Leave empty for a single repeater. | + +--- + +## Modem Type + +| Setting | Description | +|---------|-------------| +| `modemType=DVAP` | The type of modem hardware connected to this repeater. | + +Must be one of the following strings (case-sensitive): + +- `DVAP` +- `DVMEGA` +- `DV-RPTR V1` +- `DV-RPTR V2` +- `DV-RPTR V3` +- `GMSK Modem` +- `MMDVM` +- `Sound Card` +- `Split` +- `Icom Access Point/Terminal Mode` + +Only the settings for your selected modem type need to be configured — see [Modem-Specific Settings](#modem-specific-settings). + +--- + +## Timing + +| Setting | Description | +|---------|-------------| +| `timeout=180` | Transmission timeout in seconds. `0` to disable. | +| `ackTime=500` | Acknowledgement delay in milliseconds. | + +--- + +## Beacon + +| Setting | Description | +|---------|-------------| +| `beaconTime=600` | Beacon interval in seconds. `0` to disable. | +| `beaconText=D-Star Repeater` | Text transmitted in the beacon slow data field. | +| `beaconVoice=0` | `1` = transmit a voice beacon in addition to data. | +| `language=0` | Language for voice beacon text-to-speech — see [Language Values](#language-values) below. | + +### Language Values + +| Value | Language | +|:-----:|----------| +| `0` | English (UK) | +| `1` | Deutsch | +| `2` | Dansk | +| `3` | Francais | +| `4` | Italiano | +| `5` | Polski | +| `6` | Espanol | +| `7` | Svenska | +| `8` | Nederlands | +| `9` | English (US) | +| `10` | Norsk | + +--- + +## Announcement + +| Setting | Description | +|---------|-------------| +| `announcementEnabled=0` | `1` = enable the announcement/recording feature. | +| `announcementTime=500` | Announcement playback interval in milliseconds. | +| `announcementRecordRPT1=` | RPT1 callsign that triggers recording. | +| `announcementRecordRPT2=` | RPT2 callsign that triggers recording. | +| `announcementDeleteRPT1=` | RPT1 callsign that triggers deletion. | +| `announcementDeleteRPT2=` | RPT2 callsign that triggers deletion. | + +--- + +## DTMF Control + +| Setting | Description | +|---------|-------------| +| `controlEnabled=0` | `1` = enable DTMF remote control. | +| `controlRPT1=` | RPT1 callsign required for control commands. | +| `controlRPT2=` | RPT2 callsign required for control commands. | +| `controlShutdown=` | DTMF sequence to remotely shut down the repeater. | +| `controlStartup=` | DTMF sequence to remotely start up the repeater. | +| `controlStatus1=` | DTMF sequence to trigger status message 1. | +| `controlStatus2=` | DTMF sequence to trigger status message 2. | +| `controlStatus3=` | DTMF sequence to trigger status message 3. | +| `controlStatus4=` | DTMF sequence to trigger status message 4. | +| `controlStatus5=` | DTMF sequence to trigger status message 5. | +| `controlCommand1=` | DTMF sequence for custom command 1. | +| `controlCommand1Line=` | Shell command executed when command 1 is received. | +| `controlCommand2=` | DTMF sequence for custom command 2. | +| `controlCommand2Line=` | Shell command executed when command 2 is received. | +| `controlCommand3=` | DTMF sequence for custom command 3. | +| `controlCommand3Line=` | Shell command executed when command 3 is received. | +| `controlCommand4=` | DTMF sequence for custom command 4. | +| `controlCommand4Line=` | Shell command executed when command 4 is received. | +| `controlCommand5=` | DTMF sequence for custom command 5. | +| `controlCommand5Line=` | Shell command executed when command 5 is received. | +| `controlCommand6=` | DTMF sequence for custom command 6. | +| `controlCommand6Line=` | Shell command executed when command 6 is received. | +| `controlOutput1=` | DTMF sequence to toggle output relay 1. | +| `controlOutput2=` | DTMF sequence to toggle output relay 2. | +| `controlOutput3=` | DTMF sequence to toggle output relay 3. | +| `controlOutput4=` | DTMF sequence to toggle output relay 4. | + +--- + +## External Controller + +| Setting | Description | +|---------|-------------| +| `controllerType=` | Hardware controller type for PTT, LEDs, and relays — see [Controller Types](#controller-types) below. Leave empty for none. | +| `serialConfig=1` | Hardware config selection (1–5). Selects pin/output mappings on the controller. | +| `pttInvert=0` | `1` = invert PTT output (active low). | +| `activeHangTime=0` | Seconds to hold active state after last transmission ends. `0` to disable. | + +### Controller Types + +| Value | Description | +|-------|-------------| +| *(empty)* | No controller | +| `GPIO` | Raspberry Pi GPIO pins | +| `UDRC` | UDRC board (Raspberry Pi) | +| `Velleman K8055 - 0` | Velleman K8055 USB board at address 0–3 | +| `URI USB - 1` | URI USB relay card at address 1–6 | +| `Serial - /dev/ttyUSB0` | Serial port controller | +| `Arduino - /dev/ttyUSB0` | Arduino controller via serial | + +--- + +## Output Relay Defaults + +| Setting | Description | +|---------|-------------| +| `output1=0` | Default state of output relay 1 at startup. `1` = on. | +| `output2=0` | Default state of output relay 2 at startup. `1` = on. | +| `output3=0` | Default state of output relay 3 at startup. `1` = on. | +| `output4=0` | Default state of output relay 4 at startup. `1` = on. | + +--- + +## Logging + +| Setting | Description | +|---------|-------------| +| `logging=0` | `1` = enable logging to file. | + +--- + +## Window Position (GUI only) + +| Setting | Description | +|---------|-------------| +| `windowX=-1` | Window X position. `-1` for system default. | +| `windowY=-1` | Window Y position. `-1` for system default. | + +--- + +## Modem-Specific Settings + +Only configure the section that matches your `modemType`. + +### DVAP + +| Setting | Description | +|---------|-------------| +| `dvapPort=` | Serial port (e.g. `/dev/ttyUSB0`). | +| `dvapFrequency=145500000` | Operating frequency in Hz (e.g. `438500000` = 438.500 MHz). | +| `dvapPower=10` | Transmit power in dBm. | +| `dvapSquelch=-100` | Squelch level in dBm. Signals below this are ignored. | + +### GMSK Modem + +| Setting | Description | +|---------|-------------| +| `gmskAddress=768` | USB address of the GMSK modem (decimal). Default 768 = 0x0300 hex. | + +### DV-RPTR V1 + +| Setting | Description | +|---------|-------------| +| `dvrptr1Port=` | Serial port (e.g. `/dev/ttyUSB0`). | +| `dvrptr1RXInvert=0` | `1` = invert received signal polarity. | +| `dvrptr1TXInvert=0` | `1` = invert transmitted signal polarity. | +| `dvrptr1Channel=0` | `0` = Channel A, `1` = Channel B. | +| `dvrptr1ModLevel=20` | Modulation level (0–100%). | +| `dvrptr1TXDelay=150` | Transmit delay in milliseconds. | + +### DV-RPTR V2 + +| Setting | Description | +|---------|-------------| +| `dvrptr2Connection=0` | `0` = USB, `1` = Network. | +| `dvrptr2USBPort=` | USB serial port (when connection = `0`). | +| `dvrptr2Address=127.0.0.1` | Network address (when connection = `1`). | +| `dvrptr2Port=0` | Network port (when connection = `1`). | +| `dvrptr2TXInvert=0` | `1` = invert transmitted signal polarity. | +| `dvrptr2ModLevel=20` | Modulation level (0–100%). | +| `dvrptr2TXDelay=150` | Transmit delay in milliseconds. | + +### DV-RPTR V3 + +| Setting | Description | +|---------|-------------| +| `dvrptr3Connection=0` | `0` = USB, `1` = Network. | +| `dvrptr3USBPort=` | USB serial port (when connection = `0`). | +| `dvrptr3Address=127.0.0.1` | Network address (when connection = `1`). | +| `dvrptr3Port=0` | Network port (when connection = `1`). | +| `dvrptr3TXInvert=0` | `1` = invert transmitted signal polarity. | +| `dvrptr3ModLevel=20` | Modulation level (0–100%). | +| `dvrptr3TXDelay=150` | Transmit delay in milliseconds. | + +### DVMEGA + +| Setting | Description | +|---------|-------------| +| `dvmegaPort=` | Serial port (e.g. `/dev/ttyAMA0`). | +| `dvmegaVariant=0` | `0` = Modem, `1` = Radio 2m, `2` = Radio 70cm, `3` = Radio 2m/70cm. | +| `dvmegaRXInvert=0` | `1` = invert received signal polarity. | +| `dvmegaTXInvert=0` | `1` = invert transmitted signal polarity. | +| `dvmegaTXDelay=150` | Transmit delay in milliseconds. | +| `dvmegaRXFrequency=145500000` | Receive frequency in Hz (radio variants only). | +| `dvmegaTXFrequency=145500000` | Transmit frequency in Hz (radio variants only). | +| `dvmegaPower=100` | Transmit power as a percentage (0–100%, radio variants only). | + +### MMDVM + +| Setting | Description | +|---------|-------------| +| `mmdvmPort=` | Serial port (e.g. `/dev/ttyACM0`). | +| `mmdvmRXInvert=0` | `1` = invert received signal polarity. | +| `mmdvmTXInvert=0` | `1` = invert transmitted signal polarity. | +| `mmdvmPTTInvert=0` | `1` = invert PTT signal. | +| `mmdvmTXDelay=50` | Transmit delay in milliseconds. | +| `mmdvmRXLevel=100` | Receive audio level (0–100%). | +| `mmdvmTXLevel=100` | Transmit audio level (0–100%). | + +### Sound Card + +| Setting | Description | +|---------|-------------| +| `soundCardRXDevice=` | Sound card device name for receiving audio. | +| `soundCardTXDevice=` | Sound card device name for transmitting audio. | +| `soundCardRXInvert=0` | `1` = invert received signal polarity. | +| `soundCardTXInvert=0` | `1` = invert transmitted signal polarity. | +| `soundCardRXLevel=1.0000` | Receive level multiplier (1.0 = unity gain). | +| `soundCardTXLevel=1.0000` | Transmit level multiplier (1.0 = unity gain). | +| `soundCardTXDelay=150` | Transmit delay in milliseconds. | +| `soundCardTXTail=50` | Transmit tail in milliseconds (extra carrier after last frame). | + +### Icom Access Point/Terminal Mode + +| Setting | Description | +|---------|-------------| +| `icomPort=` | Serial port (e.g. `/dev/ttyUSB0`). | + +### Split + +| Setting | Description | +|---------|-------------| +| `splitLocalAddress=` | Local IP address for split RX/TX communication. | +| `splitLocalPort=0` | Local UDP port for split communication. | +| `splitTimeout=0` | Split timeout in seconds. `0` to disable. | + +Split TX/RX names are stored as indexed entries (`splitTXName0`, `splitRXName0`, etc.) — up to 5 transmitters and 25 receivers. + +--- + +## MQTT Settings + +*Only used when built with `make MQTT=1`. See [MQTT.md](MQTT.md) for full details on topics, JSON format, and Display-Driver compatibility.* + +| Setting | Description | +|---------|-------------| +| `mqttHost=127.0.0.1` | MQTT broker hostname or IP address. Set to empty to disable MQTT at runtime even when compiled in. | +| `mqttPort=1883` | MQTT broker port. | +| `mqttAuth=0` | `1` = authenticate with the broker using `mqttUsername` and `mqttPassword`. | +| `mqttUsername=` | Broker username (when `mqttAuth=1`). | +| `mqttPassword=` | Broker password (when `mqttAuth=1`). | +| `mqttKeepalive=60` | Keepalive interval in seconds. Minimum 5. | +| `mqttName=dstar-repeater` | Client name and topic prefix for all published messages. | + +### MQTT Topics + +With the default `mqttName` of `dstar-repeater`, the following topics are published: + +| Topic | Content | +|-------|---------| +| `dstar-repeater/log` | Timestamped log messages, filtered by severity. | +| `dstar-repeater/status` | JSON repeater status, published once per second. | +| `dstar-repeater/json` | Display-Driver-compatible events at state transitions. | diff --git a/Common/DStarRepeaterConfig.cpp b/Common/DStarRepeaterConfig.cpp index 2528485..48adfef 100644 --- a/Common/DStarRepeaterConfig.cpp +++ b/Common/DStarRepeaterConfig.cpp @@ -265,6 +265,24 @@ const unsigned int DEFAULT_SPLIT_TIMEOUT = 0U; const wxString DEFAULT_ICOM_PORT = wxEmptyString; +#if defined(MQTT) +const wxString KEY_MQTT_HOST = wxT("mqttHost"); +const wxString KEY_MQTT_PORT = wxT("mqttPort"); +const wxString KEY_MQTT_AUTH = wxT("mqttAuth"); +const wxString KEY_MQTT_USERNAME = wxT("mqttUsername"); +const wxString KEY_MQTT_PASSWORD = wxT("mqttPassword"); +const wxString KEY_MQTT_KEEPALIVE = wxT("mqttKeepalive"); +const wxString KEY_MQTT_NAME = wxT("mqttName"); + +const wxString DEFAULT_MQTT_HOST = wxT("127.0.0.1"); +const unsigned int DEFAULT_MQTT_PORT = 1883U; +const bool DEFAULT_MQTT_AUTH = false; +const wxString DEFAULT_MQTT_USERNAME = wxEmptyString; +const wxString DEFAULT_MQTT_PASSWORD = wxEmptyString; +const unsigned int DEFAULT_MQTT_KEEPALIVE = 60U; +const wxString DEFAULT_MQTT_NAME = wxT("dstar-repeater"); +#endif + #if defined(__WINDOWS__) CDStarRepeaterConfig::CDStarRepeaterConfig(wxConfigBase* config, const wxString& dir, const wxString& configName, const wxString& name) : @@ -389,6 +407,15 @@ m_splitTXNames(), m_splitRXNames(), m_splitTimeout(DEFAULT_SPLIT_TIMEOUT), m_icomPort(DEFAULT_ICOM_PORT) +#if defined(MQTT) +,m_mqttHost(DEFAULT_MQTT_HOST), +m_mqttPort(DEFAULT_MQTT_PORT), +m_mqttAuth(DEFAULT_MQTT_AUTH), +m_mqttUsername(DEFAULT_MQTT_USERNAME), +m_mqttPassword(DEFAULT_MQTT_PASSWORD), +m_mqttKeepalive(DEFAULT_MQTT_KEEPALIVE), +m_mqttName(DEFAULT_MQTT_NAME) +#endif { wxASSERT(config != NULL); wxASSERT(!dir.IsEmpty()); @@ -825,6 +852,15 @@ m_splitTXNames(), m_splitRXNames(), m_splitTimeout(DEFAULT_SPLIT_TIMEOUT), m_icomPort(DEFAULT_ICOM_PORT) +#if defined(MQTT) +,m_mqttHost(DEFAULT_MQTT_HOST) +,m_mqttPort(DEFAULT_MQTT_PORT) +,m_mqttAuth(DEFAULT_MQTT_AUTH) +,m_mqttUsername(DEFAULT_MQTT_USERNAME) +,m_mqttPassword(DEFAULT_MQTT_PASSWORD) +,m_mqttKeepalive(DEFAULT_MQTT_KEEPALIVE) +,m_mqttName(DEFAULT_MQTT_NAME) +#endif { wxASSERT(!dir.IsEmpty()); @@ -856,16 +892,12 @@ m_icomPort(DEFAULT_ICOM_PORT) for (wxString str = file.GetFirstLine(); !file.Eof(); str = file.GetNextLine()) { - if (str.GetChar(0U) == wxT('#')) { - str = file.GetNextLine(); + if (str.IsEmpty() || str.GetChar(0U) == wxT('#')) continue; - } int n = str.Find(wxT('=')); - if (n == wxNOT_FOUND) { - str = file.GetNextLine(); + if (n == wxNOT_FOUND) continue; - } wxString key = str.Left(n); wxString val = str.Mid(n + 1U); @@ -1158,6 +1190,25 @@ m_icomPort(DEFAULT_ICOM_PORT) m_soundCardTXTail = (unsigned int)temp2; } else if (key.IsSameAs(KEY_ICOM_PORT)) { m_icomPort = val; +#if defined(MQTT) + } else if (key.IsSameAs(KEY_MQTT_HOST)) { + m_mqttHost = val; + } else if (key.IsSameAs(KEY_MQTT_PORT)) { + val.ToULong(&temp2); + m_mqttPort = (unsigned int)temp2; + } else if (key.IsSameAs(KEY_MQTT_AUTH)) { + val.ToLong(&temp1); + m_mqttAuth = temp1 == 1L; + } else if (key.IsSameAs(KEY_MQTT_USERNAME)) { + m_mqttUsername = val; + } else if (key.IsSameAs(KEY_MQTT_PASSWORD)) { + m_mqttPassword = val; + } else if (key.IsSameAs(KEY_MQTT_KEEPALIVE)) { + val.ToULong(&temp2); + m_mqttKeepalive = (unsigned int)temp2; + } else if (key.IsSameAs(KEY_MQTT_NAME)) { + m_mqttName = val; +#endif } else if (key.IsSameAs(KEY_SPLIT_LOCALADDRESS)) { m_splitLocalAddress = val; } else if (key.IsSameAs(KEY_SPLIT_LOCALPORT)) { @@ -1608,6 +1659,30 @@ void CDStarRepeaterConfig::setIcom(const wxString& port) m_icomPort = port; } +#if defined(MQTT) +void CDStarRepeaterConfig::getMQTT(wxString& host, unsigned int& port, bool& auth, wxString& username, wxString& password, unsigned int& keepalive, wxString& name) const +{ + host = m_mqttHost; + port = m_mqttPort; + auth = m_mqttAuth; + username = m_mqttUsername; + password = m_mqttPassword; + keepalive = m_mqttKeepalive; + name = m_mqttName; +} + +void CDStarRepeaterConfig::setMQTT(const wxString& host, unsigned int port, bool auth, const wxString& username, const wxString& password, unsigned int keepalive, const wxString& name) +{ + m_mqttHost = host; + m_mqttPort = port; + m_mqttAuth = auth; + m_mqttUsername = username; + m_mqttPassword = password; + m_mqttKeepalive = keepalive; + m_mqttName = name; +} +#endif + bool CDStarRepeaterConfig::write() { #if defined(__WINDOWS__) @@ -1899,6 +1974,16 @@ bool CDStarRepeaterConfig::write() buffer.Printf(wxT("%s=%s"), KEY_ICOM_PORT.c_str(), m_icomPort.c_str()); file.AddLine(buffer); +#if defined(MQTT) + buffer.Printf(wxT("%s=%s"), KEY_MQTT_HOST.c_str(), m_mqttHost.c_str()); file.AddLine(buffer); + buffer.Printf(wxT("%s=%u"), KEY_MQTT_PORT.c_str(), m_mqttPort); file.AddLine(buffer); + buffer.Printf(wxT("%s=%d"), KEY_MQTT_AUTH.c_str(), m_mqttAuth ? 1 : 0); file.AddLine(buffer); + buffer.Printf(wxT("%s=%s"), KEY_MQTT_USERNAME.c_str(), m_mqttUsername.c_str()); file.AddLine(buffer); + buffer.Printf(wxT("%s=%s"), KEY_MQTT_PASSWORD.c_str(), m_mqttPassword.c_str()); file.AddLine(buffer); + buffer.Printf(wxT("%s=%u"), KEY_MQTT_KEEPALIVE.c_str(), m_mqttKeepalive); file.AddLine(buffer); + buffer.Printf(wxT("%s=%s"), KEY_MQTT_NAME.c_str(), m_mqttName.c_str()); file.AddLine(buffer); +#endif + buffer.Printf(wxT("%s=%s"), KEY_SPLIT_LOCALADDRESS.c_str(), m_splitLocalAddress.c_str()); file.AddLine(buffer); buffer.Printf(wxT("%s=%u"), KEY_SPLIT_LOCALPORT.c_str(), m_splitLocalPort); file.AddLine(buffer); diff --git a/Common/DStarRepeaterConfig.h b/Common/DStarRepeaterConfig.h index 4576514..4154e2a 100644 --- a/Common/DStarRepeaterConfig.h +++ b/Common/DStarRepeaterConfig.h @@ -98,6 +98,11 @@ public: void getIcom(wxString& port) const; void setIcom(const wxString& port); +#if defined(MQTT) + void getMQTT(wxString& host, unsigned int& port, bool& auth, wxString& username, wxString& password, unsigned int& keepalive, wxString& name) const; + void setMQTT(const wxString& host, unsigned int port, bool auth, const wxString& username, const wxString& password, unsigned int keepalive, const wxString& name); +#endif + bool write(); private: @@ -244,6 +249,17 @@ private: // Icom Access Point/Terminal Mode wxString m_icomPort; + +#if defined(MQTT) + // MQTT + wxString m_mqttHost; + unsigned int m_mqttPort; + bool m_mqttAuth; + wxString m_mqttUsername; + wxString m_mqttPassword; + unsigned int m_mqttKeepalive; + wxString m_mqttName; +#endif }; #endif diff --git a/Common/Logger.cpp b/Common/Logger.cpp index 6aa789b..04de9e3 100644 --- a/Common/Logger.cpp +++ b/Common/Logger.cpp @@ -18,6 +18,12 @@ #include "Logger.h" +#if defined(MQTT) +#include "MQTTConnection.h" +CMQTTConnection* g_mqtt = NULL; +unsigned int g_mqttLevel = 2U; +#endif + CLogger::CLogger(const wxString& directory, const wxString& name) : wxLog(), m_name(name), @@ -81,6 +87,22 @@ void CLogger::DoLogRecord(wxLogLevel level, const wxString& msg, const wxLogReco writeLog(message.c_str(), info.timestamp); +#if defined(MQTT) + if (g_mqtt != NULL && g_mqttLevel != 0U) { + unsigned int numLevel = 0U; + switch (level) { + case wxLOG_FatalError: numLevel = 6U; break; + case wxLOG_Error: numLevel = 5U; break; + case wxLOG_Warning: numLevel = 4U; break; + case wxLOG_Info: numLevel = 3U; break; + case wxLOG_Message: numLevel = 2U; break; + default: numLevel = 1U; break; + } + if (numLevel >= g_mqttLevel) + g_mqtt->publish("log", (const char*)message.mb_str()); + } +#endif + if (level == wxLOG_FatalError) ::abort(); } diff --git a/Common/Logger.h b/Common/Logger.h index b9ca2c2..72e7368 100644 --- a/Common/Logger.h +++ b/Common/Logger.h @@ -23,6 +23,12 @@ #include #include +#if defined(MQTT) +class CMQTTConnection; +extern CMQTTConnection* g_mqtt; +extern unsigned int g_mqttLevel; +#endif + class CLogger : public wxLog { public: CLogger(const wxString& directory, const wxString& name); diff --git a/Common/MQTTConnection.cpp b/Common/MQTTConnection.cpp new file mode 100644 index 0000000..7c63241 --- /dev/null +++ b/Common/MQTTConnection.cpp @@ -0,0 +1,236 @@ +/* + * 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(MQTT) + +#include "MQTTConnection.h" + +#include +#include +#include + +#if defined(_WIN32) || defined(_WIN64) +#include +#else +#include +#endif + + +CMQTTConnection::CMQTTConnection(const std::string& host, unsigned short port, const std::string& name, const bool authEnabled, const std::string& username, const std::string& password, const std::vector>& subs, unsigned int keepalive, MQTT_QOS qos) : +m_host(host), +m_port(port), +m_name(name), +m_authEnabled(authEnabled), +m_username(username), +m_password(password), +m_subs(subs), +m_keepalive(keepalive), +m_qos(qos), +m_mosq(nullptr), +m_connected(false) +{ + assert(!host.empty()); + assert(port > 0U); + assert(!name.empty()); + assert(keepalive >= 5U); + + ::mosquitto_lib_init(); +} + +CMQTTConnection::~CMQTTConnection() +{ + ::mosquitto_lib_cleanup(); +} + +bool CMQTTConnection::open() +{ + char name[50U]; +#if defined(_WIN32) || defined(_WIN64) + ::sprintf(name, "DStarRepeater.%u", (unsigned)::_getpid()); +#else + ::sprintf(name, "DStarRepeater.%u", (unsigned)::getpid()); +#endif + + ::fprintf(stdout, "DStarRepeater (%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(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(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_loop_stop(m_mosq, true); + ::mosquitto_destroy(m_mosq); + m_mosq = nullptr; + } +} + +void CMQTTConnection::onConnect(mosquitto* mosq, void* obj, int rc) +{ + assert(mosq != nullptr); + assert(obj != nullptr); + + ::fprintf(stdout, "MQTT: on_connect: %s\n", ::mosquitto_connack_string(rc)); + if (rc != 0) { + ::mosquitto_disconnect(mosq); + return; + } + + CMQTTConnection* p = static_cast(obj); + p->m_connected = true; + + for (std::vector>::const_iterator it = p->m_subs.cbegin(); it != p->m_subs.cend(); ++it) { + std::string topic = (*it).first; + + if (topic.find_first_of('/') == std::string::npos) { + char topicEx[100U]; + ::sprintf(topicEx, "%s/%s", p->m_name.c_str(), topic.c_str()); + + rc = ::mosquitto_subscribe(mosq, nullptr, topicEx, static_cast(p->m_qos)); + if (rc != MOSQ_ERR_SUCCESS) { + ::fprintf(stderr, "MQTT: error subscribing to %s - %s\n", topicEx, ::mosquitto_strerror(rc)); + ::mosquitto_disconnect(mosq); + } + } else { + rc = ::mosquitto_subscribe(mosq, nullptr, topic.c_str(), static_cast(p->m_qos)); + if (rc != MOSQ_ERR_SUCCESS) { + ::fprintf(stderr, "MQTT: error subscribing to %s - %s\n", topic.c_str(), ::mosquitto_strerror(rc)); + ::mosquitto_disconnect(mosq); + } + } + } +} + +void CMQTTConnection::onSubscribe(mosquitto* mosq, void* obj, int mid, int qosCount, const int* grantedQOS) +{ + assert(mosq != nullptr); + assert(obj != nullptr); + assert(grantedQOS != nullptr); + + for (int i = 0; i < qosCount; i++) + ::fprintf(stdout, "MQTT: on_subscribe: %d:%d\n", i, grantedQOS[i]); +} + +void CMQTTConnection::onMessage(mosquitto* mosq, void* obj, const mosquitto_message* message) +{ + assert(mosq != nullptr); + assert(obj != nullptr); + assert(message != nullptr); + + CMQTTConnection* p = static_cast(obj); + + for (std::vector>::const_iterator it = p->m_subs.cbegin(); it != p->m_subs.cend(); ++it) { + std::string topic = (*it).first; + + char topicEx[100U]; + ::sprintf(topicEx, "%s/%s", p->m_name.c_str(), topic.c_str()); + + if (::strcmp(topicEx, message->topic) == 0) { + (*it).second((unsigned char*)message->payload, message->payloadlen); + break; + } + } +} + +void CMQTTConnection::onDisconnect(mosquitto* mosq, void* obj, int rc) +{ + assert(mosq != nullptr); + assert(obj != nullptr); + + ::fprintf(stdout, "MQTT: on_disconnect: %s\n", ::mosquitto_reason_string(rc)); + + CMQTTConnection* p = static_cast(obj); + p->m_connected = false; +} + +#endif diff --git a/Common/MQTTConnection.h b/Common/MQTTConnection.h new file mode 100644 index 0000000..1c21188 --- /dev/null +++ b/Common/MQTTConnection.h @@ -0,0 +1,69 @@ +/* + * 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(MQTTCONNECTION_H) +#define MQTTCONNECTION_H + +#if defined(MQTT) + +#include + +#include +#include + +enum class MQTT_QOS : int { + AT_MODE_ONCE = 0, + AT_LEAST_ONCE = 1, + EXACTLY_ONCE = 2 +}; + +class CMQTTConnection { +public: + CMQTTConnection(const std::string& host, unsigned short port, const std::string& name, const bool authEnabled, const std::string& username, const std::string& password, const std::vector>& subs, unsigned int keepalive, MQTT_QOS qos = MQTT_QOS::EXACTLY_ONCE); + ~CMQTTConnection(); + + bool open(); + + bool publish(const char* topic, const char* text); + bool 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> 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 + +#endif diff --git a/Common/MQTTPublisher.h b/Common/MQTTPublisher.h new file mode 100644 index 0000000..2001586 --- /dev/null +++ b/Common/MQTTPublisher.h @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2025 by Andy Taylor MW0MWZ + * + * 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. + */ + +#ifndef MQTTPublisher_H +#define MQTTPublisher_H + +#if defined(MQTT) + +/* + * Display-Driver-compatible MQTT JSON publishing for DStarRepeater. + * + * Publishes event-driven JSON messages to the "json" topic in the format + * expected by Display-Driver, enabling LCD/OLED display output. + * + * These are separate from the periodic status messages on the "status" + * topic — the display needs discrete start/end events, not continuous + * state snapshots. + */ + +#include "MQTTConnection.h" +#include "Logger.h" + +#include + +#include + +// Publish a D-Star call start event (RF or network) +static inline void mqttPublishDStarStart(const wxString& myCall1, const wxString& myCall2, + const wxString& yourCall, const wxString& rptCall2, const char* source) +{ + if (g_mqtt == NULL) + return; + + char buf[512]; + ::snprintf(buf, sizeof(buf), + "{\"D-Star\":{\"action\":\"start\"," + "\"source_cs\":\"%s\",\"source_ext\":\"%s\"," + "\"destination_cs\":\"%s\",\"reflector\":\"%s\"," + "\"source\":\"%s\"}}", + (const char*)myCall1.mb_str(), + (const char*)myCall2.mb_str(), + (const char*)yourCall.mb_str(), + (const char*)rptCall2.mb_str(), + source); + + g_mqtt->publish("json", buf); +} + +// Publish a D-Star call end event (normal termination) +static inline void mqttPublishDStarEnd() +{ + if (g_mqtt == NULL) + return; + + g_mqtt->publish("json", "{\"D-Star\":{\"action\":\"end\"}}"); +} + +// Publish a D-Star call lost event (watchdog timeout / abnormal) +static inline void mqttPublishDStarLost() +{ + if (g_mqtt == NULL) + return; + + g_mqtt->publish("json", "{\"D-Star\":{\"action\":\"lost\"}}"); +} + +// Publish idle mode (no traffic) +static inline void mqttPublishIdle() +{ + if (g_mqtt == NULL) + return; + + g_mqtt->publish("json", "{\"MMDVM\":{\"mode\":\"idle\"}}"); +} + +// Publish D-Star BER value +static inline void mqttPublishBER(float ber) +{ + if (g_mqtt == NULL) + return; + + char buf[128]; + ::snprintf(buf, sizeof(buf), + "{\"BER\":{\"mode\":\"D-Star\",\"value\":%.1f}}", + ber); + + g_mqtt->publish("json", buf); +} + +// Publish D-Star slow data text +static inline void mqttPublishText(const wxString& text) +{ + if (g_mqtt == NULL) + return; + + char buf[256]; + ::snprintf(buf, sizeof(buf), + "{\"Text\":{\"mode\":\"D-Star\",\"value\":\"%s\"}}", + (const char*)text.mb_str()); + + g_mqtt->publish("json", buf); +} + +#endif + +#endif diff --git a/Common/Makefile b/Common/Makefile index be27c03..efba53f 100644 --- a/Common/Makefile +++ b/Common/Makefile @@ -5,7 +5,7 @@ OBJECTS = AMBEFEC.o AnnouncementUnit.o ArduinoController.o BeaconUnit.o Callsign GPIOController.o HardwareController.o HeaderData.o IcomController.o K8055Controller.o LogEvent.o Logger.o MMDVMController.o \ Modem.o OutputQueue.o RepeaterProtocolHandler.o SerialDataController.o SerialLineController.o SerialPortSelector.o \ SlowDataDecoder.o SlowDataEncoder.o SoundCardController.o SoundCardReaderWriter.o SplitController.o TCPReaderWriter.o \ - Timer.o UDPReaderWriter.o UDRCController.o URIUSBController.o Utils.o + MQTTConnection.o Timer.o UDPReaderWriter.o UDRCController.o URIUSBController.o Utils.o .PHONY: all clean diff --git a/DStarRepeater/DStarRepeaterApp.cpp b/DStarRepeater/DStarRepeaterApp.cpp index 07e5d72..beb0d92 100644 --- a/DStarRepeater/DStarRepeaterApp.cpp +++ b/DStarRepeater/DStarRepeaterApp.cpp @@ -52,6 +52,9 @@ #include "DStarDefines.h" #include "Version.h" #include "Logger.h" +#if defined(MQTT) +#include "MQTTConnection.h" +#endif wxIMPLEMENT_APP(CDStarRepeaterApp); @@ -195,6 +198,37 @@ bool CDStarRepeaterApp::OnInit() // Log the version of wxWidgets and the Operating System wxLogInfo("Using wxWidgets %d.%d.%d on %s", wxMAJOR_VERSION, wxMINOR_VERSION, wxRELEASE_NUMBER, ::wxGetOsDescription().c_str()); +#if defined(MQTT) + wxString mqttHost, mqttUsername, mqttPassword, mqttName; + unsigned int mqttPort, mqttKeepalive; + bool mqttAuth; + m_config->getMQTT(mqttHost, mqttPort, mqttAuth, mqttUsername, mqttPassword, mqttKeepalive, mqttName); + + if (!mqttHost.IsEmpty()) { + std::vector> subscriptions; + + g_mqtt = new CMQTTConnection( + std::string(mqttHost.mb_str()), + (unsigned short)mqttPort, + std::string(mqttName.mb_str()), + mqttAuth, + std::string(mqttUsername.mb_str()), + std::string(mqttPassword.mb_str()), + subscriptions, + mqttKeepalive + ); + + bool ret = g_mqtt->open(); + if (!ret) { + wxLogError("Unable to start MQTT connection to %s:%u", mqttHost.c_str(), mqttPort); + delete g_mqtt; + g_mqtt = NULL; + } else { + wxLogInfo("MQTT connected to %s:%u as %s", mqttHost.c_str(), mqttPort, mqttName.c_str()); + } + } +#endif + createThread(); return true; @@ -207,6 +241,15 @@ int CDStarRepeaterApp::OnExit() m_logChain->SetLog(NULL); m_thread->kill(); + m_thread->Wait(); + +#if defined(MQTT) + if (g_mqtt != NULL) { + g_mqtt->close(); + delete g_mqtt; + g_mqtt = NULL; + } +#endif delete m_config; diff --git a/DStarRepeater/DStarRepeaterRXThread.cpp b/DStarRepeater/DStarRepeaterRXThread.cpp index 28cf810..20bcb67 100644 --- a/DStarRepeater/DStarRepeaterRXThread.cpp +++ b/DStarRepeater/DStarRepeaterRXThread.cpp @@ -51,6 +51,9 @@ m_ambeBits(1U), m_ambeErrors(0U), m_lastAMBEBits(0U), m_lastAMBEErrors(0U) +#if defined(MQTT) +,m_mqttStatusTimer(1000U, 1U) // 1s +#endif { setRadioState(DSRXS_LISTENING); } @@ -70,6 +73,9 @@ void *CDStarRepeaterRXThread::Entry() return NULL; m_registerTimer.start(10U); +#if defined(MQTT) + m_mqttStatusTimer.start(); +#endif wxString hardware = m_type; int n = hardware.Find(wxT(' ')); @@ -94,6 +100,25 @@ void *CDStarRepeaterRXThread::Entry() m_registerTimer.start(30U); } +#if defined(MQTT) + // Publish status to MQTT every second + if (m_mqttStatusTimer.hasExpired()) { + if (g_mqtt != NULL) { + CDStarRepeaterStatusData* status = getStatus(); + std::string json = status->toJSON(); + g_mqtt->publish("status", json.c_str()); + delete status; + + // Publish BER for Display-Driver during active RF + if (m_rptState == DSRS_VALID && m_ambeBits > 0U) { + float ber = float(m_ambeErrors * 100U) / float(m_ambeBits); + mqttPublishBER(ber); + } + } + m_mqttStatusTimer.start(); + } +#endif + unsigned long ms = stopWatch.Time(); if (ms < CYCLE_TIME) { ::wxMilliSleep(CYCLE_TIME - ms); @@ -220,6 +245,10 @@ void CDStarRepeaterRXThread::receiveModem() ::memcpy(data, END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES); processRadioFrame(data, FRAME_END); setRadioState(DSRXS_LISTENING); +#if defined(MQTT) + mqttPublishDStarEnd(); + mqttPublishIdle(); +#endif endOfRadioData(); } break; @@ -357,6 +386,11 @@ bool CDStarRepeaterRXThread::processRadioHeader(CHeaderData* header) delete m_rxHeader; m_rxHeader = header; +#if defined(MQTT) + mqttPublishDStarStart(m_rxHeader->getMyCall1(), m_rxHeader->getMyCall2(), + m_rxHeader->getYourCall(), m_rxHeader->getRptCall2(), "rf"); +#endif + CHeaderData netHeader(*m_rxHeader); netHeader.setRptCall1(m_rxHeader->getRptCall2()); netHeader.setRptCall2(m_rxHeader->getRptCall1()); @@ -444,6 +478,9 @@ CDStarRepeaterStatusData* CDStarRepeaterRXThread::getStatus() void CDStarRepeaterRXThread::clock(unsigned int ms) { m_registerTimer.clock(ms); +#if defined(MQTT) + m_mqttStatusTimer.clock(ms); +#endif } void CDStarRepeaterRXThread::shutdown() diff --git a/DStarRepeater/DStarRepeaterRXThread.h b/DStarRepeater/DStarRepeaterRXThread.h index ba656f7..f19acb3 100644 --- a/DStarRepeater/DStarRepeaterRXThread.h +++ b/DStarRepeater/DStarRepeaterRXThread.h @@ -24,6 +24,9 @@ #include "HeaderData.h" #include "AMBEFEC.h" #include "Timer.h" +#if defined(MQTT) +#include "MQTTPublisher.h" +#endif #include @@ -81,6 +84,10 @@ private: unsigned int m_lastAMBEBits; unsigned int m_lastAMBEErrors; +#if defined(MQTT) + CTimer m_mqttStatusTimer; +#endif + void receiveHeader(CHeaderData* header); void receiveRadioData(unsigned char* data, unsigned int length); void receiveSlowData(unsigned char* data, unsigned int length); diff --git a/DStarRepeater/DStarRepeaterStatusData.cpp b/DStarRepeater/DStarRepeaterStatusData.cpp index 9d4dacc..003e08f 100644 --- a/DStarRepeater/DStarRepeaterStatusData.cpp +++ b/DStarRepeater/DStarRepeaterStatusData.cpp @@ -18,6 +18,10 @@ #include "DStarRepeaterStatusData.h" +#if defined(MQTT) +#include +#endif + CDStarRepeaterStatusData::CDStarRepeaterStatusData(const wxString& myCall1, const wxString& myCall2, const wxString& yourCall, const wxString& rptCall1, const wxString& rptCall2, unsigned char flag1, @@ -197,3 +201,61 @@ wxString CDStarRepeaterStatusData::getStatus5() const { return m_status5; } + +#if defined(MQTT) +static const char* rptStateToString(DSTAR_RPT_STATE state) +{ + switch (state) { + case DSRS_SHUTDOWN: return "shutdown"; + case DSRS_LISTENING: return "listening"; + case DSRS_VALID: return "valid"; + case DSRS_VALID_WAIT: return "valid_wait"; + case DSRS_INVALID: return "invalid"; + case DSRS_INVALID_WAIT: return "invalid_wait"; + case DSRS_TIMEOUT: return "timeout"; + case DSRS_TIMEOUT_WAIT: return "timeout_wait"; + case DSRS_NETWORK: return "network"; + default: return "unknown"; + } +} + +static const char* rxStateToString(DSTAR_RX_STATE state) +{ + switch (state) { + case DSRXS_LISTENING: return "listening"; + case DSRXS_PROCESS_DATA: return "process_data"; + case DSRXS_PROCESS_SLOW_DATA: return "process_slow_data"; + default: return "unknown"; + } +} + +std::string CDStarRepeaterStatusData::toJSON() const +{ + char buffer[1024]; + ::snprintf(buffer, sizeof(buffer), + "{\"myCall1\":\"%s\",\"myCall2\":\"%s\"," + "\"yourCall\":\"%s\",\"rptCall1\":\"%s\",\"rptCall2\":\"%s\"," + "\"tx\":%s,\"rxState\":\"%s\",\"rptState\":\"%s\"," + "\"ber\":%.1f," + "\"text\":\"%s\"," + "\"status1\":\"%s\",\"status2\":\"%s\",\"status3\":\"%s\"," + "\"status4\":\"%s\",\"status5\":\"%s\"}", + (const char*)m_myCall1.mb_str(), + (const char*)m_myCall2.mb_str(), + (const char*)m_yourCall.mb_str(), + (const char*)m_rptCall1.mb_str(), + (const char*)m_rptCall2.mb_str(), + m_tx ? "true" : "false", + rxStateToString(m_rxState), + rptStateToString(m_rptState), + m_percent, + (const char*)m_text.mb_str(), + (const char*)m_status1.mb_str(), + (const char*)m_status2.mb_str(), + (const char*)m_status3.mb_str(), + (const char*)m_status4.mb_str(), + (const char*)m_status5.mb_str()); + + return std::string(buffer); +} +#endif diff --git a/DStarRepeater/DStarRepeaterStatusData.h b/DStarRepeater/DStarRepeaterStatusData.h index f0ae787..9e8864d 100644 --- a/DStarRepeater/DStarRepeaterStatusData.h +++ b/DStarRepeater/DStarRepeaterStatusData.h @@ -24,6 +24,10 @@ #include +#if defined(MQTT) +#include +#endif + class CDStarRepeaterStatusData { public: CDStarRepeaterStatusData(const wxString& myCall1, const wxString& myCall2, const wxString& yourCall, @@ -72,6 +76,10 @@ public: wxString getStatus4() const; wxString getStatus5() const; +#if defined(MQTT) + std::string toJSON() const; +#endif + private: wxString m_myCall1; wxString m_myCall2; diff --git a/DStarRepeater/DStarRepeaterTRXThread.cpp b/DStarRepeater/DStarRepeaterTRXThread.cpp index f238001..1a7183c 100644 --- a/DStarRepeater/DStarRepeaterTRXThread.cpp +++ b/DStarRepeater/DStarRepeaterTRXThread.cpp @@ -119,6 +119,9 @@ m_busyData(false), m_blanking(true), m_recording(false), m_deleting(false) +#if defined(MQTT) +,m_mqttStatusTimer(1000U, 1U) // 1s +#endif { for(int i = 0; i < 5; ++i) m_statusAnnounceTimer[i] = CTimer(1000U, 3U); @@ -161,6 +164,9 @@ void *CDStarRepeaterTRXThread::Entry() m_controller->setRadioTransmit(false); m_statusTimer.start(); m_heartbeatTimer.start(); +#if defined(MQTT) + m_mqttStatusTimer.start(); +#endif if (m_protocolHandler != NULL) m_pollTimer.start(); @@ -233,6 +239,25 @@ void *CDStarRepeaterTRXThread::Entry() m_heartbeatTimer.start(); } +#if defined(MQTT) + // Publish status to MQTT every second + if (m_mqttStatusTimer.hasExpired()) { + if (g_mqtt != NULL) { + CDStarRepeaterStatusData* status = getStatus(); + std::string json = status->toJSON(); + g_mqtt->publish("status", json.c_str()); + delete status; + + // Publish BER for Display-Driver during active RF + if (m_rptState == DSRS_VALID && m_ambeBits > 0U) { + float ber = float(m_ambeErrors * 100U) / float(m_ambeBits); + mqttPublishBER(ber); + } + } + m_mqttStatusTimer.start(); + } +#endif + // Set the output state if (m_rptState == DSRS_VALID || m_rptState == DSRS_INVALID || m_rptState == DSRS_TIMEOUT || m_rptState == DSRS_VALID_WAIT || m_rptState == DSRS_INVALID_WAIT || m_rptState == DSRS_TIMEOUT_WAIT || @@ -560,6 +585,10 @@ void CDStarRepeaterTRXThread::receiveModem() ::memcpy(data, END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES); processRadioFrame(data, FRAME_END); setRadioState(DSRXS_LISTENING); +#if defined(MQTT) + mqttPublishDStarEnd(); + mqttPublishIdle(); +#endif endOfRadioData(); } break; @@ -1070,6 +1099,10 @@ void CDStarRepeaterTRXThread::repeaterStateMachine() wxLogMessage(wxT("Network watchdog has expired")); // Send end of transmission data to the radio m_networkQueue[m_writeNum]->addData(END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES, true); +#if defined(MQTT) + mqttPublishDStarLost(); + mqttPublishIdle(); +#endif endOfNetworkData(); } break; @@ -1344,6 +1377,11 @@ bool CDStarRepeaterTRXThread::processRadioHeader(CHeaderData* header) delete m_rxHeader; m_rxHeader = header; +#if defined(MQTT) + mqttPublishDStarStart(m_rxHeader->getMyCall1(), m_rxHeader->getMyCall2(), + m_rxHeader->getYourCall(), m_rxHeader->getRptCall2(), "rf"); +#endif + if (m_logging != NULL) m_logging->open(*m_rxHeader); @@ -1397,6 +1435,11 @@ void CDStarRepeaterTRXThread::processNetworkHeader(CHeaderData* header) delete m_rxHeader; m_rxHeader = header; +#if defined(MQTT) + mqttPublishDStarStart(m_rxHeader->getMyCall1(), m_rxHeader->getMyCall2(), + m_rxHeader->getYourCall(), m_rxHeader->getRptCall2(), "net"); +#endif + if (m_mode == MODE_GATEWAY) { // If in gateway mode, set the repeater bit, set flag 2 to 0x01, // and change RPT1 & RPT2 just for transmission @@ -1519,6 +1562,10 @@ unsigned int CDStarRepeaterTRXThread::processNetworkFrame(unsigned char* data, u bool end = (seqNo & 0x40U) == 0x40U; if (end) { m_networkQueue[m_writeNum]->addData(END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES, true); +#if defined(MQTT) + mqttPublishDStarEnd(); + mqttPublishIdle(); +#endif endOfNetworkData(); return 1U; } @@ -1761,6 +1808,9 @@ void CDStarRepeaterTRXThread::clock(unsigned int ms) m_announcementTimer.clock(ms); m_statusTimer.clock(ms); m_heartbeatTimer.clock(ms); +#if defined(MQTT) + m_mqttStatusTimer.clock(ms); +#endif if (m_beacon != NULL) m_beacon->clock(); if (m_announcement != NULL) diff --git a/DStarRepeater/DStarRepeaterTRXThread.h b/DStarRepeater/DStarRepeaterTRXThread.h index 4f7384a..feccad9 100644 --- a/DStarRepeater/DStarRepeaterTRXThread.h +++ b/DStarRepeater/DStarRepeaterTRXThread.h @@ -36,6 +36,9 @@ #include "AMBEFEC.h" #include "Timer.h" #include "Utils.h" +#if defined(MQTT) +#include "MQTTPublisher.h" +#endif #include #include @@ -185,6 +188,10 @@ private: bool m_recording; bool m_deleting; +#if defined(MQTT) + CTimer m_mqttStatusTimer; +#endif + void receiveHeader(CHeaderData* header); void receiveRadioData(unsigned char* data, unsigned int length); void receiveSlowData(unsigned char* data, unsigned int length); diff --git a/DStarRepeater/DStarRepeaterTXRXThread.cpp b/DStarRepeater/DStarRepeaterTXRXThread.cpp index 2465ea0..0b80aa6 100644 --- a/DStarRepeater/DStarRepeaterTXRXThread.cpp +++ b/DStarRepeater/DStarRepeaterTXRXThread.cpp @@ -73,6 +73,9 @@ m_headerTime(), m_packetTime(), m_packetCount(0U), m_packetSilence(0U) +#if defined(MQTT) +,m_mqttStatusTimer(1000U, 1U) // 1s +#endif { m_networkQueue = new COutputQueue*[NETWORK_QUEUE_COUNT]; for (unsigned int i = 0U; i < NETWORK_QUEUE_COUNT; i++) @@ -109,6 +112,9 @@ void *CDStarRepeaterTXRXThread::Entry() m_heartbeatTimer.start(); m_statusTimer.start(); m_registerTimer.start(10U); +#if defined(MQTT) + m_mqttStatusTimer.start(); +#endif wxString hardware = m_type; int n = hardware.Find(wxT(' ')); @@ -147,6 +153,25 @@ void *CDStarRepeaterTXRXThread::Entry() m_heartbeatTimer.start(); } +#if defined(MQTT) + // Publish status to MQTT every second + if (m_mqttStatusTimer.hasExpired()) { + if (g_mqtt != NULL) { + CDStarRepeaterStatusData* status = getStatus(); + std::string json = status->toJSON(); + g_mqtt->publish("status", json.c_str()); + delete status; + + // Publish BER for Display-Driver during active RF + if (m_rptState == DSRS_VALID && m_ambeBits > 0U) { + float ber = float(m_ambeErrors * 100U) / float(m_ambeBits); + mqttPublishBER(ber); + } + } + m_mqttStatusTimer.start(); + } +#endif + // Set the output state if (m_tx || (m_activeHangTimer.isRunning() && !m_activeHangTimer.hasExpired())) { m_controller->setActive(true); @@ -330,6 +355,10 @@ void CDStarRepeaterTXRXThread::receiveModem() ::memcpy(data, END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES); processRadioFrame(data, FRAME_END); setRadioState(DSRXS_LISTENING); +#if defined(MQTT) + mqttPublishDStarEnd(); + mqttPublishIdle(); +#endif endOfRadioData(); } break; @@ -549,6 +578,10 @@ void CDStarRepeaterTXRXThread::repeaterStateMachine() wxLogMessage(wxT("Network watchdog has expired")); // Send end of transmission data to the radio m_networkQueue[m_writeNum]->addData(END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES, true); +#if defined(MQTT) + mqttPublishDStarLost(); + mqttPublishIdle(); +#endif endOfNetworkData(); } } @@ -632,6 +665,11 @@ bool CDStarRepeaterTXRXThread::processRadioHeader(CHeaderData* header) delete m_rxHeader; m_rxHeader = header; +#if defined(MQTT) + mqttPublishDStarStart(m_rxHeader->getMyCall1(), m_rxHeader->getMyCall2(), + m_rxHeader->getYourCall(), m_rxHeader->getRptCall2(), "rf"); +#endif + CHeaderData netHeader(*m_rxHeader); netHeader.setRptCall1(m_rxHeader->getRptCall2()); netHeader.setRptCall2(m_rxHeader->getRptCall1()); @@ -666,6 +704,11 @@ void CDStarRepeaterTXRXThread::processNetworkHeader(CHeaderData* header) delete m_txHeader; m_txHeader = header; +#if defined(MQTT) + mqttPublishDStarStart(m_txHeader->getMyCall1(), m_txHeader->getMyCall2(), + m_txHeader->getYourCall(), m_txHeader->getRptCall2(), "net"); +#endif + m_networkSeqNo = 0U; m_transmitting = true; m_watchdogTimer.start(); @@ -724,6 +767,10 @@ unsigned int CDStarRepeaterTXRXThread::processNetworkFrame(unsigned char* data, bool end = (seqNo & 0x40U) == 0x40U; if (end) { m_networkQueue[m_writeNum]->addData(END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES, true); +#if defined(MQTT) + mqttPublishDStarEnd(); + mqttPublishIdle(); +#endif endOfNetworkData(); return 1U; } @@ -854,6 +901,9 @@ void CDStarRepeaterTXRXThread::clock(unsigned int ms) m_activeHangTimer.clock(ms); m_statusTimer.clock(ms); m_heartbeatTimer.clock(ms); +#if defined(MQTT) + m_mqttStatusTimer.clock(ms); +#endif } void CDStarRepeaterTXRXThread::shutdown() diff --git a/DStarRepeater/DStarRepeaterTXRXThread.h b/DStarRepeater/DStarRepeaterTXRXThread.h index b0d35c5..51b6daf 100644 --- a/DStarRepeater/DStarRepeaterTXRXThread.h +++ b/DStarRepeater/DStarRepeaterTXRXThread.h @@ -30,6 +30,9 @@ #include "HeaderData.h" #include "AMBEFEC.h" #include "Timer.h" +#if defined(MQTT) +#include "MQTTPublisher.h" +#endif #include @@ -107,6 +110,10 @@ private: unsigned int m_packetCount; unsigned int m_packetSilence; +#if defined(MQTT) + CTimer m_mqttStatusTimer; +#endif + void receiveHeader(CHeaderData* header); void receiveRadioData(unsigned char* data, unsigned int length); void receiveSlowData(unsigned char* data, unsigned int length); diff --git a/DStarRepeater/DStarRepeaterTXThread.cpp b/DStarRepeater/DStarRepeaterTXThread.cpp index 062b991..63bedde 100644 --- a/DStarRepeater/DStarRepeaterTXThread.cpp +++ b/DStarRepeater/DStarRepeaterTXThread.cpp @@ -57,6 +57,9 @@ m_headerTime(), m_packetTime(), m_packetCount(0U), m_packetSilence(0U) +#if defined(MQTT) +,m_mqttStatusTimer(1000U, 1U) // 1s +#endif { m_networkQueue = new COutputQueue*[NETWORK_QUEUE_COUNT]; for (unsigned int i = 0U; i < NETWORK_QUEUE_COUNT; i++) @@ -87,6 +90,9 @@ void *CDStarRepeaterTXThread::Entry() m_registerTimer.start(10U); m_statusTimer.start(); +#if defined(MQTT) + m_mqttStatusTimer.start(); +#endif wxString hardware = m_type; int n = hardware.Find(wxT(' ')); @@ -116,6 +122,10 @@ void *CDStarRepeaterTXThread::Entry() wxLogMessage(wxT("Network watchdog has expired")); // Send end of transmission data to the radio m_networkQueue[m_writeNum]->addData(END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES, true); +#if defined(MQTT) + mqttPublishDStarLost(); + mqttPublishIdle(); +#endif endOfNetworkData(); } } @@ -131,6 +141,19 @@ void *CDStarRepeaterTXThread::Entry() else if (m_networkQueue[m_readNum]->headerReady()) transmitNetworkHeader(); +#if defined(MQTT) + // Publish status to MQTT every second + if (m_mqttStatusTimer.hasExpired()) { + if (g_mqtt != NULL) { + CDStarRepeaterStatusData* status = getStatus(); + std::string json = status->toJSON(); + g_mqtt->publish("status", json.c_str()); + delete status; + } + m_mqttStatusTimer.start(); + } +#endif + unsigned long ms = stopWatch.Time(); if (ms < CYCLE_TIME) { ::wxMilliSleep(CYCLE_TIME - ms); @@ -384,6 +407,11 @@ void CDStarRepeaterTXThread::processNetworkHeader(CHeaderData* header) delete m_txHeader; m_txHeader = header; +#if defined(MQTT) + mqttPublishDStarStart(m_txHeader->getMyCall1(), m_txHeader->getMyCall2(), + m_txHeader->getYourCall(), m_txHeader->getRptCall2(), "net"); +#endif + transmitNetworkHeader(new CHeaderData(*header)); } @@ -395,6 +423,10 @@ unsigned int CDStarRepeaterTXThread::processNetworkFrame(unsigned char* data, un bool end = (seqNo & 0x40U) == 0x40U; if (end) { m_networkQueue[m_writeNum]->addData(END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES, true); +#if defined(MQTT) + mqttPublishDStarEnd(); + mqttPublishIdle(); +#endif endOfNetworkData(); return 1U; } @@ -501,6 +533,9 @@ void CDStarRepeaterTXThread::clock(unsigned int ms) m_registerTimer.clock(ms); m_watchdogTimer.clock(ms); m_statusTimer.clock(ms); +#if defined(MQTT) + m_mqttStatusTimer.clock(ms); +#endif } void CDStarRepeaterTXThread::shutdown() diff --git a/DStarRepeater/DStarRepeaterTXThread.h b/DStarRepeater/DStarRepeaterTXThread.h index a1d8f9f..1716596 100644 --- a/DStarRepeater/DStarRepeaterTXThread.h +++ b/DStarRepeater/DStarRepeaterTXThread.h @@ -24,6 +24,9 @@ #include "HeaderData.h" #include "AMBEFEC.h" #include "Timer.h" +#if defined(MQTT) +#include "MQTTPublisher.h" +#endif #include @@ -86,6 +89,10 @@ private: unsigned int m_packetCount; unsigned int m_packetSilence; +#if defined(MQTT) + CTimer m_mqttStatusTimer; +#endif + void transmitNetworkHeader(CHeaderData* header); void transmitNetworkData(); void transmitNetworkHeader(); diff --git a/MQTT.md b/MQTT.md new file mode 100644 index 0000000..ae9f647 --- /dev/null +++ b/MQTT.md @@ -0,0 +1,315 @@ +# MQTT Support for DStarRepeater + +DStarRepeater can optionally publish log messages and repeater status to +an MQTT broker, providing the same kind of live telemetry that MMDVMHost +offers. This makes it possible to monitor D-Star repeater activity from +dashboards, home-automation systems, or any MQTT client. + +**MQTT support is entirely optional.** When built without the MQTT flag +the compiled binary is identical to the tree before MQTT was added — no +MQTT code is included and no additional libraries are required. + +## Building + +### Without MQTT (default) + +``` +make +``` + +Produces the same `dstarrepeaterd` as the original codebase. No +`libmosquitto` dependency. + +### With MQTT + +Install `libmosquitto-dev` first: + +``` +sudo apt-get install libmosquitto-dev +``` + +Then build with the `MQTT` flag: + +``` +make MQTT=1 +``` + +This adds `-DMQTT` to the compiler flags and links against +`-lmosquitto`. All MQTT code paths are guarded by +`#if defined(MQTT)` / `#endif`, so the flag acts as a clean on/off +switch. + +## Configuration + +Add the following keys to your `dstarrepeater` configuration file +(the same flat key=value format used by all other settings): + +``` +mqttHost=127.0.0.1 +mqttPort=1883 +mqttAuth=0 +mqttUsername= +mqttPassword= +mqttKeepalive=60 +mqttName=dstar-repeater +``` + +| Key | Default | Description | +|-----------------|-------------------|----------------------------------------------------| +| `mqttHost` | `127.0.0.1` | MQTT broker hostname or IP address | +| `mqttPort` | `1883` | MQTT broker port | +| `mqttAuth` | `0` | Enable authentication (`0` = off, `1` = on) | +| `mqttUsername` | *(empty)* | Username when `mqttAuth=1` | +| `mqttPassword` | *(empty)* | Password when `mqttAuth=1` | +| `mqttKeepalive` | `60` | Keepalive interval in seconds | +| `mqttName` | `dstar-repeater` | Client name; also used as the MQTT topic prefix | + +If `mqttHost` is left empty, MQTT is disabled at runtime even when +compiled in. + +## MQTT Topics + +All topics are automatically prefixed with the value of `mqttName`. +For example, with the default name `dstar-repeater`: + +### `dstar-repeater/log` + +Timestamped log messages, filtered by severity. The format matches the +log file output: + +``` +M: 2025-03-08 14:22:01: Starting D-Star Repeater - 20180403 +M: 2025-03-08 14:22:01: Using wxWidgets 3.0.5 on Linux 6.1.21-v8+ +M: 2025-03-08 14:22:01: Callsign set to "GB7XX B", gateway set to "GB7XX G" +M: 2025-03-08 14:22:01: Modem type set to "MMDVM" +I: 2025-03-08 14:22:01: MQTT connected to 127.0.0.1:1883 as dstar-repeater +M: 2025-03-08 14:22:01: Starting the D-Star repeater thread +M: 2025-03-08 14:22:01: Poll text set to "linux_mmdvm-20180403" +``` + +Log levels (highest to lowest): + +| Letter | Level | Numeric | +|--------|---------------|---------| +| `F` | Fatal | 6 | +| `E` | Error | 5 | +| `W` | Warning | 4 | +| `I` | Info | 3 | +| `M` | Message | 2 | +| `D` | Debug | 1 | + +Only messages at or above the configured threshold are published. The +default threshold is **2** (Message level), matching MMDVMHost behaviour. + +### `dstar-repeater/status` + +A JSON object published once per second with the current repeater state. +This is the D-Star equivalent of MMDVMHost's status output: + +**Idle (no traffic):** +```json +{ + "myCall1": "", + "myCall2": "", + "yourCall": "", + "rptCall1": "", + "rptCall2": "", + "tx": false, + "rxState": "listening", + "rptState": "listening", + "ber": 0.0, + "text": "", + "status1": "", + "status2": "", + "status3": "", + "status4": "", + "status5": "" +} +``` + +**During an RF transmission:** +```json +{ + "myCall1": "MW0MWZ ", + "myCall2": " ", + "yourCall": "CQCQCQ ", + "rptCall1": "GB7XX B", + "rptCall2": "GB7XX G", + "tx": true, + "rxState": "process_data", + "rptState": "valid", + "ber": 1.3, + "text": "Hello from Pi-Star", + "status1": "", + "status2": "", + "status3": "", + "status4": "", + "status5": "" +} +``` + +**During a network (gateway) transmission:** +```json +{ + "myCall1": "G4KLX ", + "myCall2": " ", + "yourCall": "GB7XX B", + "rptCall1": "GB7XX B", + "rptCall2": "GB7XX G", + "tx": true, + "rxState": "listening", + "rptState": "network", + "ber": 0.0, + "text": "", + "status1": "", + "status2": "", + "status3": "", + "status4": "", + "status5": "" +} +``` + +#### Status Field Reference + +| Field | Type | Description | +|------------|---------|-----------------------------------------------------------| +| `myCall1` | string | Transmitting station's callsign (8 chars, space-padded) | +| `myCall2` | string | Transmitting station's short suffix (4 chars) | +| `yourCall` | string | Destination callsign (`CQCQCQ` for CQ calls) | +| `rptCall1` | string | Repeater callsign with module letter | +| `rptCall2` | string | Gateway callsign | +| `tx` | boolean | `true` when the repeater is transmitting | +| `rxState` | string | `listening`, `process_data`, or `process_slow_data` | +| `rptState` | string | See repeater states below | +| `ber` | float | Bit Error Rate as a percentage (0.0 when idle) | +| `text` | string | Slow-data text message (if any) | +| `status1`–`status5` | string | User-configured status text messages | + +#### Repeater States (`rptState`) + +| Value | Meaning | +|----------------|------------------------------------------------------| +| `listening` | Idle, waiting for traffic | +| `valid` | Receiving valid RF traffic | +| `valid_wait` | Valid RF transmission ended, waiting for ack window | +| `invalid` | Receiving RF traffic that failed validation | +| `invalid_wait` | Invalid RF ended, waiting | +| `timeout` | Transmission timed out | +| `timeout_wait` | Timeout ended, waiting | +| `network` | Receiving traffic from the gateway/network | +| `shutdown` | Repeater is shut down | + +### `dstar-repeater/json` + +Event-driven JSON messages published at D-Star state transitions, in the +format expected by [Display-Driver](https://github.com/MW0MWZ/Display-Driver). +This means DStarRepeater can drive an OLED/TFT display via MQTT with no +modifications to Display-Driver. + +Unlike the `status` topic (polled once per second), the `json` topic only +publishes when something actually changes — a transmission starts, ends, +or is lost. + +**RF transmission starts:** +```json +{"D-Star":{"action":"start","source_cs":"MW0MWZ ","source_ext":" ","destination_cs":"CQCQCQ ","reflector":"GB7XX G","source":"rf"}} +``` + +**Network transmission starts:** +```json +{"D-Star":{"action":"start","source_cs":"G4KLX ","source_ext":" ","destination_cs":"GB7XX B","reflector":"GB7XX G","source":"net"}} +``` + +**Transmission ends normally:** +```json +{"D-Star":{"action":"end"}} +``` + +**Transmission lost (watchdog timeout):** +```json +{"D-Star":{"action":"lost"}} +``` + +**Return to idle (sent after every end/lost):** +```json +{"MMDVM":{"mode":"idle"}} +``` + +**BER update (published once per second during active RF):** +```json +{"BER":{"mode":"D-Star","value":1.3}} +``` + +#### Display-Driver Compatibility + +These messages use the exact same JSON keys and structure that +Display-Driver expects. If your `mqttName` is set to `dstar-repeater`, +configure Display-Driver to subscribe to `dstar-repeater/json`. + +Display-Driver dispatches each top-level key to a specific parser: + +| Top-level key | Display-Driver parser | When published | +|---------------|----------------------|------------------------------------------| +| `D-Star` | `parseDStar()` | Transmission start, end, or lost | +| `BER` | `parseBER()` | Once per second during active RF | +| `MMDVM` | `parseMMDVM()` | On return to idle | + +## Subscribing to MQTT Output + +Use any MQTT client to subscribe. For example, with `mosquitto_sub`: + +```bash +# Follow all DStarRepeater topics: +mosquitto_sub -h 127.0.0.1 -t "dstar-repeater/#" + +# Log messages only: +mosquitto_sub -h 127.0.0.1 -t "dstar-repeater/log" + +# Status JSON only: +mosquitto_sub -h 127.0.0.1 -t "dstar-repeater/status" + +# Display-Driver-compatible events: +mosquitto_sub -h 127.0.0.1 -t "dstar-repeater/json" + +# Pretty-print status with jq: +mosquitto_sub -h 127.0.0.1 -t "dstar-repeater/status" | jq . + +# Pretty-print Display-Driver events with jq: +mosquitto_sub -h 127.0.0.1 -t "dstar-repeater/json" | jq . +``` + +## Comparison with MMDVMHost + +This implementation follows the same conventions as MMDVMHost's MQTT +support: + +- Same `CMQTTConnection` class wrapping `libmosquitto` +- Same topic-prefix convention (`{name}/topic`) +- Same log-level filtering and format +- Same PID-based client ID scheme (avoids the `time_t` truncation + issue on 32-bit ARM) +- Same configuration key names (`mqttHost`, `mqttPort`, etc.) +- Same QoS default (EXACTLY_ONCE / QoS 2) + +The key difference is that DStarRepeater is a **publish-only** client — +it does not subscribe to any MQTT topics or accept remote commands via +MQTT. The configuration file format also differs: DStarRepeater uses +flat `key=value` pairs rather than MMDVMHost's INI-style `[MQTT]` +section. + +## Troubleshooting + +**"Unable to start MQTT connection"** in logs: +- Check that Mosquitto (or another MQTT broker) is running on the + configured host and port +- Verify credentials if `mqttAuth=1` +- Check firewall rules if connecting to a remote broker + +**No messages appearing:** +- Confirm the binary was built with `make MQTT=1` +- Check that `mqttHost` is not empty in the config file +- Verify the broker is accessible: `mosquitto_pub -h 127.0.0.1 -t test -m hello` + +**Log messages missing but status works:** +- The log level threshold may be filtering them out. Lower-priority + messages (Debug, Trace) are not published by default. diff --git a/Makefile b/Makefile index cdb6df6..304f873 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,12 @@ else ifeq ($(BUILD), release) endif export GUILIBS := $(shell wx-config --libs adv,core,base) -lasound export LIBS := $(shell wx-config --libs base) -lasound -lusb-1.0 -export LDFLAGS := +export LDFLAGS := + +ifeq ($(MQTT), 1) + export CFLAGS := $(CFLAGS) -DMQTT + export LIBS := $(LIBS) -lmosquitto +endif all: DStarRepeater/dstarrepeaterd DStarRepeaterConfig/dstarrepeaterconfig diff --git a/README.md b/README.md index 468e506..ca9a817 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,40 @@ -This is the D-Star Repeater which controls homebrew repeater hardware and links into the ircDDB Gateway to allow for access to extra non-RF facilities, - -The hardware supported include: -* DVAP -* DV-Mega (D-Star only) -* GMSK Modems -* Soundcard repeaters (including UDRC) -* MMDVM (D-Star only) -* DV-RPTR V1, V2, and V3 -* Icom Terminal and Access Point modes - -They all build on 32-bit and 64-bit Linux as well as on Windows using Visual Studio 2017 on x86 and x64. +# DStarRepeater + +DStarRepeater is the D-Star repeater controller for homebrew repeater hardware. It links into [ircDDBGateway](https://github.com/g4klx/ircDDBGateway) or the newer [DStarGateway](https://github.com/g4klx/DStarGateway) to provide access to D-Star networking facilities such as callsign routing, reflector linking, and ircDDB. + +## Supported Hardware + +- DVAP +- DV-Mega (D-Star only) +- GMSK Modems +- Soundcard repeaters (including UDRC) +- MMDVM (D-Star only) +- DV-RPTR V1, V2, and V3 +- Icom Terminal and Access Point modes + +## Building + +Builds on 32-bit and 64-bit Linux as well as on Windows using Visual Studio 2017 (x86 and x64). + +### Standard Build + +``` +make +``` + +### With MQTT Support + +DStarRepeater can optionally publish live telemetry to an MQTT broker, including log messages, repeater status, and Display-Driver-compatible events for driving OLED/TFT displays. + +``` +sudo apt-get install libmosquitto-dev +make MQTT=1 +``` + +MQTT support is entirely opt-in. When built without the `MQTT=1` flag, the binary is identical to the tree before MQTT was added — no MQTT code is compiled in and no additional libraries are required. + +See [MQTT.md](MQTT.md) for full configuration details, topic structure, JSON examples, and Display-Driver compatibility information. + +## Licence This software is licenced under the GPL v2. diff --git a/linux/dstarrepeater_1.example b/linux/dstarrepeater.example similarity index 91% rename from linux/dstarrepeater_1.example rename to linux/dstarrepeater.example index f527634..fa26b3e 100644 --- a/linux/dstarrepeater_1.example +++ b/linux/dstarrepeater.example @@ -1,5 +1,5 @@ -callsign=N0CALL C -gateway= G +callsign=GB3IN C +gateway= mode=0 ack=1 restriction=0 @@ -11,7 +11,7 @@ gatewayPort=20010 localAddress=127.0.0.1 localPort=20011 networkName= -modemType=Sound Card +modemType=DVAP timeout=180 ackTime=500 beaconTime=600 @@ -19,7 +19,7 @@ beaconText=D-Star Repeater beaconVoice=0 language=0 announcementEnabled=0 -announcementTime=480 +announcementTime=500 announcementRecordRPT1= announcementRecordRPT2= announcementDeleteRPT1= @@ -109,6 +109,14 @@ soundCardRXLevel=1.0000 soundCardTXLevel=1.0000 soundCardTXDelay=150 soundCardTXTail=50 +icomPort= splitLocalAddress= splitLocalPort=0 splitTimeout=0 +mqttHost=127.0.0.1 +mqttPort=1883 +mqttAuth=0 +mqttUsername= +mqttPassword= +mqttKeepalive=60 +mqttName=dstar-repeater