mirror of
https://github.com/richonguzman/LoRa_APRS_iGate.git
synced 2026-10-31 11:01:58 +01:00
Compare commits
110
Commits
No files matched your search
@@ -13,12 +13,18 @@ jobs:
|
||||
target:
|
||||
- name: ttgo-lora32-v21
|
||||
chip: esp32
|
||||
- name: ttgo-lora32-v21_GPS
|
||||
chip: esp32
|
||||
- name: ttgo-lora32-v21_915
|
||||
chip: esp32
|
||||
- name: ttgo-lora32-v21_915_GPS
|
||||
chip: esp32
|
||||
- name: ttgo_lora32_t3s3_v1_2
|
||||
chip: esp32s3
|
||||
- name: heltec-lora32-v2
|
||||
chip: esp32
|
||||
- name: heltec-lora32-v2_915
|
||||
chip: esp32
|
||||
- name: heltec_wifi_lora_32_V3
|
||||
chip: esp32s3
|
||||
- name: heltec_wifi_lora_32_V3_2
|
||||
@@ -53,6 +59,8 @@ jobs:
|
||||
chip: esp32
|
||||
- name: ttgo-t-beam-v1_SX1268
|
||||
chip: esp32
|
||||
- name: ttgo-t-beam-v1_SX1262
|
||||
chip: esp32
|
||||
- name: ttgo-t-beam-v1_2_SX1262
|
||||
chip: esp32
|
||||
- name: ttgo_t_deck_plus
|
||||
@@ -93,10 +101,14 @@ jobs:
|
||||
chip: esp32s3
|
||||
- name: QRPLabs_LightGateway_Plus_1_0
|
||||
chip: esp32s3
|
||||
- name: RPC_LORA_DIGIGATE_1W
|
||||
chip: esp32
|
||||
- name: XIAO_ESP32S3_WIO_SX1262
|
||||
chip: esp32s3
|
||||
- name: TROY_LoRa_APRS
|
||||
chip: esp32
|
||||
chip: esp32
|
||||
- name: ESP32_9M2IBR_1W_LoRa_GPS
|
||||
chip: esp32
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -109,7 +121,7 @@ jobs:
|
||||
|
||||
- name: Build target
|
||||
run: |
|
||||
pio run -e ${{ matrix.target.name }}
|
||||
pio run -e ${{ matrix.target.name }}
|
||||
|
||||
- name: Build FS
|
||||
run: |
|
||||
@@ -124,7 +136,7 @@ jobs:
|
||||
cp .pio/build/${{ matrix.target.name }}/partitions.bin installer/firmware/
|
||||
cp .pio/build/${{ matrix.target.name }}/spiffs.bin installer/firmware/
|
||||
cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin installer/firmware/
|
||||
|
||||
|
||||
- name: Merge for web flashing
|
||||
run: |
|
||||
if [ "${{ matrix.target.chip }}" == "esp32" ]; then
|
||||
|
||||
Vendored
+2
-1
@@ -5,6 +5,7 @@
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
"ms-vscode.cpptools-extension-pack",
|
||||
"pioarduino.pioarduino-ide"
|
||||
]
|
||||
}
|
||||
@@ -16,23 +16,32 @@ ____________________________________________________
|
||||
|
||||
____________________________________________________
|
||||
|
||||
## New Web/UI
|
||||
|
||||
| New Menu | Map |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
____________________________________________________
|
||||
|
||||
## 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,9 +57,19 @@ ____________________________________________________
|
||||
|
||||
- Wemos Lolin32 Oled + SX1278 DIY Version.
|
||||
|
||||
- and many more...
|
||||
|
||||
<br />
|
||||
|
||||
# Timeline (Versions):
|
||||
- 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.
|
||||
- 2026-02-15 Digipeater code/logic improved.
|
||||
- 2026-02-08 Heltec V2 915MHz added.
|
||||
- 2026-02-05 Improved "digiBackupMode" to check "APRS-IS" server connection.
|
||||
- 2026-01-07 Tactical Callsign added.
|
||||
- 2026-01-05 Heltec V4 support added.
|
||||
- 2025-12-22 Heltec Wireless Paper V1.2 and VisionMaster E290 Added. Thanks HA5SZI.
|
||||
@@ -85,7 +104,7 @@ ____________________________________________________
|
||||
- 2024.10.08 New EcoMode for Remote Digipeaters without WiFi/WiFiAP, Screen, Leds (Example: LILYGO LoRa32 uses only 24mA, with WifiAP 150mA). APRS Message/Queries can start/stop this mode too.
|
||||
- 2024.10.06 Cross Frequency Digipeater Rules added.
|
||||
- 2024.09.23 Libraries Update for SDK3
|
||||
- 2024.09.23 Added Enconded Telemetry for Battery (+ External Voltage) in Station GPS Beacon Packet.
|
||||
- 2024.09.23 Added Enconded Telemetry for Battery (+ External Voltage) in Station GPS Beacon Packet.
|
||||
- 2024.08.23 Wemos S2 Mini DIY LoRa added.
|
||||
- 2024.08.19 HELTEC Wireless Paper working (still missing Epaper code).
|
||||
- 2024.08.13 Web Authentication for WebUI. Thanks Mitja S57PNX.
|
||||
|
||||
+12
-12
@@ -23,25 +23,25 @@ build_flags =
|
||||
-D RADIOLIB_EXCLUDE_SSTV=1
|
||||
-I variants/${PIOENV}
|
||||
lib_deps =
|
||||
adafruit/Adafruit Unified Sensor @ 1.1.14
|
||||
adafruit/Adafruit AHTX0 @ 2.0.5
|
||||
adafruit/Adafruit BME280 Library @ 2.2.4
|
||||
adafruit/Adafruit BMP280 Library @ 2.6.8
|
||||
adafruit/Adafruit BME680 Library @ 2.0.4
|
||||
adafruit/Adafruit Unified Sensor @ 1.1.15
|
||||
adafruit/Adafruit AHTX0 @ 2.0.6
|
||||
adafruit/Adafruit BME280 Library @ 2.3.0
|
||||
adafruit/Adafruit BMP280 Library @ 3.0.0
|
||||
adafruit/Adafruit BME680 Library @ 2.0.6
|
||||
adafruit/Adafruit INA219 @ 1.2.3
|
||||
adafruit/Adafruit Si7021 Library @ 1.5.3
|
||||
arduino-libraries/NTPClient @ 3.2.1
|
||||
ayushsharma82/ElegantOTA @ 3.1.7
|
||||
bblanchon/ArduinoJson @ 6.21.3
|
||||
jgromes/RadioLib @ 7.1.0
|
||||
bblanchon/ArduinoJson @ 7.4.3
|
||||
jgromes/RadioLib @ 7.6.0
|
||||
knolleary/PubSubClient @ 2.8
|
||||
ESP32Async/AsyncTCP @ 3.4.9
|
||||
ESP32Async/ESPAsyncWebServer @ 3.9.3
|
||||
ESP32Async/AsyncTCP @ 3.4.10
|
||||
ESP32Async/ESPAsyncWebServer @ 3.11.1
|
||||
mikalhart/TinyGPSPlus @ 1.0.3
|
||||
richonguzman/APRSPacketLib @ 1.0.4
|
||||
display_libs =
|
||||
adafruit/Adafruit GFX Library @ 1.11.9
|
||||
adafruit/Adafruit SSD1306 @ 2.5.10
|
||||
adafruit/Adafruit GFX Library @ 1.12.6
|
||||
adafruit/Adafruit SSD1306 @ 2.5.17
|
||||
usb_flags=
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
@@ -28,9 +28,9 @@
|
||||
"messagesToRF": false,
|
||||
"objectsToRF": false,
|
||||
"server": "rotate.aprs2.net",
|
||||
"passcode": "XYZVW",
|
||||
"passcode": "XYZVW",
|
||||
"port": 14580,
|
||||
"filter": "m/10"
|
||||
"filter": "m/10"
|
||||
},
|
||||
"personalNote": "",
|
||||
"blacklist": "",
|
||||
@@ -47,10 +47,11 @@
|
||||
"rxSignalBandwidth": 125000,
|
||||
"txActive": false,
|
||||
"txFreq": 433775000,
|
||||
"txSpreadingFactor": 12,
|
||||
"txCodingRate4": 5,
|
||||
"txSpreadingFactor": 9,
|
||||
"txCodingRate4": 7,
|
||||
"txSignalBandwidth": 125000,
|
||||
"power": 20
|
||||
"power": 20,
|
||||
"cadActive": true
|
||||
},
|
||||
"display": {
|
||||
"alwaysOn": true,
|
||||
|
||||
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 |
+1119
-961
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+527
-7
@@ -118,7 +118,7 @@ function loadSettings(settings) {
|
||||
document.getElementById("beacon.latitude").value = settings.beacon.latitude;
|
||||
document.getElementById("beacon.longitude").value = settings.beacon.longitude;
|
||||
document.getElementById("beacon.interval").value = settings.beacon.interval;
|
||||
document.getElementById("other.rememberStationTime").value = settings.other.rememberStationTime;
|
||||
document.getElementById("other.rememberStationTime").value = settings.other.rememberStationTime;
|
||||
document.getElementById("beacon.sendViaAPRSIS").checked = settings.beacon.sendViaAPRSIS;
|
||||
|
||||
document.getElementById("beacon.sendViaRF").checked = settings.beacon.sendViaRF;
|
||||
@@ -148,12 +148,26 @@ function loadSettings(settings) {
|
||||
document.getElementById("lora.rxSpreadingFactor").value = settings.lora.rxSpreadingFactor;
|
||||
document.getElementById("lora.rxCodingRate4").value = settings.lora.rxCodingRate4;
|
||||
document.getElementById("lora.rxSignalBandwidth").value = settings.lora.rxSignalBandwidth;
|
||||
LoRaRxCheckbox.checked = settings.lora.rxActive;
|
||||
LoRaRxFreq.disabled = !LoRaRxCheckbox.checked;
|
||||
LoRaRxSpreadingFactor.disabled = !LoRaRxCheckbox.checked;
|
||||
LoRaRxCodingRate.disabled = !LoRaRxCheckbox.checked;
|
||||
LoRaRxSignalBandwidth.disabled = !LoRaRxCheckbox.checked;
|
||||
|
||||
document.getElementById("lora.txActive").checked = settings.lora.txActive;
|
||||
document.getElementById("lora.txFreq").value = settings.lora.txFreq;
|
||||
document.getElementById("lora.txSpreadingFactor").value = settings.lora.txSpreadingFactor;
|
||||
document.getElementById("lora.txCodingRate4").value = settings.lora.txCodingRate4;
|
||||
document.getElementById("lora.txSignalBandwidth").value = settings.lora.txSignalBandwidth;
|
||||
document.getElementById("lora.txSignalBandwidth").value = settings.lora.txSignalBandwidth;
|
||||
document.getElementById("lora.power").value = settings.lora.power;
|
||||
document.getElementById("lora.cadActive").checked = settings.lora.cadActive;
|
||||
LoRaTxCheckbox.checked = settings.lora.txActive;
|
||||
LoRaTxFreq.disabled = !LoRaTxCheckbox.checked;
|
||||
LoRaTxSpreadingFactor.disabled = !LoRaTxCheckbox.checked;
|
||||
LoRaTxCodingRate.disabled = !LoRaTxCheckbox.checked;
|
||||
LoRaTxSignalBandwidth.disabled = !LoRaTxCheckbox.checked;
|
||||
LoRaPower.disabled = !LoRaTxCheckbox.checked;
|
||||
LoRaCadActive.disabled = !LoRaTxCheckbox.checked;
|
||||
|
||||
// Display
|
||||
document.getElementById("display.alwaysOn").checked = settings.display.alwaysOn;
|
||||
@@ -195,7 +209,7 @@ function loadSettings(settings) {
|
||||
TelemetryCheckbox.checked = settings.wxsensor.active;
|
||||
TelemetryHeightCorrection.disabled = !TelemetryCheckbox.checked;
|
||||
TelemetryTempCorrection.disabled = !TelemetryCheckbox.checked;
|
||||
|
||||
|
||||
// SYSLOG
|
||||
document.getElementById("syslog.active").checked = settings.syslog.active;
|
||||
document.getElementById("syslog.server").value = settings.syslog.server;
|
||||
@@ -205,13 +219,14 @@ function loadSettings(settings) {
|
||||
SyslogServer.disabled = !SyslogCheckbox.checked;
|
||||
SyslogPort.disabled = !SyslogCheckbox.checked;
|
||||
SyslogBeaconOverTCPIP.disabled = !SyslogCheckbox.checked;
|
||||
|
||||
|
||||
// TNC
|
||||
if (settings.tnc) {
|
||||
document.getElementById("tnc.enableServer").checked = settings.tnc.enableServer;
|
||||
document.getElementById("tnc.enableSerial").checked = settings.tnc.enableSerial;
|
||||
document.getElementById("tnc.acceptOwn").checked = settings.tnc.acceptOwn;
|
||||
document.getElementById("tnc.aprsBridgeActive").checked = settings.tnc.aprsBridgeActive;
|
||||
document.getElementById("tnc.kissProtocol").value = settings.tnc.kissProtocol ? "KISS" : "TNC2";
|
||||
}
|
||||
|
||||
// MQTT
|
||||
@@ -237,8 +252,10 @@ function loadSettings(settings) {
|
||||
RebootModeTime.disabled = !RebootModeCheckbox.check;
|
||||
|
||||
// WiFi Auto AP
|
||||
document.getElementById("wifi.autoAP.enabled").checked = settings.wifi.autoAP.enabled;
|
||||
document.getElementById("wifi.autoAP.password").value = settings.wifi.autoAP.password;
|
||||
document.getElementById("wifi.autoAP.timeout").value = settings.wifi.autoAP.timeout;
|
||||
toggleWiFiAutoAPFields();
|
||||
|
||||
// OTA
|
||||
document.getElementById("ota.username").value = settings.ota.username;
|
||||
@@ -388,6 +405,36 @@ TelemetryCheckbox.addEventListener("change", function () {
|
||||
TelemetryTempCorrection.disabled = !this.checked;
|
||||
});
|
||||
|
||||
// LoRa Rx Switch
|
||||
const LoRaRxCheckbox = document.querySelector('input[name="lora.rxActive"]');
|
||||
const LoRaRxFreq = document.querySelector('[name="lora.rxFreq"]');
|
||||
const LoRaRxSpreadingFactor = document.querySelector('[name="lora.rxSpreadingFactor"]');
|
||||
const LoRaRxCodingRate = document.querySelector('[name="lora.rxCodingRate4"]');
|
||||
const LoRaRxSignalBandwidth = document.querySelector('[name="lora.rxSignalBandwidth"]');
|
||||
LoRaRxCheckbox.addEventListener("change", function () {
|
||||
LoRaRxFreq.disabled = !this.checked;
|
||||
LoRaRxSpreadingFactor.disabled = !this.checked;
|
||||
LoRaRxCodingRate.disabled = !this.checked;
|
||||
LoRaRxSignalBandwidth.disabled = !this.checked;
|
||||
});
|
||||
|
||||
// LoRa Tx Switch
|
||||
const LoRaTxCheckbox = document.querySelector('input[name="lora.txActive"]');
|
||||
const LoRaTxFreq = document.querySelector('[name="lora.txFreq"]');
|
||||
const LoRaTxSpreadingFactor = document.querySelector('[name="lora.txSpreadingFactor"]');
|
||||
const LoRaTxCodingRate = document.querySelector('[name="lora.txCodingRate4"]');
|
||||
const LoRaTxSignalBandwidth = document.querySelector('[name="lora.txSignalBandwidth"]');
|
||||
const LoRaPower = document.querySelector('[name="lora.power"]');
|
||||
const LoRaCadActive = document.querySelector('[name="lora.cadActive"]');
|
||||
LoRaTxCheckbox.addEventListener("change", function () {
|
||||
LoRaTxFreq.disabled = !this.checked;
|
||||
LoRaTxSpreadingFactor.disabled = !this.checked;
|
||||
LoRaTxCodingRate.disabled = !this.checked;
|
||||
LoRaTxSignalBandwidth.disabled = !this.checked;
|
||||
LoRaPower.disabled = !this.checked;
|
||||
LoRaCadActive.disabled = !this.checked;
|
||||
});
|
||||
|
||||
// Syslog Switches
|
||||
const SyslogCheckbox = document.querySelector('input[name="syslog.active"]');
|
||||
const SyslogServer = document.querySelector('input[name="syslog.server"]');
|
||||
@@ -432,6 +479,17 @@ WebadminCheckbox.addEventListener("change", function () {
|
||||
WebadminPassword.disabled = !this.checked;
|
||||
});
|
||||
|
||||
// WiFi Auto AP Switches
|
||||
const WiFiAutoAPCheckbox = document.querySelector('input[name="wifi.autoAP.enabled"]');
|
||||
WiFiAutoAPCheckbox.addEventListener("change", function () {
|
||||
toggleWiFiAutoAPFields();
|
||||
});
|
||||
|
||||
function toggleWiFiAutoAPFields() {
|
||||
const isEnabled = WiFiAutoAPCheckbox.checked;
|
||||
const autoAPConfig = document.getElementById('wifi-autoap-config');
|
||||
if (autoAPConfig) autoAPConfig.style.display = isEnabled ? 'block' : 'none';
|
||||
}
|
||||
|
||||
document.querySelector(".new button").addEventListener("click", function () {
|
||||
const networksContainer = document.querySelector(".list-networks");
|
||||
@@ -545,7 +603,7 @@ function loadReceivedPackets(packets) {
|
||||
|
||||
packets.forEach((packet) => {
|
||||
const element = document.createElement("tr");
|
||||
|
||||
|
||||
element.innerHTML = `
|
||||
<td>${packet.rxTime}</td>
|
||||
<td>${packet.packet}</td>
|
||||
@@ -578,8 +636,470 @@ document.querySelector('a[href="/received-packets"]').addEventListener('click',
|
||||
|
||||
document.getElementById('received-packets').classList.remove('d-none');
|
||||
document.getElementById('configuration').classList.add('d-none');
|
||||
|
||||
|
||||
document.querySelector('button[type=submit]').remove();
|
||||
|
||||
fetchReceivedPackets();
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
/* ---------- Stations Map (Leaflet, CDN) ---------- */
|
||||
|
||||
let mapInstance = null;
|
||||
let mapMarkers = null;
|
||||
let mapTimer = null;
|
||||
let mapTileErrShown = false;
|
||||
let iGateLatLng = null;
|
||||
let mapAutoFitDone = false;
|
||||
|
||||
function setMapMessage(text) {
|
||||
const el = document.getElementById("map");
|
||||
if (el) {
|
||||
el.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--msh-text-dim,#8b949e);text-align:center;padding:20px;">${text}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* 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) => {
|
||||
if (s.lat === 0 && s.lon === 0) return; // descartar estaciones sin fix
|
||||
|
||||
const popup = `<b>${s.callsign}</b>`
|
||||
+ (s.lastHeard ? `<br>Last: ${s.lastHeard}` : "")
|
||||
+ `<br>RSSI ${s.RSSI} / SNR ${s.SNR}`
|
||||
+ `<br>Packets: ${s.count}`
|
||||
+ `<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();
|
||||
});
|
||||
}
|
||||
|
||||
drawn++;
|
||||
});
|
||||
|
||||
if (!mapAutoFitDone && drawn > 0) { // auto-fit la 1ª vez que hay estaciones tras abrir
|
||||
fitMapToStations();
|
||||
mapAutoFitDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Encuadra el mapa para que entren todas las estaciones (+ el iGate). 100% navegador.
|
||||
function fitMapToStations() {
|
||||
if (!mapInstance) return false;
|
||||
|
||||
const pts = [];
|
||||
if (mapMarkers) {
|
||||
mapMarkers.eachLayer((layer) => {
|
||||
if (layer.getLatLng) pts.push(layer.getLatLng());
|
||||
});
|
||||
}
|
||||
if (iGateLatLng) pts.push(iGateLatLng);
|
||||
|
||||
if (!pts.length) return false;
|
||||
mapInstance.fitBounds(L.latLngBounds(pts), { padding: [30, 30], maxZoom: 14 });
|
||||
return true;
|
||||
}
|
||||
|
||||
function fetchMapStations() {
|
||||
fetch("/stations.json")
|
||||
.then((response) => response.json())
|
||||
.then((stations) => {
|
||||
loadMapStations(stations);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
console.error(`Failed to load stations`);
|
||||
});
|
||||
}
|
||||
|
||||
window.showMap = function () {
|
||||
if (typeof L === "undefined") { // sin internet el CDN de Leaflet no cargó
|
||||
setMapMessage("Map unavailable — no internet connection.<br>The map needs internet to load (OpenStreetMap).");
|
||||
return;
|
||||
}
|
||||
|
||||
mapAutoFitDone = false; // re-encuadrar en cada apertura del Map
|
||||
|
||||
if (!mapInstance) {
|
||||
mapInstance = L.map("map");
|
||||
|
||||
const tiles = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 19,
|
||||
attribution: "© OpenStreetMap"
|
||||
}).addTo(mapInstance);
|
||||
|
||||
tiles.on("tileerror", function () { // tiles no cargan (sin salida a internet)
|
||||
if (!mapTileErrShown) {
|
||||
mapTileErrShown = true;
|
||||
if (window.showToast) showToast("Map tiles could not load — check internet connection.");
|
||||
}
|
||||
});
|
||||
|
||||
mapMarkers = L.layerGroup().addTo(mapInstance);
|
||||
|
||||
const FitControl = L.Control.extend({ // botón "Fit All Stations" (control nativo de Leaflet)
|
||||
options: { position: "topright" },
|
||||
onAdd: function () {
|
||||
const btn = L.DomUtil.create("button", "");
|
||||
btn.type = "button";
|
||||
btn.title = "Fit all stations";
|
||||
btn.textContent = "Fit All Stations";
|
||||
btn.style.cssText = "background:#fff;color:#333;border:2px solid rgba(0,0,0,.2);border-radius:4px;padding:5px 9px;cursor:pointer;font:600 12px sans-serif;box-shadow:0 1px 4px rgba(0,0,0,.3);";
|
||||
L.DomEvent.on(btn, "click", (e) => {
|
||||
L.DomEvent.stop(e);
|
||||
fitMapToStations();
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
});
|
||||
mapInstance.addControl(new FitControl());
|
||||
|
||||
fetch("/configuration.json") // centrar y marcar el iGate
|
||||
.then((response) => response.json())
|
||||
.then((config) => {
|
||||
const lat = (config.beacon && config.beacon.latitude) || 0;
|
||||
const lon = (config.beacon && config.beacon.longitude) || 0;
|
||||
const callsign = config.callsign || "iGate";
|
||||
mapInstance.setView([lat, lon], (lat || lon) ? 12 : 2);
|
||||
|
||||
if (lat || lon) { // rombo rojo en la posición del iGate
|
||||
iGateLatLng = L.latLng(lat, lon); // guardar para incluirlo en el auto-fit
|
||||
const iGateIcon = L.divIcon({
|
||||
className: "igate-marker",
|
||||
html: '<div style="width:14px;height:14px;background:#e23b3b;border:2px solid #fff;transform:rotate(45deg);box-shadow:0 0 4px rgba(0,0,0,.6);"></div>',
|
||||
iconSize: [18, 18],
|
||||
iconAnchor: [9, 9]
|
||||
});
|
||||
L.marker([lat, lon], { icon: iGateIcon })
|
||||
.bindPopup(`<b>${callsign}</b><br>This station`)
|
||||
.addTo(mapInstance); // directo al mapa: no se borra en los refrescos
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
mapInstance.setView([0, 0], 2);
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(function () { // el div estuvo oculto: recalcular tamaño
|
||||
if (mapInstance) mapInstance.invalidateSize();
|
||||
}, 0);
|
||||
|
||||
fetchMapStations();
|
||||
|
||||
if (mapTimer) clearInterval(mapTimer);
|
||||
|
||||
mapTimer = setInterval(function () { // refrescar solo si la sección está visible
|
||||
const section = document.getElementById("sec-maps");
|
||||
if (section && section.classList.contains("active")) {
|
||||
fetchMapStations();
|
||||
}
|
||||
}, 15000);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 396 KiB |
@@ -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/>.
|
||||
*/
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
+15
-12
@@ -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/>.
|
||||
*/
|
||||
@@ -32,6 +32,7 @@ public:
|
||||
|
||||
class WiFi_Auto_AP {
|
||||
public:
|
||||
bool enabled; // Enable Auto AP
|
||||
String password;
|
||||
int timeout;
|
||||
};
|
||||
@@ -44,7 +45,7 @@ public:
|
||||
int interval;
|
||||
String overlay;
|
||||
String symbol;
|
||||
String path;
|
||||
String path;
|
||||
bool sendViaAPRSIS;
|
||||
bool sendViaRF;
|
||||
int beaconFreq;
|
||||
@@ -78,13 +79,14 @@ public:
|
||||
long rxFreq;
|
||||
int rxSpreadingFactor;
|
||||
int rxCodingRate4;
|
||||
long rxSignalBandwidth;
|
||||
long rxSignalBandwidth;
|
||||
bool txActive;
|
||||
long txFreq;
|
||||
int txSpreadingFactor;
|
||||
int txCodingRate4;
|
||||
long txSignalBandwidth;
|
||||
int power;
|
||||
bool cadActive;
|
||||
};
|
||||
|
||||
class Display {
|
||||
@@ -130,6 +132,7 @@ public:
|
||||
bool enableSerial;
|
||||
bool acceptOwn;
|
||||
bool aprsBridgeActive;
|
||||
bool kissProtocol; // true = KISS, false = TNC2 -- applies to both Serial and TCP server
|
||||
};
|
||||
|
||||
class OTA {
|
||||
@@ -188,20 +191,20 @@ public:
|
||||
BATTERY battery;
|
||||
WXSENSOR wxsensor;
|
||||
SYSLOG syslog;
|
||||
TNC tnc;
|
||||
TNC tnc;
|
||||
OTA ota;
|
||||
WEBADMIN webadmin;
|
||||
NTP ntp;
|
||||
NTP ntp;
|
||||
REMOTE_MANAGEMENT remoteManagement;
|
||||
MQTT mqtt;
|
||||
|
||||
void setup();
|
||||
void setDefaultValues();
|
||||
bool writeFile();
|
||||
Configuration();
|
||||
|
||||
private:
|
||||
bool readFile();
|
||||
String _filePath;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
+7
-6
@@ -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/>.
|
||||
*/
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
|
||||
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
|
||||
|
||||
void displaySetup();
|
||||
|
||||
+8
-7
@@ -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/>.
|
||||
*/
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
|
||||
namespace GPS_Utils {
|
||||
|
||||
|
||||
String getiGateLoRaBeaconPacket();
|
||||
char *ax25_base91enc(char *s, uint8_t n, uint32_t v);
|
||||
String encodeGPS(float latitude, float longitude, const String& overlay, const String& symbol);
|
||||
@@ -32,6 +32,7 @@ namespace GPS_Utils {
|
||||
String decodeEncodedGPS(const String& packet);
|
||||
String getReceivedGPS(const String& packet);
|
||||
String getDistanceAndComment(const String& packet);
|
||||
String getDistanceAndCommentFromMicE(const String& packet);
|
||||
|
||||
void setup();
|
||||
void getData();
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/* 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 MAP_UTILS_H_
|
||||
#define MAP_UTILS_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
|
||||
struct MapStation {
|
||||
char callsign[10]; // "XX9XXX-NN" + null -> clave para deduplicar
|
||||
float latitude; // grados decimales (+N / -S)
|
||||
float longitude; // grados decimales (+E / -W)
|
||||
char symbol[3]; // tabla + codigo APRS (ej "/>"), "" si no hay
|
||||
int16_t rssi; // ultimo RSSI
|
||||
float snr; // ultimo SNR
|
||||
uint16_t count; // nro de paquetes oidos de esa estacion
|
||||
char lastHeard[10]; // "HH:MM:SS" si hay NTP valido, "" si no
|
||||
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& path, const String& symbol, int rssi, float snr);
|
||||
String getStationsJson();
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/* 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>
|
||||
#include <ETH.h>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Class for managing network connections
|
||||
*/
|
||||
class NetworkManager
|
||||
{
|
||||
private:
|
||||
class WiFiNetwork {
|
||||
public:
|
||||
String ssid;
|
||||
String psk;
|
||||
};
|
||||
|
||||
bool _wifiAPmode = false;
|
||||
bool _wifiSTAmode = false;
|
||||
bool _ethernetMode = false;
|
||||
bool _ethernetConnected = false;
|
||||
unsigned long _apStartup = 0;
|
||||
unsigned long _apTimeout = 0;
|
||||
|
||||
String _hostName = "";
|
||||
std::vector<WiFiNetwork> _wifiNetworks;
|
||||
|
||||
int _findWiFiNetworkIndex(const String& ssid) const;
|
||||
bool _connectWiFi(const WiFiNetwork& network);
|
||||
void _processAPTimeout();
|
||||
void _onNetworkEvent(arduino_event_id_t event, arduino_event_info_t /*info*/);
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
NetworkManager();
|
||||
|
||||
// Destructor
|
||||
~NetworkManager();
|
||||
|
||||
// Initialize network module
|
||||
bool setup();
|
||||
void loop();
|
||||
|
||||
void setHostName(const String& hostName);
|
||||
|
||||
// WiFi methods
|
||||
bool setupAP(String apName, String apPsk = "");
|
||||
bool disableAP();
|
||||
void setAPTimeout(unsigned long timeout);
|
||||
void addWiFiNetwork(const String& ssid, const String& psk = "");
|
||||
void clearWiFiNetworks();
|
||||
bool hasWiFiNetworks() const;
|
||||
size_t getWiFiNetworkCount() const;
|
||||
bool connectWiFi();
|
||||
bool connectWiFi(const String& ssid, const String& psk = "");
|
||||
bool disconnectWiFi();
|
||||
String getWiFiSSID() const;
|
||||
String getWiFiAPSSID() const;
|
||||
IPAddress getWiFiIP() const;
|
||||
IPAddress getWiFiAPIP() const;
|
||||
wifi_mode_t getWiFiMode() const;
|
||||
uint8_t* getWiFimacAddress(uint8_t* mac);
|
||||
String getWiFimacAddress(void) const;
|
||||
|
||||
// Ethernet methods
|
||||
bool ethernetConnect(eth_phy_type_t type, uint8_t phy_addr, uint8_t mdc, uint8_t mdio, int power, eth_clock_mode_t clock_mode, bool use_mac_from_efuse = false);
|
||||
bool setEthernetIP(const String& staticIP, const String& gateway, const String& subnet, const String& dns1, const String& dns2);
|
||||
bool ethernetDisconnect();
|
||||
IPAddress getEthernetIP() const;
|
||||
String getEthernetMACAddress() const;
|
||||
|
||||
// Check if any network is available
|
||||
bool isConnected() const;
|
||||
|
||||
// Check if specific network is connected
|
||||
bool isWiFiConnected() const;
|
||||
bool isEthernetConnected() const;
|
||||
bool isModemConnected() const;
|
||||
|
||||
bool isWifiAPActive() const;
|
||||
};
|
||||
+7
-7
@@ -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/>.
|
||||
*/
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
namespace NTP_Utils {
|
||||
|
||||
void setup();
|
||||
bool setup();
|
||||
void update();
|
||||
String getFormatedTime();
|
||||
|
||||
|
||||
+6
-6
@@ -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/>.
|
||||
*/
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
@@ -34,7 +34,7 @@ namespace POWER_Utils {
|
||||
void vext_ctrl_OFF();
|
||||
#endif
|
||||
|
||||
#ifdef ADC_CTRL
|
||||
#ifdef ADC_CTRL_PIN
|
||||
void adc_ctrl_ON();
|
||||
void adc_ctrl_OFF();
|
||||
#endif
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
@@ -21,8 +21,9 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
|
||||
namespace SLEEP_Utils {
|
||||
|
||||
|
||||
void setup();
|
||||
void checkWakeUpFlag();
|
||||
void startSleeping();
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
@@ -37,8 +37,7 @@ namespace STATION_Utils {
|
||||
void deleteNotHeard();
|
||||
void updateLastHeard(const String& station);
|
||||
bool wasHeard(const String& station);
|
||||
void clean25SegBuffer();
|
||||
bool check25SegBuffer(const String& station, const String& textMessage);
|
||||
bool isIn25SegHashBuffer(const String& station, const String& textMessage);
|
||||
void processOutputPacketBufferUltraEcoMode();
|
||||
void processOutputPacketBuffer();
|
||||
void addToOutputPacketBuffer(const String& packet, bool flag = false);
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
@@ -27,7 +27,8 @@ namespace TELEMETRY_Utils {
|
||||
void sendEquationsUnitsParameters();
|
||||
String generateEncodedTelemetryBytes(float value, bool counterBytes, byte telemetryType);
|
||||
String generateEncodedTelemetry();
|
||||
|
||||
void checkEUPInterval();
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
+6
-6
@@ -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/>.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -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
-6
@@ -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/>.
|
||||
*/
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
@@ -27,7 +27,6 @@ namespace WIFI_Utils {
|
||||
void checkWiFi();
|
||||
void startAutoAP();
|
||||
void startWiFi();
|
||||
void checkAutoAPTimeout();
|
||||
void setup();
|
||||
|
||||
}
|
||||
|
||||
+7
-7
@@ -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/>.
|
||||
*/
|
||||
@@ -20,7 +20,7 @@
|
||||
#define WX_UTILS_H_
|
||||
|
||||
#include <Adafruit_Sensor.h>
|
||||
#include <Adafruit_AHTX0.h>
|
||||
#include <Adafruit_AHTX0.h>
|
||||
#include <Adafruit_BME280.h>
|
||||
#include <Adafruit_BMP280.h>
|
||||
#include <Adafruit_BME680.h>
|
||||
|
||||
+8
-3
@@ -16,17 +16,22 @@ extra_configs =
|
||||
variants/*/platformio.ini
|
||||
|
||||
[env]
|
||||
platform = espressif32 @ 6.7.0
|
||||
platform = espressif32 @ 6.12.0
|
||||
board_build.partitions = min_spiffs.csv
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
board_build.embed_files =
|
||||
board_build.embed_files =
|
||||
data_embed/index.html.gz
|
||||
data_embed/style.css.gz
|
||||
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
|
||||
extra_scripts =
|
||||
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
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
|
||||
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
|
||||
*
|
||||
* This file is part of LoRa APRS iGate.
|
||||
*
|
||||
|
||||
+21
-13
@@ -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,9 +41,10 @@ ___________________________________________________________________*/
|
||||
#include <ElegantOTA.h>
|
||||
#include <TinyGPS++.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <WiFiClient.h>
|
||||
#include <vector>
|
||||
#include "configuration.h"
|
||||
#include "network_manager.h"
|
||||
#include "aprs_is_utils.h"
|
||||
#include "station_utils.h"
|
||||
#include "battery_utils.h"
|
||||
@@ -67,8 +68,8 @@ ___________________________________________________________________*/
|
||||
#endif
|
||||
|
||||
|
||||
String versionDate = "2026-02-20";
|
||||
String versionNumber = "3.2";
|
||||
String versionDate = "2026-09-15";
|
||||
String versionNumber = "4.0.1";
|
||||
Configuration Config;
|
||||
WiFiClient aprsIsClient;
|
||||
WiFiClient mqttClient;
|
||||
@@ -79,9 +80,7 @@ WiFiClient mqttClient;
|
||||
bool gpsInfoToggle = false;
|
||||
#endif
|
||||
|
||||
uint8_t myWiFiAPIndex = 0;
|
||||
int myWiFiAPSize = Config.wifiAPs.size();
|
||||
WiFi_AP *currentWiFi = &Config.wifiAPs[myWiFiAPIndex];
|
||||
NetworkManager *networkManager;
|
||||
|
||||
bool isUpdatingOTA = false;
|
||||
uint32_t lastBatteryCheck = 0;
|
||||
@@ -101,6 +100,13 @@ String firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seven
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Config.setup();
|
||||
networkManager = new NetworkManager();
|
||||
networkManager->setup();
|
||||
if (Config.wifiAutoAP.enabled) {
|
||||
networkManager->setAPTimeout(Config.wifiAutoAP.timeout * 60 * 1000); // Convert minutes to milliseconds
|
||||
}
|
||||
networkManager->setHostName("iGATE-" + Config.callsign);
|
||||
POWER_Utils::setup();
|
||||
Utils::setupDisplay();
|
||||
LoRa_Utils::setup();
|
||||
@@ -132,7 +138,7 @@ void loop() {
|
||||
Utils::checkSleepByLowBatteryVoltage(1);
|
||||
SLEEP_Utils::startSleeping();
|
||||
} else {
|
||||
WIFI_Utils::checkAutoAPTimeout();
|
||||
networkManager->loop();
|
||||
|
||||
if (isUpdatingOTA) {
|
||||
ElegantOTA.loop();
|
||||
@@ -161,11 +167,14 @@ void loop() {
|
||||
#endif
|
||||
|
||||
#ifdef HAS_A7670
|
||||
// TODO: Make this part of Network manager, and use ESP-IDF network stack instead manual AT commands
|
||||
if (Config.aprs_is.active && !modemLoggedToAPRSIS) A7670_Utils::APRS_IS_connect();
|
||||
#else
|
||||
WIFI_Utils::checkWiFi();
|
||||
if (Config.aprs_is.active && (WiFi.status() == WL_CONNECTED) && !aprsIsClient.connected()) APRS_IS_Utils::connect();
|
||||
if (Config.mqtt.active && (WiFi.status() == WL_CONNECTED) && !mqttClient.connected()) MQTT_Utils::connect();
|
||||
if (networkManager->isConnected()) {
|
||||
if (Config.aprs_is.active && !aprsIsClient.connected()) APRS_IS_Utils::connect();
|
||||
if (Config.mqtt.active && !mqttClient.connected()) MQTT_Utils::connect();
|
||||
}
|
||||
#endif
|
||||
|
||||
NTP_Utils::update();
|
||||
@@ -187,8 +196,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
|
||||
STATION_Utils::clean25SegBuffer();
|
||||
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
|
||||
}
|
||||
|
||||
@@ -217,4 +225,4 @@ void loop() {
|
||||
Utils::checkRebootTime();
|
||||
Utils::checkSleepByLowBatteryVoltage(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
-47
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
|
||||
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
|
||||
*
|
||||
* This file is part of LoRa APRS iGate.
|
||||
*
|
||||
@@ -17,8 +17,9 @@
|
||||
*/
|
||||
|
||||
#include <APRSPacketLib.h>
|
||||
#include <WiFi.h>
|
||||
#include <WiFiClient.h>
|
||||
#include "configuration.h"
|
||||
#include "network_manager.h"
|
||||
#include "aprs_is_utils.h"
|
||||
#include "station_utils.h"
|
||||
#include "board_pinout.h"
|
||||
@@ -32,6 +33,7 @@
|
||||
|
||||
|
||||
extern Configuration Config;
|
||||
extern NetworkManager *networkManager;
|
||||
extern WiFiClient aprsIsClient;
|
||||
extern uint32_t lastScreenOn;
|
||||
extern String firstLine;
|
||||
@@ -45,7 +47,6 @@ extern bool modemLoggedToAPRSIS;
|
||||
extern bool backupDigiMode;
|
||||
extern String versionNumber;
|
||||
|
||||
uint32_t lastRxTime = millis();
|
||||
bool passcodeValid = false;
|
||||
uint32_t lastServerCheck = 0;
|
||||
|
||||
@@ -93,7 +94,7 @@ namespace APRS_IS_Utils {
|
||||
|
||||
void checkStatus() {
|
||||
String wifiState, aprsisState;
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
if (networkManager->isWiFiConnected()) {
|
||||
wifiState = "OK";
|
||||
} else {
|
||||
if (backupDigiMode || Config.digi.ecoMode == 1 || Config.digi.ecoMode == 2) {
|
||||
@@ -135,30 +136,30 @@ namespace APRS_IS_Utils {
|
||||
}
|
||||
|
||||
String checkForStartingBytes(const String& packet) {
|
||||
if (packet.indexOf("\x3c\xff\x01") != -1) {
|
||||
return packet.substring(0, packet.indexOf("\x3c\xff\x01"));
|
||||
} else {
|
||||
return packet;
|
||||
}
|
||||
int index = packet.indexOf("\x3c\xff\x01");
|
||||
return (index != -1) ? packet.substring(0, index) : packet;
|
||||
}
|
||||
|
||||
String buildPacketToUpload(const String& packet) {
|
||||
String packetToUpload = packet.substring(3, packet.indexOf(":"));
|
||||
int colonIndex = packet.indexOf(":");
|
||||
String packetToUpload = packet.substring(3, colonIndex);
|
||||
if (Config.aprs_is.active && passcodeValid && Config.aprs_is.messagesToRF) {
|
||||
packetToUpload += ",qAR,";
|
||||
} else {
|
||||
packetToUpload += ",qAO,";
|
||||
}
|
||||
packetToUpload += Config.callsign;
|
||||
packetToUpload += checkForStartingBytes(packet.substring(packet.indexOf(":")));
|
||||
packetToUpload += checkForStartingBytes(packet.substring(colonIndex));
|
||||
return packetToUpload;
|
||||
}
|
||||
|
||||
bool processReceivedLoRaMessage(const String& sender, const String& packet, bool thirdParty) {
|
||||
String receivedMessage;
|
||||
if (packet.indexOf("{") > 0) { // ack?
|
||||
int leftCurlyBraceIndex = packet.indexOf("{");
|
||||
int colonIndex = packet.indexOf(":");
|
||||
if (leftCurlyBraceIndex > 0) { // ack?
|
||||
String ackMessage = "ack";
|
||||
ackMessage.concat(packet.substring(packet.indexOf("{") + 1));
|
||||
ackMessage.concat(packet.substring(leftCurlyBraceIndex + 1));
|
||||
ackMessage.trim();
|
||||
//Serial.println(ackMessage);
|
||||
|
||||
@@ -180,9 +181,9 @@ namespace APRS_IS_Utils {
|
||||
addToBuffer += ":";
|
||||
addToBuffer += ackMessage;
|
||||
STATION_Utils::addToOutputPacketBuffer(addToBuffer);
|
||||
receivedMessage = packet.substring(packet.indexOf(":") + 1, packet.indexOf("{"));
|
||||
receivedMessage = packet.substring(colonIndex + 1, leftCurlyBraceIndex);
|
||||
} else {
|
||||
receivedMessage = packet.substring(packet.indexOf(":") + 1);
|
||||
receivedMessage = packet.substring(colonIndex + 1);
|
||||
}
|
||||
if (receivedMessage.indexOf("?") == 0) {
|
||||
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
|
||||
@@ -207,29 +208,30 @@ namespace APRS_IS_Utils {
|
||||
if (Sender != Config.callsign && Utils::callsignIsValid(Sender)) {
|
||||
STATION_Utils::updateLastHeard(Sender);
|
||||
Utils::typeOfPacket(packet.substring(3), 0); // LoRa-APRS
|
||||
const String& AddresseeAndMessage = packet.substring(packet.indexOf("::") + 2);
|
||||
int doubleColonIndex = packet.indexOf("::");
|
||||
const String& AddresseeAndMessage = packet.substring(doubleColonIndex + 2);
|
||||
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
|
||||
Addressee.trim();
|
||||
bool queryMessage = false;
|
||||
if (packet.indexOf("::") > 10 && Addressee == Config.callsign) { // its a message for me!
|
||||
if (doubleColonIndex > 10 && Addressee == Config.callsign) { // its a message for me!
|
||||
queryMessage = processReceivedLoRaMessage(Sender, checkForStartingBytes(AddresseeAndMessage), false);
|
||||
}
|
||||
if (!queryMessage) {
|
||||
const String& aprsPacket = buildPacketToUpload(packet);
|
||||
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
|
||||
displayToggle(true);
|
||||
}
|
||||
lastScreenOn = millis();
|
||||
#ifdef HAS_A7670
|
||||
stationBeacon = true;
|
||||
A7670_Utils::uploadToAPRSIS(aprsPacket);
|
||||
stationBeacon = false;
|
||||
#else
|
||||
upload(aprsPacket);
|
||||
#endif
|
||||
Utils::println("---> Uploaded to APRS-IS");
|
||||
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
|
||||
if (queryMessage) return;
|
||||
|
||||
const String& aprsPacket = buildPacketToUpload(packet);
|
||||
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
|
||||
displayToggle(true);
|
||||
}
|
||||
lastScreenOn = millis();
|
||||
#ifdef HAS_A7670
|
||||
stationBeacon = true;
|
||||
A7670_Utils::uploadToAPRSIS(aprsPacket);
|
||||
stationBeacon = false;
|
||||
#else
|
||||
upload(aprsPacket);
|
||||
#endif
|
||||
Utils::println("---> Uploaded to APRS-IS");
|
||||
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,26 +247,30 @@ namespace APRS_IS_Utils {
|
||||
outputPacket.concat(",TCPIP,");
|
||||
outputPacket.concat(Config.callsign);
|
||||
outputPacket.concat("*");
|
||||
int colonEqualIndex = packet.indexOf(":=");
|
||||
int doubleColonIndex = packet.indexOf("::");
|
||||
int colonInvAccentIndex = packet.indexOf(":`");
|
||||
|
||||
switch (packetType) {
|
||||
case 0: // gps
|
||||
if (packet.indexOf(":=") > 0) {
|
||||
outputPacket += packet.substring(packet.indexOf(":="));
|
||||
if (colonEqualIndex > 0) {
|
||||
outputPacket += packet.substring(colonEqualIndex);
|
||||
} else {
|
||||
outputPacket += packet.substring(packet.indexOf(":!"));
|
||||
}
|
||||
break;
|
||||
case 1: // messages
|
||||
outputPacket += packet.substring(packet.indexOf("::"));
|
||||
outputPacket += packet.substring(doubleColonIndex);
|
||||
break;
|
||||
case 2: // status
|
||||
outputPacket += packet.substring(packet.indexOf(":>"));
|
||||
break;
|
||||
case 3: // telemetry
|
||||
outputPacket += packet.substring(packet.indexOf("::"));
|
||||
outputPacket += packet.substring(doubleColonIndex);
|
||||
break;
|
||||
case 4: // mic-e
|
||||
if (packet.indexOf(":`") > 0) {
|
||||
outputPacket += packet.substring(packet.indexOf(":`"));
|
||||
if (colonInvAccentIndex > 0) {
|
||||
outputPacket += packet.substring(colonInvAccentIndex);
|
||||
} else {
|
||||
outputPacket += packet.substring(packet.indexOf(":'"));
|
||||
}
|
||||
@@ -316,18 +322,21 @@ namespace APRS_IS_Utils {
|
||||
if (packet.startsWith("#")) {
|
||||
if (Config.digi.backupDigiMode) lastServerCheck = currentTime;
|
||||
} else {
|
||||
if (Config.aprs_is.messagesToRF && packet.indexOf("::") > 0) {
|
||||
int doubleColonIndex = packet.indexOf("::");
|
||||
if (Config.aprs_is.messagesToRF && doubleColonIndex > 0) {
|
||||
String Sender = packet.substring(0, packet.indexOf(">"));
|
||||
const String& AddresseeAndMessage = packet.substring(packet.indexOf("::") + 2);
|
||||
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
|
||||
const String& AddresseeAndMessage = packet.substring(doubleColonIndex + 2);
|
||||
int colonIndex = AddresseeAndMessage.indexOf(":");
|
||||
String Addressee = AddresseeAndMessage.substring(0, colonIndex);
|
||||
Addressee.trim();
|
||||
if (Addressee == Config.callsign) { // its for me!
|
||||
String receivedMessage;
|
||||
if (AddresseeAndMessage.indexOf("{") > 0) { // ack?
|
||||
int curlyBraceIndex = AddresseeAndMessage.indexOf("{");
|
||||
if (curlyBraceIndex > 0) { // ack?
|
||||
processAckMessage(Sender, AddresseeAndMessage);
|
||||
receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":") + 1, AddresseeAndMessage.indexOf("{"));
|
||||
receivedMessage = AddresseeAndMessage.substring(colonIndex + 1, curlyBraceIndex);
|
||||
} else {
|
||||
receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":") + 1);
|
||||
receivedMessage = AddresseeAndMessage.substring(colonIndex + 1);
|
||||
}
|
||||
if (receivedMessage.indexOf("?") == 0) {
|
||||
Utils::println("Rx Query (APRS-IS) : " + packet);
|
||||
@@ -393,14 +402,13 @@ namespace APRS_IS_Utils {
|
||||
String aprsisPacket = aprsIsClient.readStringUntil('\r');
|
||||
aprsisPacket.trim(); // Serial.println(aprsisPacket);
|
||||
processAPRSISPacket(aprsisPacket);
|
||||
lastRxTime = millis();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void firstConnection() {
|
||||
if (Config.aprs_is.active && (WiFi.status() == WL_CONNECTED) && !aprsIsClient.connected()) {
|
||||
if (Config.aprs_is.active && networkManager->isConnected() && !aprsIsClient.connected()) {
|
||||
connect();
|
||||
while (!passcodeValid) {
|
||||
listenAPRSIS();
|
||||
@@ -408,4 +416,4 @@ namespace APRS_IS_Utils {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
|
||||
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
|
||||
*
|
||||
* This file is part of LoRa APRS iGate.
|
||||
*
|
||||
@@ -46,7 +46,7 @@ Adafruit_INA219 ina219;
|
||||
#ifdef HAS_ADC_CALIBRATION
|
||||
#include <esp_adc_cal.h>
|
||||
|
||||
#if defined(TTGO_LORA32_V2_1) || defined(TTGO_LORA32_V2_1_915)
|
||||
#if defined(TTGO_LORA32_V2_1) || defined(TTGO_LORA32_V2_1_GPS) || defined(TTGO_LORA32_V2_1_915) || defined(TTGO_LORA32_V2_1_915_GPS)
|
||||
#define InternalBattery_ADC_Channel ADC1_CHANNEL_7 // t_lora32 pin35
|
||||
#define ExternalVoltage_ADC_Channel ADC1_CHANNEL_6 // t_lora32 pin34
|
||||
#endif
|
||||
@@ -154,7 +154,7 @@ namespace BATTERY_Utils {
|
||||
}
|
||||
#else
|
||||
|
||||
#ifdef ADC_CTRL
|
||||
#ifdef ADC_CTRL_PIN
|
||||
POWER_Utils::adc_ctrl_ON();
|
||||
#endif
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace BATTERY_Utils {
|
||||
delay(3);
|
||||
}
|
||||
|
||||
#ifdef ADC_CTRL
|
||||
#ifdef ADC_CTRL_PIN
|
||||
POWER_Utils::adc_ctrl_OFF();
|
||||
|
||||
#ifdef HELTEC_WP_V1
|
||||
|
||||
+135
-118
@@ -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/>.
|
||||
*/
|
||||
@@ -29,7 +29,7 @@ bool shouldSleepStop = true;
|
||||
bool Configuration::writeFile() {
|
||||
Serial.println("Saving configuration...");
|
||||
|
||||
StaticJsonDocument<3584> data;
|
||||
JsonDocument data;
|
||||
File configFile = SPIFFS.open("/igate_conf.json", "w");
|
||||
|
||||
if (!configFile) {
|
||||
@@ -47,6 +47,7 @@ bool Configuration::writeFile() {
|
||||
|
||||
data["other"]["startupDelay"] = startupDelay;
|
||||
|
||||
data["wifi"]["autoAP"]["enabled"] = wifiAutoAP.enabled;
|
||||
data["wifi"]["autoAP"]["password"] = wifiAutoAP.password;
|
||||
data["wifi"]["autoAP"]["timeout"] = wifiAutoAP.timeout;
|
||||
|
||||
@@ -79,6 +80,9 @@ bool Configuration::writeFile() {
|
||||
data["beacon"]["statusPacket"] = beacon.statusPacket;
|
||||
|
||||
data["beacon"]["gpsActive"] = beacon.gpsActive;
|
||||
#if !defined(HAS_GPS)
|
||||
data["beacon"]["gpsActive"] = false;
|
||||
#endif
|
||||
data["beacon"]["ambiguityLevel"] = beacon.ambiguityLevel;
|
||||
|
||||
data["personalNote"] = personalNote;
|
||||
@@ -87,6 +91,7 @@ bool Configuration::writeFile() {
|
||||
|
||||
data["digi"]["mode"] = digi.mode;
|
||||
data["digi"]["ecoMode"] = digi.ecoMode;
|
||||
if (digi.ecoMode == 1) data["aprs_is"]["active"] = false;
|
||||
#if defined(HAS_A7670)
|
||||
if (digi.ecoMode == 1) data["digi"]["ecoMode"] = 2;
|
||||
#endif
|
||||
@@ -96,12 +101,13 @@ bool Configuration::writeFile() {
|
||||
data["lora"]["rxFreq"] = loramodule.rxFreq;
|
||||
data["lora"]["rxCodingRate4"] = loramodule.rxCodingRate4;
|
||||
data["lora"]["rxSignalBandwidth"] = loramodule.rxSignalBandwidth;
|
||||
data["lora"]["txActive"] = loramodule.txActive;
|
||||
data["lora"]["txActive"] = loramodule.txActive;
|
||||
data["lora"]["txFreq"] = loramodule.txFreq;
|
||||
data["lora"]["txCodingRate4"] = loramodule.txCodingRate4;
|
||||
data["lora"]["txSignalBandwidth"] = loramodule.txSignalBandwidth;
|
||||
data["lora"]["power"] = loramodule.power;
|
||||
|
||||
data["lora"]["txSignalBandwidth"] = loramodule.txSignalBandwidth;
|
||||
data["lora"]["power"] = loramodule.power;
|
||||
data["lora"]["cadActive"] = loramodule.cadActive;
|
||||
|
||||
int rxSpreadingFactor = loramodule.rxSpreadingFactor;
|
||||
int txSpreadingFactor = loramodule.txSpreadingFactor;
|
||||
#if defined(HAS_SX1276) || defined(HAS_SX1278)
|
||||
@@ -151,6 +157,7 @@ bool Configuration::writeFile() {
|
||||
data["tnc"]["enableSerial"] = tnc.enableSerial;
|
||||
data["tnc"]["acceptOwn"] = tnc.acceptOwn;
|
||||
data["tnc"]["aprsBridgeActive"] = tnc.aprsBridgeActive;
|
||||
data["tnc"]["kissProtocol"] = tnc.kissProtocol;
|
||||
|
||||
data["mqtt"]["active"] = mqtt.active;
|
||||
data["mqtt"]["server"] = mqtt.server;
|
||||
@@ -194,8 +201,7 @@ bool Configuration::readFile() {
|
||||
|
||||
if (configFile) {
|
||||
bool needsRewrite = false;
|
||||
StaticJsonDocument<3584> data;
|
||||
|
||||
JsonDocument data;
|
||||
DeserializationError error = deserializeJson(data, configFile);
|
||||
if (error) {
|
||||
Serial.println("Failed to read file, using default configuration");
|
||||
@@ -210,26 +216,28 @@ bool Configuration::readFile() {
|
||||
wifiAPs.push_back(wifiap);
|
||||
}
|
||||
|
||||
if (!data["other"].containsKey("startupDelay")) needsRewrite = true;
|
||||
if (data["other"]["startupDelay"].isNull()) needsRewrite = true;
|
||||
startupDelay = data["other"]["startupDelay"] | 0;
|
||||
|
||||
if (!data["wifi"]["autoAP"].containsKey("password") ||
|
||||
!data["wifi"]["autoAP"].containsKey("timeout")) needsRewrite = true;
|
||||
if (data["wifi"]["autoAP"]["enabled"].isNull() ||
|
||||
data["wifi"]["autoAP"]["password"].isNull() ||
|
||||
data["wifi"]["autoAP"]["timeout"].isNull()) needsRewrite = true;
|
||||
wifiAutoAP.enabled = data["wifi"]["autoAP"]["enabled"] | true;
|
||||
wifiAutoAP.password = data["wifi"]["autoAP"]["password"] | "1234567890";
|
||||
wifiAutoAP.timeout = data["wifi"]["autoAP"]["timeout"] | 10;
|
||||
|
||||
if (!data.containsKey("callsign")) needsRewrite = true;
|
||||
if (data["callsign"].isNull()) needsRewrite = true;
|
||||
callsign = data["callsign"] | "NOCALL-10";
|
||||
if (!data.containsKey("tacticalCallsign")) needsRewrite = true;
|
||||
if (data["tacticalCallsign"].isNull()) needsRewrite = true;
|
||||
tacticalCallsign = data["tacticalCallsign"] | "";
|
||||
|
||||
if (!data["aprs_is"].containsKey("active") ||
|
||||
!data["aprs_is"].containsKey("passcode") ||
|
||||
!data["aprs_is"].containsKey("server") ||
|
||||
!data["aprs_is"].containsKey("port") ||
|
||||
!data["aprs_is"].containsKey("filter") ||
|
||||
!data["aprs_is"].containsKey("messagesToRF") ||
|
||||
!data["aprs_is"].containsKey("objectsToRF")) needsRewrite = true;
|
||||
|
||||
if (data["aprs_is"]["active"].isNull() ||
|
||||
data["aprs_is"]["passcode"].isNull() ||
|
||||
data["aprs_is"]["server"].isNull() ||
|
||||
data["aprs_is"]["port"].isNull() ||
|
||||
data["aprs_is"]["filter"].isNull() ||
|
||||
data["aprs_is"]["messagesToRF"].isNull() ||
|
||||
data["aprs_is"]["objectsToRF"].isNull()) needsRewrite = true;
|
||||
aprs_is.active = data["aprs_is"]["active"] | false;
|
||||
aprs_is.passcode = data["aprs_is"]["passcode"] | "XYZWV";
|
||||
aprs_is.server = data["aprs_is"]["server"] | "rotate.aprs2.net";
|
||||
@@ -238,20 +246,20 @@ bool Configuration::readFile() {
|
||||
aprs_is.messagesToRF = data["aprs_is"]["messagesToRF"] | false;
|
||||
aprs_is.objectsToRF = data["aprs_is"]["objectsToRF"] | false;
|
||||
|
||||
if (!data["beacon"].containsKey("latitude") ||
|
||||
!data["beacon"].containsKey("longitude") ||
|
||||
!data["beacon"].containsKey("comment") ||
|
||||
!data["beacon"].containsKey("interval") ||
|
||||
!data["beacon"].containsKey("overlay") ||
|
||||
!data["beacon"].containsKey("symbol") ||
|
||||
!data["beacon"].containsKey("path") ||
|
||||
!data["beacon"].containsKey("sendViaAPRSIS") ||
|
||||
!data["beacon"].containsKey("sendViaRF") ||
|
||||
!data["beacon"].containsKey("beaconFreq") ||
|
||||
!data["beacon"].containsKey("statusActive") ||
|
||||
!data["beacon"].containsKey("statusPacket") ||
|
||||
!data["beacon"].containsKey("gpsActive") ||
|
||||
!data["beacon"].containsKey("ambiguityLevel")) needsRewrite = true;
|
||||
if (data["beacon"]["latitude"].isNull() ||
|
||||
data["beacon"]["longitude"].isNull() ||
|
||||
data["beacon"]["comment"].isNull() ||
|
||||
data["beacon"]["interval"].isNull() ||
|
||||
data["beacon"]["overlay"].isNull() ||
|
||||
data["beacon"]["symbol"].isNull() ||
|
||||
data["beacon"]["path"].isNull() ||
|
||||
data["beacon"]["sendViaAPRSIS"].isNull() ||
|
||||
data["beacon"]["sendViaRF"].isNull() ||
|
||||
data["beacon"]["beaconFreq"].isNull() ||
|
||||
data["beacon"]["statusActive"].isNull() ||
|
||||
data["beacon"]["statusPacket"].isNull() ||
|
||||
data["beacon"]["gpsActive"].isNull() ||
|
||||
data["beacon"]["ambiguityLevel"].isNull()) needsRewrite = true;
|
||||
beacon.latitude = data["beacon"]["latitude"] | 0.0;
|
||||
beacon.longitude = data["beacon"]["longitude"] | 0.0;
|
||||
beacon.comment = data["beacon"]["comment"] | "LoRa APRS";
|
||||
@@ -267,15 +275,15 @@ bool Configuration::readFile() {
|
||||
beacon.gpsActive = data["beacon"]["gpsActive"] | false;
|
||||
beacon.ambiguityLevel = data["beacon"]["ambiguityLevel"] | 0;
|
||||
|
||||
if (!data.containsKey("personalNote")) needsRewrite = true;
|
||||
if (data["personalNote"].isNull()) needsRewrite = true;
|
||||
personalNote = data["personalNote"] | "personal note here";
|
||||
|
||||
if (!data.containsKey("blacklist")) needsRewrite = true;
|
||||
if (data["blacklist"].isNull()) needsRewrite = true;
|
||||
blacklist = data["blacklist"] | "station callsign";
|
||||
|
||||
if (!data["digi"].containsKey("mode") ||
|
||||
!data["digi"].containsKey("ecoMode") ||
|
||||
!data["digi"].containsKey("backupDigiMode")) needsRewrite = true;
|
||||
if (data["digi"]["mode"].isNull() ||
|
||||
data["digi"]["ecoMode"].isNull() ||
|
||||
data["digi"]["backupDigiMode"].isNull()) needsRewrite = true;
|
||||
digi.mode = data["digi"]["mode"] | 0;
|
||||
digi.ecoMode = data["digi"]["ecoMode"] | 0;
|
||||
if (digi.ecoMode == 1) shouldSleepStop = false;
|
||||
@@ -284,33 +292,34 @@ bool Configuration::readFile() {
|
||||
#endif
|
||||
digi.backupDigiMode = data["digi"]["backupDigiMode"] | false;
|
||||
|
||||
|
||||
if (!data["lora"].containsKey("rxActive") ||
|
||||
!data["lora"].containsKey("rxFreq") ||
|
||||
!data["lora"].containsKey("rxSpreadingFactor") ||
|
||||
!data["lora"].containsKey("rxCodingRate4") ||
|
||||
!data["lora"].containsKey("rxSignalBandwidth") ||
|
||||
!data["lora"].containsKey("txActive") ||
|
||||
!data["lora"].containsKey("txFreq") ||
|
||||
!data["lora"].containsKey("txSpreadingFactor") ||
|
||||
!data["lora"].containsKey("txCodingRate4") ||
|
||||
!data["lora"].containsKey("txSignalBandwidth") ||
|
||||
!data["lora"].containsKey("power")) needsRewrite = true;
|
||||
if (data["lora"]["rxActive"].isNull() ||
|
||||
data["lora"]["rxFreq"].isNull() ||
|
||||
data["lora"]["rxSpreadingFactor"].isNull() ||
|
||||
data["lora"]["rxCodingRate4"].isNull() ||
|
||||
data["lora"]["rxSignalBandwidth"].isNull() ||
|
||||
data["lora"]["txActive"].isNull() ||
|
||||
data["lora"]["txFreq"].isNull() ||
|
||||
data["lora"]["txSpreadingFactor"].isNull() ||
|
||||
data["lora"]["txCodingRate4"].isNull() ||
|
||||
data["lora"]["txSignalBandwidth"].isNull() ||
|
||||
data["lora"]["power"].isNull() ||
|
||||
data["lora"]["cadActive"].isNull()) needsRewrite = true;
|
||||
loramodule.rxActive = data["lora"]["rxActive"] | true;
|
||||
loramodule.rxFreq = data["lora"]["rxFreq"] | 433775000;
|
||||
loramodule.rxSpreadingFactor = data["lora"]["rxSpreadingFactor"] | 12;
|
||||
loramodule.rxCodingRate4 = data["lora"]["rxCodingRate4"] | 5;
|
||||
loramodule.rxSignalBandwidth = data["lora"]["rxSignalBandwidth"] | 125000;
|
||||
loramodule.rxSignalBandwidth = data["lora"]["rxSignalBandwidth"] | 125000;
|
||||
loramodule.txActive = data["lora"]["txActive"] | false;
|
||||
loramodule.txFreq = data["lora"]["txFreq"] | 433775000;
|
||||
loramodule.txSpreadingFactor = data["lora"]["txSpreadingFactor"] | 12;
|
||||
loramodule.txCodingRate4 = data["lora"]["txCodingRate4"] | 5;
|
||||
loramodule.txSignalBandwidth = data["lora"]["txSignalBandwidth"] | 125000;
|
||||
loramodule.power = data["lora"]["power"] | 20;
|
||||
|
||||
if (!data["display"].containsKey("alwaysOn") ||
|
||||
!data["display"].containsKey("timeout") ||
|
||||
!data["display"].containsKey("turn180")) needsRewrite = true;
|
||||
loramodule.cadActive = data["lora"]["cadActive"] | true;
|
||||
|
||||
if (data["display"]["alwaysOn"].isNull() ||
|
||||
data["display"]["timeout"].isNull() ||
|
||||
data["display"]["turn180"].isNull()) needsRewrite = true;
|
||||
#ifdef HAS_EPAPER
|
||||
display.alwaysOn = true;
|
||||
#else
|
||||
@@ -319,17 +328,17 @@ bool Configuration::readFile() {
|
||||
display.timeout = data["display"]["timeout"] | 4;
|
||||
display.turn180 = data["display"]["turn180"] | false;
|
||||
|
||||
if (!data["battery"].containsKey("sendInternalVoltage") ||
|
||||
!data["battery"].containsKey("monitorInternalVoltage") ||
|
||||
!data["battery"].containsKey("internalSleepVoltage") ||
|
||||
!data["battery"].containsKey("sendExternalVoltage") ||
|
||||
!data["battery"].containsKey("monitorExternalVoltage") ||
|
||||
!data["battery"].containsKey("externalSleepVoltage") ||
|
||||
!data["battery"].containsKey("useExternalI2CSensor") ||
|
||||
!data["battery"].containsKey("voltageDividerR1") ||
|
||||
!data["battery"].containsKey("voltageDividerR2") ||
|
||||
!data["battery"].containsKey("externalVoltagePin") ||
|
||||
!data["battery"].containsKey("sendVoltageAsTelemetry")) needsRewrite = true;
|
||||
if (data["battery"]["sendInternalVoltage"].isNull() ||
|
||||
data["battery"]["monitorInternalVoltage"].isNull() ||
|
||||
data["battery"]["internalSleepVoltage"].isNull() ||
|
||||
data["battery"]["sendExternalVoltage"].isNull() ||
|
||||
data["battery"]["monitorExternalVoltage"].isNull() ||
|
||||
data["battery"]["externalSleepVoltage"].isNull() ||
|
||||
data["battery"]["useExternalI2CSensor"].isNull() ||
|
||||
data["battery"]["voltageDividerR1"].isNull() ||
|
||||
data["battery"]["voltageDividerR2"].isNull() ||
|
||||
data["battery"]["externalVoltagePin"].isNull() ||
|
||||
data["battery"]["sendVoltageAsTelemetry"].isNull()) needsRewrite = true;
|
||||
battery.sendInternalVoltage = data["battery"]["sendInternalVoltage"] | false;
|
||||
battery.monitorInternalVoltage = data["battery"]["monitorInternalVoltage"] | false;
|
||||
battery.internalSleepVoltage = data["battery"]["internalSleepVoltage"] | 2.9;
|
||||
@@ -342,38 +351,40 @@ bool Configuration::readFile() {
|
||||
battery.externalVoltagePin = data["battery"]["externalVoltagePin"] | 34;
|
||||
battery.sendVoltageAsTelemetry = data["battery"]["sendVoltageAsTelemetry"] | false;
|
||||
|
||||
if (!data["wxsensor"].containsKey("active") ||
|
||||
!data["wxsensor"].containsKey("heightCorrection") ||
|
||||
!data["wxsensor"].containsKey("temperatureCorrection")) needsRewrite = true;
|
||||
if (data["wxsensor"]["active"].isNull() ||
|
||||
data["wxsensor"]["heightCorrection"].isNull() ||
|
||||
data["wxsensor"]["temperatureCorrection"].isNull()) needsRewrite = true;
|
||||
wxsensor.active = data["wxsensor"]["active"] | false;
|
||||
wxsensor.heightCorrection = data["wxsensor"]["heightCorrection"] | 0;
|
||||
wxsensor.temperatureCorrection = data["wxsensor"]["temperatureCorrection"] | 0.0;
|
||||
|
||||
if (!data["syslog"].containsKey("active") ||
|
||||
!data["syslog"].containsKey("server") ||
|
||||
!data["syslog"].containsKey("port") ||
|
||||
!data["syslog"].containsKey("logBeaconOverTCPIP")) needsRewrite = true;
|
||||
if (data["syslog"]["active"].isNull() ||
|
||||
data["syslog"]["server"].isNull() ||
|
||||
data["syslog"]["port"].isNull() ||
|
||||
data["syslog"]["logBeaconOverTCPIP"].isNull()) needsRewrite = true;
|
||||
syslog.active = data["syslog"]["active"] | false;
|
||||
syslog.server = data["syslog"]["server"] | "lora.link9.net";
|
||||
syslog.port = data["syslog"]["port"] | 1514;
|
||||
syslog.logBeaconOverTCPIP = data["syslog"]["logBeaconOverTCPIP"] | false;
|
||||
|
||||
if (!data["tnc"].containsKey("enableServer") ||
|
||||
!data["tnc"].containsKey("enableSerial") ||
|
||||
!data["tnc"].containsKey("acceptOwn") ||
|
||||
!data["tnc"].containsKey("aprsBridgeActive")) needsRewrite = true;
|
||||
if (data["tnc"]["enableServer"].isNull() ||
|
||||
data["tnc"]["enableSerial"].isNull() ||
|
||||
data["tnc"]["acceptOwn"].isNull() ||
|
||||
data["tnc"]["aprsBridgeActive"].isNull() ||
|
||||
data["tnc"]["kissProtocol"].isNull()) needsRewrite = true;
|
||||
tnc.enableServer = data["tnc"]["enableServer"] | false;
|
||||
tnc.enableSerial = data["tnc"]["enableSerial"] | false;
|
||||
tnc.acceptOwn = data["tnc"]["acceptOwn"] | false;
|
||||
tnc.aprsBridgeActive = data["tnc"]["aprsBridgeActive"] | false;
|
||||
tnc.kissProtocol = data["tnc"]["kissProtocol"] | true;
|
||||
|
||||
if (!data["mqtt"].containsKey("active") ||
|
||||
!data["mqtt"].containsKey("server") ||
|
||||
!data["mqtt"].containsKey("topic") ||
|
||||
!data["mqtt"].containsKey("username") ||
|
||||
!data["mqtt"].containsKey("password") ||
|
||||
!data["mqtt"].containsKey("port") ||
|
||||
!data["mqtt"].containsKey("beaconOverMqtt")) needsRewrite = true;
|
||||
if (data["mqtt"]["active"].isNull() ||
|
||||
data["mqtt"]["server"].isNull() ||
|
||||
data["mqtt"]["topic"].isNull() ||
|
||||
data["mqtt"]["username"].isNull() ||
|
||||
data["mqtt"]["password"].isNull() ||
|
||||
data["mqtt"]["port"].isNull() ||
|
||||
data["mqtt"]["beaconOverMqtt"].isNull()) needsRewrite = true;
|
||||
mqtt.active = data["mqtt"]["active"] | false;
|
||||
mqtt.server = data["mqtt"]["server"] | "";
|
||||
mqtt.topic = data["mqtt"]["topic"] | "aprs-igate";
|
||||
@@ -381,35 +392,35 @@ bool Configuration::readFile() {
|
||||
mqtt.password = data["mqtt"]["password"] | "";
|
||||
mqtt.port = data["mqtt"]["port"] | 1883;
|
||||
mqtt.beaconOverMqtt = data["mqtt"]["beaconOverMqtt"] | false;
|
||||
|
||||
if (!data["ota"].containsKey("username") ||
|
||||
!data["ota"].containsKey("password")) needsRewrite = true;
|
||||
|
||||
if (data["ota"]["username"].isNull() ||
|
||||
data["ota"]["password"].isNull()) needsRewrite = true;
|
||||
ota.username = data["ota"]["username"] | "";
|
||||
ota.password = data["ota"]["password"] | "";
|
||||
|
||||
if (!data["webadmin"].containsKey("active") ||
|
||||
!data["webadmin"].containsKey("username") ||
|
||||
!data["webadmin"].containsKey("password")) needsRewrite = true;
|
||||
if (data["webadmin"]["active"].isNull() ||
|
||||
data["webadmin"]["username"].isNull() ||
|
||||
data["webadmin"]["password"].isNull()) needsRewrite = true;
|
||||
webadmin.active = data["webadmin"]["active"] | false;
|
||||
webadmin.username = data["webadmin"]["username"] | "admin";
|
||||
webadmin.password = data["webadmin"]["password"] | "";
|
||||
|
||||
if (!data["remoteManagement"].containsKey("managers") ||
|
||||
!data["remoteManagement"].containsKey("rfOnly")) needsRewrite = true;
|
||||
if (data["remoteManagement"]["managers"].isNull() ||
|
||||
data["remoteManagement"]["rfOnly"].isNull()) needsRewrite = true;
|
||||
remoteManagement.managers = data["remoteManagement"]["managers"] | "";
|
||||
remoteManagement.rfOnly = data["remoteManagement"]["rfOnly"] | true;
|
||||
|
||||
if (!data["ntp"].containsKey("server") ||
|
||||
!data["ntp"].containsKey("gmtCorrection")) needsRewrite = true;
|
||||
if (data["ntp"]["server"].isNull() ||
|
||||
data["ntp"]["gmtCorrection"].isNull()) needsRewrite = true;
|
||||
ntp.server = data["ntp"]["server"] | "pool.ntp.org";
|
||||
ntp.gmtCorrection = data["ntp"]["gmtCorrection"] | 0.0;
|
||||
|
||||
if (!data["other"].containsKey("rebootMode") ||
|
||||
!data["other"].containsKey("rebootModeTime")) needsRewrite = true;
|
||||
if (data["other"]["rebootMode"].isNull() ||
|
||||
data["other"]["rebootModeTime"].isNull()) needsRewrite = true;
|
||||
rebootMode = data["other"]["rebootMode"] | false;
|
||||
rebootModeTime = data["other"]["rebootModeTime"] | 6;
|
||||
|
||||
if (!data["other"].containsKey("rememberStationTime")) needsRewrite = true;
|
||||
if (data["other"]["rememberStationTime"].isNull()) needsRewrite = true;
|
||||
rememberStationTime = data["other"]["rememberStationTime"] | 30;
|
||||
|
||||
if (wifiAPs.size() == 0) { // If we don't have any WiFi's from config we need to add "empty" SSID for AUTO AP
|
||||
@@ -426,7 +437,7 @@ bool Configuration::readFile() {
|
||||
writeFile();
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
Serial.println("Config read successfuly");
|
||||
return true;
|
||||
} else {
|
||||
@@ -434,7 +445,7 @@ bool Configuration::readFile() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Configuration::setDefaultValues() {
|
||||
|
||||
WiFi_AP wifiap;
|
||||
@@ -445,6 +456,7 @@ void Configuration::setDefaultValues() {
|
||||
|
||||
startupDelay = 0;
|
||||
|
||||
wifiAutoAP.enabled = true;
|
||||
wifiAutoAP.password = "1234567890";
|
||||
wifiAutoAP.timeout = 10;
|
||||
|
||||
@@ -470,14 +482,14 @@ void Configuration::setDefaultValues() {
|
||||
beacon.sendViaAPRSIS = true;
|
||||
beacon.sendViaRF = false;
|
||||
beacon.beaconFreq = 1;
|
||||
|
||||
|
||||
beacon.statusActive = false;
|
||||
beacon.statusPacket = "";
|
||||
beacon.statusPacket = "";
|
||||
|
||||
beacon.gpsActive = false;
|
||||
beacon.ambiguityLevel = 0;
|
||||
|
||||
personalNote = "";
|
||||
personalNote = "";
|
||||
|
||||
blacklist = "";
|
||||
|
||||
@@ -496,11 +508,12 @@ void Configuration::setDefaultValues() {
|
||||
loramodule.txCodingRate4 = 5;
|
||||
loramodule.txSignalBandwidth = 125000;
|
||||
loramodule.power = 20;
|
||||
loramodule.cadActive = true;
|
||||
|
||||
display.alwaysOn = true;
|
||||
display.timeout = 4;
|
||||
display.turn180 = false;
|
||||
|
||||
|
||||
battery.sendInternalVoltage = false;
|
||||
battery.monitorInternalVoltage = false;
|
||||
battery.internalSleepVoltage = 2.9;
|
||||
@@ -528,6 +541,7 @@ void Configuration::setDefaultValues() {
|
||||
tnc.enableSerial = false;
|
||||
tnc.acceptOwn = false;
|
||||
tnc.aprsBridgeActive = false;
|
||||
tnc.kissProtocol = true;
|
||||
|
||||
mqtt.active = false;
|
||||
mqtt.server = "";
|
||||
@@ -558,16 +572,19 @@ void Configuration::setDefaultValues() {
|
||||
Serial.println("New Data Created... All is Written!");
|
||||
}
|
||||
|
||||
Configuration::Configuration() {
|
||||
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);
|
||||
|
||||
+70
-33
@@ -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,7 +23,6 @@
|
||||
#include "digi_utils.h"
|
||||
#include "wifi_utils.h"
|
||||
#include "lora_utils.h"
|
||||
#include "gps_utils.h"
|
||||
#include "display.h"
|
||||
#include "utils.h"
|
||||
|
||||
@@ -43,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("*", "");
|
||||
@@ -52,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 ? ":}" : ":";
|
||||
@@ -61,23 +89,26 @@ namespace DIGI_Utils {
|
||||
int digiMode = Config.digi.mode;
|
||||
String tempPath = path;
|
||||
|
||||
if (tempPath.indexOf("WIDE1-1") != -1 && (digiMode == 2 || digiMode == 3)) { // WIDE1-1 Digipeater
|
||||
if (tempPath.indexOf("*") != -1 ) return ""; // "*" shouldn't be in WIDE1-1 (only) type of packet
|
||||
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);
|
||||
if (path.indexOf("WIDE2-1") != -1) {
|
||||
} 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 (path.indexOf("WIDE2-2") != -1) {
|
||||
} else if (tempPath.indexOf("WIDE2-2") != -1) {
|
||||
tempPath.replace("WIDE2-2", stationCallsign + "*,WIDE2-1");
|
||||
} 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;
|
||||
@@ -101,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;
|
||||
@@ -122,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 "";
|
||||
}
|
||||
@@ -149,28 +186,28 @@ namespace DIGI_Utils {
|
||||
if (Sender == stationCallsign) return; // Avoid listening to self packets
|
||||
if (!thirdPartyPacket && Config.tacticalCallsign == "" && !Utils::callsignIsValid(Sender)) return; // No thirdParty + no tactical y no valid callsign
|
||||
|
||||
if (STATION_Utils::check25SegBuffer(Sender, temp.substring(temp.indexOf(":") + 2))) {
|
||||
STATION_Utils::updateLastHeard(Sender);
|
||||
Utils::typeOfPacket(temp, 2); // Digi
|
||||
bool queryMessage = false;
|
||||
int doubleColonIndex = temp.indexOf("::");
|
||||
if (doubleColonIndex > 10) { // it's a message
|
||||
String AddresseeAndMessage = temp.substring(doubleColonIndex + 2);
|
||||
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
|
||||
Addressee.trim();
|
||||
if (Addressee == stationCallsign) { // it's a message for me!
|
||||
queryMessage = APRS_IS_Utils::processReceivedLoRaMessage(Sender, AddresseeAndMessage, thirdPartyPacket);
|
||||
}
|
||||
}
|
||||
if (queryMessage) return; // answer should not be repeated.
|
||||
if (STATION_Utils::isIn25SegHashBuffer(Sender, temp.substring(temp.indexOf(":") + 2))) return;
|
||||
|
||||
String loraPacket = generateDigipeatedPacket(packet.substring(3), thirdPartyPacket);
|
||||
if (loraPacket != "") {
|
||||
STATION_Utils::addToOutputPacketBuffer(loraPacket);
|
||||
if (Config.digi.ecoMode != 1) displayToggle(true);
|
||||
lastScreenOn = millis();
|
||||
STATION_Utils::updateLastHeard(Sender);
|
||||
Utils::typeOfPacket(temp, 2); // Digi
|
||||
bool queryMessage = false;
|
||||
int doubleColonIndex = temp.indexOf("::");
|
||||
if (doubleColonIndex > 10) { // it's a message
|
||||
String AddresseeAndMessage = temp.substring(doubleColonIndex + 2);
|
||||
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
|
||||
Addressee.trim();
|
||||
if (Addressee == stationCallsign) { // it's a message for me!
|
||||
queryMessage = APRS_IS_Utils::processReceivedLoRaMessage(Sender, AddresseeAndMessage, thirdPartyPacket);
|
||||
}
|
||||
}
|
||||
if (queryMessage) return; // answer should not be repeated.
|
||||
|
||||
String loraPacket = generateDigipeatedPacket(packet.substring(3), thirdPartyPacket);
|
||||
if (loraPacket != "") {
|
||||
STATION_Utils::addToOutputPacketBuffer(loraPacket);
|
||||
if (Config.digi.ecoMode != 1) displayToggle(true);
|
||||
lastScreenOn = millis();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+34
-20
@@ -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,7 @@
|
||||
#ifdef HAS_TFT
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
TFT_eSPI tft = TFT_eSPI();
|
||||
TFT_eSPI tft = TFT_eSPI();
|
||||
TFT_eSprite sprite = TFT_eSprite(&tft);
|
||||
|
||||
#ifdef HELTEC_WIRELESS_TRACKER
|
||||
@@ -57,15 +57,21 @@
|
||||
String lastEpaperText;
|
||||
#else
|
||||
#include <Adafruit_GFX.h>
|
||||
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
|
||||
#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
|
||||
Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST);
|
||||
Adafruit_SSD1306 display(128, SCREEN_HEIGHT, &Wire, OLED_RST);
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
@@ -75,6 +81,7 @@
|
||||
extern Configuration Config;
|
||||
|
||||
bool displayFound = false;
|
||||
int maxLines;
|
||||
|
||||
void displaySetup() {
|
||||
#ifdef HAS_DISPLAY
|
||||
@@ -97,6 +104,13 @@ void displaySetup() {
|
||||
sprite.createSprite(160, 80);
|
||||
#endif
|
||||
#else
|
||||
#if (SCREEN_HEIGHT == 64)
|
||||
maxLines = 6;
|
||||
#elif (SCREEN_HEIGHT == 32)
|
||||
maxLines = 3;
|
||||
#else
|
||||
maxLines = 6;
|
||||
#endif
|
||||
#ifdef HAS_EPAPER
|
||||
display.landscape();
|
||||
display.printCenter("LoRa APRS iGate Initialising...");
|
||||
@@ -117,7 +131,7 @@ void displaySetup() {
|
||||
digitalWrite(OLED_RST, HIGH);
|
||||
#endif
|
||||
|
||||
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
|
||||
#ifdef HAS_SH1106
|
||||
if (display.begin(0x3c, false)) {
|
||||
displayFound = true;
|
||||
if (Config.display.turn180) display.setRotation(2);
|
||||
@@ -157,7 +171,7 @@ void displayToggle(bool toggle) {
|
||||
display.printCenter("EPAPER Display Disabled by toggle...");
|
||||
display.update();
|
||||
#else
|
||||
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
|
||||
#ifdef HAS_SH1106
|
||||
if (displayFound) display.oled_command(SH110X_DISPLAYON);
|
||||
#else
|
||||
if (displayFound) display.ssd1306_command(SSD1306_DISPLAYON);
|
||||
@@ -171,12 +185,12 @@ void displayToggle(bool toggle) {
|
||||
#ifdef HAS_EPAPER
|
||||
display.update();
|
||||
#else
|
||||
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
|
||||
#ifdef HAS_SH1106
|
||||
if (displayFound) display.oled_command(SH110X_DISPLAYOFF);
|
||||
#else
|
||||
if (displayFound) display.ssd1306_command(SSD1306_DISPLAYOFF);
|
||||
#endif
|
||||
|
||||
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
@@ -222,7 +236,7 @@ void displayShow(const String& header, const String& line1, const String& line2,
|
||||
#else
|
||||
if (displayFound) {
|
||||
display.clearDisplay();
|
||||
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
|
||||
#ifdef HAS_SH1106
|
||||
display.setTextColor(SH110X_WHITE);
|
||||
#else
|
||||
display.setTextColor(WHITE);
|
||||
@@ -234,7 +248,7 @@ void displayShow(const String& header, const String& line1, const String& line2,
|
||||
display.setCursor(0, 8 + (8 * i));
|
||||
display.println(*lines[i]);
|
||||
}
|
||||
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
|
||||
#ifdef HAS_SH1106
|
||||
display.setContrast(1);
|
||||
#else
|
||||
display.ssd1306_command(SSD1306_SETCONTRAST);
|
||||
@@ -288,7 +302,7 @@ void displayShow(const String& header, const String& line1, const String& line2,
|
||||
#else
|
||||
if (displayFound) {
|
||||
display.clearDisplay();
|
||||
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
|
||||
#ifdef HAS_SH1106
|
||||
display.setTextColor(SH110X_WHITE);
|
||||
#else
|
||||
display.setTextColor(WHITE);
|
||||
@@ -297,11 +311,11 @@ void displayShow(const String& header, const String& line1, const String& line2,
|
||||
display.setCursor(0, 0);
|
||||
display.println(header);
|
||||
display.setTextSize(1);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int i = 0; i < maxLines; i++) {
|
||||
display.setCursor(0, 16 + (8 * i));
|
||||
display.println(*lines[i]);
|
||||
}
|
||||
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
|
||||
#ifdef HAS_SH1106
|
||||
display.setContrast(1);
|
||||
#else
|
||||
display.ssd1306_command(SSD1306_SETCONTRAST);
|
||||
|
||||
+25
-18
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
|
||||
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
|
||||
*
|
||||
* This file is part of LoRa APRS iGate.
|
||||
*
|
||||
@@ -154,10 +154,6 @@ namespace GPS_Utils {
|
||||
int indexOfAt = packet.indexOf(":@");
|
||||
if (indexOfAt > 10) return getReceivedGPS(packet);
|
||||
|
||||
const uint8_t nonEncondedLatitudeOffset = 9; // "N" / "S"
|
||||
const uint8_t nonEncondedLongitudeOffset = 19; // "E" / "W"
|
||||
const uint8_t encodedByteOffset = 14;
|
||||
|
||||
int indexOfExclamation = packet.indexOf(":!");
|
||||
int indexOfEqual = packet.indexOf(":=");
|
||||
int baseIndex = - 1;
|
||||
@@ -168,23 +164,34 @@ namespace GPS_Utils {
|
||||
}
|
||||
if (baseIndex == -1) return " _ / _ / _ ";
|
||||
|
||||
int latitudeIndex = baseIndex + nonEncondedLatitudeOffset;
|
||||
int longitudeIndex = baseIndex + nonEncondedLongitudeOffset;
|
||||
int encodedByteIndex = baseIndex + encodedByteOffset;
|
||||
int packetLength = packet.length();
|
||||
int packetLength = packet.length();
|
||||
if (baseIndex + 2 >= packetLength) return " _ / _ / _ ";
|
||||
|
||||
if (latitudeIndex < packetLength && longitudeIndex < packetLength) {
|
||||
char latChar = packet[latitudeIndex];
|
||||
char lngChar = packet[longitudeIndex];
|
||||
if ((latChar == 'N' || latChar == 'S') && (lngChar == 'E' || lngChar == 'W')) return getReceivedGPS(packet);
|
||||
}
|
||||
if (encodedByteIndex < packetLength) {
|
||||
char byteChar = packet[encodedByteIndex];
|
||||
if (byteChar == 'G' || byteChar == 'Q' || byteChar == '[' || byteChar == 'H' || byteChar == 'X' || byteChar == '3') return decodeEncodedGPS(packet);
|
||||
}
|
||||
// Spec-based: the first character right after the DTI decides the format.
|
||||
// Uncompressed positions always start with a latitude digit.
|
||||
// Compressed positions always start with the symbol table id: '/', '\', or an overlay A-Z/a-j.
|
||||
char firstInfoChar = packet[baseIndex + 2];
|
||||
if (firstInfoChar >= '0' && firstInfoChar <= '9') return getReceivedGPS(packet);
|
||||
if (firstInfoChar == '/' || firstInfoChar == '\\' ||
|
||||
(firstInfoChar >= 'A' && firstInfoChar <= 'Z') ||
|
||||
(firstInfoChar >= 'a' && firstInfoChar <= 'j')) return decodeEncodedGPS(packet);
|
||||
return " _ / _ / _ ";
|
||||
}
|
||||
|
||||
String getDistanceAndCommentFromMicE(const String& packet) {
|
||||
// Mic-E encodes latitude in the AX.25 destination field and longitude
|
||||
// in the info field with its own +28 offset scheme -- nothing like
|
||||
// the non-encoded/base91 formats above. APRSPacketLib::processReceivedPacket
|
||||
// already implements that decode (used for the map upsert in
|
||||
// lora_utils.cpp), so it's reused here instead of duplicating it.
|
||||
if (packet.length() < 4) return " _ / _ / _ ";
|
||||
|
||||
APRSPacket aprsPacket = APRSPacketLib::processReceivedPacket(packet.substring(3), 0, 0, 0);
|
||||
if (aprsPacket.type != 4) return " _ / _ / _ "; // 4 = Mic-E
|
||||
|
||||
return buildDistanceAndComment(aprsPacket.latitude, aprsPacket.longitude, "");
|
||||
}
|
||||
|
||||
void setup() {
|
||||
#ifdef HAS_GPS
|
||||
if (Config.beacon.gpsActive && Config.digi.ecoMode != 1) {
|
||||
|
||||
+8
-17
@@ -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,29 +29,20 @@ bool validateKISSFrame(const String& kissFormattedFrame) {
|
||||
}
|
||||
|
||||
String encodeAddressAX25(String tnc2Address) {
|
||||
bool hasBeenDigipited = tnc2Address.indexOf('*') != -1;
|
||||
int tnc2AddressIndex = tnc2Address.indexOf('-');
|
||||
if (tnc2AddressIndex == -1) {
|
||||
if (hasBeenDigipited) {
|
||||
tnc2Address = tnc2Address.substring(0, tnc2Address.length() - 1);
|
||||
}
|
||||
bool hasBeenDigipited = tnc2Address.indexOf('*') != -1;
|
||||
if (tnc2Address.indexOf('-') == -1) {
|
||||
if (hasBeenDigipited) tnc2Address = tnc2Address.substring(0, tnc2Address.length() - 1);
|
||||
tnc2Address += "-0";
|
||||
}
|
||||
|
||||
int separatorIndex = tnc2AddressIndex;
|
||||
int ssid = tnc2Address.substring(separatorIndex + 1).toInt();
|
||||
|
||||
int separatorIndex = tnc2Address.indexOf('-');;
|
||||
int ssid = tnc2Address.substring(separatorIndex + 1).toInt();
|
||||
String kissAddress = "";
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
char addressChar;
|
||||
if (tnc2Address.length() > i && i < separatorIndex) {
|
||||
addressChar = tnc2Address.charAt(i);
|
||||
} else {
|
||||
addressChar = ' ';
|
||||
}
|
||||
char addressChar = ' ';
|
||||
if (tnc2Address.length() > i && i < separatorIndex) addressChar = tnc2Address.charAt(i);
|
||||
kissAddress += (char)(addressChar << 1);
|
||||
}
|
||||
|
||||
kissAddress += (char)((ssid << 1) | 0b01100000 | (hasBeenDigipited ? HAS_BEEN_DIGIPITED_MASK : 0));
|
||||
return kissAddress;
|
||||
}
|
||||
|
||||
+71
-19
@@ -1,41 +1,46 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
#include <APRSPacketLib.h>
|
||||
#include <RadioLib.h>
|
||||
#include <WiFi.h>
|
||||
#include "configuration.h"
|
||||
#include "network_manager.h"
|
||||
#include "aprs_is_utils.h"
|
||||
#include "station_utils.h"
|
||||
#include "board_pinout.h"
|
||||
#include "syslog_utils.h"
|
||||
#include "map_utils.h"
|
||||
#include "ntp_utils.h"
|
||||
#include "display.h"
|
||||
#include "utils.h"
|
||||
|
||||
|
||||
extern Configuration Config;
|
||||
extern uint32_t lastRxTime;
|
||||
extern NetworkManager *networkManager;
|
||||
extern bool packetIsBeacon;
|
||||
|
||||
extern std::vector<ReceivedPacket> receivedPackets;
|
||||
|
||||
bool operationDone = true;
|
||||
bool transmitFlag = true;
|
||||
bool operationDone = true;
|
||||
bool transmitFlag = true;
|
||||
|
||||
#define DIFS_SLOTS 2 // Number of secuential CAD slots to consider a free channel to Tx
|
||||
int backoffMax = 4; // Max Backoff value (number of CAD slots to wait before Tx)
|
||||
|
||||
#ifdef HAS_SX1262
|
||||
SX1262 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN);
|
||||
@@ -43,7 +48,7 @@ bool transmitFlag = true;
|
||||
#ifdef HAS_SX1268
|
||||
#if defined(LIGHTGATEWAY_1_0) || defined(LIGHTGATEWAY_PLUS_1_0)
|
||||
SPIClass loraSPI(FSPI);
|
||||
SX1268 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN, loraSPI);
|
||||
SX1268 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN, loraSPI);
|
||||
#else
|
||||
SX1268 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN);
|
||||
#endif
|
||||
@@ -64,6 +69,15 @@ float snr;
|
||||
|
||||
namespace LoRa_Utils {
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -100,11 +114,11 @@ namespace LoRa_Utils {
|
||||
while (true);
|
||||
}
|
||||
#endif*/
|
||||
|
||||
|
||||
radio.setSpreadingFactor(Config.loramodule.rxSpreadingFactor);
|
||||
radio.setCodingRate(Config.loramodule.rxCodingRate4);
|
||||
float signalBandwidth = Config.loramodule.rxSignalBandwidth / 1000;
|
||||
radio.setBandwidth(signalBandwidth);
|
||||
radio.setBandwidth(signalBandwidth);
|
||||
radio.setCRC(true);
|
||||
|
||||
#if (defined(RADIO_RXEN) && defined(RADIO_TXEN)) // QRP Labs LightGateway has 400M22S (SX1268)
|
||||
@@ -166,6 +180,34 @@ namespace LoRa_Utils {
|
||||
radio.setBandwidth(signalBandwidth);
|
||||
}
|
||||
|
||||
bool doCAD() { // CAD (Channel Activity Detection)
|
||||
return radio.scanChannel() != RADIOLIB_CHANNEL_FREE; // false=channel free | true=RADIOLIB_LORA_DETECTED or CAD failed
|
||||
}
|
||||
|
||||
bool doDIFS() {
|
||||
for (uint8_t i = DIFS_SLOTS; i > 0; i--) {
|
||||
if (doCAD()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void waitForDIFS() {
|
||||
while (!doDIFS()) {
|
||||
Serial.println("CAD/DIFS failed, retry...");
|
||||
}
|
||||
}
|
||||
|
||||
void doBEB() {
|
||||
int backoffCounter = random(1, backoffMax + 1);
|
||||
while (backoffCounter > 0) {
|
||||
if (doCAD()) {
|
||||
waitForDIFS(); // busy channel: freeze backoff and restart DIFS
|
||||
} else {
|
||||
backoffCounter--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sendNewPacket(const String& newPacket) {
|
||||
if (!Config.loramodule.txActive) return;
|
||||
|
||||
@@ -174,14 +216,20 @@ namespace LoRa_Utils {
|
||||
changeFreqTx();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#ifdef INTERNAL_LED_PIN
|
||||
if (Config.digi.ecoMode != 1) digitalWrite(INTERNAL_LED_PIN, HIGH); // disabled in Ultra Eco Mode
|
||||
#endif
|
||||
|
||||
if (Config.loramodule.cadActive) {
|
||||
waitForDIFS(); // DIFS (Distributed Inter-Frame Space)
|
||||
doBEB(); // BEB (Binary Exponential Backoff)
|
||||
}
|
||||
|
||||
int state = radio.transmit("\x3c\xff\x01" + newPacket);
|
||||
transmitFlag = true;
|
||||
if (state == RADIOLIB_ERR_NONE) {
|
||||
if (Config.syslog.active && WiFi.status() == WL_CONNECTED) {
|
||||
if (Config.syslog.active && networkManager->isConnected()) {
|
||||
SYSLOG_Utils::log(3, newPacket, 0, 0.0, 0); // TX
|
||||
}
|
||||
Utils::print("---> LoRa Packet Tx : ");
|
||||
@@ -237,19 +285,23 @@ 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.syslog.active && WiFi.status() == WL_CONNECTED) {
|
||||
if (Config.syslog.active && networkManager->isConnected()) {
|
||||
SYSLOG_Utils::log(1, packet, rssi, snr, freqError); // RX
|
||||
}
|
||||
} else {
|
||||
packet = "";
|
||||
}
|
||||
lastRxTime = millis();
|
||||
return packet;
|
||||
}
|
||||
} else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
|
||||
@@ -257,7 +309,7 @@ namespace LoRa_Utils {
|
||||
snr = radio.getSNR();
|
||||
freqError = radio.getFrequencyError();
|
||||
Utils::println(F("CRC error!"));
|
||||
if (Config.syslog.active && WiFi.status() == WL_CONNECTED) {
|
||||
if (Config.syslog.active && networkManager->isConnected()) {
|
||||
SYSLOG_Utils::log(0, packet, rssi, snr, freqError); // CRC
|
||||
}
|
||||
packet = "";
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/* 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 <ArduinoJson.h>
|
||||
#include "map_utils.h"
|
||||
#include "ntp_utils.h"
|
||||
|
||||
#define MAX_MAP_STATIONS 50
|
||||
#define STATION_TTL_MS 3600000UL // 1 hora (en milisegundos)
|
||||
|
||||
|
||||
std::vector<MapStation> mapStations;
|
||||
|
||||
|
||||
namespace MAP_Utils {
|
||||
|
||||
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);
|
||||
} else {
|
||||
dst[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
s.rssi = rssi;
|
||||
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) full? delete oldest one
|
||||
size_t oldest = 0;
|
||||
for (size_t i = 1; i < mapStations.size(); i++) {
|
||||
if (mapStations[i].lastHeardMillis < mapStations[oldest].lastHeardMillis) {
|
||||
oldest = i;
|
||||
}
|
||||
}
|
||||
mapStations.erase(mapStations.begin() + oldest);
|
||||
}
|
||||
|
||||
MapStation st = {}; // 3) 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);
|
||||
}
|
||||
|
||||
void purgeOldStations() {
|
||||
uint32_t now = millis();
|
||||
for (int i = (int)mapStations.size() - 1; i >= 0; i--) {
|
||||
if (now - mapStations[i].lastHeardMillis > STATION_TTL_MS) {
|
||||
mapStations.erase(mapStations.begin() + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String getStationsJson() {
|
||||
purgeOldStations();
|
||||
JsonDocument data;
|
||||
|
||||
for (size_t i = 0; i < mapStations.size(); i++) {
|
||||
data[i]["callsign"] = mapStations[i].callsign;
|
||||
data[i]["lat"] = mapStations[i].latitude;
|
||||
data[i]["lon"] = mapStations[i].longitude;
|
||||
data[i]["symbol"] = mapStations[i].symbol;
|
||||
data[i]["RSSI"] = mapStations[i].rssi;
|
||||
data[i]["SNR"] = mapStations[i].snr;
|
||||
data[i]["count"] = mapStations[i].count;
|
||||
data[i]["lastHeard"] = mapStations[i].lastHeard;
|
||||
data[i]["path"] = mapStations[i].path;
|
||||
data[i]["heardDirect"] = mapStations[i].heardDirect;
|
||||
}
|
||||
|
||||
String buffer;
|
||||
|
||||
serializeJson(data, buffer);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
}
|
||||
+8
-8
@@ -1,22 +1,22 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <WiFiClient.h>
|
||||
#include <PubSubClient.h>
|
||||
#include "configuration.h"
|
||||
#include "station_utils.h"
|
||||
@@ -95,7 +95,7 @@ namespace MQTT_Utils {
|
||||
void setup() {
|
||||
if (!Config.mqtt.active) return;
|
||||
pubSub.setClient(mqttClient);
|
||||
pubSub.setCallback(receivedFromMqtt);
|
||||
pubSub.setCallback(receivedFromMqtt);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
/* 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"
|
||||
|
||||
// Constructor
|
||||
NetworkManager::NetworkManager() { }
|
||||
|
||||
// Destructor
|
||||
NetworkManager::~NetworkManager() { }
|
||||
|
||||
// Private methods
|
||||
|
||||
int NetworkManager::_findWiFiNetworkIndex(const String& ssid) const {
|
||||
for (size_t i = 0; i < _wifiNetworks.size(); i++) {
|
||||
if (_wifiNetworks[i].ssid == ssid) {
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool NetworkManager::_connectWiFi(const WiFiNetwork& network) {
|
||||
if (network.ssid.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_wifiSTAmode = true;
|
||||
|
||||
if (!_hostName.isEmpty()) {
|
||||
WiFi.setHostname(_hostName.c_str());
|
||||
}
|
||||
|
||||
WiFi.mode(_wifiAPmode ? WIFI_AP_STA : WIFI_STA);
|
||||
|
||||
Serial.println("[NM] Attempting to connect to WiFi: " + network.ssid);
|
||||
WiFi.begin(network.ssid.c_str(), network.psk.c_str());
|
||||
|
||||
Serial.print("[NM] Connecting ");
|
||||
|
||||
int attempts = 0;
|
||||
while (!isWiFiConnected() && attempts < 10) {
|
||||
delay(500);
|
||||
#ifdef INTERNAL_LED_PIN
|
||||
digitalWrite(INTERNAL_LED_PIN,HIGH);
|
||||
#endif
|
||||
Serial.print('.');
|
||||
delay(500);
|
||||
#ifdef INTERNAL_LED_PIN
|
||||
digitalWrite(INTERNAL_LED_PIN,LOW);
|
||||
#endif
|
||||
attempts++;
|
||||
}
|
||||
Serial.println();
|
||||
|
||||
if (isWiFiConnected()) return true;
|
||||
|
||||
Serial.println("[NM] Failed to connect to WiFi after " + String(attempts) + " attempts. SSID: " + network.ssid);
|
||||
return false;
|
||||
}
|
||||
|
||||
void NetworkManager::_processAPTimeout() {
|
||||
if (!_wifiAPmode || _apTimeout == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If any station is connected, reset the timer
|
||||
if (WiFi.softAPgetStationNum() > 0) {
|
||||
_apStartup = millis();
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() - _apStartup > _apTimeout) {
|
||||
Serial.println("[NM] AP timeout reached. Disabling AP mode.");
|
||||
disableAP();
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkManager::_onNetworkEvent(arduino_event_id_t event, arduino_event_info_t /*info*/) {
|
||||
switch (event) {
|
||||
case ARDUINO_EVENT_ETH_START:
|
||||
Serial.println("[NM] ETH Started");
|
||||
if (!_hostName.isEmpty()) {
|
||||
Serial.println("[NM] ETH Setting Hostname: " + _hostName);
|
||||
ETH.setHostname(_hostName.c_str());
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_CONNECTED:
|
||||
Serial.println("[NM] ETH Connected");
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_GOT_IP:
|
||||
Serial.println("[NM] ETH Got IP");
|
||||
_ethernetConnected = true;
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_DISCONNECTED:
|
||||
Serial.println("[NM] ETH Disconnected");
|
||||
_ethernetConnected = false;
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_STOP:
|
||||
Serial.println("[NM] ETH Stopped");
|
||||
_ethernetConnected = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
bool NetworkManager::setup() {
|
||||
Serial.println("[NM] Initializing Networking...");
|
||||
|
||||
WiFi.onEvent(
|
||||
[this](arduino_event_id_t event, arduino_event_info_t info) {
|
||||
_onNetworkEvent(event, info);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void NetworkManager::loop() {
|
||||
if (_wifiAPmode) {
|
||||
_processAPTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkManager::setHostName(const String& hostName) {
|
||||
_hostName = hostName;
|
||||
}
|
||||
|
||||
// WiFi methods
|
||||
|
||||
bool NetworkManager::setupAP(String apName, String apPsk) {
|
||||
_wifiAPmode = true;
|
||||
|
||||
Serial.println("[NM] Starting AP mode: " + apName);
|
||||
|
||||
// Full WiFi reset sequence
|
||||
WiFi.disconnect(true);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
delay(200);
|
||||
|
||||
// Set up AP mode with optimized settings
|
||||
WiFi.mode(WIFI_AP);
|
||||
|
||||
bool apStarted = WiFi.softAP(apName.c_str(), apPsk.c_str());
|
||||
delay(1000); // Give AP time to fully initialize
|
||||
|
||||
if (apStarted) {
|
||||
Serial.println("[NM] AP setup successful");
|
||||
_apStartup = millis();
|
||||
}
|
||||
else {
|
||||
Serial.println("[NM] AP setup failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
IPAddress apIP = getWiFiAPIP();
|
||||
Serial.println("[NM] AP IP assigned: " + apIP.toString());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NetworkManager::disableAP() {
|
||||
WiFi.mode(_wifiSTAmode ? WIFI_STA : WIFI_OFF);
|
||||
_wifiAPmode = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void NetworkManager::setAPTimeout(unsigned long timeout) {
|
||||
Serial.println("[NM] Setting AP timeout to " + String(timeout / 1000) + " sec");
|
||||
_apTimeout = timeout;
|
||||
}
|
||||
|
||||
void NetworkManager::addWiFiNetwork(const String& ssid, const String& psk) {
|
||||
if (ssid.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int index = _findWiFiNetworkIndex(ssid);
|
||||
if (index >= 0) {
|
||||
Serial.println("[NM] Updating WiFi network: " + ssid);
|
||||
_wifiNetworks[static_cast<size_t>(index)].psk = psk;
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("[NM] Adding WiFi network: " + ssid);
|
||||
WiFiNetwork network;
|
||||
network.ssid = ssid;
|
||||
network.psk = psk;
|
||||
_wifiNetworks.push_back(network);
|
||||
}
|
||||
|
||||
void NetworkManager::clearWiFiNetworks() {
|
||||
_wifiNetworks.clear();
|
||||
}
|
||||
|
||||
bool NetworkManager::hasWiFiNetworks() const {
|
||||
return !_wifiNetworks.empty();
|
||||
}
|
||||
|
||||
size_t NetworkManager::getWiFiNetworkCount() const {
|
||||
return _wifiNetworks.size();
|
||||
}
|
||||
|
||||
bool NetworkManager::connectWiFi() {
|
||||
if (_wifiNetworks.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < _wifiNetworks.size(); i++) {
|
||||
disconnectWiFi();
|
||||
if (_connectWiFi(_wifiNetworks[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NetworkManager::connectWiFi(const String& ssid, const String& psk) {
|
||||
addWiFiNetwork(ssid, psk);
|
||||
return connectWiFi();
|
||||
}
|
||||
|
||||
bool NetworkManager::disconnectWiFi() {
|
||||
WiFi.disconnect(true);
|
||||
WiFi.mode(_wifiAPmode ? WIFI_AP : WIFI_OFF);
|
||||
|
||||
_wifiSTAmode = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
String NetworkManager::getWiFiSSID() const {
|
||||
return WiFi.SSID();
|
||||
}
|
||||
|
||||
String NetworkManager::getWiFiAPSSID() const {
|
||||
return WiFi.softAPSSID();
|
||||
}
|
||||
|
||||
IPAddress NetworkManager::getWiFiIP() const {
|
||||
return WiFi.localIP();
|
||||
}
|
||||
|
||||
IPAddress NetworkManager::getWiFiAPIP() const {
|
||||
return WiFi.softAPIP();
|
||||
}
|
||||
|
||||
wifi_mode_t NetworkManager::getWiFiMode() const {
|
||||
return WiFi.getMode();
|
||||
}
|
||||
|
||||
uint8_t* NetworkManager::getWiFimacAddress(uint8_t* mac) {
|
||||
return WiFi.macAddress(mac);
|
||||
}
|
||||
|
||||
String NetworkManager::getWiFimacAddress(void) const {
|
||||
return WiFi.macAddress();
|
||||
}
|
||||
|
||||
// Ethernet methods
|
||||
bool NetworkManager::ethernetConnect(eth_phy_type_t type, uint8_t phy_addr, uint8_t mdc, uint8_t mdio, int power, eth_clock_mode_t clock_mode, bool use_mac_from_efuse) {
|
||||
_ethernetMode = true;
|
||||
Serial.println("[NM] Setting up Ethernet...");
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
// SDK 5.x (Arduino SDK 3.x)
|
||||
#pragma message("Compiling ETH init: SDK 5.x (Arduino core 3.x)")
|
||||
return ETH.begin(type, phy_addr, mdc, mdio, power, clock_mode, use_mac_from_efuse);
|
||||
#else
|
||||
// SDK 4.x (Arduino SDK 2.x)
|
||||
#pragma message("Compiling ETH init: SDK 4.x (Arduino core 2.x)")
|
||||
return ETH.begin(phy_addr, power, mdc, mdio, type, clock_mode, use_mac_from_efuse);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool NetworkManager::setEthernetIP(const String& staticIP, const String& gateway, const String& subnet, const String& dns1, const String& dns2) {
|
||||
if (staticIP.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IPAddress ip, gw, sn, d1, d2;
|
||||
if (!ip.fromString(staticIP) || !gw.fromString(gateway) || !sn.fromString(subnet)) {
|
||||
Serial.println("[NM] Invalid static IP configuration");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!dns1.isEmpty() && d1.fromString(dns1)) {
|
||||
if (!dns2.isEmpty() && d2.fromString(dns2)) {
|
||||
ETH.config(ip, gw, sn, d1, d2);
|
||||
} else {
|
||||
ETH.config(ip, gw, sn, d1);
|
||||
}
|
||||
} else {
|
||||
ETH.config(ip, gw, sn);
|
||||
}
|
||||
|
||||
Serial.println("[NM] Ethernet static IP: " + staticIP);
|
||||
return true;
|
||||
}
|
||||
|
||||
IPAddress NetworkManager::getEthernetIP() const {
|
||||
return ETH.localIP();
|
||||
}
|
||||
|
||||
String NetworkManager::getEthernetMACAddress() const {
|
||||
return ETH.macAddress();
|
||||
}
|
||||
|
||||
// Check if network is available
|
||||
bool NetworkManager::isConnected() const {
|
||||
return isWiFiConnected() || isEthernetConnected() || isModemConnected();
|
||||
}
|
||||
|
||||
// Check if WiFi is connected
|
||||
bool NetworkManager::isWiFiConnected() const {
|
||||
return _wifiSTAmode ? WiFi.status() == WL_CONNECTED : false;
|
||||
}
|
||||
|
||||
bool NetworkManager::isWifiAPActive() const {
|
||||
return _wifiAPmode;
|
||||
}
|
||||
|
||||
// Check if Ethernet is connected
|
||||
bool NetworkManager::isEthernetConnected() const {
|
||||
return _ethernetMode && _ethernetConnected && ETH.linkUp();
|
||||
}
|
||||
|
||||
// Check if Modem is connected
|
||||
bool NetworkManager::isModemConnected() const {
|
||||
// Implement Modem connection check logic here
|
||||
return false;
|
||||
}
|
||||
+28
-14
@@ -1,52 +1,66 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
#include <NTPClient.h>
|
||||
#include <WiFiUdp.h>
|
||||
#include <WiFi.h>
|
||||
#include "configuration.h"
|
||||
#include "network_manager.h"
|
||||
#include "ntp_utils.h"
|
||||
#include "time.h"
|
||||
|
||||
|
||||
extern Configuration Config;
|
||||
|
||||
extern NetworkManager *networkManager;
|
||||
WiFiUDP ntpUDP;
|
||||
NTPClient* timeClient;
|
||||
NTPClient* timeClient = nullptr;
|
||||
|
||||
|
||||
namespace NTP_Utils {
|
||||
|
||||
void setup() {
|
||||
if (WiFi.status() == WL_CONNECTED && Config.digi.ecoMode == 0 && Config.callsign != "NOCALL-10") {
|
||||
bool setup() {
|
||||
if (networkManager->isConnected() && Config.digi.ecoMode == 0 && Config.callsign != "NOCALL-10") {
|
||||
int gmt = Config.ntp.gmtCorrection * 3600;
|
||||
Serial.println("[NTP] Setting up, TZ offset: " + String(gmt) + " Server: " + Config.ntp.server);
|
||||
timeClient = new NTPClient(ntpUDP, Config.ntp.server.c_str(), gmt, 15 * 60 * 1000); // Update interval 15 min
|
||||
timeClient->begin();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void update() {
|
||||
if (WiFi.status() == WL_CONNECTED && Config.digi.ecoMode == 0 && Config.callsign != "NOCALL-10") timeClient->update();
|
||||
if (!networkManager->isConnected() || Config.digi.ecoMode != 0 || Config.callsign == "NOCALL-10") {
|
||||
return;
|
||||
}
|
||||
if (timeClient == nullptr) {
|
||||
if (!setup()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
timeClient->update();
|
||||
}
|
||||
|
||||
String getFormatedTime() {
|
||||
if (WiFi.status() == WL_CONNECTED && Config.digi.ecoMode == 0) return timeClient->getFormattedTime();
|
||||
if (networkManager->isConnected() && Config.digi.ecoMode == 0 && timeClient != nullptr) {
|
||||
return timeClient->getFormattedTime();
|
||||
}
|
||||
return "DigiEcoMode Active";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -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/>.
|
||||
*/
|
||||
@@ -74,7 +74,7 @@ namespace OTA_Utils {
|
||||
Serial.println(success ? "OTA update finished successfully!" : "There was an error during OTA update!");
|
||||
displayShow("", "", statusMessage, "", rebootMessage, "", "", 4000);
|
||||
|
||||
isUpdatingOTA = false;
|
||||
if (!success) isUpdatingOTA = false;
|
||||
}
|
||||
|
||||
}
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
|
||||
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
|
||||
*
|
||||
* This file is part of LoRa APRS iGate.
|
||||
*
|
||||
@@ -263,8 +263,8 @@ namespace POWER_Utils {
|
||||
pinMode(Config.battery.externalVoltagePin, INPUT);
|
||||
}
|
||||
|
||||
#ifdef VEXT_CTRL
|
||||
pinMode(VEXT_CTRL,OUTPUT); // GPS + TFT on HELTEC Wireless_Tracker and only for Oled in HELTEC V3
|
||||
#ifdef VEXT_CTRL_PIN
|
||||
pinMode(VEXT_CTRL_PIN,OUTPUT); // GPS + TFT on HELTEC Wireless_Tracker and only for Oled in HELTEC V3
|
||||
vext_ctrl_ON();
|
||||
#endif
|
||||
|
||||
@@ -272,8 +272,8 @@ namespace POWER_Utils {
|
||||
if (Config.beacon.gpsActive && Config.digi.ecoMode != 1) activateGPS();
|
||||
#endif
|
||||
|
||||
#ifdef ADC_CTRL
|
||||
pinMode(ADC_CTRL, OUTPUT);
|
||||
#ifdef ADC_CTRL_PIN
|
||||
pinMode(ADC_CTRL_PIN, OUTPUT);
|
||||
adc_ctrl_OFF();
|
||||
#endif
|
||||
|
||||
|
||||
+4
-4
@@ -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
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
|
||||
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
|
||||
*
|
||||
* This file is part of LoRa APRS iGate.
|
||||
*
|
||||
|
||||
+61
-60
@@ -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/>.
|
||||
*/
|
||||
@@ -27,11 +27,9 @@
|
||||
|
||||
|
||||
extern Configuration Config;
|
||||
extern uint32_t lastRxTime;
|
||||
extern String fourthLine;
|
||||
extern bool shouldSleepLowVoltage;
|
||||
|
||||
uint32_t lastTxTime = millis();
|
||||
std::vector<LastHeardStation> lastHeardStations;
|
||||
std::vector<String> blacklist;
|
||||
std::vector<String> managers;
|
||||
@@ -40,17 +38,16 @@ std::vector<LastHeardStation> lastHeardObjects;
|
||||
struct OutputPacketBuffer {
|
||||
String packet;
|
||||
bool isBeacon;
|
||||
OutputPacketBuffer(const String& p, bool b) : packet(p), isBeacon(b) {}
|
||||
};
|
||||
std::vector<OutputPacketBuffer> outputPacketBuffer;
|
||||
|
||||
struct Packet25SegBuffer {
|
||||
uint32_t receivedTime;
|
||||
String station;
|
||||
String payload;
|
||||
uint32_t hash;
|
||||
};
|
||||
std::vector<Packet25SegBuffer> packet25SegBuffer;
|
||||
|
||||
|
||||
bool saveNewDigiEcoModeConfig = false;
|
||||
bool packetIsBeacon = false;
|
||||
|
||||
@@ -59,19 +56,18 @@ namespace STATION_Utils {
|
||||
|
||||
std::vector<String> loadCallsignList(const String& list) {
|
||||
std::vector<String> loadedList;
|
||||
int start = 0;
|
||||
int listLength = list.length();
|
||||
|
||||
String callsigns = list;
|
||||
callsigns.trim();
|
||||
while (start < listLength) {
|
||||
while (start < listLength && list[start] == ' ') start++; // avoid blank spaces
|
||||
if (start >= listLength) break;
|
||||
|
||||
while (callsigns.length() > 0) { // != ""
|
||||
int spaceIndex = callsigns.indexOf(" ");
|
||||
if (spaceIndex == -1) { // No more spaces, add the last part
|
||||
loadedList.push_back(callsigns);
|
||||
break;
|
||||
}
|
||||
loadedList.push_back(callsigns.substring(0, spaceIndex));
|
||||
callsigns = callsigns.substring(spaceIndex + 1);
|
||||
callsigns.trim(); // Trim in case of multiple spaces
|
||||
int end = start;
|
||||
while (end < listLength && list[end] != ' ') end++; // find another blank space or reach listLength
|
||||
|
||||
loadedList.emplace_back(list.substring(start, end));
|
||||
start = end + 1; // keep on searching if listLength not reached
|
||||
}
|
||||
return loadedList;
|
||||
}
|
||||
@@ -85,8 +81,9 @@ namespace STATION_Utils {
|
||||
for (size_t i = 0; i < list.size(); i++) {
|
||||
int wildcardIndex = list[i].indexOf("*");
|
||||
if (wildcardIndex >= 0) {
|
||||
String wildcard = list[i].substring(0, wildcardIndex);
|
||||
if (callsign.startsWith(wildcard)) return true;
|
||||
if (wildcardIndex >= 2 && callsign.length() >= wildcardIndex && strncmp(callsign.c_str(), list[i].c_str(), wildcardIndex) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (list[i] == callsign) return true;
|
||||
}
|
||||
@@ -127,36 +124,33 @@ namespace STATION_Utils {
|
||||
}
|
||||
|
||||
void deleteNotHeard() {
|
||||
std::vector<LastHeardStation> lastHeardStation_temp;
|
||||
for (int i = 0; i < lastHeardStations.size(); i++) {
|
||||
if (millis() - lastHeardStations[i].lastHeardTime < Config.rememberStationTime * 60 * 1000) {
|
||||
lastHeardStation_temp.push_back(lastHeardStations[i]);
|
||||
uint32_t currentTime = millis();
|
||||
uint32_t timeout = Config.rememberStationTime * 60UL * 1000UL;
|
||||
|
||||
for (int i = lastHeardStations.size() - 1; i >= 0; i--) {
|
||||
if (currentTime - lastHeardStations[i].lastHeardTime >= timeout) {
|
||||
lastHeardStations.erase(lastHeardStations.begin() + i);
|
||||
}
|
||||
}
|
||||
lastHeardStations.clear();
|
||||
for (int j = 0; j < lastHeardStation_temp.size(); j++) {
|
||||
lastHeardStations.push_back(lastHeardStation_temp[j]);
|
||||
}
|
||||
lastHeardStation_temp.clear();
|
||||
}
|
||||
|
||||
void updateLastHeard(const String& station) {
|
||||
deleteNotHeard();
|
||||
bool stationHeard = false;
|
||||
for (int i = 0; i < lastHeardStations.size(); i++) {
|
||||
uint32_t currentTime = millis();
|
||||
for (size_t i = 0; i < lastHeardStations.size(); i++) {
|
||||
if (lastHeardStations[i].station == station) {
|
||||
lastHeardStations[i].lastHeardTime = millis();
|
||||
stationHeard = true;
|
||||
break;
|
||||
lastHeardStations[i].lastHeardTime = currentTime;
|
||||
Utils::showActiveStations();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!stationHeard) lastHeardStations.emplace_back(LastHeardStation{millis(), station});
|
||||
lastHeardStations.emplace_back(LastHeardStation{currentTime, station});
|
||||
Utils::showActiveStations();
|
||||
}
|
||||
|
||||
bool wasHeard(const String& station) {
|
||||
deleteNotHeard();
|
||||
for (int i = 0; i < lastHeardStations.size(); i++) {
|
||||
for (size_t i = 0; i < lastHeardStations.size(); i++) {
|
||||
if (lastHeardStations[i].station == station) {
|
||||
Utils::println(" ---> Listened Station");
|
||||
return true;
|
||||
@@ -166,18 +160,33 @@ namespace STATION_Utils {
|
||||
return false;
|
||||
}
|
||||
|
||||
void clean25SegBuffer() {
|
||||
if (!packet25SegBuffer.empty() && (millis() - packet25SegBuffer[0].receivedTime) > 25 * 1000) packet25SegBuffer.erase(packet25SegBuffer.begin());
|
||||
}
|
||||
|
||||
bool check25SegBuffer(const String& station, const String& textMessage) {
|
||||
if (!packet25SegBuffer.empty()) {
|
||||
for (int i = 0; i < packet25SegBuffer.size(); i++) {
|
||||
if (packet25SegBuffer[i].station == station && packet25SegBuffer[i].payload == textMessage) return false;
|
||||
void clean25SegHashBuffer() {
|
||||
uint32_t currentTime = millis();
|
||||
for (int i = packet25SegBuffer.size() - 1; i >= 0; i--) {
|
||||
if ((currentTime - packet25SegBuffer[i].receivedTime) > 25 * 1000) {
|
||||
packet25SegBuffer.erase(packet25SegBuffer.begin() + i);
|
||||
}
|
||||
}
|
||||
packet25SegBuffer.emplace_back(Packet25SegBuffer{millis(), station, textMessage});
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t makeHash(const String& station, const String& payload) { // DJB2 Hash
|
||||
uint32_t h = 5381;
|
||||
for (size_t i = 0; i < station.length(); i++)
|
||||
h = ((h << 5) + h) + station[i];
|
||||
for (size_t i = 0; i < payload.length(); i++)
|
||||
h = ((h << 5) + h) + payload[i];
|
||||
return h;
|
||||
}
|
||||
|
||||
bool isIn25SegHashBuffer(const String& station, const String& textMessage) {
|
||||
clean25SegHashBuffer();
|
||||
uint32_t newHash = makeHash(station, textMessage);
|
||||
uint32_t currentTime = millis();
|
||||
for (int i = 0; i < packet25SegBuffer.size(); i++) {
|
||||
if (packet25SegBuffer[i].hash == newHash) return true;
|
||||
}
|
||||
packet25SegBuffer.push_back({currentTime, newHash});
|
||||
return false;
|
||||
}
|
||||
|
||||
void processOutputPacketBufferUltraEcoMode() {
|
||||
@@ -201,15 +210,11 @@ namespace STATION_Utils {
|
||||
}
|
||||
|
||||
void processOutputPacketBuffer() {
|
||||
int timeToWait = 3 * 1000; // 3 segs between packet Tx and also Rx ???
|
||||
uint32_t lastRx = millis() - lastRxTime;
|
||||
uint32_t lastTx = millis() - lastTxTime;
|
||||
if (outputPacketBuffer.size() > 0 && lastTx > timeToWait && lastRx > timeToWait) {
|
||||
if (outputPacketBuffer.size() > 0) {
|
||||
if (outputPacketBuffer[0].isBeacon) packetIsBeacon = true;
|
||||
LoRa_Utils::sendNewPacket(outputPacketBuffer[0].packet);
|
||||
if (outputPacketBuffer[0].isBeacon) packetIsBeacon = false;
|
||||
outputPacketBuffer.erase(outputPacketBuffer.begin());
|
||||
lastTxTime = millis();
|
||||
}
|
||||
if (shouldSleepLowVoltage) {
|
||||
while (outputPacketBuffer.size() > 0) {
|
||||
@@ -229,11 +234,7 @@ namespace STATION_Utils {
|
||||
}
|
||||
|
||||
void addToOutputPacketBuffer(const String& packet, bool flag) {
|
||||
OutputPacketBuffer entry;
|
||||
entry.packet = packet;
|
||||
entry.isBeacon = flag;
|
||||
|
||||
outputPacketBuffer.push_back(entry);
|
||||
outputPacketBuffer.emplace_back(OutputPacketBuffer{packet, flag});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
|
||||
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
|
||||
*
|
||||
* This file is part of LoRa APRS iGate.
|
||||
*
|
||||
@@ -17,13 +17,14 @@
|
||||
*/
|
||||
|
||||
#include <WiFiUdp.h>
|
||||
#include <WiFi.h>
|
||||
#include "configuration.h"
|
||||
#include "network_manager.h"
|
||||
#include "syslog_utils.h"
|
||||
#include "gps_utils.h"
|
||||
|
||||
|
||||
extern Configuration Config;
|
||||
extern NetworkManager *networkManager;
|
||||
extern String versionDate;
|
||||
extern String versionNumber;
|
||||
|
||||
@@ -33,7 +34,7 @@ WiFiUDP udpClient;
|
||||
namespace SYSLOG_Utils {
|
||||
|
||||
void log(const uint8_t type, const String& packet, const int rssi, const float snr, const int freqError) {
|
||||
if (Config.syslog.active && WiFi.status() == WL_CONNECTED) {
|
||||
if (Config.syslog.active && networkManager->isConnected()) {
|
||||
String syslogPacket = "<165>1 - ";
|
||||
syslogPacket.concat(Config.callsign);
|
||||
syslogPacket.concat(" CA2RXU_LoRa_iGate_");
|
||||
@@ -78,11 +79,14 @@ 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));
|
||||
syslogPacket.concat(" / ");
|
||||
syslogPacket.concat(GPS_Utils::getDistanceAndCommentFromMicE(packet));
|
||||
} else if (nextChar == ';') {
|
||||
syslogPacket.concat("OBJECT / ");
|
||||
syslogPacket.concat(sender);
|
||||
@@ -140,7 +144,7 @@ namespace SYSLOG_Utils {
|
||||
}
|
||||
|
||||
void setup() {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
if (networkManager->isConnected()) {
|
||||
udpClient.begin(0);
|
||||
if (Config.syslog.active) Serial.println("init : Syslog Server ... done! (at " + Config.syslog.server + ")");
|
||||
}
|
||||
|
||||
+19
-11
@@ -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/>.
|
||||
*/
|
||||
@@ -29,10 +29,11 @@
|
||||
#include "display.h"
|
||||
|
||||
|
||||
extern Configuration Config;
|
||||
extern bool sendStartTelemetry;
|
||||
extern Configuration Config;
|
||||
|
||||
int telemetryCounter = random(1,999);
|
||||
int telemetryCounter = random(1,999);
|
||||
uint32_t telemetryEUPTime = 0;
|
||||
bool sendEUP = false; // Equations Units Parameters
|
||||
|
||||
|
||||
namespace TELEMETRY_Utils {
|
||||
@@ -89,7 +90,7 @@ namespace TELEMETRY_Utils {
|
||||
sendBaseTelemetryPacket("EQNS.", getEquationCoefficients());
|
||||
sendBaseTelemetryPacket("UNIT.", getUnitLabels());
|
||||
sendBaseTelemetryPacket("PARM.", getParameterNames());
|
||||
sendStartTelemetry = false;
|
||||
sendEUP = false;
|
||||
}
|
||||
|
||||
String generateEncodedTelemetryBytes(float value, bool counterBytes, byte telemetryType) {
|
||||
@@ -106,7 +107,7 @@ namespace TELEMETRY_Utils {
|
||||
case 3: tempValue = (value * 8); break; // Pressure
|
||||
default: tempValue = value; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int firstByte = tempValue / 91;
|
||||
tempValue -= firstByte * 91;
|
||||
@@ -127,4 +128,11 @@ namespace TELEMETRY_Utils {
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
void checkEUPInterval() {
|
||||
if (telemetryEUPTime == 0 || millis() - telemetryEUPTime > 24UL * 60UL * 60UL * 1000UL) {
|
||||
sendEUP = true;
|
||||
telemetryEUPTime = millis();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+58
-24
@@ -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/>.
|
||||
*/
|
||||
@@ -45,7 +45,7 @@ String inputSerialBuffer = "";
|
||||
|
||||
|
||||
namespace TNC_Utils {
|
||||
|
||||
|
||||
void setup() {
|
||||
if (Config.tnc.enableServer && Config.digi.ecoMode == 0) {
|
||||
tncServer.stop();
|
||||
@@ -78,7 +78,23 @@ namespace TNC_Utils {
|
||||
}
|
||||
}
|
||||
|
||||
void handleInputData(char character, int bufferIndex) {
|
||||
void processInputFrame(const String& frame, int bufferIndex, const String& protocolLabel) {
|
||||
if (bufferIndex != -1) {
|
||||
Utils::print("<--- Got from TNC (" + protocolLabel + ") : ");
|
||||
Utils::println(frame);
|
||||
}
|
||||
|
||||
String sender = frame.substring(0,frame.indexOf(">"));
|
||||
|
||||
if (Config.tnc.acceptOwn || sender != Config.callsign) {
|
||||
if (Config.loramodule.txActive) STATION_Utils::addToOutputPacketBuffer(frame);
|
||||
if (Config.tnc.aprsBridgeActive && Config.aprs_is.active && passcodeValid && aprsIsClient.connected()) APRS_IS_Utils::upload(frame);
|
||||
} else {
|
||||
Utils::println("Ignored own frame from " + protocolLabel);
|
||||
}
|
||||
}
|
||||
|
||||
void handleInputDataKISS(char character, int bufferIndex) {
|
||||
String* data = (bufferIndex == -1) ? &inputSerialBuffer : &inputServerBuffer[bufferIndex];
|
||||
if (data->length() == 0 && character != (char)FEND) return;
|
||||
|
||||
@@ -89,19 +105,7 @@ namespace TNC_Utils {
|
||||
const String& frame = decodeKISS(*data, isDataFrame);
|
||||
|
||||
if (isDataFrame) {
|
||||
if (bufferIndex != -1) {
|
||||
Utils::print("<--- Got from TNC : ");
|
||||
Utils::println(frame);
|
||||
}
|
||||
|
||||
String sender = frame.substring(0,frame.indexOf(">"));
|
||||
|
||||
if (Config.tnc.acceptOwn || sender != Config.callsign) {
|
||||
if (Config.loramodule.txActive) STATION_Utils::addToOutputPacketBuffer(frame);
|
||||
if (Config.tnc.aprsBridgeActive && Config.aprs_is.active && passcodeValid && aprsIsClient.connected()) APRS_IS_Utils::upload(frame);
|
||||
} else {
|
||||
Utils::println("Ignored own frame from KISS");
|
||||
}
|
||||
processInputFrame(frame, bufferIndex, "KISS");
|
||||
}
|
||||
data->clear();
|
||||
}
|
||||
@@ -111,6 +115,36 @@ namespace TNC_Utils {
|
||||
}
|
||||
}
|
||||
|
||||
void handleInputDataTNC2(char character, int bufferIndex) {
|
||||
String* data = (bufferIndex == -1) ? &inputSerialBuffer : &inputServerBuffer[bufferIndex];
|
||||
|
||||
if (character == '\r') return;
|
||||
|
||||
if (character == '\n') {
|
||||
String frame = *data;
|
||||
frame.trim();
|
||||
data->clear();
|
||||
|
||||
if (frame.length() > 0 && frame.indexOf(':') != -1 && frame.indexOf('>') != -1) {
|
||||
processInputFrame(frame, bufferIndex, "TNC2");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
data->concat(character);
|
||||
if (data->length() > 255) {
|
||||
data->clear();
|
||||
}
|
||||
}
|
||||
|
||||
void handleInputData(char character, int bufferIndex) {
|
||||
if (Config.tnc.kissProtocol) {
|
||||
handleInputDataKISS(character, bufferIndex);
|
||||
} else {
|
||||
handleInputDataTNC2(character, bufferIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void readFromClients() {
|
||||
for (int i = 0; i < MAX_CLIENTS; i++) {
|
||||
auto client = clients[i];
|
||||
@@ -138,13 +172,13 @@ namespace TNC_Utils {
|
||||
void sendToClients(const String& packet, bool stripBytes) {
|
||||
String cleanPacket = stripBytes ? packet.substring(3): packet;
|
||||
|
||||
const String kissEncoded = encodeKISS(cleanPacket);
|
||||
const String encoded = Config.tnc.kissProtocol ? encodeKISS(cleanPacket) : (cleanPacket + "\r\n");
|
||||
|
||||
for (int i = 0; i < MAX_CLIENTS; i++) {
|
||||
auto client = clients[i];
|
||||
if (client != nullptr) {
|
||||
if (client->connected()) {
|
||||
client->print(kissEncoded);
|
||||
client->print(encoded);
|
||||
client->flush();
|
||||
} else {
|
||||
delete client;
|
||||
@@ -158,7 +192,7 @@ namespace TNC_Utils {
|
||||
|
||||
void sendToSerial(const String& packet, bool stripBytes) {
|
||||
String cleanPacket = stripBytes ? packet.substring(3): packet;
|
||||
Serial.print(encodeKISS(cleanPacket));
|
||||
Serial.print(Config.tnc.kissProtocol ? encodeKISS(cleanPacket) : (cleanPacket + "\r\n"));
|
||||
Serial.flush();
|
||||
}
|
||||
|
||||
|
||||
+84
-61
@@ -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,9 +18,9 @@
|
||||
|
||||
#include <APRSPacketLib.h>
|
||||
#include <TinyGPS++.h>
|
||||
#include <WiFi.h>
|
||||
#include "telemetry_utils.h"
|
||||
#include "configuration.h"
|
||||
#include "network_manager.h"
|
||||
#include "station_utils.h"
|
||||
#include "battery_utils.h"
|
||||
#include "aprs_is_utils.h"
|
||||
@@ -37,6 +37,7 @@
|
||||
#define DAY_MS (24UL * 60UL * 60UL * 1000UL)
|
||||
|
||||
extern Configuration Config;
|
||||
extern NetworkManager *networkManager;
|
||||
extern TinyGPSPlus gps;
|
||||
extern String versionDate;
|
||||
extern String firstLine;
|
||||
@@ -52,17 +53,16 @@ extern int rssi;
|
||||
extern float snr;
|
||||
extern int freqError;
|
||||
extern String distance;
|
||||
extern bool WiFiConnected;
|
||||
extern int wxModuleType;
|
||||
extern bool backupDigiMode;
|
||||
extern bool shouldSleepLowVoltage;
|
||||
extern bool transmitFlag;
|
||||
extern bool passcodeValid;
|
||||
extern bool sendEUP; // Equations Units Parameters
|
||||
|
||||
extern std::vector<LastHeardStation> lastHeardStations;
|
||||
|
||||
bool statusAfterBoot = true;
|
||||
bool sendStartTelemetry = true;
|
||||
bool statusUpdate = true;
|
||||
bool beaconUpdate = false;
|
||||
uint32_t lastBeaconTx = 0;
|
||||
uint32_t lastScreenOn = millis();
|
||||
@@ -75,37 +75,43 @@ String secondaryBeaconPacket;
|
||||
namespace Utils {
|
||||
|
||||
void processStatus() {
|
||||
String status = APRSPacketLib::generateBasePacket(Config.callsign, "APLRG1", Config.beacon.path);
|
||||
bool sendOverAPRSIS = Config.beacon.sendViaAPRSIS && Config.aprs_is.active && networkManager->isConnected();
|
||||
bool sendOverRF = !Config.beacon.sendViaAPRSIS && Config.beacon.sendViaRF;
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED && Config.aprs_is.active && Config.beacon.sendViaAPRSIS) {
|
||||
delay(1000);
|
||||
status.concat(",qAC:>");
|
||||
status.concat(Config.beacon.statusPacket);
|
||||
APRS_IS_Utils::upload(status);
|
||||
SYSLOG_Utils::log(2, status, 0, 0.0, 0); // APRSIS TX
|
||||
if (!sendOverAPRSIS && !sendOverRF) {
|
||||
statusUpdate = false;
|
||||
return;
|
||||
}
|
||||
if (statusAfterBoot && !Config.beacon.sendViaAPRSIS && Config.beacon.sendViaRF) {
|
||||
status.concat(":>");
|
||||
status.concat(Config.beacon.statusPacket);
|
||||
STATION_Utils::addToOutputPacketBuffer(status, true); // treated also as beacon on Tx Freq
|
||||
|
||||
String statusPacket = APRSPacketLib::generateBasePacket(Config.callsign, "APLRG1", Config.beacon.path);
|
||||
statusPacket += sendOverAPRSIS ? ",qAC:>" : ":>";
|
||||
statusPacket += Config.beacon.statusPacket;
|
||||
|
||||
if (sendOverAPRSIS) {
|
||||
APRS_IS_Utils::upload(statusPacket);
|
||||
SYSLOG_Utils::log(2, statusPacket, 0, 0.0, 0); // APRSIS TX
|
||||
} else {
|
||||
STATION_Utils::addToOutputPacketBuffer(statusPacket, true); // treated also as beacon on Tx Freq
|
||||
}
|
||||
statusAfterBoot = false;
|
||||
statusUpdate = false;
|
||||
lastStatusTx = millis();
|
||||
}
|
||||
|
||||
void checkStatusInterval() {
|
||||
if (lastStatusTx == 0 || millis() - lastStatusTx > DAY_MS) statusAfterBoot = true;
|
||||
if (lastStatusTx == 0 || millis() - lastStatusTx > DAY_MS) statusUpdate = true;
|
||||
}
|
||||
|
||||
String getLocalIP() {
|
||||
if (Config.digi.ecoMode == 1 || Config.digi.ecoMode == 2) {
|
||||
return "** WiFi AP Killed **";
|
||||
} else if (!WiFiConnected) {
|
||||
return "IP : 192.168.4.1";
|
||||
} else if (networkManager->isEthernetConnected()) {
|
||||
return "LAN: " + networkManager->getEthernetIP().toString();
|
||||
} else if (!networkManager->isWiFiConnected() && networkManager->isWifiAPActive()) {
|
||||
return "IP : " + networkManager->getWiFiAPIP().toString();
|
||||
} else if (backupDigiMode) {
|
||||
return "- BACKUP DIGI MODE -";
|
||||
} else {
|
||||
return "IP : " + String(WiFi.localIP()[0]) + "." + String(WiFi.localIP()[1]) + "." + String(WiFi.localIP()[2]) + "." + String(WiFi.localIP()[3]);
|
||||
return "IP : " + networkManager->getWiFiIP().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,17 +153,25 @@ namespace Utils {
|
||||
beaconUpdate = true;
|
||||
}
|
||||
|
||||
bool configLocationIsValid = !(Config.beacon.latitude == 0.0 && Config.beacon.longitude == 0.0);
|
||||
#ifdef HAS_GPS
|
||||
if (Config.beacon.gpsActive && gps.location.lat() == 0.0 && gps.location.lng() == 0.0 && Config.beacon.latitude == 0.0 && Config.beacon.longitude == 0.0) {
|
||||
GPS_Utils::getData();
|
||||
beaconUpdate = false;
|
||||
if (Config.beacon.gpsActive) { // GPS activated
|
||||
if (!gps.location.isValid()) {
|
||||
GPS_Utils::getData(); // refresh GPS
|
||||
beaconUpdate = false;
|
||||
}
|
||||
} else { // GPS not active: use saved data in Config
|
||||
if (!configLocationIsValid) beaconUpdate = false;
|
||||
}
|
||||
#else // No GPS available: use saved data in Config
|
||||
if (!configLocationIsValid) beaconUpdate = false;
|
||||
#endif
|
||||
|
||||
if (beaconUpdate) {
|
||||
if (!Config.display.alwaysOn && Config.display.timeout != 0) displayToggle(true);
|
||||
|
||||
if (sendStartTelemetry &&
|
||||
TELEMETRY_Utils::checkEUPInterval();
|
||||
if (sendEUP &&
|
||||
Config.battery.sendVoltageAsTelemetry &&
|
||||
!Config.wxsensor.active &&
|
||||
(Config.battery.sendInternalVoltage || Config.battery.sendExternalVoltage) &&
|
||||
@@ -290,7 +304,7 @@ namespace Utils {
|
||||
}
|
||||
|
||||
checkStatusInterval();
|
||||
if (statusAfterBoot && Config.beacon.statusActive && !Config.beacon.statusPacket.isEmpty()) processStatus();
|
||||
if (statusUpdate && Config.beacon.statusActive && !Config.beacon.statusPacket.isEmpty()) processStatus();
|
||||
}
|
||||
|
||||
void checkDisplayInterval() {
|
||||
@@ -403,9 +417,9 @@ namespace Utils {
|
||||
if (mode == 1) { // low voltage detected after a while
|
||||
displayToggle(false);
|
||||
}
|
||||
#ifdef VEXT_CTRL
|
||||
#ifdef VEXT_CTRL_PIN
|
||||
#ifndef HELTEC_WSL_V3
|
||||
digitalWrite(VEXT_CTRL, LOW);
|
||||
digitalWrite(VEXT_CTRL_PIN, LOW);
|
||||
#endif
|
||||
#endif
|
||||
LoRa_Utils::sleepRadio();
|
||||
@@ -417,44 +431,53 @@ namespace Utils {
|
||||
|
||||
bool callsignIsValid(const String& callsign) {
|
||||
if (callsign == "WLNK-1") return true;
|
||||
int totalCallsignLength = callsign.length();
|
||||
if (totalCallsignLength < 4) return false;
|
||||
|
||||
String cleanCallsign;
|
||||
int hypenCallsignIndex = callsign.indexOf("-");
|
||||
if (hypenCallsignIndex > 0) { // SSID Validation
|
||||
cleanCallsign = callsign.substring(0, hypenCallsignIndex);
|
||||
String ssid = callsign.substring(hypenCallsignIndex + 1);
|
||||
if (ssid.indexOf("-") != -1 || ssid.length() > 2) return false;
|
||||
if (ssid.length() == 2 && ssid[0] == '0') return false;
|
||||
for (int i = 0; i < ssid.length(); i++) {
|
||||
if (!isAlphaNumeric(ssid[i])) return false;
|
||||
int hyphenIndex = callsign.indexOf("-");
|
||||
int baseCallsignLength = (hyphenIndex > 0) ? hyphenIndex : totalCallsignLength;
|
||||
|
||||
if (hyphenIndex > 0) { // SSID Validation
|
||||
if (hyphenIndex < 4) return false; // base Callsign must have at least 4 characters
|
||||
int ssidStart = hyphenIndex + 1;
|
||||
int ssidLength = totalCallsignLength - ssidStart;
|
||||
if (ssidLength == 0 || ssidLength > 2) return false;
|
||||
if (callsign.indexOf('-', ssidStart) != -1) return false; // avoid another "-" in ssid
|
||||
if (ssidLength == 2 && callsign[ssidStart] == '0') return false; // ssid can't start with "0"
|
||||
for (int i = ssidStart; i < totalCallsignLength; i++) {
|
||||
if (!isDigit(callsign[i])) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (baseCallsignLength < 4 || baseCallsignLength > 6) return false;
|
||||
|
||||
bool padded = false; // Callsigns with 4 characters like A0AA are padded into 5 characters for further analisis : A0AA --> _A0AA
|
||||
if (baseCallsignLength == 4 && isAlpha(callsign[0]) && isDigit(callsign[1]) && isAlpha(callsign[2]) && isAlpha(callsign[3])) padded = true;
|
||||
char c0, c1, c2, c3;
|
||||
if (padded) {
|
||||
c0 = ' ';
|
||||
c1 = callsign[0];
|
||||
c2 = callsign[1];
|
||||
c3 = callsign[2];
|
||||
} else {
|
||||
cleanCallsign = callsign;
|
||||
c0 = callsign[0];
|
||||
c1 = callsign[1];
|
||||
c2 = callsign[2];
|
||||
c3 = callsign[3];
|
||||
}
|
||||
if (!isDigit(c2) || !isAlpha(c3)) { // __0A__ must be validated
|
||||
if (c0 != 'R' && !isDigit(c1) && !isAlpha(c2)) return false; // to accepto R0A___
|
||||
}
|
||||
|
||||
if (cleanCallsign.length() < 4 || cleanCallsign.length() > 6) return false;
|
||||
bool isValid =
|
||||
((isAlphaNumeric(c0) || c0 == ' ') && isAlpha(c1)) || // AA0A (+A+A) + _A0AA (+A) + 0A0A (+A+A)
|
||||
(isAlpha(c0) && isDigit(c1)) || // A00A (+A+A)
|
||||
(c0 == 'R' && baseCallsignLength == 6 && isDigit(c1) && isAlpha(c2) && isAlpha(c3) && isAlpha(callsign[4])); // R0AA (+A+A)
|
||||
if (!isValid) return false; // also 00__ avoided
|
||||
|
||||
if (cleanCallsign.length() < 6 && isAlpha(cleanCallsign[0]) && isDigit(cleanCallsign[1]) && isAlpha(cleanCallsign[2]) && isAlpha(cleanCallsign[3]) ) {
|
||||
cleanCallsign = " " + cleanCallsign; // A0AA --> _A0AA
|
||||
}
|
||||
|
||||
if (!isDigit(cleanCallsign[2]) || !isAlpha(cleanCallsign[3])) { // __0A__ must be validated
|
||||
if (cleanCallsign[0] != 'R' && !isDigit(cleanCallsign[1]) && !isAlpha(cleanCallsign[2])) return false; // to accepto R0A___
|
||||
}
|
||||
|
||||
bool isValid = false;
|
||||
if ((isAlphaNumeric(cleanCallsign[0]) || cleanCallsign[0] == ' ') && isAlpha(cleanCallsign[1])) {
|
||||
isValid = true; // AA0A (+A+A) + _A0AA (+A) + 0A0A (+A+A)
|
||||
} else if (isAlpha(cleanCallsign[0]) && isDigit(cleanCallsign[1])) {
|
||||
isValid = true; // A00A (+A+A)
|
||||
} else if (cleanCallsign[0] == 'R' && cleanCallsign.length() == 6 && isDigit(cleanCallsign[1]) && isAlpha(cleanCallsign[2]) && isAlpha(cleanCallsign[3]) && isAlpha(cleanCallsign[4])) {
|
||||
isValid = true; // R0AA (+A+A)
|
||||
}
|
||||
if (!isValid) return false; // also 00__ avoided
|
||||
|
||||
if (cleanCallsign.length() > 4) { // to validate ____AA
|
||||
for (int i = 5; i <= cleanCallsign.length(); i++) {
|
||||
if (!isAlpha(cleanCallsign[i - 1])) return false;
|
||||
if (baseCallsignLength > 4 ) { // to validate ____AA
|
||||
for (int i = 4; i < baseCallsignLength; i++) {
|
||||
if (!isAlpha(callsign[i])) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -467,4 +490,4 @@ namespace Utils {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+95
-11
@@ -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/>.
|
||||
*/
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "configuration.h"
|
||||
#include "ota_utils.h"
|
||||
#include "web_utils.h"
|
||||
#include "map_utils.h"
|
||||
#include "display.h"
|
||||
#include "utils.h"
|
||||
|
||||
@@ -48,11 +49,31 @@ 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 {
|
||||
|
||||
@@ -83,12 +104,48 @@ 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()))
|
||||
return request->requestAuthentication();
|
||||
|
||||
File file = SPIFFS.open("/igate_conf.json");
|
||||
|
||||
|
||||
String fileContent;
|
||||
while(file.available()){
|
||||
fileContent += String((char)file.read());
|
||||
@@ -98,7 +155,7 @@ namespace WEB_Utils {
|
||||
}
|
||||
|
||||
void handleReceivedPackets(AsyncWebServerRequest *request) {
|
||||
StaticJsonDocument<1536> data;
|
||||
JsonDocument data;
|
||||
|
||||
for (int i = 0; i < receivedPackets.size(); i++) {
|
||||
data[i]["rxTime"] = receivedPackets[i].rxTime;
|
||||
@@ -114,6 +171,10 @@ namespace WEB_Utils {
|
||||
request->send(200, "application/json", buffer);
|
||||
}
|
||||
|
||||
void handleStations(AsyncWebServerRequest *request) {
|
||||
request->send(200, "application/json", MAP_Utils::getStationsJson());
|
||||
}
|
||||
|
||||
void handleWriteConfiguration(AsyncWebServerRequest *request) {
|
||||
Serial.println("Got new Configuration Data from www");
|
||||
|
||||
@@ -161,7 +222,7 @@ namespace WEB_Utils {
|
||||
|
||||
Config.callsign = getParamStringSafe("callsign", Config.callsign);
|
||||
Config.tacticalCallsign = getParamStringSafe("tacticalCallsign", Config.tacticalCallsign);
|
||||
|
||||
Config.wifiAutoAP.enabled = request->hasParam("wifi.autoAP.enabled", true);
|
||||
Config.wifiAutoAP.password = getParamStringSafe("wifi.autoAP.password", Config.wifiAutoAP.password);
|
||||
Config.wifiAutoAP.timeout = getParamIntSafe("wifi.autoAP.timeout", Config.wifiAutoAP.timeout);
|
||||
|
||||
@@ -213,6 +274,8 @@ namespace WEB_Utils {
|
||||
Config.loramodule.txCodingRate4 = getParamIntSafe("lora.txCodingRate4", Config.loramodule.txCodingRate4);
|
||||
Config.loramodule.txSignalBandwidth = getParamIntSafe("lora.txSignalBandwidth", Config.loramodule.txSignalBandwidth);
|
||||
Config.loramodule.power = getParamIntSafe("lora.power", Config.loramodule.power);
|
||||
if (Config.loramodule.txActive)
|
||||
Config.loramodule.cadActive = request->hasParam("lora.cadActive", true);
|
||||
|
||||
Config.display.alwaysOn = request->hasParam("display.alwaysOn", true);
|
||||
if (!Config.display.alwaysOn) {
|
||||
@@ -259,6 +322,7 @@ namespace WEB_Utils {
|
||||
Config.tnc.enableSerial = request->hasParam("tnc.enableSerial", true);
|
||||
Config.tnc.acceptOwn = request->hasParam("tnc.acceptOwn", true);
|
||||
Config.tnc.aprsBridgeActive = request->hasParam("tnc.aprsBridgeActive", true);
|
||||
Config.tnc.kissProtocol = getParamStringSafe("tnc.kissProtocol", Config.tnc.kissProtocol ? "KISS" : "TNC2") != "TNC2";
|
||||
|
||||
Config.mqtt.active = request->hasParam("mqtt.active", true);
|
||||
if (Config.mqtt.active) {
|
||||
@@ -299,7 +363,7 @@ namespace WEB_Utils {
|
||||
AsyncWebServerResponse *response = request->beginResponse(302, "text/html", "");
|
||||
response->addHeader("Location", "/?success=1");
|
||||
request->send(response);
|
||||
|
||||
|
||||
displayToggle(false);
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
@@ -309,7 +373,7 @@ namespace WEB_Utils {
|
||||
errorPage += "<h1>Configuration Error:</h1>";
|
||||
errorPage += "<p>Couldn't save new configuration. Please try again.</p>";
|
||||
errorPage += "<a href='/'>Back</a></body></html>";
|
||||
|
||||
|
||||
AsyncWebServerResponse *response = request->beginResponse(500, "text/html", errorPage);
|
||||
request->send(response);
|
||||
}
|
||||
@@ -356,11 +420,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("/stations.json", HTTP_GET, handleStations);
|
||||
server.on("/configuration.json", HTTP_GET, handleReadConfiguration);
|
||||
server.on("/configuration.json", HTTP_POST, handleWriteConfiguration);
|
||||
server.on("/action", HTTP_POST, handleAction);
|
||||
@@ -368,7 +447,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
|
||||
|
||||
|
||||
+49
-100
@@ -1,23 +1,24 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
#include <WiFi.h>
|
||||
#include "configuration.h"
|
||||
#include "network_manager.h"
|
||||
#include "board_pinout.h"
|
||||
#include "wifi_utils.h"
|
||||
#include "display.h"
|
||||
@@ -25,16 +26,11 @@
|
||||
|
||||
|
||||
extern Configuration Config;
|
||||
extern NetworkManager *networkManager;
|
||||
|
||||
extern uint8_t myWiFiAPIndex;
|
||||
extern int myWiFiAPSize;
|
||||
extern WiFi_AP *currentWiFi;
|
||||
extern bool backupDigiMode;
|
||||
extern uint32_t lastServerCheck;
|
||||
|
||||
bool WiFiConnected = false;
|
||||
uint32_t WiFiAutoAPTime = millis();
|
||||
bool WiFiAutoAPStarted = false;
|
||||
uint8_t wifiCounter = 0;
|
||||
uint32_t lastBackupDigiTime = millis();
|
||||
uint32_t lastWiFiCheck = 0;
|
||||
@@ -44,32 +40,35 @@ namespace WIFI_Utils {
|
||||
|
||||
void checkWiFi() {
|
||||
if (Config.digi.ecoMode != 0) return;
|
||||
|
||||
if (!networkManager->hasWiFiNetworks()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t currentTime = millis();
|
||||
|
||||
if (backupDigiMode) {
|
||||
if (WiFi.status() != WL_CONNECTED && ((currentTime - lastBackupDigiTime) >= 15 * 60 * 1000)) {
|
||||
if (!networkManager->isWiFiConnected() && ((currentTime - lastBackupDigiTime) >= 15 * 60 * 1000)) {
|
||||
Serial.println("*** Stopping BackUp Digi Mode ***");
|
||||
backupDigiMode = false;
|
||||
wifiCounter = 0;
|
||||
} else if (WiFi.status() == WL_CONNECTED) {
|
||||
} else if (networkManager->isWiFiConnected()) {
|
||||
Serial.println("*** WiFi Reconnect Success (Stopping Backup Digi Mode) ***");
|
||||
backupDigiMode = false;
|
||||
wifiCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!backupDigiMode && ((currentTime - lastWiFiCheck) >= 30 * 1000) && !WiFiAutoAPStarted) {
|
||||
if (!backupDigiMode && ((currentTime - lastWiFiCheck) >= 30 * 1000) && !networkManager->isWifiAPActive()) {
|
||||
lastWiFiCheck = currentTime;
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
if (networkManager->isWiFiConnected()) {
|
||||
if (Config.digi.backupDigiMode && (currentTime - lastServerCheck > 30 * 1000)) {
|
||||
Serial.println("*** Server Connection LOST → Backup Digi Mode ***");
|
||||
backupDigiMode = true;
|
||||
WiFi.disconnect();
|
||||
lastBackupDigiTime = currentTime;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Serial.println("Reconnecting to WiFi...");
|
||||
WiFi.disconnect();
|
||||
WIFI_Utils::startWiFi();
|
||||
|
||||
if (Config.digi.backupDigiMode) wifiCounter++;
|
||||
@@ -83,97 +82,47 @@ namespace WIFI_Utils {
|
||||
}
|
||||
|
||||
void startAutoAP() {
|
||||
WiFi.mode(WIFI_MODE_NULL);
|
||||
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(Config.callsign + "-AP", Config.wifiAutoAP.password);
|
||||
|
||||
WiFiAutoAPTime = millis();
|
||||
WiFiAutoAPStarted = true;
|
||||
displayShow("", " Starting Auto AP", " Please connect to it " , " loading ...", 1000);
|
||||
networkManager->setupAP(Config.callsign + "-AP", Config.wifiAutoAP.password);
|
||||
}
|
||||
|
||||
void startWiFi() {
|
||||
bool startAP = false;
|
||||
if (currentWiFi->ssid == "") {
|
||||
startAP = true;
|
||||
} else {
|
||||
uint8_t wifiCounter = 0;
|
||||
String hostName = "iGATE-" + Config.callsign;
|
||||
WiFi.setHostname(hostName.c_str());
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
delay(500);
|
||||
unsigned long start = millis();
|
||||
displayShow("", "Connecting to WiFi:", "", currentWiFi->ssid + " ...", 0);
|
||||
Serial.print("\nConnecting to WiFi '"); Serial.print(currentWiFi->ssid); Serial.print("' ");
|
||||
WiFi.begin(currentWiFi->ssid.c_str(), currentWiFi->password.c_str());
|
||||
while (WiFi.status() != WL_CONNECTED && wifiCounter < myWiFiAPSize) {
|
||||
delay(500);
|
||||
#ifdef INTERNAL_LED_PIN
|
||||
digitalWrite(INTERNAL_LED_PIN, HIGH);
|
||||
#endif
|
||||
Serial.print('.');
|
||||
delay(500);
|
||||
#ifdef INTERNAL_LED_PIN
|
||||
digitalWrite(INTERNAL_LED_PIN, LOW);
|
||||
#endif
|
||||
if ((millis() - start) > 10000){
|
||||
delay(1000);
|
||||
if (myWiFiAPIndex >= (myWiFiAPSize - 1)) {
|
||||
myWiFiAPIndex = 0;
|
||||
wifiCounter++;
|
||||
} else {
|
||||
myWiFiAPIndex++;
|
||||
}
|
||||
wifiCounter++;
|
||||
currentWiFi = &Config.wifiAPs[myWiFiAPIndex];
|
||||
start = millis();
|
||||
Serial.print("\nConnecting to WiFi '"); Serial.print(currentWiFi->ssid); Serial.println("' ...");
|
||||
displayShow("", "Connecting to WiFi:", "", currentWiFi->ssid + " ...", 0);
|
||||
WiFi.disconnect();
|
||||
WiFi.begin(currentWiFi->ssid.c_str(), currentWiFi->password.c_str());
|
||||
}
|
||||
}
|
||||
networkManager->clearWiFiNetworks();
|
||||
for (size_t i = 0; i < Config.wifiAPs.size(); i++) {
|
||||
const WiFi_AP& wifiAP = Config.wifiAPs[i];
|
||||
if (wifiAP.ssid.isEmpty()) continue;
|
||||
networkManager->addWiFiNetwork(wifiAP.ssid, wifiAP.password);
|
||||
}
|
||||
|
||||
if (!networkManager->hasWiFiNetworks()) {
|
||||
Serial.println("WiFi SSID not set!");
|
||||
if (Config.wifiAutoAP.enabled) {
|
||||
Serial.println("Starting AP fallback...");
|
||||
startAutoAP();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
displayShow("", "Connecting to WiFi:", "", " loading ...", 0);
|
||||
networkManager->connectWiFi();
|
||||
|
||||
#ifdef INTERNAL_LED_PIN
|
||||
digitalWrite(INTERNAL_LED_PIN, LOW);
|
||||
#endif
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.print("\nConnected as ");
|
||||
Serial.print(WiFi.localIP());
|
||||
if (networkManager->isWiFiConnected()) {
|
||||
Serial.print("[WiFi] Connected as ");
|
||||
Serial.print(networkManager->getWiFiIP());
|
||||
Serial.print(" / MAC Address: ");
|
||||
Serial.println(WiFi.macAddress());
|
||||
Serial.println(networkManager->getWiFimacAddress());
|
||||
displayShow("", " Connected!!", "" , " loading ...", 1000);
|
||||
} else {
|
||||
startAP = true;
|
||||
|
||||
Serial.println("\nNot connected to WiFi! Starting Auto AP");
|
||||
displayShow("", " WiFi Not Connected!", "" , " loading ...", 1000);
|
||||
}
|
||||
WiFiConnected = !startAP;
|
||||
if (startAP) {
|
||||
Serial.println("\nNot connected to WiFi! Starting Auto AP");
|
||||
displayShow("", " Starting Auto AP", " Please connect to it " , " loading ...", 1000);
|
||||
|
||||
startAutoAP();
|
||||
}
|
||||
}
|
||||
|
||||
void checkAutoAPTimeout() {
|
||||
if (WiFiAutoAPStarted && Config.wifiAutoAP.timeout > 0) {
|
||||
if (WiFi.softAPgetStationNum() > 0) {
|
||||
WiFiAutoAPTime = 0;
|
||||
Serial.println("[WiFi] Not connected to WiFi!");
|
||||
if (Config.wifiAutoAP.enabled) {
|
||||
Serial.println("Starting AP fallback...");
|
||||
displayShow("", " WiFi Not Connected!", "" , " loading ...", 1000);
|
||||
startAutoAP();
|
||||
} else {
|
||||
if (WiFiAutoAPTime == 0) {
|
||||
WiFiAutoAPTime = millis();
|
||||
} else if ((millis() - WiFiAutoAPTime) > Config.wifiAutoAP.timeout * 60 * 1000) {
|
||||
Serial.println("Stopping auto AP");
|
||||
|
||||
WiFiAutoAPStarted = false;
|
||||
WiFi.softAPdisconnect(true);
|
||||
|
||||
Serial.println("Auto AP stopped (timeout)");
|
||||
}
|
||||
displayShow("", " WiFi Not Connected!", "" , " loading ...", 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,4 +132,4 @@ namespace WIFI_Utils {
|
||||
btStop();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
|
||||
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
|
||||
*
|
||||
* This file is part of LoRa APRS iGate.
|
||||
*
|
||||
|
||||
+17
-12
@@ -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))
|
||||
@@ -0,0 +1,63 @@
|
||||
/* 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 BOARD_PINOUT_H_
|
||||
#define BOARD_PINOUT_H_
|
||||
|
||||
// LoRa Radio
|
||||
#define HAS_SX1268
|
||||
#define HAS_1W_LORA
|
||||
#define HAS_TCXO
|
||||
#define RADIO_SCLK_PIN 18
|
||||
#define RADIO_MISO_PIN 19
|
||||
#define RADIO_MOSI_PIN 23
|
||||
#define RADIO_CS_PIN 5
|
||||
#define RADIO_RST_PIN 27
|
||||
#define RADIO_DIO1_PIN 12
|
||||
#define RADIO_BUSY_PIN 14
|
||||
#define RADIO_RXEN 32
|
||||
#define RADIO_TXEN 25
|
||||
#define RADIO_WAKEUP_PIN RADIO_DIO1_PIN
|
||||
#define GPIO_WAKEUP_PIN GPIO_SEL_12
|
||||
|
||||
// I2C
|
||||
#define USE_WIRE_WITH_OLED_PINS
|
||||
|
||||
// Display
|
||||
#define HAS_DISPLAY
|
||||
#define HAS_SH1106
|
||||
|
||||
#undef OLED_SDA
|
||||
#undef OLED_SCL
|
||||
#undef OLED_RST
|
||||
|
||||
#define OLED_SDA 21
|
||||
#define OLED_SCL 22
|
||||
#define OLED_RST -1 // Reset pin # (or -1 if sharing Arduino reset pin)
|
||||
|
||||
// GPS
|
||||
#define HAS_GPS
|
||||
#define GPS_BAUDRATE 9600
|
||||
#define GPS_RX 17
|
||||
#define GPS_TX 16
|
||||
|
||||
// Aditional Config
|
||||
#define INTERNAL_LED_PIN 2
|
||||
#define BATTERY_PIN 35
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
[env:ESP32_9M2IBR_1W_LoRa_GPS]
|
||||
board = esp32dev
|
||||
build_flags =
|
||||
${common.build_flags}
|
||||
-D RADIOLIB_EXCLUDE_LR11X0=1
|
||||
-D RADIOLIB_EXCLUDE_SX127X=1
|
||||
-D RADIOLIB_EXCLUDE_SX128X=1
|
||||
-D ESP32_9M2IBR_1W_LoRa_GPS
|
||||
lib_deps =
|
||||
${common.lib_deps}
|
||||
${common.display_libs}
|
||||
adafruit/Adafruit SH110X @ 2.1.10
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/* 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 BOARD_PINOUT_H_
|
||||
#define BOARD_PINOUT_H_
|
||||
|
||||
// LoRa Radio
|
||||
#define HAS_SX1268
|
||||
#define HAS_1W_LORA
|
||||
#define HAS_TCXO
|
||||
#define RADIO_SCLK_PIN 18
|
||||
#define RADIO_MISO_PIN 19
|
||||
#define RADIO_MOSI_PIN 23
|
||||
#define RADIO_CS_PIN 5
|
||||
#define RADIO_RST_PIN 27
|
||||
#define RADIO_DIO1_PIN 12
|
||||
#define RADIO_BUSY_PIN 14
|
||||
#define RADIO_RXEN 32
|
||||
#define RADIO_TXEN 25
|
||||
#define RADIO_WAKEUP_PIN RADIO_DIO1_PIN
|
||||
#define GPIO_WAKEUP_PIN GPIO_SEL_12
|
||||
|
||||
// I2C
|
||||
#define USE_WIRE_WITH_OLED_PINS
|
||||
|
||||
// Display
|
||||
#define HAS_DISPLAY
|
||||
|
||||
#undef OLED_SDA
|
||||
#undef OLED_SCL
|
||||
#undef OLED_RST
|
||||
|
||||
#define OLED_SDA 21
|
||||
#define OLED_SCL 22
|
||||
#define OLED_RST -1 // Reset pin # (or -1 if sharing Arduino reset pin)
|
||||
|
||||
// Aditional Config
|
||||
#define INTERNAL_LED_PIN 2
|
||||
#define BATTERY_PIN 35
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
[env:RPC_LORA_DIGIGATE_1W]
|
||||
board = esp32dev
|
||||
build_flags =
|
||||
${common.build_flags}
|
||||
-D RADIOLIB_EXCLUDE_LR11X0=1
|
||||
-D RADIOLIB_EXCLUDE_SX127X=1
|
||||
-D RADIOLIB_EXCLUDE_SX128X=1
|
||||
-D RPC_LORA_DIGIGATE_1W
|
||||
lib_deps =
|
||||
${common.lib_deps}
|
||||
${common.display_libs}
|
||||
@@ -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.
|
||||
*
|
||||
@@ -49,7 +49,7 @@
|
||||
#define INTERNAL_LED_PIN 25
|
||||
#define BATTERY_PIN 37
|
||||
|
||||
#define ADC_CTRL 21
|
||||
#define ADC_CTRL_PIN 21
|
||||
#define ADC_CTRL_ON_STATE LOW
|
||||
|
||||
#endif
|
||||
@@ -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 126 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user