From 6dc8097d206bbdbe092eb0da1bbfe7b7b9eee889 Mon Sep 17 00:00:00 2001 From: Andy Taylor Date: Sun, 8 Mar 2026 14:10:06 +0000 Subject: [PATCH] Add optional MQTT support, Display-Driver compatibility, and documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MQTT telemetry (build with make MQTT=1) --------------------------------------- Add optional MQTT publishing to DStarRepeater, following the same conventions as MMDVMHost: CMQTTConnection wrapping libmosquitto, topic-prefix convention ({name}/topic), log-level filtering, PID-based client IDs, and matching config key names. All MQTT code is guarded by #if defined(MQTT) — without the flag, the binary is unchanged. Three topics are published: {name}/log Timestamped log messages filtered by severity {name}/status JSON repeater state snapshot, once per second {name}/json Display-Driver-compatible events at state transitions Display-Driver compatibility ---------------------------- Publish event-driven JSON on the "json" topic in the exact format that Display-Driver expects, enabling OLED/TFT display output with no changes to Display-Driver. Events: D-Star start (RF/net with callsign fields), end, lost (watchdog), idle, and BER. Hooks added at every state transition point in all four thread variants (TRXThread, RXThread, TXThread, TXRXThread). Shutdown safety --------------- Add mosquitto_loop_stop() before mosquitto_destroy() in close() to prevent use-after-free from the background network thread. Add Wait() after kill() in OnExit() so the repeater thread has fully exited before tearing down MQTT. Both issues also exist in MMDVMHost. Config parser bug fix --------------------- The config file parser called GetNextLine() inside comment and no-equals handlers before continuing, but the for-loop increment already advances — silently skipping the line after every comment or blank line. Remove the redundant calls and add an empty-line guard. Comments and blank lines now work correctly in config files. Documentation and config ------------------------ Add MQTT.md with build instructions, config keys, topic structure, JSON examples, and Display-Driver compatibility details. Add CONFIGURATION.md as a full reference guide for every config setting. Update README.md with gateway links (ircDDBGateway, DStarGateway) and MQTT build instructions. Rename and update the example config with corrected defaults, missing icomPort, and MQTT settings. --- CONFIGURATION.md | 349 ++++++++++++++++++ Common/DStarRepeaterConfig.cpp | 97 ++++- Common/DStarRepeaterConfig.h | 16 + Common/Logger.cpp | 22 ++ Common/Logger.h | 6 + Common/MQTTConnection.cpp | 236 ++++++++++++ Common/MQTTConnection.h | 69 ++++ Common/MQTTPublisher.h | 121 ++++++ Common/Makefile | 2 +- DStarRepeater/DStarRepeaterApp.cpp | 43 +++ DStarRepeater/DStarRepeaterRXThread.cpp | 37 ++ DStarRepeater/DStarRepeaterRXThread.h | 7 + DStarRepeater/DStarRepeaterStatusData.cpp | 62 ++++ DStarRepeater/DStarRepeaterStatusData.h | 8 + DStarRepeater/DStarRepeaterTRXThread.cpp | 50 +++ DStarRepeater/DStarRepeaterTRXThread.h | 7 + DStarRepeater/DStarRepeaterTXRXThread.cpp | 50 +++ DStarRepeater/DStarRepeaterTXRXThread.h | 7 + DStarRepeater/DStarRepeaterTXThread.cpp | 35 ++ DStarRepeater/DStarRepeaterTXThread.h | 7 + MQTT.md | 315 ++++++++++++++++ Makefile | 7 +- README.md | 50 ++- ...peater_1.example => dstarrepeater.example} | 16 +- 24 files changed, 1595 insertions(+), 24 deletions(-) create mode 100644 CONFIGURATION.md create mode 100644 Common/MQTTConnection.cpp create mode 100644 Common/MQTTConnection.h create mode 100644 Common/MQTTPublisher.h create mode 100644 MQTT.md rename linux/{dstarrepeater_1.example => dstarrepeater.example} (91%) 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