Compare commits

..

35 Commits

Author SHA1 Message Date
Matthias Wientapper
ce4c1e0be1 platformio.ini: Adjust defaults for LoRa frequncies 2026-01-29 11:39:15 +01:00
Matthias Wientapper
deba7253e7 Integration of upstrem PR #1297 2026-01-29 11:38:51 +01:00
Matthias Wientapper
f836872102 Integration of upstrem PR #1338 2026-01-29 11:38:11 +01:00
Matthias Wientapper
4ae707c778 Merge branch 'evo-build-scripts' into main-evo 2026-01-29 11:36:59 +01:00
Scott Powell
e738a74777 Merge branch 'dev' 2026-01-29 21:16:53 +11:00
Matthias Wientapper
4a9137bf00 Remove PR-1199 as its functionality is part of dev now 2026-01-26 12:47:35 +01:00
Matthias Wientapper
864a0c0421 Set default to EU/UK narrow 2026-01-26 12:45:45 +01:00
Matthias Wientapper
58decb74b8 Fix fetching same PR twice under wrong name 2026-01-26 11:09:03 +01:00
Matthias Wientapper
f9f177522b Add cli config flood.advert.base
0 = forwarding flood adverts off
1 = forwarding flood adverts on (unrestricted)
0.308 (default) = prob. forwarding according to #1338
2026-01-26 10:35:31 +01:00
Matthias Wientapper
6d3345c50f Limit flood advert packet forwarding for roomservers as well 2026-01-26 10:35:31 +01:00
Matthias Wientapper
bd4c4cf69d Limit flood advert packet forwarding, implements #1223 2026-01-26 10:35:31 +01:00
Matthias Wientapper
e6cab77670 Add scripts to help automate the fw build process 2026-01-21 09:54:56 +01:00
Liam Cottle
6b52fb3230 Merge pull request #1310 from LitBomb/patch-22
fix Station G2 output dBm typo
2026-01-03 19:39:31 +13:00
uncle lit
a93527a474 fix Station G2 output dBm typo
fix Station G2 output dBm typo reported on https://github.com/meshcore-dev/MeshCore/issues/1304

changed 26.5 dBm to 36.5 dBm
2026-01-02 22:34:10 -08:00
ripplebiz
9b08a9bd93 Merge pull request #1260 from LitBomb/patch-21
Update FAQ with new community projects and tx power settings for amped radios
2025-12-29 13:44:38 +11:00
uncle lit
27c92d2fe9 Update FAQ with new MeshCore applications and tx power settings for amped radios
Added entries for meshcore-pi and pyMC_Repeater to the FAQ
Added tx power settings for amped radios
2025-12-21 21:48:56 -08:00
Liam Cottle
2228214ded Merge pull request #1216 from mattzzw/main
Update faq.md
2025-12-15 18:18:00 +13:00
mattzzw
2bcc9c10d2 Update faq.md
Fix typo
2025-12-14 18:29:49 +01:00
ripplebiz
922e378be5 Merge pull request #1192 from LitBomb/patch-20
Update faq.md
2025-12-11 10:21:19 +11:00
uncle lit
1f5659dd26 Update faq.md
fix typo bugs found by @4np
2025-12-08 09:33:10 -08:00
uncle lit
cae37d8892 Update faq.md
add get and set prv.key
add web site to generate new private key and specific its public key's first byte value
add link to repeater observer instruction
add links to The Comms Channel's meshcore video, MCarper's Meshcore Advantages, and Austin Mesh's MeshCore vs Meshtastic comparison
add deafness instruction for agc reset interval
add reference to Liam's Windows and Intel Mac client apps
add reference to Tree's Meshcore packet decoder
add OTA BLE update addendum for Seeed Wio Tracker L1 Pro
add instruction to use T-deck's software keyboard to enter `=` at the end of the base64 public key
2025-12-07 22:31:54 -08:00
Scott Powell
6d3219329f Merge branch 'dev' 2025-11-30 18:32:49 +11:00
Scott Powell
9405e8bee3 Merge branch 'dev'
# Conflicts:
#	docs/payloads.md
2025-11-13 20:47:52 +11:00
fdlamotte
8b68b5a689 Update README.md (RAK boards don't need pio patch) 2025-11-12 16:14:57 +01:00
Liam Cottle
b2dcb06197 Merge pull request #809 from tekstrand/fixup
Change source of truth to this repo, remove whitespace
2025-10-19 12:07:53 +13:00
ripplebiz
da5dbcd274 Merge pull request #871 from spacepc-de/fix-debug-log-field
Fix debug log: use c->extra.room.push_failures instead of c->push_failures
2025-10-07 09:45:11 +11:00
tekstrand
3e3fa5b443 trim trailing whitespace, clarify repeater gps, remove outdated instructions 2025-10-04 10:54:24 -05:00
Scott Powell
f5f5886327 Merge branch 'dev' 2025-10-02 12:52:48 +10:00
Jonathan Stöcklmayer
6ee0b85195 Fix debug log: use c->extra.room.push_failures instead of non-existent c->push_failures 2025-10-01 09:50:41 +02:00
ripplebiz
86225cd24a Merge pull request #869 from LitBomb/patch-19
Update faq.md
2025-10-01 13:46:44 +10:00
uncle lit
f594f2c7e6 Update faq.md
added pyMC_core to meshcore projects
mentioned Cisien's meshcoretomqtt fork from Andrew-a-g
updated Coding Rate explanation and recommendation
updated radio presets and added how to update presets listed in the app
2025-09-30 16:01:11 -07:00
Liam Cottle
3dc04deabf Merge pull request #837 from silverphish-io/typo-fix
Typo fix
2025-09-29 10:42:23 +13:00
ripplebiz
c8a6bcf57f Update README.md 2025-09-28 21:43:30 +10:00
silverphish-io
4e886bfa90 Typo fix in faq and payloads 2025-09-25 15:01:39 +01:00
silverphish-io
816d4e2fa3 Update faq.md 2025-09-25 14:59:25 +01:00
240 changed files with 1080 additions and 3469 deletions

View File

@@ -1,43 +0,0 @@
name: PR Build Check
on:
pull_request:
branches: [main, dev]
paths:
- 'src/**'
- 'examples/**'
- 'variants/**'
- 'platformio.ini'
- '.github/workflows/pr-build-check.yml'
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
environment:
# ESP32-S3 (most common platform)
- Heltec_v3_companion_radio_ble
- Heltec_v3_repeater
- Heltec_v3_room_server
# nRF52
- RAK_4631_companion_radio_ble
- RAK_4631_repeater
- RAK_4631_room_server
# RP2040
- PicoW_repeater
# STM32
- wio-e5-mini_repeater
# ESP32-C6
- LilyGo_Tlora_C6_repeater_
steps:
- name: Clone Repo
uses: actions/checkout@v4
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build ${{ matrix.environment }}
run: pio run -e ${{ matrix.environment }}

View File

@@ -39,11 +39,9 @@ For developers;
- Clone and open the MeshCore repository in Visual Studio Code. - Clone and open the MeshCore repository in Visual Studio Code.
- See the example applications you can modify and run: - See the example applications you can modify and run:
- [Companion Radio](./examples/companion_radio) - For use with an external chat app, over BLE, USB or WiFi. - [Companion Radio](./examples/companion_radio) - For use with an external chat app, over BLE, USB or WiFi.
- [KISS Modem](./examples/kiss_modem) - Serial KISS protocol bridge for host applications. ([protocol docs](./docs/kiss_modem_protocol.md))
- [Simple Repeater](./examples/simple_repeater) - Extends network coverage by relaying messages. - [Simple Repeater](./examples/simple_repeater) - Extends network coverage by relaying messages.
- [Simple Room Server](./examples/simple_room_server) - A simple BBS server for shared Posts. - [Simple Room Server](./examples/simple_room_server) - A simple BBS server for shared Posts.
- [Simple Secure Chat](./examples/simple_secure_chat) - Secure terminal based text communication between devices. - [Simple Secure Chat](./examples/simple_secure_chat) - Secure terminal based text communication between devices.
- [Simple Sensor](./examples/simple_sensor) - Remote sensor node with telemetry and alerting.
The Simple Secure Chat example can be interacted with through the Serial Monitor in Visual Studio Code, or with a Serial USB Terminal on Android. The Simple Secure Chat example can be interacted with through the Serial Monitor in Visual Studio Code, or with a Serial USB Terminal on Android.
@@ -99,10 +97,10 @@ Here are some general principals you should try to adhere to:
There are a number of fairly major features in the pipeline, with no particular time-frames attached yet. In very rough chronological order: There are a number of fairly major features in the pipeline, with no particular time-frames attached yet. In very rough chronological order:
- [X] Companion radio: UI redesign - [X] Companion radio: UI redesign
- [ ] Repeater + Room Server: add ACL's (like Sensor Node has) - [X] Repeater + Room Server: add ACL's (like Sensor Node has)
- [ ] Standardise Bridge mode for repeaters - [X] Standardise Bridge mode for repeaters
- [ ] Repeater/Bridge: Standardise the Transport Codes for zoning/filtering - [ ] Repeater/Bridge: Standardise the Transport Codes for zoning/filtering
- [ ] Core + Repeater: enhanced zero-hop neighbour discovery - [X] Core + Repeater: enhanced zero-hop neighbour discovery
- [ ] Core: round-trip manual path support - [ ] Core: round-trip manual path support
- [ ] Companion + Apps: support for multiple sub-meshes (and 'off-grid' client repeat mode) - [ ] Companion + Apps: support for multiple sub-meshes (and 'off-grid' client repeat mode)
- [ ] Core + Apps: support for LZW message compression - [ ] Core + Apps: support for LZW message compression
@@ -115,12 +113,3 @@ There are a number of fairly major features in the pipeline, with no particular
- Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page. - Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page.
- Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz). - Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz).
- Join [MeshCore Discord](https://discord.gg/BMwCtwHj5V) to chat with the developers and get help from the community. - Join [MeshCore Discord](https://discord.gg/BMwCtwHj5V) to chat with the developers and get help from the community.
## RAK Wireless Board Support in PlatformIO
Before building/flashing the RAK4631 targets in this project, there is, unfortunately, some patching you have to do to your platformIO packages to make it work. There is a guide here on the process:
[RAK Wireless: How to Perform Installation of Board Support Package in PlatformIO](https://learn.rakwireless.com/hc/en-us/articles/26687276346775-How-To-Perform-Installation-of-Board-Support-Package-in-PlatformIO)
After building, you will need to convert the output firmware.hex file into a .uf2 file you can copy over to your RAK4631 device (after doing a full erase) by using the command `uf2conv.py -f 0xADA52840 -c firmware.hex` with the python script available from:
[GitHub: Microsoft - uf2](https://github.com/Microsoft/uf2/blob/master/utils/uf2conv.py)

View File

@@ -1,50 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DLILYGO_TBEAM_1W",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [
[
"0x303A",
"0x1001"
]
],
"mcu": "esp32s3",
"variant": "lilygo_tbeam_1w"
},
"connectivity": [
"wifi",
"bluetooth",
"lora"
],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino"
],
"name": "LilyGo TBeam-1W",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"url": "http://www.lilygo.cn/",
"vendor": "LilyGo"
}

View File

@@ -41,7 +41,7 @@
"name": "LilyGo T-Beam supreme (8MB Flash 8MB PSRAM)", "name": "LilyGo T-Beam supreme (8MB Flash 8MB PSRAM)",
"upload": { "upload": {
"flash_size": "8MB", "flash_size": "8MB",
"maximum_ram_size": 8388608, "maximum_ram_size": 327680,
"maximum_size": 8388608, "maximum_size": 8388608,
"require_upload_port": true, "require_upload_port": true,
"speed": 460800 "speed": 460800

View File

@@ -7,7 +7,6 @@ sh build.sh <command> [target]
Commands: Commands:
help|usage|-h|--help: Shows this message. help|usage|-h|--help: Shows this message.
list|-l: List firmwares available to build.
build-firmware <target>: Build the firmware for the given build target. build-firmware <target>: Build the firmware for the given build target.
build-firmwares: Build all firmwares for all targets. build-firmwares: Build all firmwares for all targets.
build-matching-firmwares <build-match-spec>: Build all firmwares for build targets containing the string given for <build-match-spec>. build-matching-firmwares <build-match-spec>: Build all firmwares for build targets containing the string given for <build-match-spec>.
@@ -47,25 +46,19 @@ $ sh build.sh build-firmware RAK_4631_repeater
EOF EOF
} }
# get a list of pio env names that start with "env:"
get_pio_envs() {
pio project config | grep 'env:' | sed 's/env://'
}
# Catch cries for help before doing anything else. # Catch cries for help before doing anything else.
case $1 in case $1 in
help|usage|-h|--help) help|usage|-h|--help)
global_usage global_usage
exit 1 exit 1
;; ;;
list|-l)
get_pio_envs
exit 0
;;
esac esac
# cache project config json for use in get_platform_for_env()
PIO_CONFIG_JSON=$(pio project config --json-output) # get a list of pio env names that start with "env:"
get_pio_envs() {
echo $(pio project config | grep 'env:' | sed 's/env://')
}
# $1 should be the string to find (case insensitive) # $1 should be the string to find (case insensitive)
get_pio_envs_containing_string() { get_pio_envs_containing_string() {
@@ -89,25 +82,6 @@ get_pio_envs_ending_with_string() {
done done
} }
# get platform flag for a given environment
# $1 should be the environment name
get_platform_for_env() {
local env_name=$1
echo "$PIO_CONFIG_JSON" | python3 -c "
import sys, json, re
data = json.load(sys.stdin)
for section, options in data:
if section == 'env:$env_name':
for key, value in options:
if key == 'build_flags':
for flag in value:
match = re.search(r'(ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM)', flag)
if match:
print(match.group(1))
sys.exit(0)
"
}
# disable all debug logging flags if DISABLE_DEBUG=1 is set # disable all debug logging flags if DISABLE_DEBUG=1 is set
disable_debug_flags() { disable_debug_flags() {
if [ "$DISABLE_DEBUG" == "1" ]; then if [ "$DISABLE_DEBUG" == "1" ]; then
@@ -117,8 +91,6 @@ disable_debug_flags() {
# build firmware for the provided pio env in $1 # build firmware for the provided pio env in $1
build_firmware() { build_firmware() {
# get env platform for post build actions
ENV_PLATFORM=($(get_platform_for_env $1))
# get git commit sha # get git commit sha
COMMIT_HASH=$(git rev-parse --short HEAD) COMMIT_HASH=$(git rev-parse --short HEAD)
@@ -149,31 +121,27 @@ build_firmware() {
# build firmware target # build firmware target
pio run -e $1 pio run -e $1
# build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin) # build merge-bin for esp32 fresh install
if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then if [ -f .pio/build/$1/firmware.bin ]; then
pio run -t mergebin -e $1 pio run -t mergebin -e $1
cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true
cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true
fi fi
# build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2) # build .uf2 for nrf52 boards
if [ "$ENV_PLATFORM" == "NRF52_PLATFORM" ]; then if [[ -f .pio/build/$1/firmware.zip && -f .pio/build/$1/firmware.hex ]]; then
python3 bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840 python3 bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840
cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true
fi fi
# for stm32, copy .bin and .hex to out folder # copy .bin, .uf2, and .zip to out folder
if [ "$ENV_PLATFORM" == "STM32_PLATFORM" ]; then # e.g: Heltec_v3_room_server-v1.0.0-SHA.bin
cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true # e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2
cp .pio/build/$1/firmware.hex out/${FIRMWARE_FILENAME}.hex 2>/dev/null || true
fi
# for rp2040, copy .bin and .uf2 to out folder # copy .bin for esp32 boards
if [ "$ENV_PLATFORM" == "RP2040_PLATFORM" ]; then cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true
cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true
cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
fi # copy .zip and .uf2 of nrf52 boards
cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true
} }

View File

@@ -642,7 +642,7 @@
**Usage:** **Usage:**
- `region` - `region`
**Serial Only:** For firmware older than 1.12.0 **Serial Only:** Yes
--- ---

View File

@@ -26,6 +26,10 @@ author: https://github.com/LitBomb<!-- omit from toc -->
- [3.2. Q: Do I need to set the location for a repeater?](#32-q-do-i-need-to-set-the-location-for-a-repeater) - [3.2. Q: Do I need to set the location for a repeater?](#32-q-do-i-need-to-set-the-location-for-a-repeater)
- [3.3. Q: What is the password to administer a repeater or a room server?](#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server) - [3.3. Q: What is the password to administer a repeater or a room server?](#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server)
- [3.4. Q: What is the password to join a room server?](#34-q-what-is-the-password-to-join-a-room-server) - [3.4. Q: What is the password to join a room server?](#34-q-what-is-the-password-to-join-a-room-server)
- [3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?](#35-q-can-i-retrieve-a-repeaters-private-key-or-set-a-repeaters-private-key)
- [3.6. Q: The first byte of my repeater's public key collides with an exisitng repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?](#36-q-the-first-byte-of-my-repeaters-public-key-collides-with-an-exisitng-repeater-on-the-mesh--how-do-i-get-a-new-private-key-with-a-matching-public-key-that-has-its-first-byte-of-my-choosing)
- [3.7. Q: My repeater maybe suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. what can I do?](#37-q-my-repeater-maybe-suffering-from-deafness-due-to-high-power-interference-near-my-meshs-frequency-it-is-not-hearing-other-in-range-meshcore-radios--what-can-i-do)
- [3.8 Q: How do I make my repeater an observer on the mesh](#38-q-how-do-i-make-my-repeater-an-observer-on-the-mesh)
- [4. T-Deck Related](#4-t-deck-related) - [4. T-Deck Related](#4-t-deck-related)
- [4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?](#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro) - [4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?](#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro)
- [4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?](#42-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode) - [4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?](#42-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode)
@@ -61,22 +65,31 @@ author: https://github.com/LitBomb<!-- omit from toc -->
- [5.14.3. Python MeshCore](#5143-python-meshcore) - [5.14.3. Python MeshCore](#5143-python-meshcore)
- [5.14.4. meshcore-cli](#5144-meshcore-cli) - [5.14.4. meshcore-cli](#5144-meshcore-cli)
- [5.14.5. meshcore.js](#5145-meshcorejs) - [5.14.5. meshcore.js](#5145-meshcorejs)
- [5.14.6. pyMC\_core](#5146-pymc_core)
- [5.14.7. MeshCore Packet Decoder](#5147-meshcore-packet-decoder)
- [5.14.8. meshcore-pi](#5148-meshcore-pi)
- [5.14.9. pyMC\_Repeater](#5149-pymc_repeater)
- [5.15. Q: Are there client applications for Windows or Mac?](#515-q-are-there-client-applications-for-windows-or-mac)
- [5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?](#516-q-are-there-any-resources-that-compare-meshcore-to-other-lora-systems)
- [6. Troubleshooting](#6-troubleshooting) - [6. Troubleshooting](#6-troubleshooting)
- [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago) - [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago)
- [6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.](#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed) - [6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.](#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed)
- [6.3. Q: How to connect to a repeater via BLE (Bluetooth)?](#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth) - [6.3. Q: How to connect to a repeater via BLE (Bluetooth)?](#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth)
- [6.4. Q: My companion isn't showing up over Bluetooth?](#64-q-my-companion-isnt-showing-up-over-bluetooth) - [6.4. Q: My companion isn't showing up over Bluetooth?](#64-q-my-companion-isnt-showing-up-over-bluetooth)
- [6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#64-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code) - [6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#65-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code)
- [6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.](#65-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection) - [6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.](#66-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection)
- [6.7. Q: My RAK/T1000-E/xiao\_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?](#66-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh) - [6.7. Q: My RAK/T1000-E/xiao\_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?](#67-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh)
- [6.8. Q: WebFlasher fails on Linux with failed to open](#67-q-webflasher-fails-on-linux-with-failed-to-open) - [6.8. Q: WebFlasher fails on Linux with failed to open](#68-q-webflasher-fails-on-linux-with-failed-to-open)
- [7. Other Questions:](#7-other-questions) - [7. Other Questions:](#7-other-questions)
- [7.1. Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?](#71-q-how-to-update-nrf-rak-t114-seed-xiao-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app) - [7.1. Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?](#71-q-how-to-update-nrf-rak-t114-seed-xiao-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app)
- [7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?](#711-q-can-i-update-seeed-studio-wio-tracker-l1-pro-using-ota)
- [7.2. Q: How to update ESP32-based devices over the air?](#72-q-how-to-update-esp32-based-devices-over-the-air) - [7.2. Q: How to update ESP32-based devices over the air?](#72-q-how-to-update-esp32-based-devices-over-the-air)
- [7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?](#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu) - [7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?](#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu)
- [7.4. Q: are the MeshCore logo and font available?](#74-q-are-the-meshcore-logo-and-font-available) - [7.4. Q: are the MeshCore logo and font available?](#74-q-are-the-meshcore-logo-and-font-available)
- [7.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code) - [7.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code)
- [7.6. Q: How do I connect to the companion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-comnpanion-via-wifi-eg-using-a-heltec-v3) - [7.6. Q: How do I connect to the companion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-companion-via-wifi-eg-using-a-heltec-v3)
- [7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with a EByte E22-900M30S or a E22-900M33S module, what should their transmit power be set to?](#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-a-ebyte-e22-900m30s-or-a-e22-900m33s-module-what-should-their-transmit-power-be-set-to)
- [| | High Output | 22 dBm | 28 dBm | |](#--high-output--22-dbm--28-dbm--)
## 1. Introduction ## 1. Introduction
@@ -180,22 +193,17 @@ The T-Deck firmware is free to download and most features are available without
### 2.3. Q: What frequencies are supported by MeshCore? ### 2.3. Q: What frequencies are supported by MeshCore?
**A:** It supports the 868MHz range in the UK/EU and the 915MHz range in New Zealand, Australia, and the USA. Countries and regions in these two frequency ranges are also supported. The firmware and client allow users to set their preferred frequency. **A:** It supports the 868MHz range in the UK/EU and the 915MHz range in New Zealand, Australia, and the USA. Countries and regions in these two frequency ranges are also supported.
- Australia and New Zealand are on **915.8MHz**
- UK and EU are on **869.525MHz**
- Canada and USA are on **910.525MHz**
- For other regions and countries, please check your local LoRa frequency
In UK and EU, 867.5MHz is not allowed to use 250kHz bandwidth and it only allows 2.5% duty cycle for clients. 869.525Mhz allows an airtime of 10%, 250KHz bandwidth, and a higher EIRP, therefore MeshCore nodes can send more often and with more power. That is why this frequency is chosen for UK and EU. This is also why Meshtastic also uses this frequency. Use the smartphone client or the repeater setup feature on there web flasher to set your radios' RF settings by choosing the preset for your regions.
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1356540643853209641) Recently, as of October 2025, many regions have moved to the "narrow" setting, aka using BW62.5 and a lower SF number (instead of the original SF11). For example, USA/Canada (Recommended) preset is 910.525MHz, SF7, BW62.5, CR5.
After extensive testing, many regions have switched or about to switch over to BW62.5 and SF7, 8, or 9. Narrower bandwidth setting and lower SF setting allow MeshCore's radio signals to fit between interference in the ISM band, provide for a lower noise floor, better SNR, and faster transmissions.
If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the [#meshcore-app](https://discord.com/channels/1343693475589263471/1391681655911088241) channel on the [MeshCore Discord server ](https://discord.gg/cYtQNYCCRK) to let Liam Cottle know.
the rest of the radio settings are the same for all frequencies:
- Spread Factor (SF): 11
- Coding Rate (CR): 5
- Bandwidth (BW): 250.00
(Originally MeshCore started with SF 10. recently (as of late April 2025) the community has advocated SF 11 also a viable option for longer range but a little slower transmission. Currently there are MeshCore meshes with SF 10 and SF 11. Liam Cottle's smartphone app's presets now recommend SF 10 for Australia and SF 11 for all other regions and countries. EU and UK has SF 10 and SF 11 presets. Work with your local meshers on deciding with SF number is best for your use cases. In the future, there may be bridge nodes that can bridge SF 10 and SF 11 (or even different frequencies) traffic.)
### 2.4. Q: What is an "advert" in MeshCore? ### 2.4. Q: What is an "advert" in MeshCore?
**A:** **A:**
@@ -243,7 +251,7 @@ Repeater or room server can be administered with one of the options below:
### 3.2. Q: Do I need to set the location for a repeater? ### 3.2. Q: Do I need to set the location for a repeater?
**A:** With location set for a repeater, it can show up on a MeshCore map in the future. Set location with the following commands: **A:** While not required, with location set for a repeater it will show up on the MeshCore map in the future. Set location with the following command:
`set lat <GPS Lat> set long <GPS Lon>` `set lat <GPS Lat> set long <GPS Lon>`
@@ -260,6 +268,34 @@ You can get the latitude and longitude from Google Maps by right-clicking the lo
`set guest.password {guest-password}` `set guest.password {guest-password}`
### 3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?
**A:** You can issue these commands to get or set a repeater's private key using a USB serial connection.
`get prv.key` to print a repeater's private key on the serial console
`set prv.key <hex>` to set a repeater's private key on the serial console
Reboot the repeater after `set prv.key <hex>` command for the new private key to take effect.
### 3.6. Q: The first byte of my repeater's public key collides with an exisitng repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?
**A:** You can generate a new private key and specific the first byte of its public key here: https://gessaman.com/mc-keygen/
### 3.7. Q: My repeater maybe suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. what can I do?
**A:** This may be due to the SX1262 radio's auto gain control feature. You can use this command to preiodically reset its AGC.
`set agc.reset.interval <number>`
The `<number>` unit is in seconds and is incremented by 4. `set agc.reset.interval 4` works well to cure deafness.
This is a very low cost operation. AGC reset is done by simply setting `state = STATE_IDLE;` in function `RadioLibWrapper::resetAGC()` in `RadioLibWrappers.cpp`
### 3.8 Q: How do I make my repeater an observer on the mesh
**A:** The observer instruction is available here: https://analyzer.letsme.sh/observer/onboard
--- ---
@@ -297,7 +333,9 @@ GPS on T-Deck is always enabled. You can skip the "GPS clock sync" and the T-De
**A:** **A:**
T-Deck uses the same key the smartphone apps use but in base64 T-Deck uses the same key the smartphone apps use but in base64
`izOH6cXN6mrJ5e26oRXNcg==` `izOH6cXN6mrJ5e26oRXNcg==`
The third character is the capital letter 'O', not zero `0`
There is no `=` key on the T-Deck's hardware keyboard. You can use the on-screen software keyboard to enter `=`. Tap the text box to enable the on-screen software keyboard.
The third character is the capital letter `O` (Oh), not zero `0`
The smartphone app key is in hex: The smartphone app key is in hex:
` 8b3387e9c5cdea6ac9e5edbaa115cd72` ` 8b3387e9c5cdea6ac9e5edbaa115cd72`
@@ -376,7 +414,23 @@ https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19
**SF is spreading factor** - how much should the communication spread in time **SF is spreading factor** - how much should the communication spread in time
**CR is coding rate** - https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate/ **CR is coding rate** - from: https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate/
TL;DR: default CR to 5 for good stable links. If it is not a solid link and is intermittent, change to CR to 7 or 8.
Forward Error Correction is a process of adding redundant bits to the data to be transmitted. During the transmission, data may get corrupted by interference (changes from 0 to 1 / 1 to 0). These error correction bits are used at the receivers for restoring corrupted bits.
The Code Rate of a forward error correction expresses the proportion of bits in a data stream that actually carry useful information.
There are 4 code rates used in LoRaWAN:
4/5
4/6
5/7
4/8
For example, if the code rate is 5/7, for every 5 bits of useful information, the coder generates a total of 7 bits of data, of which 2 bits are redundant.
Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more bytes in the same time. Making the spreading factor 1 step lower (from SF10 to SF9) allows you to send 2x more bytes in the same time. Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more bytes in the same time. Making the spreading factor 1 step lower (from SF10 to SF9) allows you to send 2x more bytes in the same time.
Lowering the spreading factor makes it more difficult for the gateway to receive a transmission, as it will be more sensitive to noise. You could compare this to two people taking in a noisy place (a bar for example). If youre far from each other, you have to talk slow (SF10), but if youre close, you can talk faster (SF7) Lowering the spreading factor makes it more difficult for the gateway to receive a transmission, as it will be more sensitive to noise. You could compare this to two people taking in a noisy place (a bar for example). If youre far from each other, you have to talk slow (SF10), but if youre close, you can talk faster (SF7)
@@ -558,7 +612,8 @@ From here, reference repeater and room server command line commands on MeshCore
**A:** Yes. See the following: **A:** Yes. See the following:
#### 5.14.1. meshcoremqtt #### 5.14.1. meshcoremqtt
A Python script to send meshore debug and packet capture data to MQTT for analysis A Python script to send meshcore debug and packet capture data to MQTT for analysis. Cisien's version is a fork of Andrew-a-g's and is being used to to collect data for https://map.w0z.is/messages and https://analyzer.letsme.sh/
https://github.com/Cisien/meshcoretomqtt
https://github.com/Andrew-a-g/meshcoretomqtt https://github.com/Andrew-a-g/meshcoretomqtt
#### 5.14.2. MeshCore for Home Assistant #### 5.14.2. MeshCore for Home Assistant
@@ -577,6 +632,40 @@ CLI interface to MeshCore companion radio over BLE, TCP, or serial. Uses Python
A JavaScript library for interacting with a MeshCore device running the companion radio firmware A JavaScript library for interacting with a MeshCore device running the companion radio firmware
https://github.com/liamcottle/meshcore.js https://github.com/liamcottle/meshcore.js
#### 5.14.6. pyMC_core
pyMC_Core is a Python port of MeshCore, designed for Raspberry Pi and similar hardware, it talks to LoRa modules over SPI.
https://github.com/rightup/pyMC_core
#### 5.14.7. MeshCore Packet Decoder
A TypeScript library for decoding MeshCore mesh networking packets with full cryptographic support. Uses WebAssembly (WASM) for Ed25519 key derivation through the orlp/ed25519 library. It powers the [MeshCore Packet Analyzer](https://analyzer.letsme.sh/packets).
https://github.com/michaelhart/meshcore-decoder
#### 5.14.8. meshcore-pi
meshcore-pi is another Python port of MeshCore, designed for Raspberry Pi and similar hardware, it talks to LoRa modules over SPI or GPIO.
https://github.com/brianwiddas/meshcore-pi
#### 5.14.9. pyMC_Repeater
pyMC_Repeater is a repeater daemon in Python built on top of the [`pymc_core`](#5146-pymc_core) library.
https://github.com/rightup/pyMC_Repeater
### 5.15. Q: Are there client applications for Windows or Mac?
**A:** Yes, the same iOS and Android client is also available for Windows and Intel Mac (sorry, not available for ARM-based Mac yet). You can find them together with the Android APK here:
https://files.liamcottle.net/MeshCore
Both the Windows and Intel Mac versions of the client app are fully unlocked and are free to use.
### 5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?
**A:** Here is a list of MeshCore comparison resources:
The Comms Channel on YouTube:
https://www.youtube.com/watch?v=guDoKGs02Us
MeshCore Advantages by MCarper:
https://github.com/mikecarper/meshfirmware/blob/main/MeshCoreAdvantages.md
Meshcore vs Meshtastic by austinmesh.org
https://www.austinmesh.org/learn/meshcore-vs-meshtastic/
--- ---
## 6. Troubleshooting ## 6. Troubleshooting
@@ -653,6 +742,12 @@ Allow the browser user on it:
13. If it fails, try turning off and on Bluetooth on your phone. If that doesn't work, try rebooting your phone. 13. If it fails, try turning off and on Bluetooth on your phone. If that doesn't work, try rebooting your phone.
14. Wait for the update to complete. It can take a few minutes. 14. Wait for the update to complete. It can take a few minutes.
#### 7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?
**A:** You can flash this safer bootloader to the Wio Tracker L1 Pro
https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX
After this bootloader is flashed onto the device, you can trigger over the air update using bluetooth by holding the button next to the D-Pad and then click the reset button. The follow the same OTA update instructions above. You can skip pass the `start ota` instruction and start the update using the DFU app.
### 7.2. Q: How to update ESP32-based devices over the air? ### 7.2. Q: How to update ESP32-based devices over the air?
@@ -673,10 +768,14 @@ Allow the browser user on it:
Refer to https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX for the latest information. Refer to https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX for the latest information.
Currently, the following boards are supported: Currently, the following boards are supported:
- Nologo ProMicro - Heltec Automation Mesh Node T114 / HT-nRF5262
- Nologo ProMicro NRF52840 (aka SuperMini NRF52840)
- Seeed Studio SenseCAP Card Tracker T1000-E
- Seeed Studio Wio Tracker L1
- Seeed Studio XIAO nRF52840 BLE - Seeed Studio XIAO nRF52840 BLE
- Seeed Studio XIAO nRF52840 BLE SENSE - Seeed Studio XIAO nRF52840 BLE SENSE
- RAK 4631 - RAK 4631 (See note)
- RAK WisMesh Tag (new 28/11/2025)
### 7.4. Q: are the MeshCore logo and font available? ### 7.4. Q: are the MeshCore logo and font available?
@@ -703,4 +802,22 @@ where `&type` is:
WiFi firmware requires you to compile it yourself, as you need to set the wifi ssid and password. WiFi firmware requires you to compile it yourself, as you need to set the wifi ssid and password.
Edit WIFI_SSID and WIFI_PWD in `./variants/heltec_v3/platformio.ini` and then flash it to your device. Edit WIFI_SSID and WIFI_PWD in `./variants/heltec_v3/platformio.ini` and then flash it to your device.
### 7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with a EByte E22-900M30S or a E22-900M33S module, what should their transmit power be set to?
**A:**
For companion radios, you can set these radios' transmit power in the smartphone app. For repeater and room server radios, you can set their transmit power using the command line command `set tx`. You can get their current value using command line comand `get tx`
> ### ⚠️ **WARNING: Set these values at your own risk. Incorrect power settings can permanently damage your radio hardware.**
| Device / Model | Region / Description | In-App Setting (dBm) | Target Radio Output | Notes |
| :--- | :--- | :--- | :--- | :--- |
| **Station G2** <br> [Reference](https://wiki.uniteng.com/en/meshtastic/station-g2) | US915 Max Output | 19 dBm | 36.5 dBm (4.46W) | |
| | US915 Recommended Max | 16 dBm | 35 dBm (3.16W) | 1dB compression point |
| | EU868 Recommended Max | 15 dBm | 34.5 dBm (2.82W) | 1dB compression point |
| | US915 1W Output | 10 dBm | 1W | |
| | EU868 1W Output | 9 dBm | 1W | |
| **Ikoka Stick E22-900M30S** | 1W Model | 19 dBm | 1W | **DO NOT EXCEED** (Risk of burn out) |
| **Ikoka Stick E22-900M33S** | 2W Model | 9 dBm | 2W | **DO NOT EXCEED** (Risk of burn out) |
| **Heltec V4** | Standard Output | 10 dBm | 22 dBm | |
| | High Output | 22 dBm | 28 dBm | |
--- ---

View File

@@ -1,282 +0,0 @@
# MeshCore KISS Modem Protocol
Standard KISS TNC firmware for MeshCore LoRa radios. Compatible with any KISS client (Direwolf, APRSdroid, YAAC, etc.) for sending and receiving raw packets. MeshCore-specific extensions (cryptography, radio configuration, telemetry) are available through the standard SetHardware (0x06) command.
## Serial Configuration
115200 baud, 8N1, no flow control.
## Frame Format
Standard KISS framing per the KA9Q/K3MC specification.
| Byte | Name | Description |
|------|------|-------------|
| `0xC0` | FEND | Frame delimiter |
| `0xDB` | FESC | Escape character |
| `0xDC` | TFEND | Escaped FEND (FESC + TFEND = 0xC0) |
| `0xDD` | TFESC | Escaped FESC (FESC + TFESC = 0xDB) |
```
┌──────┬───────────┬──────────────┬──────┐
│ FEND │ Type Byte │ Data (escaped)│ FEND │
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │
└──────┴───────────┴──────────────┴──────┘
```
### Type Byte
The type byte is split into two nibbles:
| Bits | Field | Description |
|------|-------|-------------|
| 7-4 | Port | Port number (0 for single-port TNC) |
| 3-0 | Command | Command number |
Maximum unescaped frame size: 512 bytes.
## Standard KISS Commands
### Host to TNC
| Command | Value | Data | Description |
|---------|-------|------|-------------|
| Data | `0x00` | Raw packet | Queue packet for transmission |
| TXDELAY | `0x01` | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) |
| Persistence | `0x02` | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) |
| SlotTime | `0x03` | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) |
| TXtail | `0x04` | Delay (1 byte) | Post-TX hold time in 10ms units (default: 0) |
| FullDuplex | `0x05` | Mode (1 byte) | 0 = half duplex, nonzero = full duplex (default: 0) |
| SetHardware | `0x06` | Sub-command + data | MeshCore extensions (see below) |
| Return | `0xFF` | - | Exit KISS mode (no-op) |
### TNC to Host
| Type | Value | Data | Description |
|------|-------|------|-------------|
| Data | `0x00` | Raw packet | Received packet from radio |
Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes.
### CSMA Behavior
The TNC implements p-persistent CSMA for half-duplex operation:
1. When a packet is queued, monitor carrier detect
2. When the channel clears, generate a random value 0-255
3. If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
4. Otherwise, wait SlotTime and repeat from step 1
In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.
## SetHardware Extensions (0x06)
MeshCore-specific functionality uses the standard KISS SetHardware command. The first byte of SetHardware data is a sub-command. Standard KISS clients ignore these frames.
### Frame Format
```
┌──────┬──────┬─────────────┬──────────────┬──────┐
│ FEND │ 0x06 │ Sub-command │ Data (escaped)│ FEND │
│ 0xC0 │ │ 1 byte │ variable │ 0xC0 │
└──────┴──────┴─────────────┴──────────────┴──────┘
```
### Request Sub-commands (Host to TNC)
| Sub-command | Value | Data |
|-------------|-------|------|
| GetIdentity | `0x01` | - |
| GetRandom | `0x02` | Length (1 byte, 1-64) |
| VerifySignature | `0x03` | PubKey (32) + Signature (64) + Data |
| SignData | `0x04` | Data to sign |
| EncryptData | `0x05` | Key (32) + Plaintext |
| DecryptData | `0x06` | Key (32) + MAC (2) + Ciphertext |
| KeyExchange | `0x07` | Remote PubKey (32) |
| Hash | `0x08` | Data to hash |
| SetRadio | `0x09` | Freq (4) + BW (4) + SF (1) + CR (1) |
| SetTxPower | `0x0A` | Power dBm (1) |
| GetRadio | `0x0B` | - |
| GetTxPower | `0x0C` | - |
| GetCurrentRssi | `0x0D` | - |
| IsChannelBusy | `0x0E` | - |
| GetAirtime | `0x0F` | Packet length (1) |
| GetNoiseFloor | `0x10` | - |
| GetVersion | `0x11` | - |
| GetStats | `0x12` | - |
| GetBattery | `0x13` | - |
| GetMCUTemp | `0x14` | - |
| GetSensors | `0x15` | Permissions (1) |
| GetDeviceName | `0x16` | - |
| Ping | `0x17` | - |
| Reboot | `0x18` | - |
| SetSignalReport | `0x19` | Enable (1): 0x00=disable, nonzero=enable |
| GetSignalReport | `0x1A` | - |
### Response Sub-commands (TNC to Host)
Response codes use the high-bit convention: `response = command | 0x80`. Generic and unsolicited responses use the `0xF0`+ range.
| Sub-command | Value | Data |
|-------------|-------|------|
| Identity | `0x81` | PubKey (32) |
| Random | `0x82` | Random bytes (1-64) |
| Verify | `0x83` | Result (1): 0x00=invalid, 0x01=valid |
| Signature | `0x84` | Signature (64) |
| Encrypted | `0x85` | MAC (2) + Ciphertext |
| Decrypted | `0x86` | Plaintext |
| SharedSecret | `0x87` | Shared secret (32) |
| Hash | `0x88` | SHA-256 hash (32) |
| Radio | `0x8B` | Freq (4) + BW (4) + SF (1) + CR (1) |
| TxPower | `0x8C` | Power dBm (1) |
| CurrentRssi | `0x8D` | RSSI dBm (1, signed) |
| ChannelBusy | `0x8E` | Result (1): 0x00=clear, 0x01=busy |
| Airtime | `0x8F` | Milliseconds (4) |
| NoiseFloor | `0x90` | dBm (2, signed) |
| Version | `0x91` | Version (1) + Reserved (1) |
| Stats | `0x92` | RX (4) + TX (4) + Errors (4) |
| Battery | `0x93` | Millivolts (2) |
| MCUTemp | `0x94` | Temperature (2, signed) |
| Sensors | `0x95` | CayenneLPP payload |
| DeviceName | `0x96` | Name (variable, UTF-8) |
| Pong | `0x97` | - |
| SignalReport | `0x9A` | Status (1): 0x00=disabled, 0x01=enabled |
| OK | `0xF0` | - |
| Error | `0xF1` | Error code (1) |
| TxDone | `0xF8` | Result (1): 0x00=failed, 0x01=success |
| RxMeta | `0xF9` | SNR (1) + RSSI (1) |
### Error Codes
| Code | Value | Description |
|------|-------|-------------|
| InvalidLength | `0x01` | Request data too short |
| InvalidParam | `0x02` | Invalid parameter value |
| NoCallback | `0x03` | Feature not available |
| MacFailed | `0x04` | MAC verification failed |
| UnknownCmd | `0x05` | Unknown sub-command |
| EncryptFailed | `0x06` | Encryption failed |
### Unsolicited Events
The TNC sends these SetHardware frames without a preceding request:
**TxDone (0xF8)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure.
**RxMeta (0xF9)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame.
## Data Formats
### Radio Parameters (SetRadio / Radio response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| Frequency | 4 bytes | Hz (e.g., 869618000) |
| Bandwidth | 4 bytes | Hz (e.g., 62500) |
| SF | 1 byte | Spreading factor (5-12) |
| CR | 1 byte | Coding rate (5-8) |
### Version (Version response)
| Field | Size | Description |
|-------|------|-------------|
| Version | 1 byte | Firmware version |
| Reserved | 1 byte | Always 0 |
### Encrypted (Encrypted response)
| Field | Size | Description |
|-------|------|-------------|
| MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes |
| Ciphertext | variable | AES-128-CBC encrypted data |
### Airtime (Airtime response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| Airtime | 4 bytes | uint32_t, estimated air time in milliseconds |
### Noise Floor (NoiseFloor response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| Noise floor | 2 bytes | int16_t, dBm (signed) |
The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.
### Stats (Stats response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| RX | 4 bytes | Packets received |
| TX | 4 bytes | Packets transmitted |
| Errors | 4 bytes | Receive errors |
### Battery (Battery response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| Millivolts | 2 bytes | uint16_t, battery voltage in mV |
### MCU Temperature (MCUTemp response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| Temperature | 2 bytes | int16_t, tenths of °C (e.g., 253 = 25.3°C) |
Returns `NoCallback` error if the board does not support temperature readings.
### Device Name (DeviceName response)
| Field | Size | Description |
|-------|------|-------------|
| Name | variable | UTF-8 string, no null terminator |
### Reboot
Sends an `OK` response, flushes serial, then reboots the device. The host should expect the connection to drop.
### Sensor Permissions (GetSensors)
| Bit | Value | Description |
|-----|-------|-------------|
| 0 | `0x01` | Base (battery) |
| 1 | `0x02` | Location (GPS) |
| 2 | `0x04` | Environment (temp, humidity, pressure) |
Use `0x07` for all permissions.
### Sensor Data (Sensors response)
Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.mydevices.com/docs/lorawan/cayenne-lpp) for parsing.
## Cryptographic Algorithms
| Operation | Algorithm |
|-----------|-----------|
| Identity / Signing / Verification | Ed25519 |
| Key Exchange | X25519 (ECDH) |
| Encryption | AES-128-CBC + HMAC-SHA256 (MAC truncated to 2 bytes) |
| Hashing | SHA-256 |
## Notes
- Data payload limit (255 bytes) matches MeshCore MAX_TRANS_UNIT; no change needed for KISS “1024+ recommended” (that applies to general TNCs, not MeshCore)
- Modem generates identity on first boot (stored in flash)
- All multi-byte values are little-endian unless stated otherwise
- SNR values in RxMeta are multiplied by 4 for 0.25 dB precision
- TxDone is sent as a SetHardware event after each transmission
- Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames
- See [packet_structure.md](./packet_structure.md) for packet format

View File

@@ -94,7 +94,7 @@ struct StatsRadio {
## RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2) ## RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)
**Total Frame Size:** 26 bytes (legacy) or 30 bytes (includes `recv_errors`) **Total Frame Size:** 26 bytes
| Offset | Size | Type | Field Name | Description | Range/Notes | | Offset | Size | Type | Field Name | Description | Range/Notes |
|--------|------|------|------------|-------------|-------------| |--------|------|------|------------|-------------|-------------|
@@ -106,14 +106,12 @@ struct StatsRadio {
| 14 | 4 | uint32_t | direct_tx | Packets sent via direct routing | 0 - 4,294,967,295 | | 14 | 4 | uint32_t | direct_tx | Packets sent via direct routing | 0 - 4,294,967,295 |
| 18 | 4 | uint32_t | flood_rx | Packets received via flood routing | 0 - 4,294,967,295 | | 18 | 4 | uint32_t | flood_rx | Packets received via flood routing | 0 - 4,294,967,295 |
| 22 | 4 | uint32_t | direct_rx | Packets received via direct routing | 0 - 4,294,967,295 | | 22 | 4 | uint32_t | direct_rx | Packets received via direct routing | 0 - 4,294,967,295 |
| 26 | 4 | uint32_t | recv_errors | Receive/CRC errors (RadioLib); present only in 30-byte frame | 0 - 4,294,967,295 |
### Notes ### Notes
- Counters are cumulative from boot and may wrap. - Counters are cumulative from boot and may wrap.
- `recv = flood_rx + direct_rx` - `recv = flood_rx + direct_rx`
- `sent = flood_tx + direct_tx` - `sent = flood_tx + direct_tx`
- Clients should accept frame length ≥ 26; if length ≥ 30, parse `recv_errors` at offset 26.
### Example Structure (C/C++) ### Example Structure (C/C++)
@@ -127,7 +125,6 @@ struct StatsPackets {
uint32_t direct_tx; uint32_t direct_tx;
uint32_t flood_rx; uint32_t flood_rx;
uint32_t direct_rx; uint32_t direct_rx;
uint32_t recv_errors; // present when frame size is 30
} __attribute__((packed)); } __attribute__((packed));
``` ```
@@ -186,12 +183,11 @@ def parse_stats_radio(frame):
} }
def parse_stats_packets(frame): def parse_stats_packets(frame):
"""Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)""" """Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 bytes)"""
assert len(frame) >= 26, "STATS_TYPE_PACKETS frame too short"
response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \ response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \
struct.unpack('<B B I I I I I I', frame[:26]) struct.unpack('<B B I I I I I I', frame)
assert response_code == 24 and stats_type == 2, "Invalid response type" assert response_code == 24 and stats_type == 2, "Invalid response type"
result = { return {
'recv': recv, 'recv': recv,
'sent': sent, 'sent': sent,
'flood_tx': flood_tx, 'flood_tx': flood_tx,
@@ -199,10 +195,6 @@ def parse_stats_packets(frame):
'flood_rx': flood_rx, 'flood_rx': flood_rx,
'direct_rx': direct_rx 'direct_rx': direct_rx
} }
if len(frame) >= 30:
(recv_errors,) = struct.unpack('<I', frame[26:30])
result['recv_errors'] = recv_errors
return result
``` ```
--- ---
@@ -259,7 +251,6 @@ interface StatsPackets {
direct_tx: number; direct_tx: number;
flood_rx: number; flood_rx: number;
direct_rx: number; direct_rx: number;
recv_errors?: number; // present when frame is 30 bytes
} }
function parseStatsCore(buffer: ArrayBuffer): StatsCore { function parseStatsCore(buffer: ArrayBuffer): StatsCore {
@@ -295,15 +286,12 @@ function parseStatsRadio(buffer: ArrayBuffer): StatsRadio {
function parseStatsPackets(buffer: ArrayBuffer): StatsPackets { function parseStatsPackets(buffer: ArrayBuffer): StatsPackets {
const view = new DataView(buffer); const view = new DataView(buffer);
if (buffer.byteLength < 26) {
throw new Error('STATS_TYPE_PACKETS frame too short');
}
const response_code = view.getUint8(0); const response_code = view.getUint8(0);
const stats_type = view.getUint8(1); const stats_type = view.getUint8(1);
if (response_code !== 24 || stats_type !== 2) { if (response_code !== 24 || stats_type !== 2) {
throw new Error('Invalid response type'); throw new Error('Invalid response type');
} }
const result: StatsPackets = { return {
recv: view.getUint32(2, true), recv: view.getUint32(2, true),
sent: view.getUint32(6, true), sent: view.getUint32(6, true),
flood_tx: view.getUint32(10, true), flood_tx: view.getUint32(10, true),
@@ -311,10 +299,6 @@ function parseStatsPackets(buffer: ArrayBuffer): StatsPackets {
flood_rx: view.getUint32(18, true), flood_rx: view.getUint32(18, true),
direct_rx: view.getUint32(22, true) direct_rx: view.getUint32(22, true)
}; };
if (buffer.byteLength >= 30) {
result.recv_errors = view.getUint32(26, true);
}
return result;
} }
``` ```

View File

@@ -212,7 +212,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 file.read(pad, 1); // 62
file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
@@ -222,14 +222,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76 file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77 file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
file.read((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78 file.read(pad, 2); // 78
file.read(pad, 1); // 79
file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84 file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85 file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88
file.close(); file.close();
} }
@@ -249,7 +247,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 file.write(pad, 1); // 62
file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
@@ -259,14 +257,12 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76 file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77 file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78 file.write(pad, 2); // 78
file.write(pad, 1); // 79
file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84 file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85 file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88
file.close(); file.close();
} }
@@ -564,20 +560,14 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src
} }
return false; // error return false; // error
} }
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
return true; // this is just a stub on NRF52/STM32 platforms
}
#else #else
inline void makeBlobPath(const uint8_t key[], int key_len, char* path, size_t path_size) { uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
char path[64];
char fname[18]; char fname[18];
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix) if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
mesh::Utils::toHex(fname, key, key_len); mesh::Utils::toHex(fname, key, key_len);
sprintf(path, "/bl/%s", fname); sprintf(path, "/bl/%s", fname);
}
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
char path[64];
makeBlobPath(key, key_len, path, sizeof(path));
if (_fs->exists(path)) { if (_fs->exists(path)) {
File f = openRead(_fs, path); File f = openRead(_fs, path);
@@ -592,7 +582,11 @@ uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_b
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) { bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
char path[64]; char path[64];
makeBlobPath(key, key_len, path, sizeof(path)); char fname[18];
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
mesh::Utils::toHex(fname, key, key_len);
sprintf(path, "/bl/%s", fname);
File f = openWrite(_fs, path); File f = openWrite(_fs, path);
if (f) { if (f) {
@@ -604,13 +598,4 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src
} }
return false; // error return false; // error
} }
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
char path[64];
makeBlobPath(key, key_len, path, sizeof(path));
_fs->remove(path);
return true; // return true even if file did not exist
}
#endif #endif

View File

@@ -42,7 +42,6 @@ public:
void migrateToSecondaryFS(); void migrateToSecondaryFS();
uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]); uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]);
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len); bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len);
bool deleteBlobByKey(const uint8_t key[], int key_len);
File openRead(const char* filename); File openRead(const char* filename);
File openRead(FILESYSTEM* fs, const char* filename); File openRead(FILESYSTEM* fs, const char* filename);
bool removeFile(const char* filename); bool removeFile(const char* filename);

View File

@@ -56,8 +56,6 @@
#define CMD_SEND_ANON_REQ 57 #define CMD_SEND_ANON_REQ 57
#define CMD_SET_AUTOADD_CONFIG 58 #define CMD_SET_AUTOADD_CONFIG 58
#define CMD_GET_AUTOADD_CONFIG 59 #define CMD_GET_AUTOADD_CONFIG 59
#define CMD_GET_ALLOWED_REPEAT_FREQ 60
#define CMD_SET_PATH_HASH_MODE 61
// Stats sub-types for CMD_GET_STATS // Stats sub-types for CMD_GET_STATS
#define STATS_TYPE_CORE 0 #define STATS_TYPE_CORE 0
@@ -90,7 +88,6 @@
#define RESP_CODE_TUNING_PARAMS 23 #define RESP_CODE_TUNING_PARAMS 23
#define RESP_CODE_STATS 24 // v8+, second byte is stats type #define RESP_CODE_STATS 24 // v8+, second byte is stats type
#define RESP_CODE_AUTOADD_CONFIG 25 #define RESP_CODE_AUTOADD_CONFIG 25
#define RESP_ALLOWED_REPEAT_FREQ 26
#define SEND_TIMEOUT_BASE_MILLIS 500 #define SEND_TIMEOUT_BASE_MILLIS 500
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f #define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
@@ -258,15 +255,6 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
} }
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f);
return getRNG()->nextInt(0, 5*t + 1);
}
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f);
return getRNG()->nextInt(0, 5*t + 1);
}
uint8_t MyMesh::getExtraAckTransmitCount() const { uint8_t MyMesh::getExtraAckTransmitCount() const {
return _prefs.multi_acks; return _prefs.multi_acks;
} }
@@ -318,12 +306,7 @@ bool MyMesh::shouldOverwriteWhenFull() const {
return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0; return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0;
} }
uint8_t MyMesh::getAutoAddMaxHops() const {
return _prefs.autoadd_max_hops;
}
void MyMesh::onContactOverwrite(const uint8_t* pub_key) { void MyMesh::onContactOverwrite(const uint8_t* pub_key) {
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage
if (_serial->isConnected()) { if (_serial->isConnected()) {
out_frame[0] = PUSH_CODE_CONTACT_DELETED; out_frame[0] = PUSH_CODE_CONTACT_DELETED;
memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE); memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE);
@@ -347,14 +330,13 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
_serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
} }
} else {
#ifdef DISPLAY_CLASS
if (_ui) _ui->notify(UIEventType::newContactMessage);
#endif
} }
#ifdef DISPLAY_CLASS
if (_ui && !_prefs.buzzer_quiet) _ui->notify(UIEventType::newContactMessage); //buzz if enabled
#endif
// add inbound-path to mem cache // add inbound-path to mem cache
if (path && mesh::Packet::isValidPathLen(path_len)) { // check path is valid if (path && path_len <= sizeof(AdvertPath::path)) { // check path is valid
AdvertPath* p = advert_paths; AdvertPath* p = advert_paths;
uint32_t oldest = 0xFFFFFFFF; uint32_t oldest = 0xFFFFFFFF;
for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { // check if already in table, otherwise evict oldest for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { // check if already in table, otherwise evict oldest
@@ -371,7 +353,8 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix)); memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix));
strcpy(p->name, contact.name); strcpy(p->name, contact.name);
p->recv_timestamp = getRTCClock()->getCurrentTime(); p->recv_timestamp = getRTCClock()->getCurrentTime();
p->path_len = mesh::Packet::copyPath(p->path, path, path_len); p->path_len = path_len;
memcpy(p->path, path, p->path_len);
} }
if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[] if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]
@@ -457,9 +440,7 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
if (should_display && _ui) { if (should_display && _ui) {
_ui->newMsg(path_len, from.name, text, offline_queue_len); _ui->newMsg(path_len, from.name, text, offline_queue_len);
if (!_serial->isConnected()) { if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::contactMessage); //buzz if enabled
_ui->notify(UIEventType::contactMessage);
}
} }
#endif #endif
} }
@@ -470,30 +451,26 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
return false; return false;
} }
bool MyMesh::allowPacketForward(const mesh::Packet* packet) {
return _prefs.client_repeat != 0;
}
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: dynamic send_scope, depending on recipient and current 'home' Region // TODO: dynamic send_scope, depending on recipient and current 'home' Region
if (send_scope.isNull()) { if (send_scope.isNull()) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); sendFlood(pkt, delay_millis);
} else { } else {
uint16_t codes[2]; uint16_t codes[2];
codes[0] = send_scope.calcTransportCode(pkt); codes[0] = send_scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1); sendFlood(pkt, codes, delay_millis);
} }
} }
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) { void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: have per-channel send_scope // TODO: have per-channel send_scope
if (send_scope.isNull()) { if (send_scope.isNull()) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); sendFlood(pkt, delay_millis);
} else { } else {
uint16_t codes[2]; uint16_t codes[2];
codes[0] = send_scope.calcTransportCode(pkt); codes[0] = send_scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1); sendFlood(pkt, codes, delay_millis);
} }
} }
@@ -548,11 +525,8 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
uint8_t frame[1]; uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
_serial->writeFrame(frame, 1); _serial->writeFrame(frame, 1);
} else {
#ifdef DISPLAY_CLASS
if (_ui) _ui->notify(UIEventType::channelMessage);
#endif
} }
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
// Get the channel name from the channel index // Get the channel name from the channel index
const char *channel_name = "Unknown"; const char *channel_name = "Unknown";
@@ -560,7 +534,10 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
if (getChannel(channel_idx, channel_details)) { if (getChannel(channel_idx, channel_details)) {
channel_name = channel_details.name; channel_name = channel_details.name;
} }
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len); if (_ui) {
_ui->newMsg(path_len, channel_name, text, offline_queue_len);
if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::channelMessage); //buzz if enabled
}
#endif #endif
} }
@@ -690,7 +667,7 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
if (tag == pending_discovery) { // check for matching response tag) if (tag == pending_discovery) { // check for matching response tag)
pending_discovery = 0; pending_discovery = 0;
if (!mesh::Packet::isValidPathLen(in_path_len) || !mesh::Packet::isValidPathLen(out_path_len)) { if (in_path_len > MAX_PATH_SIZE || out_path_len > MAX_PATH_SIZE) {
MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len); MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len);
} else { } else {
int i = 0; int i = 0;
@@ -699,9 +676,11 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
memcpy(&out_frame[i], contact.id.pub_key, 6); memcpy(&out_frame[i], contact.id.pub_key, 6);
i += 6; // pub_key_prefix i += 6; // pub_key_prefix
out_frame[i++] = out_path_len; out_frame[i++] = out_path_len;
i += mesh::Packet::writePath(&out_frame[i], out_path, out_path_len); memcpy(&out_frame[i], out_path, out_path_len);
i += out_path_len;
out_frame[i++] = in_path_len; out_frame[i++] = in_path_len;
i += mesh::Packet::writePath(&out_frame[i], in_path, in_path_len); memcpy(&out_frame[i], in_path, in_path_len);
i += in_path_len;
// NOTE: telemetry data in 'extra' is discarded at present // NOTE: telemetry data in 'extra' is discarded at present
_serial->writeFrame(out_frame, i); _serial->writeFrame(out_frame, i);
@@ -787,10 +766,9 @@ uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const {
return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis); return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis);
} }
uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const { uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const {
uint8_t path_hash_count = path_len & 63;
return SEND_TIMEOUT_BASE_MILLIS + return SEND_TIMEOUT_BASE_MILLIS +
((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * ((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) *
(path_hash_count + 1)); (path_len + 1));
} }
void MyMesh::onSendTimeout() {} void MyMesh::onSendTimeout() {}
@@ -818,6 +796,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.bw = LORA_BW; _prefs.bw = LORA_BW;
_prefs.cr = LORA_CR; _prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER; _prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.buzzer_quiet = 0;
_prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_enabled = 0; // GPS disabled by default
_prefs.gps_interval = 0; // No automatic GPS updates by default _prefs.gps_interval = 0; // No automatic GPS updates by default
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed //_prefs.rx_delay_base = 10.0f; enable once new algo fixed
@@ -856,7 +835,8 @@ void MyMesh::begin(bool has_display) {
_prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f); _prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f);
_prefs.sf = constrain(_prefs.sf, 5, 12); _prefs.sf = constrain(_prefs.sf, 5, 12);
_prefs.cr = constrain(_prefs.cr, 5, 8); _prefs.cr = constrain(_prefs.cr, 5, 8);
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER);
_prefs.buzzer_quiet = constrain(_prefs.buzzer_quiet, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours
@@ -899,24 +879,6 @@ uint32_t MyMesh::getBLEPin() {
return _active_ble_pin; return _active_ble_pin;
} }
struct FreqRange {
uint32_t lower_freq, upper_freq;
};
static FreqRange repeat_freq_ranges[] = {
{ 433000, 433000 },
{ 869000, 869000 },
{ 918000, 918000 }
};
bool MyMesh::isValidClientRepeatFreq(uint32_t f) const {
for (int i = 0; i < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]); i++) {
auto r = &repeat_freq_ranges[i];
if (f >= r->lower_freq && f <= r->upper_freq) return true;
}
return false;
}
void MyMesh::startInterface(BaseSerialInterface &serial) { void MyMesh::startInterface(BaseSerialInterface &serial) {
_serial = &serial; _serial = &serial;
serial.enable(); serial.enable();
@@ -940,8 +902,6 @@ void MyMesh::handleCmdFrame(size_t len) {
i += 40; i += 40;
StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20); StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20);
i += 20; i += 20;
out_frame[i++] = _prefs.client_repeat; // v9+
out_frame[i++] = _prefs.path_hash_mode; // v10+
_serial->writeFrame(out_frame, i); _serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_APP_START && } else if (cmd_frame[0] == CMD_APP_START &&
len >= 8) { // sent when app establishes connection, respond with node ID len >= 8) { // sent when app establishes connection, respond with node ID
@@ -1119,8 +1079,7 @@ void MyMesh::handleCmdFrame(size_t len) {
} }
if (pkt) { if (pkt) {
if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop)
unsigned long delay_millis = 0; sendFlood(pkt);
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else { } else {
sendZeroHop(pkt); sendZeroHop(pkt);
} }
@@ -1132,7 +1091,7 @@ void MyMesh::handleCmdFrame(size_t len) {
uint8_t *pub_key = &cmd_frame[1]; uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) { if (recipient) {
recipient->out_path_len = OUT_PATH_UNKNOWN; recipient->out_path_len = -1;
// recipient->lastmod = ?? shouldn't be needed, app already has this version of contact // recipient->lastmod = ?? shouldn't be needed, app already has this version of contact
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
writeOKFrame(); writeOKFrame();
@@ -1164,7 +1123,6 @@ void MyMesh::handleCmdFrame(size_t len) {
uint8_t *pub_key = &cmd_frame[1]; uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient && removeContact(*recipient)) { if (recipient && removeContact(*recipient)) {
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE);
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
writeOKFrame(); writeOKFrame();
} else { } else {
@@ -1247,20 +1205,13 @@ void MyMesh::handleCmdFrame(size_t len) {
i += 4; i += 4;
uint8_t sf = cmd_frame[i++]; uint8_t sf = cmd_frame[i++];
uint8_t cr = cmd_frame[i++]; uint8_t cr = cmd_frame[i++];
uint8_t repeat = 0; // default - false
if (len > i) {
repeat = cmd_frame[i++]; // FIRMWARE_VER_CODE 9+
}
if (repeat && !isValidClientRepeatFreq(freq)) { if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 &&
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 &&
bw <= 500000) { bw <= 500000) {
_prefs.sf = sf; _prefs.sf = sf;
_prefs.cr = cr; _prefs.cr = cr;
_prefs.freq = (float)freq / 1000.0; _prefs.freq = (float)freq / 1000.0;
_prefs.bw = (float)bw / 1000.0; _prefs.bw = (float)bw / 1000.0;
_prefs.client_repeat = repeat;
savePrefs(); savePrefs();
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
@@ -1274,11 +1225,10 @@ void MyMesh::handleCmdFrame(size_t len) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG); writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} }
} else if (cmd_frame[0] == CMD_SET_RADIO_TX_POWER) { } else if (cmd_frame[0] == CMD_SET_RADIO_TX_POWER) {
int8_t power = (int8_t)cmd_frame[1]; if (cmd_frame[1] > MAX_LORA_TX_POWER) {
if (power < -9 || power > MAX_LORA_TX_POWER) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG); writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else { } else {
_prefs.tx_power_dbm = power; _prefs.tx_power_dbm = cmd_frame[1];
savePrefs(); savePrefs();
radio_set_tx_power(_prefs.tx_power_dbm); radio_set_tx_power(_prefs.tx_power_dbm);
writeOKFrame(); writeOKFrame();
@@ -1317,14 +1267,6 @@ void MyMesh::handleCmdFrame(size_t len) {
} }
savePrefs(); savePrefs();
writeOKFrame(); writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_PATH_HASH_MODE && cmd_frame[1] == 0 && len >= 3) {
if (cmd_frame[2] >= 3) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
_prefs.path_hash_mode = cmd_frame[2];
savePrefs();
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) { } else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) {
if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed? if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed?
saveContacts(); saveContacts();
@@ -1462,7 +1404,7 @@ void MyMesh::handleCmdFrame(size_t len) {
memset(&req_data[2], 0, 3); // reserved memset(&req_data[2], 0, 3); // reserved
getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique
auto save = recipient->out_path_len; // temporarily force sendRequest() to flood auto save = recipient->out_path_len; // temporarily force sendRequest() to flood
recipient->out_path_len = OUT_PATH_UNKNOWN; recipient->out_path_len = -1;
int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout); int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout);
recipient->out_path_len = save; recipient->out_path_len = save;
if (result == MSG_SEND_FAILED) { if (result == MSG_SEND_FAILED) {
@@ -1622,7 +1564,7 @@ void MyMesh::handleCmdFrame(size_t len) {
sendDirect(pkt, &cmd_frame[10], path_len); sendDirect(pkt, &cmd_frame[10], path_len);
uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2); uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2);
uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len >> path_sz); uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len);
out_frame[0] = RESP_CODE_SENT; out_frame[0] = RESP_CODE_SENT;
out_frame[1] = 0; out_frame[1] = 0;
@@ -1699,12 +1641,11 @@ void MyMesh::handleCmdFrame(size_t len) {
} }
} }
if (found) { if (found) {
int i = 0; out_frame[0] = RESP_CODE_ADVERT_PATH;
out_frame[i++] = RESP_CODE_ADVERT_PATH; memcpy(&out_frame[1], &found->recv_timestamp, 4);
memcpy(&out_frame[i], &found->recv_timestamp, 4); i += 4; out_frame[5] = found->path_len;
out_frame[i++] = found->path_len; memcpy(&out_frame[6], found->path, found->path_len);
i += mesh::Packet::writePath(&out_frame[i], found->path, found->path_len); _serial->writeFrame(out_frame, 6 + found->path_len);
_serial->writeFrame(out_frame, i);
} else { } else {
writeErrFrame(ERR_CODE_NOT_FOUND); writeErrFrame(ERR_CODE_NOT_FOUND);
} }
@@ -1747,14 +1688,12 @@ void MyMesh::handleCmdFrame(size_t len) {
uint32_t n_sent_direct = getNumSentDirect(); uint32_t n_sent_direct = getNumSentDirect();
uint32_t n_recv_flood = getNumRecvFlood(); uint32_t n_recv_flood = getNumRecvFlood();
uint32_t n_recv_direct = getNumRecvDirect(); uint32_t n_recv_direct = getNumRecvDirect();
uint32_t n_recv_errors = radio_driver.getPacketsRecvErrors();
memcpy(&out_frame[i], &recv, 4); i += 4; memcpy(&out_frame[i], &recv, 4); i += 4;
memcpy(&out_frame[i], &sent, 4); i += 4; memcpy(&out_frame[i], &sent, 4); i += 4;
memcpy(&out_frame[i], &n_sent_flood, 4); i += 4; memcpy(&out_frame[i], &n_sent_flood, 4); i += 4;
memcpy(&out_frame[i], &n_sent_direct, 4); i += 4; memcpy(&out_frame[i], &n_sent_direct, 4); i += 4;
memcpy(&out_frame[i], &n_recv_flood, 4); i += 4; memcpy(&out_frame[i], &n_recv_flood, 4); i += 4;
memcpy(&out_frame[i], &n_recv_direct, 4); i += 4; memcpy(&out_frame[i], &n_recv_direct, 4); i += 4;
memcpy(&out_frame[i], &n_recv_errors, 4); i += 4;
_serial->writeFrame(out_frame, i); _serial->writeFrame(out_frame, i);
} else { } else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type
@@ -1789,25 +1728,12 @@ void MyMesh::handleCmdFrame(size_t len) {
} }
} else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) { } else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) {
_prefs.autoadd_config = cmd_frame[1]; _prefs.autoadd_config = cmd_frame[1];
if (len >= 3) {
_prefs.autoadd_max_hops = min(cmd_frame[2], (uint8_t)64);
}
savePrefs(); savePrefs();
writeOKFrame(); writeOKFrame();
} else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) { } else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) {
int i = 0; int i = 0;
out_frame[i++] = RESP_CODE_AUTOADD_CONFIG; out_frame[i++] = RESP_CODE_AUTOADD_CONFIG;
out_frame[i++] = _prefs.autoadd_config; out_frame[i++] = _prefs.autoadd_config;
out_frame[i++] = _prefs.autoadd_max_hops;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ) {
int i = 0;
out_frame[i++] = RESP_ALLOWED_REPEAT_FREQ;
for (int k = 0; k < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]) && i + 8 < sizeof(out_frame); k++) {
auto r = &repeat_freq_ranges[k];
memcpy(&out_frame[i], &r->lower_freq, 4); i += 4;
memcpy(&out_frame[i], &r->upper_freq, 4); i += 4;
}
_serial->writeFrame(out_frame, i); _serial->writeFrame(out_frame, i);
} else { } else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);

View File

@@ -5,14 +5,14 @@
#include "AbstractUITask.h" #include "AbstractUITask.h"
/*------------ Frame Protocol --------------*/ /*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 10 #define FIRMWARE_VER_CODE 8
#ifndef FIRMWARE_BUILD_DATE #ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "15 Feb 2026" #define FIRMWARE_BUILD_DATE "29 Jan 2026"
#endif #endif
#ifndef FIRMWARE_VERSION #ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.13.0" #define FIRMWARE_VERSION "v1.12.0"
#endif #endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -106,11 +106,8 @@ protected:
float getAirtimeBudgetFactor() const override; float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() const override; int getInterferenceThreshold() const override;
int calcRxDelay(float score, uint32_t air_time) const override; int calcRxDelay(float score, uint32_t air_time) const override;
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
uint8_t getExtraAckTransmitCount() const override; uint8_t getExtraAckTransmitCount() const override;
bool filterRecvFloodPacket(mesh::Packet* packet) override; bool filterRecvFloodPacket(mesh::Packet* packet) override;
bool allowPacketForward(const mesh::Packet* packet) override;
void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override; void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override; void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override;
@@ -119,7 +116,6 @@ protected:
bool isAutoAddEnabled() const override; bool isAutoAddEnabled() const override;
bool shouldAutoAddContactType(uint8_t type) const override; bool shouldAutoAddContactType(uint8_t type) const override;
bool shouldOverwriteWhenFull() const override; bool shouldOverwriteWhenFull() const override;
uint8_t getAutoAddMaxHops() const override;
void onContactsFull() override; void onContactsFull() override;
void onContactOverwrite(const uint8_t* pub_key) override; void onContactOverwrite(const uint8_t* pub_key) override;
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
@@ -180,7 +176,6 @@ private:
void checkCLIRescueCmd(); void checkCLIRescueCmd();
void checkSerialInterface(); void checkSerialInterface();
bool isValidClientRepeatFreq(uint32_t f) const;
// helpers, short-cuts // helpers, short-cuts
void saveChannels() { _store->saveChannels(this); } void saveChannels() { _store->saveChannels(this); }

View File

@@ -17,7 +17,7 @@ struct NodePrefs { // persisted to file
uint8_t multi_acks; uint8_t multi_acks;
uint8_t manual_add_contacts; uint8_t manual_add_contacts;
float bw; float bw;
int8_t tx_power_dbm; uint8_t tx_power_dbm;
uint8_t telemetry_mode_base; uint8_t telemetry_mode_base;
uint8_t telemetry_mode_loc; uint8_t telemetry_mode_loc;
uint8_t telemetry_mode_env; uint8_t telemetry_mode_env;
@@ -28,7 +28,4 @@ struct NodePrefs { // persisted to file
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled) uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval; // GPS read interval in seconds uint32_t gps_interval; // GPS read interval in seconds
uint8_t autoadd_config; // bitmask for auto-add contacts config uint8_t autoadd_config; // bitmask for auto-add contacts config
uint8_t client_repeat;
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
}; };

View File

@@ -194,7 +194,6 @@ void setup() {
); );
#ifdef WIFI_SSID #ifdef WIFI_SSID
board.setInhibitSleep(true); // prevent sleep when WiFi is active
WiFi.begin(WIFI_SSID, WIFI_PWD); WiFi.begin(WIFI_SSID, WIFI_PWD);
serial_interface.begin(TCP_PORT); serial_interface.begin(TCP_PORT);
#elif defined(BLE_PIN_CODE) #elif defined(BLE_PIN_CODE)

View File

@@ -103,14 +103,8 @@ class HomeScreen : public UIScreen {
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
// Convert millivolts to percentage // Convert millivolts to percentage
#ifndef BATT_MIN_MILLIVOLTS const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
#define BATT_MIN_MILLIVOLTS 3000 const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
#endif
#ifndef BATT_MAX_MILLIVOLTS
#define BATT_MAX_MILLIVOLTS 4200
#endif
const int minMilliVolts = BATT_MIN_MILLIVOLTS;
const int maxMilliVolts = BATT_MAX_MILLIVOLTS;
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts); int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0% if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100% if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
@@ -131,14 +125,6 @@ class HomeScreen : public UIScreen {
// fill the battery based on the percentage // fill the battery based on the percentage
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
// show muted icon if buzzer is muted
#ifdef PIN_BUZZER
if (_task->isBuzzerQuiet()) {
display.setColor(DisplayDriver::RED);
display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8);
}
#endif
} }
CayenneLPP sensors_lpp; CayenneLPP sensors_lpp;
@@ -466,17 +452,15 @@ class MsgPreviewScreen : public UIScreen {
}; };
#define MAX_UNREAD_MSGS 32 #define MAX_UNREAD_MSGS 32
int num_unread; int num_unread;
int head = MAX_UNREAD_MSGS - 1; // index of latest unread message
MsgEntry unread[MAX_UNREAD_MSGS]; MsgEntry unread[MAX_UNREAD_MSGS];
public: public:
MsgPreviewScreen(UITask* task, mesh::RTCClock* rtc) : _task(task), _rtc(rtc) { num_unread = 0; } MsgPreviewScreen(UITask* task, mesh::RTCClock* rtc) : _task(task), _rtc(rtc) { num_unread = 0; }
void addPreview(uint8_t path_len, const char* from_name, const char* msg) { void addPreview(uint8_t path_len, const char* from_name, const char* msg) {
head = (head + 1) % MAX_UNREAD_MSGS; if (num_unread >= MAX_UNREAD_MSGS) return; // full
if (num_unread < MAX_UNREAD_MSGS) num_unread++;
auto p = &unread[head]; auto p = &unread[num_unread++];
p->timestamp = _rtc->getCurrentTime(); p->timestamp = _rtc->getCurrentTime();
if (path_len == 0xFF) { if (path_len == 0xFF) {
sprintf(p->origin, "(D) %s:", from_name); sprintf(p->origin, "(D) %s:", from_name);
@@ -494,7 +478,7 @@ public:
sprintf(tmp, "Unread: %d", num_unread); sprintf(tmp, "Unread: %d", num_unread);
display.print(tmp); display.print(tmp);
auto p = &unread[head]; auto p = &unread[0];
int secs = _rtc->getCurrentTime() - p->timestamp; int secs = _rtc->getCurrentTime() - p->timestamp;
if (secs < 60) { if (secs < 60) {
@@ -530,10 +514,14 @@ public:
bool handleInput(char c) override { bool handleInput(char c) override {
if (c == KEY_NEXT || c == KEY_RIGHT) { if (c == KEY_NEXT || c == KEY_RIGHT) {
head = (head + MAX_UNREAD_MSGS - 1) % MAX_UNREAD_MSGS;
num_unread--; num_unread--;
if (num_unread == 0) { if (num_unread == 0) {
_task->gotoHomeScreen(); _task->gotoHomeScreen();
} else {
// delete first/curr item from unread queue
for (int i = 0; i < num_unread; i++) {
unread[i] = unread[i + 1];
}
} }
return true; return true;
} }

View File

@@ -78,14 +78,6 @@ public:
bool hasDisplay() const { return _display != NULL; } bool hasDisplay() const { return _display != NULL; }
bool isButtonPressed() const; bool isButtonPressed() const;
bool isBuzzerQuiet() {
#ifdef PIN_BUZZER
return buzzer.isQuiet();
#else
return true;
#endif
}
void toggleBuzzer(); void toggleBuzzer();
bool getGPSState(); bool getGPSState();
void toggleGPS(); void toggleGPS();

View File

@@ -116,7 +116,3 @@ static const uint8_t advert_icon[] = {
0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}; };
static const uint8_t muted_icon[] = {
0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20
};

View File

@@ -149,14 +149,8 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) {
// Convert millivolts to percentage // Convert millivolts to percentage
#ifndef BATT_MIN_MILLIVOLTS const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
#define BATT_MIN_MILLIVOLTS 3000 const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
#endif
#ifndef BATT_MAX_MILLIVOLTS
#define BATT_MAX_MILLIVOLTS 4200
#endif
const int minMilliVolts = BATT_MIN_MILLIVOLTS;
const int maxMilliVolts = BATT_MAX_MILLIVOLTS;
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts); int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0% if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100% if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%

View File

@@ -1,581 +0,0 @@
#include "KissModem.h"
#include <CayenneLPP.h>
KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng,
mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors)
: _serial(serial), _identity(identity), _rng(rng), _radio(radio), _board(board), _sensors(sensors) {
_rx_len = 0;
_rx_escaped = false;
_rx_active = false;
_has_pending_tx = false;
_pending_tx_len = 0;
_txdelay = KISS_DEFAULT_TXDELAY;
_persistence = KISS_DEFAULT_PERSISTENCE;
_slottime = KISS_DEFAULT_SLOTTIME;
_txtail = 0;
_fullduplex = 0;
_tx_state = TX_IDLE;
_tx_timer = 0;
_setRadioCallback = nullptr;
_setTxPowerCallback = nullptr;
_getCurrentRssiCallback = nullptr;
_getStatsCallback = nullptr;
_config = {0, 0, 0, 0, 0};
_signal_report_enabled = true;
}
void KissModem::begin() {
_rx_len = 0;
_rx_escaped = false;
_rx_active = false;
_has_pending_tx = false;
_tx_state = TX_IDLE;
}
void KissModem::writeByte(uint8_t b) {
if (b == KISS_FEND) {
_serial.write(KISS_FESC);
_serial.write(KISS_TFEND);
} else if (b == KISS_FESC) {
_serial.write(KISS_FESC);
_serial.write(KISS_TFESC);
} else {
_serial.write(b);
}
}
void KissModem::writeFrame(uint8_t type, const uint8_t* data, uint16_t len) {
_serial.write(KISS_FEND);
writeByte(type);
for (uint16_t i = 0; i < len; i++) {
writeByte(data[i]);
}
_serial.write(KISS_FEND);
}
void KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
_serial.write(KISS_FEND);
writeByte(KISS_CMD_SETHARDWARE);
writeByte(sub_cmd);
for (uint16_t i = 0; i < len; i++) {
writeByte(data[i]);
}
_serial.write(KISS_FEND);
}
void KissModem::writeHardwareError(uint8_t error_code) {
writeHardwareFrame(HW_RESP_ERROR, &error_code, 1);
}
void KissModem::loop() {
while (_serial.available()) {
uint8_t b = _serial.read();
if (b == KISS_FEND) {
if (_rx_active && _rx_len > 0) {
processFrame();
}
_rx_len = 0;
_rx_escaped = false;
_rx_active = true;
continue;
}
if (!_rx_active) continue;
if (b == KISS_FESC) {
_rx_escaped = true;
continue;
}
if (_rx_escaped) {
_rx_escaped = false;
if (b == KISS_TFEND) b = KISS_FEND;
else if (b == KISS_TFESC) b = KISS_FESC;
else continue;
}
if (_rx_len < KISS_MAX_FRAME_SIZE) {
_rx_buf[_rx_len++] = b;
} else {
/* Buffer full with no FEND; reset so we don't stay stuck ignoring input. */
_rx_len = 0;
_rx_escaped = false;
_rx_active = false;
}
}
processTx();
}
void KissModem::processFrame() {
if (_rx_len < 1) return;
uint8_t type_byte = _rx_buf[0];
if (type_byte == KISS_CMD_RETURN) return;
uint8_t port = (type_byte >> 4) & 0x0F;
uint8_t cmd = type_byte & 0x0F;
if (port != 0) return;
const uint8_t* data = &_rx_buf[1];
uint16_t data_len = _rx_len - 1;
switch (cmd) {
case KISS_CMD_DATA:
if (data_len > 0 && data_len <= KISS_MAX_PACKET_SIZE && !_has_pending_tx) {
memcpy(_pending_tx, data, data_len);
_pending_tx_len = data_len;
_has_pending_tx = true;
}
break;
case KISS_CMD_TXDELAY:
if (data_len >= 1) _txdelay = data[0];
break;
case KISS_CMD_PERSISTENCE:
if (data_len >= 1) _persistence = data[0];
break;
case KISS_CMD_SLOTTIME:
if (data_len >= 1) _slottime = data[0];
break;
case KISS_CMD_TXTAIL:
if (data_len >= 1) _txtail = data[0];
break;
case KISS_CMD_FULLDUPLEX:
if (data_len >= 1) _fullduplex = data[0];
break;
case KISS_CMD_SETHARDWARE:
if (data_len >= 1) {
handleHardwareCommand(data[0], data + 1, data_len - 1);
}
break;
default:
break;
}
}
void KissModem::handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
switch (sub_cmd) {
case HW_CMD_GET_IDENTITY:
handleGetIdentity();
break;
case HW_CMD_GET_RANDOM:
handleGetRandom(data, len);
break;
case HW_CMD_VERIFY_SIGNATURE:
handleVerifySignature(data, len);
break;
case HW_CMD_SIGN_DATA:
handleSignData(data, len);
break;
case HW_CMD_ENCRYPT_DATA:
handleEncryptData(data, len);
break;
case HW_CMD_DECRYPT_DATA:
handleDecryptData(data, len);
break;
case HW_CMD_KEY_EXCHANGE:
handleKeyExchange(data, len);
break;
case HW_CMD_HASH:
handleHash(data, len);
break;
case HW_CMD_SET_RADIO:
handleSetRadio(data, len);
break;
case HW_CMD_SET_TX_POWER:
handleSetTxPower(data, len);
break;
case HW_CMD_GET_RADIO:
handleGetRadio();
break;
case HW_CMD_GET_TX_POWER:
handleGetTxPower();
break;
case HW_CMD_GET_VERSION:
handleGetVersion();
break;
case HW_CMD_GET_CURRENT_RSSI:
handleGetCurrentRssi();
break;
case HW_CMD_IS_CHANNEL_BUSY:
handleIsChannelBusy();
break;
case HW_CMD_GET_AIRTIME:
handleGetAirtime(data, len);
break;
case HW_CMD_GET_NOISE_FLOOR:
handleGetNoiseFloor();
break;
case HW_CMD_GET_STATS:
handleGetStats();
break;
case HW_CMD_GET_BATTERY:
handleGetBattery();
break;
case HW_CMD_PING:
handlePing();
break;
case HW_CMD_GET_SENSORS:
handleGetSensors(data, len);
break;
case HW_CMD_GET_MCU_TEMP:
handleGetMCUTemp();
break;
case HW_CMD_REBOOT:
handleReboot();
break;
case HW_CMD_GET_DEVICE_NAME:
handleGetDeviceName();
break;
case HW_CMD_SET_SIGNAL_REPORT:
handleSetSignalReport(data, len);
break;
case HW_CMD_GET_SIGNAL_REPORT:
handleGetSignalReport();
break;
default:
writeHardwareError(HW_ERR_UNKNOWN_CMD);
break;
}
}
void KissModem::processTx() {
switch (_tx_state) {
case TX_IDLE:
if (_has_pending_tx) {
if (_fullduplex) {
_tx_timer = millis();
_tx_state = TX_DELAY;
} else {
_tx_state = TX_WAIT_CLEAR;
}
}
break;
case TX_WAIT_CLEAR:
if (!_radio.isReceiving()) {
uint8_t rand_val;
_rng.random(&rand_val, 1);
if (rand_val <= _persistence) {
_tx_timer = millis();
_tx_state = TX_DELAY;
} else {
_tx_timer = millis();
_tx_state = TX_SLOT_WAIT;
}
}
break;
case TX_SLOT_WAIT:
if (millis() - _tx_timer >= (uint32_t)_slottime * 10) {
_tx_state = TX_WAIT_CLEAR;
}
break;
case TX_DELAY:
if (millis() - _tx_timer >= (uint32_t)_txdelay * 10) {
_radio.startSendRaw(_pending_tx, _pending_tx_len);
_tx_state = TX_SENDING;
}
break;
case TX_SENDING:
if (_radio.isSendComplete()) {
_radio.onSendFinished();
uint8_t result = 0x01;
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
}
break;
}
}
void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) {
writeFrame(KISS_CMD_DATA, packet, len);
if (_signal_report_enabled) {
uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi };
writeHardwareFrame(HW_RESP_RX_META, meta, 2);
}
}
void KissModem::handleGetIdentity() {
writeHardwareFrame(HW_RESP(HW_CMD_GET_IDENTITY), _identity.pub_key, PUB_KEY_SIZE);
}
void KissModem::handleGetRandom(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t requested = data[0];
if (requested < 1 || requested > 64) {
writeHardwareError(HW_ERR_INVALID_PARAM);
return;
}
uint8_t buf[64];
_rng.random(buf, requested);
writeHardwareFrame(HW_RESP(HW_CMD_GET_RANDOM), buf, requested);
}
void KissModem::handleVerifySignature(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE + SIGNATURE_SIZE + 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
mesh::Identity signer(data);
const uint8_t* signature = data + PUB_KEY_SIZE;
const uint8_t* msg = data + PUB_KEY_SIZE + SIGNATURE_SIZE;
uint16_t msg_len = len - PUB_KEY_SIZE - SIGNATURE_SIZE;
uint8_t result = signer.verify(signature, msg, msg_len) ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_VERIFY_SIGNATURE), &result, 1);
}
void KissModem::handleSignData(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t signature[SIGNATURE_SIZE];
_identity.sign(signature, data, len);
writeHardwareFrame(HW_RESP(HW_CMD_SIGN_DATA), signature, SIGNATURE_SIZE);
}
void KissModem::handleEncryptData(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE + 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
const uint8_t* key = data;
const uint8_t* plaintext = data + PUB_KEY_SIZE;
uint16_t plaintext_len = len - PUB_KEY_SIZE;
uint8_t buf[KISS_MAX_FRAME_SIZE];
int encrypted_len = mesh::Utils::encryptThenMAC(key, buf, plaintext, plaintext_len);
if (encrypted_len > 0) {
writeHardwareFrame(HW_RESP(HW_CMD_ENCRYPT_DATA), buf, encrypted_len);
} else {
writeHardwareError(HW_ERR_ENCRYPT_FAILED);
}
}
void KissModem::handleDecryptData(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE + CIPHER_MAC_SIZE + 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
const uint8_t* key = data;
const uint8_t* ciphertext = data + PUB_KEY_SIZE;
uint16_t ciphertext_len = len - PUB_KEY_SIZE;
uint8_t buf[KISS_MAX_FRAME_SIZE];
int decrypted_len = mesh::Utils::MACThenDecrypt(key, buf, ciphertext, ciphertext_len);
if (decrypted_len > 0) {
writeHardwareFrame(HW_RESP(HW_CMD_DECRYPT_DATA), buf, decrypted_len);
} else {
writeHardwareError(HW_ERR_MAC_FAILED);
}
}
void KissModem::handleKeyExchange(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t shared_secret[PUB_KEY_SIZE];
_identity.calcSharedSecret(shared_secret, data);
writeHardwareFrame(HW_RESP(HW_CMD_KEY_EXCHANGE), shared_secret, PUB_KEY_SIZE);
}
void KissModem::handleHash(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t hash[32];
mesh::Utils::sha256(hash, 32, data, len);
writeHardwareFrame(HW_RESP(HW_CMD_HASH), hash, 32);
}
void KissModem::handleSetRadio(const uint8_t* data, uint16_t len) {
if (len < 10) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
if (!_setRadioCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
memcpy(&_config.freq_hz, data, 4);
memcpy(&_config.bw_hz, data + 4, 4);
_config.sf = data[8];
_config.cr = data[9];
_setRadioCallback(_config.freq_hz / 1000000.0f, _config.bw_hz / 1000.0f, _config.sf, _config.cr);
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
}
void KissModem::handleSetTxPower(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
if (!_setTxPowerCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
_config.tx_power = data[0];
_setTxPowerCallback(data[0]);
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
}
void KissModem::handleGetRadio() {
uint8_t buf[10];
memcpy(buf, &_config.freq_hz, 4);
memcpy(buf + 4, &_config.bw_hz, 4);
buf[8] = _config.sf;
buf[9] = _config.cr;
writeHardwareFrame(HW_RESP(HW_CMD_GET_RADIO), buf, 10);
}
void KissModem::handleGetTxPower() {
writeHardwareFrame(HW_RESP(HW_CMD_GET_TX_POWER), &_config.tx_power, 1);
}
void KissModem::handleGetVersion() {
uint8_t buf[2];
buf[0] = KISS_FIRMWARE_VERSION;
buf[1] = 0;
writeHardwareFrame(HW_RESP(HW_CMD_GET_VERSION), buf, 2);
}
void KissModem::handleGetCurrentRssi() {
if (!_getCurrentRssiCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
float rssi = _getCurrentRssiCallback();
int8_t rssi_byte = (int8_t)rssi;
writeHardwareFrame(HW_RESP(HW_CMD_GET_CURRENT_RSSI), (uint8_t*)&rssi_byte, 1);
}
void KissModem::handleIsChannelBusy() {
uint8_t busy = _radio.isReceiving() ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_IS_CHANNEL_BUSY), &busy, 1);
}
void KissModem::handleGetAirtime(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t packet_len = data[0];
uint32_t airtime = _radio.getEstAirtimeFor(packet_len);
writeHardwareFrame(HW_RESP(HW_CMD_GET_AIRTIME), (uint8_t*)&airtime, 4);
}
void KissModem::handleGetNoiseFloor() {
int16_t noise_floor = _radio.getNoiseFloor();
writeHardwareFrame(HW_RESP(HW_CMD_GET_NOISE_FLOOR), (uint8_t*)&noise_floor, 2);
}
void KissModem::handleGetStats() {
if (!_getStatsCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
uint32_t rx, tx, errors;
_getStatsCallback(&rx, &tx, &errors);
uint8_t buf[12];
memcpy(buf, &rx, 4);
memcpy(buf + 4, &tx, 4);
memcpy(buf + 8, &errors, 4);
writeHardwareFrame(HW_RESP(HW_CMD_GET_STATS), buf, 12);
}
void KissModem::handleGetBattery() {
uint16_t mv = _board.getBattMilliVolts();
writeHardwareFrame(HW_RESP(HW_CMD_GET_BATTERY), (uint8_t*)&mv, 2);
}
void KissModem::handlePing() {
writeHardwareFrame(HW_RESP(HW_CMD_PING), nullptr, 0);
}
void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t permissions = data[0];
CayenneLPP telemetry(255);
if (_sensors.querySensors(permissions, telemetry)) {
writeHardwareFrame(HW_RESP(HW_CMD_GET_SENSORS), telemetry.getBuffer(), telemetry.getSize());
} else {
writeHardwareFrame(HW_RESP(HW_CMD_GET_SENSORS), nullptr, 0);
}
}
void KissModem::handleGetMCUTemp() {
float temp = _board.getMCUTemperature();
if (isnan(temp)) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
int16_t temp_tenths = (int16_t)(temp * 10.0f);
writeHardwareFrame(HW_RESP(HW_CMD_GET_MCU_TEMP), (uint8_t*)&temp_tenths, 2);
}
void KissModem::handleReboot() {
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
_serial.flush();
delay(50);
_board.reboot();
}
void KissModem::handleGetDeviceName() {
const char* name = _board.getManufacturerName();
writeHardwareFrame(HW_RESP(HW_CMD_GET_DEVICE_NAME), (const uint8_t*)name, strlen(name));
}
void KissModem::handleSetSignalReport(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
_signal_report_enabled = (data[0] != 0x00);
uint8_t val = _signal_report_enabled ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_GET_SIGNAL_REPORT), &val, 1);
}
void KissModem::handleGetSignalReport() {
uint8_t val = _signal_report_enabled ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_GET_SIGNAL_REPORT), &val, 1);
}

View File

@@ -1,183 +0,0 @@
#pragma once
#include <Arduino.h>
#include <Identity.h>
#include <Utils.h>
#include <Mesh.h>
#include <helpers/SensorManager.h>
#define KISS_FEND 0xC0
#define KISS_FESC 0xDB
#define KISS_TFEND 0xDC
#define KISS_TFESC 0xDD
#define KISS_MAX_FRAME_SIZE 512
#define KISS_MAX_PACKET_SIZE 255
#define KISS_CMD_DATA 0x00
#define KISS_CMD_TXDELAY 0x01
#define KISS_CMD_PERSISTENCE 0x02
#define KISS_CMD_SLOTTIME 0x03
#define KISS_CMD_TXTAIL 0x04
#define KISS_CMD_FULLDUPLEX 0x05
#define KISS_CMD_SETHARDWARE 0x06
#define KISS_CMD_RETURN 0xFF
#define KISS_DEFAULT_TXDELAY 50
#define KISS_DEFAULT_PERSISTENCE 63
#define KISS_DEFAULT_SLOTTIME 10
#define HW_CMD_GET_IDENTITY 0x01
#define HW_CMD_GET_RANDOM 0x02
#define HW_CMD_VERIFY_SIGNATURE 0x03
#define HW_CMD_SIGN_DATA 0x04
#define HW_CMD_ENCRYPT_DATA 0x05
#define HW_CMD_DECRYPT_DATA 0x06
#define HW_CMD_KEY_EXCHANGE 0x07
#define HW_CMD_HASH 0x08
#define HW_CMD_SET_RADIO 0x09
#define HW_CMD_SET_TX_POWER 0x0A
#define HW_CMD_GET_RADIO 0x0B
#define HW_CMD_GET_TX_POWER 0x0C
#define HW_CMD_GET_CURRENT_RSSI 0x0D
#define HW_CMD_IS_CHANNEL_BUSY 0x0E
#define HW_CMD_GET_AIRTIME 0x0F
#define HW_CMD_GET_NOISE_FLOOR 0x10
#define HW_CMD_GET_VERSION 0x11
#define HW_CMD_GET_STATS 0x12
#define HW_CMD_GET_BATTERY 0x13
#define HW_CMD_GET_MCU_TEMP 0x14
#define HW_CMD_GET_SENSORS 0x15
#define HW_CMD_GET_DEVICE_NAME 0x16
#define HW_CMD_PING 0x17
#define HW_CMD_REBOOT 0x18
#define HW_CMD_SET_SIGNAL_REPORT 0x19
#define HW_CMD_GET_SIGNAL_REPORT 0x1A
/* Response code = command code | 0x80. Generic / unsolicited use 0xF0+. */
#define HW_RESP(cmd) ((cmd) | 0x80)
/* Generic responses (shared by multiple commands) */
#define HW_RESP_OK 0xF0
#define HW_RESP_ERROR 0xF1
/* Unsolicited notifications (no corresponding request) */
#define HW_RESP_TX_DONE 0xF8
#define HW_RESP_RX_META 0xF9
#define HW_ERR_INVALID_LENGTH 0x01
#define HW_ERR_INVALID_PARAM 0x02
#define HW_ERR_NO_CALLBACK 0x03
#define HW_ERR_MAC_FAILED 0x04
#define HW_ERR_UNKNOWN_CMD 0x05
#define HW_ERR_ENCRYPT_FAILED 0x06
#define KISS_FIRMWARE_VERSION 1
typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr);
typedef void (*SetTxPowerCallback)(uint8_t power);
typedef float (*GetCurrentRssiCallback)();
typedef void (*GetStatsCallback)(uint32_t* rx, uint32_t* tx, uint32_t* errors);
struct RadioConfig {
uint32_t freq_hz;
uint32_t bw_hz;
uint8_t sf;
uint8_t cr;
uint8_t tx_power;
};
enum TxState {
TX_IDLE,
TX_WAIT_CLEAR,
TX_SLOT_WAIT,
TX_DELAY,
TX_SENDING
};
class KissModem {
Stream& _serial;
mesh::LocalIdentity& _identity;
mesh::RNG& _rng;
mesh::Radio& _radio;
mesh::MainBoard& _board;
SensorManager& _sensors;
uint8_t _rx_buf[KISS_MAX_FRAME_SIZE];
uint16_t _rx_len;
bool _rx_escaped;
bool _rx_active;
uint8_t _pending_tx[KISS_MAX_PACKET_SIZE];
uint16_t _pending_tx_len;
bool _has_pending_tx;
uint8_t _txdelay;
uint8_t _persistence;
uint8_t _slottime;
uint8_t _txtail;
uint8_t _fullduplex;
TxState _tx_state;
uint32_t _tx_timer;
SetRadioCallback _setRadioCallback;
SetTxPowerCallback _setTxPowerCallback;
GetCurrentRssiCallback _getCurrentRssiCallback;
GetStatsCallback _getStatsCallback;
RadioConfig _config;
bool _signal_report_enabled;
void writeByte(uint8_t b);
void writeFrame(uint8_t type, const uint8_t* data, uint16_t len);
void writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
void writeHardwareError(uint8_t error_code);
void processFrame();
void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
void processTx();
void handleGetIdentity();
void handleGetRandom(const uint8_t* data, uint16_t len);
void handleVerifySignature(const uint8_t* data, uint16_t len);
void handleSignData(const uint8_t* data, uint16_t len);
void handleEncryptData(const uint8_t* data, uint16_t len);
void handleDecryptData(const uint8_t* data, uint16_t len);
void handleKeyExchange(const uint8_t* data, uint16_t len);
void handleHash(const uint8_t* data, uint16_t len);
void handleSetRadio(const uint8_t* data, uint16_t len);
void handleSetTxPower(const uint8_t* data, uint16_t len);
void handleGetRadio();
void handleGetTxPower();
void handleGetVersion();
void handleGetCurrentRssi();
void handleIsChannelBusy();
void handleGetAirtime(const uint8_t* data, uint16_t len);
void handleGetNoiseFloor();
void handleGetStats();
void handleGetBattery();
void handlePing();
void handleGetSensors(const uint8_t* data, uint16_t len);
void handleGetMCUTemp();
void handleReboot();
void handleGetDeviceName();
void handleSetSignalReport(const uint8_t* data, uint16_t len);
void handleGetSignalReport();
public:
KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng,
mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors);
void begin();
void loop();
void setRadioCallback(SetRadioCallback cb) { _setRadioCallback = cb; }
void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; }
void setGetCurrentRssiCallback(GetCurrentRssiCallback cb) { _getCurrentRssiCallback = cb; }
void setGetStatsCallback(GetStatsCallback cb) { _getStatsCallback = cb; }
void onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len);
bool isTxBusy() const { return _tx_state != TX_IDLE; }
/** True only when radio is actually transmitting; use to skip recvRaw in main loop. */
bool isActuallyTransmitting() const { return _tx_state == TX_SENDING; }
};

View File

@@ -1,146 +0,0 @@
#include <Arduino.h>
#include <target.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/IdentityStore.h>
#include "KissModem.h"
#if defined(NRF52_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#if defined(KISS_UART_RX) && defined(KISS_UART_TX)
#include <HardwareSerial.h>
#endif
#define NOISE_FLOOR_CALIB_INTERVAL_MS 2000
#define AGC_RESET_INTERVAL_MS 30000
StdRNG rng;
mesh::LocalIdentity identity;
KissModem* modem;
static uint32_t next_noise_floor_calib_ms = 0;
static uint32_t next_agc_reset_ms = 0;
void halt() {
while (1) ;
}
void loadOrCreateIdentity() {
#if defined(NRF52_PLATFORM)
InternalFS.begin();
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
SPIFFS.begin(true);
IdentityStore store(SPIFFS, "/identity");
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
IdentityStore store(LittleFS, "/identity");
store.begin();
#else
#error "Filesystem not defined"
#endif
if (!store.load("_main", identity)) {
identity = radio_new_identity();
while (identity.pub_key[0] == 0x00 || identity.pub_key[0] == 0xFF) {
identity = radio_new_identity();
}
store.save("_main", identity);
}
}
void onSetRadio(float freq, float bw, uint8_t sf, uint8_t cr) {
radio_set_params(freq, bw, sf, cr);
}
void onSetTxPower(uint8_t power) {
radio_set_tx_power(power);
}
float onGetCurrentRssi() {
return radio_driver.getCurrentRSSI();
}
void onGetStats(uint32_t* rx, uint32_t* tx, uint32_t* errors) {
*rx = radio_driver.getPacketsRecv();
*tx = radio_driver.getPacketsSent();
*errors = radio_driver.getPacketsRecvErrors();
}
void setup() {
board.begin();
if (!radio_init()) {
halt();
}
radio_driver.begin();
rng.begin(radio_get_rng_seed());
loadOrCreateIdentity();
sensors.begin();
#if defined(KISS_UART_RX) && defined(KISS_UART_TX)
#if defined(ESP32)
Serial1.setPins(KISS_UART_RX, KISS_UART_TX);
Serial1.begin(115200);
#elif defined(NRF52_PLATFORM)
((Uart *)&Serial1)->setPins(KISS_UART_RX, KISS_UART_TX);
Serial1.begin(115200);
#elif defined(RP2040_PLATFORM)
((SerialUART *)&Serial1)->setRX(KISS_UART_RX);
((SerialUART *)&Serial1)->setTX(KISS_UART_TX);
Serial1.begin(115200);
#elif defined(STM32_PLATFORM)
((HardwareSerial *)&Serial1)->setRx(KISS_UART_RX);
((HardwareSerial *)&Serial1)->setTx(KISS_UART_TX);
Serial1.begin(115200);
#else
#error "KISS UART not supported on this platform"
#endif
modem = new KissModem(Serial1, identity, rng, radio_driver, board, sensors);
#else
Serial.begin(115200);
uint32_t start = millis();
while (!Serial && millis() - start < 3000) delay(10);
delay(100);
modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors);
#endif
modem->setRadioCallback(onSetRadio);
modem->setTxPowerCallback(onSetTxPower);
modem->setGetCurrentRssiCallback(onGetCurrentRssi);
modem->setGetStatsCallback(onGetStats);
modem->begin();
}
void loop() {
modem->loop();
if (!modem->isActuallyTransmitting()) {
if (!modem->isTxBusy()) {
if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) {
radio_driver.resetAGC();
next_agc_reset_ms = millis();
}
}
uint8_t rx_buf[256];
int rx_len = radio_driver.recvRaw(rx_buf, sizeof(rx_buf));
if (rx_len > 0) {
int8_t snr = (int8_t)(radio_driver.getLastSNR() * 4);
int8_t rssi = (int8_t)radio_driver.getLastRSSI();
modem->onPacketReceived(snr, rssi, rx_buf, rx_len);
}
}
if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) {
radio_driver.triggerNoiseFloorCalibrate(0);
next_noise_floor_calib_ms = millis();
}
radio_driver.loop();
}

View File

@@ -129,7 +129,7 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
} }
if (is_flood) { if (is_flood) {
client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path client->out_path_len = -1; // need to rediscover out_path
} }
uint32_t now = getRTCClock()->getCurrentTimeUnique(); uint32_t now = getRTCClock()->getCurrentTimeUnique();
@@ -147,12 +147,9 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path} // request data has: {reply-path-len}{reply-path}
reply_path_len = *data & 63; reply_path_len = *data++ & 0x3F;
reply_path_hash_size = (*data >> 6) + 1; memcpy(reply_path, data, reply_path_len);
data++; // data += reply_path_len;
memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size);
// data += (uint8_t)reply_path_len * reply_path_hash_size;
memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag
uint32_t now = getRTCClock()->getCurrentTime(); uint32_t now = getRTCClock()->getCurrentTime();
@@ -166,12 +163,9 @@ uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t send
uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path} // request data has: {reply-path-len}{reply-path}
reply_path_len = *data & 63; reply_path_len = *data++ & 0x3F;
reply_path_hash_size = (*data >> 6) + 1; memcpy(reply_path, data, reply_path_len);
data++; // data += reply_path_len;
memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size);
// data += (uint8_t)reply_path_len * reply_path_hash_size;
memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag
uint32_t now = getRTCClock()->getCurrentTime(); uint32_t now = getRTCClock()->getCurrentTime();
@@ -186,12 +180,9 @@ uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender
uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path} // request data has: {reply-path-len}{reply-path}
reply_path_len = *data & 63; reply_path_len = *data++ & 0x3F;
reply_path_hash_size = (*data >> 6) + 1; memcpy(reply_path, data, reply_path_len);
data++; // data += reply_path_len;
memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size);
// data += (uint8_t)reply_path_len * reply_path_hash_size;
memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag
uint32_t now = getRTCClock()->getCurrentTime(); uint32_t now = getRTCClock()->getCurrentTime();
@@ -301,7 +292,6 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
// create copy of neighbours list, skipping empty entries so we can sort it separately from main list // create copy of neighbours list, skipping empty entries so we can sort it separately from main list
int16_t neighbours_count = 0; int16_t neighbours_count = 0;
#if MAX_NEIGHBOURS
NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS]; NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS];
for (int i = 0; i < MAX_NEIGHBOURS; i++) { for (int i = 0; i < MAX_NEIGHBOURS; i++) {
auto neighbour = &neighbours[i]; auto neighbour = &neighbours[i];
@@ -337,7 +327,6 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
return a->snr < b->snr; // asc return a->snr < b->snr; // asc
}); });
} }
#endif
// build results buffer // build results buffer
int results_count = 0; int results_count = 0;
@@ -352,7 +341,6 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
break; break;
} }
#if MAX_NEIGHBOURS
// add next neighbour to results // add next neighbour to results
auto neighbour = sorted_neighbours[index + offset]; auto neighbour = sorted_neighbours[index + offset];
uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp; uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
@@ -360,7 +348,6 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
memcpy(&results_buffer[results_offset], &heard_seconds_ago, 4); results_offset += 4; memcpy(&results_buffer[results_offset], &heard_seconds_ago, 4); results_offset += 4;
memcpy(&results_buffer[results_offset], &neighbour->snr, 1); results_offset += 1; memcpy(&results_buffer[results_offset], &neighbour->snr, 1); results_offset += 1;
results_count++; results_count++;
#endif
} }
@@ -398,7 +385,7 @@ File MyMesh::openAppend(const char *fname) {
bool MyMesh::allowPacketForward(const mesh::Packet *packet) { bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false; if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
if (packet->isRouteFlood() && recv_pkt_region == NULL) { if (packet->isRouteFlood() && recv_pkt_region == NULL) {
MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet");
return false; return false;
@@ -501,11 +488,11 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
} }
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1); return getRNG()->nextInt(0, 5*t + 1);
} }
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1); return getRNG()->nextInt(0, 5*t + 1);
} }
@@ -514,10 +501,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
} else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
if ((pkt->getPayloadType() == PAYLOAD_TYPE_GRP_TXT || if (region_map.getWildcard().flags & REGION_DENY_FLOOD) {
pkt->getPayloadType() == PAYLOAD_TYPE_GRP_DATA ||
pkt->getPayloadType() == PAYLOAD_TYPE_ADVERT) &&
region_map.getWildcard().flags & REGION_DENY_FLOOD) {
recv_pkt_region = NULL; recv_pkt_region = NULL;
} else { } else {
recv_pkt_region = &region_map.getWildcard(); recv_pkt_region = &region_map.getWildcard();
@@ -558,14 +542,13 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else if (reply_path_len < 0) { } else if (reply_path_len < 0) {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
} else { } else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
uint8_t path_len = ((reply_path_hash_size - 1) << 6) | (reply_path_len & 63); if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY);
if (reply) sendDirect(reply, reply_path, path_len, SERVER_RESPONSE_DELAY);
} }
} }
} }
@@ -634,15 +617,15 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len, mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else { } else {
mesh::Packet *reply = mesh::Packet *reply =
createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
if (reply) { if (reply) {
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else { } else {
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); sendFlood(reply, SERVER_RESPONSE_DELAY);
} }
} }
} }
@@ -672,8 +655,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
mesh::Packet *ack = createAck(ack_hash); mesh::Packet *ack = createAck(ack_hash);
if (ack) { if (ack) {
if (client->out_path_len == OUT_PATH_UNKNOWN) { if (client->out_path_len < 0) {
sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize()); sendFlood(ack, TXT_ACK_DELAY);
} else { } else {
sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
} }
@@ -700,8 +683,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
if (reply) { if (reply) {
if (client->out_path_len == OUT_PATH_UNKNOWN) { if (client->out_path_len < 0) {
sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
} else { } else {
sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS);
} }
@@ -722,8 +705,7 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len); MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len);
auto client = acl.getClientByIdx(i); auto client = acl.getClientByIdx(i);
// store a copy of path, for sendDirect() memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len);
client->last_activity = getRTCClock()->getCurrentTime(); client->last_activity = getRTCClock()->getCurrentTime();
} else { } else {
MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
@@ -764,47 +746,6 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) {
sendZeroHop(resp, getRetransmitDelay(resp)*4); // apply random delay (widened x4), as multiple nodes can respond to this sendZeroHop(resp, getRetransmitDelay(resp)*4); // apply random delay (widened x4), as multiple nodes can respond to this
} }
} }
} else if (type == CTL_TYPE_NODE_DISCOVER_RESP && packet->payload_len >= 6) {
uint8_t node_type = packet->payload[0] & 0x0F;
if (node_type != ADV_TYPE_REPEATER) {
return;
}
if (packet->payload_len < 6 + PUB_KEY_SIZE) {
MESH_DEBUG_PRINTLN("onControlDataRecv: DISCOVER_RESP pubkey too short: %d", (uint32_t)packet->payload_len);
return;
}
if (pending_discover_tag == 0 || millisHasNowPassed(pending_discover_until)) {
pending_discover_tag = 0;
return;
}
uint32_t tag;
memcpy(&tag, &packet->payload[2], 4);
if (tag != pending_discover_tag) {
return;
}
mesh::Identity id(&packet->payload[6]);
if (id.matches(self_id)) {
return;
}
putNeighbour(id, rtc_clock.getCurrentTime(), packet->getSNR());
}
}
void MyMesh::sendNodeDiscoverReq() {
uint8_t data[10];
data[0] = CTL_TYPE_NODE_DISCOVER_REQ; // prefix_only=0
data[1] = (1 << ADV_TYPE_REPEATER);
getRNG()->random(&data[2], 4); // tag
memcpy(&pending_discover_tag, &data[2], 4);
pending_discover_until = futureMillis(60000);
uint32_t since = 0;
memcpy(&data[6], &since, 4);
auto pkt = createControlData(data, sizeof(data));
if (pkt) {
sendZeroHop(pkt);
} }
} }
@@ -838,7 +779,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.airtime_factor = 1.0; _prefs.airtime_factor = 1.0;
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
_prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.tx_delay_factor = 0.5f; // was 0.25f
_prefs.direct_tx_delay_factor = 0.3f; // was 0.2 _prefs.direct_tx_delay_factor = 0.2f; // was zero
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
_prefs.node_lat = ADVERT_LAT; _prefs.node_lat = ADVERT_LAT;
_prefs.node_lon = ADVERT_LON; _prefs.node_lon = ADVERT_LON;
@@ -849,7 +790,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.cr = LORA_CR; _prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER; _prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs _prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 0; // 12 hours _prefs.flood_advert_interval = 12; // 12 hours
_prefs.flood_advert_base = 0.308f; _prefs.flood_advert_base = 0.308f;
_prefs.flood_max = 64; _prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled _prefs.interference_threshold = 0; // disabled
@@ -869,9 +810,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.advert_loc_policy = ADVERT_LOC_PREFS; _prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier _prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier
pending_discover_tag = 0;
pending_discover_until = 0;
} }
void MyMesh::begin(FILESYSTEM *fs) { void MyMesh::begin(FILESYSTEM *fs) {
@@ -929,7 +867,7 @@ void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet *pkt = createSelfAdvert(); mesh::Packet *pkt = createSelfAdvert();
if (pkt) { if (pkt) {
if (flood) { if (flood) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); sendFlood(pkt, delay_millis);
} else { } else {
sendZeroHop(pkt, delay_millis); sendZeroHop(pkt, delay_millis);
} }
@@ -970,7 +908,7 @@ void MyMesh::dumpLogFile() {
} }
} }
void MyMesh::setTxPower(int8_t power_dbm) { void MyMesh::setTxPower(uint8_t power_dbm) {
radio_set_tx_power(power_dbm); radio_set_tx_power(power_dbm);
} }
@@ -1239,15 +1177,6 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
} else { } else {
strcpy(reply, "Err - ??"); strcpy(reply, "Err - ??");
} }
} else if (memcmp(command, "discover.neighbors", 18) == 0) {
const char* sub = command + 18;
while (*sub == ' ') sub++;
if (*sub != 0) {
strcpy(reply, "Err - discover.neighbors has no options");
} else {
sendNodeDiscoverReq();
strcpy(reply, "OK - Discover sent");
}
} else{ } else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
} }
@@ -1299,8 +1228,5 @@ void MyMesh::loop() {
// To check if there is pending work // To check if there is pending work
bool MyMesh::hasPendingWork() const { bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep
#endif
return _mgr->getOutboundCount(0xFFFFFFFF) > 0; return _mgr->getOutboundCount(0xFFFFFFFF) > 0;
} }

View File

@@ -69,11 +69,11 @@ struct NeighbourInfo {
}; };
#ifndef FIRMWARE_BUILD_DATE #ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "15 Feb 2026" #define FIRMWARE_BUILD_DATE "29 Jan 2026"
#endif #endif
#ifndef FIRMWARE_VERSION #ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.13.0" #define FIRMWARE_VERSION "v1.12.0"
#endif #endif
#define FIRMWARE_ROLE "repeater" #define FIRMWARE_ROLE "repeater"
@@ -92,14 +92,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
uint8_t reply_data[MAX_PACKET_PAYLOAD]; uint8_t reply_data[MAX_PACKET_PAYLOAD];
uint8_t reply_path[MAX_PATH_SIZE]; uint8_t reply_path[MAX_PATH_SIZE];
int8_t reply_path_len; int8_t reply_path_len;
uint8_t reply_path_hash_size;
TransportKeyStore key_store; TransportKeyStore key_store;
RegionMap region_map, temp_map; RegionMap region_map, temp_map;
RegionEntry* load_stack[8]; RegionEntry* load_stack[8];
RegionEntry* recv_pkt_region; RegionEntry* recv_pkt_region;
RateLimiter discover_limiter, anon_limiter; RateLimiter discover_limiter, anon_limiter;
uint32_t pending_discover_tag;
unsigned long pending_discover_until;
bool region_load_active; bool region_load_active;
unsigned long dirty_contacts_expiry; unsigned long dirty_contacts_expiry;
#if MAX_NEIGHBOURS #if MAX_NEIGHBOURS
@@ -119,7 +116,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
#endif #endif
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
void sendNodeDiscoverReq();
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
@@ -202,7 +198,7 @@ public:
} }
void dumpLogFile() override; void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override; void setTxPower(uint8_t power_dbm) override;
void formatNeighborsReply(char *reply) override; void formatNeighborsReply(char *reply) override;
void removeNeighbor(const uint8_t* pubkey, int key_len) override; void removeNeighbor(const uint8_t* pubkey, int key_len) override;
void formatStatsReply(char *reply) override; void formatStatsReply(char *reply) override;

View File

@@ -29,12 +29,6 @@ void setup() {
board.begin(); board.begin();
#if defined(MESH_DEBUG) && defined(NRF52_PLATFORM)
// give some extra time for serial to settle so
// boot debug messages can be seen on terminal
delay(5000);
#endif
// For power saving // For power saving
lastActive = millis(); // mark last active time since boot lastActive = millis(); // mark last active time since boot
@@ -48,7 +42,6 @@ void setup() {
#endif #endif
if (!radio_init()) { if (!radio_init()) {
MESH_DEBUG_PRINTLN("Radio init failed!");
halt(); halt();
} }
@@ -134,17 +127,14 @@ void loop() {
#endif #endif
rtc_clock.tick(); rtc_clock.tick();
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { if (the_mesh.getNodePrefs()->powersaving_enabled && // To check if power saving is enabled
#if defined(NRF52_PLATFORM) the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep
board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible if (!the_mesh.hasPendingWork()) { // No pending work. Safe to sleep
#else
if (the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep
board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet
lastActive = millis(); lastActive = millis();
nextSleepinSecs = 5; // Default: To work for 5s and sleep again nextSleepinSecs = 5; // Default: To work for 5s and sleep again
} else { } else {
nextSleepinSecs += 5; // When there is pending work, to work another 5s nextSleepinSecs += 5; // When there is pending work, to work another 5s
} }
#endif
} }
} }

View File

@@ -73,15 +73,13 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) {
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len); auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len);
if (reply) { if (reply) {
if (client->out_path_len == OUT_PATH_UNKNOWN) { if (client->out_path_len < 0) {
unsigned long delay_millis = 0; sendFlood(reply);
sendFlood(reply, delay_millis, _prefs.path_hash_mode + 1);
client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD); client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD);
} else { } else {
sendDirect(reply, client->out_path, client->out_path_len); sendDirect(reply, client->out_path, client->out_path_len);
client->extra.room.ack_timeout =
uint8_t path_hash_count = client->out_path_len & 63; futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (client->out_path_len + 1));
client->extra.room.ack_timeout = futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (path_hash_count + 1));
} }
_num_post_pushes++; // stats _num_post_pushes++; // stats
} else { } else {
@@ -266,17 +264,17 @@ const char *MyMesh::getLogDateTime() {
} }
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1); return getRNG()->nextInt(0, 5*t + 1);
} }
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1); return getRNG()->nextInt(0, 5*t + 1);
} }
bool MyMesh::allowPacketForward(const mesh::Packet *packet) { bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false; if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
// Limit flood advert paket forwarding using a probabilistic reduction defined by P(h) = 0.308^(hops-1) // Limit flood advert paket forwarding using a probabilistic reduction defined by P(h) = 0.308^(hops-1)
// https://github.com/meshcore-dev/MeshCore/issues/1223 // https://github.com/meshcore-dev/MeshCore/issues/1223
@@ -344,7 +342,7 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
} }
if (packet->isRouteFlood()) { if (packet->isRouteFlood()) {
client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path client->out_path_len = -1; // need to rediscover out_path
} }
uint32_t now = getRTCClock()->getCurrentTimeUnique(); uint32_t now = getRTCClock()->getCurrentTimeUnique();
@@ -364,14 +362,14 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet *path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len, mesh::Packet *path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, 13); PAYLOAD_TYPE_RESPONSE, reply_data, 13);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else { } else {
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13); mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13);
if (reply) { if (reply) {
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else { } else {
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); sendFlood(reply, SERVER_RESPONSE_DELAY);
} }
} }
} }
@@ -459,9 +457,9 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
uint32_t delay_millis; uint32_t delay_millis;
if (send_ack) { if (send_ack) {
if (client->out_path_len == OUT_PATH_UNKNOWN) { if (client->out_path_len < 0) {
mesh::Packet *ack = createAck(ack_hash); mesh::Packet *ack = createAck(ack_hash);
if (ack) sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize()); if (ack) sendFlood(ack, TXT_ACK_DELAY);
delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS; delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
} else { } else {
uint32_t d = TXT_ACK_DELAY; uint32_t d = TXT_ACK_DELAY;
@@ -493,8 +491,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
if (reply) { if (reply) {
if (client->out_path_len == OUT_PATH_UNKNOWN) { if (client->out_path_len < 0) {
sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize()); sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY);
} else { } else {
sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY); sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY);
} }
@@ -532,7 +530,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
// if client sends too quickly, evict() // if client sends too quickly, evict()
// RULE: only send keep_alive response DIRECT! // RULE: only send keep_alive response DIRECT!
if (client->out_path_len != OUT_PATH_UNKNOWN) { if (client->out_path_len >= 0) {
uint32_t ack_hash; // calc ACK to prove to sender that we got request uint32_t ack_hash; // calc ACK to prove to sender that we got request
mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 9, client->id.pub_key, PUB_KEY_SIZE); mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 9, client->id.pub_key, PUB_KEY_SIZE);
@@ -549,14 +547,14 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len, mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else { } else {
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
if (reply) { if (reply) {
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else { } else {
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); sendFlood(reply, SERVER_RESPONSE_DELAY);
} }
} }
} }
@@ -574,7 +572,7 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
if (i >= 0 && i < acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context) if (i >= 0 && i < acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context)
MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len); MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len);
auto client = acl.getClientByIdx(i); auto client = acl.getClientByIdx(i);
client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); // store a copy of path, for sendDirect() memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
client->last_activity = getRTCClock()->getCurrentTime(); client->last_activity = getRTCClock()->getCurrentTime();
} else { } else {
MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
@@ -623,7 +621,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.tx_power_dbm = LORA_TX_POWER; _prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.disable_fwd = 1; _prefs.disable_fwd = 1;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs _prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 0; // 12 hours _prefs.flood_advert_interval = 12; // 12 hours
_prefs.flood_advert_base = 0.308f; _prefs.flood_advert_base = 0.308f;
_prefs.flood_max = 64; _prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled _prefs.interference_threshold = 0; // disabled
@@ -691,7 +689,7 @@ void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet *pkt = createSelfAdvert(); mesh::Packet *pkt = createSelfAdvert();
if (pkt) { if (pkt) {
if (flood) { if (flood) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); sendFlood(pkt, delay_millis);
} else { } else {
sendZeroHop(pkt, delay_millis); sendZeroHop(pkt, delay_millis);
} }
@@ -731,7 +729,7 @@ void MyMesh::dumpLogFile() {
} }
} }
void MyMesh::setTxPower(int8_t power_dbm) { void MyMesh::setTxPower(uint8_t power_dbm) {
radio_set_tx_power(power_dbm); radio_set_tx_power(power_dbm);
} }

View File

@@ -26,11 +26,11 @@
/* ------------------------------ Config -------------------------------- */ /* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE #ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "15 Feb 2026" #define FIRMWARE_BUILD_DATE "29 Jan 2026"
#endif #endif
#ifndef FIRMWARE_VERSION #ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.13.0" #define FIRMWARE_VERSION "v1.12.0"
#endif #endif
#ifndef LORA_FREQ #ifndef LORA_FREQ
@@ -188,7 +188,7 @@ public:
} }
void dumpLogFile() override; void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override; void setTxPower(uint8_t power_dbm) override;
void formatNeighborsReply(char *reply) override { void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported"); strcpy(reply, "not supported");

View File

@@ -66,7 +66,7 @@ struct NodePrefs { // persisted to file
char node_name[32]; char node_name[32];
double node_lat, node_lon; double node_lat, node_lon;
float freq; float freq;
int8_t tx_power_dbm; uint8_t tx_power_dbm;
uint8_t unused[3]; uint8_t unused[3];
}; };
@@ -213,7 +213,7 @@ protected:
} }
void onContactPathUpdated(const ContactInfo& contact) override { void onContactPathUpdated(const ContactInfo& contact) override {
Serial.printf("PATH to: %s, path_len=%d\n", contact.name, (uint32_t) contact.out_path_len); Serial.printf("PATH to: %s, path_len=%d\n", contact.name, (int32_t) contact.out_path_len);
saveContacts(); saveContacts();
} }
@@ -266,9 +266,8 @@ protected:
return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis); return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis);
} }
uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override { uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override {
uint8_t path_hash_count = path_len & 63;
return SEND_TIMEOUT_BASE_MILLIS + return SEND_TIMEOUT_BASE_MILLIS +
( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_hash_count + 1)); ( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_len + 1));
} }
void onSendTimeout() override { void onSendTimeout() override {
@@ -291,7 +290,7 @@ public:
} }
float getFreqPref() const { return _prefs.freq; } float getFreqPref() const { return _prefs.freq; }
int8_t getTxPowerPref() const { return _prefs.tx_power_dbm; } uint8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }
void begin(FILESYSTEM& fs) { void begin(FILESYSTEM& fs) {
_fs = &fs; _fs = &fs;

View File

@@ -258,11 +258,10 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len); auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len);
if (pkt) { if (pkt) {
if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT if (c->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(pkt, c->out_path, c->out_path_len); sendDirect(pkt, c->out_path, c->out_path_len);
} else { } else {
unsigned long delay_millis = 0; sendFlood(pkt);
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} }
} }
t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS); t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
@@ -303,7 +302,7 @@ float SensorMesh::getAirtimeBudgetFactor() const {
bool SensorMesh::allowPacketForward(const mesh::Packet* packet) { bool SensorMesh::allowPacketForward(const mesh::Packet* packet) {
if (_prefs.disable_fwd) return false; if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
return true; return true;
} }
@@ -313,11 +312,11 @@ int SensorMesh::calcRxDelay(float score, uint32_t air_time) const {
} }
uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) { uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 6)*t; return getRNG()->nextInt(0, 6)*t;
} }
uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) { uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 6)*t; return getRNG()->nextInt(0, 6)*t;
} }
int SensorMesh::getInterferenceThreshold() const { int SensorMesh::getInterferenceThreshold() const {
@@ -361,7 +360,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
} }
if (is_flood) { if (is_flood) {
client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path client->out_path_len = -1; // need to rediscover out_path
} }
uint32_t now = getRTCClock()->getCurrentTimeUnique(); uint32_t now = getRTCClock()->getCurrentTimeUnique();
@@ -469,10 +468,10 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else { } else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
} }
} }
} }
@@ -497,10 +496,10 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
} }
} }
void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) { void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash) {
if (dest.out_path_len == OUT_PATH_UNKNOWN) { if (dest.out_path_len < 0) {
mesh::Packet* ack = createAck(ack_hash); mesh::Packet* ack = createAck(ack_hash);
if (ack) sendFlood(ack, TXT_ACK_DELAY, path_hash_size); if (ack) sendFlood(ack, TXT_ACK_DELAY);
} else { } else {
uint32_t d = TXT_ACK_DELAY; uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) { if (getExtraAckTransmitCount() > 0) {
@@ -538,14 +537,14 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else { } else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len); mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
if (reply) { if (reply) {
if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT if (from->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY); sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY);
} else { } else {
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); sendFlood(reply, SERVER_RESPONSE_DELAY);
} }
} }
} }
@@ -568,9 +567,9 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize()); if (path) sendFlood(path, TXT_ACK_DELAY);
} else { } else {
sendAckTo(*from, ack_hash, packet->getPathHashSize()); sendAckTo(*from, ack_hash);
} }
} }
} else if (flags == TXT_TYPE_CLI_DATA) { } else if (flags == TXT_TYPE_CLI_DATA) {
@@ -597,8 +596,8 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len); auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len);
if (reply) { if (reply) {
if (from->out_path_len == OUT_PATH_UNKNOWN) { if (from->out_path_len < 0) {
sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
} else { } else {
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS); sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
} }
@@ -667,7 +666,7 @@ bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint
MESH_DEBUG_PRINTLN("PATH to contact, path_len=%d", (uint32_t) path_len); MESH_DEBUG_PRINTLN("PATH to contact, path_len=%d", (uint32_t) path_len);
// NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path. // NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?) // FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
from->out_path_len = mesh::Packet::copyPath(from->out_path, path, path_len); // store a copy of path, for sendDirect() memcpy(from->out_path, path, from->out_path_len = path_len); // store a copy of path, for sendDirect()
from->last_activity = getRTCClock()->getCurrentTime(); from->last_activity = getRTCClock()->getCurrentTime();
// REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes?? // REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes??
@@ -792,7 +791,7 @@ void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet* pkt = createSelfAdvert(); mesh::Packet* pkt = createSelfAdvert();
if (pkt) { if (pkt) {
if (flood) { if (flood) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); sendFlood(pkt, delay_millis);
} else { } else {
sendZeroHop(pkt, delay_millis); sendZeroHop(pkt, delay_millis);
} }
@@ -816,7 +815,7 @@ void SensorMesh::updateFloodAdvertTimer() {
} }
} }
void SensorMesh::setTxPower(int8_t power_dbm) { void SensorMesh::setTxPower(uint8_t power_dbm) {
radio_set_tx_power(power_dbm); radio_set_tx_power(power_dbm);
} }
@@ -869,8 +868,7 @@ void SensorMesh::loop() {
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
mesh::Packet* pkt = createSelfAdvert(); mesh::Packet* pkt = createSelfAdvert();
unsigned long delay_millis = 0; if (pkt) sendFlood(pkt);
if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
updateFloodAdvertTimer(); // schedule next flood advert updateFloodAdvertTimer(); // schedule next flood advert
updateAdvertTimer(); // also schedule local advert (so they don't overlap) updateAdvertTimer(); // also schedule local advert (so they don't overlap)

View File

@@ -33,11 +33,11 @@
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts #define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
#ifndef FIRMWARE_BUILD_DATE #ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "15 Feb 2026" #define FIRMWARE_BUILD_DATE "29 Jan 2026"
#endif #endif
#ifndef FIRMWARE_VERSION #ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.13.0" #define FIRMWARE_VERSION "v1.12.0"
#endif #endif
#define FIRMWARE_ROLE "sensor" #define FIRMWARE_ROLE "sensor"
@@ -66,7 +66,7 @@ public:
void setLoggingOn(bool enable) override { } void setLoggingOn(bool enable) override { }
void eraseLogFile() override { } void eraseLogFile() override { }
void dumpLogFile() override { } void dumpLogFile() override { }
void setTxPower(int8_t power_dbm) override; void setTxPower(uint8_t power_dbm) override;
void formatNeighborsReply(char *reply) override { void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported"); strcpy(reply, "not supported");
} }
@@ -128,7 +128,7 @@ protected:
void onControlDataRecv(mesh::Packet* packet) override; void onControlDataRecv(mesh::Packet* packet) override;
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override; void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len); virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len);
void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1); void sendAckTo(const ClientInfo& dest, uint32_t ack_hash);
private: private:
FILESYSTEM* _fs; FILESYSTEM* _fs;
unsigned long next_local_advert, next_flood_advert; unsigned long next_local_advert, next_flood_advert;

8
fetch_prs.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
git branch -D pr-1297
git branch -D pr-1338
# fetch PRs
git fetch upstream pull/1338/head:pr-1338
git fetch upstream pull/1297/head:pr-1297

8
merge_prs.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
git merge pr-1338 --no-edit -m "Integration of upstrem PR #1338"
git merge pr-1297 --no-edit -m "Integration of upstrem PR #1297"
git merge pio-ini-adjustments -m "platformio.ini: Adjust defaults for LoRa frequncies and advert interval limits"

View File

@@ -27,6 +27,7 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-D LORA_FREQ=869.618 -D LORA_FREQ=869.618
-D LORA_BW=62.5 -D LORA_BW=62.5
-D LORA_SF=8 -D LORA_SF=8
-D LORA_CR=8
-D ENABLE_ADVERT_ON_BOOT=1 -D ENABLE_ADVERT_ON_BOOT=1
-D ENABLE_PRIVATE_KEY_IMPORT=1 ; NOTE: comment these out for more secure firmware -D ENABLE_PRIVATE_KEY_IMPORT=1 ; NOTE: comment these out for more secure firmware
-D ENABLE_PRIVATE_KEY_EXPORT=1 -D ENABLE_PRIVATE_KEY_EXPORT=1
@@ -59,7 +60,6 @@ platform = platformio/espressif32@6.11.0
monitor_filters = esp32_exception_decoder monitor_filters = esp32_exception_decoder
extra_scripts = merge-bin.py extra_scripts = merge-bin.py
build_flags = ${arduino_base.build_flags} build_flags = ${arduino_base.build_flags}
-D ESP32_PLATFORM
; -D ESP32_CPU_FREQ=80 ; change it to your need ; -D ESP32_CPU_FREQ=80 ; change it to your need
build_src_filter = ${arduino_base.build_src_filter} build_src_filter = ${arduino_base.build_src_filter}
@@ -69,10 +69,10 @@ lib_deps =
file://arch/esp32/AsyncElegantOTA file://arch/esp32/AsyncElegantOTA
; esp32c6 uses arduino framework 3.x ; esp32c6 uses arduino framework 3.x
; WARNING: experimental. May not work as stable as other platforms. ; WARNING: experimental. pioarduino on esp32c6 needs work - it's not considered stable and has issues.
[esp32c6_base] [esp32c6_base]
extends = esp32_base extends = esp32_base
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13-1/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip
; ----------------- NRF52 --------------------- ; ----------------- NRF52 ---------------------

View File

@@ -8,9 +8,7 @@
namespace mesh { namespace mesh {
#define MAX_RX_DELAY_MILLIS 32000 // 32 seconds #define MAX_RX_DELAY_MILLIS 32000 // 32 seconds
#define MIN_TX_BUDGET_RESERVE_MS 100 // min budget (ms) required before allowing next TX
#define MIN_TX_BUDGET_AIRTIME_DIV 2 // require at least 1/N of estimated airtime as budget before TX
#ifndef NOISE_FLOOR_CALIB_INTERVAL #ifndef NOISE_FLOOR_CALIB_INTERVAL
#define NOISE_FLOOR_CALIB_INTERVAL 2000 // 2 seconds #define NOISE_FLOOR_CALIB_INTERVAL 2000 // 2 seconds
@@ -41,15 +39,15 @@ void Dispatcher::updateTxBudget() {
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor()); float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
unsigned long max_budget = (unsigned long)(getDutyCycleWindowMs() * duty_cycle); unsigned long max_budget = (unsigned long)(getDutyCycleWindowMs() * duty_cycle);
unsigned long refill = (unsigned long)(elapsed * duty_cycle);
if (refill > 0) { unsigned long refill = (unsigned long)(elapsed * duty_cycle);
tx_budget_ms += refill; tx_budget_ms += refill;
if (tx_budget_ms > max_budget) {
tx_budget_ms = max_budget; if (tx_budget_ms > max_budget) {
} tx_budget_ms = max_budget;
last_budget_update = now;
} }
last_budget_update = now;
} }
int Dispatcher::calcRxDelay(float score, uint32_t air_time) const { int Dispatcher::calcRxDelay(float score, uint32_t air_time) const {
@@ -96,16 +94,16 @@ void Dispatcher::loop() {
tx_budget_ms -= t; tx_budget_ms -= t;
} }
if (tx_budget_ms < MIN_TX_BUDGET_RESERVE_MS) { if (tx_budget_ms < 100) {
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor()); float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
unsigned long needed = MIN_TX_BUDGET_RESERVE_MS - tx_budget_ms; unsigned long needed = 100 - tx_budget_ms;
next_tx_time = futureMillis((unsigned long)(needed / duty_cycle)); next_tx_time = futureMillis((unsigned long)(needed / duty_cycle));
} else { } else {
next_tx_time = _ms->getMillis(); next_tx_time = _ms->getMillis();
} }
_radio->onSendFinished(); _radio->onSendFinished();
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); logTx(outbound, 2 + outbound->path_len + outbound->payload_len);
if (outbound->isRouteFlood()) { if (outbound->isRouteFlood()) {
n_sent_flood++; n_sent_flood++;
} else { } else {
@@ -117,7 +115,7 @@ void Dispatcher::loop() {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime());
_radio->onSendFinished(); _radio->onSendFinished();
logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); logTxFail(outbound, 2 + outbound->path_len + outbound->payload_len);
releasePacket(outbound); // return to pool releasePacket(outbound); // return to pool
outbound = NULL; outbound = NULL;
@@ -145,48 +143,6 @@ void Dispatcher::loop() {
checkSend(); checkSend();
} }
bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) {
int i = 0;
pkt->header = raw[i++];
if (pkt->getPayloadVer() > PAYLOAD_VER_1) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime());
return false;
}
if (pkt->hasTransportCodes()) {
memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2;
memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2;
} else {
pkt->transport_codes[0] = pkt->transport_codes[1] = 0;
}
pkt->path_len = raw[i++];
uint8_t path_mode = pkt->path_len >> 6; // upper 2 bits (legacy firmware: 00)
if (path_mode == 3) { // Reserved for future
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported path mode: 3", getLogDateTime());
return false;
}
uint8_t path_byte_len = (pkt->path_len & 63) * pkt->getPathHashSize();
if (path_byte_len > MAX_PATH_SIZE || i + path_byte_len > len) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): partial or corrupt packet received, len=%d", getLogDateTime(), len);
return false;
}
memcpy(pkt->path, &raw[i], path_byte_len); i += path_byte_len;
pkt->payload_len = len - i; // payload is remainder
if (pkt->payload_len > sizeof(pkt->payload)) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): packet payload too big, payload_len=%d", getLogDateTime(), (uint32_t)pkt->payload_len);
return false;
}
memcpy(pkt->payload, &raw[i], pkt->payload_len);
return true; // success
}
void Dispatcher::checkRecv() { void Dispatcher::checkRecv() {
Packet* pkt; Packet* pkt;
float score; float score;
@@ -201,14 +157,45 @@ void Dispatcher::checkRecv() {
if (pkt == NULL) { if (pkt == NULL) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): WARNING: received data, no unused packets available!", getLogDateTime()); MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): WARNING: received data, no unused packets available!", getLogDateTime());
} else { } else {
if (tryParsePacket(pkt, raw, len)) { int i = 0;
pkt->_snr = _radio->getLastSNR() * 4.0f; #ifdef NODE_ID
score = _radio->packetScore(_radio->getLastSNR(), len); uint8_t sender_id = raw[i++];
air_time = _radio->getEstAirtimeFor(len); if (sender_id == NODE_ID - 1 || sender_id == NODE_ID + 1) { // simulate that NODE_ID can only hear NODE_ID-1 or NODE_ID+1, eg. 3 can't hear 1
rx_air_time += air_time;
} else { } else {
_mgr->free(pkt); // put back into pool
return;
}
#endif
pkt->header = raw[i++];
if (pkt->hasTransportCodes()) {
memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2;
memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2;
} else {
pkt->transport_codes[0] = pkt->transport_codes[1] = 0;
}
pkt->path_len = raw[i++];
if (pkt->path_len > MAX_PATH_SIZE || i + pkt->path_len > len) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): partial or corrupt packet received, len=%d", getLogDateTime(), len);
_mgr->free(pkt); // put back into pool _mgr->free(pkt); // put back into pool
pkt = NULL; pkt = NULL;
} else {
memcpy(pkt->path, &raw[i], pkt->path_len); i += pkt->path_len;
pkt->payload_len = len - i; // payload is remainder
if (pkt->payload_len > sizeof(pkt->payload)) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): packet payload too big, payload_len=%d", getLogDateTime(), (uint32_t)pkt->payload_len);
_mgr->free(pkt); // put back into pool
pkt = NULL;
} else {
memcpy(pkt->payload, &raw[i], pkt->payload_len);
pkt->_snr = _radio->getLastSNR() * 4.0f;
score = _radio->packetScore(_radio->getLastSNR(), len);
air_time = _radio->getEstAirtimeFor(len);
rx_air_time += air_time;
}
} }
} }
} else { } else {
@@ -277,9 +264,9 @@ void Dispatcher::checkSend() {
updateTxBudget(); updateTxBudget();
uint32_t est_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT); uint32_t est_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT);
if (tx_budget_ms < est_airtime / MIN_TX_BUDGET_AIRTIME_DIV) { if (tx_budget_ms < est_airtime / 2) {
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor()); float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
unsigned long needed = est_airtime / MIN_TX_BUDGET_AIRTIME_DIV - tx_budget_ms; unsigned long needed = est_airtime / 2 - tx_budget_ms;
next_tx_time = futureMillis((unsigned long)(needed / duty_cycle)); next_tx_time = futureMillis((unsigned long)(needed / duty_cycle));
return; return;
} }
@@ -308,13 +295,16 @@ void Dispatcher::checkSend() {
int len = 0; int len = 0;
uint8_t raw[MAX_TRANS_UNIT]; uint8_t raw[MAX_TRANS_UNIT];
#ifdef NODE_ID
raw[len++] = NODE_ID;
#endif
raw[len++] = outbound->header; raw[len++] = outbound->header;
if (outbound->hasTransportCodes()) { if (outbound->hasTransportCodes()) {
memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2; memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2;
memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2; memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2;
} }
raw[len++] = outbound->path_len; raw[len++] = outbound->path_len;
len += Packet::writePath(&raw[len], outbound->path, outbound->path_len); memcpy(&raw[len], outbound->path, outbound->path_len); len += outbound->path_len;
if (len + outbound->payload_len > MAX_TRANS_UNIT) { if (len + outbound->payload_len > MAX_TRANS_UNIT) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len); MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len);
@@ -368,7 +358,7 @@ void Dispatcher::releasePacket(Packet* packet) {
} }
void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) { if (packet->path_len > MAX_PATH_SIZE || packet->payload_len > MAX_PACKET_PAYLOAD) {
MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len); MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len);
_mgr->free(packet); _mgr->free(packet);
} else { } else {

View File

@@ -193,7 +193,6 @@ public:
unsigned long futureMillis(int millis_from_now) const; unsigned long futureMillis(int millis_from_now) const;
private: private:
bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len);
void checkRecv(); void checkRecv();
void checkSend(); void checkSend();
}; };

View File

@@ -20,10 +20,6 @@ public:
memcpy(dest, pub_key, PATH_HASH_SIZE); // hash is just prefix of pub_key memcpy(dest, pub_key, PATH_HASH_SIZE); // hash is just prefix of pub_key
return PATH_HASH_SIZE; return PATH_HASH_SIZE;
} }
int copyHashTo(uint8_t* dest, uint8_t len) const {
memcpy(dest, pub_key, len); // hash is just prefix of pub_key
return len;
}
bool isHashMatch(const uint8_t* hash) const { bool isHashMatch(const uint8_t* hash) const {
return memcmp(hash, pub_key, PATH_HASH_SIZE) == 0; return memcmp(hash, pub_key, PATH_HASH_SIZE) == 0;
} }

View File

@@ -39,6 +39,11 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int
} }
DispatcherAction Mesh::onRecvPacket(Packet* pkt) { DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (pkt->getPayloadVer() > PAYLOAD_VER_1) { // not supported in this firmware version
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): unsupported packet version", getLogDateTime());
return ACTION_RELEASE;
}
if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) {
if (pkt->path_len < MAX_PATH_SIZE) { if (pkt->path_len < MAX_PATH_SIZE) {
uint8_t i = 0; uint8_t i = 0;
@@ -65,14 +70,14 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
} }
if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) { if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) {
if (pkt->getPathHashCount() == 0) { if (pkt->path_len == 0) {
onControlDataRecv(pkt); onControlDataRecv(pkt);
} }
// just zero-hop control packets allowed (for this subset of payloads) // just zero-hop control packets allowed (for this subset of payloads)
return ACTION_RELEASE; return ACTION_RELEASE;
} }
if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0) { if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) {
// check for 'early received' ACK // check for 'early received' ACK
if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
int i = 0; int i = 0;
@@ -83,7 +88,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
} }
} }
if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) { if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
return forwardMultipartDirect(pkt); return forwardMultipartDirect(pkt);
} else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
@@ -153,9 +158,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) {
int k = 0; int k = 0;
uint8_t path_len = data[k++]; uint8_t path_len = data[k++];
uint8_t hash_size = (path_len >> 6) + 1; uint8_t* path = &data[k]; k += path_len;
uint8_t hash_count = path_len & 63;
uint8_t* path = &data[k]; k += hash_size*hash_count;
uint8_t extra_type = data[k++] & 0x0F; // upper 4 bits reserved for future use uint8_t extra_type = data[k++] & 0x0F; // upper 4 bits reserved for future use
uint8_t* extra = &data[k]; uint8_t* extra = &data[k];
uint8_t extra_len = len - k; // remainder of packet (may be padded with zeroes!) uint8_t extra_len = len - k; // remainder of packet (may be padded with zeroes!)
@@ -290,7 +293,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK
Packet tmp; Packet tmp;
tmp.header = pkt->header; tmp.header = pkt->header;
tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len); tmp.path_len = pkt->path_len;
memcpy(tmp.path, pkt->path, pkt->path_len);
tmp.payload_len = pkt->payload_len - 1; tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
@@ -317,25 +321,27 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
void Mesh::removeSelfFromPath(Packet* pkt) { void Mesh::removeSelfFromPath(Packet* pkt) {
// remove our hash from 'path' // remove our hash from 'path'
pkt->setPathHashCount(pkt->getPathHashCount() - 1); // decrement the count pkt->path_len -= PATH_HASH_SIZE;
#if 0
uint8_t sz = pkt->getPathHashSize(); memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len);
for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { // shuffle path by 1 'entry' #elif PATH_HASH_SIZE == 1
memcpy(&pkt->path[k], &pkt->path[k + sz], sz); for (int k = 0; k < pkt->path_len; k++) { // shuffle bytes by 1
pkt->path[k] = pkt->path[k + 1];
} }
#else
#error "need path remove impl"
#endif
} }
DispatcherAction Mesh::routeRecvPacket(Packet* packet) { DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
uint8_t n = packet->getPathHashCount();
if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit() if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit()
&& (n + 1)*packet->getPathHashSize() <= MAX_PATH_SIZE && allowPacketForward(packet)) { && packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) {
// append this node's hash to 'path' // append this node's hash to 'path'
self_id.copyHashTo(&packet->path[n * packet->getPathHashSize()], packet->getPathHashSize()); packet->path_len += self_id.copyHashTo(&packet->path[packet->path_len]);
packet->setPathHashCount(n + 1);
uint32_t d = getRetransmitDelay(packet); uint32_t d = getRetransmitDelay(packet);
// as this propagates outwards, give it lower and lower priority // as this propagates outwards, give it lower and lower priority
return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away return ACTION_RETRANSMIT_DELAYED(packet->path_len, d); // give priority to closer sources, than ones further away
} }
return ACTION_RELEASE; return ACTION_RELEASE;
} }
@@ -347,7 +353,8 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK
Packet tmp; Packet tmp;
tmp.header = pkt->header; tmp.header = pkt->header;
tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len); tmp.path_len = pkt->path_len;
memcpy(tmp.path, pkt->path, pkt->path_len);
tmp.payload_len = pkt->payload_len - 1; tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
@@ -369,7 +376,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
delay_millis += getDirectRetransmitDelay(packet) + 300; delay_millis += getDirectRetransmitDelay(packet) + 300;
auto a1 = createMultiAck(crc, extra); auto a1 = createMultiAck(crc, extra);
if (a1) { if (a1) {
a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len); memcpy(a1->path, packet->path, a1->path_len = packet->path_len);
a1->header &= ~PH_ROUTE_MASK; a1->header &= ~PH_ROUTE_MASK;
a1->header |= ROUTE_TYPE_DIRECT; a1->header |= ROUTE_TYPE_DIRECT;
sendPacket(a1, 0, delay_millis); sendPacket(a1, 0, delay_millis);
@@ -379,7 +386,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
auto a2 = createAck(crc); auto a2 = createAck(crc);
if (a2) { if (a2) {
a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len); memcpy(a2->path, packet->path, a2->path_len = packet->path_len);
a2->header &= ~PH_ROUTE_MASK; a2->header &= ~PH_ROUTE_MASK;
a2->header |= ROUTE_TYPE_DIRECT; a2->header |= ROUTE_TYPE_DIRECT;
sendPacket(a2, 0, delay_millis); sendPacket(a2, 0, delay_millis);
@@ -432,10 +439,7 @@ Packet* Mesh::createPathReturn(const Identity& dest, const uint8_t* secret, cons
} }
Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len) { Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len) {
uint8_t path_hash_size = (path_len >> 6) + 1; if (path_len + extra_len + 5 > MAX_COMBINED_PATH) return NULL; // too long!!
uint8_t path_hash_count = path_len & 63;
if (path_hash_count*path_hash_size + extra_len + 5 > MAX_COMBINED_PATH) return NULL; // too long!!
Packet* packet = obtainNewPacket(); Packet* packet = obtainNewPacket();
if (packet == NULL) { if (packet == NULL) {
@@ -453,7 +457,7 @@ Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret,
uint8_t data[MAX_PACKET_PAYLOAD]; uint8_t data[MAX_PACKET_PAYLOAD];
data[data_len++] = path_len; data[data_len++] = path_len;
memcpy(&data[data_len], path, path_hash_count*path_hash_size); data_len += path_hash_count*path_hash_size; memcpy(&data[data_len], path, path_len); data_len += path_len;
if (extra_len > 0) { if (extra_len > 0) {
data[data_len++] = extra_type; data[data_len++] = extra_type;
memcpy(&data[data_len], extra, extra_len); data_len += extra_len; memcpy(&data[data_len], extra, extra_len); data_len += extra_len;
@@ -620,19 +624,15 @@ Packet* Mesh::createControlData(const uint8_t* data, size_t len) {
return packet; return packet;
} }
void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) { void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) {
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
return; return;
} }
if (path_hash_size == 0 || path_hash_size > 3) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime());
return;
}
packet->header &= ~PH_ROUTE_MASK; packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_FLOOD; packet->header |= ROUTE_TYPE_FLOOD;
packet->setPathHashSizeAndCount(path_hash_size, 0); packet->path_len = 0;
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
@@ -647,21 +647,17 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_si
sendPacket(packet, pri, delay_millis); sendPacket(packet, pri, delay_millis);
} }
void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_hash_size) { void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis) {
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
return; return;
} }
if (path_hash_size == 0 || path_hash_size > 3) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime());
return;
}
packet->header &= ~PH_ROUTE_MASK; packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD; packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD;
packet->transport_codes[0] = transport_codes[0]; packet->transport_codes[0] = transport_codes[0];
packet->transport_codes[1] = transport_codes[1]; packet->transport_codes[1] = transport_codes[1];
packet->setPathHashSizeAndCount(path_hash_size, 0); packet->path_len = 0;
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
@@ -683,13 +679,13 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin
uint8_t pri; uint8_t pri;
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { // TRACE packets are different if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { // TRACE packets are different
// for TRACE packets, path is appended to end of PAYLOAD. (path is used for SNR's) // for TRACE packets, path is appended to end of PAYLOAD. (path is used for SNR's)
memcpy(&packet->payload[packet->payload_len], path, path_len); // NOTE: path_len here can be > 64, and NOT in the new scheme memcpy(&packet->payload[packet->payload_len], path, path_len);
packet->payload_len += path_len; packet->payload_len += path_len;
packet->path_len = 0; packet->path_len = 0;
pri = 5; // maybe make this configurable pri = 5; // maybe make this configurable
} else { } else {
packet->path_len = Packet::copyPath(packet->path, path, path_len); memcpy(packet->path, path, packet->path_len = path_len);
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 1; // slightly less priority pri = 1; // slightly less priority
} else { } else {

View File

@@ -196,13 +196,13 @@ public:
/** /**
* \brief send a locally-generated Packet with flood routing * \brief send a locally-generated Packet with flood routing
*/ */
void sendFlood(Packet* packet, uint32_t delay_millis=0, uint8_t path_hash_size=1); void sendFlood(Packet* packet, uint32_t delay_millis=0);
/** /**
* \brief send a locally-generated Packet with flood routing * \brief send a locally-generated Packet with flood routing
* \param transport_codes array of 2 codes to attach to packet * \param transport_codes array of 2 codes to attach to packet
*/ */
void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0, uint8_t path_hash_size=1); void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0);
/** /**
* \brief send a locally-generated Packet with Direct routing * \brief send a locally-generated Packet with Direct routing

View File

@@ -55,7 +55,6 @@ public:
virtual uint32_t getGpio() { return 0; } virtual uint32_t getGpio() { return 0; }
virtual void setGpio(uint32_t values) {} virtual void setGpio(uint32_t values) {}
virtual uint8_t getStartupReason() const = 0; virtual uint8_t getStartupReason() const = 0;
virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
// Power management interface (boards with power management override these) // Power management interface (boards with power management override these)

View File

@@ -10,32 +10,8 @@ Packet::Packet() {
payload_len = 0; payload_len = 0;
} }
bool Packet::isValidPathLen(uint8_t path_len) {
uint8_t hash_count = path_len & 63;
uint8_t hash_size = (path_len >> 6) + 1;
if (hash_size == 4) return false; // Reserved for future
return hash_count*hash_size <= MAX_PATH_SIZE;
}
size_t Packet::writePath(uint8_t* dest, const uint8_t* src, uint8_t path_len) {
uint8_t hash_count = path_len & 63;
uint8_t hash_size = (path_len >> 6) + 1;
size_t len = hash_count*hash_size;
if (len > MAX_PATH_SIZE) {
MESH_DEBUG_PRINTLN("Packet::copyPath, invalid path_len=%d", (uint32_t)path_len);
return 0; // Error
}
memcpy(dest, src, len);
return len;
}
uint8_t Packet::copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len) {
writePath(dest, src, path_len);
return path_len;
}
int Packet::getRawLength() const { int Packet::getRawLength() const {
return 2 + getPathByteLen() + payload_len + (hasTransportCodes() ? 4 : 0); return 2 + path_len + payload_len + (hasTransportCodes() ? 4 : 0);
} }
void Packet::calculatePacketHash(uint8_t* hash) const { void Packet::calculatePacketHash(uint8_t* hash) const {
@@ -57,7 +33,7 @@ uint8_t Packet::writeTo(uint8_t dest[]) const {
memcpy(&dest[i], &transport_codes[1], 2); i += 2; memcpy(&dest[i], &transport_codes[1], 2); i += 2;
} }
dest[i++] = path_len; dest[i++] = path_len;
i += writePath(&dest[i], path, path_len); memcpy(&dest[i], path, path_len); i += path_len;
memcpy(&dest[i], payload, payload_len); i += payload_len; memcpy(&dest[i], payload, payload_len); i += payload_len;
return i; return i;
} }
@@ -72,11 +48,8 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) {
transport_codes[0] = transport_codes[1] = 0; transport_codes[0] = transport_codes[1] = 0;
} }
path_len = src[i++]; path_len = src[i++];
if (!isValidPathLen(path_len)) return false; // bad encoding if (path_len > sizeof(path)) return false; // bad encoding
memcpy(path, &src[i], path_len); i += path_len;
uint8_t bl = getPathByteLen();
memcpy(path, &src[i], bl); i += bl;
if (i >= len) return false; // bad encoding if (i >= len) return false; // bad encoding
payload_len = len - i; payload_len = len - i;
if (payload_len > sizeof(payload)) return false; // bad encoding if (payload_len > sizeof(payload)) return false; // bad encoding

View File

@@ -76,16 +76,6 @@ public:
*/ */
uint8_t getPayloadVer() const { return (header >> PH_VER_SHIFT) & PH_VER_MASK; } uint8_t getPayloadVer() const { return (header >> PH_VER_SHIFT) & PH_VER_MASK; }
uint8_t getPathHashSize() const { return (path_len >> 6) + 1; }
uint8_t getPathHashCount() const { return path_len & 63; }
uint8_t getPathByteLen() const { return getPathHashCount() * getPathHashSize(); }
void setPathHashCount(uint8_t n) { path_len &= ~63; path_len |= n; }
void setPathHashSizeAndCount(uint8_t sz, uint8_t n) { path_len = ((sz - 1) << 6) | (n & 63); }
static uint8_t copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len); // returns path_len
static size_t writePath(uint8_t* dest, const uint8_t* src, uint8_t path_len); // returns byte length written
static bool isValidPathLen(uint8_t path_len);
void markDoNotRetransmit() { header = 0xFF; } void markDoNotRetransmit() { header = 0xFF; }
bool isMarkedDoNotRetransmit() const { return header == 0xFF; } bool isMarkedDoNotRetransmit() const { return header == 0xFF; }

View File

@@ -39,7 +39,7 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl
} }
void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) { void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
if (dest.out_path_len == OUT_PATH_UNKNOWN) { if (dest.out_path_len < 0) {
mesh::Packet* ack = createAck(ack_hash); mesh::Packet* ack = createAck(ack_hash);
if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY); if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY);
} else { } else {
@@ -92,7 +92,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot() {
void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) { void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) {
memset(&ci, 0, sizeof(ci)); memset(&ci, 0, sizeof(ci));
ci.id = id; ci.id = id;
ci.out_path_len = OUT_PATH_UNKNOWN; ci.out_path_len = -1; // initially out_path is unknown
StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name)); StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
ci.type = parser.getType(); ci.type = parser.getType();
if (parser.hasLatLon()) { if (parser.hasLatLon()) {
@@ -131,6 +131,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
plen = packet->writeTo(temp_buf); plen = packet->writeTo(temp_buf);
packet->header = save; packet->header = save;
} }
putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
bool is_new = false; // true = not in contacts[], false = exists in contacts[] bool is_new = false; // true = not in contacts[], false = exists in contacts[]
if (from == NULL) { if (from == NULL) {
@@ -141,15 +142,6 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
return; return;
} }
// check hop limit for new contacts (0 = no limit, 1 = direct (0 hops), N = up to N-1 hops)
uint8_t max_hops = getAutoAddMaxHops();
if (max_hops > 0 && packet->getPathHashCount() >= max_hops) {
ContactInfo ci;
populateContactFromAdvert(ci, id, parser, timestamp);
onDiscoveredContact(ci, true, packet->path_len, packet->path); // let UI know
return;
}
from = allocateContactSlot(); from = allocateContactSlot();
if (from == NULL) { if (from == NULL) {
ContactInfo ci; ContactInfo ci;
@@ -165,7 +157,6 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
from->shared_secret_valid = false; from->shared_secret_valid = false;
} }
// update // update
putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name)); StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
from->type = parser.getType(); from->type = parser.getType();
if (parser.hasLatLon()) { if (parser.hasLatLon()) {
@@ -272,7 +263,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
} else { } else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len); mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len);
if (reply) { if (reply) {
if (from.out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY); sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
} else { } else {
sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY); sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY);
@@ -282,7 +273,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
} }
} else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) { } else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) {
onContactResponse(from, data, len); onContactResponse(from, data, len);
if (packet->isRouteFlood() && from.out_path_len != OUT_PATH_UNKNOWN) { if (packet->isRouteFlood() && from.out_path_len >= 0) {
// we have direct path, but other node is still sending flood response, so maybe they didn't receive reciprocal path properly(?) // we have direct path, but other node is still sending flood response, so maybe they didn't receive reciprocal path properly(?)
handleReturnPathRetry(from, packet->path, packet->path_len); handleReturnPathRetry(from, packet->path, packet->path_len);
} }
@@ -304,7 +295,7 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui
bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
// NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path. // NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?) // FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
from.out_path_len = mesh::Packet::copyPath(from.out_path, out_path, out_path_len); // store a copy of path, for sendDirect() memcpy(from.out_path, out_path, from.out_path_len = out_path_len); // store a copy of path, for sendDirect()
from.lastmod = getRTCClock()->getCurrentTime(); from.lastmod = getRTCClock()->getCurrentTime();
onContactPathUpdated(from); onContactPathUpdated(from);
@@ -326,7 +317,7 @@ void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
if (packet->isRouteFlood() && from->out_path_len != OUT_PATH_UNKNOWN) { if (packet->isRouteFlood() && from->out_path_len >= 0) {
// we have direct path, but other node is still sending flood, so maybe they didn't receive reciprocal path properly(?) // we have direct path, but other node is still sending flood, so maybe they didn't receive reciprocal path properly(?)
handleReturnPathRetry(*from, packet->path, packet->path_len); handleReturnPathRetry(*from, packet->path, packet->path_len);
} }
@@ -395,7 +386,7 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
int rc; int rc;
if (recipient.out_path_len == OUT_PATH_UNKNOWN) { if (recipient.out_path_len < 0) {
sendFloodScoped(recipient, pkt); sendFloodScoped(recipient, pkt);
txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
rc = MSG_SEND_SENT_FLOOD; rc = MSG_SEND_SENT_FLOOD;
@@ -421,7 +412,7 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
int rc; int rc;
if (recipient.out_path_len == OUT_PATH_UNKNOWN) { if (recipient.out_path_len < 0) {
sendFloodScoped(recipient, pkt); sendFloodScoped(recipient, pkt);
txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
rc = MSG_SEND_SENT_FLOOD; rc = MSG_SEND_SENT_FLOOD;
@@ -509,7 +500,7 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
} }
if (pkt) { if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len == OUT_PATH_UNKNOWN) { if (recipient.out_path_len < 0) {
sendFloodScoped(recipient, pkt); sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t); est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD; return MSG_SEND_SENT_FLOOD;
@@ -534,7 +525,7 @@ int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data,
} }
if (pkt) { if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len == OUT_PATH_UNKNOWN) { if (recipient.out_path_len < 0) {
sendFloodScoped(recipient, pkt); sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t); est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD; return MSG_SEND_SENT_FLOOD;
@@ -561,7 +552,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_
} }
if (pkt) { if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len == OUT_PATH_UNKNOWN) { if (recipient.out_path_len < 0) {
sendFloodScoped(recipient, pkt); sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t); est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD; return MSG_SEND_SENT_FLOOD;
@@ -588,7 +579,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u
} }
if (pkt) { if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len == OUT_PATH_UNKNOWN) { if (recipient.out_path_len < 0) {
sendFloodScoped(recipient, pkt); sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t); est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD; return MSG_SEND_SENT_FLOOD;
@@ -692,7 +683,7 @@ void BaseChatMesh::checkConnections() {
MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact not found!"); MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact not found!");
continue; continue;
} }
if (contact->out_path_len == OUT_PATH_UNKNOWN) { if (contact->out_path_len < 0) {
MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact, no out_path!"); MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact, no out_path!");
continue; continue;
} }
@@ -719,7 +710,7 @@ void BaseChatMesh::checkConnections() {
} }
void BaseChatMesh::resetPathTo(ContactInfo& recipient) { void BaseChatMesh::resetPathTo(ContactInfo& recipient) {
recipient.out_path_len = OUT_PATH_UNKNOWN; recipient.out_path_len = -1;
} }
static ContactInfo* table; // pass via global :-( static ContactInfo* table; // pass via global :-(

View File

@@ -98,7 +98,6 @@ protected:
virtual bool shouldAutoAddContactType(uint8_t type) const { return true; } virtual bool shouldAutoAddContactType(uint8_t type) const { return true; }
virtual void onContactsFull() {}; virtual void onContactsFull() {};
virtual bool shouldOverwriteWhenFull() const { return false; } virtual bool shouldOverwriteWhenFull() const { return false; }
virtual uint8_t getAutoAddMaxHops() const { return 0; } // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops
virtual void onContactOverwrite(const uint8_t* pub_key) {}; virtual void onContactOverwrite(const uint8_t* pub_key) {};
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
virtual ContactInfo* processAck(const uint8_t *data) = 0; virtual ContactInfo* processAck(const uint8_t *data) = 0;

View File

@@ -114,7 +114,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
memset(c, 0, sizeof(*c)); memset(c, 0, sizeof(*c));
c->permissions = init_perms; c->permissions = init_perms;
c->id = id; c->id = id;
c->out_path_len = OUT_PATH_UNKNOWN; c->out_path_len = -1; // initially out_path is unknown
return c; return c;
} }

View File

@@ -10,12 +10,10 @@
#define PERM_ACL_READ_WRITE 2 #define PERM_ACL_READ_WRITE 2
#define PERM_ACL_ADMIN 3 #define PERM_ACL_ADMIN 3
#define OUT_PATH_UNKNOWN 0xFF
struct ClientInfo { struct ClientInfo {
mesh::Identity id; mesh::Identity id;
uint8_t permissions; uint8_t permissions;
uint8_t out_path_len; int8_t out_path_len;
uint8_t out_path[MAX_PATH_SIZE]; uint8_t out_path[MAX_PATH_SIZE];
uint8_t shared_secret[PUB_KEY_SIZE]; uint8_t shared_secret[PUB_KEY_SIZE];
uint32_t last_timestamp; // by THEIR clock (transient) uint32_t last_timestamp; // by THEIR clock (transient)

View File

@@ -16,7 +16,7 @@ static uint32_t _atoi(const char* sp) {
static bool isValidName(const char *n) { static bool isValidName(const char *n) {
while (*n) { while (*n) {
if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; if (*n == '[' || *n == ']' || *n == '/' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
n++; n++;
} }
return true; return true;
@@ -63,8 +63,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 file.read((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
file.read((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116 file.read((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116
file.read((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 file.read((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 file.read(pad, 3); // 121
file.read(pad, 2); // 122
file.read((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 file.read((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
file.read((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 file.read((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
file.read((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 file.read((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
@@ -95,10 +94,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->bw = constrain(_prefs->bw, 7.8f, 500.0f); _prefs->bw = constrain(_prefs->bw, 7.8f, 500.0f);
_prefs->sf = constrain(_prefs->sf, 5, 12); _prefs->sf = constrain(_prefs->sf, 5, 12);
_prefs->cr = constrain(_prefs->cr, 5, 8); _prefs->cr = constrain(_prefs->cr, 5, 8);
_prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, -9, 30); _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, 1, 30);
_prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1); _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1);
_prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f);
_prefs->path_hash_mode = constrain(_prefs->path_hash_mode, 0, 2); // NOTE: mode 3 reserved for future
// sanitise bad bridge pref values // sanitise bad bridge pref values
_prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
@@ -153,8 +151,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 file.write((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116 file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116
file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 file.write(pad, 3); // 121
file.write(pad, 2); // 122
file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
@@ -334,10 +331,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sp++; sp++;
} }
*reply = 0; // set null terminator *reply = 0; // set null terminator
} else if (memcmp(config, "path.hash.mode", 14) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode);
} else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); sprintf(reply, "> %d", (uint32_t) _prefs->tx_power_dbm);
} else if (memcmp(config, "freq", 4) == 0) { } else if (memcmp(config, "freq", 4) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq)); sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
} else if (memcmp(config, "public.key", 10) == 0) { } else if (memcmp(config, "public.key", 10) == 0) {
@@ -373,17 +368,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else if (memcmp(config, "bridge.secret", 13) == 0) { } else if (memcmp(config, "bridge.secret", 13) == 0) {
sprintf(reply, "> %s", _prefs->bridge_secret); sprintf(reply, "> %s", _prefs->bridge_secret);
#endif #endif
} else if (memcmp(config, "bootloader.ver", 14) == 0) {
#ifdef NRF52_PLATFORM
char ver[32];
if (_board->getBootloaderVersion(ver, sizeof(ver))) {
sprintf(reply, "> %s", ver);
} else {
strcpy(reply, "> unknown");
}
#else
strcpy(reply, "ERROR: unsupported");
#endif
} else if (memcmp(config, "adc.multiplier", 14) == 0) { } else if (memcmp(config, "adc.multiplier", 14) == 0) {
float adc_mult = _board->getAdcMultiplier(); float adc_mult = _board->getAdcMultiplier();
if (adc_mult == 0.0f) { if (adc_mult == 0.0f) {
@@ -569,16 +553,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
*dp = 0; *dp = 0;
savePrefs(); savePrefs();
strcpy(reply, "OK"); strcpy(reply, "OK");
} else if (memcmp(config, "path.hash.mode ", 15) == 0) {
config += 15;
uint8_t mode = atoi(config);
if (mode < 3) {
_prefs->path_hash_mode = mode;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0,1, or 2");
}
} else if (memcmp(config, "tx ", 3) == 0) { } else if (memcmp(config, "tx ", 3) == 0) {
_prefs->tx_power_dbm = atoi(&config[3]); _prefs->tx_power_dbm = atoi(&config[3]);
savePrefs(); savePrefs();
@@ -734,9 +708,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
LocationProvider * l = _sensors->getLocationProvider(); LocationProvider * l = _sensors->getLocationProvider();
if (l != NULL) { if (l != NULL) {
l->syncTime(); l->syncTime();
strcpy(reply, "ok");
} else {
strcpy(reply, "gps provider not found");
} }
} else if (memcmp(command, "gps setloc", 10) == 0) { } else if (memcmp(command, "gps setloc", 10) == 0) {
_prefs->node_lat = _sensors->node_lat; _prefs->node_lat = _sensors->node_lat;
@@ -766,7 +737,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
_prefs->advert_loc_policy = ADVERT_LOC_SHARE; _prefs->advert_loc_policy = ADVERT_LOC_SHARE;
savePrefs(); savePrefs();
strcpy(reply, "ok"); strcpy(reply, "ok");
} else if (memcmp(command+11, "prefs", 5) == 0) { } else if (memcmp(command+11, "prefs", 4) == 0) {
_prefs->advert_loc_policy = ADVERT_LOC_PREFS; _prefs->advert_loc_policy = ADVERT_LOC_PREFS;
savePrefs(); savePrefs();
strcpy(reply, "ok"); strcpy(reply, "ok");

View File

@@ -19,7 +19,7 @@ struct NodePrefs { // persisted to file
double node_lat, node_lon; double node_lat, node_lon;
char password[16]; char password[16];
float freq; float freq;
int8_t tx_power_dbm; uint8_t tx_power_dbm;
uint8_t disable_fwd; uint8_t disable_fwd;
uint8_t advert_interval; // minutes / 2 uint8_t advert_interval; // minutes / 2
uint8_t flood_advert_interval; // hours uint8_t flood_advert_interval; // hours
@@ -53,7 +53,6 @@ struct NodePrefs { // persisted to file
uint32_t discovery_mod_timestamp; uint32_t discovery_mod_timestamp;
float adc_multiplier; float adc_multiplier;
char owner_info[120]; char owner_info[120];
uint8_t path_hash_mode; // which path mode to use when sending
}; };
class CommonCLICallbacks { class CommonCLICallbacks {
@@ -69,7 +68,7 @@ public:
virtual void setLoggingOn(bool enable) = 0; virtual void setLoggingOn(bool enable) = 0;
virtual void eraseLogFile() = 0; virtual void eraseLogFile() = 0;
virtual void dumpLogFile() = 0; virtual void dumpLogFile() = 0;
virtual void setTxPower(int8_t power_dbm) = 0; virtual void setTxPower(uint8_t power_dbm) = 0;
virtual void formatNeighborsReply(char *reply) = 0; virtual void formatNeighborsReply(char *reply) = 0;
virtual void removeNeighbor(const uint8_t* pubkey, int key_len) { virtual void removeNeighbor(const uint8_t* pubkey, int key_len) {
// no op by default // no op by default

View File

@@ -3,14 +3,12 @@
#include <Arduino.h> #include <Arduino.h>
#include <Mesh.h> #include <Mesh.h>
#define OUT_PATH_UNKNOWN 0xFF
struct ContactInfo { struct ContactInfo {
mesh::Identity id; mesh::Identity id;
char name[32]; char name[32];
uint8_t type; // on of ADV_TYPE_* uint8_t type; // on of ADV_TYPE_*
uint8_t flags; uint8_t flags;
uint8_t out_path_len; int8_t out_path_len;
mutable bool shared_secret_valid; // flag to indicate if shared_secret has been calculated mutable bool shared_secret_valid; // flag to indicate if shared_secret has been calculated
uint8_t out_path[MAX_PATH_SIZE]; uint8_t out_path[MAX_PATH_SIZE];
uint32_t last_advert_timestamp; // by THEIR clock uint32_t last_advert_timestamp; // by THEIR clock

View File

@@ -11,7 +11,6 @@
#include <SPIFFS.h> #include <SPIFFS.h>
bool ESP32Board::startOTAUpdate(const char* id, char reply[]) { bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
inhibit_sleep = true; // prevent sleep during OTA
WiFi.softAP("MeshCore-OTA", NULL); WiFi.softAP("MeshCore-OTA", NULL);
sprintf(reply, "Started: http://%s/update", WiFi.softAPIP().toString().c_str()); sprintf(reply, "Started: http://%s/update", WiFi.softAPIP().toString().c_str());

View File

@@ -8,12 +8,12 @@
#include <rom/rtc.h> #include <rom/rtc.h>
#include <sys/time.h> #include <sys/time.h>
#include <Wire.h> #include <Wire.h>
#include "esp_wifi.h"
#include "driver/rtc_io.h" #include "driver/rtc_io.h"
class ESP32Board : public mesh::MainBoard { class ESP32Board : public mesh::MainBoard {
protected: protected:
uint8_t startup_reason; uint8_t startup_reason;
bool inhibit_sleep = false;
public: public:
void begin() { void begin() {
@@ -72,7 +72,11 @@ public:
} }
void sleep(uint32_t secs) override { void sleep(uint32_t secs) override {
if (!inhibit_sleep) { // To check for WiFi status to see if there is active OTA
wifi_mode_t mode;
esp_err_t err = esp_wifi_get_mode(&mode);
if (err != ESP_OK) { // WiFi is off ~ No active OTA, safe to go to sleep
enterLightSleep(secs); // To wake up after "secs" seconds or when receiving a LoRa packet enterLightSleep(secs); // To wake up after "secs" seconds or when receiving a LoRa packet
} }
} }
@@ -122,10 +126,6 @@ public:
} }
bool startOTAUpdate(const char* id, char reply[]) override; bool startOTAUpdate(const char* id, char reply[]) override;
void setInhibitSleep(bool inhibit) {
inhibit_sleep = inhibit;
}
}; };
class ESP32RTCClock : public mesh::RTCClock { class ESP32RTCClock : public mesh::RTCClock {

View File

@@ -251,32 +251,6 @@ void NRF52BoardDCDC::begin() {
} }
} }
void NRF52Board::sleep(uint32_t secs) {
// Clear FPU interrupt flags to avoid insomnia
// see errata 87 for details https://docs.nordicsemi.com/bundle/errata_nRF52840_Rev3/page/ERR/nRF52840/Rev3/latest/anomaly_840_87.html
#if (__FPU_USED == 1)
__set_FPSCR(__get_FPSCR() & ~(0x0000009F));
(void) __get_FPSCR();
NVIC_ClearPendingIRQ(FPU_IRQn);
#endif
// On nRF52, we use event-driven sleep instead of timed sleep
// The 'secs' parameter is ignored - we wake on any interrupt
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
// first call processes pending softdevice events, second call sleeps.
sd_app_evt_wait();
sd_app_evt_wait();
} else {
// softdevice is disabled, use raw WFE
__SEV();
__WFE();
__WFE();
}
}
// Temperature from NRF52 MCU // Temperature from NRF52 MCU
float NRF52Board::getMCUTemperature() { float NRF52Board::getMCUTemperature() {
NRF_TEMP->TASKS_START = 1; // Start temperature measurement NRF_TEMP->TASKS_START = 1; // Start temperature measurement
@@ -297,25 +271,6 @@ float NRF52Board::getMCUTemperature() {
return temp * 0.25f; // Convert to *C return temp * 0.25f; // Convert to *C
} }
bool NRF52Board::getBootloaderVersion(char* out, size_t max_len) {
static const char BOOTLOADER_MARKER[] = "UF2 Bootloader ";
const uint8_t* flash = (const uint8_t*)0x000FB000; // earliest known info.txt location is 0xFB90B, latest is 0xFCC4B
for (uint32_t i = 0; i < 0x3000 - (sizeof(BOOTLOADER_MARKER) - 1); i++) {
if (memcmp(&flash[i], BOOTLOADER_MARKER, sizeof(BOOTLOADER_MARKER) - 1) == 0) {
const char* ver = (const char*)&flash[i + sizeof(BOOTLOADER_MARKER) - 1];
size_t len = 0;
while (len < max_len - 1 && ver[len] != '\0' && ver[len] != ' ' && ver[len] != '\n' && ver[len] != '\r') {
out[len] = ver[len];
len++;
}
out[len] = '\0';
return len > 0; // bootloader string is non-empty
}
}
return false;
}
bool NRF52Board::startOTAUpdate(const char *id, char reply[]) { bool NRF52Board::startOTAUpdate(const char *id, char reply[]) {
// Config the peripheral connection with maximum bandwidth // Config the peripheral connection with maximum bandwidth
// more SRAM required by SoftDevice // more SRAM required by SoftDevice

View File

@@ -50,9 +50,7 @@ public:
virtual uint8_t getStartupReason() const override { return startup_reason; } virtual uint8_t getStartupReason() const override { return startup_reason; }
virtual float getMCUTemperature() override; virtual float getMCUTemperature() override;
virtual void reboot() override { NVIC_SystemReset(); } virtual void reboot() override { NVIC_SystemReset(); }
virtual bool getBootloaderVersion(char* version, size_t max_len) override;
virtual bool startOTAUpdate(const char *id, char reply[]) override; virtual bool startOTAUpdate(const char *id, char reply[]) override;
virtual void sleep(uint32_t secs) override;
#ifdef NRF52_POWER_MANAGEMENT #ifdef NRF52_POWER_MANAGEMENT
bool isExternalPowered() override; bool isExternalPowered() override;

View File

@@ -20,10 +20,7 @@ public:
digitalWrite(_pin, _active); digitalWrite(_pin, _active);
} }
} }
void release() { void release() {
if (_claims == 0) return; // avoid negative _claims
_claims--; _claims--;
if (_claims == 0) { if (_claims == 0) {
digitalWrite(_pin, !_active); digitalWrite(_pin, !_active);

View File

@@ -11,7 +11,7 @@ PacketQueue::PacketQueue(int max_entries) {
int PacketQueue::countBefore(uint32_t now) const { int PacketQueue::countBefore(uint32_t now) const {
int n = 0; int n = 0;
for (int j = 0; j < _num; j++) { for (int j = 0; j < _num; j++) {
if ((int32_t)(_schedule_table[j] - now) > 0) continue; // scheduled for future... ignore for now if (_schedule_table[j] > now) continue; // scheduled for future... ignore for now
n++; n++;
} }
return n; return n;
@@ -21,7 +21,7 @@ mesh::Packet* PacketQueue::get(uint32_t now) {
uint8_t min_pri = 0xFF; uint8_t min_pri = 0xFF;
int best_idx = -1; int best_idx = -1;
for (int j = 0; j < _num; j++) { for (int j = 0; j < _num; j++) {
if ((int32_t)(_schedule_table[j] - now) > 0) continue; // scheduled for future... ignore for now if (_schedule_table[j] > now) continue; // scheduled for future... ignore for now
if (_pri_table[j] < min_pri) { // select most important priority amongst non-future entries if (_pri_table[j] < min_pri) { // select most important priority amongst non-future entries
min_pri = _pri_table[j]; min_pri = _pri_table[j];
best_idx = j; best_idx = j;
@@ -55,15 +55,15 @@ mesh::Packet* PacketQueue::removeByIdx(int i) {
return item; return item;
} }
bool PacketQueue::add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { void PacketQueue::add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
if (_num == _size) { if (_num == _size) {
return false; // TODO: log "FATAL: queue is full!"
return;
} }
_table[_num] = packet; _table[_num] = packet;
_pri_table[_num] = priority; _pri_table[_num] = priority;
_schedule_table[_num] = scheduled_for; _schedule_table[_num] = scheduled_for;
_num++; _num++;
return true;
} }
StaticPoolPacketManager::StaticPoolPacketManager(int pool_size): unused(pool_size), send_queue(pool_size), rx_queue(pool_size) { StaticPoolPacketManager::StaticPoolPacketManager(int pool_size): unused(pool_size), send_queue(pool_size), rx_queue(pool_size) {
@@ -82,10 +82,7 @@ void StaticPoolPacketManager::free(mesh::Packet* packet) {
} }
void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
if (!send_queue.add(packet, priority, scheduled_for)) { send_queue.add(packet, priority, scheduled_for);
MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet");
free(packet);
}
} }
mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) { mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) {
@@ -109,10 +106,7 @@ mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) {
} }
void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) { void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) {
if (!rx_queue.add(packet, 0, scheduled_for)) { rx_queue.add(packet, 0, scheduled_for);
MESH_DEBUG_PRINTLN("queueInbound: rx queue full, dropping packet");
free(packet);
}
} }
mesh::Packet* StaticPoolPacketManager::getNextInbound(uint32_t now) { mesh::Packet* StaticPoolPacketManager::getNextInbound(uint32_t now) {
return rx_queue.get(now); return rx_queue.get(now);

View File

@@ -11,7 +11,7 @@ class PacketQueue {
public: public:
PacketQueue(int max_entries); PacketQueue(int max_entries);
mesh::Packet* get(uint32_t now); mesh::Packet* get(uint32_t now);
bool add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for); void add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for);
int count() const { return _num; } int count() const { return _num; }
int countBefore(uint32_t now) const; int countBefore(uint32_t now) const;
mesh::Packet* itemAt(int i) const { return _table[i]; } mesh::Packet* itemAt(int i) const { return _table[i]; }

View File

@@ -1,5 +1,4 @@
#include "SerialBLEInterface.h" #include "SerialBLEInterface.h"
#include "esp_mac.h"
// See the following for generating UUIDs: // See the following for generating UUIDs:
// https://www.uuidgenerator.net/ // https://www.uuidgenerator.net/

View File

@@ -2,7 +2,6 @@
#include "CustomLLCC68.h" #include "CustomLLCC68.h"
#include "RadioLibWrappers.h" #include "RadioLibWrappers.h"
#include "SX126xReset.h"
class CustomLLCC68Wrapper : public RadioLibWrapper { class CustomLLCC68Wrapper : public RadioLibWrapper {
public: public:
@@ -20,6 +19,4 @@ public:
int sf = ((CustomLLCC68 *)_radio)->spreadingFactor; int sf = ((CustomLLCC68 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len); return packetScoreInt(snr, sf, packet_len);
} }
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
}; };

View File

@@ -20,8 +20,6 @@ class CustomLR1110 : public LR1110 {
return len; return len;
} }
float getFreqMHz() const { return freqMHz; }
bool isReceiving() { bool isReceiving() {
uint16_t irq = getIrqStatus(); uint16_t irq = getIrqStatus();
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));

View File

@@ -2,12 +2,10 @@
#include "CustomLR1110.h" #include "CustomLR1110.h"
#include "RadioLibWrappers.h" #include "RadioLibWrappers.h"
#include "LR11x0Reset.h"
class CustomLR1110Wrapper : public RadioLibWrapper { class CustomLR1110Wrapper : public RadioLibWrapper {
public: public:
CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); }
bool isReceivingPacket() override { bool isReceivingPacket() override {
return ((CustomLR1110 *)_radio)->isReceiving(); return ((CustomLR1110 *)_radio)->isReceiving();
} }

View File

@@ -2,7 +2,6 @@
#include "CustomSTM32WLx.h" #include "CustomSTM32WLx.h"
#include "RadioLibWrappers.h" #include "RadioLibWrappers.h"
#include "SX126xReset.h"
#include <math.h> #include <math.h>
class CustomSTM32WLxWrapper : public RadioLibWrapper { class CustomSTM32WLxWrapper : public RadioLibWrapper {
@@ -21,6 +20,4 @@ public:
int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor; int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len); return packetScoreInt(snr, sf, packet_len);
} }
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
}; };

View File

@@ -76,14 +76,6 @@ class CustomSX1262 : public SX1262 {
setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
#endif #endif
// for improved RX with Heltec v4
#ifdef SX126X_REGISTER_PATCH
uint8_t r_data = 0;
readRegister(0x8B5, &r_data, 1);
r_data |= 0x01;
writeRegister(0x8B5, &r_data, 1);
#endif
return true; // success return true; // success
} }

View File

@@ -2,7 +2,6 @@
#include "CustomSX1262.h" #include "CustomSX1262.h"
#include "RadioLibWrappers.h" #include "RadioLibWrappers.h"
#include "SX126xReset.h"
class CustomSX1262Wrapper : public RadioLibWrapper { class CustomSX1262Wrapper : public RadioLibWrapper {
public: public:
@@ -23,6 +22,4 @@ public:
virtual void powerOff() override { virtual void powerOff() override {
((CustomSX1262 *)_radio)->sleep(false); ((CustomSX1262 *)_radio)->sleep(false);
} }
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
}; };

View File

@@ -2,7 +2,6 @@
#include "CustomSX1268.h" #include "CustomSX1268.h"
#include "RadioLibWrappers.h" #include "RadioLibWrappers.h"
#include "SX126xReset.h"
class CustomSX1268Wrapper : public RadioLibWrapper { class CustomSX1268Wrapper : public RadioLibWrapper {
public: public:
@@ -20,6 +19,4 @@ public:
int sf = ((CustomSX1268 *)_radio)->spreadingFactor; int sf = ((CustomSX1268 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len); return packetScoreInt(snr, sf, packet_len);
} }
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
}; };

View File

@@ -1,21 +0,0 @@
#pragma once
#include <RadioLib.h>
// Full receiver reset for LR11x0-family chips (LR1110, LR1120, LR1121).
// Warm sleep powers down analog, calibrate(0x3F) refreshes all calibration blocks,
// then re-applies RX settings that calibration may reset.
inline void lr11x0ResetAGC(LR11x0* radio, float freqMHz) {
radio->sleep(true, 0);
radio->standby(RADIOLIB_LR11X0_STANDBY_RC, true);
radio->calibrate(RADIOLIB_LR11X0_CALIBRATE_ALL);
// calibrate(0x3F) defaults image calibration to 902-928MHz band.
// Re-calibrate for the actual operating frequency (band=4MHz matches RadioLib default).
radio->calibrateImageRejection(freqMHz - 4.0f, freqMHz + 4.0f);
#ifdef RX_BOOSTED_GAIN
radio->setRxBoostedGainMode(RX_BOOSTED_GAIN);
#endif
}

View File

@@ -53,24 +53,13 @@ void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) {
} }
} }
void RadioLibWrapper::doResetAGC() {
_radio->sleep(); // warm sleep to reset analog frontend
}
void RadioLibWrapper::resetAGC() { void RadioLibWrapper::resetAGC() {
// make sure we're not mid-receive of packet! // make sure we're not mid-receive of packet!
if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) return; if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) return;
doResetAGC(); // NOTE: according to higher powers, just issuing RadioLib's startReceive() will reset the AGC.
// revisit this if a better impl is discovered.
state = STATE_IDLE; // trigger a startReceive() state = STATE_IDLE; // trigger a startReceive()
// Reset noise floor sampling so it reconverges from scratch.
// Without this, a stuck _noise_floor of -120 makes the sampling threshold
// too low (-106) to accept normal samples (~-105), self-reinforcing the
// stuck value even after the receiver has recovered.
_noise_floor = 0;
_num_floor_samples = 0;
_floor_sample_sum = 0;
} }
void RadioLibWrapper::loop() { void RadioLibWrapper::loop() {

View File

@@ -16,7 +16,6 @@ protected:
void startRecv(); void startRecv();
float packetScoreInt(float snr, int sf, int packet_len); float packetScoreInt(float snr, int sf, int packet_len);
virtual bool isReceivingPacket() =0; virtual bool isReceivingPacket() =0;
virtual void doResetAGC();
public: public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; } RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; }

View File

@@ -1,37 +0,0 @@
#pragma once
#include <RadioLib.h>
// Full receiver reset for all SX126x-family chips (SX1262, SX1268, LLCC68, STM32WLx).
// Warm sleep powers down analog, Calibrate(0x7F) refreshes ADC/PLL/image calibration,
// then re-applies RX settings that calibration may reset.
inline void sx126xResetAGC(SX126x* radio) {
radio->sleep(true);
radio->standby(RADIOLIB_SX126X_STANDBY_RC, true);
uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL;
radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false);
radio->mod->hal->delay(5);
uint32_t start = millis();
while (radio->mod->hal->digitalRead(radio->mod->getGpio())) {
if (millis() - start > 50) break;
radio->mod->hal->yield();
}
// Calibrate(0x7F) defaults image calibration to 902-928MHz band.
// Re-calibrate for the actual operating frequency.
radio->calibrateImage(radio->freqMHz);
#ifdef SX126X_DIO2_AS_RF_SWITCH
radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH);
#endif
#ifdef SX126X_RX_BOOSTED_GAIN
radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN);
#endif
#ifdef SX126X_REGISTER_PATCH
uint8_t r_data = 0;
radio->readRegister(0x8B5, &r_data, 1);
r_data |= 0x01;
radio->writeRegister(0x8B5, &r_data, 1);
#endif
}

View File

@@ -284,7 +284,7 @@ bool EnvironmentSensorManager::begin() {
INA260_initialized = true; INA260_initialized = true;
} else { } else {
INA260_initialized = false; INA260_initialized = false;
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA260_ADDRESS); MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
} }
#endif #endif
@@ -707,9 +707,7 @@ void EnvironmentSensorManager::loop() {
static long next_gps_update = 0; static long next_gps_update = 0;
#if ENV_INCLUDE_GPS #if ENV_INCLUDE_GPS
if (gps_active) { _location->loop();
_location->loop();
}
if (millis() > next_gps_update) { if (millis() > next_gps_update) {
if(gps_active){ if(gps_active){

View File

@@ -79,9 +79,6 @@ public :
if (_pin_en != -1) { if (_pin_en != -1) {
digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE); digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE);
} }
if (_pin_reset != -1) {
digitalWrite(_pin_reset, GPS_RESET_FORCE);
}
if (_peripher_power) _peripher_power->release(); if (_peripher_power) _peripher_power->release();
} }

View File

@@ -18,23 +18,17 @@ bool SSD1306Display::begin() {
} }
void SSD1306Display::turnOn() { void SSD1306Display::turnOn() {
display.ssd1306_command(SSD1306_DISPLAYON);
if (!_isOn) { if (!_isOn) {
if (_peripher_power) _peripher_power->claim(); if (_peripher_power) _peripher_power->claim();
_isOn = true; // set before begin() to prevent double claim _isOn = true;
if (_peripher_power) begin(); // re-init display after power was cut
} }
display.ssd1306_command(SSD1306_DISPLAYON);
} }
void SSD1306Display::turnOff() { void SSD1306Display::turnOff() {
display.ssd1306_command(SSD1306_DISPLAYOFF); display.ssd1306_command(SSD1306_DISPLAYOFF);
if (_isOn) { if (_isOn) {
if (_peripher_power) { if (_peripher_power) _peripher_power->release();
#if PIN_OLED_RESET >= 0
digitalWrite(PIN_OLED_RESET, LOW);
#endif
_peripher_power->release();
}
_isOn = false; _isOn = false;
} }
} }

View File

@@ -10,13 +10,8 @@
#define Y_OFFSET 1 // Vertical offset to prevent top row cutoff #define Y_OFFSET 1 // Vertical offset to prevent top row cutoff
#endif #endif
#ifdef HELTEC_VISION_MASTER_T190 #define SCALE_X 1.875f // 240 / 128
#define SCALE_X 2.5f // 320 / 128 #define SCALE_Y 2.109375f // 135 / 64
#define SCALE_Y 2.65625f // 170 / 64
#else
#define SCALE_X 1.875f // 240 / 128
#define SCALE_Y 2.109375f // 135 / 64
#endif
bool ST7789Display::begin() { bool ST7789Display::begin() {
if(!_isOn) { if(!_isOn) {

View File

@@ -0,0 +1,64 @@
#!/bin/bash # Note: switched to bash for process substitution support
export PATH="$HOME/.platformio/penv/bin:$PATH"
LOGFILE="$PWD/meshcore-evo-fw.log"
FIRMWARE_VERSION="v1.11.0-evo_0.1.3"
FIRMWARE_BUILD_DATE=$(date '+%d-%b-%Y')
collect_bin_files(){
DEST_DIR="./firmwares"
mkdir -p "$DEST_DIR"
BUILD_DIR=".pio/build"
if [ ! -d "$BUILD_DIR" ]; then
echo "Error: $BUILD_DIR not found. Did you run the build process?"
exit 1
fi
echo "Copying firmware files to $DEST_DIR..."
for target_path in "$BUILD_DIR"/*/; do
echo $target_path
target_name=$(basename "$target_path")
# if ls "$target_path"*.bin >/dev/null 2>&1; then
for bin_file in "$target_path"*firmware*.{uf2,bin,zip}; do
filename=$(basename "$bin_file")
new_filename="${target_name}_${FIRMWARE_VERSION}_${FIRMWARE_BUILD_DATE}_${filename}"
cp "$bin_file" "$DEST_DIR/$new_filename"
echo "Done: $new_filename"
done
# fi
done
}
# Everything after this line goes to BOTH console and logfile
exec > >(tee -a "$LOGFILE") 2>&1
echo "-------------------- Build start ----------------"
date
echo "-------------------------------------------------"
# apply patches
# ./tools/maint/apply_patches.sh 1199 1338 1297
# build all repeater firmwares, the will be in .out
FIRMWARE_VERSION="v1.11_evo" ./build.sh build-repeater-firmwares
# build single firmwares
#FIRMWARE_VERSION=$FIRMWARE_VERSION FIRMWARE_BUILD_DATE=$FIRMWARE_BUILD_DATE ./build.sh build-firmware ProMicro_repeater
#FIRMWARE_VERSION=$FIRMWARE_VERSION FIRMWARE_BUILD_DATE=$FIRMWARE_BUILD_DATE ./build.sh build-firmware RAK_4631_repeater
#FIRMWARE_VERSION=$FIRMWARE_VERSION FIRMWARE_BUILD_DATE=$FIRMWARE_BUILD_DATE ./build.sh build-firmware heltec_v4_repeater
#FIRMWARE_VERSION=$FIRMWARE_VERSION FIRMWARE_BUILD_DATE=$FIRMWARE_BUILD_DATE ./build.sh build-firmware Heltec_v3_repeater
#FIRMWARE_VERSION=$FIRMWARE_VERSION FIRMWARE_BUILD_DATE=$FIRMWARE_BUILD_DATE ./build.sh build-firmware Xiao_nrf52_repeater
#FIRMWARE_VERSION=$FIRMWARE_VERSION FIRMWARE_BUILD_DATE=$FIRMWARE_BUILD_DATE ./build.sh build-firmware LilyGo_T3S3_sx1262_repeater
#FIRMWARE_VERSION=$FIRMWARE_VERSION FIRMWARE_BUILD_DATE=$FIRMWARE_BUILD_DATE ./build.sh build-firmware Heltec_t114_without_display_repeater
#FIRMWARE_VERSION=$FIRMWARE_VERSION FIRMWARE_BUILD_DATE=$FIRMWARE_BUILD_DATE ./build.sh build-firmware Heltec_t114_repeater
#collect_bin_files
echo "-------------------- Build end ------------------"
date
echo "-------------------------------------------------"
#grep -E " SUCCESS | FAILED " hansemesh_fw.log

View File

@@ -75,7 +75,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setCodingRate(cr); radio.setCodingRate(cr);
} }
void radio_set_tx_power(int8_t dbm) { void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm); radio.setOutputPower(dbm);
} }

View File

@@ -25,5 +25,5 @@ extern SensorManager sensors;
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();

View File

@@ -38,7 +38,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setCodingRate(cr); radio.setCodingRate(cr);
} }
void radio_set_tx_power(int8_t dbm) { void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm); radio.setOutputPower(dbm);
} }

View File

@@ -17,5 +17,5 @@ extern SensorManager sensors;
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();

View File

@@ -25,7 +25,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
// no-op // no-op
} }
void radio_set_tx_power(int8_t dbm) { void radio_set_tx_power(uint8_t dbm) {
radio_driver.setTxPower(dbm); radio_driver.setTxPower(dbm);
} }

View File

@@ -12,5 +12,5 @@ extern SensorManager sensors;
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();

View File

@@ -27,7 +27,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setCodingRate(cr); radio.setCodingRate(cr);
} }
void radio_set_tx_power(int8_t dbm) { void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm); radio.setOutputPower(dbm);
} }

View File

@@ -16,5 +16,5 @@ extern SensorManager sensors;
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();

View File

@@ -44,7 +44,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setCodingRate(cr); radio.setCodingRate(cr);
} }
void radio_set_tx_power(int8_t dbm) { void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm); radio.setOutputPower(dbm);
} }

View File

@@ -25,5 +25,5 @@ extern MomentaryButton user_btn;
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();

View File

@@ -44,7 +44,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setCodingRate(cr); radio.setCodingRate(cr);
} }
void radio_set_tx_power(int8_t dbm) { void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm); radio.setOutputPower(dbm);
} }

View File

@@ -25,5 +25,5 @@ extern MomentaryButton user_btn;
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();

View File

@@ -34,7 +34,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setCodingRate(cr); radio.setCodingRate(cr);
} }
void radio_set_tx_power(int8_t dbm) { void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm); radio.setOutputPower(dbm);
} }

View File

@@ -42,5 +42,5 @@ extern SolarSensorManager sensors;
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();

View File

@@ -34,6 +34,7 @@ void T114Board::initiateShutdown(uint8_t reason) {
void T114Board::begin() { void T114Board::begin() {
NRF52Board::begin(); NRF52Board::begin();
NRF_POWER->DCDCEN = 1;
pinMode(PIN_VBAT_READ, INPUT); pinMode(PIN_VBAT_READ, INPUT);

View File

@@ -6,7 +6,6 @@ extends = nrf52_base
board = heltec_t114 board = heltec_t114
board_build.ldscript = boards/nrf52840_s140_v6.ld board_build.ldscript = boards/nrf52840_s140_v6.ld
build_flags = ${nrf52_base.build_flags} build_flags = ${nrf52_base.build_flags}
${sensor_base.build_flags}
-I lib/nrf52/s140_nrf52_6.1.1_API/include -I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-I variants/heltec_t114 -I variants/heltec_t114
@@ -29,20 +28,20 @@ build_flags = ${nrf52_base.build_flags}
-D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140 -D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_RX_BOOSTED_GAIN=1
-D DISPLAY_CLASS=NullDisplayDriver
-D ST7789
-D PIN_GPS_RX=39 -D PIN_GPS_RX=39
-D PIN_GPS_TX=37 -D PIN_GPS_TX=37
-D PIN_GPS_EN=21 -D PIN_GPS_EN=21
-D PIN_GPS_RESET=38 -D PIN_GPS_RESET=38
-D PIN_GPS_RESET_ACTIVE=LOW -D PIN_GPS_RESET_ACTIVE=LOW
-D ENV_PIN_SDA=PIN_WIRE1_SDA
-D ENV_PIN_SCL=PIN_WIRE1_SCL
build_src_filter = ${nrf52_base.build_src_filter} build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp> +<helpers/*.cpp>
+<helpers/sensors>
+<../variants/heltec_t114> +<../variants/heltec_t114>
lib_deps = lib_deps =
${nrf52_base.lib_deps} ${nrf52_base.lib_deps}
${sensor_base.lib_deps} stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit GFX Library @ ^1.12.1
debug_tool = jlink debug_tool = jlink
upload_protocol = nrfutil upload_protocol = nrfutil
@@ -101,7 +100,6 @@ board_upload.maximum_size = 712704
build_flags = build_flags =
${Heltec_t114.build_flags} ${Heltec_t114.build_flags}
-I examples/companion_radio/ui-new -I examples/companion_radio/ui-new
-D DISPLAY_CLASS=NullDisplayDriver
-D MAX_CONTACTS=350 -D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40 -D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456 -D BLE_PIN_CODE=123456
@@ -124,7 +122,6 @@ board_upload.maximum_size = 712704
build_flags = build_flags =
${Heltec_t114.build_flags} ${Heltec_t114.build_flags}
-I examples/companion_radio/ui-new -I examples/companion_radio/ui-new
-D DISPLAY_CLASS=NullDisplayDriver
-D MAX_CONTACTS=350 -D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40 -D MAX_GROUP_CHANNELS=40
; -D BLE_PIN_CODE=123456 ; -D BLE_PIN_CODE=123456
@@ -147,7 +144,6 @@ extends = Heltec_t114
board = heltec_t114 board = heltec_t114
board_build.ldscript = boards/nrf52840_s140_v6.ld board_build.ldscript = boards/nrf52840_s140_v6.ld
build_flags = ${Heltec_t114.build_flags} build_flags = ${Heltec_t114.build_flags}
-D ST7789
-D HELTEC_T114_WITH_DISPLAY -D HELTEC_T114_WITH_DISPLAY
-D DISPLAY_CLASS=ST7789Display -D DISPLAY_CLASS=ST7789Display
build_src_filter = ${Heltec_t114.build_src_filter} build_src_filter = ${Heltec_t114.build_src_filter}
@@ -157,7 +153,6 @@ build_src_filter = ${Heltec_t114.build_src_filter}
+<helpers/ui/OLEDDisplayFonts.cpp> +<helpers/ui/OLEDDisplayFonts.cpp>
lib_deps = lib_deps =
${Heltec_t114.lib_deps} ${Heltec_t114.lib_deps}
adafruit/Adafruit SSD1306 @ ^2.5.13
debug_tool = jlink debug_tool = jlink
upload_protocol = nrfutil upload_protocol = nrfutil

View File

@@ -1,46 +1,28 @@
#include "target.h"
#include <Arduino.h> #include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h> #include <helpers/ArduinoHelpers.h>
#ifdef ENV_INCLUDE_GPS
#include <helpers/sensors/MicroNMEALocationProvider.h> #include <helpers/sensors/MicroNMEALocationProvider.h>
#endif
T114Board board; T114Board board;
#if defined(P_LORA_SCLK)
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
WRAPPER_CLASS radio_driver(radio, board); WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock; VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock); AutoDiscoverRTCClock rtc_clock(fallback_clock);
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
#if ENV_INCLUDE_GPS T114SensorManager sensors = T114SensorManager(nmea);
#include <helpers/sensors/MicroNMEALocationProvider.h>
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors;
#endif
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
DISPLAY_CLASS display; DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true); MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif #endif
bool radio_init() { bool radio_init() {
rtc_clock.begin(Wire); rtc_clock.begin(Wire);
#if defined(P_LORA_SCLK)
return radio.std_init(&SPI); return radio.std_init(&SPI);
#else
return radio.std_init();
#endif
} }
uint32_t radio_get_rng_seed() { uint32_t radio_get_rng_seed() {
@@ -54,11 +36,98 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setCodingRate(cr); radio.setCodingRate(cr);
} }
void radio_set_tx_power(int8_t dbm) { void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm); radio.setOutputPower(dbm);
} }
mesh::LocalIdentity radio_new_identity() { mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio); RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity return mesh::LocalIdentity(&rng); // create new random identity
}
void T114SensorManager::start_gps() {
if (!gps_active) {
gps_active = true;
_location->begin();
}
}
void T114SensorManager::stop_gps() {
if (gps_active) {
gps_active = false;
_location->stop();
}
}
bool T114SensorManager::begin() {
Serial1.begin(9600);
// Try to detect if GPS is physically connected to determine if we should expose the setting
pinMode(GPS_EN, OUTPUT);
digitalWrite(GPS_EN, HIGH); // Power on GPS
// Give GPS a moment to power up and send data
delay(1500);
// We'll consider GPS detected if we see any data on Serial1
gps_detected = (Serial1.available() > 0);
if (gps_detected) {
MESH_DEBUG_PRINTLN("GPS detected");
} else {
MESH_DEBUG_PRINTLN("No GPS detected");
}
digitalWrite(GPS_EN, LOW); // Power off GPS until the setting is changed
return true;
}
bool T114SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
return true;
}
void T114SensorManager::loop() {
static long next_gps_update = 0;
_location->loop();
if (millis() > next_gps_update) {
if (_location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
node_altitude = ((double)_location->getAltitude()) / 1000.0;
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
}
next_gps_update = millis() + 1000;
}
}
int T114SensorManager::getNumSettings() const {
return gps_detected ? 1 : 0; // only show GPS setting if GPS is detected
}
const char* T114SensorManager::getSettingName(int i) const {
return (gps_detected && i == 0) ? "gps" : NULL;
}
const char* T114SensorManager::getSettingValue(int i) const {
if (gps_detected && i == 0) {
return gps_active ? "1" : "0";
}
return NULL;
}
bool T114SensorManager::setSettingValue(const char* name, const char* value) {
if (gps_detected && strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
return true;
}
return false; // not supported
} }

View File

@@ -2,11 +2,11 @@
#define RADIOLIB_STATIC_ONLY 1 #define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h> #include <RadioLib.h>
#include <T114Board.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/radiolib/RadioLibWrappers.h> #include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/sensors/EnvironmentSensorManager.h> #include <T114Board.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/LocationProvider.h> #include <helpers/sensors/LocationProvider.h>
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
@@ -18,18 +18,37 @@
#endif #endif
#endif #endif
class T114SensorManager : public SensorManager {
bool gps_active = false;
bool gps_detected = false;
LocationProvider* _location;
void start_gps();
void stop_gps();
public:
T114SensorManager(LocationProvider &location): _location(&location) { }
bool begin() override;
bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
void loop() override;
LocationProvider* getLocationProvider() override { return gps_detected ? _location : NULL; }
int getNumSettings() const override;
const char* getSettingName(int i) const override;
const char* getSettingValue(int i) const override;
bool setSettingValue(const char* name, const char* value) override;
};
extern T114Board board; extern T114Board board;
extern WRAPPER_CLASS radio_driver; extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock; extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors; extern T114SensorManager sensors;
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display; extern DISPLAY_CLASS display;
extern MomentaryButton user_btn; extern MomentaryButton user_btn;
#endif #endif
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();

View File

@@ -14,7 +14,7 @@
#define USE_LFXO // 32.768 kHz crystal oscillator #define USE_LFXO // 32.768 kHz crystal oscillator
#define VARIANT_MCK (64000000ul) #define VARIANT_MCK (64000000ul)
#define WIRE_INTERFACES_COUNT (2) #define WIRE_INTERFACES_COUNT (1)
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Power // Power
@@ -58,11 +58,8 @@
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// I2C pin definition // I2C pin definition
#define PIN_WIRE_SDA (26) // P0.26 #define PIN_WIRE_SDA (16) // P0.16
#define PIN_WIRE_SCL (27) // P0.27 #define PIN_WIRE_SCL (13) // P0.13
#define PIN_WIRE1_SDA (7) // P0.8
#define PIN_WIRE1_SCL (8) // P0.7
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// SPI pin definition // SPI pin definition

View File

@@ -44,7 +44,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setCodingRate(cr); radio.setCodingRate(cr);
} }
void radio_set_tx_power(int8_t dbm) { void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm); radio.setOutputPower(dbm);
} }

View File

@@ -25,5 +25,5 @@ extern MomentaryButton user_btn;
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();

View File

@@ -32,11 +32,6 @@ build_flags =
-D PIN_TFT_LEDA_CTL=21 ; LEDK (switches on/off via mosfet to create the ground) -D PIN_TFT_LEDA_CTL=21 ; LEDK (switches on/off via mosfet to create the ground)
-D PIN_GPS_RX=33 -D PIN_GPS_RX=33
-D PIN_GPS_TX=34 -D PIN_GPS_TX=34
-D PIN_GPS_EN=35 ; N-ch MOSFET Q2 drives P-ch high-side switch → active HIGH (default)
-D PIN_GPS_RESET=36
-D PIN_GPS_RESET_ACTIVE=LOW
-D GPS_BAUD_RATE=115200
-D ENV_INCLUDE_GPS=1
-D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140 -D SX126X_CURRENT_LIMIT=140
@@ -48,31 +43,6 @@ lib_deps =
stevemarple/MicroNMEA @ ^2.0.6 stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
[env:Heltec_Wireless_Tracker_companion_radio_usb]
extends = Heltec_tracker_base
build_flags =
${Heltec_tracker_base.build_flags}
-I src/helpers/ui
-I examples/companion_radio/ui-new
-D DISPLAY_ROTATION=1
-D DISPLAY_CLASS=ST7735Display
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; -D BLE_PIN_CODE=123456 ; HWT will use display for pin
; -D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_tracker_base.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
+<helpers/ui/ST7735Display.cpp>
lib_deps =
${Heltec_tracker_base.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_Wireless_Tracker_companion_radio_ble] [env:Heltec_Wireless_Tracker_companion_radio_ble]
extends = Heltec_tracker_base extends = Heltec_tracker_base
build_flags = build_flags =

View File

@@ -16,8 +16,7 @@ WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock; ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock); AutoDiscoverRTCClock rtc_clock(fallback_clock);
// GPS_EN (GPIO35) drives N-ch MOSFET → P-ch high-side switch; GPS_RESET (GPIO36) active LOW MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, GPS_RESET, GPS_EN, &board.periph_power);
HWTSensorManager sensors = HWTSensorManager(nmea); HWTSensorManager sensors = HWTSensorManager(nmea);
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
@@ -48,7 +47,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setCodingRate(cr); radio.setCodingRate(cr);
} }
void radio_set_tx_power(int8_t dbm) { void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm); radio.setOutputPower(dbm);
} }
@@ -59,16 +58,18 @@ mesh::LocalIdentity radio_new_identity() {
void HWTSensorManager::start_gps() { void HWTSensorManager::start_gps() {
if (!gps_active) { if (!gps_active) {
_location->begin(); // Claims periph_power via RefCountedDigitalPin board.periph_power.claim();
gps_active = true; gps_active = true;
Serial1.println("$CFGSYS,h35155*68"); // Configure GPS for all constellations Serial1.println("$CFGSYS,h35155*68");
} }
} }
void HWTSensorManager::stop_gps() { void HWTSensorManager::stop_gps() {
if (gps_active) { if (gps_active) {
gps_active = false; gps_active = false;
_location->stop(); // Releases periph_power via RefCountedDigitalPin
board.periph_power.release();
} }
} }

View File

@@ -28,7 +28,6 @@ public:
const char* getSettingName(int i) const override; const char* getSettingName(int i) const override;
const char* getSettingValue(int i) const override; const char* getSettingValue(int i) const override;
bool setSettingValue(const char* name, const char* value) override; bool setSettingValue(const char* name, const char* value) override;
LocationProvider* getLocationProvider() override { return _location; }
}; };
extern HeltecV3Board board; extern HeltecV3Board board;
@@ -44,5 +43,5 @@ extern HWTSensorManager sensors;
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();

View File

@@ -6,26 +6,18 @@ void HeltecTrackerV2Board::begin() {
pinMode(PIN_ADC_CTRL, OUTPUT); pinMode(PIN_ADC_CTRL, OUTPUT);
digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive
// Set up digital GPIO registers before releasing RTC hold. The hold latches
// the pad state including function select, so register writes accumulate
// without affecting the pad. On hold release, all changes apply atomically
// (IO MUX switches to digital GPIO with output already HIGH — no glitch).
pinMode(P_LORA_PA_POWER, OUTPUT); pinMode(P_LORA_PA_POWER, OUTPUT);
digitalWrite(P_LORA_PA_POWER,HIGH); digitalWrite(P_LORA_PA_POWER,HIGH);
rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_POWER);
rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_EN);
pinMode(P_LORA_PA_EN, OUTPUT); pinMode(P_LORA_PA_EN, OUTPUT);
digitalWrite(P_LORA_PA_EN,HIGH); digitalWrite(P_LORA_PA_EN,HIGH);
rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_EN);
pinMode(P_LORA_PA_TX_EN, OUTPUT); pinMode(P_LORA_PA_TX_EN, OUTPUT);
digitalWrite(P_LORA_PA_TX_EN,LOW); digitalWrite(P_LORA_PA_TX_EN,LOW);
esp_reset_reason_t reason = esp_reset_reason();
if (reason != ESP_RST_DEEPSLEEP) {
delay(1); // GC1109 startup time after cold power-on
}
periph_power.begin(); periph_power.begin();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) { if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status(); long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep) if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
@@ -56,9 +48,7 @@ void HeltecTrackerV2Board::begin() {
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
// Hold GC1109 FEM pins during sleep to keep LNA active for RX wake rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_EN); //It also needs to be enabled in receive mode
rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER);
rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_EN);
if (pin_wake_btn < 0) { if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet

View File

@@ -17,16 +17,15 @@ build_flags =
-D P_LORA_SCLK=9 -D P_LORA_SCLK=9
-D P_LORA_MISO=11 -D P_LORA_MISO=11
-D P_LORA_MOSI=10 -D P_LORA_MOSI=10
-D P_LORA_PA_POWER=7 ; VFEM_Ctrl - GC1109 LDO power enable -D P_LORA_PA_POWER=7 ;power en
-D P_LORA_PA_EN=4 ; CSD - GC1109 chip enable (HIGH=on) -D P_LORA_PA_EN=4
-D P_LORA_PA_TX_EN=46 ; CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) -D P_LORA_PA_TX_EN=46 ;enable tx
-D LORA_TX_POWER=10 ; 10dBm + ~11dB GC1109 gain = ~21dBm output -D LORA_TX_POWER=10 ;If it is configured as 10 here, the final output will be 22 dbm.
-D MAX_LORA_TX_POWER=22 ; Max SX1262 output -> ~28dBm at antenna -D MAX_LORA_TX_POWER=22 ;Max SX1262 output
-D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140 -D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_RX_BOOSTED_GAIN=1
-D SX126X_REGISTER_PATCH=1
-D PIN_BOARD_SDA=5 -D PIN_BOARD_SDA=5
-D PIN_BOARD_SCL=6 -D PIN_BOARD_SCL=6
-D PIN_USER_BTN=0 -D PIN_USER_BTN=0

View File

@@ -50,7 +50,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setCodingRate(cr); radio.setCodingRate(cr);
} }
void radio_set_tx_power(int8_t dbm) { void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm); radio.setOutputPower(dbm);
} }

View File

@@ -26,5 +26,5 @@ extern EnvironmentSensorManager sensors;
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();

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