mirror of
https://github.com/richonguzman/LoRa_APRS_iGate.git
synced 2026-10-31 02:51:58 +01:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
463dd6342b | ||
|
|
505f0d6f41 | ||
|
|
01befdef92 | ||
|
|
f14b6b3cd9 |
No files matched your search
+516
-11
@@ -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">
|
||||
@@ -1823,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>
|
||||
@@ -2400,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");});
|
||||
@@ -2415,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);};}
|
||||
|
||||
@@ -566,6 +566,7 @@ function checkConnection() {
|
||||
}, 3000);
|
||||
|
||||
fetchSettings();
|
||||
fetchStationStatus();
|
||||
})
|
||||
.catch((err) => {
|
||||
setTimeout(checkConnection, 0);
|
||||
@@ -635,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) ---------- */
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -28,6 +28,7 @@ namespace MQTT_Utils {
|
||||
void connect();
|
||||
void loop();
|
||||
void setup();
|
||||
String getMQTTStatus();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace NTP_Utils {
|
||||
bool setup();
|
||||
void update();
|
||||
String getFormatedTime();
|
||||
unsigned long getTimeMillis();
|
||||
String getNTPStatus();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ namespace STATION_Utils {
|
||||
void processOutputPacketBufferUltraEcoMode();
|
||||
void processOutputPacketBuffer();
|
||||
void addToOutputPacketBuffer(const String& packet, bool flag = false);
|
||||
void markBootTime();
|
||||
unsigned long getBootTime();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -38,6 +38,8 @@ namespace WEB_Utils {
|
||||
|
||||
void setup();
|
||||
|
||||
unsigned long getLoginFailCount();
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
+5
-1
@@ -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
|
||||
@@ -63,12 +63,13 @@ ___________________________________________________________________*/
|
||||
#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-09-07";
|
||||
String versionDate = "2026-09-08";
|
||||
String versionNumber = "4.0.1";
|
||||
Configuration Config;
|
||||
WiFiClient aprsIsClient;
|
||||
@@ -128,6 +129,7 @@ void setup() {
|
||||
Utils::checkRebootMode();
|
||||
APRS_IS_Utils::firstConnection();
|
||||
SLEEP_Utils::checkSerial();
|
||||
STATION_Utils::markBootTime(); // should be last
|
||||
}
|
||||
|
||||
void loop() {
|
||||
|
||||
+113
-14
@@ -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;
|
||||
}
|
||||
}
|
||||
+105
-4
@@ -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,15 @@ 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++) {
|
||||
@@ -234,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
|
||||
@@ -272,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();
|
||||
@@ -291,8 +301,9 @@ namespace LoRa_Utils {
|
||||
receivedPackets.push_back(receivedPacket);
|
||||
|
||||
APRSPacket aprsPacket = APRSPacketLib::processReceivedPacket(packet.substring(3), rssi, snr, freqError);
|
||||
if (aprsPacket.type == 0 || aprsPacket.type == 4) { // 0 = GPS, 4 = Mic-E (only ones with position)
|
||||
MAP_Utils::upsert(aprsPacket.sender, aprsPacket.latitude, aprsPacket.longitude, aprsPacket.path, aprsPacket.overlay + aprsPacket.symbol, aprsPacket.rssi, aprsPacket.snr);
|
||||
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.path, aprsPacket.rssi, aprsPacket.snr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +311,9 @@ namespace LoRa_Utils {
|
||||
SYSLOG_Utils::log(1, packet, rssi, snr, freqError); // RX
|
||||
}
|
||||
} else {
|
||||
if (isBlacklisted) {
|
||||
incrementPacketBlacklisted();
|
||||
}
|
||||
packet = "";
|
||||
}
|
||||
return packet;
|
||||
@@ -313,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -151,4 +151,13 @@ namespace SYSLOG_Utils {
|
||||
}
|
||||
}
|
||||
|
||||
String getSyslogStatus() {
|
||||
if (Config.syslog.active) {
|
||||
if (networkManager->isConnected()) {
|
||||
return "Active";
|
||||
}
|
||||
return "Not connected";
|
||||
}
|
||||
return "Inactive";
|
||||
}
|
||||
}
|
||||
@@ -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" ;
|
||||
}
|
||||
|
||||
}
|
||||
+85
-3
@@ -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;
|
||||
@@ -78,6 +87,7 @@ extern const size_t aprs_symbols_24_2_data_len = aprs_symbols_24_2_data_end - ap
|
||||
namespace WEB_Utils {
|
||||
|
||||
AsyncWebServer server(80);
|
||||
unsigned int loginFailCount = 0;
|
||||
|
||||
void handleNotFound(AsyncWebServerRequest *request) {
|
||||
AsyncWebServerResponse *response = request->beginResponse(404, "text/plain", "Not found");
|
||||
@@ -90,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);
|
||||
@@ -141,8 +152,10 @@ namespace WEB_Utils {
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
@@ -171,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());
|
||||
}
|
||||
@@ -438,6 +519,7 @@ namespace WEB_Utils {
|
||||
server.on("/", HTTP_GET, handleHome);
|
||||
server.on("/status", HTTP_GET, handleStatus);
|
||||
server.on("/received-packets.json", HTTP_GET, handleReceivedPackets);
|
||||
server.on("/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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user