mirror of
https://github.com/richonguzman/LoRa_APRS_iGate.git
synced 2026-07-07 02:11:04 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96d7be6ffc | |||
| 94df6d2e8f | |||
| 0a222edeb8 | |||
| e35d5f6a9e | |||
| 96432b8624 | |||
| 25ea82737a | |||
| df22b9dbc0 | |||
| 9898e123d2 | |||
| a97757c0b2 | |||
| 305c5b73f7 | |||
| dd3688f23b | |||
| 6f5846c53d | |||
| 5c30e72a88 | |||
| a024b939f5 | |||
| 96c52c042e | |||
| 236ae65485 | |||
| 4263d55839 | |||
| ba96bb135e | |||
| 944d1fceaf |
@@ -101,6 +101,8 @@ jobs:
|
||||
chip: esp32s3
|
||||
- name: QRPLabs_LightGateway_Plus_1_0
|
||||
chip: esp32s3
|
||||
- name: RPC_LORA_DIGIGATE_1W
|
||||
chip: esp32
|
||||
- name: XIAO_ESP32S3_WIO_SX1262
|
||||
chip: esp32s3
|
||||
- name: TROY_LoRa_APRS
|
||||
|
||||
@@ -51,6 +51,7 @@ ____________________________________________________
|
||||
<br />
|
||||
|
||||
# Timeline (Versions):
|
||||
- 2026-06-10 CAD (Channel Activity Detection) and DIFS (Distributed Inter-Frame Space) added.
|
||||
- 2026-03-25 More Boards, SDK update, OTA fix, GPS process update.
|
||||
- 2026-02-26 9M2IBR ESP32 1W (400M30S) + GPS board added.
|
||||
- 2026-02-25 Code Improvements: reduced String comparisons and improved logic for faster code execution.
|
||||
|
||||
+4
-4
@@ -32,16 +32,16 @@ lib_deps =
|
||||
adafruit/Adafruit Si7021 Library @ 1.5.3
|
||||
arduino-libraries/NTPClient @ 3.2.1
|
||||
ayushsharma82/ElegantOTA @ 3.1.7
|
||||
bblanchon/ArduinoJson @ 7.4.2
|
||||
bblanchon/ArduinoJson @ 7.4.3
|
||||
jgromes/RadioLib @ 7.6.0
|
||||
knolleary/PubSubClient @ 2.8
|
||||
ESP32Async/AsyncTCP @ 3.4.10
|
||||
ESP32Async/ESPAsyncWebServer @ 3.10.0
|
||||
ESP32Async/ESPAsyncWebServer @ 3.11.1
|
||||
mikalhart/TinyGPSPlus @ 1.0.3
|
||||
richonguzman/APRSPacketLib @ 1.0.4
|
||||
display_libs =
|
||||
adafruit/Adafruit GFX Library @ 1.11.9
|
||||
adafruit/Adafruit SSD1306 @ 2.5.10
|
||||
adafruit/Adafruit GFX Library @ 1.12.6
|
||||
adafruit/Adafruit SSD1306 @ 2.5.17
|
||||
usb_flags=
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
@@ -28,9 +28,9 @@
|
||||
"messagesToRF": false,
|
||||
"objectsToRF": false,
|
||||
"server": "rotate.aprs2.net",
|
||||
"passcode": "XYZVW",
|
||||
"passcode": "XYZVW",
|
||||
"port": 14580,
|
||||
"filter": "m/10"
|
||||
"filter": "m/10"
|
||||
},
|
||||
"personalNote": "",
|
||||
"blacklist": "",
|
||||
@@ -47,10 +47,11 @@
|
||||
"rxSignalBandwidth": 125000,
|
||||
"txActive": false,
|
||||
"txFreq": 433775000,
|
||||
"txSpreadingFactor": 12,
|
||||
"txCodingRate4": 5,
|
||||
"txSpreadingFactor": 9,
|
||||
"txCodingRate4": 7,
|
||||
"txSignalBandwidth": 125000,
|
||||
"power": 20
|
||||
"power": 20,
|
||||
"cadActive": true
|
||||
},
|
||||
"display": {
|
||||
"alwaysOn": true,
|
||||
|
||||
+1093
-978
File diff suppressed because one or more lines are too long
+212
-7
@@ -118,7 +118,7 @@ function loadSettings(settings) {
|
||||
document.getElementById("beacon.latitude").value = settings.beacon.latitude;
|
||||
document.getElementById("beacon.longitude").value = settings.beacon.longitude;
|
||||
document.getElementById("beacon.interval").value = settings.beacon.interval;
|
||||
document.getElementById("other.rememberStationTime").value = settings.other.rememberStationTime;
|
||||
document.getElementById("other.rememberStationTime").value = settings.other.rememberStationTime;
|
||||
document.getElementById("beacon.sendViaAPRSIS").checked = settings.beacon.sendViaAPRSIS;
|
||||
|
||||
document.getElementById("beacon.sendViaRF").checked = settings.beacon.sendViaRF;
|
||||
@@ -148,12 +148,26 @@ function loadSettings(settings) {
|
||||
document.getElementById("lora.rxSpreadingFactor").value = settings.lora.rxSpreadingFactor;
|
||||
document.getElementById("lora.rxCodingRate4").value = settings.lora.rxCodingRate4;
|
||||
document.getElementById("lora.rxSignalBandwidth").value = settings.lora.rxSignalBandwidth;
|
||||
LoRaRxCheckbox.checked = settings.lora.rxActive;
|
||||
LoRaRxFreq.disabled = !LoRaRxCheckbox.checked;
|
||||
LoRaRxSpreadingFactor.disabled = !LoRaRxCheckbox.checked;
|
||||
LoRaRxCodingRate.disabled = !LoRaRxCheckbox.checked;
|
||||
LoRaRxSignalBandwidth.disabled = !LoRaRxCheckbox.checked;
|
||||
|
||||
document.getElementById("lora.txActive").checked = settings.lora.txActive;
|
||||
document.getElementById("lora.txFreq").value = settings.lora.txFreq;
|
||||
document.getElementById("lora.txSpreadingFactor").value = settings.lora.txSpreadingFactor;
|
||||
document.getElementById("lora.txCodingRate4").value = settings.lora.txCodingRate4;
|
||||
document.getElementById("lora.txSignalBandwidth").value = settings.lora.txSignalBandwidth;
|
||||
document.getElementById("lora.txSignalBandwidth").value = settings.lora.txSignalBandwidth;
|
||||
document.getElementById("lora.power").value = settings.lora.power;
|
||||
document.getElementById("lora.cadActive").checked = settings.lora.cadActive;
|
||||
LoRaTxCheckbox.checked = settings.lora.txActive;
|
||||
LoRaTxFreq.disabled = !LoRaTxCheckbox.checked;
|
||||
LoRaTxSpreadingFactor.disabled = !LoRaTxCheckbox.checked;
|
||||
LoRaTxCodingRate.disabled = !LoRaTxCheckbox.checked;
|
||||
LoRaTxSignalBandwidth.disabled = !LoRaTxCheckbox.checked;
|
||||
LoRaPower.disabled = !LoRaTxCheckbox.checked;
|
||||
LoRaCadActive.disabled = !LoRaTxCheckbox.checked;
|
||||
|
||||
// Display
|
||||
document.getElementById("display.alwaysOn").checked = settings.display.alwaysOn;
|
||||
@@ -195,7 +209,7 @@ function loadSettings(settings) {
|
||||
TelemetryCheckbox.checked = settings.wxsensor.active;
|
||||
TelemetryHeightCorrection.disabled = !TelemetryCheckbox.checked;
|
||||
TelemetryTempCorrection.disabled = !TelemetryCheckbox.checked;
|
||||
|
||||
|
||||
// SYSLOG
|
||||
document.getElementById("syslog.active").checked = settings.syslog.active;
|
||||
document.getElementById("syslog.server").value = settings.syslog.server;
|
||||
@@ -205,7 +219,7 @@ function loadSettings(settings) {
|
||||
SyslogServer.disabled = !SyslogCheckbox.checked;
|
||||
SyslogPort.disabled = !SyslogCheckbox.checked;
|
||||
SyslogBeaconOverTCPIP.disabled = !SyslogCheckbox.checked;
|
||||
|
||||
|
||||
// TNC
|
||||
if (settings.tnc) {
|
||||
document.getElementById("tnc.enableServer").checked = settings.tnc.enableServer;
|
||||
@@ -390,6 +404,36 @@ TelemetryCheckbox.addEventListener("change", function () {
|
||||
TelemetryTempCorrection.disabled = !this.checked;
|
||||
});
|
||||
|
||||
// LoRa Rx Switch
|
||||
const LoRaRxCheckbox = document.querySelector('input[name="lora.rxActive"]');
|
||||
const LoRaRxFreq = document.querySelector('[name="lora.rxFreq"]');
|
||||
const LoRaRxSpreadingFactor = document.querySelector('[name="lora.rxSpreadingFactor"]');
|
||||
const LoRaRxCodingRate = document.querySelector('[name="lora.rxCodingRate4"]');
|
||||
const LoRaRxSignalBandwidth = document.querySelector('[name="lora.rxSignalBandwidth"]');
|
||||
LoRaRxCheckbox.addEventListener("change", function () {
|
||||
LoRaRxFreq.disabled = !this.checked;
|
||||
LoRaRxSpreadingFactor.disabled = !this.checked;
|
||||
LoRaRxCodingRate.disabled = !this.checked;
|
||||
LoRaRxSignalBandwidth.disabled = !this.checked;
|
||||
});
|
||||
|
||||
// LoRa Tx Switch
|
||||
const LoRaTxCheckbox = document.querySelector('input[name="lora.txActive"]');
|
||||
const LoRaTxFreq = document.querySelector('[name="lora.txFreq"]');
|
||||
const LoRaTxSpreadingFactor = document.querySelector('[name="lora.txSpreadingFactor"]');
|
||||
const LoRaTxCodingRate = document.querySelector('[name="lora.txCodingRate4"]');
|
||||
const LoRaTxSignalBandwidth = document.querySelector('[name="lora.txSignalBandwidth"]');
|
||||
const LoRaPower = document.querySelector('[name="lora.power"]');
|
||||
const LoRaCadActive = document.querySelector('[name="lora.cadActive"]');
|
||||
LoRaTxCheckbox.addEventListener("change", function () {
|
||||
LoRaTxFreq.disabled = !this.checked;
|
||||
LoRaTxSpreadingFactor.disabled = !this.checked;
|
||||
LoRaTxCodingRate.disabled = !this.checked;
|
||||
LoRaTxSignalBandwidth.disabled = !this.checked;
|
||||
LoRaPower.disabled = !this.checked;
|
||||
LoRaCadActive.disabled = !this.checked;
|
||||
});
|
||||
|
||||
// Syslog Switches
|
||||
const SyslogCheckbox = document.querySelector('input[name="syslog.active"]');
|
||||
const SyslogServer = document.querySelector('input[name="syslog.server"]');
|
||||
@@ -559,7 +603,7 @@ function loadReceivedPackets(packets) {
|
||||
|
||||
packets.forEach((packet) => {
|
||||
const element = document.createElement("tr");
|
||||
|
||||
|
||||
element.innerHTML = `
|
||||
<td>${packet.rxTime}</td>
|
||||
<td>${packet.packet}</td>
|
||||
@@ -592,8 +636,169 @@ document.querySelector('a[href="/received-packets"]').addEventListener('click',
|
||||
|
||||
document.getElementById('received-packets').classList.remove('d-none');
|
||||
document.getElementById('configuration').classList.add('d-none');
|
||||
|
||||
|
||||
document.querySelector('button[type=submit]').remove();
|
||||
|
||||
fetchReceivedPackets();
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
/* ---------- Stations Map (Leaflet, CDN) ---------- */
|
||||
|
||||
let mapInstance = null;
|
||||
let mapMarkers = null;
|
||||
let mapTimer = null;
|
||||
let mapTileErrShown = false;
|
||||
let iGateLatLng = null;
|
||||
let mapAutoFitDone = false;
|
||||
|
||||
function setMapMessage(text) {
|
||||
const el = document.getElementById("map");
|
||||
if (el) {
|
||||
el.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--msh-text-dim,#8b949e);text-align:center;padding:20px;">${text}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function loadMapStations(stations) {
|
||||
if (!mapMarkers) return;
|
||||
|
||||
mapMarkers.clearLayers();
|
||||
|
||||
let drawn = 0;
|
||||
(stations || []).forEach((s) => {
|
||||
if (s.lat === 0 && s.lon === 0) return; // descartar estaciones sin fix
|
||||
|
||||
const popup = `<b>${s.callsign}</b>`
|
||||
+ (s.lastHeard ? `<br>Last: ${s.lastHeard}` : "")
|
||||
+ `<br>RSSI ${s.RSSI} / SNR ${s.SNR}`
|
||||
+ `<br>Packets: ${s.count}`;
|
||||
|
||||
L.circleMarker([s.lat, s.lon], {
|
||||
radius: 6,
|
||||
color: "#1d6fe0", // borde
|
||||
weight: 2,
|
||||
fillColor: "#3b8cff", // relleno
|
||||
fillOpacity: 0.9
|
||||
}).bindPopup(popup).addTo(mapMarkers);
|
||||
drawn++;
|
||||
});
|
||||
|
||||
if (!mapAutoFitDone && drawn > 0) { // auto-fit la 1ª vez que hay estaciones tras abrir
|
||||
fitMapToStations();
|
||||
mapAutoFitDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Encuadra el mapa para que entren todas las estaciones (+ el iGate). 100% navegador.
|
||||
function fitMapToStations() {
|
||||
if (!mapInstance) return false;
|
||||
|
||||
const pts = [];
|
||||
if (mapMarkers) {
|
||||
mapMarkers.eachLayer((layer) => {
|
||||
if (layer.getLatLng) pts.push(layer.getLatLng());
|
||||
});
|
||||
}
|
||||
if (iGateLatLng) pts.push(iGateLatLng);
|
||||
|
||||
if (!pts.length) return false;
|
||||
mapInstance.fitBounds(L.latLngBounds(pts), { padding: [30, 30], maxZoom: 14 });
|
||||
return true;
|
||||
}
|
||||
|
||||
function fetchMapStations() {
|
||||
fetch("/stations.json")
|
||||
.then((response) => response.json())
|
||||
.then((stations) => {
|
||||
loadMapStations(stations);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
console.error(`Failed to load stations`);
|
||||
});
|
||||
}
|
||||
|
||||
window.showMap = function () {
|
||||
if (typeof L === "undefined") { // sin internet el CDN de Leaflet no cargó
|
||||
setMapMessage("Map unavailable — no internet connection.<br>The map needs internet to load (OpenStreetMap).");
|
||||
return;
|
||||
}
|
||||
|
||||
mapAutoFitDone = false; // re-encuadrar en cada apertura del Map
|
||||
|
||||
if (!mapInstance) {
|
||||
mapInstance = L.map("map");
|
||||
|
||||
const tiles = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 19,
|
||||
attribution: "© OpenStreetMap"
|
||||
}).addTo(mapInstance);
|
||||
|
||||
tiles.on("tileerror", function () { // tiles no cargan (sin salida a internet)
|
||||
if (!mapTileErrShown) {
|
||||
mapTileErrShown = true;
|
||||
if (window.showToast) showToast("Map tiles could not load — check internet connection.");
|
||||
}
|
||||
});
|
||||
|
||||
mapMarkers = L.layerGroup().addTo(mapInstance);
|
||||
|
||||
const FitControl = L.Control.extend({ // botón "Fit All Stations" (control nativo de Leaflet)
|
||||
options: { position: "topright" },
|
||||
onAdd: function () {
|
||||
const btn = L.DomUtil.create("button", "");
|
||||
btn.type = "button";
|
||||
btn.title = "Fit all stations";
|
||||
btn.textContent = "Fit All Stations";
|
||||
btn.style.cssText = "background:#fff;color:#333;border:2px solid rgba(0,0,0,.2);border-radius:4px;padding:5px 9px;cursor:pointer;font:600 12px sans-serif;box-shadow:0 1px 4px rgba(0,0,0,.3);";
|
||||
L.DomEvent.on(btn, "click", (e) => {
|
||||
L.DomEvent.stop(e);
|
||||
fitMapToStations();
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
});
|
||||
mapInstance.addControl(new FitControl());
|
||||
|
||||
fetch("/configuration.json") // centrar y marcar el iGate
|
||||
.then((response) => response.json())
|
||||
.then((config) => {
|
||||
const lat = (config.beacon && config.beacon.latitude) || 0;
|
||||
const lon = (config.beacon && config.beacon.longitude) || 0;
|
||||
const callsign = config.callsign || "iGate";
|
||||
mapInstance.setView([lat, lon], (lat || lon) ? 12 : 2);
|
||||
|
||||
if (lat || lon) { // rombo rojo en la posición del iGate
|
||||
iGateLatLng = L.latLng(lat, lon); // guardar para incluirlo en el auto-fit
|
||||
const iGateIcon = L.divIcon({
|
||||
className: "igate-marker",
|
||||
html: '<div style="width:14px;height:14px;background:#e23b3b;border:2px solid #fff;transform:rotate(45deg);box-shadow:0 0 4px rgba(0,0,0,.6);"></div>',
|
||||
iconSize: [18, 18],
|
||||
iconAnchor: [9, 9]
|
||||
});
|
||||
L.marker([lat, lon], { icon: iGateIcon })
|
||||
.bindPopup(`<b>${callsign}</b><br>This station`)
|
||||
.addTo(mapInstance); // directo al mapa: no se borra en los refrescos
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
mapInstance.setView([0, 0], 2);
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(function () { // el div estuvo oculto: recalcular tamaño
|
||||
if (mapInstance) mapInstance.invalidateSize();
|
||||
}, 0);
|
||||
|
||||
fetchMapStations();
|
||||
|
||||
if (mapTimer) clearInterval(mapTimer);
|
||||
|
||||
mapTimer = setInterval(function () { // refrescar solo si la sección está visible
|
||||
const section = document.getElementById("sec-maps");
|
||||
if (section && section.classList.contains("active")) {
|
||||
fetchMapStations();
|
||||
}
|
||||
}, 15000);
|
||||
}
|
||||
@@ -86,6 +86,7 @@ public:
|
||||
int txCodingRate4;
|
||||
long txSignalBandwidth;
|
||||
int power;
|
||||
bool cadActive;
|
||||
};
|
||||
|
||||
class Display {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MAP_UTILS_H_
|
||||
#define MAP_UTILS_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
|
||||
struct MapStation {
|
||||
char callsign[10]; // "XX9XXX-NN" + null -> clave para deduplicar
|
||||
float latitude; // grados decimales (+N / -S)
|
||||
float longitude; // grados decimales (+E / -W)
|
||||
char symbol[3]; // tabla + codigo APRS (ej "/>"), "" si no hay
|
||||
int16_t rssi; // ultimo RSSI
|
||||
float snr; // ultimo SNR
|
||||
uint16_t count; // nro de paquetes oidos de esa estacion
|
||||
char lastHeard[10]; // "HH:MM:SS" si hay NTP valido, "" si no
|
||||
uint32_t lastHeardMillis; // millis() -> ordenar / expirar (no se muestra)
|
||||
};
|
||||
|
||||
namespace MAP_Utils {
|
||||
|
||||
void upsert(const String& callsign, float latitude, float longitude, const String& symbol, int rssi, float snr);
|
||||
String getStationsJson();
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -68,8 +68,8 @@ ___________________________________________________________________*/
|
||||
#endif
|
||||
|
||||
|
||||
String versionDate = "2026-03-25";
|
||||
String versionNumber = "3.2.3";
|
||||
String versionDate = "2026-06-12";
|
||||
String versionNumber = "4.0.0";
|
||||
Configuration Config;
|
||||
WiFiClient aprsIsClient;
|
||||
WiFiClient mqttClient;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <APRSPacketLib.h>
|
||||
#include <APRSPacketLib.h>
|
||||
#include <WiFiClient.h>
|
||||
#include "configuration.h"
|
||||
#include "network_manager.h"
|
||||
@@ -47,7 +47,6 @@ extern bool modemLoggedToAPRSIS;
|
||||
extern bool backupDigiMode;
|
||||
extern String versionNumber;
|
||||
|
||||
uint32_t lastRxTime = millis();
|
||||
bool passcodeValid = false;
|
||||
uint32_t lastServerCheck = 0;
|
||||
|
||||
@@ -403,7 +402,6 @@ namespace APRS_IS_Utils {
|
||||
String aprsisPacket = aprsIsClient.readStringUntil('\r');
|
||||
aprsisPacket.trim(); // Serial.println(aprsisPacket);
|
||||
processAPRSISPacket(aprsisPacket);
|
||||
lastRxTime = millis();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -106,6 +106,7 @@ bool Configuration::writeFile() {
|
||||
data["lora"]["txCodingRate4"] = loramodule.txCodingRate4;
|
||||
data["lora"]["txSignalBandwidth"] = loramodule.txSignalBandwidth;
|
||||
data["lora"]["power"] = loramodule.power;
|
||||
data["lora"]["cadActive"] = loramodule.cadActive;
|
||||
|
||||
int rxSpreadingFactor = loramodule.rxSpreadingFactor;
|
||||
int txSpreadingFactor = loramodule.txSpreadingFactor;
|
||||
@@ -290,7 +291,6 @@ bool Configuration::readFile() {
|
||||
#endif
|
||||
digi.backupDigiMode = data["digi"]["backupDigiMode"] | false;
|
||||
|
||||
|
||||
if (data["lora"]["rxActive"].isNull() ||
|
||||
data["lora"]["rxFreq"].isNull() ||
|
||||
data["lora"]["rxSpreadingFactor"].isNull() ||
|
||||
@@ -301,7 +301,8 @@ bool Configuration::readFile() {
|
||||
data["lora"]["txSpreadingFactor"].isNull() ||
|
||||
data["lora"]["txCodingRate4"].isNull() ||
|
||||
data["lora"]["txSignalBandwidth"].isNull() ||
|
||||
data["lora"]["power"].isNull()) needsRewrite = true;
|
||||
data["lora"]["power"].isNull() ||
|
||||
data["lora"]["cadActive"].isNull()) needsRewrite = true;
|
||||
loramodule.rxActive = data["lora"]["rxActive"] | true;
|
||||
loramodule.rxFreq = data["lora"]["rxFreq"] | 433775000;
|
||||
loramodule.rxSpreadingFactor = data["lora"]["rxSpreadingFactor"] | 12;
|
||||
@@ -313,6 +314,7 @@ bool Configuration::readFile() {
|
||||
loramodule.txCodingRate4 = data["lora"]["txCodingRate4"] | 5;
|
||||
loramodule.txSignalBandwidth = data["lora"]["txSignalBandwidth"] | 125000;
|
||||
loramodule.power = data["lora"]["power"] | 20;
|
||||
loramodule.cadActive = data["lora"]["cadActive"] | true;
|
||||
|
||||
if (data["display"]["alwaysOn"].isNull() ||
|
||||
data["display"]["timeout"].isNull() ||
|
||||
@@ -503,6 +505,7 @@ void Configuration::setDefaultValues() {
|
||||
loramodule.txCodingRate4 = 5;
|
||||
loramodule.txSignalBandwidth = 125000;
|
||||
loramodule.power = 20;
|
||||
loramodule.cadActive = true;
|
||||
|
||||
display.alwaysOn = true;
|
||||
display.timeout = 4;
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include "digi_utils.h"
|
||||
#include "wifi_utils.h"
|
||||
#include "lora_utils.h"
|
||||
#include "gps_utils.h"
|
||||
#include "display.h"
|
||||
#include "utils.h"
|
||||
|
||||
|
||||
+13
-2
@@ -65,7 +65,12 @@
|
||||
#ifdef HELTEC_WSL_V3_DISPLAY
|
||||
Adafruit_SSD1306 display(128, 64, &Wire1, OLED_RST);
|
||||
#else
|
||||
Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST);
|
||||
#if defined RPC_LORA_DIGIGATE_1W
|
||||
#define SCREEN_HEIGHT 32
|
||||
#else
|
||||
#define SCREEN_HEIGHT 64
|
||||
#endif
|
||||
Adafruit_SSD1306 display(128, SCREEN_HEIGHT, &Wire, OLED_RST);
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
@@ -75,6 +80,7 @@
|
||||
extern Configuration Config;
|
||||
|
||||
bool displayFound = false;
|
||||
int maxLines;
|
||||
|
||||
void displaySetup() {
|
||||
#ifdef HAS_DISPLAY
|
||||
@@ -97,6 +103,11 @@ void displaySetup() {
|
||||
sprite.createSprite(160, 80);
|
||||
#endif
|
||||
#else
|
||||
#if (SCREEN_HEIGHT == 64)
|
||||
maxLines = 6;
|
||||
#elif (SCREEN_HEIGHT == 32)
|
||||
maxLines = 3;
|
||||
#endif
|
||||
#ifdef HAS_EPAPER
|
||||
display.landscape();
|
||||
display.printCenter("LoRa APRS iGate Initialising...");
|
||||
@@ -297,7 +308,7 @@ void displayShow(const String& header, const String& line1, const String& line2,
|
||||
display.setCursor(0, 0);
|
||||
display.println(header);
|
||||
display.setTextSize(1);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int i = 0; i < maxLines; i++) {
|
||||
display.setCursor(0, 16 + (8 * i));
|
||||
display.println(*lines[i]);
|
||||
}
|
||||
|
||||
+46
-4
@@ -16,6 +16,7 @@
|
||||
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <APRSPacketLib.h>
|
||||
#include <RadioLib.h>
|
||||
#include "configuration.h"
|
||||
#include "network_manager.h"
|
||||
@@ -23,6 +24,7 @@
|
||||
#include "station_utils.h"
|
||||
#include "board_pinout.h"
|
||||
#include "syslog_utils.h"
|
||||
#include "map_utils.h"
|
||||
#include "ntp_utils.h"
|
||||
#include "display.h"
|
||||
#include "utils.h"
|
||||
@@ -30,13 +32,15 @@
|
||||
|
||||
extern Configuration Config;
|
||||
extern NetworkManager *networkManager;
|
||||
extern uint32_t lastRxTime;
|
||||
extern bool packetIsBeacon;
|
||||
|
||||
extern std::vector<ReceivedPacket> receivedPackets;
|
||||
|
||||
bool operationDone = true;
|
||||
bool transmitFlag = true;
|
||||
bool operationDone = true;
|
||||
bool transmitFlag = true;
|
||||
|
||||
#define DIFS_SLOTS 2 // Number of secuential CAD slots to consider a free channel to Tx
|
||||
int backoffMax = 4; // Max Backoff value (number of CAD slots to wait before Tx)
|
||||
|
||||
#ifdef HAS_SX1262
|
||||
SX1262 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN);
|
||||
@@ -167,6 +171,34 @@ namespace LoRa_Utils {
|
||||
radio.setBandwidth(signalBandwidth);
|
||||
}
|
||||
|
||||
bool doCAD() { // CAD (Channel Activity Detection)
|
||||
return radio.scanChannel() != RADIOLIB_CHANNEL_FREE; // false=channel free | true=RADIOLIB_LORA_DETECTED or CAD failed
|
||||
}
|
||||
|
||||
bool doDIFS() {
|
||||
for (uint8_t i = DIFS_SLOTS; i > 0; i--) {
|
||||
if (doCAD()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void waitForDIFS() {
|
||||
while (!doDIFS()) {
|
||||
Serial.println("CAD/DIFS failed, retry...");
|
||||
}
|
||||
}
|
||||
|
||||
void doBEB() {
|
||||
int backoffCounter = random(1, backoffMax + 1);
|
||||
while (backoffCounter > 0) {
|
||||
if (doCAD()) {
|
||||
waitForDIFS(); // busy channel: freeze backoff and restart DIFS
|
||||
} else {
|
||||
backoffCounter--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sendNewPacket(const String& newPacket) {
|
||||
if (!Config.loramodule.txActive) return;
|
||||
|
||||
@@ -179,6 +211,12 @@ namespace LoRa_Utils {
|
||||
#ifdef INTERNAL_LED_PIN
|
||||
if (Config.digi.ecoMode != 1) digitalWrite(INTERNAL_LED_PIN, HIGH); // disabled in Ultra Eco Mode
|
||||
#endif
|
||||
|
||||
if (Config.loramodule.cadActive) {
|
||||
waitForDIFS(); // DIFS (Distributed Inter-Frame Space)
|
||||
doBEB(); // BEB (Binary Exponential Backoff)
|
||||
}
|
||||
|
||||
int state = radio.transmit("\x3c\xff\x01" + newPacket);
|
||||
transmitFlag = true;
|
||||
if (state == RADIOLIB_ERR_NONE) {
|
||||
@@ -242,6 +280,11 @@ namespace LoRa_Utils {
|
||||
receivedPacket.RSSI = rssi;
|
||||
receivedPacket.SNR = snr;
|
||||
receivedPackets.push_back(receivedPacket);
|
||||
|
||||
APRSPacket aprsPacket = APRSPacketLib::processReceivedPacket(packet.substring(3), rssi, snr, freqError);
|
||||
if (aprsPacket.type == 0 || aprsPacket.type == 4) { // 0 = GPS, 4 = Mic-E (los que traen posición)
|
||||
MAP_Utils::upsert(aprsPacket.sender, aprsPacket.latitude, aprsPacket.longitude, aprsPacket.overlay + aprsPacket.symbol, aprsPacket.rssi, aprsPacket.snr);
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.syslog.active && networkManager->isConnected()) {
|
||||
@@ -250,7 +293,6 @@ namespace LoRa_Utils {
|
||||
} else {
|
||||
packet = "";
|
||||
}
|
||||
lastRxTime = millis();
|
||||
return packet;
|
||||
}
|
||||
} else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include "map_utils.h"
|
||||
#include "ntp_utils.h"
|
||||
|
||||
#define MAX_MAP_STATIONS 50
|
||||
#define STATION_TTL_MS 3600000UL // 1 hora (en milisegundos)
|
||||
|
||||
|
||||
std::vector<MapStation> mapStations;
|
||||
|
||||
|
||||
namespace MAP_Utils {
|
||||
|
||||
// Copia la hora NTP "HH:MM:SS" si es valida; si no hay red (NTP devuelve
|
||||
// un texto de fallback) deja el campo vacio para no guardar hora falsa.
|
||||
void writeFormatedTime(char* dst, size_t size) {
|
||||
String t = NTP_Utils::getFormatedTime();
|
||||
if (t.length() == 8 && t.charAt(2) == ':') {
|
||||
t.toCharArray(dst, size);
|
||||
} else {
|
||||
dst[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
// Inserta una estacion nueva o actualiza la existente (dedup por callsign).
|
||||
// Llamar solo cuando el parser ya decodifico una posicion valida.
|
||||
void upsert(const String& callsign, float latitude, float longitude, const String& symbol, int rssi, float snr) {
|
||||
for (auto &s : mapStations) { // 1) ya existe -> actualizar
|
||||
if (callsign.equals(s.callsign)) {
|
||||
s.latitude = latitude;
|
||||
s.longitude = longitude;
|
||||
s.rssi = rssi;
|
||||
s.snr = snr;
|
||||
s.count++;
|
||||
symbol.toCharArray(s.symbol, sizeof(s.symbol));
|
||||
writeFormatedTime(s.lastHeard, sizeof(s.lastHeard));
|
||||
s.lastHeardMillis = millis();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mapStations.size() >= MAX_MAP_STATIONS) { // 2) llena -> descartar la mas vieja
|
||||
size_t oldest = 0;
|
||||
for (size_t i = 1; i < mapStations.size(); i++) {
|
||||
if (mapStations[i].lastHeardMillis < mapStations[oldest].lastHeardMillis) {
|
||||
oldest = i;
|
||||
}
|
||||
}
|
||||
mapStations.erase(mapStations.begin() + oldest);
|
||||
}
|
||||
|
||||
MapStation st = {}; // 3) insertar nueva
|
||||
callsign.toCharArray(st.callsign, sizeof(st.callsign));
|
||||
symbol.toCharArray(st.symbol, sizeof(st.symbol));
|
||||
st.latitude = latitude;
|
||||
st.longitude = longitude;
|
||||
st.rssi = rssi;
|
||||
st.snr = snr;
|
||||
st.count = 1;
|
||||
writeFormatedTime(st.lastHeard, sizeof(st.lastHeard));
|
||||
st.lastHeardMillis = millis();
|
||||
mapStations.push_back(st);
|
||||
}
|
||||
|
||||
void purgeOldStations() {
|
||||
uint32_t now = millis();
|
||||
for (int i = (int)mapStations.size() - 1; i >= 0; i--) {
|
||||
if (now - mapStations[i].lastHeardMillis > STATION_TTL_MS) {
|
||||
mapStations.erase(mapStations.begin() + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String getStationsJson() {
|
||||
purgeOldStations();
|
||||
JsonDocument data;
|
||||
|
||||
for (size_t i = 0; i < mapStations.size(); i++) {
|
||||
data[i]["callsign"] = mapStations[i].callsign;
|
||||
data[i]["lat"] = mapStations[i].latitude;
|
||||
data[i]["lon"] = mapStations[i].longitude;
|
||||
data[i]["symbol"] = mapStations[i].symbol;
|
||||
data[i]["RSSI"] = mapStations[i].rssi;
|
||||
data[i]["SNR"] = mapStations[i].snr;
|
||||
data[i]["count"] = mapStations[i].count;
|
||||
data[i]["lastHeard"] = mapStations[i].lastHeard;
|
||||
}
|
||||
|
||||
String buffer;
|
||||
|
||||
serializeJson(data, buffer);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -74,7 +74,7 @@ namespace OTA_Utils {
|
||||
Serial.println(success ? "OTA update finished successfully!" : "There was an error during OTA update!");
|
||||
displayShow("", "", statusMessage, "", rebootMessage, "", "", 4000);
|
||||
|
||||
isUpdatingOTA = false;
|
||||
if (!success) isUpdatingOTA = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,15 +25,11 @@
|
||||
#include "utils.h"
|
||||
#include <vector>
|
||||
|
||||
#define SECS_TO_WAIT 3 // soon to be deleted...
|
||||
|
||||
|
||||
extern Configuration Config;
|
||||
extern uint32_t lastRxTime;
|
||||
extern String fourthLine;
|
||||
extern bool shouldSleepLowVoltage;
|
||||
|
||||
uint32_t lastTxTime = millis();
|
||||
std::vector<LastHeardStation> lastHeardStations;
|
||||
std::vector<String> blacklist;
|
||||
std::vector<String> managers;
|
||||
@@ -214,15 +210,11 @@ namespace STATION_Utils {
|
||||
}
|
||||
|
||||
void processOutputPacketBuffer() {
|
||||
int timeToWait = SECS_TO_WAIT * 1000; // 3 segs between packet Tx and also Rx ???
|
||||
uint32_t lastRx = millis() - lastRxTime;
|
||||
uint32_t lastTx = millis() - lastTxTime;
|
||||
if (outputPacketBuffer.size() > 0 && lastTx > timeToWait && lastRx > timeToWait) {
|
||||
if (outputPacketBuffer.size() > 0) {
|
||||
if (outputPacketBuffer[0].isBeacon) packetIsBeacon = true;
|
||||
LoRa_Utils::sendNewPacket(outputPacketBuffer[0].packet);
|
||||
if (outputPacketBuffer[0].isBeacon) packetIsBeacon = false;
|
||||
outputPacketBuffer.erase(outputPacketBuffer.begin());
|
||||
lastTxTime = millis();
|
||||
}
|
||||
if (shouldSleepLowVoltage) {
|
||||
while (outputPacketBuffer.size() > 0) {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "configuration.h"
|
||||
#include "ota_utils.h"
|
||||
#include "web_utils.h"
|
||||
#include "map_utils.h"
|
||||
#include "display.h"
|
||||
#include "utils.h"
|
||||
|
||||
@@ -114,6 +115,10 @@ namespace WEB_Utils {
|
||||
request->send(200, "application/json", buffer);
|
||||
}
|
||||
|
||||
void handleStations(AsyncWebServerRequest *request) {
|
||||
request->send(200, "application/json", MAP_Utils::getStationsJson());
|
||||
}
|
||||
|
||||
void handleWriteConfiguration(AsyncWebServerRequest *request) {
|
||||
Serial.println("Got new Configuration Data from www");
|
||||
|
||||
@@ -213,6 +218,8 @@ namespace WEB_Utils {
|
||||
Config.loramodule.txCodingRate4 = getParamIntSafe("lora.txCodingRate4", Config.loramodule.txCodingRate4);
|
||||
Config.loramodule.txSignalBandwidth = getParamIntSafe("lora.txSignalBandwidth", Config.loramodule.txSignalBandwidth);
|
||||
Config.loramodule.power = getParamIntSafe("lora.power", Config.loramodule.power);
|
||||
if (Config.loramodule.txActive)
|
||||
Config.loramodule.cadActive = request->hasParam("lora.cadActive", true);
|
||||
|
||||
Config.display.alwaysOn = request->hasParam("display.alwaysOn", true);
|
||||
if (!Config.display.alwaysOn) {
|
||||
@@ -361,6 +368,7 @@ namespace WEB_Utils {
|
||||
server.on("/", HTTP_GET, handleHome);
|
||||
server.on("/status", HTTP_GET, handleStatus);
|
||||
server.on("/received-packets.json", HTTP_GET, handleReceivedPackets);
|
||||
server.on("/stations.json", HTTP_GET, handleStations);
|
||||
server.on("/configuration.json", HTTP_GET, handleReadConfiguration);
|
||||
server.on("/configuration.json", HTTP_POST, handleWriteConfiguration);
|
||||
server.on("/action", HTTP_POST, handleAction);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BOARD_PINOUT_H_
|
||||
#define BOARD_PINOUT_H_
|
||||
|
||||
// LoRa Radio
|
||||
#define HAS_SX1268
|
||||
#define HAS_1W_LORA
|
||||
#define HAS_TCXO
|
||||
#define RADIO_SCLK_PIN 18
|
||||
#define RADIO_MISO_PIN 19
|
||||
#define RADIO_MOSI_PIN 23
|
||||
#define RADIO_CS_PIN 5
|
||||
#define RADIO_RST_PIN 27
|
||||
#define RADIO_DIO1_PIN 12
|
||||
#define RADIO_BUSY_PIN 14
|
||||
#define RADIO_RXEN 32
|
||||
#define RADIO_TXEN 25
|
||||
#define RADIO_WAKEUP_PIN RADIO_DIO1_PIN
|
||||
#define GPIO_WAKEUP_PIN GPIO_SEL_12
|
||||
|
||||
// I2C
|
||||
#define USE_WIRE_WITH_OLED_PINS
|
||||
|
||||
// Display
|
||||
#define HAS_DISPLAY
|
||||
|
||||
#undef OLED_SDA
|
||||
#undef OLED_SCL
|
||||
#undef OLED_RST
|
||||
|
||||
#define OLED_SDA 21
|
||||
#define OLED_SCL 22
|
||||
#define OLED_RST -1 // Reset pin # (or -1 if sharing Arduino reset pin)
|
||||
|
||||
// Aditional Config
|
||||
#define INTERNAL_LED_PIN 2
|
||||
#define BATTERY_PIN 35
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
[env:RPC_LORA_DIGIGATE_1W]
|
||||
board = esp32dev
|
||||
build_flags =
|
||||
${common.build_flags}
|
||||
-D RADIOLIB_EXCLUDE_LR11X0=1
|
||||
-D RADIOLIB_EXCLUDE_SX127X=1
|
||||
-D RADIOLIB_EXCLUDE_SX128X=1
|
||||
-D RPC_LORA_DIGIGATE_1W
|
||||
lib_deps =
|
||||
${common.lib_deps}
|
||||
${common.display_libs}
|
||||
Reference in New Issue
Block a user