mirror of
https://github.com/richonguzman/LoRa_APRS_iGate.git
synced 2026-07-04 17:01:29 +02:00
primer test solo igate
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
<title>LoRa iGate & Digi software − Ricardo Guzman CA2RXU</title>
|
||||
<link rel="stylesheet" href="/bootstrap.css" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
<link rel="icon" href="/favicon.png" type="image/x-icon">
|
||||
<script>(function(){try{var t=localStorage.getItem("igateTheme");if(!t)t=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";var r=document.documentElement;if(t==="night"){r.setAttribute("data-bs-theme","dark");r.setAttribute("data-variant","night");}else{r.setAttribute("data-bs-theme",t);}}catch(e){}})();</script>
|
||||
<style>
|
||||
@@ -95,6 +96,7 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
|
||||
d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1"
|
||||
/>
|
||||
</svg></span><span class="label">Station</span></button>
|
||||
<button class="side-link" type="button" data-target="sec-maps"><span class="side-ico"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.5.5 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103M10 1.91l-4-.8v12.98l4 .8zm1 12.98 4-.8V1.11l-4 .8zm-6-.8V1.11l-4 .8v12.98z"/></svg></span><span class="label">Maps</span></button>
|
||||
<button class="side-link" type="button" data-target="sec-beaconing"><span class="side-ico"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M15.964.686a.5.5 0 0 0-.65-.65L.767 5.855H.766l-.452.18a.5.5 0 0 0-.082.887l.41.26.001.002 4.995 3.178 3.178 4.995.002.002.26.41a.5.5 0 0 0 .886-.083zm-1.833 1.89L6.637 10.07l-.215-.338a.5.5 0 0 0-.154-.154l-.338-.215 7.494-7.494 1.178-.471z"/></svg></span><span class="label">Beaconing</span></button>
|
||||
<button class="side-link" type="button" data-target="sec-connectivity"><span class="side-ico"><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -211,6 +213,15 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
|
||||
<hr>
|
||||
</div>
|
||||
<div id="configuration">
|
||||
<section class="panel-section" id="sec-maps">
|
||||
<div class="row my-5">
|
||||
<div class="col-12">
|
||||
<h3>Stations Map</h3>
|
||||
<div id="map" style="height:70vh;border-radius:14px;"></div>
|
||||
<small>Needs internet (OpenStreetMap tiles). Updates every 15 seconds.</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel-section active" id="sec-station">
|
||||
|
||||
<div class="row my-5 d-flex align-items-top">
|
||||
@@ -2371,6 +2382,7 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
|
||||
</div>
|
||||
</body>
|
||||
<script src="/bootstrap.js"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script src="/script.js"></script>
|
||||
</html>
|
||||
<script>
|
||||
@@ -2391,6 +2403,7 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
|
||||
document.querySelectorAll(".panel-section").forEach(function(s){s.classList.remove("active");});
|
||||
b.classList.add("active");var el=document.getElementById(b.dataset.target);if(el)el.classList.add("active");
|
||||
showConfig();
|
||||
if(b.dataset.target==="sec-maps"&&window.showMap)window.showMap();
|
||||
});
|
||||
});
|
||||
var rpBtn=document.getElementById("rpToggle");
|
||||
|
||||
+110
-1
@@ -640,4 +640,113 @@ document.querySelector('a[href="/received-packets"]').addEventListener('click',
|
||||
document.querySelector('button[type=submit]').remove();
|
||||
|
||||
fetchReceivedPackets();
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
/* ---------- Stations Map (Leaflet, CDN) ---------- */
|
||||
|
||||
let mapInstance = null;
|
||||
let mapMarkers = null;
|
||||
let mapTimer = null;
|
||||
let mapTileErrShown = 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();
|
||||
|
||||
(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.marker([s.lat, s.lon]).bindPopup(popup).addTo(mapMarkers);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/* 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>
|
||||
|
||||
#define MAX_MAP_STATIONS 50
|
||||
|
||||
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, int16_t rssi, float snr);
|
||||
String getStationsJson();
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,101 @@
|
||||
/* 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"
|
||||
|
||||
|
||||
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, int16_t 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);
|
||||
}
|
||||
|
||||
String getStationsJson() {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -363,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);
|
||||
|
||||
Reference in New Issue
Block a user