diff --git a/common_settings.ini b/common_settings.ini
index 6e4b012..18670bb 100644
--- a/common_settings.ini
+++ b/common_settings.ini
@@ -33,6 +33,7 @@ lib_deps =
ayushsharma82/ElegantOTA @ 3.1.5
bblanchon/ArduinoJson @ 6.21.3
jgromes/RadioLib @ 7.1.0
+ knolleary/PubSubClient @ 2.8
mathieucarbou/AsyncTCP @ 3.2.5
mathieucarbou/ESPAsyncWebServer @ 3.2.3
mikalhart/TinyGPSPlus @ 1.0.3
diff --git a/data/igate_conf.json b/data/igate_conf.json
index 758f000..400be7d 100644
--- a/data/igate_conf.json
+++ b/data/igate_conf.json
@@ -90,7 +90,14 @@
"remoteManagement": {
"managers": "",
"rfOnly": true
- },
+ },
+ "mqtt": {
+ "active": false,
+ "host": "",
+ "login": "",
+ "password": "",
+ "port": 1883
+ },
"other": {
"rememberStationTime": 30,
"backupDigiMode": false,
diff --git a/data_embed/index.html b/data_embed/index.html
index b6ed9d4..d9161e2 100644
--- a/data_embed/index.html
+++ b/data_embed/index.html
@@ -1502,6 +1502,124 @@
+
+
+
+
+ MQTT
+
+
Set your MQTT server
+
+
+
+
+
+
+
+
+
+
+
+
+ Default is aprs-igate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Default is 1883
+
+
+
+
+
+
+
diff --git a/data_embed/script.js b/data_embed/script.js
index 84ea6e8..31c7ba9 100644
--- a/data_embed/script.js
+++ b/data_embed/script.js
@@ -224,6 +224,14 @@ function loadSettings(settings) {
// NTP
document.getElementById("ntp.gmtCorrection").value = settings.ntp.gmtCorrection;
+ // MQTT
+ document.getElementById("mqtt.active").checked = settings.mqtt.active;
+ document.getElementById("mqtt.server").value = settings.mqtt.server;
+ document.getElementById("mqtt.topic").value = settings.mqtt.topic;
+ document.getElementById("mqtt.username").value = settings.mqtt.username;
+ document.getElementById("mqtt.password").value = settings.mqtt.password;
+ document.getElementById("mqtt.port").value = settings.mqtt.port;
+
// Experimental
document.getElementById("other.backupDigiMode").checked = settings.other.backupDigiMode;
@@ -351,6 +359,32 @@ WebadminCheckbox.addEventListener("change", function () {
WebadminPassword.disabled = !this.checked;
});
+const MqttCheckbox = document.querySelector(
+ 'input[name="mqtt.active"]'
+);
+const MqttServer = document.querySelector(
+ 'input[name="mqtt.server"]'
+);
+const MqttTopic = document.querySelector(
+ 'input[name="mqtt.topic"]'
+);
+const MqttUsername = document.querySelector(
+ 'input[name="mqtt.username"]'
+);
+const MqttPassword = document.querySelector(
+ 'input[name="mqtt.password"]'
+);
+const MqttPort = document.querySelector(
+ 'input[name="mqtt.port"]'
+);
+MqttCheckbox.addEventListener("change", function () {
+ MqttServer.disabled = !this.checked;
+ MqttTopic.disabled = !this.checked;
+ MqttUsername.disabled = !this.checked;
+ MqttPassword.disabled = !this.checked;
+ MqttPort.disabled = !this.checked;
+});
+
document.querySelector(".new button").addEventListener("click", function () {
const networksContainer = document.querySelector(".list-networks");
diff --git a/include/configuration.h b/include/configuration.h
index f78584d..3cc7212 100644
--- a/include/configuration.h
+++ b/include/configuration.h
@@ -149,6 +149,16 @@ public:
bool rfOnly;
};
+class MQTT {
+public:
+ bool active;
+ String server;
+ String topic;
+ String username;
+ String password;
+ int port;
+};
+
class Configuration {
public:
String callsign;
@@ -173,6 +183,7 @@ public:
WEBADMIN webadmin;
NTP ntp;
REMOTE_MANAGEMENT remoteManagement;
+ MQTT mqtt;
void init();
void writeFile();
diff --git a/include/mqtt_utils.h b/include/mqtt_utils.h
new file mode 100644
index 0000000..1fcfa05
--- /dev/null
+++ b/include/mqtt_utils.h
@@ -0,0 +1,34 @@
+/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
+ *
+ * This file is part of LoRa APRS iGate.
+ *
+ * LoRa APRS iGate 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LoRa APRS iGate 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 LoRa APRS iGate. If not, see .
+ */
+
+#ifndef MQTT_UTILS_H_
+#define MQTT_UTILS_H_
+
+#include
+
+
+namespace MQTT_Utils {
+
+ void sendToMqtt(const String& packet);
+ void connect();
+ void loop();
+ void setup();
+
+}
+
+ #endif
\ No newline at end of file
diff --git a/src/LoRa_APRS_iGate.cpp b/src/LoRa_APRS_iGate.cpp
index 94a2a2c..cc89db8 100644
--- a/src/LoRa_APRS_iGate.cpp
+++ b/src/LoRa_APRS_iGate.cpp
@@ -51,6 +51,7 @@ ___________________________________________________________________*/
#include "syslog_utils.h"
#include "power_utils.h"
#include "sleep_utils.h"
+#include "mqtt_utils.h"
#include "lora_utils.h"
#include "wifi_utils.h"
#include "digi_utils.h"
@@ -66,9 +67,10 @@ ___________________________________________________________________*/
#endif
-String versionDate = "2025-08-26";
+String versionDate = "2025-08-27";
Configuration Config;
-WiFiClient espClient;
+WiFiClient aprsIsClient;
+WiFiClient mqttClient;
#ifdef HAS_GPS
HardwareSerial gpsSerial(1);
TinyGPSPlus gps;
@@ -118,6 +120,7 @@ void setup() {
WX_Utils::setup();
WEB_Utils::setup();
TNC_Utils::setup();
+ MQTT_Utils::setup();
#ifdef HAS_A7670
A7670_Utils::setup();
#endif
@@ -166,11 +169,13 @@ void loop() {
if (Config.aprs_is.active && !modemLoggedToAPRSIS) A7670_Utils::APRS_IS_connect();
#else
WIFI_Utils::checkWiFi();
- if (Config.aprs_is.active && (WiFi.status() == WL_CONNECTED) && !espClient.connected()) APRS_IS_Utils::connect();
+ if (Config.aprs_is.active && (WiFi.status() == WL_CONNECTED) && !aprsIsClient.connected()) APRS_IS_Utils::connect();
+ if (Config.mqtt.active && (WiFi.status() == WL_CONNECTED) && !mqttClient.connected()) MQTT_Utils::connect();
#endif
NTP_Utils::update();
TNC_Utils::loop();
+ MQTT_Utils::loop();
Utils::checkDisplayInterval();
Utils::checkBeaconInterval();
@@ -183,7 +188,7 @@ void loop() {
}
if (packet != "") {
- if (Config.aprs_is.active) { // If APRSIS enabled
+ if (Config.aprs_is.active) { // If APRSIS enabled
APRS_IS_Utils::processLoRaPacket(packet); // Send received packet to APRSIS
}
@@ -192,17 +197,12 @@ void loop() {
DIGI_Utils::processLoRaPacket(packet); // Send received packet to Digi
}
- if (Config.tnc.enableServer) { // If TNC server enabled
- TNC_Utils::sendToClients(packet); // Send received packet to TNC KISS
- }
- if (Config.tnc.enableSerial) { // If Serial KISS enabled
- TNC_Utils::sendToSerial(packet); // Send received packet to Serial KISS
- }
+ if (Config.tnc.enableServer) TNC_Utils::sendToClients(packet); // Send received packet to TNC KISS
+ if (Config.tnc.enableSerial) TNC_Utils::sendToSerial(packet); // Send received packet to Serial KISS
+ if (Config.mqtt.active) MQTT_Utils::sendToMqtt(packet); // Send received packet to MQTT
}
- if (Config.aprs_is.active) {
- APRS_IS_Utils::listenAPRSIS(); // listen received packet from APRSIS
- }
+ if (Config.aprs_is.active) APRS_IS_Utils::listenAPRSIS(); // listen received packet from APRSIS
STATION_Utils::processOutputPacketBuffer();
diff --git a/src/aprs_is_utils.cpp b/src/aprs_is_utils.cpp
index cddad0f..90d9591 100644
--- a/src/aprs_is_utils.cpp
+++ b/src/aprs_is_utils.cpp
@@ -30,7 +30,7 @@
extern Configuration Config;
-extern WiFiClient espClient;
+extern WiFiClient aprsIsClient;
extern uint32_t lastScreenOn;
extern String firstLine;
extern String secondLine;
@@ -53,17 +53,17 @@ bool passcodeValid = false;
namespace APRS_IS_Utils {
void upload(const String& line) {
- espClient.print(line + "\r\n");
+ aprsIsClient.print(line + "\r\n");
}
void connect() {
Serial.print("Connecting to APRS-IS ... ");
uint8_t count = 0;
- while (!espClient.connect(Config.aprs_is.server.c_str(), Config.aprs_is.port) && count < 20) {
+ while (!aprsIsClient.connect(Config.aprs_is.server.c_str(), Config.aprs_is.port) && count < 20) {
Serial.println("Didn't connect with server...");
delay(1000);
- espClient.stop();
- espClient.flush();
+ aprsIsClient.stop();
+ aprsIsClient.flush();
Serial.println("Run client.stop");
Serial.println("Trying to connect with Server: " + String(Config.aprs_is.server) + " AprsServerPort: " + String(Config.aprs_is.port));
count++;
@@ -110,7 +110,7 @@ namespace APRS_IS_Utils {
aprsisState = "--";
}
#else
- if (espClient.connected()) {
+ if (aprsIsClient.connected()) {
aprsisState = "OK";
} else {
aprsisState = "--";
@@ -192,7 +192,7 @@ namespace APRS_IS_Utils {
}
void processLoRaPacket(const String& packet) {
- if (passcodeValid && (espClient.connected() || modemLoggedToAPRSIS)) {
+ if (passcodeValid && (aprsIsClient.connected() || modemLoggedToAPRSIS)) {
if (packet.indexOf("NOGATE") == -1 && packet.indexOf("RFONLY") == -1) {
int firstColonIndex = packet.indexOf(":");
if (firstColonIndex > 5 && firstColonIndex < (packet.length() - 1) && packet[firstColonIndex + 1] != '}' && packet.indexOf("TCPIP") == -1) {
@@ -371,9 +371,9 @@ namespace APRS_IS_Utils {
#ifdef HAS_A7670
A7670_Utils::listenAPRSIS();
#else
- if (espClient.connected()) {
- if (espClient.available()) {
- String aprsisPacket = espClient.readStringUntil('\r');
+ if (aprsIsClient.connected()) {
+ if (aprsIsClient.available()) {
+ String aprsisPacket = aprsIsClient.readStringUntil('\r');
aprsisPacket.trim(); // Serial.println(aprsisPacket);
processAPRSISPacket(aprsisPacket);
lastRxTime = millis();
@@ -383,7 +383,7 @@ namespace APRS_IS_Utils {
}
void firstConnection() {
- if (Config.aprs_is.active && (WiFi.status() == WL_CONNECTED) && !espClient.connected()) {
+ if (Config.aprs_is.active && (WiFi.status() == WL_CONNECTED) && !aprsIsClient.connected()) {
connect();
while (!passcodeValid) {
listenAPRSIS();
diff --git a/src/configuration.cpp b/src/configuration.cpp
index 18ce845..a7818e6 100644
--- a/src/configuration.cpp
+++ b/src/configuration.cpp
@@ -136,6 +136,13 @@ void Configuration::writeFile() {
data["remoteManagement"]["managers"] = remoteManagement.managers;
data["remoteManagement"]["rfOnly"] = remoteManagement.rfOnly;
+ data["mqtt"]["active"] = mqtt.active;
+ data["mqtt"]["server"] = mqtt.server;
+ data["mqtt"]["topic"] = mqtt.topic;
+ data["mqtt"]["username"] = mqtt.username;
+ data["mqtt"]["password"] = mqtt.password;
+ data["mqtt"]["port"] = mqtt.port;
+
serializeJson(data, configFile);
configFile.close();
@@ -264,6 +271,13 @@ bool Configuration::readFile() {
remoteManagement.managers = data["remoteManagement"]["managers"] | "";
remoteManagement.rfOnly = data["remoteManagement"]["rfOnly"] | true;
+ mqtt.active = data["mqtt"]["active"] | false;
+ mqtt.server = data["mqtt"]["server"] | "";
+ mqtt.topic = data["mqtt"]["topic"] | "aprs-igate";
+ mqtt.username = data["mqtt"]["username"] | "";
+ mqtt.password = data["mqtt"]["password"] | "";
+ mqtt.port = data["mqtt"]["port"] | 1883;
+
if (wifiAPs.size() == 0) { // If we don't have any WiFi's from config we need to add "empty" SSID for AUTO AP
WiFi_AP wifiap;
wifiap.ssid = "";
@@ -382,6 +396,13 @@ void Configuration::init() {
remoteManagement.managers = "";
remoteManagement.rfOnly = true;
+ mqtt.active = false;
+ mqtt.server = "";
+ mqtt.topic = "aprs-igate";
+ mqtt.username = "";
+ mqtt.password = "";
+ mqtt.port = 1883;
+
Serial.println("All is Written!");
}
diff --git a/src/gps_utils.cpp b/src/gps_utils.cpp
index df5f89d..6d5d128 100644
--- a/src/gps_utils.cpp
+++ b/src/gps_utils.cpp
@@ -31,7 +31,6 @@
#endif
extern Configuration Config;
-extern WiFiClient espClient;
extern HardwareSerial gpsSerial;
extern TinyGPSPlus gps;
String distance, iGateBeaconPacket, iGateLoRaBeaconPacket;
diff --git a/src/mqtt_utils.cpp b/src/mqtt_utils.cpp
new file mode 100644
index 0000000..e83e92b
--- /dev/null
+++ b/src/mqtt_utils.cpp
@@ -0,0 +1,113 @@
+/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
+ *
+ * This file is part of LoRa APRS iGate.
+ *
+ * LoRa APRS iGate 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LoRa APRS iGate 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 LoRa APRS iGate. If not, see .
+ */
+
+/*#include
+#include
+#include "wifi_utils.h"
+#include "mqtt_utils.h"
+
+
+extern String mqttServer;
+extern int mqttPort;
+extern String mqttUser;
+extern String mqttPassword;
+extern uint8_t gatewayID;
+extern bool subscribed;
+extern String subscribeTopic;
+
+
+WiFiClientSecure secureClient;
+PubSubClient mqttClient(secureClient);
+
+bool mqttConnected = false;*/
+
+#include
+#include
+#include "configuration.h"
+#include "station_utils.h"
+#include "mqtt_utils.h"
+
+
+extern Configuration Config;
+extern WiFiClient mqttClient;
+
+PubSubClient pubSub;
+
+
+namespace MQTT_Utils {
+
+ void sendToMqtt(const String& packet) {
+ if (!pubSub.connected()) {
+ Serial.println("Can not send to MQTT because it is not connected");
+ return;
+ }
+ const String cleanPacket = packet.substring(3);
+ const String sender = cleanPacket.substring(0, cleanPacket.indexOf(">"));
+ const String topic = String(Config.mqtt.topic + "/" + sender);
+
+ const bool result = pubSub.publish(topic.c_str(), cleanPacket.c_str());
+ if (result) {
+ Serial.print("Packet sent to MQTT topic "); Serial.println(topic);
+ } else {
+ Serial.println("Packet not sent to MQTT (check connection)");
+ }
+ }
+
+ void receivedFromMqtt(char* topic, byte* payload, unsigned int length) {
+ Serial.print("Received from MQTT topic "); Serial.print(topic); Serial.print(": ");
+ for (int i = 0; i < length; i++) {
+ Serial.print((char)payload[i]);
+ }
+ Serial.println();
+ STATION_Utils::addToOutputPacketBuffer(String(payload, length));
+ }
+
+ void connect() {
+ if (pubSub.connected()) return;
+ if (Config.mqtt.server.isEmpty() || Config.mqtt.port <= 0) {
+ Serial.println("Connect to MQTT server KO because no host or port given");
+ return;
+ }
+ pubSub.setServer(Config.mqtt.server.c_str(), Config.mqtt.port);
+ Serial.print("Trying to connect with MQTT Server: " + String(Config.mqtt.server) + " MqttServerPort: " + String(Config.mqtt.port));
+ if (pubSub.connect(Config.callsign.c_str(), Config.mqtt.username.c_str(), Config.mqtt.password.c_str())) {
+ Serial.println(" -> Connected !");
+ const String subscribedTopic = Config.mqtt.topic + "/" + Config.callsign + "/#";
+ if (!pubSub.subscribe(subscribedTopic.c_str())) {
+ Serial.println("Subscribed to MQTT Failed");
+ }
+ Serial.print("Subscribed to MQTT topic : ");
+ Serial.println(subscribedTopic);
+ } else {
+ Serial.println(" -> Not Connected (Retry in 10 secs)");
+ }
+ }
+
+ void loop() {
+ if (!Config.mqtt.active) return;
+ if (!pubSub.connected()) return;
+ pubSub.loop();
+ }
+
+ void setup() {
+ if (!Config.mqtt.active) return;
+ pubSub.setClient(mqttClient);
+ pubSub.setCallback(receivedFromMqtt);
+ }
+
+}
\ No newline at end of file
diff --git a/src/utils.cpp b/src/utils.cpp
index e5efa40..6da7f16 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -35,7 +35,6 @@
extern Configuration Config;
-extern WiFiClient espClient;
extern TinyGPSPlus gps;
extern String versionDate;
extern String firstLine;
diff --git a/src/web_utils.cpp b/src/web_utils.cpp
index 221769d..5b9902a 100644
--- a/src/web_utils.cpp
+++ b/src/web_utils.cpp
@@ -240,6 +240,13 @@ namespace WEB_Utils {
Config.remoteManagement.managers = request->getParam("remoteManagement.managers", true)->value();
Config.remoteManagement.rfOnly = request->getParam("remoteManagement.rfOnly", true);
+ Config.mqtt.active = request->getParam("mqtt.active", true);
+ Config.mqtt.server = request->getParam("mqtt.server", true)->value();
+ Config.mqtt.topic = request->getParam("mqtt.topic", true)->value();
+ Config.mqtt.username = request->getParam("mqtt.username", true)->value();
+ Config.mqtt.password = request->getParam("mqtt.password", true)->value();
+ Config.mqtt.port = request->getParam("mqtt.port", true)->value().toInt();
+
Config.writeFile();
AsyncWebServerResponse *response = request->beginResponse(302, "text/html", "");