Compare commits

...
Author SHA1 Message Date
Ricardo Guzman e52d4566ba WiFi loop protection 2026-09-23 12:39:07 -03:00
Ricardo Guzman c80eb9bd84 update of names 2026-09-23 12:08:57 -03:00
Ricardo Guzman 6b187da9f4 E32 400M30S 2026-09-23 11:28:17 -03:00
Ricardo Guzman ab27c11532 updated build again 2026-09-23 11:06:47 -03:00
Ricardo Guzman 43bfff5033 sin update build 2026-09-23 11:04:06 -03:00
Ricardo Guzman 8fa10a7813 RAK3312 and 3112 added 2026-09-23 11:02:34 -03:00
Ricardo Guzman 316c746936 WebUI fixes 2026-09-23 05:12:51 -03:00
Ricardo Guzman 3618558b21 update and clean code 2026-09-22 21:17:33 -03:00
Ricardo Guzman e1eda2e64a updated aprspacket lib y otros refactoring 2026-09-22 19:59:24 -03:00
Ricardo Guzman 759cddd0e5 date update 2026-09-22 14:28:29 -03:00
Ricardo Guzman 8186ff4fbd updated digi cleanPath and process 2026-09-22 14:27:48 -03:00
Ricardo Guzman 3720fe9881 added fix for mic encoded syslog 2026-09-16 17:12:27 -03:00
Ricardo Guzman 5d04c56fd2 tnc2 added 2026-09-15 11:52:09 -03:00
Ricardo Guzman ee65f953d6 date update 2026-09-07 10:54:24 -03:00
Ricardo Guzman c952ea0159 Merge pull request #456 from cwb/fix-oled-screen-height
display: fix SCREEN_HEIGHT definition and maxLines for SH1106 OLED
2026-09-07 10:52:20 -03:00
Ricardo Guzman 21a42ef157 update de iconos 2026-09-07 10:46:39 -03:00
Ricardo Guzman 53ee07a7c1 heard direct and repeated 2026-08-27 22:45:08 -04:00
Ricardo Guzman 12a0f5bed1 update a 24px symbol 2026-08-25 20:20:09 -04:00
Ricardo Guzman 27f34c5f67 48px 2026-08-25 20:04:12 -04:00
Craig 08bfb013ce display: fix SCREEN_HEIGHT definition and maxLines for SH1106 OLED 2026-08-25 17:03:01 -07:00
Ricardo Guzman d573c66fd7 casi 2026-08-25 19:50:37 -04:00
Ricardo Guzman e0dc2f81ee ready for testing 2026-08-24 23:18:25 -04:00
Ricardo Guzman 6e4dc3c301 update needed for t-decks 2026-08-16 12:28:40 -04:00
Ricardo Guzman d2e42c8571 readme update2 2026-08-16 12:01:31 -04:00
Ricardo Guzman 6b321bc1e0 images update 2026-08-16 11:51:27 -04:00
Ricardo Guzman f7c9201830 now LEAFET is local flash 2026-07-14 00:33:54 -04:00
Ricardo Guzman 1a042b7d0e blockeando leafet 1 2026-07-14 00:17:56 -04:00
Ricardo Guzman aaa44200b5 test query callsign fix 2026-07-06 13:09:19 -04:00
Ricardo Guzman c17ad32650 2026 update in copywrite info 2026-06-29 13:06:58 -04:00
Ricardo Guzman (Richonguzman) 26b5df1bd7 update delete of star in digipeated packet 2026-06-22 22:16:14 -04:00
Ricardo Guzman (Richonguzman) 0c3555bdbe new digimode added 2026-06-18 13:58:10 -04:00
Ricardo Guzman (Richonguzman) cf858ffad2 digi patch 2 2026-06-16 11:29:32 -04:00
Ricardo Guzman (Richonguzman) cbac8b0365 digi patch 1 2026-06-16 10:41:44 -04:00
Ricardo Guzman (Richonguzman) b76aed4d57 cambio digi modes de 2 y 3 a 1 y 2 para agregar patch 2026-06-16 10:25:22 -04:00
Ricardo Guzman (Richonguzman) 96d7be6ffc Map cargado y funcionando 2026-06-12 00:09:02 -04:00
Ricardo Guzman (Richonguzman) 94df6d2e8f map con estaciones 2026-06-11 23:59:24 -04:00
Ricardo Guzman (Richonguzman) 0a222edeb8 primer test solo igate 2026-06-11 22:15:37 -04:00
Ricardo Guzman (Richonguzman) e35d5f6a9e about update 2026-06-11 01:55:45 -04:00
Ricardo Guzman (Richonguzman) 96432b8624 NEW WEB 2026-06-11 01:39:34 -04:00
Ricardo Guzman (Richonguzman) 25ea82737a cad test 3 2026-06-10 10:46:07 -04:00
Ricardo Guzman (Richonguzman) df22b9dbc0 readme and date update 2026-06-10 00:27:15 -04:00
Ricardo Guzman (Richonguzman) 9898e123d2 CAD with backoff process 2026-06-10 00:25:27 -04:00
Ricardo Guzman (Richonguzman) a97757c0b2 test CAD 2026-06-09 22:46:41 -04:00
Ricardo Guzman (Richonguzman) 305c5b73f7 mayusculas a RPC 2026-04-21 11:16:02 -04:00
Ricardo Guzman (Richonguzman) dd3688f23b fake name 2026-04-21 11:12:00 -04:00
Ricardo Guzman (Richonguzman) 6f5846c53d test algo dos 2026-04-21 11:05:52 -04:00
Ricardo Guzman (Richonguzman) 5c30e72a88 test algo 2026-04-21 11:05:27 -04:00
Ricardo Guzman (Richonguzman) a024b939f5 Merge pull request #423 from richonguzman/richonguzman-patch-19
Add RPC_LORA_DIGIGATE_1W to build workflow
2026-04-21 10:42:19 -04:00
Ricardo Guzman (Richonguzman) 96c52c042e Add RPC_LORA_DIGIGATE_1W to build workflow 2026-04-21 10:40:33 -04:00
Ricardo Guzman (Richonguzman) 236ae65485 ElegantOta fix 2026-04-21 10:34:20 -04:00
Ricardo Guzman (Richonguzman) 4263d55839 screen height fix 2026-04-20 12:56:03 -04:00
Ricardo Guzman (Richonguzman) ba96bb135e test new screen 2026-04-20 12:37:52 -04:00
Ricardo Guzman (Richonguzman) 944d1fceaf cleaning code again 2026-04-13 15:24:55 -04:00
Ricardo Guzman (Richonguzman) 005abb3593 Merge pull request #412 from richonguzman/richonguzman-patch-18
Add 2 new target configurations for GPS variants
2026-03-25 12:22:03 -03:00
Ricardo Guzman (Richonguzman) fcf77f6d4b Add 2 new target configurations for GPS variants 2026-03-25 12:20:40 -03:00
Ricardo Guzman (Richonguzman) f576ac6c04 readme update 2026-03-25 12:09:43 -03:00
Ricardo Guzman (Richonguzman) f9f8a9b957 test new boards and gps read update 2026-03-25 12:03:00 -03:00
Ricardo Guzman (Richonguzman) 7f8c2af86b killing blank spaces 2026-03-20 16:47:05 -03:00
Ricardo Guzman (Richonguzman) 198245f600 minor blackspace fix 2026-03-10 23:43:50 -03:00
Ricardo Guzman (Richonguzman) 372bb532b8 Merge pull request #410 from richonguzman/richonguzman-patch-17
Add ttgo-t-beam-v1_SX1262 configuration
2026-03-10 23:38:02 -03:00
Ricardo Guzman (Richonguzman) 05f69fe450 Add ttgo-t-beam-v1_SX1262 configuration 2026-03-10 23:34:22 -03:00
Ricardo Guzman (Richonguzman) 0e8337292f added Tbeam AXP192 and SX1262 2026-03-10 23:25:04 -03:00
Ricardo Guzman (Richonguzman) f87a89213f ArduinoJson 7 update 2026-03-10 23:10:04 -03:00
Ricardo Guzman (Richonguzman) 3810821f45 Minor improvements for OTA and others 2026-03-10 22:44:02 -03:00
Ricardo Guzman (Richonguzman) 6a3b1c903d Merge pull request #382 from petrkr/networkmanager
Network manager
2026-03-10 19:40:14 -03:00
Ricardo Guzman (Richonguzman) 11c36a91fb Safe Poing V3.2.2 2026-03-08 10:32:18 -03:00
Ricardo Guzman (Richonguzman) a97ffe709f Merge pull request #407 from richonguzman/backBefore-FIX-OTA
Back before fix ota
2026-03-08 10:21:40 -03:00
Ricardo Guzman (Richonguzman) c80b565730 Merge branch 'main' into backBefore-FIX-OTA 2026-03-08 10:18:44 -03:00
Ricardo Guzman (Richonguzman) 84cbcc30e8 intentando arreglar 2026-03-08 10:12:57 -03:00
Ricardo Guzman (Richonguzman) 11d413cb17 corrigiendo desorden 2026-03-08 10:11:43 -03:00
Ricardo Guzman (Richonguzman) 19d767e3cb corrigiendo desorden 2026-03-08 10:11:10 -03:00
Ricardo Guzman (Richonguzman) 803bde1008 Merge pull request #405 from petrkr/fixconfig
Fix OTA update due bad configuration behaviour
2026-03-08 09:51:40 -03:00
Ricardo Guzman (Richonguzman) 8ab1f5ec24 many more libraries update 2026-03-08 09:21:34 -03:00
Ricardo Guzman (Richonguzman) cfc9474469 RadioLib 7.6.0 update 2026-03-08 09:12:56 -03:00
Ricardo Guzman (Richonguzman) 949807bc14 Merge pull request #249 from petrkr/updates
Updates
2026-03-08 09:00:06 -03:00
Petr Kracik 37e3a2f831 Wifi: use hasWifiNetworks 2026-03-06 04:08:17 +01:00
Petr Kracik 8bca896e31 Utils: Added Ethernet IP 2026-03-06 04:08:17 +01:00
Petr Kracik e241321abc Use AutoAP 2026-03-06 04:08:17 +01:00
Petr Kracik d8b658df30 Prepare AutoAP enable/disable 2026-03-06 04:08:17 +01:00
Petr Kracik 9775c601bb Wifi utils add multiple SSID 2026-03-06 04:08:17 +01:00
Petr Kracik b0d441a519 Use setHostname from network manager 2026-03-06 04:08:16 +01:00
Petr Kracik 413318e94b Use SoftAP from network manager 2026-03-06 04:08:16 +01:00
Petr Kracik ec28dc0978 Removed deprecated WiFiConnected extern 2026-03-06 04:08:16 +01:00
Petr Kracik e5fa6574f0 Syslog uses networkManager 2026-03-06 04:08:16 +01:00
Petr Kracik 8a16be1bea NTP: Fix nullptr if network is connected after setup() 2026-03-06 04:08:16 +01:00
Petr Kracik 891fc906bb NTP uses networkManager 2026-03-06 04:08:16 +01:00
Petr Kracik eb4087073b Lora uses networkManager 2026-03-06 04:08:16 +01:00
Petr Kracik 10d1fb4ede Utils uses networkManager 2026-03-06 04:08:16 +01:00
Petr Kracik f18d16a6fb ARPS Utils uses networkManager 2026-03-06 04:08:16 +01:00
Petr Kracik 1edb333b1d WiFi utils uses networkManager 2026-03-06 04:08:16 +01:00
Petr Kracik ca567e720d NetworkManager: NM log prefix 2026-03-06 04:08:09 +01:00
Petr Kracík 35760b74f7 Fix OTA update due bad configuration behaviour 2026-03-06 03:49:11 +01:00
Petr Kracik 801641f781 Network manager supports basic Ethernet 2026-03-06 00:09:51 +01:00
Petr Kracik 447c2d4937 Network manager support more Wifi networks 2026-03-06 00:08:44 +01:00
Petr Kracik 1668fc1412 Network manager: AutoAP Disable 2026-03-06 00:08:44 +01:00
Petr Kracik 1ff5504956 Network manager: wifi connect use LED 2026-03-05 23:04:14 +01:00
Petr Kracik e0c6608055 Network manager supports setHostname 2026-03-05 23:04:14 +01:00
Petr Kracík d2c9bcb71d NetworkManager: Initial commit 2026-03-05 23:04:14 +01:00
Petr Kracik 630de55feb Main include WiFiClient instead generic WiFi 2026-03-05 23:04:14 +01:00
Petr Kracík b5690a2f37 Updated espressif SDK 2026-03-05 22:06:00 +01:00
Petr Kracík a7ae6c9fd4 Fix include WiFiClient in MQTT client 2026-03-05 22:01:35 +01:00
Ricardo Guzman (Richonguzman) b448e2fc6b ADC_CTRL_PIN fix 2026-03-05 10:06:22 -03:00
Ricardo Guzman (Richonguzman) c33720a5fb VEXT_CTRL_PIN definition missing fix 2026-03-04 12:24:16 -03:00
Ricardo Guzman (Richonguzman) e57fad6666 aprsis.active update in digi.ecoMode == 1 ? 2026-03-04 11:21:26 -03:00
Ricardo Guzman (Richonguzman) 2418291ac9 digiEcoMode forces APRS.active = false 2026-03-03 11:22:41 -03:00
Ricardo Guzman (Richonguzman) 796ba35357 add new ESP32 device 9M2IBR 2026-02-26 12:28:52 -03:00
Ricardo Guzman (Richonguzman) 57873e4181 readme and date update 2026-02-26 11:51:59 -03:00
Ricardo Guzman (Richonguzman) 453222d69f new board added 9M2IBR 2026-02-26 11:08:49 -03:00
Ricardo Guzman (Richonguzman) fbb347fb7f new update to kiss utils 2026-02-26 10:31:45 -03:00
Ricardo Guzman (Richonguzman) 50fd831fdb kiss correction of missing new indexOf 2026-02-26 10:23:40 -03:00
Ricardo Guzman (Richonguzman) 3a2c0304d0 status logic improved 2026-02-25 12:25:40 -03:00
Ricardo Guzman (Richonguzman) b24c2b2527 update build for heltec v2 915 2026-02-25 10:57:07 -03:00
Ricardo Guzman (Richonguzman) db0da96d7f ReadMe update 2026-02-25 10:45:49 -03:00
Ricardo Guzman (Richonguzman) bf6a0faa90 killing blank spaces 2026-02-24 23:33:58 -03:00
Ricardo Guzman (Richonguzman) 61cf118a3b cleaning Headers 2026-02-24 23:27:36 -03:00
Ricardo Guzman (Richonguzman) 44719083a6 new callsignIsValid update 2026-02-24 21:26:20 -03:00
Ricardo Guzman (Richonguzman) 808d740477 update indexOf kill 2026-02-24 19:29:20 -03:00
Ricardo Guzman (Richonguzman) 23c8257c80 date update 2026-02-24 07:55:53 -03:00
Ricardo Guzman (Richonguzman) db4582db13 telemetry EUP beacon update 2026-02-24 07:55:32 -03:00
Ricardo Guzman (Richonguzman) 09e06d36e8 checkCallsignList update 2026-02-24 06:50:27 -03:00
Ricardo Guzman (Richonguzman) e6d44c1b7f replaced string for hash in 25segBuffer 2026-02-23 18:59:27 -03:00
Ricardo Guzman (Richonguzman) d4fc99466f update digi2000 2026-02-23 18:07:26 -03:00
Ricardo Guzman (Richonguzman) 41f09af7b5 digipeater logic improved 2026-02-23 17:32:52 -03:00
Ricardo Guzman (Richonguzman) 0eec028c5d update SENSOR BUS 2026-02-23 12:00:55 -03:00
Ricardo Guzman (Richonguzman) c48dd15bd6 better ADC and VEXT control 2026-02-23 11:14:42 -03:00
Ricardo Guzman (Richonguzman) f26ded5b5c less memory fixes too 2026-02-20 17:36:26 -03:00
Ricardo Guzman (Richonguzman) c887689406 indexOf transformation 2026-02-20 17:03:39 -03:00
Ricardo Guzman (Richonguzman) 775e08a10a stationCallsignIsValid 2026-02-20 10:03:01 -03:00
Ricardo Guzman (Richonguzman) f9291821d2 2 decimales for QTH query 2026-02-18 11:03:06 -03:00
Ricardo Guzman (Richonguzman) 12d0bb760e version update 2026-02-16 23:46:03 -03:00
Ricardo Guzman (Richonguzman) f020eb7491 update a nombre CPU 2026-02-16 23:01:05 -03:00
Ricardo Guzman (Richonguzman) 63f4660ef6 power utils update con cpu 2026-02-16 22:48:28 -03:00
Ricardo Guzman (Richonguzman) efabe9b1f7 start Heltec V2 915 2026-02-16 21:37:54 -03:00
Ricardo Guzman (Richonguzman) beae88d557 less time 2026-02-11 01:03:54 -03:00
Ricardo Guzman (Richonguzman) 8b4d8c2d1d day reset of status tx 2026-02-10 23:01:49 -03:00
Ricardo Guzman (Richonguzman) 08ae1e322b Symbol Explanation added to index.html 2026-02-03 17:09:27 -03:00
Ricardo Guzman (Richonguzman) d576be0f2b fix telemetry tactical callsign 2026-01-31 11:47:27 -03:00
Ricardo Guzman (Richonguzman) 7c9903ac12 WEBUI update 2026-01-20 22:44:03 -03:00
Ricardo Guzman (Richonguzman) 0d7cff14f5 lastServerCheck update 2026-01-20 21:19:53 -03:00
Ricardo Guzman (Richonguzman) 5c89635a23 lastServerCheck 2026-01-20 21:08:28 -03:00
Ricardo Guzman (Richonguzman) 7a35f9e32a Server Alive mod 2026-01-20 20:56:20 -03:00
Ricardo Guzman (Richonguzman) 7695675a7c check server 2026-01-20 09:50:09 -03:00
Ricardo Guzman (Richonguzman) c8c0be636d check internet connection 2026-01-20 00:16:24 -03:00
Ricardo Guzman (Richonguzman) 76fe27a0ab APRSSSR into APRSSR fix 2026-01-16 09:41:20 -03:00
Ricardo Guzman (Richonguzman) da80391921 change Freq with bandWidth fix 2026-01-13 01:50:12 -03:00
Ricardo Guzman (Richonguzman) d638093dbf Tactical 6 update 2026-01-11 11:46:23 -03:00
Ricardo Guzman (Richonguzman) 66a5f03c12 de update for tactical 2026-01-09 00:02:29 -03:00
Ricardo Guzman (Richonguzman) ce8cf3a2fe minimal interval updated 2026-01-08 14:08:07 -03:00
Ricardo Guzman (Richonguzman) 4b45b90c88 minor path updates 2026-01-08 12:21:19 -03:00
Ricardo Guzman (Richonguzman) dbe980a081 tactical callsign ready for testing 2026-01-07 23:18:14 -03:00
Ricardo Guzman (Richonguzman) bb3d59a20d digi tactical changes 2026-01-07 17:25:45 -03:00
Ricardo Guzman (Richonguzman) fe590b41b0 tactical callsign displayShow 2026-01-07 14:30:32 -03:00
Ricardo Guzman (Richonguzman) 3f76005949 few delays killed 2026-01-07 12:19:51 -03:00
Ricardo Guzman (Richonguzman) 6d95231b9c web mods for tactical 2026-01-07 11:53:10 -03:00
Ricardo Guzman (Richonguzman) 81692010cf tactical validation 2026-01-07 11:44:00 -03:00
Ricardo Guzman (Richonguzman) 50b738d04b Spreading Factor update 2026-01-07 10:37:11 -03:00
Ricardo Guzman (Richonguzman) 026d6b2eeb starting Heltec V4 2026-01-07 10:14:42 -03:00
Ricardo Guzman (Richonguzman) fe705519cb V3.1.7 OTA and mode 2025-12-29 09:10:17 -03:00
Ricardo Guzman (Richonguzman) 1fa74b8697 GPS encoded payload comment fix 2025-12-28 13:45:29 -03:00
Ricardo Guzman (Richonguzman) a3794085b4 OTA update fixed 2025-12-28 11:59:05 -03:00
Ricardo Guzman (Richonguzman) 0a898a40e6 gps payload decoding fix2 2025-12-28 11:46:43 -03:00
Ricardo Guzman (Richonguzman) ff8c7581fa gps payload decoding fix 2025-12-28 10:06:34 -03:00
Ricardo Guzman (Richonguzman) 449a8557d2 definition fix for VisionMaster 2025-12-23 08:32:06 -03:00
Ricardo Guzman (Richonguzman) 4419c98920 Merge pull request #375 from richonguzman/richonguzman-patch-16
added Heltec VisionMaster E290 and WirelessPaper V1.2
2025-12-22 23:02:46 -03:00
Ricardo Guzman (Richonguzman) 45bf90817b added Heltec VisionMaster E290 and WirelessPaper V1.2 2025-12-22 22:59:21 -03:00
Ricardo Guzman (Richonguzman) ef30a1bf58 readme update 2025-12-22 21:34:58 -03:00
Ricardo Guzman (Richonguzman) 9a705d3dfa Heltec Wireless Paper V1.2 added 2025-12-22 21:29:50 -03:00
Ricardo Guzman (Richonguzman) 63f74396d2 Heltec Vision Master update 2025-12-22 21:15:03 -03:00
Ricardo Guzman (Richonguzman) 400b77c2d3 date update 2025-12-22 19:57:18 -03:00
Ricardo Guzman (Richonguzman) 31daddf917 invalid Passcode non blocking fix 2025-12-22 19:56:15 -03:00
Ricardo Guzman (Richonguzman) 529a44018f readme and version update 2025-12-18 09:47:56 -03:00
Ricardo Guzman (Richonguzman) 5ba9c5b382 test 2025-12-18 09:28:56 -03:00
Ricardo Guzman (Richonguzman) c7a0e3773b ina bme update 2025-12-14 05:53:43 -03:00
Ricardo Guzman (Richonguzman) a5f9e5b844 typo TCXO fix 2025-12-12 12:25:41 -03:00
Ricardo Guzman (Richonguzman) 24f407d51c TCXO add 2025-12-12 12:24:53 -03:00
Ricardo Guzman (Richonguzman) 2c6665b557 FIX for boards with GPS onboard 2025-12-11 13:44:06 -03:00
Ricardo Guzman (Richonguzman) 38f52564f1 lib def fix 2025-12-11 10:27:29 -03:00
Ricardo Guzman (Richonguzman) 49e92c622f README UPDATE for v3.1.5 manual 2025-12-07 08:46:45 -03:00
Ricardo Guzman (Richonguzman) 5370850ae1 update to fix ntp without wifi 2025-12-03 12:31:36 -03:00
Ricardo Guzman (Richonguzman) 984e9f9561 encoded telemetry fix for INA219 2025-12-01 19:53:14 -03:00
Ricardo Guzman (Richonguzman) f60252ef94 fix for same declaration of variables 2025-12-01 16:02:26 -03:00
Ricardo Guzman (Richonguzman) 63257d6329 readme update 2025-12-01 13:30:37 -03:00
Ricardo Guzman (Richonguzman) 18929ad379 INA219 final test 2025-12-01 13:29:54 -03:00
Ricardo Guzman (Richonguzman) cda901142d INA219 3 2025-12-01 12:45:14 -03:00
Ricardo Guzman (Richonguzman) 8008143267 configuration INA219 add 2025-12-01 10:55:31 -03:00
Ricardo Guzman (Richonguzman) 3416e21a8c ready radiolib excludes 2025-12-01 10:45:14 -03:00
Ricardo Guzman (Richonguzman) 5882b54c62 radiolib exclude start 2025-12-01 10:11:15 -03:00
Ricardo Guzman (Richonguzman) 6a4c0ef01a readme update 2025-12-01 09:38:37 -03:00
Ricardo Guzman (Richonguzman) 0bd3201ef7 AHT20 add 2025-12-01 09:37:02 -03:00
Ricardo Guzman (Richonguzman) b5d9103ea5 gps beacon fix 2025-11-30 10:47:12 -03:00
Ricardo Guzman (Richonguzman) 20a5029da8 index fix 2025-11-30 10:37:23 -03:00
Ricardo Guzman (Richonguzman) b3cc8af180 test update aprspacketlib 2025-11-30 10:15:01 -03:00
Ricardo Guzman (Richonguzman) ca16761f68 loadCallsignList update 2025-11-29 14:11:43 -03:00
Ricardo Guzman (Richonguzman) e593b2c670 version number update 2025-11-08 21:16:03 -03:00
Ricardo Guzman (Richonguzman) 08a7e0aac1 T-Beam Supreme Screen Fix 2025-11-08 21:13:57 -03:00
Ricardo Guzman (Richonguzman) 9b258c42ab checkValidCallsign update 2025-11-06 10:11:55 -03:00
richonguzman 8712122d33 raw images 2025-10-29 10:34:43 -03:00
richonguzman 9e34684627 update pdf y blob 2025-10-29 10:28:25 -03:00
richonguzman e4aa52241d sin gitattributes ahora 2025-10-29 10:11:18 -03:00
richonguzman c840ef01fa sin manual en github 2025-10-29 10:10:58 -03:00
richonguzman 98f85725f8 test con Vista Previa 2025-10-29 09:13:48 -03:00
richonguzman 10bde884b1 new gitattributes 2025-10-28 22:54:22 -03:00
richonguzman 546e4f63a4 update pdf to v1.4 2025-10-28 22:46:44 -03:00
Ricardo Guzman (Richonguzman) 87f2ec2b7b test new version pdf 20250128 2025-10-28 22:40:15 -03:00
Ricardo Guzman (Richonguzman) c9c7e24aae Merge pull request #356 from richonguzman/richonguzman-patch-15
Delete manual/LoRa_APRS_iGate_CA2RXU_Firmware_Manual.pdf
2025-10-28 22:38:23 -03:00
Ricardo Guzman (Richonguzman) b86133223d Delete manual/LoRa_APRS_iGate_CA2RXU_Firmware_Manual.pdf 2025-10-28 22:38:02 -03:00
richonguzman 880a41bfd8 update manual 20251028 2025-10-28 22:34:06 -03:00
richonguzman d610b3a90f mqtt beacon fix 2025-10-21 14:33:58 -03:00
richonguzman 23a33ed27c new message warning 2025-10-19 14:20:11 -03:00
richonguzman 72ef827900 new update pdf manual v314 2025-10-18 11:42:18 -03:00
richonguzman b756f97f55 v314 manual update 2025-10-18 10:50:54 -03:00
richonguzman 2625e7b91d update readme v314 2025-10-15 17:29:26 -03:00
richonguzman 75337d6c29 update to Tnc strip bytes 2025-10-15 17:12:38 -03:00
richonguzman a618383617 APRS Bridge for TNC 2025-10-15 15:11:07 -03:00
richonguzman 8add599838 NTP server change fix 2025-10-15 14:31:02 -03:00
richonguzman 83ec2265c6 Rx and Tx Freq selection fix 2025-10-15 14:18:43 -03:00
richonguzman bec4f4f473 beaconOnRxFreq into beacon config 2025-10-15 10:58:30 -03:00
richonguzman 87a67cd2c9 readmeupdate otro mas 2025-10-13 13:29:56 -03:00
richonguzman 4e48b2d02d manual update 2025-10-13 13:11:51 -03:00
richonguzman 607277bfe9 readme update2 2025-10-13 12:44:47 -03:00
richonguzman c2f8596667 readme update 2025-10-13 12:44:15 -03:00
richonguzman ba7ff2a2d9 WebUI change 2025-10-13 12:38:52 -03:00
richonguzman b44eb1028d no more speed standards on index 2025-10-13 02:03:18 -03:00
richonguzman d7602268f2 changes for Rx and Tx Lora Freq 2025-10-13 02:00:05 -03:00
richonguzman 2f6f9be28e Lora Config separated for Rx and Tx 2025-10-13 01:33:41 -03:00
richonguzman 92572245cd beacon Freq on digi mod 2025-10-12 21:45:23 -03:00
richonguzman 152217e71c startup delay added 2025-10-12 14:21:51 -03:00
richonguzman d334164b6f Beacon Freq on Digi Mode 2025-10-11 18:27:06 -03:00
richonguzman 61409ce683 index correction 2025-10-11 15:57:29 -03:00
richonguzman 742a6b6477 readme update 2025-10-11 13:23:39 -03:00
richonguzman dd2fce3908 igate beacon over mqtt 2025-10-11 13:22:08 -03:00
richonguzman 90b29b66d5 callsign trim 2025-10-11 12:50:53 -03:00
richonguzman 2a86cdb0d8 Op ntp server 2025-10-11 12:40:07 -03:00
richonguzman 1abaa299e0 update readme 2025-10-10 12:44:15 -03:00
Ricardo Guzman (Richonguzman) 9cbd7c22fd Merge pull request #342 from richonguzman/richonguzman-patch-14
Update README.md
2025-10-10 12:15:18 -03:00
Ricardo Guzman (Richonguzman) e15e6def91 Update README.md 2025-10-10 12:14:56 -03:00
richonguzman 33c8be4f48 added manual for 3.1.2 2025-10-10 12:11:24 -03:00
Ricardo Guzman (Richonguzman) c2a7c26d78 Merge pull request #341 from richonguzman/richonguzman-patch-13
Update README.md
2025-10-10 12:08:41 -03:00
Ricardo Guzman (Richonguzman) 387e8a988a Update README.md 2025-10-10 12:08:28 -03:00
Ricardo Guzman (Richonguzman) 48b075a554 Merge pull request #340 from richonguzman/richonguzman-patch-12
Update README.md
2025-10-10 12:06:38 -03:00
Ricardo Guzman (Richonguzman) 7e76e43817 Update README.md 2025-10-10 12:06:25 -03:00
Ricardo Guzman (Richonguzman) 9b7b4e1838 Merge pull request #339 from richonguzman/richonguzman-patch-11
Update README.md
2025-10-10 12:04:17 -03:00
Ricardo Guzman (Richonguzman) 5b9d56843b Update README.md 2025-10-10 12:04:04 -03:00
Ricardo Guzman (Richonguzman) 30eb1023dc Merge pull request #338 from richonguzman/richonguzman-patch-10
Update README.md
2025-10-10 12:03:09 -03:00
Ricardo Guzman (Richonguzman) 1ba86af61b Update README.md 2025-10-10 12:02:54 -03:00
Ricardo Guzman (Richonguzman) 1d475bc8a2 Merge pull request #337 from richonguzman/richonguzman-patch-9
Update README.md
2025-10-10 11:58:13 -03:00
Ricardo Guzman (Richonguzman) 70f9a6fa04 Update README.md 2025-10-10 11:57:50 -03:00
Ricardo Guzman (Richonguzman) 6747511850 Merge pull request #336 from richonguzman/richonguzman-patch-8
Update README.md
2025-10-10 11:56:25 -03:00
Ricardo Guzman (Richonguzman) 96a4394e89 Update README.md 2025-10-10 11:56:10 -03:00
Ricardo Guzman (Richonguzman) 4e009bc14c Merge pull request #332 from richonguzman/richonguzman-patch-7
adding heltec wireless bridge
2025-09-29 15:52:00 -03:00
Ricardo Guzman (Richonguzman) 4c11c66a1c adding heltec wireless bridge 2025-09-29 15:51:41 -03:00
richonguzman a89181b5df update complete 2025-09-26 11:37:02 -03:00
richonguzman 9a94a193f9 update before release 2025-09-25 22:01:38 -03:00
richonguzman 9c7f6c7934 led update 2025-09-25 17:54:53 -03:00
richonguzman 53e8941262 blue led add 2025-09-25 17:33:59 -03:00
richonguzman 60a148d362 testing1 2025-09-25 15:55:18 -03:00
richonguzman 36cd1578cc adding basic stuff 2025-09-25 15:50:28 -03:00
richonguzman c3036f290f TCPIP correction 2025-09-25 15:35:38 -03:00
richonguzman 8eb4ef1dc2 testeando ultimos valores 2025-09-17 17:00:49 -03:00
richonguzman d5930380b4 testing new config init validation 2025-09-17 16:53:09 -03:00
richonguzman e42f4b59ed prueba nueva web write desde formulario 2025-09-17 15:29:13 -03:00
richonguzman 2697f2a5f2 update a configuration cpp 2025-09-17 14:29:37 -03:00
richonguzman 36c970aed9 json file size fix 2025-09-14 20:00:56 -03:00
richonguzman 10e1581bc2 new board 2025-09-14 19:54:20 -03:00
Ricardo Guzman (Richonguzman) bc57d1609a Merge pull request #321 from richonguzman/richonguzman-patch-6
Update README.md
2025-09-09 10:22:52 -03:00
Ricardo Guzman (Richonguzman) 37380c6b9d Update README.md 2025-09-09 10:22:27 -03:00
Ricardo Guzman (Richonguzman) 855c84c079 Merge pull request #320 from richonguzman/richonguzman-patch-5
Update build for Heltec wireless paper v1
2025-09-09 10:02:05 -03:00
224 changed files with 7215 additions and 3394 deletions

No files matched your search

+41 -17
View File
@@ -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
@@ -29,17 +35,21 @@ jobs:
chip: esp32s3
- name: heltec_wireless_stick_lite_v3_display
chip: esp32s3
- name: ESP32_DIY_LoRa
- name: heltec_wireless_bridge
chip: esp32
- name: ESP32_DIY_LoRa_915
- name: ESP32_DIY_LoRa_SX1278
chip: esp32
- name: ESP32_DIY_1W_LoRa
- name: ESP32_DIY_LoRa_915_SX1276
chip: esp32
- name: ESP32_DIY_1W_LoRa_915
- name: ESP32_DIY_1W_LoRa_E22_400M30S
chip: esp32
- name: ESP32_DIY_1W_LoRa_LLCC68
- name: ESP32_DIY_1W_LoRa_915_E22_900M30S
chip: esp32
- name: ESP32_DIY_1W_LoRa_Mesh_V1_2
- name: ESP32_DIY_1W_LoRa_E220_400M30S
chip: esp32
- name: ESP32_DIY_1W_LoRa_E32_400M30S
chip: esp32
- name: ESP32_DIY_1W_LoRa_Mesh_V1_2_E22_400M30S
chip: esp32
- name: ttgo-t-beam-v1_2
chip: esp32
@@ -51,6 +61,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
@@ -59,9 +71,9 @@ jobs:
chip: esp32s3
- name: ttgo_t_beam_s3_SUPREME_v3
chip: esp32s3
- name: ESP32_DIY_LoRa_A7670
- name: ESP32_DIY_LoRa_A7670_SX1278
chip: esp32
- name: ESP32_DIY_LoRa_A7670_915
- name: ESP32_DIY_LoRa_A7670_915_SX1276
chip: esp32
- name: heltec_wireless_tracker
chip: esp32s3
@@ -69,28 +81,40 @@ jobs:
chip: esp32c3
- name: heltec_wireless_paper_v1
chip: esp32s3
- name: heltec_wireless_paper_v1_2
chip: esp32s3
- name: heltec_vision_master_e290
chip: esp32s3
- name: OE5HWN_MeshCom
chip: esp32
- name: WEMOS-LOLIN32-OLED-DIY
- name: WEMOS_LOLIN32_OLED_DIY_LoRa_SX1278
chip: esp32
- name: WEMOS-D1-R32-RA02
- name: WEMOS_D1_R32_DIY_LoRa_SX1278
chip: esp32
- name: WEMOS_S2_MINI_DIY_LoRa
- name: WEMOS_S2_MINI_DIY_LoRa_SX1278
chip: esp32s2
- name: esp32c3_DIY_1W_LoRa
- name: ESP32C3_DIY_1W_LoRa_E22_400M30S
chip: esp32c3
- name: esp32c3_DIY_1W_LoRa_915
- name: ESP32C3_DIY_1W_LoRa_915_E22_900M30S
chip: esp32c3
- name: ESP32_C3_OctopusLab_LoRa
- name: ESP32C3_OctopusLab_LoRa
chip: esp32c3
- name: QRPLabs_LightGateway_1_0
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: RAK3312
chip: esp32s3
- name: RAK3112
chip: esp32s3
- name: TROY_LoRa_APRS
chip: esp32
chip: esp32
- name: ESP32_9M2IBR_1W_LoRa_GPS_E22_400M30S
chip: esp32
steps:
- uses: actions/checkout@v3
@@ -103,7 +127,7 @@ jobs:
- name: Build target
run: |
pio run -e ${{ matrix.target.name }}
pio run -e ${{ matrix.target.name }}
- name: Build FS
run: |
@@ -118,7 +142,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
+2 -1
View File
@@ -5,6 +5,7 @@
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
"ms-vscode.cpptools-extension-pack",
"pioarduino.pioarduino-ide"
]
}
+48 -23
View File
@@ -2,53 +2,54 @@
This firmware is for using ESP32 based boards with LoRa Modules and GPS to live in the APRS world.
![Screenshot](https://github.com/richonguzman/LoRa_APRS_iGate/blob/main/images/iGateOledScreen.jpeg)
![Screenshot](https://github.com/richonguzman/LoRa_APRS_iGate/raw/main/images/iGateOledScreen.jpeg)
__(This iGate Firmware works with all LoRa Tracker Firmwares (specially this <a href="https://github.com/richonguzman/LoRa_APRS_Tracker" target="_blank">LoRa APRS Tracker Firmware</a>))__
<br />
____________________________________________________
## You can support this project to continue to grow:
# <a href="https://richonguzman.github.io/lora-igate-web-flasher/installer.html" target="_blank">WEB FLASHER/INSTALLER</a>
[<img src="https://github.com/richonguzman/LoRa_APRS_Tracker/blob/main/images/github-sponsors.png">](https://github.com/sponsors/richonguzman) [<img src="https://github.com/richonguzman/LoRa_APRS_Tracker/blob/main/images/paypalme.png">](http://paypal.me/richonguzman)
<br />
# WEB FLASHER/INSTALLER is <a href="https://richonguzman.github.io/lora-igate-web-flasher/installer.html" target="_blank">here</a>
# <a href="https://drive.google.com/file/d/1Hff_Szd7ks8RC7_RiV6POxPJlclbO05M/view?usp=sharing" target="_blank">LoRa APRS iGate CA2RXU Firmware Manual</a>
____________________________________________________
# WIKI
## New Web/UI
### FAQ, BME280, TNC and more --> <a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/00.-FAQ-(frequently-asked-questions)" target="_blank">here</a>.
| New Menu | Map |
| --- | --- |
| ![new menu](images/1.png) | ![map](images/2.png) |
### Installation Guide --> <a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/01.-Installation-Guide" target="_blank">here</a>.
<br />
____________________________________________________
# SUPPORTED BOARDS
## You can support this project to continue to grow:
### Buying links --> <a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/108.-Supported-Boards-and-Buying-Links" target="_blank">here</a>.
[<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).
- 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, 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(19003)
- RAK Wireless 4631 + 19007(or 19003), RAK3312 and RAK3112.
- Faketec (NRF52840 + Heltec HTRA62(SX1262))
- Faketec V3 (NRF52840 + Heltec HTRA62(SX1262))
- QRP Labs LightGateway 1.0 and Plus 1.0.
- Faketec V3 (NRF52840 + Heltec HTRA62 SX1262)
- ESP32 Wroom + SX1278 LoRa Module or Ebyte 400M30S (or 900M30S) 1W LoRa Module for a DIY Versions.
- ESP32 + SX1278 LoRa Module or Ebyte 400M30S (or 900M30S) or Ebyte E32 400M30S 1W LoRa Module for a DIY Versions.
- ESP32C3 + Ebyte 400M30S(or 900M30S) 1W LoRa Module for another DIY version.
@@ -56,10 +57,34 @@ ____________________________________________________
- Wemos Lolin32 Oled + SX1278 DIY Version.
- and many more...
<br />
## Timeline (Versions):
# Timeline (Versions):
- 2026-09-23 APRSPacketLib V1.0.7 update and many code cleaning with this.
- 2026-09-17 Real APRS Symbols added to map and TNC2 output as a posibility in TNC mode.
- 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.
- 2025-12-18 TCXO and packet decoding updates.
- 2025-12-01 APRSPacketLib updates, AHT20 sensor added, INA219 support added.
- 2025-10-15 APRS Bridge for TNC added.
- 2025-10-13 Rx and Tx Frequencies are now with fully configurable.
- 2025-10-13 Startup Delay to allow the Router/Modem to start WiFiAP before connecting.
- 2025-10-12 Choose to send Beacon on Rx or Tx frequency.
- 2025-10-11 User defined NTP server and send beacon over MQTT added.
- 2025-10-10 Converted the Wiki into a PDF manual.
- 2025-09-26 Heltec Wireless Bridge support added.
- 2025-09-09 MQTT added (pub+sub), Status defined by Op now and many fixes more.
- 2025-06-20 Digipeaters now with updated EcoMode (Board Sleeps until packet Rx reducing current consumption to almost 10% at idle).
- 2025-06-20 New Boards Added: Heltec T114 MeshNode, Faketec V3 as Digipeaters and QRP Labs LightGateway Plus 1.0.
- 2025-06-19 DateVersion format Change. Licence changed into GNU GPLv3.
@@ -81,7 +106,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.
+34 -33
View File
@@ -2,45 +2,46 @@
build_flags =
-Werror -Wall
-DELEGANTOTA_USE_ASYNC_WEBSERVER=1
-DRADIOLIB_EXCLUDE_CC1101=1
-DRADIOLIB_EXCLUDE_NRF24=1
-DRADIOLIB_EXCLUDE_RF69=1
-DRADIOLIB_EXCLUDE_SX1231=1
-DRADIOLIB_EXCLUDE_SX1233=1
-DRADIOLIB_EXCLUDE_SI443X=1
-DRADIOLIB_EXCLUDE_RFM2X=1
-DRADIOLIB_EXCLUDE_AFSK=1
-DRADIOLIB_EXCLUDE_BELL=1
-DRADIOLIB_EXCLUDE_HELLSCHREIBER=1
-DRADIOLIB_EXCLUDE_MORSE=1
-DRADIOLIB_EXCLUDE_RTTY=1
-DRADIOLIB_EXCLUDE_SSTV=1
-DRADIOLIB_EXCLUDE_AX25=1
-DRADIOLIB_EXCLUDE_DIRECT_RECEIVE=1
-DRADIOLIB_EXCLUDE_BELL=1
-DRADIOLIB_EXCLUDE_PAGER=1
-DRADIOLIB_EXCLUDE_FSK4=1
-DRADIOLIB_EXCLUDE_APRS=1
-DRADIOLIB_EXCLUDE_LORAWAN=1
-D RADIOLIB_EXCLUDE_CC1101=1
-D RADIOLIB_EXCLUDE_RF69=1
-D RADIOLIB_EXCLUDE_RFM2X=1
-D RADIOLIB_EXCLUDE_SX1231=1
-D RADIOLIB_EXCLUDE_SX1233=1
-D RADIOLIB_EXCLUDE_SI443X=1
-D RADIOLIB_EXCLUDE_NRF24=1
-D RADIOLIB_EXCLUDE_AFSK=1
-D RADIOLIB_EXCLUDE_APRS=1
-D RADIOLIB_EXCLUDE_AX25=1
-D RADIOLIB_EXCLUDE_BELL=1
-D RADIOLIB_EXCLUDE_FSK4=1
-D RADIOLIB_EXCLUDE_HELLSCHREIBER=1
-D RADIOLIB_EXCLUDE_LORAWAN=1
-D RADIOLIB_EXCLUDE_MORSE=1
-D RADIOLIB_EXCLUDE_PAGER=1
-D RADIOLIB_EXCLUDE_DIRECT_RECEIVE=1
-D RADIOLIB_EXCLUDE_RTTY=1
-D RADIOLIB_EXCLUDE_SSTV=1
-I variants/${PIOENV}
lib_deps =
adafruit/Adafruit Unified Sensor @ 1.1.14
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.5
bblanchon/ArduinoJson @ 6.21.3
jgromes/RadioLib @ 7.1.0
ayushsharma82/ElegantOTA @ 3.1.7
bblanchon/ArduinoJson @ 7.4.3
jgromes/RadioLib @ 7.6.0
knolleary/PubSubClient @ 2.8
mathieucarbou/AsyncTCP @ 3.2.5
mathieucarbou/ESPAsyncWebServer @ 3.2.3
ESP32Async/AsyncTCP @ 3.4.10
ESP32Async/ESPAsyncWebServer @ 3.11.1
mikalhart/TinyGPSPlus @ 1.0.3
richonguzman/APRSPacketLib @1.0.0
richonguzman/APRSPacketLib @ 1.0.7
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
+25 -15
View File
@@ -17,35 +17,41 @@
"path": "WIDE1-1",
"sendViaAPRSIS": false,
"sendViaRF": false,
"beaconFreq": 1,
"statusActive": false,
"statusPacket": "",
"gpsActive": false,
"gpsAmbiguity": false
"ambiguityLevel": 0
},
"aprs_is": {
"active": false,
"messagesToRF": false,
"objectsToRF": false,
"server": "rotate.aprs2.net",
"passcode": "XYZVW",
"passcode": "XYZVW",
"port": 14580,
"filter": "m/10"
"filter": "m/10"
},
"personalNote": "",
"blacklist": "",
"digi": {
"mode": 0,
"ecoMode": 0
"ecoMode": 0,
"backupDigiMode": false
},
"lora": {
"txFreq": 433775000,
"rxActive": true,
"rxFreq": 433775000,
"spreadingFactor": 12,
"signalBandwidth": 125000,
"codingRate4": 5,
"power": 20,
"rxSpreadingFactor": 12,
"rxCodingRate4": 5,
"rxSignalBandwidth": 125000,
"txActive": false,
"rxActive": true
"txFreq": 433775000,
"txSpreadingFactor": 9,
"txCodingRate4": 7,
"txSignalBandwidth": 125000,
"power": 20,
"cadActive": true
},
"display": {
"alwaysOn": true,
@@ -57,11 +63,12 @@
"monitorInternalVoltage": false,
"internalSleepVoltage": 2.9,
"sendExternalVoltage": false,
"externalVoltagePin": 34,
"monitorExternalVoltage": false,
"externalSleepVoltage": 10.9,
"useExternalI2CSensor": false,
"voltageDividerR1": 100.0,
"voltageDividerR2": 27.0,
"externalVoltagePin": 34,
"sendVoltageAsTelemetry": false
},
"wxsensor": {
@@ -78,7 +85,8 @@
"tnc": {
"enableServer": false,
"enableSerial": false,
"acceptOwn": false
"acceptOwn": false,
"aprsBrigdeActive": false
},
"mqtt": {
"active": false,
@@ -86,7 +94,8 @@
"topic": "",
"username": "",
"password": "",
"port": 1883
"port": 1883,
"beaconOverMqtt": false
},
"ota": {
"username": "",
@@ -101,12 +110,13 @@
"rfOnly": true
},
"ntp": {
"server": "pool.ntp.org",
"gmtCorrection": 0.0
},
"other": {
"rememberStationTime": 30,
"backupDigiMode": false,
"rebootMode": false,
"rebootModeTime": 6
"rebootModeTime": 6,
"startupDelay": 0
}
}
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

+1370 -1128
View File
File diff suppressed because it is too large. Load diff
+661
View File
@@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
+6
View File
@@ -0,0 +1,6 @@
/* @preserve
* Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com
* (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n<o;n++)for(e in i=arguments[n])t[e]=i[e];return t}var R=Object.create||function(t){return N.prototype=t,new N};function N(){}function a(t,e){var i,n=Array.prototype.slice;return t.bind?t.bind.apply(t,n.call(arguments,1)):(i=n.call(arguments,2),function(){return t.apply(e,i.length?i.concat(n.call(arguments)):arguments)})}var D=0;function h(t){return"_leaflet_id"in t||(t._leaflet_id=++D),t._leaflet_id}function j(t,e,i){var n,o,s=function(){n=!1,o&&(r.apply(i,o),o=!1)},r=function(){n?o=arguments:(t.apply(i,arguments),setTimeout(s,e),n=!0)};return r}function H(t,e,i){var n=e[1],e=e[0],o=n-e;return t===n&&i?t:((t-e)%o+o)%o+e}function u(){return!1}function i(t,e){return!1===e?t:(e=Math.pow(10,void 0===e?6:e),Math.round(t*e)/e)}function W(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}function F(t){return W(t).split(/\s+/)}function c(t,e){for(var i in Object.prototype.hasOwnProperty.call(t,"options")||(t.options=t.options?R(t.options):{}),e)t.options[i]=e[i];return t.options}function U(t,e,i){var n,o=[];for(n in t)o.push(encodeURIComponent(i?n.toUpperCase():n)+"="+encodeURIComponent(t[n]));return(e&&-1!==e.indexOf("?")?"&":"?")+o.join("&")}var V=/\{ *([\w_ -]+) *\}/g;function q(t,i){return t.replace(V,function(t,e){e=i[e];if(void 0===e)throw new Error("No value provided for variable "+t);return e="function"==typeof e?e(i):e})}var d=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)};function G(t,e){for(var i=0;i<t.length;i++)if(t[i]===e)return i;return-1}var K="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";function Y(t){return window["webkit"+t]||window["moz"+t]||window["ms"+t]}var X=0;function J(t){var e=+new Date,i=Math.max(0,16-(e-X));return X=e+i,window.setTimeout(t,i)}var $=window.requestAnimationFrame||Y("RequestAnimationFrame")||J,Q=window.cancelAnimationFrame||Y("CancelAnimationFrame")||Y("CancelRequestAnimationFrame")||function(t){window.clearTimeout(t)};function x(t,e,i){if(!i||$!==J)return $.call(window,a(t,e));t.call(e)}function r(t){t&&Q.call(window,t)}var tt={__proto__:null,extend:l,create:R,bind:a,get lastId(){return D},stamp:h,throttle:j,wrapNum:H,falseFn:u,formatNum:i,trim:W,splitWords:F,setOptions:c,getParamString:U,template:q,isArray:d,indexOf:G,emptyImageUrl:K,requestFn:$,cancelFn:Q,requestAnimFrame:x,cancelAnimFrame:r};function et(){}et.extend=function(t){function e(){c(this),this.initialize&&this.initialize.apply(this,arguments),this.callInitHooks()}var i,n=e.__super__=this.prototype,o=R(n);for(i in(o.constructor=e).prototype=o,this)Object.prototype.hasOwnProperty.call(this,i)&&"prototype"!==i&&"__super__"!==i&&(e[i]=this[i]);if(t.statics&&l(e,t.statics),t.includes){var s=t.includes;if("undefined"!=typeof L&&L&&L.Mixin){s=d(s)?s:[s];for(var r=0;r<s.length;r++)s[r]===L.Mixin.Events&&console.warn("Deprecated include of L.Mixin.Events: this property will be removed in future releases, please inherit from L.Evented instead.",(new Error).stack)}l.apply(null,[o].concat(t.includes))}return l(o,t),delete o.statics,delete o.includes,o.options&&(o.options=n.options?R(n.options):{},l(o.options,t.options)),o._initHooks=[],o.callInitHooks=function(){if(!this._initHooksCalled){n.callInitHooks&&n.callInitHooks.call(this),this._initHooksCalled=!0;for(var t=0,e=o._initHooks.length;t<e;t++)o._initHooks[t].call(this)}},e},et.include=function(t){var e=this.prototype.options;return l(this.prototype,t),t.options&&(this.prototype.options=e,this.mergeOptions(t.options)),this},et.mergeOptions=function(t){return l(this.prototype.options,t),this},et.addInitHook=function(t){var e=Array.prototype.slice.call(arguments,1),i="function"==typeof t?t:function(){this[t].apply(this,e)};return this.prototype._initHooks=this.prototype._initHooks||[],this.prototype._initHooks.push(i),this};var e={on:function(t,e,i){if("object"==typeof t)for(var n in t)this._on(n,t[n],e);else for(var o=0,s=(t=F(t)).length;o<s;o++)this._on(t[o],e,i);return this},off:function(t,e,i){if(arguments.length)if("object"==typeof t)for(var n in t)this._off(n,t[n],e);else{t=F(t);for(var o=1===arguments.length,s=0,r=t.length;s<r;s++)o?this._off(t[s]):this._off(t[s],e,i)}else delete this._events;return this},_on:function(t,e,i,n){"function"!=typeof e?console.warn("wrong listener type: "+typeof e):!1===this._listens(t,e,i)&&(e={fn:e,ctx:i=i===this?void 0:i},n&&(e.once=!0),this._events=this._events||{},this._events[t]=this._events[t]||[],this._events[t].push(e))},_off:function(t,e,i){var n,o,s;if(this._events&&(n=this._events[t]))if(1===arguments.length){if(this._firingCount)for(o=0,s=n.length;o<s;o++)n[o].fn=u;delete this._events[t]}else"function"!=typeof e?console.warn("wrong listener type: "+typeof e):!1!==(e=this._listens(Line truncated
//# sourceMappingURL=leaflet.js.map
+646 -87
View File
@@ -54,6 +54,7 @@ function loadSettings(settings) {
currentSettings = settings;
// General
document.getElementById("callsign").value = settings.callsign;
document.getElementById("tacticalCallsign").value = settings.tacticalCallsign;
document.getElementById("beacon.comment").value = settings.beacon.comment;
document.getElementById("beacon.path").value = settings.beacon.path;
document.getElementById("beacon.symbol").value = settings.beacon.symbol;
@@ -95,6 +96,7 @@ function loadSettings(settings) {
networksContainer.appendChild(networkElement);
networkCount++;
});
document.getElementById("startupDelay").value = settings.other.startupDelay;
// APRS-IS
document.getElementById("aprs_is.active").checked = settings.aprs_is.active;
@@ -116,9 +118,13 @@ 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;
document.getElementById("beacon.beaconFreq").value = settings.beacon.beaconFreq;
BeaconingViaRFCheckbox.checked = settings.beacon.sendViaRF;
BeaconingFrequency.disabled = !BeaconingViaRFCheckbox.checked;
document.getElementById("beacon.statusActive").checked = settings.beacon.statusActive;
document.getElementById("beacon.statusPacket").value = settings.beacon.statusPacket;
@@ -126,7 +132,7 @@ function loadSettings(settings) {
StatusPacket.disabled = !StatusCheckbox.checked;
document.getElementById("beacon.gpsActive").checked = settings.beacon.gpsActive;
document.getElementById("beacon.gpsAmbiguity").checked = settings.beacon.gpsAmbiguity;
document.getElementById("beacon.ambiguityLevel").value = settings.beacon.ambiguityLevel;
// Black List
document.getElementById("blacklist").value = settings.blacklist;
@@ -134,16 +140,34 @@ function loadSettings(settings) {
// Digi
document.getElementById("digi.mode").value = settings.digi.mode;
document.getElementById("digi.ecoMode").value = settings.digi.ecoMode;
document.getElementById("digi.backupDigiMode").checked = settings.digi.backupDigiMode;
// LoRa
document.getElementById("lora.txFreq").value = settings.lora.txFreq;
document.getElementById("lora.rxFreq").value = settings.lora.rxFreq;
document.getElementById("lora.txActive").checked = settings.lora.txActive;
document.getElementById("lora.rxActive").checked = settings.lora.rxActive;
document.getElementById("lora.spreadingFactor").value = settings.lora.spreadingFactor;
document.getElementById("lora.signalBandwidth").value = settings.lora.signalBandwidth;
document.getElementById("lora.codingRate4").value = settings.lora.codingRate4;
document.getElementById("lora.rxFreq").value = settings.lora.rxFreq;
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.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;
@@ -160,13 +184,16 @@ function loadSettings(settings) {
MonitorInternalSleepVoltage.disabled = !MonitorInternalVoltageCheckbox.checked;
document.getElementById("battery.sendExternalVoltage").checked = settings.battery.sendExternalVoltage;
document.getElementById("battery.useExternalI2CSensor").checked = settings.battery.useExternalI2CSensor;
document.getElementById("battery.externalVoltagePin").value = settings.battery.externalVoltagePin;
document.getElementById("battery.voltageDividerR1").value = settings.battery.voltageDividerR1.toFixed(1);
document.getElementById("battery.voltageDividerR2").value = settings.battery.voltageDividerR2.toFixed(1);
SendExternalVoltageCheckbox.checked = settings.battery.sendExternalVoltage;
ExternalVoltagePin.disabled = !SendExternalVoltageCheckbox.checked;
ExternalVoltageDividerR1.disabled = !SendExternalVoltageCheckbox.checked;
ExternalVoltageDividerR2.disabled = !SendExternalVoltageCheckbox.checked;
UseExternalI2CSensorCheckbox.disabled = !SendExternalVoltageCheckbox.checked;
ExternalVoltagePin.disabled = !SendExternalVoltageCheckbox.checked || UseExternalI2CSensorCheckbox.checked;
ExternalVoltageDividerR1.disabled = !SendExternalVoltageCheckbox.checked || UseExternalI2CSensorCheckbox.checked;
ExternalVoltageDividerR2.disabled = !SendExternalVoltageCheckbox.checked || UseExternalI2CSensorCheckbox.checked;
document.getElementById("battery.monitorExternalVoltage").checked = settings.battery.monitorExternalVoltage;
document.getElementById("battery.externalSleepVoltage").value = settings.battery.externalSleepVoltage.toFixed(1);
@@ -182,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;
@@ -192,12 +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
@@ -207,12 +236,14 @@ function loadSettings(settings) {
document.getElementById("mqtt.username").value = settings.mqtt.username;
document.getElementById("mqtt.password").value = settings.mqtt.password;
document.getElementById("mqtt.port").value = settings.mqtt.port;
MqttCheckbox.checked = settings.mqtt.active;
MqttServer.disabled = !MqttCheckbox.check;
MqttTopic.disabled = !MqttCheckbox.check;
MqttUsername.disabled = !MqttCheckbox.check;
MqttPassword.disabled = !MqttCheckbox.check;
MqttPort.disabled = !MqttCheckbox.check;
document.getElementById("mqtt.beaconOverMqtt").checked = settings.mqtt.beaconOverMqtt;
MqttCheckbox.checked = settings.mqtt.active;
MqttServer.disabled = !MqttCheckbox.checked;
MqttTopic.disabled = !MqttCheckbox.checked;
MqttUsername.disabled = !MqttCheckbox.checked;
MqttPassword.disabled = !MqttCheckbox.checked;
MqttPort.disabled = !MqttCheckbox.checked;
MqttBeaconOverMqtt.disabled = !MqttCheckbox.checked;
// Reboot
document.getElementById("other.rebootMode").checked = settings.other.rebootMode;
@@ -221,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;
@@ -241,13 +274,12 @@ function loadSettings(settings) {
document.getElementById("remoteManagement.rfOnly").checked = settings.remoteManagement.rfOnly;
// NTP
document.getElementById("ntp.server").value = settings.ntp.server;
document.getElementById("ntp.gmtCorrection").value = settings.ntp.gmtCorrection;
// Experimental
document.getElementById("other.backupDigiMode").checked = settings.other.backupDigiMode;
clampOutOfRangeInputs();
updateImage();
refreshSpeedStandard();
}
function showToast(message) {
@@ -299,6 +331,12 @@ function updateImage() {
}
}
// Beaconing Switches
const BeaconingViaRFCheckbox = document.querySelector('input[name="beacon.sendViaRF"]');
const BeaconingFrequency = document.querySelector('select[name="beacon.beaconFreq"]');
BeaconingViaRFCheckbox.addEventListener("change", function() {
BeaconingFrequency.disabled = !this.checked;
});
// Status Switch
const StatusCheckbox = document.querySelector('input[name="beacon.statusActive"]');
@@ -343,13 +381,21 @@ MonitorExternalVoltageCheckbox.addEventListener("change", function () {
MonitorExternalSleepVoltage.disabled = !this.checked;
});
const SendExternalVoltageCheckbox = document.querySelector('input[name="battery.sendExternalVoltage"]');
const UseExternalI2CSensorCheckbox = document.querySelector('input[name="battery.useExternalI2CSensor"]');
const ExternalVoltagePin = document.querySelector('input[name="battery.externalVoltagePin"]');
const ExternalVoltageDividerR1 = document.querySelector('input[name="battery.voltageDividerR1"]');
const ExternalVoltageDividerR2 = document.querySelector('input[name="battery.voltageDividerR2"]');
SendExternalVoltageCheckbox.addEventListener("change", function () {
ExternalVoltagePin.disabled = !this.checked;
ExternalVoltageDividerR1.disabled = !this.checked;
ExternalVoltageDividerR2.disabled = !this.checked;
UseExternalI2CSensorCheckbox.disabled = !this.checked;
ExternalVoltagePin.disabled = !this.checked || UseExternalI2CSensorCheckbox.checked;
ExternalVoltageDividerR1.disabled = !this.checked || UseExternalI2CSensorCheckbox.checked;
ExternalVoltageDividerR2.disabled = !this.checked || UseExternalI2CSensorCheckbox.checked;
});
UseExternalI2CSensorCheckbox.addEventListener("change", function () {
ExternalVoltagePin.disabled = this.checked;
ExternalVoltageDividerR1.disabled = this.checked;
ExternalVoltageDividerR2.disabled = this.checked;
});
// Telemetry Switches
@@ -361,6 +407,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"]');
@@ -379,12 +455,14 @@ const MqttTopic = document.querySelector('input[name="mqtt.topic
const MqttUsername = document.querySelector('input[name="mqtt.username"]');
const MqttPassword = document.querySelector('input[name="mqtt.password"]');
const MqttPort = document.querySelector('input[name="mqtt.port"]');
const MqttBeaconOverMqtt = document.querySelector('input[name="mqtt.beaconOverMqtt"]');
MqttCheckbox.addEventListener("change", function () {
MqttServer.disabled = !this.checked;
MqttTopic.disabled = !this.checked;
MqttUsername.disabled = !this.checked;
MqttPassword.disabled = !this.checked;
MqttPort.disabled = !this.checked;
MqttBeaconOverMqtt.disabled = !this.checked;
});
// Reboot Switches
@@ -403,6 +481,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");
@@ -451,65 +540,6 @@ document
updateImage();
});
const speedStandards = {
300: [125, 5, 12],
244: [125, 6, 12],
209: [125, 7, 12],
183: [125, 8, 12],
610: [125, 8, 10],
1200: [125, 7, 9],
};
function refreshSpeedStandard() {
const bw = Number(document.getElementById("lora.signalBandwidth").value);
const cr4 = Number(document.getElementById("lora.codingRate4").value);
const sf = Number(document.getElementById("lora.spreadingFactor").value);
let found = false;
for (const speed in speedStandards) {
const standard = speedStandards[speed];
if (standard[0] !== bw / 1000) continue;
if (standard[1] !== cr4) continue;
if (standard[2] !== sf) continue;
document.getElementById("action.speed").value = speed;
found = true;
break;
}
if (!found) {
document.getElementById("action.speed").value = "";
}
}
document
.getElementById("lora.signalBandwidth")
.addEventListener("focusout", refreshSpeedStandard);
document
.getElementById("lora.codingRate4")
.addEventListener("focusout", refreshSpeedStandard);
document
.getElementById("lora.spreadingFactor")
.addEventListener("focusout", refreshSpeedStandard);
document.getElementById("action.speed").addEventListener("change", function () {
const speed = document.getElementById("action.speed").value;
if (speed !== "") {
const value = speedStandards[Number(speed)];
const bw = value[0];
const cr4 = value[1];
const sf = value[2];
document.getElementById("lora.signalBandwidth").value = bw * 1000;
document.getElementById("lora.codingRate4").value = cr4;
document.getElementById("lora.spreadingFactor").value = sf;
}
});
const form = document.querySelector("form");
@@ -575,7 +605,7 @@ function loadReceivedPackets(packets) {
packets.forEach((packet) => {
const element = document.createElement("tr");
element.innerHTML = `
<td>${packet.rxTime}</td>
<td>${packet.packet}</td>
@@ -608,8 +638,537 @@ 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 &mdash; 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: "&copy; 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);
}
/* ------------------------------------------------- keeping the form submittable
* A control that fails HTML5 validation blocks the whole form, and when it sits in
* a panel that is not the visible one the browser cannot show its bubble either:
* pressing Save then does nothing at all -- no message, no request, no clue. Two
* guards against that: pull stored values back inside the allowed range on load,
* and, if validation still fails, open the section holding the offending control
* and show what is wrong.
*/
function clampOutOfRangeInputs() {
const fixed = [];
document.querySelectorAll("#configuration input[type=number]").forEach((el) => {
if (el.disabled || el.value === "") return;
const value = parseFloat(el.value);
if (isNaN(value)) return;
const min = el.min !== "" ? parseFloat(el.min) : null;
const max = el.max !== "" ? parseFloat(el.max) : null;
let clamped = value;
if (max !== null && value > max) clamped = max;
if (min !== null && value < min) clamped = min;
if (clamped !== value) {
el.value = clamped;
fixed.push((el.name || el.id) + ": " + value + " &rarr; " + clamped);
}
});
if (fixed.length) {
showToast("Stored values outside the allowed range, adjusted:<br>" + fixed.join("<br>")
+ "<br><small>Save to keep them.</small>");
}
}
let invalidFieldReported = false;
form.addEventListener("invalid", function (event) {
const el = event.target;
if (!invalidFieldReported) { // only the first one: the rest would pile up toasts
invalidFieldReported = true;
setTimeout(() => { invalidFieldReported = false; }, 500);
const section = el.closest(".panel-section");
if (section) {
const link = document.querySelector('.side-link[data-target="' + section.id + '"]');
if (link) {
link.click(); // reuses the sidebar switcher (also shows the config view)
} else {
document.querySelectorAll(".panel-section").forEach((s) => s.classList.remove("active"));
section.classList.add("active");
document.getElementById("configuration").classList.remove("d-none");
}
}
showToast("<b>" + (el.name || el.id) + "</b>: " + (el.validationMessage || "invalid value"));
setTimeout(() => {
el.focus();
el.scrollIntoView({ block: "center", behavior: "smooth" });
}, 0);
}
}, true);
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 KiB

+6 -6
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
+7 -8
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -28,10 +28,9 @@ namespace APRS_IS_Utils {
void connect();
void checkStatus();
String checkForStartingBytes(const String& packet);
String buildPacketToUpload(const String& packet);
bool processReceivedLoRaMessage(const String& sender, const String& packet, bool thirdParty);
bool processReceivedLoRaMessage(const String& sender, const String& packet);
void processLoRaPacket(const String& packet);
String buildPacketToTx(const String& aprsisPacket, uint8_t packetType);
+6 -6
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
+36 -23
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -32,6 +32,7 @@ public:
class WiFi_Auto_AP {
public:
bool enabled; // Enable Auto AP
String password;
int timeout;
};
@@ -44,13 +45,14 @@ public:
int interval;
String overlay;
String symbol;
String path;
bool sendViaRF;
String path;
bool sendViaAPRSIS;
bool gpsActive;
bool gpsAmbiguity;
bool sendViaRF;
int beaconFreq;
bool statusActive;
String statusPacket;
bool gpsActive;
int ambiguityLevel;
};
class APRS_IS {
@@ -68,18 +70,23 @@ class DIGI {
public:
int mode;
int ecoMode; // 0 = Not Active | 1 = Ultra EcoMode | 2 = Not Active (WiFi OFF, Serial ON)
bool backupDigiMode;
};
class LoraModule {
public:
long txFreq;
long rxFreq;
bool txActive;
bool rxActive;
int spreadingFactor;
long signalBandwidth;
int codingRate4;
long rxFreq;
int rxSpreadingFactor;
int rxCodingRate4;
long rxSignalBandwidth;
bool txActive;
long txFreq;
int txSpreadingFactor;
int txCodingRate4;
long txSignalBandwidth;
int power;
bool cadActive;
};
class Display {
@@ -98,6 +105,7 @@ public:
int externalVoltagePin;
bool monitorExternalVoltage;
float externalSleepVoltage;
bool useExternalI2CSensor;
float voltageDividerR1;
float voltageDividerR2;
bool sendVoltageAsTelemetry;
@@ -123,6 +131,8 @@ public:
bool enableServer;
bool enableSerial;
bool acceptOwn;
bool aprsBridgeActive;
bool kissProtocol; // true = KISS, false = TNC2 -- applies to both Serial and TCP server
};
class OTA {
@@ -140,6 +150,7 @@ public:
class NTP {
public:
String server;
float gmtCorrection;
};
@@ -157,15 +168,17 @@ public:
String username;
String password;
int port;
bool beaconOverMqtt;
};
class Configuration {
public:
String callsign;
String tacticalCallsign;
int rememberStationTime;
bool backupDigiMode;
bool rebootMode;
int rebootModeTime;
int startupDelay;
String personalNote;
String blacklist;
std::vector<WiFi_AP> wifiAPs;
@@ -178,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 init();
void writeFile();
Configuration();
void setup();
void setDefaultValues();
bool writeFile();
private:
bool readFile();
String _filePath;
};
#endif
#endif
+8 -9
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -24,10 +24,9 @@
namespace DIGI_Utils {
String buildPacket(const String& path, const String& packet, bool thirdParty, bool crossFreq);
String generateDigipeatedPacket(const String& packet, bool thirdParty);
String buildPacket(const String& path, const String& packet, bool crossFreq);
String generateDigipeatedPacket(const String& packet);
void processLoRaPacket(const String& packet);
void checkEcoMode();
}
+7 -6
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -21,6 +21,7 @@
#include <Arduino.h>
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
void displaySetup();
+8 -13
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -23,15 +23,10 @@
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);
void generateBeaconFirstPart();
void generateBeacons();
String decodeEncodedGPS(const String& packet);
String getReceivedGPS(const String& packet);
String getDistanceAndComment(const String& packet);
String buildDistanceAndComment(float latitude, float longitude, const String& comment);
void setup();
void getData();
+6 -6
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
+6 -6
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
+6 -6
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
+47
View File
@@ -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
+6 -6
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
+102
View File
@@ -0,0 +1,102 @@
/* 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);
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;
// Check if any network interface was started (lwip TCP/IP stack is initialized)
bool hasActiveInterface() const;
};
+7 -7
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -24,7 +24,7 @@
namespace NTP_Utils {
void setup();
bool setup();
void update();
String getFormatedTime();
+6 -6
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
+7 -7
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -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
+6 -6
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
+8 -7
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -21,8 +21,9 @@
#include <Arduino.h>
namespace SLEEP_Utils {
void setup();
void checkWakeUpFlag();
void startSleeping();
+8 -15
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -23,12 +23,6 @@
#include <Arduino.h>
struct Packet25SegBuffer {
uint32_t receivedTime;
String station;
String payload;
};
struct LastHeardStation {
uint32_t lastHeardTime;
String station;
@@ -43,11 +37,10 @@ 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);
void addToOutputPacketBuffer(const String& packet, bool flag = false);
}
+11 -7
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -19,12 +19,16 @@
#ifndef SYSLOG_H_
#define SYSLOG_H_
#include <APRSPacketLib.h>
#include <Arduino.h>
namespace SYSLOG_Utils {
void log(const uint8_t type ,const String& packet, const int rssi, const float snr, const int freqError);
void logLoRaRx(APRSPacket& aprsPacket, const String& packet, const int rssi, const float snr, const int freqError);
void logAPRSISTx(const String& packet);
void logLoRaTx(const String& packet);
void logCRCError(const String& packet, const int rssi, const float snr, const int freqError);
void setup();
}
+8 -7
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -27,7 +27,8 @@ namespace TELEMETRY_Utils {
void sendEquationsUnitsParameters();
String generateEncodedTelemetryBytes(float value, bool counterBytes, byte telemetryType);
String generateEncodedTelemetry();
void checkEUPInterval();
}
#endif
+8 -8
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -27,8 +27,8 @@ namespace TNC_Utils {
void setup();
void loop();
void sendToClients(const String& packet);
void sendToSerial(const String& packet);
void sendToClients(const String& packet, bool stripBytes = false);
void sendToSerial(const String& packet, bool stripBytes = false);
}
+12 -10
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -19,9 +19,9 @@
#ifndef UTILS_H_
#define UTILS_H_
#include <APRSPacketLib.h>
#include <Arduino.h>
class ReceivedPacket {
public:
String rxTime;
@@ -35,17 +35,19 @@ namespace Utils {
void processStatus();
String getLocalIP();
void setupDisplay();
void activeStations();
void showActiveStations();
void checkBeaconInterval();
void checkDisplayInterval();
void validateFreqs();
void typeOfPacket(const String& packet, const uint8_t packetType);
void updateLoRaPacketDisplayInfo(APRSPacket& aprsPacket, const uint8_t packetType);
void updateAPRSISPacketDisplayInfo(const String& packet);
void print(const String& text);
void println(const String& text);
void checkRebootMode();
void checkRebootTime();
void checkSleepByLowBatteryVoltage(uint8_t mode);
bool checkValidCallsign(const String& callsign);
bool callsignIsValid(const String& callsign);
void startupDelay();
}
+6 -6
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
+6 -7
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -27,7 +27,6 @@ namespace WIFI_Utils {
void checkWiFi();
void startAutoAP();
void startWiFi();
void checkAutoAPTimeout();
void setup();
}
+7 -6
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -20,6 +20,7 @@
#define WX_UTILS_H_
#include <Adafruit_Sensor.h>
#include <Adafruit_AHTX0.h>
#include <Adafruit_BME280.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_BME680.h>
+8 -3
View File
@@ -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
+15 -13
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -26,7 +26,7 @@
#ifdef HAS_A7670
#define TINY_GSM_MODEM_SIM7600 //The AT instruction of A7670 is compatible with SIM7600
#define TINY_GSM_MODEM_SIM7600 //The AT instruction of A7670 is compatible with SIM7600
#define TINY_GSM_RX_BUFFER 1024 // Set RX buffer to 1Kb
#define SerialAT Serial1
#include <TinyGsmClient.h>
@@ -115,6 +115,8 @@
if (SerialAT.available()) {
String response = SerialAT.readString();
//Serial.println(response); // DEBUG of Modem AT message
int responseOKIndex = response.indexOf("OK");
int callsignIndex = ATMessage.indexOf(Config.callsign);
if(response.indexOf("verified") >= 0) {
Serial.println("Logged! (User Validated)\n");
displayShow(firstLine, "Connecting APRS-IS...", "---> Logged!", " ", 1000);
@@ -122,7 +124,7 @@
validAT = true;
i = 1;
delayATMessage = 0;
} else if (ATMessage == "AT+NETOPEN" && response.indexOf("OK") >= 0) {
} else if (ATMessage == "AT+NETOPEN" && responseOKIndex >= 0) {
Serial.println("Port Open!");
displayShow(firstLine, "Opening Port...", "---> Port Open", " ", 0);
validAT = true;
@@ -145,14 +147,14 @@
validAT = true;
i = 1;
delayATMessage = 0;
} else if (ATMessage.indexOf(Config.callsign) >= 3 && !modemLoggedToAPRSIS && response.indexOf("OK") >= 0 && !stationBeacon) { // login info
} else if (callsignIndex >= 3 && !modemLoggedToAPRSIS && responseOKIndex >= 0 && !stationBeacon) { // login info
validAT = true;
delayATMessage = 0;
} else if (ATMessage.indexOf(Config.callsign) == 0 && !beaconSent && response.indexOf("OK") >= 0 && !stationBeacon) { // self beacon or querys
} else if (callsignIndex == 0 && !beaconSent && responseOKIndex >= 0 && !stationBeacon) { // self beacon or querys
validAT = true;
i = 1;
delayATMessage = 0;
} else if (stationBeacon && response.indexOf("OK") >= 0) { //upload others beacons
} else if (stationBeacon && responseOKIndex >= 0) { //upload others beacons
validAT = true;
i = 1;
delayATMessage = 0;
@@ -200,7 +202,7 @@
if (beaconBytesSent) {
Serial.print(".");
beaconSent = checkATResponse(packet);
}
}
if (!beaconSent) {
Serial.println("------------------------------------> UPLOAD FAILED!!!");
} else {
@@ -221,5 +223,5 @@
delay(1);
}
}
#endif
+35 -33
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -24,7 +24,7 @@
██║ ██║ ██║██╔══██╗██╔══██║ ██╔══██║██╔═══╝ ██╔══██╗╚════██║
███████╗╚██████╔╝██║ ██║██║ ██║ ██║ ██║██║ ██║ ██║███████║
╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝
██╗ ██████╗ █████╗ ████████╗███████╗
██║██╔════╝ ██╔══██╗╚══██╔══╝██╔════╝
██║██║ ███╗███████║ ██║ █████╗
@@ -33,7 +33,7 @@
╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
Ricardo Guzman - CA2RXU
Ricardo Guzman - CA2RXU
https://github.com/richonguzman/LoRa_APRS_iGate
(donations : http://paypal.me/richonguzman)
___________________________________________________________________*/
@@ -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 = "2025-09-09";
String versionNumber = "3.1";
String versionDate = "2026-09-23";
String versionNumber = "4.0.1";
Configuration Config;
WiFiClient aprsIsClient;
WiFiClient mqttClient;
@@ -79,14 +80,12 @@ 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;
bool backUpDigiMode = false;
bool backupDigiMode = false;
bool modemLoggedToAPRSIS = false;
#ifdef HAS_EPAPER
@@ -97,23 +96,24 @@ bool modemLoggedToAPRSIS = false;
std::vector<ReceivedPacket> receivedPackets;
String firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine;
//#define STARTUP_DELAY 5 //min
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();
Utils::validateFreqs();
GPS_Utils::setup();
STATION_Utils::loadBlacklistAndManagers();
#ifdef STARTUP_DELAY // (TEST) just to wait for WiFi init of Routers
displayShow("", " STARTUP DELAY ...", "", "", 0);
delay(STARTUP_DELAY * 60 * 1000);
#endif
Utils::startupDelay();
SLEEP_Utils::setup();
WIFI_Utils::setup();
NTP_Utils::setup();
@@ -138,13 +138,13 @@ void loop() {
Utils::checkSleepByLowBatteryVoltage(1);
SLEEP_Utils::startSleeping();
} else {
WIFI_Utils::checkAutoAPTimeout();
networkManager->loop();
if (isUpdatingOTA) {
ElegantOTA.loop();
return; // Don't process IGate and Digi during OTA update
}
#ifdef HAS_GPS
if (Config.beacon.gpsActive) {
if (millis() - gpsSatelliteTime > 5000) {
@@ -167,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();
@@ -180,7 +183,7 @@ void loop() {
Utils::checkDisplayInterval();
Utils::checkBeaconInterval();
APRS_IS_Utils::checkStatus(); // Need that to update display, maybe split this and send APRSIS status to display func?
String packet = "";
@@ -193,14 +196,13 @@ 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
}
if (Config.tnc.enableServer) TNC_Utils::sendToClients(packet); // Send received packet to TNC KISS
if (Config.tnc.enableSerial) TNC_Utils::sendToSerial(packet); // Send received packet to Serial KISS
if (Config.mqtt.active) MQTT_Utils::sendToMqtt(packet); // Send received packet to MQTT
if (Config.tnc.enableServer) TNC_Utils::sendToClients(packet, true); // Send received packet to TNC KISS
if (Config.tnc.enableSerial) TNC_Utils::sendToSerial(packet, true); // Send received packet to Serial KISS
if (Config.mqtt.active) MQTT_Utils::sendToMqtt(packet); // Send received packet to MQTT
}
if (Config.aprs_is.active) APRS_IS_Utils::listenAPRSIS(); // listen received packet from APRSIS
@@ -223,4 +225,4 @@ void loop() {
Utils::checkRebootTime();
Utils::checkSleepByLowBatteryVoltage(1);
}
}
}
+161 -150
View File
@@ -1,23 +1,25 @@
/* 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 <APRSPacketLib.h>
#include <WiFiClient.h>
#include "configuration.h"
#include "network_manager.h"
#include "aprs_is_utils.h"
#include "station_utils.h"
#include "board_pinout.h"
@@ -25,11 +27,14 @@
#include "query_utils.h"
#include "A7670_utils.h"
#include "digi_utils.h"
#include "tnc_utils.h"
#include "display.h"
#include "utils.h"
extern Configuration Config;
extern APRSPacket lastAprsPacket;
extern NetworkManager *networkManager;
extern WiFiClient aprsIsClient;
extern uint32_t lastScreenOn;
extern String firstLine;
@@ -40,11 +45,12 @@ extern String fifthLine;
extern String sixthLine;
extern String seventhLine;
extern bool modemLoggedToAPRSIS;
extern bool backUpDigiMode;
extern bool backupDigiMode;
extern String versionNumber;
uint32_t lastRxTime = millis();
bool passcodeValid = false;
uint32_t lastServerCheck = 0;
#ifdef HAS_A7670
extern bool stationBeacon;
@@ -89,10 +95,10 @@ 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) {
if (backupDigiMode || Config.digi.ecoMode == 1 || Config.digi.ecoMode == 2) {
wifiState = "--";
} else {
wifiState = "AP";
@@ -130,37 +136,32 @@ namespace APRS_IS_Utils {
secondLine += aprsisState;
}
String checkForStartingBytes(const String& packet) {
if (packet.indexOf("\x3c\xff\x01") != -1) {
return packet.substring(0, packet.indexOf("\x3c\xff\x01"));
} else {
return 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 += APRSPacketLib::checkForStartingBytes(packet.substring(colonIndex));
return packetToUpload;
}
bool processReceivedLoRaMessage(const String& sender, const String& packet, bool thirdParty) {
bool processReceivedLoRaMessage(const String& sender, const String& packet) {
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);
String addToBuffer = Config.callsign;
addToBuffer += ">APLRG1";
if (!thirdParty) addToBuffer += ",RFONLY";
if (lastAprsPacket.header == "") addToBuffer += ",RFONLY";
if (Config.beacon.path != "") {
addToBuffer += ",";
addToBuffer += Config.beacon.path;
@@ -176,56 +177,50 @@ 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) {
displayToggle(true);
}
STATION_Utils::addToOutputPacketBuffer(QUERY_Utils::process(receivedMessage, sender, false, thirdParty));
if (!Config.display.alwaysOn && Config.display.timeout != 0) displayToggle(true);
STATION_Utils::addToOutputPacketBuffer(QUERY_Utils::process(receivedMessage, sender, false, lastAprsPacket.header != ""));
lastScreenOn = millis();
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, "Callsign = " + sender, "TYPE --> QUERY", 0);
return true;
}
else {
} else {
return false;
}
}
void processLoRaPacket(const String& packet) {
if (passcodeValid && (aprsIsClient.connected() || modemLoggedToAPRSIS)) {
if (packet.indexOf("NOGATE") == -1 && packet.indexOf("RFONLY") == -1) {
int firstColonIndex = packet.indexOf(":");
if (firstColonIndex > 5 && firstColonIndex < (packet.length() - 1) && packet[firstColonIndex + 1] != '}' && packet.indexOf("TCPIP") == -1) {
const String& Sender = packet.substring(3, packet.indexOf(">"));
if (Sender != Config.callsign && Utils::checkValidCallsign(Sender)) {
STATION_Utils::updateLastHeard(Sender);
Utils::typeOfPacket(packet.substring(3), 0); // LoRa-APRS
const String& AddresseeAndMessage = packet.substring(packet.indexOf("::") + 2);
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
Addressee.trim();
if (lastAprsPacket.path.indexOf("NOGATE") == -1 && lastAprsPacket.path.indexOf("RFONLY") == -1) {
if (lastAprsPacket.header == "" && lastAprsPacket.path.indexOf("TCPIP") == -1) {
if (lastAprsPacket.sender != Config.callsign && Utils::callsignIsValid(lastAprsPacket.sender)) {
STATION_Utils::updateLastHeard(lastAprsPacket.sender);
Utils::updateLoRaPacketDisplayInfo(lastAprsPacket, 0); // LoRa-APRS
bool queryMessage = false;
if (packet.indexOf("::") > 10 && Addressee == Config.callsign) { // its a message for me!
queryMessage = processReceivedLoRaMessage(Sender, checkForStartingBytes(AddresseeAndMessage), false);
if (lastAprsPacket.type == 1 && lastAprsPacket.addressee == Config.callsign) { // its a message for me!
String AddresseeAndMessage = lastAprsPacket.addressee + ":" + lastAprsPacket.payload;
queryMessage = processReceivedLoRaMessage(lastAprsPacket.sender, APRSPacketLib::checkForStartingBytes(AddresseeAndMessage));
}
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& aprsPacketToUpload = buildPacketToUpload(packet);
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
}
lastScreenOn = millis();
#ifdef HAS_A7670
stationBeacon = true;
A7670_Utils::uploadToAPRSIS(aprsPacketToUpload);
stationBeacon = false;
#else
upload(aprsPacketToUpload);
#endif
Utils::println("(Uploaded to APRS-IS)");
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
}
}
}
@@ -235,37 +230,36 @@ namespace APRS_IS_Utils {
String buildPacketToTx(const String& aprsisPacket, uint8_t packetType) {
String packet = aprsisPacket;
packet.trim();
String outputPacket = Config.callsign;
outputPacket += ">APLRG1";
if (Config.beacon.path != "") {
outputPacket += ",";
outputPacket += Config.beacon.path;
}
String outputPacket = APRSPacketLib::generateBasePacket(Config.callsign, "APLRG1", Config.beacon.path);
outputPacket += ":}";
outputPacket += packet.substring(0, packet.indexOf(",")); // Callsign>Tocall
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(":'"));
}
@@ -277,94 +271,112 @@ namespace APRS_IS_Utils {
return outputPacket;
}
void processAckMessage(const String& sender, const String& message) {
String ackPacket = Config.callsign;
ackPacket += ">APLRG1,TCPIP,qAC::";
String senderCallsign = sender;
for (int i = sender.length(); i < 9; i++) {
senderCallsign += ' ';
}
ackPacket += senderCallsign;
ackPacket += ":";
String ackMessage = "ack";
ackMessage += message.substring(message.indexOf("{") + 1);
ackMessage.trim();
ackPacket += ackMessage;
#ifdef HAS_A7670
A7670_Utils::uploadToAPRSIS(ackPacket);
#else
upload(ackPacket);
#endif
}
void processAPRSISPacket(const String& packet) {
uint32_t currentTime = millis();
if (!passcodeValid && packet.indexOf(Config.callsign) != -1) {
if (packet.indexOf("unverified") != -1 ) {
Serial.println("\n****APRS PASSCODE NOT VALID****\n");
displayShow(firstLine, "", " APRS PASSCODE", " NOT VALID !!!", "", "", "", 0);
while (1) {};
displayShow(firstLine, "", " APRS PASSCODE", " NOT VALID !!!", "", "", "", 3000);
aprsIsClient.stop();
Config.aprs_is.active = false;
} else if (packet.indexOf("verified") != -1 ) {
if (Config.digi.backupDigiMode) lastServerCheck = currentTime;
passcodeValid = true;
}
}
if (passcodeValid && !packet.startsWith("#")) {
if (Config.aprs_is.messagesToRF && packet.indexOf("::") > 0) {
String Sender = packet.substring(0, packet.indexOf(">"));
const String& AddresseeAndMessage = packet.substring(packet.indexOf("::") + 2);
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
Addressee.trim();
if (Addressee == Config.callsign) { // its for me!
String receivedMessage;
if (AddresseeAndMessage.indexOf("{") > 0) { // ack?
String ackMessage = "ack";
ackMessage += AddresseeAndMessage.substring(AddresseeAndMessage.indexOf("{") + 1);
ackMessage.trim();
delay(4000);
for (int i = Sender.length(); i < 9; i++) {
Sender += ' ';
if (passcodeValid) {
if (packet.startsWith("#")) {
if (Config.digi.backupDigiMode) lastServerCheck = currentTime;
} else {
int doubleColonIndex = packet.indexOf("::");
if (Config.aprs_is.messagesToRF && doubleColonIndex > 0) {
String Sender = packet.substring(0, packet.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;
int curlyBraceIndex = AddresseeAndMessage.indexOf("{");
if (curlyBraceIndex > 0) { // ack?
processAckMessage(Sender, AddresseeAndMessage);
receivedMessage = AddresseeAndMessage.substring(colonIndex + 1, curlyBraceIndex);
} else {
receivedMessage = AddresseeAndMessage.substring(colonIndex + 1);
}
String ackPacket = Config.callsign;
ackPacket += ">APLRG1,TCPIP,qAC::";
ackPacket += Sender;
ackPacket += ":";
ackPacket += ackMessage;
#ifdef HAS_A7670
A7670_Utils::uploadToAPRSIS(ackPacket);
#else
upload(ackPacket);
#endif
receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":") + 1, AddresseeAndMessage.indexOf("{"));
} else {
receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":") + 1);
}
if (receivedMessage.indexOf("?") == 0) {
Utils::println("Rx Query (APRS-IS) : " + packet);
Sender.trim();
String queryAnswer = QUERY_Utils::process(receivedMessage, Sender, true, false);
//Serial.println("---> QUERY Answer : " + queryAnswer.substring(0,queryAnswer.indexOf("\n")));
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
if (receivedMessage.indexOf("?") == 0) {
Utils::println("Rx Query (APRS-IS) : " + packet);
String queryAnswer = QUERY_Utils::process(receivedMessage, Sender, true, false);
//Serial.println("---> QUERY Answer : " + queryAnswer.substring(0,queryAnswer.indexOf("\n")));
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
}
lastScreenOn = currentTime;
#ifdef HAS_A7670
A7670_Utils::uploadToAPRSIS(queryAnswer);
#else
upload(queryAnswer);
#endif
SYSLOG_Utils::logAPRSISTx(queryAnswer);
fifthLine = "APRS-IS ----> APRS-IS";
sixthLine = Config.callsign;
for (int j = sixthLine.length();j < 9;j++) {
sixthLine += " ";
}
sixthLine += "> ";
sixthLine += Sender;
seventhLine = "QUERY = ";
seventhLine += receivedMessage;
}
lastScreenOn = millis();
delay(500);
#ifdef HAS_A7670
A7670_Utils::uploadToAPRSIS(queryAnswer);
#else
upload(queryAnswer);
#endif
SYSLOG_Utils::log(2, queryAnswer, 0, 0.0, 0); // APRSIS TX
fifthLine = "APRS-IS ----> APRS-IS";
sixthLine = Config.callsign;
for (int j = sixthLine.length();j < 9;j++) {
sixthLine += " ";
}
sixthLine += "> ";
sixthLine += Sender;
seventhLine = "QUERY = ";
seventhLine += receivedMessage;
}
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
} else {
Utils::print("Rx Message (APRS-IS): " + packet);
if (STATION_Utils::wasHeard(Addressee) && packet.indexOf("EQNS.") == -1 && packet.indexOf("UNIT.") == -1 && packet.indexOf("PARM.") == -1) {
STATION_Utils::addToOutputPacketBuffer(buildPacketToTx(packet, 1));
displayToggle(true);
lastScreenOn = millis();
Utils::typeOfPacket(packet, 1); // APRS-LoRa
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
} else {
Utils::print("Rx Message (APRS-IS): " + packet);
if (STATION_Utils::wasHeard(Addressee) && packet.indexOf("EQNS.") == -1 && packet.indexOf("UNIT.") == -1 && packet.indexOf("PARM.") == -1) {
STATION_Utils::addToOutputPacketBuffer(buildPacketToTx(packet, 1));
displayToggle(true);
lastScreenOn = currentTime;
Utils::updateAPRSISPacketDisplayInfo(packet); // APRS-LoRa
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
}
}
} else if (Config.aprs_is.objectsToRF && packet.indexOf(":;") > 0) {
Utils::print("Rx Object (APRS-IS) : " + packet);
if (STATION_Utils::checkObjectTime(packet)) {
STATION_Utils::addToOutputPacketBuffer(buildPacketToTx(packet, 5));
displayToggle(true);
lastScreenOn = currentTime;
Utils::updateAPRSISPacketDisplayInfo(packet); // APRS-LoRa
Serial.println();
} else {
Serial.println(" ---> Rejected (Time): No Tx");
}
}
} else if (Config.aprs_is.objectsToRF && packet.indexOf(":;") > 0) {
Utils::print("Rx Object (APRS-IS) : " + packet);
if (STATION_Utils::checkObjectTime(packet)) {
STATION_Utils::addToOutputPacketBuffer(buildPacketToTx(packet, 5));
displayToggle(true);
lastScreenOn = millis();
Utils::typeOfPacket(packet, 1); // APRS-LoRa
Serial.println();
} else {
Serial.println(" ---> Rejected (Time): No Tx");
if (Config.tnc.aprsBridgeActive) {
if (Config.tnc.enableServer) TNC_Utils::sendToClients(packet); // Send received packet to TNC KISS
if (Config.tnc.enableSerial) TNC_Utils::sendToSerial(packet); // Send received packet to Serial KISS
}
}
}
@@ -379,14 +391,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();
@@ -394,4 +405,4 @@ namespace APRS_IS_Utils {
}
}
}
}
+91 -39
View File
@@ -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 <Arduino.h>
#include <Adafruit_INA219.h>
#include "battery_utils.h"
#include "configuration.h"
#include "board_pinout.h"
@@ -37,12 +37,16 @@ float multiplyCorrection = 0.035;
float voltageDividerTransformation = 0.0;
uint8_t externalI2CSensorAddress = 0x00;
int externalI2CSensorType = 0; // 0 = None | 1 = INA219
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
@@ -98,6 +102,30 @@ namespace BATTERY_Utils {
#endif
}
void getI2CVoltageSensorAddress() {
uint8_t err, addr;
for(addr = 1; addr < 0x7F; addr++) {
#ifdef SENSOR_I2C_BUS
SENSOR_I2C_BUS.beginTransmission(addr);
err = SENSOR_I2C_BUS.endTransmission();
#else
Wire.beginTransmission(addr);
err = Wire.endTransmission();
#endif
delay(5);
if (err == 0) {
if (addr == 0x40) { // INA219
externalI2CSensorAddress = addr;
}
}
}
}
bool detectINA219(uint8_t addr) {
ina219 = Adafruit_INA219(addr);
return ina219.begin();
}
void setup() {
if ((Config.battery.sendExternalVoltage || Config.battery.monitorExternalVoltage) && Config.battery.voltageDividerR2 != 0) voltageDividerTransformation = (Config.battery.voltageDividerR1 + Config.battery.voltageDividerR2) / Config.battery.voltageDividerR2;
@@ -107,9 +135,17 @@ namespace BATTERY_Utils {
adcCalibration();
}
#endif
getI2CVoltageSensorAddress();
if (externalI2CSensorAddress != 0x00) {
if (detectINA219(externalI2CSensorAddress)) {
Serial.println("INA219 sensor found");
externalI2CSensorType = 1; // INA219
}
}
}
float checkInternalVoltage() {
float checkInternalVoltage() {
#if defined(HAS_AXP192) || defined(HAS_AXP2101)
if(POWER_Utils::isBatteryConnected()) {
return POWER_Utils::getBatteryVoltage();
@@ -117,14 +153,14 @@ namespace BATTERY_Utils {
return 0.0;
}
#else
#ifdef ADC_CTRL
#ifdef ADC_CTRL_PIN
POWER_Utils::adc_ctrl_ON();
#endif
int sampleSum = 0;
for (int i = 0; i < adcReadings; i++) {
#if defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_LoRa_915) || defined(ESP32_DIY_1W_LoRa) || defined(ESP32_DIY_1W_LoRa_915)
#if defined(ESP32_DIY_LoRa_SX1278) || defined(ESP32_DIY_LoRa_915_SX1276) || defined(ESP32_DIY_1W_LoRa_E22_400M30S) || defined(ESP32_DIY_1W_LoRa_915_E22_900M30S) || defined(ESP32_DIY_1W_LoRa_E32_400M30S)
sampleSum = 0;
#else
#ifdef HAS_ADC_CALIBRATION
@@ -141,10 +177,10 @@ namespace BATTERY_Utils {
#endif
#endif
#endif
delay(3);
delay(3);
}
#ifdef ADC_CTRL
#ifdef ADC_CTRL_PIN
POWER_Utils::adc_ctrl_OFF();
#ifdef HELTEC_WP_V1
@@ -167,6 +203,10 @@ namespace BATTERY_Utils {
#ifdef LIGHTGATEWAY_PLUS_1_0
double inputDivider = (1.0 / (560.0 + 100.0)) * 100.0; // The voltage divider is a 560k + 100k resistor in series, 100k on the low side.
return (((sampleSum/adcReadings) * adcReadingTransformation) / inputDivider) + 0.41;
#elif defined(RAK3112)
return (sampleSum/adcReadings) * adcReadingTransformation * (3 * 1.73 * 1.187 / 3.3); // multiplier from MeshCore RAK3112Board.h
#elif defined(RAK3312)
return (sampleSum/adcReadings) * adcReadingTransformation * (3 * 1.73 * 1.187 / 3.3) * (1.667 / 2.11); // RAK3112 value scaled by Meshtastic RAK3312/RAK3112 ratio
#else
return (2 * (sampleSum/adcReadings) * adcReadingTransformation) + voltageDividerCorrection; // raw voltage without mapping
#endif
@@ -177,37 +217,49 @@ namespace BATTERY_Utils {
}
float checkExternalVoltage() {
int sample;
int sampleSum = 0;
for (int i = 0; i < 100; i++) {
#ifdef HAS_ADC_CALIBRATION
if (calibrationEnable){
sample = adc1_get_raw(ExternalVoltage_ADC_Channel);
} else {
if (externalI2CSensorType == 0) {
int sample;
int sampleSum = 0;
for (int i = 0; i < 100; i++) {
#ifdef HAS_ADC_CALIBRATION
if (calibrationEnable){
sample = adc1_get_raw(ExternalVoltage_ADC_Channel);
} else {
sample = analogRead(Config.battery.externalVoltagePin);
}
#else
sample = analogRead(Config.battery.externalVoltagePin);
#endif
sampleSum += sample;
delayMicroseconds(50);
}
float extVoltage;
#ifdef HAS_ADC_CALIBRATION
if (calibrationEnable) {
extVoltage = esp_adc_cal_raw_to_voltage(sampleSum / 100.0, &adc_chars) * voltageDividerTransformation; // in mV
extVoltage /= 1000.0;
} else {
extVoltage = ((((sampleSum/100.0)* adcReadingTransformation) + readingCorrection) * voltageDividerTransformation) - multiplyCorrection;
}
#else
sample = analogRead(Config.battery.externalVoltagePin);
extVoltage = ((((sampleSum/100.0)* adcReadingTransformation) + readingCorrection) * voltageDividerTransformation) - multiplyCorrection;
#endif
sampleSum += sample;
delayMicroseconds(50);
}
float extVoltage;
#ifdef HAS_ADC_CALIBRATION
if (calibrationEnable){
extVoltage = esp_adc_cal_raw_to_voltage(sampleSum / 100, &adc_chars) * voltageDividerTransformation; // in mV
extVoltage /= 1000;
} else {
extVoltage = ((((sampleSum/100)* adcReadingTransformation) + readingCorrection) * voltageDividerTransformation) - multiplyCorrection;
return extVoltage; // raw voltage without mapping
// return mapVoltage(voltage, 5.05, 6.32, 4.5, 5.5); // mapped voltage
} else if (externalI2CSensorType == 1) { // INA219
int sampleSum = 0;
for (int i = 0; i < 100; i++) {
sampleSum += ina219.getBusVoltage_V() * 1000.0;
delayMicroseconds(50);
}
#else
extVoltage = ((((sampleSum/100)* adcReadingTransformation) + readingCorrection) * voltageDividerTransformation) - multiplyCorrection;
#endif
return extVoltage; // raw voltage without mapping
// return mapVoltage(voltage, 5.05, 6.32, 4.5, 5.5); // mapped voltage
float extVoltage = sampleSum/100.0;
return extVoltage/1000.0;
} else {
return 0.0;
}
}
void startupBatteryHealth() {
+314 -147
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -26,139 +26,182 @@
bool shouldSleepStop = true;
void Configuration::writeFile() {
Serial.println("Saving config...");
bool Configuration::writeFile() {
Serial.println("Saving configuration...");
StaticJsonDocument<2560> data;
JsonDocument data;
File configFile = SPIFFS.open("/igate_conf.json", "w");
if (wifiAPs[0].ssid != "") { // We don't want to save Auto AP empty SSID
for (int i = 0; i < wifiAPs.size(); i++) {
data["wifi"]["AP"][i]["ssid"] = wifiAPs[i].ssid;
data["wifi"]["AP"][i]["password"] = wifiAPs[i].password;
}
if (!configFile) {
Serial.println("Error: Could not open config file for writing");
return false;
}
try {
data["wifi"]["autoAP"]["password"] = wifiAutoAP.password;
data["wifi"]["autoAP"]["timeout"] = wifiAutoAP.timeout;
if (wifiAPs[0].ssid != "") { // We don't want to save Auto AP empty SSID
for (int i = 0; i < wifiAPs.size(); i++) {
data["wifi"]["AP"][i]["ssid"] = wifiAPs[i].ssid;
data["wifi"]["AP"][i]["password"] = wifiAPs[i].password;
}
}
data["callsign"] = callsign;
data["other"]["startupDelay"] = startupDelay;
data["aprs_is"]["active"] = aprs_is.active;
data["aprs_is"]["passcode"] = aprs_is.passcode;
data["aprs_is"]["server"] = aprs_is.server;
data["aprs_is"]["port"] = aprs_is.port;
data["aprs_is"]["filter"] = aprs_is.filter;
data["aprs_is"]["messagesToRF"] = aprs_is.messagesToRF;
data["aprs_is"]["objectsToRF"] = aprs_is.objectsToRF;
data["wifi"]["autoAP"]["enabled"] = wifiAutoAP.enabled;
data["wifi"]["autoAP"]["password"] = wifiAutoAP.password;
data["wifi"]["autoAP"]["timeout"] = wifiAutoAP.timeout;
data["beacon"]["comment"] = beacon.comment;
data["beacon"]["interval"] = beacon.interval;
data["beacon"]["latitude"] = beacon.latitude;
data["beacon"]["longitude"] = beacon.longitude;
data["beacon"]["overlay"] = beacon.overlay;
data["beacon"]["symbol"] = beacon.symbol;
data["beacon"]["sendViaAPRSIS"] = beacon.sendViaAPRSIS;
data["beacon"]["sendViaRF"] = beacon.sendViaRF;
data["beacon"]["path"] = beacon.path;
callsign.trim();
data["callsign"] = callsign;
tacticalCallsign.trim();
data["tacticalCallsign"] = tacticalCallsign;
data["beacon"]["statusActive"] = beacon.statusActive;
data["beacon"]["statusPacket"] = beacon.statusPacket;
data["aprs_is"]["active"] = aprs_is.active;
data["aprs_is"]["passcode"] = aprs_is.passcode;
data["aprs_is"]["server"] = aprs_is.server;
data["aprs_is"]["port"] = aprs_is.port;
data["aprs_is"]["filter"] = aprs_is.filter;
data["aprs_is"]["messagesToRF"] = aprs_is.messagesToRF;
data["aprs_is"]["objectsToRF"] = aprs_is.objectsToRF;
data["beacon"]["gpsActive"] = beacon.gpsActive;
data["beacon"]["gpsAmbiguity"] = beacon.gpsAmbiguity;
data["beacon"]["comment"] = beacon.comment;
data["beacon"]["interval"] = beacon.interval;
data["beacon"]["latitude"] = beacon.latitude;
data["beacon"]["longitude"] = beacon.longitude;
data["beacon"]["overlay"] = beacon.overlay;
data["beacon"]["symbol"] = beacon.symbol;
data["beacon"]["path"] = beacon.path;
data["personalNote"] = personalNote;
data["beacon"]["sendViaAPRSIS"] = beacon.sendViaAPRSIS;
data["beacon"]["sendViaRF"] = beacon.sendViaRF;
data["beacon"]["beaconFreq"] = beacon.beaconFreq;
data["blacklist"] = blacklist;
data["beacon"]["statusActive"] = beacon.statusActive;
data["beacon"]["statusPacket"] = beacon.statusPacket;
data["digi"]["mode"] = digi.mode;
data["digi"]["ecoMode"] = digi.ecoMode;
#if defined(HAS_A7670)
if (digi.ecoMode == 1) data["digi"]["ecoMode"] = 2;
#endif
data["beacon"]["gpsActive"] = beacon.gpsActive;
#if !defined(HAS_GPS)
data["beacon"]["gpsActive"] = false;
#endif
data["beacon"]["ambiguityLevel"] = beacon.ambiguityLevel;
data["lora"]["rxFreq"] = loramodule.rxFreq;
data["lora"]["txFreq"] = loramodule.txFreq;
data["lora"]["spreadingFactor"] = loramodule.spreadingFactor;
data["lora"]["signalBandwidth"] = loramodule.signalBandwidth;
data["lora"]["codingRate4"] = loramodule.codingRate4;
data["lora"]["power"] = loramodule.power;
data["lora"]["txActive"] = loramodule.txActive;
data["lora"]["rxActive"] = loramodule.rxActive;
data["personalNote"] = personalNote;
data["display"]["alwaysOn"] = display.alwaysOn;
data["display"]["timeout"] = display.timeout;
data["display"]["turn180"] = display.turn180;
data["blacklist"] = blacklist;
data["battery"]["sendInternalVoltage"] = battery.sendInternalVoltage;
data["battery"]["monitorInternalVoltage"] = battery.monitorInternalVoltage;
data["battery"]["internalSleepVoltage"] = battery.internalSleepVoltage;
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
data["digi"]["backupDigiMode"] = digi.backupDigiMode;
data["battery"]["sendExternalVoltage"] = battery.sendExternalVoltage;
data["battery"]["externalVoltagePin"] = battery.externalVoltagePin;
data["battery"]["monitorExternalVoltage"] = battery.monitorExternalVoltage;
data["battery"]["externalSleepVoltage"] = battery.externalSleepVoltage;
data["battery"]["voltageDividerR1"] = battery.voltageDividerR1;
data["battery"]["voltageDividerR2"] = battery.voltageDividerR2;
data["lora"]["rxActive"] = loramodule.rxActive;
data["lora"]["rxFreq"] = loramodule.rxFreq;
data["lora"]["rxCodingRate4"] = loramodule.rxCodingRate4;
data["lora"]["rxSignalBandwidth"] = loramodule.rxSignalBandwidth;
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"]["cadActive"] = loramodule.cadActive;
data["battery"]["sendVoltageAsTelemetry"] = battery.sendVoltageAsTelemetry;
int rxSpreadingFactor = loramodule.rxSpreadingFactor;
int txSpreadingFactor = loramodule.txSpreadingFactor;
#if defined(HAS_SX1276) || defined(HAS_SX1278)
const int minSF = 6, maxSF = 12;
#endif
#if defined(HAS_SX1262) || defined(HAS_SX1268)
const int minSF = 5, maxSF = 12;
#endif
#if defined(HAS_LLCC68)
const int minSF = 5, maxSF = 11;
#endif
data["wxsensor"]["active"] = wxsensor.active;
data["wxsensor"]["heightCorrection"] = wxsensor.heightCorrection;
data["wxsensor"]["temperatureCorrection"] = wxsensor.temperatureCorrection;
rxSpreadingFactor = (rxSpreadingFactor < minSF) ? minSF : (rxSpreadingFactor > maxSF) ? maxSF : rxSpreadingFactor;
txSpreadingFactor = (txSpreadingFactor < minSF) ? minSF : (txSpreadingFactor > maxSF) ? maxSF : txSpreadingFactor;
data["syslog"]["active"] = syslog.active;
data["syslog"]["server"] = syslog.server;
data["syslog"]["port"] = syslog.port;
data["syslog"]["logBeaconOverTCPIP"] = syslog.logBeaconOverTCPIP;
data["lora"]["rxSpreadingFactor"] = rxSpreadingFactor;
data["lora"]["txSpreadingFactor"] = txSpreadingFactor;
data["tnc"]["enableServer"] = tnc.enableServer;
data["tnc"]["enableSerial"] = tnc.enableSerial;
data["tnc"]["acceptOwn"] = tnc.acceptOwn;
data["display"]["alwaysOn"] = display.alwaysOn;
data["display"]["timeout"] = display.timeout;
data["display"]["turn180"] = display.turn180;
data["mqtt"]["active"] = mqtt.active;
data["mqtt"]["server"] = mqtt.server;
data["mqtt"]["topic"] = mqtt.topic;
data["mqtt"]["username"] = mqtt.username;
data["mqtt"]["password"] = mqtt.password;
data["mqtt"]["port"] = mqtt.port;
data["battery"]["sendInternalVoltage"] = battery.sendInternalVoltage;
data["battery"]["monitorInternalVoltage"] = battery.monitorInternalVoltage;
data["battery"]["internalSleepVoltage"] = battery.internalSleepVoltage;
data["ota"]["username"] = ota.username;
data["ota"]["password"] = ota.password;
data["battery"]["sendExternalVoltage"] = battery.sendExternalVoltage;
data["battery"]["monitorExternalVoltage"] = battery.monitorExternalVoltage;
data["battery"]["externalSleepVoltage"] = battery.externalSleepVoltage;
data["battery"]["useExternalI2CSensor"] = battery.useExternalI2CSensor;
data["battery"]["voltageDividerR1"] = battery.voltageDividerR1;
data["battery"]["voltageDividerR2"] = battery.voltageDividerR2;
data["battery"]["externalVoltagePin"] = battery.externalVoltagePin;
data["webadmin"]["active"] = webadmin.active;
data["webadmin"]["username"] = webadmin.username;
data["webadmin"]["password"] = webadmin.password;
data["battery"]["sendVoltageAsTelemetry"] = battery.sendVoltageAsTelemetry;
data["remoteManagement"]["managers"] = remoteManagement.managers;
data["remoteManagement"]["rfOnly"] = remoteManagement.rfOnly;
data["wxsensor"]["active"] = wxsensor.active;
data["wxsensor"]["heightCorrection"] = wxsensor.heightCorrection;
data["wxsensor"]["temperatureCorrection"] = wxsensor.temperatureCorrection;
data["ntp"]["gmtCorrection"] = ntp.gmtCorrection;
data["syslog"]["active"] = syslog.active;
data["syslog"]["server"] = syslog.server;
data["syslog"]["port"] = syslog.port;
data["syslog"]["logBeaconOverTCPIP"] = syslog.logBeaconOverTCPIP;
data["other"]["rebootMode"] = rebootMode;
data["other"]["rebootModeTime"] = rebootModeTime;
data["tnc"]["enableServer"] = tnc.enableServer;
data["tnc"]["enableSerial"] = tnc.enableSerial;
data["tnc"]["acceptOwn"] = tnc.acceptOwn;
data["tnc"]["aprsBridgeActive"] = tnc.aprsBridgeActive;
data["tnc"]["kissProtocol"] = tnc.kissProtocol;
data["other"]["rememberStationTime"] = rememberStationTime;
data["mqtt"]["active"] = mqtt.active;
data["mqtt"]["server"] = mqtt.server;
data["mqtt"]["topic"] = mqtt.topic;
data["mqtt"]["username"] = mqtt.username;
data["mqtt"]["password"] = mqtt.password;
data["mqtt"]["port"] = mqtt.port;
data["mqtt"]["beaconOverMqtt"] = mqtt.beaconOverMqtt;
data["other"]["backupDigiMode"] = backupDigiMode;
data["ota"]["username"] = ota.username;
data["ota"]["password"] = ota.password;
serializeJson(data, configFile);
data["webadmin"]["active"] = webadmin.active;
data["webadmin"]["username"] = webadmin.username;
data["webadmin"]["password"] = webadmin.password;
configFile.close();
data["remoteManagement"]["managers"] = remoteManagement.managers;
data["remoteManagement"]["rfOnly"] = remoteManagement.rfOnly;
Serial.println("Config saved");
delay(200);
data["ntp"]["server"] = ntp.server;
data["ntp"]["gmtCorrection"] = ntp.gmtCorrection;
data["other"]["rebootMode"] = rebootMode;
data["other"]["rebootModeTime"] = rebootModeTime;
data["other"]["rememberStationTime"] = rememberStationTime;
serializeJson(data, configFile);
configFile.close();
return true;
} catch (...) {
Serial.println("Error: Exception occurred while saving config");
configFile.close();
return false;
}
}
bool Configuration::readFile() {
Serial.println("Reading config..");
File configFile = SPIFFS.open("/igate_conf.json", "r");
if (configFile) {
StaticJsonDocument<2560> data;
bool needsRewrite = false;
JsonDocument data;
DeserializationError error = deserializeJson(data, configFile);
if (error) {
Serial.println("Failed to read file, using default configuration");
@@ -173,12 +216,28 @@ bool Configuration::readFile() {
wifiAPs.push_back(wifiap);
}
if (data["other"]["startupDelay"].isNull()) needsRewrite = true;
startupDelay = data["other"]["startupDelay"] | 0;
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["callsign"].isNull()) needsRewrite = true;
callsign = data["callsign"] | "NOCALL-10";
if (data["tacticalCallsign"].isNull()) needsRewrite = true;
tacticalCallsign = data["tacticalCallsign"] | "";
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";
@@ -187,7 +246,20 @@ bool Configuration::readFile() {
aprs_is.messagesToRF = data["aprs_is"]["messagesToRF"] | false;
aprs_is.objectsToRF = data["aprs_is"]["objectsToRF"] | false;
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";
@@ -197,90 +269,160 @@ bool Configuration::readFile() {
beacon.path = data["beacon"]["path"] | "WIDE1-1";
beacon.sendViaAPRSIS = data["beacon"]["sendViaAPRSIS"] | false;
beacon.sendViaRF = data["beacon"]["sendViaRF"] | false;
beacon.beaconFreq = data["beacon"]["beaconFreq"] | 1;
beacon.statusActive = data["beacon"]["statusActive"] | false;
beacon.statusPacket = data["beacon"]["statusPacket"] | "";
beacon.gpsActive = data["beacon"]["gpsActive"] | false;
beacon.gpsAmbiguity = data["beacon"]["gpsAmbiguity"] | false;
beacon.ambiguityLevel = data["beacon"]["ambiguityLevel"] | 0;
if (data["personalNote"].isNull()) needsRewrite = true;
personalNote = data["personalNote"] | "personal note here";
if (data["blacklist"].isNull()) needsRewrite = true;
blacklist = data["blacklist"] | "station callsign";
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;
#if defined(HAS_A7670)
if (digi.ecoMode == 1) digi.ecoMode = 2;
#endif
digi.backupDigiMode = data["digi"]["backupDigiMode"] | false;
loramodule.txFreq = data["lora"]["txFreq"] | 433775000;
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.spreadingFactor = data["lora"]["spreadingFactor"] | 12;
loramodule.signalBandwidth = data["lora"]["signalBandwidth"] | 125000;
loramodule.codingRate4 = data["lora"]["codingRate4"] | 5;
loramodule.power = data["lora"]["power"] | 20;
loramodule.rxSpreadingFactor = data["lora"]["rxSpreadingFactor"] | 12;
loramodule.rxCodingRate4 = data["lora"]["rxCodingRate4"] | 5;
loramodule.rxSignalBandwidth = data["lora"]["rxSignalBandwidth"] | 125000;
loramodule.txActive = data["lora"]["txActive"] | false;
loramodule.rxActive = data["lora"]["rxActive"] | 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;
loramodule.cadActive = data["lora"]["cadActive"] | true;
display.alwaysOn = data["display"]["alwaysOn"] | true;
if (data["display"]["alwaysOn"].isNull() ||
data["display"]["timeout"].isNull() ||
data["display"]["turn180"].isNull()) needsRewrite = true;
#ifdef HAS_EPAPER
display.alwaysOn = true;
#else
display.alwaysOn = data["display"]["alwaysOn"] | true;
#endif
display.timeout = data["display"]["timeout"] | 4;
display.turn180 = data["display"]["turn180"] | false;
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;
battery.sendExternalVoltage = data["battery"]["sendExternalVoltage"] | false;
battery.externalVoltagePin = data["battery"]["externalVoltagePin"] | 34;
battery.monitorExternalVoltage = data["battery"]["monitorExternalVoltage"] | false;
battery.externalSleepVoltage = data["battery"]["externalSleepVoltage"] | 10.9;
battery.useExternalI2CSensor = data["battery"]["useExternalI2CSensor"] | false;
battery.voltageDividerR1 = data["battery"]["voltageDividerR1"] | 100.0;
battery.voltageDividerR2 = data["battery"]["voltageDividerR2"] | 27.0;
battery.externalVoltagePin = data["battery"]["externalVoltagePin"] | 34;
battery.sendVoltageAsTelemetry = data["battery"]["sendVoltageAsTelemetry"] | false;
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"]["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"]["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"]["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";
mqtt.username = data["mqtt"]["username"] | "";
mqtt.password = data["mqtt"]["password"] | "";
mqtt.port = data["mqtt"]["port"] | 1883;
mqtt.beaconOverMqtt = data["mqtt"]["beaconOverMqtt"] | false;
if (data["ota"]["username"].isNull() ||
data["ota"]["password"].isNull()) needsRewrite = true;
ota.username = data["ota"]["username"] | "";
ota.password = data["ota"]["password"] | "";
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"]["managers"].isNull() ||
data["remoteManagement"]["rfOnly"].isNull()) needsRewrite = true;
remoteManagement.managers = data["remoteManagement"]["managers"] | "";
remoteManagement.rfOnly = data["remoteManagement"]["rfOnly"] | 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"]["rebootMode"].isNull() ||
data["other"]["rebootModeTime"].isNull()) needsRewrite = true;
rebootMode = data["other"]["rebootMode"] | false;
rebootModeTime = data["other"]["rebootModeTime"] | 6;
if (data["other"]["rememberStationTime"].isNull()) needsRewrite = true;
rememberStationTime = data["other"]["rememberStationTime"] | 30;
backupDigiMode = data["other"]["backupDigiMode"] | false;
if (wifiAPs.size() == 0) { // If we don't have any WiFi's from config we need to add "empty" SSID for AUTO AP
WiFi_AP wifiap;
wifiap.ssid = "";
@@ -289,6 +431,13 @@ bool Configuration::readFile() {
wifiAPs.push_back(wifiap);
}
configFile.close();
if (needsRewrite) {
Serial.println("Config JSON incomplete, rewriting...");
writeFile();
delay(1000);
ESP.restart();
}
Serial.println("Config read successfuly");
return true;
} else {
@@ -296,8 +445,8 @@ bool Configuration::readFile() {
return false;
}
}
void Configuration::init() {
void Configuration::setDefaultValues() {
WiFi_AP wifiap;
wifiap.ssid = "";
@@ -305,10 +454,14 @@ void Configuration::init() {
wifiAPs.push_back(wifiap);
startupDelay = 0;
wifiAutoAP.enabled = true;
wifiAutoAP.password = "1234567890";
wifiAutoAP.timeout = 10;
callsign = "N0CALL-10";
tacticalCallsign = "";
aprs_is.active = false;
aprs_is.passcode = "XYZVW";
@@ -324,46 +477,54 @@ void Configuration::init() {
beacon.interval = 15;
beacon.overlay = "L";
beacon.symbol = "a";
beacon.sendViaAPRSIS = true;
beacon.sendViaRF = false;
beacon.path = "WIDE1-1";
beacon.sendViaAPRSIS = true;
beacon.sendViaRF = false;
beacon.beaconFreq = 1;
beacon.statusActive = false;
beacon.statusPacket = "";
beacon.statusPacket = "";
beacon.gpsActive = false;
beacon.gpsAmbiguity = false;
beacon.ambiguityLevel = 0;
personalNote = "";
personalNote = "";
blacklist = "";
digi.mode = 0;
digi.ecoMode = 0;
digi.backupDigiMode = false;
loramodule.txFreq = 433775000;
loramodule.rxFreq = 433775000;
loramodule.spreadingFactor = 12;
loramodule.signalBandwidth = 125000;
loramodule.codingRate4 = 5;
loramodule.power = 20;
loramodule.txActive = false;
loramodule.rxActive = true;
loramodule.rxFreq = 433775000;
loramodule.rxSpreadingFactor = 12;
loramodule.rxCodingRate4 = 5;
loramodule.rxSignalBandwidth = 125000;
loramodule.txActive = false;
loramodule.txFreq = 433775000;
loramodule.txSpreadingFactor = 12;
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;
battery.sendExternalVoltage = false;
battery.externalVoltagePin = 34;
battery.monitorExternalVoltage = false;
battery.externalSleepVoltage = 10.9;
battery.useExternalI2CSensor = false;
battery.voltageDividerR1 = 100.0;
battery.voltageDividerR2 = 27.0;
battery.externalVoltagePin = 34;
battery.sendVoltageAsTelemetry = false;
@@ -379,6 +540,8 @@ void Configuration::init() {
tnc.enableServer = false;
tnc.enableSerial = false;
tnc.acceptOwn = false;
tnc.aprsBridgeActive = false;
tnc.kissProtocol = true;
mqtt.active = false;
mqtt.server = "";
@@ -386,6 +549,7 @@ void Configuration::init() {
mqtt.username = "";
mqtt.password = "";
mqtt.port = 1883;
mqtt.beaconOverMqtt = false;
ota.username = "";
ota.password = "";
@@ -397,6 +561,7 @@ void Configuration::init() {
remoteManagement.managers = "";
remoteManagement.rfOnly = true;
ntp.server = "pool.ntp.org";
ntp.gmtCorrection = 0.0;
rebootMode = false;
@@ -404,23 +569,25 @@ void Configuration::init() {
rememberStationTime = 30;
backupDigiMode = false;
Serial.println("All is Written!");
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) {
init();
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);
ESP.restart();
}
+161 -118
View File
@@ -1,21 +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 <APRSPacketLib.h>
#include <WiFi.h>
#include "configuration.h"
#include "station_utils.h"
@@ -23,13 +24,13 @@
#include "digi_utils.h"
#include "wifi_utils.h"
#include "lora_utils.h"
#include "gps_utils.h"
#include "display.h"
#include "utils.h"
extern Configuration Config;
extern uint32_t lastScreenOn;
extern APRSPacket lastAprsPacket;
extern String iGateBeaconPacket;
extern String firstLine;
extern String secondLine;
@@ -38,142 +39,184 @@ extern String fourthLine;
extern String fifthLine;
extern String sixthLine;
extern String seventhLine;
extern bool backUpDigiMode;
extern bool backupDigiMode;
namespace DIGI_Utils {
String buildPacket(const String& path, const String& packet, bool thirdParty, bool crossFreq) {
if (!crossFreq) {
String packetToRepeat = packet.substring(0, packet.indexOf(",") + 1);
String tempPath = path;
if (path.indexOf("WIDE1-1") != -1 && (Config.digi.mode == 2 || Config.digi.mode == 3)) {
tempPath.replace("WIDE1-1", Config.callsign + "*");
} else if (path.indexOf("WIDE2-") != -1 && Config.digi.mode == 3) {
if (path.indexOf(",WIDE1*") != -1) {
tempPath.remove(path.indexOf(",WIDE1*"), 7);
}
if (path.indexOf("*") != -1) {
tempPath.remove(path.indexOf("*"), 1);
}
if (path.indexOf("WIDE2-1") != -1) {
tempPath.replace("WIDE2-1", Config.callsign + "*");
} else if (path.indexOf("WIDE2-2") != -1) {
tempPath.replace("WIDE2-2", Config.callsign + "*,WIDE2-1");
} else {
return "";
}
String cleanPath(const String& path) {
String result;
unsigned int start = 0;
while (true) {
int delim = path.indexOf(',', start);
int end = (delim == -1) ? path.length() : delim;
String token = path.substring(start, end);
if (token != "WIDE1*" && token != "WIDE2*") {
if (result.length() > 0) result += ",";
result += token;
}
packetToRepeat += tempPath;
if (thirdParty) {
packetToRepeat += APRS_IS_Utils::checkForStartingBytes(packet.substring(packet.indexOf(":}")));
} else {
packetToRepeat += APRS_IS_Utils::checkForStartingBytes(packet.substring(packet.indexOf(":")));
}
return packetToRepeat;
} else { // CrossFreq Digipeater
String suffix = thirdParty ? ":}" : ":";
String packetToRepeat = packet.substring(0, packet.indexOf(suffix));
String terms[] = {",WIDE1*", ",WIDE2*", "*"};
for (String term : terms) {
int index = packetToRepeat.indexOf(term);
if (index != -1) {
packetToRepeat.remove(index, term.length());
}
}
packetToRepeat += ",";
packetToRepeat += Config.callsign;
packetToRepeat += "*";
packetToRepeat += APRS_IS_Utils::checkForStartingBytes(packet.substring(packet.indexOf(suffix)));
return packetToRepeat;
if (delim == -1) break;
start = delim + 1;
}
return result;
}
String generateDigipeatedPacket(const String& packet, bool thirdParty){
static int pathTokenIndex(const String& path, const String& token) {
unsigned int start = 0;
while (start < path.length()) {
int end = path.indexOf(",", start);
if (end == -1) end = path.length();
if (path.substring(start, end) == token) return start;
start = end + 1;
}
return -1;
}
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 crossFreq) {
String stationCallsign = (Config.tacticalCallsign == "" ? Config.callsign : Config.tacticalCallsign);
String suffix = (lastAprsPacket.header != "") ? ":}" : ":";
int suffixIndex = packet.indexOf(suffix);
String packetToRepeat;
if (!crossFreq) {
int digiMode = Config.digi.mode;
String tempPath = path;
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 == 2) { // WIDE2-n Digipeater
tempPath = cleanPath(path);
int idx = pathTokenIndex(tempPath, "WIDE2-1");
if (idx != -1) {
tempPath = tempPath.substring(0, idx) + stationCallsign + "*" + tempPath.substring(idx + 7);
} else {
idx = pathTokenIndex(tempPath, "WIDE2-2");
if (idx == -1) return "";
tempPath = tempPath.substring(0, idx) + stationCallsign + "*,WIDE2-1" + tempPath.substring(idx + 7);
}
} 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 = cleanPath(packet.substring(0, suffixIndex));
if (packetToRepeat.indexOf(stationCallsign) != -1) return ""; // stationCallsign shouldn't be in path
packetToRepeat += ",";
packetToRepeat += stationCallsign;
packetToRepeat += "*";
}
packetToRepeat += APRSPacketLib::checkForStartingBytes(packet.substring(suffixIndex));
return packetToRepeat;
}
String generateDigipeatedPacket(const String& packet){
String temp;
if (thirdParty) { // only header is used
if (lastAprsPacket.header != "") { // thirdparty : only header is used
const String& header = packet.substring(0, packet.indexOf(":}"));
temp = header.substring(header.indexOf(">") + 1);
} else {
temp = packet.substring(packet.indexOf(">") + 1, packet.indexOf(":"));
}
if (temp.indexOf(",") > 2) { // checks for path
const String& path = temp.substring(temp.indexOf(",") + 1); // after tocall
if (Config.digi.mode == 2 || backUpDigiMode) {
if (path.indexOf("WIDE1-1") != - 1) {
return buildPacket(path, packet, thirdParty, false);
} else if (path.indexOf("WIDE1-1") == -1 && (abs(Config.loramodule.txFreq - Config.loramodule.rxFreq) >= 125000)) { // CrossFreq Digi
return buildPacket(path, packet, thirdParty, true);
} else {
return "";
}
} else if (Config.digi.mode == 3) {
if (path.indexOf("WIDE1-1") != -1 || path.indexOf("WIDE2-") != -1) {
int wide1Index = path.indexOf("WIDE1-1");
int wide2Index = path.indexOf("WIDE2-");
int commaIndex = temp.indexOf(",");
int digiMode = Config.digi.mode;
bool crossFreq = abs(Config.loramodule.txFreq - Config.loramodule.rxFreq) >= 125000; // CrossFreq Digi
// WIDE1-1 && WIDE2-n / only WIDE1-1 / only WIDE2-n
if ((wide1Index != -1 && wide2Index != -1 && wide1Index < wide2Index) || (wide1Index != -1 && wide2Index == -1) || (wide1Index == -1 && wide2Index != -1)) {
return buildPacket(path, packet, thirdParty, false);
}
return "";
} else if (path.indexOf("WIDE1-1") == -1 && path.indexOf("WIDE2-") == -1 && (abs(Config.loramodule.txFreq - Config.loramodule.rxFreq) >= 125000)) { // CrossFreq Digi
return buildPacket(path, packet, thirdParty, true);
} else {
return "";
if (commaIndex > 2) { // "path" found
const String& path = temp.substring(commaIndex + 1);
if (digiMode == 1 || backupDigiMode) {
bool hasWide = path.indexOf("WIDE1-1") != -1;
if (hasWide || crossFreq) {
return buildPacket(path, packet, !hasWide);
}
} else {
return "";
}
} else if (temp.indexOf(",") == -1 && (Config.digi.mode == 2 || backUpDigiMode || Config.digi.mode == 3) && (abs(Config.loramodule.txFreq - Config.loramodule.rxFreq) >= 125000)) {
return buildPacket("", packet, thirdParty, true);
} else {
if (digiMode == 2) {
int wide1Index = path.indexOf("WIDE1-1");
int wide2Index = path.indexOf("WIDE2-");
bool hasWide1 = wide1Index != -1;
bool hasWide2 = wide2Index != -1;
if (hasWide1 && hasWide2 && wide2Index < wide1Index) return ""; // check that WIDE1 before WIDE2
if (hasWide1 || hasWide2) return buildPacket(path, packet, false); // regular APRS with WIDEn-N
if (crossFreq) return buildPacket(path, packet, true); // CrossFreq (without WIDE)
return "";
}
if (digiMode == 3) {
String stationCallsign = (Config.tacticalCallsign == "" ? Config.callsign : Config.tacticalCallsign);
bool containsOwnCall = path.indexOf(stationCallsign) != -1;
if (containsOwnCall) return buildPacket(path, packet, false);
return "";
}
return "";
}
if (commaIndex == -1 && (digiMode == 1 || backupDigiMode || digiMode == 2) && crossFreq) return buildPacket("", packet, true); // no "path" but is CrossFreq Digi
return "";
}
void processLoRaPacket(const String& packet) {
if (packet.indexOf("NOGATE") == -1) {
bool thirdPartyPacket = false;
String temp, Sender;
int firstColonIndex = packet.indexOf(":");
if (firstColonIndex > 5 && firstColonIndex < (packet.length() - 1) && packet[firstColonIndex + 1] == '}' && packet.indexOf("TCPIP") > 0) { // 3rd Party
thirdPartyPacket = true;
temp = packet.substring(packet.indexOf(":}") + 2);
Sender = temp.substring(0, temp.indexOf(">"));
} else {
temp = packet.substring(3);
Sender = packet.substring(3, packet.indexOf(">"));
}
if (Sender != Config.callsign) { // Avoid listening to own packets
if (!thirdPartyPacket && !Utils::checkValidCallsign(Sender)) {
return;
}
if (STATION_Utils::check25SegBuffer(Sender, temp.substring(temp.indexOf(":") + 2))) {
STATION_Utils::updateLastHeard(Sender);
Utils::typeOfPacket(temp, 2); // Digi
bool queryMessage = false;
if (temp.indexOf("::") > 10) { // it's a message
String AddresseeAndMessage = temp.substring(temp.indexOf("::") + 2);
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
Addressee.trim();
if (Addressee == Config.callsign) { // it's a message for me!
queryMessage = APRS_IS_Utils::processReceivedLoRaMessage(Sender, AddresseeAndMessage, thirdPartyPacket);
}
}
if (!queryMessage) {
String loraPacket = generateDigipeatedPacket(packet.substring(3), thirdPartyPacket);
if (loraPacket != "") {
STATION_Utils::addToOutputPacketBuffer(loraPacket);
if (Config.digi.ecoMode != 1) displayToggle(true);
lastScreenOn = millis();
}
}
}
if (lastAprsPacket.path.indexOf("NOGATE") >= 0) return;
String temp = (lastAprsPacket.header != "") ? packet.substring(packet.indexOf(":}") + 2) : packet.substring(3);
String stationCallsign = Config.tacticalCallsign == "" ? Config.callsign : Config.tacticalCallsign;
if (lastAprsPacket.sender == stationCallsign) return; // Avoid listening to self packets
if (lastAprsPacket.header == "" && Config.tacticalCallsign == "" && !Utils::callsignIsValid(lastAprsPacket.sender)) return; // No thirdParty + no tactical y no valid callsign
if (STATION_Utils::isIn25SegHashBuffer(lastAprsPacket.sender, temp.substring(temp.indexOf(":") + 2))) return;
STATION_Utils::updateLastHeard(lastAprsPacket.sender);
Utils::updateLoRaPacketDisplayInfo(lastAprsPacket, 1); // Digi
bool queryMessage = false;
if (lastAprsPacket.type == 1) { // MESSAGE
if (lastAprsPacket.addressee == stationCallsign) { // it's a message for me!
String AddresseeAndMessage = lastAprsPacket.addressee + ":" + lastAprsPacket.payload;
queryMessage = APRS_IS_Utils::processReceivedLoRaMessage(lastAprsPacket.sender, APRSPacketLib::checkForStartingBytes(AddresseeAndMessage));
}
}
if (queryMessage) return; // answer should not be repeated.
String loraPacket = generateDigipeatedPacket(packet.substring(3));
if (loraPacket != "") {
STATION_Utils::addToOutputPacketBuffer(loraPacket);
if (Config.digi.ecoMode != 1) displayToggle(true);
lastScreenOn = millis();
}
}
}
+53 -28
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -26,7 +26,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
@@ -47,21 +47,31 @@
#ifdef HELTEC_WP_V1
EInkDisplay_WirelessPaperV1_1 display;
#endif
/*#ifdef HELTEC_WP_V1_2 // SOON!
#ifdef HELTEC_WP_V1_2
EInkDisplay_WirelessPaperV1_2 display;
#endif*/
#endif
#ifdef HELTEC_VM_E290
EInkDisplay_VisionMasterE290 display;
#endif
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
@@ -71,6 +81,7 @@
extern Configuration Config;
bool displayFound = false;
int maxLines;
void displaySetup() {
#ifdef HAS_DISPLAY
@@ -88,14 +99,29 @@ void displaySetup() {
tft.setTextFont(0);
tft.fillScreen(TFT_BLACK);
#if defined(TTGO_T_DECK_GPS) || defined(TTGO_T_DECK_PLUS)
sprite.createSprite(320,240);
sprite.createSprite(320, 240);
#else
sprite.createSprite(160,80);
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...");
if (Config.display.turn180) {
#if defined(HELTEC_VM_E290) || defined(HELTEC_WP_V1)
display.setRotation(3);
#endif
#if defined(HELTEC_WP_V1_2)
display.setRotation(1);
#endif
}
display.update();
#else
#ifdef OLED_DISPLAY_HAS_RST_PIN
@@ -105,8 +131,8 @@ void displaySetup() {
digitalWrite(OLED_RST, HIGH);
#endif
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
if (!display.begin(0x3c, false)) {
#ifdef HAS_SH1106
if (display.begin(0x3c, false)) {
displayFound = true;
if (Config.display.turn180) display.setRotation(2);
display.clearDisplay();
@@ -145,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);
@@ -157,15 +183,14 @@ void displayToggle(bool toggle) {
digitalWrite(TFT_BL, LOW);
#else
#ifdef HAS_EPAPER
display.printCenter("Enabled EPAPER Display...");
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
}
@@ -195,7 +220,7 @@ void displayShow(const String& header, const String& line1, const String& line2,
sprite.drawString(*lines[i], 3, (lineSpacing * (2 + i)) - 2);
}
sprite.pushSprite(0,0);
sprite.pushSprite(0, 0);
#else
#ifdef HAS_EPAPER
display.clearMemory();
@@ -211,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);
@@ -223,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);
@@ -260,7 +285,7 @@ void displayShow(const String& header, const String& line1, const String& line2,
sprite.drawString(*lines[i], 3, (lineSpacing * (2 + i)) - 2);
}
sprite.pushSprite(0,0);
sprite.pushSprite(0, 0);
#else
#ifdef HAS_EPAPER
lastEpaperText = header + line1 + line2 + line3 + line4 + line5 + line6;
@@ -277,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);
@@ -286,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);
+47 -186
View File
@@ -1,21 +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 <APRSPacketLib.h>
#include <TinyGPS++.h>
#include <WiFi.h>
#include "configuration.h"
@@ -33,6 +34,7 @@
extern Configuration Config;
extern HardwareSerial gpsSerial;
extern TinyGPSPlus gps;
extern bool stationCallsignIsValid;
String distance, iGateBeaconPacket, iGateLoRaBeaconPacket;
@@ -42,83 +44,38 @@ namespace GPS_Utils {
return iGateLoRaBeaconPacket;
}
char *ax25_base91enc(char *s, uint8_t n, uint32_t v) {
for(s += n, *s = '\0'; n; n--) {
*(--s) = v % 91 + 33;
v /= 91;
}
return(s);
}
float roundToTwoDecimals(float degrees) {
return round(degrees * 100) / 100;
}
String encodeGPS(float latitude, float longitude, const String& overlay, const String& symbol) {
String encodedData = overlay;
uint32_t aprs_lat, aprs_lon;
float processedLatitude = latitude;
float processedLongitude = longitude;
if (Config.beacon.gpsActive && Config.beacon.gpsAmbiguity) {
processedLatitude = roundToTwoDecimals(latitude);
processedLongitude = roundToTwoDecimals(longitude);
}
aprs_lat = 900000000 - processedLatitude * 10000000;
aprs_lat = aprs_lat / 26 - aprs_lat / 2710 + aprs_lat / 15384615;
aprs_lon = 900000000 + processedLongitude * 10000000 / 2;
aprs_lon = aprs_lon / 26 - aprs_lon / 2710 + aprs_lon / 15384615;
String Ns, Ew, helper;
if(processedLatitude < 0) { Ns = "S"; } else { Ns = "N"; }
if(processedLatitude < 0) { processedLatitude = -processedLatitude; }
if(processedLongitude < 0) { Ew = "W"; } else { Ew = "E"; }
if(processedLongitude < 0) { processedLongitude = -processedLongitude; }
char helper_base91[] = {"0000\0"};
int i;
ax25_base91enc(helper_base91, 4, aprs_lat);
for (i = 0; i < 4; i++) {
encodedData += helper_base91[i];
}
ax25_base91enc(helper_base91, 4, aprs_lon);
for (i = 0; i < 4; i++) {
encodedData += helper_base91[i];
}
encodedData += symbol;
encodedData += " ";
encodedData += "\x47";
return encodedData;
}
void generateBeaconFirstPart() {
String beaconPacket = Config.callsign;
beaconPacket += ">APLRG1";
if (Config.beacon.path.indexOf("WIDE") == 0) {
beaconPacket += ",";
beaconPacket += Config.beacon.path;
}
iGateBeaconPacket = beaconPacket;
iGateBeaconPacket += ",qAC:!";
iGateLoRaBeaconPacket = beaconPacket;
iGateLoRaBeaconPacket += ":!";
}
void generateBeacons() {
if (Config.callsign.indexOf("NOCALL-10") != 0 && !Utils::checkValidCallsign(Config.callsign)) {
displayShow("***** ERROR ******", "CALLSIGN = NOT VALID!", "", "Only Rx Mode Active", 3000);
Config.loramodule.txActive = false;
Config.aprs_is.messagesToRF = false;
Config.aprs_is.objectsToRF = false;
String beaconPacket = APRSPacketLib::generateBasePacket(Config.callsign, "APLRG1", Config.beacon.path);
String encodedGPS = APRSPacketLib::encodeGPSIntoBase91(Config.beacon.latitude, Config.beacon.longitude, 0, 0, Config.beacon.symbol, false, 0, true, Config.beacon.ambiguityLevel);
if (Config.callsign.indexOf("NOCALL-10") != 0) {
if (!stationCallsignIsValid) {
displayShow("***** ERROR ******", "CALLSIGN = NOT VALID!", "", "Only Rx Mode Active", 3000);
Config.loramodule.txActive = false;
Config.aprs_is.messagesToRF = false;
Config.aprs_is.objectsToRF = false;
Config.beacon.sendViaRF = false;
Config.digi.mode = 0;
Config.digi.backupDigiMode = false;
} else if (stationCallsignIsValid && Config.tacticalCallsign != "") {
beaconPacket = APRSPacketLib::generateBasePacket(Config.tacticalCallsign, "APLRG1", Config.beacon.path);
Config.aprs_is.active = false;
Config.beacon.sendViaAPRSIS = false;
Config.digi.backupDigiMode = false;
}
} else {
Config.beacon.sendViaAPRSIS = false;
Config.beacon.sendViaRF = false;
Config.digi.mode = 0;
Config.backupDigiMode = false;
}
generateBeaconFirstPart();
String encodedGPS = encodeGPS(Config.beacon.latitude, Config.beacon.longitude, Config.beacon.overlay, Config.beacon.symbol);
iGateBeaconPacket = beaconPacket;
iGateBeaconPacket += ",qAC:=";
iGateBeaconPacket += Config.beacon.overlay;
iGateBeaconPacket += encodedGPS;
iGateLoRaBeaconPacket = beaconPacket;
iGateLoRaBeaconPacket += ":=";
iGateLoRaBeaconPacket += Config.beacon.overlay;
iGateLoRaBeaconPacket += encodedGPS;
}
@@ -126,118 +83,22 @@ namespace GPS_Utils {
return TinyGPSPlus::distanceBetween(Config.beacon.latitude,Config.beacon.longitude, latitude, longitude) / 1000.0;
}
String decodeEncodedGPS(const String& packet) {
int indexOfExclamation = packet.indexOf(":!");
int indexOfEqual = packet.indexOf(":=");
String buildDistanceAndComment(float latitude, float longitude, const String& comment) {
distance = String(calculateDistanceTo(latitude, longitude),1);
const uint8_t OFFSET = 3; // Offset for encoded data in the packet
String GPSPacket;
if (indexOfExclamation > 10) {
GPSPacket = packet.substring(indexOfExclamation + OFFSET);
} else if (indexOfEqual > 10) {
GPSPacket = packet.substring(indexOfEqual + OFFSET);
}
String distanceAndComment = String(latitude,5);
distanceAndComment += "N / ";
distanceAndComment += String(longitude,5);
distanceAndComment += "E / ";
distanceAndComment += distance;
distanceAndComment += "km";
String encodedLatitude = GPSPacket.substring(0,4);
int Y1 = encodedLatitude[0] - 33;
int Y2 = encodedLatitude[1] - 33;
int Y3 = encodedLatitude[2] - 33;
int Y4 = encodedLatitude[3] - 33;
float decodedLatitude = 90.0 - (((Y1 * pow(91,3)) + (Y2 * pow(91,2)) + (Y3 * 91) + Y4) / 380926.0);
String encodedLongitude = GPSPacket.substring(4,8);
int X1 = encodedLongitude[0] - 33;
int X2 = encodedLongitude[1] - 33;
int X3 = encodedLongitude[2] - 33;
int X4 = encodedLongitude[3] - 33;
float decodedLongitude = -180.0 + (((X1 * pow(91,3)) + (X2 * pow(91,2)) + (X3 * 91) + X4) / 190463.0);
distance = String(calculateDistanceTo(decodedLatitude, decodedLongitude),1);
String decodedGPS = String(decodedLatitude,5);
decodedGPS += "N / ";
decodedGPS += String(decodedLongitude,5);
decodedGPS += "E / ";
decodedGPS += distance;
decodedGPS += "km";
String comment = GPSPacket.substring(12);
if (comment != "") {
decodedGPS += " / ";
decodedGPS += comment;
distanceAndComment += " / ";
distanceAndComment += comment;
}
return decodedGPS;
}
return distanceAndComment;
String getReceivedGPS(const String& packet) {
int indexOfExclamation = packet.indexOf(":!");
int indexOfEqual = packet.indexOf(":=");
int indexOfAt = packet.indexOf(":@");
String infoGPS;
if (indexOfExclamation > 10) {
infoGPS = packet.substring(indexOfExclamation + 2);
} else if (indexOfEqual > 10) {
infoGPS = packet.substring(indexOfEqual + 2);
} else if (indexOfAt > 10) {
infoGPS = packet.substring(indexOfAt + 9); // 9 = 2+7 (when 7 is timestamp characters)
}
String Latitude = infoGPS.substring(0,8); // First 8 characters are Latitude
float convertedLatitude = Latitude.substring(0,2).toFloat(); // First 2 digits (Degrees)
convertedLatitude += Latitude.substring(2,4).toFloat() / 60; // Next 2 digits (Minutes)
convertedLatitude += Latitude.substring(Latitude.indexOf(".") + 1, Latitude.indexOf(".") + 3).toFloat() / (60*100);
if (Latitude.endsWith("S")) convertedLatitude = -convertedLatitude; // Handle Southern Hemisphere
String Longitude = infoGPS.substring(9,18); // Next 9 characters are Longitude
float convertedLongitude = Longitude.substring(0,3).toFloat(); // First 3 digits (Degrees)
convertedLongitude += Longitude.substring(3,5).toFloat() / 60; // Next 2 digits (Minutes)
convertedLongitude += Longitude.substring(Longitude.indexOf(".") + 1, Longitude.indexOf(".") + 3).toFloat() / (60*100);
if (Longitude.endsWith("W")) convertedLongitude = -convertedLongitude; // Handle Western Hemisphere
distance = String(calculateDistanceTo(convertedLatitude, convertedLongitude),1);
String decodedGPS = String(convertedLatitude,5);
decodedGPS += "N / ";
decodedGPS += String(convertedLongitude,5);
decodedGPS += "E / ";
decodedGPS += distance;
decodedGPS += "km";
String comment = infoGPS.substring(19);
if (comment != "") {
decodedGPS += " / ";
decodedGPS += comment;
}
return decodedGPS;
}
String getDistanceAndComment(const String& packet) {
int indexOfAt = packet.indexOf(":@");
if (indexOfAt > 10) {
return getReceivedGPS(packet);
} else {
const uint8_t ENCODED_BYTE_OFFSET = 14; // Offset for encoded data in the packet
int indexOfExclamation = packet.indexOf(":!");
int indexOfEqual = packet.indexOf(":=");
uint8_t encodedBytePosition = 0;
if (indexOfExclamation > 10) { // Determine the position where encoded data starts
encodedBytePosition = indexOfExclamation + ENCODED_BYTE_OFFSET;
} else if (indexOfEqual > 10) {
encodedBytePosition = indexOfEqual + ENCODED_BYTE_OFFSET;
}
if (encodedBytePosition != 0) {
char currentChar = packet[encodedBytePosition];
if (currentChar == 'G' || currentChar == 'Q' || currentChar == '[' || currentChar == 'H' || currentChar == 'X') {
return decodeEncodedGPS(packet); // If valid encoded data position is found, decode it
} else {
return getReceivedGPS(packet);
}
} else {
return " _ / _ / _ ";
}
}
}
void setup() {
+15 -24
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -30,29 +30,19 @@ bool validateKISSFrame(const String& kissFormattedFrame) {
String encodeAddressAX25(String tnc2Address) {
bool hasBeenDigipited = tnc2Address.indexOf('*') != -1;
if (tnc2Address.indexOf('-') == -1) {
if (hasBeenDigipited) {
tnc2Address = tnc2Address.substring(0, tnc2Address.length() - 1);
}
if (hasBeenDigipited) tnc2Address = tnc2Address.substring(0, tnc2Address.length() - 1);
tnc2Address += "-0";
}
int separatorIndex = tnc2Address.indexOf('-');
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;
}
@@ -131,8 +121,9 @@ String encodeKISS(const String& frame) {
if (validateTNC2Frame(frame)) {
String address = "";
bool dstAddresWritten = false;
for (int p = 0; p <= frame.indexOf(':'); p++) {
bool dstAddresWritten = false;
int colonFrameIndex = frame.indexOf(':');
for (int p = 0; p <= colonFrameIndex; p++) {
char currentChar = frame.charAt(p);
if (currentChar == ':' || currentChar == '>' || currentChar == ',') {
if (!dstAddresWritten && (currentChar == ',' || currentChar == ':')) {
@@ -151,7 +142,7 @@ String encodeKISS(const String& frame) {
ax25Frame.setCharAt(ax25Frame.length() - 1, (char)(lastAddressChar | IS_LAST_ADDRESS_POSITION_MASK));
ax25Frame += (char)APRS_CONTROL_FIELD;
ax25Frame += (char)APRS_INFORMATION_FIELD;
ax25Frame += frame.substring(frame.indexOf(':') + 1);
ax25Frame += frame.substring(colonFrameIndex + 1);
}
String kissFrame = encapsulateKISS(ax25Frame, CMD_DATA);
+125 -36
View File
@@ -1,40 +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);
@@ -42,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
@@ -59,10 +65,20 @@ bool transmitFlag = true;
int rssi, freqError;
float snr;
APRSPacket lastAprsPacket;
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;
}
@@ -75,14 +91,19 @@ namespace LoRa_Utils {
#else
SPI.begin(RADIO_SCLK_PIN, RADIO_MISO_PIN, RADIO_MOSI_PIN);
#endif
#ifdef RADIO_ANT_SW_PIN // RAK3312 antenna switch needs power
pinMode(RADIO_ANT_SW_PIN, OUTPUT);
digitalWrite(RADIO_ANT_SW_PIN, RADIO_ANT_SW_ON_STATE);
#endif
float freq = (float)Config.loramodule.rxFreq / 1000000;
#if defined(RADIO_HAS_XTAL)
radio.XTAL = true;
#endif
#if (defined(RADIO_RXEN) && defined(RADIO_TXEN)) // before begin() so RF switch is driven from start (Ebyte E22/E32 1W, QRP Labs LightGateway)
radio.setRfSwitchPins(RADIO_RXEN, RADIO_TXEN);
#endif
int state = radio.begin(freq);
if (state == RADIOLIB_ERR_NONE) {
Utils::println("Initializing LoRa Module");
} else {
if (state != RADIOLIB_ERR_NONE) {
Utils::println("Starting LoRa failed! State: " + String(state));
while (true);
}
@@ -92,15 +113,26 @@ namespace LoRa_Utils {
#if defined(HAS_SX1278) || defined(HAS_SX1276)
radio.setDio0Action(setFlag, RISING);
#endif
radio.setSpreadingFactor(Config.loramodule.spreadingFactor);
float signalBandwidth = Config.loramodule.signalBandwidth/1000;
/*#ifdef SX126X_DIO3_TCXO_VOLTAGE
if (radio.setTCXO(float(SX126X_DIO3_TCXO_VOLTAGE)) == RADIOLIB_ERR_NONE) {
Utils::println("Set LoRa Module TCXO Voltage to:" + String(SX126X_DIO3_TCXO_VOLTAGE));
} else {
Utils::println("Set LoRa Module TCXO Voltage failed! State: " + String(state));
while (true);
}
#endif*/
radio.setSpreadingFactor(Config.loramodule.rxSpreadingFactor);
radio.setCodingRate(Config.loramodule.rxCodingRate4);
float signalBandwidth = Config.loramodule.rxSignalBandwidth / 1000;
radio.setBandwidth(signalBandwidth);
radio.setCodingRate(Config.loramodule.codingRate4);
radio.setCRC(true);
#if (defined(RADIO_RXEN) && defined(RADIO_TXEN)) // QRP Labs LightGateway has 400M22S (SX1268)
radio.setRfSwitchPins(RADIO_RXEN, RADIO_TXEN);
#endif
/*#ifdef SX126X_DIO2_AS_RF_SWITCH
radio.setRfSwitchPins(RADIO_RXEN, RADIOLIB_NC);
radio.setDio2AsRfSwitch(true);
#endif*/
#ifdef HAS_1W_LORA // Ebyte E22 400M30S (SX1268) / 900M30S (SX1262) / Ebyte E220 400M30S (LLCC68)
state = radio.setOutputPower(Config.loramodule.power); // max value 20dB for 1W modules as they have Low Noise Amp
@@ -119,6 +151,13 @@ namespace LoRa_Utils {
radio.setRxBoostedGainMode(true);
#endif
#if defined(HAS_TCXO) && !defined(HAS_1W_LORA)
radio.setDio2AsRfSwitch();
#endif
#ifdef HAS_TCXO
radio.setTCXO(1.8);
#endif
if (state == RADIOLIB_ERR_NONE) {
Utils::println("init : LoRa Module ... done!");
} else {
@@ -128,32 +167,74 @@ namespace LoRa_Utils {
}
void changeFreqTx() {
delay(500);
float freq = (float)Config.loramodule.txFreq / 1000000;
radio.setFrequency(freq);
radio.setSpreadingFactor(Config.loramodule.txSpreadingFactor);
radio.setCodingRate(Config.loramodule.txCodingRate4);
float signalBandwidth = Config.loramodule.txSignalBandwidth / 1000;
radio.setBandwidth(signalBandwidth);
}
void changeFreqRx() {
delay(500);
float freq = (float)Config.loramodule.rxFreq / 1000000;
radio.setFrequency(freq);
radio.setSpreadingFactor(Config.loramodule.rxSpreadingFactor);
radio.setCodingRate(Config.loramodule.rxCodingRate4);
float signalBandwidth = Config.loramodule.rxSignalBandwidth / 1000;
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;
if (Config.loramodule.txFreq != Config.loramodule.rxFreq) {
changeFreqTx();
if (!packetIsBeacon || (packetIsBeacon && Config.beacon.beaconFreq == 1)) {
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) {
SYSLOG_Utils::log(3, newPacket, 0, 0.0, 0); // TX
if (Config.syslog.active && networkManager->isConnected()) {
SYSLOG_Utils::logLoRaTx(newPacket);
}
Utils::print("---> LoRa Packet Tx : ");
Utils::println(newPacket);
@@ -165,7 +246,9 @@ namespace LoRa_Utils {
if (Config.digi.ecoMode != 1) digitalWrite(INTERNAL_LED_PIN, LOW); // disabled in Ultra Eco Mode
#endif
if (Config.loramodule.txFreq != Config.loramodule.rxFreq) {
changeFreqRx();
if (!packetIsBeacon || (packetIsBeacon && Config.beacon.beaconFreq == 1)) {
changeFreqRx();
}
}
}
@@ -193,32 +276,35 @@ namespace LoRa_Utils {
if (packet != "") {
String sender = packet.substring(3, packet.indexOf(">"));
if (packet.substring(0,3) == "\x3c\xff\x01" && !STATION_Utils::isBlacklisted(sender)){ // avoid processing BlackListed stations
if (packet.substring(0,3) == "\x3c\xff\x01" && !STATION_Utils::isBlacklisted(sender)) { // avoid processing BlackListed stations
rssi = radio.getRSSI();
snr = radio.getSNR();
freqError = radio.getFrequencyError();
Utils::println("<--- LoRa Packet Rx : " + packet.substring(3));
Utils::println("(RSSI:" + String(rssi) + " / SNR:" + String(snr) + " / FreqErr:" + String(freqError) + ")");
Utils::println("<--- LoRa Packet Rx (RSSI:" + String(rssi) + " | SNR:" + String(snr) + " | FreqErr:" + String(freqError) + ") : " + packet.substring(3));
lastAprsPacket = APRSPacketLib::processReceivedPacket(packet.substring(3), rssi, snr, freqError);
if (Config.digi.ecoMode == 0) {
if (receivedPackets.size() >= 10) {
receivedPackets.erase(receivedPackets.begin());
}
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);
if (lastAprsPacket.type == 0 || lastAprsPacket.type == 4) { // 0 = GPS, 4 = Mic-E (only ones with position)
MAP_Utils::upsert(lastAprsPacket.sender, lastAprsPacket.latitude, lastAprsPacket.longitude, lastAprsPacket.path, lastAprsPacket.overlay + lastAprsPacket.symbol, lastAprsPacket.rssi, lastAprsPacket.snr);
}
}
if (Config.syslog.active && WiFi.status() == WL_CONNECTED) {
SYSLOG_Utils::log(1, packet, rssi, snr, freqError); // RX
if (Config.syslog.active && networkManager->isConnected()) {
SYSLOG_Utils::logLoRaRx(lastAprsPacket, packet, rssi, snr, freqError); // RX
}
} else {
packet = "";
}
lastRxTime = millis();
return packet;
}
} else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
@@ -226,8 +312,8 @@ namespace LoRa_Utils {
snr = radio.getSNR();
freqError = radio.getFrequencyError();
Utils::println(F("CRC error!"));
if (Config.syslog.active && WiFi.status() == WL_CONNECTED) {
SYSLOG_Utils::log(0, packet, rssi, snr, freqError); // CRC
if (Config.syslog.active && networkManager->isConnected()) {
SYSLOG_Utils::logCRCError(packet, rssi, snr, freqError);
}
packet = "";
} else {
@@ -246,6 +332,9 @@ namespace LoRa_Utils {
void sleepRadio() {
radio.sleep();
#ifdef RADIO_ANT_SW_PIN
digitalWrite(RADIO_ANT_SW_PIN, !RADIO_ANT_SW_ON_STATE);
#endif
}
}
+116
View File
@@ -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
View File
@@ -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);
}
}
+355
View File
@@ -0,0 +1,355 @@
/* 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;
}
bool NetworkManager::hasActiveInterface() const {
return _wifiSTAmode || _wifiAPmode || _ethernetMode;
}
// 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;
}
+30 -16
View File
@@ -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(ntpUDP, "pool.ntp.org", 0, 15 * 60 * 1000); // Update interval 15 min
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;
timeClient.setTimeOffset(gmt);
timeClient.begin();
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 (Config.digi.ecoMode == 0) return timeClient.getFormattedTime();
if (networkManager->isConnected() && Config.digi.ecoMode == 0 && timeClient != nullptr) {
return timeClient->getFormattedTime();
}
return "DigiEcoMode Active";
}
}
}
+7 -7
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -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;
}
}
+29 -51
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -20,6 +20,7 @@
#include "battery_utils.h"
#include "board_pinout.h"
#include "power_utils.h"
#include "utils.h"
#if defined(HAS_AXP192) || defined(HAS_AXP2101)
#ifdef TTGO_T_Beam_S3_SUPREME_V3
@@ -43,48 +44,28 @@
#endif
extern Configuration Config;
extern bool stationCallsignIsValid;
namespace POWER_Utils {
#ifdef VEXT_CTRL
void vext_ctrl_ON() {
#if defined(HELTEC_WIRELESS_TRACKER) || defined(HELTEC_V3)
digitalWrite(VEXT_CTRL, Config.digi.ecoMode == 1 ? LOW : HIGH);
#endif
#if defined(HELTEC_WP_V1) || defined(HELTEC_WS) || defined(HELTEC_V3_2) || defined(HELTEC_WSL_V3)
digitalWrite(VEXT_CTRL, Config.digi.ecoMode == 1 ? HIGH : LOW);
#endif
}
void vext_ctrl_OFF() {
#if defined(HELTEC_WIRELESS_TRACKER) || defined(HELTEC_V3)
digitalWrite(VEXT_CTRL, Config.digi.ecoMode == 1 ? HIGH : LOW);
#endif
#if defined(HELTEC_WP_V1) || defined(HELTEC_WS) || defined(HELTEC_V3_2) || defined(HELTEC_WSL_V3)
digitalWrite(VEXT_CTRL, Config.digi.ecoMode == 1 ? LOW : HIGH);
#endif
}
#endif
#ifdef ADC_CTRL
#ifdef ADC_CTRL_PIN
void adc_ctrl_ON() {
#if defined(HELTEC_WIRELESS_TRACKER) || defined(HELTEC_V3_2)
digitalWrite(ADC_CTRL, HIGH);
#endif
#if defined(HELTEC_V3) || defined(HELTEC_V2) || defined(HELTEC_WSL_V3) || defined(HELTEC_WP_V1)
digitalWrite(ADC_CTRL, LOW);
#endif
digitalWrite(ADC_CTRL_PIN, ADC_CTRL_ON_STATE);
}
void adc_ctrl_OFF() {
#if defined(HELTEC_WIRELESS_TRACKER) || defined(HELTEC_V3_2)
digitalWrite(ADC_CTRL, LOW);
#endif
#if defined(HELTEC_V3) || defined(HELTEC_V2) || defined(HELTEC_WSL_V3) || defined(HELTEC_WP_V1)
digitalWrite(ADC_CTRL, HIGH);
#endif
digitalWrite(ADC_CTRL_PIN, !ADC_CTRL_ON_STATE);
}
#endif
#ifdef VEXT_CTRL_PIN
void vext_ctrl_ON() {
digitalWrite(VEXT_CTRL_PIN, Config.digi.ecoMode == 1 ? !VEXT_CTRL_ON_STATE : VEXT_CTRL_ON_STATE);
}
void vext_ctrl_OFF() {
digitalWrite(VEXT_CTRL_PIN, Config.digi.ecoMode == 1 ? VEXT_CTRL_ON_STATE : !VEXT_CTRL_ON_STATE);
}
#endif
@@ -112,7 +93,7 @@ namespace POWER_Utils {
#else
return false;
#endif
}
}
void activateGPS() {
#ifdef HAS_AXP192
@@ -282,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
@@ -291,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
@@ -315,17 +296,14 @@ namespace POWER_Utils {
Wire.begin(OLED_SDA, OLED_SCL);
#endif
#ifdef USE_WIRE_WITH_BOARD_I2C_PINS
Wire.begin(BOARD_I2C_SDA, BOARD_I2C_SCL);
#ifdef SENSOR_I2C_BUS
SENSOR_I2C_BUS.begin(BOARD_I2C_SDA, BOARD_I2C_SCL);
#endif
#ifdef USE_WIRE1_WITH_BOARD_I2C_PINS
Wire1.begin(BOARD_I2C_SDA, BOARD_I2C_SCL);
#endif
delay(1000);
BATTERY_Utils::setup();
BATTERY_Utils::startupBatteryHealth();
stationCallsignIsValid = Utils::callsignIsValid(Config.callsign);
setCpuFrequencyMhz(80);
}
+36 -34
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -41,7 +41,7 @@ namespace QUERY_Utils {
String queryQuestion = query;
queryQuestion.toUpperCase();
if (queryQuestion == "?APRS?" || queryQuestion == "H" || queryQuestion == "HELP" || queryQuestion=="?") {
answer.concat("?APRSV ?APRSP ?APRSL ?APRSSSR ?EM=? ?TX=? "); // ?APRSH ?WHERE callsign
answer.concat("?APRSV ?APRSP ?APRSL ?APRSSR ?EM=? ?TX=? "); // ?APRSH ?WHERE callsign
} else if (queryQuestion == "?APRSV") {
answer.concat("CA2RXU_LoRa_iGate v");
answer.concat(versionNumber);
@@ -49,9 +49,9 @@ namespace QUERY_Utils {
answer.concat(versionDate);
} else if (queryQuestion == "?APRSP") {
answer.concat("iGate QTH: ");
answer.concat(String(Config.beacon.latitude,3));
answer.concat(String(Config.beacon.latitude,2));
answer.concat(" ");
answer.concat(String(Config.beacon.longitude,3));
answer.concat(String(Config.beacon.longitude,2));
} else if (queryQuestion == "?APRSL") {
if (lastHeardStations.size() == 0) {
char answerArray[50];
@@ -66,21 +66,25 @@ namespace QUERY_Utils {
} else if (queryQuestion == "?APRSSR") {
char signalData[35];
snprintf(signalData, sizeof(signalData), " %ddBm / %.2fdB / %dHz", rssi, snr, freqError);
answer.concat(signalData);
answer.concat(signalData);
} /*else if (queryQuestion.indexOf("?APRSH") == 0) {
// sacar callsign despues de ?APRSH
Serial.println("escuchaste a X estacion? en las ultimas 24 o 8 horas?");
answer.concat("?APRSH on development 73!");
} *//*else if (queryQuestion.indexOf("?WHERE") == 0) {
} *//*else if (queryQuestion.indexOf("?WHERE") == 0) {
// agregar callsign para completar donde esta X callsign --> posicion
Serial.println("estaciones escuchadas directo (ultimos 30 min)");
answer.concat("?WHERE on development 73!");
} */
else if (STATION_Utils::isManager(station) && (!queryFromAPRSIS || !Config.remoteManagement.rfOnly)) {
if (queryQuestion.indexOf("?EM=OFF") == 0) {
if ((Config.digi.mode == 2 || Config.digi.mode == 3) && Config.loramodule.txActive && Config.loramodule.rxActive && !Config.aprs_is.active) {
if (Config.digi.ecoMode == 1 || Config.digi.ecoMode == 2) { // Exit Digipeater EcoMode or Digipeater without WiFiAP
answer = (Config.digi.ecoMode == 1) ? "DigiEcoMode:OFF" : "Digipeater + WiFiAP enabled";
int digiMode = Config.digi.mode;
int digiEcoMode = Config.digi.ecoMode;
int radioTxActive = Config.loramodule.txActive;
bool onlyRadioActive = radioTxActive && Config.loramodule.rxActive && !Config.aprs_is.active;
if (queryQuestion.startsWith("?EM=OFF")) {
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;
Config.display.alwaysOn = true;
Config.display.timeout = 10;
@@ -92,9 +96,9 @@ namespace QUERY_Utils {
} else {
answer = "Digipeater Mode control not possible";
}
} else if (queryQuestion.indexOf("?EM=ON") == 0) {
if ((Config.digi.mode == 2 || Config.digi.mode == 3) && Config.loramodule.txActive && Config.loramodule.rxActive && !Config.aprs_is.active) {
if (Config.digi.ecoMode == 0) { // Start Digipeater EcoMode
} else if (queryQuestion.startsWith("?EM=ON")) {
if ((digiMode == 1 || digiMode == 2 || digiMode == 3) && onlyRadioActive) {
if (digiEcoMode == 0) { // Start Digipeater EcoMode
answer = "DigiEcoMode:ON";
Config.digi.ecoMode = 1;
shouldSleepLowVoltage = true; // to make sure all packets in outputPacketBuffer are sent before restart.
@@ -105,31 +109,29 @@ namespace QUERY_Utils {
} else {
answer = "Digipeater Mode control not possible";
}
} else if (queryQuestion.indexOf("?EM=?") == 0) { // Digipeater EcoMode Status
if (Config.digi.ecoMode == 0) {
answer = "DigiEcoMode:OFF";
} else if (Config.digi.ecoMode == 1) {
answer = "DigiEcoMode:ON";
} else {
answer = "DigiEcoMode:OFF/Only Serial Output";
} else if (queryQuestion.startsWith("?EM=?")) { // Digipeater EcoMode Status
switch (digiEcoMode) {
case 0: answer = "DigiEcoMode:OFF"; break;
case 1: answer = "DigiEcoMode:ON"; break;
default: answer = "DigiEcoMode:OFF/Only Serial Output";
}
} else if (queryQuestion.indexOf("?TX=ON") == 0) {
if (Config.loramodule.txActive) {
} else if (queryQuestion.startsWith("?TX=ON")) {
if (radioTxActive) {
answer = "TX was ON";
} else {
Config.loramodule.txActive = true;
answer = "TX=ON";
}
} else if (queryQuestion.indexOf("?TX=OFF") == 0) {
if (!Config.loramodule.txActive) {
} else if (queryQuestion.startsWith("?TX=OFF")) {
if (!radioTxActive) {
answer = "TX was OFF";
} else {
Config.loramodule.txActive = false;
answer = "TX=OFF";
}
} else if (queryQuestion.indexOf("?TX=?") == 0) {
answer = (Config.loramodule.txActive) ? "TX=ON" : "TX=OFF";
} else if (queryQuestion.indexOf("?COMMIT") == 0) { // saving for next reboot
}
} else if (queryQuestion.startsWith("?TX=?")) {
answer = (radioTxActive) ? "TX=ON" : "TX=OFF";
} else if (queryQuestion.startsWith("?COMMIT")) { // saving for next reboot
answer = "New Config Saved";
Config.writeFile();
}
@@ -137,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";
+10 -9
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -51,12 +51,13 @@ namespace SLEEP_Utils {
if (Config.digi.ecoMode == 1) {
pinMode(RADIO_WAKEUP_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(RADIO_WAKEUP_PIN), wakeUpLoRaPacketReceived, RISING);
#if defined(TTGO_LORA32_V2_1) || defined(TTGO_LORA32_V2_1_915) || defined(TTGO_LORA32_T3S3_V1_2) || defined(TTGO_T_BEAM_V1_0) || defined(TTGO_T_BEAM_V1_0_915) || defined(TTGO_T_BEAM_V1_0_SX1268) || defined(TTGO_T_BEAM_V1_2) || defined(TTGO_T_BEAM_V1_2_915) || defined(TTGO_T_BEAM_V1_2_SX1262) || defined(TTGO_T_DECK_PLUS) || defined(TTGO_T_DECK_GPS) || defined(TTGO_T_Beam_S3_SUPREME_V3) || defined(HELTEC_V3) || defined(HELTEC_V3_2) || defined(HELTEC_WP_V1) || defined(HELTEC_WS) || defined(HELTEC_WSL_V3) || defined(HELTEC_WSL_V3_DISPLAY) || defined(HELTEC_WIRELESS_TRACKER) || defined(HELTEC_V2) || defined(XIAO_ESP32S3_LORA) || defined(LIGHTGATEWAY_1_0) || defined(LIGHTGATEWAY_PLUS_1_0) || defined(TROY_LoRa_APRS) || defined(OE5HWN_MeshCom) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_LoRa_915) || defined(ESP32_DIY_1W_LoRa) || defined(ESP32_DIY_1W_LoRa_915) || defined(ESP32_DIY_1W_LoRa_LLCC68) || defined(ESP32_DIY_1W_LoRa_Mesh_V1_2) || defined(WEMOS_S2_MINI_DIY_LoRa) || defined(WEMOS_D1_R32_RA02) || defined(WEMOS_LOLIN32_OLED_DIY_LoRa)
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)
esp_sleep_enable_ext1_wakeup(GPIO_WAKEUP_PIN, ESP_EXT1_WAKEUP_ANY_HIGH);
#endif
#if defined(HELTEC_HTCT62) || defined(ESP32C3_DIY_1W_LoRa) || defined(ESP32C3_DIY_1W_LoRa_915) || defined(ESP32_C3_OctopusLab_LoRa)
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
esp_deep_sleep_enable_gpio_wakeup(1ULL << GPIO_WAKEUP_PIN, ESP_GPIO_WAKEUP_GPIO_HIGH);
#endif
//#if defined(ARDUINO_ARCH_STM32) || defined(ARDUINO_ARCH_NRF52)
}
#endif
}
+88 -64
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -27,53 +27,63 @@
extern Configuration Config;
extern uint32_t lastRxTime;
extern String fourthLine;
extern bool shouldSleepLowVoltage;
uint32_t lastTxTime = millis();
std::vector<LastHeardStation> lastHeardStations;
std::vector<String> outputPacketBuffer;
std::vector<Packet25SegBuffer> packet25SegBuffer;
std::vector<String> blacklist;
std::vector<String> managers;
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;
uint32_t hash;
};
std::vector<Packet25SegBuffer> packet25SegBuffer;
bool saveNewDigiEcoModeConfig = false;
bool packetIsBeacon = false;
namespace STATION_Utils {
std::vector<String> loadCallSignList(const String& list) {
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;
}
void loadBlacklistAndManagers() {
blacklist = loadCallSignList(Config.blacklist);
managers = loadCallSignList(Config.remoteManagement.managers);
blacklist = loadCallsignList(Config.blacklist);
managers = loadCallsignList(Config.remoteManagement.managers);
}
bool checkCallsignList(const std::vector<String>& list, const String& callsign) {
for (int i = 0; i < list.size(); i++) {
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;
}
@@ -114,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});
Utils::activeStations();
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;
@@ -153,25 +160,42 @@ 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() {
size_t currentIndex = 0;
while (currentIndex < outputPacketBuffer.size()) { // this sends all packets from output buffer
delay(3000); // and cleans buffer to avoid sending packets with time offset
LoRa_Utils::sendNewPacket(outputPacketBuffer[currentIndex]); // next time it wakes up
if (outputPacketBuffer[currentIndex].isBeacon) packetIsBeacon = true;
LoRa_Utils::sendNewPacket(outputPacketBuffer[currentIndex].packet); // next time it wakes up
if (outputPacketBuffer[currentIndex].isBeacon) packetIsBeacon = false;
currentIndex++;
}
outputPacketBuffer.clear();
@@ -186,17 +210,17 @@ 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) {
LoRa_Utils::sendNewPacket(outputPacketBuffer[0]);
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) {
LoRa_Utils::sendNewPacket(outputPacketBuffer[0]);
if (outputPacketBuffer[0].isBeacon) packetIsBeacon = true;
LoRa_Utils::sendNewPacket(outputPacketBuffer[0].packet);
if (outputPacketBuffer[0].isBeacon) packetIsBeacon = false;
outputPacketBuffer.erase(outputPacketBuffer.begin());
delay(4000);
}
@@ -209,8 +233,8 @@ namespace STATION_Utils {
}
}
void addToOutputPacketBuffer(const String& packet) {
outputPacketBuffer.push_back(packet);
void addToOutputPacketBuffer(const String& packet, bool flag) {
outputPacketBuffer.emplace_back(OutputPacketBuffer{packet, flag});
}
}
+142 -106
View File
@@ -1,150 +1,186 @@
/* 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 <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;
WiFiUDP udpClient;
namespace {
String createSyslogStart() {
String syslogStartPacket = "<165>1 - ";
syslogStartPacket.concat(Config.callsign);
syslogStartPacket.concat(" CA2RXU_LoRa_iGate_");
syslogStartPacket.concat(versionNumber);
syslogStartPacket.concat(" - - - "); //RFC5424 The Syslog Protocol
return syslogStartPacket;
}
void sendSyslogPacket(const String& syslogPacket) {
udpClient.beginPacket(Config.syslog.server.c_str(), Config.syslog.port);
udpClient.write((const uint8_t*)syslogPacket.c_str(), syslogPacket.length());
udpClient.endPacket();
}
String formatSignalData(int rssi, float snr, int freqError) {
char signalData[35];
snprintf(signalData, sizeof(signalData), " / %ddBm / %.2fdB / %dHz", rssi, snr, freqError);
return String(signalData);
}
}
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) {
String syslogPacket = "<165>1 - ";
syslogPacket.concat(Config.callsign);
syslogPacket.concat(" CA2RXU_LoRa_iGate_");
syslogPacket.concat(versionNumber);
syslogPacket.concat(" - - - "); //RFC5424 The Syslog Protocol
void logLoRaRx(APRSPacket& aprsPacket, const String& packet, const int rssi, const float snr, const int freqError) {
if (Config.syslog.active && networkManager->isConnected()) {
String syslogPacket = createSyslogStart();
syslogPacket.concat("RX / ");
char signalData[35];
snprintf(signalData, sizeof(signalData), " / %ddBm / %.2fdB / %dHz", rssi, snr, freqError);
int colonIndex = packet.indexOf(":");
char nextChar = packet[colonIndex + 1];
String sender = packet.substring(3, packet.indexOf(">"));
switch (type) {
case 0: // CRC
syslogPacket.concat("CRC / CRC-ERROR / ");
syslogPacket.concat(packet);
syslogPacket.concat(signalData);
switch (aprsPacket.type) {
case 1: // MESSAGE
syslogPacket.concat("MESSAGE / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(aprsPacket.addressee);
syslogPacket.concat(":");
syslogPacket.concat(aprsPacket.payload);
break;
case 1: // RX
syslogPacket.concat("RX / ");
if (nextChar == ':') {
syslogPacket.concat("MESSAGE / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else if (nextChar == '!' || nextChar == '=' || nextChar == '@') {
syslogPacket.concat("GPS / ");
syslogPacket.concat(sender);
syslogPacket.concat(" / ");
int greaterThanIndex = packet.indexOf(">");
if (packet.indexOf("WIDE1-1") > 10) {
syslogPacket.concat(packet.substring(greaterThanIndex + 1, packet.indexOf(",")));
syslogPacket.concat(" / WIDE1-1");
} else {
syslogPacket.concat(packet.substring(greaterThanIndex + 1, colonIndex));
syslogPacket.concat(" / -");
case 0: // GPS
syslogPacket.concat("GPS / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" / ");
if (aprsPacket.path.indexOf("WIDE1-1") != -1) {
syslogPacket.concat(aprsPacket.tocall);
syslogPacket.concat(" / WIDE1-1");
} else {
syslogPacket.concat(aprsPacket.tocall);
if (aprsPacket.path != "") {
syslogPacket.concat(",");
syslogPacket.concat(aprsPacket.path);
}
} else if (nextChar == '>') {
syslogPacket.concat("STATUS / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else if (nextChar == '`') {
syslogPacket.concat("MIC-E / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else if (nextChar == ';') {
syslogPacket.concat("OBJECT / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else if (packet.indexOf(":T#") >= 10 && packet.indexOf(":=/") == -1) {
syslogPacket.concat("TELEMETRY / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(packet.indexOf(":T#") + 3));
} else {
syslogPacket.concat(packet);
}
syslogPacket.concat(signalData);
if (nextChar == '!' || nextChar == '=' || nextChar == '@') {
syslogPacket.concat(" / ");
syslogPacket.concat(GPS_Utils::getDistanceAndComment(packet));
syslogPacket.concat(" / -");
}
break;
case 2: // APRSIS TX
syslogPacket.concat("APRSIS TX / ");
if (nextChar == '>') {
syslogPacket.concat("StartUp_Status / ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else if (nextChar == ':') {
syslogPacket.concat("QUERY / ");
syslogPacket.concat(packet);
} else {
syslogPacket.concat("BEACON / ");
syslogPacket.concat(packet);
}
case 2: // STATUS
syslogPacket.concat("STATUS / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(aprsPacket.payload);
break;
case 3: // TX
syslogPacket.concat("TX / ");
if (packet.indexOf("RFONLY") > 10) {
syslogPacket.concat("RFONLY / ");
syslogPacket.concat(packet);
} else if (nextChar == ':') {
syslogPacket.concat("MESSAGE / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else {
syslogPacket.concat(packet);
}
case 4: // MIC-E
syslogPacket.concat("MIC-E / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.indexOf(":`") != -1 ? "`" : "'"); // restore the actual indicator received
break;
default:
syslogPacket = "<165>1 - ERROR LoRa - - - ERROR / Error in Syslog Packet"; //RFC5424 The Syslog Protocol
case 5: // OBJECT
syslogPacket.concat("OBJECT / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(aprsPacket.payload);
break;
case 3: // TELEMETRY
syslogPacket.concat("TELEMETRY / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(aprsPacket.payload);
break;
default: // type == 6, unrecognized -- payload is the full, uncut packet
syslogPacket.concat(aprsPacket.payload);
break;
}
udpClient.beginPacket(Config.syslog.server.c_str(), Config.syslog.port);
udpClient.write((const uint8_t*)syslogPacket.c_str(), syslogPacket.length());
udpClient.endPacket();
syslogPacket.concat(formatSignalData(rssi, snr, freqError));
if (aprsPacket.type == 0 || aprsPacket.type == 4) { // GPS or Mic-E -- both already carry
syslogPacket.concat(" / "); // clean lat/lon + comment in aprsPacket
syslogPacket.concat(GPS_Utils::buildDistanceAndComment(aprsPacket.latitude, aprsPacket.longitude, aprsPacket.payload));
}
sendSyslogPacket(syslogPacket);
}
}
void logAPRSISTx(const String& packet) {
if (Config.syslog.active && networkManager->isConnected()) {
String syslogPacket = createSyslogStart();
syslogPacket.concat("APRSIS TX / ");
APRSPacket aprsPacket = APRSPacketLib::processReceivedPacket(packet, 0, 0, 0);
if (aprsPacket.type == 2) { // STATUS
syslogPacket.concat("StartUp_Status / ");
syslogPacket.concat(aprsPacket.payload);
} else if (aprsPacket.type == 1) { // MESSAGE
syslogPacket.concat("QUERY / ");
syslogPacket.concat(packet);
} else {
syslogPacket.concat("BEACON / ");
syslogPacket.concat(packet);
}
sendSyslogPacket(syslogPacket);
}
}
void logLoRaTx(const String& packet) {
if (Config.syslog.active && networkManager->isConnected()) {
String syslogPacket = createSyslogStart();
syslogPacket.concat("TX / ");
APRSPacket aprsPacket = APRSPacketLib::processReceivedPacket(packet, 0, 0, 0);
if (aprsPacket.path.indexOf("RFONLY") != -1) {
syslogPacket.concat("RFONLY / ");
syslogPacket.concat(packet);
} else if (aprsPacket.type == 1) {
syslogPacket.concat("MESSAGE / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(aprsPacket.addressee);
syslogPacket.concat(":");
syslogPacket.concat(aprsPacket.payload);
} else {
syslogPacket.concat(packet);
}
sendSyslogPacket(syslogPacket);
}
}
void logCRCError(const String& packet, const int rssi, const float snr, const int freqError) {
if (Config.syslog.active && networkManager->isConnected()) {
String syslogPacket = createSyslogStart();
syslogPacket.concat("CRC / CRC-ERROR / ");
syslogPacket.concat(packet);
syslogPacket.concat(formatSignalData(rssi, snr, freqError));
sendSyslogPacket(syslogPacket);
}
}
void setup() {
if (WiFi.status() == WL_CONNECTED) {
if (networkManager->isConnected()) {
udpClient.begin(0);
udpClient.beginPacket("syslog.trackiot.cc", 15243);
String hiddenLogPacket = Config.callsign + "," + versionDate;
udpClient.write((const uint8_t*)hiddenLogPacket.c_str(), hiddenLogPacket.length());
udpClient.endPacket();
if (Config.syslog.active) Serial.println("init : Syslog Server ... done! (at " + Config.syslog.server + ")");
}
}
+24 -16
View File
@@ -1,17 +1,17 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -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 {
@@ -68,10 +69,10 @@ namespace TELEMETRY_Utils {
}
void sendBaseTelemetryPacket(const String& prefix, const std::vector<String>& values) {
String packet = prefix + joinWithCommas(values);
String packet = prefix + joinWithCommas(values);
String currentCallsign = (Config.tacticalCallsign != "") ? Config.tacticalCallsign : Config.callsign;
if (Config.beacon.sendViaAPRSIS) {
String baseAPRSISTelemetryPacket = APRSPacketLib::generateMessagePacket(Config.callsign, "APLRG1", "TCPIP,qAC", Config.callsign, packet);
String baseAPRSISTelemetryPacket = APRSPacketLib::generateMessagePacket(currentCallsign, "APLRG1", "TCPIP,qAC", currentCallsign, packet);
#ifdef HAS_A7670
A7670_Utils::uploadToAPRSIS(baseAPRSISTelemetryPacket);
#else
@@ -79,7 +80,7 @@ namespace TELEMETRY_Utils {
#endif
delay(300);
} else if (Config.beacon.sendViaRF) {
String baseRFTelemetryPacket = APRSPacketLib::generateMessagePacket(Config.callsign, "APLRG1", Config.beacon.path, Config.callsign, packet);
String baseRFTelemetryPacket = APRSPacketLib::generateMessagePacket(currentCallsign, "APLRG1", Config.beacon.path, currentCallsign, packet);
LoRa_Utils::sendNewPacket(baseRFTelemetryPacket);
delay(3000);
}
@@ -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;
@@ -122,9 +123,16 @@ namespace TELEMETRY_Utils {
telemetryCounter++;
if (telemetryCounter == 1000) telemetryCounter = 0;
if (Config.battery.sendInternalVoltage) telemetry += generateEncodedTelemetryBytes(BATTERY_Utils::checkInternalVoltage(), false, 0);
if (Config.battery.sendExternalVoltage) telemetry += generateEncodedTelemetryBytes(BATTERY_Utils::checkExternalVoltage(), false, 1);
if (Config.battery.sendExternalVoltage) telemetry += generateEncodedTelemetryBytes(BATTERY_Utils::checkExternalVoltage(), false, Config.battery.useExternalI2CSensor ? 0 : 1);
telemetry += "|";
return telemetry;
}
void checkEUPInterval() {
if (telemetryEUPTime == 0 || millis() - telemetryEUPTime > 24UL * 60UL * 60UL * 1000UL) {
sendEUP = true;
telemetryEUPTime = millis();
}
}
}
+69 -29
View File
@@ -1,32 +1,37 @@
/* 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 "ESPmDNS.h"
#include "network_manager.h"
#include "configuration.h"
#include "station_utils.h"
#include "kiss_protocol.h"
#include "aprs_is_utils.h"
#include "kiss_utils.h"
#include "tnc_utils.h"
#include "utils.h"
extern Configuration Config;
extern Configuration Config;
extern NetworkManager *networkManager;
extern WiFiClient aprsIsClient;
extern bool passcodeValid;
#define MAX_CLIENTS 4
#define INPUT_BUFFER_SIZE (2 + MAX_CLIENTS)
@@ -42,9 +47,9 @@ String inputSerialBuffer = "";
namespace TNC_Utils {
void setup() {
if (Config.tnc.enableServer && Config.digi.ecoMode == 0) {
if (Config.tnc.enableServer && Config.digi.ecoMode == 0 && networkManager->hasActiveInterface()) { // no WiFi STA/AP or Ethernet started = no TCP/IP stack
tncServer.stop();
tncServer.begin();
String host = "igate-" + Config.callsign;
@@ -75,7 +80,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;
@@ -86,18 +107,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) {
STATION_Utils::addToOutputPacketBuffer(frame);
} else {
Utils::println("Ignored own frame from KISS");
}
processInputFrame(frame, bufferIndex, "KISS");
}
data->clear();
}
@@ -107,6 +117,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];
@@ -131,16 +171,16 @@ namespace TNC_Utils {
}
}
void sendToClients(const String& packet) {
String cleanPacket = packet.substring(3);
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;
@@ -152,9 +192,9 @@ namespace TNC_Utils {
Utils::println(cleanPacket);
}
void sendToSerial(const String& packet) {
String cleanPacket = packet.substring(3);
Serial.print(encodeKISS(cleanPacket));
void sendToSerial(const String& packet, bool stripBytes) {
String cleanPacket = stripBytes ? packet.substring(3): packet;
Serial.print(Config.tnc.kissProtocol ? encodeKISS(cleanPacket) : (cleanPacket + "\r\n"));
Serial.flush();
}
+197 -124
View File
@@ -1,25 +1,26 @@
/* 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 <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"
@@ -33,8 +34,10 @@
#include "display.h"
#include "utils.h"
#define DAY_MS (24UL * 60UL * 60UL * 1000UL)
extern Configuration Config;
extern NetworkManager *networkManager;
extern TinyGPSPlus gps;
extern String versionDate;
extern String firstLine;
@@ -50,20 +53,21 @@ extern int rssi;
extern float snr;
extern int freqError;
extern String distance;
extern bool WiFiConnected;
extern int wxModuleType;
extern bool backUpDigiMode;
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 beaconUpdate = false;
uint32_t lastBeaconTx = 0;
uint32_t lastScreenOn = millis();
bool statusUpdate = true;
bool beaconUpdate = false;
uint32_t lastBeaconTx = 0;
uint32_t lastScreenOn = millis();
uint32_t lastStatusTx = 0;
bool stationCallsignIsValid = false;
String beaconPacket;
String secondaryBeaconPacket;
@@ -71,37 +75,43 @@ String secondaryBeaconPacket;
namespace Utils {
void processStatus() {
String status = Config.callsign;
status.concat(">APLRG1");
if (Config.beacon.path.indexOf("WIDE") == 0) {
status.concat(",");
status.concat(Config.beacon.path);
bool sendOverAPRSIS = Config.beacon.sendViaAPRSIS && Config.aprs_is.active && networkManager->isConnected();
bool sendOverRF = !Config.beacon.sendViaAPRSIS && Config.beacon.sendViaRF;
if (!sendOverAPRSIS && !sendOverRF) {
statusUpdate = false;
return;
}
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
statusAfterBoot = false;
}
if (statusAfterBoot && !Config.beacon.sendViaAPRSIS && Config.beacon.sendViaRF) {
status.concat(":>");
status.concat(Config.beacon.statusPacket);
STATION_Utils::addToOutputPacketBuffer(status);
statusAfterBoot = false;
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::logAPRSISTx(statusPacket);
} else {
STATION_Utils::addToOutputPacketBuffer(statusPacket, true); // treated also as beacon on Tx Freq
}
statusUpdate = false;
lastStatusTx = millis();
}
void checkStatusInterval() {
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 (backUpDigiMode) {
} 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();
}
}
@@ -123,11 +133,15 @@ namespace Utils {
#ifdef INTERNAL_LED_PIN
digitalWrite(INTERNAL_LED_PIN,LOW);
#endif
firstLine = Config.callsign;
if (Config.tacticalCallsign != "") {
firstLine = Config.tacticalCallsign;
} else {
firstLine = Config.callsign;
}
seventhLine = " listening...";
}
void activeStations() {
void showActiveStations() {
char buffer[30]; // Adjust size as needed
sprintf(buffer, "Stations (%dmin) = %2d", Config.rememberStationTime, lastHeardStations.size());
fourthLine = buffer;
@@ -139,19 +153,27 @@ 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.wxsensor.active &&
(Config.battery.sendInternalVoltage || Config.battery.sendExternalVoltage) &&
(lastBeaconTx > 0)) {
TELEMETRY_Utils::sendEquationsUnitsParameters();
@@ -159,7 +181,7 @@ namespace Utils {
STATION_Utils::deleteNotHeard();
activeStations();
showActiveStations();
beaconPacket = iGateBeaconPacket;
secondaryBeaconPacket = iGateLoRaBeaconPacket;
@@ -167,10 +189,18 @@ namespace Utils {
if (Config.beacon.gpsActive && Config.digi.ecoMode == 0) {
GPS_Utils::getData();
if (gps.location.isUpdated() && gps.location.lat() != 0.0 && gps.location.lng() != 0.0) {
GPS_Utils::generateBeaconFirstPart();
String encodedGPS = GPS_Utils::encodeGPS(gps.location.lat(), gps.location.lng(), Config.beacon.overlay, Config.beacon.symbol);
beaconPacket = iGateBeaconPacket + encodedGPS;
secondaryBeaconPacket = iGateLoRaBeaconPacket + encodedGPS;
String basePacket = APRSPacketLib::generateBasePacket(Config.callsign, "APLRG1", Config.beacon.path);
String encodedGPS = APRSPacketLib::encodeGPSIntoBase91(gps.location.lat(),gps.location.lng(), 0, 0, Config.beacon.symbol, false, 0, true, Config.beacon.ambiguityLevel);
beaconPacket = basePacket;
beaconPacket += ",qAC:!";
beaconPacket += Config.beacon.overlay;
beaconPacket += encodedGPS;
secondaryBeaconPacket = basePacket;
secondaryBeaconPacket += ":=";
secondaryBeaconPacket += Config.beacon.overlay;
secondaryBeaconPacket += encodedGPS;
}
}
#endif
@@ -182,6 +212,12 @@ namespace Utils {
}
beaconPacket += Config.beacon.comment;
secondaryBeaconPacket += Config.beacon.comment;
if (stationCallsignIsValid && Config.tacticalCallsign != "") {
beaconPacket += " de ";
beaconPacket += Config.callsign;
secondaryBeaconPacket += " de ";
secondaryBeaconPacket += Config.callsign;
}
#if defined(BATTERY_PIN) || defined(HAS_AXP192) || defined(HAS_AXP2101)
if (Config.battery.sendInternalVoltage || Config.battery.monitorInternalVoltage) {
@@ -222,7 +258,7 @@ namespace Utils {
if (Config.battery.sendExternalVoltage) {
char externalVoltageInfo[10]; // "xx.xxV\0" (max 7 chars)
snprintf(externalVoltageInfo, sizeof(externalVoltageInfo), "%.2fV", externalVoltage);
char sixthLineBuffer[25]; // Ensure enough space
snprintf(sixthLineBuffer, sizeof(sixthLineBuffer), " (Ext V=%s)", externalVoltageInfo);
sixthLine = sixthLineBuffer;
@@ -243,7 +279,7 @@ namespace Utils {
secondaryBeaconPacket += encodedTelemetry;
}
if (Config.beacon.sendViaAPRSIS && Config.aprs_is.active && passcodeValid && !backUpDigiMode) {
if (Config.beacon.sendViaAPRSIS && Config.aprs_is.active && passcodeValid && !backupDigiMode) {
Utils::println("-- Sending Beacon to APRSIS --");
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, "SENDING IGATE BEACON", 0);
seventhLine = " listening...";
@@ -252,14 +288,14 @@ namespace Utils {
#else
APRS_IS_Utils::upload(beaconPacket);
#endif
if (Config.syslog.logBeaconOverTCPIP) SYSLOG_Utils::log(1, "tcp" + beaconPacket, 0, 0.0, 0); // APRSIS TX
if (Config.syslog.logBeaconOverTCPIP) SYSLOG_Utils::logAPRSISTx(beaconPacket);
}
if (Config.beacon.sendViaRF || backUpDigiMode) {
if (Config.beacon.sendViaRF || backupDigiMode) {
Utils::println("-- Sending Beacon to RF --");
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, "SENDING DIGI BEACON", 0);
seventhLine = " listening...";
STATION_Utils::addToOutputPacketBuffer(secondaryBeaconPacket);
STATION_Utils::addToOutputPacketBuffer(secondaryBeaconPacket, true);
}
lastBeaconTx = millis();
@@ -267,9 +303,8 @@ namespace Utils {
beaconUpdate = false;
}
if (statusAfterBoot && Config.beacon.statusActive && !Config.beacon.statusPacket.isEmpty()) {
processStatus();
}
checkStatusInterval();
if (statusUpdate && Config.beacon.statusActive && !Config.beacon.statusPacket.isEmpty()) processStatus();
}
void checkDisplayInterval() {
@@ -284,58 +319,61 @@ namespace Utils {
Serial.println("Tx Freq less than 125kHz from Rx Freq ---> NOT VALID");
displayShow("Tx Freq is less than ", "125kHz from Rx Freq", "device will autofix", "and then reboot", 1000);
Config.loramodule.txFreq = Config.loramodule.rxFreq; // Inform about that but then change the TX QRG to RX QRG and reset the device
Config.beacon.beaconFreq = 1; // return to LoRa Tx Beacon Freq
Config.writeFile();
ESP.restart();
}
}
void typeOfPacket(const String& packet, const uint8_t packetType) {
String sender = packet.substring(0,packet.indexOf(">"));
void updateLoRaPacketDisplayInfo(APRSPacket& aprsPacket, const uint8_t packetType) {
switch (packetType) {
case 0: // LoRa-APRS
fifthLine = "LoRa Rx ----> APRS-IS";
break;
case 1: // APRS-LoRa
fifthLine = "APRS-IS ----> LoRa Tx";
break;
case 2: // Digipeater
case 1: // Digipeater
fifthLine = "LoRa Rx ----> LoRa Tx";
break;
}
int firstColonIndex = packet.indexOf(":");
char nextChar = packet[firstColonIndex + 1];
String sender = aprsPacket.sender;
for (int i = sender.length(); i < 9; i++) {
sender += " ";
}
sixthLine = sender;
if (nextChar == ':') {
sixthLine += "> MESSAGE";
} else if (nextChar == '>') {
sixthLine += "> NEW STATUS";
} else if (nextChar == '!' || nextChar == '=' || nextChar == '@') {
sixthLine += "> GPS BEACON";
if (!Config.syslog.active) GPS_Utils::getDistanceAndComment(packet); // to be checked!!!
seventhLine = "RSSI:";
seventhLine += String(rssi);
seventhLine += "dBm";
seventhLine += (rssi <= -100) ? " " : " ";
if (distance.indexOf(".") == 1) seventhLine += " ";
seventhLine += "D:";
seventhLine += distance;
seventhLine += "km";
} else if (nextChar == '`' || nextChar == '\'') {
sixthLine += "> MIC-E";
} else if (nextChar == ';') {
sixthLine += "> OBJECT";
} else if (packet.indexOf(":T#") >= 10 && packet.indexOf(":=/") == -1) {
sixthLine += "> TELEMETRY";
} else {
sixthLine += "> ??????????";
switch (aprsPacket.type) {
case 1: // MESSAGE
sixthLine += "> MESSAGE";
break;
case 2: // STATUS
sixthLine += "> NEW STATUS";
break;
case 0: // GPS
sixthLine += "> GPS BEACON";
if (!Config.syslog.active) GPS_Utils::buildDistanceAndComment(aprsPacket.latitude, aprsPacket.longitude, aprsPacket.payload);
seventhLine = "RSSI:";
seventhLine += String(rssi);
seventhLine += "dBm";
seventhLine += (rssi <= -100) ? " " : " ";
if (distance.indexOf(".") == 1) seventhLine += " ";
seventhLine += "D:";
seventhLine += distance;
seventhLine += "km";
break;
case 3: // TELEMETRY
sixthLine += "> TELEMETRY";
break;
case 4: // MIC-E
sixthLine += "> MIC-E";
break;
case 5: // OBJECT
sixthLine += "> OBJECT";
break;
default: // unrecognized
sixthLine += "> ?????????";
break;
}
if (nextChar != '!' && nextChar != '=' && nextChar != '@') { // Common assignment for non-GPS cases
if (aprsPacket.type != 0) { // Common assignment for non-GPS cases
seventhLine = "RSSI:";
seventhLine += String(rssi);
seventhLine += "dBm SNR: ";
@@ -344,6 +382,23 @@ namespace Utils {
}
}
void updateAPRSISPacketDisplayInfo(const String& packet) {
fifthLine = "APRS-IS ----> LoRa Tx";
String sender = packet.substring(0,packet.indexOf(">"));
for (int i = sender.length(); i < 9; i++) {
sender += " ";
}
sixthLine = sender;
if (packet.indexOf("::") > 0) {
sixthLine += "> MESSAGE";
} else {
sixthLine += "> OBJECT";
}
seventhLine = "";
}
void print(const String& text) {
if (!Config.tnc.enableSerial) {
Serial.print(text);
@@ -381,9 +436,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();
@@ -393,47 +448,65 @@ namespace Utils {
}
}
bool checkValidCallsign(const String& callsign) {
bool callsignIsValid(const String& callsign) {
if (callsign == "WLNK-1") return true;
String cleanCallsign;
if (callsign.indexOf("-") > 0) { // SSID Validation
cleanCallsign = callsign.substring(0, callsign.indexOf("-"));
String ssid = callsign.substring(callsign.indexOf("-") + 1);
if (ssid.indexOf("-") != -1 || ssid.length() > 2) return false;
for (int i = 0; i < ssid.length(); i++) {
if (!isAlphaNumeric(ssid[i])) return false;
int totalCallsignLength = callsign.length();
if (totalCallsignLength < 4) 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;
}
}
void startupDelay() {
if (Config.startupDelay > 0) {
displayShow("", " STARTUP DELAY ...", "", "", 0);
delay(Config.startupDelay * 60 * 1000);
}
}
}
+224 -93
View File
@@ -1,30 +1,33 @@
/* 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 <ArduinoJson.h>
#include "network_manager.h"
#include "configuration.h"
#include "ota_utils.h"
#include "web_utils.h"
#include "map_utils.h"
#include "display.h"
#include "utils.h"
extern Configuration Config;
extern NetworkManager *networkManager;
extern uint32_t lastBeaconTx;
extern std::vector<ReceivedPacket> receivedPackets;
@@ -48,11 +51,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 +106,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 +157,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,160 +173,212 @@ namespace WEB_Utils {
request->send(200, "application/json", buffer);
}
void handleWriteConfiguration(AsyncWebServerRequest *request) {
Serial.println("Got new config from www");
void handleStations(AsyncWebServerRequest *request) {
request->send(200, "application/json", MAP_Utils::getStationsJson());
}
int networks = request->getParam("wifi.APs", true)->value().toInt();
void handleWriteConfiguration(AsyncWebServerRequest *request) {
Serial.println("Got new Configuration Data from www");
auto getParamStringSafe = [&](const String& name, const String& defaultValue = "") -> String {
if (request->hasParam(name, true)) {
return request->getParam(name, true)->value();
}
return defaultValue;
};
auto getParamIntSafe = [&](const String& name, int defaultValue = 0) -> int {
if (request->hasParam(name, true)) {
return request->getParam(name, true)->value().toInt();
}
return defaultValue;
};
auto getParamFloatSafe = [&](const String& name, float defaultValue = 0.0) -> float {
if (request->hasParam(name, true)) {
return request->getParam(name, true)->value().toFloat();
}
return defaultValue;
};
auto getParamDoubleSafe = [&](const String& name, double defaultValue = 0.0) -> double {
if (request->hasParam(name, true)) {
return request->getParam(name, true)->value().toDouble();
}
return defaultValue;
};
int networks = getParamIntSafe("wifi.APs");
Config.wifiAPs = {};
for (int i=0; i<networks; i++) {
for (int i = 0; i < networks; i++) {
WiFi_AP wifiap;
wifiap.ssid = request->getParam("wifi.AP." + String(i) + ".ssid", true)->value();
wifiap.password = request->getParam("wifi.AP." + String(i) + ".password", true)->value();
wifiap.ssid = getParamStringSafe("wifi.AP." + String(i) + ".ssid");
wifiap.password = getParamStringSafe("wifi.AP." + String(i) + ".password");
Config.wifiAPs.push_back(wifiap);
}
Config.callsign = request->getParam("callsign", true)->value();
Config.wifiAutoAP.password = request->getParam("wifi.autoAP.password", true)->value();
Config.wifiAutoAP.timeout = request->getParam("wifi.autoAP.timeout", true)->value().toInt();
Config.startupDelay = getParamIntSafe("startupDelay", Config.startupDelay);
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);
Config.aprs_is.active = request->hasParam("aprs_is.active", true);
if (Config.aprs_is.active) {
Config.aprs_is.messagesToRF = request->hasParam("aprs_is.messagesToRF", true);
Config.aprs_is.objectsToRF = request->hasParam("aprs_is.objectsToRF", true);
Config.aprs_is.server = request->getParam("aprs_is.server", true)->value();
Config.aprs_is.passcode = request->getParam("aprs_is.passcode", true)->value();
Config.aprs_is.port = request->getParam("aprs_is.port", true)->value().toInt();
Config.aprs_is.filter = request->getParam("aprs_is.filter", true)->value();
Config.aprs_is.server = getParamStringSafe("aprs_is.server", Config.aprs_is.server);
Config.aprs_is.passcode = getParamStringSafe("aprs_is.passcode", Config.aprs_is.passcode);
Config.aprs_is.port = getParamIntSafe("aprs_is.port", Config.aprs_is.port);
Config.aprs_is.filter = getParamStringSafe("aprs_is.filter", Config.aprs_is.filter);
}
Config.beacon.interval = request->getParam("beacon.interval", true)->value().toInt();
Config.beacon.interval = getParamIntSafe("beacon.interval", Config.beacon.interval);
Config.beacon.sendViaAPRSIS = request->hasParam("beacon.sendViaAPRSIS", true);
Config.beacon.sendViaRF = request->hasParam("beacon.sendViaRF", true);
Config.beacon.latitude = request->getParam("beacon.latitude", true)->value().toDouble();
Config.beacon.longitude = request->getParam("beacon.longitude", true)->value().toDouble();
Config.beacon.comment = request->getParam("beacon.comment", true)->value();
Config.beacon.overlay = request->getParam("beacon.overlay", true)->value();
Config.beacon.symbol = request->getParam("beacon.symbol", true)->value();
Config.beacon.path = request->getParam("beacon.path", true)->value();
Config.beacon.beaconFreq = getParamIntSafe("beacon.beaconFreq", Config.beacon.beaconFreq);
Config.beacon.latitude = getParamDoubleSafe("beacon.latitude", Config.beacon.latitude);
Config.beacon.longitude = getParamDoubleSafe("beacon.longitude", Config.beacon.longitude);
Config.beacon.comment = getParamStringSafe("beacon.comment", Config.beacon.comment);
Config.beacon.overlay = getParamStringSafe("beacon.overlay", Config.beacon.overlay);
Config.beacon.symbol = getParamStringSafe("beacon.symbol", Config.beacon.symbol);
Config.beacon.path = getParamStringSafe("beacon.path", Config.beacon.path);
Config.beacon.statusActive = request->hasParam("beacon.statusActive", true);
if (Config.beacon.statusActive) {
Config.beacon.statusPacket = request->getParam("beacon.statusPacket", true)->value();
Config.beacon.statusPacket = getParamStringSafe("beacon.statusPacket", Config.beacon.statusPacket);
}
Config.beacon.gpsActive = request->hasParam("beacon.gpsActive", true);
Config.beacon.gpsAmbiguity = request->hasParam("beacon.gpsAmbiguity", true);
Config.beacon.ambiguityLevel = getParamIntSafe("beacon.ambiguityLevel", Config.beacon.ambiguityLevel);
Config.personalNote = request->getParam("personalNote", true)->value();
Config.personalNote = getParamStringSafe("personalNote", Config.personalNote);
Config.blacklist = request->getParam("blacklist", true)->value();
Config.blacklist = getParamStringSafe("blacklist", Config.blacklist);
Config.digi.mode = request->getParam("digi.mode", true)->value().toInt();
Config.digi.ecoMode = request->getParam("digi.ecoMode", true)->value().toInt();;
Config.digi.mode = getParamIntSafe("digi.mode", Config.digi.mode);
Config.digi.ecoMode = getParamIntSafe("digi.ecoMode", Config.digi.ecoMode);
Config.digi.backupDigiMode = request->hasParam("digi.backupDigiMode", true);
Config.loramodule.txFreq = request->getParam("lora.txFreq", true)->value().toInt();
Config.loramodule.rxFreq = request->getParam("lora.rxFreq", true)->value().toInt();
Config.loramodule.spreadingFactor = request->getParam("lora.spreadingFactor", true)->value().toInt();
Config.loramodule.signalBandwidth = request->getParam("lora.signalBandwidth", true)->value().toInt();
Config.loramodule.codingRate4 = request->getParam("lora.codingRate4", true)->value().toInt();
Config.loramodule.power = request->getParam("lora.power", true)->value().toInt();
Config.loramodule.txActive = request->hasParam("lora.txActive", true);
Config.loramodule.rxActive = request->hasParam("lora.rxActive", true);
Config.loramodule.rxFreq = getParamIntSafe("lora.rxFreq", Config.loramodule.rxFreq);
Config.loramodule.rxSpreadingFactor = getParamIntSafe("lora.rxSpreadingFactor", Config.loramodule.rxSpreadingFactor);
Config.loramodule.rxCodingRate4 = getParamIntSafe("lora.rxCodingRate4", Config.loramodule.rxCodingRate4);
Config.loramodule.rxSignalBandwidth = getParamIntSafe("lora.rxSignalBandwidth", Config.loramodule.rxSignalBandwidth);
Config.loramodule.txActive = request->hasParam("lora.txActive", true);
Config.loramodule.txFreq = getParamIntSafe("lora.txFreq", Config.loramodule.txFreq);
Config.loramodule.txSpreadingFactor = getParamIntSafe("lora.txSpreadingFactor", Config.loramodule.txSpreadingFactor);
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) {
Config.display.timeout = request->getParam("display.timeout", true)->value().toInt();
Config.display.timeout = getParamIntSafe("display.timeout", Config.display.timeout);
}
Config.display.turn180 = request->hasParam("display.turn180", true);
Config.battery.sendInternalVoltage = request->hasParam("battery.sendInternalVoltage", true);
Config.battery.monitorInternalVoltage = request->hasParam("battery.monitorInternalVoltage", true);
Config.battery.sendInternalVoltage = request->hasParam("battery.sendInternalVoltage", true);
Config.battery.monitorInternalVoltage = request->hasParam("battery.monitorInternalVoltage", true);
if (Config.battery.monitorInternalVoltage) {
Config.battery.internalSleepVoltage = request->getParam("battery.internalSleepVoltage", true)->value().toFloat();
}
Config.battery.internalSleepVoltage = getParamFloatSafe("battery.internalSleepVoltage", Config.battery.internalSleepVoltage);
}
Config.battery.sendExternalVoltage = request->hasParam("battery.sendExternalVoltage", true);
Config.battery.sendExternalVoltage = request->hasParam("battery.sendExternalVoltage", true);
if (Config.battery.sendExternalVoltage) {
Config.battery.externalVoltagePin = request->getParam("battery.externalVoltagePin", true)->value().toInt();
Config.battery.voltageDividerR1 = request->getParam("battery.voltageDividerR1", true)->value().toFloat();
Config.battery.voltageDividerR2 = request->getParam("battery.voltageDividerR2", true)->value().toFloat();
Config.battery.useExternalI2CSensor = request->hasParam("battery.useExternalI2CSensor", true);
}
Config.battery.monitorExternalVoltage = request->hasParam("battery.monitorExternalVoltage", true);
if (Config.battery.sendExternalVoltage) {
Config.battery.externalVoltagePin = getParamIntSafe("battery.externalVoltagePin", Config.battery.externalVoltagePin);
Config.battery.voltageDividerR1 = getParamFloatSafe("battery.voltageDividerR1", Config.battery.voltageDividerR1);
Config.battery.voltageDividerR2 = getParamFloatSafe("battery.voltageDividerR2", Config.battery.voltageDividerR2);
}
Config.battery.monitorExternalVoltage = request->hasParam("battery.monitorExternalVoltage", true);
if (Config.battery.monitorExternalVoltage) {
Config.battery.externalSleepVoltage = request->getParam("battery.externalSleepVoltage", true)->value().toFloat();
Config.battery.externalSleepVoltage = getParamFloatSafe("battery.externalSleepVoltage", Config.battery.externalSleepVoltage);
}
Config.battery.sendVoltageAsTelemetry = request->hasParam("battery.sendVoltageAsTelemetry", true);
Config.battery.sendVoltageAsTelemetry = request->hasParam("battery.sendVoltageAsTelemetry", true);
Config.wxsensor.active = request->hasParam("wxsensor.active", true);
if (Config.wxsensor.active) {
Config.wxsensor.heightCorrection = request->getParam("wxsensor.heightCorrection", true)->value().toInt();
Config.wxsensor.temperatureCorrection = request->getParam("wxsensor.temperatureCorrection", true)->value().toFloat();
Config.wxsensor.heightCorrection = getParamIntSafe("wxsensor.heightCorrection", Config.wxsensor.heightCorrection);
Config.wxsensor.temperatureCorrection = getParamFloatSafe("wxsensor.temperatureCorrection", Config.wxsensor.temperatureCorrection);
Config.beacon.symbol = "_";
}
Config.syslog.active = request->hasParam("syslog.active", true);
if (Config.syslog.active) {
Config.syslog.server = request->getParam("syslog.server", true)->value();
Config.syslog.port = request->getParam("syslog.port", true)->value().toInt();
Config.syslog.server = getParamStringSafe("syslog.server", Config.syslog.server);
Config.syslog.port = getParamIntSafe("syslog.port", Config.syslog.port);
Config.syslog.logBeaconOverTCPIP = request->hasParam("syslog.logBeaconOverTCPIP", true);
}
Config.tnc.enableServer = request->hasParam("tnc.enableServer", true);
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.tnc.enableServer = request->hasParam("tnc.enableServer", true);
Config.tnc.enableSerial = request->hasParam("tnc.enableSerial", true);
Config.tnc.acceptOwn = request->hasParam("tnc.acceptOwn", true);
Config.mqtt.active = request->hasParam("mqtt.active", true);
Config.mqtt.active = request->hasParam("mqtt.active", true);
if (Config.mqtt.active) {
Config.mqtt.server = request->getParam("mqtt.server", true)->value();
Config.mqtt.topic = request->getParam("mqtt.topic", true)->value();
Config.mqtt.username = request->getParam("mqtt.username", true)->value();
Config.mqtt.password = request->getParam("mqtt.password", true)->value();
Config.mqtt.port = request->getParam("mqtt.port", true)->value().toInt();
Config.mqtt.server = getParamStringSafe("mqtt.server", Config.mqtt.server);
Config.mqtt.topic = getParamStringSafe("mqtt.topic", Config.mqtt.topic);
Config.mqtt.username = getParamStringSafe("mqtt.username", Config.mqtt.username);
Config.mqtt.password = getParamStringSafe("mqtt.password", Config.mqtt.password);
Config.mqtt.port = getParamIntSafe("mqtt.port", Config.mqtt.port);
Config.mqtt.beaconOverMqtt = request->hasParam("mqtt.beaconOverMqtt", true);
}
Config.rebootMode = request->hasParam("other.rebootMode", true);
Config.rebootMode = request->hasParam("other.rebootMode", true);
if (Config.rebootMode) {
Config.rebootModeTime = request->getParam("other.rebootModeTime", true)->value().toInt();
Config.rebootModeTime = getParamIntSafe("other.rebootModeTime", Config.rebootModeTime);
}
Config.ota.username = request->getParam("ota.username", true)->value();
Config.ota.password = request->getParam("ota.password", true)->value();
Config.ota.username = getParamStringSafe("ota.username", Config.ota.username);
Config.ota.password = getParamStringSafe("ota.password", Config.ota.password);
Config.webadmin.active = request->hasParam("webadmin.active", true);
if (Config.webadmin.active) {
Config.webadmin.username = request->getParam("webadmin.username", true)->value();
Config.webadmin.password = request->getParam("webadmin.password", true)->value();
Config.webadmin.username = getParamStringSafe("webadmin.username", Config.webadmin.username);
Config.webadmin.password = getParamStringSafe("webadmin.password", Config.webadmin.password);
}
Config.remoteManagement.managers = request->getParam("remoteManagement.managers", true)->value();
Config.remoteManagement.managers = getParamStringSafe("remoteManagement.managers", Config.remoteManagement.managers);
Config.remoteManagement.rfOnly = request->hasParam("remoteManagement.rfOnly", true);
Config.ntp.gmtCorrection = request->getParam("ntp.gmtCorrection", true)->value().toFloat();
Config.ntp.server = getParamStringSafe("ntp.server", Config.ntp.server);
Config.ntp.gmtCorrection = getParamFloatSafe("ntp.gmtCorrection", Config.ntp.gmtCorrection);
Config.rememberStationTime = request->getParam("other.rememberStationTime", true)->value().toInt();
Config.rememberStationTime = getParamIntSafe("other.rememberStationTime", Config.rememberStationTime);
Config.backupDigiMode = request->hasParam("other.backupDigiMode", true);
bool saveSuccess = Config.writeFile();
Config.writeFile();
if (saveSuccess) {
Serial.println("Configuration saved successfully");
AsyncWebServerResponse *response = request->beginResponse(302, "text/html", "");
response->addHeader("Location", "/?success=1");
request->send(response);
AsyncWebServerResponse *response = request->beginResponse(302, "text/html", "");
response->addHeader("Location", "/");
request->send(response);
displayToggle(false);
delay(200);
ESP.restart();
displayToggle(false);
delay(500);
ESP.restart();
} else {
Serial.println("Error saving configuration!");
String errorPage = "<!DOCTYPE html><html><head><title>Error</title></head><body>";
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);
}
}
void handleAction(AsyncWebServerRequest *request) {
@@ -311,11 +422,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) {
if (Config.digi.ecoMode == 0 && networkManager->hasActiveInterface()) { // no WiFi STA/AP or Ethernet started = no TCP/IP stack
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);
@@ -323,7 +449,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
+80 -121
View File
@@ -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,148 +26,106 @@
extern Configuration Config;
extern NetworkManager *networkManager;
extern uint8_t myWiFiAPIndex;
extern int myWiFiAPSize;
extern WiFi_AP *currentWiFi;
extern bool backUpDigiMode;
extern bool backupDigiMode;
extern uint32_t lastServerCheck;
bool WiFiConnected = false;
uint32_t WiFiAutoAPTime = millis();
bool WiFiAutoAPStarted = false;
uint32_t previousWiFiMillis = 0;
uint8_t wifiCounter = 0;
uint32_t lastBackupDigiTime = millis();
uint32_t lastWiFiCheck = 0;
namespace WIFI_Utils {
void checkWiFi() {
if (Config.digi.ecoMode == 0) {
if (backUpDigiMode) {
uint32_t WiFiCheck = millis() - lastBackupDigiTime;
if (WiFi.status() != WL_CONNECTED && WiFiCheck >= 15 * 60 * 1000) {
Serial.println("*** Stopping BackUp Digi Mode ***");
backUpDigiMode = false;
wifiCounter = 0;
} else if (WiFi.status() == WL_CONNECTED) {
Serial.println("*** WiFi Reconnect Success (Stopping Backup Digi Mode) ***");
backUpDigiMode = false;
wifiCounter = 0;
}
if (Config.digi.ecoMode != 0) return;
if (!networkManager->hasWiFiNetworks()) {
return;
}
uint32_t currentTime = millis();
if (backupDigiMode) {
if (!networkManager->isWiFiConnected() && ((currentTime - lastBackupDigiTime) >= 15 * 60 * 1000)) {
Serial.println("*** Stopping BackUp Digi Mode ***");
backupDigiMode = false;
wifiCounter = 0;
} else if (networkManager->isWiFiConnected()) {
Serial.println("*** WiFi Reconnect Success (Stopping Backup Digi Mode) ***");
backupDigiMode = false;
wifiCounter = 0;
}
}
if (!backUpDigiMode && (WiFi.status() != WL_CONNECTED) && ((millis() - previousWiFiMillis) >= 30 * 1000) && !WiFiAutoAPStarted) {
Serial.print(millis());
Serial.println("Reconnecting to WiFi...");
WiFi.disconnect();
WIFI_Utils::startWiFi();//WiFi.reconnect();
previousWiFiMillis = millis();
if (Config.backupDigiMode) {
wifiCounter++;
if (!backupDigiMode && ((currentTime - lastWiFiCheck) >= 30 * 1000) && !networkManager->isWifiAPActive()) {
lastWiFiCheck = currentTime;
if (networkManager->isWiFiConnected()) {
if (Config.digi.backupDigiMode && (currentTime - lastServerCheck > 30 * 1000)) {
Serial.println("*** Server Connection LOST → Backup Digi Mode ***");
backupDigiMode = true;
lastBackupDigiTime = currentTime;
}
} else {
Serial.println("Reconnecting to WiFi...");
WIFI_Utils::startWiFi();
if (Config.digi.backupDigiMode) wifiCounter++;
if (wifiCounter >= 2) {
Serial.println("*** Starting BackUp Digi Mode ***");
backUpDigiMode = true;
lastBackupDigiTime = millis();
backupDigiMode = true;
lastBackupDigiTime = currentTime;
}
}
}
}
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.println("' ...");
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);
}
#ifdef INTERNAL_LED_PIN
digitalWrite(INTERNAL_LED_PIN,LOW);
#endif
if (WiFi.status() == WL_CONNECTED) {
Serial.print("Connected as ");
Serial.print(WiFi.localIP());
Serial.print(" / MAC Address: ");
Serial.println(WiFi.macAddress());
displayShow("", " Connected!!", "" , " loading ...", 1000);
} else if (WiFi.status() != WL_CONNECTED) {
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;
if (!networkManager->hasWiFiNetworks()) {
Serial.println("WiFi SSID not set!");
if (Config.wifiAutoAP.enabled) {
Serial.println("Starting AP fallback...");
startAutoAP();
} else {
if (WiFiAutoAPTime == 0) {
WiFiAutoAPTime = millis();
} else if ((millis() - WiFiAutoAPTime) > Config.wifiAutoAP.timeout * 60 * 1000) {
Serial.println("Stopping auto AP");
Serial.println("AutoAP disabled: no network, WebUI/TNC server not started (reconfigure over USB)");
displayShow("", " No WiFi / No AP", " WebUI disabled", " loading ...", 2000);
}
return;
}
WiFiAutoAPStarted = false;
WiFi.softAPdisconnect(true);
displayShow("", "Connecting to WiFi:", "", " loading ...", 0);
networkManager->connectWiFi();
Serial.println("Auto AP stopped (timeout)");
}
#ifdef INTERNAL_LED_PIN
digitalWrite(INTERNAL_LED_PIN, LOW);
#endif
if (networkManager->isWiFiConnected()) {
Serial.print("[WiFi] Connected as ");
Serial.print(networkManager->getWiFiIP());
Serial.print(" / MAC Address: ");
Serial.println(networkManager->getWiFimacAddress());
displayShow("", " Connected!!", "" , " loading ...", 1000);
} else {
Serial.println("[WiFi] Not connected to WiFi!");
if (Config.wifiAutoAP.enabled) {
Serial.println("Starting AP fallback...");
displayShow("", " WiFi Not Connected!", "" , " loading ...", 1000);
startAutoAP();
} else {
displayShow("", " WiFi Not Connected!", "" , " loading ...", 1000);
}
}
}
@@ -176,4 +135,4 @@ namespace WIFI_Utils {
btStop();
}
}
}
Loaded 100 of 224 files, more files were not shown because too many files have changed in this diff. Show more