Compare commits

...
Author SHA1 Message Date
Ricardo Guzman f2f0d1725e date and update version 2026-10-18 17:00:52 -03:00
Ricardo Guzman ef7d38e940 is2 added for testing 2026-10-18 17:00:24 -03:00
Ricardo Guzman 1591d9f7fe all updated for V4.0.4 2026-10-14 10:00:17 -03:00
Ricardo Guzman 1747922036 date update 2026-10-14 09:32:51 -03:00
Ricardo Guzman 37afd9a659 lora settings fix 2026-10-14 09:32:07 -03:00
Ricardo Guzman f9439704f7 testing Tbeam1W 2026-10-09 13:02:48 -03:00
Ricardo Guzman 9314bf10ba VEXT fix for Heltec Wireless Tracker 2026-10-07 15:09:16 -03:00
Ricardo Guzman a045599df1 gps fix for supreme 2026-10-07 14:08:56 -03:00
Ricardo Guzman 9930603e99 ALDO1 2 update 2026-10-07 13:56:44 -03:00
Ricardo Guzman 9a8bbc2a99 about info update 2026-10-05 14:56:05 -03:00
Ricardo Guzman c0d67acffd save always fixed value 2026-10-05 14:50:19 -03:00
Ricardo Guzman 075ed0e6b4 version update 2026-10-05 14:35:32 -03:00
Ricardo Guzman 8ded0e96df Lora SetPower 2026-10-05 14:32:59 -03:00
Ricardo Guzman d13227c519 libraries update 2026-10-05 14:05:48 -03:00
Ricardo Guzman 9556eabd80 aprspacketlib 1.0.10 test 2026-10-05 01:09:18 -03:00
Ricardo Guzman d75c88fc91 update Status Messages y Acks a APRSPacketLib 109 2026-10-05 00:30:59 -03:00
Ricardo Guzman 1257e5d9e0 date update 2026-10-04 13:31:46 -03:00
Ricardo Guzman bfeb0f13eb APRSPacketLib update again with cleaning updates 2026-10-04 13:30:31 -03:00
Ricardo Guzman 9bddbdf20a variable name change and payload id change for messages 2026-10-04 12:16:41 -03:00
Ricardo Guzman e6f8953da2 better position of rfCommentEveryXBeacons 2026-10-02 17:48:53 -03:00
Ricardo Guzman 1876016509 rfCommentEveryXBeacons 2026-10-02 17:43:04 -03:00
Ricardo Guzman 7717fb5d6a Map and Rx Packets fixes and security updates 2026-10-02 10:40:30 -03:00
Ricardo Guzman 5e133e9f11 Merge pull request #470 from potato-pancake-png/fix/packet-display
Fix stored XSS in received packet rendering
2026-10-02 09:50:23 -03:00
potato-pancake-png 206d831941 Fix stored XSS in received packet rendering 2026-10-02 13:32:57 +09:00
Ricardo Guzman b2ba3d992a commit update too 2026-10-01 14:50:57 -03:00
Ricardo Guzman 683bc7fe1a date update 2026-10-01 14:22:31 -03:00
Ricardo Guzman e692ce2729 Wifi warning with bad password 2026-10-01 14:18:35 -03:00
Ricardo Guzman 90b18ef088 date now on rx packets 2026-09-30 16:08:50 -03:00
Ricardo Guzman 477df390e1 mDNS updated with kiss-tnc for phone apps 2026-09-30 15:43:47 -03:00
Ricardo Guzman fd9df47bd3 update esptool to build 2026-09-30 13:50:33 -03:00
Ricardo Guzman a763200220 build versions update 2026-09-30 12:04:36 -03:00
Ricardo Guzman e47a97491e version update 2026-09-30 11:38:36 -03:00
Ricardo Guzman 5036c20ca9 readme update 2026-09-30 11:36:42 -03:00
Ricardo Guzman aa17b8ded1 date update 2026-09-30 11:27:09 -03:00
Ricardo Guzman 228a3dde25 update to 2026 2026-09-30 00:09:35 -03:00
Ricardo Guzman 7f195e1725 TCXO update 2026-09-29 22:02:59 -03:00
Ricardo Guzman 48538a6162 BAUD RATE GPS AUTO DETECT 2026-09-29 19:50:28 -03:00
Ricardo Guzman a472c307d3 cad timeout y oled update 2026-09-29 18:46:41 -03:00
Ricardo Guzman fc256de09c fix to crossfreq 2026-09-28 15:24:17 -03:00
Ricardo Guzman 9b5fa49e31 mejora digi moricef 2026-09-28 15:03:59 -03:00
Ricardo Guzman 102169c0aa gitignore claude update 2026-09-27 12:10:31 -03:00
Ricardo Guzman 295d1fa2ef picture update 2026-09-23 22:45:48 -03:00
Ricardo Guzman 033657836f QRP Labs Gateway update 2026-09-23 19:14:24 -03:00
Ricardo Guzman f739b90adc update Screen Display Eco fix 2026-09-23 15:29:51 -03:00
Ricardo Guzman 5e2341299a ESP32C3_SuperMini_DIY_LoRa_SX1278 added 2026-09-23 14:03:30 -03:00
Ricardo Guzman 8ba8981a32 NTP updated 2026-09-23 13:50:05 -03:00
Ricardo Guzman e52d4566ba WiFi loop protection 2026-09-23 12:39:07 -03:00
Ricardo Guzman c80eb9bd84 update of names 2026-09-23 12:08:57 -03:00
Ricardo Guzman 6b187da9f4 E32 400M30S 2026-09-23 11:28:17 -03:00
Ricardo Guzman ab27c11532 updated build again 2026-09-23 11:06:47 -03:00
Ricardo Guzman 43bfff5033 sin update build 2026-09-23 11:04:06 -03:00
Ricardo Guzman 8fa10a7813 RAK3312 and 3112 added 2026-09-23 11:02:34 -03:00
Ricardo Guzman 316c746936 WebUI fixes 2026-09-23 05:12:51 -03:00
Ricardo Guzman 3618558b21 update and clean code 2026-09-22 21:17:33 -03:00
Ricardo Guzman e1eda2e64a updated aprspacket lib y otros refactoring 2026-09-22 19:59:24 -03:00
Ricardo Guzman 759cddd0e5 date update 2026-09-22 14:28:29 -03:00
Ricardo Guzman 8186ff4fbd updated digi cleanPath and process 2026-09-22 14:27:48 -03:00
Ricardo Guzman 3720fe9881 added fix for mic encoded syslog 2026-09-16 17:12:27 -03:00
Ricardo Guzman 5d04c56fd2 tnc2 added 2026-09-15 11:52:09 -03:00
Ricardo Guzman ee65f953d6 date update 2026-09-07 10:54:24 -03:00
Ricardo Guzman c952ea0159 Merge pull request #456 from cwb/fix-oled-screen-height
display: fix SCREEN_HEIGHT definition and maxLines for SH1106 OLED
2026-09-07 10:52:20 -03:00
Ricardo Guzman 21a42ef157 update de iconos 2026-09-07 10:46:39 -03:00
Ricardo Guzman 53ee07a7c1 heard direct and repeated 2026-08-27 22:45:08 -04:00
Ricardo Guzman 12a0f5bed1 update a 24px symbol 2026-08-25 20:20:09 -04:00
Ricardo Guzman 27f34c5f67 48px 2026-08-25 20:04:12 -04:00
Craig 08bfb013ce display: fix SCREEN_HEIGHT definition and maxLines for SH1106 OLED 2026-08-25 17:03:01 -07:00
Ricardo Guzman d573c66fd7 casi 2026-08-25 19:50:37 -04:00
Ricardo Guzman e0dc2f81ee ready for testing 2026-08-24 23:18:25 -04:00
Ricardo Guzman 6e4dc3c301 update needed for t-decks 2026-08-16 12:28:40 -04:00
Ricardo Guzman d2e42c8571 readme update2 2026-08-16 12:01:31 -04:00
Ricardo Guzman 6b321bc1e0 images update 2026-08-16 11:51:27 -04:00
Ricardo Guzman f7c9201830 now LEAFET is local flash 2026-07-14 00:33:54 -04:00
Ricardo Guzman 1a042b7d0e blockeando leafet 1 2026-07-14 00:17:56 -04:00
Ricardo Guzman aaa44200b5 test query callsign fix 2026-07-06 13:09:19 -04:00
Ricardo Guzman c17ad32650 2026 update in copywrite info 2026-06-29 13:06:58 -04:00
Ricardo Guzman (Richonguzman) 26b5df1bd7 update delete of star in digipeated packet 2026-06-22 22:16:14 -04:00
Ricardo Guzman (Richonguzman) 0c3555bdbe new digimode added 2026-06-18 13:58:10 -04:00
Ricardo Guzman (Richonguzman) cf858ffad2 digi patch 2 2026-06-16 11:29:32 -04:00
Ricardo Guzman (Richonguzman) cbac8b0365 digi patch 1 2026-06-16 10:41:44 -04:00
Ricardo Guzman (Richonguzman) b76aed4d57 cambio digi modes de 2 y 3 a 1 y 2 para agregar patch 2026-06-16 10:25:22 -04:00
Ricardo Guzman (Richonguzman) 96d7be6ffc Map cargado y funcionando 2026-06-12 00:09:02 -04:00
Ricardo Guzman (Richonguzman) 94df6d2e8f map con estaciones 2026-06-11 23:59:24 -04:00
Ricardo Guzman (Richonguzman) 0a222edeb8 primer test solo igate 2026-06-11 22:15:37 -04:00
Ricardo Guzman (Richonguzman) e35d5f6a9e about update 2026-06-11 01:55:45 -04:00
Ricardo Guzman (Richonguzman) 96432b8624 NEW WEB 2026-06-11 01:39:34 -04:00
Ricardo Guzman (Richonguzman) 25ea82737a cad test 3 2026-06-10 10:46:07 -04:00
Ricardo Guzman (Richonguzman) df22b9dbc0 readme and date update 2026-06-10 00:27:15 -04:00
Ricardo Guzman (Richonguzman) 9898e123d2 CAD with backoff process 2026-06-10 00:25:27 -04:00
Ricardo Guzman (Richonguzman) a97757c0b2 test CAD 2026-06-09 22:46:41 -04:00
Ricardo Guzman (Richonguzman) 305c5b73f7 mayusculas a RPC 2026-04-21 11:16:02 -04:00
Ricardo Guzman (Richonguzman) dd3688f23b fake name 2026-04-21 11:12:00 -04:00
Ricardo Guzman (Richonguzman) 6f5846c53d test algo dos 2026-04-21 11:05:52 -04:00
Ricardo Guzman (Richonguzman) 5c30e72a88 test algo 2026-04-21 11:05:27 -04:00
Ricardo Guzman (Richonguzman) a024b939f5 Merge pull request #423 from richonguzman/richonguzman-patch-19
Add RPC_LORA_DIGIGATE_1W to build workflow
2026-04-21 10:42:19 -04:00
Ricardo Guzman (Richonguzman) 96c52c042e Add RPC_LORA_DIGIGATE_1W to build workflow 2026-04-21 10:40:33 -04:00
Ricardo Guzman (Richonguzman) 236ae65485 ElegantOta fix 2026-04-21 10:34:20 -04:00
Ricardo Guzman (Richonguzman) 4263d55839 screen height fix 2026-04-20 12:56:03 -04:00
Ricardo Guzman (Richonguzman) ba96bb135e test new screen 2026-04-20 12:37:52 -04:00
Ricardo Guzman (Richonguzman) 944d1fceaf cleaning code again 2026-04-13 15:24:55 -04:00
Ricardo Guzman (Richonguzman) 005abb3593 Merge pull request #412 from richonguzman/richonguzman-patch-18
Add 2 new target configurations for GPS variants
2026-03-25 12:22:03 -03:00
187 changed files with 4694 additions and 9753 deletions

No files matched your search

+56 -41
View File
@@ -18,7 +18,7 @@ jobs:
- name: ttgo-lora32-v21_915
chip: esp32
- name: ttgo-lora32-v21_915_GPS
chip: esp32
chip: esp32
- name: ttgo_lora32_t3s3_v1_2
chip: esp32s3
- name: heltec-lora32-v2
@@ -37,17 +37,19 @@ jobs:
chip: esp32s3
- name: heltec_wireless_bridge
chip: esp32
- name: ESP32_DIY_LoRa
- name: ESP32_DIY_LoRa_SX1278
chip: esp32
- name: ESP32_DIY_LoRa_915
- name: ESP32_DIY_LoRa_915_SX1276
chip: esp32
- name: ESP32_DIY_1W_LoRa
- name: ESP32_DIY_1W_LoRa_E22_400M30S
chip: esp32
- name: ESP32_DIY_1W_LoRa_915
- name: ESP32_DIY_1W_LoRa_915_E22_900M30S
chip: esp32
- name: ESP32_DIY_1W_LoRa_LLCC68
- name: ESP32_DIY_1W_LoRa_E220_400M30S
chip: esp32
- name: ESP32_DIY_1W_LoRa_Mesh_V1_2
- name: ESP32_DIY_1W_LoRa_E32_400M30S
chip: esp32
- name: ESP32_DIY_1W_LoRa_Mesh_V1_2_E22_400M30S
chip: esp32
- name: ttgo-t-beam-v1_2
chip: esp32
@@ -69,9 +71,11 @@ jobs:
chip: esp32s3
- name: ttgo_t_beam_s3_SUPREME_v3
chip: esp32s3
- name: ESP32_DIY_LoRa_A7670
- name: ttgo-t-beam-1W
chip: esp32s3
- name: ESP32_DIY_LoRa_A7670_SX1278
chip: esp32
- name: ESP32_DIY_LoRa_A7670_915
- name: ESP32_DIY_LoRa_A7670_915_SX1276
chip: esp32
- name: heltec_wireless_tracker
chip: esp32s3
@@ -85,34 +89,45 @@ jobs:
chip: esp32s3
- name: OE5HWN_MeshCom
chip: esp32
- name: WEMOS-LOLIN32-OLED-DIY
- name: WEMOS_LOLIN32_OLED_DIY_LoRa_SX1278
chip: esp32
- name: WEMOS-D1-R32-RA02
- name: WEMOS_D1_R32_DIY_LoRa_SX1278
chip: esp32
- name: WEMOS_S2_MINI_DIY_LoRa
- name: WEMOS_S2_MINI_DIY_LoRa_SX1278
chip: esp32s2
- name: esp32c3_DIY_1W_LoRa
- name: ESP32C3_DIY_1W_LoRa_E22_400M30S
chip: esp32c3
- name: esp32c3_DIY_1W_LoRa_915
- name: ESP32C3_DIY_1W_LoRa_915_E22_900M30S
chip: esp32c3
- name: ESP32_C3_OctopusLab_LoRa
- name: ESP32C3_OctopusLab_LoRa
chip: esp32c3
- name: ESP32C3_SuperMini_DIY_LoRa_SX1278
chip: esp32c3
- name: QRPLabs_LightGateway_1_0
chip: esp32s3
- name: QRPLabs_LightGateway_Plus_1_0
chip: esp32s3
- name: RPC_LORA_DIGIGATE_1W
chip: esp32
- name: XIAO_ESP32S3_WIO_SX1262
chip: esp32s3
- name: RAK3312
chip: esp32s3
- name: RAK3112
chip: esp32s3
- name: TROY_LoRa_APRS
chip: esp32
- name: ESP32_9M2IBR_1W_LoRa_GPS
- name: ESP32_9M2IBR_1W_LoRa_GPS_E22_400M30S
chip: esp32
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
- uses: actions/setup-python@v4
- uses: actions/setup-python@v7
with:
python-version: "3.9"
python-version: "3.13"
- name: Install esptool
run: pip install "esptool>=5,<6"
- name: Install PlatformIO Core
run: pip install --upgrade platformio
@@ -138,44 +153,44 @@ jobs:
- name: Merge for web flashing
run: |
if [ "${{ matrix.target.chip }}" == "esp32" ]; then
python installer/bin/esptool/esptool.py --chip esp32 merge_bin \
esptool --chip esp32 merge-bin \
-o installer/web_factory.bin \
--flash_mode dio \
--flash_freq 40m \
--flash_size 4MB \
--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 \
esptool --chip esp32s2 merge-bin \
-o installer/web_factory.bin \
--flash_mode dio \
--flash_freq 40m \
--flash_size 4MB \
--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 \
esptool --chip esp32s3 merge-bin \
-o installer/web_factory.bin \
--flash_mode dio \
--flash_freq 40m \
--flash_size 8MB \
--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 \
esptool --chip esp32c3 merge-bin \
-o installer/web_factory.bin \
--flash_mode dio \
--flash_freq 40m \
--flash_size 4MB \
--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 \
@@ -189,12 +204,12 @@ jobs:
- name: Archive Files
run: zip -r installer.zip installer/
- name: Rename zip for upload
run: mv installer.zip ${{ matrix.target.name }}.zip
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
uses: softprops/action-gh-release@v3
with:
files: ${{ matrix.target.name }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./installer.zip
asset_name: ${{ matrix.target.name }}.zip
asset_content_type: application/zip
+3 -3
View File
@@ -15,11 +15,11 @@ jobs:
- name: ttgo-lora32-v21
chip: esp32
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
- uses: actions/setup-python@v4
- uses: actions/setup-python@v7
with:
python-version: "3.9"
python-version: "3.13"
- name: Install PlatformIO Core
run: pip install --upgrade platformio
+2 -1
View File
@@ -7,4 +7,5 @@
/data_embed/*.gz
installer/firmware
installer/*.bin
**/__pycache__/
**/__pycache__/
.claude/
+2 -1
View File
@@ -5,6 +5,7 @@
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
"ms-vscode.cpptools-extension-pack",
"pioarduino.pioarduino-ide"
]
}
+25 -5
View File
@@ -16,41 +16,61 @@ ____________________________________________________
____________________________________________________
## New Web/UI
| New Menu | Map |
| --- | --- |
| ![new menu](images/1.png) | ![map](images/2.jpg) |
____________________________________________________
## You can support this project to continue to grow:
[<img src="https://github.com/richonguzman/LoRa_APRS_Tracker/raw/main/images/github-sponsors.png">](https://github.com/sponsors/richonguzman) [<img src="https://github.com/richonguzman/LoRa_APRS_Tracker/raw/main/images/paypalme.png">](http://paypal.me/richonguzman)
____________________________________________________
## SUPPORTED BOARDS (<a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/Supported-Boards-and-Buying-Links" target="_blank">Buying links</a>).
(NOTE: all boards with 433-868-915 MHz versions)
- TTGO Lilygo LoRa32 T3S3 V1.2 and LoRa32 V2.1 (V1.6 is the same).
- TTGO Lilygo LoRa32 LoRa32 V2.1 (V1.6 is the same) and T3S3 V1.2.
- TTGO T-Beam V1.0 , V1.1, V1.2 (also variations with SX1262 and SX1268 LoRa Modules) and Supreme V3.
- TTGO T-Beam V1.0 , V1.1, V1.2 (also variations with SX1262 and SX1268 LoRa Modules), Supreme V3 and T-Beam 1W.
- T-Deck Plus (and also regular T-Deck with/without GPS).
- HELTEC V2, V3, V3.2, T114, Wireless Stick, Wireless Stick Lite V3/V3.2, HT-CT62, Wireless Tracker, Wireless Paper.
- HELTEC V2, V3, V3.2, V4, T114, Wireless Stick, Wireless Stick Lite V3/V3.2, HT-CT62, Wireless Tracker, Wireless Paper.
- RAK Wireless 4631 + 19007(or 19003)
- RAK Wireless 4631 + 19007(or 19003), RAK3312 and RAK3112.
- Faketec V3 (NRF52840 + Heltec HTRA62(SX1262))
- QRP Labs LightGateway 1.0 and Plus 1.0.
- ESP32 + SX1278 LoRa Module or Ebyte 400M30S (or 900M30S) 1W LoRa Module for a DIY Versions.
- ESP32 + SX1278 LoRa Module or Ebyte 400M30S (or 900M30S) or Ebyte E32 400M30S 1W LoRa Module for a DIY Versions.
- ESP32C3 + Ebyte 400M30S(or 900M30S) 1W LoRa Module for another DIY version.
- ESP32C3 SuperMini + SX1278 (RA-01/RA-02) LoRa Module DIY version.
- ESP32 + 4G/LTE A7670 Modem + SX1278 DIY Version.
- Wemos Lolin32 Oled + SX1278 DIY Version.
- and many more...
<br />
# Timeline (Versions):
- 2026-10-14 T-Beam 1W board added (with fan control and Tx over-temperature protection).
- 2026-10-05 Fix for stored XSS in received packets, Wifi warning with bad password, date on Rx packets, mDNS kiss-tnc for phone apps, APRSPacketLib V1.0.10 update.
- 2026-09-30 GPS baud rate auto-detection, CAD timeout (packet dropped after 10s of busy channel)
- 2026-09-23 APRSPacketLib V1.0.7 update and many code cleaning with this.
- 2026-09-17 Real APRS Symbols added to map and TNC2 output as a posibility in TNC mode.
- 2026-08-16 New UI (Web) and also Live View over a Map to see Rx stations.
- 2026-08-10 CAD (Channel Activity Detection) and DIFS (Distributed Inter-Frame Space) added.
- 2026-03-25 More Boards, SDK update, OTA fix, GPS process update.
- 2026-02-26 9M2IBR ESP32 1W (400M30S) + GPS board added.
- 2026-02-25 Code Improvements: reduced String comparisons and improved logic for faster code execution.
+7 -8
View File
@@ -30,18 +30,17 @@ lib_deps =
adafruit/Adafruit BME680 Library @ 2.0.6
adafruit/Adafruit INA219 @ 1.2.3
adafruit/Adafruit Si7021 Library @ 1.5.3
arduino-libraries/NTPClient @ 3.2.1
ayushsharma82/ElegantOTA @ 3.1.7
bblanchon/ArduinoJson @ 7.4.2
bblanchon/ArduinoJson @ 7.4.3
jgromes/RadioLib @ 7.6.0
knolleary/PubSubClient @ 2.8
ESP32Async/AsyncTCP @ 3.4.10
ESP32Async/ESPAsyncWebServer @ 3.10.0
mikalhart/TinyGPSPlus @ 1.0.3
richonguzman/APRSPacketLib @ 1.0.4
ESP32Async/AsyncTCP @ 3.5.0
ESP32Async/ESPAsyncWebServer @ 3.12.1
mikalhart/TinyGPSPlus @ 1.1.0
richonguzman/APRSPacketLib @ 1.0.10
display_libs =
adafruit/Adafruit GFX Library @ 1.11.9
adafruit/Adafruit SSD1306 @ 2.5.10
adafruit/Adafruit GFX Library @ 1.12.6
adafruit/Adafruit SSD1306 @ 2.5.17
usb_flags=
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
+5 -3
View File
@@ -27,10 +27,11 @@
"active": false,
"messagesToRF": false,
"objectsToRF": false,
"is2Metadata": false,
"server": "rotate.aprs2.net",
"passcode": "XYZVW",
"passcode": "XYZVW",
"port": 14580,
"filter": "m/10"
"filter": "m/10"
},
"personalNote": "",
"blacklist": "",
@@ -50,7 +51,8 @@
"txSpreadingFactor": 12,
"txCodingRate4": 5,
"txSignalBandwidth": 125000,
"power": 20
"power": 20,
"cadActive": true
},
"display": {
"alwaysOn": true,
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+1286 -1103
View File
File diff suppressed because it is too large. Load diff
+661
View File
@@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
+6
View File
@@ -0,0 +1,6 @@
/* @preserve
* Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com
* (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n<o;n++)for(e in i=arguments[n])t[e]=i[e];return t}var R=Object.create||function(t){return N.prototype=t,new N};function N(){}function a(t,e){var i,n=Array.prototype.slice;return t.bind?t.bind.apply(t,n.call(arguments,1)):(i=n.call(arguments,2),function(){return t.apply(e,i.length?i.concat(n.call(arguments)):arguments)})}var D=0;function h(t){return"_leaflet_id"in t||(t._leaflet_id=++D),t._leaflet_id}function j(t,e,i){var n,o,s=function(){n=!1,o&&(r.apply(i,o),o=!1)},r=function(){n?o=arguments:(t.apply(i,arguments),setTimeout(s,e),n=!0)};return r}function H(t,e,i){var n=e[1],e=e[0],o=n-e;return t===n&&i?t:((t-e)%o+o)%o+e}function u(){return!1}function i(t,e){return!1===e?t:(e=Math.pow(10,void 0===e?6:e),Math.round(t*e)/e)}function W(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}function F(t){return W(t).split(/\s+/)}function c(t,e){for(var i in Object.prototype.hasOwnProperty.call(t,"options")||(t.options=t.options?R(t.options):{}),e)t.options[i]=e[i];return t.options}function U(t,e,i){var n,o=[];for(n in t)o.push(encodeURIComponent(i?n.toUpperCase():n)+"="+encodeURIComponent(t[n]));return(e&&-1!==e.indexOf("?")?"&":"?")+o.join("&")}var V=/\{ *([\w_ -]+) *\}/g;function q(t,i){return t.replace(V,function(t,e){e=i[e];if(void 0===e)throw new Error("No value provided for variable "+t);return e="function"==typeof e?e(i):e})}var d=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)};function G(t,e){for(var i=0;i<t.length;i++)if(t[i]===e)return i;return-1}var K="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";function Y(t){return window["webkit"+t]||window["moz"+t]||window["ms"+t]}var X=0;function J(t){var e=+new Date,i=Math.max(0,16-(e-X));return X=e+i,window.setTimeout(t,i)}var $=window.requestAnimationFrame||Y("RequestAnimationFrame")||J,Q=window.cancelAnimationFrame||Y("CancelAnimationFrame")||Y("CancelRequestAnimationFrame")||function(t){window.clearTimeout(t)};function x(t,e,i){if(!i||$!==J)return $.call(window,a(t,e));t.call(e)}function r(t){t&&Q.call(window,t)}var tt={__proto__:null,extend:l,create:R,bind:a,get lastId(){return D},stamp:h,throttle:j,wrapNum:H,falseFn:u,formatNum:i,trim:W,splitWords:F,setOptions:c,getParamString:U,template:q,isArray:d,indexOf:G,emptyImageUrl:K,requestFn:$,cancelFn:Q,requestAnimFrame:x,cancelAnimFrame:r};function et(){}et.extend=function(t){function e(){c(this),this.initialize&&this.initialize.apply(this,arguments),this.callInitHooks()}var i,n=e.__super__=this.prototype,o=R(n);for(i in(o.constructor=e).prototype=o,this)Object.prototype.hasOwnProperty.call(this,i)&&"prototype"!==i&&"__super__"!==i&&(e[i]=this[i]);if(t.statics&&l(e,t.statics),t.includes){var s=t.includes;if("undefined"!=typeof L&&L&&L.Mixin){s=d(s)?s:[s];for(var r=0;r<s.length;r++)s[r]===L.Mixin.Events&&console.warn("Deprecated include of L.Mixin.Events: this property will be removed in future releases, please inherit from L.Evented instead.",(new Error).stack)}l.apply(null,[o].concat(t.includes))}return l(o,t),delete o.statics,delete o.includes,o.options&&(o.options=n.options?R(n.options):{},l(o.options,t.options)),o._initHooks=[],o.callInitHooks=function(){if(!this._initHooksCalled){n.callInitHooks&&n.callInitHooks.call(this),this._initHooksCalled=!0;for(var t=0,e=o._initHooks.length;t<e;t++)o._initHooks[t].call(this)}},e},et.include=function(t){var e=this.prototype.options;return l(this.prototype,t),t.options&&(this.prototype.options=e,this.mergeOptions(t.options)),this},et.mergeOptions=function(t){return l(this.prototype.options,t),this},et.addInitHook=function(t){var e=Array.prototype.slice.call(arguments,1),i="function"==typeof t?t:function(){this[t].apply(this,e)};return this.prototype._initHooks=this.prototype._initHooks||[],this.prototype._initHooks.push(i),this};var e={on:function(t,e,i){if("object"==typeof t)for(var n in t)this._on(n,t[n],e);else for(var o=0,s=(t=F(t)).length;o<s;o++)this._on(t[o],e,i);return this},off:function(t,e,i){if(arguments.length)if("object"==typeof t)for(var n in t)this._off(n,t[n],e);else{t=F(t);for(var o=1===arguments.length,s=0,r=t.length;s<r;s++)o?this._off(t[s]):this._off(t[s],e,i)}else delete this._events;return this},_on:function(t,e,i,n){"function"!=typeof e?console.warn("wrong listener type: "+typeof e):!1===this._listens(t,e,i)&&(e={fn:e,ctx:i=i===this?void 0:i},n&&(e.once=!0),this._events=this._events||{},this._events[t]=this._events[t]||[],this._events[t].push(e))},_off:function(t,e,i){var n,o,s;if(this._events&&(n=this._events[t]))if(1===arguments.length){if(this._firingCount)for(o=0,s=n.length;o<s;o++)n[o].fn=u;delete this._events[t]}else"function"!=typeof e?console.warn("wrong listener type: "+typeof e):!1!==(e=this._listens(Line truncated
//# sourceMappingURL=leaflet.js.map
+603 -15
View File
@@ -56,6 +56,7 @@ function loadSettings(settings) {
document.getElementById("callsign").value = settings.callsign;
document.getElementById("tacticalCallsign").value = settings.tacticalCallsign;
document.getElementById("beacon.comment").value = settings.beacon.comment;
document.getElementById("beacon.rfCommentEveryXBeacons").value = settings.beacon.rfCommentEveryXBeacons;
document.getElementById("beacon.path").value = settings.beacon.path;
document.getElementById("beacon.symbol").value = settings.beacon.symbol;
document.getElementById("beacon.overlay").value = settings.beacon.overlay;
@@ -96,12 +97,13 @@ function loadSettings(settings) {
networksContainer.appendChild(networkElement);
networkCount++;
});
document.getElementById("startupDelay").value = settings.startupDelay;
document.getElementById("startupDelay").value = settings.other.startupDelay;
// APRS-IS
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.is2Metadata").checked = settings.aprs_is.is2Metadata;
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;
@@ -109,6 +111,7 @@ function loadSettings(settings) {
APRSISCheckbox.checked = settings.aprs_is.active;
APRSISGateMessages.disabled = !APRSISCheckbox.checked;
APRSISGateObjects.disabled = !APRSISCheckbox.checked;
APRSISMetadata.disabled = !APRSISCheckbox.checked;
APRSISServer.disabled = !APRSISCheckbox.checked;
APRSISPort.disabled = !APRSISCheckbox.checked;
APRSISPasscode.disabled = !APRSISCheckbox.checked;
@@ -118,7 +121,7 @@ function loadSettings(settings) {
document.getElementById("beacon.latitude").value = settings.beacon.latitude;
document.getElementById("beacon.longitude").value = settings.beacon.longitude;
document.getElementById("beacon.interval").value = settings.beacon.interval;
document.getElementById("other.rememberStationTime").value = settings.other.rememberStationTime;
document.getElementById("other.rememberStationTime").value = settings.other.rememberStationTime;
document.getElementById("beacon.sendViaAPRSIS").checked = settings.beacon.sendViaAPRSIS;
document.getElementById("beacon.sendViaRF").checked = settings.beacon.sendViaRF;
@@ -148,12 +151,26 @@ function loadSettings(settings) {
document.getElementById("lora.rxSpreadingFactor").value = settings.lora.rxSpreadingFactor;
document.getElementById("lora.rxCodingRate4").value = settings.lora.rxCodingRate4;
document.getElementById("lora.rxSignalBandwidth").value = settings.lora.rxSignalBandwidth;
LoRaRxCheckbox.checked = settings.lora.rxActive;
LoRaRxFreq.disabled = !LoRaRxCheckbox.checked;
LoRaRxSpreadingFactor.disabled = !LoRaRxCheckbox.checked;
LoRaRxCodingRate.disabled = !LoRaRxCheckbox.checked;
LoRaRxSignalBandwidth.disabled = !LoRaRxCheckbox.checked;
document.getElementById("lora.txActive").checked = settings.lora.txActive;
document.getElementById("lora.txFreq").value = settings.lora.txFreq;
document.getElementById("lora.txSpreadingFactor").value = settings.lora.txSpreadingFactor;
document.getElementById("lora.txCodingRate4").value = settings.lora.txCodingRate4;
document.getElementById("lora.txSignalBandwidth").value = settings.lora.txSignalBandwidth;
document.getElementById("lora.txSignalBandwidth").value = settings.lora.txSignalBandwidth;
document.getElementById("lora.power").value = settings.lora.power;
document.getElementById("lora.cadActive").checked = settings.lora.cadActive;
LoRaTxCheckbox.checked = settings.lora.txActive;
LoRaTxFreq.disabled = !LoRaTxCheckbox.checked;
LoRaTxSpreadingFactor.disabled = !LoRaTxCheckbox.checked;
LoRaTxCodingRate.disabled = !LoRaTxCheckbox.checked;
LoRaTxSignalBandwidth.disabled = !LoRaTxCheckbox.checked;
LoRaPower.disabled = !LoRaTxCheckbox.checked;
LoRaCadActive.disabled = !LoRaTxCheckbox.checked;
// Display
document.getElementById("display.alwaysOn").checked = settings.display.alwaysOn;
@@ -195,7 +212,7 @@ function loadSettings(settings) {
TelemetryCheckbox.checked = settings.wxsensor.active;
TelemetryHeightCorrection.disabled = !TelemetryCheckbox.checked;
TelemetryTempCorrection.disabled = !TelemetryCheckbox.checked;
// SYSLOG
document.getElementById("syslog.active").checked = settings.syslog.active;
document.getElementById("syslog.server").value = settings.syslog.server;
@@ -205,13 +222,14 @@ function loadSettings(settings) {
SyslogServer.disabled = !SyslogCheckbox.checked;
SyslogPort.disabled = !SyslogCheckbox.checked;
SyslogBeaconOverTCPIP.disabled = !SyslogCheckbox.checked;
// TNC
if (settings.tnc) {
document.getElementById("tnc.enableServer").checked = settings.tnc.enableServer;
document.getElementById("tnc.enableSerial").checked = settings.tnc.enableSerial;
document.getElementById("tnc.acceptOwn").checked = settings.tnc.acceptOwn;
document.getElementById("tnc.aprsBridgeActive").checked = settings.tnc.aprsBridgeActive;
document.getElementById("tnc.kissProtocol").value = settings.tnc.kissProtocol ? "KISS" : "TNC2";
}
// MQTT
@@ -262,6 +280,8 @@ function loadSettings(settings) {
document.getElementById("ntp.server").value = settings.ntp.server;
document.getElementById("ntp.gmtCorrection").value = settings.ntp.gmtCorrection;
clampOutOfRangeInputs();
updateImage();
}
@@ -332,6 +352,7 @@ StatusCheckbox.addEventListener("change", function() {
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 APRSISMetadata = document.querySelector('input[name="aprs_is.is2Metadata"]');
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"]');
@@ -339,6 +360,7 @@ const APRSISFilter = document.querySelector('input[name="aprs_is.fi
APRSISCheckbox.addEventListener("change", function() {
APRSISGateMessages.disabled = !this.checked;
APRSISGateObjects.disabled = !this.checked;
APRSISMetadata.disabled = !this.checked;
APRSISServer.disabled = !this.checked;
APRSISPort.disabled = !this.checked;
APRSISPasscode.disabled = !this.checked;
@@ -390,6 +412,36 @@ TelemetryCheckbox.addEventListener("change", function () {
TelemetryTempCorrection.disabled = !this.checked;
});
// LoRa Rx Switch
const LoRaRxCheckbox = document.querySelector('input[name="lora.rxActive"]');
const LoRaRxFreq = document.querySelector('[name="lora.rxFreq"]');
const LoRaRxSpreadingFactor = document.querySelector('[name="lora.rxSpreadingFactor"]');
const LoRaRxCodingRate = document.querySelector('[name="lora.rxCodingRate4"]');
const LoRaRxSignalBandwidth = document.querySelector('[name="lora.rxSignalBandwidth"]');
LoRaRxCheckbox.addEventListener("change", function () {
LoRaRxFreq.disabled = !this.checked;
LoRaRxSpreadingFactor.disabled = !this.checked;
LoRaRxCodingRate.disabled = !this.checked;
LoRaRxSignalBandwidth.disabled = !this.checked;
});
// LoRa Tx Switch
const LoRaTxCheckbox = document.querySelector('input[name="lora.txActive"]');
const LoRaTxFreq = document.querySelector('[name="lora.txFreq"]');
const LoRaTxSpreadingFactor = document.querySelector('[name="lora.txSpreadingFactor"]');
const LoRaTxCodingRate = document.querySelector('[name="lora.txCodingRate4"]');
const LoRaTxSignalBandwidth = document.querySelector('[name="lora.txSignalBandwidth"]');
const LoRaPower = document.querySelector('[name="lora.power"]');
const LoRaCadActive = document.querySelector('[name="lora.cadActive"]');
LoRaTxCheckbox.addEventListener("change", function () {
LoRaTxFreq.disabled = !this.checked;
LoRaTxSpreadingFactor.disabled = !this.checked;
LoRaTxCodingRate.disabled = !this.checked;
LoRaTxSignalBandwidth.disabled = !this.checked;
LoRaPower.disabled = !this.checked;
LoRaCadActive.disabled = !this.checked;
});
// Syslog Switches
const SyslogCheckbox = document.querySelector('input[name="syslog.active"]');
const SyslogServer = document.querySelector('input[name="syslog.server"]');
@@ -446,7 +498,6 @@ function toggleWiFiAutoAPFields() {
if (autoAPConfig) autoAPConfig.style.display = isEnabled ? 'block' : 'none';
}
document.querySelector(".new button").addEventListener("click", function () {
const networksContainer = document.querySelector(".list-networks");
@@ -559,13 +610,12 @@ function loadReceivedPackets(packets) {
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>
`;
['rxDate', 'rxTime', 'packet', 'RSSI', 'SNR'].forEach((field) => {
const cell = document.createElement("td");
cell.textContent = String(packet[field]);
element.appendChild(cell);
});
container.appendChild(element);
})
@@ -592,8 +642,546 @@ document.querySelector('a[href="/received-packets"]').addEventListener('click',
document.getElementById('received-packets').classList.remove('d-none');
document.getElementById('configuration').classList.add('d-none');
document.querySelector('button[type=submit]').remove();
fetchReceivedPackets();
})
})
/* ---------- Stations Map (Leaflet, CDN) ---------- */
let mapInstance = null;
let mapMarkers = null;
let mapTimer = null;
let mapTileErrShown = false;
let iGateLatLng = null;
let mapAutoFitDone = false;
function escapeHtml(value) { // shows received data as text inside HTML strings (map popups / icons)
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function setMapMessage(text) {
const el = document.getElementById("map");
if (el) {
el.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--msh-text-dim,#8b949e);text-align:center;padding:20px;">${text}</div>`;
}
}
/*
* APRS symbol renderer for Stations Map
*
* s.symbol contains APRS table + symbol code, for example:
* "/>" Primary table
* "\\>" Alternate table
* "L&" Alternate table with overlay
*
* Sprite layout:
* aprs-symbols-24-0.png = Primary table
* aprs-symbols-24-1.png = Alternate table
* aprs-symbols-24-2.png = Overlay characters
*/
function getAprsSymbolIcon(symbol, callsign = "") {
if (!symbol || String(symbol).length < 2) {
return null;
}
const value = String(symbol);
let table = value.charAt(0);
let code = value.charAt(1);
let overlay = null;
/*
* Standard APRS:
* "/" = Primary
* "\\" = Alternate
*
* A-Z / 0-9 / a-j in table position are overlays
* for the Alternate table.
*/
if (
table !== "/" &&
table !== "\\"
) {
overlay = table;
table = "\\";
}
const ascii = code.charCodeAt(0);
/*
* APRS printable symbol codes are ASCII 33..126.
*/
if (
ascii < 33 ||
ascii > 126
) {
return null;
}
const index = ascii - 33;
/*
* Sprite sheet contains 16 columns x 6 rows.
* Each cell occupies 24x24 CSS px; the
* "background-size" below tells the browser to scale the sheet back
* down to its original 24px-per-cell layout, so all position math
* stays in 24px units regardless of source resolution.
*/
const columns = 16;
const sheetSize = "384px 144px"; // 16*24 x 6*24, same for all 3 sheets
const col = index % columns;
const row = Math.floor(index / columns);
const sprite =
table === "/"
? "/aprs-symbols-24-0.png"
: "/aprs-symbols-24-1.png";
const x = -(col * 24);
const y = -(row * 24);
let overlayHtml = "";
if (overlay) {
let displayOverlay = overlay;
/*
* APRS compressed numeric overlay:
* a-j -> 0-9
*/
if (
overlay >= "a" &&
overlay <= "j"
) {
displayOverlay =
String(
overlay.charCodeAt(0) -
"a".charCodeAt(0)
);
}
/*
* The overlay sprite sheet does NOT follow the ascii-33 layout used
* by the primary/alternate tables above; it has its own grid:
* "0" at row 0 col 15, "1".."9" at row 1 col 0-8, "A".."P" at row 2,
* "Q".."Z" at row 3 col 0-9. Verified by hand against the PNG.
*/
let overlayCol = null;
let overlayRow = null;
if (displayOverlay === "0") {
overlayCol = 15;
overlayRow = 0;
} else if (displayOverlay >= "1" && displayOverlay <= "9") {
overlayCol = displayOverlay.charCodeAt(0) - "1".charCodeAt(0);
overlayRow = 1;
} else if (displayOverlay >= "A" && displayOverlay <= "P") {
overlayCol = displayOverlay.charCodeAt(0) - "A".charCodeAt(0);
overlayRow = 2;
} else if (displayOverlay >= "Q" && displayOverlay <= "Z") {
overlayCol = displayOverlay.charCodeAt(0) - "Q".charCodeAt(0);
overlayRow = 3;
}
if (overlayCol !== null) {
const ox = -(overlayCol * 24);
const oy = -(overlayRow * 24);
overlayHtml = `
<span style="
position:absolute;
left:0;
top:0;
width:24px;
height:24px;
background-image:url('/aprs-symbols-24-2.png');
background-repeat:no-repeat;
background-position:${ox}px ${oy}px;
background-size:${sheetSize};
pointer-events:none;
"></span>
`;
}
}
return L.divIcon({
className: "",
html: `
<div style="
position:relative;
width:24px;
height:24px;
background-image:url('${sprite}');
background-repeat:no-repeat;
background-position:${x}px ${y}px;
background-size:${sheetSize};
">
${overlayHtml}
<span style="
position:absolute;
left:27px;
top:6px;
font:600 10px Arial,sans-serif;
line-height:12px;
white-space:nowrap;
color:#111;
text-shadow:
-1px -1px 0 #fff,
1px -1px 0 #fff,
-1px 1px 0 #fff,
1px 1px 0 #fff;
pointer-events:none;
">${escapeHtml(callsign)}</span>
</div>
`,
iconSize: [24, 24],
iconAnchor: [12, 12],
popupAnchor: [0, -12]
});
}
let mapStationsCache = [];
let mapRouteLayer = null;
function getUsedDigipeaters(path) {
if (!path) return [];
return path
.split(",")
.map((x) => x.trim())
.filter((x) => x.endsWith("*"))
.map((x) => x.replace(/\*+$/, ""))
.filter((x) =>
x &&
!x.startsWith("WIDE") &&
!x.startsWith("TRACE") &&
!x.startsWith("TCPIP") &&
!x.startsWith("TCPXX") &&
!x.startsWith("qA")
);
}
function describeMapPath(station) {
const digis = getUsedDigipeaters(station.path);
// heardDirect viene del firmware: true si ALGUNA VEZ se oyo directo,
// aunque el ultimo paquete haya llegado repetido.
if (station.heardDirect && digis.length) {
return "Direct+Via: " + digis.join(" → ");
}
if (digis.length) {
return "Via: " + digis.join(" → ");
}
return "Direct";
}
function clearMapRoute() {
if (mapRouteLayer && mapInstance) {
mapInstance.removeLayer(mapRouteLayer);
}
mapRouteLayer = null;
}
function drawStationRoute(station) {
if (!mapInstance || !iGateLatLng) return;
clearMapRoute();
const points = [
L.latLng(station.lat, station.lon)
];
const digis = getUsedDigipeaters(station.path);
digis.forEach((digiCall) => {
const digi = mapStationsCache.find((s) =>
String(s.callsign || "").toUpperCase() === digiCall.toUpperCase()
);
if (digi && !(digi.lat === 0 && digi.lon === 0)) {
points.push(L.latLng(digi.lat, digi.lon));
}
});
points.push(iGateLatLng);
const routeColor = digis.length > 0 ? "#2e7d32" : "#1565c0";
mapRouteLayer = L.polyline(points, {
color: routeColor,
weight: 3,
opacity: 0.9
}).addTo(mapInstance);
}
function loadMapStations(stations) {
if (!mapMarkers) return;
mapStationsCache = stations || [];
mapMarkers.clearLayers();
clearMapRoute();
let drawn = 0;
(stations || []).forEach((s) => {
if (s.lat === 0 && s.lon === 0) return; // descartar estaciones sin fix
const popup = `<b>${escapeHtml(s.callsign)}</b>`
+ (s.lastHeard ? `<br>Last: ${escapeHtml(s.lastHeard)}` : "")
+ `<br>RSSI ${s.RSSI} / SNR ${s.SNR}`
+ `<br>Packets: ${s.count}`
+ `<br><b>${escapeHtml(describeMapPath(s))}</b>`
+ (s.path ? `<br><small>Path: ${escapeHtml(s.path)}</small>` : "");
const aprsIcon =
getAprsSymbolIcon(s.symbol, s.callsign);
if (aprsIcon) {
const marker = L.marker(
[s.lat, s.lon],
{
icon: aprsIcon
}
)
.bindPopup(popup)
.addTo(mapMarkers);
marker.on("mouseover", function () {
drawStationRoute(s);
});
marker.on("mouseout", function () {
clearMapRoute();
});
} else { // Fallback: station with missing or invalid APRS symbol
const marker = L.circleMarker([s.lat, s.lon], {
radius: 6,
color: "#1d6fe0",
weight: 2,
fillColor: "#3b8cff",
fillOpacity: 0.9
})
.bindPopup(popup)
.addTo(mapMarkers);
marker.on("mouseover", function () {
drawStationRoute(s);
});
marker.on("mouseout", function () {
clearMapRoute();
});
}
drawn++;
});
if (!mapAutoFitDone && drawn > 0) { // auto-fit la 1ª vez que hay estaciones tras abrir
fitMapToStations();
mapAutoFitDone = true;
}
}
// Encuadra el mapa para que entren todas las estaciones (+ el iGate). 100% navegador.
function fitMapToStations() {
if (!mapInstance) return false;
const pts = [];
if (mapMarkers) {
mapMarkers.eachLayer((layer) => {
if (layer.getLatLng) pts.push(layer.getLatLng());
});
}
if (iGateLatLng) pts.push(iGateLatLng);
if (!pts.length) return false;
mapInstance.fitBounds(L.latLngBounds(pts), { padding: [30, 30], maxZoom: 14 });
return true;
}
function fetchMapStations() {
fetch("/stations.json")
.then((response) => response.json())
.then((stations) => {
loadMapStations(stations);
})
.catch((err) => {
console.error(err);
console.error(`Failed to load stations`);
});
}
window.showMap = function () {
if (typeof L === "undefined") { // sin internet el CDN de Leaflet no cargó
setMapMessage("Map unavailable &mdash; no internet connection.<br>The map needs internet to load (OpenStreetMap).");
return;
}
mapAutoFitDone = false; // re-encuadrar en cada apertura del Map
if (!mapInstance) {
mapInstance = L.map("map");
const tiles = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "&copy; OpenStreetMap"
}).addTo(mapInstance);
tiles.on("tileerror", function () { // tiles no cargan (sin salida a internet)
if (!mapTileErrShown) {
mapTileErrShown = true;
if (window.showToast) showToast("Map tiles could not load — check internet connection.");
}
});
mapMarkers = L.layerGroup().addTo(mapInstance);
const FitControl = L.Control.extend({ // botón "Fit All Stations" (control nativo de Leaflet)
options: { position: "topright" },
onAdd: function () {
const btn = L.DomUtil.create("button", "");
btn.type = "button";
btn.title = "Fit all stations";
btn.textContent = "Fit All Stations";
btn.style.cssText = "background:#fff;color:#333;border:2px solid rgba(0,0,0,.2);border-radius:4px;padding:5px 9px;cursor:pointer;font:600 12px sans-serif;box-shadow:0 1px 4px rgba(0,0,0,.3);";
L.DomEvent.on(btn, "click", (e) => {
L.DomEvent.stop(e);
fitMapToStations();
});
return btn;
}
});
mapInstance.addControl(new FitControl());
fetch("/configuration.json") // centrar y marcar el iGate
.then((response) => response.json())
.then((config) => {
const lat = (config.beacon && config.beacon.latitude) || 0;
const lon = (config.beacon && config.beacon.longitude) || 0;
const callsign = config.callsign || "iGate";
mapInstance.setView([lat, lon], (lat || lon) ? 12 : 2);
if (lat || lon) { // rombo rojo en la posición del iGate
iGateLatLng = L.latLng(lat, lon); // guardar para incluirlo en el auto-fit
const iGateIcon = L.divIcon({
className: "igate-marker",
html: '<div style="width:14px;height:14px;background:#e23b3b;border:2px solid #fff;transform:rotate(45deg);box-shadow:0 0 4px rgba(0,0,0,.6);"></div>',
iconSize: [18, 18],
iconAnchor: [9, 9]
});
L.marker([lat, lon], { icon: iGateIcon })
.bindPopup(`<b>${callsign}</b><br>This station`)
.addTo(mapInstance); // directo al mapa: no se borra en los refrescos
}
})
.catch(() => {
mapInstance.setView([0, 0], 2);
});
}
setTimeout(function () { // el div estuvo oculto: recalcular tamaño
if (mapInstance) mapInstance.invalidateSize();
}, 0);
fetchMapStations();
if (mapTimer) clearInterval(mapTimer);
mapTimer = setInterval(function () { // refrescar solo si la sección está visible
const section = document.getElementById("sec-maps");
if (section && section.classList.contains("active")) {
fetchMapStations();
}
}, 15000);
}
/* ------------------------------------------------- keeping the form submittable
* A control that fails HTML5 validation blocks the whole form, and when it sits in
* a panel that is not the visible one the browser cannot show its bubble either:
* pressing Save then does nothing at all -- no message, no request, no clue. Two
* guards against that: pull stored values back inside the allowed range on load,
* and, if validation still fails, open the section holding the offending control
* and show what is wrong.
*/
function clampOutOfRangeInputs() {
const fixed = [];
document.querySelectorAll("#configuration input[type=number]").forEach((el) => {
if (el.disabled || el.value === "") return;
const value = parseFloat(el.value);
if (isNaN(value)) return;
const min = el.min !== "" ? parseFloat(el.min) : null;
const max = el.max !== "" ? parseFloat(el.max) : null;
let clamped = value;
if (max !== null && value > max) clamped = max;
if (min !== null && value < min) clamped = min;
if (clamped !== value) {
el.value = clamped;
fixed.push((el.name || el.id) + ": " + value + " &rarr; " + clamped);
}
});
if (fixed.length) {
showToast("Stored values outside the allowed range, adjusted:<br>" + fixed.join("<br>")
+ "<br><small>Save to keep them.</small>");
}
}
let invalidFieldReported = false;
form.addEventListener("invalid", function (event) {
const el = event.target;
if (!invalidFieldReported) { // only the first one: the rest would pile up toasts
invalidFieldReported = true;
setTimeout(() => { invalidFieldReported = false; }, 500);
const section = el.closest(".panel-section");
if (section) {
const link = document.querySelector('.side-link[data-target="' + section.id + '"]');
if (link) {
link.click(); // reuses the sidebar switcher (also shows the config view)
} else {
document.querySelectorAll(".panel-section").forEach((s) => s.classList.remove("active"));
section.classList.add("active");
document.getElementById("configuration").classList.remove("d-none");
}
}
showToast("<b>" + (el.name || el.id) + "</b>: " + (el.validationMessage || "invalid value"));
setTimeout(() => {
el.focus();
el.scrollIntoView({ block: "center", behavior: "smooth" });
}, 0);
}
}, true);
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+3 -4
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -27,11 +27,10 @@ namespace APRS_IS_Utils {
void upload(const String& line);
void connect();
void checkStatus();
String checkForStartingBytes(const String& packet);
void updateWiFiAPRSISDisplayInfo();
String buildPacketToUpload(const String& packet);
bool processReceivedLoRaMessage(const String& sender, const String& packet, bool thirdParty);
bool processReceivedLoRaMessage(const String& sender, const String& packet);
void processLoRaPacket(const String& packet);
String buildPacketToTx(const String& aprsisPacket, uint8_t packetType);
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+5 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -42,6 +42,7 @@ public:
double latitude;
double longitude;
String comment;
int rfCommentEveryXBeacons;
int interval;
String overlay;
String symbol;
@@ -64,6 +65,7 @@ public:
String filter;
bool messagesToRF;
bool objectsToRF;
bool is2Metadata;
};
class DIGI {
@@ -86,6 +88,7 @@ public:
int txCodingRate4;
long txSignalBandwidth;
int power;
bool cadActive;
};
class Display {
@@ -131,6 +134,7 @@ public:
bool enableSerial;
bool acceptOwn;
bool aprsBridgeActive;
bool kissProtocol; // true = KISS, false = TNC2 -- applies to both Serial and TCP server
};
class OTA {
+3 -4
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -24,10 +24,9 @@
namespace DIGI_Utils {
String buildPacket(const String& path, const String& packet, bool thirdParty, bool crossFreq);
String generateDigipeatedPacket(const String& packet, bool thirdParty);
String buildPacket(const String& path, const String& packet, bool crossFreq);
String generateDigipeatedPacket(const String& packet);
void processLoRaPacket(const String& packet);
void checkEcoMode();
}
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+2 -8
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -24,14 +24,8 @@
namespace GPS_Utils {
String getiGateLoRaBeaconPacket();
char *ax25_base91enc(char *s, uint8_t n, uint32_t v);
String encodeGPS(float latitude, float longitude, const String& overlay, const String& symbol);
void generateBeaconFirstPart();
void generateBeacons();
String decodeEncodedGPS(const String& packet);
String getReceivedGPS(const String& packet);
String getDistanceAndComment(const String& packet);
String buildDistanceAndComment(float latitude, float longitude, const String& comment);
void setup();
void getData();
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+2 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -24,6 +24,7 @@
namespace LoRa_Utils {
int validPower(int requested);
void setup();
void sendNewPacket(const String& newPacket);
String receivePacketFromSleep();
+47
View File
@@ -0,0 +1,47 @@
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef MAP_UTILS_H_
#define MAP_UTILS_H_
#include <Arduino.h>
#include <vector>
struct MapStation {
char callsign[10]; // "XX9XXX-NN" + null -> clave para deduplicar
float latitude; // grados decimales (+N / -S)
float longitude; // grados decimales (+E / -W)
char symbol[3]; // tabla + codigo APRS (ej "/>"), "" si no hay
int16_t rssi; // ultimo RSSI
float snr; // ultimo SNR
uint16_t count; // nro de paquetes oidos de esa estacion
char lastHeard[10]; // "HH:MM:SS" si hay NTP valido, "" si no
char path[96]; // last path received
uint32_t lastHeardMillis; // millis() -> ordenar / expirar (no se muestra)
bool heardDirect; // true si ALGUNA VEZ se oyo sin digipeaters (no se resetea)
};
namespace MAP_Utils {
void upsert(const String& callsign, float latitude, float longitude, const String& path, const String& symbol, int rssi, float snr);
String getStationsJson();
}
#endif
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+25 -1
View File
@@ -1,3 +1,21 @@
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include <SPI.h>
#include <WiFi.h>
@@ -25,9 +43,12 @@ private:
String _hostName = "";
std::vector<WiFiNetwork> _wifiNetworks;
volatile uint8_t _lastWiFiDisconnectReason = 0;
String _wifiAuthFailSSID = "";
int _findWiFiNetworkIndex(const String& ssid) const;
bool _connectWiFi(const WiFiNetwork& network);
bool _isWiFiAuthFailure() const;
void _processAPTimeout();
void _onNetworkEvent(arduino_event_id_t event, arduino_event_info_t /*info*/);
@@ -57,6 +78,7 @@ public:
bool disconnectWiFi();
String getWiFiSSID() const;
String getWiFiAPSSID() const;
String getWiFiAuthFailSSID() const;
IPAddress getWiFiIP() const;
IPAddress getWiFiAPIP() const;
wifi_mode_t getWiFiMode() const;
@@ -66,7 +88,6 @@ public:
// Ethernet methods
bool ethernetConnect(eth_phy_type_t type, uint8_t phy_addr, uint8_t mdc, uint8_t mdio, int power, eth_clock_mode_t clock_mode, bool use_mac_from_efuse = false);
bool setEthernetIP(const String& staticIP, const String& gateway, const String& subnet, const String& dns1, const String& dns2);
bool ethernetDisconnect();
IPAddress getEthernetIP() const;
String getEthernetMACAddress() const;
@@ -79,4 +100,7 @@ public:
bool isModemConnected() const;
bool isWifiAPActive() const;
// Check if any network interface was started (lwip TCP/IP stack is initialized)
bool hasActiveInterface() const;
};
+2 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -27,6 +27,7 @@ namespace NTP_Utils {
bool setup();
void update();
String getFormatedTime();
String getFormatedDate();
}
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+5 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -29,7 +29,7 @@
namespace POWER_Utils {
#ifdef VEXT_CTRL
#ifdef VEXT_CTRL_PIN
void vext_ctrl_ON();
void vext_ctrl_OFF();
#endif
@@ -46,6 +46,9 @@ namespace POWER_Utils {
void activateLoRa();
void deactivateLoRa();
bool begin(TwoWire &port);
#ifdef SOLAR_CHARGE_PIN
void checkSolarCharge();
#endif
void setup();
}
+3 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -24,7 +24,8 @@
namespace QUERY_Utils {
String process(const String& query, const String& station, bool queryFromAPRSIS, bool thirdParty);
bool isQuery(const String& message);
String process(const String& query, const String& station, bool queryFromAPRSIS, bool thirdParty);
}
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+6 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -19,12 +19,16 @@
#ifndef SYSLOG_H_
#define SYSLOG_H_
#include <APRSPacketLib.h>
#include <Arduino.h>
namespace SYSLOG_Utils {
void log(const uint8_t type ,const String& packet, const int rssi, const float snr, const int freqError);
void logLoRaRx(APRSPacket& aprsPacket, const String& packet, const int rssi, const float snr, const int freqError);
void logAPRSISTx(const String& packet);
void logLoRaTx(const String& packet);
void logCRCError(const String& packet, const int rssi, const float snr, const int freqError);
void setup();
}
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+41
View File
@@ -0,0 +1,41 @@
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
// Fan / thermal control adapted from KJ7NYE's LoRa_FieldOps_APRS_Tracker (GPLv3)
#ifndef THERMAL_UTILS_H_
#define THERMAL_UTILS_H_
#include "board_pinout.h"
#ifdef FAN_CTRL_PIN
namespace THERMAL_Utils {
void setup();
void monitor();
void onTxStart();
void onTxEnd();
void turnOffFan();
bool isTxBlocked();
}
#endif
#endif
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+6 -3
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -19,11 +19,12 @@
#ifndef UTILS_H_
#define UTILS_H_
#include <APRSPacketLib.h>
#include <Arduino.h>
class ReceivedPacket {
public:
String rxDate;
String rxTime;
String packet;
int RSSI;
@@ -39,7 +40,9 @@ namespace Utils {
void checkBeaconInterval();
void checkDisplayInterval();
void validateFreqs();
void typeOfPacket(const String& packet, const uint8_t packetType);
String padForDisplay(const String& text);
void updateLoRaPacketDisplayInfo(APRSPacket& aprsPacket, const uint8_t packetType);
void updateAPRSISPacketDisplayInfo(const String& packet);
void print(const String& text);
void println(const String& text);
void checkRebootMode();
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+2 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -36,6 +36,7 @@ namespace WX_Utils {
String generateHumString(const float sensorHum);
String generatePresString(const float sensorPres);
String readDataSensor();
float readBoardTemperature();
}
-34
View File
@@ -1,34 +0,0 @@
#!/usr/bin/env python
#
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
# This executable script is a thin wrapper around the main functionality
# in the esptool Python package
# When updating this script, please also update espefuse.py and espsecure.py
import contextlib
import os
import sys
if os.name != "nt":
# Linux/macOS: remove current script directory to avoid importing this file
# as a module; we want to import the installed esptool module instead
with contextlib.suppress(ValueError):
if sys.path[0].endswith("/bin"):
sys.path.pop(0)
sys.path.remove(os.path.dirname(sys.executable))
# Linux/macOS: delete imported module entry to force Python to load
# the module from scratch; this enables importing esptool module in
# other Python scripts
with contextlib.suppress(KeyError):
del sys.modules["esptool"]
import esptool
if __name__ == "__main__":
esptool._main()
File diff suppressed because it is too large. Load diff
@@ -1,9 +0,0 @@
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import esptool
if __name__ == "__main__":
esptool._main()
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-92
View File
@@ -1,92 +0,0 @@
# SPDX-FileCopyrightText: 2014-2023 Espressif Systems (Shanghai) CO LTD,
# other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import configparser
import os
CONFIG_OPTIONS = [
"timeout",
"chip_erase_timeout",
"max_timeout",
"sync_timeout",
"md5_timeout_per_mb",
"erase_region_timeout_per_mb",
"erase_write_timeout_per_mb",
"mem_end_rom_timeout",
"serial_write_timeout",
"connect_attempts",
"write_block_attempts",
"reset_delay",
"custom_reset_sequence",
]
def _validate_config_file(file_path, verbose=False):
if not os.path.exists(file_path):
return False
cfg = configparser.RawConfigParser()
try:
cfg.read(file_path, encoding="UTF-8")
# Only consider it a valid config file if it contains [esptool] section
if cfg.has_section("esptool"):
if verbose:
unknown_opts = list(set(cfg.options("esptool")) - set(CONFIG_OPTIONS))
unknown_opts.sort()
no_of_unknown_opts = len(unknown_opts)
if no_of_unknown_opts > 0:
suffix = "s" if no_of_unknown_opts > 1 else ""
print(
"Ignoring unknown config file option{}: {}".format(
suffix, ", ".join(unknown_opts)
)
)
return True
except (UnicodeDecodeError, configparser.Error) as e:
if verbose:
print(f"Ignoring invalid config file {file_path}: {e}")
return False
def _find_config_file(dir_path, verbose=False):
for candidate in ("esptool.cfg", "setup.cfg", "tox.ini"):
cfg_path = os.path.join(dir_path, candidate)
if _validate_config_file(cfg_path, verbose):
return cfg_path
return None
def load_config_file(verbose=False):
set_with_env_var = False
env_var_path = os.environ.get("ESPTOOL_CFGFILE")
if env_var_path is not None and _validate_config_file(env_var_path):
cfg_file_path = env_var_path
set_with_env_var = True
else:
home_dir = os.path.expanduser("~")
os_config_dir = (
f"{home_dir}/.config/esptool"
if os.name == "posix"
else f"{home_dir}/AppData/Local/esptool/"
)
# Search priority: 1) current dir, 2) OS specific config dir, 3) home dir
for dir_path in (os.getcwd(), os_config_dir, home_dir):
cfg_file_path = _find_config_file(dir_path, verbose)
if cfg_file_path:
break
cfg = configparser.ConfigParser()
cfg["esptool"] = {} # Create an empty esptool config for when no file is found
if cfg_file_path is not None:
# If config file is found and validated, read and parse it
cfg.read(cfg_file_path)
if verbose:
msg = " (set with ESPTOOL_CFGFILE)" if set_with_env_var else ""
print(
f"Loaded custom configuration from "
f"{os.path.abspath(cfg_file_path)}{msg}"
)
return cfg, cfg_file_path
File diff suppressed because it is too large. Load diff
-178
View File
@@ -1,178 +0,0 @@
# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import os
import struct
import time
from .util import FatalError
# Used for resetting into bootloader on Unix-like systems
if os.name != "nt":
import fcntl
import termios
# Constants used for terminal status lines reading/setting.
# Taken from pySerial's backend for IO:
# https://github.com/pyserial/pyserial/blob/master/serial/serialposix.py
TIOCMSET = getattr(termios, "TIOCMSET", 0x5418)
TIOCMGET = getattr(termios, "TIOCMGET", 0x5415)
TIOCM_DTR = getattr(termios, "TIOCM_DTR", 0x002)
TIOCM_RTS = getattr(termios, "TIOCM_RTS", 0x004)
DEFAULT_RESET_DELAY = 0.05 # default time to wait before releasing boot pin after reset
class ResetStrategy(object):
def __init__(self, port, reset_delay=DEFAULT_RESET_DELAY):
self.port = port
self.reset_delay = reset_delay
def __call__():
pass
def _setDTR(self, state):
self.port.setDTR(state)
def _setRTS(self, state):
self.port.setRTS(state)
# Work-around for adapters on Windows using the usbser.sys driver:
# generate a dummy change to DTR so that the set-control-line-state
# request is sent with the updated RTS state and the same DTR state
self.port.setDTR(self.port.dtr)
def _setDTRandRTS(self, dtr=False, rts=False):
status = struct.unpack(
"I", fcntl.ioctl(self.port.fileno(), TIOCMGET, struct.pack("I", 0))
)[0]
if dtr:
status |= TIOCM_DTR
else:
status &= ~TIOCM_DTR
if rts:
status |= TIOCM_RTS
else:
status &= ~TIOCM_RTS
fcntl.ioctl(self.port.fileno(), TIOCMSET, struct.pack("I", status))
class ClassicReset(ResetStrategy):
"""
Classic reset sequence, sets DTR and RTS lines sequentially.
"""
def __call__(self):
self._setDTR(False) # IO0=HIGH
self._setRTS(True) # EN=LOW, chip in reset
time.sleep(0.1)
self._setDTR(True) # IO0=LOW
self._setRTS(False) # EN=HIGH, chip out of reset
time.sleep(self.reset_delay)
self._setDTR(False) # IO0=HIGH, done
class UnixTightReset(ResetStrategy):
"""
UNIX-only reset sequence with custom implementation,
which allows setting DTR and RTS lines at the same time.
"""
def __call__(self):
self._setDTRandRTS(False, False)
self._setDTRandRTS(True, True)
self._setDTRandRTS(False, True) # IO0=HIGH & EN=LOW, chip in reset
time.sleep(0.1)
self._setDTRandRTS(True, False) # IO0=LOW & EN=HIGH, chip out of reset
time.sleep(self.reset_delay)
self._setDTRandRTS(False, False) # IO0=HIGH, done
self._setDTR(False) # Needed in some environments to ensure IO0=HIGH
class USBJTAGSerialReset(ResetStrategy):
"""
Custom reset sequence, which is required when the device
is connecting via its USB-JTAG-Serial peripheral.
"""
def __call__(self):
self._setRTS(False)
self._setDTR(False) # Idle
time.sleep(0.1)
self._setDTR(True) # Set IO0
self._setRTS(False)
time.sleep(0.1)
self._setRTS(True) # Reset. Calls inverted to go through (1,1) instead of (0,0)
self._setDTR(False)
self._setRTS(True) # RTS set as Windows only propagates DTR on RTS setting
time.sleep(0.1)
self._setDTR(False)
self._setRTS(False) # Chip out of reset
class HardReset(ResetStrategy):
"""
Reset sequence for hard resetting the chip.
Can be used to reset out of the bootloader or to restart a running app.
"""
def __init__(self, port, uses_usb_otg=False):
super().__init__(port)
self.uses_usb_otg = uses_usb_otg
def __call__(self):
self._setRTS(True) # EN->LOW
if self.uses_usb_otg:
# Give the chip some time to come out of reset,
# to be able to handle further DTR/RTS transitions
time.sleep(0.2)
self._setRTS(False)
time.sleep(0.2)
else:
time.sleep(0.1)
self._setRTS(False)
class CustomReset(ResetStrategy):
"""
Custom reset strategy defined with a string.
CustomReset object is created as "rst = CustomReset(port, seq_str)"
and can be later executed simply with "rst()"
The seq_str input string consists of individual commands divided by "|".
Commands (e.g. R0) are defined by a code (R) and an argument (0).
The commands are:
D: setDTR - 1=True / 0=False
R: setRTS - 1=True / 0=False
U: setDTRandRTS (Unix-only) - 0,0 / 0,1 / 1,0 / or 1,1
W: Wait (time delay) - positive float number
e.g.
"D0|R1|W0.1|D1|R0|W0.05|D0" represents the ClassicReset strategy
"U1,1|U0,1|W0.1|U1,0|W0.05|U0,0" represents the UnixTightReset strategy
"""
format_dict = {
"D": "self.port.setDTR({})",
"R": "self.port.setRTS({})",
"W": "time.sleep({})",
"U": "self._setDTRandRTS({})",
}
def __call__(self):
exec(self.constructed_strategy)
def __init__(self, port, seq_str):
super().__init__(port)
self.constructed_strategy = self._parse_string_to_seq(seq_str)
def _parse_string_to_seq(self, seq_str):
try:
cmds = seq_str.split("|")
fn_calls_list = [self.format_dict[cmd[0]].format(cmd[1:]) for cmd in cmds]
except Exception as e:
raise FatalError(f'Invalid "custom_reset_sequence" option format: {e}')
return "\n".join(fn_calls_list)
@@ -1,31 +0,0 @@
from .esp32 import ESP32ROM
from .esp32c2 import ESP32C2ROM
from .esp32c3 import ESP32C3ROM
from .esp32c6 import ESP32C6ROM
from .esp32c6beta import ESP32C6BETAROM
from .esp32h2 import ESP32H2ROM
from .esp32h2beta1 import ESP32H2BETA1ROM
from .esp32h2beta2 import ESP32H2BETA2ROM
from .esp32s2 import ESP32S2ROM
from .esp32s3 import ESP32S3ROM
from .esp32s3beta2 import ESP32S3BETA2ROM
from .esp8266 import ESP8266ROM
CHIP_DEFS = {
"esp8266": ESP8266ROM,
"esp32": ESP32ROM,
"esp32s2": ESP32S2ROM,
"esp32s3beta2": ESP32S3BETA2ROM,
"esp32s3": ESP32S3ROM,
"esp32c3": ESP32C3ROM,
"esp32c6beta": ESP32C6BETAROM,
"esp32h2beta1": ESP32H2BETA1ROM,
"esp32h2beta2": ESP32H2BETA2ROM,
"esp32c2": ESP32C2ROM,
"esp32c6": ESP32C6ROM,
"esp32h2": ESP32H2ROM,
}
CHIP_LIST = list(CHIP_DEFS.keys())
ROM_LIST = list(CHIP_DEFS.values())
@@ -1,393 +0,0 @@
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import struct
import time
from ..loader import ESPLoader
from ..util import FatalError, NotSupportedError
class ESP32ROM(ESPLoader):
"""Access class for ESP32 ROM bootloader"""
CHIP_NAME = "ESP32"
IMAGE_CHIP_ID = 0
IS_STUB = False
FPGA_SLOW_BOOT = True
CHIP_DETECT_MAGIC_VALUE = [0x00F01D83]
IROM_MAP_START = 0x400D0000
IROM_MAP_END = 0x40400000
DROM_MAP_START = 0x3F400000
DROM_MAP_END = 0x3F800000
# ESP32 uses a 4 byte status reply
STATUS_BYTES_LENGTH = 4
SPI_REG_BASE = 0x3FF42000
SPI_USR_OFFS = 0x1C
SPI_USR1_OFFS = 0x20
SPI_USR2_OFFS = 0x24
SPI_MOSI_DLEN_OFFS = 0x28
SPI_MISO_DLEN_OFFS = 0x2C
EFUSE_RD_REG_BASE = 0x3FF5A000
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + 0x18
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 7 # EFUSE_RD_DISABLE_DL_ENCRYPT
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_RD_REG_BASE # EFUSE_BLK0_WDATA0_REG
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7F << 20 # EFUSE_FLASH_CRYPT_CNT
EFUSE_RD_ABS_DONE_REG = EFUSE_RD_REG_BASE + 0x018
EFUSE_RD_ABS_DONE_0_MASK = 1 << 4
EFUSE_RD_ABS_DONE_1_MASK = 1 << 5
DR_REG_SYSCON_BASE = 0x3FF66000
APB_CTL_DATE_ADDR = DR_REG_SYSCON_BASE + 0x7C
APB_CTL_DATE_V = 0x1
APB_CTL_DATE_S = 31
SPI_W0_OFFS = 0x80
UART_CLKDIV_REG = 0x3FF40014
XTAL_CLK_DIVIDER = 1
RTCCALICFG1 = 0x3FF5F06C
TIMERS_RTC_CALI_VALUE = 0x01FFFFFF
TIMERS_RTC_CALI_VALUE_S = 7
FLASH_SIZES = {
"1MB": 0x00,
"2MB": 0x10,
"4MB": 0x20,
"8MB": 0x30,
"16MB": 0x40,
"32MB": 0x50,
"64MB": 0x60,
"128MB": 0x70,
}
FLASH_FREQUENCY = {
"80m": 0xF,
"40m": 0x0,
"26m": 0x1,
"20m": 0x2,
}
BOOTLOADER_FLASH_OFFSET = 0x1000
OVERRIDE_VDDSDIO_CHOICES = ["1.8V", "1.9V", "OFF"]
MEMORY_MAP = [
[0x00000000, 0x00010000, "PADDING"],
[0x3F400000, 0x3F800000, "DROM"],
[0x3F800000, 0x3FC00000, "EXTRAM_DATA"],
[0x3FF80000, 0x3FF82000, "RTC_DRAM"],
[0x3FF90000, 0x40000000, "BYTE_ACCESSIBLE"],
[0x3FFAE000, 0x40000000, "DRAM"],
[0x3FFE0000, 0x3FFFFFFC, "DIRAM_DRAM"],
[0x40000000, 0x40070000, "IROM"],
[0x40070000, 0x40078000, "CACHE_PRO"],
[0x40078000, 0x40080000, "CACHE_APP"],
[0x40080000, 0x400A0000, "IRAM"],
[0x400A0000, 0x400BFFFC, "DIRAM_IRAM"],
[0x400C0000, 0x400C2000, "RTC_IRAM"],
[0x400D0000, 0x40400000, "IROM"],
[0x50000000, 0x50002000, "RTC_DATA"],
]
FLASH_ENCRYPTED_WRITE_ALIGN = 32
""" Try to read the BLOCK1 (encryption key) and check if it is valid """
def is_flash_encryption_key_valid(self):
"""Bit 0 of efuse_rd_disable[3:0] is mapped to BLOCK1
this bit is at position 16 in EFUSE_BLK0_RDATA0_REG"""
word0 = self.read_efuse(0)
rd_disable = (word0 >> 16) & 0x1
# reading of BLOCK1 is NOT ALLOWED so we assume valid key is programmed
if rd_disable:
return True
else:
# reading of BLOCK1 is ALLOWED so we will read and verify for non-zero.
# When ESP32 has not generated AES/encryption key in BLOCK1,
# the contents will be readable and 0.
# If the flash encryption is enabled it is expected to have a valid
# non-zero key. We break out on first occurance of non-zero value
key_word = [0] * 7
for i in range(len(key_word)):
key_word[i] = self.read_efuse(14 + i)
# key is non-zero so break & return
if key_word[i] != 0:
return True
return False
def get_flash_crypt_config(self):
"""For flash encryption related commands we need to make sure
user has programmed all the relevant efuse correctly so before
writing encrypted write_flash_encrypt esptool will verify the values
of flash_crypt_config to be non zero if they are not read
protected. If the values are zero a warning will be printed
bit 3 in efuse_rd_disable[3:0] is mapped to flash_crypt_config
this bit is at position 19 in EFUSE_BLK0_RDATA0_REG"""
word0 = self.read_efuse(0)
rd_disable = (word0 >> 19) & 0x1
if rd_disable == 0:
"""we can read the flash_crypt_config efuse value
so go & read it (EFUSE_BLK0_RDATA5_REG[31:28])"""
word5 = self.read_efuse(5)
word5 = (word5 >> 28) & 0xF
return word5
else:
# if read of the efuse is disabled we assume it is set correctly
return 0xF
def get_encrypted_download_disabled(self):
return (
self.read_reg(self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG)
& self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT
)
def get_flash_encryption_enabled(self):
flash_crypt_cnt = (
self.read_reg(self.EFUSE_SPI_BOOT_CRYPT_CNT_REG)
& self.EFUSE_SPI_BOOT_CRYPT_CNT_MASK
)
# Flash encryption enabled when odd number of bits are set
return bin(flash_crypt_cnt).count("1") & 1 != 0
def get_secure_boot_enabled(self):
efuses = self.read_reg(self.EFUSE_RD_ABS_DONE_REG)
rev = self.get_chip_revision()
return efuses & self.EFUSE_RD_ABS_DONE_0_MASK or (
rev >= 300 and efuses & self.EFUSE_RD_ABS_DONE_1_MASK
)
def get_pkg_version(self):
word3 = self.read_efuse(3)
pkg_version = (word3 >> 9) & 0x07
pkg_version += ((word3 >> 2) & 0x1) << 3
return pkg_version
def get_chip_revision(self):
return self.get_major_chip_version() * 100 + self.get_minor_chip_version()
def get_minor_chip_version(self):
return (self.read_efuse(5) >> 24) & 0x3
def get_major_chip_version(self):
rev_bit0 = (self.read_efuse(3) >> 15) & 0x1
rev_bit1 = (self.read_efuse(5) >> 20) & 0x1
apb_ctl_date = self.read_reg(self.APB_CTL_DATE_ADDR)
rev_bit2 = (apb_ctl_date >> self.APB_CTL_DATE_S) & self.APB_CTL_DATE_V
combine_value = (rev_bit2 << 2) | (rev_bit1 << 1) | rev_bit0
revision = {
0: 0,
1: 1,
3: 2,
7: 3,
}.get(combine_value, 0)
return revision
def get_chip_description(self):
pkg_version = self.get_pkg_version()
major_rev = self.get_major_chip_version()
minor_rev = self.get_minor_chip_version()
rev3 = major_rev == 3
single_core = self.read_efuse(3) & (1 << 0) # CHIP_VER DIS_APP_CPU
chip_name = {
0: "ESP32-S0WDQ6" if single_core else "ESP32-D0WDQ6",
1: "ESP32-S0WD" if single_core else "ESP32-D0WD",
2: "ESP32-D2WD",
4: "ESP32-U4WDH",
5: "ESP32-PICO-V3" if rev3 else "ESP32-PICO-D4",
6: "ESP32-PICO-V3-02",
7: "ESP32-D0WDR2-V3",
}.get(pkg_version, "unknown ESP32")
# ESP32-D0WD-V3, ESP32-D0WDQ6-V3
if chip_name.startswith("ESP32-D0WD") and rev3:
chip_name += "-V3"
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
def get_chip_features(self):
features = ["WiFi"]
word3 = self.read_efuse(3)
# names of variables in this section are lowercase
# versions of EFUSE names as documented in TRM and
# ESP-IDF efuse_reg.h
chip_ver_dis_bt = word3 & (1 << 1)
if chip_ver_dis_bt == 0:
features += ["BT"]
chip_ver_dis_app_cpu = word3 & (1 << 0)
if chip_ver_dis_app_cpu:
features += ["Single Core"]
else:
features += ["Dual Core"]
chip_cpu_freq_rated = word3 & (1 << 13)
if chip_cpu_freq_rated:
chip_cpu_freq_low = word3 & (1 << 12)
if chip_cpu_freq_low:
features += ["160MHz"]
else:
features += ["240MHz"]
pkg_version = self.get_pkg_version()
if pkg_version in [2, 4, 5, 6]:
features += ["Embedded Flash"]
if pkg_version == 6:
features += ["Embedded PSRAM"]
word4 = self.read_efuse(4)
adc_vref = (word4 >> 8) & 0x1F
if adc_vref:
features += ["VRef calibration in efuse"]
blk3_part_res = word3 >> 14 & 0x1
if blk3_part_res:
features += ["BLK3 partially reserved"]
word6 = self.read_efuse(6)
coding_scheme = word6 & 0x3
features += [
"Coding Scheme %s"
% {0: "None", 1: "3/4", 2: "Repeat (UNSUPPORTED)", 3: "Invalid"}[
coding_scheme
]
]
return features
def read_efuse(self, n):
"""Read the nth word of the ESP3x EFUSE region."""
return self.read_reg(self.EFUSE_RD_REG_BASE + (4 * n))
def chip_id(self):
raise NotSupportedError(self, "chip_id")
def read_mac(self):
"""Read MAC from EFUSE region"""
words = [self.read_efuse(2), self.read_efuse(1)]
bitstring = struct.pack(">II", *words)
bitstring = bitstring[2:8] # trim the 2 byte CRC
return tuple(bitstring)
def get_erase_size(self, offset, size):
return size
def override_vddsdio(self, new_voltage):
new_voltage = new_voltage.upper()
if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES:
raise FatalError(
"The only accepted VDDSDIO overrides are '1.8V', '1.9V' and 'OFF'"
)
RTC_CNTL_SDIO_CONF_REG = 0x3FF48074
RTC_CNTL_XPD_SDIO_REG = 1 << 31
RTC_CNTL_DREFH_SDIO_M = 3 << 29
RTC_CNTL_DREFM_SDIO_M = 3 << 27
RTC_CNTL_DREFL_SDIO_M = 3 << 25
# RTC_CNTL_SDIO_TIEH = (1 << 23)
# not used here, setting TIEH=1 would set 3.3V output,
# not safe for esptool.py to do
RTC_CNTL_SDIO_FORCE = 1 << 22
RTC_CNTL_SDIO_PD_EN = 1 << 21
reg_val = RTC_CNTL_SDIO_FORCE # override efuse setting
reg_val |= RTC_CNTL_SDIO_PD_EN
if new_voltage != "OFF":
reg_val |= RTC_CNTL_XPD_SDIO_REG # enable internal LDO
if new_voltage == "1.9V":
reg_val |= (
RTC_CNTL_DREFH_SDIO_M | RTC_CNTL_DREFM_SDIO_M | RTC_CNTL_DREFL_SDIO_M
) # boost voltage
self.write_reg(RTC_CNTL_SDIO_CONF_REG, reg_val)
print("VDDSDIO regulator set to %s" % new_voltage)
def read_flash_slow(self, offset, length, progress_fn):
BLOCK_LEN = 64 # ROM read limit per command (this limit is why it's so slow)
data = b""
while len(data) < length:
block_len = min(BLOCK_LEN, length - len(data))
r = self.check_command(
"read flash block",
self.ESP_READ_FLASH_SLOW,
struct.pack("<II", offset + len(data), block_len),
)
if len(r) < block_len:
raise FatalError(
"Expected %d byte block, got %d bytes. Serial errors?"
% (block_len, len(r))
)
# command always returns 64 byte buffer,
# regardless of how many bytes were actually read from flash
data += r[:block_len]
if progress_fn and (len(data) % 1024 == 0 or len(data) == length):
progress_fn(len(data), length)
return data
def get_rom_cal_crystal_freq(self):
"""
Get the crystal frequency calculated by the ROM
"""
# - Simulate the calculation in the ROM to get the XTAL frequency
# calculated by the ROM
cali_val = (
self.read_reg(self.RTCCALICFG1) >> self.TIMERS_RTC_CALI_VALUE_S
) & self.TIMERS_RTC_CALI_VALUE
clk_8M_freq = self.read_efuse(4) & (0xFF) # EFUSE_RD_CK8M_FREQ
rom_calculated_freq = cali_val * 15625 * clk_8M_freq / 40
return rom_calculated_freq
def change_baud(self, baud):
# It's a workaround to avoid esp32 CK_8M frequency drift.
rom_calculated_freq = self.get_rom_cal_crystal_freq()
valid_freq = 40000000 if rom_calculated_freq > 33000000 else 26000000
false_rom_baud = int(baud * rom_calculated_freq // valid_freq)
print(f"Changing baud rate to {baud}")
self.command(self.ESP_CHANGE_BAUDRATE, struct.pack("<II", false_rom_baud, 0))
print("Changed.")
self._set_port_baudrate(baud)
time.sleep(0.05) # get rid of garbage sent during baud rate change
self.flush_input()
class ESP32StubLoader(ESP32ROM):
"""Access class for ESP32 stub loader, runs on top of ROM."""
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
IS_STUB = True
def __init__(self, rom_loader):
self.secure_download_mode = rom_loader.secure_download_mode
self._port = rom_loader._port
self._trace_enabled = rom_loader._trace_enabled
self.cache = rom_loader.cache
self.flush_input() # resets _slip_reader
def change_baud(self, baud):
ESPLoader.change_baud(self, baud)
ESP32ROM.STUB_CLASS = ESP32StubLoader
@@ -1,165 +0,0 @@
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import struct
import time
from .esp32c3 import ESP32C3ROM
from ..loader import ESPLoader
class ESP32C2ROM(ESP32C3ROM):
CHIP_NAME = "ESP32-C2"
IMAGE_CHIP_ID = 12
IROM_MAP_START = 0x42000000
IROM_MAP_END = 0x42400000
DROM_MAP_START = 0x3C000000
DROM_MAP_END = 0x3C400000
# Magic value for ESP32C2 ECO0 and ECO1 respectively
CHIP_DETECT_MAGIC_VALUE = [0x6F51306F, 0x7C41A06F]
EFUSE_BASE = 0x60008800
EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x040
MAC_EFUSE_REG = EFUSE_BASE + 0x040
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x30
EFUSE_SECURE_BOOT_EN_MASK = 1 << 21
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x30
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_BASE + 0x30
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 6
EFUSE_XTS_KEY_LENGTH_256_REG = EFUSE_BASE + 0x30
EFUSE_XTS_KEY_LENGTH_256 = 1 << 10
EFUSE_BLOCK_KEY0_REG = EFUSE_BASE + 0x60
EFUSE_RD_DIS_REG = EFUSE_BASE + 0x30
EFUSE_RD_DIS = 3
FLASH_FREQUENCY = {
"60m": 0xF,
"30m": 0x0,
"20m": 0x1,
"15m": 0x2,
}
MEMORY_MAP = [
[0x00000000, 0x00010000, "PADDING"],
[0x3C000000, 0x3C400000, "DROM"],
[0x3FCA0000, 0x3FCE0000, "DRAM"],
[0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"],
[0x3FF00000, 0x3FF50000, "DROM_MASK"],
[0x40000000, 0x40090000, "IROM_MASK"],
[0x42000000, 0x42400000, "IROM"],
[0x4037C000, 0x403C0000, "IRAM"],
]
def get_pkg_version(self):
num_word = 1
return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 22) & 0x07
def get_chip_description(self):
chip_name = {
0: "ESP32-C2",
1: "ESP32-C2",
}.get(self.get_pkg_version(), "unknown ESP32-C2")
major_rev = self.get_major_chip_version()
minor_rev = self.get_minor_chip_version()
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
def get_minor_chip_version(self):
num_word = 1
return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 16) & 0xF
def get_major_chip_version(self):
num_word = 1
return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 20) & 0x3
def get_crystal_freq(self):
# The crystal detection algorithm of ESP32/ESP8266 works for ESP32-C2 as well.
return ESPLoader.get_crystal_freq(self)
def change_baud(self, baud):
rom_with_26M_XTAL = not self.IS_STUB and self.get_crystal_freq() == 26
if rom_with_26M_XTAL:
# The code is copied over from ESPLoader.change_baud().
# Probably this is just a temporary solution until the next chip revision.
# The ROM code thinks it uses a 40 MHz XTAL. Recompute the baud rate
# in order to trick the ROM code to set the correct baud rate for
# a 26 MHz XTAL.
false_rom_baud = baud * 40 // 26
print(f"Changing baud rate to {baud}")
self.command(
self.ESP_CHANGE_BAUDRATE, struct.pack("<II", false_rom_baud, 0)
)
print("Changed.")
self._set_port_baudrate(baud)
time.sleep(0.05) # get rid of garbage sent during baud rate change
self.flush_input()
else:
ESPLoader.change_baud(self, baud)
def _post_connect(self):
# ESP32C2 ECO0 is no longer supported by the flasher stub
if self.get_chip_revision() == 0:
self.stub_is_disabled = True
self.IS_STUB = False
""" Try to read (encryption key) and check if it is valid """
def is_flash_encryption_key_valid(self):
key_len_256 = (
self.read_reg(self.EFUSE_XTS_KEY_LENGTH_256_REG)
& self.EFUSE_XTS_KEY_LENGTH_256
)
word0 = self.read_reg(self.EFUSE_RD_DIS_REG) & self.EFUSE_RD_DIS
rd_disable = word0 == 3 if key_len_256 else word0 == 1
# reading of BLOCK3 is NOT ALLOWED so we assume valid key is programmed
if rd_disable:
return True
else:
# reading of BLOCK3 is ALLOWED so we will read and verify for non-zero.
# When chip has not generated AES/encryption key in BLOCK3,
# the contents will be readable and 0.
# If the flash encryption is enabled it is expected to have a valid
# non-zero key. We break out on first occurance of non-zero value
key_word = [0] * 7 if key_len_256 else [0] * 3
for i in range(len(key_word)):
key_word[i] = self.read_reg(self.EFUSE_BLOCK_KEY0_REG + i * 4)
# key is non-zero so break & return
if key_word[i] != 0:
return True
return False
class ESP32C2StubLoader(ESP32C2ROM):
"""Access class for ESP32C2 stub loader, runs on top of ROM.
(Basically the same as ESP32StubLoader, but different base class.
Can possibly be made into a mixin.)
"""
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
IS_STUB = True
def __init__(self, rom_loader):
self.secure_download_mode = rom_loader.secure_download_mode
self._port = rom_loader._port
self._trace_enabled = rom_loader._trace_enabled
self.cache = rom_loader.cache
self.flush_input() # resets _slip_reader
ESP32C2ROM.STUB_CLASS = ESP32C2StubLoader
@@ -1,210 +0,0 @@
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import struct
from .esp32 import ESP32ROM
from ..loader import ESPLoader
from ..util import FatalError, NotImplementedInROMError
class ESP32C3ROM(ESP32ROM):
CHIP_NAME = "ESP32-C3"
IMAGE_CHIP_ID = 5
FPGA_SLOW_BOOT = False
IROM_MAP_START = 0x42000000
IROM_MAP_END = 0x42800000
DROM_MAP_START = 0x3C000000
DROM_MAP_END = 0x3C800000
SPI_REG_BASE = 0x60002000
SPI_USR_OFFS = 0x18
SPI_USR1_OFFS = 0x1C
SPI_USR2_OFFS = 0x20
SPI_MOSI_DLEN_OFFS = 0x24
SPI_MISO_DLEN_OFFS = 0x28
SPI_W0_OFFS = 0x58
BOOTLOADER_FLASH_OFFSET = 0x0
# Magic value for ESP32C3 eco 1+2 and ESP32C3 eco3 respectivly
CHIP_DETECT_MAGIC_VALUE = [0x6921506F, 0x1B31506F]
UART_DATE_REG_ADDR = 0x60000000 + 0x7C
UART_CLKDIV_REG = 0x60000014
EFUSE_BASE = 0x60008800
EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
MAC_EFUSE_REG = EFUSE_BASE + 0x044
EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
EFUSE_PURPOSE_KEY0_SHIFT = 24
EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
EFUSE_PURPOSE_KEY1_SHIFT = 28
EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY2_SHIFT = 0
EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY3_SHIFT = 4
EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY4_SHIFT = 8
EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY5_SHIFT = 12
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
PURPOSE_VAL_XTS_AES128_KEY = 4
SUPPORTS_ENCRYPTED_FLASH = True
FLASH_ENCRYPTED_WRITE_ALIGN = 16
UARTDEV_BUF_NO = 0x3FCDF07C # Variable in ROM .bss which indicates the port in use
UARTDEV_BUF_NO_USB_JTAG_SERIAL = 3 # The above var when USB-JTAG/Serial is used
RTC_CNTL_WDT_WKEY = 0x50D83AA1
RTCCNTL_BASE_REG = 0x60008000
RTC_CNTL_WDTCONFIG0_REG = RTCCNTL_BASE_REG + 0x0090
RTC_CNTL_WDTWPROTECT_REG = RTCCNTL_BASE_REG + 0x00A8
MEMORY_MAP = [
[0x00000000, 0x00010000, "PADDING"],
[0x3C000000, 0x3C800000, "DROM"],
[0x3FC80000, 0x3FCE0000, "DRAM"],
[0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"],
[0x3FF00000, 0x3FF20000, "DROM_MASK"],
[0x40000000, 0x40060000, "IROM_MASK"],
[0x42000000, 0x42800000, "IROM"],
[0x4037C000, 0x403E0000, "IRAM"],
[0x50000000, 0x50002000, "RTC_IRAM"],
[0x50000000, 0x50002000, "RTC_DRAM"],
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
]
def get_pkg_version(self):
num_word = 3
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
def get_minor_chip_version(self):
hi_num_word = 5
hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
low_num_word = 3
low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
return (hi << 3) + low
def get_major_chip_version(self):
num_word = 5
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
def get_chip_description(self):
chip_name = {
0: "ESP32-C3",
}.get(self.get_pkg_version(), "unknown ESP32-C3")
major_rev = self.get_major_chip_version()
minor_rev = self.get_minor_chip_version()
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
def get_chip_features(self):
return ["WiFi", "BLE"]
def get_crystal_freq(self):
# ESP32C3 XTAL is fixed to 40MHz
return 40
def override_vddsdio(self, new_voltage):
raise NotImplementedInROMError(
"VDD_SDIO overrides are not supported for ESP32-C3"
)
def read_mac(self):
mac0 = self.read_reg(self.MAC_EFUSE_REG)
mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
bitstring = struct.pack(">II", mac1, mac0)[2:]
return tuple(bitstring)
def get_flash_crypt_config(self):
return None # doesn't exist on ESP32-C3
def get_secure_boot_enabled(self):
return (
self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
& self.EFUSE_SECURE_BOOT_EN_MASK
)
def get_key_block_purpose(self, key_block):
if key_block < 0 or key_block > 5:
raise FatalError("Valid key block numbers must be in range 0-5")
reg, shift = [
(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
(self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
(self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
(self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
(self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
(self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
][key_block]
return (self.read_reg(reg) >> shift) & 0xF
def is_flash_encryption_key_valid(self):
# Need to see an AES-128 key
purposes = [self.get_key_block_purpose(b) for b in range(6)]
return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
def change_baud(self, baud):
ESPLoader.change_baud(self, baud)
def uses_usb_jtag_serial(self):
"""
Check the UARTDEV_BUF_NO register to see if USB-JTAG/Serial is being used
"""
if self.secure_download_mode:
return False # Can't detect USB-JTAG/Serial in secure download mode
return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_JTAG_SERIAL
def disable_rtc_watchdog(self):
# When USB-JTAG/Serial is used, the RTC watchdog is not reset
# and can then reset the board during flashing. Disable it.
if self.uses_usb_jtag_serial():
self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY)
self.write_reg(self.RTC_CNTL_WDTCONFIG0_REG, 0)
self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0)
def _post_connect(self):
if not self.sync_stub_detected: # Don't run if stub is reused
self.disable_rtc_watchdog()
class ESP32C3StubLoader(ESP32C3ROM):
"""Access class for ESP32C3 stub loader, runs on top of ROM.
(Basically the same as ESP32StubLoader, but different base class.
Can possibly be made into a mixin.)
"""
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
IS_STUB = True
def __init__(self, rom_loader):
self.secure_download_mode = rom_loader.secure_download_mode
self._port = rom_loader._port
self._trace_enabled = rom_loader._trace_enabled
self.cache = rom_loader.cache
self.flush_input() # resets _slip_reader
ESP32C3ROM.STUB_CLASS = ESP32C3StubLoader
@@ -1,189 +0,0 @@
# SPDX-FileCopyrightText: 2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import struct
from .esp32c3 import ESP32C3ROM
from ..util import FatalError, NotImplementedInROMError
class ESP32C6ROM(ESP32C3ROM):
CHIP_NAME = "ESP32-C6"
IMAGE_CHIP_ID = 13
FPGA_SLOW_BOOT = False
IROM_MAP_START = 0x42000000
IROM_MAP_END = 0x42800000
DROM_MAP_START = 0x42800000
DROM_MAP_END = 0x43000000
BOOTLOADER_FLASH_OFFSET = 0x0
# Magic value for ESP32C6
CHIP_DETECT_MAGIC_VALUE = [0x2CE0806F]
SPI_REG_BASE = 0x60003000
SPI_USR_OFFS = 0x18
SPI_USR1_OFFS = 0x1C
SPI_USR2_OFFS = 0x20
SPI_MOSI_DLEN_OFFS = 0x24
SPI_MISO_DLEN_OFFS = 0x28
SPI_W0_OFFS = 0x58
UART_DATE_REG_ADDR = 0x60000000 + 0x7C
EFUSE_BASE = 0x600B0800
EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
MAC_EFUSE_REG = EFUSE_BASE + 0x044
EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
EFUSE_PURPOSE_KEY0_SHIFT = 24
EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
EFUSE_PURPOSE_KEY1_SHIFT = 28
EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY2_SHIFT = 0
EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY3_SHIFT = 4
EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY4_SHIFT = 8
EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY5_SHIFT = 12
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
PURPOSE_VAL_XTS_AES128_KEY = 4
SUPPORTS_ENCRYPTED_FLASH = True
FLASH_ENCRYPTED_WRITE_ALIGN = 16
UARTDEV_BUF_NO = 0x4087F580 # Variable in ROM .bss which indicates the port in use
UARTDEV_BUF_NO_USB_JTAG_SERIAL = 3 # The above var when USB-JTAG/Serial is used
DR_REG_LP_WDT_BASE = 0x600B1C00
RTC_CNTL_WDTCONFIG0_REG = DR_REG_LP_WDT_BASE + 0x0 # LP_WDT_RWDT_CONFIG0_REG
RTC_CNTL_WDTWPROTECT_REG = DR_REG_LP_WDT_BASE + 0x0018 # LP_WDT_RWDT_WPROTECT_REG
FLASH_FREQUENCY = {
"80m": 0x0, # workaround for wrong mspi HS div value in ROM
"40m": 0x0,
"20m": 0x2,
}
MEMORY_MAP = [
[0x00000000, 0x00010000, "PADDING"],
[0x42800000, 0x43000000, "DROM"],
[0x40800000, 0x40880000, "DRAM"],
[0x40800000, 0x40880000, "BYTE_ACCESSIBLE"],
[0x4004AC00, 0x40050000, "DROM_MASK"],
[0x40000000, 0x4004AC00, "IROM_MASK"],
[0x42000000, 0x42800000, "IROM"],
[0x40800000, 0x40880000, "IRAM"],
[0x50000000, 0x50004000, "RTC_IRAM"],
[0x50000000, 0x50004000, "RTC_DRAM"],
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
]
def get_pkg_version(self):
num_word = 3
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
def get_minor_chip_version(self):
hi_num_word = 5
hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
low_num_word = 3
low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
return (hi << 3) + low
def get_major_chip_version(self):
num_word = 5
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
def get_chip_description(self):
chip_name = {
0: "ESP32-C6",
}.get(self.get_pkg_version(), "unknown ESP32-C6")
major_rev = self.get_major_chip_version()
minor_rev = self.get_minor_chip_version()
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
def get_chip_features(self):
return ["WiFi 6", "BT 5", "IEEE802.15.4"]
def get_crystal_freq(self):
# ESP32C6 XTAL is fixed to 40MHz
return 40
def override_vddsdio(self, new_voltage):
raise NotImplementedInROMError(
"VDD_SDIO overrides are not supported for ESP32-C6"
)
def read_mac(self):
mac0 = self.read_reg(self.MAC_EFUSE_REG)
mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
bitstring = struct.pack(">II", mac1, mac0)[2:]
return tuple(bitstring)
def get_flash_crypt_config(self):
return None # doesn't exist on ESP32-C6
def get_secure_boot_enabled(self):
return (
self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
& self.EFUSE_SECURE_BOOT_EN_MASK
)
def get_key_block_purpose(self, key_block):
if key_block < 0 or key_block > 5:
raise FatalError("Valid key block numbers must be in range 0-5")
reg, shift = [
(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
(self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
(self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
(self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
(self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
(self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
][key_block]
return (self.read_reg(reg) >> shift) & 0xF
def is_flash_encryption_key_valid(self):
# Need to see an AES-128 key
purposes = [self.get_key_block_purpose(b) for b in range(6)]
return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
class ESP32C6StubLoader(ESP32C6ROM):
"""Access class for ESP32C6 stub loader, runs on top of ROM.
(Basically the same as ESP32StubLoader, but different base class.
Can possibly be made into a mixin.)
"""
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
IS_STUB = True
def __init__(self, rom_loader):
self.secure_download_mode = rom_loader.secure_download_mode
self._port = rom_loader._port
self._trace_enabled = rom_loader._trace_enabled
self.cache = rom_loader.cache
self.flush_input() # resets _slip_reader
ESP32C6ROM.STUB_CLASS = ESP32C6StubLoader
@@ -1,26 +0,0 @@
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
from .esp32c3 import ESP32C3ROM
class ESP32C6BETAROM(ESP32C3ROM):
CHIP_NAME = "ESP32-C6(beta)"
IMAGE_CHIP_ID = 7
CHIP_DETECT_MAGIC_VALUE = [0x0DA1806F]
UART_DATE_REG_ADDR = 0x00000500
def get_chip_description(self):
chip_name = {
0: "ESP32-C6",
}.get(self.get_pkg_version(), "unknown ESP32-C6")
major_rev = self.get_major_chip_version()
minor_rev = self.get_minor_chip_version()
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
def _post_connect(self):
pass
@@ -1,65 +0,0 @@
# SPDX-FileCopyrightText: 2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
from .esp32c6 import ESP32C6ROM
class ESP32H2ROM(ESP32C6ROM):
CHIP_NAME = "ESP32-H2"
IMAGE_CHIP_ID = 16
# Magic value for ESP32H2
CHIP_DETECT_MAGIC_VALUE = [0xD7B73E80]
FLASH_FREQUENCY = {
"48m": 0xF,
"24m": 0x0,
"16m": 0x1,
"12m": 0x2,
}
def get_pkg_version(self):
num_word = 3
block1_addr = self.EFUSE_BASE + 0x044
word3 = self.read_reg(block1_addr + (4 * num_word))
pkg_version = (word3 >> 21) & 0x0F
return pkg_version
def get_chip_description(self):
chip_name = {
0: "ESP32-H2",
}.get(self.get_pkg_version(), "unknown ESP32-H2")
major_rev = self.get_major_chip_version()
minor_rev = self.get_minor_chip_version()
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
def get_chip_features(self):
return ["BLE"]
def get_crystal_freq(self):
# ESP32H2 XTAL is fixed to 32MHz
return 32
class ESP32H2StubLoader(ESP32H2ROM):
"""Access class for ESP32H2 stub loader, runs on top of ROM.
(Basically the same as ESP32StubLoader, but different base class.
Can possibly be made into a mixin.)
"""
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
IS_STUB = True
def __init__(self, rom_loader):
self.secure_download_mode = rom_loader.secure_download_mode
self._port = rom_loader._port
self._trace_enabled = rom_loader._trace_enabled
self.cache = rom_loader.cache
self.flush_input() # resets _slip_reader
ESP32H2ROM.STUB_CLASS = ESP32H2StubLoader
@@ -1,164 +0,0 @@
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import struct
from .esp32c3 import ESP32C3ROM
from ..util import FatalError, NotImplementedInROMError
class ESP32H2BETA1ROM(ESP32C3ROM):
CHIP_NAME = "ESP32-H2(beta1)"
IMAGE_CHIP_ID = 10
IROM_MAP_START = 0x42000000
IROM_MAP_END = 0x42800000
DROM_MAP_START = 0x3C000000
DROM_MAP_END = 0x3C800000
SPI_REG_BASE = 0x60002000
SPI_USR_OFFS = 0x18
SPI_USR1_OFFS = 0x1C
SPI_USR2_OFFS = 0x20
SPI_MOSI_DLEN_OFFS = 0x24
SPI_MISO_DLEN_OFFS = 0x28
SPI_W0_OFFS = 0x58
BOOTLOADER_FLASH_OFFSET = 0x0
CHIP_DETECT_MAGIC_VALUE = [0xCA26CC22]
UART_DATE_REG_ADDR = 0x60000000 + 0x7C
EFUSE_BASE = 0x6001A000
EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
MAC_EFUSE_REG = EFUSE_BASE + 0x044
EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
EFUSE_PURPOSE_KEY0_SHIFT = 24
EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
EFUSE_PURPOSE_KEY1_SHIFT = 28
EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY2_SHIFT = 0
EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY3_SHIFT = 4
EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY4_SHIFT = 8
EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY5_SHIFT = 12
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
PURPOSE_VAL_XTS_AES128_KEY = 4
SUPPORTS_ENCRYPTED_FLASH = True
FLASH_ENCRYPTED_WRITE_ALIGN = 16
MEMORY_MAP = []
FLASH_FREQUENCY = {
"48m": 0xF,
"24m": 0x0,
"16m": 0x1,
"12m": 0x2,
}
def get_pkg_version(self):
num_word = 3
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x0F
def get_minor_chip_version(self):
hi_num_word = 5
hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
low_num_word = 3
low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
return (hi << 3) + low
def get_major_chip_version(self):
num_word = 5
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
def get_chip_description(self):
chip_name = {
0: "ESP32-H2",
}.get(self.get_pkg_version(), "unknown ESP32-H2")
major_rev = self.get_major_chip_version()
minor_rev = self.get_minor_chip_version()
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
def get_chip_features(self):
return ["BLE", "IEEE802.15.4"]
def get_crystal_freq(self):
return 32
def override_vddsdio(self, new_voltage):
raise NotImplementedInROMError(
"VDD_SDIO overrides are not supported for ESP32-H2"
)
def read_mac(self):
mac0 = self.read_reg(self.MAC_EFUSE_REG)
mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
bitstring = struct.pack(">II", mac1, mac0)[2:]
return tuple(bitstring)
def get_flash_crypt_config(self):
return None # doesn't exist on ESP32-H2
def get_key_block_purpose(self, key_block):
if key_block < 0 or key_block > 5:
raise FatalError("Valid key block numbers must be in range 0-5")
reg, shift = [
(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
(self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
(self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
(self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
(self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
(self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
][key_block]
return (self.read_reg(reg) >> shift) & 0xF
def is_flash_encryption_key_valid(self):
# Need to see an AES-128 key
purposes = [self.get_key_block_purpose(b) for b in range(6)]
return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
def _post_connect(self):
pass
class ESP32H2BETA1StubLoader(ESP32H2BETA1ROM):
"""Access class for ESP32H2BETA1 stub loader, runs on top of ROM.
(Basically the same as ESP32StubLoader, but different base class.
Can possibly be made into a mixin.)
"""
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
IS_STUB = True
def __init__(self, rom_loader):
self.secure_download_mode = rom_loader.secure_download_mode
self._port = rom_loader._port
self._trace_enabled = rom_loader._trace_enabled
self.cache = rom_loader.cache
self.flush_input() # resets _slip_reader
ESP32H2BETA1ROM.STUB_CLASS = ESP32H2BETA1StubLoader
@@ -1,43 +0,0 @@
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
from .esp32h2beta1 import ESP32H2BETA1ROM
class ESP32H2BETA2ROM(ESP32H2BETA1ROM):
CHIP_NAME = "ESP32-H2(beta2)"
IMAGE_CHIP_ID = 14
CHIP_DETECT_MAGIC_VALUE = [0x6881B06F]
def get_chip_description(self):
chip_name = {
1: "ESP32-H2(beta2)",
}.get(self.get_pkg_version(), "unknown ESP32-H2")
major_rev = self.get_major_chip_version()
minor_rev = self.get_minor_chip_version()
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
class ESP32H2BETA2StubLoader(ESP32H2BETA2ROM):
"""Access class for ESP32H2BETA2 stub loader, runs on top of ROM.
(Basically the same as ESP32StubLoader, but different base class.
Can possibly be made into a mixin.)
"""
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
IS_STUB = True
def __init__(self, rom_loader):
self.secure_download_mode = rom_loader.secure_download_mode
self._port = rom_loader._port
self._trace_enabled = rom_loader._trace_enabled
self.cache = rom_loader.cache
self.flush_input() # resets _slip_reader
ESP32H2BETA2ROM.STUB_CLASS = ESP32H2BETA2StubLoader
@@ -1,305 +0,0 @@
# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import os
import struct
from .esp32 import ESP32ROM
from ..loader import ESPLoader
from ..reset import HardReset
from ..util import FatalError, NotImplementedInROMError
class ESP32S2ROM(ESP32ROM):
CHIP_NAME = "ESP32-S2"
IMAGE_CHIP_ID = 2
FPGA_SLOW_BOOT = False
IROM_MAP_START = 0x40080000
IROM_MAP_END = 0x40B80000
DROM_MAP_START = 0x3F000000
DROM_MAP_END = 0x3F3F0000
CHIP_DETECT_MAGIC_VALUE = [0x000007C6]
SPI_REG_BASE = 0x3F402000
SPI_USR_OFFS = 0x18
SPI_USR1_OFFS = 0x1C
SPI_USR2_OFFS = 0x20
SPI_MOSI_DLEN_OFFS = 0x24
SPI_MISO_DLEN_OFFS = 0x28
SPI_W0_OFFS = 0x58
MAC_EFUSE_REG = 0x3F41A044 # ESP32-S2 has special block for MAC efuses
UART_CLKDIV_REG = 0x3F400014
SUPPORTS_ENCRYPTED_FLASH = True
FLASH_ENCRYPTED_WRITE_ALIGN = 16
# todo: use espefuse APIs to get this info
EFUSE_BASE = 0x3F41A000
EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x05C
EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
EFUSE_PURPOSE_KEY0_SHIFT = 24
EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
EFUSE_PURPOSE_KEY1_SHIFT = 28
EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY2_SHIFT = 0
EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY3_SHIFT = 4
EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY4_SHIFT = 8
EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY5_SHIFT = 12
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 19
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
EFUSE_RD_REPEAT_DATA3_REG = EFUSE_BASE + 0x3C
EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK = 1 << 9
PURPOSE_VAL_XTS_AES256_KEY_1 = 2
PURPOSE_VAL_XTS_AES256_KEY_2 = 3
PURPOSE_VAL_XTS_AES128_KEY = 4
UARTDEV_BUF_NO = 0x3FFFFD14 # Variable in ROM .bss which indicates the port in use
UARTDEV_BUF_NO_USB_OTG = 2 # Value of the above indicating that USB-OTG is in use
USB_RAM_BLOCK = 0x800 # Max block size USB-OTG is used
GPIO_STRAP_REG = 0x3F404038
GPIO_STRAP_SPI_BOOT_MASK = 0x8 # Not download mode
RTC_CNTL_OPTION1_REG = 0x3F408128
RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1 # Is download mode forced over USB?
MEMORY_MAP = [
[0x00000000, 0x00010000, "PADDING"],
[0x3F000000, 0x3FF80000, "DROM"],
[0x3F500000, 0x3FF80000, "EXTRAM_DATA"],
[0x3FF9E000, 0x3FFA0000, "RTC_DRAM"],
[0x3FF9E000, 0x40000000, "BYTE_ACCESSIBLE"],
[0x3FF9E000, 0x40072000, "MEM_INTERNAL"],
[0x3FFB0000, 0x40000000, "DRAM"],
[0x40000000, 0x4001A100, "IROM_MASK"],
[0x40020000, 0x40070000, "IRAM"],
[0x40070000, 0x40072000, "RTC_IRAM"],
[0x40080000, 0x40800000, "IROM"],
[0x50000000, 0x50002000, "RTC_DATA"],
]
def get_pkg_version(self):
num_word = 4
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
def get_minor_chip_version(self):
hi_num_word = 3
hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 20) & 0x01
low_num_word = 4
low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 4) & 0x07
return (hi << 3) + low
def get_major_chip_version(self):
num_word = 3
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x03
def get_flash_version(self):
num_word = 3
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x0F
def get_psram_version(self):
num_word = 3
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 28) & 0x0F
def get_block2_version(self):
# BLK_VERSION_MINOR
num_word = 4
return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 4) & 0x07
def get_chip_description(self):
chip_name = {
0: "ESP32-S2",
1: "ESP32-S2FH2",
2: "ESP32-S2FH4",
102: "ESP32-S2FNR2",
100: "ESP32-S2R2",
}.get(
self.get_flash_version() + self.get_psram_version() * 100,
"unknown ESP32-S2",
)
major_rev = self.get_major_chip_version()
minor_rev = self.get_minor_chip_version()
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
def get_chip_features(self):
features = ["WiFi"]
if self.secure_download_mode:
features += ["Secure Download Mode Enabled"]
flash_version = {
0: "No Embedded Flash",
1: "Embedded Flash 2MB",
2: "Embedded Flash 4MB",
}.get(self.get_flash_version(), "Unknown Embedded Flash")
features += [flash_version]
psram_version = {
0: "No Embedded PSRAM",
1: "Embedded PSRAM 2MB",
2: "Embedded PSRAM 4MB",
}.get(self.get_psram_version(), "Unknown Embedded PSRAM")
features += [psram_version]
block2_version = {
0: "No calibration in BLK2 of efuse",
1: "ADC and temperature sensor calibration in BLK2 of efuse V1",
2: "ADC and temperature sensor calibration in BLK2 of efuse V2",
}.get(self.get_block2_version(), "Unknown Calibration in BLK2")
features += [block2_version]
return features
def get_crystal_freq(self):
# ESP32-S2 XTAL is fixed to 40MHz
return 40
def override_vddsdio(self, new_voltage):
raise NotImplementedInROMError(
"VDD_SDIO overrides are not supported for ESP32-S2"
)
def read_mac(self):
mac0 = self.read_reg(self.MAC_EFUSE_REG)
mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
bitstring = struct.pack(">II", mac1, mac0)[2:]
return tuple(bitstring)
def flash_type(self):
return (
1
if self.read_reg(self.EFUSE_RD_REPEAT_DATA3_REG)
& self.EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK
else 0
)
def get_flash_crypt_config(self):
return None # doesn't exist on ESP32-S2
def get_secure_boot_enabled(self):
return (
self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
& self.EFUSE_SECURE_BOOT_EN_MASK
)
def get_key_block_purpose(self, key_block):
if key_block < 0 or key_block > 5:
raise FatalError("Valid key block numbers must be in range 0-5")
reg, shift = [
(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
(self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
(self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
(self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
(self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
(self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
][key_block]
return (self.read_reg(reg) >> shift) & 0xF
def is_flash_encryption_key_valid(self):
# Need to see either an AES-128 key or two AES-256 keys
purposes = [self.get_key_block_purpose(b) for b in range(6)]
if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes):
return True
return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) and any(
p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes
)
def uses_usb_otg(self):
"""
Check the UARTDEV_BUF_NO register to see if USB-OTG console is being used
"""
if self.secure_download_mode:
return False # can't detect native USB in secure download mode
return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_OTG
def _post_connect(self):
if self.uses_usb_otg():
self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
def _check_if_can_reset(self):
"""
Check the strapping register to see if we can reset out of download mode.
"""
if os.getenv("ESPTOOL_TESTING") is not None:
print("ESPTOOL_TESTING is set, ignoring strapping mode check")
# Esptool tests over USB-OTG run with GPIO0 strapped low,
# don't complain in this case.
return
strap_reg = self.read_reg(self.GPIO_STRAP_REG)
force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG)
if (
strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0
and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0
):
print(
"WARNING: {} chip was placed into download mode using GPIO0.\n"
"esptool.py can not exit the download mode over USB. "
"To run the app, reset the chip manually.\n"
"To suppress this note, set --after option to 'no_reset'.".format(
self.get_chip_description()
)
)
raise SystemExit(1)
def hard_reset(self):
uses_usb_otg = self.uses_usb_otg()
if uses_usb_otg:
self._check_if_can_reset()
print("Hard resetting via RTS pin...")
HardReset(self._port, uses_usb_otg)()
def change_baud(self, baud):
ESPLoader.change_baud(self, baud)
class ESP32S2StubLoader(ESP32S2ROM):
"""Access class for ESP32-S2 stub loader, runs on top of ROM.
(Basically the same as ESP32StubLoader, but different base class.
Can possibly be made into a mixin.)
"""
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
IS_STUB = True
def __init__(self, rom_loader):
self.secure_download_mode = rom_loader.secure_download_mode
self._port = rom_loader._port
self._trace_enabled = rom_loader._trace_enabled
self.cache = rom_loader.cache
self.flush_input() # resets _slip_reader
if rom_loader.uses_usb_otg():
self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK
ESP32S2ROM.STUB_CLASS = ESP32S2StubLoader
@@ -1,315 +0,0 @@
# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import os
import struct
from .esp32 import ESP32ROM
from ..loader import ESPLoader
from ..reset import HardReset
from ..util import FatalError, NotImplementedInROMError
class ESP32S3ROM(ESP32ROM):
CHIP_NAME = "ESP32-S3"
IMAGE_CHIP_ID = 9
CHIP_DETECT_MAGIC_VALUE = [0x9]
FPGA_SLOW_BOOT = False
IROM_MAP_START = 0x42000000
IROM_MAP_END = 0x44000000
DROM_MAP_START = 0x3C000000
DROM_MAP_END = 0x3E000000
UART_DATE_REG_ADDR = 0x60000080
SPI_REG_BASE = 0x60002000
SPI_USR_OFFS = 0x18
SPI_USR1_OFFS = 0x1C
SPI_USR2_OFFS = 0x20
SPI_MOSI_DLEN_OFFS = 0x24
SPI_MISO_DLEN_OFFS = 0x28
SPI_W0_OFFS = 0x58
BOOTLOADER_FLASH_OFFSET = 0x0
SUPPORTS_ENCRYPTED_FLASH = True
FLASH_ENCRYPTED_WRITE_ALIGN = 16
# todo: use espefuse APIs to get this info
EFUSE_BASE = 0x60007000 # BLOCK0 read base address
EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x44
EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x5C
MAC_EFUSE_REG = EFUSE_BASE + 0x044
EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
EFUSE_PURPOSE_KEY0_SHIFT = 24
EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
EFUSE_PURPOSE_KEY1_SHIFT = 28
EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY2_SHIFT = 0
EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY3_SHIFT = 4
EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY4_SHIFT = 8
EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
EFUSE_PURPOSE_KEY5_SHIFT = 12
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
EFUSE_RD_REPEAT_DATA3_REG = EFUSE_BASE + 0x3C
EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK = 1 << 9
PURPOSE_VAL_XTS_AES256_KEY_1 = 2
PURPOSE_VAL_XTS_AES256_KEY_2 = 3
PURPOSE_VAL_XTS_AES128_KEY = 4
UARTDEV_BUF_NO = 0x3FCEF14C # Variable in ROM .bss which indicates the port in use
UARTDEV_BUF_NO_USB_OTG = 3 # The above var when USB-OTG is used
UARTDEV_BUF_NO_USB_JTAG_SERIAL = 4 # The above var when USB-JTAG/Serial is used
RTC_CNTL_WDT_WKEY = 0x50D83AA1
RTCCNTL_BASE_REG = 0x60008000
RTC_CNTL_WDTCONFIG0_REG = RTCCNTL_BASE_REG + 0x0090
RTC_CNTL_WDTWPROTECT_REG = RTCCNTL_BASE_REG + 0x00B0
USB_RAM_BLOCK = 0x800 # Max block size USB-OTG is used
GPIO_STRAP_REG = 0x60004038
GPIO_STRAP_SPI_BOOT_MASK = 0x8 # Not download mode
RTC_CNTL_OPTION1_REG = 0x6000812C
RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1 # Is download mode forced over USB?
UART_CLKDIV_REG = 0x60000014
MEMORY_MAP = [
[0x00000000, 0x00010000, "PADDING"],
[0x3C000000, 0x3D000000, "DROM"],
[0x3D000000, 0x3E000000, "EXTRAM_DATA"],
[0x600FE000, 0x60100000, "RTC_DRAM"],
[0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"],
[0x3FC88000, 0x403E2000, "MEM_INTERNAL"],
[0x3FC88000, 0x3FD00000, "DRAM"],
[0x40000000, 0x4001A100, "IROM_MASK"],
[0x40370000, 0x403E0000, "IRAM"],
[0x600FE000, 0x60100000, "RTC_IRAM"],
[0x42000000, 0x42800000, "IROM"],
[0x50000000, 0x50002000, "RTC_DATA"],
]
def get_pkg_version(self):
num_word = 3
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
def is_eco0(self, minor_raw):
# Workaround: The major version field was allocated to other purposes
# when block version is v1.1.
# Luckily only chip v0.0 have this kind of block version and efuse usage.
return (
(minor_raw & 0x7) == 0
and self.get_blk_version_major() == 1
and self.get_blk_version_minor() == 1
)
def get_minor_chip_version(self):
minor_raw = self.get_raw_minor_chip_version()
if self.is_eco0(minor_raw):
return 0
return minor_raw
def get_raw_minor_chip_version(self):
hi_num_word = 5
hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
low_num_word = 3
low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
return (hi << 3) + low
def get_blk_version_major(self):
num_word = 4
return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 0) & 0x03
def get_blk_version_minor(self):
num_word = 3
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x07
def get_major_chip_version(self):
minor_raw = self.get_raw_minor_chip_version()
if self.is_eco0(minor_raw):
return 0
return self.get_raw_major_chip_version()
def get_raw_major_chip_version(self):
num_word = 5
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
def get_chip_description(self):
major_rev = self.get_major_chip_version()
minor_rev = self.get_minor_chip_version()
return f"{self.CHIP_NAME} (revision v{major_rev}.{minor_rev})"
def get_chip_features(self):
return ["WiFi", "BLE"]
def get_crystal_freq(self):
# ESP32S3 XTAL is fixed to 40MHz
return 40
def get_flash_crypt_config(self):
return None # doesn't exist on ESP32-S3
def get_key_block_purpose(self, key_block):
if key_block < 0 or key_block > 5:
raise FatalError("Valid key block numbers must be in range 0-5")
reg, shift = [
(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
(self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
(self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
(self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
(self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
(self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
][key_block]
return (self.read_reg(reg) >> shift) & 0xF
def is_flash_encryption_key_valid(self):
# Need to see either an AES-128 key or two AES-256 keys
purposes = [self.get_key_block_purpose(b) for b in range(6)]
if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes):
return True
return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) and any(
p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes
)
def get_secure_boot_enabled(self):
return (
self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
& self.EFUSE_SECURE_BOOT_EN_MASK
)
def override_vddsdio(self, new_voltage):
raise NotImplementedInROMError(
"VDD_SDIO overrides are not supported for ESP32-S3"
)
def read_mac(self):
mac0 = self.read_reg(self.MAC_EFUSE_REG)
mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
bitstring = struct.pack(">II", mac1, mac0)[2:]
return tuple(bitstring)
def flash_type(self):
return (
1
if self.read_reg(self.EFUSE_RD_REPEAT_DATA3_REG)
& self.EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK
else 0
)
def uses_usb_otg(self):
"""
Check the UARTDEV_BUF_NO register to see if USB-OTG console is being used
"""
if self.secure_download_mode:
return False # can't detect native USB in secure download mode
return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_OTG
def uses_usb_jtag_serial(self):
"""
Check the UARTDEV_BUF_NO register to see if USB-JTAG/Serial is being used
"""
if self.secure_download_mode:
return False # can't detect USB-JTAG/Serial in secure download mode
return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_JTAG_SERIAL
def disable_rtc_watchdog(self):
# When USB-JTAG/Serial is used, the RTC watchdog is not reset
# and can then reset the board during flashing. Disable it.
if self.uses_usb_jtag_serial():
self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY)
self.write_reg(self.RTC_CNTL_WDTCONFIG0_REG, 0)
self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0)
def _post_connect(self):
if self.uses_usb_otg():
self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
if not self.sync_stub_detected: # Don't run if stub is reused
self.disable_rtc_watchdog()
def _check_if_can_reset(self):
"""
Check the strapping register to see if we can reset out of download mode.
"""
if os.getenv("ESPTOOL_TESTING") is not None:
print("ESPTOOL_TESTING is set, ignoring strapping mode check")
# Esptool tests over USB-OTG run with GPIO0 strapped low,
# don't complain in this case.
return
strap_reg = self.read_reg(self.GPIO_STRAP_REG)
force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG)
if (
strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0
and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0
):
print(
"WARNING: {} chip was placed into download mode using GPIO0.\n"
"esptool.py can not exit the download mode over USB. "
"To run the app, reset the chip manually.\n"
"To suppress this note, set --after option to 'no_reset'.".format(
self.get_chip_description()
)
)
raise SystemExit(1)
def hard_reset(self):
uses_usb_otg = self.uses_usb_otg()
if uses_usb_otg:
self._check_if_can_reset()
print("Hard resetting via RTS pin...")
HardReset(self._port, uses_usb_otg)()
def change_baud(self, baud):
ESPLoader.change_baud(self, baud)
class ESP32S3StubLoader(ESP32S3ROM):
"""Access class for ESP32S3 stub loader, runs on top of ROM.
(Basically the same as ESP32StubLoader, but different base class.
Can possibly be made into a mixin.)
"""
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
IS_STUB = True
def __init__(self, rom_loader):
self.secure_download_mode = rom_loader.secure_download_mode
self._port = rom_loader._port
self._trace_enabled = rom_loader._trace_enabled
self.cache = rom_loader.cache
self.flush_input() # resets _slip_reader
if rom_loader.uses_usb_otg():
self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK
ESP32S3ROM.STUB_CLASS = ESP32S3StubLoader
@@ -1,42 +0,0 @@
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
from .esp32s3 import ESP32S3ROM
class ESP32S3BETA2ROM(ESP32S3ROM):
CHIP_NAME = "ESP32-S3(beta2)"
IMAGE_CHIP_ID = 4
CHIP_DETECT_MAGIC_VALUE = [0xEB004136]
EFUSE_BASE = 0x6001A000 # BLOCK0 read base address
def get_chip_description(self):
major_rev = self.get_major_chip_version()
minor_rev = self.get_minor_chip_version()
return f"{self.CHIP_NAME} (revision v{major_rev}.{minor_rev})"
class ESP32S3BETA2StubLoader(ESP32S3BETA2ROM):
"""Access class for ESP32S3 stub loader, runs on top of ROM.
(Basically the same as ESP32StubLoader, but different base class.
Can possibly be made into a mixin.)
"""
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
IS_STUB = True
def __init__(self, rom_loader):
self.secure_download_mode = rom_loader.secure_download_mode
self._port = rom_loader._port
self._trace_enabled = rom_loader._trace_enabled
self.cache = rom_loader.cache
self.flush_input() # resets _slip_reader
ESP32S3BETA2ROM.STUB_CLASS = ESP32S3BETA2StubLoader
@@ -1,191 +0,0 @@
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
from ..loader import ESPLoader
from ..util import FatalError, NotImplementedInROMError
class ESP8266ROM(ESPLoader):
"""Access class for ESP8266 ROM bootloader"""
CHIP_NAME = "ESP8266"
IS_STUB = False
CHIP_DETECT_MAGIC_VALUE = [0xFFF0C101]
# OTP ROM addresses
ESP_OTP_MAC0 = 0x3FF00050
ESP_OTP_MAC1 = 0x3FF00054
ESP_OTP_MAC3 = 0x3FF0005C
SPI_REG_BASE = 0x60000200
SPI_USR_OFFS = 0x1C
SPI_USR1_OFFS = 0x20
SPI_USR2_OFFS = 0x24
SPI_MOSI_DLEN_OFFS = None
SPI_MISO_DLEN_OFFS = None
SPI_W0_OFFS = 0x40
UART_CLKDIV_REG = 0x60000014
XTAL_CLK_DIVIDER = 2
FLASH_SIZES = {
"512KB": 0x00,
"256KB": 0x10,
"1MB": 0x20,
"2MB": 0x30,
"4MB": 0x40,
"2MB-c1": 0x50,
"4MB-c1": 0x60,
"8MB": 0x80,
"16MB": 0x90,
}
FLASH_FREQUENCY = {
"80m": 0xF,
"40m": 0x0,
"26m": 0x1,
"20m": 0x2,
}
BOOTLOADER_FLASH_OFFSET = 0
MEMORY_MAP = [
[0x3FF00000, 0x3FF00010, "DPORT"],
[0x3FFE8000, 0x40000000, "DRAM"],
[0x40100000, 0x40108000, "IRAM"],
[0x40201010, 0x402E1010, "IROM"],
]
def get_efuses(self):
# Return the 128 bits of ESP8266 efuse as a single Python integer
result = self.read_reg(0x3FF0005C) << 96
result |= self.read_reg(0x3FF00058) << 64
result |= self.read_reg(0x3FF00054) << 32
result |= self.read_reg(0x3FF00050)
return result
def _get_flash_size(self, efuses):
# rX_Y = EFUSE_DATA_OUTX[Y]
r0_4 = (efuses & (1 << 4)) != 0
r3_25 = (efuses & (1 << 121)) != 0
r3_26 = (efuses & (1 << 122)) != 0
r3_27 = (efuses & (1 << 123)) != 0
if r0_4 and not r3_25:
if not r3_27 and not r3_26:
return 1
elif not r3_27 and r3_26:
return 2
if not r0_4 and r3_25:
if not r3_27 and not r3_26:
return 2
elif not r3_27 and r3_26:
return 4
return -1
def get_chip_description(self):
efuses = self.get_efuses()
is_8285 = (
efuses & ((1 << 4) | 1 << 80)
) != 0 # One or the other efuse bit is set for ESP8285
if is_8285:
flash_size = self._get_flash_size(efuses)
max_temp = (
efuses & (1 << 5)
) != 0 # This efuse bit identifies the max flash temperature
chip_name = {
1: "ESP8285H08" if max_temp else "ESP8285N08",
2: "ESP8285H16" if max_temp else "ESP8285N16",
}.get(flash_size, "ESP8285")
return chip_name
return "ESP8266EX"
def get_chip_features(self):
features = ["WiFi"]
if "ESP8285" in self.get_chip_description():
features += ["Embedded Flash"]
return features
def flash_spi_attach(self, hspi_arg):
if self.IS_STUB:
super(ESP8266ROM, self).flash_spi_attach(hspi_arg)
else:
# ESP8266 ROM has no flash_spi_attach command in serial protocol,
# but flash_begin will do it
self.flash_begin(0, 0)
def flash_set_parameters(self, size):
# not implemented in ROM, but OK to silently skip for ROM
if self.IS_STUB:
super(ESP8266ROM, self).flash_set_parameters(size)
def chip_id(self):
"""
Read Chip ID from efuse - the equivalent of the SDK system_get_chip_id() func
"""
id0 = self.read_reg(self.ESP_OTP_MAC0)
id1 = self.read_reg(self.ESP_OTP_MAC1)
return (id0 >> 24) | ((id1 & 0xFFFFFF) << 8)
def read_mac(self):
"""Read MAC from OTP ROM"""
mac0 = self.read_reg(self.ESP_OTP_MAC0)
mac1 = self.read_reg(self.ESP_OTP_MAC1)
mac3 = self.read_reg(self.ESP_OTP_MAC3)
if mac3 != 0:
oui = ((mac3 >> 16) & 0xFF, (mac3 >> 8) & 0xFF, mac3 & 0xFF)
elif ((mac1 >> 16) & 0xFF) == 0:
oui = (0x18, 0xFE, 0x34)
elif ((mac1 >> 16) & 0xFF) == 1:
oui = (0xAC, 0xD0, 0x74)
else:
raise FatalError("Unknown OUI")
return oui + ((mac1 >> 8) & 0xFF, mac1 & 0xFF, (mac0 >> 24) & 0xFF)
def get_erase_size(self, offset, size):
"""Calculate an erase size given a specific size in bytes.
Provides a workaround for the bootloader erase bug."""
sectors_per_block = 16
sector_size = self.FLASH_SECTOR_SIZE
num_sectors = (size + sector_size - 1) // sector_size
start_sector = offset // sector_size
head_sectors = sectors_per_block - (start_sector % sectors_per_block)
if num_sectors < head_sectors:
head_sectors = num_sectors
if num_sectors < 2 * head_sectors:
return (num_sectors + 1) // 2 * sector_size
else:
return (num_sectors - head_sectors) * sector_size
def override_vddsdio(self, new_voltage):
raise NotImplementedInROMError(
"Overriding VDDSDIO setting only applies to ESP32"
)
class ESP8266StubLoader(ESP8266ROM):
"""Access class for ESP8266 stub loader, runs on top of ROM."""
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
IS_STUB = True
def __init__(self, rom_loader):
self.secure_download_mode = rom_loader.secure_download_mode
self._port = rom_loader._port
self._trace_enabled = rom_loader._trace_enabled
self.cache = rom_loader.cache
self.flush_input() # resets _slip_reader
def get_erase_size(self, offset, size):
return size # stub doesn't have same size bug as ROM loader
ESP8266ROM.STUB_CLASS = ESP8266StubLoader
@@ -1,7 +0,0 @@
{
"entry": 1074521560,
"text": "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",
"text_start": 1074520064,
"data": "GOv8P9jnC0Bx6AtA8+wLQO3oC0CP6AtA7egLQEnpC0AG6gtAeOoLQCHqC0CB5wtAo+kLQPjpC0Bn6QtAmuoLQI7pC0Ca6gtAXegLQLPoC0Dt6AtASekLQHfoC0BM6wtAs+wLQKXmC0DX7AtApeYLQKXmC0Cl5gtApeYLQKXmC0Cl5gtApeYLQKXmC0Dz6gtApeYLQM3rC0Cz7AtA",
"data_start": 1073605544
}
@@ -1,7 +0,0 @@
{
"entry": 1077413304,
"text": "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",
"text_start": 1077411840,
"data": "GGvKP+AIOEAsCThAhAk4QCgKOECUCjhAQgo4QKgHOEDkCThAJAo4QJgJOEBYBzhAzAk4QFgHOEC6CDhA/gg4QCwJOECECThAzAg4QBIIOEBCCDhAyAg4QOYMOEAsCThArAs4QJoMOECkBjhAxAw4QKQGOECkBjhApAY4QKQGOECkBjhApAY4QKQGOECkBjhASAs4QKQGOEDICzhAmgw4QA==",
"data_start": 1070295976
}
@@ -1,7 +0,0 @@
{
"entry": 1077413532,
"text": "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",
"text_start": 1077411840,
"data": "IGvIP1YKOECmCjhA/go4QKILOEAODDhAvAs4QCIJOEBeCzhAngs4QBILOEDSCDhARgs4QNIIOEAwCjhAdgo4QKYKOED+CjhAQgo4QIYJOEC2CThAPgo4QGAOOECmCjhAJg04QBgOOEASCDhAQA44QBIIOEASCDhAEgg4QBIIOEASCDhAEgg4QBIIOEASCDhAwgw4QBIIOEBEDThAGA44QA==",
"data_start": 1070164912
}
@@ -1,7 +0,0 @@
{
"entry": 1082132112,
"text": "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",
"text_start": 1082130432,
"data": "HCuEQCoKgEB6CoBA0gqAQHYLgEDiC4BAkAuAQPYIgEAyC4BAcguAQOYKgECmCIBAGguAQKYIgEAECoBASgqAQHoKgEDSCoBAFgqAQFoJgECKCYBAEgqAQDQOgEB6CoBA+gyAQOwNgEDmB4BAFA6AQOYHgEDmB4BA5geAQOYHgEDmB4BA5geAQOYHgEDmB4BAlgyAQOYHgEAYDYBA7A2AQA==",
"data_start": 1082469292
}
@@ -1,7 +0,0 @@
{
"entry": 1077413318,
"text": "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",
"text_start": 1077411840,
"data": "GGvIP/gIOEBECThAnAk4QEAKOECsCjhAWgo4QMAHOED8CThAPAo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QP4MOEBECThAxAs4QLIMOEC8BjhA3Aw4QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAYAs4QLwGOEDgCzhAsgw4QA==",
"data_start": 1070164904
}
@@ -1,7 +0,0 @@
{
"entry": 1082132112,
"text": "QREixCbCBsa39wBgEUc3BINA2Mu39ABgEwQEANxAkYuR57JAIkSSREEBgoCIQBxAE3X1D4KX3bcBEbcHAGBOxoOphwBKyDcJg0AmylLEBs4izLcEAGB9WhMJCQDATBN09A8N4PJAYkQjqDQBQknSRLJJIkoFYYKAiECDJwkAE3X1D4KXfRTjGUT/yb8TBwAMlEGqh2MY5QCFR4XGI6AFAHlVgoAFR2OH5gAJRmONxgB9VYKAQgUTB7ANQYVjlecCiUecwfW3kwbADWMW1QCYwRMFAAyCgJMG0A19VWOV1wCYwRMFsA2CgLc1hEBBEZOFRboGxmE/Y0UFBrc3hECTh8exA6cHCAPWRwgTdfUPkwYWAMIGwYIjktcIMpcjAKcAA9dHCJFnk4cHBGMe9wI3t4NAEwfHsaFnupcDpgcIt/aDQLc3hECTh8exk4bGtWMf5gAjpscII6DXCCOSBwghoPlX4wb1/LJAQQGCgCOm1wgjoOcI3bc3NwBgfEudi/X/NycAYHxLnYv1/4KAQREGxt03tzcAYCOmBwI3BwAImMOYQ33/yFeyQBNF9f8FiUEBgoBBEQbG2T993TcHAEC3NwBgmMM3NwBgHEP9/7JAQQGCgEERIsQ3BINAkwcEAUrAA6kHAQbGJsJjCgkERTc5xb1HEwQEAYFEY9YnAQREvYiTtBQAfTeFPxxENwaAABOXxwCZ4DcGAAG39v8AdY+3NgBg2MKQwphCff9BR5HgBUczCelAupcjKCQBHMSyQCJEkkQCSUEBgoABEQbOIswlNzcEhUBsABMFBP+XAID/54Ag8qqHBUWV57JHk/cHID7GiTc3NwBgHEe3BkAAEwUE/9WPHMeyRZcAgP/ngKDvMzWgAPJAYkQFYYKAQRG3B4NABsaThwcBBUcjgOcAE9fFAJjHBWd9F8zDyMf5jTqVqpWxgYzLI6oHAEE3GcETBVAMskBBAYKAAREizDcEg0CTBwQBJsrER07GBs5KyKqJEwQEAWPzlQCuhKnAAylEACaZE1nJABxIY1XwABxEY175ArU9fd1IQCaGzoWXAID/54Cg4hN19Q8BxZMHQAxcyFxAppdcwFxEhY9cxPJAYkTSREJJskkFYYKAaTVtv0ERBsaXAID/54BA1gNFhQGyQHUVEzUVAEEBgoBBEQbGxTcNxbcHg0CThwcA1EOZzjdnCWATB8cQHEM3Bv3/fRbxjzcGAwDxjtWPHMOyQEEBgoBBEQbGbTcRwQ1FskBBARcDgP9nAIPMQREGxpcAgP/ngEDKcTcBxbJAQQHZv7JAQQGCgEERBsYTBwAMYxrlABMFsA3RPxMFwA2yQEEB6bcTB7AN4xvl/sE3EwXQDfW3QREixCbCBsYqhLMEtQBjF5QAskAiRJJEQQGCgANFBAAFBE0/7bc1cSbLTsf9coVp/XQizUrJUsVWwwbPk4SE+haRk4cJB6aXGAizhOcAKokmhS6ElwCA/+eAgCyThwkHGAgFarqXs4pHQTHkBWd9dZMFhfqTBwcHEwWF+RQIqpczhdcAkwcHB66Xs4XXACrGlwCA/+eAQCkyRcFFlTcBRYViFpH6QGpE2kRKSbpJKkqaSg1hgoCiiWNzigCFaU6G1oVKhZcAgP/ngIDIE3X1DwHtTobWhSaFlwCA/+eAgCROmTMENEFRtxMFMAZVvxMFAAzZtTFx/XIFZ07XUtVW017PBt8i3SbbStla0WLNZstqyW7H/XcWkRMHBwc+lxwIupc+xiOqB/iqiS6Ksoq2iwU1kwcAAhnBtwcCAD6FlwCA/+eAIB2FZ2PlVxMFZH15EwmJ+pMHBAfKlxgIM4nnAEqFlwCA/+eAoBt9exMMO/mTDIv5EwcEB5MHBAcUCGKX5peBRDMM1wCzjNcAUk1jfE0JY/GkA0GomT+ihQgBjTW5NyKGDAFKhZcAgP/ngIAXopmilGP1RAOzh6RBY/F3AzMEmkBj84oAVoQihgwBToWXAID/54DAtxN19Q9V3QLMAUR5XY1NowkBAGKFlwCA/+eAgKd9+QNFMQHmhVE8Y08FAOPijf6FZ5OHBweilxgIupfalyOKp/gFBPG34xWl/ZFH4wX09gVnfXWTBwcHkwWF+hMFhfkUCKqXM4XXAJMHBweul7OF1wAqxpcAgP/ngKANcT0yRcFFZTNRPdU5twcCABnhkwcAAj6FlwCA/+eAoAqFYhaR+lBqVNpUSlm6WSpamloKW/pLakzaTEpNuk0pYYKAt1dBSRlxk4f3hAFFht6i3KbaytjO1tLU1tLa0N7O4szmyurI7sY+zpcAgP/ngMCgcTENwTdnCWATB8cQHEO3BoNAI6L2ALcG/f/9FvWPwWbVjxzDpTEFwbcnC2A3R9hQk4aHwRMHF6qYwpOHB8AjoAcAI6AGALcHg0A3N4RAk4cHABMHx7ohoCOgBwCRB+Pt5/7hM5FFaAjROcEzt7eDQJOHx7EhZz6XIyD3CLcHgEA3CYNAk4eHDiMg+QC3OYRA9T4TCQkAk4nJsWMHBRC3BwFgRUcjqucIhUVFRZcAgP/ngID3twWAQAFGk4UFAEVFlwCA/+eAgPg39wBgHEs3BQIAk+dHABzLlwCA/+eAgPe3FwlgiF+BRbcEg0BxiWEVEzUVAJcAgP/ngACiwWf9FxMHABCFZkFmtwUAAQFFk4QEAQ1qtzqDQJcAgP/ngACYJpoTi8qxg6fJCPXfg6vJCIVHI6YJCCMC8QKDxxsACUcjE+ECowLxAgLUTUdjgecIUUdjj+cGKUdjn+cAg8c7AAPHKwCiB9mPEUdjlucAg6eLAJxDPtRVOaFFSBDBNoPHOwADxysAogfZjxFnQQdjdPcEEwWwDbk+EwXADaE+EwXgDok+WTFBt7cFgEABRpOFhQMVRZcAgP/ngEDpNwcAYFxHEwUAApPnFxBcxzG3yUcjE/ECTbcDxxsA0UZj5+YChUZj5uYAAUwTBPAPhah5FxN39w/JRuPo5v63NoRACgeThga7NpcYQwKHkwYHA5P29g8RRuNp1vwTB/cCE3f3D41GY+vmCLc2hEAKB5OGxr82lxhDAocTB0ACY5jnEALUHUQBRUU8AUXhNMU2+T6hRUgQfRTlNHX0AUwBRBN19A9FPBN1/A9tNMk24x4E6oPHGwBJR2Nj9y4JR+N29+r1F5P39w89R+Ng9+o3N4RAigcTB8fAupecQ4KHBUSd63AQgUUBRZfwf//ngIB1HeHRRWgQaTQBRDGoBUSB75fwf//ngAB6MzSgACmgIUdjhecABUQBTGG3A6yLAAOkywCzZ4wA0gf19+/wP4p98cFsIpz9HH19MwWMQFXcs3eVAZXjwWwzBYxAY+aMAv18MwWMQFXQMYGX8H//54CAdlX5ZpT1tzGBl/B//+eAgHVV8WqU0bdBgZfwf//ngMB0UfkzBJRBwbchR+OJ5/ABTBMEAAwxt0FHzb9BRwVE45zn9oOlywADpYsA9Tqxv0FHBUTjkuf2A6cLAZFnY+XnHIOlSwEDpYsA7/B/hTW/QUcFROOS5/SDpwsBEWdjZfcaA6fLAIOlSwEDpYsAM4TnAu/w/4IjrAQAIySKsDG3A8cEAGMOBxADp4sAwRcTBAAMYxP3AMBIAUeTBvAOY0b3AoPHWwADx0sAAUyiB9mPA8drAEIHXY+Dx3sA4gfZj+OB9uYTBBAMqb0zhusAA0aGAQUHsY7ht4PHBADxw9xEY5gHEsBII4AEAH21YUdjlucCg6fLAQOniwGDpksBA6YLAYOlywADpYsAl/B//+eAQGUqjDM0oAAptQFMBUQRtRFHBUTjmufmA6WLAIFFl/B//+eAwGqRtRP39wDjGgfsk9xHABOEiwABTH1d43mc3UhEl/B//+eAQE8YRFRAEED5jmMHpwEcQhNH9/99j9mOFMIFDEEE2b8RR0m9QUcFROOc5+CDp4sAA6dLASMm+QAjJOkA3bODJYkAwReR5YnPAUwTBGAMtbsDJ8kAY2b3BhP3NwDjHgfkAyjJAAFGAUczBehAs4blAGNp9wDjCQbUIyapACMk2QCZszOG6wAQThEHkMIFRum/IUcFROOW59oDJMkAGcATBIAMIyYJACMkCQAzNIAASbsBTBMEIAwRuwFMEwSADDGzAUwTBJAMEbMTByANY4PnDBMHQA3jkOe8A8Q7AIPHKwAiBF2Ml/B//+eA4E0DrMQAQRRjc4QBIozjDgy4wEBilDGAnEhjVfAAnERjW/QK7/DP0nXdyEBihpOFiwGX8H//54DgSQHFkwdADNzI3EDil9zA3ESzh4dB3MSX8H//54DASIm+CWUTBQVxA6zLAAOkiwCX8H//54DAObcHAGDYS7cGAAHBFpNXRwESB3WPvYvZj7OHhwMBRbPVhwKX8H//54DgOhMFgD6X8H//54BgNhG2g6ZLAQOmCwGDpcsAA6WLAO/wz//9tIPFOwCDxysAE4WLAaIF3Y3BFe/wr9zZvO/wD8w9v4PHOwADxysAE4yLAaIH2Y8TjQf/BUS3O4RA3ERjBQ0AmcNjTIAAY1AEChMHcAzYyOOfB6iTB5AMYaiTh8u6mEO3t4NAk4fHsZmPPtaDJ4qwtzyDQGrQk4wMAZONy7oFSGNz/QANSELGOsTv8A/FIkcySDcFg0DihXwQk4bKsRAQEwWFApfwf//ngMA1glcDp4ywg6UNADMN/UAdjz6cslcjpOywKoS+lSOgvQCTh8qxnY0BxaFn45L19lqF7/Av0COgbQGZvy3044MHoJMHgAzcyPW6g6eLAOObB57v8K/aCWUTBQVxl/B//+eAICTv8C/Wl/B//+eAYCjRugOkywDjBwSc7/Av2BMFgD6X8H//54DAIe/wz9MClFW67/BP0/ZQZlTWVEZZtlkmWpZaBlv2S2ZM1kxGTbZNCWGCgAAA",
"text_start": 1082130432,
"data": "HCuDQCoKgEB6CoBA0gqAQHYLgEDiC4BAkAuAQPYIgEAyC4BAcguAQOYKgECmCIBAGguAQKYIgEAECoBASgqAQHoKgEDSCoBAFgqAQFoJgECKCYBAEgqAQDQOgEB6CoBA+gyAQOwNgEDmB4BAFA6AQOYHgEDmB4BA5geAQOYHgEDmB4BA5geAQOYHgEDmB4BAlgyAQOYHgEAYDYBA7A2AQA==",
"data_start": 1082403756
}
@@ -1,7 +0,0 @@
{
"entry": 1077413318,
"text": "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",
"text_start": 1077411840,
"data": "GGvIP/gIOEBECThAnAk4QEAKOECsCjhAWgo4QMAHOED8CThAPAo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QP4MOEBECThAxAs4QLIMOEC8BjhA3Aw4QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAYAs4QLwGOEDgCzhAsgw4QA==",
"data_start": 1070164904
}
@@ -1,7 +0,0 @@
{
"entry": 1077413318,
"text": "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",
"text_start": 1077411840,
"data": "GGvIP/gIOEBECThAnAk4QEAKOECsCjhAWgo4QMAHOED8CThAPAo4QLAJOEBwBzhA5Ak4QHAHOEDSCDhAFgk4QEQJOECcCThA5Ag4QCoIOEBaCDhA4Ag4QP4MOEBECThAxAs4QLIMOEC8BjhA3Aw4QLwGOEC8BjhAvAY4QLwGOEC8BjhAvAY4QLwGOEC8BjhAYAs4QLwGOEDgCzhAsgw4QA==",
"data_start": 1070164904
}
@@ -1,7 +0,0 @@
{
"entry": 1073907696,
"text": "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 truncated
"text_start": 1073905664,
"data": "ZCv9PzaLAkDBiwJAhpACQEqMAkDjiwJASowCQKmMAkByjQJA5Y0CQI2NAkDAigJAC40CQGSNAkDMjAJACI4CQPaMAkAIjgJAr4sCQA6MAkBKjAJAqYwCQMeLAkACiwJAx44CQD2QAkDYiQJAZZACQNiJAkDYiQJA2IkCQNiJAkDYiQJA2IkCQNiJAkDYiQJAZI4CQNiJAkBZjwJAPZACQA==",
"data_start": 1073622012
}
@@ -1,7 +0,0 @@
{
"entry": 1077381684,
"text": "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 truncated
"text_start": 1077379072,
"data": "ZCvKP5aNN0B7jjdAPJM3QAaPN0CbjjdABo83QGWPN0AykDdApZA3QE2QN0AhjTdAyI83QCSQN0CIjzdAx5A3QLKPN0DHkDdAaY43QMaON0AGjzdAZY83QIGON0BijTdAiJE3QAKTN0A+jDdAIpM3QD6MN0A+jDdAPow3QD6MN0A+jDdAPow3QD6MN0A+jDdAIpE3QD6MN0AdkjdAApM3QAQInwAAAAAAAAAYAQQIBQAAAAAAAAAIAQQIBgAAAAAAAAAAAQQIIQAAAAAAIAAAEQQI3AAAAAAAIAAAEQQIDAAAAAAAIAAAAQQIEgAAAAAAIAAAESAoDAAQAQAA",
"data_start": 1070279668
}
@@ -1,7 +0,0 @@
{
"entry": 1077381176,
"text": "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 truncated
"text_start": 1077379072,
"data": "ZCvKP4KLN0APjDdA15A3QJqMN0AvjDdAmow3QPmMN0DGjTdAOY43QOGNN0ANizdAXI03QLiNN0AcjTdAXI43QEaNN0BcjjdA/Ys3QFqMN0CajDdA+Yw3QBWMN0BOizdAHI83QJuQN0AsijdAvZA3QCyKN0AsijdALIo3QCyKN0AsijdALIo3QCyKN0AsijdAto43QCyKN0CyjzdAm5A3QA==",
"data_start": 1070279668
}
@@ -1,7 +0,0 @@
{
"entry": 1074843652,
"text": "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 truncated
"text_start": 1074843648,
"data": "CIH+PwUFBAACAwcAAwMLALnXEEDv1xBAHdgQQLrYEEBo5xBAHtkQQHTZEEDA2RBAaOcQQILaEED/2hBAwNsQQGjnEEBo5xBAWNwQQGjnEEA33xBAAOAQQDvgEEBo5xBAaOcQQNfgEEBo5xBAv+EQQGXiEECj4xBAY+QQQDTlEEBo5xBAaOcQQGjnEEBo5xBAYuYQQGjnEEBX5xBAkN0QQI/YEECm5RBAq9oQQPzZEEBo5xBA7OYQQDHnEEBo5xBAaOcQQGjnEEBo5xBAaOcQQGjnEEBo5xBAaOcQQCLaEEBf2hBAvuUQQAEAAAACAAAAAwAAAAQAAAAFAAAABwAAAAkAAAANAAAAEQAAABkAAAAhAAAAMQAAAEEAAABhAAAAgQAAAMEAAAABAQAAgQEAAAECAAABAwAAAQQAAAEGAAABCAAAAQwAAAEQAAABGAAAASAAAAEwAAABQAAAAWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAgAAAAIAAAADAAAAAwAAAAQAAAAEAAAABQAAAAUAAAAGAAAABgAAAAcAAAAHAAAACAAAAAgAAAAJAAAACQAAAAoAAAAKAAAACwAAAAsAAAAMAAAADAAAAA0AAAANAAAAAAAAAAAAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAANAAAADwAAABEAAAATAAAAFwAAABsAAAAfAAAAIwAAACsAAAAzAAAAOwAAAEMAAABTAAAAYwAAAHMAAACDAAAAowAAAMMAAADjAAAAAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAAEAAAABAAAAAQAAAAEAAAABQAAAAUAAAAFAAAABQAAAAAAAAAAAAAAAAAAABAREgAIBwkGCgULBAwDDQIOAQ8AAQEAAAEAAAAEAAAA",
"data_start": 1073720488
}
-174
View File
@@ -1,174 +0,0 @@
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import re
import struct
import sys
def byte(bitstr, index):
return bitstr[index]
def mask_to_shift(mask):
"""Return the index of the least significant bit in the mask"""
shift = 0
while mask & 0x1 == 0:
shift += 1
mask >>= 1
return shift
def div_roundup(a, b):
"""Return a/b rounded up to nearest integer,
equivalent result to int(math.ceil(float(int(a)) / float(int(b))), only
without possible floating point accuracy errors.
"""
return (int(a) + int(b) - 1) // int(b)
def flash_size_bytes(size):
"""Given a flash size of the type passed in args.flash_size
(ie 512KB or 1MB) then return the size in bytes.
"""
if "MB" in size:
return int(size[: size.index("MB")]) * 1024 * 1024
elif "KB" in size:
return int(size[: size.index("KB")]) * 1024
else:
raise FatalError("Unknown size %s" % size)
def hexify(s, uppercase=True):
format_str = "%02X" if uppercase else "%02x"
return "".join(format_str % c for c in s)
def pad_to(data, alignment, pad_character=b"\xFF"):
"""Pad to the next alignment boundary"""
pad_mod = len(data) % alignment
if pad_mod != 0:
data += pad_character * (alignment - pad_mod)
return data
def print_overwrite(message, last_line=False):
"""Print a message, overwriting the currently printed line.
If last_line is False, don't append a newline at the end
(expecting another subsequent call will overwrite this one.)
After a sequence of calls with last_line=False, call once with last_line=True.
If output is not a TTY (for example redirected a pipe),
no overwriting happens and this function is the same as print().
"""
if sys.stdout.isatty():
print("\r%s" % message, end="\n" if last_line else "")
else:
print(message)
def expand_chip_name(chip_name):
"""Change chip name to official form, e.g. `esp32s3beta2` -> `ESP32-S3(beta2)`"""
# Put "-" after "esp32"
chip_name = re.sub(r"(esp32)(?!$)", r"\1-", chip_name)
# Put "()" around "betaN"
chip_name = re.sub(r"(beta\d*)", r"(\1)", chip_name)
# Uppercase everything before "(betaN)"
chip_name = re.sub(r"^[^\(]+", lambda x: x.group(0).upper(), chip_name)
return chip_name
def strip_chip_name(chip_name):
"""Strip chip name to normalized form, e.g. `ESP32-S3(beta2)` -> `esp32s3beta2`"""
return re.sub(r"[-()]", "", chip_name.lower())
class FatalError(RuntimeError):
"""
Wrapper class for runtime errors that aren't caused by internal bugs, but by
ESP ROM responses or input content.
"""
def __init__(self, message):
RuntimeError.__init__(self, message)
@staticmethod
def WithResult(message, result):
"""
Return a fatal error object that appends the hex values of
'result' and its meaning as a string formatted argument.
"""
err_defs = {
# ROM error codes
0x101: "Out of memory",
0x102: "Invalid argument",
0x103: "Invalid state",
0x104: "Invalid size",
0x105: "Requested resource not found",
0x106: "Operation or feature not supported",
0x107: "Operation timed out",
0x108: "Received response was invalid",
0x109: "CRC or checksum was invalid",
0x10A: "Version was invalid",
0x10B: "MAC address was invalid",
# Flasher stub error codes
0xC000: "Bad data length",
0xC100: "Bad data checksum",
0xC200: "Bad blocksize",
0xC300: "Invalid command",
0xC400: "Failed SPI operation",
0xC500: "Failed SPI unlock",
0xC600: "Not in flash mode",
0xC700: "Inflate error",
0xC800: "Not enough data",
0xC900: "Too much data",
0xFF00: "Command not implemented",
}
err_code = struct.unpack(">H", result[:2])
message += " (result was {}: {})".format(
hexify(result), err_defs.get(err_code[0], "Unknown result")
)
return FatalError(message)
class NotImplementedInROMError(FatalError):
"""
Wrapper class for the error thrown when a particular ESP bootloader function
is not implemented in the ROM bootloader.
"""
def __init__(self, bootloader, func):
FatalError.__init__(
self,
"%s ROM does not support function %s."
% (bootloader.CHIP_NAME, func.__name__),
)
class NotSupportedError(FatalError):
def __init__(self, esp, function_name):
FatalError.__init__(
self,
"Function %s is not supported for %s." % (function_name, esp.CHIP_NAME),
)
class UnsupportedCommandError(RuntimeError):
"""
Wrapper class for when ROM loader returns an invalid command response.
Usually this indicates the loader is running in Secure Download Mode.
"""
def __init__(self, esp, op):
if esp.secure_download_mode:
msg = "This command (0x%x) is not supported in Secure Download Mode" % op
else:
msg = "Invalid (unsupported) command 0x%x" % op
RuntimeError.__init__(self, msg)
+5
View File
@@ -26,7 +26,12 @@ board_build.embed_files =
data_embed/script.js.gz
data_embed/bootstrap.css.gz
data_embed/bootstrap.js.gz
data_embed/leaflet.css.gz
data_embed/leaflet.js.gz
data_embed/favicon.png.gz
data_embed/aprs-symbols-24-0.png.gz
data_embed/aprs-symbols-24-1.png.gz
data_embed/aprs-symbols-24-2.png.gz
extra_scripts =
pre:tools/compress.py
debug_tool = esp-prog
+2 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -197,6 +197,7 @@
}
void uploadToAPRSIS(const String& packet) {
if (packet == "") return; // never send an empty line to APRS-IS
beaconBytesSent = checkATResponse("AT+CIPSEND=0," + String(packet.length()+1));
delay(2000);
if (beaconBytesSent) {
+15 -5
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -52,6 +52,7 @@ ___________________________________________________________________*/
#include "syslog_utils.h"
#include "power_utils.h"
#include "sleep_utils.h"
#include "thermal_utils.h"
#include "mqtt_utils.h"
#include "lora_utils.h"
#include "wifi_utils.h"
@@ -68,8 +69,8 @@ ___________________________________________________________________*/
#endif
String versionDate = "2026-03-25";
String versionNumber = "3.2.3";
String versionDate = "2026-10-18";
String versionNumber = "4.0.5";
Configuration Config;
WiFiClient aprsIsClient;
WiFiClient mqttClient;
@@ -108,6 +109,9 @@ void setup() {
}
networkManager->setHostName("iGATE-" + Config.callsign);
POWER_Utils::setup();
#ifdef FAN_CTRL_PIN
THERMAL_Utils::setup();
#endif
Utils::setupDisplay();
LoRa_Utils::setup();
Utils::validateFreqs();
@@ -135,6 +139,9 @@ void loop() {
SLEEP_Utils::checkWakeUpFlag();
Utils::checkBeaconInterval();
STATION_Utils::processOutputPacketBufferUltraEcoMode();
#ifdef FAN_CTRL_PIN
THERMAL_Utils::monitor();
#endif
Utils::checkSleepByLowBatteryVoltage(1);
SLEEP_Utils::startSleeping();
} else {
@@ -184,7 +191,7 @@ void loop() {
Utils::checkDisplayInterval();
Utils::checkBeaconInterval();
APRS_IS_Utils::checkStatus(); // Need that to update display, maybe split this and send APRSIS status to display func?
APRS_IS_Utils::updateWiFiAPRSISDisplayInfo(); // WiFi / APRS-IS status line (wakes display only on status change)
String packet = "";
if (Config.loramodule.rxActive) {
@@ -196,7 +203,7 @@ void loop() {
APRS_IS_Utils::processLoRaPacket(packet); // Send received packet to APRSIS
}
if (Config.loramodule.txActive && (Config.digi.mode == 2 || Config.digi.mode == 3 || backupDigiMode)) { // If Digi enabled
if (Config.loramodule.txActive && (Config.digi.mode == 1 || Config.digi.mode == 2 || Config.digi.mode == 3 || backupDigiMode)) { // If Digi enabled
DIGI_Utils::processLoRaPacket(packet); // Send received packet to Digi
}
@@ -222,6 +229,9 @@ void loop() {
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
#endif
#ifdef FAN_CTRL_PIN
THERMAL_Utils::monitor();
#endif
Utils::checkRebootTime();
Utils::checkSleepByLowBatteryVoltage(1);
}
+72 -97
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -16,7 +16,7 @@
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <APRSPacketLib.h>
#include <APRSPacketLib.h>
#include <WiFiClient.h>
#include "configuration.h"
#include "network_manager.h"
@@ -33,6 +33,7 @@
extern Configuration Config;
extern APRSPacket lastAprsPacket;
extern NetworkManager *networkManager;
extern WiFiClient aprsIsClient;
extern uint32_t lastScreenOn;
@@ -47,7 +48,6 @@ extern bool modemLoggedToAPRSIS;
extern bool backupDigiMode;
extern String versionNumber;
uint32_t lastRxTime = millis();
bool passcodeValid = false;
uint32_t lastServerCheck = 0;
@@ -60,6 +60,7 @@ uint32_t lastServerCheck = 0;
namespace APRS_IS_Utils {
void upload(const String& line) {
if (line == "") return; // never send an empty line to APRS-IS
aprsIsClient.print(line + "\r\n");
}
@@ -93,7 +94,9 @@ namespace APRS_IS_Utils {
}
}
void checkStatus() {
void updateWiFiAPRSISDisplayInfo() {
static String lastWifiState = "";
static String lastAprsisState = "";
String wifiState, aprsisState;
if (networkManager->isWiFiConnected()) {
wifiState = "OK";
@@ -103,10 +106,6 @@ namespace APRS_IS_Utils {
} else {
wifiState = "AP";
}
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
}
lastScreenOn = millis();
}
if (!Config.aprs_is.active) {
@@ -125,22 +124,21 @@ namespace APRS_IS_Utils {
aprsisState = "--";
}
#endif
if(aprsisState == "--" && !Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
lastScreenOn = millis();
}
}
if (wifiState != lastWifiState || aprsisState != lastAprsisState) { // wake display only on status change (not every loop) so display timeout works
if (!Config.display.alwaysOn && Config.display.timeout != 0) displayToggle(true);
lastScreenOn = millis();
lastWifiState = wifiState;
lastAprsisState = aprsisState;
}
secondLine = "WiFi: ";
secondLine += wifiState;
secondLine += " APRS-IS: ";
secondLine += aprsisState;
}
String checkForStartingBytes(const String& packet) {
int index = packet.indexOf("\x3c\xff\x01");
return (index != -1) ? packet.substring(0, index) : packet;
}
String buildPacketToUpload(const String& packet) {
int colonIndex = packet.indexOf(":");
String packetToUpload = packet.substring(3, colonIndex);
@@ -150,88 +148,79 @@ namespace APRS_IS_Utils {
packetToUpload += ",qAO,";
}
packetToUpload += Config.callsign;
packetToUpload += checkForStartingBytes(packet.substring(colonIndex));
packetToUpload += APRSPacketLib::checkForStartingBytes(packet.substring(colonIndex));
return packetToUpload;
}
bool processReceivedLoRaMessage(const String& sender, const String& packet, bool thirdParty) {
String buildIS2Meta() {
static String fixedTail; // rx_modem + rx_bitrate + rx_frequency: RX config doesn't change at runtime
if (fixedTail == "") {
// LoRa bit rate = SF * (BW / 2^SF) * 4/CR (CR = denominator: 5..8)
uint32_t bitrate = (uint32_t)(Config.loramodule.rxSpreadingFactor
* ((float)Config.loramodule.rxSignalBandwidth / (1 << Config.loramodule.rxSpreadingFactor))
* (4.0 / Config.loramodule.rxCodingRate4));
fixedTail = " rx_modem=2 rx_bitrate=" + String(bitrate) + " rx_frequency=" + String(Config.loramodule.rxFreq);
}
return "#is2-meta rx_rssi_dbm=" + String(lastAprsPacket.rssi) + " rx_snr_db=" + String(lastAprsPacket.snr, 1) + fixedTail;
}
bool processReceivedLoRaMessage(const String& sender, const String& packet) {
String receivedMessage;
int leftCurlyBraceIndex = packet.indexOf("{");
int colonIndex = packet.indexOf(":");
if (leftCurlyBraceIndex > 0) { // ack?
String ackMessage = "ack";
ackMessage.concat(packet.substring(leftCurlyBraceIndex + 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);
String ackCallsign = (Config.tacticalCallsign == "") ? Config.callsign : Config.tacticalCallsign; // ack from the callsign the message was addressed to
String ackPath = (lastAprsPacket.header == "") ? "RFONLY," + Config.beacon.path : Config.beacon.path;
String ackPacket = APRSPacketLib::generateAckPacket(ackCallsign, "APLRG1", ackPath, sender, packet);
if (ackPacket != "") STATION_Utils::addToOutputPacketBuffer(ackPacket);
receivedMessage = packet.substring(colonIndex + 1, leftCurlyBraceIndex);
} else {
receivedMessage = packet.substring(colonIndex + 1);
}
if (receivedMessage.indexOf("?") == 0) {
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
}
STATION_Utils::addToOutputPacketBuffer(QUERY_Utils::process(receivedMessage, sender, false, thirdParty));
if (QUERY_Utils::isQuery(receivedMessage)) {
if (!Config.display.alwaysOn && Config.display.timeout != 0) displayToggle(true);
String queryAnswer = QUERY_Utils::process(receivedMessage, sender, false, lastAprsPacket.header != "");
if (queryAnswer != "") STATION_Utils::addToOutputPacketBuffer(queryAnswer); // unknown query: no answer
lastScreenOn = millis();
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, "Callsign = " + sender, "TYPE --> QUERY", 0);
return true;
}
else {
} else {
return false;
}
}
void processLoRaPacket(const String& packet) {
if (passcodeValid && (aprsIsClient.connected() || modemLoggedToAPRSIS)) {
if (packet.indexOf("NOGATE") == -1 && packet.indexOf("RFONLY") == -1) {
int firstColonIndex = packet.indexOf(":");
if (firstColonIndex > 5 && firstColonIndex < (packet.length() - 1) && packet[firstColonIndex + 1] != '}' && packet.indexOf("TCPIP") == -1) {
const String& Sender = packet.substring(3, packet.indexOf(">"));
if (Sender != Config.callsign && Utils::callsignIsValid(Sender)) {
STATION_Utils::updateLastHeard(Sender);
Utils::typeOfPacket(packet.substring(3), 0); // LoRa-APRS
int doubleColonIndex = packet.indexOf("::");
const String& AddresseeAndMessage = packet.substring(doubleColonIndex + 2);
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
Addressee.trim();
if (lastAprsPacket.path.indexOf("NOGATE") == -1 && lastAprsPacket.path.indexOf("RFONLY") == -1) {
if (lastAprsPacket.header == "" && lastAprsPacket.path.indexOf("TCPIP") == -1) {
if (lastAprsPacket.sender != Config.callsign && Utils::callsignIsValid(lastAprsPacket.sender)) {
STATION_Utils::updateLastHeard(lastAprsPacket.sender);
Utils::updateLoRaPacketDisplayInfo(lastAprsPacket, 0); // LoRa-APRS
bool queryMessage = false;
if (doubleColonIndex > 10 && Addressee == Config.callsign) { // its a message for me!
queryMessage = processReceivedLoRaMessage(Sender, checkForStartingBytes(AddresseeAndMessage), false);
if (lastAprsPacket.type == 1 && lastAprsPacket.addressee == Config.callsign) { // its a message for me!
String AddresseeAndMessage = lastAprsPacket.addressee + ":" + lastAprsPacket.payload;
queryMessage = processReceivedLoRaMessage(lastAprsPacket.sender, APRSPacketLib::checkForStartingBytes(AddresseeAndMessage));
}
if (queryMessage) return;
const String& aprsPacket = buildPacketToUpload(packet);
const String& aprsPacketToUpload = buildPacketToUpload(packet);
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
}
lastScreenOn = millis();
#ifdef HAS_A7670
stationBeacon = true;
A7670_Utils::uploadToAPRSIS(aprsPacket);
A7670_Utils::uploadToAPRSIS(aprsPacketToUpload);
stationBeacon = false;
#else
upload(aprsPacket);
if (Config.aprs_is.is2Metadata && aprsPacketToUpload != "") {
upload(buildIS2Meta() + "\r\n" + aprsPacketToUpload); // IS2PLAIN: meta line right before the packet, same write
} else {
upload(aprsPacketToUpload);
}
#endif
Utils::println("---> Uploaded to APRS-IS");
Utils::println("(Uploaded to APRS-IS)");
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
}
}
@@ -284,21 +273,9 @@ namespace APRS_IS_Utils {
}
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;
String ackPacket = APRSPacketLib::generateAckPacket(Config.callsign, "APLRG1", "", sender, message);
if (ackPacket == "") return;
ackPacket = APRSPacketLib::generateAPRSISPacket(ackPacket);
#ifdef HAS_A7670
A7670_Utils::uploadToAPRSIS(ackPacket);
#else
@@ -324,7 +301,7 @@ namespace APRS_IS_Utils {
if (Config.digi.backupDigiMode) lastServerCheck = currentTime;
} else {
int doubleColonIndex = packet.indexOf("::");
if (Config.aprs_is.messagesToRF && doubleColonIndex > 0) {
if (doubleColonIndex > 0) { // messages for this station are always answered; messagesToRF only gates forwarding to RF
String Sender = packet.substring(0, packet.indexOf(">"));
const String& AddresseeAndMessage = packet.substring(doubleColonIndex + 2);
int colonIndex = AddresseeAndMessage.indexOf(":");
@@ -339,7 +316,7 @@ namespace APRS_IS_Utils {
} else {
receivedMessage = AddresseeAndMessage.substring(colonIndex + 1);
}
if (receivedMessage.indexOf("?") == 0) {
if (QUERY_Utils::isQuery(receivedMessage)) {
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")));
@@ -347,30 +324,29 @@ namespace APRS_IS_Utils {
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 += " ";
if (queryAnswer != "") { // unknown query: no answer
#ifdef HAS_A7670
A7670_Utils::uploadToAPRSIS(queryAnswer);
#else
upload(queryAnswer);
#endif
SYSLOG_Utils::logAPRSISTx(queryAnswer);
}
fifthLine = "APRS-IS ----> APRS-IS";
sixthLine = Utils::padForDisplay(Config.callsign);
sixthLine += "> ";
sixthLine += Sender;
seventhLine = "QUERY = ";
seventhLine += receivedMessage;
}
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
} else {
} else if (Config.aprs_is.messagesToRF) {
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
Utils::updateAPRSISPacketDisplayInfo(packet); // APRS-LoRa
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
}
}
@@ -380,7 +356,7 @@ namespace APRS_IS_Utils {
STATION_Utils::addToOutputPacketBuffer(buildPacketToTx(packet, 5));
displayToggle(true);
lastScreenOn = currentTime;
Utils::typeOfPacket(packet, 1); // APRS-LoRa
Utils::updateAPRSISPacketDisplayInfo(packet); // APRS-LoRa
Serial.println();
} else {
Serial.println(" ---> Rejected (Time): No Tx");
@@ -403,7 +379,6 @@ namespace APRS_IS_Utils {
String aprsisPacket = aprsIsClient.readStringUntil('\r');
aprsisPacket.trim(); // Serial.println(aprsisPacket);
processAPRSISPacket(aprsisPacket);
lastRxTime = millis();
}
}
#endif
+6 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -160,7 +160,7 @@ namespace BATTERY_Utils {
int sampleSum = 0;
for (int i = 0; i < adcReadings; i++) {
#if defined(ESP32_DIY_LoRa) || defined(ESP32_DIY_LoRa_915) || defined(ESP32_DIY_1W_LoRa) || defined(ESP32_DIY_1W_LoRa_915)
#if defined(ESP32_DIY_LoRa_SX1278) || defined(ESP32_DIY_LoRa_915_SX1276) || defined(ESP32_DIY_1W_LoRa_E22_400M30S) || defined(ESP32_DIY_1W_LoRa_915_E22_900M30S) || defined(ESP32_DIY_1W_LoRa_E32_400M30S) || defined(ESP32C3_SuperMini_DIY_LoRa_SX1278)
sampleSum = 0;
#else
#ifdef HAS_ADC_CALIBRATION
@@ -203,6 +203,10 @@ namespace BATTERY_Utils {
#ifdef LIGHTGATEWAY_PLUS_1_0
double inputDivider = (1.0 / (560.0 + 100.0)) * 100.0; // The voltage divider is a 560k + 100k resistor in series, 100k on the low side.
return (((sampleSum/adcReadings) * adcReadingTransformation) / inputDivider) + 0.41;
#elif defined(RAK3112)
return (sampleSum/adcReadings) * adcReadingTransformation * (3 * 1.73 * 1.187 / 3.3); // multiplier from MeshCore RAK3112Board.h
#elif defined(RAK3312)
return (sampleSum/adcReadings) * adcReadingTransformation * (3 * 1.73 * 1.187 / 3.3) * (1.667 / 2.11); // RAK3112 value scaled by Meshtastic RAK3312/RAK3112 ratio
#else
return (2 * (sampleSum/adcReadings) * adcReadingTransformation) + voltageDividerCorrection; // raw voltage without mapping
#endif
+30 -12
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -63,8 +63,10 @@ bool Configuration::writeFile() {
data["aprs_is"]["filter"] = aprs_is.filter;
data["aprs_is"]["messagesToRF"] = aprs_is.messagesToRF;
data["aprs_is"]["objectsToRF"] = aprs_is.objectsToRF;
data["aprs_is"]["is2Metadata"] = aprs_is.is2Metadata;
data["beacon"]["comment"] = beacon.comment;
data["beacon"]["rfCommentEveryXBeacons"] = beacon.rfCommentEveryXBeacons;
data["beacon"]["interval"] = beacon.interval;
data["beacon"]["latitude"] = beacon.latitude;
data["beacon"]["longitude"] = beacon.longitude;
@@ -106,6 +108,7 @@ bool Configuration::writeFile() {
data["lora"]["txCodingRate4"] = loramodule.txCodingRate4;
data["lora"]["txSignalBandwidth"] = loramodule.txSignalBandwidth;
data["lora"]["power"] = loramodule.power;
data["lora"]["cadActive"] = loramodule.cadActive;
int rxSpreadingFactor = loramodule.rxSpreadingFactor;
int txSpreadingFactor = loramodule.txSpreadingFactor;
@@ -156,6 +159,7 @@ bool Configuration::writeFile() {
data["tnc"]["enableSerial"] = tnc.enableSerial;
data["tnc"]["acceptOwn"] = tnc.acceptOwn;
data["tnc"]["aprsBridgeActive"] = tnc.aprsBridgeActive;
data["tnc"]["kissProtocol"] = tnc.kissProtocol;
data["mqtt"]["active"] = mqtt.active;
data["mqtt"]["server"] = mqtt.server;
@@ -235,7 +239,8 @@ bool Configuration::readFile() {
data["aprs_is"]["port"].isNull() ||
data["aprs_is"]["filter"].isNull() ||
data["aprs_is"]["messagesToRF"].isNull() ||
data["aprs_is"]["objectsToRF"].isNull()) needsRewrite = true;
data["aprs_is"]["objectsToRF"].isNull() ||
data["aprs_is"]["is2Metadata"].isNull()) needsRewrite = true;
aprs_is.active = data["aprs_is"]["active"] | false;
aprs_is.passcode = data["aprs_is"]["passcode"] | "XYZWV";
aprs_is.server = data["aprs_is"]["server"] | "rotate.aprs2.net";
@@ -243,10 +248,12 @@ bool Configuration::readFile() {
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.is2Metadata = data["aprs_is"]["is2Metadata"] | false;
if (data["beacon"]["latitude"].isNull() ||
data["beacon"]["longitude"].isNull() ||
data["beacon"]["comment"].isNull() ||
data["beacon"]["rfCommentEveryXBeacons"].isNull() ||
data["beacon"]["interval"].isNull() ||
data["beacon"]["overlay"].isNull() ||
data["beacon"]["symbol"].isNull() ||
@@ -261,6 +268,7 @@ bool Configuration::readFile() {
beacon.latitude = data["beacon"]["latitude"] | 0.0;
beacon.longitude = data["beacon"]["longitude"] | 0.0;
beacon.comment = data["beacon"]["comment"] | "LoRa APRS";
beacon.rfCommentEveryXBeacons = data["beacon"]["rfCommentEveryXBeacons"] | 1;
beacon.interval = data["beacon"]["interval"] | 15;
beacon.overlay = data["beacon"]["overlay"] | "L";
beacon.symbol = data["beacon"]["symbol"] | "a";
@@ -290,7 +298,6 @@ bool Configuration::readFile() {
#endif
digi.backupDigiMode = data["digi"]["backupDigiMode"] | false;
if (data["lora"]["rxActive"].isNull() ||
data["lora"]["rxFreq"].isNull() ||
data["lora"]["rxSpreadingFactor"].isNull() ||
@@ -301,7 +308,8 @@ bool Configuration::readFile() {
data["lora"]["txSpreadingFactor"].isNull() ||
data["lora"]["txCodingRate4"].isNull() ||
data["lora"]["txSignalBandwidth"].isNull() ||
data["lora"]["power"].isNull()) needsRewrite = true;
data["lora"]["power"].isNull() ||
data["lora"]["cadActive"].isNull()) needsRewrite = true;
loramodule.rxActive = data["lora"]["rxActive"] | true;
loramodule.rxFreq = data["lora"]["rxFreq"] | 433775000;
loramodule.rxSpreadingFactor = data["lora"]["rxSpreadingFactor"] | 12;
@@ -313,6 +321,7 @@ bool Configuration::readFile() {
loramodule.txCodingRate4 = data["lora"]["txCodingRate4"] | 5;
loramodule.txSignalBandwidth = data["lora"]["txSignalBandwidth"] | 125000;
loramodule.power = data["lora"]["power"] | 20;
loramodule.cadActive = data["lora"]["cadActive"] | true;
if (data["display"]["alwaysOn"].isNull() ||
data["display"]["timeout"].isNull() ||
@@ -367,11 +376,13 @@ bool Configuration::readFile() {
if (data["tnc"]["enableServer"].isNull() ||
data["tnc"]["enableSerial"].isNull() ||
data["tnc"]["acceptOwn"].isNull() ||
data["tnc"]["aprsBridgeActive"].isNull()) needsRewrite = true;
data["tnc"]["aprsBridgeActive"].isNull() ||
data["tnc"]["kissProtocol"].isNull()) needsRewrite = true;
tnc.enableServer = data["tnc"]["enableServer"] | false;
tnc.enableSerial = data["tnc"]["enableSerial"] | false;
tnc.acceptOwn = data["tnc"]["acceptOwn"] | false;
tnc.aprsBridgeActive = data["tnc"]["aprsBridgeActive"] | false;
tnc.kissProtocol = data["tnc"]["kissProtocol"] | true;
if (data["mqtt"]["active"].isNull() ||
data["mqtt"]["server"].isNull() ||
@@ -465,8 +476,10 @@ void Configuration::setDefaultValues() {
aprs_is.filter = "m/10";
aprs_is.messagesToRF = false;
aprs_is.objectsToRF = false;
aprs_is.is2Metadata = false;
beacon.comment = "LoRa APRS";
beacon.rfCommentEveryXBeacons = 1;
beacon.latitude = 0.0;
beacon.longitude = 0.0;
beacon.interval = 15;
@@ -503,6 +516,7 @@ void Configuration::setDefaultValues() {
loramodule.txCodingRate4 = 5;
loramodule.txSignalBandwidth = 125000;
loramodule.power = 20;
loramodule.cadActive = true;
display.alwaysOn = true;
display.timeout = 4;
@@ -535,6 +549,7 @@ void Configuration::setDefaultValues() {
tnc.enableSerial = false;
tnc.acceptOwn = false;
tnc.aprsBridgeActive = false;
tnc.kissProtocol = true;
mqtt.active = false;
mqtt.server = "";
@@ -567,14 +582,17 @@ void Configuration::setDefaultValues() {
void Configuration::setup() {
if (!SPIFFS.begin(false)) {
Serial.println("SPIFFS Mount Failed");
return;
} else {
Serial.println("SPIFFS Mounted");
}
Serial.println("SPIFFS Mount Failed, formatting...");
bool exists = SPIFFS.exists("/igate_conf.json");
if (!exists) {
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS Format Failed");
return;
}
}
Serial.println("SPIFFS Ready");
if (!SPIFFS.exists("/igate_conf.json")) {
Serial.println("Config not found, creating default...");
setDefaultValues();
writeFile();
delay(1000);
+105 -57
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -16,6 +16,7 @@
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <APRSPacketLib.h>
#include <WiFi.h>
#include "configuration.h"
#include "station_utils.h"
@@ -23,14 +24,13 @@
#include "digi_utils.h"
#include "wifi_utils.h"
#include "lora_utils.h"
#include "gps_utils.h"
#include "display.h"
#include "utils.h"
extern Configuration Config;
extern uint32_t lastScreenOn;
extern String iGateBeaconPacket;
extern APRSPacket lastAprsPacket;
extern String firstLine;
extern String secondLine;
extern String thirdLine;
@@ -43,53 +43,108 @@ extern bool backupDigiMode;
namespace DIGI_Utils {
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("*", "");
String cleanPath(const String& path) {
String result;
unsigned int start = 0;
while (true) {
int delim = path.indexOf(',', start);
int end = (delim == -1) ? path.length() : delim;
String token = path.substring(start, end);
if (token != "WIDE1*" && token != "WIDE2*") {
if (result.length() > 0) result += ",";
result += token;
}
if (delim == -1) break;
start = delim + 1;
}
return path;
return result;
}
String buildPacket(const String& path, const String& packet, bool thirdParty, bool crossFreq) {
static int pathTokenIndex(const String& path, const String& token) {
unsigned int start = 0;
while (start < path.length()) {
int end = path.indexOf(",", start);
if (end == -1) end = path.length();
if (path.substring(start, end) == token) return start;
start = end + 1;
}
return -1;
}
String processMode3Path(const String& path, const String& stationCallsign) {
int start = 0;
bool prevTokensAllStar = true;
int ownTokenEnd = -1;
while (start < path.length()) {
int delim = path.indexOf(',', start);
if (delim == -1) delim = path.length(); // busca todo hasta lograr encontra una coma o el final del string
String token = path.substring(start, delim);
bool tokenIsOwn = (token == stationCallsign) || (token == stationCallsign + "*");
bool tokenStar = token.endsWith("*");
if (tokenIsOwn) {
if (tokenStar) return ""; // already digipeated
if (!prevTokensAllStar) return ""; // earlier tokens must be marked
ownTokenEnd = delim;
break;
}
if (!tokenStar) prevTokensAllStar = false;
start = delim + 1;
}
if (ownTokenEnd == -1) return "";
String tempPacket = cleanPath(path.substring(0, ownTokenEnd));
return tempPacket + "*" + path.substring(ownTokenEnd);
}
String buildPacket(const String& path, const String& packet, bool crossFreq) {
String stationCallsign = (Config.tacticalCallsign == "" ? Config.callsign : Config.tacticalCallsign);
String suffix = thirdParty ? ":}" : ":";
String suffix = (lastAprsPacket.header != "") ? ":}" : ":";
int suffixIndex = packet.indexOf(suffix);
String packetToRepeat;
if (!crossFreq) {
int digiMode = Config.digi.mode;
String tempPath = path;
if (tempPath.indexOf("WIDE1-1") != -1 && (digiMode == 2 || digiMode == 3)) { // WIDE1-1
if (tempPath.indexOf("WIDE1-1") != -1 && (digiMode == 1 || digiMode == 2 || backupDigiMode)) { // WIDE1-1
if (tempPath.indexOf("*") != -1 ) return ""; // "*" shouldn't be in WIDE1-1 (only) type of packet
tempPath.replace("WIDE1-1", stationCallsign + "*");
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 3) { // WIDE2-n Digipeater
tempPath = cleanPathAsterisks(path);
if (tempPath.indexOf("WIDE2-1") != -1) {
tempPath.replace("WIDE2-1", stationCallsign + "*");
} else if (tempPath.indexOf("WIDE2-2") != -1) {
tempPath.replace("WIDE2-2", stationCallsign + "*,WIDE2-1");
if (pathTokenIndex(tempPath, "WIDE1-1") != 0) return ""; // WIDE1-1 must be the first hop
tempPath = stationCallsign + "*" + tempPath.substring(7);
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 2) { // WIDE2-n Digipeater
tempPath = cleanPath(path);
int idx = pathTokenIndex(tempPath, "WIDE2-1");
if (idx != -1) {
tempPath = tempPath.substring(0, idx) + stationCallsign + "*" + tempPath.substring(idx + 7);
} else {
return "";
idx = pathTokenIndex(tempPath, "WIDE2-2");
if (idx == -1) return "";
tempPath = tempPath.substring(0, idx) + stationCallsign + "*,WIDE2-1" + tempPath.substring(idx + 7);
}
} else if (digiMode == 3) { // Repeat if station callsign is in path (free to repeat).
tempPath = processMode3Path(tempPath, stationCallsign);
if (tempPath == "") return "";
}
packetToRepeat = packet.substring(0, packet.indexOf(",") + 1);
packetToRepeat += tempPath;
} else { // CrossFreq Digipeater
packetToRepeat = cleanPathAsterisks(packet.substring(0, suffixIndex));
if (packetToRepeat.indexOf(stationCallsign) != -1) return ""; // stationCallsign shouldn't be in path
String cleanedPath = cleanPath(path);
if (pathTokenIndex(cleanedPath, stationCallsign) != -1 ||
pathTokenIndex(cleanedPath, stationCallsign + "*") != -1) return ""; // stationCallsign shouldn't be in path (exact token, path only)
packetToRepeat = cleanPath(packet.substring(0, suffixIndex));
packetToRepeat += ",";
packetToRepeat += stationCallsign;
packetToRepeat += "*";
}
packetToRepeat += APRS_IS_Utils::checkForStartingBytes(packet.substring(suffixIndex));
packetToRepeat += APRSPacketLib::checkForStartingBytes(packet.substring(suffixIndex));
return packetToRepeat;
}
String generateDigipeatedPacket(const String& packet, bool thirdParty){
String generateDigipeatedPacket(const String& packet){
String temp;
if (thirdParty) { // only header is used
if (lastAprsPacket.header != "") { // thirdparty : only header is used
const String& header = packet.substring(0, packet.indexOf(":}"));
temp = header.substring(header.indexOf(">") + 1);
} else {
@@ -101,14 +156,14 @@ namespace DIGI_Utils {
if (commaIndex > 2) { // "path" found
const String& path = temp.substring(commaIndex + 1);
if (digiMode == 2 || backupDigiMode) {
if (digiMode == 1 || backupDigiMode) {
bool hasWide = path.indexOf("WIDE1-1") != -1;
if (hasWide || crossFreq) {
return buildPacket(path, packet, thirdParty, !hasWide);
return buildPacket(path, packet, !hasWide);
}
return "";
}
if (digiMode == 3) {
if (digiMode == 2) {
int wide1Index = path.indexOf("WIDE1-1");
int wide2Index = path.indexOf("WIDE2-");
bool hasWide1 = wide1Index != -1;
@@ -116,56 +171,49 @@ namespace DIGI_Utils {
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 (hasWide1 || hasWide2) return buildPacket(path, packet, false); // regular APRS with WIDEn-N
if (crossFreq) return buildPacket(path, packet, thirdParty, true); // CrossFreq (without WIDE)
if (crossFreq) return buildPacket(path, packet, true); // CrossFreq (without WIDE)
return "";
}
if (digiMode == 3) {
String stationCallsign = (Config.tacticalCallsign == "" ? Config.callsign : Config.tacticalCallsign);
bool containsOwnCall = path.indexOf(stationCallsign) != -1;
if (containsOwnCall) return buildPacket(path, packet, false);
return "";
}
return "";
}
if (commaIndex == -1 && (digiMode == 2 || backupDigiMode || digiMode == 3) && crossFreq) return buildPacket("", packet, thirdParty, true); // no "path" but is CrossFreq Digi
if (commaIndex == -1 && (digiMode == 1 || backupDigiMode || digiMode == 2) && crossFreq) return buildPacket("", packet, true); // no "path" but is CrossFreq Digi
return "";
}
void processLoRaPacket(const String& packet) {
if (packet.indexOf("NOGATE") >= 0) return;
if (lastAprsPacket.path.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 temp = (lastAprsPacket.header != "") ? packet.substring(packet.indexOf(":}") + 2) : packet.substring(3);
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 (lastAprsPacket.sender == stationCallsign) return; // Avoid listening to self packets
if (lastAprsPacket.header == "" && Config.tacticalCallsign == "" && !Utils::callsignIsValid(lastAprsPacket.sender)) return; // No thirdParty + no tactical y no valid callsign
if (STATION_Utils::isIn25SegHashBuffer(Sender, temp.substring(temp.indexOf(":") + 2))) return;
if (STATION_Utils::isIn25SegHashBuffer(lastAprsPacket.sender, temp.substring(temp.indexOf(":") + 2))) return;
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);
STATION_Utils::updateLastHeard(lastAprsPacket.sender);
Utils::updateLoRaPacketDisplayInfo(lastAprsPacket, 1); // Digi
bool queryMessage = false;
if (lastAprsPacket.type == 1) { // MESSAGE
if (lastAprsPacket.addressee == stationCallsign) { // it's a message for me!
String AddresseeAndMessage = lastAprsPacket.addressee + ":" + lastAprsPacket.payload;
queryMessage = APRS_IS_Utils::processReceivedLoRaMessage(lastAprsPacket.sender, APRSPacketLib::checkForStartingBytes(AddresseeAndMessage));
}
}
if (queryMessage) return; // answer should not be repeated.
String loraPacket = generateDigipeatedPacket(packet.substring(3), thirdPartyPacket);
String loraPacket = generateDigipeatedPacket(packet.substring(3));
if (loraPacket != "") {
STATION_Utils::addToOutputPacketBuffer(loraPacket);
if (Config.digi.ecoMode != 1) displayToggle(true);
+19 -5
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -57,15 +57,21 @@
String lastEpaperText;
#else
#include <Adafruit_GFX.h>
#if defined RPC_LORA_DIGIGATE_1W
#define SCREEN_HEIGHT 32
#else
#define SCREEN_HEIGHT 64
#endif
#ifdef HAS_SH1106
#include <Adafruit_SH110X.h>
Adafruit_SH1106G display(128, 64, &Wire, OLED_RST);
Adafruit_SH1106G display(128, SCREEN_HEIGHT, &Wire, OLED_RST);
#else
#include <Adafruit_SSD1306.h>
#ifdef HELTEC_WSL_V3_DISPLAY
Adafruit_SSD1306 display(128, 64, &Wire1, OLED_RST);
Adafruit_SSD1306 display(128, SCREEN_HEIGHT, &Wire1, OLED_RST);
#else
Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST);
Adafruit_SSD1306 display(128, SCREEN_HEIGHT, &Wire, OLED_RST);
#endif
#endif
#endif
@@ -75,6 +81,7 @@
extern Configuration Config;
bool displayFound = false;
int maxLines;
void displaySetup() {
#ifdef HAS_DISPLAY
@@ -97,6 +104,13 @@ void displaySetup() {
sprite.createSprite(160, 80);
#endif
#else
#if (SCREEN_HEIGHT == 64)
maxLines = 6;
#elif (SCREEN_HEIGHT == 32)
maxLines = 3;
#else
maxLines = 6;
#endif
#ifdef HAS_EPAPER
display.landscape();
display.printCenter("LoRa APRS iGate Initialising...");
@@ -297,7 +311,7 @@ void displayShow(const String& header, const String& line1, const String& line2,
display.setCursor(0, 0);
display.println(header);
display.setTextSize(1);
for (int i = 0; i < 6; i++) {
for (int i = 0; i < maxLines; i++) {
display.setCursor(0, 16 + (8 * i));
display.println(*lines[i]);
}
+89 -100
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -18,6 +18,7 @@
#include <APRSPacketLib.h>
#include <TinyGPS++.h>
#include <SPIFFS.h>
#include <WiFi.h>
#include "configuration.h"
#include "board_pinout.h"
@@ -31,22 +32,91 @@
#define GPS_BAUD 9600
#endif
#define GPS_BAUD_FILE "/gps_baud.dat"
#define GPS_BAUD_PROBE_TIME_MS 1500
const uint32_t gpsBaudRates[] = {9600, 115200, 38400, 4800, 57600}; // most common first
extern Configuration Config;
extern HardwareSerial gpsSerial;
extern TinyGPSPlus gps;
extern bool stationCallsignIsValid;
String distance, iGateBeaconPacket, iGateLoRaBeaconPacket;
String distance, iGateAPRSISBeaconPacket, iGateLoRaBeaconPacket;
namespace GPS_Utils {
String getiGateLoRaBeaconPacket() {
return iGateLoRaBeaconPacket;
#ifdef HAS_GPS
bool isKnownBaudRate(uint32_t baud) {
if (baud == GPS_BAUD) return true;
for (uint32_t knownBaud : gpsBaudRates) {
if (knownBaud == baud) return true;
}
return false;
}
uint32_t readSavedBaudRate() {
File file = SPIFFS.open(GPS_BAUD_FILE, "r");
if (!file) return 0;
uint32_t baud = file.readString().toInt();
file.close();
return isKnownBaudRate(baud) ? baud : 0;
}
void saveBaudRate(uint32_t baud) {
File file = SPIFFS.open(GPS_BAUD_FILE, "w");
if (!file) {
Utils::println("GPS: could not save baud rate");
return;
}
file.print(baud);
file.close();
}
bool probeBaudRate(uint32_t baud) {
gpsSerial.end();
delay(50);
gpsSerial.begin(baud, SERIAL_8N1, GPS_TX, GPS_RX);
uint32_t validSentences = gps.passedChecksum();
uint32_t probeStart = millis();
while (millis() - probeStart < GPS_BAUD_PROBE_TIME_MS) {
while (gpsSerial.available() > 0) gps.encode(gpsSerial.read());
if (gps.passedChecksum() > validSentences) return true; // NMEA with valid checksum
delay(10);
}
return false;
}
uint32_t detectBaudRate() {
uint32_t savedBaud = readSavedBaudRate();
uint32_t candidates[2 + sizeof(gpsBaudRates) / sizeof(gpsBaudRates[0])];
size_t count = 0;
auto addCandidate = [&](uint32_t baud) {
if (baud == 0) return;
for (size_t i = 0; i < count; i++) {
if (candidates[i] == baud) return;
}
candidates[count++] = baud;
};
addCandidate(savedBaud); // saved first, then board default, then common ones
addCandidate(GPS_BAUD);
for (uint32_t baud : gpsBaudRates) addCandidate(baud);
for (size_t i = 0; i < count; i++) {
if (i == 1) displayShow("GPS", "Searching", "baud rate...", "", 0);
bool found = probeBaudRate(candidates[i]);
if (!found && i == 0) found = probeBaudRate(candidates[i]); // retry first one: GPS may be slow to start
if (found) {
if (candidates[i] != savedBaud) saveBaudRate(candidates[i]);
return candidates[i];
}
}
return 0;
}
#endif
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);
String beaconCallsign = Config.callsign;
if (Config.callsign.indexOf("NOCALL-10") != 0) {
if (!stationCallsignIsValid) {
@@ -58,7 +128,7 @@ namespace GPS_Utils {
Config.digi.mode = 0;
Config.digi.backupDigiMode = false;
} else if (stationCallsignIsValid && Config.tacticalCallsign != "") {
beaconPacket = APRSPacketLib::generateBasePacket(Config.tacticalCallsign, "APLRG1", Config.beacon.path);
beaconCallsign = Config.tacticalCallsign;
Config.aprs_is.active = false;
Config.beacon.sendViaAPRSIS = false;
Config.digi.backupDigiMode = false;
@@ -68,15 +138,10 @@ namespace GPS_Utils {
Config.beacon.sendViaRF = false;
}
iGateBeaconPacket = beaconPacket;
iGateBeaconPacket += ",qAC:=";
iGateBeaconPacket += Config.beacon.overlay;
iGateBeaconPacket += encodedGPS;
String encodedGPS = APRSPacketLib::encodeGPSIntoBase91(Config.beacon.latitude, Config.beacon.longitude, 0, 0, Config.beacon.symbol, false, 0, true, Config.beacon.ambiguityLevel);
iGateLoRaBeaconPacket = beaconPacket;
iGateLoRaBeaconPacket += ":=";
iGateLoRaBeaconPacket += Config.beacon.overlay;
iGateLoRaBeaconPacket += encodedGPS;
iGateLoRaBeaconPacket = APRSPacketLib::generateBase91GPSBeaconPacket(beaconCallsign, "APLRG1", Config.beacon.path, Config.beacon.overlay, encodedGPS);
iGateAPRSISBeaconPacket = APRSPacketLib::generateAPRSISPacket(iGateLoRaBeaconPacket);
}
double calculateDistanceTo(double latitude, double longitude) {
@@ -101,94 +166,18 @@ namespace GPS_Utils {
}
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 (indexOfExclamation > 10) {
infoGPS = packet.substring(indexOfExclamation + OFFSET);
} else if (indexOfEqual > 10) {
infoGPS = packet.substring(indexOfEqual + OFFSET);
}
float decodedLatitude = APRSPacketLib::decodeBase91EncodedLatitude(infoGPS.substring(0,4));
float decodedLongitude = APRSPacketLib::decodeBase91EncodedLongitude(infoGPS.substring(4,8));
return buildDistanceAndComment(decodedLatitude, decodedLongitude, infoGPS.substring(12));
}
String getReceivedGPS(const String& packet) {
int indexOfExclamation = packet.indexOf(":!");
int indexOfEqual = packet.indexOf(":=");
int indexOfAt = packet.indexOf(":@");
String infoGPS;
if (indexOfExclamation > 10) {
infoGPS = packet.substring(indexOfExclamation + 2);
} else if (indexOfEqual > 10) {
infoGPS = packet.substring(indexOfEqual + 2);
} else if (indexOfAt > 10) {
infoGPS = packet.substring(indexOfAt + 9); // 9 = 2+7 (when 7 is timestamp characters)
}
String Latitude = infoGPS.substring(0,8); // First 8 characters are Latitude
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 (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);
uint32_t detectedBaud = detectBaudRate();
if (detectedBaud != 0) {
Utils::println("GPS: baud rate detected: " + String(detectedBaud));
} else {
Utils::println("GPS: no NMEA data found, using default baud rate: " + String(GPS_BAUD));
gpsSerial.end();
delay(50);
gpsSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_TX, GPS_RX);
}
}
#endif
generateBeacons();
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+154 -46
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -16,6 +16,7 @@
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <APRSPacketLib.h>
#include <RadioLib.h>
#include "configuration.h"
#include "network_manager.h"
@@ -23,20 +24,26 @@
#include "station_utils.h"
#include "board_pinout.h"
#include "syslog_utils.h"
#include "map_utils.h"
#include "ntp_utils.h"
#include "display.h"
#include "utils.h"
#include "thermal_utils.h"
extern Configuration Config;
extern NetworkManager *networkManager;
extern uint32_t lastRxTime;
extern bool packetIsBeacon;
extern std::vector<ReceivedPacket> receivedPackets;
bool operationDone = true;
bool transmitFlag = true;
bool operationDone = true;
bool transmitFlag = true;
#define DIFS_SLOTS 2 // Number of secuential CAD slots to consider a free channel to Tx
int backoffMax = 4; // Max Backoff value (number of CAD slots to wait before Tx)
#define CAD_MAX_WAIT_MS 10000 // Max total time waiting for a free channel, then the packet is dropped
unsigned long cadStartTime = 0;
#ifdef HAS_SX1262
SX1262 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN);
@@ -61,26 +68,68 @@ bool transmitFlag = true;
int rssi, freqError;
float snr;
APRSPacket lastAprsPacket;
#if defined(HAS_SX1278) || defined(HAS_SX1276)
#define CHIP_MIN_POWER 2 // PA_BOOST (below 2 RadioLib switches to RFO, not wired on most modules)
#define CHIP_MAX_POWER 20
#else // SX1262 / SX1268 / LLCC68
#define CHIP_MIN_POWER -9
#define CHIP_MAX_POWER 22
#endif
#ifndef RADIO_MAX_POWER // optional per board in board_pinout.h (e.g. 1W PA modules)
#define RADIO_MAX_POWER CHIP_MAX_POWER
#endif
namespace LoRa_Utils {
static String sanitizeForWeb(const String& s) { // replaces non-printable ASCII with '.' for WebUI display
String out = s;
for (int i = 0; i < (int)out.length(); i++) {
uint8_t c = (uint8_t)out[i];
if (c < 32 || c == 127) out.setCharAt(i, '.');
}
return out;
}
void setFlag(void) {
operationDone = true;
}
int validPower(int requested) {
const int maxPower = (RADIO_MAX_POWER < CHIP_MAX_POWER) ? RADIO_MAX_POWER : CHIP_MAX_POWER;
int power = constrain(requested, CHIP_MIN_POWER, maxPower);
#if defined(HAS_SX1278) || defined(HAS_SX1276)
if (power > 17 && power < 20) power = 17; // SX127x PA_BOOST: only 2-17 or 20
#endif
return power;
}
void setup() {
#ifdef RADIO_VCC_PIN // LoRa module power switch (QRP Labs LightGateway, T-Beam 1W)
pinMode(RADIO_VCC_PIN, OUTPUT);
digitalWrite(RADIO_VCC_PIN, HIGH);
#endif
#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
#ifdef RADIO_ANT_SW_PIN // RAK3312 antenna switch needs power
pinMode(RADIO_ANT_SW_PIN, OUTPUT);
digitalWrite(RADIO_ANT_SW_PIN, RADIO_ANT_SW_ON_STATE);
#endif
float freq = (float)Config.loramodule.rxFreq / 1000000;
#if defined(RADIO_HAS_XTAL)
radio.XTAL = true;
#endif
#if (defined(RADIO_RXEN) && defined(RADIO_TXEN)) // before begin() so RF switch is driven from start (Ebyte E22/E32 1W, QRP Labs LightGateway)
radio.setRfSwitchPins(RADIO_RXEN, RADIO_TXEN);
#elif defined(RADIO_RXEN) // T-Beam 1W: RXEN = LNA only, DIO2 drives the PA
radio.setRfSwitchPins(RADIO_RXEN, RADIOLIB_NC);
#endif
int state = radio.begin(freq);
if (state != RADIOLIB_ERR_NONE) {
Utils::println("Starting LoRa failed! State: " + String(state));
@@ -108,25 +157,17 @@ namespace LoRa_Utils {
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 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
int power = validPower(Config.loramodule.power);
if (power != Config.loramodule.power) {
Utils::println("LoRa power adjusted: " + String(Config.loramodule.power) + " -> " + String(power));
}
state = radio.setOutputPower(power);
if (state != RADIOLIB_ERR_NONE) {
Utils::println("LoRa setOutputPower failed! State: " + String(state)); // log and keep going
}
#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(120); // OCP ceiling for SX127x: ~120mA needed at +20dBm (not a fixed consumption)
#else // SX1262 / SX1268 / LLCC68 (also 1W Ebyte E22 / E220 modules)
radio.setCurrentLimit(140);
#endif
@@ -137,16 +178,15 @@ namespace LoRa_Utils {
#if defined(HAS_TCXO) && !defined(HAS_1W_LORA)
radio.setDio2AsRfSwitch();
#endif
#if defined(TTGO_T_BEAM_1W)
radio.setDio2AsRfSwitch(true); // DIO2 drives the 1W PA
radio.setPaRampTime(RADIOLIB_SX126X_PA_RAMP_800U); // PA needs >800us to settle (default 200us)
#endif
#ifdef HAS_TCXO
radio.setTCXO(1.8);
#endif
if (state == RADIOLIB_ERR_NONE) {
Utils::println("init : LoRa Module ... done!");
} else {
Utils::println("Starting LoRa failed! State: " + String(state));
while (true);
}
Utils::println("init : LoRa Module ... done!");
}
void changeFreqTx() {
@@ -167,8 +207,48 @@ namespace LoRa_Utils {
radio.setBandwidth(signalBandwidth);
}
bool doCAD() { // CAD (Channel Activity Detection)
return radio.scanChannel() != RADIOLIB_CHANNEL_FREE; // false=channel free | true=RADIOLIB_LORA_DETECTED or CAD failed
}
bool doDIFS() {
for (uint8_t i = DIFS_SLOTS; i > 0; i--) {
if (doCAD()) return false;
}
return true;
}
bool cadTimedOut() {
return millis() - cadStartTime > CAD_MAX_WAIT_MS;
}
void waitForDIFS() {
while (!doDIFS()) {
if (cadTimedOut()) return;
Serial.println("CAD/DIFS failed, retry...");
}
}
void doBEB() {
int backoffCounter = random(1, backoffMax + 1);
while (backoffCounter > 0) {
if (cadTimedOut()) return;
if (doCAD()) {
waitForDIFS(); // busy channel: freeze backoff and restart DIFS
} else {
backoffCounter--;
}
}
}
void sendNewPacket(const String& newPacket) {
if (!Config.loramodule.txActive) return;
#ifdef FAN_CTRL_PIN
if (THERMAL_Utils::isTxBlocked()) {
Utils::println("Thermal: Tx blocked (over-temperature), packet dropped: " + newPacket);
return;
}
#endif
if (Config.loramodule.txFreq != Config.loramodule.rxFreq) {
if (!packetIsBeacon || (packetIsBeacon && Config.beacon.beaconFreq == 1)) {
@@ -179,17 +259,37 @@ namespace LoRa_Utils {
#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 && networkManager->isConnected()) {
SYSLOG_Utils::log(3, newPacket, 0, 0.0, 0); // TX
bool cadDropped = false;
if (Config.loramodule.cadActive) {
cadStartTime = millis();
waitForDIFS(); // DIFS (Distributed Inter-Frame Space)
doBEB(); // BEB (Binary Exponential Backoff)
if (cadTimedOut()) {
Utils::println("CAD timeout, packet dropped: " + newPacket);
cadDropped = true;
}
}
if (!cadDropped) {
#ifdef FAN_CTRL_PIN
THERMAL_Utils::onTxStart();
#endif
int state = radio.transmit("\x3c\xff\x01" + newPacket);
#ifdef FAN_CTRL_PIN
THERMAL_Utils::onTxEnd();
#endif
transmitFlag = true;
if (state == RADIOLIB_ERR_NONE) {
if (Config.syslog.active && networkManager->isConnected()) {
SYSLOG_Utils::logLoRaTx(newPacket);
}
Utils::print("---> LoRa Packet Tx : ");
Utils::println(newPacket);
} else {
Utils::print(F("failed, code "));
Utils::println(String(state));
}
Utils::print("---> LoRa Packet Tx : ");
Utils::println(newPacket);
} else {
Utils::print(F("failed, code "));
Utils::println(String(state));
}
#ifdef INTERNAL_LED_PIN
if (Config.digi.ecoMode != 1) digitalWrite(INTERNAL_LED_PIN, LOW); // disabled in Ultra Eco Mode
@@ -199,6 +299,7 @@ namespace LoRa_Utils {
changeFreqRx();
}
}
if (cadDropped) radio.startReceive(); // no Tx end IRQ will restart Rx
}
String receivePacketFromSleep() {
@@ -229,28 +330,32 @@ namespace LoRa_Utils {
rssi = radio.getRSSI();
snr = radio.getSNR();
freqError = radio.getFrequencyError();
Utils::println("<--- LoRa Packet Rx : " + packet.substring(3));
Utils::println("(RSSI:" + String(rssi) + " / SNR:" + String(snr) + " / FreqErr:" + String(freqError) + ")");
Utils::println("<--- LoRa Packet Rx (RSSI:" + String(rssi) + " | SNR:" + String(snr) + " | FreqErr:" + String(freqError) + ") : " + packet.substring(3));
lastAprsPacket = APRSPacketLib::processReceivedPacket(packet.substring(3), rssi, snr, freqError);
if (Config.digi.ecoMode == 0) {
if (receivedPackets.size() >= 10) {
receivedPackets.erase(receivedPackets.begin());
}
ReceivedPacket receivedPacket;
receivedPacket.rxDate = NTP_Utils::getFormatedDate();
receivedPacket.rxTime = NTP_Utils::getFormatedTime();
receivedPacket.packet = packet.substring(3);
receivedPacket.packet = sanitizeForWeb(packet.substring(3));
receivedPacket.RSSI = rssi;
receivedPacket.SNR = snr;
receivedPackets.push_back(receivedPacket);
if (lastAprsPacket.type == 0 || lastAprsPacket.type == 4) { // 0 = GPS, 4 = Mic-E (only ones with position)
MAP_Utils::upsert(lastAprsPacket.sender, lastAprsPacket.latitude, lastAprsPacket.longitude, lastAprsPacket.path, lastAprsPacket.overlay + lastAprsPacket.symbol, lastAprsPacket.rssi, lastAprsPacket.snr);
}
}
if (Config.syslog.active && networkManager->isConnected()) {
SYSLOG_Utils::log(1, packet, rssi, snr, freqError); // RX
SYSLOG_Utils::logLoRaRx(lastAprsPacket, packet, rssi, snr, freqError); // RX
}
} else {
packet = "";
}
lastRxTime = millis();
return packet;
}
} else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
@@ -259,7 +364,7 @@ namespace LoRa_Utils {
freqError = radio.getFrequencyError();
Utils::println(F("CRC error!"));
if (Config.syslog.active && networkManager->isConnected()) {
SYSLOG_Utils::log(0, packet, rssi, snr, freqError); // CRC
SYSLOG_Utils::logCRCError(packet, rssi, snr, freqError);
}
packet = "";
} else {
@@ -278,6 +383,9 @@ namespace LoRa_Utils {
void sleepRadio() {
radio.sleep();
#ifdef RADIO_ANT_SW_PIN
digitalWrite(RADIO_ANT_SW_PIN, !RADIO_ANT_SW_ON_STATE);
#endif
}
}
+116
View File
@@ -0,0 +1,116 @@
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <ArduinoJson.h>
#include "map_utils.h"
#include "ntp_utils.h"
#define MAX_MAP_STATIONS 50
#define STATION_TTL_MS 3600000UL // 1 hora (en milisegundos)
std::vector<MapStation> mapStations;
namespace MAP_Utils {
void writeFormatedTime(char* dst, size_t size) { // gets NTP time ("HH:MM:SS") and writes it to dst if valid
String t = NTP_Utils::getFormatedTime();
if (t.length() == 8 && t.charAt(2) == ':') {
t.toCharArray(dst, size);
} else {
dst[0] = '\0';
}
}
void upsert(const String& callsign, float latitude, float longitude, const String& path, const String& symbol, int rssi, float snr) { // Upsert station only with valid position with callsign dedup.
for (auto &s : mapStations) { // 1) update
if (callsign.equals(s.callsign)) {
s.latitude = latitude;
s.longitude = longitude;
s.rssi = rssi;
s.snr = snr;
s.count++;
symbol.toCharArray(s.symbol, sizeof(s.symbol));
path.toCharArray(s.path, sizeof(s.path));
if (path.indexOf('*') == -1) s.heardDirect = true; // se oyo directo alguna vez; no se resetea si despues llega repetido
writeFormatedTime(s.lastHeard, sizeof(s.lastHeard));
s.lastHeardMillis = millis();
return;
}
}
if (mapStations.size() >= MAX_MAP_STATIONS) { // 2) full? delete oldest one
size_t oldest = 0;
for (size_t i = 1; i < mapStations.size(); i++) {
if (mapStations[i].lastHeardMillis < mapStations[oldest].lastHeardMillis) {
oldest = i;
}
}
mapStations.erase(mapStations.begin() + oldest);
}
MapStation st = {}; // 3) insert new
callsign.toCharArray(st.callsign, sizeof(st.callsign));
symbol.toCharArray(st.symbol, sizeof(st.symbol));
path.toCharArray(st.path, sizeof(st.path));
st.latitude = latitude;
st.longitude = longitude;
st.rssi = rssi;
st.snr = snr;
st.count = 1;
st.heardDirect = (path.indexOf('*') == -1); // primer paquete: directo si no trae digis usados
writeFormatedTime(st.lastHeard, sizeof(st.lastHeard));
st.lastHeardMillis = millis();
mapStations.push_back(st);
}
void purgeOldStations() {
uint32_t now = millis();
for (int i = (int)mapStations.size() - 1; i >= 0; i--) {
if (now - mapStations[i].lastHeardMillis > STATION_TTL_MS) {
mapStations.erase(mapStations.begin() + i);
}
}
}
String getStationsJson() {
purgeOldStations();
JsonDocument data;
for (size_t i = 0; i < mapStations.size(); i++) {
data[i]["callsign"] = mapStations[i].callsign;
data[i]["lat"] = mapStations[i].latitude;
data[i]["lon"] = mapStations[i].longitude;
data[i]["symbol"] = mapStations[i].symbol;
data[i]["RSSI"] = mapStations[i].rssi;
data[i]["SNR"] = mapStations[i].snr;
data[i]["count"] = mapStations[i].count;
data[i]["lastHeard"] = mapStations[i].lastHeard;
data[i]["path"] = mapStations[i].path;
data[i]["heardDirect"] = mapStations[i].heardDirect;
}
String buffer;
serializeJson(data, buffer);
return buffer;
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
+42 -1
View File
@@ -1,3 +1,21 @@
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "network_manager.h"
@@ -33,12 +51,14 @@ bool NetworkManager::_connectWiFi(const WiFiNetwork& network) {
WiFi.mode(_wifiAPmode ? WIFI_AP_STA : WIFI_STA);
Serial.println("[NM] Attempting to connect to WiFi: " + network.ssid);
_lastWiFiDisconnectReason = 0;
WiFi.begin(network.ssid.c_str(), network.psk.c_str());
Serial.print("[NM] Connecting ");
int attempts = 0;
while (!isWiFiConnected() && attempts < 10) {
if (_lastWiFiDisconnectReason == WIFI_REASON_AUTH_FAIL) break;
delay(500);
#ifdef INTERNAL_LED_PIN
digitalWrite(INTERNAL_LED_PIN,HIGH);
@@ -54,10 +74,19 @@ bool NetworkManager::_connectWiFi(const WiFiNetwork& network) {
if (isWiFiConnected()) return true;
if (_isWiFiAuthFailure()) {
Serial.println("[NM] Wrong WiFi password for SSID: " + network.ssid);
if (_wifiAuthFailSSID.isEmpty()) _wifiAuthFailSSID = network.ssid;
}
Serial.println("[NM] Failed to connect to WiFi after " + String(attempts) + " attempts. SSID: " + network.ssid);
return false;
}
bool NetworkManager::_isWiFiAuthFailure() const {
uint8_t reason = _lastWiFiDisconnectReason;
return reason == WIFI_REASON_AUTH_FAIL || reason == WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT || reason == WIFI_REASON_HANDSHAKE_TIMEOUT;
}
void NetworkManager::_processAPTimeout() {
if (!_wifiAPmode || _apTimeout == 0) {
return;
@@ -75,8 +104,11 @@ void NetworkManager::_processAPTimeout() {
}
}
void NetworkManager::_onNetworkEvent(arduino_event_id_t event, arduino_event_info_t /*info*/) {
void NetworkManager::_onNetworkEvent(arduino_event_id_t event, arduino_event_info_t info) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
_lastWiFiDisconnectReason = info.wifi_sta_disconnected.reason;
break;
case ARDUINO_EVENT_ETH_START:
Serial.println("[NM] ETH Started");
if (!_hostName.isEmpty()) {
@@ -206,6 +238,7 @@ bool NetworkManager::connectWiFi() {
if (_wifiNetworks.empty()) {
return false;
}
_wifiAuthFailSSID = "";
for (size_t i = 0; i < _wifiNetworks.size(); i++) {
disconnectWiFi();
@@ -238,6 +271,10 @@ String NetworkManager::getWiFiAPSSID() const {
return WiFi.softAPSSID();
}
String NetworkManager::getWiFiAuthFailSSID() const {
return _wifiAuthFailSSID;
}
IPAddress NetworkManager::getWiFiIP() const {
return WiFi.localIP();
}
@@ -321,6 +358,10 @@ bool NetworkManager::isWifiAPActive() const {
return _wifiAPmode;
}
bool NetworkManager::hasActiveInterface() const {
return _wifiSTAmode || _wifiAPmode || _ethernetMode;
}
// Check if Ethernet is connected
bool NetworkManager::isEthernetConnected() const {
return _ethernetMode && _ethernetConnected && ETH.linkUp();
+31 -15
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -16,8 +16,7 @@
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <esp_sntp.h>
#include "configuration.h"
#include "network_manager.h"
#include "ntp_utils.h"
@@ -26,18 +25,19 @@
extern Configuration Config;
extern NetworkManager *networkManager;
WiFiUDP ntpUDP;
NTPClient* timeClient = nullptr;
bool ntpStarted = false;
bool ntpSynced = false;
namespace NTP_Utils {
bool setup() {
if (networkManager->isConnected() && Config.digi.ecoMode == 0 && Config.callsign != "NOCALL-10") {
int gmt = Config.ntp.gmtCorrection * 3600;
long gmt = Config.ntp.gmtCorrection * 3600;
Serial.println("[NTP] Setting up, TZ offset: " + String(gmt) + " Server: " + Config.ntp.server);
timeClient = new NTPClient(ntpUDP, Config.ntp.server.c_str(), gmt, 15 * 60 * 1000); // Update interval 15 min
timeClient->begin();
sntp_set_sync_interval(3 * 60 * 60 * 1000); // Update interval 3 hours (ESP32 internal clock drift is negligible in that time)
configTime(gmt, 0, Config.ntp.server.c_str()); // SNTP runs in background (DNS + retries) and never blocks the loop
ntpStarted = true;
return true;
}
return false;
@@ -47,20 +47,36 @@ namespace NTP_Utils {
if (!networkManager->isConnected() || Config.digi.ecoMode != 0 || Config.callsign == "NOCALL-10") {
return;
}
if (timeClient == nullptr) {
if (!ntpStarted) {
if (!setup()) {
return;
}
}
timeClient->update();
if (!ntpSynced) {
struct tm timeinfo;
if (getLocalTime(&timeinfo, 0)) {
ntpSynced = true;
Serial.println("[NTP] Time synced: " + getFormatedTime());
}
}
}
String getFormatedTime() {
if (networkManager->isConnected() && Config.digi.ecoMode == 0 && timeClient != nullptr) {
return timeClient->getFormattedTime();
}
return "DigiEcoMode Active";
if (Config.digi.ecoMode != 0) return "DigiEcoMode Active";
struct tm timeinfo;
if (!ntpStarted || !getLocalTime(&timeinfo, 0)) return ""; // 0 ms: don't wait (default timeout would block 5 s)
char formatedTime[9];
strftime(formatedTime, sizeof(formatedTime), "%H:%M:%S", &timeinfo);
return String(formatedTime);
}
String getFormatedDate() {
if (Config.digi.ecoMode != 0) return "";
struct tm timeinfo;
if (!ntpStarted || !getLocalTime(&timeinfo, 0)) return ""; // 0 ms: don't wait (default timeout would block 5 s)
char formatedDate[11];
strftime(formatedDate, sizeof(formatedDate), "%Y-%m-%d", &timeinfo);
return String(formatedDate);
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -74,7 +74,7 @@ namespace OTA_Utils {
Serial.println(success ? "OTA update finished successfully!" : "There was an error during OTA update!");
displayShow("", "", statusMessage, "", rebootMessage, "", "", 4000);
isUpdatingOTA = false;
if (!success) isUpdatingOTA = false;
}
}
+32 -9
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -20,6 +20,7 @@
#include "battery_utils.h"
#include "board_pinout.h"
#include "power_utils.h"
#include "wx_utils.h"
#include "utils.h"
#if defined(HAS_AXP192) || defined(HAS_AXP2101)
@@ -65,7 +66,7 @@ namespace POWER_Utils {
}
void vext_ctrl_OFF() {
digitalWrite(VEXT_CTRL_PIN, Config.digi.ecoMode == 1 ? VEXT_CTRL_ON_STATE : !VEXT_CTRL_ON_STATE);
digitalWrite(VEXT_CTRL_PIN, !VEXT_CTRL_ON_STATE); // always off, regardless of ecoMode
}
#endif
@@ -110,9 +111,7 @@ namespace POWER_Utils {
PMU.enableALDO3();
#endif
#endif
#ifdef HELTEC_WIRELESS_TRACKER
adc_ctrl_ON();
#endif
// HELTEC_WIRELESS_TRACKER: GPS shares Vext (GPIO3) with the TFT, so it follows the display power (not controlled here)
//gpsIsActive = true;
}
@@ -128,9 +127,7 @@ namespace POWER_Utils {
PMU.disableALDO3();
#endif
#endif
#ifdef HELTEC_WIRELESS_TRACKER
adc_ctrl_OFF();
#endif
// HELTEC_WIRELESS_TRACKER: GPS shares Vext with the TFT, turned off with it (see activateGPS)
//gpsIsActive = false;
}
@@ -198,7 +195,10 @@ namespace POWER_Utils {
PMU.setDC1Voltage(3300);
PMU.enableDC1();
#ifdef TTGO_T_Beam_S3_SUPREME_V3
PMU.setALDO1Voltage(3300);
PMU.setALDO1Voltage(3300); // BME280, QMC6310, QMI8658, OLED
PMU.enableALDO1();
PMU.setALDO2Voltage(3300); // sensors/OLED I2C + RTC PCF8563 (must stay on)
PMU.enableALDO2();
#endif
PMU.setButtonBatteryChargeVoltage(3300);
PMU.enableButtonBatteryCharge();
@@ -210,6 +210,23 @@ namespace POWER_Utils {
#endif
}
#ifdef SOLAR_CHARGE_PIN
void checkSolarCharge() { // called on each beacon: disable solar charging when too cold for Li-Ion
static bool chargingEnabled = true;
float boardTemp = WX_Utils::readBoardTemperature();
if (isnan(boardTemp)) {
chargingEnabled = true; // sensor failure: keep charging (previous behaviour)
} else if (chargingEnabled && boardTemp < SOLAR_CHARGE_DISABLE_TEMP) {
chargingEnabled = false;
Serial.println("[SOLAR] Charging disabled, board temp: " + String(boardTemp, 1) + "C");
} else if (!chargingEnabled && boardTemp > SOLAR_CHARGE_ENABLE_TEMP) {
chargingEnabled = true;
Serial.println("[SOLAR] Charging enabled, board temp: " + String(boardTemp, 1) + "C");
}
digitalWrite(SOLAR_CHARGE_PIN, chargingEnabled ? SOLAR_CHARGE_ENABLED_STATE : !SOLAR_CHARGE_ENABLED_STATE);
}
#endif
void setup() {
#ifdef HAS_AXP192
Wire.begin(SDA, SCL);
@@ -254,6 +271,12 @@ namespace POWER_Utils {
pinMode(BATTERY_PIN, INPUT);
#endif
#ifdef SOLAR_CHARGE_PIN
pinMode(SOLAR_CHARGE_PIN, OUTPUT);
digitalWrite(SOLAR_CHARGE_PIN, SOLAR_CHARGE_ENABLED_STATE); // solar charging enabled by default
gpio_hold_dis((gpio_num_t)SOLAR_CHARGE_PIN); // release hold kept during deep sleep (if any)
#endif
#ifdef INTERNAL_LED_PIN
pinMode(INTERNAL_LED_PIN, OUTPUT);
digitalWrite(INTERNAL_LED_PIN, LOW);
+19 -29
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -16,6 +16,7 @@
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <APRSPacketLib.h>
#include "configuration.h"
#include "battery_utils.h"
#include "station_utils.h"
@@ -36,11 +37,17 @@ extern String versionNumber;
namespace QUERY_Utils {
bool isQuery(const String& message) { // "?..." queries, plus "HELP" (any case) without the "?"
String queryQuestion = message;
queryQuestion.toUpperCase();
return queryQuestion.startsWith("?") || queryQuestion == "HELP";
}
String process(const String& query, const String& station, bool queryFromAPRSIS, bool thirdParty) {
String answer;
String queryQuestion = query;
queryQuestion.toUpperCase();
if (queryQuestion == "?APRS?" || queryQuestion == "H" || queryQuestion == "HELP" || queryQuestion=="?") {
if (queryQuestion == "?APRS?" || queryQuestion == "?" || queryQuestion == "HELP") {
answer.concat("?APRSV ?APRSP ?APRSL ?APRSSR ?EM=? ?TX=? "); // ?APRSH ?WHERE callsign
} else if (queryQuestion == "?APRSV") {
answer.concat("CA2RXU_LoRa_iGate v");
@@ -82,7 +89,7 @@ namespace QUERY_Utils {
int radioTxActive = Config.loramodule.txActive;
bool onlyRadioActive = radioTxActive && Config.loramodule.rxActive && !Config.aprs_is.active;
if (queryQuestion.startsWith("?EM=OFF")) {
if ((digiMode == 2 || digiMode == 3) && onlyRadioActive) {
if ((digiMode == 1 || digiMode == 2 || digiMode == 3) && onlyRadioActive) {
if (digiEcoMode == 1 || digiEcoMode == 2) { // Exit Digipeater EcoMode or Digipeater without WiFiAP
answer = (digiEcoMode == 1) ? "DigiEcoMode:OFF" : "Digipeater + WiFiAP enabled";
Config.digi.ecoMode = 0;
@@ -97,7 +104,7 @@ namespace QUERY_Utils {
answer = "Digipeater Mode control not possible";
}
} else if (queryQuestion.startsWith("?EM=ON")) {
if ((digiMode == 2 || digiMode == 3) && onlyRadioActive) {
if ((digiMode == 1 || digiMode == 2 || digiMode == 3) && onlyRadioActive) {
if (digiEcoMode == 0) { // Start Digipeater EcoMode
answer = "DigiEcoMode:ON";
Config.digi.ecoMode = 1;
@@ -139,32 +146,15 @@ namespace QUERY_Utils {
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;
}
}
queryAnswer += "::";
answer += " *";
answer += char(random(97, 123));
answer += char(random(97, 123));
answer += "*";
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;
String queryCallsign = (Config.tacticalCallsign == "") ? Config.callsign : Config.tacticalCallsign;
String queryPath = queryFromAPRSIS ? "" : (thirdParty ? Config.beacon.path : "RFONLY," + Config.beacon.path);
String queryAnswer = APRSPacketLib::generateMessagePacket(queryCallsign, "APLRG1", queryPath, station, answer);
return queryFromAPRSIS ? APRSPacketLib::generateAPRSISPacket(queryAnswer) : queryAnswer;
}
}
+5 -1
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -21,6 +21,7 @@
#include "sleep_utils.h"
#include "digi_utils.h"
#include "lora_utils.h"
#include "thermal_utils.h"
extern Configuration Config;
@@ -75,6 +76,9 @@ namespace SLEEP_Utils {
Serial.print("(Sleeping : "); Serial.print(timeToSleep); Serial.println("seconds)");
delay(100);
LoRa_Utils::wakeRadio();
#ifdef FAN_CTRL_PIN
THERMAL_Utils::turnOffFan(); // GPIOs keep their level in light sleep
#endif
esp_light_sleep_start();
}
}
+3 -10
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -25,15 +25,11 @@
#include "utils.h"
#include <vector>
#define SECS_TO_WAIT 3 // soon to be deleted...
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;
@@ -214,15 +210,11 @@ namespace STATION_Utils {
}
void processOutputPacketBuffer() {
int timeToWait = SECS_TO_WAIT * 1000; // 3 segs between packet Tx and also Rx ???
uint32_t lastRx = millis() - lastRxTime;
uint32_t lastTx = millis() - lastTxTime;
if (outputPacketBuffer.size() > 0 && lastTx > timeToWait && lastRx > timeToWait) {
if (outputPacketBuffer.size() > 0) {
if (outputPacketBuffer[0].isBeacon) packetIsBeacon = true;
LoRa_Utils::sendNewPacket(outputPacketBuffer[0].packet);
if (outputPacketBuffer[0].isBeacon) packetIsBeacon = false;
outputPacketBuffer.erase(outputPacketBuffer.begin());
lastTxTime = millis();
}
if (shouldSleepLowVoltage) {
while (outputPacketBuffer.size() > 0) {
@@ -242,6 +234,7 @@ namespace STATION_Utils {
}
void addToOutputPacketBuffer(const String& packet, bool flag) {
if (packet == "") return; // never queue an empty packet
outputPacketBuffer.emplace_back(OutputPacketBuffer{packet, flag});
}
+133 -95
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -16,6 +16,7 @@
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#include <APRSPacketLib.h>
#include <WiFiUdp.h>
#include "configuration.h"
#include "network_manager.h"
@@ -30,113 +31,150 @@ extern String versionNumber;
WiFiUDP udpClient;
namespace {
String createSyslogStart() {
String syslogStartPacket = "<165>1 - ";
syslogStartPacket.concat(Config.callsign);
syslogStartPacket.concat(" CA2RXU_LoRa_iGate_");
syslogStartPacket.concat(versionNumber);
syslogStartPacket.concat(" - - - "); //RFC5424 The Syslog Protocol
return syslogStartPacket;
}
void sendSyslogPacket(const String& syslogPacket) {
udpClient.beginPacket(Config.syslog.server.c_str(), Config.syslog.port);
udpClient.write((const uint8_t*)syslogPacket.c_str(), syslogPacket.length());
udpClient.endPacket();
}
String formatSignalData(int rssi, float snr, int freqError) {
char signalData[35];
snprintf(signalData, sizeof(signalData), " / %ddBm / %.2fdB / %dHz", rssi, snr, freqError);
return String(signalData);
}
}
namespace SYSLOG_Utils {
void log(const uint8_t type, const String& packet, const int rssi, const float snr, const int freqError) {
void logLoRaRx(APRSPacket& aprsPacket, const String& packet, const int rssi, const float snr, const int freqError) {
if (Config.syslog.active && networkManager->isConnected()) {
String syslogPacket = "<165>1 - ";
syslogPacket.concat(Config.callsign);
syslogPacket.concat(" CA2RXU_LoRa_iGate_");
syslogPacket.concat(versionNumber);
syslogPacket.concat(" - - - "); //RFC5424 The Syslog Protocol
String syslogPacket = createSyslogStart();
syslogPacket.concat("RX / ");
char signalData[35];
snprintf(signalData, sizeof(signalData), " / %ddBm / %.2fdB / %dHz", rssi, snr, freqError);
int colonIndex = packet.indexOf(":");
char nextChar = packet[colonIndex + 1];
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);
switch (aprsPacket.type) {
case 1: // MESSAGE
syslogPacket.concat("MESSAGE / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(aprsPacket.addressee);
syslogPacket.concat(":");
syslogPacket.concat(aprsPacket.payload);
break;
case 1: // RX
syslogPacket.concat("RX / ");
if (nextChar == ':') {
syslogPacket.concat("MESSAGE / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else if (nextChar == '!' || nextChar == '=' || nextChar == '@') {
syslogPacket.concat("GPS / ");
syslogPacket.concat(sender);
syslogPacket.concat(" / ");
if (packet.indexOf("WIDE1-1") > 10) {
syslogPacket.concat(packet.substring(greaterThanIndex + 1, packet.indexOf(",")));
syslogPacket.concat(" / WIDE1-1");
} else {
syslogPacket.concat(packet.substring(greaterThanIndex + 1, colonIndex));
syslogPacket.concat(" / -");
case 0: // GPS
syslogPacket.concat("GPS / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" / ");
if (aprsPacket.path.indexOf("WIDE1-1") != -1) {
syslogPacket.concat(aprsPacket.tocall);
syslogPacket.concat(" / WIDE1-1");
} else {
syslogPacket.concat(aprsPacket.tocall);
if (aprsPacket.path != "") {
syslogPacket.concat(",");
syslogPacket.concat(aprsPacket.path);
}
} else if (nextChar == '>') {
syslogPacket.concat("STATUS / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else if (nextChar == '`') {
syslogPacket.concat("MIC-E / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else if (nextChar == ';') {
syslogPacket.concat("OBJECT / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else if (telemetryPacketIndex >= 10 && packet.indexOf(":=/") == -1) {
syslogPacket.concat("TELEMETRY / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(telemetryPacketIndex + 3));
} else {
syslogPacket.concat(packet);
}
syslogPacket.concat(signalData);
if (nextChar == '!' || nextChar == '=' || nextChar == '@') {
syslogPacket.concat(" / ");
syslogPacket.concat(GPS_Utils::getDistanceAndComment(packet));
syslogPacket.concat(" / -");
}
break;
case 2: // APRSIS TX
syslogPacket.concat("APRSIS TX / ");
if (nextChar == '>') {
syslogPacket.concat("StartUp_Status / ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else if (nextChar == ':') {
syslogPacket.concat("QUERY / ");
syslogPacket.concat(packet);
} else {
syslogPacket.concat("BEACON / ");
syslogPacket.concat(packet);
}
case 2: // STATUS
syslogPacket.concat("STATUS / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(aprsPacket.payload);
break;
case 3: // TX
syslogPacket.concat("TX / ");
if (packet.indexOf("RFONLY") > 10) {
syslogPacket.concat("RFONLY / ");
syslogPacket.concat(packet);
} else if (nextChar == ':') {
syslogPacket.concat("MESSAGE / ");
syslogPacket.concat(sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.substring(colonIndex + 2));
} else {
syslogPacket.concat(packet);
}
case 4: // MIC-E
syslogPacket.concat("MIC-E / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(packet.indexOf(":`") != -1 ? "`" : "'"); // restore the actual indicator received
break;
default:
syslogPacket = "<165>1 - ERROR LoRa - - - ERROR / Error in Syslog Packet"; //RFC5424 The Syslog Protocol
case 5: // OBJECT
syslogPacket.concat("OBJECT / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(aprsPacket.payload);
break;
case 3: // TELEMETRY
syslogPacket.concat("TELEMETRY / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(aprsPacket.payload);
break;
default: // type == 6, unrecognized -- payload is the full, uncut packet
syslogPacket.concat(aprsPacket.payload);
break;
}
udpClient.beginPacket(Config.syslog.server.c_str(), Config.syslog.port);
udpClient.write((const uint8_t*)syslogPacket.c_str(), syslogPacket.length());
udpClient.endPacket();
syslogPacket.concat(formatSignalData(rssi, snr, freqError));
if (aprsPacket.type == 0 || aprsPacket.type == 4) { // GPS or Mic-E -- both already carry
syslogPacket.concat(" / "); // clean lat/lon + comment in aprsPacket
syslogPacket.concat(GPS_Utils::buildDistanceAndComment(aprsPacket.latitude, aprsPacket.longitude, aprsPacket.payload));
}
sendSyslogPacket(syslogPacket);
}
}
void logAPRSISTx(const String& packet) {
if (Config.syslog.active && networkManager->isConnected()) {
String syslogPacket = createSyslogStart();
syslogPacket.concat("APRSIS TX / ");
APRSPacket aprsPacket = APRSPacketLib::processReceivedPacket(packet, 0, 0, 0);
if (aprsPacket.type == 2) { // STATUS
syslogPacket.concat("StartUp_Status / ");
syslogPacket.concat(aprsPacket.payload);
} else if (aprsPacket.type == 1) { // MESSAGE
syslogPacket.concat("QUERY / ");
syslogPacket.concat(packet);
} else {
syslogPacket.concat("BEACON / ");
syslogPacket.concat(packet);
}
sendSyslogPacket(syslogPacket);
}
}
void logLoRaTx(const String& packet) {
if (Config.syslog.active && networkManager->isConnected()) {
String syslogPacket = createSyslogStart();
syslogPacket.concat("TX / ");
APRSPacket aprsPacket = APRSPacketLib::processReceivedPacket(packet, 0, 0, 0);
if (aprsPacket.path.indexOf("RFONLY") != -1) {
syslogPacket.concat("RFONLY / ");
syslogPacket.concat(packet);
} else if (aprsPacket.type == 1) {
syslogPacket.concat("MESSAGE / ");
syslogPacket.concat(aprsPacket.sender);
syslogPacket.concat(" ---> ");
syslogPacket.concat(aprsPacket.addressee);
syslogPacket.concat(":");
syslogPacket.concat(aprsPacket.payload);
} else {
syslogPacket.concat(packet);
}
sendSyslogPacket(syslogPacket);
}
}
void logCRCError(const String& packet, const int rssi, const float snr, const int freqError) {
if (Config.syslog.active && networkManager->isConnected()) {
String syslogPacket = createSyslogStart();
syslogPacket.concat("CRC / CRC-ERROR / ");
syslogPacket.concat(packet);
syslogPacket.concat(formatSignalData(rssi, snr, freqError));
sendSyslogPacket(syslogPacket);
}
}
+3 -3
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
@@ -71,8 +71,9 @@ namespace TELEMETRY_Utils {
void sendBaseTelemetryPacket(const String& prefix, const std::vector<String>& values) {
String packet = prefix + joinWithCommas(values);
String currentCallsign = (Config.tacticalCallsign != "") ? Config.tacticalCallsign : Config.callsign;
String baseRFTelemetryPacket = APRSPacketLib::generateMessagePacket(currentCallsign, "APLRG1", Config.beacon.path, currentCallsign, packet);
if (Config.beacon.sendViaAPRSIS) {
String baseAPRSISTelemetryPacket = APRSPacketLib::generateMessagePacket(currentCallsign, "APLRG1", "TCPIP,qAC", currentCallsign, packet);
String baseAPRSISTelemetryPacket = APRSPacketLib::generateAPRSISPacket(baseRFTelemetryPacket);
#ifdef HAS_A7670
A7670_Utils::uploadToAPRSIS(baseAPRSISTelemetryPacket);
#else
@@ -80,7 +81,6 @@ namespace TELEMETRY_Utils {
#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);
}
+154
View File
@@ -0,0 +1,154 @@
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
// Fan / thermal control adapted from KJ7NYE's LoRa_FieldOps_APRS_Tracker (GPLv3)
// NTC circuit (T-Beam 1W V1.0 schematic): 3V3 -> NCP18XH103F03RB -> TEMP_PIN -> 10K -> GND
#include "board_pinout.h"
#ifdef FAN_CTRL_PIN
#include <Arduino.h>
#include <math.h>
#include "thermal_utils.h"
#include "utils.h"
#define NTC_B 3380.0f // B25/50 (K)
#define NTC_R25 10000.0f // NTC resistance at 25C (ohm)
#define NTC_T0 298.15f // 25C in Kelvin
#define NTC_R_FIXED 10000.0f // pull-down resistor (ohm)
#define NTC_VCC_MV 3300.0f
#define NTC_MIN_VALID_MV 100 // below this the sample is a failed ADC2 read (100mV = ~-45C)
#define FAN_ON_TEMP 50.0f // fan on at or above
#define FAN_OFF_TEMP 42.0f // fan off below (hysteresis)
#define TEMP_WARNING 75.0f
#define TEMP_TX_BLOCK 85.0f // Tx blocked at or above
#define TEMP_TX_UNBLOCK 75.0f // Tx allowed again below (hysteresis)
#define TX_COOLDOWN_MS 30000 // fan kept on after TX
#define TEMP_SAMPLE_MS 30000
static float currentTemperature = 25.0f;
static bool fanOn = false;
static bool txBlocked = false;
static bool txCooldownActive = false;
static uint32_t txEndTime = 0;
static uint32_t lastTempSample = 0;
static bool firstTempSample = true;
namespace THERMAL_Utils {
float readTemperature() { // TEMP_PIN is on ADC2: reads fail (return 0) while Wi-Fi holds the ADC2 arbiter
analogReadMilliVolts(TEMP_PIN); // dummy read to settle the ADC
delay(1);
uint32_t sum = 0;
int validSamples = 0;
for (int i = 0; i < 5; i++) {
uint32_t sample = analogReadMilliVolts(TEMP_PIN);
if (sample > NTC_MIN_VALID_MV) {
sum += sample;
validSamples++;
}
delay(3);
}
if (validSamples == 0) return NAN;
float mv = (float)sum / validSamples;
if (mv >= NTC_VCC_MV) return NAN;
float ntcResistance = NTC_R_FIXED * (NTC_VCC_MV - mv) / mv;
float kelvin = 1.0f / (1.0f / NTC_T0 + (1.0f / NTC_B) * logf(ntcResistance / NTC_R25));
return kelvin - 273.15f;
}
void setFan(bool on, const String& reason) {
if (fanOn == on) return;
fanOn = on;
digitalWrite(FAN_CTRL_PIN, on ? HIGH : LOW);
Utils::println("Thermal: Fan " + String(on ? "ON" : "OFF") + " (" + reason + ", " + String(currentTemperature, 1) + " C)");
}
void updateFan() {
if (!fanOn) {
if (txCooldownActive) {
setFan(true, "TX cooldown");
} else if (currentTemperature >= FAN_ON_TEMP) {
setFan(true, "temperature");
}
} else if (!txCooldownActive && currentTemperature < FAN_OFF_TEMP) {
setFan(false, "cooled down");
}
}
void setup() {
pinMode(TEMP_PIN, INPUT);
analogSetPinAttenuation(TEMP_PIN, ADC_11db);
pinMode(FAN_CTRL_PIN, OUTPUT);
digitalWrite(FAN_CTRL_PIN, LOW);
fanOn = false;
firstTempSample = true; // first monitor() call samples immediately
}
void monitor() {
uint32_t now = millis();
if (txCooldownActive && (now - txEndTime >= TX_COOLDOWN_MS)) {
txCooldownActive = false;
updateFan();
}
if (!firstTempSample && (now - lastTempSample < TEMP_SAMPLE_MS)) return;
firstTempSample = false;
lastTempSample = now;
float temperature = readTemperature();
if (!isnan(temperature)) currentTemperature = temperature;
if (!txBlocked && currentTemperature >= TEMP_TX_BLOCK) {
txBlocked = true;
Utils::println("Thermal: Over-temperature at " + String(currentTemperature, 1) + " C, Tx blocked");
} else if (txBlocked && currentTemperature < TEMP_TX_UNBLOCK) {
txBlocked = false;
Utils::println("Thermal: Temperature back to " + String(currentTemperature, 1) + " C, Tx allowed");
} else if (!txBlocked && currentTemperature >= TEMP_WARNING) {
Utils::println("Thermal: High temperature: " + String(currentTemperature, 1) + " C");
}
updateFan();
}
void onTxStart() {
txCooldownActive = false;
setFan(true, "TX");
}
void onTxEnd() {
txCooldownActive = true;
txEndTime = millis();
}
void turnOffFan() { // before sleeping: no Tx while asleep, so the PA doesn't heat up
txCooldownActive = false;
setFan(false, "sleep");
}
bool isTxBlocked() {
return txBlocked;
}
}
#endif
Loaded 100 of 187 files, more files were not shown because too many files have changed in this diff. Show more