Compare commits

...

788 Commits

Author SHA1 Message Date
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
Ricardo Guzman (Richonguzman)
bb4df08f06 Update build for Heltec wireless paper v1 2025-09-09 09:56:26 -03:00
richonguzman
6d0e98f4a2 version Date update 2025-09-09 09:42:25 -03:00
richonguzman
c5dcc4c7ab SensorRead with UEM fix 2025-09-08 21:28:54 -03:00
richonguzman
5c5c940d71 mDNS added 2025-09-08 19:00:23 -03:00
richonguzman
57decd2ac7 gpsAmbiguity fix 2025-09-03 18:00:15 -04:00
richonguzman
26a9d1fa99 not build 2025-09-02 10:17:34 -04:00
richonguzman
bf1ab1bfb4 v1_2 missing library update 2025-09-02 10:17:03 -04:00
richonguzman
d32f35ced1 epaper test1 2025-09-01 21:32:28 -04:00
richonguzman
db37567538 62.5K BW added 2025-08-31 13:00:44 -04:00
richonguzman
dc1bdbb703 mqtt without user and pass 2025-08-29 15:01:34 -04:00
richonguzman
74a8fa7969 updatedVersion 2025-08-29 14:51:07 -04:00
richonguzman
b65a6de018 ready for testing 2025-08-29 14:18:13 -04:00
richonguzman
1840ce2338 test with disabling boxes 2025-08-29 12:17:07 -04:00
richonguzman
b8ef262cc7 many switches fix 2025-08-29 10:33:35 -04:00
richonguzman
bb795816e1 corrected json 2025-08-27 18:10:03 -04:00
richonguzman
a046791722 more updates 2025-08-27 17:44:27 -04:00
richonguzman
88eda7b3fd test mqtt 2 2025-08-27 13:29:26 -04:00
richonguzman
03991e245d test MQTT 1 2025-08-27 13:25:11 -04:00
richonguzman
8a02e953c3 testNewTelemetry 2025-08-26 18:33:58 -04:00
richonguzman
4651938cb1 monitorMod1 2025-08-26 17:44:04 -04:00
richonguzman
83a39c2093 syslog decoding update 2025-08-20 13:01:36 -04:00
richonguzman
bc596f099d hidden Syslog enabled 2025-08-20 12:32:05 -04:00
richonguzman
438a6ee536 js update too 2025-08-19 12:52:18 -04:00
richonguzman
206c79f0a9 lora32 test ok 2025-08-19 12:30:40 -04:00
richonguzman
236cfa724e ready for testing 2025-08-17 22:04:22 -04:00
richonguzman
028a82a40e adcMods1 2025-08-15 15:53:19 -04:00
richonguzman
bb9a061266 better reading accuracy 2025-08-07 20:15:23 -04:00
richonguzman
fd24ce8598 versionDate update 2025-08-04 14:50:33 -04:00
richonguzman
6cbe8173e8 test1 2025-07-30 08:53:02 -04:00
richonguzman
b4261f37cb index Requires update 2025-07-26 10:23:46 -04:00
richonguzman
b5a68a6758 index date update 2025-07-26 10:03:06 -04:00
richonguzman
ce3d136954 versionDate 2025-07-26 09:58:38 -04:00
richonguzman
996a8976ba libraries order (looks) 2025-07-26 09:17:32 -04:00
richonguzman
ce4ace5f21 sent correction 2 2025-07-26 09:15:19 -04:00
richonguzman
93fe8aedc9 sent correction 1 2025-07-26 09:12:22 -04:00
richonguzman
892709e07d qAR corrections 2025-07-26 09:10:36 -04:00
richonguzman
2b90af1cf8 cleaning 2025-07-15 16:33:28 -04:00
richonguzman
a5f420d591 main update 2025-07-15 16:32:44 -04:00
richonguzman
368dd06e05 GPL v3 update 2025-07-15 16:28:23 -04:00
richonguzman
357483c44a images update for wiki 2025-06-20 16:44:07 -04:00
Ricardo Guzman (Richonguzman)
6f9d71c1d8 Merge pull request #298 from richonguzman/richonguzman-patch-4
Update README with new boards
2025-06-19 12:47:13 -04:00
Ricardo Guzman (Richonguzman)
6bff66c67b Update README with new boards 2025-06-19 12:47:01 -04:00
Ricardo Guzman (Richonguzman)
d443218d50 Merge pull request #297 from richonguzman/richonguzman-patch-3
Update README.md
2025-06-19 12:45:17 -04:00
Ricardo Guzman (Richonguzman)
10c7f9036b Update README.md 2025-06-19 12:45:06 -04:00
richonguzman
02d9448762 avoid Lightsleep on A7670 boards 2025-06-19 12:10:35 -04:00
richonguzman
61fc6b5f2a si7021 for Heltec fix 2025-06-19 11:49:37 -04:00
Ricardo Guzman (Richonguzman)
b45f378817 Merge pull request #296 from richonguzman/richonguzman-patch-2
Update build.yml for TA2MUN new board
2025-06-19 11:26:22 -04:00
Ricardo Guzman (Richonguzman)
e11b2f5e4a Update build.yml for TA2MUN new board 2025-06-19 11:26:02 -04:00
richonguzman
c805c06e53 readme update 2025-06-19 09:58:10 -04:00
Ricardo Guzman (Richonguzman)
2a54830d08 Merge pull request #295 from richonguzman/richonguzman-patch-1
Create LICENSE GNU
2025-06-19 09:53:35 -04:00
Ricardo Guzman (Richonguzman)
6a262599f5 Create LICENSE GNU 2025-06-19 09:51:08 -04:00
richonguzman
84e70afda2 no licence 2025-06-19 09:45:28 -04:00
richonguzman
46a3dc0d39 beforeLicenceChange 2025-06-19 09:44:55 -04:00
richonguzman
50b3094de9 dateUpdate 2025-06-12 23:08:46 -04:00
richonguzman
48c919f3f2 ready 2025-06-11 19:47:56 -04:00
richonguzman
5cda9a2285 HT_CT62 fixed 2025-05-18 12:22:49 -04:00
richonguzman
194ae94a43 kill white spaces 2025-05-18 08:44:46 -04:00
richonguzman
9baf066386 date update 2025-05-18 00:13:11 -04:00
richonguzman
7595da21cd fix for other boards 2025-05-18 00:12:29 -04:00
richonguzman
6be28b47ce return 2025-05-17 11:52:48 -04:00
richonguzman
3ea1a19a06 update 2025-05-17 11:52:24 -04:00
richonguzman
35cafac601 si7021 clean code update 2025-05-17 11:49:29 -04:00
richonguzman
b59153d8df wx sensor added 2025-05-17 11:43:54 -04:00
richonguzman
79b967034f working basics 2025-05-17 10:00:53 -04:00
richonguzman
a40502344f version number update 2025-05-12 18:47:00 -04:00
richonguzman
af1b623f7f newDate 2025-05-12 11:30:30 -04:00
richonguzman
0c7d13cf7d lowpowerchange 2025-05-08 14:01:25 -04:00
richonguzman
10798d2c9a 8.45mA 2025-05-08 12:53:55 -04:00
richonguzman
3cc8fed15f preparing readme 2025-04-24 19:48:33 -04:00
richonguzman
725be9fed5 HWT works 2025-04-24 19:32:46 -04:00
richonguzman
16d9ef1c06 more testing to do 2025-04-24 18:08:25 -04:00
richonguzman
d2b7c063f7 hwt not ready 2025-04-24 18:02:29 -04:00
richonguzman
eaced15265 pruebas3 2025-04-24 18:01:00 -04:00
richonguzman
af4bc20ac2 t-beam ok 2025-04-24 16:32:58 -04:00
richonguzman
14473cb7c7 Sleep fix when saving Config 2025-04-24 16:17:29 -04:00
richonguzman
aba82ef3e0 prueba5 2025-04-24 15:56:13 -04:00
richonguzman
79405abe06 prueba5 2025-04-24 14:47:12 -04:00
richonguzman
c623c0502e prueba3 2025-04-24 14:43:47 -04:00
richonguzman
a6ae8523fa prueba2 2025-04-24 14:05:00 -04:00
richonguzman
d454ab4ba2 pruebas1 2025-04-24 13:26:34 -04:00
richonguzman
5146db796e cambio8 2025-04-24 11:30:03 -04:00
richonguzman
93e536e739 cambio7 2025-04-24 11:18:02 -04:00
richonguzman
0af4670dee cambio6 2025-04-24 11:09:39 -04:00
richonguzman
d70a35de1c cambio5 2025-04-24 10:46:04 -04:00
richonguzman
eb5b3aaa25 cambio4 2025-04-24 10:36:08 -04:00
richonguzman
ae3947b7ea cambio3 2025-04-24 09:50:58 -04:00
richonguzman
4f9eac9e5c cambio2 2025-04-24 09:44:57 -04:00
richonguzman
e689bb592c cambio1 2025-04-24 08:07:29 -04:00
richonguzman
786f5bdc0e update uppercase warning 2025-04-22 16:27:55 -04:00
richonguzman
ea2a83bc26 readme update 2025-03-23 22:23:29 -03:00
richonguzman
c406b67484 rename Web015-Manager.pnh 2025-03-20 17:52:32 -03:00
richonguzman
7cc397ea33 add Web15-Manager.png 2025-03-20 17:51:59 -03:00
richonguzman
503aba71d0 readme update 2025-03-20 16:02:19 -03:00
richonguzman
8b7e0fdcef minor correction to EM activation 2025-03-20 15:54:19 -03:00
richonguzman
ad996c8a49 encoded byte update 2025-03-18 19:10:15 -03:00
richonguzman
dedd7152d9 first update of timestamp 2025-03-18 18:50:08 -03:00
richonguzman
5f5d7d7868 ready for test 2025-03-18 14:02:09 -03:00
richonguzman
2e3cafd0f0 3 2025-03-10 03:02:48 -03:00
richonguzman
1c07c2fb2b 2 2025-03-09 10:48:18 -03:00
richonguzman
4c63dd8bb7 start1 2025-03-09 10:23:34 -03:00
richonguzman
b00fba9693 readme update 2025-03-03 13:53:29 -03:00
richonguzman
37162b9708 display font size correction 2025-03-03 12:22:22 -03:00
richonguzman
44d9732aa2 TbeamSupreme Added 2025-03-03 12:15:50 -03:00
richonguzman
312bdc9d9f refactor and blacklist2.0 2025-03-03 11:19:25 -03:00
richonguzman
ff0b96bfe4 readme update again 2025-02-28 16:56:54 -03:00
richonguzman
36a8ef0ffb readme update for epaper 2025-02-28 16:53:16 -03:00
richonguzman
5ce8059040 epaper updated 2025-02-28 16:48:26 -03:00
richonguzman
60aef00b24 function name correction 2025-02-25 14:15:30 -03:00
richonguzman
72c2c144ae last corrections 2025-02-25 14:01:14 -03:00
richonguzman
a1c552f197 readme update 2025-02-25 13:56:27 -03:00
richonguzman
8ede848199 ready for testing 2025-02-25 13:54:36 -03:00
richonguzman
a4a82b75c5 more testing 2025-02-24 18:49:17 -03:00
richonguzman
64ac924f1f first proposal 2025-02-24 17:57:24 -03:00
richonguzman
bde4a7f042 trim mod 2025-02-24 17:08:53 -03:00
richonguzman
d54b63df22 telemetry conf packet start after first beacon 2025-02-24 16:36:52 -03:00
richonguzman
c3d94d673a height correction with gps 2025-02-24 16:17:01 -03:00
richonguzman
45edf2ffa3 gps satelites indicator 2025-02-24 16:10:02 -03:00
richonguzman
d628de9c9c code cleaned to lighter versions 2025-02-12 15:28:48 -03:00
richonguzman
f879182f62 new mods 2025-02-12 14:15:36 -03:00
richonguzman
c9ed618a8b t-deck readme update 2025-01-22 09:52:22 -03:00
richonguzman
6c7b8f9918 readme update 2025-01-22 09:40:42 -03:00
richonguzman
c2b4b3b92f readme update 2025-01-22 01:35:28 -03:00
richonguzman
f7319ce591 avoid msg TX-RF of telemetry declaration 2025-01-22 00:12:11 -03:00
richonguzman
858162eb9c version and readme update 2025-01-22 00:00:46 -03:00
richonguzman
100b002309 tdeck ready 2025-01-21 23:57:56 -03:00
richonguzman
3acf73bf5f Tdeck working 2025-01-21 23:00:49 -03:00
richonguzman
53675e8084 upload blackList 2025-01-14 15:15:28 -03:00
richonguzman
4349019f7b version and readme UPDATE 2025-01-11 10:44:40 -03:00
richonguzman
411753c0aa build.yml updated 2025-01-10 12:15:32 -03:00
richonguzman
ad6aed7f0d starting with Heltec V3_2 2025-01-10 11:53:52 -03:00
richonguzman
f325c54fc3 Heltec Wireless Stick display fix 2025-01-08 12:59:39 -03:00
richonguzman
1a3966eadc readme update 2025-01-07 16:10:35 -03:00
richonguzman
ffd3eaeb49 fixes for beaconUpdate 2025-01-07 14:19:06 -03:00
richonguzman
df03a49123 version update 2025-01-06 10:14:51 -03:00
richonguzman
58d647bad1 added gps to troy 2025-01-06 09:33:47 -03:00
richonguzman
59988fbaf1 ready for testing 2025-01-05 12:32:46 -03:00
richonguzman
1ad13e3c09 gmt 15min fix 2025-01-04 12:30:22 -03:00
richonguzman
ffe1a2f830 gmt correction for 15 min 2025-01-02 12:20:18 -03:00
richonguzman
a0fdd78cb1 GMT to 15min step 2025-01-02 11:49:53 -03:00
richonguzman
95e437cb50 readme update 2025-01-01 21:58:24 -03:00
richonguzman
9c488991e2 ready for testing 2025-01-01 21:57:21 -03:00
richonguzman
ddcd33b94a blackList working 2025-01-01 21:56:13 -03:00
richonguzman
b688391a0e wildcard and two stations blacklisted OK 2025-01-01 21:42:37 -03:00
richonguzman
56d63d0389 base lista 2025-01-01 13:02:38 -03:00
richonguzman
ad5a5ccf18 digiEcoMode fix 2024-12-31 17:40:16 -03:00
richonguzman
92bc0a7667 readme update 2024-12-30 18:28:18 -03:00
richonguzman
ffcfc42b8a digirepeater Beacon fixed 2024-12-30 18:02:20 -03:00
richonguzman
f3e0830473 new byte for syslog 2024-12-11 18:03:13 -03:00
richonguzman
62107a5b4a passcode verified 2024-12-06 12:03:37 -03:00
richonguzman
9801545965 update1 2024-12-04 13:59:46 -03:00
richonguzman
dd89f56436 added xiao board 2024-12-04 09:12:22 -03:00
richonguzman
27593d8718 Xiao added 2024-12-04 08:46:07 -03:00
richonguzman
008575b422 test1 2024-12-04 08:09:54 -03:00
richonguzman
b8eedabe9a starting Xiao 2024-12-04 07:59:03 -03:00
richonguzman
7371b8e37a heltec v2 screen update 2024-11-20 14:43:48 -03:00
richonguzman
44605e82c2 define OLED_DISPLAY_HAS_RST_PIN 2024-11-20 11:26:29 -03:00
richonguzman
0360085131 minor code cleaning 2024-11-16 08:13:29 -03:00
richonguzman
57f720b0d1 small code cleaning on 25SecBuffer 2024-11-16 08:02:15 -03:00
richonguzman
3d01ea03c0 syslog server update 2024-11-07 09:34:27 -03:00
richonguzman
afb6a60bfe readme update 2024-11-06 12:49:37 -03:00
richonguzman
06ef37e4dc eliminando espacios en blanco 2024-11-06 12:47:42 -03:00
richonguzman
9159362796 update 2024-11-06 10:07:44 -03:00
richonguzman
47c136fdd5 correcciones 2024-11-05 23:51:10 -03:00
richonguzman
970d743b80 refactor inis 2024-11-05 23:21:05 -03:00
richonguzman
7c5e58f3b2 ready for testing 2024-11-05 19:14:22 -03:00
richonguzman
210c7acb70 testing3 2024-11-05 18:20:44 -03:00
richonguzman
8d102a73c1 testing2 2024-11-05 18:15:39 -03:00
richonguzman
5030d2d645 casi casi1 2024-11-05 16:59:21 -03:00
richonguzman
a95f55273f up1 2024-11-05 16:10:41 -03:00
richonguzman
4014db03c8 2 listas 2024-11-05 15:14:56 -03:00
richonguzman
d96d5f1963 testing 2024-11-05 14:41:41 -03:00
richonguzman
d1cb732ae1 33 folders 2024-11-05 14:12:56 -03:00
richonguzman
1504bf5a7f code cleaning 2024-10-29 18:57:23 -03:00
richonguzman
3c27e52df7 readme update for T3S3 2024-10-29 10:13:23 -03:00
richonguzman
3d5f3a4914 add Lilygo T3S3 2024-10-29 09:57:26 -03:00
richonguzman
db6292ce59 added but reboots 2024-10-28 09:33:27 -03:00
richonguzman
7995d23779 cleaned 2024-10-25 15:56:03 -03:00
richonguzman
7f150bc4a0 minor letter fix 2024-10-25 15:22:24 -03:00
richonguzman
ff5650f0c5 readme and build update 2024-10-25 12:50:15 -03:00
richonguzman
a91f0f3f3c WIRE update 2024-10-25 11:50:57 -03:00
richonguzman
c9577b6c21 more display testing 2024-10-25 11:08:00 -03:00
richonguzman
10cb7c6e45 added LightGateway 2024-10-25 10:42:15 -03:00
richonguzman
99052e594a typo fix 2024-10-23 12:09:24 -03:00
richonguzman
a05917c494 update 2024-10-22 22:14:29 -03:00
richonguzman
e2e935797f images update for V2.1 2024-10-21 12:26:40 -03:00
richonguzman
5ceff0064f small update to avoid sending gps = 0 2024-10-21 11:16:31 -03:00
richonguzman
8beb7c0465 readme update 2024-10-21 09:09:47 -03:00
richonguzman
112e38312d codingRate4 = 4 does not exists 2024-10-21 09:08:53 -03:00
richonguzman
5a1c6e7ed9 testing GPS 2024-10-21 08:28:35 -03:00
richonguzman
0defd5b289 added HWT with GPS 2024-10-14 17:48:14 -03:00
richonguzman
cdac54c34e cleaning code 2024-10-14 17:44:01 -03:00
richonguzman
71e98487f6 readme and index update 2024-10-14 17:23:42 -03:00
richonguzman
510b5bdc59 index reads better 2024-10-14 17:15:56 -03:00
richonguzman
00db358cc3 GPS for iGate 2024-10-14 17:04:28 -03:00
richonguzman
c0641986aa RxPacket freeze fix 2024-10-14 12:49:01 -03:00
richonguzman
55378bb9f3 readme Update for NTP update 2024-10-14 11:56:48 -03:00
richonguzman
79cf50a630 NTP time added 2024-10-14 11:44:22 -03:00
richonguzman
03ccd2b12e r up 2024-10-11 07:16:44 -03:00
richonguzman
d5ffd26c3c double validation for DigiEcoMode start stop 2024-10-10 18:45:12 -03:00
richonguzman
2f0f991785 readme2 2024-10-09 11:46:45 -03:00
richonguzman
975b140c73 readme1 2024-10-09 11:45:01 -03:00
richonguzman
f2abe34af1 readme Update Digi Eco Mode 2024-10-08 12:25:09 -03:00
richonguzman
a786d83b9e Queries for DigiEcoMode 2024-10-08 12:19:22 -03:00
richonguzman
bf0a0d6e4c EcoMode Digi 2024-10-08 11:04:36 -03:00
richonguzman
d08e5b1b27 ready for testing LowPowerDigi 2024-10-08 01:22:10 -03:00
richonguzman
f387e03b53 testing LowPowerDigi 2024-10-08 01:03:58 -03:00
richonguzman
16215f9e56 minor update for Internal Battery Sleep 2024-10-07 11:10:19 -03:00
richonguzman
ce01d0aa4d minor white spaces kill 2024-10-07 09:39:31 -03:00
richonguzman
b0a05f89f6 readme update 2024-10-06 23:35:51 -03:00
richonguzman
58d8736969 test for Killed Wifi Active 2024-10-06 23:23:22 -03:00
richonguzman
df9060d906 testing not active 2024-10-06 23:02:10 -03:00
richonguzman
2bd6844d89 WiFiAP Active switch added 2024-10-06 22:59:58 -03:00
richonguzman
0a79b6459b added initial corrections to WiFiAP 2024-10-06 22:30:44 -03:00
richonguzman
d5eb32a38b cross digi rules added 2024-10-06 11:37:25 -03:00
richonguzman
d6c5741fde cross digi added 2024-10-06 11:36:37 -03:00
richonguzman
1063a82294 se viene thirdParty 2024-10-05 20:25:29 -03:00
richonguzman
6913b6b89f ahora a probar thirdParty 2024-10-05 20:23:58 -03:00
richonguzman
42c990f5cf !thirdParty con o sin path 2024-10-05 20:21:36 -03:00
richonguzman
a17900e4d0 bme into wxsensor 2024-10-05 08:48:08 -03:00
richonguzman
3fd819bd4e not encoded telemetry as default 2024-09-25 11:23:38 -03:00
richonguzman
640166a50b update for wiki 2024-09-23 17:41:05 -03:00
richonguzman
a9bcae62e2 readme update 2024-09-23 15:15:22 -03:00
richonguzman
3e0368383a better writting 2024-09-23 15:11:35 -03:00
richonguzman
8b790ef656 updated libraries for SDK3 2024-09-23 14:31:45 -03:00
richonguzman
c110b41b99 build update for new boards 2024-09-23 12:42:03 -03:00
richonguzman
be7fe23cd5 Added LLCC68 1W board 2024-09-23 12:37:35 -03:00
richonguzman
202d03c61f shortening reduced Wx report 2024-09-23 12:15:58 -03:00
richonguzman
ddb45544db readme UPdate 2024-09-22 22:35:51 -03:00
richonguzman
c9ea51c6da update Version 2024-09-22 22:34:00 -03:00
richonguzman
72fd2b45ec added mods to include Telemetry 2024-09-22 22:31:33 -03:00
richonguzman
0e46c64880 primera mejora Telemetria 2024-09-22 13:34:13 -03:00
richonguzman
dd12330e85 killing some delays 2024-09-17 14:25:00 -03:00
richonguzman
2edb183cad minor update on Heltec WS 2024-09-11 16:14:40 -03:00
richonguzman
775677d5e8 trying heltec wireless stick oled fix 2024-09-11 15:49:58 -03:00
richonguzman
a60a6d6e4b update WIRE 2024-09-10 14:26:10 -03:00
richonguzman
14abe14703 updated structs of last Stations and 25seg buffer 2024-09-10 14:06:10 -03:00
richonguzman
345fb1e937 up 2024-09-02 09:40:17 -04:00
richonguzman
916eb038e5 updated WiFi.setHostName() 2024-09-02 09:12:18 -04:00
richonguzman
40fc177828 small fix on wire begin 2024-08-30 15:46:50 -04:00
richonguzman
5dc6e99972 low external voltage RF Tx fix 2024-08-28 17:11:50 -04:00
richonguzman
db5710ec6a test 2024-08-28 12:21:54 -04:00
richonguzman
65acb54dbe wrong external sleep voltage definition fix 2024-08-28 11:45:16 -04:00
richonguzman
6088c61e73 versionUpdate + utils beacon message update 2024-08-26 10:33:49 -04:00
Ricardo Guzman (Richonguzman)
a8c972c5e1 Merge pull request #157 from S57PNX/main
Correct logic of serial notifications for APRS-IS and RF beacons
2024-08-26 10:32:01 -04:00
S57PNX
bd3a9f91d6 Merge branch 'richonguzman:main' into main 2024-08-26 10:25:17 +02:00
richonguzman
643a18f40c Wemos S2 Mini DIY LoRa added 2024-08-23 18:05:27 -04:00
richonguzman
46e5bdd21e added led 2024-08-23 18:00:25 -04:00
richonguzman
ea426f4126 board update 2024-08-22 16:27:41 -04:00
richonguzman
5cbdd318a2 test for WEMOS S2 Mini DIY LORA 2024-08-22 15:00:56 -04:00
richonguzman
735988ee1c adding WEMOS S2 MINI 2024-08-22 14:24:30 -04:00
richonguzman
d5b666f458 HELTEC WS update 2024-08-22 13:25:46 -04:00
richonguzman
702f4abcbb update for gps beacon with timestamp 2024-08-21 19:11:05 -04:00
richonguzman
d0070e4381 updated config 2024-08-20 19:01:04 -04:00
richonguzman
f4744eab6a Heltec Wireless Paper base 2024-08-19 11:37:55 -04:00
richonguzman
bd672857f4 testing for external battery block 2024-08-19 10:41:46 -04:00
richonguzman
9cc26ccdae wireless paper battery reading fix 2024-08-18 21:06:57 -04:00
richonguzman
8863838210 updated battery readings 2024-08-17 13:16:44 -04:00
richonguzman
f11b6de57a fix for normal display 2024-08-16 16:00:58 -04:00
richonguzman
55d6bc325f version update 2024-08-16 15:52:55 -04:00
richonguzman
ee687e6959 first test 2024-08-16 15:52:20 -04:00
S57PNX
d1732a6bba Correct logic of serial notifications for APRS-IS
and RF beacons
2024-08-15 20:54:26 +02:00
richonguzman
4fb4712a33 backupDigimode avoided in only rx mode 2024-08-14 13:37:52 -04:00
richonguzman
39276b0d32 update to display h and cpp 2024-08-14 12:32:34 -04:00
richonguzman
22203a7c24 just code cleaning 2024-08-14 12:07:17 -04:00
richonguzman
d8d832fde9 configuration test without fails 2024-08-14 11:50:39 -04:00
richonguzman
61db6c3132 backUpDigiMode and BME for HWSL v3 fix 2024-08-13 17:56:27 -04:00
richonguzman
9521a357b1 readmeUpdate 2024-08-13 14:49:30 -04:00
richonguzman
ee95931f00 web user and password 2024-08-13 14:48:08 -04:00
richonguzman
60aa2b05ac more boards update 2024-08-13 12:46:20 -04:00
richonguzman
bc5183a192 Ht-ct62 updates 2024-08-13 12:36:37 -04:00
richonguzman
1019306c7c wide2-n fix 2024-08-12 13:49:58 -04:00
richonguzman
fbe00903ad version Date on footer added 2024-08-12 11:01:35 -04:00
richonguzman
291a6bc80e added mesh - diy board v1-2 2024-08-12 10:35:45 -04:00
richonguzman
3b550c7f81 mode3 fix WIDE2-2 2024-08-06 10:57:00 -04:00
richonguzman
4659f8b2ff mode3start 2024-08-06 10:11:50 -04:00
richonguzman
62d157d7f0 state add at boot 2024-08-06 09:12:01 -04:00
richonguzman
b0e116ee1d Readme Update Digis 2024-08-05 17:42:59 -04:00
richonguzman
00b12c2bf5 version update 2024-08-05 15:43:42 -04:00
richonguzman
85b21565db index fix 2024-08-05 15:43:02 -04:00
richonguzman
b9e2c0e8f0 digimode 3 improvement 2024-08-05 15:42:33 -04:00
richonguzman
b763b4af78 RNAAAA add + WIDE2 description in index 2024-08-05 15:20:58 -04:00
richonguzman
5553e7ae5c added mode 3 for digirepeater 2024-08-05 14:37:16 -04:00
richonguzman
1afd039c3f digi mode 3 added 2024-08-03 21:55:32 -04:00
richonguzman
1888084f31 WSL v3 wire1 add 2024-08-02 16:15:10 -04:00
richonguzman
dc92b822c8 fixed missing wire begin 2024-08-02 15:28:31 -04:00
richonguzman
3c305259e0 update to recover from wifi AP 2024-08-02 13:08:54 -04:00
richonguzman
497d648c17 ready for testing 2024-07-18 14:41:09 -04:00
richonguzman
d267f6699c HWSL_V3_Display add 2024-07-17 16:20:20 -04:00
richonguzman
16ce6bc6d2 a7670 voltage calibration fix 2024-07-17 10:47:42 -04:00
richonguzman
008b6250ea Non Ham mods only for Rx 2024-07-10 15:13:57 -04:00
richonguzman
ddb83d1368 voltageCalibrationCorrection 2024-07-10 09:49:13 -04:00
richonguzman
26e2dec2b2 font size update 2024-07-09 10:41:26 -04:00
richonguzman
174507377b fix for other boards 2024-07-09 08:39:59 -04:00
richonguzman
864ed44a5d favicon and battery adc calibration 2024-07-08 23:29:04 -04:00
richonguzman
5d69cfbe1c 1 2024-07-08 23:06:03 -04:00
richonguzman
e57d356fd1 new ADC calibration test 2024-07-08 23:02:45 -04:00
richonguzman
8381f0916c variable name change 2024-07-05 13:12:21 -04:00
richonguzman
beca09293a queryUppercase fix 2024-07-05 11:58:18 -04:00
richonguzman
dcca18eeac readme Update 2024-07-01 18:30:11 -04:00
richonguzman
2494acea55 espacio en blanco 2024-06-30 10:36:15 -04:00
richonguzman
0c50ce8de4 tyeofpacket update 2024-06-30 09:24:21 -04:00
richonguzman
2f07d35861 readmeupdate7 2024-06-28 16:20:07 -04:00
richonguzman
91240c5be6 readmeupdate6 2024-06-28 16:19:18 -04:00
richonguzman
b4308a95d5 readmeupdate5 2024-06-28 16:18:41 -04:00
richonguzman
6c301795d6 readmeupdate4 2024-06-28 16:18:13 -04:00
richonguzman
5f0c6f4986 readmeupdate3 2024-06-28 16:15:56 -04:00
richonguzman
7c989e6e9e readmeupdate2 2024-06-28 16:15:22 -04:00
richonguzman
2bcda6d3a4 readme update 2024-06-28 16:14:42 -04:00
richonguzman
848492dc36 updated QueryAnswer for 3rdPartyPacket 2024-06-28 16:05:04 -04:00
richonguzman
413bef67fb readme update 2024-06-27 16:25:58 -04:00
richonguzman
3fa4c9371a new battery web config 2024-06-27 16:02:20 -04:00
richonguzman
e846c38f97 externalBatdivider 2024-06-27 15:39:53 -04:00
richonguzman
b8446e3f1d bat and sh gone 2024-06-27 13:19:51 -04:00
richonguzman
c3984bc8da casi listo para prueba 2024-06-26 23:42:24 -04:00
richonguzman
601b72da9f funciona 2024-06-26 18:13:02 -04:00
richonguzman
5d9493314d casi 2024-06-26 17:56:11 -04:00
richonguzman
86cf12d6c1 carga datos base 2024-06-26 17:10:07 -04:00
richonguzman
e5e2c6a980 readmeUpdate 2024-06-26 13:26:17 -04:00
richonguzman
3b9d49c5f9 personalNote implementation 2024-06-26 13:24:36 -04:00
richonguzman
ab9443140b personalNote1 2024-06-26 13:05:45 -04:00
richonguzman
ad1129c588 callsign validation process fix 2024-06-25 16:36:49 -04:00
richonguzman
8bb0b0446c more 3rd party filtering 2024-06-24 13:25:30 -04:00
richonguzman
6b1d319901 fix firstColon on last position 2024-06-24 11:36:40 -04:00
richonguzman
1ceaf159ba colonIndex fix3 2024-06-24 11:18:42 -04:00
richonguzman
f1de8d2df0 colonIndex upload fix 2024-06-24 11:16:52 -04:00
richonguzman
b9b4f46c66 typeOfPacket fix3 2024-06-24 11:02:38 -04:00
richonguzman
22b2c679d2 typeofPacket initial fix 2024-06-24 10:54:21 -04:00
richonguzman
7baa98bf7e first 3rdparty correction 2024-06-24 10:32:58 -04:00
richonguzman
35e79709e3 HeltecWSL V3 battery reading fix 2024-06-21 15:33:56 -04:00
richonguzman
f4bae74c26 build fix 2024-06-21 11:38:54 -04:00
richonguzman
04e721ac5a build correction 2024-06-21 11:15:52 -04:00
richonguzman
1922d7453b readmeUpdate 3rd Party 2024-06-21 11:11:40 -04:00
richonguzman
f1bd751585 readmeUpdate3rdParty 2024-06-21 11:02:34 -04:00
richonguzman
c346bc0f39 version and readme update 2024-06-21 10:58:54 -04:00
richonguzman
bbc1996918 update const String 2024-06-21 02:13:34 -04:00
richonguzman
505bb3d5a4 version update 2024-06-20 21:14:30 -04:00
richonguzman
b97457146a many updates const String and 3rd party 2024-06-20 21:12:54 -04:00
richonguzman
64b51fea72 first test with new 3rdParty 2024-06-20 14:06:14 -04:00
richonguzman
d82cf9235d starting with builing Tx packet 2024-06-18 23:42:09 -04:00
richonguzman
d83bb19bc8 A7670 code cleanup 2024-06-18 19:55:33 -04:00
richonguzman
0feb0045d0 build update 2024-06-18 19:36:17 -04:00
richonguzman
4999c89820 all boards with 915 versions added 2024-06-18 19:30:34 -04:00
richonguzman
f5048edf84 all sx1276 added 2024-06-18 18:43:25 -04:00
richonguzman
691f60925a only digi with packetbuffer25seg 2024-06-18 16:45:01 -04:00
richonguzman
4bf4e781df not callsign filtering from aprs-is feed 2024-06-18 15:29:00 -04:00
richonguzman
0c09f5a934 callsign filter changed 2024-06-18 15:15:27 -04:00
richonguzman
494be4a393 readmeupdate5 2024-06-17 21:41:29 -04:00
richonguzman
16e79d291c readmeupdate4 2024-06-17 21:40:54 -04:00
richonguzman
c9a52d5b61 readmeupdate3 2024-06-17 21:40:30 -04:00
richonguzman
7d813524e3 readme update2 2024-06-17 21:39:26 -04:00
richonguzman
2d7d02f2df update readme 2024-06-17 21:37:53 -04:00
richonguzman
562b0a46ea update of Configuration images 2024-06-17 20:32:39 -04:00
richonguzman
69c074a834 readmeMods 2024-06-16 20:55:54 -04:00
richonguzman
bf04507ed0 new webflasher image 2024-06-16 20:53:36 -04:00
richonguzman
e653d39004 build updated for missing letter in sx1268 tbeam 2024-06-15 09:50:30 -04:00
richonguzman
68edade5b4 readyForNewRelease1.0.3 2024-06-15 09:01:33 -04:00
richonguzman
685d629ef6 callsign test corrected 2024-06-14 23:25:47 -04:00
richonguzman
b7f6d84ac8 build test 2024-06-14 15:15:43 -04:00
richonguzman
dda4b06714 update 2024-06-14 11:01:32 -04:00
richonguzman
6095b4ff72 Si7021 fix for HELTEC V3 2024-06-13 13:36:18 -04:00
richonguzman
f77d7078b6 updateVersion 2024-06-12 09:34:13 -04:00
richonguzman
1dc1a291cd not funny 2024-06-12 09:23:01 -04:00
richonguzman
4396078d76 readmeUpdate 2024-06-10 17:02:27 -04:00
richonguzman
9924fb9a38 test 2024-06-10 14:13:28 -04:00
richonguzman
72a6c48553 testing ESP32C31WLORA 2024-06-10 14:09:30 -04:00
richonguzman
217e5b07e1 ESP32C3 1W DIY TEST 2024-06-10 14:00:15 -04:00
richonguzman
ce7ed2519f version update 2024-06-10 13:17:01 -04:00
richonguzman
72f0c80a29 betterCallsign Validation process 2024-06-10 11:33:26 -04:00
richonguzman
62db4c18da testing SI7021 2024-06-09 22:25:15 -04:00
richonguzman
7e91317730 update 2024-06-08 15:59:16 -04:00
richonguzman
49cffe7d0c avoid 3 bytes again 2024-06-08 15:50:55 -04:00
richonguzman
7537c1ee50 readmeUpdate 2024-06-08 15:28:16 -04:00
richonguzman
b2a8def481 update for NOCALL-10 2024-06-08 15:26:49 -04:00
richonguzman
b3441ece02 WIDE fix when not present 2024-06-08 14:39:15 -04:00
richonguzman
6ee86f7557 const corrections 2024-06-08 14:12:20 -04:00
richonguzman
0f5330141f testing state 2024-06-08 13:57:58 -04:00
richonguzman
d04b009a49 test1 2024-06-08 12:52:47 -04:00
richonguzman
397a94cc47 callsignValidationTest 2024-06-07 17:29:55 -04:00
richonguzman
a8e1a48d62 version update 2024-06-07 00:16:03 -04:00
richonguzman
d75be8f007 code cleaning 2024-06-07 00:10:04 -04:00
richonguzman
c89ca7316f more const cleaning 2024-06-05 23:49:16 -04:00
richonguzman
18de124c04 switch and display cleaning 2024-06-05 23:20:34 -04:00
richonguzman
01cee5d51e libraries update 2024-06-04 13:33:34 -04:00
richonguzman
005f114a97 more - less code 2024-06-03 00:02:43 -04:00
richonguzman
e133d50cd9 syslog Fix 2024-05-31 15:09:28 -04:00
richonguzman
d3ebe43ebb readme update 2024-05-31 07:58:40 -04:00
richonguzman
c67f5b60e5 minor code cleaning 2024-05-30 20:04:46 -04:00
richonguzman
d2923cc0b6 SignalReport Query Added 2024-05-30 19:58:33 -04:00
richonguzman
a87cc0be58 testing lowerMemory Ussssage 2024-05-30 16:27:07 -04:00
richonguzman
e63d54d9f0 signalReport available 2024-05-30 15:38:30 -04:00
richonguzman
f6793c01c4 syslog call and code update 2024-05-30 15:12:34 -04:00
richonguzman
12bcc76452 deepSleepReboot update 2024-05-29 15:46:07 -04:00
richonguzman
4cf3a333c9 deepSleep update 2024-05-29 15:43:23 -04:00
richonguzman
c5217842ff Battery Monitor for T-Beams 2024-05-27 11:46:14 -04:00
richonguzman
e9bb0391f2 update for minSleepVoltage recovery 2024-05-26 21:51:22 -04:00
richonguzman
edd2ff65b9 update3 2024-05-25 14:08:57 -04:00
richonguzman
dcc0340c90 buildUpdate2.3 2024-05-25 14:00:27 -04:00
richonguzman
4aa8f1aba8 buildUpdate2.2 2024-05-25 13:58:52 -04:00
richonguzman
7c7b8507eb buildUpdate2.1 2024-05-25 13:57:45 -04:00
richonguzman
3d1ae2fa70 buildUpdate2 2024-05-25 13:57:26 -04:00
richonguzman
095cc7edae buildUpdate1 2024-05-25 12:57:39 -04:00
richonguzman
c1d1069785 update6-12 2024-05-24 18:36:43 -04:00
richonguzman
bc29afa94e update6-11 2024-05-24 18:19:24 -04:00
richonguzman
eeba0df050 update6-10 2024-05-24 18:12:39 -04:00
richonguzman
8f40563ae5 update6-5 2024-05-24 17:39:05 -04:00
richonguzman
8bcf95548c update6-4 2024-05-24 17:34:40 -04:00
richonguzman
6809d96b59 update6-3 2024-05-24 17:30:04 -04:00
richonguzman
09bf14f8d1 update6-2 2024-05-24 17:13:57 -04:00
richonguzman
666c57738b update6-1 2024-05-24 17:02:07 -04:00
richonguzman
e4d41380bf update6 2024-05-24 16:48:55 -04:00
richonguzman
afdde6ad2d update4 2024-05-24 16:15:04 -04:00
richonguzman
f7ce94c494 update3 2024-05-24 16:07:02 -04:00
richonguzman
ce69b0c950 update2 2024-05-24 15:55:21 -04:00
richonguzman
6e381fe7fc update 2024-05-24 15:45:17 -04:00
richonguzman
dcc549c5ba update build comming 2024-05-24 15:18:21 -04:00
richonguzman
618be275af battery monitor update 2024-05-24 14:42:39 -04:00
richonguzman
2c71e3ed65 update for HT-CT62 2024-05-24 14:27:39 -04:00
richonguzman
3589f54d02 full update for BatteryHealthMonitor 2024-05-24 14:24:40 -04:00
richonguzman
c6687529da comienzo dormir ext 2024-05-24 13:05:28 -04:00
richonguzman
3c2c49aa01 updated 2024-05-24 11:59:06 -04:00
richonguzman
9044b93090 ordenando 2024-05-24 11:48:12 -04:00
richonguzman
076f3bfcb5 funciona 2024-05-24 11:08:08 -04:00
richonguzman
c6888adca8 battery5-1 2024-05-24 10:43:06 -04:00
richonguzman
b06b91d258 starting 2024-05-24 10:00:21 -04:00
richonguzman
bbdcf8ce0d new names ok 2024-05-23 22:52:17 -04:00
richonguzman
992641578d battery 5 andando 2024-05-23 20:39:21 -04:00
richonguzman
f70d836285 reorden index 2024-05-23 20:29:35 -04:00
richonguzman
d50b0767c4 battery basico funciona 2024-05-23 20:22:12 -04:00
richonguzman
fc8d3fd9fb justo antes de fallar 2024-05-23 20:13:05 -04:00
richonguzman
345a6c2c21 a 2024-05-23 19:27:10 -04:00
richonguzman
5986125661 wifiInterval not used 2024-05-23 18:29:08 -04:00
richonguzman
6090b83d34 web edit 2024-05-22 19:24:28 -04:00
richonguzman
7cc1a759cc readme update 2024-05-22 19:20:27 -04:00
richonguzman
51f9e0f0cc Reboot Mode Added 2024-05-22 19:19:25 -04:00
richonguzman
960df01d61 readme update again 2024-05-22 16:45:21 -04:00
richonguzman
60a8fe01a3 readme update 2024-05-22 16:43:49 -04:00
richonguzman
46d8b12896 Testing backupDigiMode return2 2024-05-22 16:41:20 -04:00
richonguzman
5296b19ef2 first test backupDigiMode 2024-05-22 16:19:45 -04:00
richonguzman
35fbd7a5dc testing backupDigiMode 2024-05-22 15:41:25 -04:00
richonguzman
adf4c1fcb8 testing Mode5 return1 2024-05-22 15:29:00 -04:00
richonguzman
0921d01340 starting mods 2024-05-22 11:41:08 -04:00
richonguzman
0c81df7cc5 lora power mod 2024-05-22 10:54:20 -04:00
richonguzman
789b3732f3 fix battery calculations 2024-05-22 08:36:54 -04:00
richonguzman
8be573e2b6 new buildPacketToTx 2024-05-21 00:00:23 -04:00
richonguzman
7022046f21 new order in WebUi 2024-05-20 23:33:59 -04:00
richonguzman
d19284df12 Objects received 2024-05-20 23:03:57 -04:00
richonguzman
05a5c096c3 ready to test Objects to Rf 2024-05-20 21:54:55 -04:00
richonguzman
7add09d346 tx objects 2024-05-20 17:57:58 -04:00
richonguzman
9e87c50966 welcomeMessage update 2024-05-20 11:18:01 -04:00
richonguzman
9a33df49e3 testing for heltec v2_1 2024-05-20 07:49:52 -04:00
richonguzman
916cbade8c testing new Syslog Comments info 2024-05-19 23:34:29 -04:00
richonguzman
2496c12763 WemosD1 update 2024-05-18 11:51:28 -04:00
richonguzman
166773b81d Wemos D1 added 2024-05-18 11:48:50 -04:00
richonguzman
b8d000356a boards_pinout change 2024-05-18 11:27:14 -04:00
richonguzman
49d3801e83 updateNewStuff 2024-05-17 15:14:58 -04:00
richonguzman
9dead41e79 version update 2024-05-17 15:11:59 -04:00
richonguzman
f3eaa28c76 lora code update 2024-05-17 09:01:15 -04:00
richonguzman
1bdfa3a117 adding Heltec WSL v3 2024-05-17 00:12:46 -04:00
richonguzman
8af1f4cea3 another Radiolib Test 2024-05-16 23:22:27 -04:00
richonguzman
d3e45c57e0 test1 2024-05-16 17:52:25 -04:00
richonguzman
8024925950 HELTEC WSL V3 added 2024-05-16 15:43:13 -04:00
richonguzman
8e6346aa97 build mods 2024-05-16 14:35:53 -04:00
richonguzman
cfca59cf20 const String 2 2024-05-15 17:47:29 -04:00
richonguzman
2430f92193 tnc + station const String 2024-05-15 16:41:07 -04:00
richonguzman
3cef4676a7 A7670 const String 2024-05-15 16:30:00 -04:00
richonguzman
87a1830970 adding decimal to BME Pressure measurement 2024-05-15 12:12:05 -04:00
richonguzman
b8fb7cfba0 readme update 2024-05-14 11:44:28 -04:00
richonguzman
3b81460798 HELTEC_V3 BME680 fix 2024-05-14 11:43:29 -04:00
richonguzman
630ec062c1 HT_CT_62 beaconPacket fix 2024-05-14 09:23:22 -04:00
richonguzman
dc39eec941 testing heltecv3 0.3 2024-05-14 09:14:25 -04:00
richonguzman
bdf3fe7cbc test HELTECv3 0.2 2024-05-14 09:08:24 -04:00
richonguzman
3de3aac026 testing HeltecV3 0.1 2024-05-14 08:59:47 -04:00
richonguzman
3a148fa55d testing heltecv3 2024-05-14 08:47:57 -04:00
richonguzman
c7e79eba43 bme680 fix for heltec v3 2024-05-14 02:01:24 -04:00
richonguzman
8429bb4730 25SegBuffer fix 2024-05-14 01:14:02 -04:00
richonguzman
c940cb99d9 testing actions 2024-05-14 00:36:32 -04:00
richonguzman
0be60b76b0 mayor code cleaning 2024-05-13 23:30:15 -04:00
richonguzman
794cc0640f buffer implemented 2024-05-13 21:47:52 -04:00
richonguzman
c5dcfd86b2 buffer 0.1 2024-05-13 21:24:44 -04:00
richonguzman
4d4598e09e buffer added to digimode 2024-05-13 21:00:42 -04:00
richonguzman
2e4ee4792e 1 2024-05-13 20:29:08 -04:00
richonguzman
bee09386e1 less lora state validations 2024-05-13 18:12:51 -04:00
richonguzman
0a15813ef2 WebUpdate for auto BME detect 2024-05-13 17:17:07 -04:00
richonguzman
6ecb733bea saving works kinda 2024-05-13 16:08:27 -04:00
richonguzman
1345ddd608 index1.1 2024-05-13 14:48:07 -04:00
richonguzman
5c0fba60b5 index mod1 2024-05-13 14:40:30 -04:00
richonguzman
67c2a61f09 first test mod WebUI 2024-05-13 14:13:00 -04:00
richonguzman
f1e712a363 bme autodetect 2024-05-13 14:00:07 -04:00
richonguzman
7753618756 test startup delay 1 2024-05-13 08:50:34 -04:00
richonguzman
61168d9217 Startup delay test added 2024-05-13 08:45:19 -04:00
richonguzman
d9af677f69 tft rotation fix 2024-05-12 22:57:45 -04:00
richonguzman
cc976a4a4a small code refactor 2024-05-12 22:02:12 -04:00
Ricardo Guzman (Richonguzman)
e6402409fc bme comment fix 2024-05-12 11:42:28 -04:00
richonguzman
12860410fd code cleaning1 2024-05-11 12:59:25 -04:00
richonguzman
886b661a62 CodeCleaning 2024-05-11 12:59:07 -04:00
richonguzman
53461f289c batteryReading for Heltec WT 2024-05-11 12:38:05 -04:00
richonguzman
54496a9f86 readme update 2024-05-11 12:29:10 -04:00
richonguzman
5e0294fb14 typeOfPacket fix 2024-05-11 12:27:13 -04:00
richonguzman
d7c1c70f10 TFT fix and ??? in beacon Rx 2024-05-11 12:19:19 -04:00
richonguzman
ad507cb4c9 Heltec WT added1 2024-05-11 12:02:08 -04:00
richonguzman
c54843622c setRxBoostedGainMode 2024-05-10 23:18:27 -04:00
richonguzman
2d820bbcc4 callsign always as uppercase 2024-05-10 16:36:28 -04:00
richonguzman
691f967aaf git Ignore update 2024-05-10 10:20:04 -04:00
richonguzman
3b85a3a5bf small battery measurement update 2024-05-09 13:49:09 -04:00
richonguzman
983467e059 BME warning fix 2024-05-08 15:05:43 -04:00
richonguzman
2b6066f62e versionUpdate 2024-05-05 08:17:18 -04:00
richonguzman
b81d986c58 blackSpacekiller 2024-05-05 08:16:58 -04:00
richonguzman
71c5e608b6 better heared station function 2024-05-05 08:16:15 -04:00
richonguzman
db24293b91 better definitions 2024-05-02 15:31:31 -04:00
richonguzman
611ea87264 better battery code 2024-05-02 13:41:21 -04:00
richonguzman
04a7f89891 code cleaning in power_utils 2024-05-02 12:57:18 -04:00
richonguzman
d744f0a0f0 heltec v3 battery test 2024-05-01 08:06:31 -04:00
richonguzman
a14c570db0 heltecV3 battery readings fix 2024-04-30 17:18:02 -04:00
richonguzman
708742f43d update display toggle 2024-04-29 09:19:25 -04:00
richonguzman
e182dd163e code cleaning 2024-04-28 09:30:56 -04:00
richonguzman
b2b388da99 syslog update 2024-04-28 09:06:45 -04:00
richonguzman
c6929e161a readme update1.1 2024-04-26 20:56:44 -04:00
richonguzman
26e4984225 readme update1 2024-04-26 20:55:54 -04:00
richonguzman
fe41189418 black space again 2024-04-23 22:40:03 -04:00
richonguzman
9742e25100 sx1276 && 915 added 2024-04-23 22:25:20 -04:00
richonguzman
e2c3edd0f1 delete old images 2024-04-23 17:13:03 -04:00
richonguzman
ffc3bd7624 batteryPin measurement A7670 2024-04-23 16:37:49 -04:00
richonguzman
7743cbe221 battery pin update 2024-04-23 16:36:08 -04:00
richonguzman
da0178bc56 battery and led pin add 2024-04-23 16:28:19 -04:00
richonguzman
5f145751c0 ESP32 & A7670 & LoRa added 2024-04-23 14:10:04 -04:00
richonguzman
cda72194db Add ESP32 & Modem A7670 2024-04-23 13:57:21 -04:00
richonguzman
47e6623881 first test of Modem 2024-04-23 12:30:57 -04:00
richonguzman
7e4fdff0a3 digi packet process fix 2024-04-23 10:53:53 -04:00
richonguzman
eb472b1506 first library cleaning in platformio 2024-04-23 10:29:46 -04:00
richonguzman
f2a6bbf0a9 start addA7670 2024-04-23 09:16:39 -04:00
richonguzman
3df25d71e2 readme update 2024-04-22 13:38:03 -04:00
richonguzman
1b5a89b31b Merge branch 'main' of https://github.com/richonguzman/LoRa_APRS_iGate 2024-04-22 13:33:21 -04:00
richonguzman
bd8c8e2eb1 readme change and version update 2024-04-22 13:31:02 -04:00
richonguzman
2b0cc514f0 WEMOS LOLIN32 added for testing 2024-04-22 13:25:52 -04:00
richonguzman
deb4ea6974 heltec v3 battery 2024-04-20 14:02:11 -04:00
richonguzman
5333a68d04 ht-ce62 fix 2024-04-20 13:17:25 -04:00
richonguzman
7ab5ffa81d more cleaning 2024-04-20 12:38:45 -04:00
richonguzman
3964354844 QTH images add for wiki 2024-04-20 12:18:19 -04:00
Ricardo Guzman (Richonguzman)
1eee529f9f qth image add 2024-04-20 12:15:15 -04:00
richonguzman
38b81ffbd7 version update 2024-04-20 11:09:11 -04:00
richonguzman
7dfbaf45ef readme update 5 2024-04-20 11:08:50 -04:00
richonguzman
13adc2d2d0 upload images 2024-04-20 11:00:04 -04:00
Ricardo Guzman (Richonguzman)
ce088035ff adding pinpoint and aprsis32 2024-04-20 10:57:47 -04:00
richonguzman
4eb46d59f6 readme update 4 2024-04-20 10:55:09 -04:00
richonguzman
2cbb6c1a1a readme update3 2024-04-20 10:52:26 -04:00
Ricardo Guzman (Richonguzman)
507a365b88 new webflasher image 2024-04-20 10:50:47 -04:00
richonguzman
5f04468285 readme update2 2024-04-20 10:43:38 -04:00
richonguzman
fc2a35a058 readme update 2024-04-20 10:41:48 -04:00
richonguzman
7987ceb262 final mods to test outputbuffer 2024-04-20 10:06:22 -04:00
richonguzman
8f1629abbc lastRxTime added to process buffer 2024-04-20 09:37:22 -04:00
richonguzman
b4f2daca42 outputBuffer Created-all LoRa send to Buffer 2024-04-20 09:27:20 -04:00
richonguzman
ce058ed424 HELTEC V3 battery input fix 2024-04-19 11:06:26 -04:00
Ricardo Guzman (Richonguzman)
b00b29a5da Merge pull request #76 from SQ2CPA/main
Merged bins for web flash
2024-04-16 15:25:52 -04:00
SQ2CPA
a64899d44c chore: workflow for merged bins 2024-04-16 21:16:28 +02:00
richonguzman
af9f3fd09b json memory fix 2024-04-16 15:01:53 -04:00
richonguzman
0fb2c95338 just small typo 2024-04-15 11:00:35 -04:00
richonguzman
3885a43324 update 2024-04-13 13:06:13 -04:00
Ricardo Guzman (Richonguzman)
0695e6643e Merge pull request #74 from SQ2CPA/main
Received packets list
2024-04-13 12:48:54 -04:00
SQ2CPA
71473164f4 feat: received packets list 2024-04-13 18:20:10 +02:00
richonguzman
8e505df768 duplicated comment fix 2024-04-11 11:07:28 -04:00
richonguzman
81295bc3e2 removing LoRa dot h library 2024-04-10 09:13:56 -04:00
richonguzman
16f5338953 testing syslog 2024-04-09 12:51:51 -04:00
richonguzman
0b22d27b40 status mod for TCPIP and RF 2024-04-09 12:20:31 -04:00
richonguzman
aea0aa21af rx Fix 2024-04-09 11:20:09 -04:00
richonguzman
a6ce44f8d3 new Compressed GPS data instead of classic NMEA 2024-04-09 11:08:23 -04:00
richonguzman
3ce5d08c44 typeOfPacket fix 2024-04-09 10:01:44 -04:00
richonguzman
51b59ab555 crc mismatch fix 2024-04-07 08:16:28 -04:00
richonguzman
b9024d34cb RFONLY coma fix 2024-04-05 12:10:12 -03:00
richonguzman
e4bc8eecbe RFONLY update 2024-04-04 15:54:49 -03:00
Ricardo Guzman (Richonguzman)
c10645c2c0 Merge pull request #68 from SQ2CPA/main
Migrate from LoRa.h into RadioLib and more
2024-04-04 14:09:13 -03:00
SQ2CPA
0d64c65aa5 feat: add build env and date to web ui footer 2024-04-04 18:28:38 +02:00
SQ2CPA
d16e407269 improvement: use LoRa WX icon when using BME 2024-04-04 17:58:07 +02:00
SQ2CPA
c8cde0a404 fix: use path from config everywhere 2024-04-04 17:56:37 +02:00
SQ2CPA
c1cc7c9ab0 fix: use battery settings for comment in low power mode 2024-04-04 17:55:20 +02:00
SQ2CPA
4d23681935 improvement: migrate from lora.h to radiolib 2024-04-04 17:42:36 +02:00
richonguzman
1bcb119cad 1 2024-03-29 10:35:49 -03:00
richonguzman
43c0021ba7 supported boards text add and link 2024-03-29 10:35:09 -03:00
richonguzman
daa245ff7c supported boards add 2024-03-29 10:33:45 -03:00
richonguzman
e52ac86e3e sender vs Sender fix 2024-03-28 16:28:14 -03:00
Ricardo Guzman (Richonguzman)
c1bb6e39ef Merge pull request #64 from SQ2CPA/main
Experimental Low power mode and Low voltage cut off
2024-03-28 16:20:08 -03:00
SQ2CPA
0e94444198 fix: rename heltec wireless lite to ht-ct62 2024-03-28 18:33:11 +01:00
SQ2CPA
968d9188be improvement: low voltage cut off as web ui option 2024-03-28 18:28:31 +01:00
SQ2CPA
db21a08904 chore: ota update binary in installer zip 2024-03-28 18:01:42 +01:00
SQ2CPA
14999d1b66 feat: low power mode 2024-03-28 18:01:42 +01:00
SQ2CPA
a8a7c3e3a3 fix: missing sender variable 2024-03-28 18:01:42 +01:00
richonguzman
ca3c1eaf9f uint8_t update 2024-03-28 10:22:54 -03:00
richonguzman
dec018f232 new picture 2024-03-28 00:11:36 -03:00
richonguzman
f49472efd1 new igate picture 2024-03-28 00:05:46 -03:00
richonguzman
df3b15ff35 just small code cleaning 2024-03-27 15:28:27 -03:00
richonguzman
d004efb193 version Update 2024-03-27 13:12:47 -03:00
richonguzman
d98334a489 preparing for Notification LowVoltBattery 2024-03-27 11:42:04 -03:00
richonguzman
1365ae67a1 update in APRS listening 2024-03-27 11:26:02 -03:00
richonguzman
110c6d600b esp32 super mini killed 2024-03-26 17:43:50 -03:00
richonguzman
9557df983b test1 2024-03-26 16:43:17 -03:00
richonguzman
0e9dba76a8 low_voltage_cutoff settings 2024-03-24 10:01:54 -03:00
richonguzman
caa48694b4 new date 2024-03-23 23:48:32 -03:00
Ricardo Guzman (Richonguzman)
0ac2705609 fix lora problem 2024-03-23 23:36:18 -03:00
Ricardo Guzman (Richonguzman)
ed9d80fd78 Add files via upload 2024-03-23 09:48:12 -03:00
Ricardo Guzman (Richonguzman)
cb7e9f3235 Merge pull request #57 from SQ2CPA/main
Hotfix TNC config only if available
2024-03-22 16:45:54 -03:00
SQ2CPA
8fdab82684 fix: tnc config only if available 2024-03-22 20:42:55 +01:00
Ricardo Guzman (Richonguzman)
0d1162786c Merge pull request #56 from SQ2CPA/main
TNC and few more changes and fixes
2024-03-22 15:28:14 -03:00
SQ2CPA
0b40cb0889 fix: path is not required 2024-03-22 19:07:35 +01:00
SQ2CPA
e33b639f12 fix: saving empty APs 2024-03-22 19:07:35 +01:00
SQ2CPA
15be5fc611 feat: battery voltage cutoff monitor 2024-03-22 19:07:35 +01:00
SQ2CPA
a44c6ca96c chore: commit test build workflow 2024-03-22 19:07:35 +01:00
SQ2CPA
7897e9a5be feat: heltec wireless stick lite board 2024-03-22 19:07:35 +01:00
SQ2CPA
a33875a608 fix: send battery voltage as separate 2024-03-22 19:07:35 +01:00
SQ2CPA
5337cb2d46 feat: send beacon and reboot buttons 2024-03-22 19:07:35 +01:00
SQ2CPA
67e1c528f3 refactor: tnc handle input data 2024-03-22 19:07:35 +01:00
SQ2CPA
9f617c406e feat: new navbar 2024-03-22 19:07:35 +01:00
SQ2CPA
d9629d0929 feat: serial kiss 2024-03-22 19:07:32 +01:00
SQ2CPA
2a5cc33711 feat: tnc server 2024-03-22 19:05:09 +01:00
richonguzman
6bd6cce82b digiMode self hearing fix 2024-03-21 15:21:29 -03:00
richonguzman
54bbede56c digimode fix 2024-03-21 15:19:30 -03:00
richonguzman
85b7596cbb minor code update 2024-03-21 00:09:35 -03:00
richonguzman
fb4b6a25bd signalBandwidth fixed 2024-03-19 22:41:07 -03:00
richonguzman
4c30422635 refactor queries 2024-03-18 14:48:56 -03:00
richonguzman
e059c65a99 testing Queries for DigiMode 2024-03-18 10:38:16 -03:00
richonguzman
3fda399ecf OE5HWN_MeshCom readme update 2024-03-18 09:40:57 -03:00
richonguzman
492f571183 OE5HWN_MeshCom added 2024-03-18 09:40:05 -03:00
richonguzman
b287704293 start 2024-03-18 08:42:18 -03:00
richonguzman
d802fb4c91 readme update 2024-03-12 16:45:22 -03:00
richonguzman
4247d00be4 TNC Text cleaning 2024-03-10 12:16:08 -03:00
richonguzman
7401c36fdf readme update 2024-03-10 09:32:50 -03:00
Ricardo Guzman (Richonguzman)
cc2d4a669d Merge pull request #44 from richonguzman/adding-HELTEC-Wireless-Stick
Adding heltec wireless stick
2024-03-09 20:18:51 -03:00
richonguzman
706d84cf07 adding WebUI pics 2024-03-09 20:14:24 -03:00
richonguzman
4ae33a8362 first Test Heltec WS 2024-03-09 09:43:51 -03:00
Ricardo Guzman (Richonguzman)
5334260a30 Merge pull request #43 from SQ2CPA/main
Changes before release
2024-03-08 15:03:49 -03:00
SQ2CPA
0868448a8c improvement: required fields 2024-03-08 18:03:42 +01:00
SQ2CPA
849d5d7c34 fix: digi mode 2024-03-08 18:03:42 +01:00
SQ2CPA
6f426edaf4 fix: av false positive 2024-03-08 18:03:42 +01:00
richonguzman
d50f0dd300 date change 2024-03-07 22:48:05 -03:00
Ricardo Guzman (Richonguzman)
5dd6108350 Merge pull request #42 from SQ2CPA/main
web support footer
2024-03-07 14:56:59 -03:00
SQ2CPA
c2b4c612df feat: web support footer 2024-03-07 18:55:35 +01:00
Ricardo Guzman (Richonguzman)
32fc475a1b Merge pull request #41 from SQ2CPA/main
stationMode removed and more features
2024-03-07 13:58:43 -03:00
SQ2CPA
eafd7d4e81 fix: default aprsis filter 2024-03-07 17:50:47 +01:00
SQ2CPA
43ea66e304 refactor: remove old firmware bin 2024-03-07 17:47:24 +01:00
SQ2CPA
a46a97691d feat: stationMode removed 2024-03-07 17:46:50 +01:00
SQ2CPA
8a7d91a7f6 improvement: workaround for windows via lnk 2024-03-07 17:46:11 +01:00
SQ2CPA
7af7ca5fba feat: enable esp prog debugger 2024-03-07 17:45:41 +01:00
richonguzman
ca995821c8 BME fixes 2024-03-06 09:01:50 -03:00
richonguzman
e8c903d20c BME fix 2024-03-06 09:01:17 -03:00
185 changed files with 12460 additions and 3456 deletions

View File

@@ -11,16 +11,92 @@ jobs:
fail-fast: false
matrix:
target:
- "ttgo-lora32-v21"
- "heltec-lora32-v2"
- "heltec_wifi_lora_32_V3"
- "ESP32_DIY_LoRa"
- "ESP32_DIY_1W_LoRa"
- "ttgo-t-beam-v1_2"
- "ttgo-t-beam-v1"
- "ttgo-t-beam-v1_SX1268"
- "ttgo-t-beam-v1_2_SX1262"
# - "heltec_wireless_stick_lite" # NOT FULLY TESTED
- name: ttgo-lora32-v21
chip: esp32
- name: ttgo-lora32-v21_915
chip: esp32
- name: ttgo_lora32_t3s3_v1_2
chip: esp32s3
- name: heltec-lora32-v2
chip: esp32
- name: heltec_wifi_lora_32_V3
chip: esp32s3
- name: heltec_wifi_lora_32_V3_2
chip: esp32s3
- name: heltec_wireless_stick
chip: esp32s3
- name: heltec_wireless_stick_lite_v3
chip: esp32s3
- name: heltec_wireless_stick_lite_v3_display
chip: esp32s3
- name: heltec_wireless_bridge
chip: esp32
- name: ESP32_DIY_LoRa
chip: esp32
- name: ESP32_DIY_LoRa_915
chip: esp32
- name: ESP32_DIY_1W_LoRa
chip: esp32
- name: ESP32_DIY_1W_LoRa_915
chip: esp32
- name: ESP32_DIY_1W_LoRa_LLCC68
chip: esp32
- name: ESP32_DIY_1W_LoRa_Mesh_V1_2
chip: esp32
- name: ttgo-t-beam-v1_2
chip: esp32
- name: ttgo-t-beam-v1_2_915
chip: esp32
- name: ttgo-t-beam-v1
chip: esp32
- name: ttgo-t-beam-v1_915
chip: esp32
- name: ttgo-t-beam-v1_SX1268
chip: esp32
- name: ttgo-t-beam-v1_2_SX1262
chip: esp32
- name: ttgo_t_deck_plus
chip: esp32s3
- name: ttgo_t_deck_GPS
chip: esp32s3
- name: ttgo_t_beam_s3_SUPREME_v3
chip: esp32s3
- name: ESP32_DIY_LoRa_A7670
chip: esp32
- name: ESP32_DIY_LoRa_A7670_915
chip: esp32
- name: heltec_wireless_tracker
chip: esp32s3
- name: heltec_ht-ct62
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
chip: esp32
- name: WEMOS-D1-R32-RA02
chip: esp32
- name: WEMOS_S2_MINI_DIY_LoRa
chip: esp32s2
- name: esp32c3_DIY_1W_LoRa
chip: esp32c3
- name: esp32c3_DIY_1W_LoRa_915
chip: esp32c3
- name: ESP32_C3_OctopusLab_LoRa
chip: esp32c3
- name: QRPLabs_LightGateway_1_0
chip: esp32s3
- name: QRPLabs_LightGateway_Plus_1_0
chip: esp32s3
- name: XIAO_ESP32S3_WIO_SX1262
chip: esp32s3
- name: TROY_LoRa_APRS
chip: esp32
steps:
- uses: actions/checkout@v3
@@ -32,19 +108,70 @@ jobs:
run: pip install --upgrade platformio
- name: Build target
run: pio run -e ${{ matrix.target }}
run: |
pio run -e ${{ matrix.target.name }}
- name: Build FS
run: pio run --target buildfs -e ${{ matrix.target }}
run: |
pio run --target buildfs -e ${{ matrix.target.name }}
- name: Move Files
run: |
mkdir -p installer/firmware
cp .pio/build/${{ matrix.target }}/firmware.bin installer/firmware/
cp .pio/build/${{ matrix.target }}/bootloader.bin installer/firmware/
cp .pio/build/${{ matrix.target }}/partitions.bin installer/firmware/
cp .pio/build/${{ matrix.target }}/spiffs.bin installer/firmware/
cp .pio/build/${{ matrix.target.name }}/firmware.bin installer/ota_update.bin
cp .pio/build/${{ matrix.target.name }}/firmware.bin installer/firmware/
cp .pio/build/${{ matrix.target.name }}/bootloader.bin installer/firmware/
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
python installer/bin/esptool/esptool.py --chip esp32 merge_bin \
-o installer/web_factory.bin \
--flash_mode dio \
--flash_freq 40m \
--flash_size 4MB \
0x1000 installer/firmware/bootloader.bin \
0x8000 installer/firmware/partitions.bin \
0xe000 installer/firmware/boot_app0.bin \
0x10000 installer/firmware/firmware.bin \
0x3D0000 installer/firmware/spiffs.bin
elif [ "${{ matrix.target.chip }}" == "esp32s2" ]; then
python installer/bin/esptool/esptool.py --chip esp32s2 merge_bin \
-o installer/web_factory.bin \
--flash_mode dio \
--flash_freq 40m \
--flash_size 4MB \
0x1000 installer/firmware/bootloader.bin \
0x8000 installer/firmware/partitions.bin \
0xe000 installer/firmware/boot_app0.bin \
0x10000 installer/firmware/firmware.bin \
0x3D0000 installer/firmware/spiffs.bin
elif [ "${{ matrix.target.chip }}" == "esp32s3" ]; then
python installer/bin/esptool/esptool.py --chip esp32s3 merge_bin \
-o installer/web_factory.bin \
--flash_mode dio \
--flash_freq 40m \
--flash_size 8MB \
0x0000 installer/firmware/bootloader.bin \
0x8000 installer/firmware/partitions.bin \
0xe000 installer/firmware/boot_app0.bin \
0x10000 installer/firmware/firmware.bin \
0x3D0000 installer/firmware/spiffs.bin
elif [ "${{ matrix.target.chip }}" == "esp32c3" ]; then
python installer/bin/esptool/esptool.py --chip esp32c3 merge_bin \
-o installer/web_factory.bin \
--flash_mode dio \
--flash_freq 40m \
--flash_size 4MB \
0x1000 installer/firmware/bootloader.bin \
0x8000 installer/firmware/partitions.bin \
0xe000 installer/firmware/boot_app0.bin \
0x10000 installer/firmware/firmware.bin \
0x3D0000 installer/firmware/spiffs.bin
fi
- name: Install Zip
run: sudo apt-get install zip
@@ -59,5 +186,5 @@ jobs:
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./installer.zip
asset_name: ${{ matrix.target }}.zip
asset_name: ${{ matrix.target.name }}.zip
asset_content_type: application/zip

31
.github/workflows/commit.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Commit Test Build
on:
push:
branches:
- '*'
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- name: ttgo-lora32-v21
chip: esp32
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install PlatformIO Core
run: pip install --upgrade platformio
- name: Build target
run: pio run -e ${{ matrix.target.name }}
- name: Build FS
run: pio run --target buildfs -e ${{ matrix.target.name }}

View File

@@ -1,4 +1,4 @@
{
"editor.tabSize": 4,
"editor.formatOnSave": true
}
"editor.formatOnSave": false
}

687
LICENSE
View File

@@ -1,21 +1,674 @@
MIT License
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (c) 2023 Ricardo Guzman (Richonguzman)
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Preamble
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program 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.
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

191
README.md
View File

@@ -1,80 +1,143 @@
# Richonguzman / CA2RXU LoRa APRS iGate/Digirepeater
# CA2RXU LoRa APRS iGate/Digipeater
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/OledScreen.jpeg)
![Screenshot](https://github.com/richonguzman/LoRa_APRS_iGate/raw/main/images/iGateOledScreen.jpeg)
__(NOTE: This iGate Firmware was develop to work with all LoRa APRS Trackers and specially with this firmware <a href="https://github.com/richonguzman/LoRa_APRS_Tracker" target="_blank">LoRa APRS Tracker</a>)__
__(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 />
___________________________________________________
____________________________________________________
# <a href="https://richonguzman.github.io/lora-igate-web-flasher/installer.html" target="_blank">WEB FLASHER/INSTALLER</a>
# <a href="https://drive.google.com/file/d/1Hff_Szd7ks8RC7_RiV6POxPJlclbO05M/view?usp=sharing" target="_blank">LoRa APRS iGate CA2RXU Firmware Manual</a>
____________________________________________________
## You can support this project to continue to grow:
[<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)
[<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)
____________________________________________________
This next generation LoRa iGate can work as:
- pure RX-iGate,
- Rx+Tx-iGate and distribute messages and weather forecasts to heard trackers, and
- Digipeater in simplex or split-frequency environment.
## SUPPORTED BOARDS (<a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/Supported-Boards-and-Buying-Links" target="_blank">Buying links</a>).
In all configurations the display shows the current stationMode, heard packets and events the iGate is currently performing.
(NOTE: all boards with 433-868-915 MHz versions)
But under the hood is much more:
- TTGO Lilygo LoRa32 T3S3 V1.2 and LoRa32 V2.1 (V1.6 is the same).
- Web Configuration UI.
- Sending events to remote syslog server.
- OTA update capability (for Firmware and Filesystem).
- RX first, TX will only be done if there is no traffic on the frequency.
- automatic update of the Lora symbol at APRS-IS, black "L" for pure RX, red "L" for TX capability, green star "L" for digipeater and blue round "L" for WX iGate.
- support for multiple WLAN with corresponding coordinates.
- support for BME/BMP280 and BME680 sensors, sending to WX data to APRS-IS.
- TTGO T-Beam V1.0 , V1.1, V1.2 (also variations with SX1262 and SX1268 LoRa Modules) and Supreme V3.
and more will come:
- More Web UI Station Information
- T-Deck Plus (and also regular T-Deck with/without GPS).
____________________________________________________
- HELTEC V2, V3, V3.2, T114, Wireless Stick, Wireless Stick Lite V3/V3.2, HT-CT62, Wireless Tracker, Wireless Paper.
# WIKI
- RAK Wireless 4631 + 19007(or 19003)
### 1. Installation Guide --> <a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/1.-Installation-Guide" target="_blank">here</a>.
- Faketec V3 (NRF52840 + Heltec HTRA62(SX1262))
### 2. iGate Configuration and Explanation for each setting --> <a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/2.-iGate-Configuration" target="_blank">here</a>.
- QRP Labs LightGateway 1.0 and Plus 1.0.
### 3. Supported Boards and Environment Selection --> <a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/3.-Supported-Boards-and-Environment-Selection" target="_blank">here</a>.
- ESP32 + SX1278 LoRa Module or Ebyte 400M30S (or 900M30S) 1W LoRa Module for a DIY Versions.
### 4. Upload Firmware and Filesystem --> <a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/4.-Upload-Firmware-and-Filesystem" target="_blank">here</a>.
- ESP32C3 + Ebyte 400M30S(or 900M30S) 1W LoRa Module for another DIY version.
### 5. Adding BME280 Module --> <a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/5.-Adding-BME280-Module" target="_blank">here</a>.
- ESP32 + 4G/LTE A7670 Modem + SX1278 DIY Version.
____________________________________________________
## Timeline (Versions):
- Wemos Lolin32 Oled + SX1278 DIY Version.
<br />
# Timeline (Versions):
- 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.
- 2025.03.20 Manager List added to enable/disable DigiEcoMode and Tx Control. Thanks LB5JJ.
- 2025.03.03 T-Beam Supreme board added and more BlackList rules added.
- 2025.02.28 Heltec Wireless Paper with Epaper working. Thanks SzymonPriv for pointing to the right library.
- 2025.02.25 Objects Rules update, GPS Boards: Satellites on Screen, Wx Height Correction from GPS Data.
- 2025.01.22 Added LILYGO T-DECK PLUS (and DIY+GPS version) board support.
- 2025.01.11 Added HELTEC V3.2 board support.
- 2025.01.07 TROY_LoRa_APRS board added. GMT in quarter hour fix and Beacon fix for TNC.
- 2025.01.02 Callsign Black List added.
- 2024.12.30 Fixed missing validation for correct Digipeater mode when not connected to APRS-IS.
- 2024.12.06 APRS-IS connnection and passcode validation added.
- 2024.11.06 (Silent Update) Working now with Board "VARIANTS".
- 2024.10.29 Added LILYGO Lora32 T3S3 support.
- 2024.10.25 Added QRP Labs LightGateway 1.0 support.
- 2024.10.21 Boards with GPS can now send Real-GPS Beacon (also posible: GPS ambiguity of ~ 1 km).
- 2024.10.14 Received Packets in WebUI show real Local Time (NTP with GMT offset).
- 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.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.
- 2024.08.05 WIDE2-n added to WIDE1-n in Digipeater Modes.
- 2024.06.27 External Voltage Divider Resistor configuration on WebUI. Thanks Tilen S54B.
- 2024.06.26 Personal Note information on WebUI for the Station. Thanks Tilen S54B.
- 2024.06.24 Callsign Validation fix. Thanks Helge SA7SKY.
- 2024.06.21 Tx packets coming from APRS-IS are (now) formatted into 3rd Party (as they should have been since the beginning). Thanks Lynn KJ4ERJ and Geoffrey F4FXL.
- 2024.06.18 All boards with 433 / 868 / 915 MHz versions.
- 2024.06.10 ESP32C3 + 1W LoRa Module (E22 400M30S) support added.
- 2024.06.09 Si7021 module added (with autodetected I2C Address)
- 2024.06.08 Callsign Validation for all Station that iGate/Digi hears.
- 2024.05.27 Battery Monitor for internal and External Voltages (to make board sleep and avoid low discharge of batterys) T-Beam boards now with Battery readings as well.
- 2024.05.23 Forced Reboot Mode added.
- 2024.05.22 Experimental backup-Digipeater-Mode when "only" iGate mode loses WiFi connection added.
- 2024.05.20 WebConfig update to control whether Messages and Objects should be Tx to RF.
- 2024.05.17 HELTEC Wireless Stick Lite v3 support added.
- 2024.05.14 BME modules will be autodetected (I2C Address and if it is BME280/BMP280/BME680).
- 2024.05.13 PacketBuffer for Rx (25 Seg).
- 2024.05.11 HELTEC Wireless Tracker support added.
- 2024.04.23 T-LoRa32 v1.6/v2.1 with 915MHz support added.
- 2024.04.23 ESP32 + 4G/LTE MODEM A7670SA + LoRa (SX1278) support added.
- 2024.04.22 Wemos Lolin32 OLED DIY LoRa support added .
- 2024.04.21 WEB INSTALLER (thanks Damian SQ2CPA).
- 2024.04.20 New Output Buffer process: no more packets lost.
- 2024.04.13 Received Packets added on WebUI.
- 2024.04.09 iGate/Digipeater own GPS beacon is encoded (Base91) now.
- 2024.03.18 OE5HWN MeshCom board support added.
- 2024.02.25 New Web Configuration UI with WiFi AP (thanks Damian SQ2CPA).
- 2023.01.28 Updated to ElegantOTA v.3 (AsyncElegantOTA was deprecated).
- 2024.01.19 TextSerialOutputForApp added to get text from Serial-Output over USB into PC for PinPoint App (https://www.pinpointaprs.com) and APRSIS32 App (http://aprsisce.wikidot.com)
- 2024.01.12 Added iGate Mode to also repeat packets (like a iGate+DigiRepeater) in stationMode 2 and 5.
- 2024.01.12 Added iGate Mode to also repeat packets (like a iGate+Digipeater) in stationMode 2 and 5.
- 2024.01.11 Added iGate Mode to enable APRS-IS and LoRa beacon report at the same time.
- 2024.01.05 Added support for Lilygo TTGO T-Beam V1, V1.2, V1 + SX1268, V1.2 + SX1262.
- 2024.01.02 Added support for EByte 400M30S 1Watt LoRa module for DIY ESP32 iGate.
- 2024.01.05 Lilygo TTGO T-Beam V1, V1.2, V1 + SX1268, V1.2 + SX1262 support added.
- 2024.01.02 EByte 400M30S 1 Watt LoRa module for DIY ESP32 iGate support added.
- 2023.12.27 HELTEC V3 board support added. Thanks Luc ON2ON.
- 2023.12.26 Added BME680 module to BME/BMP280 modules supported.
- 2023.12.07 MIC-E process and syslog added.
- 2023.12.26 BME680 module support added.
- 2023.12.07 MIC-E process and Syslog added.
- 2023.12.06 HELTEC V2 board support added.
- 2023.11.26 Small correction to enable Syslog in stationMode5.
- 2023.10.09 Added "WIDE1-1" to Tx packets from iGate to be *repeated* by Digirepeaters.
- 2023.10.09 Added Support also for BMP280 module.
- 2023.10.09 Added "WIDE1-1" to Tx packets from iGate to be *repeated* by Digipeaters.
- 2023.10.09 BMP280 module support added.
- 2023.08.20 Added External Voltage Measurement (Max 15V!)
- 2023.08.05 Ground Height Correction for Pressure readings added.
- 2023.07.31 StationMode5 added: iGate when WiFi and APRS available, DigiRepeater when not.
- 2023.07.31 StationMode5 added: iGate when WiFi and APRS available, Digipeater when not.
- 2023.07.16 Small OTA, BME module update.
- 2023.07.05 Adding monitor info of Battery connected.
- 2023.06.18 Info on Oled Screen mayor update, added RSSI and Distance to Listened Station.
- 2023.06.17 Support for BME280 Module (Temperature, Humidity, Pressure) added.
- 2023.06.17 BME280 Module (Temperature, Humidity, Pressure) support added.
- 2023.06.12 Syslog added.
- 2023.06.10 OTA update support for Firmware and Filesystem.
- 2023.06.08 Adding Digirepeater Functions.
- 2023.06.08 Adding Digipeater Functions.
- 2023.06.06 Full repack of Code and adding _enableTx_ only for Ham Ops.
- 2023.05.23 Processing Query's from RF/LoRa or APRS-IS (Send "Help" Message to test).
- 2023.05.19 Saving Last-Heard Stations for validating Tx Responses.
@@ -83,54 +146,6 @@ ____________________________________________________
- 2023.02.17 Receiving Feed from APRS-IS.
- 2023.02.10 First Beta (receiving LoRa Beacon/Packets and uploading to APRS-IS).
____________________________________________________
Instructions (add your information into the '/data/igate_conf.json'):
a) Change _callsign_ from "NOCALL-10" to your CALLSIGN + SSID.
b) Choose _stationMode_:
1 = RX iGate, black "L" as symbol
2 = Rx + TX iGate, red "L" as symbol, HAM only. RX will be sent to APRS-IS, Messages will be sent via Lora. Same frequency for RX and TX. By using this feature you have comply with the regulations of your country.
3 = Digipeater simplex, green "L" as symbol, HAM only. Received packets containing WIDEx-x in path will be digipeated on the same frequency. By using this feature you have comply with the regulations of your country.
4 = Digipeater split frequency, green "L" as symbol, HAM only. Received packets will be digipeated on a different frequency. Frequency separation must be 125kHz or more. By using this feature you have comply with the regulations of your country.
IgateComment and DigirepeaterComment will be sent to APRS-IS or via RF, depending on your stationmode
c) WiFi section:
adjust SSID and Password to you WiFi, add the GPS to "Latitude" and "Longitude" (info from GoogleMaps) of your new LoRa iGate. (If stationMode 3 or 4 selected, add also GPS info to Digirepeater Section).
d) APRS_IS section:
change "passcode" from "VWXYZ" to yours (remember that is 5 digits integer) and choose a server close to your location (see https://www.aprs2.net/)
e) LORA section:
adjust TX frequency and RX frequency matching your stationmode and country. Remember,
at stationmode 1, 2, and 3, RX and TX frequency shall be set to 433775000 (443.775MHz, deviations possible, depending on your country)
at stationmode 4, RX frequency shall be set to 433775000, TX frequency shall be set to 433900000 (deviations possible, depending on your country). There must be a frequency separation of 125kHz or more.
adjust power to your need, valid values are from 1 to 20
f) Syslog section:
adjust server and port to a suitable value if needed.
g) BME section:
adjust to "active" if BME280 sensor connected through I2C pins
__________________________________________
Special Thanks to the help in testing and developing to Manfred (DC2MH) , for showing me the "way of good coding" to Tihomir (CA3TSK) and much more Ham Licence Ops all over the world.
# Hope You Enjoy this, 73 !! CA2RXU , Valparaiso, Chile
# Hope You Enjoy this, 73! CA2RXU, Valparaiso, Chile

47
common_settings.ini Normal file
View File

@@ -0,0 +1,47 @@
[common]
build_flags =
-Werror -Wall
-DELEGANTOTA_USE_ASYNC_WEBSERVER=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 AHTX0 @ 2.0.5
adafruit/Adafruit BME280 Library @ 2.2.4
adafruit/Adafruit BMP280 Library @ 2.6.8
adafruit/Adafruit BME680 Library @ 2.0.4
adafruit/Adafruit INA219 @ 1.2.3
adafruit/Adafruit Si7021 Library @ 1.5.3
arduino-libraries/NTPClient @ 3.2.1
ayushsharma82/ElegantOTA @ 3.1.7
bblanchon/ArduinoJson @ 6.21.3
jgromes/RadioLib @ 7.1.0
knolleary/PubSubClient @ 2.8
ESP32Async/AsyncTCP @ 3.4.9
ESP32Async/ESPAsyncWebServer @ 3.9.3
mikalhart/TinyGPSPlus @ 1.0.3
richonguzman/APRSPacketLib @ 1.0.4
display_libs =
adafruit/Adafruit GFX Library @ 1.11.9
adafruit/Adafruit SSD1306 @ 2.5.10
usb_flags=
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1

View File

@@ -1,45 +1,55 @@
{
"callsign": "NOCALL-10",
"stationMode": 2,
"iGateComment": "LoRa_APRS_iGate",
"wifi": {
"autoAP": {
"password": "1234567890",
"powerOff": 10
"timeout": 10
},
"AP": [
{
"ssid": "WIFI_1",
"password": "WIFI_1_password",
"latitude": 0.0,
"longitude": 0.0
},
{
"ssid": "WIFI_2",
"password": "WIFI_2_password",
"latitude": 0.0,
"longitude": 0.0
}
]
"AP": []
},
"digi": {
"comment": "LoRa_APRS_Digirepeater",
"beacon": {
"latitude": 0.0,
"longitude": 0.0
"longitude": 0.0,
"comment": "LoRa APRS",
"interval": 15,
"overlay": "L",
"symbol": "a",
"path": "WIDE1-1",
"sendViaAPRSIS": false,
"sendViaRF": false,
"beaconFreq": 1,
"statusActive": false,
"statusPacket": "",
"gpsActive": false,
"ambiguityLevel": 0
},
"aprs_is": {
"passcode": "XYZVW",
"active": false,
"messagesToRF": false,
"objectsToRF": false,
"server": "rotate.aprs2.net",
"passcode": "XYZVW",
"port": 14580,
"reportingDistance": 30
"filter": "m/10"
},
"personalNote": "",
"blacklist": "",
"digi": {
"mode": 0,
"ecoMode": 0,
"backupDigiMode": false
},
"lora": {
"iGateFreq": 433775000,
"digirepeaterTxFreq": 433775000,
"digirepeaterRxFreq": 433900000,
"spreadingFactor": 12,
"signalBandwidth": 125000,
"codingRate4": 5,
"rxActive": true,
"rxFreq": 433775000,
"rxSpreadingFactor": 12,
"rxCodingRate4": 5,
"rxSignalBandwidth": 125000,
"txActive": false,
"txFreq": 433775000,
"txSpreadingFactor": 12,
"txCodingRate4": 5,
"txSignalBandwidth": 125000,
"power": 20
},
"display": {
@@ -47,25 +57,65 @@
"timeout": 4,
"turn180": false
},
"battery": {
"sendInternalVoltage": false,
"monitorInternalVoltage": false,
"internalSleepVoltage": 2.9,
"sendExternalVoltage": false,
"monitorExternalVoltage": false,
"externalSleepVoltage": 10.9,
"useExternalI2CSensor": false,
"voltageDividerR1": 100.0,
"voltageDividerR2": 27.0,
"externalVoltagePin": 34,
"sendVoltageAsTelemetry": false
},
"wxsensor": {
"active": false,
"heightCorrection": 0,
"temperatureCorrection": 0.0
},
"syslog": {
"active": false,
"server": "192.168.0.100",
"port": 514
"server": "lora.link9.net",
"port": 1514,
"logBeaconOverTCPIP": false
},
"bme": {
"active": false
"tnc": {
"enableServer": false,
"enableSerial": false,
"acceptOwn": false,
"aprsBrigdeActive": false
},
"mqtt": {
"active": false,
"server": "",
"topic": "",
"username": "",
"password": "",
"port": 1883,
"beaconOverMqtt": false
},
"ota": {
"username": "",
"password": ""
},
"webadmin": {
"username": "admin",
"password": ""
},
"remoteManagement": {
"managers": "",
"rfOnly": true
},
"ntp": {
"server": "pool.ntp.org",
"gmtCorrection": 0.0
},
"other": {
"beaconInterval": 15,
"igateSendsLoRaBeacons": false,
"igateRepeatsLoRaPackets": false,
"rememberStationTime": 30,
"sendBatteryVoltage": false,
"externalVoltageMeasurement": false,
"externalVoltagePin": 34
"rebootMode": false,
"rebootModeTime": 6,
"startupDelay": 0
}
}
}

BIN
data_embed/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

File diff suppressed because one or more lines are too long

View File

@@ -49,40 +49,23 @@ function fetchSettings() {
});
}
const alwaysOnCheckbox = document.querySelector(
'input[name="display.alwaysOn"]'
);
const timeoutInput = document.querySelector('input[name="display.timeout"]');
alwaysOnCheckbox.addEventListener("change", function () {
timeoutInput.disabled = this.checked;
});
// timeoutInput.addEventListener("change", function () {
// alwaysOnCheckbox.disabled = this.value !== "";
// });
const logCheckbox = document.querySelector('input[name="syslog.active"]');
const serverField = document.querySelector('input[name="syslog.server"]');
const portField = document.querySelector('input[name="syslog.port"]');
logCheckbox.addEventListener("change", function () {
serverField.disabled = !this.checked;
portField.disabled = !this.checked;
});
function loadSettings(settings) {
currentSettings = settings;
// General
document.getElementById("callsign").value = settings.callsign;
document.getElementById("stationMode").value = settings.stationMode;
document.getElementById("bme.active").checked = settings.bme.active;
document.getElementById("iGateComment").value = settings.iGateComment;
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;
document.getElementById("beacon.overlay").value = settings.beacon.overlay;
document.getElementById("personalNote").value = settings.personalNote;
document.getElementById("action.symbol").value = settings.beacon.overlay + settings.beacon.symbol;
document.querySelector(".list-networks").innerHTML = "";
document.querySelector(".list-networks").innerHTML = "";
// Networks
const wifiNetworks = settings.wifi.AP;
const wifiNetworks = settings.wifi.AP || [];
const networksContainer = document.querySelector(".list-networks");
let networkCount = 0;
@@ -94,23 +77,15 @@ function loadSettings(settings) {
// Increment the name, id, and for attributes
const attributeName = `wifi.AP.${networkCount}`;
networkElement.innerHTML = `
<div class="form-floating col-6 col-md-3 px-1 mb-2">
<div class="form-floating col-5 px-1 mb-2">
<input type="text" class="form-control form-control-sm" name="${attributeName}.ssid" id="${attributeName}.ssid" value="${network.ssid}">
<label for="${attributeName}.ssid">SSID</label>
</div>
<div class="form-floating col-6 col-md-3 px-1 mb-2">
<input type="text" class="form-control form-control-sm" name="${attributeName}.password" id="${attributeName}.password" value="${network.password}">
<div class="form-floating col-5 px-1 mb-2">
<input type="password" class="form-control form-control-sm" name="${attributeName}.password" id="${attributeName}.password" value="${network.password}">
<label for="${attributeName}.password">Passphrase</label>
</div>
<div class="col-4 col-md-2 form-floating px-1 mb-2">
<input type="text" class="form-control form-control-sm latitude" name="${attributeName}.latitude" id="${attributeName}.latitude" value="${network.latitude}">
<label for="${attributeName}.latitude">Latitude</label>
</div>
<div class="col-4 col-md-2 form-floating px-1 mb-2">
<input type="text" class="form-control form-control-sm longitude" name="${attributeName}.longitude" id="${attributeName}.longitude" value="${network.longitude}">
<label for="${attributeName}.longitude">Longitude</label>
</div>
<div class="col-4 col-md-2 d-flex align-items-center justify-content-end">
<div class="col-2 d-flex align-items-center justify-content-end">
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-danger" title="Delete" onclick="return this.parentNode.parentNode.parentNode.remove();"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash3-fill" viewBox="0 0 16 16">
<path d="M11 1.5v1h3.5a.5.5 0 0 1 0 1h-.538l-.853 10.66A2 2 0 0 1 11.115 16h-6.23a2 2 0 0 1-1.994-1.84L2.038 3.5H1.5a.5.5 0 0 1 0-1H5v-1A1.5 1.5 0 0 1 6.5 0h3A1.5 1.5 0 0 1 11 1.5m-5 0v1h4v-1a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5M4.5 5.029l.5 8.5a.5.5 0 1 0 .998-.06l-.5-8.5a.5.5 0 1 0-.998.06m6.53-.528a.5.5 0 0 0-.528.47l-.5 8.5a.5.5 0 0 0 .998.058l.5-8.5a.5.5 0 0 0-.47-.528M8 4.5a.5.5 0 0 0-.5.5v8.5a.5.5 0 0 0 1 0V5a.5.5 0 0 0-.5-.5"/>
@@ -121,174 +96,343 @@ function loadSettings(settings) {
networksContainer.appendChild(networkElement);
networkCount++;
});
document.getElementById("startupDelay").value = settings.startupDelay;
// APRS-IS
document.getElementById("aprs_is.server").value = settings.aprs_is.server;
document.getElementById("aprs_is.port").value = settings.aprs_is.port;
document.getElementById("aprs_is.reportingDistance").value =
settings.aprs_is.reportingDistance;
document.getElementById("aprs_is.passcode").value =
settings.aprs_is.passcode;
// Display
document.getElementById("display.alwaysOn").checked =
settings.display.alwaysOn;
document.getElementById("display.turn180").checked =
settings.display.turn180;
document.getElementById("display.timeout").value = settings.display.timeout;
if (settings.display.alwaysOn) {
timeoutInput.disabled = true;
}
// WiFi Auto AP
document.getElementById("wifi.autoAP.password").value =
settings.wifi.autoAP.password;
document.getElementById("wifi.autoAP.powerOff").value =
settings.wifi.autoAP.powerOff;
// Digi
document.getElementById("digi.comment").value = settings.digi.comment;
document.getElementById("digi.latitude").value = settings.digi.latitude;
document.getElementById("digi.longitude").value = settings.digi.longitude;
// OTA
document.getElementById("ota.username").value = settings.ota.username;
document.getElementById("ota.password").value = settings.ota.password;
document.getElementById("aprs_is.active").checked = settings.aprs_is.active;
document.getElementById("aprs_is.messagesToRF").checked = settings.aprs_is.messagesToRF;
document.getElementById("aprs_is.objectsToRF").checked = settings.aprs_is.objectsToRF;
document.getElementById("aprs_is.server").value = settings.aprs_is.server;
document.getElementById("aprs_is.port").value = settings.aprs_is.port;
document.getElementById("aprs_is.filter").value = settings.aprs_is.filter;
document.getElementById("aprs_is.passcode").value = settings.aprs_is.passcode;
APRSISCheckbox.checked = settings.aprs_is.active;
APRSISGateMessages.disabled = !APRSISCheckbox.checked;
APRSISGateObjects.disabled = !APRSISCheckbox.checked;
APRSISServer.disabled = !APRSISCheckbox.checked;
APRSISPort.disabled = !APRSISCheckbox.checked;
APRSISPasscode.disabled = !APRSISCheckbox.checked;
APRSISFilter.disabled = !APRSISCheckbox.checked;
// Beacon
document.getElementById("other.beaconInterval").value =
settings.other.beaconInterval;
document.getElementById("other.rememberStationTime").value =
settings.other.rememberStationTime;
document.getElementById("other.sendBatteryVoltage").checked =
settings.other.sendBatteryVoltage;
document.getElementById("other.externalVoltageMeasurement").checked =
settings.other.externalVoltageMeasurement;
document.getElementById("other.externalVoltagePin").value =
settings.other.externalVoltagePin;
document.getElementById("other.igateSendsLoRaBeacon").value =
settings.other.igateSendsLoRaBeacon;
document.getElementById("other.igateRepeatLoRaPackets").value =
settings.other.igateRepeatLoRaPackets;
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("beacon.sendViaAPRSIS").checked = settings.beacon.sendViaAPRSIS;
// Syslog
document.getElementById("syslog.active").checked = settings.syslog.active;
document.getElementById("syslog.server").value = settings.syslog.server;
document.getElementById("syslog.port").value = settings.syslog.port;
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;
if (settings.syslog.active) {
serverField.disabled = false;
portField.disabled = false;
}
document.getElementById("beacon.statusActive").checked = settings.beacon.statusActive;
document.getElementById("beacon.statusPacket").value = settings.beacon.statusPacket;
StatusCheckbox.checked = settings.beacon.statusActive;
StatusPacket.disabled = !StatusCheckbox.checked;
document.getElementById("beacon.gpsActive").checked = settings.beacon.gpsActive;
document.getElementById("beacon.ambiguityLevel").value = settings.beacon.ambiguityLevel;
// Black List
document.getElementById("blacklist").value = settings.blacklist;
// 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.digirepeaterTxFreq").value =
settings.lora.digirepeaterTxFreq;
document.getElementById("lora.iGateFreq").value = settings.lora.iGateFreq;
document.getElementById("lora.digirepeaterRxFreq").value =
settings.lora.digirepeaterRxFreq;
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.power").value = settings.lora.power;
document.getElementById("lora.rxActive").checked = settings.lora.rxActive;
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;
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;
// Display
document.getElementById("display.alwaysOn").checked = settings.display.alwaysOn;
document.getElementById("display.turn180").checked = settings.display.turn180;
document.getElementById("display.timeout").value = settings.display.timeout;
DisplayAlwaysOnCheckbox.checked = settings.display.alwaysOn;
DisplayTimeout.disabled = DisplayAlwaysOnCheckbox.checked;
// BATTERY
document.getElementById("battery.sendInternalVoltage").checked = settings.battery.sendInternalVoltage;
document.getElementById("battery.monitorInternalVoltage").checked = settings.battery.monitorInternalVoltage;
document.getElementById("battery.internalSleepVoltage").value = settings.battery.internalSleepVoltage.toFixed(1);
MonitorInternalVoltageCheckbox.checked = settings.battery.monitorInternalVoltage;
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;
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);
MonitorExternalVoltageCheckbox.checked = settings.battery.monitorExternalVoltage;
MonitorExternalSleepVoltage.disabled = !MonitorExternalVoltageCheckbox.checked;
document.getElementById("battery.sendVoltageAsTelemetry").checked = settings.battery.sendVoltageAsTelemetry;
// TELEMETRY WX SENSOR
document.getElementById("wxsensor.active").checked = settings.wxsensor.active;
document.getElementById("wxsensor.heightCorrection").value = settings.wxsensor.heightCorrection;
document.getElementById("wxsensor.temperatureCorrection").value = settings.wxsensor.temperatureCorrection.toFixed(1);
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;
document.getElementById("syslog.port").value = settings.syslog.port;
document.getElementById("syslog.logBeaconOverTCPIP").checked = settings.syslog.logBeaconOverTCPIP;
SyslogCheckbox.checked = settings.syslog.active;
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;
}
// MQTT
document.getElementById("mqtt.active").checked = settings.mqtt.active;
document.getElementById("mqtt.server").value = settings.mqtt.server;
document.getElementById("mqtt.topic").value = settings.mqtt.topic;
document.getElementById("mqtt.username").value = settings.mqtt.username;
document.getElementById("mqtt.password").value = settings.mqtt.password;
document.getElementById("mqtt.port").value = settings.mqtt.port;
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;
document.getElementById("other.rebootModeTime").value = settings.other.rebootModeTime;
RebootModeCheckbox.checked = settings.other.rebootMode;
RebootModeTime.disabled = !RebootModeCheckbox.check;
// WiFi Auto AP
document.getElementById("wifi.autoAP.password").value = settings.wifi.autoAP.password;
document.getElementById("wifi.autoAP.timeout").value = settings.wifi.autoAP.timeout;
// OTA
document.getElementById("ota.username").value = settings.ota.username;
document.getElementById("ota.password").value = settings.ota.password;
// Webadmin
document.getElementById("webadmin.active").checked = settings.webadmin.active;
document.getElementById("webadmin.username").value = settings.webadmin.username;
document.getElementById("webadmin.password").value = settings.webadmin.password;
WebadminCheckbox.checked = settings.webadmin.active;
WebadminUsername.disabled = !WebadminCheckbox.check;
WebadminPassword.disabled = !WebadminCheckbox.check;
// Management over APRS
document.getElementById("remoteManagement.managers").value = settings.remoteManagement.managers;
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;
updateImage();
}
const bmeCheckbox = document.querySelector("input[name='bme.active']");
function showToast(message) {
const el = document.querySelector('#toast');
const stationModeSelect = document.querySelector("select[name='stationMode']");
el.querySelector('.toast-body').innerHTML = message;
const digirepeaterTxFreqInput = document.querySelector(
"input[name='lora.digirepeaterRxFreq']"
);
(new bootstrap.Toast(el)).show();
}
document.getElementById('send-beacon').addEventListener('click', function (e) {
e.preventDefault();
fetch("/action?type=send-beacon", { method: "POST" });
showToast("Your beacon will be sent in a moment. <br> <u>This action will be ignored if you have APRSIS and LoRa TX disabled!</u>");
});
document.getElementById('reboot').addEventListener('click', function (e) {
e.preventDefault();
fetch("/action?type=reboot", { method: "POST" });
showToast("Your device will be rebooted in a while");
});
function updateImage() {
const value = document.getElementById("beacon.overlay").value + document.getElementById("beacon.symbol").value;
const image = document.querySelector("img");
const isChecked = bmeCheckbox.checked;
if (isChecked) {
image.src =
"data:image/png;base64,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";
} else {
const selectedValue = stationModeSelect.value;
if (selectedValue === "1") {
if (bmeCheckbox.checked) {
image.src =
"data:image/png;base64,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";
} else {
image.src =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQ6SURBVHgB7d0/axRBGMfx50IMWPoKkpA02gUsLfZdqOCLOPBFWAliHbCUVNpaxQNrIY0WVjZaJlUsIsZ5uDu9bGb/zMwzs/PM/L7wIG4Ocflwl7ndvVsihBBCmTajunpk5uHG33+aOSGkvhdmLs1cW+bKzDEhle2a+UZ22Pacrx6PlMRYXc/arrkkIKvIBxfISgrBBXLmSeACOdMkcYGcWTFwgZxJMXGBPHEpcIE8USlxgSzcbGD2KD0ukIXKGRfIAuWOC+TANOACOSAtuEAemQ10a2P2KV9cIA+0ZZltMzurOaT8cYHcUx+wJlwgd9QFrBEXyJZswAekFxfIrdq4GhZUQO7Itlq+QzoXVEC21AdcIm51yF3AJeNWhWwDLmFBBeRVmg4/Atmj2nGLR/bCbZrm2qWx/+7Ekwx5i9LGO/XFzF2qO97/r5QAOSUwcG+WBDkVMHDtRUdOAcz/ed4J4NqLihwbGLjjioYcExi4bkVBjgUMXL/EkWMAAzcsUWRpYODKJIYsCQxc2USQpYCBG6dgZAlg4MYtCDkUGLhp8kYOAQZu2ryQfYGT4S7PAKJVzsg+wEmfubNZbd+XOpgTsiswXpbzaDSyCzBw82oU8lhg4ObZIPIYYODmXS/yEDBwdcQ+72w/6AMGrq6OaHnLghv1AQNXX6/bG7qAPxNwNbbf3mAD5qf5EaEisgG/IVRMNuA9QsVkA94mpLWP7Q2pP5uE4vayvQHA5bQw86m90Qb8i5C22Kyx/cAG/IGQphj3ftcPbcBzM78JaWiN+73rATZgfvAzWn4SHeXbIC7Xtcg6MfOUgJxro3C5vlU0kPNsNC439DYJyHnlhMuNeR8M5DxyxuXGHugA8rR54XIuR7KAPE3euJzroUogpy0Il/M5Fg3kNAXjcr4nG4AcNxFcLuTc78nqz7e0/A7KLDo9PbV+nkn6Q2xnZ2c0n88pQmK4XOjJ/eyQm6YhxYnichLng/FyLZM4Lid1wh/IYUXB5SSv6ACyX9FwOelLdoDsVlRcLsY1WUAeV3RcLtZFd0Duj3EfUGRcLuZVlUC2lwyXi32Re/D75IuLC1osFtYDFXxAo73dts21MV/8wgc6PEqKm7LHZv5QnndASTV8p5U9unl7oaJ6QvUi23CLA+YdqhGZcQ/Jfmu/olrvVE3Ia9wdqgB4sxp+JzPuPv2/+XV1lYzcxq32Q34lvlzbcKsF5h0vCZlxD+g2btXApSCvF1TbBOB/ba4oNSOvn7lr3KpWzC5pXHgVf4RKOk3P5CqOUEmn5eW6CxfAA2n4ndyHC+AR5YwMXOFyWngx7i4h8XJABm7kpkQGbqKmQAZu4lIiA3eiUiADd+JiIgM3k2IgAzezJJGBm2kSyMDNvBBk4CrJBxm4ynJB/kHAVRnf3OucumGvzBxTBZV+eouhn5u5t7HtvZlXhBBCCCEUsb+qGDWF0d8l5AAAAABJRU5ErkJggg==";
}
digirepeaterTxFreqInput.disabled = true;
} else if (selectedValue === "2") {
if (bmeCheckbox.checked) {
image.src =
"data:image/png;base64,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";
} else {
image.src =
"data:image/png;base64,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";
}
digirepeaterTxFreqInput.disabled = true;
} else if (selectedValue === "3" || selectedValue === "4") {
switch (value) {
case "L&":
image.src =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQ6SURBVHgB7d0/axRBGMfx50IMWPoKkpA02gUsLfZdqOCLOPBFWAliHbCUVNpaxQNrIY0WVjZaJlUsIsZ5uDu9bGb/zMwzs/PM/L7wIG4Ocflwl7ndvVsihBBCmTajunpk5uHG33+aOSGkvhdmLs1cW+bKzDEhle2a+UZ22Pacrx6PlMRYXc/arrkkIKvIBxfISgrBBXLmSeACOdMkcYGcWTFwgZxJMXGBPHEpcIE8USlxgSzcbGD2KD0ukIXKGRfIAuWOC+TANOACOSAtuEAemQ10a2P2KV9cIA+0ZZltMzurOaT8cYHcUx+wJlwgd9QFrBEXyJZswAekFxfIrdq4GhZUQO7Itlq+QzoXVEC21AdcIm51yF3AJeNWhWwDLmFBBeRVmg4/Atmj2nGLR/bCbZrm2qWx/+7Ekwx5i9LGO/XFzF2qO97/r5QAOSUwcG+WBDkVMHDtRUdOAcz/ed4J4NqLihwbGLjjioYcExi4bkVBjgUMXL/EkWMAAzcsUWRpYODKJIYsCQxc2USQpYCBG6dgZAlg4MYtCDkUGLhp8kYOAQZu2ryQfYGT4S7PAKJVzsg+wEmfubNZbd+XOpgTsiswXpbzaDSyCzBw82oU8lhg4ObZIPIYYODmXS/yEDBwdcQ+72w/6AMGrq6OaHnLghv1AQNXX6/bG7qAPxNwNbbf3mAD5qf5EaEisgG/IVRMNuA9QsVkA94mpLWP7Q2pP5uE4vayvQHA5bQw86m90Qb8i5C22Kyx/cAG/IGQphj3ftcPbcBzM78JaWiN+73rATZgfvAzWn4SHeXbIC7Xtcg6MfOUgJxro3C5vlU0kPNsNC439DYJyHnlhMuNeR8M5DxyxuXGHugA8rR54XIuR7KAPE3euJzroUogpy0Il/M5Fg3kNAXjcr4nG4AcNxFcLuTc78nqz7e0/A7KLDo9PbV+nkn6Q2xnZ2c0n88pQmK4XOjJ/eyQm6YhxYnichLng/FyLZM4Lid1wh/IYUXB5SSv6ACyX9FwOelLdoDsVlRcLsY1WUAeV3RcLtZFd0Duj3EfUGRcLuZVlUC2lwyXi32Re/D75IuLC1osFtYDFXxAo73dts21MV/8wgc6PEqKm7LHZv5QnndASTV8p5U9unl7oaJ6QvUi23CLA+YdqhGZcQ/Jfmu/olrvVE3Ia9wdqgB4sxp+JzPuPv2/+XV1lYzcxq32Q34lvlzbcKsF5h0vCZlxD+g2btXApSCvF1TbBOB/ba4oNSOvn7lr3KpWzC5pXHgVf4RKOk3P5CqOUEmn5eW6CxfAA2n4ndyHC+AR5YwMXOFyWngx7i4h8XJABm7kpkQGbqKmQAZu4lIiA3eiUiADd+JiIgM3k2IgAzezJJGBm2kSyMDNvBBk4CrJBxm4ynJB/kHAVRnf3OucumGvzBxTBZV+eouhn5u5t7HtvZlXhBBCCCEUsb+qGDWF0d8l5AAAAABJRU5ErkJggg==";
break;
case "L#":
image.src =
"data:image/png;base64,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";
digirepeaterTxFreqInput.disabled = selectedValue === "3";
} else if (selectedValue === "5") {
if (bmeCheckbox.checked) {
image.src =
"data:image/png;base64,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";
} else {
image.src =
"data:image/png;base64,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";
}
digirepeaterTxFreqInput.disabled = true;
}
break;
case "L_":
image.src =
"data:image/png;base64,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";
break;
case "La":
image.src =
"data:image/png;base64,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";
break;
}
}
stationModeSelect.addEventListener("change", updateImage);
bmeCheckbox.addEventListener("change", updateImage);
function toggleFields() {
const sendBatteryVoltageCheckbox = document.querySelector(
'input[name="other.sendBatteryVoltage"]'
);
const externalVoltageMeasurementCheckbox = document.querySelector(
'input[name="other.externalVoltageMeasurement"]'
);
const externalVoltagePinInput = document.querySelector(
'input[name="other.externalVoltagePin"]'
);
externalVoltageMeasurementCheckbox.disabled =
!sendBatteryVoltageCheckbox.checked;
externalVoltagePinInput.disabled =
!externalVoltageMeasurementCheckbox.checked;
}
const sendBatteryVoltageCheckbox = document.querySelector(
'input[name="other.sendBatteryVoltage"]'
);
const externalVoltageMeasurementCheckbox = document.querySelector(
'input[name="other.externalVoltageMeasurement"]'
);
const externalVoltagePinInput = document.querySelector(
'input[name="other.externalVoltagePin"]'
);
sendBatteryVoltageCheckbox.addEventListener("change", function () {
externalVoltageMeasurementCheckbox.disabled = !this.checked;
externalVoltagePinInput.disabled =
!externalVoltageMeasurementCheckbox.checked;
// 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"]');
const StatusPacket = document.querySelector('input[name="beacon.statusPacket"]');
StatusCheckbox.addEventListener("change", function() {
StatusPacket.disabled = !this.checked;
});
// APRS-IS Switches
const APRSISCheckbox = document.querySelector('input[name="aprs_is.active"]');
const APRSISGateMessages = document.querySelector('input[name="aprs_is.messagesToRF"]');
const APRSISGateObjects = document.querySelector('input[name="aprs_is.objectsToRF"]');
const APRSISServer = document.querySelector('input[name="aprs_is.server"]');
const APRSISPort = document.querySelector('input[name="aprs_is.port"]');
const APRSISPasscode = document.querySelector('input[name="aprs_is.passcode"]');
const APRSISFilter = document.querySelector('input[name="aprs_is.filter"]');
APRSISCheckbox.addEventListener("change", function() {
APRSISGateMessages.disabled = !this.checked;
APRSISGateObjects.disabled = !this.checked;
APRSISServer.disabled = !this.checked;
APRSISPort.disabled = !this.checked;
APRSISPasscode.disabled = !this.checked;
APRSISFilter.disabled = !this.checked;
});
// Display Switches
const DisplayAlwaysOnCheckbox = document.querySelector('input[name="display.alwaysOn"]');
const DisplayTimeout = document.querySelector('input[name="display.timeout"]');
DisplayAlwaysOnCheckbox.addEventListener("change", function () {
DisplayTimeout.disabled = this.checked;
});
// Battery Switches
const MonitorInternalVoltageCheckbox = document.querySelector('input[name="battery.monitorInternalVoltage"]');
const MonitorInternalSleepVoltage = document.querySelector('input[name="battery.internalSleepVoltage"]');
MonitorInternalVoltageCheckbox.addEventListener("change", function () {
MonitorInternalSleepVoltage.disabled = !this.checked;
});
const MonitorExternalVoltageCheckbox = document.querySelector('input[name="battery.monitorExternalVoltage"]');
const MonitorExternalSleepVoltage = document.querySelector('input[name="battery.externalSleepVoltage"]');
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 () {
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
const TelemetryCheckbox = document.querySelector('input[name="wxsensor.active"]');
const TelemetryHeightCorrection = document.querySelector('input[name="wxsensor.heightCorrection"]');
const TelemetryTempCorrection = document.querySelector('input[name="wxsensor.temperatureCorrection"]');
TelemetryCheckbox.addEventListener("change", function () {
TelemetryHeightCorrection.disabled = !this.checked;
TelemetryTempCorrection.disabled = !this.checked;
});
// Syslog Switches
const SyslogCheckbox = document.querySelector('input[name="syslog.active"]');
const SyslogServer = document.querySelector('input[name="syslog.server"]');
const SyslogPort = document.querySelector('input[name="syslog.port"]');
const SyslogBeaconOverTCPIP = document.querySelector('input[name="syslog.logBeaconOverTCPIP"]');
SyslogCheckbox.addEventListener("change", function () {
SyslogServer.disabled = !this.checked;
SyslogPort.disabled = !this.checked;
SyslogBeaconOverTCPIP.disabled = !this.checked
});
// MQTT Switches
const MqttCheckbox = document.querySelector('input[name="mqtt.active"]');
const MqttServer = document.querySelector('input[name="mqtt.server"]');
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
const RebootModeCheckbox = document.querySelector('input[name="other.rebootMode"]');
const RebootModeTime = document.querySelector('input[name="other.rebootModeTime"]');
RebootModeCheckbox.addEventListener("change", function() {
RebootModeTime.disabled = !this.checked;
});
// Web Admin Switches
const WebadminCheckbox = document.querySelector('input[name="webadmin.active"]');
const WebadminUsername = document.querySelector('input[name="webadmin.username"]');
const WebadminPassword = document.querySelector('input[name="webadmin.password"]');
WebadminCheckbox.addEventListener("change", function () {
WebadminUsername.disabled = !this.checked;
WebadminPassword.disabled = !this.checked;
});
document.querySelector(".new button").addEventListener("click", function () {
const networksContainer = document.querySelector(".list-networks");
@@ -301,22 +445,14 @@ document.querySelector(".new button").addEventListener("click", function () {
// Increment the name, id, and for attributes
const attributeName = `wifi.AP.${networkCount}`;
networkElement.innerHTML = `
<div class="form-floating col-6 col-md-3 px-1 mb-2">
<div class="form-floating col-6 col-md-5 px-1 mb-2">
<input type="text" class="form-control form-control-sm" name="${attributeName}.ssid" id="${attributeName}.ssid" placeholder="" >
<label for="${attributeName}.ssid">SSID</label>
</div>
<div class="form-floating col-6 col-md-3 px-1 mb-2">
<input type="text" class="form-control form-control-sm" name="${attributeName}.password" id="${attributeName}.password" placeholder="">
<div class="form-floating col-6 col-md-5 px-1 mb-2">
<input type="password" class="form-control form-control-sm" name="${attributeName}.password" id="${attributeName}.password" placeholder="">
<label for="${attributeName}.password">Passphrase</label>
</div>
<div class="col-4 col-md-2 form-floating px-1 mb-2">
<input type="text" class="form-control form-control-sm latitude" name="${attributeName}.latitude" id="${attributeName}.latitude" placeholder="">
<label for="${attributeName}.latitude">Latitude</label>
</div>
<div class="col-4 col-md-2 form-floating px-1 mb-2">
<input type="text" class="form-control form-control-sm longitude" name="${attributeName}.longitude" id="${attributeName}.longitude" placeholder="">
<label for="${attributeName}.longitude">Longitude</label>
</div>
<div class="col-4 col-md-2 d-flex align-items-center justify-content-end">
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-danger" title="Delete" onclick="return this.parentNode.parentNode.parentNode.remove();"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash3-fill" viewBox="0 0 16 16">
@@ -333,7 +469,17 @@ document.querySelector(".new button").addEventListener("click", function () {
document.querySelector(".new").before(networkElement);
});
toggleFields();
document
.getElementById("action.symbol")
.addEventListener("change", function () {
const value = document.getElementById("action.symbol").value;
document.getElementById("beacon.overlay").value = value[0];
document.getElementById("beacon.symbol").value = value[1];
updateImage();
});
const form = document.querySelector("form");
@@ -382,7 +528,58 @@ form.addEventListener("submit", async (event) => {
saveModal.show();
setTimeout(checkConnection, 0);
setTimeout(checkConnection, 2000);
});
fetchSettings();
function loadReceivedPackets(packets) {
if (packets) {
document.querySelector('#received-packets tbody').innerHTML = '';
const container = document.querySelector("#received-packets tbody");
container.innerHTML = '';
const date = new Date();
packets.forEach((packet) => {
const element = document.createElement("tr");
element.innerHTML = `
<td>${packet.rxTime}</td>
<td>${packet.packet}</td>
<td>${packet.RSSI}</td>
<td>${packet.SNR}</td>
`;
container.appendChild(element);
})
}
setTimeout(fetchReceivedPackets, 15000);
}
function fetchReceivedPackets() {
fetch("/received-packets.json")
.then((response) => response.json())
.then((packets) => {
loadReceivedPackets(packets);
})
.catch((err) => {
console.error(err);
console.error(`Failed to load received packets`);
});
}
document.querySelector('a[href="/received-packets"]').addEventListener('click', function (e) {
e.preventDefault();
document.getElementById('received-packets').classList.remove('d-none');
document.getElementById('configuration').classList.add('d-none');
document.querySelector('button[type=submit]').remove();
fetchReceivedPackets();
})

View File

@@ -0,0 +1,8 @@
.alert-fixed {
position:fixed;
top: 0px;
left: 0px;
width: 100%;
z-index:9999;
border-radius:0px
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 KiB

BIN
images/iGateOledScreen.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

36
include/A7670_utils.h Normal file
View File

@@ -0,0 +1,36 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef A7670_UTILS_H_
#define A7670_UTILS_H_
#include <Arduino.h>
namespace A7670_Utils {
bool checkModemOn();
void setup();
bool checkATResponse(const String& ATMessage);
void APRS_IS_connect();
void uploadToAPRSIS(const String& packet);
void listenAPRSIS();
}
#endif

View File

@@ -1,39 +0,0 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

45
include/aprs_is_utils.h Normal file
View File

@@ -0,0 +1,45 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef APRS_IS_UTILS_H_
#define APRS_IS_UTILS_H_
#include <Arduino.h>
namespace APRS_IS_Utils {
void upload(const String& line);
void connect();
void checkStatus();
String checkForStartingBytes(const String& packet);
String buildPacketToUpload(const String& packet);
bool processReceivedLoRaMessage(const String& sender, const String& packet, bool thirdParty);
void processLoRaPacket(const String& packet);
String buildPacketToTx(const String& aprsisPacket, uint8_t packetType);
void processAPRSISPacket(const String& packet);
void listenAPRSIS();
void firstConnection();
}
#endif

37
include/battery_utils.h Normal file
View File

@@ -0,0 +1,37 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef BATTERY_UTILS_H_
#define BATTERY_UTILS_H_
#include <Arduino.h>
namespace BATTERY_Utils {
void adcCalibration();
void adcCalibrationCheck();
void setup();
float checkInternalVoltage();
float checkExternalVoltage();
void startupBatteryHealth();
}
#endif

207
include/configuration.h Normal file
View File

@@ -0,0 +1,207 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef CONFIGURATION_H_
#define CONFIGURATION_H_
#include <Arduino.h>
#include <vector>
#include <FS.h>
class WiFi_AP {
public:
String ssid;
String password;
};
class WiFi_Auto_AP {
public:
String password;
int timeout;
};
class BEACON {
public:
double latitude;
double longitude;
String comment;
int interval;
String overlay;
String symbol;
String path;
bool sendViaAPRSIS;
bool sendViaRF;
int beaconFreq;
bool statusActive;
String statusPacket;
bool gpsActive;
int ambiguityLevel;
};
class APRS_IS {
public:
bool active;
String passcode;
String server;
int port;
String filter;
bool messagesToRF;
bool objectsToRF;
};
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:
bool rxActive;
long rxFreq;
int rxSpreadingFactor;
int rxCodingRate4;
long rxSignalBandwidth;
bool txActive;
long txFreq;
int txSpreadingFactor;
int txCodingRate4;
long txSignalBandwidth;
int power;
};
class Display {
public:
bool alwaysOn;
int timeout;
bool turn180;
};
class BATTERY {
public:
bool sendInternalVoltage;
bool monitorInternalVoltage;
float internalSleepVoltage;
bool sendExternalVoltage;
int externalVoltagePin;
bool monitorExternalVoltage;
float externalSleepVoltage;
bool useExternalI2CSensor;
float voltageDividerR1;
float voltageDividerR2;
bool sendVoltageAsTelemetry;
};
class WXSENSOR {
public:
bool active;
int heightCorrection;
float temperatureCorrection;
};
class SYSLOG {
public:
bool active;
String server;
int port;
bool logBeaconOverTCPIP;
};
class TNC {
public:
bool enableServer;
bool enableSerial;
bool acceptOwn;
bool aprsBridgeActive;
};
class OTA {
public:
String username;
String password;
};
class WEBADMIN {
public:
bool active;
String username;
String password;
};
class NTP {
public:
String server;
float gmtCorrection;
};
class REMOTE_MANAGEMENT {
public:
String managers;
bool rfOnly;
};
class MQTT {
public:
bool active;
String server;
String topic;
String username;
String password;
int port;
bool beaconOverMqtt;
};
class Configuration {
public:
String callsign;
String tacticalCallsign;
int rememberStationTime;
bool rebootMode;
int rebootModeTime;
int startupDelay;
String personalNote;
String blacklist;
std::vector<WiFi_AP> wifiAPs;
WiFi_Auto_AP wifiAutoAP;
BEACON beacon;
APRS_IS aprs_is;
DIGI digi;
LoraModule loramodule;
Display display;
BATTERY battery;
WXSENSOR wxsensor;
SYSLOG syslog;
TNC tnc;
OTA ota;
WEBADMIN webadmin;
NTP ntp;
REMOTE_MANAGEMENT remoteManagement;
MQTT mqtt;
void setDefaultValues();
bool writeFile();
Configuration();
private:
bool readFile();
String _filePath;
};
#endif

34
include/digi_utils.h Normal file
View File

@@ -0,0 +1,34 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef DIGI_UTILS_H_
#define DIGI_UTILS_H_
#include <Arduino.h>
namespace DIGI_Utils {
String buildPacket(const String& path, const String& packet, bool thirdParty, bool crossFreq);
String generateDigipeatedPacket(const String& packet, bool thirdParty);
void processLoRaPacket(const String& packet);
void checkEcoMode();
}
#endif

32
include/display.h Normal file
View File

@@ -0,0 +1,32 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef DISPLAY_H_
#define DISPLAY_H_
#include <Arduino.h>
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
void displaySetup();
void displayToggle(bool toggle);
void displayShow(const String& header, const String& line1, const String& line2, const String& line3, int wait = 0);
void displayShow(const String& header, const String& line1, const String& line2, const String& line3, const String& line4, const String& line5, const String& line6, int wait = 0);
#endif

41
include/gps_utils.h Normal file
View File

@@ -0,0 +1,41 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef GPS_UTILS_H_
#define GPS_UTILS_H_
#include <Arduino.h>
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);
void setup();
void getData();
}
#endif

41
include/kiss_protocol.h Normal file
View File

@@ -0,0 +1,41 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#define DCD_ON 0x03
#define FEND 0xC0
#define FESC 0xDB
#define TFEND 0xDC
#define TFESC 0xDD
#define CMD_UNKNOWN 0xFE
#define CMD_DATA 0x00
#define CMD_HARDWARE 0x06
#define HW_RSSI 0x21
#define CMD_ERROR 0x90
#define ERROR_INITRADIO 0x01
#define ERROR_TXFAILED 0x02
#define ERROR_QUEUE_FULL 0x04
#define APRS_CONTROL_FIELD 0x03
#define APRS_INFORMATION_FIELD 0xf0
#define HAS_BEEN_DIGIPITED_MASK 0b10000000
#define IS_LAST_ADDRESS_POSITION_MASK 0b1

34
include/kiss_utils.h Normal file
View File

@@ -0,0 +1,34 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef KISS_UTILS_H_
#define KISS_UTILS_H_
#include <Arduino.h>
String encodeAddressAX25(String tnc2Address);
String decodeAddressAX25(const String& ax25Address, bool& isLast, bool isRelay);
String encapsulateKISS(const String& ax25Frame, uint8_t cmd);
String decapsulateKISS(const String& frame);
String encodeKISS(const String& frame);
String decodeKISS(const String& inputFrame, bool& dataFrame);
#endif

38
include/lora_utils.h Normal file
View File

@@ -0,0 +1,38 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef LORA_UTILS_H_
#define LORA_UTILS_H_
#include <Arduino.h>
namespace LoRa_Utils {
void setup();
void sendNewPacket(const String& newPacket);
String receivePacketFromSleep();
String receivePacket();
void changeFreqTx();
void changeFreqRx();
void wakeRadio();
void sleepRadio();
}
#endif

34
include/mqtt_utils.h Normal file
View File

@@ -0,0 +1,34 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef MQTT_UTILS_H_
#define MQTT_UTILS_H_
#include <Arduino.h>
namespace MQTT_Utils {
void sendToMqtt(const String& packet);
void connect();
void loop();
void setup();
}
#endif

33
include/ntp_utils.h Normal file
View File

@@ -0,0 +1,33 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef NTP_UTILS_H_
#define NTP_UTILS_H_
#include <Arduino.h>
namespace NTP_Utils {
void setup();
void update();
String getFormatedTime();
}
#endif

35
include/ota_utils.h Normal file
View File

@@ -0,0 +1,35 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef OTA_UTILS_H_
#define OTA_UTILS_H_
#include <ESPAsyncWebServer.h>
#include <Arduino.h>
namespace OTA_Utils {
void setup(AsyncWebServer *server);
void onOTAStart();
void onOTAProgress(size_t current, size_t final);
void onOTAEnd(bool success);
}
#endif

53
include/power_utils.h Normal file
View File

@@ -0,0 +1,53 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef POWER_UTILS_H_
#define POWER_UTILS_H_
#include <Arduino.h>
#if defined(HAS_AXP192) || defined(HAS_AXP2101)
#include "XPowersLib.h"
#else
#include <Wire.h>
#endif
namespace POWER_Utils {
#ifdef VEXT_CTRL
void vext_ctrl_ON();
void vext_ctrl_OFF();
#endif
#ifdef ADC_CTRL
void adc_ctrl_ON();
void adc_ctrl_OFF();
#endif
double getBatteryVoltage();
bool isBatteryConnected();
void activateGPS();
void deactivateGPS();
void activateLoRa();
void deactivateLoRa();
bool begin(TwoWire &port);
void setup();
}
#endif

31
include/query_utils.h Normal file
View File

@@ -0,0 +1,31 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef QUERY_UTILS_H_
#define QUERY_UTILS_H_
#include <Arduino.h>
namespace QUERY_Utils {
String process(const String& query, const String& station, bool queryFromAPRSIS, bool thirdParty);
}
#endif

33
include/sleep_utils.h Normal file
View File

@@ -0,0 +1,33 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef SLEEP_UTILS_H_
#define SLEEP_UTILS_H_
#include <Arduino.h>
namespace SLEEP_Utils {
void setup();
void checkWakeUpFlag();
void startSleeping();
void checkSerial();
}
#endif

48
include/station_utils.h Normal file
View File

@@ -0,0 +1,48 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef STATION_UTILS_H_
#define STATION_UTILS_H_
#include <Arduino.h>
struct LastHeardStation {
uint32_t lastHeardTime;
String station;
};
namespace STATION_Utils {
void loadBlacklistAndManagers();
bool isBlacklisted(const String& callsign);
bool isManager(const String& callsign);
bool checkObjectTime(const String& packet);
void deleteNotHeard();
void updateLastHeard(const String& station);
bool wasHeard(const String& station);
void clean25SegBuffer();
bool check25SegBuffer(const String& station, const String& textMessage);
void processOutputPacketBufferUltraEcoMode();
void processOutputPacketBuffer();
void addToOutputPacketBuffer(const String& packet, bool flag = false);
}
#endif

32
include/syslog_utils.h Normal file
View File

@@ -0,0 +1,32 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef SYSLOG_H_
#define SYSLOG_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 setup();
}
#endif

33
include/telemetry_utils.h Normal file
View File

@@ -0,0 +1,33 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef TELEMETRY_UTILS_H_
#define TELEMETRY_UTILS_H_
#include <Arduino.h>
namespace TELEMETRY_Utils {
void sendEquationsUnitsParameters();
String generateEncodedTelemetryBytes(float value, bool counterBytes, byte telemetryType);
String generateEncodedTelemetry();
}
#endif

35
include/tnc_utils.h Normal file
View File

@@ -0,0 +1,35 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef TNC_UTILS_H_
#define TNC_UTILS_H_
#include <Arduino.h>
namespace TNC_Utils {
void setup();
void loop();
void sendToClients(const String& packet, bool stripBytes = false);
void sendToSerial(const String& packet, bool stripBytes = false);
}
#endif

53
include/utils.h Normal file
View File

@@ -0,0 +1,53 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef UTILS_H_
#define UTILS_H_
#include <Arduino.h>
class ReceivedPacket {
public:
String rxTime;
String packet;
int RSSI;
float SNR;
};
namespace Utils {
void processStatus();
String getLocalIP();
void setupDisplay();
void showActiveStations();
void checkBeaconInterval();
void checkDisplayInterval();
void validateFreqs();
void typeOfPacket(const String& packet, const uint8_t packetType);
void print(const String& text);
void println(const String& text);
void checkRebootMode();
void checkRebootTime();
void checkSleepByLowBatteryVoltage(uint8_t mode);
bool callsignIsValid(const String& callsign);
void startupDelay();
}
#endif

43
include/web_utils.h Normal file
View File

@@ -0,0 +1,43 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef WEB_UTILS_H_
#define WEB_UTILS_H_
#include <ESPAsyncWebServer.h>
#include <ESPmDNS.h>
#include <Arduino.h>
#include <SPIFFS.h>
#include <WiFi.h>
namespace WEB_Utils {
void handleNotFound(AsyncWebServerRequest *request);
void handleStatus(AsyncWebServerRequest *request);
void handleHome(AsyncWebServerRequest *request);
void handleStyle(AsyncWebServerRequest *request);
void handleScript(AsyncWebServerRequest *request);
void handleBootstrapStyle(AsyncWebServerRequest *request);
void handleBootstrapScript(AsyncWebServerRequest *request);
void setup();
}
#endif

35
include/wifi_utils.h Normal file
View File

@@ -0,0 +1,35 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef WIFI_UTILS_H_
#define WIFI_UTILS_H_
#include <Arduino.h>
namespace WIFI_Utils {
void checkWiFi();
void startAutoAP();
void startWiFi();
void checkAutoAPTimeout();
void setup();
}
#endif

42
include/wx_utils.h Normal file
View File

@@ -0,0 +1,42 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef WX_UTILS_H_
#define WX_UTILS_H_
#include <Adafruit_Sensor.h>
#include <Adafruit_AHTX0.h>
#include <Adafruit_BME280.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_BME680.h>
#include "Adafruit_Si7021.h"
#include <Arduino.h>
namespace WX_Utils {
void getWxModuleAddres();
void setup();
String generateTempString(const float sensorTemp);
String generateHumString(const float sensorHum);
String generatePresString(const float sensorPres);
String readDataSensor();
}
#endif

View File

@@ -1,39 +0,0 @@
@echo off
echo Loading...
where /q python.exe
if %errorlevel% neq 0 (
echo "Python is not installed. Please install it and try again."
exit /b
)
where /q pip
if %errorlevel% neq 0 (
echo "pip is not installed. Please install it and try again."
exit /b
)
pip show pyserial >nul 2>&1
if %errorlevel% neq 0 (
echo "Installing pyserial..."
pip install pyserial
)
echo Your firmware will be upgraded without factory reset.
echo:
echo IF THIS IS YOUR FIRST FLASH ON THIS BOARD PLEASE USE install_with_factory_reset.bat INSTEAD!
echo:
echo Available COM ports:
python.exe -c "import serial.tools.list_ports; print('\n'.join([str(c) for c in serial.tools.list_ports.comports()]))"
echo:
set /p port="Enter COM port (for example COM5): "
python.exe ./bin/esptool/esptool.py --chip esp32 --port "%port%" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 0x1000 firmware/bootloader.bin 0x8000 firmware/partitions.bin 0xe000 firmware/boot_app0.bin 0x10000 firmware/firmware.bin
pause

View File

@@ -1,9 +0,0 @@
#!/bin/bash
echo "Your firmware will be upgraded without factory reset."
echo "IF THIS IS YOUR FIRST FLASH ON THIS BOARD PLEASE USE install_with_factory_reset.sh INSTEAD!"
read -p "Enter COM port (for example /dev/ttyS5): " port
python3 ./bin/esptool/esptool.py --chip esp32 --port "$port" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 0x1000 firmware/bootloader.bin 0x8000 firmware/partitions.bin 0xe000 firmware/boot_app0.bin 0x10000 firmware/firmware.bin
echo "Firmware flashed"

View File

@@ -1,44 +0,0 @@
@echo off
echo Loading...
where /q python.exe
if %errorlevel% neq 0 (
echo "Python is not installed. Please install it and try again."
exit /b
)
where /q pip
if %errorlevel% neq 0 (
echo "pip is not installed. Please install it and try again."
exit /b
)
pip show pyserial >nul 2>&1
if %errorlevel% neq 0 (
echo "Installing pyserial..."
pip install pyserial
)
echo Your current configuration will be LOST!!!
echo Your current configuration will be LOST!!!
echo Your current configuration will be LOST!!!
echo:
echo If you already have this board flashed with our firmware please use install_upgrade.bat instead!
echo:
echo Available COM ports:
python.exe -c "import serial.tools.list_ports; print('\n'.join([str(c) for c in serial.tools.list_ports.comports()]))"
echo:
set /p port="Enter COM port (for example COM5): "
python.exe bin/esptool/esptool.py --chip esp32 --port "%port%" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 2686976 firmware/spiffs.bin
python.exe bin/esptool/esptool.py --chip esp32 --port "%port%" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 4MB 0x1000 firmware/bootloader.bin 0x8000 firmware/partitions.bin 0xe000 firmware/boot_app0.bin 0x10000 firmware/firmware.bin
pause

View File

@@ -11,8 +11,13 @@
[platformio]
default_envs = ttgo-lora32-v21
extra_configs =
common_settings.ini
variants/*/platformio.ini
[env]
platform = espressif32 @ 6.3.1
platform = espressif32 @ 6.7.0
board_build.partitions = min_spiffs.csv
framework = arduino
monitor_speed = 115200
board_build.embed_files =
@@ -21,65 +26,7 @@ board_build.embed_files =
data_embed/script.js.gz
data_embed/bootstrap.css.gz
data_embed/bootstrap.js.gz
lib_deps =
bblanchon/ArduinoJson@^6.20.2
sandeepmistry/LoRa@^0.8.0
adafruit/Adafruit GFX Library @ 1.11.5
adafruit/Adafruit SSD1306 @ 2.5.7
mikalhart/TinyGPSPlus @ 1.0.3
adafruit/Adafruit Unified Sensor@^1.1.9
adafruit/Adafruit BME280 Library@^2.2.2
adafruit/Adafruit BMP280 Library@^2.6.8
adafruit/Adafruit BME680 Library@^2.0.4
jgromes/RadioLib @ 6.1.0
lewisxhe/XPowersLib@^0.1.8
ayushsharma82/ElegantOTA@^3.1.0
ottowinter/ESPAsyncWebServer-esphome@3.0.0
esphome/AsyncTCP-esphome@2.1.1
data_embed/favicon.png.gz
extra_scripts =
pre:tools/compress.py
[env:ttgo-lora32-v21]
board = ttgo-lora32-v21
build_flags = -Werror -Wall -DTTGO_T_LORA32_V2_1 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
[env:heltec-lora32-v2]
board = ttgo-lora32-v21
build_flags = -Werror -Wall -DHELTEC_V2 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
[env:heltec_wifi_lora_32_V3]
board = heltec_wifi_lora_32_V3
build_flags = -Werror -Wall -DHELTEC_V3 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
board_build.mcu = esp32s3
[env:ESP32_DIY_LoRa]
board = esp32dev
build_flags = -Werror -Wall -DESP32_DIY_LoRa -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
[env:ESP32_DIY_1W_LoRa]
board = esp32dev
build_flags = -Werror -Wall -DESP32_DIY_1W_LoRa -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
[env:ttgo-t-beam-v1_2]
board = ttgo-t-beam
build_flags = -Werror -Wall -DTTGO_T_Beam_V1_2 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
[env:ttgo-t-beam-v1]
board = ttgo-t-beam
build_flags = -Werror -Wall -DTTGO_T_Beam_V1_0 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
[env:ttgo-t-beam-v1_SX1268]
board = ttgo-t-beam
build_flags = -Werror -Wall -DTTGO_T_Beam_V1_0_SX1268 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
[env:ttgo-t-beam-v1_2_SX1262]
board = ttgo-t-beam
build_flags = -Werror -Wall -DTTGO_T_Beam_V1_2_SX1262 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
[env:heltec_wireless_stick_lite]
platform = espressif32
board = heltec_wireless_stick_lite
board_build.mcu = esp32c3
board_build.f_cpu = 240000000L
build_flags = -Werror -Wall -DHELTEC_CT62 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1
debug_tool = esp-prog

227
src/A7670_utils.cpp Normal file
View File

@@ -0,0 +1,227 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include "configuration.h"
#include "aprs_is_utils.h"
#include "board_pinout.h"
#include "A7670_utils.h"
#include "lora_utils.h"
#include "display.h"
#include "utils.h"
#ifdef HAS_A7670
#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>
#include <StreamDebugger.h>
StreamDebugger debugger(SerialAT, Serial);
TinyGsm modem(debugger);
extern Configuration Config;
extern String firstLine;
bool modemStartUp = false;
bool serverStartUp = false;
bool userBytesSent = false;
bool beaconBytesSent = false;
bool beaconSent = false;
bool stationBeacon = false;
extern bool modemLoggedToAPRSIS;
namespace A7670_Utils {
bool checkModemOn() {
bool modemReady = false;
Serial.print("Starting Modem ... ");
displayShow(firstLine, "Starting Modem...", " ", " ", 0);
pinMode(A7670_ResetPin, OUTPUT); //A7670 Reset
digitalWrite(A7670_ResetPin, LOW);
delay(100);
digitalWrite(A7670_ResetPin, HIGH);
delay(3000);
digitalWrite(A7670_ResetPin, LOW);
pinMode(A7670_PWR_PIN, OUTPUT);
digitalWrite(A7670_PWR_PIN, LOW);
delay(100);
digitalWrite(A7670_PWR_PIN, HIGH);
delay(1000);
digitalWrite(A7670_PWR_PIN, LOW);
int i = 20;
while (i) {
SerialAT.println("AT");
delay(500);
if (SerialAT.available()) {
String r = SerialAT.readString();
//Serial.println(r);
if ( r.indexOf("OK") >= 0 ) {
modemReady = true;
i = 1;
Serial.println("Modem Ready!\n");
displayShow(firstLine, "Starting Modem...", "---> Modem Ready", " ", 0);
return true;
}
}
if (!modemReady) {
delay(500);
}
i--;
}
return false;
}
void setup() {
SerialAT.begin(115200, SERIAL_8N1, A7670_RX_PIN, A7670_TX_PIN);
if (checkModemOn()) {;
delay(1000);
//setup_gps(); // if gps active / won't be need for now
} else {
displayShow(firstLine, "Starting Modem...", "---> Failed !!!", " ", 0);
Serial.println(F("*********** Failed to connect to the modem! ***********"));
}
}
bool checkATResponse(const String& ATMessage) {
int delayATMessage = 3000;
bool validAT = false;
//Serial.println(ATMessage);
int i = 10;
while (i) {
if (!validAT) {
SerialAT.println(ATMessage);
}
delay(500);
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);
Serial.println("#################### APRS-IS FEED ####################");
validAT = true;
i = 1;
delayATMessage = 0;
} else if (ATMessage == "AT+NETOPEN" && responseOKIndex >= 0) {
Serial.println("Port Open!");
displayShow(firstLine, "Opening Port...", "---> Port Open", " ", 0);
validAT = true;
i = 1;
delayATMessage = 0;
} else if (ATMessage == "AT+NETOPEN" && response.indexOf("Network is already opened") >= 0) {
Serial.println("Port Open! (was already opened)");
displayShow(firstLine, "Opening Port...", "---> Port Open", " ", 0);
validAT = true;
i = 1;
delayATMessage = 0;
} else if (ATMessage.indexOf("AT+CIPOPEN") == 0 && response.indexOf("PB DONE") >= 0) {
Serial.println("Contacted!");
displayShow(firstLine, "Connecting APRS-IS...", "---> Contacted", " ", 0);
validAT = true;
i = 1;
delayATMessage = 0;
} else if (ATMessage.indexOf("AT+CIPSEND=0,") == 0 && response.indexOf(">") >= 0) {
Serial.print(".");
validAT = true;
i = 1;
delayATMessage = 0;
} else if (callsignIndex >= 3 && !modemLoggedToAPRSIS && responseOKIndex >= 0 && !stationBeacon) { // login info
validAT = true;
delayATMessage = 0;
} else if (callsignIndex == 0 && !beaconSent && responseOKIndex >= 0 && !stationBeacon) { // self beacon or querys
validAT = true;
i = 1;
delayATMessage = 0;
} else if (stationBeacon && responseOKIndex >= 0) { //upload others beacons
validAT = true;
i = 1;
delayATMessage = 0;
}
}
delay(delayATMessage);
i--;
}
return validAT;
}
void APRS_IS_connect() {
String loginInfo = "user ";
loginInfo += Config.callsign;
loginInfo += " pass ";
loginInfo += String(Config.aprs_is.passcode);
loginInfo += " vers CA2RXU_LoRa_iGate 1.3 filter ";
loginInfo += Config.aprs_is.filter;
Serial.println("-----> Connecting to APRS IS");
while (!modemStartUp) {
Serial.print("Opening Port... ");
displayShow(firstLine, "Opening Port...", " ", " ", 0);
modemStartUp = checkATResponse("AT+NETOPEN");
delay(2000);
} while (!serverStartUp) {
Serial.print("Connecting APRS-IS Server... ");
displayShow(firstLine, "Connecting APRS-IS...", " ", " ", 0);
serverStartUp = checkATResponse("AT+CIPOPEN=0,\"TCP\",\"" + String(Config.aprs_is.server) + "\"," + String(Config.aprs_is.port));
delay(2000);
} while (!userBytesSent) {
Serial.print("Writing User Login Data ");
displayShow(firstLine, "Connecting APRS-IS...", "---> User Login Data", " ", 0);
userBytesSent = checkATResponse("AT+CIPSEND=0," + String(loginInfo.length()+1));
delay(2000);
} while (!modemLoggedToAPRSIS) {
Serial.print(".");
modemLoggedToAPRSIS = checkATResponse(loginInfo);
delay(2000);
}
}
void uploadToAPRSIS(const String& packet) {
beaconBytesSent = checkATResponse("AT+CIPSEND=0," + String(packet.length()+1));
delay(2000);
if (beaconBytesSent) {
Serial.print(".");
beaconSent = checkATResponse(packet);
}
if (!beaconSent) {
Serial.println("------------------------------------> UPLOAD FAILED!!!");
} else {
Serial.println("Packet Uploaded to APRS-IS!");
}
beaconBytesSent = false;
beaconSent = false;
}
void listenAPRSIS() {
if (SerialAT.available()) {
String packetAPRSIS = SerialAT.readStringUntil('\r');
packetAPRSIS.trim();
if (!packetAPRSIS.startsWith("#") && packetAPRSIS.indexOf("+IPD") == -1 && packetAPRSIS.indexOf("RECV") == -1 && packetAPRSIS.length() > 0) {
APRS_IS_Utils::processAPRSISPacket(packetAPRSIS);
}
}
delay(1);
}
}
#endif

View File

@@ -1,113 +1,220 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
/*___________________________________________________________________
██╗ ██████╗ ██████╗ █████╗ █████╗ ██████╗ ██████╗ ███████╗
██║ ██╔═══██╗██╔══██╗██╔══██╗ ██╔══██╗██╔══██╗██╔══██╗██╔════╝
██║ ██║ ██║██████╔╝███████║ ███████║██████╔╝██████╔╝███████╗
██║ ██║ ██║██╔══██╗██╔══██║ ██╔══██║██╔═══╝ ██╔══██╗╚════██║
███████╗╚██████╔╝██║ ██║██║ ██║ ██║ ██║██║ ██║ ██║███████║
╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝
██╗ ██████╗ █████╗ ████████╗███████╗
██║██╔════╝ ██╔══██╗╚══██╔══╝██╔════╝
██║██║ ███╗███████║ ██║ █████╗
██║██║ ██║██╔══██║ ██║ ██╔══╝
██║╚██████╔╝██║ ██║ ██║ ███████╗
╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
Ricardo Guzman - CA2RXU
https://github.com/richonguzman/LoRa_APRS_iGate
(donations : http://paypal.me/richonguzman)
___________________________________________________________________*/
#include <ElegantOTA.h>
#include <TinyGPS++.h>
#include <Arduino.h>
#include <WiFi.h>
#include <vector>
#include "configuration.h"
#include "aprs_is_utils.h"
#include "station_utils.h"
#include "battery_utils.h"
#include "board_pinout.h"
#include "syslog_utils.h"
#include "pins_config.h"
#include "query_utils.h"
#include "power_utils.h"
#include "sleep_utils.h"
#include "mqtt_utils.h"
#include "lora_utils.h"
#include "wifi_utils.h"
#include "digi_utils.h"
#include "gps_utils.h"
#include "bme_utils.h"
#include "web_utils.h"
#include "tnc_utils.h"
#include "ntp_utils.h"
#include "wx_utils.h"
#include "display.h"
#include "utils.h"
#ifdef HAS_A7670
#include "A7670_utils.h"
#endif
Configuration Config;
WiFiClient espClient;
String versionDate = "2026-02-20";
String versionNumber = "3.2";
Configuration Config;
WiFiClient aprsIsClient;
WiFiClient mqttClient;
#ifdef HAS_GPS
HardwareSerial gpsSerial(1);
TinyGPSPlus gps;
uint32_t gpsSatelliteTime = 0;
bool gpsInfoToggle = false;
#endif
String versionDate = "2024.02.25";
int myWiFiAPIndex = 0;
int myWiFiAPSize = Config.wifiAPs.size();
WiFi_AP *currentWiFi = &Config.wifiAPs[myWiFiAPIndex];
uint8_t myWiFiAPIndex = 0;
int myWiFiAPSize = Config.wifiAPs.size();
WiFi_AP *currentWiFi = &Config.wifiAPs[myWiFiAPIndex];
int stationMode = Config.stationMode;
bool statusAfterBoot = true;
bool beaconUpdate = true;
uint32_t lastBeaconTx = 0;
uint32_t previousWiFiMillis = 0;
uint32_t lastScreenOn = millis();
bool isUpdatingOTA = false;
uint32_t lastBatteryCheck = 0;
uint32_t lastWiFiCheck = 0;
bool WiFiConnect = true;
bool WiFiConnected = false;
int lastStationModeState = 1;
bool backupDigiMode = false;
bool modemLoggedToAPRSIS = false;
bool WiFiAutoAPStarted = false;
long WiFiAutoAPTime = false;
#ifdef HAS_EPAPER
uint32_t lastEpaperTime = 0;
extern String lastEpaperText;
#endif
String batteryVoltage;
std::vector<ReceivedPacket> receivedPackets;
std::vector<String> lastHeardStation;
std::vector<String> lastHeardStation_temp;
std::vector<String> packetBuffer;
std::vector<String> packetBuffer_temp;
String firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine;
String firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, iGateBeaconPacket, iGateLoRaBeaconPacket;
void setup() {
Serial.begin(115200);
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2)
pinMode(batteryPin, INPUT);
#endif
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
pinMode(internalLedPin, OUTPUT);
#endif
if (Config.externalVoltageMeasurement) {
pinMode(Config.externalVoltagePin, INPUT);
}
#if defined(TTGO_T_Beam_V1_0) || defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2) || defined(TTGO_T_Beam_V1_2_SX1262)
POWER_Utils::setup();
#endif
delay(1000);
Utils::setupDisplay();
WIFI_Utils::setup();
LoRa_Utils::setup();
Utils::validateDigiFreqs();
iGateBeaconPacket = GPS_Utils::generateBeacon();
iGateLoRaBeaconPacket = GPS_Utils::generateiGateLoRaBeacon();
Utils::validateFreqs();
GPS_Utils::setup();
STATION_Utils::loadBlacklistAndManagers();
Utils::startupDelay();
SLEEP_Utils::setup();
WIFI_Utils::setup();
NTP_Utils::setup();
SYSLOG_Utils::setup();
BME_Utils::setup();
WX_Utils::setup();
WEB_Utils::setup();
TNC_Utils::setup();
MQTT_Utils::setup();
#ifdef HAS_A7670
A7670_Utils::setup();
#endif
Utils::checkRebootMode();
APRS_IS_Utils::firstConnection();
SLEEP_Utils::checkSerial();
}
void loop() {
WEB_Utils::loop();
if (Config.digi.ecoMode == 1) {
SLEEP_Utils::checkWakeUpFlag();
Utils::checkBeaconInterval();
STATION_Utils::processOutputPacketBufferUltraEcoMode();
Utils::checkSleepByLowBatteryVoltage(1);
SLEEP_Utils::startSleeping();
} else {
WIFI_Utils::checkAutoAPTimeout();
WIFI_Utils::checkIfAutoAPShouldPowerOff();
if (isUpdatingOTA) {
ElegantOTA.loop();
return; // Don't process IGate and Digi during OTA update
}
if (stationMode==1 || stationMode==2 ) { // iGate (1 Only Rx / 2 Rx+Tx)
if (!WiFiConnected) {
#ifdef HAS_GPS
if (Config.beacon.gpsActive) {
if (millis() - gpsSatelliteTime > 5000) {
gpsInfoToggle = !gpsInfoToggle;
gpsSatelliteTime = millis();
}
if (gpsInfoToggle) {
thirdLine = "Satellite(s): ";
String gpsData = String(gps.satellites.value());
if (gpsData.length() < 2) gpsData = "0" + gpsData; // Ensure two-digit formatting
thirdLine += gpsData;
} else {
thirdLine = Utils::getLocalIP();
}
} else {
thirdLine = Utils::getLocalIP();
}
#else
thirdLine = Utils::getLocalIP();
#endif
#ifdef HAS_A7670
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();
#endif
NTP_Utils::update();
TNC_Utils::loop();
MQTT_Utils::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 = "";
if (Config.loramodule.rxActive) {
packet = LoRa_Utils::receivePacket(); // We need to fetch LoRa packet above APRSIS and Digi
}
WIFI_Utils::checkWiFi();
if (!espClient.connected()) {
APRS_IS_Utils::connect();
}
APRS_IS_Utils::loop();
} else if (stationMode==3 || stationMode==4) { // DigiRepeater (3 RxFreq=TxFreq / 4 RxFreq!=TxFreq)
DIGI_Utils::loop();
} else if (stationMode==5) { // iGate when WiFi and APRS available , DigiRepeater when not (RxFreq=TxFreq)
Utils::checkWiFiInterval();
thirdLine = Utils::getLocalIP();
if (WiFi.status() == WL_CONNECTED) { // iGate Mode
if (!espClient.connected()) {
APRS_IS_Utils::connect();
if (packet != "") {
if (Config.aprs_is.active) { // If APRSIS enabled
APRS_IS_Utils::processLoRaPacket(packet); // Send received packet to APRSIS
}
if (lastStationModeState == 1) {
iGateBeaconPacket = GPS_Utils::generateBeacon();
lastStationModeState = 0;
if (Config.loramodule.txActive && (Config.digi.mode == 2 || Config.digi.mode == 3 || backupDigiMode)) { // If Digi enabled
STATION_Utils::clean25SegBuffer();
DIGI_Utils::processLoRaPacket(packet); // Send received packet to Digi
}
APRS_IS_Utils::loop();
} else { // DigiRepeater Mode
DIGI_Utils::loop();
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
STATION_Utils::processOutputPacketBuffer();
#ifdef HAS_EPAPER // Only consider updating every 10 seconds (when data to show is different from before)
if(lastEpaperTime == 0 || millis() - lastEpaperTime > 10000) {
String posibleEpaperText = firstLine + secondLine + thirdLine + fourthLine + fifthLine + sixthLine + seventhLine;
if (lastEpaperText != posibleEpaperText) {
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
lastEpaperText = posibleEpaperText;
lastEpaperTime = millis();
}
}
#else
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
#endif
Utils::checkRebootTime();
Utils::checkSleepByLowBatteryVoltage(1);
}
}

View File

@@ -1,44 +1,74 @@
#include <ElegantOTA.h>
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <APRSPacketLib.h>
#include <WiFi.h>
#include "configuration.h"
#include "aprs_is_utils.h"
#include "station_utils.h"
#include "board_pinout.h"
#include "syslog_utils.h"
#include "query_utils.h"
#include "lora_utils.h"
#include "A7670_utils.h"
#include "digi_utils.h"
#include "tnc_utils.h"
#include "display.h"
#include "utils.h"
extern Configuration Config;
extern WiFiClient espClient;
extern int internalLedPin;
extern uint32_t lastScreenOn;
extern int stationMode;
extern String firstLine;
extern String secondLine;
extern String thirdLine;
extern String fourthLine;
extern String fifthLine;
extern String sixthLine;
extern String seventhLine;
extern Configuration Config;
extern WiFiClient aprsIsClient;
extern uint32_t lastScreenOn;
extern String firstLine;
extern String secondLine;
extern String thirdLine;
extern String fourthLine;
extern String fifthLine;
extern String sixthLine;
extern String seventhLine;
extern bool modemLoggedToAPRSIS;
extern bool backupDigiMode;
extern String versionNumber;
uint32_t lastRxTime = millis();
bool passcodeValid = false;
uint32_t lastServerCheck = 0;
#ifdef HAS_A7670
extern bool stationBeacon;
#endif
namespace APRS_IS_Utils {
void upload(String line) {
espClient.print(line + "\r\n");
void upload(const String& line) {
aprsIsClient.print(line + "\r\n");
}
void connect(){
int count = 0;
String aprsauth;
void connect() {
Serial.print("Connecting to APRS-IS ... ");
while (!espClient.connect(Config.aprs_is.server.c_str(), Config.aprs_is.port) && count < 20) {
uint8_t count = 0;
while (!aprsIsClient.connect(Config.aprs_is.server.c_str(), Config.aprs_is.port) && count < 20) {
Serial.println("Didn't connect with server...");
delay(1000);
espClient.stop();
espClient.flush();
aprsIsClient.stop();
aprsIsClient.flush();
Serial.println("Run client.stop");
Serial.println("Trying to connect with Server: " + String(Config.aprs_is.server) + " AprsServerPort: " + String(Config.aprs_is.port));
count++;
@@ -47,192 +77,334 @@ namespace APRS_IS_Utils {
if (count == 20) {
Serial.println("Tried: " + String(count) + " FAILED!");
} else {
Serial.println("Connected!\n(Server: " + String(Config.aprs_is.server) + " / Port: " + String(Config.aprs_is.port) +")");
aprsauth = "user " + Config.callsign + " pass " + Config.aprs_is.passcode + " vers CA2RXU_LoRa_iGate 1.3 filter t/m/" + Config.callsign + "/" + (String)Config.aprs_is.reportingDistance;// + "\r\n";
upload(aprsauth);
delay(200);
Serial.println("Connected!\n(Server: " + String(Config.aprs_is.server) + " / Port: " + String(Config.aprs_is.port) + ")");
// String filter = "t/m/" + Config.callsign + "/" + (String)Config.aprs_is.reportingDistance;
String aprsAuth = "user ";
aprsAuth += Config.callsign;
aprsAuth += " pass ";
aprsAuth += Config.aprs_is.passcode;
aprsAuth += " vers CA2RXUiGate ";
aprsAuth += versionNumber;
aprsAuth += " filter ";
aprsAuth += Config.aprs_is.filter;
upload(aprsAuth);
}
}
void checkStatus() {
String wifiState, aprsisState;
if (WiFi.status() == WL_CONNECTED) {
wifiState = "OK";
wifiState = "OK";
} else {
wifiState = "AP";
if (!Config.display.alwaysOn) {
display_toggle(true);
}
lastScreenOn = millis();
}
if (espClient.connected()) {
aprsisState = "OK";
} else {
aprsisState = "--";
if (!Config.display.alwaysOn) {
display_toggle(true);
}
lastScreenOn = millis();
}
secondLine = "WiFi: " + wifiState + " APRS-IS: " + aprsisState;
}
String createPacket(String packet) {
if (stationMode>1) {
return packet.substring(3, packet.indexOf(":")) + ",qAR," + Config.callsign + packet.substring(packet.indexOf(":"));// + "\n";
} else {
return packet.substring(3, packet.indexOf(":")) + ",qAO," + Config.callsign + packet.substring(packet.indexOf(":"));// + "\n";
}
}
void processLoRaPacket(String packet) {
bool queryMessage = false;
String aprsPacket, Sender, AddresseeAndMessage, Addressee, ackMessage, receivedMessage;
if (packet != "") {
#ifdef TextSerialOutputForApp
Serial.println(packet.substring(3));
#else
Serial.print("Received Lora Packet : " + String(packet));
#endif
if ((packet.substring(0,3) == "\x3c\xff\x01") && (packet.indexOf("TCPIP") == -1) && (packet.indexOf("NOGATE") == -1) && (packet.indexOf("RFONLY") == -1)) {
#ifndef TextSerialOutputForApp
Serial.print(" ---> APRS LoRa Packet!");
#endif
Sender = packet.substring(3,packet.indexOf(">"));
if (Sender != Config.callsign) { // avoid listening yourself by digirepeating
AddresseeAndMessage = packet.substring(packet.indexOf("::")+2);
Addressee = AddresseeAndMessage.substring(0,AddresseeAndMessage.indexOf(":"));
Addressee.trim();
if (stationMode!=1 && Config.igateRepeatsLoRaPackets && Addressee != Config.callsign) { // if its not for me
String digiRepeatedPacket = DIGI_Utils::generateDigiRepeatedPacket(packet.substring(3), Config.callsign);
if (digiRepeatedPacket != "X") {
delay(500);
LoRa_Utils::sendNewPacket("APRS", digiRepeatedPacket);
}
}
if (packet.indexOf("::") > 10 && Addressee == Config.callsign) { // its a message for me!
if (AddresseeAndMessage.indexOf("{")>0) { // ack?
ackMessage = "ack" + AddresseeAndMessage.substring(AddresseeAndMessage.indexOf("{")+1);
ackMessage.trim();
delay(4000);
//Serial.println(ackMessage);
for(int i = Sender.length(); i < 9; i++) {
Sender += ' ';
}
LoRa_Utils::sendNewPacket("APRS", Config.callsign + ">APLRG1,RFONLY,WIDE1-1::" + Sender + ":" + ackMessage);
receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1, AddresseeAndMessage.indexOf("{"));
} else {
receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1);
}
if (receivedMessage.indexOf("?") == 0) {
queryMessage = true;
delay(2000);
if (!Config.display.alwaysOn) {
display_toggle(true);
}
LoRa_Utils::sendNewPacket("APRS", QUERY_Utils::process(receivedMessage, Sender, "LoRa"));
lastScreenOn = millis();
show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, "Callsign = " + Sender, "TYPE --> QUERY", 0);
}
}
if (!queryMessage) {
aprsPacket = createPacket(packet);
if (!Config.display.alwaysOn) {
display_toggle(true);
}
lastScreenOn = millis();
upload(aprsPacket);
#ifndef TextSerialOutputForApp
Serial.println(" ---> Uploaded to APRS-IS");
#endif
STATION_Utils::updateLastHeard(Sender);
Utils::typeOfPacket(aprsPacket, "LoRa-APRS");
show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
}
}
if (backupDigiMode || Config.digi.ecoMode == 1 || Config.digi.ecoMode == 2) {
wifiState = "--";
} else {
#ifndef TextSerialOutputForApp
Serial.println(" ---> LoRa Packet Ignored (first 3 bytes or TCPIP/NOGATE/RFONLY)\n");
#endif
wifiState = "AP";
}
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
}
lastScreenOn = millis();
}
if (!Config.aprs_is.active) {
aprsisState = "OFF";
} else {
#ifdef HAS_A7670
if (modemLoggedToAPRSIS) {
aprsisState = "OK";
} else {
aprsisState = "--";
}
#else
if (aprsIsClient.connected()) {
aprsisState = "OK";
} else {
aprsisState = "--";
}
#endif
if(aprsisState == "--" && !Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
lastScreenOn = millis();
}
}
secondLine = "WiFi: ";
secondLine += wifiState;
secondLine += " APRS-IS: ";
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;
}
}
void processAPRSISPacket(String packet) {
String Sender, AddresseeAndMessage, Addressee, receivedMessage;
if (!packet.startsWith("#")){
if (packet.indexOf("::")>0) {
Sender = packet.substring(0,packet.indexOf(">"));
AddresseeAndMessage = packet.substring(packet.indexOf("::")+2);
Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
Addressee.trim();
if (Addressee == Config.callsign) { // its for me!
if (AddresseeAndMessage.indexOf("{")>0) { // ack?
String ackMessage = "ack" + AddresseeAndMessage.substring(AddresseeAndMessage.indexOf("{")+1);
ackMessage.trim();
delay(4000);
//Serial.println(ackMessage);
for(int i = Sender.length(); i < 9; i++) {
Sender += ' ';
String buildPacketToUpload(const String& packet) {
String packetToUpload = packet.substring(3, packet.indexOf(":"));
if (Config.aprs_is.active && passcodeValid && Config.aprs_is.messagesToRF) {
packetToUpload += ",qAR,";
} else {
packetToUpload += ",qAO,";
}
packetToUpload += Config.callsign;
packetToUpload += checkForStartingBytes(packet.substring(packet.indexOf(":")));
return packetToUpload;
}
bool processReceivedLoRaMessage(const String& sender, const String& packet, bool thirdParty) {
String receivedMessage;
if (packet.indexOf("{") > 0) { // ack?
String ackMessage = "ack";
ackMessage.concat(packet.substring(packet.indexOf("{") + 1));
ackMessage.trim();
//Serial.println(ackMessage);
String addToBuffer = Config.callsign;
addToBuffer += ">APLRG1";
if (!thirdParty) addToBuffer += ",RFONLY";
if (Config.beacon.path != "") {
addToBuffer += ",";
addToBuffer += Config.beacon.path;
}
addToBuffer += "::";
String processedSender = sender;
for (int i = sender.length(); i < 9; i++) {
processedSender += ' ';
}
addToBuffer += processedSender;
addToBuffer += ":";
addToBuffer += ackMessage;
STATION_Utils::addToOutputPacketBuffer(addToBuffer);
receivedMessage = packet.substring(packet.indexOf(":") + 1, packet.indexOf("{"));
} else {
receivedMessage = packet.substring(packet.indexOf(":") + 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));
lastScreenOn = millis();
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, "Callsign = " + sender, "TYPE --> QUERY", 0);
return true;
}
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::callsignIsValid(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();
bool queryMessage = false;
if (packet.indexOf("::") > 10 && Addressee == Config.callsign) { // its a message for me!
queryMessage = processReceivedLoRaMessage(Sender, checkForStartingBytes(AddresseeAndMessage), false);
}
String ackPacket = Config.callsign + ">APLRG1,TCPIP,qAC::" + Sender + ":" + ackMessage;// + "\n";
upload(ackPacket);
receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1, AddresseeAndMessage.indexOf("{"));
} else {
receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":")+1);
}
if (receivedMessage.indexOf("?") == 0) {
#ifndef TextSerialOutputForApp
Serial.println("Received Query APRS-IS : " + packet);
#endif
String queryAnswer = QUERY_Utils::process(receivedMessage, Sender, "APRSIS");
//Serial.println("---> QUERY Answer : " + queryAnswer.substring(0,queryAnswer.indexOf("\n")));
if (!Config.display.alwaysOn) {
display_toggle(true);
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);
}
lastScreenOn = millis();
delay(500);
upload(queryAnswer);
SYSLOG_Utils::log("APRSIS Tx", queryAnswer,0,0,0);
fifthLine = "APRS-IS ----> APRS-IS";
sixthLine = Config.callsign;
for (int j=sixthLine.length();j<9;j++) {
sixthLine += " ";
}
sixthLine += "> " + Sender;
seventhLine = "QUERY = " + receivedMessage;
}
} else {
#ifndef TextSerialOutputForApp
Serial.print("Received from APRS-IS : " + packet);
#endif
if ((stationMode==2 || stationMode==5) && STATION_Utils::wasHeard(Addressee)) {
LoRa_Utils::sendNewPacket("APRS", LoRa_Utils::generatePacket(packet));
display_toggle(true);
lastScreenOn = millis();
Utils::typeOfPacket(packet, "APRS-LoRa");
}
}
show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
}
}
}
}
void loop() {
checkStatus();
show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
while (espClient.connected()) {
Utils::checkDisplayInterval();
Utils::checkBeaconInterval();
processLoRaPacket(LoRa_Utils::receivePacket());
if (espClient.available()) {
String aprsisPacket;
aprsisPacket.concat(espClient.readStringUntil('\r'));
processAPRSISPacket(aprsisPacket);
String buildPacketToTx(const String& aprsisPacket, uint8_t packetType) {
String packet = aprsisPacket;
packet.trim();
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("*");
switch (packetType) {
case 0: // gps
if (packet.indexOf(":=") > 0) {
outputPacket += packet.substring(packet.indexOf(":="));
} else {
outputPacket += packet.substring(packet.indexOf(":!"));
}
break;
case 1: // messages
outputPacket += packet.substring(packet.indexOf("::"));
break;
case 2: // status
outputPacket += packet.substring(packet.indexOf(":>"));
break;
case 3: // telemetry
outputPacket += packet.substring(packet.indexOf("::"));
break;
case 4: // mic-e
if (packet.indexOf(":`") > 0) {
outputPacket += packet.substring(packet.indexOf(":`"));
} else {
outputPacket += packet.substring(packet.indexOf(":'"));
}
break;
case 5: // object
outputPacket += packet.substring(packet.indexOf(":;"));
break;
}
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 !!!", "", "", "", 3000);
aprsIsClient.stop();
Config.aprs_is.active = false;
} else if (packet.indexOf("verified") != -1 ) {
if (Config.digi.backupDigiMode) lastServerCheck = currentTime;
passcodeValid = true;
}
}
if (passcodeValid) {
if (packet.startsWith("#")) {
if (Config.digi.backupDigiMode) lastServerCheck = currentTime;
} else {
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?
processAckMessage(Sender, AddresseeAndMessage);
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);
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::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 = currentTime;
Utils::typeOfPacket(packet, 1); // 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::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
}
}
}
}
void listenAPRSIS() {
#ifdef HAS_A7670
A7670_Utils::listenAPRSIS();
#else
if (aprsIsClient.connected()) {
if (aprsIsClient.available()) {
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()) {
connect();
while (!passcodeValid) {
listenAPRSIS();
}
ElegantOTA.loop();
}
}

View File

@@ -1,26 +0,0 @@
#ifndef APRS_IS_UTILS_H_
#define APRS_IS_UTILS_H_
#include <Arduino.h>
//#define TextSerialOutputForApp
/* uncomment the previous line to get text from Serial-Output over USB into PC for:
- PinPoint App ( https://www.pinpointaprs.com )
- APRSIS32 App ( http://aprsisce.wikidot.com )
*/
namespace APRS_IS_Utils {
void upload(String line);
void connect();
void checkStatus();
String createPacket(String unprocessedPacket);
void processLoRaPacket(String packet);
void processAPRSISPacket(String packet);
void loop();
}
#endif

View File

@@ -1,46 +1,277 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Adafruit_INA219.h>
#include "battery_utils.h"
#include "configuration.h"
#include "pins_config.h"
#include "board_pinout.h"
#include "power_utils.h"
#include "utils.h"
extern Configuration Config;
float adcReadingTransformation = (3.3/4095);
float voltageDividerCorrection = 0.288;
extern Configuration Config;
extern uint32_t lastBatteryCheck;
// for External Voltage Measurment (MAX = 15Volts !!!)
float R1 = 100.000; //in Kilo-Ohms
float R2 = 27.000; //in Kilo-Ohms
float readingCorrection = 0.125;
float multiplyCorrection = 0.035;
bool shouldSleepLowVoltage = false;
float adcReadingTransformation = (3.3/4095);
int adcReadings = 20;
float voltageDividerCorrection = 0.288;
float readingCorrection = 0.125;
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)
#define InternalBattery_ADC_Channel ADC1_CHANNEL_7 // t_lora32 pin35
#define ExternalVoltage_ADC_Channel ADC1_CHANNEL_6 // t_lora32 pin34
#endif
#if CONFIG_IDF_TARGET_ESP32
#define ADC_EXAMPLE_CALI_SCHEME ESP_ADC_CAL_VAL_EFUSE_VREF
#elif CONFIG_IDF_TARGET_ESP32S2
#define ADC_EXAMPLE_CALI_SCHEME ESP_ADC_CAL_VAL_EFUSE_TP
#elif CONFIG_IDF_TARGET_ESP32C3
#define ADC_EXAMPLE_CALI_SCHEME ESP_ADC_CAL_VAL_EFUSE_TP
#elif CONFIG_IDF_TARGET_ESP32S3
#define ADC_EXAMPLE_CALI_SCHEME ESP_ADC_CAL_VAL_EFUSE_TP_FIT
#endif
esp_adc_cal_characteristics_t adc_chars;
#endif
bool calibrationEnable = false;
namespace BATTERY_Utils {
float checkBattery() {
int sample;
int sampleSum = 0;
for (int i=0; i<100; i++) {
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2)
sample = analogRead(batteryPin);
float mapVoltage(float voltage, float in_min, float in_max, float out_min, float out_max) {
return (voltage - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
void adcCalibration() {
#ifdef HAS_ADC_CALIBRATION
if (calibrationEnable) {
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(InternalBattery_ADC_Channel, ADC_ATTEN_DB_12);
adc1_config_channel_atten(ExternalVoltage_ADC_Channel, ADC_ATTEN_DB_12);
}
#endif
}
void adcCalibrationCheck() {
#ifdef HAS_ADC_CALIBRATION
esp_err_t ret;
ret = esp_adc_cal_check_efuse(ADC_EXAMPLE_CALI_SCHEME);
/*if (ret == ESP_ERR_NOT_SUPPORTED) {
Serial.println("Calibration scheme not supported, skip software calibration");
} else if (ret == ESP_ERR_INVALID_VERSION) {
Serial.println("eFuse not burnt, skip software calibration");
} else */
if (ret == ESP_OK) {
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_12, ADC_WIDTH_BIT_12, 1100, &adc_chars);
//Serial.printf("eFuse Vref:%u mV\n", adc_chars.vref);
calibrationEnable = true;
} /*else {
Serial.println("Invalid Calibration Arg");
}*/
#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
#if defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
sample = 0;
#endif
sampleSum += sample;
delayMicroseconds(50);
delay(5);
if (err == 0) {
if (addr == 0x40) { // INA219
externalI2CSensorAddress = addr;
}
}
}
return (2 * (sampleSum/100) * adcReadingTransformation) + voltageDividerCorrection;
}
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;
#if defined(HAS_ADC_CALIBRATION)
if (Config.battery.sendInternalVoltage || Config.battery.monitorInternalVoltage || Config.battery.sendExternalVoltage || Config.battery.monitorExternalVoltage) {
adcCalibrationCheck();
adcCalibration();
}
#endif
getI2CVoltageSensorAddress();
if (externalI2CSensorAddress != 0x00) {
if (detectINA219(externalI2CSensorAddress)) {
Serial.println("INA219 sensor found");
externalI2CSensorType = 1; // INA219
}
}
}
float checkInternalVoltage() {
#if defined(HAS_AXP192) || defined(HAS_AXP2101)
if(POWER_Utils::isBatteryConnected()) {
return POWER_Utils::getBatteryVoltage();
} else {
return 0.0;
}
#else
#ifdef ADC_CTRL
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)
sampleSum = 0;
#else
#ifdef HAS_ADC_CALIBRATION
if (calibrationEnable){
sampleSum += adc1_get_raw(InternalBattery_ADC_Channel);
} else {
sampleSum += analogRead(BATTERY_PIN);
}
#else
#ifdef BATTERY_PIN
sampleSum += analogRead(BATTERY_PIN);
#else
sampleSum += 0;
#endif
#endif
#endif
delay(3);
}
#ifdef ADC_CTRL
POWER_Utils::adc_ctrl_OFF();
#ifdef HELTEC_WP_V1
double inputDivider = (1.0 / (10.0 + 10.0)) * 10.0; // The voltage divider is a 10k + 10k resistor in series
#else
double inputDivider = (1.0 / (390.0 + 100.0)) * 100.0; // The voltage divider is a 390k + 100k resistor in series, 100k on the low side.
#endif
return (((sampleSum/adcReadings) * adcReadingTransformation) / inputDivider) + 0.285; // Yes, this offset is excessive, but the ADC on the ESP32s3 is quite inaccurate and noisy. Adjust to own measurements.
#else
#ifdef HAS_ADC_CALIBRATION
if (calibrationEnable){
float voltage = esp_adc_cal_raw_to_voltage(sampleSum / adcReadings, &adc_chars);
voltage *= 2; // for 100K/100K voltage divider
voltage /= 1000;
return voltage;
} else {
return (2 * (sampleSum/adcReadings) * adcReadingTransformation) + voltageDividerCorrection; // raw voltage without mapping
}
#else
#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;
#else
return (2 * (sampleSum/adcReadings) * adcReadingTransformation) + voltageDividerCorrection; // raw voltage without mapping
#endif
#endif
#endif
// return mapVoltage(voltage, 3.34, 4.71, 3.0, 4.2); // mapped voltage
#endif
}
float checkExternalVoltage() {
int sample;
int sampleSum = 0;
for (int i=0; i<100; i++) {
sample = analogRead(Config.externalVoltagePin);
sampleSum += sample;
delayMicroseconds(50);
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
extVoltage = ((((sampleSum/100.0)* adcReadingTransformation) + readingCorrection) * voltageDividerTransformation) - multiplyCorrection;
#endif
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);
}
float extVoltage = sampleSum/100.0;
return extVoltage/1000.0;
} else {
return 0.0;
}
}
void startupBatteryHealth() {
#ifdef BATTERY_PIN
if (Config.battery.monitorInternalVoltage && checkInternalVoltage() < Config.battery.internalSleepVoltage + 0.1) {
shouldSleepLowVoltage = true;
}
#endif
#ifndef HELTEC_WP_V1
if (Config.battery.monitorExternalVoltage && checkExternalVoltage() < Config.battery.externalSleepVoltage + 0.1) {
shouldSleepLowVoltage = true;
}
#endif
if (shouldSleepLowVoltage) {
Utils::checkSleepByLowBatteryVoltage(0);
}
return ((((sampleSum/100)* adcReadingTransformation) + readingCorrection) * ((R1+R2)/R2)) - multiplyCorrection;
}
}

View File

@@ -1,14 +0,0 @@
#ifndef BATTERY_UTILS_H_
#define BATTERY_UTILS_H_
#include <Arduino.h>
namespace BATTERY_Utils {
float checkBattery();
float checkExternalVoltage();
}
#endif

View File

@@ -1,156 +0,0 @@
#include "bme_utils.h"
#include "configuration.h"
#include "gps_utils.h"
#include "display.h"
#define SEALEVELPRESSURE_HPA (1013.25)
#define HEIGHT_CORRECTION 0 // in meters
#define CORRECTION_FACTOR (8.2296) // for meters
extern Configuration Config;
extern String fifthLine;
namespace BME_Utils {
#ifdef BME280Sensor
Adafruit_BME280 bme;
#endif
#ifdef BMP280Sensor
Adafruit_BMP280 bme;
#endif
#ifdef BME680Sensor
Adafruit_BME680 bme;
#endif
void setup() {
if (Config.bme.active) {
bool status;
status = bme.begin(0x76); // Don't forget to join pins for righ direction on BME280!
if (!status) {
Serial.println("Could not find a valid BME280 or BMP280 sensor, check wiring!");
show_display("ERROR", "", "BME/BMP sensor active", "but no sensor found...");
while (1); // sacar esto para que quede pegado si no encuentra BME280
} else {
#ifdef BME280Sensor
Serial.println("init : BME280 Module ... done!");
#endif
#ifdef BMP280Sensor
Serial.println("init : BMP280 Module ... done!");
#endif
#ifdef BME680Sensor
Serial.println("init : BME680 Module ... done!");
#endif
}
} else {
Serial.println("(BME/BMP sensor not 'active' in 'igate_conf.json')");
}
}
String generateTempString(float bmeTemp) {
String strTemp;
strTemp = String((int)bmeTemp);
switch (strTemp.length()) {
case 1:
return "00" + strTemp;
break;
case 2:
return "0" + strTemp;
break;
case 3:
return strTemp;
break;
default:
return "-999";
}
}
String generateHumString(float bmeHum) {
String strHum;
strHum = String((int)bmeHum);
switch (strHum.length()) {
case 1:
return "0" + strHum;
break;
case 2:
return strHum;
break;
case 3:
if ((int)bmeHum == 100) {
return "00";
} else {
return "-99";
}
break;
default:
return "-99";
}
}
String generatePresString(float bmePress) {
String strPress;
strPress = String((int)bmePress);
switch (strPress.length()) {
case 1:
return "000" + strPress + "0";
break;
case 2:
return "00" + strPress + "0";
break;
case 3:
return "0" + strPress + "0";
break;
case 4:
return strPress + "0";
break;
case 5:
return strPress;
break;
default:
return "-99999";
}
}
String readDataSensor() {
String wx, tempStr, humStr, presStr;
float newHum;
float newTemp = bme.readTemperature();
#if defined(BME280Sensor) || defined(BME680Sensor)
newHum = bme.readHumidity();
#endif
#ifdef BMP280Sensor
newHum = 0;
#endif
float newPress = (bme.readPressure() / 100.0F);
#ifdef BME680Sensor
float newGas = bme.gas_resistance / 1000.0; // in Kilo ohms
#endif
//bme.readAltitude(SEALEVELPRESSURE_HPA) // this is for approximate Altitude Calculation.
if (isnan(newTemp) || isnan(newHum) || isnan(newPress)) {
Serial.println("BME/BMP Module data failed");
wx = ".../...g...t...r...p...P...h..b.....";
fifthLine = "";
return wx;
} else {
tempStr = generateTempString((newTemp * 1.8) + 32);
#if defined(BME280Sensor) || defined(BME680Sensor)
humStr = generateHumString(newHum);
#endif
#ifdef BMP280Sensor
humStr = "..";
#endif
presStr = generatePresString(newPress + (HEIGHT_CORRECTION/CORRECTION_FACTOR));
fifthLine = "BME-> " + String(int(newTemp))+"C " + humStr + "% " + presStr.substring(0,4) + "hPa";
wx = ".../...g...t" + tempStr + "r...p...P...h" + humStr + "b" + presStr;
#ifdef BME680Sensor
wx += "Gas: " + String(newGas) + "Kohms ";
#endif
return wx;
}
}
}

View File

@@ -1,33 +0,0 @@
#ifndef BME_UTILS_H_
#define BME_UTILS_H_
#include <Arduino.h>
#include <Adafruit_Sensor.h>
#define BME280Sensor // its set by default but you should comment it with "//"
//#define BMP280Sensor // and delete "//" from the one you want to use.
//#define BME680Sensor
#ifdef BME280Sensor
#include <Adafruit_BME280.h>
#endif
#ifdef BMP280Sensor
#include <Adafruit_BMP280.h>
#endif
#ifdef BME680Sensor
#include <Adafruit_BME680.h>
#endif
namespace BME_Utils {
void setup();
String generateTempString(float bmeTemp);
String generateHumString(float bmeHum);
String generatePresString(float bmePress);
String readDataSensor();
}
#endif

View File

@@ -1,83 +1,200 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <ArduinoJson.h>
#include <SPIFFS.h>
#include "configuration.h"
#include "board_pinout.h"
#include "display.h"
void Configuration::writeFile() {
Serial.println("Saving config..");
bool shouldSleepStop = true;
StaticJsonDocument<1536> data;
bool Configuration::writeFile() {
Serial.println("Saving configuration...");
StaticJsonDocument<3584> 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;
data["wifi"]["AP"][i]["latitude"] = wifiAPs[i].latitude;
data["wifi"]["AP"][i]["longitude"] = wifiAPs[i].longitude;
}
if (!configFile) {
Serial.println("Error: Could not open config file for writing");
return false;
}
try {
data["wifi"]["autoAP"]["password"] = wifiAutoAP.password;
data["wifi"]["autoAP"]["powerOff"] = wifiAutoAP.powerOff;
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["stationMode"] = stationMode;
data["iGateComment"] = iGateComment;
data["other"]["startupDelay"] = startupDelay;
data["other"]["beaconInterval"] = beaconInterval;
data["other"]["igateSendsLoRaBeacons"] = igateSendsLoRaBeacons;
data["other"]["igateRepeatsLoRaPackets"] = igateRepeatsLoRaPackets;
data["other"]["rememberStationTime"] = rememberStationTime;
data["other"]["sendBatteryVoltage"] = sendBatteryVoltage;
data["other"]["externalVoltageMeasurement"] = externalVoltageMeasurement;
data["other"]["externalVoltagePin"] = externalVoltagePin;
data["wifi"]["autoAP"]["password"] = wifiAutoAP.password;
data["wifi"]["autoAP"]["timeout"] = wifiAutoAP.timeout;
data["digi"]["comment"] = digi.comment;
data["digi"]["latitude"] = digi.latitude;
data["digi"]["longitude"] = digi.longitude;
callsign.trim();
data["callsign"] = callsign;
tacticalCallsign.trim();
data["tacticalCallsign"] = tacticalCallsign;
data["aprs_is"]["passcode"] = aprs_is.passcode;
data["aprs_is"]["server"] = aprs_is.server;
data["aprs_is"]["port"] = aprs_is.port;
data["aprs_is"]["reportingDistance"] = aprs_is.reportingDistance;
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["lora"]["iGateFreq"] = loramodule.iGateFreq;
data["lora"]["digirepeaterTxFreq"] = loramodule.digirepeaterTxFreq;
data["lora"]["digirepeaterRxFreq"] = loramodule.digirepeaterRxFreq;
data["lora"]["spreadingFactor"] = loramodule.spreadingFactor;
data["lora"]["signalBandwidth"] = loramodule.signalBandwidth;
data["lora"]["codingRate4"] = loramodule.codingRate4;
data["lora"]["power"] = loramodule.power;
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["display"]["alwaysOn"] = display.alwaysOn;
data["display"]["timeout"] = display.timeout;
data["display"]["turn180"] = display.turn180;
data["beacon"]["sendViaAPRSIS"] = beacon.sendViaAPRSIS;
data["beacon"]["sendViaRF"] = beacon.sendViaRF;
data["beacon"]["beaconFreq"] = beacon.beaconFreq;
data["syslog"]["active"] = syslog.active;
data["syslog"]["server"] = syslog.server;
data["syslog"]["port"] = syslog.port;
data["beacon"]["statusActive"] = beacon.statusActive;
data["beacon"]["statusPacket"] = beacon.statusPacket;
data["bme"]["active"] = bme.active;
data["beacon"]["gpsActive"] = beacon.gpsActive;
data["beacon"]["ambiguityLevel"] = beacon.ambiguityLevel;
data["ota"]["username"] = ota.username;
data["ota"]["password"] = ota.password;
serializeJson(data, configFile);
data["personalNote"] = personalNote;
configFile.close();
data["blacklist"] = blacklist;
Serial.println("Config saved");
data["digi"]["mode"] = digi.mode;
data["digi"]["ecoMode"] = digi.ecoMode;
#if defined(HAS_A7670)
if (digi.ecoMode == 1) data["digi"]["ecoMode"] = 2;
#endif
data["digi"]["backupDigiMode"] = digi.backupDigiMode;
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;
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
rxSpreadingFactor = (rxSpreadingFactor < minSF) ? minSF : (rxSpreadingFactor > maxSF) ? maxSF : rxSpreadingFactor;
txSpreadingFactor = (txSpreadingFactor < minSF) ? minSF : (txSpreadingFactor > maxSF) ? maxSF : txSpreadingFactor;
data["lora"]["rxSpreadingFactor"] = rxSpreadingFactor;
data["lora"]["txSpreadingFactor"] = txSpreadingFactor;
data["display"]["alwaysOn"] = display.alwaysOn;
data["display"]["timeout"] = display.timeout;
data["display"]["turn180"] = display.turn180;
data["battery"]["sendInternalVoltage"] = battery.sendInternalVoltage;
data["battery"]["monitorInternalVoltage"] = battery.monitorInternalVoltage;
data["battery"]["internalSleepVoltage"] = battery.internalSleepVoltage;
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["battery"]["sendVoltageAsTelemetry"] = battery.sendVoltageAsTelemetry;
data["wxsensor"]["active"] = wxsensor.active;
data["wxsensor"]["heightCorrection"] = wxsensor.heightCorrection;
data["wxsensor"]["temperatureCorrection"] = wxsensor.temperatureCorrection;
data["syslog"]["active"] = syslog.active;
data["syslog"]["server"] = syslog.server;
data["syslog"]["port"] = syslog.port;
data["syslog"]["logBeaconOverTCPIP"] = syslog.logBeaconOverTCPIP;
data["tnc"]["enableServer"] = tnc.enableServer;
data["tnc"]["enableSerial"] = tnc.enableSerial;
data["tnc"]["acceptOwn"] = tnc.acceptOwn;
data["tnc"]["aprsBridgeActive"] = tnc.aprsBridgeActive;
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["ota"]["username"] = ota.username;
data["ota"]["password"] = ota.password;
data["webadmin"]["active"] = webadmin.active;
data["webadmin"]["username"] = webadmin.username;
data["webadmin"]["password"] = webadmin.password;
data["remoteManagement"]["managers"] = remoteManagement.managers;
data["remoteManagement"]["rfOnly"] = remoteManagement.rfOnly;
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<1536> data;
bool needsRewrite = false;
StaticJsonDocument<3584> data;
DeserializationError error = deserializeJson(data, configFile);
if (error) {
@@ -89,66 +206,227 @@ bool Configuration::readFile() {
WiFi_AP wifiap;
wifiap.ssid = WiFiArray[i]["ssid"].as<String>();
wifiap.password = WiFiArray[i]["password"].as<String>();
wifiap.latitude = WiFiArray[i]["latitude"].as<double>();
wifiap.longitude = WiFiArray[i]["longitude"].as<double>();
wifiAPs.push_back(wifiap);
}
wifiAutoAP.password = data["wifi"]["autoAP"]["password"].as<String>();
wifiAutoAP.powerOff = data["wifi"]["autoAP"]["powerOff"].as<int>();
if (!data["other"].containsKey("startupDelay")) needsRewrite = true;
startupDelay = data["other"]["startupDelay"] | 0;
callsign = data["callsign"].as<String>();
stationMode = data["stationMode"].as<int>();
iGateComment = data["iGateComment"].as<String>();
beaconInterval = data["other"]["beaconInterval"].as<int>();
igateSendsLoRaBeacons = data["other"]["igateSendsLoRaBeacons"].as<bool>();
igateRepeatsLoRaPackets = data["other"]["igateRepeatsLoRaPackets"].as<bool>();
rememberStationTime = data["other"]["rememberStationTime"].as<int>();
sendBatteryVoltage = data["other"]["sendBatteryVoltage"].as<bool>();
externalVoltageMeasurement = data["other"]["externalVoltageMeasurement"].as<bool>();
externalVoltagePin = data["other"]["externalVoltagePin"].as<int>();
if (!data["wifi"]["autoAP"].containsKey("password") ||
!data["wifi"]["autoAP"].containsKey("timeout")) needsRewrite = true;
wifiAutoAP.password = data["wifi"]["autoAP"]["password"] | "1234567890";
wifiAutoAP.timeout = data["wifi"]["autoAP"]["timeout"] | 10;
digi.comment = data["digi"]["comment"].as<String>();
digi.latitude = data["digi"]["latitude"].as<double>();
digi.longitude = data["digi"]["longitude"].as<double>();
if (!data.containsKey("callsign")) needsRewrite = true;
callsign = data["callsign"] | "NOCALL-10";
if (!data.containsKey("tacticalCallsign")) needsRewrite = true;
tacticalCallsign = data["tacticalCallsign"] | "";
if (!data["aprs_is"].containsKey("active") ||
!data["aprs_is"].containsKey("passcode") ||
!data["aprs_is"].containsKey("server") ||
!data["aprs_is"].containsKey("port") ||
!data["aprs_is"].containsKey("filter") ||
!data["aprs_is"].containsKey("messagesToRF") ||
!data["aprs_is"].containsKey("objectsToRF")) needsRewrite = true;
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";
aprs_is.port = data["aprs_is"]["port"] | 14580;
aprs_is.filter = data["aprs_is"]["filter"] | "m/10";
aprs_is.messagesToRF = data["aprs_is"]["messagesToRF"] | false;
aprs_is.objectsToRF = data["aprs_is"]["objectsToRF"] | false;
aprs_is.passcode = data["aprs_is"]["passcode"].as<String>();
aprs_is.server = data["aprs_is"]["server"].as<String>();
aprs_is.port = data["aprs_is"]["port"].as<int>();
aprs_is.reportingDistance = data["aprs_is"]["reportingDistance"].as<int>();
if (!data["beacon"].containsKey("latitude") ||
!data["beacon"].containsKey("longitude") ||
!data["beacon"].containsKey("comment") ||
!data["beacon"].containsKey("interval") ||
!data["beacon"].containsKey("overlay") ||
!data["beacon"].containsKey("symbol") ||
!data["beacon"].containsKey("path") ||
!data["beacon"].containsKey("sendViaAPRSIS") ||
!data["beacon"].containsKey("sendViaRF") ||
!data["beacon"].containsKey("beaconFreq") ||
!data["beacon"].containsKey("statusActive") ||
!data["beacon"].containsKey("statusPacket") ||
!data["beacon"].containsKey("gpsActive") ||
!data["beacon"].containsKey("ambiguityLevel")) needsRewrite = true;
beacon.latitude = data["beacon"]["latitude"] | 0.0;
beacon.longitude = data["beacon"]["longitude"] | 0.0;
beacon.comment = data["beacon"]["comment"] | "LoRa APRS";
beacon.interval = data["beacon"]["interval"] | 15;
beacon.overlay = data["beacon"]["overlay"] | "L";
beacon.symbol = data["beacon"]["symbol"] | "a";
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.ambiguityLevel = data["beacon"]["ambiguityLevel"] | 0;
loramodule.iGateFreq = data["lora"]["iGateFreq"].as<long>();
loramodule.digirepeaterTxFreq = data["lora"]["digirepeaterTxFreq"].as<long>();
loramodule.digirepeaterRxFreq = data["lora"]["digirepeaterRxFreq"].as<long>();
loramodule.spreadingFactor = data["lora"]["spreadingFactor"].as<int>();
loramodule.signalBandwidth = data["lora"]["signalBandwidth"].as<long>();
loramodule.codingRate4 = data["lora"]["codingRate4"].as<int>();
loramodule.power = data["lora"]["power"].as<int>();
if (!data.containsKey("personalNote")) needsRewrite = true;
personalNote = data["personalNote"] | "personal note here";
display.alwaysOn = data["display"]["alwaysOn"].as<bool>();
display.timeout = data["display"]["timeout"].as<int>();
display.turn180 = data["display"]["turn180"].as<bool>();
if (!data.containsKey("blacklist")) needsRewrite = true;
blacklist = data["blacklist"] | "station callsign";
syslog.active = data["syslog"]["active"].as<bool>();
syslog.server = data["syslog"]["server"].as<String>();
syslog.port = data["syslog"]["port"].as<int>();
if (!data["digi"].containsKey("mode") ||
!data["digi"].containsKey("ecoMode") ||
!data["digi"].containsKey("backupDigiMode")) 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;
bme.active = data["bme"]["active"].as<bool>();
ota.username = data["ota"]["username"].as<String>();
ota.password = data["ota"]["password"].as<String>();
if (!data["lora"].containsKey("rxActive") ||
!data["lora"].containsKey("rxFreq") ||
!data["lora"].containsKey("rxSpreadingFactor") ||
!data["lora"].containsKey("rxCodingRate4") ||
!data["lora"].containsKey("rxSignalBandwidth") ||
!data["lora"].containsKey("txActive") ||
!data["lora"].containsKey("txFreq") ||
!data["lora"].containsKey("txSpreadingFactor") ||
!data["lora"].containsKey("txCodingRate4") ||
!data["lora"].containsKey("txSignalBandwidth") ||
!data["lora"].containsKey("power")) needsRewrite = true;
loramodule.rxActive = data["lora"]["rxActive"] | true;
loramodule.rxFreq = data["lora"]["rxFreq"] | 433775000;
loramodule.rxSpreadingFactor = data["lora"]["rxSpreadingFactor"] | 12;
loramodule.rxCodingRate4 = data["lora"]["rxCodingRate4"] | 5;
loramodule.rxSignalBandwidth = data["lora"]["rxSignalBandwidth"] | 125000;
loramodule.txActive = data["lora"]["txActive"] | false;
loramodule.txFreq = data["lora"]["txFreq"] | 433775000;
loramodule.txSpreadingFactor = data["lora"]["txSpreadingFactor"] | 12;
loramodule.txCodingRate4 = data["lora"]["txCodingRate4"] | 5;
loramodule.txSignalBandwidth = data["lora"]["txSignalBandwidth"] | 125000;
loramodule.power = data["lora"]["power"] | 20;
if (!data["display"].containsKey("alwaysOn") ||
!data["display"].containsKey("timeout") ||
!data["display"].containsKey("turn180")) needsRewrite = true;
#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"].containsKey("sendInternalVoltage") ||
!data["battery"].containsKey("monitorInternalVoltage") ||
!data["battery"].containsKey("internalSleepVoltage") ||
!data["battery"].containsKey("sendExternalVoltage") ||
!data["battery"].containsKey("monitorExternalVoltage") ||
!data["battery"].containsKey("externalSleepVoltage") ||
!data["battery"].containsKey("useExternalI2CSensor") ||
!data["battery"].containsKey("voltageDividerR1") ||
!data["battery"].containsKey("voltageDividerR2") ||
!data["battery"].containsKey("externalVoltagePin") ||
!data["battery"].containsKey("sendVoltageAsTelemetry")) needsRewrite = true;
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.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"].containsKey("active") ||
!data["wxsensor"].containsKey("heightCorrection") ||
!data["wxsensor"].containsKey("temperatureCorrection")) needsRewrite = true;
wxsensor.active = data["wxsensor"]["active"] | false;
wxsensor.heightCorrection = data["wxsensor"]["heightCorrection"] | 0;
wxsensor.temperatureCorrection = data["wxsensor"]["temperatureCorrection"] | 0.0;
if (!data["syslog"].containsKey("active") ||
!data["syslog"].containsKey("server") ||
!data["syslog"].containsKey("port") ||
!data["syslog"].containsKey("logBeaconOverTCPIP")) needsRewrite = true;
syslog.active = data["syslog"]["active"] | false;
syslog.server = data["syslog"]["server"] | "lora.link9.net";
syslog.port = data["syslog"]["port"] | 1514;
syslog.logBeaconOverTCPIP = data["syslog"]["logBeaconOverTCPIP"] | false;
if (!data["tnc"].containsKey("enableServer") ||
!data["tnc"].containsKey("enableSerial") ||
!data["tnc"].containsKey("acceptOwn") ||
!data["tnc"].containsKey("aprsBridgeActive")) needsRewrite = true;
tnc.enableServer = data["tnc"]["enableServer"] | false;
tnc.enableSerial = data["tnc"]["enableSerial"] | false;
tnc.acceptOwn = data["tnc"]["acceptOwn"] | false;
tnc.aprsBridgeActive = data["tnc"]["aprsBridgeActive"] | false;
if (!data["mqtt"].containsKey("active") ||
!data["mqtt"].containsKey("server") ||
!data["mqtt"].containsKey("topic") ||
!data["mqtt"].containsKey("username") ||
!data["mqtt"].containsKey("password") ||
!data["mqtt"].containsKey("port") ||
!data["mqtt"].containsKey("beaconOverMqtt")) needsRewrite = true;
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"].containsKey("username") ||
!data["ota"].containsKey("password")) needsRewrite = true;
ota.username = data["ota"]["username"] | "";
ota.password = data["ota"]["password"] | "";
if (!data["webadmin"].containsKey("active") ||
!data["webadmin"].containsKey("username") ||
!data["webadmin"].containsKey("password")) needsRewrite = true;
webadmin.active = data["webadmin"]["active"] | false;
webadmin.username = data["webadmin"]["username"] | "admin";
webadmin.password = data["webadmin"]["password"] | "";
if (!data["remoteManagement"].containsKey("managers") ||
!data["remoteManagement"].containsKey("rfOnly")) needsRewrite = true;
remoteManagement.managers = data["remoteManagement"]["managers"] | "";
remoteManagement.rfOnly = data["remoteManagement"]["rfOnly"] | true;
if (!data["ntp"].containsKey("server") ||
!data["ntp"].containsKey("gmtCorrection")) needsRewrite = true;
ntp.server = data["ntp"]["server"] | "pool.ntp.org";
ntp.gmtCorrection = data["ntp"]["gmtCorrection"] | 0.0;
if (!data["other"].containsKey("rebootMode") ||
!data["other"].containsKey("rebootModeTime")) needsRewrite = true;
rebootMode = data["other"]["rebootMode"] | false;
rebootModeTime = data["other"]["rebootModeTime"] | 6;
if (!data["other"].containsKey("rememberStationTime")) needsRewrite = true;
rememberStationTime = data["other"]["rememberStationTime"] | 30;
if (wifiAPs.size() == 0) { // If we don't have any WiFi's from config we need to add "empty" SSID for AUTO AP
WiFi_AP wifiap;
wifiap.ssid = "";
wifiap.password = "";
wifiap.latitude = 0.0;
wifiap.longitude = 0.0;
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 {
@@ -157,60 +435,127 @@ bool Configuration::readFile() {
}
}
void Configuration::init() {
void Configuration::setDefaultValues() {
WiFi_AP wifiap;
wifiap.ssid = "";
wifiap.password = "";
wifiap.latitude = 0.0;
wifiap.longitude = 0.0;
wifiap.ssid = "";
wifiap.password = "";
wifiAPs.push_back(wifiap);
wifiAutoAP.password = "1234567890";
wifiAutoAP.powerOff = 15;
startupDelay = 0;
callsign = "N0CALL";
stationMode = 1;
iGateComment = "LoRa_APRS_iGate Development";
wifiAutoAP.password = "1234567890";
wifiAutoAP.timeout = 10;
digi.comment = "LoRa_APRS_iGate Development";
digi.latitude = 0.0;
digi.longitude = 0.0;
callsign = "N0CALL-10";
tacticalCallsign = "";
aprs_is.passcode = "XYZVW";
aprs_is.server = "rotate.aprs2.net";
aprs_is.port = 14580;
aprs_is.reportingDistance = 30;
aprs_is.active = false;
aprs_is.passcode = "XYZVW";
aprs_is.server = "rotate.aprs2.net";
aprs_is.port = 14580;
aprs_is.filter = "m/10";
aprs_is.messagesToRF = false;
aprs_is.objectsToRF = false;
loramodule.iGateFreq = 433775000;
loramodule.digirepeaterTxFreq = 433775000;
loramodule.digirepeaterRxFreq = 433900000;
loramodule.spreadingFactor = 12;
loramodule.signalBandwidth = 125000;
loramodule.codingRate4 = 5;
loramodule.power = 20;
beacon.comment = "LoRa APRS";
beacon.latitude = 0.0;
beacon.longitude = 0.0;
beacon.interval = 15;
beacon.overlay = "L";
beacon.symbol = "a";
beacon.path = "WIDE1-1";
display.alwaysOn = true;
display.timeout = 4;
display.turn180 = false;
beacon.sendViaAPRSIS = true;
beacon.sendViaRF = false;
beacon.beaconFreq = 1;
beacon.statusActive = false;
beacon.statusPacket = "";
syslog.active = false;
syslog.server = "192.168.0.100";
syslog.port = 514;
beacon.gpsActive = false;
beacon.ambiguityLevel = 0;
bme.active = false;
personalNote = "";
ota.username = "";
ota.password = "";
blacklist = "";
beaconInterval = 15;
igateSendsLoRaBeacons = false;
igateRepeatsLoRaPackets = false;
rememberStationTime = 30;
sendBatteryVoltage = false;
externalVoltageMeasurement = false;
externalVoltagePin = 34;
digi.mode = 0;
digi.ecoMode = 0;
digi.backupDigiMode = false;
Serial.println("todo escrito");
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;
display.alwaysOn = true;
display.timeout = 4;
display.turn180 = false;
battery.sendInternalVoltage = false;
battery.monitorInternalVoltage = false;
battery.internalSleepVoltage = 2.9;
battery.sendExternalVoltage = false;
battery.monitorExternalVoltage = false;
battery.externalSleepVoltage = 10.9;
battery.useExternalI2CSensor = false;
battery.voltageDividerR1 = 100.0;
battery.voltageDividerR2 = 27.0;
battery.externalVoltagePin = 34;
battery.sendVoltageAsTelemetry = false;
wxsensor.active = false;
wxsensor.heightCorrection = 0;
wxsensor.temperatureCorrection = 0.0;
syslog.active = false;
syslog.server = "lora.link9.net";
syslog.port = 1514;
syslog.logBeaconOverTCPIP = false;
tnc.enableServer = false;
tnc.enableSerial = false;
tnc.acceptOwn = false;
tnc.aprsBridgeActive = false;
mqtt.active = false;
mqtt.server = "";
mqtt.topic = "aprs-igate";
mqtt.username = "";
mqtt.password = "";
mqtt.port = 1883;
mqtt.beaconOverMqtt = false;
ota.username = "";
ota.password = "";
webadmin.active = false;
webadmin.username = "admin";
webadmin.password = "";
remoteManagement.managers = "";
remoteManagement.rfOnly = true;
ntp.server = "pool.ntp.org";
ntp.gmtCorrection = 0.0;
rebootMode = false;
rebootModeTime = 0;
rememberStationTime = 30;
Serial.println("New Data Created... All is Written!");
}
Configuration::Configuration() {
@@ -218,13 +563,16 @@ Configuration::Configuration() {
Serial.println("SPIFFS Mount Failed");
return;
} else {
Serial.println("montado");
Serial.println("SPIFFS Mounted");
}
bool exists = SPIFFS.exists("/igate_conf.json");
if (!exists) {
init();
setDefaultValues();
writeFile();
delay(1000);
ESP.restart();
}
readFile();
}

View File

@@ -1,108 +0,0 @@
#ifndef CONFIGURATION_H_
#define CONFIGURATION_H_
#include <Arduino.h>
#include <vector>
#include <FS.h>
class WiFi_AP {
public:
String ssid;
String password;
double latitude;
double longitude;
};
class WiFi_Auto_AP {
public:
String password;
int powerOff;
};
class DIGI {
public:
String comment;
double latitude;
double longitude;
};
class APRS_IS {
public:
String passcode;
String server;
int port;
int reportingDistance;
};
class LoraModule {
public:
long iGateFreq;
long digirepeaterTxFreq;
long digirepeaterRxFreq;
int spreadingFactor;
long signalBandwidth;
int codingRate4;
int power;
};
class Display {
public:
bool alwaysOn;
int timeout;
bool turn180;
};
class SYSLOG {
public:
bool active;
String server;
int port;
};
class BME {
public:
bool active;
};
class OTA {
public:
String username;
String password;
};
class Configuration {
public:
String callsign;
int stationMode;
String iGateComment;
int beaconInterval;
bool igateSendsLoRaBeacons;
bool igateRepeatsLoRaPackets;
int rememberStationTime;
bool sendBatteryVoltage;
bool externalVoltageMeasurement;
int externalVoltagePin;
std::vector<WiFi_AP> wifiAPs;
WiFi_Auto_AP wifiAutoAP;
DIGI digi;
APRS_IS aprs_is;
LoraModule loramodule;
Display display;
SYSLOG syslog;
BME bme;
OTA ota;
void init();
void writeFile();
Configuration();
private:
bool readFile();
String _filePath;
};
#endif

View File

@@ -1,18 +1,35 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <WiFi.h>
#include "configuration.h"
#include "station_utils.h"
#include "aprs_is_utils.h"
#include "lora_utils.h"
#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 int stationMode;
extern uint32_t lastScreenOn;
extern int lastStationModeState;
extern String iGateBeaconPacket;
extern String firstLine;
extern String secondLine;
@@ -21,94 +38,138 @@ extern String fourthLine;
extern String fifthLine;
extern String sixthLine;
extern String seventhLine;
extern bool backupDigiMode;
namespace DIGI_Utils {
String generateDigiRepeatedPacket(String packet, String callsign) {
String sender, temp0, tocall, path;
sender = packet.substring(0,packet.indexOf(">"));
temp0 = packet.substring(packet.indexOf(">")+1,packet.indexOf(":"));
if (temp0.indexOf(",") > 2) {
tocall = temp0.substring(0,temp0.indexOf(","));
path = temp0.substring(temp0.indexOf(",")+1,temp0.indexOf(":"));
if (path.indexOf("WIDE1-")>=0) {
String hop = path.substring(path.indexOf("WIDE1-")+6, path.indexOf("WIDE1-")+7);
if (hop.toInt()>=1 && hop.toInt()<=7) {
if (hop.toInt()==1) {
path.replace("WIDE1-1", callsign + "*");
} else {
path.replace("WIDE1-" + hop , callsign + "*,WIDE1-" + String(hop.toInt()-1));
}
String repeatedPacket = sender + ">" + tocall + "," + path + packet.substring(packet.indexOf(":"));
return repeatedPacket;
String cleanPathAsterisks(String path) {
String terms[] = {",WIDE1*", ",WIDE2*", "*"};
for (String term : terms) {
int index = path.indexOf(term);
if (index != -1) path.remove(index, term.length()); // less memory than: tempPath.replace("*", "");
}
return path;
}
String buildPacket(const String& path, const String& packet, bool thirdParty, bool crossFreq) {
String stationCallsign = (Config.tacticalCallsign == "" ? Config.callsign : Config.tacticalCallsign);
String suffix = thirdParty ? ":}" : ":";
int suffixIndex = packet.indexOf(suffix);
String packetToRepeat;
if (!crossFreq) {
int digiMode = Config.digi.mode;
String tempPath = path;
if (tempPath.indexOf("WIDE1-1") != -1 && (digiMode == 2 || digiMode == 3)) { // WIDE1-1 Digipeater
if (tempPath.indexOf("*") != -1 ) return ""; // "*" shouldn't be in WIDE1-1 (only) type of packet
tempPath.replace("WIDE1-1", stationCallsign + "*");
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 3) { // WIDE2-n Digipeater
tempPath = cleanPathAsterisks(path);
if (path.indexOf("WIDE2-1") != -1) {
tempPath.replace("WIDE2-1", stationCallsign + "*");
} else if (path.indexOf("WIDE2-2") != -1) {
tempPath.replace("WIDE2-2", stationCallsign + "*,WIDE2-1");
} else {
return "X";
return "";
}
} else {
return "X";
}
packetToRepeat = packet.substring(0, packet.indexOf(",") + 1);
packetToRepeat += tempPath;
} else { // CrossFreq Digipeater
packetToRepeat = cleanPathAsterisks(packet.substring(0, suffixIndex));
if (packetToRepeat.indexOf(stationCallsign) != -1) return ""; // stationCallsign shouldn't be in path
packetToRepeat += ",";
packetToRepeat += stationCallsign;
packetToRepeat += "*";
}
packetToRepeat += APRS_IS_Utils::checkForStartingBytes(packet.substring(suffixIndex));
return packetToRepeat;
}
String generateDigipeatedPacket(const String& packet, bool thirdParty){
String temp;
if (thirdParty) { // only header is used
const String& header = packet.substring(0, packet.indexOf(":}"));
temp = header.substring(header.indexOf(">") + 1);
} else {
return "X";
temp = packet.substring(packet.indexOf(">") + 1, packet.indexOf(":"));
}
}
int commaIndex = temp.indexOf(",");
int digiMode = Config.digi.mode;
bool crossFreq = abs(Config.loramodule.txFreq - Config.loramodule.rxFreq) >= 125000; // CrossFreq Digi
void processPacket(String packet) {
String loraPacket;
if (packet != "") {
Serial.print("Received Lora Packet : " + String(packet));
if ((packet.substring(0, 3) == "\x3c\xff\x01") && (packet.indexOf("NOGATE") == -1)) {
Serial.println(" ---> APRS LoRa Packet");
String sender = packet.substring(3,packet.indexOf(">"));
STATION_Utils::updateLastHeard(sender);
STATION_Utils::updatePacketBuffer(packet);
Utils::typeOfPacket(packet, "Digi");
if ((stationMode==3 || stationMode==5) && (packet.indexOf("WIDE1-") > 10)) {
loraPacket = generateDigiRepeatedPacket(packet.substring(3), Config.callsign);
if (loraPacket != "X") {
delay(500);
Serial.println(loraPacket);
LoRa_Utils::sendNewPacket("APRS", loraPacket);
display_toggle(true);
lastScreenOn = millis();
}
} else if (stationMode==4){
if (packet.indexOf("WIDE1-") == -1) {
loraPacket = packet.substring(3,packet.indexOf(":")) + "," + Config.callsign + "*" + packet.substring(packet.indexOf(":"));
} else {
loraPacket = packet.substring(3,packet.indexOf(",")+1) + Config.callsign + "*" + packet.substring(packet.indexOf(","));
}
delay(500);
if (stationMode==4) {
LoRa_Utils::changeFreqTx();
}
LoRa_Utils::sendNewPacket("APRS", loraPacket);
if (stationMode==4) {
LoRa_Utils::changeFreqRx();
}
display_toggle(true);
lastScreenOn = millis();
if (commaIndex > 2) { // "path" found
const String& path = temp.substring(commaIndex + 1);
if (digiMode == 2 || backupDigiMode) {
bool hasWide = path.indexOf("WIDE1-1") != -1;
if (hasWide || crossFreq) {
return buildPacket(path, packet, thirdParty, !hasWide);
}
} else {
Serial.println(" ---> LoRa Packet Ignored (first 3 bytes or NOGATE)\n");
return "";
}
if (digiMode == 3) {
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, thirdParty, false); // regular APRS with WIDEn-N
if (crossFreq) return buildPacket(path, packet, thirdParty, true); // CrossFreq (without WIDE)
return "";
}
return "";
}
if (commaIndex == -1 && (digiMode == 2 || backupDigiMode || digiMode == 3) && crossFreq) return buildPacket("", packet, thirdParty, true); // no "path" but is CrossFreq Digi
return "";
}
void loop() {
if (stationMode==3 || stationMode==4 || stationMode==5) {
if (lastStationModeState==0 && stationMode==5) {
iGateBeaconPacket = GPS_Utils::generateBeacon();
lastStationModeState = 1;
String Tx = String(Config.loramodule.digirepeaterTxFreq);
secondLine = "Rx:" + String(Tx.substring(0,3)) + "." + String(Tx.substring(3,6));
secondLine += " Tx:" + String(Tx.substring(0,3)) + "." + String(Tx.substring(3,6));
thirdLine = "<< DigiRepeater >>";
void processLoRaPacket(const String& packet) {
if (packet.indexOf("NOGATE") >= 0) return;
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(">"));
}
String stationCallsign = Config.tacticalCallsign == "" ? Config.callsign : Config.tacticalCallsign;
if (Sender == stationCallsign) return; // Avoid listening to self packets
if (!thirdPartyPacket && Config.tacticalCallsign == "" && !Utils::callsignIsValid(Sender)) return; // No thirdParty + no tactical y no valid callsign
if (STATION_Utils::check25SegBuffer(Sender, temp.substring(temp.indexOf(":") + 2))) {
STATION_Utils::updateLastHeard(Sender);
Utils::typeOfPacket(temp, 2); // Digi
bool queryMessage = false;
int doubleColonIndex = temp.indexOf("::");
if (doubleColonIndex > 10) { // it's a message
String AddresseeAndMessage = temp.substring(doubleColonIndex + 2);
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
Addressee.trim();
if (Addressee == stationCallsign) { // it's a message for me!
queryMessage = APRS_IS_Utils::processReceivedLoRaMessage(Sender, AddresseeAndMessage, thirdPartyPacket);
}
}
if (queryMessage) return; // answer should not be repeated.
String loraPacket = generateDigipeatedPacket(packet.substring(3), thirdPartyPacket);
if (loraPacket != "") {
STATION_Utils::addToOutputPacketBuffer(loraPacket);
if (Config.digi.ecoMode != 1) displayToggle(true);
lastScreenOn = millis();
}
Utils::checkDisplayInterval();
Utils::checkBeaconInterval();
show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
processPacket(LoRa_Utils::receivePacket());
}
}

View File

@@ -1,15 +0,0 @@
#ifndef DIGI_UTILS_H_
#define DIGI_UTILS_H_
#include <Arduino.h>
namespace DIGI_Utils {
String generateDigiRepeatedPacket(String packet, String callsign);
void processPacket(String packet);
void loop();
}
#endif

View File

@@ -1,165 +1,316 @@
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Wire.h>
#include "configuration.h"
#include "pins_config.h"
#include "board_pinout.h"
#include "display.h"
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
extern Configuration Config;
#ifdef HAS_DISPLAY
#ifdef HAS_TFT
#include <TFT_eSPI.h>
void setup_display() {
Wire.begin(OLED_SDA, OLED_SCL);
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite sprite = TFT_eSprite(&tft);
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
if (Config.display.turn180) {
display.setRotation(2);
}
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
display.display();
delay(1000);
#ifdef HELTEC_WIRELESS_TRACKER
#define bigSizeFont 2
#define smallSizeFont 1
#define lineSpacing 10
#endif
#if defined(TTGO_T_DECK_GPS) || defined(TTGO_T_DECK_PLUS)
#define bigSizeFont 5
#define smallSizeFont 2
#define lineSpacing 25
#endif
uint16_t redColor = 0xc8a2;
#else
#ifdef HAS_EPAPER
#include <heltec-eink-modules.h>
#include "Fonts/FreeSansBold9pt7b.h"
#ifdef HELTEC_WP_V1
EInkDisplay_WirelessPaperV1_1 display;
#endif
#ifdef HELTEC_WP_V1_2
EInkDisplay_WirelessPaperV1_2 display;
#endif
#ifdef HELTEC_VM_E290
EInkDisplay_VisionMasterE290 display;
#endif
String lastEpaperText;
#else
#include <Adafruit_GFX.h>
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
#include <Adafruit_SH110X.h>
Adafruit_SH1106G display(128, 64, &Wire, OLED_RST);
#else
#include <Adafruit_SSD1306.h>
#ifdef HELTEC_WSL_V3_DISPLAY
Adafruit_SSD1306 display(128, 64, &Wire1, OLED_RST);
#else
Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST);
#endif
#endif
#endif
#endif
#endif
extern Configuration Config;
bool displayFound = false;
void displaySetup() {
#ifdef HAS_DISPLAY
delay(500);
#ifdef HAS_TFT
tft.init();
tft.begin();
if (Config.display.turn180) {
tft.setRotation(3);
} else {
tft.setRotation(1);
}
pinMode(TFT_BL, OUTPUT);
digitalWrite(TFT_BL, HIGH);
tft.setTextFont(0);
tft.fillScreen(TFT_BLACK);
#if defined(TTGO_T_DECK_GPS) || defined(TTGO_T_DECK_PLUS)
sprite.createSprite(320, 240);
#else
sprite.createSprite(160, 80);
#endif
#else
#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
pinMode(OLED_RST, OUTPUT);
digitalWrite(OLED_RST, LOW);
delay(20);
digitalWrite(OLED_RST, HIGH);
#endif
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
if (display.begin(0x3c, false)) {
displayFound = true;
if (Config.display.turn180) display.setRotation(2);
display.clearDisplay();
display.setTextColor(SH110X_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.setContrast(1);
display.display();
}
#else
if(display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
displayFound = true;
if (Config.display.turn180) display.setRotation(2);
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
display.display();
}
#endif
#endif
#endif
delay(1000);
#endif
}
void display_toggle(bool toggle) {
if (toggle) {
display.ssd1306_command(SSD1306_DISPLAYON);
} else {
display.ssd1306_command(SSD1306_DISPLAYOFF);
}
void displayToggle(bool toggle) {
#ifdef HAS_DISPLAY
if (toggle) {
#ifdef HAS_TFT
digitalWrite(TFT_BL, HIGH);
#else
#ifdef HAS_EPAPER
display.printCenter("EPAPER Display Disabled by toggle...");
display.update();
#else
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
if (displayFound) display.oled_command(SH110X_DISPLAYON);
#else
if (displayFound) display.ssd1306_command(SSD1306_DISPLAYON);
#endif
#endif
#endif
} else {
#ifdef HAS_TFT
digitalWrite(TFT_BL, LOW);
#else
#ifdef HAS_EPAPER
display.update();
#else
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
if (displayFound) display.oled_command(SH110X_DISPLAYOFF);
#else
if (displayFound) display.ssd1306_command(SSD1306_DISPLAYOFF);
#endif
#endif
#endif
}
#endif
}
void show_display(String line1, int wait) {
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println(line1);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
display.display();
delay(wait);
void displayShow(const String& header, const String& line1, const String& line2, const String& line3, int wait) {
#ifdef HAS_DISPLAY
const String* const lines[] = {&line1, &line2, &line3};
#ifdef HAS_TFT
sprite.fillSprite(TFT_BLACK);
#if defined(HELTEC_WIRELESS_TRACKER)
sprite.fillRect(0, 0, 160, 19, redColor);
#endif
#if defined(TTGO_T_DECK_GPS) || defined(TTGO_T_DECK_PLUS)
sprite.fillRect(0, 0, 320, 43, redColor);
#endif
sprite.setTextFont(0);
sprite.setTextSize(bigSizeFont);
sprite.setTextColor(TFT_WHITE, redColor);
sprite.drawString(header, 3, 3);
sprite.setTextSize(smallSizeFont);
sprite.setTextColor(TFT_WHITE, TFT_BLACK);
for (int i = 0; i < 3; i++) {
sprite.drawString(*lines[i], 3, (lineSpacing * (2 + i)) - 2);
}
sprite.pushSprite(0, 0);
#else
#ifdef HAS_EPAPER
display.clearMemory();
display.setCursor(5,10);
display.setFont(&FreeSansBold9pt7b);
display.println(header);
display.setFont(NULL);
for (int i = 0; i < 3; i++) {
display.setCursor(0, 25 + (14 * i));
display.println(*lines[i]);
}
display.update();
#else
if (displayFound) {
display.clearDisplay();
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
display.setTextColor(SH110X_WHITE);
#else
display.setTextColor(WHITE);
#endif
display.setTextSize(1);
display.setCursor(0, 0);
display.println(header);
for (int i = 0; i < 3; i++) {
display.setCursor(0, 8 + (8 * i));
display.println(*lines[i]);
}
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
display.setContrast(1);
#else
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
#endif
display.display();
}
#endif
#endif
delay(wait);
#endif
}
void show_display(String line1, String line2, int wait) {
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println(line1);
display.setCursor(0, 8);
display.println(line2);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
display.display();
delay(wait);
}
void displayShow(const String& header, const String& line1, const String& line2, const String& line3, const String& line4, const String& line5, const String& line6, int wait) {
#ifdef HAS_DISPLAY
const String* const lines[] = {&line1, &line2, &line3, &line4, &line5, &line6};
#ifdef HAS_TFT
sprite.fillSprite(TFT_BLACK);
#if defined(HELTEC_WIRELESS_TRACKER)
sprite.fillRect(0, 0, 160, 19, redColor);
#endif
#if defined(TTGO_T_DECK_GPS) || defined(TTGO_T_DECK_PLUS)
sprite.fillRect(0, 0, 320, 43, redColor);
#endif
sprite.setTextFont(0);
sprite.setTextSize(bigSizeFont);
sprite.setTextColor(TFT_WHITE, redColor);
sprite.drawString(header, 3, 3);
void show_display(String line1, String line2, String line3, int wait) {
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println(line1);
display.setCursor(0, 8);
display.println(line2);
display.setCursor(0, 16);
display.println(line3);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
display.display();
delay(wait);
}
sprite.setTextSize(smallSizeFont);
sprite.setTextColor(TFT_WHITE, TFT_BLACK);
void show_display(String line1, String line2, String line3, String line4, int wait) {
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println(line1);
display.setCursor(0, 8);
display.println(line2);
display.setCursor(0, 16);
display.println(line3);
display.setCursor(0, 24);
display.println(line4);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
display.display();
delay(wait);
}
for (int i = 0; i < 6; i++) {
sprite.drawString(*lines[i], 3, (lineSpacing * (2 + i)) - 2);
}
void show_display(String line1, String line2, String line3, String line4, String line5, int wait) {
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println(line1);
display.setCursor(0, 8);
display.println(line2);
display.setCursor(0, 16);
display.println(line3);
display.setCursor(0, 24);
display.println(line4);
display.setCursor(0, 32);
display.println(line5);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
display.display();
delay(wait);
}
void show_display(String line1, String line2, String line3, String line4, String line5, String line6, int wait) {
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println(line1);
display.setCursor(0, 8);
display.println(line2);
display.setCursor(0, 16);
display.println(line3);
display.setCursor(0, 24);
display.println(line4);
display.setCursor(0, 32);
display.println(line5);
display.setCursor(0, 40);
display.println(line6);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
display.display();
delay(wait);
}
void show_display(String line1, String line2, String line3, String line4, String line5, String line6, String line7, int wait) {
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(2);
display.setCursor(0, 0);
display.println(line1);
display.setTextSize(1);
display.setCursor(0, 16);
display.println(line2);
display.setCursor(0, 24);
display.println(line3);
display.setCursor(0, 32);
display.println(line4);
display.setCursor(0, 40);
display.println(line5);
display.setCursor(0, 48);
display.println(line6);
display.setCursor(0, 56);
display.println(line7);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
display.display();
delay(wait);
sprite.pushSprite(0, 0);
#else
#ifdef HAS_EPAPER
lastEpaperText = header + line1 + line2 + line3 + line4 + line5 + line6;
display.clearMemory();
display.setCursor(5,10);
display.setFont(&FreeSansBold9pt7b);
display.println(header);
display.setFont(NULL);
for (int i = 0; i < 6; i++) {
display.setCursor(0, 25 + (14 * i));
display.println(*lines[i]);
}
display.update();
#else
if (displayFound) {
display.clearDisplay();
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
display.setTextColor(SH110X_WHITE);
#else
display.setTextColor(WHITE);
#endif
display.setTextSize(2);
display.setCursor(0, 0);
display.println(header);
display.setTextSize(1);
for (int i = 0; i < 6; i++) {
display.setCursor(0, 16 + (8 * i));
display.println(*lines[i]);
}
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
display.setContrast(1);
#else
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(1);
#endif
display.display();
}
#endif
#endif
delay(wait);
#endif
}

View File

@@ -1,22 +0,0 @@
#ifndef DISPLAY_H_
#define DISPLAY_H_
#include <Arduino.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
void setup_display();
void display_toggle(bool toggle);
void show_display(String line1, int wait = 0);
void show_display(String line1, String line2, int wait = 0);
void show_display(String line1, String line2, String line3, int wait = 0);
void show_display(String line1, String line2, String line3, String line4, int wait = 0);
void show_display(String line1, String line2, String line3, String line4, String line5, int wait = 0);
void show_display(String line1, String line2, String line3, String line4, String line5, String line6, int wait = 0);
void show_display(String line1, String line2, String line3, String line4, String line5, String line6, String line7, int wait = 0);
#endif

View File

@@ -1,198 +1,202 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <APRSPacketLib.h>
#include <TinyGPS++.h>
#include <WiFi.h>
#include "configuration.h"
#include "board_pinout.h"
#include "gps_utils.h"
#include "display.h"
#include "utils.h"
extern Configuration Config;
extern WiFi_AP *currentWiFi;
extern WiFiClient espClient;
extern int stationMode;
String distance;
#ifdef GPS_BAUDRATE
#define GPS_BAUD GPS_BAUDRATE
#else
#define GPS_BAUD 9600
#endif
extern Configuration Config;
extern HardwareSerial gpsSerial;
extern TinyGPSPlus gps;
extern bool stationCallsignIsValid;
String distance, iGateBeaconPacket, iGateLoRaBeaconPacket;
namespace GPS_Utils {
String double2string(double n, int ndec) {
String r = "";
if (n>-1 && n<0) {
r = "-";
}
int v = n;
r += v;
r += '.';
for (int i=0;i<ndec;i++) {
n -= v;
n = 10 * abs(n);
v = n;
r += v;
}
return r;
String getiGateLoRaBeaconPacket() {
return iGateLoRaBeaconPacket;
}
String processLatitudeAPRS(double lat) {
String degrees = double2string(lat,6);
String north_south, latitude, convDeg3;
float convDeg, convDeg2;
void generateBeacons() {
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 (abs(degrees.toFloat()) < 10) {
latitude += "0";
}
Serial.println(latitude);
if (degrees.indexOf("-") == 0) {
north_south = "S";
latitude += degrees.substring(1,degrees.indexOf("."));
} else {
north_south = "N";
latitude += degrees.substring(0,degrees.indexOf("."));
}
convDeg = abs(degrees.toFloat()) - abs(int(degrees.toFloat()));
convDeg2 = (convDeg * 60)/100;
convDeg3 = String(convDeg2,6);
latitude += convDeg3.substring(convDeg3.indexOf(".")+1,convDeg3.indexOf(".")+3) + "." + convDeg3.substring(convDeg3.indexOf(".")+3,convDeg3.indexOf(".")+5);
latitude += north_south;
return latitude;
}
String processLongitudeAPRS(double lon) {
String degrees = double2string(lon,6);
String east_west, longitude, convDeg3;
float convDeg, convDeg2;
if (abs(degrees.toFloat()) < 100) {
longitude += "0";
}
if (abs(degrees.toFloat()) < 10) {
longitude += "0";
}
if (degrees.indexOf("-") == 0) {
east_west = "W";
longitude += degrees.substring(1,degrees.indexOf("."));
} else {
east_west = "E";
longitude += degrees.substring(0,degrees.indexOf("."));
}
convDeg = abs(degrees.toFloat()) - abs(int(degrees.toFloat()));
convDeg2 = (convDeg * 60)/100;
convDeg3 = String(convDeg2,6);
longitude += convDeg3.substring(convDeg3.indexOf(".")+1,convDeg3.indexOf(".")+3) + "." + convDeg3.substring(convDeg3.indexOf(".")+3,convDeg3.indexOf(".")+5);
longitude += east_west;
return longitude;
}
String generateBeacon() {
String stationLatitude, stationLongitude, beaconPacket;
if (stationMode==1 || stationMode==2 || (stationMode==5 && WiFi.status()==WL_CONNECTED && espClient.connected())) {
stationLatitude = processLatitudeAPRS(currentWiFi->latitude);
stationLongitude = processLongitudeAPRS(currentWiFi->longitude);
beaconPacket = Config.callsign + ">APLRG1,WIDE1-1";
if (stationMode!=6) {
beaconPacket += ",qAC";
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;
}
beaconPacket += ":=" + stationLatitude + "L" + stationLongitude;
if (stationMode==1) {
beaconPacket += "&";
} else {
beaconPacket += "a";
}
beaconPacket += Config.iGateComment;
} else { //stationMode 3, 4 and 5
if (stationMode==5) {
stationLatitude = processLatitudeAPRS(currentWiFi->latitude);
stationLongitude = processLongitudeAPRS(currentWiFi->longitude);
} else {
stationLatitude = processLatitudeAPRS(Config.digi.latitude);
stationLongitude = processLongitudeAPRS(Config.digi.longitude);
}
beaconPacket = Config.callsign + ">APLRG1,WIDE1-1:=" + stationLatitude + "L" + stationLongitude + "#" + Config.digi.comment;
}
return beaconPacket;
}
String generateiGateLoRaBeacon() {
String stationLatitude, stationLongitude, beaconPacket;
stationLatitude = processLatitudeAPRS(currentWiFi->latitude);
stationLongitude = processLongitudeAPRS(currentWiFi->longitude);
beaconPacket = Config.callsign + ">APLRG1,RFONLY:=" + stationLatitude + "L" + stationLongitude;
if (Config.bme.active) {
beaconPacket += "_";
} else {
beaconPacket += "a";
Config.beacon.sendViaAPRSIS = false;
Config.beacon.sendViaRF = false;
}
return beaconPacket;
iGateBeaconPacket = beaconPacket;
iGateBeaconPacket += ",qAC:=";
iGateBeaconPacket += Config.beacon.overlay;
iGateBeaconPacket += encodedGPS;
iGateLoRaBeaconPacket = beaconPacket;
iGateLoRaBeaconPacket += ":=";
iGateLoRaBeaconPacket += Config.beacon.overlay;
iGateLoRaBeaconPacket += encodedGPS;
}
double calculateDistanceTo(double latitude, double longitude) {
return TinyGPSPlus::distanceBetween(currentWiFi->latitude,currentWiFi->longitude, latitude, longitude) / 1000.0;
return TinyGPSPlus::distanceBetween(Config.beacon.latitude,Config.beacon.longitude, latitude, longitude) / 1000.0;
}
String decodeEncodedGPS(String packet) {
String GPSPacket = packet.substring(packet.indexOf(":!")+3);
String encodedLatitude = GPSPacket.substring(0,4);
String encodedLongtitude = GPSPacket.substring(4,8);
String buildDistanceAndComment(float latitude, float longitude, const String& comment) {
distance = String(calculateDistanceTo(latitude, longitude),1);
String distanceAndComment = String(latitude,5);
distanceAndComment += "N / ";
distanceAndComment += String(longitude,5);
distanceAndComment += "E / ";
distanceAndComment += distance;
distanceAndComment += "km";
if (comment != "") {
distanceAndComment += " / ";
distanceAndComment += comment;
}
return distanceAndComment;
int Y1 = int(encodedLatitude[0]);
int Y2 = int(encodedLatitude[1]);
int Y3 = int(encodedLatitude[2]);
int Y4 = int(encodedLatitude[3]);
float decodedLatitude = 90.0 - ((((Y1-33) * pow(91,3)) + ((Y2-33) * pow(91,2)) + ((Y3-33) * 91) + Y4-33) / 380926.0);
int X1 = int(encodedLongtitude[0]);
int X2 = int(encodedLongtitude[1]);
int X3 = int(encodedLongtitude[2]);
int X4 = int(encodedLongtitude[3]);
float decodedLongitude = -180.0 + ((((X1-33) * pow(91,3)) + ((X2-33) * pow(91,2)) + ((X3-33) * 91) + X4-33) / 190463.0);
distance = String(calculateDistanceTo(decodedLatitude, decodedLongitude),1);
return String(decodedLatitude,5) + "N / " + String(decodedLongitude,5) + "E / " + distance + "km";
}
String getReceivedGPS(String packet) {
String decodeEncodedGPS(const String& packet) {
int indexOfExclamation = packet.indexOf(":!");
int indexOfEqual = packet.indexOf(":=");
const uint8_t OFFSET = 3; // Offset for encoded data in the packet
String infoGPS;
if (packet.indexOf(":!") > 10) {
infoGPS = packet.substring(packet.indexOf(":!")+2);
} else if (packet.indexOf(":=") > 10) {
infoGPS = packet.substring(packet.indexOf(":=")+2);
if (indexOfExclamation > 10) {
infoGPS = packet.substring(indexOfExclamation + OFFSET);
} else if (indexOfEqual > 10) {
infoGPS = packet.substring(indexOfEqual + OFFSET);
}
String Latitude = infoGPS.substring(0,8);
String Longitude = infoGPS.substring(9,18);
float convertedLatitude, convertedLongitude;
String firstLatPart = Latitude.substring(0,2);
String secondLatPart = Latitude.substring(2,4);
String thirdLatPart = Latitude.substring(Latitude.indexOf(".")+1,Latitude.indexOf(".")+3);
String firstLngPart = Longitude.substring(0,3);
String secondLngPart = Longitude.substring(3,5);
String thirdLngPart = Longitude.substring(Longitude.indexOf(".")+1,Longitude.indexOf(".")+3);
convertedLatitude = firstLatPart.toFloat() + (secondLatPart.toFloat()/60) + (thirdLatPart.toFloat()/(60*100));
convertedLongitude = firstLngPart.toFloat() + (secondLngPart.toFloat()/60) + (thirdLngPart.toFloat()/(60*100));
String LatSign = String(Latitude[7]);
String LngSign = String(Longitude[8]);
if (LatSign == "S") {
convertedLatitude = -convertedLatitude;
}
if (LngSign == "W") {
convertedLongitude = -convertedLongitude;
}
distance = String(calculateDistanceTo(convertedLatitude, convertedLongitude),1);
return String(convertedLatitude,5) + "N / " + String(convertedLongitude,5) + "E / " + distance + "km";
float decodedLatitude = APRSPacketLib::decodeBase91EncodedLatitude(infoGPS.substring(0,4));
float decodedLongitude = APRSPacketLib::decodeBase91EncodedLongitude(infoGPS.substring(4,8));
return buildDistanceAndComment(decodedLatitude, decodedLongitude, infoGPS.substring(12));
}
String getDistance(String packet) {
int encodedBytePosition = 0;
if (packet.indexOf(":!") > 10) {
encodedBytePosition = packet.indexOf(":!") + 14;
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)
}
if (packet.indexOf(":=") > 10) {
encodedBytePosition = packet.indexOf(":=") + 14;
String Latitude = infoGPS.substring(0,8); // First 8 characters are Latitude
int latitudeColonIndex = Latitude.indexOf(".");
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(latitudeColonIndex + 1, latitudeColonIndex + 3).toFloat() / (60*100);
if (Latitude.endsWith("S")) convertedLatitude = -convertedLatitude; // Handle Southern Hemisphere
String Longitude = infoGPS.substring(9,18); // Next 9 characters are Longitude
int longitudeColonIndex = Longitude.indexOf(".");
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(longitudeColonIndex + 1, longitudeColonIndex + 3).toFloat() / (60*100);
if (Longitude.endsWith("W")) convertedLongitude = -convertedLongitude; // Handle Western Hemisphere
return buildDistanceAndComment(convertedLatitude, convertedLongitude, infoGPS.substring(19));
}
String getDistanceAndComment(const String& packet) {
int indexOfAt = packet.indexOf(":@");
if (indexOfAt > 10) return getReceivedGPS(packet);
const uint8_t nonEncondedLatitudeOffset = 9; // "N" / "S"
const uint8_t nonEncondedLongitudeOffset = 19; // "E" / "W"
const uint8_t encodedByteOffset = 14;
int indexOfExclamation = packet.indexOf(":!");
int indexOfEqual = packet.indexOf(":=");
int baseIndex = - 1;
if (indexOfExclamation > 10) {
baseIndex = indexOfExclamation;
} else if (indexOfEqual > 10) {
baseIndex = indexOfEqual;
}
if (encodedBytePosition != 0) {
if (String(packet[encodedBytePosition]) == "G" || String(packet[encodedBytePosition]) == "Q" || String(packet[encodedBytePosition]) == "[" || String(packet[encodedBytePosition]) == "H") {
return decodeEncodedGPS(packet);
} else {
return getReceivedGPS(packet);
if (baseIndex == -1) return " _ / _ / _ ";
int latitudeIndex = baseIndex + nonEncondedLatitudeOffset;
int longitudeIndex = baseIndex + nonEncondedLongitudeOffset;
int encodedByteIndex = baseIndex + encodedByteOffset;
int packetLength = packet.length();
if (latitudeIndex < packetLength && longitudeIndex < packetLength) {
char latChar = packet[latitudeIndex];
char lngChar = packet[longitudeIndex];
if ((latChar == 'N' || latChar == 'S') && (lngChar == 'E' || lngChar == 'W')) return getReceivedGPS(packet);
}
if (encodedByteIndex < packetLength) {
char byteChar = packet[encodedByteIndex];
if (byteChar == 'G' || byteChar == 'Q' || byteChar == '[' || byteChar == 'H' || byteChar == 'X' || byteChar == '3') return decodeEncodedGPS(packet);
}
return " _ / _ / _ ";
}
void setup() {
#ifdef HAS_GPS
if (Config.beacon.gpsActive && Config.digi.ecoMode != 1) {
gpsSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_TX, GPS_RX);
}
} else {
return " _ / _ / _ ";
#endif
generateBeacons();
}
void getData() {
while (gpsSerial.available() > 0) {
gps.encode(gpsSerial.read());
}
}

View File

@@ -1,21 +0,0 @@
#ifndef GPS_UTILS_H_
#define GPS_UTILS_H_
#include <Arduino.h>
namespace GPS_Utils {
String double2string(double n, int ndec);
String processLatitudeAPRS();
String processLongitudeAPRS();
String generateBeacon();
String generateiGateLoRaBeacon();
double calculateDistanceCourse(double latitude, double longitude);
String decodeEncodedGPS(String packet);
String getReceivedGPS(String packet);
String getDistance(String packet);
}
#endif

189
src/kiss_utils.cpp Normal file
View File

@@ -0,0 +1,189 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "kiss_protocol.h"
bool validateTNC2Frame(const String& tnc2FormattedFrame) {
return (tnc2FormattedFrame.indexOf(':') != -1) && (tnc2FormattedFrame.indexOf('>') != -1);
}
bool validateKISSFrame(const String& kissFormattedFrame) {
return kissFormattedFrame.charAt(0) == (char)FEND && kissFormattedFrame.charAt(kissFormattedFrame.length() - 1) == (char)FEND;
}
String encodeAddressAX25(String tnc2Address) {
bool hasBeenDigipited = tnc2Address.indexOf('*') != -1;
int tnc2AddressIndex = tnc2Address.indexOf('-');
if (tnc2AddressIndex == -1) {
if (hasBeenDigipited) {
tnc2Address = tnc2Address.substring(0, tnc2Address.length() - 1);
}
tnc2Address += "-0";
}
int separatorIndex = tnc2AddressIndex;
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 = ' ';
}
kissAddress += (char)(addressChar << 1);
}
kissAddress += (char)((ssid << 1) | 0b01100000 | (hasBeenDigipited ? HAS_BEEN_DIGIPITED_MASK : 0));
return kissAddress;
}
String decodeAddressAX25(const String& ax25Address, bool& isLast, bool isRelay) {
String address = "";
for (int i = 0; i < 6; ++i) {
uint8_t currentCharacter = ax25Address.charAt(i);
currentCharacter >>= 1;
if (currentCharacter != ' ') {
address += (char)currentCharacter;
}
}
auto ssidChar = (uint8_t)ax25Address.charAt(6);
bool hasBeenDigipited = ssidChar & HAS_BEEN_DIGIPITED_MASK;
isLast = ssidChar & IS_LAST_ADDRESS_POSITION_MASK;
ssidChar >>= 1;
int ssid = 0b1111 & ssidChar;
if (ssid) {
address += '-';
address += ssid;
}
if (isRelay && hasBeenDigipited) {
address += '*';
}
return address;
}
String encapsulateKISS(const String& ax25Frame, uint8_t cmd) {
String kissFrame = "";
kissFrame += (char)FEND;
kissFrame += (char)(0x0f & cmd);
for (int i = 0; i < ax25Frame.length(); ++i) {
char currentChar = ax25Frame.charAt(i);
if (currentChar == (char)FEND) {
kissFrame += (char)FESC;
kissFrame += (char)TFEND;
} else if (currentChar == (char)FESC) {
kissFrame += (char)FESC;
kissFrame += (char)TFESC;
} else {
kissFrame += currentChar;
}
}
kissFrame += (char)FEND; // end of frame
return kissFrame;
}
String decapsulateKISS(const String& frame) {
String ax25Frame = "";
for (int i = 2; i < frame.length() - 1; ++i) {
char currentChar = frame.charAt(i);
if (currentChar == (char)FESC) {
char nextChar = frame.charAt(i + 1);
if (nextChar == (char)TFEND) {
ax25Frame += (char)FEND;
} else if (nextChar == (char)TFESC) {
ax25Frame += (char)FESC;
}
i++;
} else {
ax25Frame += currentChar;
}
}
return ax25Frame;
}
String encodeKISS(const String& frame) {
String ax25Frame = "";
if (validateTNC2Frame(frame)) {
String address = "";
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 == ':')) {
ax25Frame = encodeAddressAX25(address) + ax25Frame;
dstAddresWritten = true;
} else {
ax25Frame += encodeAddressAX25(address);
}
address = "";
} else {
address += currentChar;
}
}
auto lastAddressChar = (uint8_t)ax25Frame.charAt(ax25Frame.length() - 1);
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(colonFrameIndex + 1);
}
String kissFrame = encapsulateKISS(ax25Frame, CMD_DATA);
return kissFrame;
}
String decodeKISS(const String& inputFrame, bool& dataFrame) {
String frame = "";
if (validateKISSFrame(inputFrame)) {
dataFrame = inputFrame.charAt(1) == CMD_DATA;
if (dataFrame) {
String ax25Frame = decapsulateKISS(inputFrame);
bool isLast = false;
String dstAddr = decodeAddressAX25(ax25Frame.substring(0, 7), isLast, false);
String srcAddr = decodeAddressAX25(ax25Frame.substring(7, 14), isLast, false);
frame = srcAddr + ">" + dstAddr;
int digiInfoIndex = 14;
while (!isLast && digiInfoIndex + 7 < ax25Frame.length()) {
String digiAddr = decodeAddressAX25(ax25Frame.substring(digiInfoIndex, digiInfoIndex + 7), isLast, true);
frame += ',' + digiAddr;
digiInfoIndex += 7;
}
frame += ':';
frame += ax25Frame.substring(digiInfoIndex + 2);
} else {
frame += inputFrame;
}
}
return frame;
}

View File

@@ -1,24 +1,61 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <RadioLib.h>
#include <LoRa.h>
#include <WiFi.h>
#include "configuration.h"
#include "aprs_is_utils.h"
#include "station_utils.h"
#include "board_pinout.h"
#include "syslog_utils.h"
#include "pins_config.h"
#include "ntp_utils.h"
#include "display.h"
#include "utils.h"
extern Configuration Config;
extern int stationMode;
#if defined(HELTEC_V3) || defined(TTGO_T_Beam_V1_2_SX1262)
SX1262 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN);
bool transmissionFlag = true;
bool enableInterrupt = true;
extern Configuration Config;
extern uint32_t lastRxTime;
extern bool packetIsBeacon;
extern std::vector<ReceivedPacket> receivedPackets;
bool operationDone = true;
bool transmitFlag = true;
#ifdef HAS_SX1262
SX1262 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN);
#endif
#if defined(ESP32_DIY_1W_LoRa) || defined(TTGO_T_Beam_V1_0_SX1268)
SX1268 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN);
bool transmissionFlag = true;
bool enableInterrupt = 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);
#else
SX1268 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN);
#endif
#endif
#ifdef HAS_SX1278
SX1278 radio = new Module(RADIO_CS_PIN, RADIO_BUSY_PIN, RADIO_RST_PIN);
#endif
#ifdef HAS_SX1276
SX1276 radio = new Module(RADIO_CS_PIN, RADIO_BUSY_PIN, RADIO_RST_PIN);
#endif
#if defined(HAS_LLCC68) //LLCC68 supports spreading factor only in range of 5-11!
LLCC68 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN);
#endif
int rssi, freqError;
@@ -28,198 +65,218 @@ float snr;
namespace LoRa_Utils {
void setFlag(void) {
#ifdef HAS_SX126X
transmissionFlag = true;
#endif
operationDone = true;
}
void setup() {
#ifdef HAS_SX127X
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
LoRa.setPins(LORA_CS, LORA_RST, LORA_IRQ);
long freq;
if (stationMode==1 || stationMode==2) {
freq = Config.loramodule.iGateFreq;
} else {
freq = Config.loramodule.digirepeaterTxFreq;
}
if (!LoRa.begin(freq)) {
Serial.println("Starting LoRa failed!");
show_display("ERROR", "Starting LoRa failed!");
while (true) {
delay(1000);
}
}
LoRa.setSpreadingFactor(Config.loramodule.spreadingFactor);
LoRa.setSignalBandwidth(Config.loramodule.signalBandwidth);
LoRa.setCodingRate4(Config.loramodule.codingRate4);
LoRa.enableCrc();
LoRa.setTxPower(Config.loramodule.power);
Serial.print("init : LoRa Module ... done!");
#if defined (LIGHTGATEWAY_1_0) || defined(LIGHTGATEWAY_PLUS_1_0)
pinMode(RADIO_VCC_PIN,OUTPUT);
digitalWrite(RADIO_VCC_PIN,HIGH);
loraSPI.begin(RADIO_SCLK_PIN, RADIO_MISO_PIN, RADIO_MOSI_PIN, RADIO_CS_PIN);
#else
SPI.begin(RADIO_SCLK_PIN, RADIO_MISO_PIN, RADIO_MOSI_PIN);
#endif
float freq = (float)Config.loramodule.rxFreq / 1000000;
#if defined(RADIO_HAS_XTAL)
radio.XTAL = true;
#endif
#ifdef HAS_SX126X
SPI.begin(RADIO_SCLK_PIN, RADIO_MISO_PIN, RADIO_MOSI_PIN);
float freq = (float)Config.loramodule.iGateFreq/1000000;
int state = radio.begin(freq);
if (state == RADIOLIB_ERR_NONE) {
Serial.print("Initializing SX126X LoRa Module");
} else {
Serial.println("Starting LoRa failed!");
while (true);
}
radio.setDio1Action(setFlag);
radio.setSpreadingFactor(Config.loramodule.spreadingFactor);
radio.setBandwidth(Config.loramodule.signalBandwidth);
radio.setCodingRate(Config.loramodule.codingRate4);
radio.setCRC(true);
#if defined(ESP32_DIY_1W_LoRa)
radio.setRfSwitchPins(RADIO_RXEN, RADIO_TXEN);
#endif
#if defined(HELTEC_V3) || defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2_SX1262)
state = radio.setOutputPower(Config.loramodule.power + 2); // values available: 10, 17, 22 --> if 20 in tracker_conf.json it will be updated to 22.
#endif
#ifdef ESP32_DIY_1W_LoRa_GPS
state = radio.setOutputPower(Config.loramodule.power); // max value 20 (when 20dB in setup 30dB in output as 400M30S has Low Noise Amp)
#endif
if (state == RADIOLIB_ERR_NONE) {
Serial.println("init : LoRa Module ... done!");
} else {
Serial.println("Starting LoRa failed!");
if (state != RADIOLIB_ERR_NONE) {
Utils::println("Starting LoRa failed! State: " + String(state));
while (true);
}
#if defined(HAS_SX1262) || defined(HAS_SX1268) || defined(HAS_LLCC68)
radio.setDio1Action(setFlag);
#endif
#if defined(HAS_SX1278) || defined(HAS_SX1276)
radio.setDio0Action(setFlag, RISING);
#endif
}
void sendNewPacket(const String &typeOfMessage, const String &newPacket) {
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
digitalWrite(internalLedPin,HIGH);
#endif
#ifdef HAS_SX127X
LoRa.beginPacket();
LoRa.write('<');
if (typeOfMessage == "APRS") {
LoRa.write(0xFF);
} else if (typeOfMessage == "LoRa") {
LoRa.write(0xF8);
/*#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);
}
LoRa.write(0x01);
LoRa.write((const uint8_t *)newPacket.c_str(), newPacket.length());
LoRa.endPacket();
#endif*/
radio.setSpreadingFactor(Config.loramodule.rxSpreadingFactor);
radio.setCodingRate(Config.loramodule.rxCodingRate4);
float signalBandwidth = Config.loramodule.rxSignalBandwidth / 1000;
radio.setBandwidth(signalBandwidth);
radio.setCRC(true);
#if (defined(RADIO_RXEN) && defined(RADIO_TXEN)) // QRP Labs LightGateway has 400M22S (SX1268)
radio.setRfSwitchPins(RADIO_RXEN, RADIO_TXEN);
#endif
#ifdef HAS_SX126X
int state = radio.transmit("\x3c\xff\x01" + newPacket);
/*#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
radio.setCurrentLimit(140); // to be validated (100 , 120, 140)?
#endif
#if defined(HAS_SX1278) || defined(HAS_SX1276)
state = radio.setOutputPower(Config.loramodule.power); // max value 20dB for 400M30S as it has Low Noise Amp
radio.setCurrentLimit(100); // to be validated (80 , 100)?
#endif
#if (defined(HAS_SX1268) || defined(HAS_SX1262)) && !defined(HAS_1W_LORA)
state = radio.setOutputPower(Config.loramodule.power + 2); // values available: 10, 17, 22 --> if 20 in tracker_conf.json it will be updated to 22.
radio.setCurrentLimit(140);
#endif
#if defined(HAS_SX1262) || defined(HAS_SX1268) || defined(HAS_LLCC68)
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) {
//Serial.println(F("success!"));
} else if (state == RADIOLIB_ERR_PACKET_TOO_LONG) {
Serial.println(F("too long!"));
} else if (state == RADIOLIB_ERR_TX_TIMEOUT) {
Serial.println(F("timeout!"));
Utils::println("init : LoRa Module ... done!");
} else {
Serial.print(F("failed, code "));
Serial.println(state);
Utils::println("Starting LoRa failed! State: " + String(state));
while (true);
}
#endif
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
digitalWrite(internalLedPin,LOW);
#endif
SYSLOG_Utils::log("Tx", newPacket,0,0,0);
Serial.print("---> LoRa Packet Tx : ");
Serial.println(newPacket);
}
String generatePacket(String aprsisPacket) {
String firstPart, messagePart;
aprsisPacket.trim();
firstPart = aprsisPacket.substring(0, aprsisPacket.indexOf(","));
messagePart = aprsisPacket.substring(aprsisPacket.indexOf("::")+2);
return firstPart + ",TCPIP,WIDE1-1," + Config.callsign + "::" + messagePart;
void changeFreqTx() {
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);
}
String packetSanitization(String packet) {
Serial.println(packet);
if (packet.indexOf("\0")>0) {
packet.replace("\0","");
void changeFreqRx() {
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);
}
void sendNewPacket(const String& newPacket) {
if (!Config.loramodule.txActive) return;
if (Config.loramodule.txFreq != Config.loramodule.rxFreq) {
if (!packetIsBeacon || (packetIsBeacon && Config.beacon.beaconFreq == 1)) {
changeFreqTx();
}
}
if (packet.indexOf("\r")>0) {
packet.replace("\r","");
#ifdef INTERNAL_LED_PIN
if (Config.digi.ecoMode != 1) digitalWrite(INTERNAL_LED_PIN, HIGH); // disabled in Ultra Eco Mode
#endif
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
}
Utils::print("---> LoRa Packet Tx : ");
Utils::println(newPacket);
} else {
Utils::print(F("failed, code "));
Utils::println(String(state));
}
if (packet.indexOf("\n")>0) {
packet.replace("\n","");
#ifdef INTERNAL_LED_PIN
if (Config.digi.ecoMode != 1) digitalWrite(INTERNAL_LED_PIN, LOW); // disabled in Ultra Eco Mode
#endif
if (Config.loramodule.txFreq != Config.loramodule.rxFreq) {
if (!packetIsBeacon || (packetIsBeacon && Config.beacon.beaconFreq == 1)) {
changeFreqRx();
}
}
}
String receivePacketFromSleep() {
String packet = "";
int state = radio.readData(packet);
if (state == RADIOLIB_ERR_NONE) {
Utils::println("<--- LoRa Packet Rx : " + packet.substring(3));
} else {
packet = "";
}
return packet;
}
String receivePacket() {
String loraPacket = "";
#ifdef HAS_SX127X
int packetSize = LoRa.parsePacket();
if (packetSize) {
while (LoRa.available()) {
int inChar = LoRa.read();
loraPacket += (char)inChar;
}
rssi = LoRa.packetRssi();
snr = LoRa.packetSnr();
freqError = LoRa.packetFrequencyError();
}
#endif
#ifdef HAS_SX126X
if (transmissionFlag) {
transmissionFlag = false;
radio.startReceive();
int state = radio.readData(loraPacket);
if (state == RADIOLIB_ERR_NONE) {
Serial.println("LoRa Rx ---> " + loraPacket);
rssi = radio.getRSSI();
snr = radio.getSNR();
freqError = radio.getFrequencyError();
} else if (state == RADIOLIB_ERR_RX_TIMEOUT) {
// timeout occurred while waiting for a packet
} else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
Serial.println(F("CRC error!"));
String packet = "";
if (operationDone) {
operationDone = false;
if (transmitFlag) {
radio.startReceive();
transmitFlag = false;
} else {
Serial.print(F("failed, code "));
Serial.println(state);
int state = radio.readData(packet);
if (state == RADIOLIB_ERR_NONE) {
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
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) + ")");
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.RSSI = rssi;
receivedPacket.SNR = snr;
receivedPackets.push_back(receivedPacket);
}
if (Config.syslog.active && WiFi.status() == WL_CONNECTED) {
SYSLOG_Utils::log(1, packet, rssi, snr, freqError); // RX
}
} else {
packet = "";
}
lastRxTime = millis();
return packet;
}
} else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
rssi = radio.getRSSI();
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
}
packet = "";
} else {
Utils::print(F("failed, code "));
Utils::println(String(state));
packet = "";
}
}
}
#endif
if ((loraPacket.indexOf("\0")!=-1) || (loraPacket.indexOf("\r")!=-1) || (loraPacket.indexOf("\n")!=-1)) {
loraPacket = packetSanitization(loraPacket);
}
#ifndef TextSerialOutputForApp
if (loraPacket!="") {
Serial.println("(RSSI:" +String(rssi) + " / SNR:" + String(snr) + " / FreqErr:" + String(freqError) + ")");
}
#endif
if (Config.syslog.active && (stationMode==1 || stationMode==2 || (stationMode==5 && WiFi.status()==WL_CONNECTED)) && loraPacket!="") {
SYSLOG_Utils::log("Rx", loraPacket, rssi, snr, freqError);
}
return loraPacket;
return packet;
}
void changeFreqTx() {
delay(500);
#ifdef HAS_SX127X
LoRa.setFrequency(Config.loramodule.digirepeaterTxFreq);
#endif
#ifdef HAS_SX126X
float freq = (float)Config.loramodule.digirepeaterTxFreq/1000000;
radio.setFrequency(freq);
#endif
void wakeRadio() {
radio.startReceive();
}
void changeFreqRx() {
delay(500);
#ifdef HAS_SX127X
LoRa.setFrequency(Config.loramodule.digirepeaterRxFreq);
#endif
#ifdef HAS_SX126X
float freq = (float)Config.loramodule.digirepeaterRxFreq/1000000;
radio.setFrequency(freq);
#endif
void sleepRadio() {
radio.sleep();
}
}

View File

@@ -1,19 +0,0 @@
#ifndef LORA_UTILS_H_
#define LORA_UTILS_H_
#include <Arduino.h>
namespace LoRa_Utils {
void setup();
void sendNewPacket(const String &typeOfMessage, const String &newPacket);
String generatePacket(String aprsisPacket);
String packetSanitization(String packet);
String receivePacket();
void changeFreqTx();
void changeFreqRx();
}
#endif

101
src/mqtt_utils.cpp Normal file
View File

@@ -0,0 +1,101 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include "configuration.h"
#include "station_utils.h"
#include "mqtt_utils.h"
extern Configuration Config;
extern WiFiClient mqttClient;
PubSubClient pubSub;
namespace MQTT_Utils {
void sendToMqtt(const String& packet) {
if (!pubSub.connected()) {
Serial.println("Can not send to MQTT because it is not connected");
return;
}
const String cleanPacket = packet.substring(3);
const String sender = cleanPacket.substring(0, cleanPacket.indexOf(">"));
const String topic = String(Config.mqtt.topic + "/" + sender);
const bool result = pubSub.publish(topic.c_str(), cleanPacket.c_str());
if (result) {
Serial.print("Packet sent to MQTT topic "); Serial.println(topic);
} else {
Serial.println("Packet not sent to MQTT (check connection)");
}
}
void receivedFromMqtt(char* topic, byte* payload, unsigned int length) {
Serial.print("Received from MQTT topic "); Serial.print(topic); Serial.print(": ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
STATION_Utils::addToOutputPacketBuffer(String(payload, length));
}
void connect() {
if (pubSub.connected()) return;
if (Config.mqtt.server.isEmpty() || Config.mqtt.port <= 0) {
Serial.println("Connect to MQTT server KO because no host or port given");
return;
}
pubSub.setServer(Config.mqtt.server.c_str(), Config.mqtt.port);
Serial.print("Trying to connect with MQTT Server: " + String(Config.mqtt.server) + " MqttServerPort: " + String(Config.mqtt.port));
bool connected = false;
if (!Config.mqtt.username.isEmpty()) {
connected = pubSub.connect(Config.callsign.c_str(), Config.mqtt.username.c_str(), Config.mqtt.password.c_str());
} else {
connected = pubSub.connect(Config.callsign.c_str());
}
if (connected) {
Serial.println(" -> Connected !");
const String subscribedTopic = Config.mqtt.topic + "/" + Config.callsign + "/#";
if (!pubSub.subscribe(subscribedTopic.c_str())) {
Serial.println("Subscribed to MQTT Failed");
}
Serial.print("Subscribed to MQTT topic : ");
Serial.println(subscribedTopic);
} else {
Serial.println(" -> Not Connected (Retry in a few secs)");
}
}
void loop() {
if (!Config.mqtt.active) return;
if (!pubSub.connected()) return;
pubSub.loop();
}
void setup() {
if (!Config.mqtt.active) return;
pubSub.setClient(mqttClient);
pubSub.setCallback(receivedFromMqtt);
}
}

52
src/ntp_utils.cpp Normal file
View File

@@ -0,0 +1,52 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <WiFi.h>
#include "configuration.h"
#include "ntp_utils.h"
#include "time.h"
extern Configuration Config;
WiFiUDP ntpUDP;
NTPClient* timeClient;
namespace NTP_Utils {
void setup() {
if (WiFi.status() == WL_CONNECTED && Config.digi.ecoMode == 0 && Config.callsign != "NOCALL-10") {
int gmt = Config.ntp.gmtCorrection * 3600;
timeClient = new NTPClient(ntpUDP, Config.ntp.server.c_str(), gmt, 15 * 60 * 1000); // Update interval 15 min
timeClient->begin();
}
}
void update() {
if (WiFi.status() == WL_CONNECTED && Config.digi.ecoMode == 0 && Config.callsign != "NOCALL-10") timeClient->update();
}
String getFormatedTime() {
if (WiFi.status() == WL_CONNECTED && Config.digi.ecoMode == 0) return timeClient->getFormattedTime();
return "DigiEcoMode Active";
}
}

View File

@@ -1,3 +1,21 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <ESPAsyncWebServer.h>
#include <ElegantOTA.h>
#include <AsyncTCP.h>
@@ -5,21 +23,23 @@
#include "ota_utils.h"
#include "display.h"
extern Configuration Config;
extern uint32_t lastScreenOn;
extern bool isUpdatingOTA;
unsigned long ota_progress_millis = 0;
namespace OTA_Utils {
void setup(AsyncWebServer *server) {
if (Config.ota.username != "" && Config.ota.password != "") {
ElegantOTA.begin(server, Config.ota.username.c_str(), Config.ota.password.c_str());
} else {
ElegantOTA.begin(server);
}
ElegantOTA.setAutoReboot(true);
ElegantOTA.onStart(onOTAStart);
ElegantOTA.onProgress(onOTAProgress);
@@ -28,31 +48,33 @@ namespace OTA_Utils {
void onOTAStart() {
Serial.println("OTA update started!");
display_toggle(true);
displayToggle(true);
lastScreenOn = millis();
show_display("", "", "", " OTA update started!", "", "", "", 1000);
displayShow("", "", "", " OTA update started!", "", "", "", 1000);
isUpdatingOTA = true;
}
void onOTAProgress(size_t current, size_t final) {
if (millis() - ota_progress_millis > 1000) {
display_toggle(true);
displayToggle(true);
lastScreenOn = millis();
ota_progress_millis = millis();
Serial.printf("OTA Progress Current: %u bytes, Final: %u bytes\n", current, final);
show_display("", "", " OTA Progress : " + String((current*100)/final) + "%", "", "", "", "", 100);
displayShow("", "", " OTA Progress : " + String((current*100)/final) + "%", "", "", "", "", 100);
}
}
void onOTAEnd(bool success) {
display_toggle(true);
displayToggle(true);
lastScreenOn = millis();
if (success) {
Serial.println("OTA update finished successfully!");
show_display("", "", " OTA update success!", "", " Rebooting ...", "", "", 4000);
} else {
Serial.println("There was an error during OTA update!");
show_display("", "", " OTA update fail!", "", "", "", "", 4000);
}
String statusMessage = success ? "OTA update success!" : "OTA update fail!";
String rebootMessage = success ? "Rebooting ..." : "";
Serial.println(success ? "OTA update finished successfully!" : "There was an error during OTA update!");
displayShow("", "", statusMessage, "", rebootMessage, "", "", 4000);
isUpdatingOTA = false;
}
}

View File

@@ -1,17 +0,0 @@
#ifndef OTA_UTILS_H_
#define OTA_UTILS_H_
#include <ESPAsyncWebServer.h>
#include <Arduino.h>
namespace OTA_Utils {
void setup(AsyncWebServer *server);
void onOTAStart();
void onOTAProgress(size_t current, size_t final);
void onOTAEnd(bool success);
}
#endif

View File

@@ -1,118 +0,0 @@
#ifndef PINS_CONFIG_H_
#define PINS_CONFIG_H_
#include <Arduino.h>
#undef OLED_SDA
#undef OLED_SCL
#undef OLED_RST
#if defined(HELTEC_V3) || defined(ESP32_DIY_1W_LoRa) || defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2_SX1262)
#define HAS_SX126X
#endif
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(ESP32_DIY_LoRa) || defined(TTGO_T_Beam_V1_0) || defined(TTGO_T_Beam_V1_2)
#define HAS_SX127X
#endif
#if defined(TTGO_T_Beam_V1_0) || defined(TTGO_T_Beam_V1_0_SX1268)
#define HAS_AXP192
#endif
#if defined(TTGO_T_Beam_V1_2) || defined(TTGO_T_Beam_V1_2_SX1262)
#define HAS_AXP2101
#endif
// LORA MODULES
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(ESP32_DIY_LoRa)
#undef LORA_RST
#define LORA_SCK 5 // GPIO5 - SX1276 SCK
#define LORA_MISO 19 // GPIO19 - SX1276 MISO
#define LORA_MOSI 27 // GPIO27 - SX1276 MOSI
#define LORA_CS 18 // GPIO18 - SX1276 CS ---> NSS
#define LORA_RST 14 // GPIO14 - SX1276 RST
#define LORA_IRQ 26 // GPIO26 - SX1276 IRQ ---->DIO0
#endif
#ifdef HELTEC_V3
#define RADIO_SCLK_PIN 9 // SX1262 SCK
#define RADIO_MISO_PIN 11 // SX1262 MISO
#define RADIO_MOSI_PIN 10 // SX1262 MOSI
#define RADIO_CS_PIN 8 // SX1262 NSS
#define RADIO_RST_PIN 12 // SX1262 RST
#define RADIO_DIO1_PIN 14 // SX1262 DIO1
#define RADIO_BUSY_PIN 13 // SX1262 BUSY
#endif
#ifdef ESP32_DIY_1W_LoRa // Ebyte E22 400M30S / SX1268
#define RADIO_SCLK_PIN 18
#define RADIO_MISO_PIN 19
#define RADIO_MOSI_PIN 23
#define RADIO_CS_PIN 5
#define RADIO_RST_PIN 27
#define RADIO_DIO1_PIN 12
#define RADIO_BUSY_PIN 14
#define RADIO_RXEN 32
#define RADIO_TXEN 25
#endif
#if defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2_SX1262)
#define RADIO_SCLK_PIN 5
#define RADIO_MISO_PIN 19
#define RADIO_MOSI_PIN 27
#define RADIO_CS_PIN 18
#define RADIO_DIO0_PIN 26
#define RADIO_RST_PIN 23
#define RADIO_DIO1_PIN 33
#define RADIO_BUSY_PIN 32
#endif
// OLED
#if defined(TTGO_T_LORA32_V2_1) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa) || defined(TTGO_T_Beam_V1_0) || defined(TTGO_T_Beam_V1_2) || defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2_SX1262)
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#endif
#ifdef HELTEC_V2
#define OLED_SDA 4
#define OLED_SCL 15
#define OLED_RESET 16
#endif
#ifdef HELTEC_V3
#define OLED_SDA 17
#define OLED_SCL 18
#define OLED_RESET 21
#endif
// Leds and other stuff
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2)
#define internalLedPin 25 // Green Led
#define batteryPin 35
#endif
#ifdef HELTEC_V3
#define internalLedPin 35
#endif
#if defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
#define internalLedPin 2
#endif
/* (Same pins for LILYGO LoRa32 and ESP32 Wroom Dev )
SX1278-------------------> ESP32 ttgo-lora32-v21 and ESP32 WROOM Dev
GND GND
DIO1 -
DIO2 -
DIO3 -
VCC 3.3V
MISO 19
MOSI 27
SCLK 5
NSS 18
DIO0 26
REST 14
GND - */
#endif

View File

@@ -1,140 +1,310 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include "configuration.h"
#include "battery_utils.h"
#include "board_pinout.h"
#include "power_utils.h"
#include "pins_config.h"
#include "utils.h"
#if defined(HAS_AXP192) || defined(HAS_AXP2101)
#define I2C_SDA 21
#define I2C_SCL 22
#define IRQ_PIN 35
#ifdef TTGO_T_Beam_S3_SUPREME_V3
#define I2C0_SDA 17
#define I2C0_SCL 18
#define I2C1_SDA 42
#define I2C1_SCL 41
#define IRQ_PIN 40
#else
#define I2C_SDA 21
#define I2C_SCL 22
#define IRQ_PIN 35
#endif
#endif
#ifdef HAS_AXP192
XPowersAXP192 PMU;
XPowersAXP192 PMU;
#endif
#ifdef HAS_AXP2101
XPowersAXP2101 PMU;
XPowersAXP2101 PMU;
#endif
extern Configuration Config;
extern bool stationCallsignIsValid;
namespace POWER_Utils {
bool BatteryIsConnected = false;
String batteryVoltage = "";
String batteryChargeDischargeCurrent = "";
#ifdef ADC_CTRL_PIN
void adc_ctrl_ON() {
digitalWrite(ADC_CTRL_PIN, ADC_CTRL_ON_STATE);
}
void activateMeasurement() {
void adc_ctrl_OFF() {
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
#if defined(HAS_AXP192) || defined(HAS_AXP2101)
void activateMeasurement() {
PMU.disableTSPinMeasure();
PMU.enableBattDetection();
PMU.enableVbusVoltageMeasure();
PMU.enableBattVoltageMeasure();
PMU.enableSystemVoltageMeasure();
}
#endif
double getBatteryVoltage() {
#if defined(HAS_AXP192) || defined(HAS_AXP2101)
PMU.disableTSPinMeasure();
PMU.enableBattDetection();
PMU.enableVbusVoltageMeasure();
PMU.enableBattVoltageMeasure();
PMU.enableSystemVoltageMeasure();
return (PMU.getBattVoltage() / 1000.0);
#else
return 0.0;
#endif
}
bool isBatteryConnected() {
#if defined(HAS_AXP192) || defined(HAS_AXP2101)
return PMU.isBatteryConnect();
#else
return false;
#endif
}
void activateGPS() {
#ifdef HAS_AXP192
PMU.setLDO3Voltage(3300);
PMU.enableLDO3();
#endif
#ifdef HAS_AXP2101
#ifdef TTGO_T_Beam_S3_SUPREME_V3
PMU.setALDO4Voltage(3300);
PMU.enableALDO4();
#else
PMU.setALDO3Voltage(3300);
PMU.enableALDO3();
#endif
#endif
#ifdef HELTEC_WIRELESS_TRACKER
adc_ctrl_ON();
#endif
//gpsIsActive = true;
}
void deactivateGPS() {
#ifdef HAS_AXP192
PMU.disableLDO3();
#endif
#ifdef HAS_AXP2101
#ifdef TTGO_T_Beam_S3_SUPREME_V3
PMU.disableALDO4();
#else
PMU.disableALDO3();
#endif
#endif
#ifdef HELTEC_WIRELESS_TRACKER
adc_ctrl_OFF();
#endif
//gpsIsActive = false;
}
void activateLoRa() {
#ifdef HAS_AXP192
PMU.setLDO2Voltage(3300);
PMU.enableLDO2();
PMU.setLDO2Voltage(3300);
PMU.enableLDO2();
#endif
#ifdef HAS_AXP2101
PMU.setALDO2Voltage(3300);
PMU.enableALDO2();
#ifdef TTGO_T_Beam_S3_SUPREME_V3
PMU.setALDO3Voltage(3300);
PMU.enableALDO3();
#else
PMU.setALDO2Voltage(3300);
PMU.enableALDO2();
#endif
#endif
}
void deactivateLoRa() {
#ifdef HAS_AXP192
PMU.disableLDO2();
PMU.disableLDO2();
#endif
#ifdef HAS_AXP2101
PMU.disableALDO2();
#ifdef TTGO_T_Beam_S3_SUPREME_V3
PMU.disableALDO3();
#else
PMU.disableALDO2();
#endif
#endif
}
bool begin(TwoWire &port) {
#if defined(HAS_AXP192)
bool result = PMU.begin(Wire, AXP192_SLAVE_ADDRESS, I2C_SDA, I2C_SCL);
if (result) {
PMU.disableDC2();
PMU.disableLDO2();
PMU.disableLDO3();
PMU.setDC1Voltage(3300);
PMU.enableDC1();
PMU.setProtectedChannel(XPOWERS_DCDC3);
PMU.disableIRQ(XPOWERS_AXP192_ALL_IRQ);
}
return result;
bool result = PMU.begin(Wire, AXP192_SLAVE_ADDRESS, I2C_SDA, I2C_SCL);
if (result) {
PMU.disableDC2();
PMU.disableLDO2();
PMU.disableLDO3();
PMU.setDC1Voltage(3300);
PMU.enableDC1();
PMU.setProtectedChannel(XPOWERS_DCDC3);
PMU.disableIRQ(XPOWERS_AXP192_ALL_IRQ);
}
return result;
#elif defined(HAS_AXP2101)
bool result = PMU.begin(Wire, AXP2101_SLAVE_ADDRESS, I2C_SDA, I2C_SCL);
if (result) {
PMU.disableDC2();
PMU.disableDC3();
PMU.disableDC4();
PMU.disableDC5();
PMU.disableALDO1();
PMU.disableALDO4();
PMU.disableBLDO1();
PMU.disableBLDO2();
PMU.disableDLDO1();
PMU.disableDLDO2();
PMU.setDC1Voltage(3300);
PMU.enableDC1();
PMU.setButtonBatteryChargeVoltage(3300);
PMU.enableButtonBatteryCharge();
PMU.disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
}
return result;
#ifdef TTGO_T_Beam_S3_SUPREME_V3
bool result = PMU.begin(Wire1, AXP2101_SLAVE_ADDRESS, I2C1_SDA, I2C1_SCL);
#else
bool result = PMU.begin(Wire, AXP2101_SLAVE_ADDRESS, I2C_SDA, I2C_SCL);
#endif
if (result) {
PMU.disableDC2();
PMU.disableDC3();
PMU.disableDC4();
PMU.disableDC5();
#ifndef TTGO_T_Beam_S3_SUPREME_V3
PMU.disableALDO1();
PMU.disableALDO4();
#endif
PMU.disableBLDO1();
PMU.disableBLDO2();
PMU.disableDLDO1();
PMU.disableDLDO2();
PMU.setDC1Voltage(3300);
PMU.enableDC1();
#ifdef TTGO_T_Beam_S3_SUPREME_V3
PMU.setALDO1Voltage(3300);
#endif
PMU.setButtonBatteryChargeVoltage(3300);
PMU.enableButtonBatteryCharge();
PMU.disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
}
return result;
#else
return true;
return true;
#endif
}
void setup() {
Wire.end();
#ifdef HAS_AXP192
Wire.begin(SDA, SCL);
if (begin(Wire)) {
Serial.println("AXP192 init done!");
} else {
Serial.println("AXP192 init failed!");
}
activateLoRa();
activateMeasurement();
PMU.setChargerTerminationCurr(XPOWERS_AXP192_CHG_ITERM_LESS_10_PERCENT);
PMU.setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
PMU.setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_780MA);
PMU.setSysPowerDownVoltage(2600);
Wire.begin(SDA, SCL);
if (begin(Wire)) {
Serial.println("AXP192 init done!");
} else {
Serial.println("AXP192 init failed!");
}
activateLoRa();
activateMeasurement();
PMU.setChargerTerminationCurr(XPOWERS_AXP192_CHG_ITERM_LESS_10_PERCENT);
PMU.setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
PMU.setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_780MA);
PMU.setSysPowerDownVoltage(2600);
#endif
#ifdef HAS_AXP2101
Wire.begin(SDA, SCL);
if (begin(Wire)) {
Serial.println("AXP2101 init done!");
} else {
Serial.println("AXP2101 init failed!");
}
activateLoRa();
activateMeasurement();
PMU.setPrechargeCurr(XPOWERS_AXP2101_PRECHARGE_200MA);
PMU.setChargerTerminationCurr(XPOWERS_AXP2101_CHG_ITERM_25MA);
PMU.setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
PMU.setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_800MA);
PMU.setSysPowerDownVoltage(2600);
bool beginStatus = false;
#ifdef TTGO_T_Beam_S3_SUPREME_V3
Wire1.begin(I2C1_SDA, I2C1_SCL);
Wire.begin(I2C0_SDA, I2C0_SCL);
if (begin(Wire1)) beginStatus = true;
#else
Wire.begin(SDA, SCL);
if (begin(Wire)) beginStatus = true;
#endif
if (beginStatus) {
Serial.println("AXP2101 init done!");
} else {
Serial.println("AXP2101 init failed!");
}
activateLoRa();
activateMeasurement();
PMU.setPrechargeCurr(XPOWERS_AXP2101_PRECHARGE_200MA);
PMU.setChargerTerminationCurr(XPOWERS_AXP2101_CHG_ITERM_25MA);
PMU.setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
PMU.setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_800MA);
PMU.setSysPowerDownVoltage(2600);
#endif
#ifdef BATTERY_PIN
pinMode(BATTERY_PIN, INPUT);
#endif
#ifdef INTERNAL_LED_PIN
pinMode(INTERNAL_LED_PIN, OUTPUT);
digitalWrite(INTERNAL_LED_PIN, LOW);
#endif
if (Config.battery.sendExternalVoltage || Config.battery.monitorExternalVoltage) {
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
vext_ctrl_ON();
#endif
#ifdef HAS_GPS
if (Config.beacon.gpsActive && Config.digi.ecoMode != 1) activateGPS();
#endif
#ifdef ADC_CTRL
pinMode(ADC_CTRL, OUTPUT);
adc_ctrl_OFF();
#endif
#if defined(TTGO_T_DECK_GPS) || defined(TTGO_T_DECK_PLUS)
pinMode(BOARD_POWERON, OUTPUT);
digitalWrite(BOARD_POWERON, HIGH);
pinMode(BOARD_SDCARD_CS, OUTPUT);
pinMode(RADIO_CS_PIN, OUTPUT);
pinMode(TFT_CS, OUTPUT);
digitalWrite(BOARD_SDCARD_CS, HIGH);
digitalWrite(RADIO_CS_PIN, HIGH);
digitalWrite(TFT_CS, HIGH);
delay(500);
#endif
#ifdef USE_WIRE_WITH_OLED_PINS
Wire.begin(OLED_SDA, OLED_SCL);
#endif
#ifdef SENSOR_I2C_BUS
SENSOR_I2C_BUS.begin(BOARD_I2C_SDA, BOARD_I2C_SCL);
#endif
delay(1000);
BATTERY_Utils::setup();
BATTERY_Utils::startupBatteryHealth();
stationCallsignIsValid = Utils::callsignIsValid(Config.callsign);
setCpuFrequencyMhz(80);
}
/*void lowerCpuFrequency() {
#if defined(TTGO_T_Beam_V1_0) || defined(TTGO_T_Beam_V1_0_SX1268) || defined(TTGO_T_Beam_V1_2) || defined(ESP32_DIY_LoRa_GPS) || defined(TTGO_T_LORA32_V2_1_GPS) || defined(TTGO_T_LORA32_V2_1_TNC) || defined(ESP32_DIY_1W_LoRa_GPS) || defined(TTGO_T_Beam_V1_2_SX1262)
if (setCpuFrequencyMhz(80)) {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "Main", "CPU frequency set to 80MHz");
} else {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_WARN, "Main", "CPU frequency unchanged");
}
#endif
}*/
}

View File

@@ -1,19 +0,0 @@
#ifndef POWER_UTILS_H_
#define POWER_UTILS_H_
#include <Arduino.h>
#include "XPowersLib.h"
namespace POWER_Utils {
void activateMeasurement();
void activateLoRa();
void deactivateLoRa();
bool begin(TwoWire &port);
void setup();
//void lowerCpuFrequency();
}
#endif

View File

@@ -1,50 +1,170 @@
#include "configuration.h"
#include "query_utils.h"
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
extern Configuration Config;
extern WiFi_AP *currentWiFi;
extern std::vector<String> lastHeardStation;
extern std::vector<String> lastHeardStation_temp;
extern String versionDate;
extern int stationMode;
#include "configuration.h"
#include "battery_utils.h"
#include "station_utils.h"
#include "query_utils.h"
#include "lora_utils.h"
extern Configuration Config;
extern std::vector<LastHeardStation> lastHeardStations;
extern String versionDate;
extern int rssi;
extern float snr;
extern int freqError;
extern bool shouldSleepLowVoltage;
extern bool saveNewDigiEcoModeConfig;
extern String versionNumber;
namespace QUERY_Utils {
String process(String query, String station, String queryOrigin) {
String process(const String& query, const String& station, bool queryFromAPRSIS, bool thirdParty) {
String answer;
if (query=="?APRS?" || query=="?aprs?" || query=="?Aprs?" || query=="H" || query=="h" || query=="HELP" || query=="Help" || query=="help" || query=="?") {
answer = "?APRSV ?APRSP ?APRSL ?APRSH ?WHERE callsign";
} else if (query=="?APRSV" || query=="?aprsv" || query=="?Aprsv") {
answer = "CA2RXU_LoRa_iGate 1.3 v" + versionDate + " sM" + String(stationMode);
} else if (query=="?APRSP" || query=="?aprsp" || query=="?Aprsp") {
answer = "iGate QTH: " + String(currentWiFi->latitude,2) + " " + String(currentWiFi->longitude,2);
} else if (query=="?APRSL" || query=="?aprsl" || query=="?Aprsl") {
if (lastHeardStation.size() == 0) {
answer = "No Station Listened in the last " + String(Config.rememberStationTime) + "min.";
String queryQuestion = query;
queryQuestion.toUpperCase();
if (queryQuestion == "?APRS?" || queryQuestion == "H" || queryQuestion == "HELP" || queryQuestion=="?") {
answer.concat("?APRSV ?APRSP ?APRSL ?APRSSR ?EM=? ?TX=? "); // ?APRSH ?WHERE callsign
} else if (queryQuestion == "?APRSV") {
answer.concat("CA2RXU_LoRa_iGate v");
answer.concat(versionNumber);
answer.concat(" ");
answer.concat(versionDate);
} else if (queryQuestion == "?APRSP") {
answer.concat("iGate QTH: ");
answer.concat(String(Config.beacon.latitude,2));
answer.concat(" ");
answer.concat(String(Config.beacon.longitude,2));
} else if (queryQuestion == "?APRSL") {
if (lastHeardStations.size() == 0) {
char answerArray[50];
snprintf(answerArray, sizeof(answerArray), "No Station Listened in the last %d min.", Config.rememberStationTime);
answer.concat(answerArray);
} else {
for (int i=0; i<lastHeardStation.size(); i++) {
answer += lastHeardStation[i].substring(0,lastHeardStation[i].indexOf(",")) + " ";
for (int i=0; i<lastHeardStations.size(); i++) {
answer += lastHeardStations[i].station + " ";
}
answer.trim();
}
} else if (query.indexOf("?APRSH") == 0 || query.indexOf("?aprsh") == 0 || query.indexOf("?Aprsh") == 0) {
} else if (queryQuestion == "?APRSSR") {
char signalData[35];
snprintf(signalData, sizeof(signalData), " %ddBm / %.2fdB / %dHz", rssi, snr, freqError);
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 = "APRSH on development 73!";
} else if (query.indexOf("?WHERE") == 0) {
answer.concat("?APRSH on development 73!");
} *//*else if (queryQuestion.indexOf("?WHERE") == 0) {
// agregar callsign para completar donde esta X callsign --> posicion
Serial.println("estaciones escuchadas directo (ultimos 30 min)");
answer = "?WHERE on development 73!";
answer.concat("?WHERE on development 73!");
} */
else if (STATION_Utils::isManager(station) && (!queryFromAPRSIS || !Config.remoteManagement.rfOnly)) {
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 == 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;
shouldSleepLowVoltage = true; // to make sure all packets in outputPacketBuffer are sent before restart.
saveNewDigiEcoModeConfig = true;
} else {
answer = "DigiEcoMode was OFF";
}
} else {
answer = "Digipeater Mode control not possible";
}
} else if (queryQuestion.startsWith("?EM=ON")) {
if ((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.
saveNewDigiEcoModeConfig = true;
} else {
answer = "DigiEcoMode was ON";
}
} else {
answer = "Digipeater Mode control not possible";
}
} 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.startsWith("?TX=ON")) {
if (radioTxActive) {
answer = "TX was ON";
} else {
Config.loramodule.txActive = true;
answer = "TX=ON";
}
} else if (queryQuestion.startsWith("?TX=OFF")) {
if (!radioTxActive) {
answer = "TX was OFF";
} else {
Config.loramodule.txActive = false;
answer = "TX=OFF";
}
} 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();
}
}
for(int i = station.length(); i < 9; i++) {
station += ' ';
if (answer == "") return "";
String queryAnswer = Config.callsign;
queryAnswer += ">APLRG1";
if (queryFromAPRSIS) {
queryAnswer += ",TCPIP,qAC";
} else {
if (!thirdParty) queryAnswer += ",RFONLY";
if (Config.beacon.path != "") {
queryAnswer += ",";
queryAnswer += Config.beacon.path;
}
}
if (queryOrigin == "APRSIS") {
return Config.callsign + ">APLRG1,TCPIP,qAC::" + station + ":" + answer;// + "\n";
} else { //} if (queryOrigin == "LoRa") {
return Config.callsign + ">APLRG1,RFONLY,WIDE1-1::" + station + ":" + answer;
queryAnswer += "::";
String processedStation = station;
for (int i = station.length(); i < 9; i++) {
processedStation += ' ';
}
queryAnswer += processedStation;
queryAnswer += ":";
queryAnswer += answer;
queryAnswer += " *";
queryAnswer += char(random(97, 123));
queryAnswer += char(random(97, 123));
queryAnswer += "*";
return queryAnswer;
}
}

View File

@@ -1,13 +0,0 @@
#ifndef QUERY_UTILS_H_
#define QUERY_UTILS_H_
#include <Arduino.h>
namespace QUERY_Utils {
String process(String query, String station, String queryOrigin);
}
#endif

86
src/sleep_utils.cpp Normal file
View File

@@ -0,0 +1,86 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include "configuration.h"
#include "board_pinout.h"
#include "sleep_utils.h"
#include "digi_utils.h"
#include "lora_utils.h"
extern Configuration Config;
extern bool shouldSleepStop;
extern uint32_t lastBeaconTx;
bool wakeUpFlag = false;
namespace SLEEP_Utils {
void wakeUpLoRaPacketReceived() {
wakeUpFlag = true;
}
void checkWakeUpFlag() {
if (wakeUpFlag) {
String packet = LoRa_Utils::receivePacketFromSleep();
if (packet != "") {
DIGI_Utils::processLoRaPacket(packet);
}
wakeUpFlag = false;
}
}
void setup() {
#ifndef HAS_A7670
if (Config.digi.ecoMode == 1) {
pinMode(RADIO_WAKEUP_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(RADIO_WAKEUP_PIN), wakeUpLoRaPacketReceived, RISING);
#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);
#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
}
uint32_t getSecondsToSleep() {
uint32_t elapsedTime = (millis() - lastBeaconTx) / 1000; // in secs
uint32_t intervalTime = Config.beacon.interval * 60; // in secs
return (elapsedTime < intervalTime) ? (intervalTime - elapsedTime) : 0;
}
void startSleeping() {
if (!shouldSleepStop) {
uint32_t timeToSleep = getSecondsToSleep();
esp_sleep_enable_timer_wakeup(timeToSleep * 1000000); // 1 min = 60sec
Serial.print("(Sleeping : "); Serial.print(timeToSleep); Serial.println("seconds)");
delay(100);
LoRa_Utils::wakeRadio();
esp_light_sleep_start();
}
}
void checkSerial() {
if (Config.digi.ecoMode == 1) Serial.end();
}
}

View File

@@ -1,105 +1,239 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include "station_utils.h"
#include "battery_utils.h"
#include "aprs_is_utils.h"
#include "configuration.h"
#include "lora_utils.h"
#include "display.h"
#include "utils.h"
#include <vector>
extern Configuration Config;
extern std::vector<String> lastHeardStation;
extern std::vector<String> lastHeardStation_temp;
extern std::vector<String> packetBuffer;
extern std::vector<String> packetBuffer_temp;
extern String fourthLine;
extern Configuration Config;
extern uint32_t lastRxTime;
extern String fourthLine;
extern bool shouldSleepLowVoltage;
uint32_t lastTxTime = millis();
std::vector<LastHeardStation> lastHeardStations;
std::vector<String> blacklist;
std::vector<String> managers;
std::vector<LastHeardStation> lastHeardObjects;
struct OutputPacketBuffer {
String packet;
bool isBeacon;
};
std::vector<OutputPacketBuffer> outputPacketBuffer;
struct Packet25SegBuffer {
uint32_t receivedTime;
String station;
String payload;
};
std::vector<Packet25SegBuffer> packet25SegBuffer;
bool saveNewDigiEcoModeConfig = false;
bool packetIsBeacon = false;
namespace STATION_Utils {
void deleteNotHeard() {
for (int i=0; i<lastHeardStation.size(); i++) {
String deltaTimeString = lastHeardStation[i].substring(lastHeardStation[i].indexOf(",")+1);
uint32_t deltaTime = deltaTimeString.toInt();
if ((millis() - deltaTime) < Config.rememberStationTime*60*1000) {
lastHeardStation_temp.push_back(lastHeardStation[i]);
std::vector<String> loadCallsignList(const String& list) {
std::vector<String> loadedList;
String callsigns = list;
callsigns.trim();
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
}
return loadedList;
}
void loadBlacklistAndManagers() {
blacklist = loadCallsignList(Config.blacklist);
managers = loadCallsignList(Config.remoteManagement.managers);
}
bool checkCallsignList(const std::vector<String>& list, const String& callsign) {
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;
} else {
if (list[i] == callsign) return true;
}
}
lastHeardStation.clear();
for (int j=0; j<lastHeardStation_temp.size(); j++) {
lastHeardStation.push_back(lastHeardStation_temp[j]);
return false;
}
bool isBlacklisted(const String& callsign) {
return checkCallsignList(blacklist, callsign);
}
bool isManager(const String& callsign) {
return checkCallsignList(managers, callsign);
}
void cleanObjectsHeard() {
for (auto it = lastHeardObjects.begin(); it != lastHeardObjects.end(); ) {
if (millis() - it->lastHeardTime >= 9.75 * 60 * 1000) { // 9.75 = 9min 45secs
it = lastHeardObjects.erase(it); // erase() returns the next valid iterator
} else {
++it; // Only increment if not erasing
}
}
}
bool checkObjectTime(const String& packet) {
cleanObjectsHeard();
int objectIDIndex = packet.indexOf(":;");
String object = packet.substring(objectIDIndex + 2, objectIDIndex + 11);
object.trim();
for (int i = 0; i < lastHeardObjects.size(); i++) { // Check if i should Tx object
if (lastHeardObjects[i].station == object) return false;
}
lastHeardObjects.emplace_back(LastHeardStation{millis(), object}); // Add new object and Tx
return true;
}
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]);
}
}
lastHeardStations.clear();
for (int j = 0; j < lastHeardStation_temp.size(); j++) {
lastHeardStations.push_back(lastHeardStation_temp[j]);
}
lastHeardStation_temp.clear();
}
void updateLastHeard(String station) {
void updateLastHeard(const String& station) {
deleteNotHeard();
bool stationHeard = false;
for (int i=0; i<lastHeardStation.size(); i++) {
if (lastHeardStation[i].substring(0,lastHeardStation[i].indexOf(",")) == station) {
lastHeardStation[i] = station + "," + String(millis());
for (int i = 0; i < lastHeardStations.size(); i++) {
if (lastHeardStations[i].station == station) {
lastHeardStations[i].lastHeardTime = millis();
stationHeard = true;
break;
}
}
if (!stationHeard) {
lastHeardStation.push_back(station + "," + String(millis()));
}
fourthLine = "Stations (" + String(Config.rememberStationTime) + "min) = ";
if (lastHeardStation.size() < 10) {
fourthLine += " ";
}
fourthLine += String(lastHeardStation.size());
#ifndef TextSerialOutputForApp ////// This is just for debugging
Serial.print("Stations Near (last " + String(Config.rememberStationTime) + " minutes): ");
for (int k=0; k<lastHeardStation.size(); k++) {
Serial.print(lastHeardStation[k].substring(0,lastHeardStation[k].indexOf(","))); Serial.print(" ");
}
Serial.println("");
#endif
if (!stationHeard) lastHeardStations.emplace_back(LastHeardStation{millis(), station});
Utils::showActiveStations();
}
bool wasHeard(String station) {
bool wasHeard(const String& station) {
deleteNotHeard();
for (int i=0; i<lastHeardStation.size(); i++) {
if (lastHeardStation[i].substring(0,lastHeardStation[i].indexOf(",")) == station) {
Serial.println(" ---> Listened Station");
for (int i = 0; i < lastHeardStations.size(); i++) {
if (lastHeardStations[i].station == station) {
Utils::println(" ---> Listened Station");
return true;
}
}
}
Serial.println(" ---> Station not Heard for last 30 min (Not Tx)\n");
Utils::println(" ---> Station not Heard in " + String(Config.rememberStationTime) + " min: No Tx");
return false;
}
void checkBuffer() {
for (int i=0; i<packetBuffer.size(); i++) {
String deltaTimeString = packetBuffer[i].substring(0,packetBuffer[i].indexOf(","));
uint32_t deltaTime = deltaTimeString.toInt();
if ((millis() - deltaTime) < 60*1000) { // cambiar a 15 segundos?
packetBuffer_temp.push_back(packetBuffer[i]);
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;
}
}
packetBuffer.clear();
for (int j=0; j<packetBuffer_temp.size(); j++) {
packetBuffer.push_back(packetBuffer_temp[j]);
}
packetBuffer_temp.clear();
packet25SegBuffer.emplace_back(Packet25SegBuffer{millis(), station, textMessage});
return true;
}
// BORRAR ESTO !!
for (int i=0; i<packetBuffer.size(); i++) {
Serial.println(packetBuffer[i]);
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
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();
//
if (saveNewDigiEcoModeConfig) {
Config.writeFile();
delay(1000);
displayToggle(false);
ESP.restart();
}
//
}
void updatePacketBuffer(String packet) {
if ((packet.indexOf(":!") == -1) && (packet.indexOf(":=") == -1) && (packet.indexOf(":>") == -1) && (packet.indexOf(":`") == -1)) {
String sender = packet.substring(3,packet.indexOf(">"));
String tempAddressee = packet.substring(packet.indexOf("::") + 2);
String addressee = tempAddressee.substring(0,tempAddressee.indexOf(":"));
addressee.trim();
String message = tempAddressee.substring(tempAddressee.indexOf(":")+1);
//Serial.println(String(millis()) + "," + sender + "," + addressee + "," + message);
packetBuffer.push_back(String(millis()) + "," + sender + "," + addressee + "," + message);
checkBuffer();
void processOutputPacketBuffer() {
int timeToWait = 3 * 1000; // 3 segs between packet Tx and also Rx ???
uint32_t lastRx = millis() - lastRxTime;
uint32_t lastTx = millis() - lastTxTime;
if (outputPacketBuffer.size() > 0 && lastTx > timeToWait && lastRx > timeToWait) {
if (outputPacketBuffer[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) {
if (outputPacketBuffer[0].isBeacon) packetIsBeacon = true;
LoRa_Utils::sendNewPacket(outputPacketBuffer[0].packet);
if (outputPacketBuffer[0].isBeacon) packetIsBeacon = false;
outputPacketBuffer.erase(outputPacketBuffer.begin());
delay(4000);
}
}
if (saveNewDigiEcoModeConfig) {
Config.writeFile();
delay(1000);
displayToggle(false);
ESP.restart();
}
}
void addToOutputPacketBuffer(const String& packet, bool flag) {
OutputPacketBuffer entry;
entry.packet = packet;
entry.isBeacon = flag;
outputPacketBuffer.push_back(entry);
}
}

View File

@@ -1,17 +0,0 @@
#ifndef STATION_UTILS_H_
#define STATION_UTILS_H_
#include <Arduino.h>
namespace STATION_Utils {
void deleteNotHeard();
void updateLastHeard(String station);
bool wasHeard(String station);
void checkBuffer();
void updatePacketBuffer(String packet);
}
#endif

View File

@@ -1,64 +1,137 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <WiFiUdp.h>
#include <WiFi.h>
#include "configuration.h"
#include "syslog_utils.h"
#include "gps_utils.h"
extern Configuration Config;
extern int stationMode;
extern String versionDate;
extern String versionNumber;
WiFiUDP udpClient;
namespace SYSLOG_Utils {
void log(String type, String packet, int rssi, float snr, int freqError) {
String syslogPacket = "<165>1 - " + Config.callsign + " CA2RXU_LoRa_iGate_1.2" + " - - - "; //RFC5424 The Syslog Protocol
if (Config.syslog.active && (stationMode==1 || stationMode==2 || (stationMode==5 && WiFi.status()==WL_CONNECTED))) {
if (type == "APRSIS Tx") {
if (packet.indexOf(":>") > 10) {
syslogPacket += type + " / StartUp_Status / " + packet.substring(packet.indexOf(":>")+2);
} else {
syslogPacket += type + " / QUERY / " + packet;
}
} else if (type == "Rx") {
if (packet.indexOf("::") > 10) {
syslogPacket += type + " / MESSAGE / " + packet.substring(3,packet.indexOf(">")) + " ---> " + packet.substring(packet.indexOf("::")+2);
syslogPacket += " / " + String(rssi) + "dBm / " + String(snr) + "dB / " + String(freqError) + "Hz";
} else if (packet.indexOf(":!") > 10 || packet.indexOf(":=") > 10) {
syslogPacket += type + " / GPS / " + packet.substring(3,packet.indexOf(">")) + " / ";
if (packet.indexOf("WIDE1-1") > 10) {
syslogPacket += packet.substring(packet.indexOf(">")+1,packet.indexOf(",")) + " / WIDE1-1 / ";
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
char signalData[35];
snprintf(signalData, sizeof(signalData), " / %ddBm / %.2fdB / %dHz", rssi, snr, freqError);
int colonIndex = packet.indexOf(":");
char nextChar = packet[colonIndex + 1];
int greaterThanIndex = packet.indexOf(">");
int telemetryPacketIndex = packet.indexOf(":T#");
String sender = packet.substring(3, greaterThanIndex);
switch (type) {
case 0: // CRC
syslogPacket.concat("CRC / CRC-ERROR / ");
syslogPacket.concat(packet);
syslogPacket.concat(signalData);
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(" / ");
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(" / -");
}
} 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 (telemetryPacketIndex >= 10 && packet.indexOf(":=/") == -1) {
syslogPacket.concat("TELEMETRY / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(telemetryPacketIndex + 3));
} else {
syslogPacket += packet.substring(packet.indexOf(">")+1,packet.indexOf(":")) + " / - / ";
syslogPacket.concat(packet);
}
syslogPacket += String(rssi) + "dBm / " + String(snr) + "dB / " + String(freqError) + "Hz / " + GPS_Utils::getDistance(packet);
} else if (packet.indexOf(":>") > 10) {
syslogPacket += type + " / STATUS / " + packet.substring(3,packet.indexOf(">")) + " ---> " + packet.substring(packet.indexOf(":>")+2);
syslogPacket += " / " + String(rssi) + "dBm / " + String(snr) + "dB / " + String(freqError) + "Hz";
} else if (packet.indexOf(":`") > 10) {
syslogPacket += type + " / MIC-E / " + packet.substring(3,packet.indexOf(">")) + " ---> " + packet.substring(packet.indexOf(":`")+2);
syslogPacket += " / " + String(rssi) + "dBm / " + String(snr) + "dB / " + String(freqError) + "Hz";
} else if (packet.indexOf(":T#") >= 10 && packet.indexOf(":=/") == -1) {
syslogPacket += type + " / TELEMETRY / " + packet.substring(3,packet.indexOf(">")) + " ---> " + packet.substring(packet.indexOf(":T#")+3);
syslogPacket += " / " + String(rssi) + "dBm / " + String(snr) + "dB / " + String(freqError) + "Hz";
} else if (packet.indexOf(":;") > 10) {
syslogPacket += type + " / OBJECT / " + packet.substring(3,packet.indexOf(">")) + " ---> " + packet.substring(packet.indexOf(":;")+2);
syslogPacket += " / " + String(rssi) + "dBm / " + String(snr) + "dB / " + String(freqError) + "Hz";
} else {
syslogPacket += type + " / " + packet;
syslogPacket += " / " + String(rssi) + "dBm / " + String(snr) + "dB / " + String(freqError) + "Hz";
}
} else if (type == "Tx") {
if (packet.indexOf("RFONLY") > 10) {
syslogPacket += type + " / RFONLY / " + packet;
} else if (packet.indexOf("::") > 10) {
syslogPacket += type + " / MESSAGE / " + packet.substring(0,packet.indexOf(">")) + " ---> " + packet.substring(packet.indexOf("::")+2);
} else {
syslogPacket += type + " / " + packet;
}
} else {
syslogPacket = "<165>1 - ERROR LoRa - - - ERROR / Error in Syslog Packet"; //RFC5424 The Syslog Protocol
syslogPacket.concat(signalData);
if (nextChar == '!' || nextChar == '=' || nextChar == '@') {
syslogPacket.concat(" / ");
syslogPacket.concat(GPS_Utils::getDistanceAndComment(packet));
}
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);
}
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);
}
break;
default:
syslogPacket = "<165>1 - ERROR LoRa - - - ERROR / Error in Syslog Packet"; //RFC5424 The Syslog Protocol
break;
}
udpClient.beginPacket(Config.syslog.server.c_str(), Config.syslog.port);
udpClient.write((const uint8_t*)syslogPacket.c_str(), syslogPacket.length());
@@ -67,9 +140,9 @@ namespace SYSLOG_Utils {
}
void setup() {
if (Config.syslog.active && (stationMode==1 || stationMode==2 || (stationMode==5 && WiFi.status()==WL_CONNECTED))) {
udpClient.begin(WiFi.localIP(), 0);
Serial.println("init : Syslog Server ... done! (at " + Config.syslog.server + ")");
if (WiFi.status() == WL_CONNECTED) {
udpClient.begin(0);
if (Config.syslog.active) Serial.println("init : Syslog Server ... done! (at " + Config.syslog.server + ")");
}
}

View File

@@ -1,14 +0,0 @@
#ifndef SYSLOG_H_
#define SYSLOG_H_
#include <Arduino.h>
namespace SYSLOG_Utils {
void log(String type ,String packet, int rssi, float snr, int freqError);
void setup();
}
#endif

130
src/telemetry_utils.cpp Normal file
View File

@@ -0,0 +1,130 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <APRSPacketLib.h>
#include <Arduino.h>
#include <vector>
#include "telemetry_utils.h"
#include "aprs_is_utils.h"
#include "configuration.h"
#include "station_utils.h"
#include "battery_utils.h"
#include "lora_utils.h"
#include "wx_utils.h"
#include "display.h"
extern Configuration Config;
extern bool sendStartTelemetry;
int telemetryCounter = random(1,999);
namespace TELEMETRY_Utils {
String joinWithCommas(const std::vector<String>& items) {
String result;
for (size_t i = 0; i < items.size(); ++i) {
result += items[i];
if (i < items.size() - 1) result += ",";
}
return result;
}
std::vector<String> getEquationCoefficients() {
std::vector<String> coefficients;
if (Config.battery.sendInternalVoltage) coefficients.push_back("0,0.01,0");
if (Config.battery.sendExternalVoltage) coefficients.push_back("0,0.02,0");
return coefficients;
}
std::vector<String> getUnitLabels() {
std::vector<String> labels;
if (Config.battery.sendInternalVoltage) labels.push_back("VDC");
if (Config.battery.sendExternalVoltage) labels.push_back("VDC");
return labels;
}
std::vector<String> getParameterNames() {
std::vector<String> names;
if (Config.battery.sendInternalVoltage) names.push_back("V_Batt");
if (Config.battery.sendExternalVoltage) names.push_back("V_Ext");
return names;
}
void sendBaseTelemetryPacket(const String& prefix, const std::vector<String>& values) {
String packet = prefix + joinWithCommas(values);
String currentCallsign = (Config.tacticalCallsign != "") ? Config.tacticalCallsign : Config.callsign;
if (Config.beacon.sendViaAPRSIS) {
String baseAPRSISTelemetryPacket = APRSPacketLib::generateMessagePacket(currentCallsign, "APLRG1", "TCPIP,qAC", currentCallsign, packet);
#ifdef HAS_A7670
A7670_Utils::uploadToAPRSIS(baseAPRSISTelemetryPacket);
#else
APRS_IS_Utils::upload(baseAPRSISTelemetryPacket);
#endif
delay(300);
} else if (Config.beacon.sendViaRF) {
String baseRFTelemetryPacket = APRSPacketLib::generateMessagePacket(currentCallsign, "APLRG1", Config.beacon.path, currentCallsign, packet);
LoRa_Utils::sendNewPacket(baseRFTelemetryPacket);
delay(3000);
}
}
void sendEquationsUnitsParameters() {
sendBaseTelemetryPacket("EQNS.", getEquationCoefficients());
sendBaseTelemetryPacket("UNIT.", getUnitLabels());
sendBaseTelemetryPacket("PARM.", getParameterNames());
sendStartTelemetry = false;
}
String generateEncodedTelemetryBytes(float value, bool counterBytes, byte telemetryType) {
String encodedBytes;
int tempValue;
if (counterBytes) {
tempValue = value;
} else {
switch (telemetryType) {
case 0: tempValue = value * 100; break; // Internal voltage (0-4,2V), Humidity, Gas calculation
case 1: tempValue = (value * 100) / 2; break; // External voltage calculation (0-15V)
case 2: tempValue = (value * 10) + 500; break; // Temperature
case 3: tempValue = (value * 8); break; // Pressure
default: tempValue = value; break;
}
}
int firstByte = tempValue / 91;
tempValue -= firstByte * 91;
encodedBytes = char(firstByte + 33);
encodedBytes += char(tempValue + 33);
return encodedBytes;
}
String generateEncodedTelemetry() {
String telemetry = "|";
telemetry += generateEncodedTelemetryBytes(telemetryCounter, true, 0);
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, Config.battery.useExternalI2CSensor ? 0 : 1);
telemetry += "|";
return telemetry;
}
}

176
src/tnc_utils.cpp Normal file
View File

@@ -0,0 +1,176 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <WiFi.h>
#include "ESPmDNS.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 WiFiClient aprsIsClient;
extern bool passcodeValid;
#define MAX_CLIENTS 4
#define INPUT_BUFFER_SIZE (2 + MAX_CLIENTS)
#define TNC_PORT 8001
WiFiClient* clients[MAX_CLIENTS];
WiFiServer tncServer(TNC_PORT);
String inputServerBuffer[INPUT_BUFFER_SIZE];
String inputSerialBuffer = "";
namespace TNC_Utils {
void setup() {
if (Config.tnc.enableServer && Config.digi.ecoMode == 0) {
tncServer.stop();
tncServer.begin();
String host = "igate-" + Config.callsign;
if (!MDNS.begin(host.c_str())) {
Serial.println("Error Starting mDNS");
tncServer.stop();
return;
}
if (!MDNS.addService("tnc", "tcp", TNC_PORT)) {
Serial.println("Error: Could not add mDNS service");
}
Serial.println("TNC server started successfully");
Serial.println("mDNS Host: " + host + ".local");
}
}
void checkNewClients() {
WiFiClient new_client = tncServer.accept();
if (new_client.connected()) {
for (int i = 0; i < MAX_CLIENTS; i++) {
WiFiClient* client = clients[i];
if (client == nullptr) {
clients[i] = new WiFiClient(new_client);
Utils::println("New TNC client connected");
break;
}
}
}
}
void handleInputData(char character, int bufferIndex) {
String* data = (bufferIndex == -1) ? &inputSerialBuffer : &inputServerBuffer[bufferIndex];
if (data->length() == 0 && character != (char)FEND) return;
data->concat(character);
if (character == (char)FEND && data->length() > 3) {
bool isDataFrame = false;
const String& frame = decodeKISS(*data, isDataFrame);
if (isDataFrame) {
if (bufferIndex != -1) {
Utils::print("<--- Got from TNC : ");
Utils::println(frame);
}
String sender = frame.substring(0,frame.indexOf(">"));
if (Config.tnc.acceptOwn || sender != Config.callsign) {
if (Config.loramodule.txActive) STATION_Utils::addToOutputPacketBuffer(frame);
if (Config.tnc.aprsBridgeActive && Config.aprs_is.active && passcodeValid && aprsIsClient.connected()) APRS_IS_Utils::upload(frame);
} else {
Utils::println("Ignored own frame from KISS");
}
}
data->clear();
}
if (data->length() > 255) {
data->clear();
}
}
void readFromClients() {
for (int i = 0; i < MAX_CLIENTS; i++) {
auto client = clients[i];
if (client != nullptr) {
if (client->connected()) {
while (client->available() > 0) {
char character = client->read();
handleInputData(character, 2 + i);
}
} else {
delete client;
clients[i] = nullptr;
}
}
}
}
void readFromSerial() {
while (Serial.available() > 0) {
char character = Serial.read();
handleInputData(character, -1);
}
}
void sendToClients(const String& packet, bool stripBytes) {
String cleanPacket = stripBytes ? packet.substring(3): packet;
const String kissEncoded = encodeKISS(cleanPacket);
for (int i = 0; i < MAX_CLIENTS; i++) {
auto client = clients[i];
if (client != nullptr) {
if (client->connected()) {
client->print(kissEncoded);
client->flush();
} else {
delete client;
clients[i] = nullptr;
}
}
}
Utils::print("---> Sent to TNC : ");
Utils::println(cleanPacket);
}
void sendToSerial(const String& packet, bool stripBytes) {
String cleanPacket = stripBytes ? packet.substring(3): packet;
Serial.print(encodeKISS(cleanPacket));
Serial.flush();
}
void loop() {
if (Config.digi.ecoMode == 0) {
if (Config.tnc.enableServer) {
checkNewClients();
readFromClients();
}
if (Config.tnc.enableSerial) {
readFromSerial();
}
}
}
}

View File

@@ -1,21 +1,44 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <APRSPacketLib.h>
#include <TinyGPS++.h>
#include <WiFi.h>
#include "telemetry_utils.h"
#include "configuration.h"
#include "station_utils.h"
#include "battery_utils.h"
#include "aprs_is_utils.h"
#include "board_pinout.h"
#include "syslog_utils.h"
#include "pins_config.h"
#include "wifi_utils.h"
#include "A7670_utils.h"
#include "lora_utils.h"
#include "wifi_utils.h"
#include "gps_utils.h"
#include "bme_utils.h"
#include "wx_utils.h"
#include "display.h"
#include "utils.h"
extern WiFiClient espClient;
#define DAY_MS (24UL * 60UL * 60UL * 1000UL)
extern Configuration Config;
extern TinyGPSPlus gps;
extern String versionDate;
extern bool statusAfterBoot;
extern String firstLine;
extern String secondLine;
extern String thirdLine;
@@ -23,290 +46,424 @@ extern String fourthLine;
extern String fifthLine;
extern String sixthLine;
extern String seventhLine;
extern uint32_t lastBeaconTx;
extern uint32_t lastScreenOn;
extern bool beaconUpdate;
extern int stationMode;
extern String iGateBeaconPacket;
extern String iGateLoRaBeaconPacket;
extern std::vector<String> lastHeardStation;
extern int rssi;
extern float snr;
extern int freqError;
extern String distance;
extern uint32_t lastWiFiCheck;
extern bool WiFiConnect;
extern bool WiFiConnected;
extern int wxModuleType;
extern bool backupDigiMode;
extern bool shouldSleepLowVoltage;
extern bool transmitFlag;
extern bool passcodeValid;
extern std::vector<LastHeardStation> lastHeardStations;
bool statusAfterBoot = true;
bool sendStartTelemetry = true;
bool beaconUpdate = false;
uint32_t lastBeaconTx = 0;
uint32_t lastScreenOn = millis();
uint32_t lastStatusTx = 0;
bool stationCallsignIsValid = false;
String beaconPacket;
String secondaryBeaconPacket;
namespace Utils {
void processStatus() {
String status = Config.callsign + ">APLRG1,WIDE1-1";
if (stationMode==1 || stationMode==2 || (stationMode==5 && WiFi.status() == WL_CONNECTED)) {
String status = APRSPacketLib::generateBasePacket(Config.callsign, "APLRG1", Config.beacon.path);
if (WiFi.status() == WL_CONNECTED && Config.aprs_is.active && Config.beacon.sendViaAPRSIS) {
delay(1000);
status += ",qAC:>https://github.com/richonguzman/LoRa_APRS_iGate " + versionDate;
status.concat(",qAC:>");
status.concat(Config.beacon.statusPacket);
APRS_IS_Utils::upload(status);
SYSLOG_Utils::log("APRSIS Tx", status,0,0,0);
} else {
delay(5000);
status += ":>https://github.com/richonguzman/LoRa_APRS_iGate " + versionDate;
if (stationMode==4) {
LoRa_Utils::changeFreqTx();
}
LoRa_Utils::sendNewPacket("APRS", status);
if (stationMode==4) {
LoRa_Utils::changeFreqRx();
}
SYSLOG_Utils::log(2, status, 0, 0.0, 0); // APRSIS TX
}
if (statusAfterBoot && !Config.beacon.sendViaAPRSIS && Config.beacon.sendViaRF) {
status.concat(":>");
status.concat(Config.beacon.statusPacket);
STATION_Utils::addToOutputPacketBuffer(status, true); // treated also as beacon on Tx Freq
}
statusAfterBoot = false;
lastStatusTx = millis();
}
void checkStatusInterval() {
if (lastStatusTx == 0 || millis() - lastStatusTx > DAY_MS) statusAfterBoot = true;
}
String getLocalIP() {
if (!WiFiConnected) {
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) {
return "- BACKUP DIGI MODE -";
} else {
return "IP : " + String(WiFi.localIP()[0]) + "." + String(WiFi.localIP()[1]) + "." + String(WiFi.localIP()[2]) + "." + String(WiFi.localIP()[3]);
}
}
}
void setupDisplay() {
setup_display();
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
digitalWrite(internalLedPin,HIGH);
if (Config.digi.ecoMode != 1) displaySetup();
#ifdef INTERNAL_LED_PIN
digitalWrite(INTERNAL_LED_PIN,HIGH);
#endif
Serial.println("\nStarting Station: " + Config.callsign + " Version: " + versionDate);
show_display(" LoRa APRS", "", " ( iGATE & DIGI )", "", "", "Richonguzman / CA2RXU", " " + versionDate, 4000);
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
digitalWrite(internalLedPin,LOW);
Serial.print("(DigiEcoMode: ");
if (Config.digi.ecoMode == 0) {
Serial.println("OFF)");
} else if (Config.digi.ecoMode == 1) {
Serial.println("ON)");
} else {
Serial.println("ON / Only Serial Output)");
}
displayShow(" LoRa APRS", "", "", " ( iGATE & DIGI )", "", "" , " CA2RXU " + versionDate, 4000);
#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() {
fourthLine = "Stations (" + String(Config.rememberStationTime) + "min) = ";
if (lastHeardStation.size() < 10) {
fourthLine += " ";
}
fourthLine += String(lastHeardStation.size());
void showActiveStations() {
char buffer[30]; // Adjust size as needed
sprintf(buffer, "Stations (%dmin) = %2d", Config.rememberStationTime, lastHeardStations.size());
fourthLine = buffer;
}
void checkBeaconInterval() {
uint32_t lastTx = millis() - lastBeaconTx;
String beaconPacket, secondaryBeaconPacket;
if (lastTx >= Config.beaconInterval*60*1000) {
beaconUpdate = true;
if (lastBeaconTx == 0 || lastTx >= Config.beacon.interval * 60 * 1000) {
beaconUpdate = true;
}
#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;
}
#endif
if (beaconUpdate) {
display_toggle(true);
Serial.println("---- Sending iGate Beacon ----");
if (!Config.display.alwaysOn && Config.display.timeout != 0) displayToggle(true);
if (sendStartTelemetry &&
Config.battery.sendVoltageAsTelemetry &&
!Config.wxsensor.active &&
(Config.battery.sendInternalVoltage || Config.battery.sendExternalVoltage) &&
(lastBeaconTx > 0)) {
TELEMETRY_Utils::sendEquationsUnitsParameters();
}
STATION_Utils::deleteNotHeard();
activeStations();
if (Config.bme.active) {
String sensorData = BME_Utils::readDataSensor();
beaconPacket = iGateBeaconPacket.substring(0,iGateBeaconPacket.indexOf(":=")+20) + "_" + sensorData + iGateBeaconPacket.substring(iGateBeaconPacket.indexOf(":=")+21) + " + WX";
if (Config.igateSendsLoRaBeacons && stationMode!=1) {
secondaryBeaconPacket = iGateLoRaBeaconPacket + sensorData + Config.iGateComment + " + WX";
showActiveStations();
beaconPacket = iGateBeaconPacket;
secondaryBeaconPacket = iGateLoRaBeaconPacket;
#ifdef HAS_GPS
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) {
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;
}
}
} else {
beaconPacket = iGateBeaconPacket;
if (Config.igateSendsLoRaBeacons && stationMode!=1) {
secondaryBeaconPacket = iGateLoRaBeaconPacket + Config.iGateComment;
}
}
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2)
if (Config.sendBatteryVoltage) {
beaconPacket += " (Batt=" + String(BATTERY_Utils::checkBattery(),2) + "V)";
}
#endif
if (Config.externalVoltageMeasurement) {
beaconPacket += " (Ext V=" + String(BATTERY_Utils::checkExternalVoltage(),2) + "V)";
if (Config.wxsensor.active) {
String sensorData = (wxModuleType == 0) ? ".../...g...t..." : WX_Utils::readDataSensor();
beaconPacket += sensorData;
secondaryBeaconPacket += sensorData;
}
if (stationMode==1 || stationMode==2) {
thirdLine = getLocalIP();
if (!Config.bme.active) {
fifthLine = "";
}
sixthLine = "";
show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, "SENDING iGate BEACON", 1000);
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2)
if (Config.sendBatteryVoltage) {
sixthLine = " (Batt=" + String(BATTERY_Utils::checkBattery(),2) + "V)";
}
#endif
if (Config.externalVoltageMeasurement) {
sixthLine = " (Ext V=" + String(BATTERY_Utils::checkExternalVoltage(),2) + "V)";
}
seventhLine = " listening...";
APRS_IS_Utils::upload(beaconPacket);
if (Config.igateSendsLoRaBeacons && stationMode==2) {
LoRa_Utils::sendNewPacket("APRS", secondaryBeaconPacket);
}
show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
} else if (stationMode==3 || stationMode==4) {
String Rx = String(Config.loramodule.digirepeaterRxFreq);
String Tx = String(Config.loramodule.digirepeaterTxFreq);
if (stationMode==3) {
secondLine = "Rx:" + String(Tx.substring(0,3)) + "." + String(Tx.substring(3,6));
} else {
secondLine = "Rx:" + String(Rx.substring(0,3)) + "." + String(Rx.substring(3,6));
}
secondLine += " Tx:" + String(Tx.substring(0,3)) + "." + String(Tx.substring(3,6));
thirdLine = "<< DigiRepeater >>";
fifthLine = "";
sixthLine = "";
show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, "SENDING iGate BEACON", 0);
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2)
if (Config.sendBatteryVoltage) {
sixthLine = " (Batt=" + String(BATTERY_Utils::checkBattery(),2) + "V)";
}
#endif
if (Config.externalVoltageMeasurement) {
sixthLine = " (Ext V=" + String(BATTERY_Utils::checkExternalVoltage(),2) + "V)";
}
seventhLine = " listening...";
if (stationMode==4) {
LoRa_Utils::changeFreqTx();
}
LoRa_Utils::sendNewPacket("APRS", beaconPacket);
if (stationMode==4) {
LoRa_Utils::changeFreqRx();
}
} else if (stationMode==5) {
if (!Config.bme.active) {
fifthLine = "";
}
sixthLine = "";
if (WiFi.status() == WL_CONNECTED && espClient.connected()) {
APRS_IS_Utils::checkStatus();
thirdLine = getLocalIP();
show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, "SENDING iGate BEACON", 1000);
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2)
if (Config.sendBatteryVoltage) {
sixthLine = " (Batt=" + String(BATTERY_Utils::checkBattery(),2) + "V)";
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) {
float internalVoltage = BATTERY_Utils::checkInternalVoltage();
if (Config.battery.monitorInternalVoltage && internalVoltage < Config.battery.internalSleepVoltage) {
beaconPacket += " **IntBatWarning:SLEEP**";
secondaryBeaconPacket += " **IntBatWarning:SLEEP**";
shouldSleepLowVoltage = true;
}
#endif
if (Config.externalVoltageMeasurement) {
sixthLine = " (Ext V=" + String(BATTERY_Utils::checkExternalVoltage(),2) + "V)";
if (Config.battery.sendInternalVoltage) {
char internalVoltageInfo[10]; // Enough to hold "xx.xxV\0"
snprintf(internalVoltageInfo, sizeof(internalVoltageInfo), "%.2fV", internalVoltage);
char sixthLineBuffer[25]; // Enough to hold " (Batt=xx.xxV)"
snprintf(sixthLineBuffer, sizeof(sixthLineBuffer), " (Batt=%s)", internalVoltageInfo);
sixthLine = sixthLineBuffer;
if (!Config.battery.sendVoltageAsTelemetry) {
beaconPacket += " Batt=";
beaconPacket += internalVoltageInfo;
secondaryBeaconPacket += " Batt=";
secondaryBeaconPacket += internalVoltageInfo;
}
}
seventhLine = " listening...";
}
#endif
#ifndef HELTEC_WP_V1
if (Config.battery.sendExternalVoltage || Config.battery.monitorExternalVoltage) {
float externalVoltage = BATTERY_Utils::checkExternalVoltage();
if (Config.battery.monitorExternalVoltage && externalVoltage < Config.battery.externalSleepVoltage) {
beaconPacket += " **ExtBatWarning:SLEEP**";
secondaryBeaconPacket += " **ExtBatWarning:SLEEP**";
shouldSleepLowVoltage = true;
}
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;
if (!Config.battery.sendVoltageAsTelemetry) {
beaconPacket += " Ext=";
beaconPacket += externalVoltageInfo;
secondaryBeaconPacket += " Ext=";
secondaryBeaconPacket += externalVoltageInfo;
}
}
}
#endif
if (Config.battery.sendVoltageAsTelemetry && !Config.wxsensor.active && (Config.battery.sendInternalVoltage || Config.battery.sendExternalVoltage)){
String encodedTelemetry = TELEMETRY_Utils::generateEncodedTelemetry();
beaconPacket += encodedTelemetry;
secondaryBeaconPacket += encodedTelemetry;
}
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...";
#ifdef HAS_A7670
A7670_Utils::uploadToAPRSIS(beaconPacket);
#else
APRS_IS_Utils::upload(beaconPacket);
if (Config.igateSendsLoRaBeacons) {
LoRa_Utils::sendNewPacket("APRS", secondaryBeaconPacket);
}
show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
} else {
show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, "SENDING iGate BEACON", 0);
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2)
if (Config.sendBatteryVoltage) {
sixthLine = " (Batt=" + String(BATTERY_Utils::checkBattery(),2) + "V)";
}
#endif
if (Config.externalVoltageMeasurement) {
sixthLine = " (Ext V=" + String(BATTERY_Utils::checkExternalVoltage(),2) + "V)";
}
seventhLine = " listening...";
LoRa_Utils::sendNewPacket("APRS", beaconPacket);
}
#endif
if (Config.syslog.logBeaconOverTCPIP) SYSLOG_Utils::log(1, "tcp" + beaconPacket, 0, 0.0, 0); // APRSIS TX
}
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, true);
}
lastBeaconTx = millis();
lastScreenOn = millis();
beaconUpdate = false;
}
if (statusAfterBoot) {
processStatus();
}
checkStatusInterval();
if (statusAfterBoot && Config.beacon.statusActive && !Config.beacon.statusPacket.isEmpty()) processStatus();
}
void checkDisplayInterval() {
uint32_t lastDisplayTime = millis() - lastScreenOn;
if (!Config.display.alwaysOn) {
if (lastDisplayTime >= Config.display.timeout*1000) {
display_toggle(false);
}
if (!Config.display.alwaysOn && lastDisplayTime >= Config.display.timeout * 1000) {
displayToggle(false);
}
}
void checkWiFiInterval() {
uint32_t WiFiCheck = millis() - lastWiFiCheck;
if (WiFi.status() != WL_CONNECTED && WiFiCheck >= 15*60*1000) {
WiFiConnect = true;
}
if (WiFiConnect) {
Serial.println("\nConnecting to WiFi ...");
WIFI_Utils::startWiFi();
lastWiFiCheck = millis();
WiFiConnect = false;
void validateFreqs() {
if (Config.loramodule.txFreq != Config.loramodule.rxFreq && abs(Config.loramodule.txFreq - Config.loramodule.rxFreq) < 125000) {
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 validateDigiFreqs() {
if (stationMode==4) {
if (abs(Config.loramodule.digirepeaterTxFreq - Config.loramodule.digirepeaterRxFreq) < 125000) {
Serial.println("Tx Freq less than 125kHz from Rx Freq ---> NOT VALID");
show_display("Tx Freq is less than ", "125kHz from Rx Freq", "device will autofix", "and then reboot", 1000);
Config.loramodule.digirepeaterTxFreq = Config.loramodule.digirepeaterRxFreq; // Inform about that but then change the digirepeaterTxFreq to digirepeaterRxFreq and reset the device
Config.writeFile();
void typeOfPacket(const String& packet, 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
fifthLine = "LoRa Rx ----> LoRa Tx";
break;
}
int firstColonIndex = packet.indexOf(":");
char nextChar = packet[firstColonIndex + 1];
String sender = packet.substring(0,packet.indexOf(">"));
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 += "> ??????????";
}
if (nextChar != '!' && nextChar != '=' && nextChar != '@') { // Common assignment for non-GPS cases
seventhLine = "RSSI:";
seventhLine += String(rssi);
seventhLine += "dBm SNR: ";
seventhLine += String(snr);
seventhLine += "dBm";
}
}
void print(const String& text) {
if (!Config.tnc.enableSerial) {
Serial.print(text);
}
}
void println(const String& text) {
if (!Config.tnc.enableSerial) {
Serial.println(text);
}
}
void checkRebootMode() {
if (Config.rebootMode && Config.rebootModeTime > 0) {
Serial.println("(Reboot Time Set to " + String(Config.rebootModeTime) + " hours)");
}
}
void checkRebootTime() {
if (Config.rebootMode && Config.rebootModeTime > 0) {
if (millis() > Config.rebootModeTime * 60 * 60 * 1000) {
Serial.println("\n*** Automatic Reboot Time Restart! ****\n");
ESP.restart();
}
}
}
void typeOfPacket(String packet, String packetType) {
String sender;
if (stationMode==1 || stationMode==2 || (stationMode==5 && WiFi.status()==WL_CONNECTED)) {
if (packetType == "LoRa-APRS") {
fifthLine = "LoRa Rx ----> APRS-IS";
} else if (packetType == "APRS-LoRa") {
fifthLine = "APRS-IS ----> LoRa Tx";
void checkSleepByLowBatteryVoltage(uint8_t mode) {
if (shouldSleepLowVoltage) {
if (mode == 0) { // at startup
delay(3000);
}
sender = packet.substring(0,packet.indexOf(">"));
} else {
fifthLine = "LoRa Rx ----> LoRa Tx";
sender = packet.substring(3,packet.indexOf(">"));
Serial.println("\n\n*** Sleeping Low Battey Voltage ***\n\n");
esp_sleep_enable_timer_wakeup(30 * 60 * 1000000); // sleep 30 min
if (mode == 1) { // low voltage detected after a while
displayToggle(false);
}
#ifdef VEXT_CTRL
#ifndef HELTEC_WSL_V3
digitalWrite(VEXT_CTRL, LOW);
#endif
#endif
LoRa_Utils::sleepRadio();
transmitFlag = true;
delay(100);
esp_deep_sleep_start();
}
for (int i=sender.length();i<9;i++) {
sender += " ";
}
if (packet.indexOf("::") >= 10) {
if (packetType == "APRS-LoRa") {
String addresseeAndMessage = packet.substring(packet.indexOf("::")+2);
String addressee = addresseeAndMessage.substring(0, addresseeAndMessage.indexOf(":"));
addressee.trim();
sixthLine = sender + " > " + addressee;
} else {
sixthLine = sender + "> MESSAGE";
}
bool callsignIsValid(const String& callsign) {
if (callsign == "WLNK-1") return true;
String cleanCallsign;
int hypenCallsignIndex = callsign.indexOf("-");
if (hypenCallsignIndex > 0) { // SSID Validation
cleanCallsign = callsign.substring(0, hypenCallsignIndex);
String ssid = callsign.substring(hypenCallsignIndex + 1);
if (ssid.indexOf("-") != -1 || ssid.length() > 2) return false;
if (ssid.length() == 2 && ssid[0] == '0') return false;
for (int i = 0; i < ssid.length(); i++) {
if (!isAlphaNumeric(ssid[i])) return false;
}
seventhLine = "RSSI:" + String(rssi) + "dBm SNR: " + String(snr) + "dBm";
} else if (packet.indexOf(":>") >= 10) {
sixthLine = sender + "> NEW STATUS";
seventhLine = "RSSI:" + String(rssi) + "dBm SNR: " + String(snr) + "dBm";
} else if (packet.indexOf(":!") >= 10 || packet.indexOf(":=") >= 10) {
sixthLine = sender + "> GPS BEACON";
GPS_Utils::getDistance(packet);
seventhLine = "RSSI:" + String(rssi) + "dBm";
if (rssi <= -100) {
seventhLine += " ";
} else {
seventhLine += " ";
}
if (distance.indexOf(".") == 1) {
seventhLine += " ";
}
seventhLine += "D:" + distance + "km";
} else if (packet.indexOf(":T#") >= 10 && packet.indexOf(":=/") == -1) {
sixthLine = sender + "> TELEMETRY";
seventhLine = "RSSI:" + String(rssi) + "dBm SNR: " + String(snr) + "dBm";
} else if (packet.indexOf(":`") >= 10) {
sixthLine = sender + "> MIC-E";
seventhLine = "RSSI:" + String(rssi) + "dBm SNR: " + String(snr) + "dBm";
} else if (packet.indexOf(":;") >= 10) {
sixthLine = sender + "> OBJECT";
seventhLine = "RSSI:" + String(rssi) + "dBm SNR: " + String(snr) + "dBm";
} else {
sixthLine = sender + "> ??????????";
seventhLine = "RSSI:" + String(rssi) + "dBm SNR: " + String(snr) + "dBm";
cleanCallsign = callsign;
}
if (cleanCallsign.length() < 4 || cleanCallsign.length() > 6) return false;
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;
}
}
return true;
}
void startupDelay() {
if (Config.startupDelay > 0) {
displayShow("", " STARTUP DELAY ...", "", "", 0);
delay(Config.startupDelay * 60 * 1000);
}
}

View File

@@ -1,24 +0,0 @@
#ifndef UTILS_H_
#define UTILS_H_
#include <Arduino.h>
namespace Utils {
void processStatus();
String getLocalIP();
void setupDisplay();
void activeStations();
void checkBeaconInterval();
void checkDisplayInterval();
void checkWiFiInterval();
void validateDigiFreqs();
void typeOfPacket(String packet, String packetType);
void onOTAStart();
void onOTAProgress(size_t current, size_t final);
void onOTAEnd(bool success);
}
#endif

View File

@@ -1,8 +1,32 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <ArduinoJson.h>
#include "configuration.h"
#include "ota_utils.h"
#include "web_utils.h"
#include "display.h"
#include "utils.h"
extern Configuration Config;
extern Configuration Config;
extern uint32_t lastBeaconTx;
extern std::vector<ReceivedPacket> receivedPackets;
extern const char web_index_html[] asm("_binary_data_embed_index_html_gz_start");
extern const char web_index_html_end[] asm("_binary_data_embed_index_html_gz_end");
@@ -24,15 +48,16 @@ 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;
// 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;
namespace WEB_Utils {
AsyncWebServer server(80);
void loop() {
}
void handleNotFound(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(404, "text/plain", "Not found");
response->addHeader("Cache-Control", "max-age=3600");
@@ -44,12 +69,24 @@ namespace WEB_Utils {
}
void handleHome(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", (const uint8_t*)web_index_html, web_index_html_len);
if(Config.webadmin.active && !request->authenticate(Config.webadmin.username.c_str(), Config.webadmin.password.c_str()))
return request->requestAuthentication();
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", (const uint8_t*)web_index_html, web_index_html_len);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
}
void handleFavicon(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(200, "image/x-icon", (const uint8_t*)favicon_data, favicon_data_len);
response->addHeader("Content-Encoding", "gzip");
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;
@@ -60,135 +97,285 @@ namespace WEB_Utils {
request->send(200, "application/json", fileContent);
}
void handleWriteConfiguration(AsyncWebServerRequest *request) {
Serial.println("Got new config from www");
void handleReceivedPackets(AsyncWebServerRequest *request) {
StaticJsonDocument<1536> data;
int networks = request->getParam("wifi.APs", true)->value().toInt();
for (int i = 0; i < receivedPackets.size(); i++) {
data[i]["rxTime"] = receivedPackets[i].rxTime;
data[i]["packet"] = receivedPackets[i].packet;
data[i]["RSSI"] = receivedPackets[i].RSSI;
data[i]["SNR"] = receivedPackets[i].SNR;
}
String buffer;
serializeJson(data, buffer);
request->send(200, "application/json", buffer);
}
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.latitude = request->getParam("wifi.AP." + String(i) + ".latitude", true)->value().toDouble();
wifiap.longitude = request->getParam("wifi.AP." + String(i) + ".longitude", true)->value().toDouble();
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.stationMode = request->getParam("stationMode", true)->value().toInt();
Config.iGateComment = request->getParam("iGateComment", true)->value();
Config.startupDelay = getParamIntSafe("startupDelay", Config.startupDelay);
Config.callsign = getParamStringSafe("callsign", Config.callsign);
Config.tacticalCallsign = getParamStringSafe("tacticalCallsign", Config.tacticalCallsign);
Config.wifiAutoAP.password = request->getParam("wifi.autoAP.password", true)->value();
Config.wifiAutoAP.powerOff = request->getParam("wifi.autoAP.powerOff", true)->value().toInt();
Config.wifiAutoAP.password = getParamStringSafe("wifi.autoAP.password", Config.wifiAutoAP.password);
Config.wifiAutoAP.timeout = getParamIntSafe("wifi.autoAP.timeout", Config.wifiAutoAP.timeout);
Config.digi.comment = request->getParam("digi.comment", true)->value();
Config.digi.latitude = request->getParam("digi.latitude", true)->value().toDouble();
Config.digi.longitude = request->getParam("digi.longitude", true)->value().toDouble();
Config.aprs_is.passcode = request->getParam("aprs_is.passcode", true)->value();
Config.aprs_is.server = request->getParam("aprs_is.server", true)->value();
Config.aprs_is.port = request->getParam("aprs_is.port", true)->value().toInt();
Config.aprs_is.reportingDistance = request->getParam("aprs_is.reportingDistance", true)->value().toInt();
Config.loramodule.iGateFreq = request->getParam("lora.iGateFreq", true)->value().toInt();
if (request->hasParam("lora.digirepeaterTxFreq", true)) {
Config.loramodule.digirepeaterTxFreq = request->getParam("lora.digirepeaterTxFreq", true)->value().toInt();
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 = 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);
}
if (request->hasParam("lora.digirepeaterRxFreq", true)) {
Config.loramodule.digirepeaterRxFreq = request->getParam("lora.digirepeaterRxFreq", 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.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 = getParamStringSafe("beacon.statusPacket", Config.beacon.statusPacket);
}
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.beacon.gpsActive = request->hasParam("beacon.gpsActive", true);
Config.beacon.ambiguityLevel = getParamIntSafe("beacon.ambiguityLevel", Config.beacon.ambiguityLevel);
Config.display.alwaysOn = request->hasParam("display.alwaysOn", true);
Config.personalNote = getParamStringSafe("personalNote", Config.personalNote);
Config.blacklist = getParamStringSafe("blacklist", Config.blacklist);
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.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);
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);
if (Config.battery.monitorInternalVoltage) {
Config.battery.internalSleepVoltage = getParamFloatSafe("battery.internalSleepVoltage", Config.battery.internalSleepVoltage);
}
Config.display.turn180 = request->hasParam("display.turn180", true);
Config.battery.sendExternalVoltage = request->hasParam("battery.sendExternalVoltage", true);
if (Config.battery.sendExternalVoltage) {
Config.battery.useExternalI2CSensor = request->hasParam("battery.useExternalI2CSensor", 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 = getParamFloatSafe("battery.externalSleepVoltage", Config.battery.externalSleepVoltage);
}
Config.battery.sendVoltageAsTelemetry = request->hasParam("battery.sendVoltageAsTelemetry", true);
Config.syslog.active = request->hasParam("syslog.active", true);
Config.wxsensor.active = request->hasParam("wxsensor.active", true);
if (Config.wxsensor.active) {
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.bme.active = request->hasParam("bme.active", 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.ota.username = request->getParam("ota.username", true)->value();
Config.ota.password = request->getParam("ota.password", true)->value();
Config.beaconInterval = request->getParam("other.beaconInterval", true)->value().toInt();
Config.igateSendsLoRaBeacons = request->hasParam("other.igateSendsLoRaBeacons", true);
Config.igateRepeatsLoRaPackets = request->hasParam("other.igateRepeatsLoRaPackets", true);
Config.rememberStationTime = request->getParam("other.rememberStationTime", true)->value().toInt();
Config.sendBatteryVoltage = request->hasParam("other.sendBatteryVoltage", true);
Config.externalVoltageMeasurement = request->hasParam("other.externalVoltageMeasurement", true);
if (Config.externalVoltageMeasurement) {
Config.externalVoltagePin = request->getParam("other.externalVoltagePin", true)->value().toInt();
Config.mqtt.active = request->hasParam("mqtt.active", true);
if (Config.mqtt.active) {
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.writeFile();
Config.rebootMode = request->hasParam("other.rebootMode", true);
if (Config.rebootMode) {
Config.rebootModeTime = getParamIntSafe("other.rebootModeTime", Config.rebootModeTime);
}
AsyncWebServerResponse *response = request->beginResponse(302, "text/html", "");
response->addHeader("Location", "/");
request->send(response);
Config.ota.username = getParamStringSafe("ota.username", Config.ota.username);
Config.ota.password = getParamStringSafe("ota.password", Config.ota.password);
ESP.restart();
Config.webadmin.active = request->hasParam("webadmin.active", true);
if (Config.webadmin.active) {
Config.webadmin.username = getParamStringSafe("webadmin.username", Config.webadmin.username);
Config.webadmin.password = getParamStringSafe("webadmin.password", Config.webadmin.password);
}
Config.remoteManagement.managers = getParamStringSafe("remoteManagement.managers", Config.remoteManagement.managers);
Config.remoteManagement.rfOnly = request->hasParam("remoteManagement.rfOnly", true);
Config.ntp.server = getParamStringSafe("ntp.server", Config.ntp.server);
Config.ntp.gmtCorrection = getParamFloatSafe("ntp.gmtCorrection", Config.ntp.gmtCorrection);
Config.rememberStationTime = getParamIntSafe("other.rememberStationTime", Config.rememberStationTime);
bool saveSuccess = Config.writeFile();
if (saveSuccess) {
Serial.println("Configuration saved successfully");
AsyncWebServerResponse *response = request->beginResponse(302, "text/html", "");
response->addHeader("Location", "/?success=1");
request->send(response);
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) {
String type = request->getParam("type", false)->value();
if (type == "send-beacon") {
lastBeaconTx = 0;
request->send(200, "text/plain", "Beacon will be sent in a while");
} else if (type == "reboot") {
displayToggle(false);
ESP.restart();
} else {
request->send(404, "text/plain", "Not Found");
}
}
void handleStyle(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", (const uint8_t*)web_style_css, web_style_css_len);
AsyncWebServerResponse *response = request->beginResponse(200, "text/css", (const uint8_t*)web_style_css, web_style_css_len);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
}
void handleScript(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", (const uint8_t*)web_script_js, web_script_js_len);
AsyncWebServerResponse *response = request->beginResponse(200, "text/javascript", (const uint8_t*)web_script_js, web_script_js_len);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
}
void handleBootstrapStyle(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", (const uint8_t*)web_bootstrap_css, web_bootstrap_css_len);
AsyncWebServerResponse *response = request->beginResponse(200, "text/css", (const uint8_t*)web_bootstrap_css, web_bootstrap_css_len);
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Cache-Control", "max-age=3600");
request->send(response);
}
void handleBootstrapScript(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", (const uint8_t*)web_bootstrap_js, web_bootstrap_js_len);
AsyncWebServerResponse *response = request->beginResponse(200, "text/javascript", (const uint8_t*)web_bootstrap_js, web_bootstrap_js_len);
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Cache-Control", "max-age=3600");
request->send(response);
}
void setup() {
server.on("/", HTTP_GET, handleHome);
server.on("/status", HTTP_GET, handleStatus);
server.on("/configuration.json", HTTP_GET, handleReadConfiguration);
server.on("/configuration.json", HTTP_POST, handleWriteConfiguration);
server.on("/style.css", HTTP_GET, handleStyle);
server.on("/script.js", HTTP_GET, handleScript);
server.on("/bootstrap.css", HTTP_GET, handleBootstrapStyle);
server.on("/bootstrap.js", HTTP_GET, handleBootstrapScript);
if (Config.digi.ecoMode == 0) {
server.on("/", HTTP_GET, handleHome);
server.on("/status", HTTP_GET, handleStatus);
server.on("/received-packets.json", HTTP_GET, handleReceivedPackets);
server.on("/configuration.json", HTTP_GET, handleReadConfiguration);
server.on("/configuration.json", HTTP_POST, handleWriteConfiguration);
server.on("/action", HTTP_POST, handleAction);
server.on("/style.css", HTTP_GET, handleStyle);
server.on("/script.js", HTTP_GET, handleScript);
server.on("/bootstrap.css", HTTP_GET, handleBootstrapStyle);
server.on("/bootstrap.js", HTTP_GET, handleBootstrapScript);
server.on("/favicon.png", HTTP_GET, handleFavicon);
OTA_Utils::setup(&server); // Include OTA Updater for WebServer
OTA_Utils::setup(&server); // Include OTA Updater for WebServer
server.onNotFound(handleNotFound);
server.onNotFound(handleNotFound);
server.begin();
server.begin();
}
}
}

View File

@@ -1,31 +0,0 @@
#ifndef WEB_UTILS_H_
#define WEB_UTILS_H_
#include <ESPAsyncWebServer.h>
#include <ESPmDNS.h>
#include <Arduino.h>
#include <SPIFFS.h>
#include <WiFi.h>
namespace WEB_Utils {
void loop();
void handleNotFound(AsyncWebServerRequest *request);
void handleStatus(AsyncWebServerRequest *request);
void handleHome(AsyncWebServerRequest *request);
//void handleReadConfiguration(AsyncWebServerRequest *request);
//void handleWriteConfiguration(AsyncWebServerRequest *request);
void handleStyle(AsyncWebServerRequest *request);
void handleScript(AsyncWebServerRequest *request);
void handleBootstrapStyle(AsyncWebServerRequest *request);
void handleBootstrapScript(AsyncWebServerRequest *request);
void setup();
}
#endif

View File

@@ -1,30 +1,84 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <WiFi.h>
#include "configuration.h"
#include "pins_config.h"
#include "board_pinout.h"
#include "wifi_utils.h"
#include "display.h"
#include "utils.h"
extern Configuration Config;
extern WiFi_AP *currentWiFi;
extern int myWiFiAPIndex;
extern int myWiFiAPSize;
extern int stationMode;
extern uint32_t previousWiFiMillis;
extern bool WiFiConnected;
extern long WiFiAutoAPTime;
extern bool WiFiAutoAPStarted;
extern Configuration Config;
extern uint8_t myWiFiAPIndex;
extern int myWiFiAPSize;
extern WiFi_AP *currentWiFi;
extern bool backupDigiMode;
extern uint32_t lastServerCheck;
bool WiFiConnected = false;
uint32_t WiFiAutoAPTime = millis();
bool WiFiAutoAPStarted = false;
uint8_t wifiCounter = 0;
uint32_t lastBackupDigiTime = millis();
uint32_t lastWiFiCheck = 0;
namespace WIFI_Utils {
void checkWiFi() {
if ((WiFi.status() != WL_CONNECTED) && ((millis() - previousWiFiMillis) >= 30*1000) && !WiFiAutoAPStarted) {
Serial.print(millis());
Serial.println("Reconnecting to WiFi...");
WiFi.disconnect();
WiFi.reconnect();
previousWiFiMillis = millis();
if (Config.digi.ecoMode != 0) return;
uint32_t currentTime = millis();
if (backupDigiMode) {
if (WiFi.status() != WL_CONNECTED && ((currentTime - lastBackupDigiTime) >= 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 (!backupDigiMode && ((currentTime - lastWiFiCheck) >= 30 * 1000) && !WiFiAutoAPStarted) {
lastWiFiCheck = currentTime;
if (WiFi.status() == WL_CONNECTED) {
if (Config.digi.backupDigiMode && (currentTime - lastServerCheck > 30 * 1000)) {
Serial.println("*** Server Connection LOST → Backup Digi Mode ***");
backupDigiMode = true;
WiFi.disconnect();
lastBackupDigiTime = currentTime;
}
} else {
Serial.println("Reconnecting to WiFi...");
WiFi.disconnect();
WIFI_Utils::startWiFi();
if (Config.digi.backupDigiMode) wifiCounter++;
if (wifiCounter >= 2) {
Serial.println("*** Starting BackUp Digi Mode ***");
backupDigiMode = true;
lastBackupDigiTime = currentTime;
}
}
}
}
@@ -32,7 +86,7 @@ namespace WIFI_Utils {
WiFi.mode(WIFI_MODE_NULL);
WiFi.mode(WIFI_AP);
WiFi.softAP(Config.callsign + " AP", "1234567890");
WiFi.softAP(Config.callsign + "-AP", Config.wifiAutoAP.password);
WiFiAutoAPTime = millis();
WiFiAutoAPStarted = true;
@@ -43,31 +97,31 @@ namespace WIFI_Utils {
if (currentWiFi->ssid == "") {
startAP = true;
} else {
int wifiCounter = 0;
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();
show_display("", "", "Connecting to Wifi:", "", currentWiFi->ssid + " ...", 0);
Serial.print("\nConnecting to WiFi '"); Serial.print(currentWiFi->ssid); Serial.println("' ...");
displayShow("", "Connecting to WiFi:", "", currentWiFi->ssid + " ...", 0);
Serial.print("\nConnecting to WiFi '"); Serial.print(currentWiFi->ssid); Serial.print("' ");
WiFi.begin(currentWiFi->ssid.c_str(), currentWiFi->password.c_str());
while (WiFi.status() != WL_CONNECTED && wifiCounter<myWiFiAPSize) {
while (WiFi.status() != WL_CONNECTED && wifiCounter < myWiFiAPSize) {
delay(500);
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
digitalWrite(internalLedPin,HIGH);
#ifdef INTERNAL_LED_PIN
digitalWrite(INTERNAL_LED_PIN, HIGH);
#endif
Serial.print('.');
delay(500);
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
digitalWrite(internalLedPin,LOW);
#ifdef INTERNAL_LED_PIN
digitalWrite(INTERNAL_LED_PIN, LOW);
#endif
if ((millis() - start) > 10000){
delay(1000);
if(myWiFiAPIndex >= (myWiFiAPSize-1)) {
if (myWiFiAPIndex >= (myWiFiAPSize - 1)) {
myWiFiAPIndex = 0;
if (stationMode==5) {
wifiCounter++;
}
wifiCounter++;
} else {
myWiFiAPIndex++;
}
@@ -75,42 +129,44 @@ namespace WIFI_Utils {
currentWiFi = &Config.wifiAPs[myWiFiAPIndex];
start = millis();
Serial.print("\nConnecting to WiFi '"); Serial.print(currentWiFi->ssid); Serial.println("' ...");
show_display("", "", "Connecting to Wifi:", "", currentWiFi->ssid + " ...", 0);
displayShow("", "Connecting to WiFi:", "", currentWiFi->ssid + " ...", 0);
WiFi.disconnect();
WiFi.begin(currentWiFi->ssid.c_str(), currentWiFi->password.c_str());
}
}
}
#if defined(TTGO_T_LORA32_V2_1) || defined(HELTEC_V2) || defined(HELTEC_V3) || defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_1W_LoRa)
digitalWrite(internalLedPin,LOW);
#ifdef INTERNAL_LED_PIN
digitalWrite(INTERNAL_LED_PIN, LOW);
#endif
if (WiFi.status() == WL_CONNECTED) {
Serial.print("Connected as ");
Serial.println(WiFi.localIP());
show_display("", "", " Connected!!", "" , " loading ...", 1000);
} else if (WiFi.status() != WL_CONNECTED) {
Serial.print("\nConnected as ");
Serial.print(WiFi.localIP());
Serial.print(" / MAC Address: ");
Serial.println(WiFi.macAddress());
displayShow("", " Connected!!", "" , " loading ...", 1000);
} else {
startAP = true;
Serial.println("\nNot connected to WiFi! Starting Auto AP");
show_display("", "", " WiFi Not Connected!", "" , " loading ...", 1000);
displayShow("", " WiFi Not Connected!", "" , " loading ...", 1000);
}
WiFiConnected = !startAP;
if (startAP) {
Serial.println("\nNot connected to WiFi! Starting Auto AP");
show_display("", "", " Starting Auto AP", " Please connect to it " , " loading ...", 1000);
displayShow("", " Starting Auto AP", " Please connect to it " , " loading ...", 1000);
startAutoAP();
}
}
void checkIfAutoAPShouldPowerOff() {
if (WiFiAutoAPStarted && Config.wifiAutoAP.powerOff > 0) {
void checkAutoAPTimeout() {
if (WiFiAutoAPStarted && Config.wifiAutoAP.timeout > 0) {
if (WiFi.softAPgetStationNum() > 0) {
WiFiAutoAPTime = 0;
} else {
if (WiFiAutoAPTime == 0) {
WiFiAutoAPTime = millis();
} else if ((millis() - WiFiAutoAPTime) > Config.wifiAutoAP.powerOff * 60 * 1000) {
} else if ((millis() - WiFiAutoAPTime) > Config.wifiAutoAP.timeout * 60 * 1000) {
Serial.println("Stopping auto AP");
WiFiAutoAPStarted = false;
@@ -123,36 +179,8 @@ namespace WIFI_Utils {
}
void setup() {
if (stationMode==1 || stationMode==2) {
if (stationMode==1) {
Serial.println("stationMode ---> iGate (only Rx)");
} else {
Serial.println("stationMode ---> iGate (Rx + Tx)");
}
startWiFi();
btStop();
} else if (stationMode==3 || stationMode==4) {
if (stationMode==3) {
Serial.println("stationMode ---> DigiRepeater (Rx freq == Tx freq)");
} else {
Serial.println("stationMode ---> DigiRepeater (Rx freq != Tx freq)");
}
startAutoAP();
btStop();
} else if (stationMode==5) {
Serial.println("stationMode ---> iGate when Wifi/APRS available (DigiRepeater when not)");
startWiFi();
btStop();
} else {
Serial.println("stationMode ---> NOT VALID");
show_display("------- ERROR -------", "stationMode Not Valid", "device will autofix", "and then reboot", 1000);
Config.stationMode = 1; // Inform about that but then change the station mode to 1 and reset the device
Config.writeFile();
ESP.restart();
}
if (Config.digi.ecoMode == 0) startWiFi();
btStop();
}
}

View File

@@ -1,17 +0,0 @@
#ifndef WIFI_UTILS_H_
#define WIFI_UTILS_H_
#include <Arduino.h>
namespace WIFI_Utils {
void checkWiFi();
void startAutoAP();
void startWiFi();
void checkIfAutoAPShouldPowerOff();
void setup();
}
#endif

337
src/wx_utils.cpp Normal file
View File

@@ -0,0 +1,337 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <TinyGPS++.h>
#ifdef LIGHTGATEWAY_PLUS_1_0
#include "Adafruit_SHTC3.h"
#endif
#include "configuration.h"
#include "board_pinout.h"
#include "wx_utils.h"
#include "display.h"
#define SEALEVELPRESSURE_HPA (1013.25)
#define CORRECTION_FACTOR (8.2296) // for meters
extern Configuration Config;
extern String fifthLine;
#ifdef HAS_GPS
extern TinyGPSPlus gps;
#endif
int wxModuleType = 0;
uint8_t wxModuleAddress = 0x00;
float newHum, newTemp, newPress, newGas;
Adafruit_BME280 bme280;
Adafruit_AHTX0 aht20;
#if defined(HELTEC_V3) || defined(HELTEC_V3_2)
Adafruit_BMP280 bmp280(&Wire1);
Adafruit_Si7021 si7021 = Adafruit_Si7021();
#else
Adafruit_BMP280 bmp280;
Adafruit_BME680 bme680;
Adafruit_Si7021 si7021 = Adafruit_Si7021();
#endif
#ifdef LIGHTGATEWAY_PLUS_1_0
Adafruit_SHTC3 shtc3 = Adafruit_SHTC3();
#endif
namespace WX_Utils {
void getWxModuleAddres() {
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);
#ifdef LIGHTGATEWAY_PLUS_1_0
Wire.write(0x35);
Wire.write(0x17);
#endif
err = Wire.endTransmission();
#endif
delay(5);
if (err == 0) {
//Serial.println(addr); //this shows any connected board to I2C
if (addr == 0x76 || addr == 0x77) { // BME or BMP
wxModuleAddress = addr;
return;
} else if (addr == 0x40) { // Si7011
wxModuleAddress = addr;
return;
} else if (addr == 0x70) { // SHTC3
wxModuleAddress = addr;
return;
}
}
}
}
void setup() {
if (Config.wxsensor.active) {
getWxModuleAddres();
if (wxModuleAddress != 0x00) {
bool wxModuleFound = false;
if (wxModuleAddress == 0x76 || wxModuleAddress == 0x77) {
#if defined(HELTEC_V3) || defined(HELTEC_V3_2) || defined(HELTEC_WSL_V3) || defined(HELTEC_WSL_V3_DISPLAY)
if (bme280.begin(wxModuleAddress, &Wire1)) {
Serial.println("BME280 sensor found");
wxModuleType = 1;
wxModuleFound = true;
}
#else
if (bme280.begin(wxModuleAddress)) {
Serial.println("BME280 sensor found");
wxModuleType = 1;
wxModuleFound = true;
}
if (!wxModuleFound) {
if (bme680.begin(wxModuleAddress)) {
Serial.println("BME680 sensor found");
wxModuleType = 3;
wxModuleFound = true;
}
}
#endif
if (!wxModuleFound) {
if (bmp280.begin(wxModuleAddress)) {
Serial.println("BMP280 sensor found");
wxModuleType = 2;
wxModuleFound = true;
if (aht20.begin()) {
Serial.println("AHT20 sensor found");
if (wxModuleType == 2) wxModuleType = 6;
}
}
}
} else if (wxModuleAddress == 0x40 && Config.battery.useExternalI2CSensor == false) {
if(si7021.begin()) {
Serial.println("Si7021 sensor found");
wxModuleType = 4;
wxModuleFound = true;
}
}
#ifdef LIGHTGATEWAY_PLUS_1_0
else if (wxModuleAddress == 0x70) {
if (shtc3.begin()) {
Serial.println("SHTC3 sensor found");
wxModuleType = 5;
wxModuleFound = true;
}
}
#endif
if (!wxModuleFound) {
displayShow("ERROR", "", "BME/BMP/Si7021/SHTC3 sensor active", "but no sensor found...", 2000);
Serial.println("BME/BMP/Si7021/SHTC3 sensor Active in config but not found! Check Wiring");
} else {
switch (wxModuleType) {
case 1:
bme280.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::FILTER_OFF
);
Serial.println("BME280 Module init done!");
break;
case 2:
bmp280.setSampling(Adafruit_BMP280::MODE_FORCED,
Adafruit_BMP280::SAMPLING_X1,
Adafruit_BMP280::SAMPLING_X1,
Adafruit_BMP280::FILTER_OFF
);
Serial.println("BMP280 Module init done!");
break;
case 3:
#if !defined(HELTEC_V3) && !defined(HELTEC_V3_2)
bme680.setTemperatureOversampling(BME680_OS_1X);
bme680.setHumidityOversampling(BME680_OS_1X);
bme680.setPressureOversampling(BME680_OS_1X);
bme680.setIIRFilterSize(BME680_FILTER_SIZE_0);
Serial.println("BMP680 Module init done!");
#endif
break;
}
}
}
}
}
String generateTempString(const float sensorTemp) {
String strTemp = String((int)sensorTemp);
switch (strTemp.length()) {
case 1:
return "00" + strTemp;
case 2:
return "0" + strTemp;
case 3:
return strTemp;
default:
return "-999";
}
}
String generateHumString(const float sensorHum) {
String strHum = String((int)sensorHum);
switch (strHum.length()) {
case 1:
return "0" + strHum;
case 2:
return strHum;
case 3:
if ((int)sensorHum == 100) {
return "00";
} else {
return "-99";
}
default:
return "-99";
}
}
String generatePresString(const float sensorPres) {
String strPress = String((int)sensorPres);
String decPress = String(int((sensorPres - int(sensorPres)) * 10));
switch (strPress.length()) {
case 1:
return "000" + strPress + decPress;
case 2:
return "00" + strPress + decPress;
case 3:
return "0" + strPress + decPress;
case 4:
return strPress + decPress;
case 5:
return strPress;
default:
return "-99999";
}
}
float getAltitudeCorrection() {
#ifdef HAS_GPS
return Config.beacon.gpsActive ? gps.altitude.meters() : Config.wxsensor.heightCorrection;
#else
return Config.wxsensor.heightCorrection;
#endif
}
String readDataSensor() {
switch (wxModuleType) {
case 1: // BME280
bme280.takeForcedMeasurement();
newTemp = bme280.readTemperature();
newPress = (bme280.readPressure() / 100.0F);
newHum = bme280.readHumidity();
break;
case 2: // BMP280
bmp280.takeForcedMeasurement();
newTemp = bmp280.readTemperature();
newPress = (bmp280.readPressure() / 100.0F);
newHum = 0;
break;
case 3: // BME680
#if !defined(HELTEC_V3) && !defined(HELTEC_V3_2)
bme680.performReading();
delay(50);
if (bme680.endReading()) {
newTemp = bme680.temperature;
newPress = (bme680.pressure / 100.0F);
newHum = bme680.humidity;
newGas = bme680.gas_resistance / 1000.0; // in Kilo ohms
}
#endif
break;
case 4: // Si7021
newTemp = si7021.readTemperature();
newHum = si7021.readHumidity();
newPress = 0;
break;
case 5: // SHTC3
{
#ifdef LIGHTGATEWAY_PLUS_1_0
sensors_event_t humidity, temp;
shtc3.getEvent(&humidity, &temp);
newTemp = temp.temperature;
newHum = humidity.relative_humidity;
newPress = 0;
#endif
}
break;
case 6: // BMP280 + AHT20
{
bmp280.takeForcedMeasurement();
newTemp = bmp280.readTemperature();
newPress = (bmp280.readPressure() / 100.0F);
sensors_event_t humidity, temp;
aht20.getEvent(&humidity, &temp);
newHum = humidity.relative_humidity;
}
break;
}
if (isnan(newTemp) || isnan(newHum) || isnan(newPress)) {
Serial.println("BME/BMP/Si7021 Module data failed");
fifthLine = "";
return ".../...g...t...";
} else {
String tempStr = generateTempString(((newTemp + Config.wxsensor.temperatureCorrection) * 1.8) + 32);
String humStr;
if (wxModuleType == 1 || wxModuleType == 3 || wxModuleType == 4 || wxModuleType == 5 || wxModuleType == 6) {
humStr = generateHumString(newHum);
} else if (wxModuleType == 2) {
humStr = "..";
}
String presStr = (wxModuleType == 4 || wxModuleType == 5)
? "....."
: generatePresString(newPress + getAltitudeCorrection() / CORRECTION_FACTOR);
fifthLine = "BME-> ";
fifthLine += String(int(newTemp + Config.wxsensor.temperatureCorrection));
fifthLine += "C ";
fifthLine += humStr;
fifthLine += "% ";
fifthLine += presStr.substring(0,4);
fifthLine += "hPa";
String wxPayload = ".../...g...t";
wxPayload += tempStr;
wxPayload += "h";
wxPayload += humStr;
wxPayload += "b";
wxPayload += presStr;
if (wxModuleType == 3) {
wxPayload += "Gas: ";
wxPayload += String(newGas);
wxPayload += "Kohms";
}
return wxPayload;
}
}
}

View File

@@ -1,11 +0,0 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html

View File

@@ -1,4 +1,24 @@
# Copyright (C) 2025 Ricardo Guzman - CA2RXU
#
# This file is part of LoRa APRS iGate.
#
# LoRa APRS iGate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# LoRa APRS iGate is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
import gzip
import os
import datetime
Import("env")
files = [
'data_embed/index.html',
@@ -6,15 +26,34 @@ files = [
'data_embed/style.css',
'data_embed/bootstrap.js',
'data_embed/bootstrap.css',
'data_embed/favicon.png',
]
string_to_find_str = "String"
string_to_find_ver = "versionDate"
with open('src/LoRa_APRS_iGate.cpp', encoding='utf-8') as cpp_file:
for line in cpp_file:
if string_to_find_str in line and string_to_find_ver in line:
start = line.find('"') + 1
end = line.find('"', start)
if start > 0 and end > start:
versionDate = line[start:end]
break
for src in files:
out = src + ".gz"
with open(src, 'rb') as f:
content = f.read()
if src == 'data_embed/index.html':
env_vars = env["BOARD"] + "<br>" + ','.join(env["BUILD_FLAGS"]).replace('-Werror -Wall,', '').replace(',-DELEGANTOTA_USE_ASYNC_WEBSERVER=1', '') + "<br>" + "Version date: " + versionDate
current_date = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') + " UTC"
build_info = f'{env_vars}<br>Build date: {current_date}'.encode()
content = content.replace(b'%BUILD_INFO%', build_info)
with open(out, 'wb') as f:
f.write(gzip.compress(content, compresslevel=9))

View File

@@ -0,0 +1,49 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef BOARD_PINOUT_H_
#define BOARD_PINOUT_H_
// LoRa Radio
#define HAS_SX1268
#define RADIO_HAS_XTAL
#define RADIO_SCLK_PIN 6
#define RADIO_MISO_PIN 4
#define RADIO_MOSI_PIN 7
#define RADIO_CS_PIN 5
#define RADIO_DIO1_PIN 3
#define RADIO_RST_PIN -1
#define RADIO_BUSY_PIN 8
#define RADIO_WAKEUP_PIN RADIO_DIO1_PIN
#define GPIO_WAKEUP_PIN GPIO_NUM_3
// I2C
#define USE_WIRE_WITH_OLED_PINS
// Display
#define HAS_DISPLAY
#undef OLED_SDA
#undef OLED_SCL
#undef OLED_RST
#define OLED_SDA 0
#define OLED_SCL 1
#define OLED_RST -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#endif

View File

@@ -0,0 +1,13 @@
[env:ESP32_C3_OctopusLab_LoRa]
board = esp32-c3-devkitm-1
board_build.mcu = esp32c3
build_flags =
${common.build_flags}
${common.usb_flags}
-D RADIOLIB_EXCLUDE_LR11X0=1
-D RADIOLIB_EXCLUDE_SX127X=1
-D RADIOLIB_EXCLUDE_SX128X=1
-D ESP32_C3_OctopusLab_LoRa
lib_deps =
${common.lib_deps}
${common.display_libs}

View File

@@ -0,0 +1,55 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef BOARD_PINOUT_H_
#define BOARD_PINOUT_H_
// LoRa Radio
#define HAS_SX1268
#define HAS_1W_LORA
#define HAS_TCXO
#define RADIO_SCLK_PIN 18
#define RADIO_MISO_PIN 19
#define RADIO_MOSI_PIN 23
#define RADIO_CS_PIN 5
#define RADIO_RST_PIN 27
#define RADIO_DIO1_PIN 12
#define RADIO_BUSY_PIN 14
#define RADIO_RXEN 32
#define RADIO_TXEN 25
#define RADIO_WAKEUP_PIN RADIO_DIO1_PIN
#define GPIO_WAKEUP_PIN GPIO_SEL_12
// I2C
#define USE_WIRE_WITH_OLED_PINS
// Display
#define HAS_DISPLAY
#undef OLED_SDA
#undef OLED_SCL
#undef OLED_RST
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RST -1 // Reset pin # (or -1 if sharing Arduino reset pin)
// Aditional Config
#define INTERNAL_LED_PIN 2
#endif

View File

@@ -0,0 +1,11 @@
[env:ESP32_DIY_1W_LoRa]
board = esp32dev
build_flags =
${common.build_flags}
-D RADIOLIB_EXCLUDE_LR11X0=1
-D RADIOLIB_EXCLUDE_SX127X=1
-D RADIOLIB_EXCLUDE_SX128X=1
-D ESP32_DIY_1W_LoRa
lib_deps =
${common.lib_deps}
${common.display_libs}

View File

@@ -0,0 +1,55 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef BOARD_PINOUT_H_
#define BOARD_PINOUT_H_
// LoRa Radio
#define HAS_SX1262
#define HAS_1W_LORA
#define HAS_TCXO
#define RADIO_SCLK_PIN 18
#define RADIO_MISO_PIN 19
#define RADIO_MOSI_PIN 23
#define RADIO_CS_PIN 5
#define RADIO_RST_PIN 27
#define RADIO_DIO1_PIN 12
#define RADIO_BUSY_PIN 14
#define RADIO_RXEN 32
#define RADIO_TXEN 25
#define RADIO_WAKEUP_PIN RADIO_DIO1_PIN
#define GPIO_WAKEUP_PIN GPIO_SEL_12
// I2C
#define USE_WIRE_WITH_OLED_PINS
// Display
#define HAS_DISPLAY
#undef OLED_SDA
#undef OLED_SCL
#undef OLED_RST
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RST -1 // Reset pin # (or -1 if sharing Arduino reset pin)
// Aditional Config
#define INTERNAL_LED_PIN 2
#endif

View File

@@ -0,0 +1,11 @@
[env:ESP32_DIY_1W_LoRa_915]
board = esp32dev
build_flags =
${common.build_flags}
-D RADIOLIB_EXCLUDE_LR11X0=1
-D RADIOLIB_EXCLUDE_SX127X=1
-D RADIOLIB_EXCLUDE_SX128X=1
-D ESP32_DIY_1W_LoRa_915
lib_deps =
${common.lib_deps}
${common.display_libs}

View File

@@ -0,0 +1,51 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef BOARD_PINOUT_H_
#define BOARD_PINOUT_H_
// LoRa Radio
#define HAS_LLCC68
#define HAS_1W_LORA
#define RADIO_SCLK_PIN 18
#define RADIO_MISO_PIN 19
#define RADIO_MOSI_PIN 23
#define RADIO_CS_PIN 5
#define RADIO_RST_PIN 27
#define RADIO_DIO1_PIN 12
#define RADIO_BUSY_PIN 14
#define RADIO_RXEN 32
#define RADIO_TXEN 25
#define RADIO_WAKEUP_PIN RADIO_DIO1_PIN
#define GPIO_WAKEUP_PIN GPIO_SEL_12
// I2C
#define USE_WIRE_WITH_OLED_PINS
// Display
#define HAS_DISPLAY
#undef OLED_SDA
#undef OLED_SCL
#undef OLED_RST
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RST -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#endif

Some files were not shown because too many files have changed in this diff Show More