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", "");