Compare commits

...
Author SHA1 Message Date
Ricardo Guzman 463dd6342b Merge branch 'pr-458' into TestPRJohnRokicki 2026-09-07 17:31:25 -03:00
John Rokicki 505f0d6f41 renames as requested 2026-09-07 16:02:59 -04:00
John Rokicki 01befdef92 fix mobile device formatting 2026-09-07 15:13:11 -04:00
Ricardo Guzman ee65f953d6 date update 2026-09-07 10:54:24 -03:00
Ricardo Guzman c952ea0159 Merge pull request #456 from cwb/fix-oled-screen-height
display: fix SCREEN_HEIGHT definition and maxLines for SH1106 OLED
2026-09-07 10:52:20 -03:00
Ricardo Guzman 21a42ef157 update de iconos 2026-09-07 10:46:39 -03:00
Ricardo Guzman 53ee07a7c1 heard direct and repeated 2026-08-27 22:45:08 -04:00
John Rokicki f14b6b3cd9 Station status support 2026-08-27 21:28:34 -04:00
Ricardo Guzman 12a0f5bed1 update a 24px symbol 2026-08-25 20:20:09 -04:00
Ricardo Guzman 27f34c5f67 48px 2026-08-25 20:04:12 -04:00
Craig 08bfb013ce display: fix SCREEN_HEIGHT definition and maxLines for SH1106 OLED 2026-08-25 17:03:01 -07:00
Ricardo Guzman d573c66fd7 casi 2026-08-25 19:50:37 -04:00
Ricardo Guzman e0dc2f81ee ready for testing 2026-08-24 23:18:25 -04:00
Ricardo Guzman 6e4dc3c301 update needed for t-decks 2026-08-16 12:28:40 -04:00
Ricardo Guzman d2e42c8571 readme update2 2026-08-16 12:01:31 -04:00
Ricardo Guzman 6b321bc1e0 images update 2026-08-16 11:51:27 -04:00
Ricardo Guzman f7c9201830 now LEAFET is local flash 2026-07-14 00:33:54 -04:00
Ricardo Guzman 1a042b7d0e blockeando leafet 1 2026-07-14 00:17:56 -04:00
Ricardo Guzman aaa44200b5 test query callsign fix 2026-07-06 13:09:19 -04:00
Ricardo Guzman c17ad32650 2026 update in copywrite info 2026-06-29 13:06:58 -04:00
Ricardo Guzman (Richonguzman) 26b5df1bd7 update delete of star in digipeated packet 2026-06-22 22:16:14 -04:00
Ricardo Guzman (Richonguzman) 0c3555bdbe new digimode added 2026-06-18 13:58:10 -04:00
Ricardo Guzman (Richonguzman) cf858ffad2 digi patch 2 2026-06-16 11:29:32 -04:00
Ricardo Guzman (Richonguzman) cbac8b0365 digi patch 1 2026-06-16 10:41:44 -04:00
Ricardo Guzman (Richonguzman) b76aed4d57 cambio digi modes de 2 y 3 a 1 y 2 para agregar patch 2026-06-16 10:25:22 -04:00
120 changed files with 2482 additions and 205 deletions

No files matched your search

+2 -1
View File
@@ -5,6 +5,7 @@
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
"ms-vscode.cpptools-extension-pack",
"pioarduino.pioarduino-ide"
]
}
+15 -3
View File
@@ -16,23 +16,32 @@ ____________________________________________________
____________________________________________________
## New Web/UI
| New Menu | Map |
| --- | --- |
| ![new menu](images/1.png) | ![map](images/2.png) |
____________________________________________________
## You can support this project to continue to grow:
[<img src="https://github.com/richonguzman/LoRa_APRS_Tracker/raw/main/images/github-sponsors.png">](https://github.com/sponsors/richonguzman) [<img src="https://github.com/richonguzman/LoRa_APRS_Tracker/raw/main/images/paypalme.png">](http://paypal.me/richonguzman)
____________________________________________________
## SUPPORTED BOARDS (<a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/Supported-Boards-and-Buying-Links" target="_blank">Buying links</a>).
(NOTE: all boards with 433-868-915 MHz versions)
- TTGO Lilygo LoRa32 T3S3 V1.2 and LoRa32 V2.1 (V1.6 is the same).
- TTGO Lilygo LoRa32 LoRa32 V2.1 (V1.6 is the same) and T3S3 V1.2.
- TTGO T-Beam V1.0 , V1.1, V1.2 (also variations with SX1262 and SX1268 LoRa Modules) and Supreme V3.
- T-Deck Plus (and also regular T-Deck with/without GPS).
- HELTEC V2, V3, V3.2, T114, Wireless Stick, Wireless Stick Lite V3/V3.2, HT-CT62, Wireless Tracker, Wireless Paper.
- HELTEC V2, V3, V3.2, V4, T114, Wireless Stick, Wireless Stick Lite V3/V3.2, HT-CT62, Wireless Tracker, Wireless Paper.
- RAK Wireless 4631 + 19007(or 19003)
@@ -48,10 +57,13 @@ ____________________________________________________
- Wemos Lolin32 Oled + SX1278 DIY Version.
- and many more...
<br />
# Timeline (Versions):
- 2026-06-10 CAD (Channel Activity Detection) and DIFS (Distributed Inter-Frame Space) added.
- 2026-08-16 New UI (Web) and also Live View over a Map to see Rx stations.
- 2026-08-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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+524 -15
View File
@@ -7,7 +7,7 @@
<title>LoRa iGate & Digi software &minus; 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="stylesheet" href="/leaflet.css" />
<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,7 +95,7 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
fill-rule="evenodd"
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>
</svg></span><span class="label">General</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"
@@ -186,6 +186,8 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
<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">Map</span></button>
<button class="side-link" type="button" id="rpToggle"><span class="side-ico"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg></span><span class="label">Received Packets</span></button>
<a href="/received-packets" id="rpDummy" style="display:none" aria-hidden="true"></a>
<button class="side-link" type="button" id="ssToggle"><span class="side-ico"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964 4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2"/></svg></span><span class="label">Station</span></button>
<a href="/station-status" id="ssDummy" style="display:none" aria-hidden="true"></a>
<a class="side-link side-ota" href="/update"><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="M8 2a5.53 5.53 0 0 0-3.594 1.342c-.766.66-1.321 1.52-1.464 2.383C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13h8.906C14.502 13 16 11.57 16 9.773c0-1.636-1.242-2.969-2.834-3.194C12.923 3.999 10.69 2 8 2m2.354 5.146a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708l2-2a.5.5 0 0 1 .708 0z"/></svg></span><span class="label">Update OTA</span></a>
</aside>
<main class="app-main">
@@ -212,6 +214,503 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
</div>
<hr>
</div>
<div class="container d-none" id="station-status">
<div class="row my-5 d-flex align-items-top">
<div class="col-12">
<h3>Current station status</h3>
<div id="stationStatus.uptime">
<div class="row my-5 d-flex align-items-top">
<div class="col-lg-3 col-sm-12">
<h5>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
class="bi bi-heart-pulse-fill"
viewBox="0 0 16 16"
>
<path
d="M1.475 9C2.702 10.84 4.779 12.871 8 15c3.221-2.129 5.298-4.16 6.525-6H12a.5.5 0 0 1-.464-.314l-1.457-3.642-1.598 5.593a.5.5 0 0 1-.945.049L5.889 6.568l-1.473 2.21A.5.5 0 0 1 4 9z"
/>
<path
d="M.88 8C-2.427 1.68 4.41-2 7.823 1.143q.09.083.176.171a3 3 0 0 1 .176-.17C11.59-2 18.426 1.68 15.12 8h-2.783l-1.874-4.686a.5.5 0 0 0-.945.049L7.921 8.956 6.464 5.314a.5.5 0 0 0-.88-.091L3.732 8z"
/>
</svg>
Up-time
</h5>
<small>Current device up-time</small>
</div>
<div class="col-lg-9 col-sm-12">
<div class="row mt-2">
<div class="col-6 mt-1">
<label for="stationStatus.elapsedTime" class="form-label"
>Elapsed time</label
>
<div class="input-group">
<input
type="text"
name="stationStatus.elapsedTime"
id="stationStatus.elapsedTime"
placeholder="000d 00h 00m 00s"
class="form-control"
readonly
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="stationStatus.voltage">
<div class="row my-5 d-flex align-items-top">
<div class="col-lg-3 col-sm-12">
<h5>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
class="bi bi-battery-fill"
viewBox="0 0 16 16">
<path d="M2 4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H2zm1 2h8v4H3V6z"/>
<path d="M15 6.5a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1V6.5z"/>
<path d="M4 7h6v2H4V7z"/>
</svg>
Battery
</h5>
<small>Current Battery status</small>
</div>
<div class="col-lg-9 col-sm-12">
<div class="row mt-2">
<div class="col-6 mt-1">
<label
for="stationStatus.voltageInternal"
class="form-label">
Internal Voltage
</label>
<div class="input-group">
<input
type="text"
name="stationStatus.voltageInternal"
id="stationStatus.voltageInternal"
placeholder="0.00"
class="form-control"
readonly
/>
<span class="input-group-text"
>Volts
</span>
</div>
</div>
<div class="col-6 mt-1">
<label for="stationStatus.voltageExternal" class="form-label"
>External voltage</label
>
<div class="input-group">
<input
type="text"
name="stationStatus.voltageExternal"
id="stationStatus.voltageExternal"
placeholder="0.00"
class="form-control"
readonly
/>
<span class="input-group-text"
>Volts</span
>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="stationStatus.connectivity">
<div class="row my-5 d-flex align-items-top">
<div class="col-lg-3 col-sm-12">
<h5>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
class="bi bi-router-fill"
viewBox="0 0 16 16"
>
<path
d="M5.525 3.025a3.5 3.5 0 0 1 4.95 0 .5.5 0 1 0 .707-.707 4.5 4.5 0 0 0-6.364 0 .5.5 0 0 0 .707.707"
/>
<path
d="M6.94 4.44a1.5 1.5 0 0 1 2.12 0 .5.5 0 0 0 .708-.708 2.5 2.5 0 0 0-3.536 0 .5.5 0 0 0 .707.707Z"
/>
<path
d="M2.974 2.342a.5.5 0 1 0-.948.316L3.806 8H1.5A1.5 1.5 0 0 0 0 9.5v2A1.5 1.5 0 0 0 1.5 13H2a.5.5 0 0 0 .5.5h2A.5.5 0 0 0 5 13h6a.5.5 0 0 0 .5.5h2a.5.5 0 0 0 .5-.5h.5a1.5 1.5 0 0 0 1.5-1.5v-2A1.5 1.5 0 0 0 14.5 8h-2.306l1.78-5.342a.5.5 0 1 0-.948-.316L11.14 8H4.86zM2.5 11a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1m4.5-.5a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0m2.5.5a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1m1.5-.5a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0m2 0a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0"
/>
<path
d="M8.5 5.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0"
/>
</svg>
Connectivity
</h5>
<small>Current device connectivity and service status</small>
</div>
<div class="col-9">
<div class="row mt-2">
<div class="col-6">
<label for="stationStatus.statusWifi" class="form-label"
>WiFi status</label
>
<div class="input-group">
<input
name="stationStatus.statusWifi"
id="stationStatus.statusWifi"
class="form-control"
readonly
/>
</div>
</div>
<div class="col-6">
<label for="stationStatus.statusIS" class="form-label"
>IS status</label
>
<div class="input-group">
<input
name="stationStatus.statusIS"
id="stationStatus.statusIS"
class="form-control"
readonly
/>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label for="stationStatus.statusNTP" class="form-label"
>NTP status</label
>
<div class="input-group">
<input
name="stationStatus.statusNTP"
id="stationStatus.statusNTP"
class="form-control"
readonly
/>
</div>
</div>
<div class="col-6">
<label for="stationStatus.statusSyslog" class="form-label"
>Syslog status</label
>
<div class="input-group">
<input
name="stationStatus.statusSyslog"
id="stationStatus.statusSyslog"
class="form-control"
readonly
/>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label for="stationStatus.statusMQTT" class="form-label"
>MQTT status</label
>
<div class="input-group">
<input
name="stationStatus.statusMQTT"
id="stationStatus.statusMQTT"
class="form-control"
readonly
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="stationStatus.traffic">
<div class="row my-5 d-flex align-items-top">
<div class="col-lg-3 col-sm-12">
<h5>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
class="bi bi-broadcast"
viewBox="0 0 16 16"
>
<path
d="M3.05 3.05a7 7 0 0 0 0 9.9.5.5 0 0 1-.707.707 8 8 0 0 1 0-11.314.5.5 0 0 1 .707.707m2.122 2.122a4 4 0 0 0 0 5.656.5.5 0 1 1-.708.708 5 5 0 0 1 0-7.072.5.5 0 0 1 .708.708m5.656-.708a.5.5 0 0 1 .708 0 5 5 0 0 1 0 7.072.5.5 0 1 1-.708-.708 4 4 0 0 0 0-5.656.5.5 0 0 1 0-.708m2.122-2.12a.5.5 0 0 1 .707 0 8 8 0 0 1 0 11.313.5.5 0 0 1-.707-.707 7 7 0 0 0 0-9.9.5.5 0 0 1 0-.707zM10 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0"
/>
</svg>
Traffic
</h5>
<small>Current packet traffic status. Resets every 24 hours.</small>
</div>
<div class="col-9">
<div class="row mt-2">
<div class="col-6">
<label for="stationStatus.packetsRFSendSuccess" class="form-label"
>RF send success</label
>
<div class="input-group">
<input
name="stationStatus.packetsRFSendSuccess"
id="stationStatus.packetsRFSendSuccess"
class="form-control"
readonly
/>
</div>
</div>
<div class="col-6">
<label for="stationStatus.packetsRFSendFail" class="form-label"
>RF send failures</label
>
<div class="input-group">
<input
name="stationStatus.packetsRFSendFail"
id="stationStatus.packetsRFSendFail"
class="form-control"
readonly
/>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label for="stationStatus.packetsRFReceiveSuccess" class="form-label"
>RF receive success</label
>
<div class="input-group">
<input
name="stationStatus.packetsRFReceiveSuccess"
id="stationStatus.packetsRFReceiveSuccess"
class="form-control"
readonly
/>
</div>
</div>
<div class="col-6">
<label for="stationStatus.packetsRFReceiveFail" class="form-label"
>RF receive failures</label
>
<div class="input-group">
<input
name="stationStatus.packetsRFReceiveFail"
id="stationStatus.packetsRFReceiveFail"
class="form-control"
readonly
/>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label for="stationStatus.packetsISSendSuccess" class="form-label"
>IS send success</label
>
<div class="input-group">
<input
name="stationStatus.packetsISSendSuccess"
id="stationStatus.packetsISSendSuccess"
class="form-control"
readonly
/>
</div>
</div>
<div class="col-6">
<label for="stationStatus.packetsISSendFail" class="form-label"
>IS send failures</label
>
<div class="input-group">
<input
name="stationStatus.packetsISSendFail"
id="stationStatus.packetsISSendFail"
class="form-control"
readonly
/>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label for="stationStatus.packetsISReceiveSuccess" class="form-label"
>IS receive success</label
>
<div class="input-group">
<input
name="stationStatus.packetsISReceiveSuccess"
id="stationStatus.packetsISReceiveSuccess"
class="form-control"
readonly
/>
</div>
</div>
<div class="col-6">
<label for="stationStatus.packetsISReceiveFail" class="form-label"
>IS receive failures</label
>
<div class="input-group">
<input
name="stationStatus.packetsISReceiveFail"
id="stationStatus.packetsISReceiveFail"
class="form-control"
readonly
/>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label for="stationStatus.packetsBlacklisted" class="form-label"
>Blacklisted packets</label
>
<div class="input-group">
<input
name="stationStatus.packetsBlacklisted"
id="stationStatus.packetsBlacklisted"
class="form-control"
readonly
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="stationStatus.login">
<div class="row my-5 d-flex align-items-top">
<div class="col-lg-3 col-sm-12">
<h5>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16"><path d="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1 1H6.663a3.5 3.5 0 0 1-3.163 2M2.5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>
Login Events
</h5>
<small>Current login statistics</small>
</div>
<div class="col-9">
<div class="row mt-2">
<div class="col-6">
<label for="stationStatus.loginsInvalid" class="form-label"
>Invalid logins</label
>
<div class="input-group">
<input
name="stationStatus.loginsInvalid"
id="stationStatus.loginsInvalid"
class="form-control"
readonly
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="stationStatus.weather">
<div class="row my-5 d-flex align-items-top">
<div class="col-lg-3 col-sm-12">
<h5>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
class="bi bi-broadcast-pin"
viewBox="0 0 16 16"
>
<path
d="M3.05 3.05a7 7 0 0 0 0 9.9.5.5 0 0 1-.707.707 8 8 0 0 1 0-11.314.5.5 0 0 1 .707.707m2.122 2.122a4 4 0 0 0 0 5.656.5.5 0 1 1-.708.708 5 5 0 0 1 0-7.072.5.5 0 0 1 .708.708m5.656-.708a.5.5 0 0 1 .708 0 5 5 0 0 1 0 7.072.5.5 0 1 1-.708-.708 4 4 0 0 0 0-5.656.5.5 0 0 1 0-.708m2.122-2.12a.5.5 0 0 1 .707 0 8 8 0 0 1 0 11.313.5.5 0 0 1-.707-.707 7 7 0 0 0 0-9.9.5.5 0 0 1 0-.707zM6 8a2 2 0 1 1 2.5 1.937V15.5a.5.5 0 0 1-1 0V9.937A2 2 0 0 1 6 8"
/>
</svg>
Weather
</h5>
<small>Current Weather status</small>
</div>
<div class="col-9">
<div class="row mt-2">
<div class="col-6">
<label for="stationStatus.weatherTemperature" class="form-label"
>Temperature</label
>
<div class="input-group">
<input
name="stationStatus.weatherTemperature"
id="stationStatus.weatherTemperature"
class="form-control"
readonly
/>
<span class="input-group-text"
>C°</span
>
</div>
</div>
<div class="col-6">
<label for="stationStatus.weatherHumidity" class="form-label"
>Humidity</label
>
<div class="input-group">
<input
name="stationStatus.weatherHumidity"
id="stationStatus.weatherHumidity"
class="form-control"
readonly
/>
<span class="input-group-text"
>percent</span
>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label for="stationStatus.weatherBarometricPressure" class="form-label"
>Barometric Pressure</label
>
<div class="input-group">
<input
name="stationStatus.weatherBarometricPressure"
id="stationStatus.weatherBarometricPressure"
class="form-control"
readonly
/>
<span class="input-group-text"
>Mb</span
>
</div>
</div>
<div class="col-6">
<label for="stationStatus.weatherGas" class="form-label"
>Gas</label
>
<div class="input-group">
<input
name="stationStatus.weatherGas"
id="stationStatus.weatherGas"
class="form-control"
readonly
/>
<span class="input-group-text"
>Kohms</span
>
</div>
</div>
</div>
</div>
</div>
</div>
<span>Status updates automatically every 15 seconds.</span><br>
</div>
</div>
<hr>
</div>
<div id="configuration">
<section class="panel-section" id="sec-maps">
<div class="row my-5">
@@ -1007,10 +1506,14 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
id="digi.mode"
>
<option value="0" selected>OFF</option>
<option value="2">WIDE1 (fill-in) Digi</option>
<option value="3">WIDE2 (+WIDE1) Digi</option>
<option value="1">WIDE1 (fill-in) Digi</option>
<option value="2">WIDE2 (+WIDE1) Digi</option>
<option value="3">Own Callsign Path Digi</option>
</select>
</div>
<div class="col-12 mt-2">
+ <small class="text-muted">Mode 4 digipeats packets whose path already contains your callsign, preserving the path and appending a * to your callsign.</small>
+ </div>
<div class="col-12 mt-3">
<label
for="digi.ecoMode"
@@ -1819,15 +2322,11 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
width="20"
height="20"
fill="currentColor"
class="bi bi-heart-pulse-fill"
viewBox="0 0 16 16"
>
<path
d="M1.475 9C2.702 10.84 4.779 12.871 8 15c3.221-2.129 5.298-4.16 6.525-6H12a.5.5 0 0 1-.464-.314l-1.457-3.642-1.598 5.593a.5.5 0 0 1-.945.049L5.889 6.568l-1.473 2.21A.5.5 0 0 1 4 9z"
/>
<path
d="M.88 8C-2.427 1.68 4.41-2 7.823 1.143q.09.083.176.171a3 3 0 0 1 .176-.17C11.59-2 18.426 1.68 15.12 8h-2.783l-1.874-4.686a.5.5 0 0 0-.945.049L7.921 8.956 6.464 5.314a.5.5 0 0 0-.88-.091L3.732 8z"
/>
class="bi bi-battery-fill"
viewBox="0 0 16 16">
<path d="M2 4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H2zm1 2h8v4H3V6z"/>
<path d="M15 6.5a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1V6.5z"/>
<path d="M4 7h6v2H4V7z"/>
</svg>
Battery
</h5>
@@ -2382,7 +2881,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="/leaflet.js"></script>
<script src="/script.js"></script>
</html>
<script>
@@ -2396,7 +2895,7 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
function paint(name){var r=document.documentElement;if(name==="night"){r.setAttribute("data-bs-theme","dark");r.setAttribute("data-variant","night");}else{r.setAttribute("data-bs-theme",name);r.removeAttribute("data-variant");}if(btn)btn.innerHTML=ICONS[name]||MOON;}
paint(logical());
if(btn)btn.addEventListener("click",function(){var c=logical();var n=ORDER[(ORDER.indexOf(c)+1)%ORDER.length];try{localStorage.setItem("igateTheme",n);}catch(e){}paint(n);});
function showConfig(){document.getElementById("configuration").classList.remove("d-none");var rp=document.getElementById("received-packets");if(rp)rp.classList.add("d-none");}
function showConfig(){document.getElementById("configuration").classList.remove("d-none");var rp=document.getElementById("received-packets");if(rp)rp.classList.add("d-none");var ss=document.getElementById("station-status");if(ss)ss.classList.add("d-none");}
document.querySelectorAll(".side-link[data-target]").forEach(function(b){
b.addEventListener("click",function(){
document.querySelectorAll(".side-link").forEach(function(x){x.classList.remove("active");});
@@ -2411,9 +2910,19 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
document.querySelectorAll(".side-link").forEach(function(x){x.classList.remove("active");});
rpBtn.classList.add("active");
document.getElementById("configuration").classList.add("d-none");
document.getElementById("station-status").classList.add("d-none");
var rp=document.getElementById("received-packets");if(rp)rp.classList.remove("d-none");
if(window.fetchReceivedPackets)window.fetchReceivedPackets();
});
var ssBtn=document.getElementById("ssToggle");
if(ssBtn)ssBtn.addEventListener("click",function(){
document.querySelectorAll(".side-link").forEach(function(x){x.classList.remove("active");});
ssBtn.classList.add("active");
document.getElementById("configuration").classList.add("d-none");
document.getElementById("received-packets").classList.add("d-none");
var ss=document.getElementById("station-status");if(ss)ss.classList.remove("d-none");
if(window.fetchStationStatus)window.fetchStationStatus();
});
var bi=document.getElementById("buildInfo");
if(bi){var h=bi.innerHTML; if(/Version date/i.test(h)){h=h.replace(/(<br\s*\/?>)[\s\S]*?(?=Version date)/i,"$1");bi.innerHTML="Board / Environment = "+h;}}
if(window.loadReceivedPackets){var _o=window.loadReceivedPackets;window.loadReceivedPackets=function(p){if(p&&p.slice){p=p.slice().reverse();}return _o(p);};}
+661
View File
@@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
+6
View File
@@ -0,0 +1,6 @@
/* @preserve
* Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com
* (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n<o;n++)for(e in i=arguments[n])t[e]=i[e];return t}var R=Object.create||function(t){return N.prototype=t,new N};function N(){}function a(t,e){var i,n=Array.prototype.slice;return t.bind?t.bind.apply(t,n.call(arguments,1)):(i=n.call(arguments,2),function(){return t.apply(e,i.length?i.concat(n.call(arguments)):arguments)})}var D=0;function h(t){return"_leaflet_id"in t||(t._leaflet_id=++D),t._leaflet_id}function j(t,e,i){var n,o,s=function(){n=!1,o&&(r.apply(i,o),o=!1)},r=function(){n?o=arguments:(t.apply(i,arguments),setTimeout(s,e),n=!0)};return r}function H(t,e,i){var n=e[1],e=e[0],o=n-e;return t===n&&i?t:((t-e)%o+o)%o+e}function u(){return!1}function i(t,e){return!1===e?t:(e=Math.pow(10,void 0===e?6:e),Math.round(t*e)/e)}function W(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}function F(t){return W(t).split(/\s+/)}function c(t,e){for(var i in Object.prototype.hasOwnProperty.call(t,"options")||(t.options=t.options?R(t.options):{}),e)t.options[i]=e[i];return t.options}function U(t,e,i){var n,o=[];for(n in t)o.push(encodeURIComponent(i?n.toUpperCase():n)+"="+encodeURIComponent(t[n]));return(e&&-1!==e.indexOf("?")?"&":"?")+o.join("&")}var V=/\{ *([\w_ -]+) *\}/g;function q(t,i){return t.replace(V,function(t,e){e=i[e];if(void 0===e)throw new Error("No value provided for variable "+t);return e="function"==typeof e?e(i):e})}var d=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)};function G(t,e){for(var i=0;i<t.length;i++)if(t[i]===e)return i;return-1}var K="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";function Y(t){return window["webkit"+t]||window["moz"+t]||window["ms"+t]}var X=0;function J(t){var e=+new Date,i=Math.max(0,16-(e-X));return X=e+i,window.setTimeout(t,i)}var $=window.requestAnimationFrame||Y("RequestAnimationFrame")||J,Q=window.cancelAnimationFrame||Y("CancelAnimationFrame")||Y("CancelRequestAnimationFrame")||function(t){window.clearTimeout(t)};function x(t,e,i){if(!i||$!==J)return $.call(window,a(t,e));t.call(e)}function r(t){t&&Q.call(window,t)}var tt={__proto__:null,extend:l,create:R,bind:a,get lastId(){return D},stamp:h,throttle:j,wrapNum:H,falseFn:u,formatNum:i,trim:W,splitWords:F,setOptions:c,getParamString:U,template:q,isArray:d,indexOf:G,emptyImageUrl:K,requestFn:$,cancelFn:Q,requestAnimFrame:x,cancelAnimFrame:r};function et(){}et.extend=function(t){function e(){c(this),this.initialize&&this.initialize.apply(this,arguments),this.callInitHooks()}var i,n=e.__super__=this.prototype,o=R(n);for(i in(o.constructor=e).prototype=o,this)Object.prototype.hasOwnProperty.call(this,i)&&"prototype"!==i&&"__super__"!==i&&(e[i]=this[i]);if(t.statics&&l(e,t.statics),t.includes){var s=t.includes;if("undefined"!=typeof L&&L&&L.Mixin){s=d(s)?s:[s];for(var r=0;r<s.length;r++)s[r]===L.Mixin.Events&&console.warn("Deprecated include of L.Mixin.Events: this property will be removed in future releases, please inherit from L.Evented instead.",(new Error).stack)}l.apply(null,[o].concat(t.includes))}return l(o,t),delete o.statics,delete o.includes,o.options&&(o.options=n.options?R(n.options):{},l(o.options,t.options)),o._initHooks=[],o.callInitHooks=function(){if(!this._initHooksCalled){n.callInitHooks&&n.callInitHooks.call(this),this._initHooksCalled=!0;for(var t=0,e=o._initHooks.length;t<e;t++)o._initHooks[t].call(this)}},e},et.include=function(t){var e=this.prototype.options;return l(this.prototype,t),t.options&&(this.prototype.options=e,this.mergeOptions(t.options)),this},et.mergeOptions=function(t){return l(this.prototype.options,t),this},et.addInitHook=function(t){var e=Array.prototype.slice.call(arguments,1),i="function"==typeof t?t:function(){this[t].apply(this,e)};return this.prototype._initHooks=this.prototype._initHooks||[],this.prototype._initHooks.push(i),this};var e={on:function(t,e,i){if("object"==typeof t)for(var n in t)this._on(n,t[n],e);else for(var o=0,s=(t=F(t)).length;o<s;o++)this._on(t[o],e,i);return this},off:function(t,e,i){if(arguments.length)if("object"==typeof t)for(var n in t)this._off(n,t[n],e);else{t=F(t);for(var o=1===arguments.length,s=0,r=t.length;s<r;s++)o?this._off(t[s]):this._off(t[s],e,i)}else delete this._events;return this},_on:function(t,e,i,n){"function"!=typeof e?console.warn("wrong listener type: "+typeof e):!1===this._listens(t,e,i)&&(e={fn:e,ctx:i=i===this?void 0:i},n&&(e.once=!0),this._events=this._events||{},this._events[t]=this._events[t]||[],this._events[t].push(e))},_off:function(t,e,i){var n,o,s;if(this._events&&(n=this._events[t]))if(1===arguments.length){if(this._firingCount)for(o=0,s=n.length;o<s;o++)n[o].fn=u;delete this._events[t]}else"function"!=typeof e?console.warn("wrong listener type: "+typeof e):!1!==(e=this._listens(Line truncated
//# sourceMappingURL=leaflet.js.map
+391 -9
View File
@@ -490,7 +490,6 @@ function toggleWiFiAutoAPFields() {
if (autoAPConfig) autoAPConfig.style.display = isEnabled ? 'block' : 'none';
}
document.querySelector(".new button").addEventListener("click", function () {
const networksContainer = document.querySelector(".list-networks");
@@ -567,6 +566,7 @@ function checkConnection() {
}, 3000);
fetchSettings();
fetchStationStatus();
})
.catch((err) => {
setTimeout(checkConnection, 0);
@@ -636,12 +636,93 @@ document.querySelector('a[href="/received-packets"]').addEventListener('click',
document.getElementById('received-packets').classList.remove('d-none');
document.getElementById('configuration').classList.add('d-none');
document.getElementById('station-status').classList.add('d-none');
document.querySelector('button[type=submit]').remove();
fetchReceivedPackets();
})
function loadStationStatus(stationStatus) {
if (stationStatus) {
document.getElementById("stationStatus.voltageInternal").value = stationStatus.stationStatus.voltageInternal;
document.getElementById("stationStatus.voltageExternal").value = stationStatus.stationStatus.voltageExternal;
document.getElementById("stationStatus.voltage").style.display = "block";
document.getElementById("stationStatus.elapsedTime").value = stationStatus.stationStatus.elapsedTime;
document.getElementById("stationStatus.uptime").style.display = "block";
document.getElementById("stationStatus.statusWifi").value = stationStatus.stationStatus.statusWifi;
document.getElementById("stationStatus.statusIS").value = stationStatus.stationStatus.statusIS;
document.getElementById("stationStatus.statusNTP").value = stationStatus.stationStatus.statusNTP;
document.getElementById("stationStatus.statusSyslog").value = stationStatus.stationStatus.statusSyslog;
document.getElementById("stationStatus.statusMQTT").value = stationStatus.stationStatus.statusMQTT;
document.getElementById("stationStatus.connectivity").style.display = "block";
if (stationStatus.stationStatus.webAdminActive) {
document.getElementById("stationStatus.loginsInvalid").value = stationStatus.stationStatus.loginsInvalid;
document.getElementById("stationStatus.login").style.display = "block";
} else {
document.getElementById("stationStatus.login").style.display = "none";
}
document.getElementById("stationStatus.packetsRFSendSuccess").value = stationStatus.stationStatus.packetsRFSendSuccess;
document.getElementById("stationStatus.packetsRFReceiveSuccess").value = stationStatus.stationStatus.packetsRFReceiveSuccess;
document.getElementById("stationStatus.packetsISSendSuccess").value = stationStatus.stationStatus.packetsISSendSuccess;
document.getElementById("stationStatus.packetsISReceiveSuccess").value = stationStatus.stationStatus.packetsISReceiveSuccess;
document.getElementById("stationStatus.packetsRFSendFail").value = stationStatus.stationStatus.packetsRFSendFail;
document.getElementById("stationStatus.packetsRFReceiveFail").value = stationStatus.stationStatus.packetsRFReceiveFail;
document.getElementById("stationStatus.packetsISSendFail").value = stationStatus.stationStatus.packetsISSendFail;
document.getElementById("stationStatus.packetsISReceiveFail").value = stationStatus.stationStatus.packetsISReceiveFail;
document.getElementById("stationStatus.packetsBlacklisted").value = stationStatus.stationStatus.packetsBlacklisted;
document.getElementById("stationStatus.traffic").style.display = "block";
if (stationStatus.stationStatus.weatherModulePresent) {
document.getElementById("stationStatus.weatherTemperature").value = stationStatus.stationStatus.weatherTemperature;
document.getElementById("stationStatus.weatherHumidity").value = stationStatus.stationStatus.weatherHumidity;
document.getElementById("stationStatus.weatherBarometricPressure").value = stationStatus.stationStatus.weatherBarometricPressure;
document.getElementById("stationStatus.weatherGas").value = stationStatus.stationStatus.weatherGas;
document.getElementById("stationStatus.weather").style.display = "block";
} else {
document.getElementById("stationStatus.weather").style.display = "none";
}
} else {
document.getElementById("stationStatus.voltage").style.display = "none";
document.getElementById("stationStatus.weather").style.display = "none";
document.getElementById("stationStatus.uptime").style.display = "none";
document.getElementById("stationStatus.connectivity").style.display = "none";
document.getElementById("stationStatus.traffic").style.display = "none";
document.getElementById("stationStatus.login").style.display = "none";
}
setTimeout(fetchStationStatus, 15000);
}
fetchStationStatus();
function fetchStationStatus() {
fetch("/station-status.json")
.then((response) => response.json())
.then((deviceTelemetry) => {
loadStationStatus(deviceTelemetry);
})
.catch((err) => {
console.error(err);
console.error(`Failed to load station status`);
});
}
document.querySelector('a[href="/station-status"]').addEventListener('click', function (e) {
e.preventDefault();
document.getElementById('station-status').classList.remove('d-none');
document.getElementById('received-packets').classList.add('d-none');
document.getElementById('configuration').classList.add('d-none');
document.querySelector('button[type=submit]').remove();
fetchDeviceTelemetry();
})
/* ---------- Stations Map (Leaflet, CDN) ---------- */
@@ -659,10 +740,274 @@ function setMapMessage(text) {
}
}
/*
* APRS symbol renderer for Stations Map
*
* s.symbol contains APRS table + symbol code, for example:
* "/>" Primary table
* "\\>" Alternate table
* "L&" Alternate table with overlay
*
* Sprite layout:
* aprs-symbols-24-0.png = Primary table
* aprs-symbols-24-1.png = Alternate table
* aprs-symbols-24-2.png = Overlay characters
*/
function getAprsSymbolIcon(symbol, callsign = "") {
if (!symbol || String(symbol).length < 2) {
return null;
}
const value = String(symbol);
let table = value.charAt(0);
let code = value.charAt(1);
let overlay = null;
/*
* Standard APRS:
* "/" = Primary
* "\\" = Alternate
*
* A-Z / 0-9 / a-j in table position are overlays
* for the Alternate table.
*/
if (
table !== "/" &&
table !== "\\"
) {
overlay = table;
table = "\\";
}
const ascii = code.charCodeAt(0);
/*
* APRS printable symbol codes are ASCII 33..126.
*/
if (
ascii < 33 ||
ascii > 126
) {
return null;
}
const index = ascii - 33;
/*
* Sprite sheet contains 16 columns x 6 rows.
* Each cell occupies 24x24 CSS px; the
* "background-size" below tells the browser to scale the sheet back
* down to its original 24px-per-cell layout, so all position math
* stays in 24px units regardless of source resolution.
*/
const columns = 16;
const sheetSize = "384px 144px"; // 16*24 x 6*24, same for all 3 sheets
const col = index % columns;
const row = Math.floor(index / columns);
const sprite =
table === "/"
? "/aprs-symbols-24-0.png"
: "/aprs-symbols-24-1.png";
const x = -(col * 24);
const y = -(row * 24);
let overlayHtml = "";
if (overlay) {
let displayOverlay = overlay;
/*
* APRS compressed numeric overlay:
* a-j -> 0-9
*/
if (
overlay >= "a" &&
overlay <= "j"
) {
displayOverlay =
String(
overlay.charCodeAt(0) -
"a".charCodeAt(0)
);
}
/*
* The overlay sprite sheet does NOT follow the ascii-33 layout used
* by the primary/alternate tables above; it has its own grid:
* "0" at row 0 col 15, "1".."9" at row 1 col 0-8, "A".."P" at row 2,
* "Q".."Z" at row 3 col 0-9. Verified by hand against the PNG.
*/
let overlayCol = null;
let overlayRow = null;
if (displayOverlay === "0") {
overlayCol = 15;
overlayRow = 0;
} else if (displayOverlay >= "1" && displayOverlay <= "9") {
overlayCol = displayOverlay.charCodeAt(0) - "1".charCodeAt(0);
overlayRow = 1;
} else if (displayOverlay >= "A" && displayOverlay <= "P") {
overlayCol = displayOverlay.charCodeAt(0) - "A".charCodeAt(0);
overlayRow = 2;
} else if (displayOverlay >= "Q" && displayOverlay <= "Z") {
overlayCol = displayOverlay.charCodeAt(0) - "Q".charCodeAt(0);
overlayRow = 3;
}
if (overlayCol !== null) {
const ox = -(overlayCol * 24);
const oy = -(overlayRow * 24);
overlayHtml = `
<span style="
position:absolute;
left:0;
top:0;
width:24px;
height:24px;
background-image:url('/aprs-symbols-24-2.png');
background-repeat:no-repeat;
background-position:${ox}px ${oy}px;
background-size:${sheetSize};
pointer-events:none;
"></span>
`;
}
}
return L.divIcon({
className: "",
html: `
<div style="
position:relative;
width:24px;
height:24px;
background-image:url('${sprite}');
background-repeat:no-repeat;
background-position:${x}px ${y}px;
background-size:${sheetSize};
">
${overlayHtml}
<span style="
position:absolute;
left:27px;
top:6px;
font:600 10px Arial,sans-serif;
line-height:12px;
white-space:nowrap;
color:#111;
text-shadow:
-1px -1px 0 #fff,
1px -1px 0 #fff,
-1px 1px 0 #fff,
1px 1px 0 #fff;
pointer-events:none;
">${callsign}</span>
</div>
`,
iconSize: [24, 24],
iconAnchor: [12, 12],
popupAnchor: [0, -12]
});
}
let mapStationsCache = [];
let mapRouteLayer = null;
function getUsedDigipeaters(path) {
if (!path) return [];
return path
.split(",")
.map((x) => x.trim())
.filter((x) => x.endsWith("*"))
.map((x) => x.replace(/\*+$/, ""))
.filter((x) =>
x &&
!x.startsWith("WIDE") &&
!x.startsWith("TRACE") &&
!x.startsWith("TCPIP") &&
!x.startsWith("TCPXX") &&
!x.startsWith("qA")
);
}
function describeMapPath(station) {
const digis = getUsedDigipeaters(station.path);
// heardDirect viene del firmware: true si ALGUNA VEZ se oyo directo,
// aunque el ultimo paquete haya llegado repetido.
if (station.heardDirect && digis.length) {
return "Direct+Via: " + digis.join(" → ");
}
if (digis.length) {
return "Via: " + digis.join(" → ");
}
return "Direct";
}
function clearMapRoute() {
if (mapRouteLayer && mapInstance) {
mapInstance.removeLayer(mapRouteLayer);
}
mapRouteLayer = null;
}
function drawStationRoute(station) {
if (!mapInstance || !iGateLatLng) return;
clearMapRoute();
const points = [
L.latLng(station.lat, station.lon)
];
const digis = getUsedDigipeaters(station.path);
digis.forEach((digiCall) => {
const digi = mapStationsCache.find((s) =>
String(s.callsign || "").toUpperCase() === digiCall.toUpperCase()
);
if (digi && !(digi.lat === 0 && digi.lon === 0)) {
points.push(L.latLng(digi.lat, digi.lon));
}
});
points.push(iGateLatLng);
const routeColor = digis.length > 0 ? "#2e7d32" : "#1565c0";
mapRouteLayer = L.polyline(points, {
color: routeColor,
weight: 3,
opacity: 0.9
}).addTo(mapInstance);
}
function loadMapStations(stations) {
if (!mapMarkers) return;
mapStationsCache = stations || [];
mapMarkers.clearLayers();
clearMapRoute();
let drawn = 0;
(stations || []).forEach((s) => {
@@ -671,15 +1016,52 @@ function loadMapStations(stations) {
const popup = `<b>${s.callsign}</b>`
+ (s.lastHeard ? `<br>Last: ${s.lastHeard}` : "")
+ `<br>RSSI ${s.RSSI} / SNR ${s.SNR}`
+ `<br>Packets: ${s.count}`;
+ `<br>Packets: ${s.count}`
+ `<br><b>${describeMapPath(s)}</b>`
+ (s.path ? `<br><small>Path: ${s.path}</small>` : "");
const aprsIcon =
getAprsSymbolIcon(s.symbol, s.callsign);
if (aprsIcon) {
const marker = L.marker(
[s.lat, s.lon],
{
icon: aprsIcon
}
)
.bindPopup(popup)
.addTo(mapMarkers);
marker.on("mouseover", function () {
drawStationRoute(s);
});
marker.on("mouseout", function () {
clearMapRoute();
});
} else { // Fallback: station with missing or invalid APRS symbol
const marker = L.circleMarker([s.lat, s.lon], {
radius: 6,
color: "#1d6fe0",
weight: 2,
fillColor: "#3b8cff",
fillOpacity: 0.9
})
.bindPopup(popup)
.addTo(mapMarkers);
marker.on("mouseover", function () {
drawStationRoute(s);
});
marker.on("mouseout", function () {
clearMapRoute();
});
}
L.circleMarker([s.lat, s.lon], {
radius: 6,
color: "#1d6fe0", // borde
weight: 2,
fillColor: "#3b8cff", // relleno
fillOpacity: 0.9
}).bindPopup(popup).addTo(mapMarkers);
drawn++;
});
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+15 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -40,6 +40,20 @@ namespace APRS_IS_Utils {
void firstConnection();
String getAPRSISState();
String getWifiState();
void incrementPacketSendSuccess();
unsigned int getPacketSendSuccess();
void incrementPacketSendFail();
unsigned int getPacketSendFail();
void incrementPacketReceiveSuccess();
unsigned int getPacketReceiveSuccess();
void incrementPacketReceiveFail();
unsigned int getPacketReceiveFail();
bool isBeyondWindow(unsigned long time);
void resetPacketCounters(unsigned long time);
}
#endif
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+14 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -33,6 +33,19 @@ namespace LoRa_Utils {
void wakeRadio();
void sleepRadio();
void incrementPacketSendSuccess();
unsigned int getPacketSendSuccess();
void incrementPacketSendFail();
unsigned int getPacketSendFail();
void incrementPacketReceiveSuccess();
unsigned int getPacketReceiveSuccess();
void incrementPacketReceiveFail();
unsigned int getPacketReceiveFail();
void incrementPacketBlacklisted();
unsigned int getPacketBlacklisted();
bool isBeyondWindow(unsigned long time);
void resetPacketCounters(unsigned long time);
}
#endif
+4 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -32,12 +32,14 @@ struct MapStation {
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
char path[96]; // last path received
uint32_t lastHeardMillis; // millis() -> ordenar / expirar (no se muestra)
bool heardDirect; // true si ALGUNA VEZ se oyo sin digipeaters (no se resetea)
};
namespace MAP_Utils {
void upsert(const String& callsign, float latitude, float longitude, const String& symbol, int rssi, float snr);
void upsert(const String& callsign, float latitude, float longitude, const String& path, const String& symbol, int rssi, float snr);
String getStationsJson();
}
+2 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -28,6 +28,7 @@ namespace MQTT_Utils {
void connect();
void loop();
void setup();
String getMQTTStatus();
}
+18
View File
@@ -1,3 +1,21 @@
/* Copyright (C) 2026 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 <Arduino.h>
#include <SPI.h>
#include <WiFi.h>
+3 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -27,6 +27,8 @@ namespace NTP_Utils {
bool setup();
void update();
String getFormatedTime();
unsigned long getTimeMillis();
String getNTPStatus();
}
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+3 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -41,6 +41,8 @@ namespace STATION_Utils {
void processOutputPacketBufferUltraEcoMode();
void processOutputPacketBuffer();
void addToOutputPacketBuffer(const String& packet, bool flag = false);
void markBootTime();
unsigned long getBootTime();
}
+2 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -26,6 +26,7 @@ namespace SYSLOG_Utils {
void log(const uint8_t type ,const String& packet, const int rssi, const float snr, const int freqError);
void setup();
String getSyslogStatus();
}
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+32
View File
@@ -0,0 +1,32 @@
/* Copyright (C) 2026 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 TIME_UTILS_H_
#define TIME_UTILS_H_
#include <Arduino.h>
namespace TIME_Utils {
String getFormattedTime(unsigned long timeMillis);
String getFormattedDateTime(unsigned long timeMillis);
String generateDuration(unsigned long startMillis, unsigned long endMillis);
boolean isValid(unsigned long timeMillis);
}
#endif
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+3 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -38,6 +38,8 @@ namespace WEB_Utils {
void setup();
unsigned long getLoginFailCount();
}
#endif
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+6 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -36,7 +36,11 @@ namespace WX_Utils {
String generateHumString(const float sensorHum);
String generatePresString(const float sensorPres);
String readDataSensor();
float getHumidity();
float getTemperature();
float getBarometricPressure();
float getGas();
boolean isWeatherModulePresent();
}
#endif
+5
View File
@@ -26,7 +26,12 @@ board_build.embed_files =
data_embed/script.js.gz
data_embed/bootstrap.css.gz
data_embed/bootstrap.js.gz
data_embed/leaflet.css.gz
data_embed/leaflet.js.gz
data_embed/favicon.png.gz
data_embed/aprs-symbols-24-0.png.gz
data_embed/aprs-symbols-24-1.png.gz
data_embed/aprs-symbols-24-2.png.gz
extra_scripts =
pre:tools/compress.py
debug_tool = esp-prog
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+6 -4
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -63,13 +63,14 @@ ___________________________________________________________________*/
#include "wx_utils.h"
#include "display.h"
#include "utils.h"
#include "telemetry_utils.h"
#ifdef HAS_A7670
#include "A7670_utils.h"
#endif
String versionDate = "2026-06-12";
String versionNumber = "4.0.0";
String versionDate = "2026-09-08";
String versionNumber = "4.0.1";
Configuration Config;
WiFiClient aprsIsClient;
WiFiClient mqttClient;
@@ -128,6 +129,7 @@ void setup() {
Utils::checkRebootMode();
APRS_IS_Utils::firstConnection();
SLEEP_Utils::checkSerial();
STATION_Utils::markBootTime(); // should be last
}
void loop() {
@@ -196,7 +198,7 @@ void loop() {
APRS_IS_Utils::processLoRaPacket(packet); // Send received packet to APRSIS
}
if (Config.loramodule.txActive && (Config.digi.mode == 2 || Config.digi.mode == 3 || backupDigiMode)) { // If Digi enabled
if (Config.loramodule.txActive && (Config.digi.mode == 1 || Config.digi.mode == 2 || Config.digi.mode == 3 || backupDigiMode)) { // If Digi enabled
DIGI_Utils::processLoRaPacket(packet); // Send received packet to Digi
}
+114 -15
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -50,7 +50,6 @@ extern String versionNumber;
bool passcodeValid = false;
uint32_t lastServerCheck = 0;
#ifdef HAS_A7670
extern bool stationBeacon;
#endif
@@ -58,7 +57,14 @@ uint32_t lastServerCheck = 0;
namespace APRS_IS_Utils {
unsigned int packetSendSuccess = 0;
unsigned int packetSendFail = 0;
unsigned int packetReceiveSuccess = 0;
unsigned int packetReceiveFail = 0;
unsigned long packetCountStartTime = 0;
void upload(const String& line) {
incrementPacketSendSuccess();
aprsIsClient.print(line + "\r\n");
}
@@ -93,7 +99,30 @@ namespace APRS_IS_Utils {
}
void checkStatus() {
String wifiState, aprsisState;
String wifiState = getWifiState();
String aprsisState = getAPRSISState();
if (!networkManager->isWiFiConnected()) {
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
}
lastScreenOn = millis();
}
if (Config.aprs_is.active) {
if(aprsisState == "--" && !Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
lastScreenOn = millis();
}
}
secondLine = "WiFi: ";
secondLine += wifiState;
secondLine += " APRS-IS: ";
secondLine += aprsisState;
}
String getWifiState() {
String wifiState;
if (networkManager->isWiFiConnected()) {
wifiState = "OK";
} else {
@@ -102,12 +131,14 @@ namespace APRS_IS_Utils {
} else {
wifiState = "AP";
}
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
}
lastScreenOn = millis();
}
return wifiState;
}
String getAPRSISState() {
String aprsisState;
if (!Config.aprs_is.active) {
aprsisState = "OFF";
} else {
@@ -124,15 +155,8 @@ namespace APRS_IS_Utils {
aprsisState = "--";
}
#endif
if(aprsisState == "--" && !Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
lastScreenOn = millis();
}
}
secondLine = "WiFi: ";
secondLine += wifiState;
secondLine += " APRS-IS: ";
secondLine += aprsisState;
return aprsisState;
}
String checkForStartingBytes(const String& packet) {
@@ -225,6 +249,7 @@ namespace APRS_IS_Utils {
lastScreenOn = millis();
#ifdef HAS_A7670
stationBeacon = true;
incrementPacketReceiveSuccess();
A7670_Utils::uploadToAPRSIS(aprsPacket);
stationBeacon = false;
#else
@@ -299,6 +324,7 @@ namespace APRS_IS_Utils {
ackPacket += ackMessage;
#ifdef HAS_A7670
incrementPacketReceiveSuccess();
A7670_Utils::uploadToAPRSIS(ackPacket);
#else
upload(ackPacket);
@@ -322,6 +348,7 @@ namespace APRS_IS_Utils {
if (packet.startsWith("#")) {
if (Config.digi.backupDigiMode) lastServerCheck = currentTime;
} else {
incrementPacketReceiveSuccess();
int doubleColonIndex = packet.indexOf("::");
if (Config.aprs_is.messagesToRF && doubleColonIndex > 0) {
String Sender = packet.substring(0, packet.indexOf(">"));
@@ -347,6 +374,7 @@ namespace APRS_IS_Utils {
}
lastScreenOn = currentTime;
#ifdef HAS_A7670
incrementPacketReceiveSuccess();
A7670_Utils::uploadToAPRSIS(queryAnswer);
#else
upload(queryAnswer);
@@ -416,4 +444,75 @@ namespace APRS_IS_Utils {
}
}
bool isBeyondWindow(unsigned long time) {
unsigned long window = 24*60*60*1000;
if (time > packetCountStartTime + window) {
return true;
}
return false;
}
void resetPacketCounters(unsigned long time) {
packetCountStartTime = time;
packetSendSuccess = 0;
packetSendFail = 0;
packetReceiveSuccess = 0;
packetReceiveFail = 0;
}
void incrementPacketSendSuccess() {
unsigned int now = millis();
if (isBeyondWindow(now)) {
resetPacketCounters(now);
packetSendSuccess = 1;
} else {
packetSendSuccess++;
}
}
unsigned int getPacketSendSuccess() {
return packetSendSuccess;
}
void incrementPacketSendFail() {
unsigned int now = millis();
if (isBeyondWindow(now)) {
packetCountStartTime = now;
packetSendFail = 1;
} else {
packetSendFail++;
}
}
unsigned int getPacketSendFail() {
return packetSendFail;
}
void incrementPacketReceiveSuccess() {
unsigned int now = millis();
if (isBeyondWindow(now)) {
packetCountStartTime = now;
packetReceiveSuccess = 1;
} else {
packetReceiveSuccess++;
}
}
unsigned int getPacketReceiveSuccess() {
return packetReceiveSuccess;
}
void incrementPacketReceiveFail() {
unsigned int now = millis();
if (isBeyondWindow(now)) {
packetCountStartTime = now;
packetReceiveFail = 1;
} else {
packetReceiveFail++;
}
}
unsigned int getPacketReceiveFail() {
return packetReceiveFail;
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+11 -8
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -570,14 +570,17 @@ void Configuration::setDefaultValues() {
void Configuration::setup() {
if (!SPIFFS.begin(false)) {
Serial.println("SPIFFS Mount Failed");
return;
} else {
Serial.println("SPIFFS Mounted");
}
Serial.println("SPIFFS Mount Failed, formatting...");
bool exists = SPIFFS.exists("/igate_conf.json");
if (!exists) {
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS Format Failed");
return;
}
}
Serial.println("SPIFFS Ready");
if (!SPIFFS.exists("/igate_conf.json")) {
Serial.println("Config not found, creating default...");
setDefaultValues();
writeFile();
delay(1000);
+48 -10
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -42,8 +42,8 @@ extern bool backupDigiMode;
namespace DIGI_Utils {
String cleanPathAsterisks(String path) {
String terms[] = {",WIDE1*", ",WIDE2*", "*"};
String cleanPath(String path) {
String terms[] = {"WIDE1*,", "WIDE2*,", "*"};
for (String term : terms) {
int index = path.indexOf(term);
if (index != -1) path.remove(index, term.length()); // less memory than: tempPath.replace("*", "");
@@ -51,6 +51,35 @@ namespace DIGI_Utils {
return path;
}
String processMode3Path(const String& path, const String& stationCallsign) {
int start = 0;
bool prevTokensAllStar = true;
int ownTokenEnd = -1;
while (start < path.length()) {
int delim = path.indexOf(',', start);
if (delim == -1) delim = path.length(); // busca todo hasta lograr encontra una coma o el final del string
String token = path.substring(start, delim);
bool tokenIsOwn = (token == stationCallsign) || (token == stationCallsign + "*");
bool tokenStar = token.endsWith("*");
if (tokenIsOwn) {
if (tokenStar) return ""; // already digipeated
if (!prevTokensAllStar) return ""; // earlier tokens must be marked
ownTokenEnd = delim;
break;
}
if (!tokenStar) prevTokensAllStar = false;
start = delim + 1;
}
if (ownTokenEnd == -1) return "";
String tempPacket = cleanPath(path.substring(0, ownTokenEnd));
return tempPacket + "*" + path.substring(ownTokenEnd);
}
String buildPacket(const String& path, const String& packet, bool thirdParty, bool crossFreq) {
String stationCallsign = (Config.tacticalCallsign == "" ? Config.callsign : Config.tacticalCallsign);
String suffix = thirdParty ? ":}" : ":";
@@ -60,11 +89,11 @@ namespace DIGI_Utils {
int digiMode = Config.digi.mode;
String tempPath = path;
if (tempPath.indexOf("WIDE1-1") != -1 && (digiMode == 2 || digiMode == 3)) { // WIDE1-1
if (tempPath.indexOf("WIDE1-1") != -1 && (digiMode == 1 || digiMode == 2)) { // WIDE1-1
if (tempPath.indexOf("*") != -1 ) return ""; // "*" shouldn't be in WIDE1-1 (only) type of packet
tempPath.replace("WIDE1-1", stationCallsign + "*");
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 3) { // WIDE2-n Digipeater
tempPath = cleanPathAsterisks(path);
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 2) { // WIDE2-n Digipeater
tempPath = cleanPath(path);
if (tempPath.indexOf("WIDE2-1") != -1) {
tempPath.replace("WIDE2-1", stationCallsign + "*");
} else if (tempPath.indexOf("WIDE2-2") != -1) {
@@ -72,11 +101,14 @@ namespace DIGI_Utils {
} else {
return "";
}
} else if (digiMode == 3) { // Repeat if station callsign is in path (free to repeat).
tempPath = processMode3Path(tempPath, stationCallsign);
if (tempPath == "") return "";
}
packetToRepeat = packet.substring(0, packet.indexOf(",") + 1);
packetToRepeat += tempPath;
} else { // CrossFreq Digipeater
packetToRepeat = cleanPathAsterisks(packet.substring(0, suffixIndex));
packetToRepeat = cleanPath(packet.substring(0, suffixIndex));
if (packetToRepeat.indexOf(stationCallsign) != -1) return ""; // stationCallsign shouldn't be in path
packetToRepeat += ",";
packetToRepeat += stationCallsign;
@@ -100,14 +132,14 @@ namespace DIGI_Utils {
if (commaIndex > 2) { // "path" found
const String& path = temp.substring(commaIndex + 1);
if (digiMode == 2 || backupDigiMode) {
if (digiMode == 1 || backupDigiMode) {
bool hasWide = path.indexOf("WIDE1-1") != -1;
if (hasWide || crossFreq) {
return buildPacket(path, packet, thirdParty, !hasWide);
}
return "";
}
if (digiMode == 3) {
if (digiMode == 2) {
int wide1Index = path.indexOf("WIDE1-1");
int wide2Index = path.indexOf("WIDE2-");
bool hasWide1 = wide1Index != -1;
@@ -121,10 +153,16 @@ namespace DIGI_Utils {
return "";
}
if (digiMode == 3) {
String stationCallsign = (Config.tacticalCallsign == "" ? Config.callsign : Config.tacticalCallsign);
bool containsOwnCall = path.indexOf(stationCallsign) != -1;
if (containsOwnCall) return buildPacket(path, packet, thirdParty, false);
return "";
}
return "";
}
if (commaIndex == -1 && (digiMode == 2 || backupDigiMode || digiMode == 3) && crossFreq) return buildPacket("", packet, thirdParty, true); // no "path" but is CrossFreq Digi
if (commaIndex == -1 && (digiMode == 1 || backupDigiMode || digiMode == 2) && crossFreq) return buildPacket("", packet, thirdParty, true); // no "path" but is CrossFreq Digi
return "";
}
+11 -8
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -57,19 +57,20 @@
String lastEpaperText;
#else
#include <Adafruit_GFX.h>
#if defined RPC_LORA_DIGIGATE_1W
#define SCREEN_HEIGHT 32
#else
#define SCREEN_HEIGHT 64
#endif
#ifdef HAS_SH1106
#include <Adafruit_SH110X.h>
Adafruit_SH1106G display(128, 64, &Wire, OLED_RST);
Adafruit_SH1106G display(128, SCREEN_HEIGHT, &Wire, OLED_RST);
#else
#include <Adafruit_SSD1306.h>
#ifdef HELTEC_WSL_V3_DISPLAY
Adafruit_SSD1306 display(128, 64, &Wire1, OLED_RST);
Adafruit_SSD1306 display(128, SCREEN_HEIGHT, &Wire1, OLED_RST);
#else
#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
@@ -107,6 +108,8 @@ void displaySetup() {
maxLines = 6;
#elif (SCREEN_HEIGHT == 32)
maxLines = 3;
#else
maxLines = 6;
#endif
#ifdef HAS_EPAPER
display.landscape();
+11 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -185,6 +185,16 @@ namespace GPS_Utils {
return " _ / _ / _ ";
}
/*String getDistanceAndCommentFromMicE(const String& packet) {
int indexOfMicE1 = packet.indexOf(":`");
int indexOfMicE2 = packet.indexOf(":\'");
if (indexOfMicE1 > 10 || indexOfMicE2 > 10) {
return decodeEncodedGPS(packet);
}
return " _ / _ / _ ";
}*/
void setup() {
#ifdef HAS_GPS
if (Config.beacon.gpsActive && Config.digi.ecoMode != 1) {
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+115 -5
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -20,6 +20,7 @@
#include <RadioLib.h>
#include "configuration.h"
#include "network_manager.h"
#include "lora_utils.h"
#include "aprs_is_utils.h"
#include "station_utils.h"
#include "board_pinout.h"
@@ -66,9 +67,24 @@ int backoffMax = 4; // Max Backoff value (number of CAD slots to wai
int rssi, freqError;
float snr;
namespace LoRa_Utils {
unsigned int packetSendSuccess = 0;
unsigned int packetSendFail = 0;
unsigned int packetReceiveSuccess = 0;
unsigned int packetReceiveFail = 0;
unsigned int packetBlacklisted = 0;
unsigned long packetCountStartTime = 0;
static String sanitizeForWeb(const String& s) { // replaces non-printable ASCII with '.' for WebUI display
String out = s;
for (int i = 0; i < (int)out.length(); i++) {
uint8_t c = (uint8_t)out[i];
if (c < 32 || c == 127) out.setCharAt(i, '.');
}
return out;
}
void setFlag(void) {
operationDone = true;
}
@@ -225,9 +241,11 @@ namespace LoRa_Utils {
}
Utils::print("---> LoRa Packet Tx : ");
Utils::println(newPacket);
incrementPacketSendSuccess();
} else {
Utils::print(F("failed, code "));
Utils::println(String(state));
incrementPacketSendFail();
}
#ifdef INTERNAL_LED_PIN
if (Config.digi.ecoMode != 1) digitalWrite(INTERNAL_LED_PIN, LOW); // disabled in Ultra Eco Mode
@@ -263,7 +281,8 @@ namespace LoRa_Utils {
if (packet != "") {
String sender = packet.substring(3, packet.indexOf(">"));
if (packet.substring(0,3) == "\x3c\xff\x01" && !STATION_Utils::isBlacklisted(sender)) { // avoid processing BlackListed stations
bool isBlacklisted = STATION_Utils::isBlacklisted(sender);
if (packet.substring(0,3) == "\x3c\xff\x01" && !isBlacklisted) { // avoid processing BlackListed stations
rssi = radio.getRSSI();
snr = radio.getSNR();
freqError = radio.getFrequencyError();
@@ -276,14 +295,15 @@ namespace LoRa_Utils {
}
ReceivedPacket receivedPacket;
receivedPacket.rxTime = NTP_Utils::getFormatedTime();
receivedPacket.packet = packet.substring(3);
receivedPacket.packet = sanitizeForWeb(packet.substring(3));
receivedPacket.RSSI = rssi;
receivedPacket.SNR = snr;
receivedPackets.push_back(receivedPacket);
APRSPacket aprsPacket = APRSPacketLib::processReceivedPacket(packet.substring(3), rssi, snr, freqError);
incrementPacketReceiveSuccess();
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);
MAP_Utils::upsert(aprsPacket.sender, aprsPacket.latitude, aprsPacket.longitude, aprsPacket.overlay + aprsPacket.symbol, aprsPacket.path, aprsPacket.rssi, aprsPacket.snr);
}
}
@@ -291,6 +311,9 @@ namespace LoRa_Utils {
SYSLOG_Utils::log(1, packet, rssi, snr, freqError); // RX
}
} else {
if (isBlacklisted) {
incrementPacketBlacklisted();
}
packet = "";
}
return packet;
@@ -304,10 +327,12 @@ namespace LoRa_Utils {
SYSLOG_Utils::log(0, packet, rssi, snr, freqError); // CRC
}
packet = "";
incrementPacketReceiveFail();
} else {
Utils::print(F("failed, code "));
Utils::println(String(state));
packet = "";
incrementPacketReceiveFail();
}
}
}
@@ -322,4 +347,89 @@ namespace LoRa_Utils {
radio.sleep();
}
bool isBeyondWindow(unsigned long time) {
unsigned long window = 24*60*60*1000;
if (time > packetCountStartTime + window) {
return true;
}
return false;
}
void resetPacketCounters(unsigned long time) {
packetCountStartTime = time;
packetSendSuccess = 0;
packetSendFail = 0;
packetReceiveSuccess = 0;
packetReceiveFail = 0;
}
void incrementPacketSendSuccess() {
unsigned int now = millis();
if (isBeyondWindow(now)) {
resetPacketCounters(now);
packetSendSuccess = 1;
} else {
packetSendSuccess++;
}
}
unsigned int getPacketSendSuccess() {
return packetSendSuccess;
}
void incrementPacketSendFail() {
unsigned int now = millis();
if (isBeyondWindow(now)) {
packetCountStartTime = now;
packetSendFail = 1;
} else {
packetSendFail++;
}
}
unsigned int getPacketSendFail() {
return packetSendFail;
}
void incrementPacketReceiveSuccess() {
unsigned int now = millis();
if (isBeyondWindow(now)) {
packetCountStartTime = now;
packetReceiveSuccess = 1;
} else {
packetReceiveSuccess++;
}
}
unsigned int getPacketReceiveSuccess() {
return packetReceiveSuccess;
}
void incrementPacketReceiveFail() {
unsigned int now = millis();
if (isBeyondWindow(now)) {
packetCountStartTime = now;
packetReceiveFail = 1;
} else {
packetReceiveFail++;
}
}
unsigned int getPacketReceiveFail() {
return packetReceiveFail;
}
void incrementPacketBlacklisted() {
unsigned int now = millis();
if (isBeyondWindow(now)) {
packetCountStartTime = now;
packetBlacklisted = 1;
} else {
packetBlacklisted++;
}
}
unsigned int getPacketBlacklisted() {
return packetBlacklisted;
}
}
+12 -10
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -29,9 +29,7 @@ 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) {
void writeFormatedTime(char* dst, size_t size) { // gets NTP time ("HH:MM:SS") and writes it to dst if valid
String t = NTP_Utils::getFormatedTime();
if (t.length() == 8 && t.charAt(2) == ':') {
t.toCharArray(dst, size);
@@ -40,10 +38,8 @@ namespace MAP_Utils {
}
}
// 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
void upsert(const String& callsign, float latitude, float longitude, const String& path, const String& symbol, int rssi, float snr) { // Upsert station only with valid position with callsign dedup.
for (auto &s : mapStations) { // 1) update
if (callsign.equals(s.callsign)) {
s.latitude = latitude;
s.longitude = longitude;
@@ -51,13 +47,15 @@ namespace MAP_Utils {
s.snr = snr;
s.count++;
symbol.toCharArray(s.symbol, sizeof(s.symbol));
path.toCharArray(s.path, sizeof(s.path));
if (path.indexOf('*') == -1) s.heardDirect = true; // se oyo directo alguna vez; no se resetea si despues llega repetido
writeFormatedTime(s.lastHeard, sizeof(s.lastHeard));
s.lastHeardMillis = millis();
return;
}
}
if (mapStations.size() >= MAX_MAP_STATIONS) { // 2) llena -> descartar la mas vieja
if (mapStations.size() >= MAX_MAP_STATIONS) { // 2) full? delete oldest one
size_t oldest = 0;
for (size_t i = 1; i < mapStations.size(); i++) {
if (mapStations[i].lastHeardMillis < mapStations[oldest].lastHeardMillis) {
@@ -67,14 +65,16 @@ namespace MAP_Utils {
mapStations.erase(mapStations.begin() + oldest);
}
MapStation st = {}; // 3) insertar nueva
MapStation st = {}; // 3) insert new
callsign.toCharArray(st.callsign, sizeof(st.callsign));
symbol.toCharArray(st.symbol, sizeof(st.symbol));
path.toCharArray(st.path, sizeof(st.path));
st.latitude = latitude;
st.longitude = longitude;
st.rssi = rssi;
st.snr = snr;
st.count = 1;
st.heardDirect = (path.indexOf('*') == -1); // primer paquete: directo si no trae digis usados
writeFormatedTime(st.lastHeard, sizeof(st.lastHeard));
st.lastHeardMillis = millis();
mapStations.push_back(st);
@@ -102,6 +102,8 @@ namespace MAP_Utils {
data[i]["SNR"] = mapStations[i].snr;
data[i]["count"] = mapStations[i].count;
data[i]["lastHeard"] = mapStations[i].lastHeard;
data[i]["path"] = mapStations[i].path;
data[i]["heardDirect"] = mapStations[i].heardDirect;
}
String buffer;
+10 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -98,4 +98,13 @@ namespace MQTT_Utils {
pubSub.setCallback(receivedFromMqtt);
}
String getMQTTStatus() {
if (!Config.mqtt.active) {
return "Inactive";
}
if (pubSub.connected()) {
return "Connected";
}
return "Not connected";
}
}
+18
View File
@@ -1,3 +1,21 @@
/* Copyright (C) 2026 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 <Arduino.h>
#include "network_manager.h"
+20 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -63,4 +63,23 @@ namespace NTP_Utils {
return "DigiEcoMode Active";
}
unsigned long getTimeMillis() {
if (networkManager->isConnected() && Config.digi.ecoMode == 0 && timeClient != nullptr) {
return timeClient->getEpochTime()*1000;
}
return millis();
}
String getNTPStatus() {
if ((networkManager == nullptr) || (timeClient == nullptr)) {
return "Not connected";
}
if (networkManager->isConnected() && Config.digi.ecoMode != 0) {
return "DigiEcoMode Active";
}
if (timeClient->isTimeSet()) {
return "OK";
}
return "Error";
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+4 -4
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -82,7 +82,7 @@ namespace QUERY_Utils {
int radioTxActive = Config.loramodule.txActive;
bool onlyRadioActive = radioTxActive && Config.loramodule.rxActive && !Config.aprs_is.active;
if (queryQuestion.startsWith("?EM=OFF")) {
if ((digiMode == 2 || digiMode == 3) && onlyRadioActive) {
if ((digiMode == 1 || digiMode == 2 || digiMode == 3) && onlyRadioActive) {
if (digiEcoMode == 1 || digiEcoMode == 2) { // Exit Digipeater EcoMode or Digipeater without WiFiAP
answer = (digiEcoMode == 1) ? "DigiEcoMode:OFF" : "Digipeater + WiFiAP enabled";
Config.digi.ecoMode = 0;
@@ -97,7 +97,7 @@ namespace QUERY_Utils {
answer = "Digipeater Mode control not possible";
}
} else if (queryQuestion.startsWith("?EM=ON")) {
if ((digiMode == 2 || digiMode == 3) && onlyRadioActive) {
if ((digiMode == 1 || digiMode == 2 || digiMode == 3) && onlyRadioActive) {
if (digiEcoMode == 0) { // Start Digipeater EcoMode
answer = "DigiEcoMode:ON";
Config.digi.ecoMode = 1;
@@ -139,7 +139,7 @@ namespace QUERY_Utils {
if (answer == "") return "";
String queryAnswer = Config.callsign;
String queryAnswer = (Config.tacticalCallsign == "" ? Config.callsign : Config.tacticalCallsign);
queryAnswer += ">APLRG1";
if (queryFromAPRSIS) {
queryAnswer += ",TCPIP,qAC";
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+10 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -50,6 +50,7 @@ std::vector<Packet25SegBuffer> packet25SegBuffer;
bool saveNewDigiEcoModeConfig = false;
bool packetIsBeacon = false;
uint32_t bootTimeMillis = 0;
namespace STATION_Utils {
@@ -237,4 +238,12 @@ namespace STATION_Utils {
outputPacketBuffer.emplace_back(OutputPacketBuffer{packet, flag});
}
void markBootTime() {
bootTimeMillis = millis();
}
unsigned long getBootTime() {
return bootTimeMillis;
}
}
+15 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -79,11 +79,15 @@ namespace SYSLOG_Utils {
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else if (nextChar == '`') {
} else if (nextChar == '`' || nextChar == '\'') { // both Mic-E DTIs are valid
syslogPacket.concat("MIC-E / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(String(nextChar)); // restore the actual indicator received
syslogPacket.concat(packet.substring(colonIndex + 2));
// append distance for Mic-E, same as the GPS path
//syslogPacket.concat(" / ");
//syslogPacket.concat(GPS_Utils::getDistanceAndComment(packet));
} else if (nextChar == ';') {
syslogPacket.concat("OBJECT / ");
syslogPacket.concat(sender);
@@ -147,4 +151,13 @@ namespace SYSLOG_Utils {
}
}
String getSyslogStatus() {
if (Config.syslog.active) {
if (networkManager->isConnected()) {
return "Active";
}
return "Not connected";
}
return "Inactive";
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+92
View File
@@ -0,0 +1,92 @@
/* Copyright (C) 2026 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 "time_utils.h"
#include <iostream>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>
namespace TIME_Utils {
String getFormattedTime(unsigned long timeMillis) {
// Split into total seconds and remaining milliseconds
std::time_t seconds = timeMillis / 1000;
// Convert seconds to UTC calendar structure safely
std::tm time_info;
gmtime_r(&seconds, &time_info); // POSIX safe variant
// Format the base date/time structure
char buffer[30];
std::strftime(buffer, sizeof(buffer), "%H:%M:%S", &time_info);
// Append fractional milliseconds and UTC anchor 'Z'
std::ostringstream oss;
oss << buffer;
return String(oss.str().c_str());
}
String getFormattedDateTime(unsigned long timeMillis) {
if (!isValid(timeMillis)) {
return "Not set";
}
// Split into total seconds and remaining milliseconds
std::time_t seconds = timeMillis / 1000;
// Convert seconds to UTC calendar structure safely
std::tm time_info;
gmtime_r(&seconds, &time_info); // POSIX safe variant
// Format the base date/time structure
char buffer[30];
std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%S", &time_info);
// Append fractional milliseconds and UTC anchor 'Z'
std::ostringstream oss;
oss << buffer;
return String(String(oss.str().c_str())+" "+String(timeMillis));
}
boolean isValid(unsigned long timeMillis) {
if (timeMillis < 1767225600) { // 2026-01-01T00:00:00
return false;
}
return true;
}
String generateDuration(unsigned long startMillis, unsigned long endMillis) {
unsigned long diff = endMillis - startMillis;
diff /= 1000; // convert to seconds
unsigned long seconds = diff - ((diff/60) * 60);
diff -= seconds;
diff /= 60; // convert to minutes
unsigned long minutes = diff - ((diff/60) * 60);
diff -= minutes;
diff /= 60; // convert to hours
unsigned long hours = diff - ((diff/24) * 24);
diff -= hours;
diff /= 24; // convert to days
unsigned long days = diff;
return String(days) + " d " + String(hours) + " h " + String(minutes) + " m " + String(seconds) + " s" ;
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+161 -4
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -23,6 +23,15 @@
#include "map_utils.h"
#include "display.h"
#include "utils.h"
#include "battery_utils.h"
#include "wx_utils.h"
#include "station_utils.h"
#include "ntp_utils.h"
#include "time_utils.h"
#include "aprs_is_utils.h"
#include "syslog_utils.h"
#include "mqtt_utils.h"
#include "lora_utils.h"
extern Configuration Config;
@@ -49,15 +58,36 @@ extern const char web_bootstrap_js[] asm("_binary_data_embed_bootstrap_js_gz_sta
extern const char web_bootstrap_js_end[] asm("_binary_data_embed_bootstrap_js_gz_end");
extern const size_t web_bootstrap_js_len = web_bootstrap_js_end - web_bootstrap_js;
extern const char web_leaflet_css[] asm("_binary_data_embed_leaflet_css_gz_start");
extern const char web_leaflet_css_end[] asm("_binary_data_embed_leaflet_css_gz_end");
extern const size_t web_leaflet_css_len = web_leaflet_css_end - web_leaflet_css;
extern const char web_leaflet_js[] asm("_binary_data_embed_leaflet_js_gz_start");
extern const char web_leaflet_js_end[] asm("_binary_data_embed_leaflet_js_gz_end");
extern const size_t web_leaflet_js_len = web_leaflet_js_end - web_leaflet_js;
// Declare external symbols for the embedded image data
extern const unsigned char favicon_data[] asm("_binary_data_embed_favicon_png_gz_start");
extern const unsigned char favicon_data_end[] asm("_binary_data_embed_favicon_png_gz_end");
extern const size_t favicon_data_len = favicon_data_end - favicon_data;
extern const unsigned char aprs_symbols_24_0_data[] asm("_binary_data_embed_aprs_symbols_24_0_png_gz_start");
extern const unsigned char aprs_symbols_24_0_data_end[] asm("_binary_data_embed_aprs_symbols_24_0_png_gz_end");
extern const size_t aprs_symbols_24_0_data_len = aprs_symbols_24_0_data_end - aprs_symbols_24_0_data;
extern const unsigned char aprs_symbols_24_1_data[] asm("_binary_data_embed_aprs_symbols_24_1_png_gz_start");
extern const unsigned char aprs_symbols_24_1_data_end[] asm("_binary_data_embed_aprs_symbols_24_1_png_gz_end");
extern const size_t aprs_symbols_24_1_data_len = aprs_symbols_24_1_data_end - aprs_symbols_24_1_data;
extern const unsigned char aprs_symbols_24_2_data[] asm("_binary_data_embed_aprs_symbols_24_2_png_gz_start");
extern const unsigned char aprs_symbols_24_2_data_end[] asm("_binary_data_embed_aprs_symbols_24_2_png_gz_end");
extern const size_t aprs_symbols_24_2_data_len = aprs_symbols_24_2_data_end - aprs_symbols_24_2_data;
namespace WEB_Utils {
AsyncWebServer server(80);
unsigned int loginFailCount = 0;
void handleNotFound(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(404, "text/plain", "Not found");
@@ -70,9 +100,10 @@ namespace WEB_Utils {
}
void handleHome(AsyncWebServerRequest *request) {
if(Config.webadmin.active && !request->authenticate(Config.webadmin.username.c_str(), Config.webadmin.password.c_str()))
if(Config.webadmin.active && !request->authenticate(Config.webadmin.username.c_str(), Config.webadmin.password.c_str())) {
loginFailCount++;
return request->requestAuthentication();
}
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", (const uint8_t*)web_index_html, web_index_html_len);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
@@ -84,9 +115,47 @@ namespace WEB_Utils {
request->send(response);
}
void handleAprsSymbols24Primary(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(
200,
"image/png",
(const uint8_t*)aprs_symbols_24_0_data,
aprs_symbols_24_0_data_len
);
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Cache-Control", "max-age=3600");
request->send(response);
}
void handleAprsSymbols24Alternate(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(
200,
"image/png",
(const uint8_t*)aprs_symbols_24_1_data,
aprs_symbols_24_1_data_len
);
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Cache-Control", "max-age=3600");
request->send(response);
}
void handleAprsSymbols24Overlay(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(
200,
"image/png",
(const uint8_t*)aprs_symbols_24_2_data,
aprs_symbols_24_2_data_len
);
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Cache-Control", "max-age=3600");
request->send(response);
}
void handleReadConfiguration(AsyncWebServerRequest *request) {
if(Config.webadmin.active && !request->authenticate(Config.webadmin.username.c_str(), Config.webadmin.password.c_str()))
if(Config.webadmin.active && !request->authenticate(Config.webadmin.username.c_str(), Config.webadmin.password.c_str())) {
loginFailCount++;
return request->requestAuthentication();
}
File file = SPIFFS.open("/igate_conf.json");
@@ -115,6 +184,74 @@ namespace WEB_Utils {
request->send(200, "application/json", buffer);
}
void handleStationStatus(AsyncWebServerRequest *request) {
JsonDocument data;
// voltage
char voltageInfo[10]; // Enough to hold "xx.xx\0"
snprintf(voltageInfo, sizeof(voltageInfo), "%.2f", BATTERY_Utils::checkInternalVoltage());
data["stationStatus"]["voltageInternal"] = voltageInfo;
snprintf(voltageInfo, sizeof(voltageInfo), "%.2f", BATTERY_Utils::checkExternalVoltage());
data["stationStatus"]["voltageExternal"] = voltageInfo;
// weather
boolean weatherModulePresent = WX_Utils::isWeatherModulePresent();
data["stationStatus"]["weatherModulePresent"] = weatherModulePresent;
if (weatherModulePresent) {
data["stationStatus"]["weatherTemperature"] = WX_Utils::getTemperature();
data["stationStatus"]["weatherHumidity"] = WX_Utils::getHumidity();
data["stationStatus"]["weatherBarometricPressure"] = WX_Utils::getBarometricPressure();
data["stationStatus"]["weatherGas"] = WX_Utils::getGas();
} else {
data["stationStatus"]["weatherTemperature"] = "Unknown";
data["stationStatus"]["weatherHumidity"] = "Unknown";
data["stationStatus"]["weatherBarometricPressure"] = "Unknown";
data["stationStatus"]["weatherGas"] = "Unknown";
}
// elapsed time
unsigned long now = millis();
unsigned long bootTime = STATION_Utils::getBootTime();
data["stationStatus"]["elapsedTime"] = TIME_Utils::generateDuration(bootTime, now);
// logins
data["stationStatus"]["loginsInvalid"] = getLoginFailCount();
data["stationStatus"]["webAdminActive"] = Config.webadmin.active;
// packets
data["stationStatus"]["packetsRFSendSuccess"] = LoRa_Utils::getPacketSendSuccess();
data["stationStatus"]["packetsRFReceiveSuccess"] = LoRa_Utils::getPacketReceiveSuccess();
data["stationStatus"]["packetsISSendSuccess"] = APRS_IS_Utils::getPacketSendSuccess();
data["stationStatus"]["packetsISReceiveSuccess"] = APRS_IS_Utils::getPacketReceiveSuccess();
data["stationStatus"]["packetsRFSendFail"] = LoRa_Utils::getPacketSendFail();
data["stationStatus"]["packetsRFReceiveFail"] = LoRa_Utils::getPacketReceiveFail();
data["stationStatus"]["packetsISSendFail"] = APRS_IS_Utils::getPacketSendFail();
data["stationStatus"]["packetsISReceiveFail"] = APRS_IS_Utils::getPacketReceiveFail();
data["stationStatus"]["packetsBlacklisted"] = LoRa_Utils::getPacketBlacklisted();
// connectivity / service status
data["stationStatus"]["statusWifi"] = APRS_IS_Utils::getWifiState();
data["stationStatus"]["statusIS"] = APRS_IS_Utils::getAPRSISState();
data["stationStatus"]["statusNTP"] = NTP_Utils::getNTPStatus();
data["stationStatus"]["statusSyslog"] = SYSLOG_Utils::getSyslogStatus();
data["stationStatus"]["statusMQTT"] = MQTT_Utils::getMQTTStatus();
// misc
data["stationStatus"]["currentTimeRaw"] = now;
data["stationStatus"]["bootTimeRaw"] = bootTime;
data["stationStatus"]["elapsedTimeRaw"] = now - bootTime;
String buffer;
serializeJson(data, buffer);
request->send(200, "application/json", buffer);
}
unsigned long getLoginFailCount() {
return loginFailCount;
}
void handleStations(AsyncWebServerRequest *request) {
request->send(200, "application/json", MAP_Utils::getStationsJson());
}
@@ -363,11 +500,26 @@ namespace WEB_Utils {
request->send(response);
}
void handleLeafletStyle(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(200, "text/css", (const uint8_t*)web_leaflet_css, web_leaflet_css_len);
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Cache-Control", "max-age=3600");
request->send(response);
}
void handleLeafletScript(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(200, "text/javascript", (const uint8_t*)web_leaflet_js, web_leaflet_js_len);
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Cache-Control", "max-age=3600");
request->send(response);
}
void setup() {
if (Config.digi.ecoMode == 0) {
server.on("/", HTTP_GET, handleHome);
server.on("/status", HTTP_GET, handleStatus);
server.on("/received-packets.json", HTTP_GET, handleReceivedPackets);
server.on("/station-status.json", HTTP_GET, handleStationStatus);
server.on("/stations.json", HTTP_GET, handleStations);
server.on("/configuration.json", HTTP_GET, handleReadConfiguration);
server.on("/configuration.json", HTTP_POST, handleWriteConfiguration);
@@ -376,7 +528,12 @@ namespace WEB_Utils {
server.on("/script.js", HTTP_GET, handleScript);
server.on("/bootstrap.css", HTTP_GET, handleBootstrapStyle);
server.on("/bootstrap.js", HTTP_GET, handleBootstrapScript);
server.on("/leaflet.css", HTTP_GET, handleLeafletStyle);
server.on("/leaflet.js", HTTP_GET, handleLeafletScript);
server.on("/favicon.png", HTTP_GET, handleFavicon);
server.on("/aprs-symbols-24-0.png", HTTP_GET, handleAprsSymbols24Primary);
server.on("/aprs-symbols-24-1.png", HTTP_GET, handleAprsSymbols24Alternate);
server.on("/aprs-symbols-24-2.png", HTTP_GET, handleAprsSymbols24Overlay);
OTA_Utils::setup(&server); // Include OTA Updater for WebServer
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+18 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -39,6 +39,7 @@ int wxModuleType = 0;
uint8_t wxModuleAddress = 0x00;
float newHum, newTemp, newPress, newGas;
bool weatherModulePresent = false;
Adafruit_BME280 bme280;
@@ -175,6 +176,7 @@ namespace WX_Utils {
break;
}
}
weatherModulePresent = wxModuleFound;
}
}
}
@@ -334,4 +336,19 @@ namespace WX_Utils {
}
}
float getHumidity() {
return newHum;
}
float getTemperature() {
return newTemp;
}
float getBarometricPressure() {
return newPress;
}
float getGas() {
return newGas;
}
boolean isWeatherModulePresent() {
return weatherModulePresent;
}
}
+17 -12
View File
@@ -1,17 +1,17 @@
# Copyright (C) 2025 Ricardo Guzman - CA2RXU
#
# Copyright (C) 2026 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
# 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/>.
@@ -26,7 +26,12 @@ files = [
'data_embed/style.css',
'data_embed/bootstrap.js',
'data_embed/bootstrap.css',
'data_embed/leaflet.js',
'data_embed/leaflet.css',
'data_embed/favicon.png',
'data_embed/aprs-symbols-24-0.png',
'data_embed/aprs-symbols-24-1.png',
'data_embed/aprs-symbols-24-2.png',
]
string_to_find_str = "String"
@@ -40,20 +45,20 @@ with open('src/LoRa_APRS_iGate.cpp', encoding='utf-8') as cpp_file:
if start > 0 and end > start:
versionDate = line[start:end]
break
for src in files:
out = src + ".gz"
with open(src, 'rb') as f:
content = f.read()
if src == 'data_embed/index.html':
env_vars = env["BOARD"] + "<br>" + ','.join(env["BUILD_FLAGS"]).replace('-Werror -Wall,', '').replace(',-DELEGANTOTA_USE_ASYNC_WEBSERVER=1', '') + "<br>" + "Version date: " + versionDate
current_date = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') + " UTC"
build_info = f'{env_vars}<br>Build date: {current_date}'.encode()
content = content.replace(b'%BUILD_INFO%', build_info)
with open(out, 'wb') as f:
f.write(gzip.compress(content, compresslevel=9))
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff. Show more