From 913c2e5fca845aedd1d15a7d5a9ad112e0f491ff Mon Sep 17 00:00:00 2001 From: KrzysKond Date: Mon, 25 May 2026 12:45:22 -0700 Subject: [PATCH 1/6] feat: GPS webserver hosted on esp32 --- src/esp32s3/esp32s3.ino | 78 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/esp32s3/esp32s3.ino b/src/esp32s3/esp32s3.ino index fa97b4d..15cf0ea 100644 --- a/src/esp32s3/esp32s3.ino +++ b/src/esp32s3/esp32s3.ino @@ -2,6 +2,8 @@ #include // from RadioLib by Jan Gromes v7.6.0 #include // from TinyGPSPlus by Mikal Hart v1.0.3 #include "simba_headers/simba/mavlink.h" // generated from simba.xml +#include +#include #include "LoRaQueue.hpp" @@ -32,7 +34,17 @@ #define UART_RING_BUFFER_SIZE 8096 // zabezpieczenie przed burstami danych +#define WIFI_AP_SSID "LoRa-GPS" +#define WIFI_AP_PASS "simba1234" +#define WIFI_AP_IP "192.168.4.1" +#define WEB_REFRESH_SEC 10 + LoRaQueue loraQueue; +WebServer webServer(80); + +mavlink_simba_gps_t loraRxGps = {}; +bool loraRxValid = false; +unsigned long loraRxLastUpdateMs = 0; SPIClass spiLora(HSPI); SX1262 radio = new Module(LORA_NSS, LORA_DIO1, LORA_RESET, LORA_BUSY, spiLora); @@ -54,6 +66,7 @@ void readNoise(); void readMavlinkUART(); void sendBytesToComputer(uint8_t* data, size_t length); +void handleWebRoot(); volatile bool radioEvent = false; void radioEventCallback() { @@ -124,6 +137,17 @@ void setup() { Serial.printf("[INIT] BLAD LoRa: %d\n", state); while (true); } + + IPAddress ip, gateway, subnet; + ip.fromString(WIFI_AP_IP); + gateway.fromString(WIFI_AP_IP); + subnet.fromString("255.255.255.0"); + WiFi.softAPConfig(ip, gateway, subnet); + WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASS); + Serial.printf("[INIT] WiFi AP: %s IP: %s\n", WIFI_AP_SSID, WIFI_AP_IP); + + webServer.on("/", handleWebRoot); + webServer.begin(); } void loop() { @@ -161,6 +185,8 @@ void loop() { // send link stats to Serial once in a while sendRadioStatsToComputer(); + webServer.handleClient(); + // count delay and imply a delay so it is a LOOP_DELAY_MS delay between the start of each loop iteration unsigned long loopDuration = millis() - loopStart; if (loopDuration < LOOP_DELAY_MS) { @@ -382,6 +408,19 @@ void readRadioTransmission() // no errors // send bytes to the computer sendBytesToComputer(data, numBytes); + + // parse for SIMBA_GPS to update the web page + static mavlink_message_t rxMsg; + static mavlink_status_t rxStatus; + for (int i = 0; i < numBytes; i++) { + if (mavlink_parse_char(MAVLINK_COMM_1, data[i], &rxMsg, &rxStatus)) { + if (rxMsg.msgid == MAVLINK_MSG_ID_SIMBA_GPS) { + mavlink_msg_simba_gps_decode(&rxMsg, &loraRxGps); + loraRxValid = true; + loraRxLastUpdateMs = millis(); + } + } + } } } @@ -422,4 +461,43 @@ void sendBytesToComputer(uint8_t* data, size_t length) Serial.write(data, length); // also the mavlink serial (rocket) SerialMAV.write(data, length); +} + +void handleWebRoot() +{ + String html = F("" + "" + "LoRa GPS" + "" + "" + "

LoRa GPS Tracker

"); + + html += F("

Rocket (LoRa RX)

"); + if (loraRxValid) { + unsigned long ageSec = (millis() - loraRxLastUpdateMs) / 1000UL; + double lat = loraRxGps.lat / 1e7; + double lon = loraRxGps.lon / 1e7; + float alt = loraRxGps.altitude / 100.0f; + String cls = (ageSec > 10) ? " class='stale'" : ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += "
LatLonAlt (m)Age
" + String(lat, 7) + "" + String(lon, 7) + "" + String(alt, 1) + "" + String(ageSec) + " s
"; + html += "

Open in Google Maps

"; + } else { + html += F("

No data received yet.

"); + } + + html += F("

Refreshes every "); + html += WEB_REFRESH_SEC; + html += F(" s

"); + webServer.send(200, "text/html", html); } \ No newline at end of file From d845a412553e83056b536a7b972edb34ff8dbb3b Mon Sep 17 00:00:00 2001 From: KrzysKond Date: Mon, 25 May 2026 13:30:43 -0700 Subject: [PATCH 2/6] feat: run webserve only in transceiver mode, when transceiver mode set, sned internal gps every 5s --- src/esp32s3/esp32s3.ino | 58 +++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/src/esp32s3/esp32s3.ino b/src/esp32s3/esp32s3.ino index 15cf0ea..705f227 100644 --- a/src/esp32s3/esp32s3.ino +++ b/src/esp32s3/esp32s3.ino @@ -11,6 +11,7 @@ #define MODE_RECEIVER 0 #define MODE_TRANSMITTER 1 +#define MODE_TRANSCEIVER 2 #define MODULE_MODE MODE_RECEIVER #define LORA_NSS 41 @@ -27,6 +28,7 @@ #define MAV_TX 1 #define TX_INTERVAL_MS 2000UL +#define TX_INTERVAL_TRANSCEIVER_MS 5000UL #define TX_GNSS_TO_COMPUTER_INTERVAL_MS 1000UL #define TX_RADIO_STATS_TO_COMPUTER_INTERVAL_MS 1000 #define READ_NOISE_INTERVAL_MS 50UL @@ -36,7 +38,7 @@ #define WIFI_AP_SSID "LoRa-GPS" #define WIFI_AP_PASS "simba1234" -#define WIFI_AP_IP "192.168.4.1" +#define WIFI_AP_IP "192.168.10.131" #define WEB_REFRESH_SEC 10 LoRaQueue loraQueue; @@ -138,16 +140,18 @@ void setup() { while (true); } - IPAddress ip, gateway, subnet; - ip.fromString(WIFI_AP_IP); - gateway.fromString(WIFI_AP_IP); - subnet.fromString("255.255.255.0"); - WiFi.softAPConfig(ip, gateway, subnet); - WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASS); - Serial.printf("[INIT] WiFi AP: %s IP: %s\n", WIFI_AP_SSID, WIFI_AP_IP); - - webServer.on("/", handleWebRoot); - webServer.begin(); + if (radioStatus.mode == RadioMode::TRANSCEIVER) { + IPAddress ip, gateway, subnet; + ip.fromString(WIFI_AP_IP); + gateway.fromString(WIFI_AP_IP); + subnet.fromString("255.255.255.0"); + WiFi.softAPConfig(ip, gateway, subnet); + WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASS); + Serial.printf("[INIT] WiFi AP: %s IP: %s\n", WIFI_AP_SSID, WIFI_AP_IP); + + webServer.on("/", handleWebRoot); + webServer.begin(); + } } void loop() { @@ -185,7 +189,9 @@ void loop() { // send link stats to Serial once in a while sendRadioStatsToComputer(); - webServer.handleClient(); + if (radioStatus.mode == RadioMode::TRANSCEIVER) { + webServer.handleClient(); + } // count delay and imply a delay so it is a LOOP_DELAY_MS delay between the start of each loop iteration unsigned long loopDuration = millis() - loopStart; @@ -252,9 +258,13 @@ void sendInternalGPSPos() static unsigned long lastSend = 0; - if (millis() - lastSend < TX_INTERVAL_MS) { + unsigned long interval = (radioStatus.mode == RadioMode::TRANSCEIVER) + ? TX_INTERVAL_TRANSCEIVER_MS + : TX_INTERVAL_MS; + if (millis() - lastSend < interval) { return; // not time to send yet } + lastSend = millis(); static mavlink_message_t msg; static mavlink_status_t mav_status; @@ -409,15 +419,17 @@ void readRadioTransmission() // send bytes to the computer sendBytesToComputer(data, numBytes); - // parse for SIMBA_GPS to update the web page - static mavlink_message_t rxMsg; - static mavlink_status_t rxStatus; - for (int i = 0; i < numBytes; i++) { - if (mavlink_parse_char(MAVLINK_COMM_1, data[i], &rxMsg, &rxStatus)) { - if (rxMsg.msgid == MAVLINK_MSG_ID_SIMBA_GPS) { - mavlink_msg_simba_gps_decode(&rxMsg, &loraRxGps); - loraRxValid = true; - loraRxLastUpdateMs = millis(); + // parse for SIMBA_GPS to update the web page (transceiver only) + if (radioStatus.mode == RadioMode::TRANSCEIVER) { + static mavlink_message_t rxMsg; + static mavlink_status_t rxStatus; + for (int i = 0; i < numBytes; i++) { + if (mavlink_parse_char(MAVLINK_COMM_1, data[i], &rxMsg, &rxStatus)) { + if (rxMsg.msgid == MAVLINK_MSG_ID_SIMBA_GPS) { + mavlink_msg_simba_gps_decode(&rxMsg, &loraRxGps); + loraRxValid = true; + loraRxLastUpdateMs = millis(); + } } } } @@ -483,7 +495,7 @@ void handleWebRoot() double lat = loraRxGps.lat / 1e7; double lon = loraRxGps.lon / 1e7; float alt = loraRxGps.altitude / 100.0f; - String cls = (ageSec > 10) ? " class='stale'" : ""; + String cls = (ageSec > WEB_REFRESH_SEC) ? " class='stale'" : ""; html += ""; html += ""; html += ""; From faec92be583677bcad3b16fbeb7715fe2a5cdfd8 Mon Sep 17 00:00:00 2001 From: KrzysKond Date: Mon, 25 May 2026 15:40:29 -0700 Subject: [PATCH 3/6] feat: add GS position, fix refresh --- src/esp32s3/esp32s3.ino | 48 ++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/esp32s3/esp32s3.ino b/src/esp32s3/esp32s3.ino index 705f227..2e3c456 100644 --- a/src/esp32s3/esp32s3.ino +++ b/src/esp32s3/esp32s3.ino @@ -44,7 +44,9 @@ LoRaQueue loraQueue; WebServer webServer(80); -mavlink_simba_gps_t loraRxGps = {}; +mavlink_simba_gps_t loraRxRocketGps = {}; +mavlink_simba_gps_t loraRxGSGps = {}; + bool loraRxValid = false; unsigned long loraRxLastUpdateMs = 0; @@ -426,7 +428,12 @@ void readRadioTransmission() for (int i = 0; i < numBytes; i++) { if (mavlink_parse_char(MAVLINK_COMM_1, data[i], &rxMsg, &rxStatus)) { if (rxMsg.msgid == MAVLINK_MSG_ID_SIMBA_GPS) { - mavlink_msg_simba_gps_decode(&rxMsg, &loraRxGps); + if (rxMsg.sysid == 1 && rxMsg.compid == 200){ + mavlink_msg_simba_gps_decode(&rxMsg, &loraRxRocketGps); + } + else { + mavlink_msg_simba_gps_decode(&rxMsg, &loraRxGSGps); + } loraRxValid = true; loraRxLastUpdateMs = millis(); } @@ -479,28 +486,43 @@ void handleWebRoot() { String html = F("" "" - "LoRa GPS" + "LoRa GPS" "" + "" "" "

LoRa GPS Tracker

"); html += F("

Rocket (LoRa RX)

"); if (loraRxValid) { - unsigned long ageSec = (millis() - loraRxLastUpdateMs) / 1000UL; - double lat = loraRxGps.lat / 1e7; - double lon = loraRxGps.lon / 1e7; - float alt = loraRxGps.altitude / 100.0f; - String cls = (ageSec > WEB_REFRESH_SEC) ? " class='stale'" : ""; - html += "
LatLonAlt (m)Age
" + String(lat, 7) + "" + String(lon, 7) + "
"; + double lat = loraRxRocketGps.lat / 1e7; + double lon = loraRxRocketGps.lon / 1e7; + float alt = loraRxRocketGps.altitude / 100.0f; + html += F("
LatLonAlt (m)Age
"); + html += ""; + html += ""; + html += "
LatLonAlt (m)
" + String(lat, 7) + "" + String(lon, 7) + "" + String(alt, 1) + "
"; + html += "

Open in Google Maps

"; + } else { + html += F("

No data received yet.

"); + } + + html += F("
"); + html += F("

Ground Station (LoRa RX)

"); + if (loraRxValid) { + double lat = loraRxGSGps.lat / 1e7; + double lon = loraRxGSGps.lon / 1e7; + float alt = loraRxGSGps.altitude / 100.0f; + html += F(""); html += ""; html += ""; - html += ""; - html += "
LatLonAlt (m)
" + String(lat, 7) + "" + String(lon, 7) + "" + String(alt, 1) + "" + String(ageSec) + " s
"; + html += "" + String(alt, 1) + ""; html += "

Open in Google Maps

"; From 06ebdac86416774b234866509fc6a937b9f00db9 Mon Sep 17 00:00:00 2001 From: KrzysKond Date: Tue, 26 May 2026 06:37:03 -0700 Subject: [PATCH 4/6] feat: hardcode bunker position --- src/esp32s3/esp32s3.ino | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/esp32s3/esp32s3.ino b/src/esp32s3/esp32s3.ino index 2e3c456..2839538 100644 --- a/src/esp32s3/esp32s3.ino +++ b/src/esp32s3/esp32s3.ino @@ -41,11 +41,13 @@ #define WIFI_AP_IP "192.168.10.131" #define WEB_REFRESH_SEC 10 +#define LAT_BUNKER 35.346444 +#define LON_BUNKER -117.808194 + LoRaQueue loraQueue; WebServer webServer(80); mavlink_simba_gps_t loraRxRocketGps = {}; -mavlink_simba_gps_t loraRxGSGps = {}; bool loraRxValid = false; unsigned long loraRxLastUpdateMs = 0; @@ -431,9 +433,6 @@ void readRadioTransmission() if (rxMsg.sysid == 1 && rxMsg.compid == 200){ mavlink_msg_simba_gps_decode(&rxMsg, &loraRxRocketGps); } - else { - mavlink_msg_simba_gps_decode(&rxMsg, &loraRxGSGps); - } loraRxValid = true; loraRxLastUpdateMs = millis(); } @@ -514,21 +513,13 @@ void handleWebRoot() } html += F("
"); - html += F("

Ground Station (LoRa RX)

"); - if (loraRxValid) { - double lat = loraRxGSGps.lat / 1e7; - double lon = loraRxGSGps.lon / 1e7; - float alt = loraRxGSGps.altitude / 100.0f; - html += F(""); - html += ""; - html += ""; - html += "
LatLonAlt (m)
" + String(lat, 7) + "" + String(lon, 7) + "" + String(alt, 1) + "
"; - html += "

Open in Google Maps

"; - } else { - html += F("

No data received yet.

"); - } + html += F("

Ground Station (bunker)

"); + html += F(""); + html += ""; + html += "
LatLonAlt (m)
" + String(LAT_BUNKER, 7) + "" + String(LON_BUNKER, 7) + "
"; + html += "

Open in Google Maps

"; html += F("

Refreshes every "); html += WEB_REFRESH_SEC; From 3fd5dbcb1945df4e085f7c1e1a829687353f58d9 Mon Sep 17 00:00:00 2001 From: KrzysKond Date: Tue, 26 May 2026 06:38:59 -0700 Subject: [PATCH 5/6] fix: remove orphan column --- src/esp32s3/esp32s3.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esp32s3/esp32s3.ino b/src/esp32s3/esp32s3.ino index 2839538..bdd36ef 100644 --- a/src/esp32s3/esp32s3.ino +++ b/src/esp32s3/esp32s3.ino @@ -501,7 +501,7 @@ void handleWebRoot() double lat = loraRxRocketGps.lat / 1e7; double lon = loraRxRocketGps.lon / 1e7; float alt = loraRxRocketGps.altitude / 100.0f; - html += F(""); + html += F("
LatLonAlt (m)
"); html += ""; html += ""; html += "
LatLon
" + String(lat, 7) + "" + String(lon, 7) + "" + String(alt, 1) + "
"; From 391f4ce9a7ba21b7a382126b135fb9b035964f20 Mon Sep 17 00:00:00 2001 From: KrzysKond Date: Sat, 30 May 2026 13:05:38 -0700 Subject: [PATCH 6/6] review fixes v1 --- src/esp32s3/esp32s3.ino | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/esp32s3/esp32s3.ino b/src/esp32s3/esp32s3.ino index bdd36ef..495b4fb 100644 --- a/src/esp32s3/esp32s3.ino +++ b/src/esp32s3/esp32s3.ino @@ -2,9 +2,8 @@ #include // from RadioLib by Jan Gromes v7.6.0 #include // from TinyGPSPlus by Mikal Hart v1.0.3 #include "simba_headers/simba/mavlink.h" // generated from simba.xml -#include -#include - +#include // from the ESP32 Arduino framework +#include // from the ESP32 Arduino framework #include "LoRaQueue.hpp" #define DEBUG @@ -41,8 +40,8 @@ #define WIFI_AP_IP "192.168.10.131" #define WEB_REFRESH_SEC 10 -#define LAT_BUNKER 35.346444 -#define LON_BUNKER -117.808194 +#define LAT_SETUP_AREA 35.342322 +#define LON_SETUP_AREA -117.825152 LoRaQueue loraQueue; WebServer webServer(80); @@ -513,12 +512,12 @@ void handleWebRoot() } html += F("


"); - html += F("

Ground Station (bunker)

"); + html += F("

Setup area)

"); html += F(""); - html += ""; - html += "
LatLonAlt (m)
" + String(LAT_BUNKER, 7) + "" + String(LON_BUNKER, 7) + "
"; + html += "" + String(LAT_SETUP_AREA, 7) + ""; + html += "" + String(LON_SETUP_AREA, 7) + ""; html += "

Open in Google Maps

"; html += F("

Refreshes every ");