Compare commits

...

431 Commits

Author SHA1 Message Date
Scott Powell
def01889aa Merge branch 'dev' into multibyte-paths 2026-02-25 17:11:51 +11:00
Scott Powell
8737c64fdb * Packet::copyPath() fix 2026-02-25 17:10:31 +11:00
Liam Cottle
e6e87fb8ca Merge pull request #1838 from weebl2000/github_workflows_sanitycheck
Add basic sanity test github PR workflow
2026-02-25 14:52:10 +13:00
Wessel Nieboer
15cce12efd Add basic sanity test github PR workflow
Build a few generic variants to verify at least those compile. Can't
hurt.
2026-02-25 02:43:48 +01:00
Scott Powell
f4748a7f9d * misc 2026-02-24 21:30:04 +11:00
Scott Powell
b14879ce2d * CMD_GET_ADVERT_PATH bug fix 2026-02-24 14:23:59 +11:00
Liam Cottle
f7c8cf1146 Merge pull request #1808 from callum5892/dev
Added build flags for M5Stack Unit C6L
2026-02-24 12:24:56 +13:00
callum5892
9f4eeeeceb Added build flags for M5Stack Unit C6L
Enabled USB-CDC on boot for M5Stack_Unit_C6L_companion_radio_usb to fix serial connection issues
2026-02-23 17:31:18 +00:00
Scott Powell
9d5c4865c3 * room server fix 2026-02-24 01:08:11 +11:00
Scott Powell
213d085012 * revert CMD_SEND_SELF_ADVERT, use _prefs.path_hash_mode 2026-02-24 00:08:13 +11:00
Scott Powell
45564bad9b * Dispatcher bug fixes 2026-02-23 23:51:30 +11:00
Scott Powell
5b0884ad2d * added CMD_SET_PATH_HASH_MODE 2026-02-23 21:08:22 +11:00
Scott Powell
e52d57c065 * companion: new pref: path_hash_mode (0..2)
* companion: new field in CMD_SET_OTHER_PARAMS, path_hash_mode
* companion: CMD_SEND_SELF_ADVERT, cmd_frame[1] now holds the path hash size (0 = zero hop, 1..3 = flood path hash size)
2026-02-23 18:26:56 +11:00
Scott Powell
a66773bac0 * CommonCLI: added "get/set path.hash.mode " 2026-02-23 14:25:19 +11:00
Scott Powell
05e7b682b9 Merge branch 'dev' into multibyte-paths 2026-02-23 13:11:36 +11:00
ripplebiz
9c318561da Merge pull request #1792 from ElectroMW/feature/t-beam-supreme-improvements
T-Beam Supreme - Make full use of board's 8MB Flash and add Companion WiFi target
2026-02-23 12:38:56 +11:00
ripplebiz
2e0fa3ec46 Merge pull request #1794 from accumulator/heltec_wireless_tracker_companion_usb
add companion usb build target for Heltec Wireless Tracker
2026-02-23 12:37:02 +11:00
ripplebiz
8ee4867397 Merge pull request #1795 from DanielNovak/fix-packetqueue-millis-wraparound
Fix millis() wraparound in PacketQueue time comparisons
2026-02-23 12:33:21 +11:00
Sam Koucha
5a885bffe4 Make full use of board's 8MB Flash and add companion WiFI target 2026-02-22 18:14:39 +00:00
Daniel Novak
011edd3c99 Fix millis() wraparound in PacketQueue time comparisons
PacketQueue::countBefore() and PacketQueue::get() use unsigned
comparison (_schedule_table[j] > now) to check if a packet is
scheduled for the future. This breaks when millis() wraps around
after ~49.7 days: packets scheduled just before the wrap appear
to be in the far future and get stuck in the queue.

Use signed subtraction instead, matching the approach already used
by Dispatcher::millisHasNowPassed(). This correctly handles the
wraparound for time differences up to ~24.8 days in either
direction, well beyond the maximum queue delay of 32 seconds.
2026-02-22 18:01:55 +01:00
Sander van Grieken
3dc14976a0 add companion usb build target for Heltec Wireless Tracker 2026-02-22 17:57:36 +01:00
Scott Powell
3e76161e9c * refactor of Contact/Client out_path_len (stored in files), from signed to unsigned byte (+2 squashed commits)
Squashed commits:
[f326e25] * misc
[fa5152e] * new 'path mode' parsing in Dispatcher
2026-02-21 19:35:51 +11:00
ripplebiz
d05d6abab8 Merge pull request #1726 from weebl2000/fix-packet-pool-leak-queue-full
Fix packet pool leak when rx queue is full
2026-02-21 17:18:02 +11:00
ripplebiz
c2abe894c9 Merge pull request #1728 from oltaco/nrf52-bootloader-version
NRF52: Add get bootloader.ver command for NRF52
2026-02-21 12:56:52 +11:00
taco
1500a5a9cb add get bootloader.ver command for nrf52 2026-02-18 15:35:20 +11:00
Wessel Nieboer
ffc9815e9a Fix packet pool leak when rx queue is full
PacketQueue::add() silently dropped packets when the queue was at
capacity. The packet pointer was lost — never enqueued, never returned
to the unused pool. Each occurrence permanently shrank the 32-packet
pool until allocNew() returned NULL and the node went deaf. Return bool
from add() and free the packet back to the pool on failure.
2026-02-17 23:54:33 +01:00
Liam Cottle
bbc5f0c11a Merge pull request #1718 from realtag-github/repeater-v1.13-implement-discover
discover sends a single repeater discovery request and populates the neighbor list; self is excluded
2026-02-17 23:53:28 +13:00
Scott Powell
2e00298128 * companion: retransmit delays now hard-coded (only for client repeat mode) 2026-02-17 20:25:56 +11:00
Scott Powell
5de3e1bf32 * repeater: slight increase to default direct.txdelay 2026-02-17 20:10:13 +11:00
ripplebiz
a073ba4707 Merge pull request #1719 from 3dpgg/pr_lilygo_tlora_terminal_chat
Fix LilyGo_TLora_V2_1_1_6_terminal_chat build
2026-02-17 15:34:56 +11:00
3DPGG
3e53df5082 Fix LilyGo_TLora_V2_1_1_6_terminal_chat build
This change addresses two issues. The first is that the
LilyGo_TLora_V2_1_1_6_terminal_chat build would try to compile
simple_repeater/MyMesh.cpp. All other examples of terminal chat
targets are instead building simple_secure_chat/main.cpp . This
change would align this build to the rest of the builds.

The second issue, found during the course of investigating the
first, stems from simple_repeater/MyMesh.cpp using the
MAX_NEIGHBOURS #define to control whether the neighbor list is kept.
Repeaters that keep this list must define this value, and if the
value is not defined, then all neighbor-related functionality is
compiled out. However, the code that replies to
REQ_TYPE_GET_NEIGHBOURS did not properly check for this #define,
and thus any target that compiles simple_repeater/MyMesh.cpp
without defining MAX_NEIGHBOURS would get an undefined variable
compilation error.

As a practical matter though, there are no targets that compile
simple_repeater/MyMesh.cpp AND do not define MAX_NEIGHBOURS,
except this build due to the first issue. As a result, the
second issue is addressed only as a matter of completeness. The
expected behavior with this change is that such a repeater would
send a valid reply indicating zero known neighbors.
2026-02-16 18:10:29 -08:00
realtag
0770618ee2 Allow repeater discovery even if repeater mode is disabled on the requesting repeater. 2026-02-17 01:39:04 +00:00
realtag
bf9c6cb50f Increased the timeout timer to 60 seconds, up from 30 seconds. 2026-02-17 01:22:17 +00:00
realtag
87c78a98bd discover.neighbors sends a tagged repeater discovery request and only accepts matching repeater responses 2026-02-17 01:04:14 +00:00
realtag
e8785dd9b0 discover sends a single repeater discovery request and populates the neighbor list; self is excluded 2026-02-17 00:41:24 +00:00
ripplebiz
2005977403 Merge pull request #1699 from recrof/m5stack-m6l-build-fix
fix M5Stack Unit M6L build errors
2026-02-15 21:38:00 +11:00
recrof
cafc212bb2 fix M5Stack Unit M6L build errors 2026-02-15 11:25:27 +01:00
Scott Powell
e2571accbe * ver 1.13.0 2026-02-15 17:24:37 +11:00
ripplebiz
88452c412e Merge pull request #1603 from oltaco/fix-build.sh-for-RP2040-and-STM32
Add RP2040 and STM32 support to build.sh
2026-02-15 16:09:46 +11:00
ripplebiz
2220eca4f3 Merge pull request #1669 from Azuresword/fix/wio-tracker-l1-grove-sensor-v2
Fix WioTrackerL1 BLE companion: route sensors to Grove I2C bus (Wire1)
2026-02-15 15:55:25 +11:00
ripplebiz
a6e741e30e Merge pull request #1672 from ChaoticLeah/feature/mute-buzzer-icon
Add muted icon to show when buzzer is muted
2026-02-15 15:54:33 +11:00
Scott Powell
0abac35744 * client_repeat state now in _DEVICE_INFO response 2026-02-14 16:45:41 +11:00
Scott Powell
564a19d125 * companion client repeat mode support 2026-02-14 15:50:06 +11:00
taco
5df139f3d6 update build.sh to support RP2040 and STM32 2026-02-13 12:43:04 +11:00
taco
77675ab496 add -D ESP32_PLATFORM to esp32_base 2026-02-13 12:01:04 +11:00
Liam Cottle
5ccae4bddc Merge pull request #1671 from recrof/rak3112-fix
fix: usb and ui for rak 3112
2026-02-12 11:39:27 +13:00
Leah
fb025fb67e Add muted icon to show when buzzer is muted 2026-02-11 10:00:20 +01:00
Rastislav Vysoky
beff18c53b fix usb and build for rak 3112 2026-02-11 09:34:41 +01:00
dylan
f720338c03 Fix WioTrackerL1 BLE companion: route sensors to Grove I2C bus (Wire1)
Sensors connected via the Grove I2C connector (D18/D17) were not detected
because the firmware scanned the OLED I2C bus (Wire, D14/D15) by default.
Adding ENV_PIN_SDA/SCL flags directs EnvironmentSensorManager to use Wire1,
matching the physical Grove connector pinout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 14:12:48 +08:00
ripplebiz
e33d93dc7f Merge pull request #1611 from weebl2000/semtech-patch-tracker-v2
Enable register patch heltec tracker v2
2026-02-11 14:04:58 +11:00
ripplebiz
8db42146d1 Merge pull request #1645 from Snayler/Snayler-patch-1
Enable TX LED for LilyGo LoRa32 V2.1_1.6
2026-02-11 13:52:47 +11:00
Liam Cottle
e418b0c0ab Merge pull request #1557 from mattzzw/dev
cli_commands.md: `region` available via remote cli in 1.12.0
2026-02-11 11:58:11 +13:00
mattzzw
d11d8ea626 Merge branch 'meshcore-dev:dev' into dev 2026-02-10 22:33:43 +01:00
Liam Cottle
4944a46af1 Merge pull request #1638 from thanegill/build_sh-list
build.sh: add list and -l to list firmwares available to build
2026-02-10 22:27:35 +13:00
Liam Cottle
7d7e88e950 Merge pull request #1651 from liamcottle/fix/path-trace-timeout
Fix: Path Trace Timeout
2026-02-10 17:17:50 +13:00
liamcottle
bafa2ccd22 fix estimated timeout for multi byte path traces 2026-02-10 17:01:30 +13:00
Snayler
810fd561d2 Enable TX LED for LilyGo LoRa32 V2.1_1.6
Working on my device, green TX LED starts blinking every time I transmit
2026-02-09 23:20:29 +00:00
ripplebiz
3f33455b4d Merge pull request #1570 from Quency-D/fix-heltec-power-consumption
Fix heltec-v4 power consumption
2026-02-09 16:49:49 +11:00
ripplebiz
248c3107b4 Merge pull request #1605 from ViezeVingertjes/kiss-modem-spec-compliance
Kiss modem spec compliance
2026-02-09 16:45:20 +11:00
Quency-D
9efb7cb0b6 Merge pull request #2 from weebl2000/pr-1570-fix-heltec-v4-power
Fix double claim, eliminate dead code at compile time
2026-02-09 10:06:59 +08:00
Thane Gill
3ff1394dd2 build.sh: add list and -l to list firmwares available to build. 2026-02-08 14:49:57 -08:00
Michael
e4ef6dba3d Merge pull request #2 from agessaman/fix-simplify-kiss-noise-floor
Simplify kiss noise floor
2026-02-08 16:26:25 +01:00
Liam Cottle
b1094c2842 Merge pull request #1616 from jbrazio/2026/raknorm
Refactor environment names and build flags for RAK variants
2026-02-09 01:33:26 +13:00
ripplebiz
f6fc05601c Merge pull request #1609 from weebl2000/bridge-always-has-work
Bridge always has work (prevents sleep)
2026-02-08 22:42:05 +11:00
Liam Cottle
736ddbfd99 Merge pull request #1620 from thanegill/patch-2
Correct manufacturer name 'Elecrow ThinkNode M5'
2026-02-08 14:07:00 +13:00
Thane Gill
31a2e74ada Correct manufacturer name 'Elecrow ThinkNode M5' 2026-02-07 17:05:51 -08:00
Liam Cottle
10eacc4e95 Merge pull request #1316 from weebl2000/allow-negative-tx
Allow negative tx
2026-02-08 12:43:10 +13:00
João Brázio
fcfbb458f8 Refactor environment names and build flags for RAK variants 2026-02-07 21:26:28 +00:00
Wessel Nieboer
e8646f5ede Parse as signed int 2026-02-07 16:58:06 +01:00
agessaman
776131e263 simplify kiss noise floor sampling 2026-02-07 07:42:52 -08:00
Wessel Nieboer
23b4baa066 Enable register patch heltec tracker v2 2026-02-07 16:04:01 +01:00
Wessel Nieboer
c4c287d01b Bridge always has work (prevents sleep) 2026-02-07 15:39:24 +01:00
ViezeVingertjes
f6ebbd978e Remove redundant locals in handleSetRadio 2026-02-07 14:32:11 +01:00
ViezeVingertjes
5157daf1c1 Remove individual HW_RESP_* defines, use HW_RESP() macro directly 2026-02-07 14:24:34 +01:00
ViezeVingertjes
00b44c4114 Remove redundant send/complete/finished callbacks, use Radio interface directly 2026-02-07 14:22:21 +01:00
ViezeVingertjes
362b5eb0a1 Update protocol docs for new response codes and signal reporting 2026-02-07 10:26:08 +01:00
ViezeVingertjes
5ccd99e25f Add toggleable per-packet signal reporting 2026-02-07 10:21:36 +01:00
ViezeVingertjes
7982d1ce1f Use high-bit convention for hardware response codes 2026-02-07 10:21:32 +01:00
ViezeVingertjes
49e7516145 Add KISS UART support 2026-02-07 08:51:52 +01:00
agessaman
f445b5acdc fix(kiss_modem): improve RX delivery and noise floor sampling 2026-02-07 08:51:52 +01:00
ViezeVingertjes
1af013c741 Clarify data frame limitations in KISS modem documentation. 2026-02-07 08:51:52 +01:00
ViezeVingertjes
02ddc05c30 Reorganise KISS protocol to close gaps. 2026-02-07 08:51:52 +01:00
ViezeVingertjes
203d86f87d Update documentation. 2026-02-07 08:51:52 +01:00
ViezeVingertjes
f78617dbdb Add periodic noise floor calibration and AGC reset 2026-02-07 08:51:52 +01:00
ViezeVingertjes
5dcc377b77 Rewrite KISS modem to be fully spec-compliant 2026-02-07 08:51:52 +01:00
ripplebiz
bcb7a8067e Merge pull request #1578 from recrof/m5stack-unit-c6l
initial port of M5Stack Unit C6L
2026-02-07 14:21:47 +11:00
ripplebiz
c1c9848569 Merge pull request #1596 from jbrazio/2026/t114-sensors
Refactor Heltec T114 sensor management
2026-02-07 14:17:15 +11:00
ripplebiz
6502067368 Merge pull request #1591 from agessaman/fix-kiss-noise-floor
fix(kiss): periodic noise floor calibration and AGC reset
2026-02-07 14:08:26 +11:00
Wessel Nieboer
0b1fd580f1 Fix double claim, eliminate dead code at compile time 2026-02-06 11:35:05 +01:00
Wessel Nieboer
d0720c63c2 Allow negative tx power
Like SX1262 allows -9 dBm lowest, some allow lower but that probably
isn't useful
2026-02-06 02:24:51 +01:00
Adam Gessaman
c0b81b9ad8 Clean up comments on kiss noise floor changes. 2026-02-05 09:46:30 -08:00
João Brázio
18473333df Merge remote-tracking branch 'upstream/dev' into 2026/t114-sensors 2026-02-05 13:36:22 +00:00
João Brázio
5cb26b91f6 Refactor Heltec T114 sensor management 2026-02-05 13:35:04 +00:00
João Brázio
f0aa12faac Merge remote-tracking branch 'pikolin/T114_sensors' into 2026/t114-sensors 2026-02-05 12:57:27 +00:00
ripplebiz
f7e92a7cd1 Merge pull request #1398 from Socalix/heltecv4-register1
Improve Heltec v4 RX reception with undocumented register patch
2026-02-05 19:14:18 +11:00
ripplebiz
76a46a74e9 Merge pull request #1562 from oltaco/nrf52-sleep-repeater
Add Low-Power Sleep for nRF52 Boards
2026-02-05 13:16:47 +11:00
ripplebiz
4b9d5462bb Merge pull request #1532 from oltaco/esp32board.h-removewifi
Refactor ESP32Board.h to save ~500 bytes of DRAM
2026-02-05 13:09:04 +11:00
agessaman
0fb570338f fix(kiss): periodic noise floor calibration and AGC reset
- Trigger noise floor calibration every 2s and AGC reset every 30s in main loop.
- Reorder loop to match Dispatcher: calibrate + radio.loop() before AGC reset
  and recvRaw() so RSSI is never sampled right after startReceive().
- Update protocol doc with calibration intervals and typical noise floor range.
- Variant platformio.ini updates (heltec_v3, rak4631).
2026-02-03 20:58:39 -08:00
Liam Cottle
236025913d Merge pull request #1487 from oltaco/refactor-ui-with-ringbuffer
Show most recent message on display when no client is connected
2026-02-04 15:48:39 +13:00
taco
598489be47 refactor ui with ring buffer and display most recent 2026-02-04 09:38:05 +11:00
ripplebiz
e03f311e51 Merge pull request #1545 from ViezeVingertjes/kiss-modem-firmware
Add KISS-like Modem example and protocol documentation
2026-02-03 12:33:19 +11:00
Rastislav Vysoky
84e68cf4cb initial port of M5Stack Unit C6L, update pioarduino to newer bugfix release 2026-02-02 22:58:55 +01:00
ViezeVingertjes
f0ba14ff75 Remove sync word handling from KissModem. 2026-02-02 18:05:26 +01:00
Quency-D
e15503d50d Fix low power consumption issues 2026-02-02 14:19:42 +08:00
ripplebiz
ab994664ac Merge pull request #1549 from Meshcore-Portugal/jbrazio/t114-fix
Revert "Merge pull request #1428 from etienn01/update-t114-i2c"
2026-02-02 13:36:25 +11:00
Jan Pinkas
223930765c Enable I2C sensors and EnvironmentSensorManager for Heltec T114 2026-02-01 09:05:03 +01:00
taco
a342ab8437 nrf52: allow repeater to sleep when idle 2026-02-01 14:51:27 +11:00
ripplebiz
699d1cd0b6 Merge pull request #1495 from oltaco/esp32-advblob-removal
Fix unlimited advert blob growth on ESP32
2026-02-01 13:37:28 +11:00
ripplebiz
13cfc9ad4c Merge pull request #1498 from piotrmalek/fix-env-sensor-manager
Fix incorrect INA260 address in debug message
2026-02-01 13:34:58 +11:00
ripplebiz
f231347131 Merge pull request #1512 from litruv/dev
Refactor display scaling definitions for HELTEC_VISION_MASTER_T190
2026-02-01 13:33:53 +11:00
ripplebiz
852c0b0b78 Merge pull request #1547 from oltaco/t114-remove-extra-dcdcen
Heltec T114: Remove extra DCDC enable call
2026-02-01 13:18:21 +11:00
Matthias Wientapper
2b754d4295 cli_commands.md: region available via remote cli in 1.12.0
changed with #1476
2026-01-31 23:20:56 +01:00
ViezeVingertjes
240b5ea1e3 Refactor KissModem to integrate radio and sensor management directly, removing callback dependencies. 2026-01-31 15:15:19 +01:00
ViezeVingertjes
1bcb52bab3 Add new commands and responses for RSSI, channel status, airtime, noise floor, statistics, battery, and sensors. 2026-01-31 15:05:25 +01:00
João Brázio
b5248faec4 Revert "Merge pull request #1428 from etienn01/update-t114-i2c"
This reverts commit 616eb57b16, reversing
changes made to 537acd7ea1.

This patch needs to be reverted because it boot freezes t114 433Mhz variant.
2026-01-31 13:45:58 +00:00
taco
8d5eaf500d add makeBlobPath inline helper for esp32 2026-02-01 00:02:30 +11:00
taco
31ba971c60 only store advblob when adding/updating contacts 2026-02-01 00:02:30 +11:00
taco
e6e1b810f8 add DataStore::deleteBlobByKey() 2026-02-01 00:02:29 +11:00
taco
c5b1d30280 t114: remove extra DCDC enable 2026-01-31 23:48:28 +11:00
ViezeVingertjes
c786cfe613 Add KISS Modem firmware 2026-01-31 10:22:32 +01:00
Liam Cottle
06a83c0453 Merge pull request #1531 from agessaman/add-recv-errors-stats
Add recv_errors to CMD_GET_STATS STATS_TYPE_PACKETS response
2026-01-30 20:57:07 +13:00
agessaman
019bbf74d3 Add recv_errors to CMD_GET_STATS STATS_TYPE_PACKETS response
Append uint32_t recv_errors (RadioLib receive/CRC errors) to packet stats
binary frame. Frame size 26 -> 30 bytes. Update stats_binary_frames.md and
Python/TypeScript parsing examples for backward compatibility (accept >=26).
2026-01-29 20:44:11 -08:00
taco
c7eea3915d fix: remove esp_wifi.h from esp32board.h
saves ~500 bytes of dram and allows Tbeam to compile again
2026-01-30 15:07:40 +11:00
ripplebiz
cf7d95c6de Merge pull request #1509 from stevenlafl/tbeam-1w
Add LilyGO T-Beam 1W Support
2026-01-30 14:55:17 +11:00
ripplebiz
e60fb14e88 Merge pull request #1500 from Meshcore-Portugal/jbrazio/2026_03a6aa94
Allow usage of "/" in radio names
2026-01-30 12:09:08 +11:00
Liam Cottle
e50fe31fd6 Merge pull request #1528 from recrof/revert-1437-dev
Revert "Remove _serial->isConnected() logic from buzzer notifications"
2026-01-30 12:16:12 +13:00
Rastislav Vysoky
c345f1da8e Revert "Remove _serial->isConnected() logic from buzzer notifications" 2026-01-30 00:12:04 +01:00
SoCalix
31fbb679d5 Merge branch 'dev' into heltecv4-register1 2026-01-29 09:12:35 -06:00
fdlamotte
acca73f57e Merge pull request #1524 from recrof/thinknode-m3-m6-fixes
Elecrow ThinkNode M3, M6 build errors fix after NRF52Board base class migration
2026-01-29 11:07:32 -04:00
Rastislav Vysoky
2a321b53eb renamed board files 2026-01-29 16:00:19 +01:00
Rastislav Vysoky
3a7ccc085d fixed build errors and typos/inconsistencies 2026-01-29 15:32:51 +01:00
Scott Powell
465776d667 * ver 1.12.0 2026-01-29 21:12:31 +11:00
Steven Linn
a9a8299e14 Set LilyGO T-Beam 1W to use TX0 3.0V (within reference +2.85V~+3.15V) 2026-01-28 22:24:57 -07:00
Steven Linn
44e7c092c8 Add battery min/max voltage parameter support 2026-01-28 22:24:49 -07:00
Steven Linn
f7e54ea797 Add LilyGO T-Beam 1W Support 2026-01-28 22:24:43 -07:00
Liam Cottle
629adc23c5 Merge pull request #1508 from recrof/rak4631-cleanup
cleanup: moved RAK4631 pindefs from board file to variant.h
2026-01-29 12:35:47 +13:00
Liam Cottle
8f605f83fc Merge pull request #1507 from recrof/rak3401-board-fix
fix: build errors because of changes in RAK3401Board base class
2026-01-29 12:05:58 +13:00
Max Litruv Boonzaayer
dd2a9044f3 Refactor display scaling definitions for HELTEC_VISION_MASTER_T190 2026-01-29 08:05:26 +11:00
Rastislav Vysoky
f41872420e moved pindefs from board file to variant.h 2026-01-28 17:28:48 +01:00
Rastislav Vysoky
d5a73b2394 fix: build errors because of changes in NRF52 base class 2026-01-28 17:18:39 +01:00
ripplebiz
93367b9f8f Merge pull request #1493 from Cisien/dev
Add a cli command reference document
2026-01-28 15:44:26 +11:00
Liam Cottle
3fc736e3b0 Merge pull request #1499 from Meshcore-Portugal/jbrazio/2026_2768c833
Update runArgs in devcontainer.json
2026-01-28 16:44:02 +13:00
Chris
4e1e8bbffb Add a cli command reference document 2026-01-27 19:08:04 -08:00
João Brázio
edeafde51c Fix: Correct validation logic in isValidName function 2026-01-27 19:36:12 +00:00
ripplebiz
58a3782325 Merge pull request #1497 from oltaco/meshtiny-build-fix
Build fix for Meshtiny
2026-01-28 05:55:01 +11:00
João Brázio
9665feeebf Update runArgs in devcontainer.json 2026-01-27 16:57:54 +00:00
Piotr Małek
3845a1c021 Fix incorrect INA260 address in debug message 2026-01-27 16:29:31 +01:00
taco
4a83a6658a build fix for meshtiny (nrf52board ota refactor) 2026-01-28 00:59:42 +11:00
fdlamotte
ac79b38fa6 Merge pull request #1246 from fschrempf/nrf-dcdc
NRF52 boards: Enable internal DC/DC regulator to reduce power consumption and enable OTA support for all boards
2026-01-27 09:02:47 -04:00
fdlamotte
3f3978c7d3 Merge branch 'dev' into nrf-dcdc 2026-01-27 08:42:58 -04:00
ripplebiz
c0194d889a Merge pull request #1492 from oltaco/meshtiny
Support for Meshtiny
2026-01-27 19:54:16 +11:00
ripplebiz
fedf703262 Merge pull request #1486 from Quency-D/fix-heltec-v4-tft
Add heltec v4-tft code.
2026-01-27 19:47:20 +11:00
Scott Powell
5ff6e813bd * Fix: RegionMap build fail on _max 2026-01-27 18:16:21 +11:00
Scott Powell
5627500988 * new "clkreboot" CLI command 2026-01-27 15:22:18 +11:00
taco
5a20e8674f support for meshtiny 2026-01-27 14:15:02 +11:00
ripplebiz
d81616ec68 Merge pull request #1476 from mattzzw/region_via_LoRa
Add cli command `region list {allowed|denied}`, enable output of region cmd via remote cli
2026-01-27 11:07:22 +11:00
Matthias Wientapper
0805a47f35 Add output of region cmd via lora cli
Add cli commands "region list {allowed|denied}"
2026-01-26 17:44:43 +01:00
ripplebiz
f1be7d0914 Merge pull request #1488 from liamcottle/firmware/boot-adverts
Change advert on boot from flood to zero hop
2026-01-26 21:55:48 +11:00
liamcottle
7e24bd00b9 increase maximum flood advert interval to 168 hours (7 days) 2026-01-26 23:05:10 +13:00
liamcottle
d13bc446de added build flag to enable/disable boot advert 2026-01-26 22:39:39 +13:00
liamcottle
ed589f9620 boot adverts are now zero hop instead of flood 2026-01-26 22:20:36 +13:00
ripplebiz
4b7684c7df Merge pull request #1477 from Cisien/dev
Expose a counter to track RadioLib receive errors
2026-01-26 19:04:48 +11:00
Quency-D
c7ac16f0e3 Add v4-tft code. 2026-01-26 13:48:15 +08:00
Scott Powell
7ae164217c * region names now don't need '#' prefix. (SHA still adds a '#' for back compat) 2026-01-25 18:35:55 +11:00
Chris
c16bcd2fe3 Expose a counter to track RadioLib receive errors
This change counts when readData returns an err code other than RADIOLIB_ERR_NONE. In most cases this is going to be a CRC error. This counter is exposed in the `stats-packets` command, and in the repeater stats payload (4 additional bytes to the payload, which is now 56 bytes with this change. My incompetent robot claims the total payload size is 96 bytes (unverified but probably close).
2026-01-24 20:06:29 -08:00
ripplebiz
a5f3766016 Merge pull request #1429 from Snayler/dev
Fix Serial and TX LED not working on Heltec Wireless Paper V1.2
2026-01-25 14:58:07 +11:00
ripplebiz
f0269c9bff Merge pull request #1465 from recrof/rak3112-port
initial RAK 3112 support
2026-01-25 14:56:17 +11:00
ripplebiz
153bcdc6a3 Merge pull request #1457 from oltaco/remote-set-prvkey
Allow set prv.key over LoRa, clear ACL and validate key
2026-01-25 14:46:41 +11:00
taco
96ef5e5efe allow set prv.key from remote, validate new prv.key 2026-01-25 01:32:48 +11:00
taco
988287bfd7 recalc ClientACL shared_secrets at startup 2026-01-25 01:32:44 +11:00
taco
6336bd5b72 refactor ClientACL and CommonCLI, add ClientACL::clear() 2026-01-25 01:31:53 +11:00
Scott Powell
f46f0d0ed1 * WIO tracker l1: BLE companion. default node name now MAC address 2026-01-24 22:08:05 +11:00
ripplebiz
c7b3d34963 Merge pull request #1456 from Quency-D/fix-env-i2c
Fix env i2c errors
2026-01-24 21:58:14 +11:00
ripplebiz
e744adfa39 Merge pull request #1413 from entr0p1/powermgt-nrf52840-v2
nRF52840 Power Management v2 Phase 1 - LPCOMP Wake and startup lockout
2026-01-24 21:51:06 +11:00
Liam Cottle
b853c7ced5 Merge pull request #1459 from oltaco/fix-roomserver-debug
Build fix for room server with MESH_DEBUG
2026-01-24 19:31:31 +13:00
Rastislav Vysoky
266f6ee856 fixed battery measurement 2026-01-23 23:35:00 +01:00
Rastislav Vysoky
e7c72c5c6a initial port of rak3112 2026-01-23 22:26:24 +01:00
taco
9dd52bd0cc build fix for room server with MESH_DEBUG=1 2026-01-23 23:43:05 +11:00
entr0p1
1f59e52880 nRF52840 Power Management - Phase 1 - Boot Low VBAT Voltage Lockout
Added NRF52840 power management core functionality:
- Boot‑voltage lockout
- Initial support for shutdown/reset reason storage and capture (via RESETREAS/GPREGRET2)
- LPCOMP wake (for voltage-driven shutdowns)
- VBUS wake (for voltage-driven shutdowns)
- Per-board shutdown handler for board-specific tasks
- Exposed CLI queries for power‑management status in CommonCLI.cpp
- Added documentation in docs/nrf52_power_management.md.
- Enabled power management support in Xiao nRF52840, RAK4631, Heltec T114 boards
2026-01-23 17:18:41 +11:00
Scott Powell
3c27132914 * T1000e BLE - default node name is now the MAC address 2026-01-23 15:53:58 +11:00
Quency-D
fc61018d4d Fix the issue of inconsistent I2C usage in the environmental sensor. 2026-01-23 10:45:13 +08:00
ripplebiz
616eb57b16 Merge pull request #1428 from etienn01/update-t114-i2c
Update T114 I2C pins for external RTC
2026-01-23 12:25:07 +11:00
ripplebiz
537acd7ea1 Merge pull request #1437 from nakoeppen/dev
Remove _serial->isConnected() logic from buzzer notifications
2026-01-23 12:20:57 +11:00
ripplebiz
32230f6167 Merge pull request #1415 from WattleFoxxo/StationG2-tx-power-changes
Change the Station G2's default TX power
2026-01-23 11:58:53 +11:00
Liam Cottle
bccefd6e37 Merge pull request #1445 from oltaco/thinknode_m1-gps-fix
ThinkNode M1 GPS fixes
2026-01-22 20:02:41 +13:00
taco
36f230d074 thinknode m1: allow GPS to sync clock 2026-01-22 14:42:43 +11:00
taco
ea85486dca thinknode m1: add missing GPS page to new UI 2026-01-22 14:42:08 +11:00
taco
b09ddfc5e1 thinknode m1: add missing getLocationProvider() override 2026-01-22 14:41:07 +11:00
Socalix
46e4cc06e3 Revert boosted gain flag to original 2026-01-21 21:12:54 -06:00
nakoeppen
d68bc74514 Remove _serial->isConnected() logic from buzzer notifications 2026-01-20 20:19:10 -06:00
Miguel de Matos
a7cadc8e44 Fix Serial and TX LED not working on Heltec Wireless Paper V1.2
As described on #1276, tested and working on my heltec wireless paper v1.2
2026-01-20 01:52:45 +00:00
Étienne Fesser
e51a2d1ba0 Update T114 I2C pins 2026-01-19 21:39:01 +01:00
ripplebiz
56ab59ded2 Merge pull request #1387 from chrisdavis2110/rak3401
Add Variant rak3401 (for new 1W booster kit)
2026-01-19 16:11:07 +11:00
ripplebiz
bf0777845a Merge pull request #1408 from oltaco/improved-contact-mgmt
Contact management tweaks
2026-01-19 12:29:52 +11:00
chrisdavis2110
ed5d2909fc updated variant rak3401 2026-01-17 22:54:20 -08:00
Chris Davis
5e4b33a1a0 Merge pull request #4 from chrisdavis2110/var-rak3401
Var rak3401
2026-01-17 22:47:25 -08:00
WattleFoxxo
5c7b28f110 Change the Station G2 default tx power
set the default TX power to 7dBm to avoid illegal power output by default.
2026-01-18 14:29:50 +11:00
taco
b919119faf only write contacts when changed 2026-01-16 13:15:35 +11:00
taco
c61fde9328 always send PUSH_CODE_NEW_ADVERT when advert was not added to contacts[] 2026-01-16 13:15:35 +11:00
Liam Cottle
7d1f52252b Merge pull request #1402 from recrof/v3-usb-contact-fix
fix: bump max contacts for heltec v3 companion usb
2026-01-16 09:50:06 +13:00
Rastislav Vysoky
11565673c3 fix: bump max contacts for v3 companion usb 2026-01-15 15:39:44 +01:00
Liam Cottle
23f1f2a3fa Merge pull request #1399 from mannkind/patch-1
Fix Ikoka Stick builds
2026-01-15 23:32:34 +13:00
ripplebiz
d41a968d1d Merge pull request #1379 from oltaco/improved-contact-mgmt
Companion: Improved Contact Management
2026-01-15 20:36:28 +11:00
taco
df6687034a bootstrap RTC from contact.lastmod and improve slot overwrite logic
slot overwrite logic can now safely use contact.lastmod to find oldest contact for overwrite
2026-01-15 18:01:20 +11:00
taco
741564dd48 refactor: add populateContactFromAdvert() 2026-01-15 18:01:20 +11:00
taco
403ce1db08 contacts: granular autoadd and overwrite-oldest 2026-01-15 18:01:20 +11:00
Dustin Brewer
31f98bdd43 Fix Ikoka Stick builds 2026-01-14 17:53:42 -08:00
Socalix
4575800e40 Turn on register 0x8B5 LSB for improved RX, turn off boosted gain 2026-01-14 17:52:15 -06:00
Liam Cottle
56eb5b0499 Merge pull request #1373 from liquidraver/buildwithoutdebug
DISABLE_DEBUG=1 env variable to build.sh
2026-01-15 06:36:00 +13:00
chrisdavis2110
06c4ca19ab added variant rak3401 2026-01-13 10:06:50 -08:00
liquidraver
a48b185189 DISABLE_DEBUG=1 env variable to build.sh 2026-01-13 12:48:53 +01:00
Liam Cottle
4643f4d3a3 Merge pull request #1378 from recrof/ikoka-cleanup
cleanup ikoka variants and add all supported sensors
2026-01-13 17:21:24 +13:00
Liam Cottle
77257a376b Merge pull request #1377 from recrof/t3s3-sx1276-fix
FIX: remove serial debug logging from t3s3 sx1276 companion usb
2026-01-13 10:34:45 +13:00
Rastislav Vysoky
324eab9394 cleanup ikoka variants and add all supported sensors 2026-01-12 19:29:32 +01:00
Rastislav Vysoky
266e4893fd remove serial debug logging from t3s3 sx1276 companion usb 2026-01-12 19:19:23 +01:00
Scott Powell
bafbfaf2b5 Merge branch 'regions-request' into dev 2026-01-12 17:48:19 +11:00
Scott Powell
69a71d0e25 * repeater login response, FIRMWARE_VER_LEVEL now bumped to 2 2026-01-12 17:47:51 +11:00
Scott Powell
b6110eee38 * new req/resp (after login): REQ_TYPE_GET_OWNER_INFO (includes firmware-ver)
* ANON_REQ_TYPE_OWNER, firmware-ver removed (security exploit)
* ANON_REQ_TYPE_BASIC, formware-ver removed, just remote clock + some 'feature' bits
* CTL_TYPE_NODE_DISCOVER_REQ now ingored if 'repeat off' has been set
2026-01-12 16:58:35 +11:00
Scott Powell
4e4f6d92a0 * ANON_REQ_TYPE_VER_OWNER now delimited by newline chars 2026-01-09 16:32:08 +11:00
Scott Powell
65796c8f20 * CommonCLI: added "set name ..." validation
* ANON_REQ_TYPE_VER_OWNER, now removes commas from node_name
2026-01-09 16:28:08 +11:00
Scott Powell
fd69acb421 * new ANON_REQ_TYPE_VER (for just simple clock + ver info) 2026-01-09 13:54:18 +11:00
Scott Powell
2a035ad816 * ANON_REQ_TYPE_VER_OWNER, now includes node_name 2026-01-09 13:20:20 +11:00
Scott Powell
5475043083 * new ANON_REQ_TYPE_VER_OWNER
* CommonCLI: new "get/set owner.info ..."
2026-01-09 11:07:31 +11:00
Frieder Schrempf
4f46ec75dd Remove NRF52BoardOTA class and integrate it into NRF52Board
As all NRF52 boards now have OTA support, let's remove the subclass
and integrate it into the base class.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:46:20 +01:00
Frieder Schrempf
686d887f72 variants: T1000E: Add OTA support
To reduce the number of different code paths, add OTA support for the
remaining NRF52 boards.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:46:09 +01:00
Frieder Schrempf
1651db81f9 variants: Sensecap Solar: Use DC/DC regulator
The schematic shows the LC circuit for the internal DC/DC regulator
to be available. Enable it to save power.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:44:23 +01:00
Frieder Schrempf
80ca720002 variants: ProMicro: Use DC/DC regulator
The schematic shows the LC circuit for the internal DC/DC regulator
to be available. Enable it to save power.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:44:22 +01:00
Frieder Schrempf
137eed3ede variants: Minewsemi ME25LS01: Use DC/DC regulator
The schematic shows the LC circuit for the internal DC/DC regulator
to be available. Enable it to save power.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:44:21 +01:00
Frieder Schrempf
465b481a2e variants: Mesh Pocket: Use DC/DC regulator
The schematic shows the LC circuit for the internal DC/DC regulator
to be available. Enable it to save power.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:44:20 +01:00
Frieder Schrempf
bf93d6cf7a variants: Lilygo T-Echo (Lite): Use DC/DC regulator
The schematic shows the LC circuit for the internal DC/DC regulator
to be available. Enable it to save power.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:44:19 +01:00
Frieder Schrempf
041f67ab71 variants: Ikoka NRF: Use DC/DC regulator
The Ikoka boards are based on the Xioa NRF52840 module which is known
to have the LC circuit for the internal DC/DC regulator to be
available. Enable it to save power.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:44:18 +01:00
Frieder Schrempf
3b0870e2c1 variants: Heltec T114: Use DC/DC regulator
The schematic shows the LC circuit for the internal DC/DC regulator
to be available. Enable it to save power.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:44:16 +01:00
Frieder Schrempf
24a4b99e31 variants: Heltec Mesh Solar: Use DC/DC regulator
The schematic shows the LC circuit for the internal DC/DC regulator
to be available. Enable it to save power.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:44:15 +01:00
Frieder Schrempf
578d55b28a variants: Thinknode M3/M6: Use common Nrf52Board class
Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:44:14 +01:00
Frieder Schrempf
57fa1ba854 variants: Wio WM1110: Use common implementation of startOTAUpdate()
Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:44:13 +01:00
Frieder Schrempf
fa48d4fe81 variants: Nano G2 Ultra: Use common implementation of startOTAUpdate()
Signed-off-by: Frieder Schrempf <frieder@fris.de>
2026-01-08 22:43:38 +01:00
Liam Cottle
5b7f66712c Merge pull request #1337 from fmckeogh/dev
Fix capitalization in T1000-E manufacturer string
2026-01-08 23:31:10 +13:00
Scott Powell
5cc44dd802 * ANON_REQ_TYPE_REGIONS now direct only, with reply_path encoded in request 2026-01-08 13:20:52 +11:00
Ferdia McKeogh
55fc03b109 Fix capitalization in T1000-E manufacturer string 2026-01-07 14:24:25 +01:00
Scott Powell
8d51126956 Merge branch 'dev' into regions-request 2026-01-08 00:21:08 +11:00
Liam Cottle
ff973e43b9 Merge pull request #1334 from olanwe/wifi-queuesize
Apply #1331 to other WiFi companions
2026-01-07 22:47:13 +13:00
ripplebiz
3eaaf96ed3 Merge pull request #1300 from fschrempf/fix-rak4631-gps
Fix RAK4631 GPS Detection
2026-01-07 14:56:58 +11:00
ripplebiz
ebfe6e4ba5 Merge pull request #1320 from alex-vg/dev
Add Xiao_S3_WIO WiFi companion env (Xiao_S3_WIO_companion_radio_wifi)
2026-01-07 14:37:32 +11:00
Oliver Weyhmüller
a7a6bb51ce Apply #1331 to other WiFi companions 2026-01-07 03:40:00 +01:00
Liam Cottle
c14362d80f Merge pull request #1331 from an0key/wifi-offlinequeuesizemissing
OFFLINE_QUEUE_SIZE for Heltec Wifi companions
2026-01-07 10:46:56 +13:00
Luke
d4a2e5789f OFFLINE_QUEUE_SIZE for Heltec Wifi companions
OFFLINE_QUEUE_SIZE missing from h3/h4 wifi companions, causing them only store 16 messages.
2026-01-06 14:49:15 +00:00
alex-vg
818f5e9da5 variants: Xiao_S3_WIO: Add WiFi companion env 2026-01-05 02:25:30 +01:00
ripplebiz
09005fa455 Merge pull request #1308 from liamcottle/fix/wifi-interface-frames
Fix: WiFi Interface Frame Parsing
2026-01-04 16:19:57 +11:00
liamcottle
8708fa012a simplify reading frame header 2026-01-04 17:43:25 +13:00
ripplebiz
c5c67ee1a5 Merge pull request #1313 from recrof/thinknode_m6_companion_fix
fix compilation errors for m6 companion roles
2026-01-04 13:30:52 +11:00
Liam Cottle
badcefb9f8 Merge pull request #1317 from cj-vana/fix/typos
Fix typos in README and source comments
2026-01-04 14:57:43 +13:00
cj-vana
63767cdb7d Fix typos in README and source comments 2026-01-03 18:54:48 -07:00
Rastislav Vysoky
63ae92aa09 fix compilation errors for m6 companion roles 2026-01-03 16:32:36 +01:00
liamcottle
71bb49e556 remove use of dynamic allocation 2026-01-03 16:36:19 +13:00
liamcottle
ed263b0727 implement frame header parising for wifi interface 2026-01-03 15:39:57 +13:00
Scott Powell
3af25495bb * Repeater: new anon request sub-type: ANON_REQ_TYPE_REGIONS (rate limited to max 4 every 3 mins)
* Companion: new CMD_SEND_ANON_REQ command (reply with existing RESP_CODE_SENT frame)
2026-01-03 12:02:15 +11:00
ripplebiz
e31c46ff56 Merge pull request #1294 from liquidraver/factorynvsreset
Add NVS partition reset to ESP factory reset
2026-01-03 11:57:14 +11:00
liquidraver
faf177de46 ESP factory reset clear NVS too 2026-01-02 08:37:22 +01:00
Scott Powell
813e502970 * added protocol_guide doc 2026-01-02 12:54:57 +11:00
ripplebiz
2f5a8c59ea Merge pull request #1299 from entr0p1/companion-timestamp-fix
BUGFIX: Companion packet timestamp mismatch trips replay protection
2026-01-02 12:44:45 +11:00
Frieder Schrempf
ab7935142c EnvironmentSensorManager.cpp: Cleanup after failed RAK4631 GPS detection
If no GPS was detected, revert the hardware to the initial state,
otherwise we may see conflicts or increased power consumption on some
boards.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-12-31 14:42:42 +01:00
Frieder Schrempf
e79ee11872 EnvironmentSensorManager.cpp: Fix RAK4631 serial GPS detection
Serial1 is always true. If we want to check for the presence of a GPS
receiver, we need to check if any data was received.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-12-31 14:42:41 +01:00
Liam Cottle
84b84717cc Merge pull request #1293 from weebl2000/gitignorevenv
Add common venv dirs to .gitignore
2026-01-01 01:29:55 +13:00
Wessel Nieboer
7ea751d3a0 Add venv dirs to .gitignore 2025-12-31 13:01:56 +01:00
ripplebiz
f9720f0b0c Merge pull request #1266 from IoTThinks/MCdev-Powersaving-for-esp32-202512
Added powersaving to all ESP32 boards with RTC-supported DIO1
2025-12-31 11:35:46 +11:00
entr0p1
4a869163b2 BUGFIX: replay protection on repeaters tripped by timestamp sent from companion node mobile app. Send the node's RTC timestamp for TXT_TYPE_CLI_DATA messages instead of the timestamp from the app (matches the sendRequest() code logic). 2025-12-30 21:58:59 +11:00
Kevin Le
d911a34eeb Used esp_wifi_get_mode instead of WiFi.getMode() to reduce the code size 2025-12-29 22:38:05 +07:00
Kevin Le
33b1e7edb9 Added pad after powersaving_enabled 2025-12-29 21:49:13 +07:00
ripplebiz
8edbb085fb Merge pull request #1254 from entr0p1/tx-led-fix-v2
Fix TX LED stuck on when StartTransmit() fails
2025-12-29 16:09:08 +11:00
ripplebiz
1c594d4cbd Merge pull request #1274 from IoTThinks/MCdev-FixedMCUTemperature
To fix MCU Temperature for repeaters
2025-12-29 15:05:31 +11:00
ripplebiz
1d9d37c654 Merge pull request #1247 from entr0p1/dev
Fixed T1000-E temperature, lux and BME280 sensor reading accuracies
2025-12-29 12:42:05 +11:00
Liam Cottle
3d6e523ec8 Merge pull request #1281 from Meshcore-Portugal/jbrazio/promicro_rs232
Add RS232 bridge environment configuration for ProMicro
2025-12-29 12:57:00 +13:00
João Brázio
992d971f07 Add RS232 bridge environment configuration for ProMicro 2025-12-28 20:04:57 +00:00
Scott Powell
90d1e87ba1 * check for 'early receive' ACK 2025-12-27 20:46:28 +11:00
Kevin Le
0b30d2433f To get and average the temperature so it is more accurate, especially in low temperature 2025-12-27 15:25:21 +07:00
Kevin Le
26321162ee To fix the default temperature to be overridden by external sensors (if any) 2025-12-27 15:23:23 +07:00
Kevin Le
def1902688 Fixed T-Beam board to work with sleep 2025-12-24 12:04:39 +07:00
Kevin Le
0d11a02e71 Added extra check for P_LORA_DIO_1 before going to sleep 2025-12-24 11:47:19 +07:00
Kevin Le
89a289eb22 Added powersaving_enabled sanitization
Moved powersaving_enabled to match serialization order
2025-12-24 11:23:19 +07:00
Kevin Le
1706f759b7 Modified hasPendingWork to return bool 2025-12-24 11:00:34 +07:00
Kevin Le
5c6c15942b Added powersaving to all ESP32 boards with RTC-supported DIO1
Added CLI to enable/disable powersaving
2025-12-23 12:48:08 +07:00
entr0p1
245a818085 Fix TX LED stuck on when StartTransmit() fails 2025-12-20 23:15:41 +11:00
entr0p1
cc28b1a34d EnvironmentSensorManager.cpp: Mitigate BME280 self-heating causing inaccurate readings. 2025-12-20 21:51:51 +11:00
entr0p1
6c993827de Fixed T1000-E temperature and lux sensors 2025-12-19 23:51:36 +11:00
Liam Cottle
0c3fb918b2 Merge pull request #1203 from liquidraver/fix-gps-popup
Fix GPS/Buzzer toggle UI popup
2025-12-18 21:39:25 +13:00
liquidraver
e855706abb move showalert after saveprefs 2025-12-17 21:27:22 +01:00
fdlamotte
2ddd5ca0c3 Merge pull request #1235 from liquidraver/btfixv7
queue throttling + slave latency and minor refactor
2025-12-17 15:08:20 +01:00
liquidraver
cba29ea50c queue throttling + slave latency and minor refactor 2025-12-17 13:46:58 +01:00
fdlamotte
9b13106b6f Merge pull request #1201 from fschrempf/nrf52-board-deduplication
NRF52 Board Code Deduplication
2025-12-17 11:29:33 +01:00
Frieder Schrempf
8eb229bcf8 variants: RAK4631: Enable DC/DC regulator to reduce power consumption
The RAK4631/RAK4630 module are able to use the DC/DC converter. Enable
it to reduce power consumption.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-12-17 10:39:55 +01:00
Frieder Schrempf
22b1585959 NRF52Board.h: Mark getMCUTemperature() as virtual
The function in the derived class is virtual per definition. Mark it
to make this clearer to the reader.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-12-17 10:39:54 +01:00
Frieder Schrempf
b024b9e1a1 Deduplicate NRF52 startOTAUpdate()
The startOTAUpdate() is the same for all NRF52 boards. Use a common
implementation for all boards that currently have a specific
implementation.

The following boards currently have an empty startOTAUpdate() for
whatever reasons and therefore are not inheriting NRF52BoardOTA to
keep the same state: Nano G2 Ultra, Seeed SenseCAP T1000-E,
Wio WM1110.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-12-17 10:30:50 +01:00
Frieder Schrempf
e3bb225efb Deduplicate DC/DC regulator enable for NRF52 boards
Some NRF52 boards are able to use the internal power-efficient DC/DC
regulator. Add a new class that can be inherited by board classes to
enable this feature and reduce the power consumption.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-12-17 10:29:14 +01:00
Frieder Schrempf
93d1560d14 Use common NRF52 begin() and deduplicate() startup reason init
Use a common begin() method that can be called from derived classes
to contain the shared initialization code.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-12-17 10:26:57 +01:00
Frieder Schrempf
87b0e432bb Deduplicate reboot() for NRF52 boards
The reboot() method is the same for all NRF52 boards. Use a shared
implementation.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-12-17 10:25:16 +01:00
Frieder Schrempf
6486192477 variants: IkokaNrf52Board: Use NRF52Board base class
Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-12-17 10:22:15 +01:00
ripplebiz
d67f311c3d Merge pull request #1206 from IoTThinks/MCdev-MCUTemperature-for-repeaters-202512
Added default temperature from ESP32 MCU and NRF52 MCU
2025-12-15 19:37:34 +11:00
fdlamotte
f38b951e87 Merge pull request #1142 from Meshcore-Portugal/jbrazio/2025_7bc6ab2c
Add devcontainer configuration for vscode
2025-12-13 09:07:05 +01:00
Kevin Le
2deb9cf144 Fixed to call getMCUTemperature once. 2025-12-13 07:32:26 +07:00
João Brázio
0df8c86b98 Refactor devcontainer runArgs 2025-12-12 17:24:28 +00:00
Florent
aba868f324 Merge branch 'thinknode_m3_port' into dev 2025-12-12 17:20:06 +01:00
Florent
bde4fc3a23 thinknode_m3: initial commit 2025-12-12 17:16:28 +01:00
Florent
e7ed69bdb6 Merge branch 'thinknode_m6_port' into dev 2025-12-12 16:39:37 +01:00
Florent
14efaf6fd3 thinknode_m6: initial port 2025-12-12 16:37:57 +01:00
Kevin Le
4504ad4daf Added default temperature from ESP32 MCU and NRF52 MCU
Added NRF52Board.h and NRF52Board.cpp
Modified NRF52 variants to extend from NRF52Board to share common feature
2025-12-12 19:01:15 +07:00
ripplebiz
9bba417ebc Merge pull request #1160 from flol/rak11310
Support for RAK11310 WisBlock
2025-12-11 10:47:12 +11:00
ripplebiz
f378e103c2 Merge pull request #1171 from luigi311/techo_hibernate_led
variants: lilygo_techo: variant: Turn off leds on poweroff
2025-12-11 10:24:05 +11:00
ripplebiz
fc4f9e8f33 Merge pull request #1197 from agessaman/LPS22HB-fix
fix output from LPS22HB sensor: convert barometric pressure from kPa to hPa
2025-12-11 10:14:10 +11:00
agessaman
b91b854a1d fix output from LPS22HB: convert barometric pressure from kPa to hPa in EnvironmentSensorManager 2025-12-08 19:53:33 -08:00
Liam Cottle
09c121efae Merge pull request #1178 from fschrempf/xiao-nrf-button-pullup-fix
Xiao NRF52: Enable pullup on button input
2025-12-07 19:18:48 +13:00
Scott Powell
676c317f78 * refactor: on-demand getSharedSecret() 2025-12-06 19:17:45 +11:00
ripplebiz
46f6146df7 Merge pull request #1180 from oltaco/shared-secret-on-demand
Companion: Faster startup by calculating shared_secret on demand
2025-12-06 18:59:19 +11:00
Scott Powell
d7adcc136b * LPPDataHelpers, readCurrent() signed value 2025-12-06 16:49:25 +11:00
taco
638f41d143 calculate shared_secret on demand 2025-12-06 16:21:17 +11:00
ripplebiz
9ee3008f88 Merge pull request #1177 from liquidraver/btfixv6
Fix BLE semaphore leak and improve SerialBLEInterface
2025-12-06 14:23:27 +11:00
ripplebiz
4040f201a8 Merge pull request #1179 from carroarmato0/tdeck-gps
Enable GPS for LilyGo TDeck + Optimizations
2025-12-06 14:11:58 +11:00
Christophe Vanlancker
01eb8716af fix(core): optimize GPS loop and add display GPIO safeguards 2025-12-05 20:45:10 +01:00
Christophe Vanlancker
d834d66803 feat(tdeck): enable GPS support and configure pins 2025-12-05 20:44:56 +01:00
Frieder Schrempf
10b43a8f9f variants: XIAO NRF52: Enable button pullup
Some versions of the Wio-SX1262 board don't have the button and the
pullup resistor populated. Enable the internal pullup to prevent
a floating pin and spurious button presses on those boards.

This fixes #1173.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-12-05 12:40:12 +01:00
liquidraver
73ab0d8813 Improve SerialBLEInterface 2025-12-05 07:39:48 +01:00
Florent de Lamotte
6db57677f9 tracker_l1: enable dc/dc converter 2025-12-04 12:01:00 +01:00
liquidraver
1a3f7a7ea9 Fix BLE semaphore leak in Bluefruit library
Patches Bluefruit library to fix semaphore leak bug that causes device lockup
when BLE central disconnects unexpectedly (e.g., going out of range, supervision timeout).

Co-authored-by: Liam Cottle <liamcottle@users.noreply.github.com>
Co-authored-by: oltaco <oltaco@users.noreply.github.com>
2025-12-04 11:47:41 +01:00
fdlamotte
01f7a3c95e Merge pull request #1057 from liquidraver/wiodev
Disable screen switching on when connected
2025-12-04 11:10:51 +01:00
Luis Garcia
ec375fa248 variants: lilygo_techo: variant: Turn off leds on poweroff
Signed-off-by: Luis Garcia <git@luigi311.com>
2025-12-03 19:30:47 -07:00
ripplebiz
441d768ddb Merge pull request #1172 from oltaco/nrf52-power-changes
NRF52 Power related changes
2025-12-03 17:50:15 +11:00
taco
e1d3da942b fix DC/DC enable for boards which currently have it.
this fixes how the reg1 dc/dc converter is enabled on WisMesh Tag / T1000e / WM1110 and Xiao NRF52
2025-12-03 15:59:59 +11:00
taco
dde9b7cc76 remove calls to sd_power_mode_set(NRF_POWER_MODE_LOWPWR);
this is the default mode, there is no need to call it unless previously changing it.
2025-12-03 15:59:59 +11:00
ripplebiz
0082149c60 Merge pull request #996 from mattzzw/dev
Add display of IP address to companion screen
2025-12-03 13:52:48 +11:00
ripplebiz
a616a843a9 Merge pull request #1039 from ViezeVingertjes/feat/support-nibble-screen-connect
Add ESP32-S3-Zero board configuration and Nibble Screen Connect variant
2025-12-03 13:49:51 +11:00
ripplebiz
c77391c5dd Merge pull request #1169 from Meshcore-Portugal/jbrazio/2025_db83f76e
Bridge: Fix RAK4631 GPS conflict and add T114 RS232 targets
2025-12-03 13:43:19 +11:00
ripplebiz
acc32aa166 Merge pull request #1156 from csrutil/persist-gps
Persist GPS enabled state to preferences
2025-12-02 21:50:21 +11:00
João Brázio
69a9a0bce9 Bridge: Add t114 rs232 targets 2025-12-02 10:31:24 +00:00
João Brázio
f56172738d Bridge: Fix RAK4631 serial2 GPS conflict 2025-12-02 10:30:45 +00:00
Florian Lippert
07d6484b61 Support for RAK11310 WisBlock 2025-12-01 21:29:03 +01:00
Florent
405f703bfe thinknode_m5: fix repeater build 2025-12-01 09:40:02 +01:00
ripplebiz
eee25605ca Merge pull request #1162 from recrof/led_state_fix
add default LED_STATE_ON for boards that don't have it defined
2025-11-30 21:09:01 +11:00
Rastislav Vysoky
052f17738c add default LED_STATE_ON for boards that don't have it defined 2025-11-30 10:52:33 +01:00
Scott Powell
e054597a18 * ver 1.11.0 2025-11-30 18:32:10 +11:00
csrutil
cfb7ed876c CMD_SET_CUSTOM_VAR will update gps and gps_interval 2025-11-30 09:45:56 +08:00
csrutil
df3cb3d192 _location->loop() should be in the next tick 2025-11-29 20:29:52 +08:00
csrutil
62e180dc0f changed ms to sec 2025-11-29 19:02:00 +08:00
csrutil
39503ad0b4 move GPS preference initialization to UITask 2025-11-29 18:35:34 +08:00
csrutil
4aebc57add fixed gps init value 2025-11-29 18:02:08 +08:00
csrutil
678915ef3b add GPS interval validation and bounds checking 2025-11-29 17:30:13 +08:00
csrutil
88fb173297 add configurable GPS update interval
Make GPS update interval configurable via settings instead of using hardcoded 1 second value. The interval is persisted from preferences and can be adjusted at runtime through the sensor manager settings interface
2025-11-29 17:20:21 +08:00
csrutil
c641beabd3 https://github.com/meshcore-dev/MeshCore/issues/989 - persist GPS enabled state to preferences
Add GPS configuration to NodePrefs structure and persist the GPS
enabled state when toggled via UI. This ensures GPS settings are
retained across device restarts.
2025-11-29 16:37:23 +08:00
ripplebiz
fe874032d5 Merge pull request #1153 from fdlamotte/thinknode_m5
Thinknode m5 support
2025-11-29 12:13:30 +11:00
Florent
1c0017b634 thinknode_m5: gps support 2025-11-28 13:15:11 +01:00
Florent
ee4e87c3ee thinknode_m5: manage baclight 2025-11-28 10:33:19 +01:00
Florent
dfec6d3483 thinknode_m5: tx_led 2025-11-28 09:57:58 +01:00
Florent
24edd3cf20 thinknode_m5: add pca9557 expander 2025-11-27 22:55:21 +01:00
Florent
d0f6def4f9 thinknode_m5: initial port 2025-11-27 21:49:04 +01:00
Scott Powell
0307b64721 Merge branch 'dev' into ext-trace 2025-11-27 23:22:06 +11:00
Scott Powell
3ddfdd477b Revert "add heltec_v4 tft expansion box"
This reverts commit 310618e689.
2025-11-27 21:34:52 +11:00
Scott Powell
5b975d9e94 Merge branch 'dev' into ext-trace 2025-11-27 19:31:03 +11:00
ripplebiz
ffbc24b3e7 Merge pull request #1148 from Meshcore-Portugal/jbrazio/2025_6d9681e2
Add RAK4631 support for rs232 bridge
2025-11-27 17:01:30 +11:00
ripplebiz
eae2fba73c Merge pull request #1082 from KR4DIO/dev
Ikoka Handheld variant
2025-11-27 16:12:22 +11:00
ripplebiz
13bf82f1c4 Merge pull request #1130 from zaquaz/buzzer-feature-pr
Added buzzer config persistance across restart
2025-11-27 16:04:44 +11:00
zaquaz
6c7b5390e2 Remove default setting, since it is handled in MyMesh 2025-11-26 18:37:56 -08:00
ripplebiz
59fc28b344 Merge pull request #1150 from recrof/heltec_build_fixes
fix building issues with heltec wireless paper and heltec tracker
2025-11-26 22:18:23 +11:00
ripplebiz
2ca15ef3dc Merge pull request #1151 from recrof/m2_fixes
ThinkNode M2: build fix
2025-11-26 22:17:17 +11:00
ripplebiz
c17bd5d6fc Merge pull request #1122 from fschrempf/xiao-nrf-ui-and-power-optimizations
Companion Power Optimizations and UI Support for XIAO NRF52
2025-11-26 22:11:02 +11:00
Rastislav Vysoky
e98c79ae48 added missing NonBlockingRTTTL dependency, added USB and WIFI companions 2025-11-25 19:45:51 +01:00
Rastislav Vysoky
5b7d73866c fix building issues with heltec wireless paper and heltec tracker 2025-11-25 19:41:01 +01:00
João Brázio
baedddb25d Rename RS232 bridge environments and update build flags for Serial1 and Serial2 2025-11-25 16:19:28 +00:00
João Brázio
eafbd85d17 Add RAK4631 support for rs232 bridge 2025-11-25 11:53:21 +00:00
ripplebiz
8340d0e060 Merge pull request #1104 from zjs81/Fix-BW-setting-and-returning
Refactor float conversion in CommonCLI
2025-11-25 22:32:35 +11:00
ripplebiz
a9397c17d1 Merge pull request #1118 from Quency-D/dev-heltec-v4-tft
add heltec_v4 tft  expansion box
2025-11-25 22:18:05 +11:00
ripplebiz
79a036f995 Merge pull request #1131 from wel97459/dev-uint
Changed uint used in flags to uint8_t
2025-11-25 21:51:36 +11:00
Scott Powell
cdbeacdc4d Merge branch 'dev' into ext-trace 2025-11-25 15:17:48 +11:00
Scott Powell
30ccc1fa01 * BUG FIX: remote login fix same as repeater 2025-11-25 15:12:48 +11:00
Scott Powell
0e903de72c * BUG FIX: same remote login fix as repeater 2025-11-25 15:09:51 +11:00
Scott Powell
dc58f0ea83 * BUG FIX: repeater remote admin, flood login should invalidate the client->out_path 2025-11-24 22:56:55 +11:00
ripplebiz
f2740150df Merge pull request #1075 from agessaman/companion-stats
Add statistics commands to return device metrics on companions
2025-11-24 12:34:35 +11:00
João Brázio
d84e615466 Add devcontainer configuration for vscode 2025-11-23 14:25:38 +00:00
Brad Ferguson
2a33246c6f Merge branch 'meshcore-dev:dev' into dev 2025-11-22 23:48:01 -06:00
Frieder Schrempf
7723a4cb34 variants: Heltec T114: Enable DC/DC regulator
According to the documentation and experiments on other boards using
NRF52 + SX1262 this reduces the power consumption significantly.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-11-22 15:05:12 +01:00
Jaroslav Škarvada
32d622d969 variants: Heltec T114: Disable LED and GPS when powering off
This should reduce power consumption in hibernation.

Signed-off-by: Jaroslav Škarvada <jskarvad@redhat.com>
2025-11-22 15:05:11 +01:00
Frieder Schrempf
5235516dc7 variants: XIAO NRF52: Enable status LED
Fix the active state of the LEDs (active low) and enable the status
LED.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-11-22 15:05:09 +01:00
Frieder Schrempf
048bd268a1 companion: ui: Respect LED_STATE_ON for status LED
The current logic only works for active high LEDs. Some boards need
an active low level control and therefore they set LED_STATE_ON to 0.
Take this into account and use the correct LED pattern for both cases.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-11-22 15:05:08 +01:00
Frieder Schrempf
4a8dcb4906 variants: XIAO NRF52: Support power-off via user button
Add the necessary code to properly power-off the Xiao + Wio
companions. This way we can achieve around 15 microamps of power
consumption in the off state.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-11-22 15:05:07 +01:00
Frieder Schrempf
c76d337a00 variants: XIAO NRF52: Enable user button
The Xiao nRF52840 combined with the Wio-SX1262 is often used for
cheap and compact DIY companion nodes. The Wio actually has an onboard
pushbutton that can be used as user button. Enable support for the
button.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-11-22 15:05:06 +01:00
Frieder Schrempf
11f119a7fb variants: XIAO NRF52: Enable DC/DC regulator
This reduces the power consumption by approximately 25%.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-11-22 15:05:05 +01:00
Frieder Schrempf
b9b82fcf1b variants: WisMesh Tag: Enable status LED
Use the blue LED as status LED. This prevents the blue LED from
being always-on and draining the battery. Instead use it as status
LED with blink patterns as other companion devices do.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-11-22 15:05:04 +01:00
Frieder Schrempf
0f565323a0 variants: WisMesh Tag: Enable DC/DC regulator
According to the documentation and experiments on other boards using
NRF52 + SX1262 this reduces the power consumption significantly. This
assumes that the hardware actually has the inductor for the internal
DC/DC regulator populated which is very likely. Even if not, it won't
hurt.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
2025-11-22 15:05:02 +01:00
Jaroslav Škarvada
07e7e2d44b companion: Suspend radio when hibernating
This should significantly reduce power consumption in hibernation.

Fixes: #1014

Signed-off-by: Jaroslav Škarvada <jskarvad@redhat.com>
Signed-off-by: Frieder Schrempf <frieder@fris.de> # generalize for all radios and UIs
2025-11-22 15:05:01 +01:00
ripplebiz
5f06dc4a2f Merge pull request #1133 from oltaco/repeater-adc-multiplier-setting
Feature: configurable adc.multiplier for repeaters
2025-11-22 14:50:23 +11:00
taco
fc93d84fb8 tweaks get/set adcMultiplier logic 2025-11-21 23:44:17 +11:00
taco
e13c064487 add board.setAdcMultiplier to room server and sensor 2025-11-21 21:46:55 +11:00
ripplebiz
fc68203275 Merge pull request #1127 from oltaco/rename-faketec-to-promicro
Rename Faketec to ProMicro
2025-11-21 19:40:31 +11:00
taco
5a3ea64a97 Repeater: add adc.multiplier setting 2025-11-21 18:15:30 +11:00
taco
454f6b2583 rename adverts 2025-11-21 17:57:49 +11:00
Winston Lowe
031fa1e704 Changed uint to a uint8_t 2025-11-20 21:58:42 -08:00
Scott Powell
b33d226c58 * proposal for 'Extended Trace' packets. Using 'flags' byte, lower 2 bits, for path hash size. 2025-11-21 15:44:31 +11:00
zaquaz
2bd47de3b9 Added buzzer config persistance accross restart 2025-11-20 19:02:32 -08:00
taco
ed9655e14e rename faketec to promicro 2025-11-21 12:48:33 +11:00
ripplebiz
f5a56c537f Merge pull request #1113 from recrof/bme280_fix
fix: make bme280 sensor usable again
2025-11-19 15:39:48 +11:00
Quency-D
310618e689 add heltec_v4 tft expansion box 2025-11-19 11:43:52 +08:00
recrof
88a6141943 fix: move bme680 detection before bme280 2025-11-18 15:36:25 +01:00
agessaman
a3c9a07377 Modify CMD_GET_STATS with sub-types for core, radio, and packet statistics. Consolidated to a single RESP_CODE_STATS with a second byte to identify response structure. Updated documentation and examples to reflect the new command structure and response parsing. 2025-11-17 09:57:36 -08:00
ripplebiz
459169e8cb Merge pull request #1092 from liquidraver/rakgps
Fix RAK4631 GPS UART pin macros for L76K to work
2025-11-17 14:04:09 +11:00
ripplebiz
caf421b591 Merge pull request #1106 from oltaco/keepteen-lt1
Add support for Keepteen LT1
2025-11-17 13:50:16 +11:00
Florent
838e83b3b5 xiao_s3: relocate serial pins on repeater_bridge_rs232
* conflicts with i2c pins that are documented on the same pins
* this is a commented target
2025-11-16 17:07:33 +01:00
Florent
3dd6dc02ea xiao_s3: use environment sensor manager and add sensor role 2025-11-16 16:55:16 +01:00
taco
bc2256f232 Keepteen LT1: remove terminal_chat and sensor targets 2025-11-16 16:17:11 +11:00
taco
2058af8453 initial support: Keepteen LT1 2025-11-16 15:55:49 +11:00
zach
850d57a8f2 Refactor float conversion in CommonCLI to use strtof for improved precision and add ftoa3 function for formatting floats with three decimal places in TxtDataHelpers to fix display issue in app and repeater config ui in web
REPO:
1. Flash a repeater
2. Connect over lora
3. Set bw to 42.7 KHZ

It will revert back due to converting a double to a float.

REPO2:
1.Flash Repeater
2. Apply float fix
3. Set to say 20.8
4. try to get value via app or web cli repeater config
It wil show blank because it doesnt return a good value. It would be something like 20.7999992 which the app and web apps dont like so the ftoa3 rounds it and returns a 3 decimal point float
2025-11-14 21:51:28 -07:00
ripplebiz
8dbb0f5f23 Merge pull request #1071 from tpp-at-idx/thinknode_m2
Thinknode_M2: better battery reading accuracy and no display on powerup fix
2025-11-14 15:11:06 +11:00
ripplebiz
ff67c786ef Merge pull request #985 from liquidraver/dev3
Fix reversed GPS PINs on G2 and enable timesync
2025-11-14 14:34:23 +11:00
ripplebiz
11a0bd6ef1 Merge pull request #1035 from liquidraver/devt114
Add Heltec T114 GPS + timesync
2025-11-14 14:33:05 +11:00
Liam Cottle
9bfbb777a1 Merge pull request #1100 from stphnrdmr/dev
Allow SF smaller than 7 to be saved
2025-11-14 12:15:10 +13:00
Stephan Rodemeier
16c294ce60 Allow SF smaller than 7 to be saved 2025-11-13 22:25:55 +01:00
Brad Ferguson
15d52a6e27 Merge branch 'meshcore-dev:dev' into dev 2025-11-13 14:52:02 -06:00
liquidraver
b0ce00652f Fix RAK4631 GPS UART pin macros 2025-11-11 13:00:27 +01:00
agessaman
39f83efbfe Remove unused statistics formatting methods and associated header includes from MyMesh class. Whoops. 2025-11-09 11:39:47 -08:00
agessaman
80d6dd4367 Update statistics handling to use binary frames instead of JSON formatting for consistency with other companion commands. Added documentation of frame structure with code examples. 2025-11-09 11:28:49 -08:00
agessaman
c9aa536ca6 Reverted MyMesh constructor for simplicity.
Updated formatStatsReply method to use new member variables for statistics handling.
Removed excess variable creation
2025-11-09 11:28:49 -08:00
agessaman
df4dab8509 Add statistics commands and response handling in MyMesh
- Introduced new commands for retrieving statistics: CMD_GET_STATS_CORE, CMD_GET_STATS_RADIO, and CMD_GET_STATS_PACKETS.
- Implemented corresponding response handling methods to format and send statistics data.
- Updated MyMesh constructor to initialize new member variables for managing statistics.
- Included StatsFormatHelper for formatting statistics replies.
2025-11-09 11:28:49 -08:00
brad
00e0635ab5 add variant files for ikoka handheld (nrf52 with e22 radio) 2025-11-08 20:05:46 -06:00
Tomas P
a0bf66f9d8 Fix for display not coming on after poweron 2025-11-07 09:50:21 +01:00
Tomas P
429f82106b tweak getBattMilliVolts to report battery more accurately 2025-11-07 09:48:57 +01:00
Tomas P
c0a51aff66 change ADC_MULTIPLIER to better reflect battery voltage 2025-11-07 09:47:18 +01:00
liquidraver
99a3473169 even less comments \o/ 2025-11-03 21:41:11 +01:00
liquidraver
eae16cfc5f less unnecessary comments, less lines of code :) 2025-11-03 21:39:35 +01:00
liquidraver
397d280c3b stop OLED powering on every message if connected to phone 2025-11-03 21:25:31 +01:00
ViezeVingertjes
ff4fa7be31 Add ESP32-S3-Zero board configuration and Nibble Screen Connect variant 2025-10-31 14:42:16 +01:00
liquidraver
07e58d8ab5 Merge branch 'dev' into devt114 2025-10-30 10:10:06 +01:00
liquidraver
f3b20d5e70 t114 gps 2025-10-30 08:35:01 +01:00
Matthias Wientapper
f339c74bb4 * Add #ifdef, reuse variable 2025-10-27 17:58:29 +01:00
Matthias Wientapper
a38418e09a * Add display of IP address to companion screen 2025-10-22 20:01:15 +02:00
liquidraver
0920dc6663 Fix reversed GPS PINs on G2 and enable timesync 2025-10-21 12:23:45 +02:00
342 changed files with 13437 additions and 2533 deletions

View File

@@ -0,0 +1,45 @@
{
"name": "MeshCore",
"image": "mcr.microsoft.com/devcontainers/python:3-bookworm",
"features": {
"ghcr.io/rocker-org/devcontainer-features/apt-packages:1": {
"packages": [
"sudo"
]
}
},
"runArgs": [
"--privileged",
"--network=host",
"--volume=/dev/bus/usb:/dev/bus/usb:ro",
// arch tty* is owned by uucp (986)
// debian tty* is owned by dialout (20)
"--group-add=20",
"--group-add=986"
],
"postCreateCommand": {
"platformio": "pipx install platformio"
},
"customizations": {
"vscode": {
"settings": {
"platformio-ide.disablePIOHomeStartup": true,
"editor.formatOnSave": false,
"workbench.colorCustomizations": {
"titleBar.activeBackground": "#0d1a2b",
"titleBar.activeForeground": "#ffffff",
"titleBar.inactiveBackground": "#0d1a2b99",
"titleBar.inactiveForeground": "#ffffff99"
}
},
"extensions": [
"platformio.platformio-ide",
"github.vscode-github-actions",
"GitHub.vscode-pull-request-github"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}
}
}

43
.github/workflows/pr-build-check.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
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 }}

2
.gitignore vendored
View File

@@ -14,3 +14,5 @@ cmake-*
.cache
.ccls
compile_commands.json
.venv/
venv/

View File

@@ -39,9 +39,11 @@ For developers;
- Clone and open the MeshCore repository in Visual Studio Code.
- 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.
- [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 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 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.
@@ -89,7 +91,7 @@ Please submit PR's using 'dev' as the base branch!
For minor changes just submit your PR and I'll try to review it, but for anything more 'impactful' please open an Issue first and start a discussion. Is better to sound out what it is you want to achieve first, and try to come to a consensus on what the best approach is, especially when it impacts the structure or architecture of this codebase.
Here are some general principals you should try to adhere to:
* Keep it simple. Please, don't think like a high-level lang programmer. Think embedded, and keep code concise, without any unecessary layers.
* Keep it simple. Please, don't think like a high-level lang programmer. Think embedded, and keep code concise, without any unnecessary layers.
* No dynamic memory allocation, except during setup/begin functions.
* Use the same brace and indenting style that's in the core source modules. (A .clang-format is prob going to be added soon, but please do NOT retroactively re-format existing code. This just creates unnecessary diffs that make finding problems harder)
@@ -106,7 +108,7 @@ There are a number of fairly major features in the pipeline, with no particular
- [ ] Core + Apps: support for LZW message compression
- [ ] Core: dynamic CR (Coding Rate) for weak vs strong hops
- [ ] Core: new framework for hosting multiple virtual nodes on one physical device
- [ ] V2 protocol spec: discussion and concensus around V2 packet protocol, including path hashes, new encryption specs, etc
- [ ] V2 protocol spec: discussion and consensus around V2 packet protocol, including path hashes, new encryption specs, etc
## 📞 Get Support

View File

@@ -0,0 +1,198 @@
"""
Bluefruit BLE Patch Script
Patches Bluefruit library to fix semaphore leak bug that causes device lockup
when BLE central disconnects unexpectedly (e.g., going out of range, supervision timeout).
Patches applied:
1. BLEConnection.h: Add _hvn_qsize member to track semaphore queue size
2. BLEConnection.cpp: Store hvn_qsize and restore semaphore on disconnect
Bug description:
- When a BLE central disconnects unexpectedly (reason=8 supervision timeout),
the BLE_GATTS_EVT_HVN_TX_COMPLETE event may never fire
- This leaves the _hvn_sem counting semaphore in a decremented state
- Since BLEConnection objects are reused (destructor never called), the
semaphore count is never restored
- Eventually all semaphore counts are exhausted and notify() blocks/fails
"""
from pathlib import Path
Import("env") # pylint: disable=undefined-variable
def _patch_ble_connection_header(source: Path) -> bool:
"""
Add _hvn_qsize member variable to BLEConnection class.
This is needed to restore the semaphore to its correct count on disconnect.
Returns True if patch was applied or already applied, False on error.
"""
try:
content = source.read_text()
# Check if already patched
if "_hvn_qsize" in content:
return True # Already patched
# Find the location to insert - after _phy declaration
original_pattern = ''' uint8_t _phy;
uint8_t _role;'''
patched_pattern = ''' uint8_t _phy;
uint8_t _hvn_qsize;
uint8_t _role;'''
if original_pattern not in content:
print("Bluefruit patch: WARNING - BLEConnection.h pattern not found")
return False
content = content.replace(original_pattern, patched_pattern)
source.write_text(content)
# Verify
if "_hvn_qsize" not in source.read_text():
return False
return True
except Exception as e:
print(f"Bluefruit patch: ERROR patching BLEConnection.h: {e}")
return False
def _patch_ble_connection_source(source: Path) -> bool:
"""
Patch BLEConnection.cpp to:
1. Store hvn_qsize in constructor
2. Restore _hvn_sem semaphore to full count on disconnect
Returns True if patch was applied or already applied, False on error.
"""
try:
content = source.read_text()
# Check if already patched (look for the restore loop)
if "uxSemaphoreGetCount(_hvn_sem)" in content:
return True # Already patched
# Patch 1: Store queue size in constructor
constructor_original = ''' _hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);'''
constructor_patched = ''' _hvn_qsize = hvn_qsize;
_hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);'''
if constructor_original not in content:
print("Bluefruit patch: WARNING - BLEConnection.cpp constructor pattern not found")
return False
content = content.replace(constructor_original, constructor_patched)
# Patch 2: Restore semaphore on disconnect
disconnect_original = ''' case BLE_GAP_EVT_DISCONNECTED:
// mark as disconnected
_connected = false;
break;'''
disconnect_patched = ''' case BLE_GAP_EVT_DISCONNECTED:
// Restore notification semaphore to full count
// This fixes lockup when disconnect occurs with notifications in flight
while (uxSemaphoreGetCount(_hvn_sem) < _hvn_qsize) {
xSemaphoreGive(_hvn_sem);
}
// Release indication semaphore if waiting
if (_hvc_sem) {
_hvc_received = false;
xSemaphoreGive(_hvc_sem);
}
// mark as disconnected
_connected = false;
break;'''
if disconnect_original not in content:
print("Bluefruit patch: WARNING - BLEConnection.cpp disconnect pattern not found")
return False
content = content.replace(disconnect_original, disconnect_patched)
source.write_text(content)
# Verify
verify_content = source.read_text()
if "uxSemaphoreGetCount(_hvn_sem)" not in verify_content:
return False
if "_hvn_qsize = hvn_qsize" not in verify_content:
return False
return True
except Exception as e:
print(f"Bluefruit patch: ERROR patching BLEConnection.cpp: {e}")
return False
def _apply_bluefruit_patches(target, source, env): # pylint: disable=unused-argument
framework_path = env.get("PLATFORMFW_DIR")
if not framework_path:
framework_path = env.PioPlatform().get_package_dir("framework-arduinoadafruitnrf52")
if not framework_path:
print("Bluefruit patch: ERROR - framework directory not found")
env.Exit(1)
return
framework_dir = Path(framework_path)
bluefruit_lib = framework_dir / "libraries" / "Bluefruit52Lib" / "src"
patch_failed = False
# Patch BLEConnection.h
conn_header = bluefruit_lib / "BLEConnection.h"
if conn_header.exists():
before = conn_header.read_text()
success = _patch_ble_connection_header(conn_header)
after = conn_header.read_text()
if success:
if before != after:
print("Bluefruit patch: OK - Applied BLEConnection.h fix (added _hvn_qsize member)")
else:
print("Bluefruit patch: OK - BLEConnection.h already patched")
else:
print("Bluefruit patch: FAILED - BLEConnection.h")
patch_failed = True
else:
print(f"Bluefruit patch: ERROR - BLEConnection.h not found at {conn_header}")
patch_failed = True
# Patch BLEConnection.cpp
conn_source = bluefruit_lib / "BLEConnection.cpp"
if conn_source.exists():
before = conn_source.read_text()
success = _patch_ble_connection_source(conn_source)
after = conn_source.read_text()
if success:
if before != after:
print("Bluefruit patch: OK - Applied BLEConnection.cpp fix (restore semaphore on disconnect)")
else:
print("Bluefruit patch: OK - BLEConnection.cpp already patched")
else:
print("Bluefruit patch: FAILED - BLEConnection.cpp")
patch_failed = True
else:
print(f"Bluefruit patch: ERROR - BLEConnection.cpp not found at {conn_source}")
patch_failed = True
if patch_failed:
print("Bluefruit patch: CRITICAL - Patch failed! Build aborted.")
env.Exit(1)
# Register the patch to run before build
bluefruit_action = env.VerboseAction(_apply_bluefruit_patches, "Applying Bluefruit BLE patches...")
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", bluefruit_action)
# Also run immediately to patch before any compilation
_apply_bluefruit_patches(None, None, env)

40
boards/esp32-s3-zero.json Normal file
View File

@@ -0,0 +1,40 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld"
},
"core": "esp32",
"extra_flags": [
"-D ARDUINO_USB_CDC_ON_BOOT=1",
"-D ARDUINO_USB_MSC_ON_BOOT=0",
"-D ARDUINO_USB_DFU_ON_BOOT=0",
"-D ARDUINO_USB_MODE=1",
"-D ARDUINO_RUNNING_CORE=1",
"-D ARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "ESP32-S3-Zero",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.espressif.com",
"vendor": "Espressif"
}

79
boards/keepteen_lt1.json Normal file
View File

@@ -0,0 +1,79 @@
{
"build": {
"arduino":{
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
[
"0x239A",
"0x00B3"
],
[
"0x239A",
"0x8029"
],
[
"0x239A",
"0x0029"
],
[
"0x239A",
"0x002A"
],
[
"0x239A",
"0x802A"
]
],
"usb_product": "Keepteen LT1",
"mcu": "nrf52840",
"variant": "Keepteen LT1",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": [
"bluetooth"
],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52.cfg"
},
"frameworks": [
"arduino",
"zephyr"
],
"name": "Keepteen LT1",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "http://www.keepteen.com/",
"vendor": "Keepteen"
}

74
boards/meshtiny.json Normal file
View File

@@ -0,0 +1,74 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
[
"0x239A",
"0x8029"
],
[
"0x239A",
"0x0029"
],
[
"0x239A",
"0x002A"
],
[
"0x239A",
"0x802A"
]
],
"usb_product": "Meshtiny",
"mcu": "nrf52840",
"variant": "meshtiny",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": [
"bluetooth"
],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": [
"arduino",
"freertos"
],
"name": "Meshtiny",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://shop.mtoolstec.com/product/meshtiny",
"vendor": "MTools Tec"
}

72
boards/rak3401.json Normal file
View File

@@ -0,0 +1,72 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
[
"0x239A",
"0x8029"
],
[
"0x239A",
"0x0029"
],
[
"0x239A",
"0x002A"
],
[
"0x239A",
"0x802A"
]
],
"usb_product": "WisCore RAK3401 Board",
"mcu": "nrf52840",
"variant": "WisCore_RAK3401_Board",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": [
"bluetooth"
],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd"
},
"frameworks": [
"arduino"
],
"name": "WisCore RAK3401 Board",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://www.rakwireless.com",
"vendor": "RAKwireless"
}

50
boards/t_beam_1w.json Normal file
View File

@@ -0,0 +1,50 @@
{
"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)",
"upload": {
"flash_size": "8MB",
"maximum_ram_size": 327680,
"maximum_ram_size": 8388608,
"maximum_size": 8388608,
"require_upload_port": true,
"speed": 460800

72
boards/thinknode_m3.json Normal file
View File

@@ -0,0 +1,72 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
[
"0x239A",
"0x4405"
],
[
"0x239A",
"0x0029"
],
[
"0x239A",
"0x002A"
]
],
"usb_product": "elecrow_eink",
"mcu": "nrf52840",
"variant": "ELECROW-ThinkNode-M3",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": [
"bluetooth"
],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": [
"jlink"
],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52.cfg"
},
"frameworks": [
"arduino"
],
"name": "elecrow nrf",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink"
]
},
"url": "https://github.com/Elecrow-RD",
"vendor": "ELECROW"
}

72
boards/thinknode_m6.json Normal file
View File

@@ -0,0 +1,72 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_ELECROW_M6 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
[
"0x239A",
"0x4405"
],
[
"0x239A",
"0x0029"
],
[
"0x239A",
"0x002A"
]
],
"usb_product": "elecrow_solar",
"mcu": "nrf52840",
"variant": "ELECROW-ThinkNode-M6",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": [
"bluetooth"
],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": [
"jlink"
],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52.cfg"
},
"frameworks": [
"arduino"
],
"name": "elecrow solar",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink"
]
},
"url": "https://github.com/Elecrow-RD",
"vendor": "ELECROW"
}

View File

@@ -7,6 +7,7 @@ sh build.sh <command> [target]
Commands:
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-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>.
@@ -29,22 +30,42 @@ $ sh build.sh build-repeater-firmwares
Build all chat room server firmwares
$ sh build.sh build-room-server-firmwares
Environment Variables:
DISABLE_DEBUG=1: Disables all debug logging flags (MESH_DEBUG, MESH_PACKET_LOGGING, etc.)
If not set, debug flags from variant platformio.ini files are used.
Examples:
Build without debug logging:
$ export FIRMWARE_VERSION=v1.0.0
$ export DISABLE_DEBUG=1
$ sh build.sh build-firmware RAK_4631_repeater
Build with debug logging (default, uses flags from variant files):
$ export FIRMWARE_VERSION=v1.0.0
$ sh build.sh build-firmware RAK_4631_repeater
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.
case $1 in
help|usage|-h|--help)
global_usage
exit 1
;;
list|-l)
get_pio_envs
exit 0
;;
esac
# get a list of pio env names that start with "env:"
get_pio_envs() {
echo $(pio project config | grep 'env:' | sed 's/env://')
}
# cache project config json for use in get_platform_for_env()
PIO_CONFIG_JSON=$(pio project config --json-output)
# $1 should be the string to find (case insensitive)
get_pio_envs_containing_string() {
@@ -68,8 +89,36 @@ get_pio_envs_ending_with_string() {
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_debug_flags() {
if [ "$DISABLE_DEBUG" == "1" ]; then
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG -UBLE_DEBUG_LOGGING -UWIFI_DEBUG_LOGGING -UBRIDGE_DEBUG -UGPS_NMEA_DEBUG -UCORE_DEBUG_LEVEL -UESPNOW_DEBUG_LOGGING -UDEBUG_RP2040_WIRE -UDEBUG_RP2040_SPI -UDEBUG_RP2040_CORE -UDEBUG_RP2040_PORT -URADIOLIB_DEBUG_SPI -UCFG_DEBUG -URADIOLIB_DEBUG_BASIC -URADIOLIB_DEBUG_PROTOCOL"
fi
}
# build firmware for the provided pio env in $1
build_firmware() {
# get env platform for post build actions
ENV_PLATFORM=($(get_platform_for_env $1))
# get git commit sha
COMMIT_HASH=$(git rev-parse --short HEAD)
@@ -94,30 +143,37 @@ build_firmware() {
# add firmware version info to end of existing platformio build flags in environment vars
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'"
# disable debug flags if requested
disable_debug_flags
# build firmware target
pio run -e $1
# build merge-bin for esp32 fresh install
if [ -f .pio/build/$1/firmware.bin ]; then
# build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin)
if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then
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
# build .uf2 for nrf52 boards
if [[ -f .pio/build/$1/firmware.zip && -f .pio/build/$1/firmware.hex ]]; then
# build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2)
if [ "$ENV_PLATFORM" == "NRF52_PLATFORM" ]; then
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
# copy .bin, .uf2, and .zip to out folder
# e.g: Heltec_v3_room_server-v1.0.0-SHA.bin
# e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2
# for stm32, copy .bin and .hex to out folder
if [ "$ENV_PLATFORM" == "STM32_PLATFORM" ]; then
cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true
cp .pio/build/$1/firmware.hex out/${FIRMWARE_FILENAME}.hex 2>/dev/null || true
fi
# copy .bin for esp32 boards
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
# 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
# for rp2040, copy .bin and .uf2 to out folder
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.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
fi
}

881
docs/cli_commands.md Normal file
View File

@@ -0,0 +1,881 @@
# MeshCore Repeater & Room Server CLI Commands
## Navigation
- [Operational](#operational)
- [Neighbors](#neighbors-repeater-only)
- [Statistics](#statistics)
- [Logging](#logging)
- [Information](#info)
- [Configuration](#configuration)
- [Radio](#radio)
- [System](#system)
- [Routing](#routing)
- [ACL](#acl)
- [Region Management](#region-management-v110)
- [Region Examples](#region-examples)
- [GPS](#gps-when-gps-support-is-compiled-in)
- [Sensors](#sensors-when-sensor-support-is-compiled-in)
- [Bridge](#bridge-when-bridge-support-is-compiled-in)
---
## Operational
### Reboot the node
**Usage:**
- `reboot`
---
### Reset the clock and reboot
**Usage:**
- `clkreboot`
---
### Sync the clock with the remote device
**Usage:**
- `clock sync`
---
### Display current time in UTC
**Usage:**
- `clock`
---
### Set the time to a specific timestamp
**Usage:**
- `time <epoch_seconds>`
**Parameters:**
- `epoc_seconds`: Unix epoc time
---
### Send a flood advert
**Usage:**
- `advert`
---
### Start an Over-The-Air (OTA) firmware update
**Usage:**
- `start ota`
---
### Erase/Factory Reset
**Usage:**
- `erase`
**Serial Only:** Yes
**Warning:** _**This is destructive!**_
---
## Neighbors (Repeater Only)
### List nearby neighbors
**Usage:**
- `neighbors`
**Note:** The output of this command is limited to the 8 most recent adverts.
**Note:** Each line is encoded as `{pubkey-prefix}:{timestamp}:{snr*4}`
---
### Remove a neighbor
**Usage:**
- `neighbor.remove <pubkey_prefix>`
**Parameters:**
- `pubkey_prefix`: The public key of the node to remove from the neighbors list
---
## Statistics
### Clear Stats
**Usage:** `clear stats`
---
### System Stats - Battery, Uptime, Queue Length and Debug Flags
**Usage:**
- `stats-core`
**Serial Only:** Yes
---
### Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors
**Usage:** `stats-radio`
**Serial Only:** Yes
---
### Packet stats - Packet counters: Received, Sent
**Usage:** `stats-packets`
**Serial Only:** Yes
---
## Logging
### Begin capture of rx log to node storage
**Usage:** `log start`
---
### End capture of rx log to node sotrage
**Usage:** `log stop`
---
### Erase captured log
**Usage:** `log erase`
---
### Print the captured log to the serial terminal
**Usage:** `log`
**Serial Only:** Yes
---
## Info
### Get the Version
**Usage:** `ver`
---
### Show the hardware name
**Usage:** `board`
---
## Configuration
### Radio
#### View or change this node's radio parameters
**Usage:**
- `get radio`
- `set radio <freq>,<bw>,<sf>,<cr>`
**Parameters:**
- `freq`: Frequency in MHz
- `bw`: Bandwidth in kHz
- `sf`: Spreading factor (5-12)
- `cr`: Coding rate (5-8)
**Set by build flag:** `LORA_FREQ`, `LORA_BW`, `LORA_SF`, `LORA_CR`
**Default:** `869.525,250,11,5`
**Note:** Requires reboot to apply
---
#### View or change this node's transmit power
**Usage:**
- `get tx`
- `set tx <dbm>`
**Parameters:**
- `dbm`: Power level in dBm (1-22)
**Set by build flag:** `LORA_TX_POWER`
**Default:** Varies by board
**Notes:** This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Referr to the node's manual for the correct setting to use. **Setting a value too high may violate the laws in your country.**
---
#### Change the radio parameters for a set duration
**Usage:**
- `tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>`
**Parameters:**
- `freq`: Frequency in MHz (300-2500)
- `bw`: Bandwidth in kHz (7.8-500)
- `sf`: Spreading factor (5-12)
- `cr`: Coding rate (5-8)
- `timeout_mins`: Duration in minutes (must be > 0)
**Note:** This is not saved to preferences and will clear on reboot
---
#### View or change this node's frequency
**Usage:**
- `get freq`
- `set freq <frequency>`
**Parameters:**
- `frequency`: Frequency in MHz
**Default:** `869.525`
**Note:** Requires reboot to apply
### System
#### View or change this node's name
**Usage:**
- `get name`
- `set name <name>`
**Parameters:**
- `name`: Node name
**Set by build flag:** `ADVERT_NAME`
**Default:** Varies by board
**Note:** Max length varies. If a location is set, the max length is 24 bytes; 32 otherwise. Emoji and unicode characters may take more than one byte.
---
#### View or change this node's latitude
**Usage:**
- `get lat`
- `set lat <degrees>`
**Set by build flag:** `ADVERT_LAT`
**Default:** `0`
**Parameters:**
- `degrees`: Latitude in degrees
---
#### View or change this node's longitude
**Usage:**
- `get lon`
- `set lon <degrees>`
**Set by build flag:** `ADVERT_LON`
**Default:** `0`
**Parameters:**
- `degrees`: Longitude in degrees
---
#### View or change this node's identity (Private Key)
**Usage:**
- `get prv.key`
- `set prv.key <private_key>`
**Parameters:**
- `private_key`: Private key in hex format (64 hex characters)
**Serial Only:**
- `get prv.key`: Yes
- `set prv.key`: No
**Note:** Requires reboot to take effect after setting
---
#### View or change this node's admin password
**Usage:**
- `get password`
- `set password <password>`
**Parameters:**
- `password`: Admin password
**Set by build flag:** `ADMIN_PASSWORD`
**Default:** `password`
**Note:** Echoed back for confirmation
**Note:** Any node using this password will be added to the admin ACL list.
---
#### View or change this node's guest password
**Usage:**
- `get guest.password`
- `set guest.password <password>`
**Parameters:**
- `password`: Guest password
**Set by build flag:** `ROOM_PASSWORD` (Room Server only)
**Default:** `<blank>`
---
#### View or change this node's owner info
**Usage:**
- `get owner.info`
- `set owner.info <text>`
**Parameters:**
- `text`: Owner information text
**Default:** `<blank>`
**Note:** `|` characters are translated to newlines
**Note:** Requires firmware 1.12.+
---
#### Fine-tune the battery reading
**Usage:**
- `get adc.multiplier`
- `set adc.multiplier <value>`
**Parameters:**
- `value`: ADC multiplier (0.0-10.0)
**Default:** `0.0` (value defined by board)
**Note:** Returns "Error: unsupported by this board" if hardware doesn't support it
---
#### View or change this node's power saving flag (Repeater Only)
**Usage:**
- `powersaving <state>`
- `powersaving`
**Parameters:**
- `state`: `on`|`off`
**Default:** `on`
**Note:** When enabled, device enters sleep mode between radio transmissions
---
### Routing
#### View or change this node's repeat flag
**Usage:**
- `get repeat`
- `set repeat <state>`
**Parameters:**
- `state`: `on`|`off`
**Default:** `on`
---
#### View or change the retransmit delay factor for flood traffic
**Usage:**
- `get txdelay`
- `set txdelay <value>`
**Parameters:**
- `value`: Transmit delay factor (0-2)
**Default:** `0.5`
---
#### View or change the retransmit delay factor for direct traffic
**Usage:**
- `get direct.txdelay`
- `set direct.txdelay <value>`
**Parameters:**
- `value`: Direct transmit delay factor (0-2)
**Default:** `0.2`
---
#### [Experimental] View or change the processing delay for received traffic
**Usage:**
- `get rxdelay`
- `set rxdelay <value>`
**Parameters:**
- `value`: Receive delay base (0-20)
**Default:** `0.0`
---
#### View or change the airtime factor (duty cycle limit)
**Usage:**
- `get af`
- `set af <value>`
**Parameters:**
- `value`: Airtime factor (0-9)
**Default:** `1.0`
---
#### View or change the local interference threshold
**Usage:**
- `get int.thresh`
- `set int.thresh <value>`
**Parameters:**
- `value`: Interference threshold value
**Default:** `0.0`
---
#### View or change the AGC Reset Interval
**Usage:**
- `get agc.reset.interval`
- `set agc.reset.interval <value>`
**Parameters:**
- `value`: Interval in seconds rounded down to a multiple of 4 (17 becomes 16)
**Default:** `0.0`
---
#### Enable or disable Multi-Acks support
**Usage:**
- `get multi.acks`
- `set multi.acks <state>`
**Parameters:**
- `state`: `0` (disable) or `1` (enable)
**Default:** `0`
---
#### View or change the flood advert interval
**Usage:**
- `get flood.advert.interval`
- `set flood.advert.interval <hours>`
**Parameters:**
- `hours`: Interval in hours (3-168)
**Default:** `12` (Repeater) - `0` (Sensor)
---
#### View or change the zero-hop advert interval
**Usage:**
- `get advert.interval`
- `set advert.interval <minutes>`
**Parameters:**
- `minutes`: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)
**Default:** `0`
---
#### Limit the number of hops for a flood message
**Usage:**
- `get flood.max`
- `set flood.max <value>`
**Parameters:**
- `value`: Maximum flood hop count (0-64)
**Default:** `64`
---
### ACL
#### Add, update or remove permissions for a companion
**Usage:**
- `setperm <pubkey> <permissions>`
**Parameters:**
- `pubkey`: Companion public key
- `permissions`:
- `0`: Guest
- `1`: Read-only
- `2`: Read-write
- `3`: Admin
**Note:** Removes the entry when `permissions` is omitted
---
#### View the current ACL
**Usage:**
- `get acl`
**Serial Only:** Yes
---
#### View or change this room server's 'read-only' flag
**Usage:**
- `get allow.read.only`
- `set allow.read.only <state>`
**Parameters:**
- `state`: `on` (enable) or `off` (disable)
**Default:** `off`
---
### Region Management (v1.10.+)
#### Bulk-load region lists
**Usage:**
- `region load`
- `region load <name> [flood_flag]`
**Parameters:**
- `name`: A name of a region. `*` represents the wildcard region
**Note:** `flood_flag`: Optional `F` to allow flooding
**Note:** Indentation creates parent-child relationships (max 8 levels)
**Note:** `region load` with an empty name will not work remotely (it's interactive)
---
#### Save any changes to regions made since reboot
**Usage:**
- `region save`
---
#### Allow a region
**Usage:**
- `region allowf <name>`
**Parameters:**
- `name`: Region name (or `*` for wildcard)
**Note:** Setting on wildcard `*` allows packets without region transport codes
---
#### Block a region
**Usage:**
- `region denyf <name>`
**Parameters:**
- `name`: Region name (or `*` for wildcard)
**Note:** Setting on wildcard `*` drops packets without region transport codes
---
#### Show information for a region
**Usage:**
- `region get <name>`
**Parameters:**
- `name`: Region name (or `*` for wildcard)
---
#### View or change the home region for this node
**Usage:**
- `region home`
- `region home <name>`
**Parameters:**
- `name`: Region name
---
#### Create a new region
**Usage:**
- `region put <name> [parent_name]`
**Parameters:**
- `name`: Region name
- `parent_name`: Parent region name (optional, defaults to wildcard)
---
#### Remove a region
**Usage:**
- `region remove <name>`
**Parameters:**
- `name`: Region name
**Note:** Must remove all child regions before the region can be removed
---
#### View all regions
**Usage:**
- `region list <filter>`
**Serial Only:** Yes
**Parameters:**
- `filter`: `allowed`|`denied`
**Note:** Requires firmware 1.12.+
---
#### Dump all defined regions and flood permissions
**Usage:**
- `region`
**Serial Only:** For firmware older than 1.12.0
---
### Region Examples
**Example 1: Using F Flag with Named Public Region**
```
region load
#Europe F
<blank line to end region load>
region save
```
**Explanation:**
- Creates a region named `#Europe` with flooding enabled
- Packets from this region will be flooded to other nodes
---
**Example 2: Using Wildcard with F Flag**
```
region load
* F
<blank line to end region load>
region save
```
**Explanation:**
- Creates a wildcard region `*` with flooding enabled
- Enables flooding for all regions automatically
- Applies only to packets without transport codes
---
**Example 3: Using Wildcard Without F Flag**
```
region load
*
<blank line to end region load>
region save
```
**Explanation:**
- Creates a wildcard region `*` without flooding
- This region exists but doesn't affect packet distribution
- Used as a default/empty region
---
**Example 4: Nested Public Region with F Flag**
```
region load
#Europe F
#UK
#London
#Manchester
#France
#Paris
#Lyon
<blank line to end region load>
region save
```
**Explanation:**
- Creates `#Europe` region with flooding enabled
- Adds nested child regions (`#UK`, `#France`)
- All nested regions inherit the flooding flag from parent
---
**Example 5: Wildcard with Nested Public Regions**
```
region load
* F
#NorthAmerica
#USA
#NewYork
#California
#Canada
#Ontario
#Quebec
<blank line to end region load>
region save
```
**Explanation:**
- Creates wildcard region `*` with flooding enabled
- Adds nested `#NorthAmerica` hierarchy
- Enables flooding for all child regions automatically
- Useful for global networks with specific regional rules
---
### GPS (When GPS support is compiled in)
#### View or change GPS state
**Usage:**
- `gps`
- `gps <state>`
**Parameters:**
- `state`: `on`|`off`
**Default:** `off`
**Note:** Output format: `{status}, {fix}, {sat count}` (when enabled)
---
#### Sync this node's clock with GPS time
**Usage:**
- `gps sync`
---
#### Set this node's location based on the GPS coordinates
**Usage:**
- `gps setloc`
---
#### View or change the GPS advert policy
**Usage:**
- `gps advert`
- `gps advert <policy>`
**Parameters:**
- `policy`: `none`|`shared`|`prefs`
- `none`: don't include location in adverts
- `share`: share gps location (from SensorManager)
- `prefs`: location stored in node's lat and lon settings
**Default:** `prefs`
---
### Sensors (When sensor support is compiled in)
#### View the list of sensors on this node
**Usage:** `sensor list [start]`
**Parameters:**
- `start`: Optional starting index (defaults to 0)
**Note:** Output format: `<var_name>=<value>\n`
---
#### View or change thevalue of a sensor
**Usage:**
- `sensor get <key>`
- `sensor set <key> <value>`
**Parameters:**
- `key`: Sensor setting name
- `value`: The value to set the sensor to
---
### Bridge (When bridge support is compiled in)
#### View or change the bridge enabled flag
**Usage:**
- `get bridge.enabled`
- `set bridge.enabled <state>`
**Parameters:**
- `state`: `on`|`off`
**Default:** `off`
---
#### View the bridge source
**Usage:**
- `get bridge.source`
---
#### Add a delay to packets routed through this bridge
**Usage:**
- `get bridge.delay`
- `set bridge.delay <ms>`
**Parameters:**
- `ms`: Delay in milliseconds (0-10000)
**Default:** `500`
---
#### View or change the source of packets bridged to the external interface
**Usage:**
- `get bridge.source`
- `set bridge.source <source>`
**Parameters:**
- `source`:
- `rx`: bridges received packets
- `tx`: bridges transmitted packets
**Default:** `tx`
---
#### View or change the speed of the bridge (RS-232 only)
**Usage:**
- `get bridge.baud`
- `set bridge.baud <rate>`
**Parameters:**
- `rate`: Baud rate (`9600`, `19200`, `38400`, `57600`, or `115200`)
**Default:** `115200`
---
#### View or change the channel used for bridging (ESPNow only)
**Usage:**
- `get bridge.channel`
- `set bridge.channel <channel>`
**Parameters:**
- `channel`: Channel number (1-14)
---
#### Set the ESP-Now secret
**Usage:**
- `get bridge.secret`
- `set bridge.secret <secret>`
**Parameters:**
- `secret`: 16-character encryption secret
**Default:** Varies by board
---

282
docs/kiss_modem_protocol.md Normal file
View File

@@ -0,0 +1,282 @@
# 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

@@ -0,0 +1,213 @@
# nRF52 Power Management
## Overview
The nRF52 Power Management module provides battery protection features to prevent over-discharge, minimise likelihood of brownout and flash corruption conditions existing, and enable safe voltage-based recovery.
## Features
### Boot Voltage Protection
- Checks battery voltage immediately after boot and before mesh operations commence
- If voltage is below a configurable threshold (e.g., 3300mV), the device configures voltage wake (LPCOMP + VBUS) and enters protective shutdown (SYSTEMOFF)
- Prevents boot loops when battery is critically low
- Skipped when external power (USB VBUS) is detected
### Voltage Wake (LPCOMP + VBUS)
- Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF
- Enables USB VBUS detection so external power can wake the device
- Device automatically wakes when battery voltage rises above recovery threshold or when VBUS is detected
### Early Boot Register Capture
- Captures RESETREAS (reset reason) and GPREGRET2 (shutdown reason) before SystemInit() clears them
- Allows firmware to determine why it booted (cold boot, watchdog, LPCOMP wake, etc.)
- Allows firmware to determine why it last shut down (user request, low voltage, boot protection)
### Shutdown Reason Tracking
Shutdown reason codes (stored in GPREGRET2):
| Code | Name | Description |
|------|------|-------------|
| 0x00 | NONE | Normal boot / no previous shutdown |
| 0x4C | LOW_VOLTAGE | Runtime low voltage threshold reached |
| 0x55 | USER | User requested powerOff() |
| 0x42 | BOOT_PROTECT | Boot voltage protection triggered |
## Supported Boards
| Board | Implemented | LPCOMP wake | VBUS wake |
|-------|-------------|-------------|-----------|
| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | Yes | Yes |
| RAK4631 (`rak4631`) | Yes | Yes | Yes |
| Heltec T114 (`heltec_t114`) | Yes | Yes | Yes |
| Promicro nRF52840 | No | No | No |
| RAK WisMesh Tag | No | No | No |
| Heltec Mesh Solar | No | No | No |
| LilyGo T-Echo / T-Echo Lite | No | No | No |
| SenseCAP Solar | No | No | No |
| WIO Tracker L1 / L1 E-Ink | No | No | No |
| WIO WM1110 | No | No | No |
| Mesh Pocket | No | No | No |
| Nano G2 Ultra | No | No | No |
| ThinkNode M1/M3/M6 | No | No | No |
| T1000-E | No | No | No |
| Ikoka Nano/Stick/Handheld (nRF) | No | No | No |
| Keepteen LT1 | No | No | No |
| Minewsemi ME25LS01 | No | No | No |
Notes:
- "Implemented" reflects Phase 1 (boot lockout + shutdown reason capture).
- User power-off on Heltec T114 does not enable LPCOMP wake.
- VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52.
## Technical Details
### Architecture
The power management functionality is integrated into the `NRF52Board` base class in `src/helpers/NRF52Board.cpp`. Board variants provide hardware-specific configuration via a `PowerMgtConfig` struct and override `initiateShutdown(uint8_t reason)` to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).
### Early Boot Capture
A static constructor with priority 101 in `NRF52Board.cpp` captures the RESETREAS and GPREGRET2 registers before:
- SystemInit() (priority 102) - which clears RESETREAS
- Static C++ constructors (default priority 65535)
This ensures we capture the true reset reason before any initialisation code runs.
### Board Implementation
To enable power management on a board variant:
1. **Enable in platformio.ini**:
```ini
-D NRF52_POWER_MANAGEMENT
```
2. **Define configuration in variant.h**:
```c
#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV)
#define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing
#define PWRMGT_LPCOMP_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
```
3. **Implement in board .cpp file**:
```cpp
#ifdef NRF52_POWER_MANAGEMENT
const PowerMgtConfig power_config = {
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
};
void MyBoard::initiateShutdown(uint8_t reason) {
// Board-specific shutdown preparation (e.g., disable peripherals)
bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
reason == SHUTDOWN_REASON_BOOT_PROTECT);
if (enable_lpcomp) {
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
}
enterSystemOff(reason);
}
#endif
void MyBoard::begin() {
NRF52Board::begin(); // or NRF52BoardDCDC::begin()
// ... board setup ...
#ifdef NRF52_POWER_MANAGEMENT
checkBootVoltage(&power_config);
#endif
}
```
For user-initiated shutdowns, `powerOff()` remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).
4. **Declare override in board .h file**:
```cpp
#ifdef NRF52_POWER_MANAGEMENT
void initiateShutdown(uint8_t reason) override;
#endif
```
### Voltage Wake Configuration
The LPCOMP (Low Power Comparator) is configured to:
- Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31)
- Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
- Detect UP events (voltage rising above threshold)
- Use 50mV hysteresis for noise immunity
- Wake the device from SYSTEMOFF when triggered
VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever `configureVoltageWake()` is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).
**LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL)**:
| REFSEL | Fraction | VBAT @ 1M/1M divider (VDD=3.0-3.3) | VBAT @ 1.5M/1M divider (VDD=3.0-3.3) |
|--------|----------|------------------------------------|--------------------------------------|
| 0 | 1/8 | 0.75-0.82 V | 0.94-1.03 V |
| 1 | 2/8 | 1.50-1.65 V | 1.88-2.06 V |
| 2 | 3/8 | 2.25-2.47 V | 2.81-3.09 V |
| 3 | 4/8 | 3.00-3.30 V | 3.75-4.12 V |
| 4 | 5/8 | 3.75-4.12 V | 4.69-5.16 V |
| 5 | 6/8 | 4.50-4.95 V | 5.62-6.19 V |
| 6 | 7/8 | 5.25-5.77 V | 6.56-7.22 V |
| 7 | ARef | - | - |
| 8 | 1/16 | 0.38-0.41 V | 0.47-0.52 V |
| 9 | 3/16 | 1.12-1.24 V | 1.41-1.55 V |
| 10 | 5/16 | 1.88-2.06 V | 2.34-2.58 V |
| 11 | 7/16 | 2.62-2.89 V | 3.28-3.61 V |
| 12 | 9/16 | 3.38-3.71 V | 4.22-4.64 V |
| 13 | 11/16 | 4.12-4.54 V | 5.16-5.67 V |
| 14 | 13/16 | 4.88-5.36 V | 6.09-6.70 V |
| 15 | 15/16 | 5.62-6.19 V | 7.03-7.73 V |
**Important**: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use:
`VBAT_threshold ≈ (VDD * fraction) * divider_scale`, where `divider_scale = (Rtop + Rbottom) / Rbottom` (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).
### SoftDevice Compatibility
The power management code checks whether SoftDevice is enabled and uses the appropriate API:
- When SD enabled: `sd_power_*` functions
- When SD disabled: Direct register access (NRF_POWER->*)
This ensures compatibility regardless of BLE stack state.
## CLI Commands
Power management status can be queried via the CLI:
| Command | Description |
|---------|-------------|
| `get pwrmgt.support` | Returns "supported" or "unsupported" |
| `get pwrmgt.source` | Returns current power source - "battery" or "external" (5V/USB power) |
| `get pwrmgt.bootreason` | Returns reset and shutdown reason strings |
| `get pwrmgt.bootmv` | Returns boot voltage in millivolts |
On boards without power management enabled, all commands except `get pwrmgt.support` return:
```
ERROR: Power management not supported
```
## Debug Output
When `MESH_DEBUG=1` is enabled, the power management module outputs:
```
DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)
DEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)
DEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)
```
## Phase 2 (Planned)
- Runtime voltage monitoring
- Voltage state machine (Normal -> Warning -> Critical -> Shutdown)
- Configurable thresholds
- Load shedding callbacks for power reduction
- Deep sleep integration
- Scheduled wake-up
- Extended sleep with periodic monitoring
## References
- [nRF52840 Product Specification - POWER](https://infocenter.nordicsemi.com/topic/ps_nrf52840/power.html)
- [nRF52840 Product Specification - LPCOMP](https://infocenter.nordicsemi.com/topic/ps_nrf52840/lpcomp.html)
- [SoftDevice S140 API - Power Management](https://infocenter.nordicsemi.com/topic/sdk_nrf5_v17.1.0/group__nrf__sdm__api.html)

View File

@@ -103,7 +103,9 @@ Request type
| `0x02` | keepalive | (deprecated) |
| `0x03` | get telemetry data | TODO |
| `0x04` | get min,max,avg data | sensor nodes - get min, max, average for given time span |
| `0x05` | get access list | get node's approved access list |
| `0x05` | get access list | get node's approved access list |
| `0x06` | get neighbors | get repeater node's neighbors |
| `0x07` | get owner info | get repeater firmware-ver/name/owner info |
### Get stats
@@ -132,6 +134,27 @@ Gets information about the node, possibly including the following:
Request data about sensors on the node, including battery level.
### Get Telemetry
TODO
### Get Min/Max/Ave (Sensor nodes)
TODO
### Get Access List
TODO
### Get Neighors
TODO
### Get Owner Info
TODO
## Response
| Field | Size (bytes) | Description |
@@ -179,6 +202,34 @@ txt_type
| timestamp | 4 | sender time (unix timestamp) |
| password | rest of message | password for repeater/sensor |
## Repeater - Regions request
| Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------|
| timestamp | 4 | sender time (unix timestamp) |
| req type | 1 | 0x01 (request sub type) |
| reply path len | 1 | path len for reply |
| reply path | (variable) | reply path |
## Repeater - Owner info request
| Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------|
| timestamp | 4 | sender time (unix timestamp) |
| req type | 1 | 0x02 (request sub type) |
| reply path len | 1 | path len for reply |
| reply path | (variable) | reply path |
## Repeater - Clock and status request
| Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------|
| timestamp | 4 | sender time (unix timestamp) |
| req type | 1 | 0x03 (request sub type) |
| reply path len | 1 | path len for reply |
| reply path | (variable) | reply path |
# Group text message / datagram
| Field | Size (bytes) | Description |

1201
docs/protocol_guide.md Normal file

File diff suppressed because it is too large Load Diff

328
docs/stats_binary_frames.md Normal file
View File

@@ -0,0 +1,328 @@
# Stats Binary Frame Structures
Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.
## Command Codes
| Command | Code | Description |
|---------|------|-------------|
| `CMD_GET_STATS` | 56 | Get statistics (2-byte command: code + sub-type) |
### Stats Sub-Types
The `CMD_GET_STATS` command uses a 2-byte frame structure:
- **Byte 0:** `CMD_GET_STATS` (56)
- **Byte 1:** Stats sub-type:
- `STATS_TYPE_CORE` (0) - Get core device statistics
- `STATS_TYPE_RADIO` (1) - Get radio statistics
- `STATS_TYPE_PACKETS` (2) - Get packet statistics
## Response Codes
| Response | Code | Description |
|----------|------|-------------|
| `RESP_CODE_STATS` | 24 | Statistics response (2-byte response: code + sub-type) |
### Stats Response Sub-Types
The `RESP_CODE_STATS` response uses a 2-byte header structure:
- **Byte 0:** `RESP_CODE_STATS` (24)
- **Byte 1:** Stats sub-type (matches command sub-type):
- `STATS_TYPE_CORE` (0) - Core device statistics response
- `STATS_TYPE_RADIO` (1) - Radio statistics response
- `STATS_TYPE_PACKETS` (2) - Packet statistics response
---
## RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)
**Total Frame Size:** 11 bytes
| Offset | Size | Type | Field Name | Description | Range/Notes |
|--------|------|------|------------|-------------|-------------|
| 0 | 1 | uint8_t | response_code | Always `0x18` (24) | - |
| 1 | 1 | uint8_t | stats_type | Always `0x00` (STATS_TYPE_CORE) | - |
| 2 | 2 | uint16_t | battery_mv | Battery voltage in millivolts | 0 - 65,535 |
| 4 | 4 | uint32_t | uptime_secs | Device uptime in seconds | 0 - 4,294,967,295 |
| 8 | 2 | uint16_t | errors | Error flags bitmask | - |
| 10 | 1 | uint8_t | queue_len | Outbound packet queue length | 0 - 255 |
### Example Structure (C/C++)
```c
struct StatsCore {
uint8_t response_code; // 0x18
uint8_t stats_type; // 0x00 (STATS_TYPE_CORE)
uint16_t battery_mv;
uint32_t uptime_secs;
uint16_t errors;
uint8_t queue_len;
} __attribute__((packed));
```
---
## RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1)
**Total Frame Size:** 14 bytes
| Offset | Size | Type | Field Name | Description | Range/Notes |
|--------|------|------|------------|-------------|-------------|
| 0 | 1 | uint8_t | response_code | Always `0x18` (24) | - |
| 1 | 1 | uint8_t | stats_type | Always `0x01` (STATS_TYPE_RADIO) | - |
| 2 | 2 | int16_t | noise_floor | Radio noise floor in dBm | -140 to +10 |
| 4 | 1 | int8_t | last_rssi | Last received signal strength in dBm | -128 to +127 |
| 5 | 1 | int8_t | last_snr | SNR scaled by 4 | Divide by 4.0 for dB |
| 6 | 4 | uint32_t | tx_air_secs | Cumulative transmit airtime in seconds | 0 - 4,294,967,295 |
| 10 | 4 | uint32_t | rx_air_secs | Cumulative receive airtime in seconds | 0 - 4,294,967,295 |
### Example Structure (C/C++)
```c
struct StatsRadio {
uint8_t response_code; // 0x18
uint8_t stats_type; // 0x01 (STATS_TYPE_RADIO)
int16_t noise_floor;
int8_t last_rssi;
int8_t last_snr; // Divide by 4.0 to get actual SNR in dB
uint32_t tx_air_secs;
uint32_t rx_air_secs;
} __attribute__((packed));
```
---
## RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)
**Total Frame Size:** 26 bytes (legacy) or 30 bytes (includes `recv_errors`)
| Offset | Size | Type | Field Name | Description | Range/Notes |
|--------|------|------|------------|-------------|-------------|
| 0 | 1 | uint8_t | response_code | Always `0x18` (24) | - |
| 1 | 1 | uint8_t | stats_type | Always `0x02` (STATS_TYPE_PACKETS) | - |
| 2 | 4 | uint32_t | recv | Total packets received | 0 - 4,294,967,295 |
| 6 | 4 | uint32_t | sent | Total packets sent | 0 - 4,294,967,295 |
| 10 | 4 | uint32_t | flood_tx | Packets sent via flood 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 |
| 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
- Counters are cumulative from boot and may wrap.
- `recv = flood_rx + direct_rx`
- `sent = flood_tx + direct_tx`
- Clients should accept frame length ≥ 26; if length ≥ 30, parse `recv_errors` at offset 26.
### Example Structure (C/C++)
```c
struct StatsPackets {
uint8_t response_code; // 0x18
uint8_t stats_type; // 0x02 (STATS_TYPE_PACKETS)
uint32_t recv;
uint32_t sent;
uint32_t flood_tx;
uint32_t direct_tx;
uint32_t flood_rx;
uint32_t direct_rx;
uint32_t recv_errors; // present when frame size is 30
} __attribute__((packed));
```
---
## Command Usage Example (Python)
```python
# Send CMD_GET_STATS command
def send_get_stats_core(serial_interface):
"""Send command to get core stats"""
cmd = bytes([56, 0]) # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)
serial_interface.write(cmd)
def send_get_stats_radio(serial_interface):
"""Send command to get radio stats"""
cmd = bytes([56, 1]) # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)
serial_interface.write(cmd)
def send_get_stats_packets(serial_interface):
"""Send command to get packet stats"""
cmd = bytes([56, 2]) # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)
serial_interface.write(cmd)
```
---
## Response Parsing Example (Python)
```python
import struct
def parse_stats_core(frame):
"""Parse RESP_CODE_STATS + STATS_TYPE_CORE frame (11 bytes)"""
response_code, stats_type, battery_mv, uptime_secs, errors, queue_len = \
struct.unpack('<B B H I H B', frame)
assert response_code == 24 and stats_type == 0, "Invalid response type"
return {
'battery_mv': battery_mv,
'uptime_secs': uptime_secs,
'errors': errors,
'queue_len': queue_len
}
def parse_stats_radio(frame):
"""Parse RESP_CODE_STATS + STATS_TYPE_RADIO frame (14 bytes)"""
response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \
struct.unpack('<B B h b b I I', frame)
assert response_code == 24 and stats_type == 1, "Invalid response type"
return {
'noise_floor': noise_floor,
'last_rssi': last_rssi,
'last_snr': last_snr / 4.0, # Unscale SNR
'tx_air_secs': tx_air_secs,
'rx_air_secs': rx_air_secs
}
def parse_stats_packets(frame):
"""Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 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 = \
struct.unpack('<B B I I I I I I', frame[:26])
assert response_code == 24 and stats_type == 2, "Invalid response type"
result = {
'recv': recv,
'sent': sent,
'flood_tx': flood_tx,
'direct_tx': direct_tx,
'flood_rx': flood_rx,
'direct_rx': direct_rx
}
if len(frame) >= 30:
(recv_errors,) = struct.unpack('<I', frame[26:30])
result['recv_errors'] = recv_errors
return result
```
---
## Command Usage Example (JavaScript/TypeScript)
```typescript
// Send CMD_GET_STATS command
const CMD_GET_STATS = 56;
const STATS_TYPE_CORE = 0;
const STATS_TYPE_RADIO = 1;
const STATS_TYPE_PACKETS = 2;
function sendGetStatsCore(serialInterface: SerialPort): void {
const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_CORE]);
serialInterface.write(cmd);
}
function sendGetStatsRadio(serialInterface: SerialPort): void {
const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_RADIO]);
serialInterface.write(cmd);
}
function sendGetStatsPackets(serialInterface: SerialPort): void {
const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_PACKETS]);
serialInterface.write(cmd);
}
```
---
## Response Parsing Example (JavaScript/TypeScript)
```typescript
interface StatsCore {
battery_mv: number;
uptime_secs: number;
errors: number;
queue_len: number;
}
interface StatsRadio {
noise_floor: number;
last_rssi: number;
last_snr: number;
tx_air_secs: number;
rx_air_secs: number;
}
interface StatsPackets {
recv: number;
sent: number;
flood_tx: number;
direct_tx: number;
flood_rx: number;
direct_rx: number;
recv_errors?: number; // present when frame is 30 bytes
}
function parseStatsCore(buffer: ArrayBuffer): StatsCore {
const view = new DataView(buffer);
const response_code = view.getUint8(0);
const stats_type = view.getUint8(1);
if (response_code !== 24 || stats_type !== 0) {
throw new Error('Invalid response type');
}
return {
battery_mv: view.getUint16(2, true),
uptime_secs: view.getUint32(4, true),
errors: view.getUint16(8, true),
queue_len: view.getUint8(10)
};
}
function parseStatsRadio(buffer: ArrayBuffer): StatsRadio {
const view = new DataView(buffer);
const response_code = view.getUint8(0);
const stats_type = view.getUint8(1);
if (response_code !== 24 || stats_type !== 1) {
throw new Error('Invalid response type');
}
return {
noise_floor: view.getInt16(2, true),
last_rssi: view.getInt8(4),
last_snr: view.getInt8(5) / 4.0, // Unscale SNR
tx_air_secs: view.getUint32(6, true),
rx_air_secs: view.getUint32(10, true)
};
}
function parseStatsPackets(buffer: ArrayBuffer): StatsPackets {
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 stats_type = view.getUint8(1);
if (response_code !== 24 || stats_type !== 2) {
throw new Error('Invalid response type');
}
const result: StatsPackets = {
recv: view.getUint32(2, true),
sent: view.getUint32(6, true),
flood_tx: view.getUint32(10, true),
direct_tx: view.getUint32(14, true),
flood_rx: view.getUint32(18, true),
direct_rx: view.getUint32(22, true)
};
if (buffer.byteLength >= 30) {
result.recv_errors = view.getUint32(26, true);
}
return result;
}
```
---
## Field Size Considerations
- Packet counters (uint32_t): May wrap after extended high-traffic operation.
- Time fields (uint32_t): Max ~136 years.
- SNR (int8_t, scaled by 4): Range -32 to +31.75 dB, 0.25 dB precision.

View File

@@ -65,6 +65,7 @@ void DataStore::begin() {
#if defined(ESP32)
#include <SPIFFS.h>
#include <nvs_flash.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -172,7 +173,9 @@ bool DataStore::formatFileSystem() {
#elif defined(RP2040_PLATFORM)
return LittleFS.format();
#elif defined(ESP32)
return ((fs::SPIFFSFS *)_fs)->format();
bool fs_success = ((fs::SPIFFSFS *)_fs)->format();
esp_err_t nvs_err = nvs_flash_erase(); // no need to reinit, will be done by reboot
return fs_success && (nvs_err == ESP_OK);
#else
#error "need to implement format()"
#endif
@@ -209,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.sf, sizeof(_prefs.sf)); // 60
file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.read(pad, 1); // 62
file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62
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.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
@@ -219,8 +222,13 @@ 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.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
file.read(pad, 2); // 78
file.read((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78
file.read(pad, 1); // 79
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.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
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.close();
}
@@ -240,7 +248,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.sf, sizeof(_prefs.sf)); // 60
file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.write(pad, 1); // 62
file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62
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.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
@@ -250,8 +258,13 @@ 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.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
file.write(pad, 2); // 78
file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78
file.write(pad, 1); // 79
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.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
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.close();
}
@@ -549,14 +562,20 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src
}
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
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
char path[64];
inline void makeBlobPath(const uint8_t key[], int key_len, char* path, size_t path_size) {
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);
}
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)) {
File f = openRead(_fs, path);
@@ -571,11 +590,7 @@ 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) {
char path[64];
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);
makeBlobPath(key, key_len, path, sizeof(path));
File f = openWrite(_fs, path);
if (f) {
@@ -587,4 +602,13 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src
}
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

View File

@@ -42,6 +42,7 @@ public:
void migrateToSecondaryFS();
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 deleteBlobByKey(const uint8_t key[], int key_len);
File openRead(const char* filename);
File openRead(FILESYSTEM* fs, const char* filename);
bool removeFile(const char* filename);

View File

@@ -52,6 +52,17 @@
#define CMD_SEND_PATH_DISCOVERY_REQ 52
#define CMD_SET_FLOOD_SCOPE 54 // v8+
#define CMD_SEND_CONTROL_DATA 55 // v8+
#define CMD_GET_STATS 56 // v8+, second byte is stats type
#define CMD_SEND_ANON_REQ 57
#define CMD_SET_AUTOADD_CONFIG 58
#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
#define STATS_TYPE_CORE 0
#define STATS_TYPE_RADIO 1
#define STATS_TYPE_PACKETS 2
#define RESP_CODE_OK 0
#define RESP_CODE_ERR 1
@@ -77,6 +88,9 @@
#define RESP_CODE_CUSTOM_VARS 21
#define RESP_CODE_ADVERT_PATH 22
#define RESP_CODE_TUNING_PARAMS 23
#define RESP_CODE_STATS 24 // v8+, second byte is stats type
#define RESP_CODE_AUTOADD_CONFIG 25
#define RESP_ALLOWED_REPEAT_FREQ 26
#define SEND_TIMEOUT_BASE_MILLIS 500
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
@@ -102,6 +116,8 @@
#define PUSH_CODE_BINARY_RESPONSE 0x8C
#define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D
#define PUSH_CODE_CONTROL_DATA 0x8E // v8+
#define PUSH_CODE_CONTACT_DELETED 0x8F // used to notify client app of deleted contact when overwriting oldest
#define PUSH_CODE_CONTACTS_FULL 0x90 // used to notify client app that contacts storage is full
#define ERR_CODE_UNSUPPORTED_CMD 1
#define ERR_CODE_NOT_FOUND 2
@@ -112,6 +128,15 @@
#define MAX_SIGN_DATA_LEN (8 * 1024) // 8K
// Auto-add config bitmask
// Bit 0: If set, overwrite oldest non-favourite contact when contacts file is full
// Bits 1-4: these indicate which contact types to auto-add when manual_contact_mode = 0x01
#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0) // 0x01 - overwrite oldest non-favourite when full
#define AUTO_ADD_CHAT (1 << 1) // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
#define AUTO_ADD_REPEATER (1 << 2) // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
#define AUTO_ADD_ROOM_SERVER (1 << 3) // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
#define AUTO_ADD_SENSOR (1 << 4) // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
void MyMesh::writeOKFrame() {
uint8_t buf[1];
buf[0] = RESP_CODE_OK;
@@ -233,6 +258,15 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
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 {
return _prefs.multi_acks;
}
@@ -254,9 +288,55 @@ bool MyMesh::isAutoAddEnabled() const {
return (_prefs.manual_add_contacts & 1) == 0;
}
bool MyMesh::shouldAutoAddContactType(uint8_t contact_type) const {
if ((_prefs.manual_add_contacts & 1) == 0) {
return true;
}
uint8_t type_bit = 0;
switch (contact_type) {
case ADV_TYPE_CHAT:
type_bit = AUTO_ADD_CHAT;
break;
case ADV_TYPE_REPEATER:
type_bit = AUTO_ADD_REPEATER;
break;
case ADV_TYPE_ROOM:
type_bit = AUTO_ADD_ROOM_SERVER;
break;
case ADV_TYPE_SENSOR:
type_bit = AUTO_ADD_SENSOR;
break;
default:
return false; // Unknown type, don't auto-add
}
return (_prefs.autoadd_config & type_bit) != 0;
}
bool MyMesh::shouldOverwriteWhenFull() const {
return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0;
}
void MyMesh::onContactOverwrite(const uint8_t* pub_key) {
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage
if (_serial->isConnected()) {
out_frame[0] = PUSH_CODE_CONTACT_DELETED;
memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE);
_serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
}
}
void MyMesh::onContactsFull() {
if (_serial->isConnected()) {
out_frame[0] = PUSH_CODE_CONTACTS_FULL;
_serial->writeFrame(out_frame, 1);
}
}
void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) {
if (_serial->isConnected()) {
if (!isAutoAddEnabled() && is_new) {
if (is_new) {
writeContactRespFrame(PUSH_CODE_NEW_ADVERT, contact);
} else {
out_frame[0] = PUSH_CODE_ADVERT;
@@ -270,7 +350,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
}
// add inbound-path to mem cache
if (path && path_len <= sizeof(AdvertPath::path)) { // check path is valid
if (path && mesh::Packet::isValidPathLen(path_len)) { // check path is valid
AdvertPath* p = advert_paths;
uint32_t oldest = 0xFFFFFFFF;
for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { // check if already in table, otherwise evict oldest
@@ -287,11 +367,10 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix));
strcpy(p->name, contact.name);
p->recv_timestamp = getRTCClock()->getCurrentTime();
p->path_len = path_len;
memcpy(p->path, path, p->path_len);
p->path_len = mesh::Packet::copyPath(p->path, path, path_len);
}
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]
}
static int sort_by_recent(const void *a, const void *b) {
@@ -387,26 +466,30 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
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) {
// TODO: dynamic send_scope, depending on recipient and current 'home' Region
if (send_scope.isNull()) {
sendFlood(pkt, delay_millis);
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
uint16_t codes[2];
codes[0] = send_scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
sendFlood(pkt, codes, delay_millis);
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
}
}
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: have per-channel send_scope
if (send_scope.isNull()) {
sendFlood(pkt, delay_millis);
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
uint16_t codes[2];
codes[0] = send_scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
sendFlood(pkt, codes, delay_millis);
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
}
}
@@ -603,7 +686,7 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
if (tag == pending_discovery) { // check for matching response tag)
pending_discovery = 0;
if (in_path_len > MAX_PATH_SIZE || out_path_len > MAX_PATH_SIZE) {
if (!mesh::Packet::isValidPathLen(in_path_len) || !mesh::Packet::isValidPathLen(out_path_len)) {
MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len);
} else {
int i = 0;
@@ -612,11 +695,9 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
memcpy(&out_frame[i], contact.id.pub_key, 6);
i += 6; // pub_key_prefix
out_frame[i++] = out_path_len;
memcpy(&out_frame[i], out_path, out_path_len);
i += out_path_len;
i += mesh::Packet::writePath(&out_frame[i], out_path, out_path_len);
out_frame[i++] = in_path_len;
memcpy(&out_frame[i], in_path, in_path_len);
i += in_path_len;
i += mesh::Packet::writePath(&out_frame[i], in_path, in_path_len);
// NOTE: telemetry data in 'extra' is discarded at present
_serial->writeFrame(out_frame, i);
@@ -670,6 +751,11 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) {
void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags,
const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) {
uint8_t path_sz = flags & 0x03; // NEW v1.11+
if (12 + path_len + (path_len >> path_sz) + 1 > sizeof(out_frame)) {
MESH_DEBUG_PRINTLN("onTraceRecv(), path_len is too long: %d", (uint32_t)path_len);
return;
}
int i = 0;
out_frame[i++] = PUSH_CODE_TRACE_DATA;
out_frame[i++] = 0; // reserved
@@ -681,8 +767,9 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code,
i += 4;
memcpy(&out_frame[i], path_hashes, path_len);
i += path_len;
memcpy(&out_frame[i], path_snrs, path_len);
i += path_len;
memcpy(&out_frame[i], path_snrs, path_len >> path_sz);
i += path_len >> path_sz;
out_frame[i++] = (int8_t)(packet->getSNR() * 4); // extra/final SNR (to this node)
if (_serial->isConnected()) {
@@ -696,9 +783,10 @@ uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const {
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 {
uint8_t path_hash_count = path_len & 63;
return SEND_TIMEOUT_BASE_MILLIS +
((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) *
(path_len + 1));
(path_hash_count + 1));
}
void MyMesh::onSendTimeout() {}
@@ -726,6 +814,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.bw = LORA_BW;
_prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.gps_enabled = 0; // GPS disabled by default
_prefs.gps_interval = 0; // No automatic GPS updates by default
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed
}
@@ -742,14 +832,14 @@ void MyMesh::begin(bool has_display) {
_store->saveMainIdentity(self_id);
}
// if name is provided as a build flag, use that as default node name instead
#ifdef ADVERT_NAME
strcpy(_prefs.node_name, ADVERT_NAME);
#else
// use hex of first 4 bytes of identity public key as default node name
char pub_key_hex[10];
mesh::Utils::toHex(pub_key_hex, self_id.pub_key, 4);
strcpy(_prefs.node_name, pub_key_hex);
// if name is provided as a build flag, use that as default node name instead
#ifdef ADVERT_NAME
strcpy(_prefs.node_name, ADVERT_NAME);
#endif
// load persisted prefs
@@ -762,7 +852,9 @@ void MyMesh::begin(bool has_display) {
_prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f);
_prefs.sf = constrain(_prefs.sf, 5, 12);
_prefs.cr = constrain(_prefs.cr, 5, 8);
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER);
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER);
_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
#ifdef BLE_PIN_CODE // 123456 by default
if (_prefs.ble_pin == 0) {
@@ -785,6 +877,7 @@ void MyMesh::begin(bool has_display) {
resetContacts();
_store->loadContacts(this);
bootstrapRTCfromContacts();
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
_store->loadChannels(this);
@@ -802,6 +895,24 @@ uint32_t MyMesh::getBLEPin() {
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) {
_serial = &serial;
serial.enable();
@@ -825,6 +936,8 @@ void MyMesh::handleCmdFrame(size_t len) {
i += 40;
StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20);
i += 20;
out_frame[i++] = _prefs.client_repeat; // v9+
out_frame[i++] = _prefs.path_hash_mode; // v10+
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_APP_START &&
len >= 8) { // sent when app establishes connection, respond with node ID
@@ -886,6 +999,7 @@ void MyMesh::handleCmdFrame(size_t len) {
int result;
uint32_t expected_ack;
if (txt_type == TXT_TYPE_CLI_DATA) {
msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection
result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout);
expected_ack = 0; // no Ack expected
} else {
@@ -1001,7 +1115,8 @@ void MyMesh::handleCmdFrame(size_t len) {
}
if (pkt) {
if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop)
sendFlood(pkt);
unsigned long delay_millis = 0;
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
sendZeroHop(pkt);
}
@@ -1013,7 +1128,7 @@ void MyMesh::handleCmdFrame(size_t len) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
recipient->out_path_len = -1;
recipient->out_path_len = OUT_PATH_UNKNOWN;
// recipient->lastmod = ?? shouldn't be needed, app already has this version of contact
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
writeOKFrame();
@@ -1045,6 +1160,7 @@ void MyMesh::handleCmdFrame(size_t len) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient && removeContact(*recipient)) {
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE);
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
writeOKFrame();
} else {
@@ -1127,13 +1243,20 @@ void MyMesh::handleCmdFrame(size_t len) {
i += 4;
uint8_t sf = 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 (freq >= 300000 && freq <= 2500000 && sf >= 7 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 &&
if (repeat && !isValidClientRepeatFreq(freq)) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 &&
bw <= 500000) {
_prefs.sf = sf;
_prefs.cr = cr;
_prefs.freq = (float)freq / 1000.0;
_prefs.bw = (float)bw / 1000.0;
_prefs.client_repeat = repeat;
savePrefs();
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
@@ -1147,10 +1270,11 @@ void MyMesh::handleCmdFrame(size_t len) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else if (cmd_frame[0] == CMD_SET_RADIO_TX_POWER) {
if (cmd_frame[1] > MAX_LORA_TX_POWER) {
int8_t power = (int8_t)cmd_frame[1];
if (power < -9 || power > MAX_LORA_TX_POWER) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
_prefs.tx_power_dbm = cmd_frame[1];
_prefs.tx_power_dbm = power;
savePrefs();
radio_set_tx_power(_prefs.tx_power_dbm);
writeOKFrame();
@@ -1189,6 +1313,14 @@ void MyMesh::handleCmdFrame(size_t len) {
}
savePrefs();
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) {
if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed?
saveContacts();
@@ -1216,16 +1348,20 @@ void MyMesh::handleCmdFrame(size_t len) {
#endif
} else if (cmd_frame[0] == CMD_IMPORT_PRIVATE_KEY && len >= 65) {
#if ENABLE_PRIVATE_KEY_IMPORT
mesh::LocalIdentity identity;
identity.readFrom(&cmd_frame[1], 64);
if (_store->saveMainIdentity(identity)) {
self_id = identity;
writeOKFrame();
// re-load contacts, to recalc shared secrets
resetContacts();
_store->loadContacts(this);
if (!mesh::LocalIdentity::validatePrivateKey(&cmd_frame[1])) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid key
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
mesh::LocalIdentity identity;
identity.readFrom(&cmd_frame[1], 64);
if (_store->saveMainIdentity(identity)) {
self_id = identity;
writeOKFrame();
// re-load contacts, to invalidate ecdh shared_secrets
resetContacts();
_store->loadContacts(this);
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
}
#else
writeDisabledFrame();
@@ -1268,6 +1404,27 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
}
} else if (cmd_frame[0] == CMD_SEND_ANON_REQ && len > 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
uint8_t *data = &cmd_frame[1 + PUB_KEY_SIZE];
if (recipient) {
uint32_t tag, est_timeout;
int result = sendAnonReq(*recipient, data, len - (1 + PUB_KEY_SIZE), tag, est_timeout);
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
clearPendingReqs();
pending_req = tag; // match this to onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
memcpy(&out_frame[2], &tag, 4);
memcpy(&out_frame[6], &est_timeout, 4);
_serial->writeFrame(out_frame, 10);
}
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
}
} else if (cmd_frame[0] == CMD_SEND_STATUS_REQ && len >= 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
@@ -1301,7 +1458,7 @@ void MyMesh::handleCmdFrame(size_t len) {
memset(&req_data[2], 0, 3); // reserved
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
recipient->out_path_len = -1;
recipient->out_path_len = OUT_PATH_UNKNOWN;
int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout);
recipient->out_path_len = save;
if (result == MSG_SEND_FAILED) {
@@ -1446,25 +1603,31 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_BAD_STATE);
}
} else if (cmd_frame[0] == CMD_SEND_TRACE_PATH && len > 10 && len - 10 < MAX_PATH_SIZE) {
uint32_t tag, auth;
memcpy(&tag, &cmd_frame[1], 4);
memcpy(&auth, &cmd_frame[5], 4);
auto pkt = createTrace(tag, auth, cmd_frame[9]);
if (pkt) {
uint8_t path_len = len - 10;
sendDirect(pkt, &cmd_frame[10], path_len);
uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2);
uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len);
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = 0;
memcpy(&out_frame[2], &tag, 4);
memcpy(&out_frame[6], &est_timeout, 4);
_serial->writeFrame(out_frame, 10);
} else if (cmd_frame[0] == CMD_SEND_TRACE_PATH && len > 10 && len - 10 < MAX_PACKET_PAYLOAD-5) {
uint8_t path_len = len - 10;
uint8_t flags = cmd_frame[9];
uint8_t path_sz = flags & 0x03; // NEW v1.11+
if ((path_len >> path_sz) > MAX_PATH_SIZE || (path_len % (1 << path_sz)) != 0) { // make sure is multiple of path_sz
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
uint32_t tag, auth;
memcpy(&tag, &cmd_frame[1], 4);
memcpy(&auth, &cmd_frame[5], 4);
auto pkt = createTrace(tag, auth, flags);
if (pkt) {
sendDirect(pkt, &cmd_frame[10], path_len);
uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2);
uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len >> path_sz);
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = 0;
memcpy(&out_frame[2], &tag, 4);
memcpy(&out_frame[6], &est_timeout, 4);
_serial->writeFrame(out_frame, 10);
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
}
} else if (cmd_frame[0] == CMD_SET_DEVICE_PIN && len >= 5) {
@@ -1502,6 +1665,17 @@ void MyMesh::handleCmdFrame(size_t len) {
*np++ = 0; // modify 'cmd_frame', replace ':' with null
bool success = sensors.setSettingValue(sp, np);
if (success) {
#if ENV_INCLUDE_GPS == 1
// Update node preferences for GPS settings
if (strcmp(sp, "gps") == 0) {
_prefs.gps_enabled = (np[0] == '1') ? 1 : 0;
savePrefs();
} else if (strcmp(sp, "gps_interval") == 0) {
uint32_t interval_seconds = atoi(np);
_prefs.gps_interval = constrain(interval_seconds, 0, 86400);
savePrefs();
}
#endif
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
@@ -1521,15 +1695,71 @@ void MyMesh::handleCmdFrame(size_t len) {
}
}
if (found) {
out_frame[0] = RESP_CODE_ADVERT_PATH;
memcpy(&out_frame[1], &found->recv_timestamp, 4);
out_frame[5] = found->path_len;
memcpy(&out_frame[6], found->path, found->path_len);
_serial->writeFrame(out_frame, 6 + found->path_len);
int i = 0;
out_frame[i++] = RESP_CODE_ADVERT_PATH;
memcpy(&out_frame[i], &found->recv_timestamp, 4); i += 4;
out_frame[i++] = found->path_len;
i += mesh::Packet::writePath(&out_frame[i], found->path, found->path_len);
_serial->writeFrame(out_frame, i);
} else {
writeErrFrame(ERR_CODE_NOT_FOUND);
}
} else if (cmd_frame[0] == CMD_GET_STATS && len >= 2) {
uint8_t stats_type = cmd_frame[1];
if (stats_type == STATS_TYPE_CORE) {
int i = 0;
out_frame[i++] = RESP_CODE_STATS;
out_frame[i++] = STATS_TYPE_CORE;
uint16_t battery_mv = board.getBattMilliVolts();
uint32_t uptime_secs = _ms->getMillis() / 1000;
uint8_t queue_len = (uint8_t)_mgr->getOutboundCount(0xFFFFFFFF);
memcpy(&out_frame[i], &battery_mv, 2); i += 2;
memcpy(&out_frame[i], &uptime_secs, 4); i += 4;
memcpy(&out_frame[i], &_err_flags, 2); i += 2;
out_frame[i++] = queue_len;
_serial->writeFrame(out_frame, i);
} else if (stats_type == STATS_TYPE_RADIO) {
int i = 0;
out_frame[i++] = RESP_CODE_STATS;
out_frame[i++] = STATS_TYPE_RADIO;
int16_t noise_floor = (int16_t)_radio->getNoiseFloor();
int8_t last_rssi = (int8_t)radio_driver.getLastRSSI();
int8_t last_snr = (int8_t)(radio_driver.getLastSNR() * 4); // scaled by 4 for 0.25 dB precision
uint32_t tx_air_secs = getTotalAirTime() / 1000;
uint32_t rx_air_secs = getReceiveAirTime() / 1000;
memcpy(&out_frame[i], &noise_floor, 2); i += 2;
out_frame[i++] = last_rssi;
out_frame[i++] = last_snr;
memcpy(&out_frame[i], &tx_air_secs, 4); i += 4;
memcpy(&out_frame[i], &rx_air_secs, 4); i += 4;
_serial->writeFrame(out_frame, i);
} else if (stats_type == STATS_TYPE_PACKETS) {
int i = 0;
out_frame[i++] = RESP_CODE_STATS;
out_frame[i++] = STATS_TYPE_PACKETS;
uint32_t recv = radio_driver.getPacketsRecv();
uint32_t sent = radio_driver.getPacketsSent();
uint32_t n_sent_flood = getNumSentFlood();
uint32_t n_sent_direct = getNumSentDirect();
uint32_t n_recv_flood = getNumRecvFlood();
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], &sent, 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_recv_flood, 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);
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type
}
} else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) {
if (_serial) {
MESH_DEBUG_PRINTLN("Factory reset: disabling serial interface to prevent reconnects (BLE/WiFi)");
_serial->disable(); // Phone app disconnects before we can send OK frame so it's safe here
}
bool success = _store->formatFileSystem();
if (success) {
writeOKFrame();
@@ -1553,6 +1783,24 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
} else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) {
_prefs.autoadd_config = cmd_frame[1];
savePrefs();
writeOKFrame();
} else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) {
int i = 0;
out_frame[i++] = RESP_CODE_AUTOADD_CONFIG;
out_frame[i++] = _prefs.autoadd_config;
_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);
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);
@@ -1797,4 +2045,4 @@ bool MyMesh::advert() {
} else {
return false;
}
}
}

View File

@@ -5,14 +5,14 @@
#include "AbstractUITask.h"
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 8
#define FIRMWARE_VER_CODE 10
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "13 Nov 2025"
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.10.0"
#define FIRMWARE_VERSION "v1.13.0"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -106,14 +106,21 @@ protected:
float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() 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;
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 mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override;
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
bool isAutoAddEnabled() const override;
bool shouldAutoAddContactType(uint8_t type) const override;
bool shouldOverwriteWhenFull() const override;
void onContactsFull() 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;
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
void onContactPathUpdated(const ContactInfo &contact) override;
@@ -152,6 +159,9 @@ protected:
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
}
public:
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
private:
void writeOKFrame();
void writeErrFrame(uint8_t err_code);
@@ -169,13 +179,12 @@ private:
void checkCLIRescueCmd();
void checkSerialInterface();
bool isValidClientRepeatFreq(uint32_t f) const;
// helpers, short-cuts
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
void saveChannels() { _store->saveChannels(this); }
void saveContacts() { _store->saveContacts(this); }
private:
DataStore* _store;
NodePrefs _prefs;
uint32_t pending_login;

View File

@@ -17,11 +17,17 @@ struct NodePrefs { // persisted to file
uint8_t multi_acks;
uint8_t manual_add_contacts;
float bw;
uint8_t tx_power_dbm;
int8_t tx_power_dbm;
uint8_t telemetry_mode_base;
uint8_t telemetry_mode_loc;
uint8_t telemetry_mode_env;
float rx_delay_base;
uint32_t ble_pin;
uint8_t advert_loc_policy;
uint8_t buzzer_quiet;
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval; // GPS read interval in seconds
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
};

View File

@@ -151,9 +151,7 @@ void setup() {
);
#ifdef BLE_PIN_CODE
char dev_name[32+16];
sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName());
serial_interface.begin(dev_name, the_mesh.getBLEPin());
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#else
serial_interface.begin(Serial);
#endif
@@ -196,12 +194,11 @@ void setup() {
);
#ifdef WIFI_SSID
board.setInhibitSleep(true); // prevent sleep when WiFi is active
WiFi.begin(WIFI_SSID, WIFI_PWD);
serial_interface.begin(TCP_PORT);
#elif defined(BLE_PIN_CODE)
char dev_name[32+16];
sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName());
serial_interface.begin(dev_name, the_mesh.getBLEPin());
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#elif defined(SERIAL_RX)
companion_serial.setPins(SERIAL_RX, SERIAL_TX);
companion_serial.begin(115200);

View File

@@ -2,6 +2,9 @@
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#include "target.h"
#ifdef WIFI_SSID
#include <WiFi.h>
#endif
#ifndef AUTO_OFF_MILLIS
#define AUTO_OFF_MILLIS 15000 // 15 seconds
@@ -100,8 +103,14 @@ class HomeScreen : public UIScreen {
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
// Convert millivolts to percentage
const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
#ifndef BATT_MIN_MILLIVOLTS
#define BATT_MIN_MILLIVOLTS 3000
#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);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
@@ -122,6 +131,14 @@ class HomeScreen : public UIScreen {
// fill the battery based on the percentage
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
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;
@@ -129,7 +146,7 @@ class HomeScreen : public UIScreen {
bool sensors_scroll = false;
int sensors_scroll_offset = 0;
int next_sensors_refresh = 0;
void refresh_sensors() {
if (millis() > next_sensors_refresh) {
sensors_lpp.reset();
@@ -192,10 +209,17 @@ public:
sprintf(tmp, "MSG: %d", _task->getMsgCount());
display.drawTextCentered(display.width() / 2, 20, tmp);
#ifdef WIFI_SSID
IPAddress ip = WiFi.localIP();
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 54, tmp);
#endif
if (_task->hasConnection()) {
display.setColor(DisplayDriver::GREEN);
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 43, "< Connected >");
} else if (the_mesh.getBLEPin() != 0) { // BT pin
display.setColor(DisplayDriver::RED);
display.setTextSize(2);
@@ -260,13 +284,24 @@ public:
#if ENV_INCLUDE_GPS == 1
} else if (_page == HomePage::GPS) {
LocationProvider* nmea = sensors.getLocationProvider();
char buf[50];
int y = 18;
display.drawTextLeftAlign(0, y, _task->getGPSState() ? "gps on" : "gps off");
bool gps_state = _task->getGPSState();
#ifdef PIN_GPS_SWITCH
bool hw_gps_state = digitalRead(PIN_GPS_SWITCH);
if (gps_state != hw_gps_state) {
strcpy(buf, gps_state ? "gps off(hw)" : "gps off(sw)");
} else {
strcpy(buf, gps_state ? "gps on" : "gps off");
}
#else
strcpy(buf, gps_state ? "gps on" : "gps off");
#endif
display.drawTextLeftAlign(0, y, buf);
if (nmea == NULL) {
y = y + 12;
display.drawTextLeftAlign(0, y, "Can't access GPS");
} else {
char buf[50];
strcpy(buf, nmea->isValid()?"fix":"no fix");
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
@@ -431,15 +466,17 @@ class MsgPreviewScreen : public UIScreen {
};
#define MAX_UNREAD_MSGS 32
int num_unread;
int head = MAX_UNREAD_MSGS - 1; // index of latest unread message
MsgEntry unread[MAX_UNREAD_MSGS];
public:
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) {
if (num_unread >= MAX_UNREAD_MSGS) return; // full
head = (head + 1) % MAX_UNREAD_MSGS;
if (num_unread < MAX_UNREAD_MSGS) num_unread++;
auto p = &unread[num_unread++];
auto p = &unread[head];
p->timestamp = _rtc->getCurrentTime();
if (path_len == 0xFF) {
sprintf(p->origin, "(D) %s:", from_name);
@@ -457,7 +494,7 @@ public:
sprintf(tmp, "Unread: %d", num_unread);
display.print(tmp);
auto p = &unread[0];
auto p = &unread[head];
int secs = _rtc->getCurrentTime() - p->timestamp;
if (secs < 60) {
@@ -493,14 +530,10 @@ public:
bool handleInput(char c) override {
if (c == KEY_NEXT || c == KEY_RIGHT) {
head = (head + MAX_UNREAD_MSGS - 1) % MAX_UNREAD_MSGS;
num_unread--;
if (num_unread == 0) {
_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;
}
@@ -526,12 +559,26 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#endif
_node_prefs = node_prefs;
#if ENV_INCLUDE_GPS == 1
// Apply GPS preferences from stored prefs
if (_sensors != NULL && _node_prefs != NULL) {
_sensors->setSettingValue("gps", _node_prefs->gps_enabled ? "1" : "0");
if (_node_prefs->gps_interval > 0) {
char interval_str[12]; // Max: 24 hours = 86400 seconds (5 digits + null)
sprintf(interval_str, "%u", _node_prefs->gps_interval);
_sensors->setSettingValue("gps_interval", interval_str);
}
}
#endif
if (_display != NULL) {
_display->turnOn();
}
#ifdef PIN_BUZZER
buzzer.begin();
buzzer.quiet(_node_prefs->buzzer_quiet);
#endif
#ifdef PIN_VIBRATION
@@ -596,9 +643,13 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
setCurrScreen(msg_preview);
if (_display != NULL) {
if (!_display->isOn()) _display->turnOn();
if (!_display->isOn() && !hasConnection()) {
_display->turnOn();
}
if (_display->isOn()) {
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
_next_refresh = 100; // trigger refresh
}
}
}
@@ -618,7 +669,7 @@ void UITask::userLedHandler() {
led_state = 0;
next_led_change = cur_time + LED_CYCLE_MILLIS - last_led_increment;
}
digitalWrite(PIN_STATUS_LED, led_state);
digitalWrite(PIN_STATUS_LED, led_state == LED_STATE_ON);
}
#endif
}
@@ -650,6 +701,7 @@ void UITask::shutdown(bool restart){
_board->reboot();
} else {
_display->turnOff();
radio_driver.powerOff();
_board->powerOff();
}
}
@@ -714,10 +766,14 @@ void UITask::loop() {
_analogue_pin_read_millis = millis();
}
#endif
#if defined(DISP_BACKLIGHT) && defined(BACKLIGHT_BTN)
#if defined(BACKLIGHT_BTN)
if (millis() > next_backlight_btn_check) {
bool touch_state = digitalRead(PIN_BUTTON2);
#if defined(DISP_BACKLIGHT)
digitalWrite(DISP_BACKLIGHT, !touch_state);
#elif defined(EXP_PIN_BACKLIGHT)
expander.digitalWrite(EXP_PIN_BACKLIGHT, !touch_state);
#endif
next_backlight_btn_check = millis() + 300;
}
#endif
@@ -846,13 +902,15 @@ void UITask::toggleGPS() {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
_sensors->setSettingValue("gps", "0");
_node_prefs->gps_enabled = 0;
notify(UIEventType::ack);
showAlert("GPS: Disabled", 800);
} else {
_sensors->setSettingValue("gps", "1");
_node_prefs->gps_enabled = 1;
notify(UIEventType::ack);
showAlert("GPS: Enabled", 800);
}
the_mesh.savePrefs();
showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800);
_next_refresh = 0;
break;
}
@@ -866,11 +924,12 @@ void UITask::toggleBuzzer() {
if (buzzer.isQuiet()) {
buzzer.quiet(false);
notify(UIEventType::ack);
showAlert("Buzzer: ON", 800);
} else {
buzzer.quiet(true);
showAlert("Buzzer: OFF", 800);
}
_node_prefs->buzzer_quiet = buzzer.isQuiet();
the_mesh.savePrefs();
showAlert(buzzer.isQuiet() ? "Buzzer: OFF" : "Buzzer: ON", 800);
_next_refresh = 0; // trigger refresh
#endif
}

View File

@@ -8,6 +8,10 @@
#include <Arduino.h>
#include <helpers/sensors/LPPDataHelpers.h>
#ifndef LED_STATE_ON
#define LED_STATE_ON 1
#endif
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
@@ -50,7 +54,7 @@ class UITask : public AbstractUITask {
UIScreen* curr;
void userLedHandler();
// Button action handlers
char checkDisplayOn(char c);
char handleLongPress(char c);
@@ -74,6 +78,14 @@ public:
bool hasDisplay() const { return _display != NULL; }
bool isButtonPressed() const;
bool isBuzzerQuiet() {
#ifdef PIN_BUZZER
return buzzer.isQuiet();
#else
return true;
#endif
}
void toggleBuzzer();
bool getGPSState();
void toggleGPS();

View File

@@ -115,4 +115,8 @@ static const uint8_t advert_icon[] = {
0x38, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x30,
0x04, 0x00, 0x00, 0x20, 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

@@ -56,6 +56,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#ifdef PIN_BUZZER
buzzer.begin();
buzzer.quiet(_node_prefs->buzzer_quiet);
#endif
// Initialize digital button if available
@@ -136,16 +137,26 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
StrHelper::strncpy(_msg, text, sizeof(_msg));
if (_display != NULL) {
if (!_display->isOn()) _display->turnOn();
if (!_display->isOn() && !hasConnection()) {
_display->turnOn();
}
if (_display->isOn()) {
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
_need_refresh = true;
}
}
}
void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) {
// Convert millivolts to percentage
const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
#ifndef BATT_MIN_MILLIVOLTS
#define BATT_MIN_MILLIVOLTS 3000
#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);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
@@ -269,7 +280,7 @@ void UITask::userLedHandler() {
state = 0;
next_change = cur_time + LED_CYCLE_MILLIS - last_increment;
}
digitalWrite(PIN_STATUS_LED, state);
digitalWrite(PIN_STATUS_LED, state == LED_STATE_ON);
}
#endif
}
@@ -292,10 +303,12 @@ void UITask::shutdown(bool restart){
#endif // PIN_BUZZER
if (restart)
if (restart) {
_board->reboot();
else
} else {
radio_driver.powerOff();
_board->powerOff();
}
}
void UITask::loop() {
@@ -394,6 +407,8 @@ void UITask::handleButtonTriplePress() {
buzzer.quiet(true);
sprintf(_alert, "Buzzer: OFF");
}
_node_prefs->buzzer_quiet = buzzer.isQuiet();
the_mesh.savePrefs();
_need_refresh = true;
#endif
}

View File

@@ -0,0 +1,581 @@
#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

@@ -0,0 +1,183 @@
#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

@@ -0,0 +1,146 @@
#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

@@ -41,16 +41,21 @@
#define TXT_ACK_DELAY 200
#endif
#define FIRMWARE_VER_LEVEL 1
#define FIRMWARE_VER_LEVEL 2
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
#define REQ_TYPE_GET_ACCESS_LIST 0x05
#define REQ_TYPE_GET_NEIGHBOURS 0x06
#define REQ_TYPE_GET_OWNER_INFO 0x07 // FIRMWARE_VER_LEVEL >= 2
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
#define ANON_REQ_TYPE_REGIONS 0x01
#define ANON_REQ_TYPE_OWNER 0x02
#define ANON_REQ_TYPE_BASIC 0x03 // just remote clock
#define CLI_REPLY_DELAY_MILLIS 600
#define LAZY_CONTACTS_WRITE_DELAY 5000
@@ -82,7 +87,7 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn
#endif
}
uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) {
uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) {
ClientInfo* client = NULL;
if (data[0] == 0) { // blank password, just check if sender is in ACL
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
@@ -123,6 +128,10 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
}
}
if (is_flood) {
client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path
}
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
reply_data[4] = RESP_SERVER_LOGIN_OK;
@@ -135,6 +144,73 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
return 13; // reply length
}
uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path}
reply_path_len = *data & 63;
reply_path_hash_size = (*data >> 6) + 1;
data++;
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
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness)
return 8 + region_map.exportNamesTo((char *) &reply_data[8], sizeof(reply_data) - 12, REGION_DENY_FLOOD); // reply length
}
return 0;
}
uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path}
reply_path_len = *data & 63;
reply_path_hash_size = (*data >> 6) + 1;
data++;
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
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness)
sprintf((char *) &reply_data[8], "%s\n%s", _prefs.node_name, _prefs.owner_info);
return 8 + strlen((char *) &reply_data[8]); // reply length
}
return 0;
}
uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path}
reply_path_len = *data & 63;
reply_path_hash_size = (*data >> 6) + 1;
data++;
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
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness)
reply_data[8] = 0; // features
#ifdef WITH_RS232_BRIDGE
reply_data[8] |= 0x01; // is bridge, type UART
#elif WITH_ESPNOW_BRIDGE
reply_data[8] |= 0x03; // is bridge, type ESP-NOW
#endif
if (_prefs.disable_fwd) { // is this repeater currently disabled
reply_data[8] |= 0x80; // is disabled
}
// TODO: add some kind of moving-window utilisation metric, so can query 'how busy' is this repeater
return 9; // reply length
}
return 0;
}
int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len) {
// uint32_t now = getRTCClock()->getCurrentTimeUnique();
// memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
@@ -159,7 +235,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
stats.total_rx_air_time_secs = getReceiveAirTime() / 1000;
stats.n_recv_errors = radio_driver.getPacketsRecvErrors();
memcpy(&reply_data[4], &stats, sizeof(stats));
return 4 + sizeof(stats); // reply_len
@@ -169,12 +245,19 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
if ((sender->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) {
perm_mask = 0x00; // just base telemetry allowed
}
sensors.querySensors(perm_mask, telemetry);
// This default temperature will be overridden by external sensors (if any)
float temperature = board.getMCUTemperature();
if(!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature
}
uint8_t tlen = telemetry.getSize();
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
return 4 + tlen; // reply_len
@@ -218,6 +301,7 @@ 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
int16_t neighbours_count = 0;
#if MAX_NEIGHBOURS
NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS];
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
auto neighbour = &neighbours[i];
@@ -253,6 +337,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
return a->snr < b->snr; // asc
});
}
#endif
// build results buffer
int results_count = 0;
@@ -267,6 +352,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
break;
}
#if MAX_NEIGHBOURS
// add next neighbour to results
auto neighbour = sorted_neighbours[index + offset];
uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
@@ -274,6 +360,7 @@ 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], &neighbour->snr, 1); results_offset += 1;
results_count++;
#endif
}
@@ -285,6 +372,9 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
return reply_offset;
}
} else if (payload[0] == REQ_TYPE_GET_OWNER_INFO) {
sprintf((char *) &reply_data[4], "%s\n%s\n%s", FIRMWARE_VERSION, _prefs.node_name, _prefs.owner_info);
return 4 + strlen((char *) &reply_data[4]);
}
return 0; // unknown command
}
@@ -308,7 +398,7 @@ File MyMesh::openAppend(const char *fname) {
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
if (packet->isRouteFlood() && recv_pkt_region == NULL) {
MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet");
return false;
@@ -403,11 +493,11 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
}
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1);
}
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1);
}
@@ -437,12 +527,18 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
data[len] = 0; // ensure null terminator
uint8_t reply_len;
reply_path_len = -1;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
//} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes
// TODO
reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood());
} else if (data[4] == ANON_REQ_TYPE_REGIONS && packet->isRouteDirect()) {
reply_len = handleAnonRegionsReq(sender, timestamp, &data[5]);
} else if (data[4] == ANON_REQ_TYPE_OWNER && packet->isRouteDirect()) {
reply_len = handleAnonOwnerReq(sender, timestamp, &data[5]);
} else if (data[4] == ANON_REQ_TYPE_BASIC && packet->isRouteDirect()) {
reply_len = handleAnonClockReq(sender, timestamp, &data[5]);
} else {
reply_len = 0; // unknown request type
reply_len = 0; // unknown/invalid request type
}
if (reply_len == 0) return; // invalid request
@@ -451,10 +547,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
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else if (reply_path_len < 0) {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
uint8_t path_len = ((reply_path_hash_size - 1) << 6) | (reply_path_len & 63);
if (reply) sendDirect(reply, reply_path, path_len, SERVER_RESPONSE_DELAY);
}
}
}
@@ -523,15 +623,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
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else {
mesh::Packet *reply =
createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
if (reply) {
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply, SERVER_RESPONSE_DELAY);
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
}
}
}
@@ -541,7 +641,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->isAdmin()) { // a CLI command
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint flags = (data[4] >> 2); // message attempt number, and other flags
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
@@ -561,8 +661,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
mesh::Packet *ack = createAck(ack_hash);
if (ack) {
if (client->out_path_len < 0) {
sendFlood(ack, TXT_ACK_DELAY);
if (client->out_path_len == OUT_PATH_UNKNOWN) {
sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize());
} else {
sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
}
@@ -589,8 +689,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);
if (reply) {
if (client->out_path_len < 0) {
sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
if (client->out_path_len == OUT_PATH_UNKNOWN) {
sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
} else {
sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS);
}
@@ -611,7 +711,8 @@ 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);
auto client = acl.getClientByIdx(i);
memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
// 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();
} else {
MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
@@ -626,7 +727,9 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
void MyMesh::onControlDataRecv(mesh::Packet* packet) {
uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6 && discover_limiter.allow(rtc_clock.getCurrentTime())) {
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6
&& !_prefs.disable_fwd && discover_limiter.allow(rtc_clock.getCurrentTime())
) {
int i = 1;
uint8_t filter = packet->payload[i++];
uint32_t tag;
@@ -650,14 +753,56 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) {
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);
}
}
MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
mesh::RTCClock &rtc, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
_cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store),
discover_limiter(4, 120) // max 4 every 2 minutes
_cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store),
discover_limiter(4, 120), // max 4 every 2 minutes
anon_limiter(4, 180) // max 4 every 3 minutes
#if defined(WITH_RS232_BRIDGE)
, bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc)
#endif
@@ -682,7 +827,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.airtime_factor = 1.0; // one half
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
_prefs.tx_delay_factor = 0.5f; // was 0.25f
_prefs.direct_tx_delay_factor = 0.2f; // was zero
_prefs.direct_tx_delay_factor = 0.3f; // was 0.2
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
_prefs.node_lat = ADVERT_LAT;
_prefs.node_lon = ADVERT_LON;
@@ -710,6 +855,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_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) {
@@ -717,7 +867,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
_fs = fs;
// load persisted prefs
_cli.loadPrefs(_fs);
acl.load(_fs);
acl.load(_fs, self_id);
// TODO: key_store.begin();
region_map.load(_fs);
@@ -733,6 +883,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
updateAdvertTimer();
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
#endif
@@ -761,10 +913,14 @@ bool MyMesh::formatFileSystem() {
#endif
}
void MyMesh::sendSelfAdvertisement(int delay_millis) {
void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet *pkt = createSelfAdvert();
if (pkt) {
sendFlood(pkt, delay_millis);
if (flood) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
sendZeroHop(pkt, delay_millis);
}
} else {
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
}
@@ -802,7 +958,7 @@ void MyMesh::dumpLogFile() {
}
}
void MyMesh::setTxPower(uint8_t power_dbm) {
void MyMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}
@@ -875,7 +1031,6 @@ void MyMesh::formatPacketStatsReply(char *reply) {
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
self_id = new_id;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
#elif defined(ESP32)
@@ -885,7 +1040,7 @@ void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#else
#error "need to define saveIdentity()"
#endif
store.save("_main", self_id);
store.save("_main", new_id);
}
void MyMesh::clearStats() {
@@ -976,8 +1131,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
const char* parts[4];
int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
if (n == 1 && sender_timestamp == 0) {
region_map.exportTo(Serial);
if (n == 1) {
region_map.exportTo(reply, 160);
} else if (n >= 2 && strcmp(parts[1], "load") == 0) {
temp_map.resetFrom(region_map); // rebuild regions in a temp instance
memset(load_stack, 0, sizeof(load_stack));
@@ -1050,9 +1205,37 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
} else {
strcpy(reply, "Err - not found");
}
} else if (n >= 3 && strcmp(parts[1], "list") == 0) {
uint8_t mask = 0;
bool invert = false;
if (strcmp(parts[2], "allowed") == 0) {
mask = REGION_DENY_FLOOD;
invert = false; // list regions that DON'T have DENY flag
} else if (strcmp(parts[2], "denied") == 0) {
mask = REGION_DENY_FLOOD;
invert = true; // list regions that DO have DENY flag
} else {
strcpy(reply, "Err - use 'allowed' or 'denied'");
return;
}
int len = region_map.exportNamesTo(reply, 160, mask, invert);
if (len == 0) {
strcpy(reply, "-none-");
}
} else {
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{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
@@ -1101,3 +1284,11 @@ void MyMesh::loop() {
uptime_millis += now - last_millis;
last_millis = now;
}
// To check if there is pending work
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;
}

View File

@@ -54,6 +54,7 @@ struct RepeaterStats {
int16_t last_snr; // x 4
uint16_t n_direct_dups, n_flood_dups;
uint32_t total_rx_air_time_secs;
uint32_t n_recv_errors;
};
#ifndef MAX_CLIENTS
@@ -68,11 +69,11 @@ struct NeighbourInfo {
};
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "13 Nov 2025"
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.10.0"
#define FIRMWARE_VERSION "v1.13.0"
#endif
#define FIRMWARE_ROLE "repeater"
@@ -86,14 +87,19 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
unsigned long next_local_advert, next_flood_advert;
bool _logging;
NodePrefs _prefs;
ClientACL acl;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
ClientACL acl;
uint8_t reply_path[MAX_PATH_SIZE];
int8_t reply_path_len;
uint8_t reply_path_hash_size;
TransportKeyStore key_store;
RegionMap region_map, temp_map;
RegionEntry* load_stack[8];
RegionEntry* recv_pkt_region;
RateLimiter discover_limiter;
RateLimiter discover_limiter, anon_limiter;
uint32_t pending_discover_tag;
unsigned long pending_discover_until;
bool region_load_active;
unsigned long dirty_contacts_expiry;
#if MAX_NEIGHBOURS
@@ -113,7 +119,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
#endif
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
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 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 handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
mesh::Packet* createSelfAdvert();
@@ -181,7 +191,7 @@ public:
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis) override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;
@@ -192,7 +202,7 @@ public:
}
void dumpLogFile() override;
void setTxPower(uint8_t power_dbm) override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override;
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
void formatStatsReply(char *reply) override;
@@ -225,4 +235,7 @@ public:
bridge.begin();
}
#endif
// To check if there is pending work
bool hasPendingWork() const;
};

View File

@@ -19,12 +19,25 @@ void halt() {
static char command[160];
// For power saving
unsigned long lastActive = 0; // mark last active time
unsigned long nextSleepinSecs = 120; // next sleep in seconds. The first sleep (if enabled) is after 2 minutes from boot
void setup() {
Serial.begin(115200);
delay(1000);
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
lastActive = millis(); // mark last active time since boot
#ifdef DISPLAY_CLASS
if (display.begin()) {
display.startFrame();
@@ -35,6 +48,7 @@ void setup() {
#endif
if (!radio_init()) {
MESH_DEBUG_PRINTLN("Radio init failed!");
halt();
}
@@ -80,8 +94,10 @@ void setup() {
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
// send out initial Advertisement to the mesh
the_mesh.sendSelfAdvertisement(16000);
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
}
void loop() {
@@ -117,4 +133,18 @@ void loop() {
ui_task.loop();
#endif
rtc_clock.tick();
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible
#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
lastActive = millis();
nextSleepinSecs = 5; // Default: To work for 5s and sleep again
} else {
nextSleepinSecs += 5; // When there is pending work, to work another 5s
}
#endif
}
}

View File

@@ -73,13 +73,15 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) {
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len);
if (reply) {
if (client->out_path_len < 0) {
sendFlood(reply);
if (client->out_path_len == OUT_PATH_UNKNOWN) {
unsigned long delay_millis = 0;
sendFlood(reply, delay_millis, _prefs.path_hash_mode + 1);
client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD);
} else {
sendDirect(reply, client->out_path, client->out_path_len);
client->extra.room.ack_timeout =
futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (client->out_path_len + 1));
uint8_t path_hash_count = client->out_path_len & 63;
client->extra.room.ack_timeout = futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (path_hash_count + 1));
}
_num_post_pushes++; // stats
} else {
@@ -264,17 +266,17 @@ const char *MyMesh::getLogDateTime() {
}
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1);
}
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1);
}
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
return true;
}
@@ -332,6 +334,10 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
if (packet->isRouteFlood()) {
client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path
}
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
// TODO: maybe reply with count of messages waiting to be synced for THIS client?
@@ -349,14 +355,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
mesh::Packet *path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, 13);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else {
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13);
if (reply) {
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply, SERVER_RESPONSE_DELAY);
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
}
}
}
@@ -394,7 +400,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { // a CLI command or new Post
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint flags = (data[4] >> 2); // message attempt number, and other flags
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags);
@@ -444,9 +450,9 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
uint32_t delay_millis;
if (send_ack) {
if (client->out_path_len < 0) {
if (client->out_path_len == OUT_PATH_UNKNOWN) {
mesh::Packet *ack = createAck(ack_hash);
if (ack) sendFlood(ack, TXT_ACK_DELAY);
if (ack) sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize());
delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
} else {
uint32_t d = TXT_ACK_DELAY;
@@ -478,8 +484,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);
if (reply) {
if (client->out_path_len < 0) {
sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY);
if (client->out_path_len == OUT_PATH_UNKNOWN) {
sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else {
sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY);
}
@@ -517,7 +523,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
// if client sends too quickly, evict()
// RULE: only send keep_alive response DIRECT!
if (client->out_path_len >= 0) {
if (client->out_path_len != OUT_PATH_UNKNOWN) {
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);
@@ -534,14 +540,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
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else {
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
if (reply) {
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply, SERVER_RESPONSE_DELAY);
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
}
}
}
@@ -559,7 +565,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)
MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len);
auto client = acl.getClientByIdx(i);
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); // store a copy of path, for sendDirect()
client->last_activity = getRTCClock()->getCurrentTime();
} else {
MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
@@ -583,7 +589,7 @@ void MyMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) {
MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
mesh::RTCClock &rtc, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
_cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
_cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
last_millis = 0;
uptime_millis = 0;
next_local_advert = next_flood_advert = 0;
@@ -633,7 +639,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
// load persisted prefs
_cli.loadPrefs(_fs);
acl.load(_fs);
acl.load(_fs, self_id);
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
@@ -641,6 +647,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
updateAdvertTimer();
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
#endif
@@ -669,10 +677,14 @@ bool MyMesh::formatFileSystem() {
#endif
}
void MyMesh::sendSelfAdvertisement(int delay_millis) {
void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet *pkt = createSelfAdvert();
if (pkt) {
sendFlood(pkt, delay_millis);
if (flood) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
sendZeroHop(pkt, delay_millis);
}
} else {
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
}
@@ -709,12 +721,11 @@ void MyMesh::dumpLogFile() {
}
}
void MyMesh::setTxPower(uint8_t power_dbm) {
void MyMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
self_id = new_id;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
#elif defined(ESP32)
@@ -724,7 +735,7 @@ void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#else
#error "need to define saveIdentity()"
#endif
store.save("_main", self_id);
store.save("_main", new_id);
}
void MyMesh::clearStats() {
@@ -809,7 +820,7 @@ void MyMesh::loop() {
if (c->extra.room.pending_ack && millisHasNowPassed(c->extra.room.ack_timeout)) {
c->extra.room.push_failures++;
c->extra.room.pending_ack = 0; // reset (TODO: keep prev expected_ack's in a list, incase they arrive LATER, after we retry)
MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->push_failures);
MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->extra.room.push_failures);
}
}
// check next Round-Robin client, and sync next new post

View File

@@ -26,11 +26,11 @@
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "13 Nov 2025"
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.10.0"
#define FIRMWARE_VERSION "v1.13.0"
#endif
#ifndef LORA_FREQ
@@ -94,8 +94,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
unsigned long next_local_advert, next_flood_advert;
bool _logging;
NodePrefs _prefs;
CommonCLI _cli;
ClientACL acl;
CommonCLI _cli;
unsigned long dirty_contacts_expiry;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
unsigned long next_push;
@@ -177,7 +177,7 @@ public:
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis) override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;
@@ -188,7 +188,7 @@ public:
}
void dumpLogFile() override;
void setTxPower(uint8_t power_dbm) override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");

View File

@@ -76,8 +76,10 @@ void setup() {
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
// send out initial Advertisement to the mesh
the_mesh.sendSelfAdvertisement(16000);
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
}
void loop() {

View File

@@ -66,7 +66,7 @@ struct NodePrefs { // persisted to file
char node_name[32];
double node_lat, node_lon;
float freq;
uint8_t tx_power_dbm;
int8_t tx_power_dbm;
uint8_t unused[3];
};
@@ -213,7 +213,7 @@ protected:
}
void onContactPathUpdated(const ContactInfo& contact) override {
Serial.printf("PATH to: %s, path_len=%d\n", contact.name, (int32_t) contact.out_path_len);
Serial.printf("PATH to: %s, path_len=%d\n", contact.name, (uint32_t) contact.out_path_len);
saveContacts();
}
@@ -266,8 +266,9 @@ protected:
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 {
uint8_t path_hash_count = path_len & 63;
return SEND_TIMEOUT_BASE_MILLIS +
( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_len + 1));
( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_hash_count + 1));
}
void onSendTimeout() override {
@@ -290,7 +291,7 @@ public:
}
float getFreqPref() const { return _prefs.freq; }
uint8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }
int8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }
void begin(FILESYSTEM& fs) {
_fs = &fs;
@@ -582,7 +583,9 @@ void setup() {
the_mesh.showWelcome();
// send out initial Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvert(1200); // add slight delay
#endif
}
void loop() {

View File

@@ -258,10 +258,11 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len);
if (pkt) {
if (c->out_path_len >= 0) { // we have an out_path, so send DIRECT
if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
sendDirect(pkt, c->out_path, c->out_path_len);
} else {
sendFlood(pkt);
unsigned long delay_millis = 0;
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
}
}
t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
@@ -302,7 +303,7 @@ float SensorMesh::getAirtimeBudgetFactor() const {
bool SensorMesh::allowPacketForward(const mesh::Packet* packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
return true;
}
@@ -312,11 +313,11 @@ int SensorMesh::calcRxDelay(float score, uint32_t air_time) const {
}
uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
int SensorMesh::getInterferenceThreshold() const {
@@ -326,7 +327,7 @@ int SensorMesh::getAGCResetInterval() const {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) {
uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) {
ClientInfo* client;
if (data[0] == 0) { // blank password, just check if sender is in ACL
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
@@ -359,6 +360,10 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
if (is_flood) {
client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path
}
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
reply_data[4] = RESP_SERVER_LOGIN_OK;
@@ -451,7 +456,7 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
data[len] = 0; // ensure null terminator
uint8_t reply_len;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood());
//} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes
// TODO
} else {
@@ -464,10 +469,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
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
}
}
}
@@ -492,10 +497,10 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
}
}
void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash) {
if (dest.out_path_len < 0) {
void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) {
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
mesh::Packet* ack = createAck(ack_hash);
if (ack) sendFlood(ack, TXT_ACK_DELAY);
if (ack) sendFlood(ack, TXT_ACK_DELAY, path_hash_size);
} else {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
@@ -533,14 +538,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
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
if (reply) {
if (from->out_path_len >= 0) { // we have an out_path, so send DIRECT
if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply, SERVER_RESPONSE_DELAY);
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
}
}
}
@@ -550,7 +555,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && from->isAdmin()) { // a CLI command
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint flags = (data[4] >> 2); // message attempt number, and other flags
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags
if (sender_timestamp > from->last_timestamp) { // prevent replay attacks
if (flags == TXT_TYPE_PLAIN) {
@@ -563,9 +568,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
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
if (path) sendFlood(path, TXT_ACK_DELAY);
if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize());
} else {
sendAckTo(*from, ack_hash);
sendAckTo(*from, ack_hash, packet->getPathHashSize());
}
}
} else if (flags == TXT_TYPE_CLI_DATA) {
@@ -592,8 +597,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);
if (reply) {
if (from->out_path_len < 0) {
sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
if (from->out_path_len == OUT_PATH_UNKNOWN) {
sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
} else {
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
}
@@ -608,7 +613,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
}
}
bool SensorMesh::handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint flags, size_t len) {
bool SensorMesh::handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len) {
MESH_DEBUG_PRINT("handleIncomingMsg: unhandled msg from ");
#ifdef MESH_DEBUG
mesh::Utils::printHex(Serial, from.id.pub_key, PUB_KEY_SIZE);
@@ -662,7 +667,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);
// 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'(?)
memcpy(from->out_path, path, from->out_path_len = path_len); // store a copy of path, for sendDirect()
from->out_path_len = mesh::Packet::copyPath(from->out_path, path, path_len); // store a copy of path, for sendDirect()
from->last_activity = getRTCClock()->getCurrentTime();
// REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes??
@@ -691,7 +696,7 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
_cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
_cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
{
next_local_advert = next_flood_advert = 0;
dirty_contacts_expiry = 0;
@@ -732,7 +737,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {
// load persisted prefs
_cli.loadPrefs(_fs);
acl.load(_fs);
acl.load(_fs, self_id);
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
@@ -740,6 +745,8 @@ void SensorMesh::begin(FILESYSTEM* fs) {
updateAdvertTimer();
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
#endif
@@ -759,7 +766,6 @@ bool SensorMesh::formatFileSystem() {
}
void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
self_id = new_id;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
#elif defined(ESP32)
@@ -769,7 +775,7 @@ void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
#else
#error "need to define saveIdentity()"
#endif
store.save("_main", self_id);
store.save("_main", new_id);
}
void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
@@ -782,10 +788,14 @@ void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t
revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params
}
void SensorMesh::sendSelfAdvertisement(int delay_millis) {
void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) {
sendFlood(pkt, delay_millis);
if (flood) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
sendZeroHop(pkt, delay_millis);
}
} else {
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
}
@@ -806,7 +816,7 @@ void SensorMesh::updateFloodAdvertTimer() {
}
}
void SensorMesh::setTxPower(uint8_t power_dbm) {
void SensorMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}
@@ -859,7 +869,8 @@ void SensorMesh::loop() {
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) sendFlood(pkt);
unsigned long delay_millis = 0;
if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
updateFloodAdvertTimer(); // schedule next flood advert
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
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "13 Nov 2025"
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.10.0"
#define FIRMWARE_VERSION "v1.13.0"
#endif
#define FIRMWARE_ROLE "sensor"
@@ -60,13 +60,13 @@ public:
NodePrefs* getNodePrefs() { return &_prefs; }
void savePrefs() override { _cli.savePrefs(_fs); }
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis) override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;
void setLoggingOn(bool enable) override { }
void eraseLogFile() override { }
void dumpLogFile() override { }
void setTxPower(uint8_t power_dbm) override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}
@@ -127,15 +127,15 @@ protected:
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onControlDataRecv(mesh::Packet* packet) override;
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint flags, size_t len);
void sendAckTo(const ClientInfo& dest, uint32_t ack_hash);
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);
private:
FILESYSTEM* _fs;
unsigned long next_local_advert, next_flood_advert;
NodePrefs _prefs;
ClientACL acl;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
ClientACL acl;
unsigned long dirty_contacts_expiry;
CayenneLPP telemetry;
uint32_t last_read_time;
@@ -148,7 +148,7 @@ private:
uint8_t pending_sf;
uint8_t pending_cr;
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
mesh::Packet* createSelfAdvert();

View File

@@ -110,8 +110,10 @@ void setup() {
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
// send out initial Advertisement to the mesh
the_mesh.sendSelfAdvertisement(16000);
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
}
void loop() {

View File

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

View File

@@ -68,7 +68,7 @@ void Dispatcher::loop() {
next_tx_time = futureMillis(t * getAirtimeBudgetFactor());
_radio->onSendFinished();
logTx(outbound, 2 + outbound->path_len + outbound->payload_len);
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
if (outbound->isRouteFlood()) {
n_sent_flood++;
} else {
@@ -80,7 +80,7 @@ void Dispatcher::loop() {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime());
_radio->onSendFinished();
logTxFail(outbound, 2 + outbound->path_len + outbound->payload_len);
logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
releasePacket(outbound); // return to pool
outbound = NULL;
@@ -108,6 +108,48 @@ void Dispatcher::loop() {
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() {
Packet* pkt;
float score;
@@ -122,45 +164,14 @@ void Dispatcher::checkRecv() {
if (pkt == NULL) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): WARNING: received data, no unused packets available!", getLogDateTime());
} else {
int i = 0;
#ifdef NODE_ID
uint8_t sender_id = raw[i++];
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
if (tryParsePacket(pkt, raw, len)) {
pkt->_snr = _radio->getLastSNR() * 4.0f;
score = _radio->packetScore(_radio->getLastSNR(), len);
air_time = _radio->getEstAirtimeFor(len);
rx_air_time += air_time;
} 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
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 {
@@ -249,16 +260,13 @@ void Dispatcher::checkSend() {
int len = 0;
uint8_t raw[MAX_TRANS_UNIT];
#ifdef NODE_ID
raw[len++] = NODE_ID;
#endif
raw[len++] = outbound->header;
if (outbound->hasTransportCodes()) {
memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2;
memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2;
}
raw[len++] = outbound->path_len;
memcpy(&raw[len], outbound->path, outbound->path_len); len += outbound->path_len;
len += Packet::writePath(&raw[len], outbound->path, outbound->path_len);
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);
@@ -312,7 +320,7 @@ void Dispatcher::releasePacket(Packet* packet) {
}
void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
if (packet->path_len > MAX_PATH_SIZE || packet->payload_len > MAX_PACKET_PAYLOAD) {
if (!Packet::isValidPathLen(packet->path_len) || 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);
_mgr->free(packet);
} else {

View File

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

View File

@@ -48,6 +48,50 @@ LocalIdentity::LocalIdentity(RNG* rng) {
ed25519_create_keypair(pub_key, prv_key, seed);
}
bool LocalIdentity::validatePrivateKey(const uint8_t prv[64]) {
uint8_t pub[32];
ed25519_derive_pub(pub, prv); // derive public key from given private key
// disallow 00 or FF prefixed public keys
if (pub[0] == 0x00 || pub[0] == 0xFF) return false;
// known good test client keypair
const uint8_t test_client_prv[64] = {
0x70, 0x65, 0xe1, 0x8f, 0xd9, 0xfa, 0xbb, 0x70,
0xc1, 0xed, 0x90, 0xdc, 0xa1, 0x99, 0x07, 0xde,
0x69, 0x8c, 0x88, 0xb7, 0x09, 0xea, 0x14, 0x6e,
0xaf, 0xd9, 0x3d, 0x9b, 0x83, 0x0c, 0x7b, 0x60,
0xc4, 0x68, 0x11, 0x93, 0xc7, 0x9b, 0xbc, 0x39,
0x94, 0x5b, 0xa8, 0x06, 0x41, 0x04, 0xbb, 0x61,
0x8f, 0x8f, 0xd7, 0xa8, 0x4a, 0x0a, 0xf6, 0xf5,
0x70, 0x33, 0xd6, 0xe8, 0xdd, 0xcd, 0x64, 0x71
};
const uint8_t test_client_pub[32] = {
0x1e, 0xc7, 0x71, 0x75, 0xb0, 0x91, 0x8e, 0xd2,
0x06, 0xf9, 0xae, 0x04, 0xec, 0x13, 0x6d, 0x6d,
0x5d, 0x43, 0x15, 0xbb, 0x26, 0x30, 0x54, 0x27,
0xf6, 0x45, 0xb4, 0x92, 0xe9, 0x35, 0x0c, 0x10
};
uint8_t ss1[32], ss2[32];
// shared secret we calculte from test client pubkey and given private key
ed25519_key_exchange(ss1, test_client_pub, prv);
// shared secret they calculate from our derived public key and test client private key
ed25519_key_exchange(ss2, pub, test_client_prv);
// check that both shared secrets match
if (memcmp(ss1, ss2, 32) != 0) return false;
// reject all-zero shared secret
for (int i = 0; i < 32; i++) {
if (ss1[i] != 0) return true;
}
return false;
}
bool LocalIdentity::readFrom(Stream& s) {
bool success = (s.readBytes(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE);
success = success && (s.readBytes(prv_key, PRV_KEY_SIZE) == PRV_KEY_SIZE);

View File

@@ -20,9 +20,16 @@ public:
memcpy(dest, pub_key, PATH_HASH_SIZE); // hash is just prefix of pub_key
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 {
return memcmp(hash, pub_key, PATH_HASH_SIZE) == 0;
}
bool isHashMatch(const uint8_t* hash, uint8_t len) const {
return memcmp(hash, pub_key, len) == 0;
}
/**
* \brief Performs Ed25519 signature verification.
@@ -73,6 +80,13 @@ public:
*/
void calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const;
/**
* \brief Validates that a given private key can be used for ECDH / shared-secret operations.
* \param prv IN - the private key to validate (must be PRV_KEY_SIZE bytes)
* \returns true, if the private key is valid for login.
*/
static bool validatePrivateKey(const uint8_t prv[64]);
bool readFrom(Stream& s);
bool writeTo(Stream& s) const;
void printTo(Stream& s) const;

View File

@@ -39,11 +39,6 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int
}
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->path_len < MAX_PATH_SIZE) {
uint8_t i = 0;
@@ -52,14 +47,15 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
uint32_t auth_code;
memcpy(&auth_code, &pkt->payload[i], 4); i += 4;
uint8_t flags = pkt->payload[i++];
uint8_t path_sz = flags & 0x03; // NEW v1.11+: lower 2 bits is path hash size
uint8_t len = pkt->payload_len - i;
if (pkt->path_len >= len) { // TRACE has reached end of given path
uint8_t offset = pkt->path_len << path_sz;
if (offset >= len) { // TRACE has reached end of given path
onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len);
} else if (self_id.isHashMatch(&pkt->payload[i + pkt->path_len]) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) {
} else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) {
// append SNR (Not hash!)
pkt->path[pkt->path_len] = (int8_t) (pkt->getSNR()*4);
pkt->path_len += PATH_HASH_SIZE;
pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4);
uint32_t d = getDirectRetransmitDelay(pkt);
return ACTION_RETRANSMIT_DELAYED(5, d); // schedule with priority 5 (for now), maybe make configurable?
@@ -69,15 +65,25 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
}
if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) {
if (pkt->path_len == 0) {
if (pkt->getPathHashCount() == 0) {
onControlDataRecv(pkt);
}
// just zero-hop control packets allowed (for this subset of payloads)
return ACTION_RELEASE;
}
if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) {
if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) {
if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0) {
// check for 'early received' ACK
if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
int i = 0;
uint32_t ack_crc;
memcpy(&ack_crc, &pkt->payload[i], 4); i += 4;
if (i <= pkt->payload_len) {
onAckRecv(pkt, ack_crc);
}
}
if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
return forwardMultipartDirect(pkt);
} else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
@@ -147,7 +153,9 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) {
int k = 0;
uint8_t path_len = data[k++];
uint8_t* path = &data[k]; k += path_len;
uint8_t hash_size = (path_len >> 6) + 1;
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 = &data[k];
uint8_t extra_len = len - k; // remainder of packet (may be padded with zeroes!)
@@ -282,8 +290,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK
Packet tmp;
tmp.header = pkt->header;
tmp.path_len = pkt->path_len;
memcpy(tmp.path, pkt->path, pkt->path_len);
tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len);
tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
@@ -310,27 +317,25 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
void Mesh::removeSelfFromPath(Packet* pkt) {
// remove our hash from 'path'
pkt->path_len -= PATH_HASH_SIZE;
#if 0
memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len);
#elif PATH_HASH_SIZE == 1
for (int k = 0; k < pkt->path_len; k++) { // shuffle bytes by 1
pkt->path[k] = pkt->path[k + 1];
pkt->setPathHashCount(pkt->getPathHashCount() - 1); // decrement the count
uint8_t sz = pkt->getPathHashSize();
for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { // shuffle path by 1 'entry'
memcpy(&pkt->path[k], &pkt->path[k + sz], sz);
}
#else
#error "need path remove impl"
#endif
}
DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
uint8_t n = packet->getPathHashCount();
if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit()
&& packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) {
&& (n + 1)*packet->getPathHashSize() <= MAX_PATH_SIZE && allowPacketForward(packet)) {
// append this node's hash to 'path'
packet->path_len += self_id.copyHashTo(&packet->path[packet->path_len]);
self_id.copyHashTo(&packet->path[n * packet->getPathHashSize()], packet->getPathHashSize());
packet->setPathHashCount(n + 1);
uint32_t d = getRetransmitDelay(packet);
// as this propagates outwards, give it lower and lower priority
return ACTION_RETRANSMIT_DELAYED(packet->path_len, d); // give priority to closer sources, than ones further away
return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away
}
return ACTION_RELEASE;
}
@@ -342,8 +347,7 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK
Packet tmp;
tmp.header = pkt->header;
tmp.path_len = pkt->path_len;
memcpy(tmp.path, pkt->path, pkt->path_len);
tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len);
tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
@@ -365,7 +369,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
delay_millis += getDirectRetransmitDelay(packet) + 300;
auto a1 = createMultiAck(crc, extra);
if (a1) {
memcpy(a1->path, packet->path, a1->path_len = packet->path_len);
a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len);
a1->header &= ~PH_ROUTE_MASK;
a1->header |= ROUTE_TYPE_DIRECT;
sendPacket(a1, 0, delay_millis);
@@ -375,7 +379,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
auto a2 = createAck(crc);
if (a2) {
memcpy(a2->path, packet->path, a2->path_len = packet->path_len);
a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len);
a2->header &= ~PH_ROUTE_MASK;
a2->header |= ROUTE_TYPE_DIRECT;
sendPacket(a2, 0, delay_millis);
@@ -428,7 +432,10 @@ 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) {
if (path_len + extra_len + 5 > MAX_COMBINED_PATH) return NULL; // too long!!
uint8_t path_hash_size = (path_len >> 6) + 1;
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();
if (packet == NULL) {
@@ -446,7 +453,7 @@ Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret,
uint8_t data[MAX_PACKET_PAYLOAD];
data[data_len++] = path_len;
memcpy(&data[data_len], path, path_len); data_len += path_len;
memcpy(&data[data_len], path, path_hash_count*path_hash_size); data_len += path_hash_count*path_hash_size;
if (extra_len > 0) {
data[data_len++] = extra_type;
memcpy(&data[data_len], extra, extra_len); data_len += extra_len;
@@ -613,15 +620,19 @@ Packet* Mesh::createControlData(const uint8_t* data, size_t len) {
return packet;
}
void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) {
void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) {
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
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 |= ROUTE_TYPE_FLOOD;
packet->path_len = 0;
packet->setPathHashSizeAndCount(path_hash_size, 0);
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
@@ -636,17 +647,21 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) {
sendPacket(packet, pri, delay_millis);
}
void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis) {
void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_hash_size) {
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
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 |= ROUTE_TYPE_TRANSPORT_FLOOD;
packet->transport_codes[0] = transport_codes[0];
packet->transport_codes[1] = transport_codes[1];
packet->path_len = 0;
packet->setPathHashSizeAndCount(path_hash_size, 0);
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
@@ -668,13 +683,13 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin
uint8_t pri;
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)
memcpy(&packet->payload[packet->payload_len], path, path_len);
memcpy(&packet->payload[packet->payload_len], path, path_len); // NOTE: path_len here can be > 64, and NOT in the new scheme
packet->payload_len += path_len;
packet->path_len = 0;
pri = 5; // maybe make this configurable
} else {
memcpy(packet->path, path, packet->path_len = path_len);
packet->path_len = Packet::copyPath(packet->path, path, path_len);
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 1; // slightly less priority
} else {

View File

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

View File

@@ -1,6 +1,7 @@
#pragma once
#include <stdint.h>
#include <math.h>
#define MAX_HASH_SIZE 8
#define PUB_KEY_SIZE 32
@@ -42,15 +43,28 @@ namespace mesh {
class MainBoard {
public:
virtual uint16_t getBattMilliVolts() = 0;
virtual float getMCUTemperature() { return NAN; }
virtual bool setAdcMultiplier(float multiplier) { return false; };
virtual float getAdcMultiplier() const { return 0.0f; }
virtual const char* getManufacturerName() const = 0;
virtual void onBeforeTransmit() { }
virtual void onAfterTransmit() { }
virtual void reboot() = 0;
virtual void powerOff() { /* no op */ }
virtual void sleep(uint32_t secs) { /* no op */ }
virtual uint32_t getGpio() { return 0; }
virtual void setGpio(uint32_t values) {}
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
// Power management interface (boards with power management override these)
virtual bool isExternalPowered() { return false; }
virtual uint16_t getBootVoltage() { return 0; }
virtual uint32_t getResetReason() const { return 0; }
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
virtual uint8_t getShutdownReason() const { return 0; }
virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; }
};
/**

View File

@@ -10,8 +10,32 @@ Packet::Packet() {
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 {
return 2 + path_len + payload_len + (hasTransportCodes() ? 4 : 0);
return 2 + getPathByteLen() + payload_len + (hasTransportCodes() ? 4 : 0);
}
void Packet::calculatePacketHash(uint8_t* hash) const {

View File

@@ -76,6 +76,16 @@ public:
*/
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; }
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) {
if (dest.out_path_len < 0) {
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
mesh::Packet* ack = createAck(ack_hash);
if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY);
} else {
@@ -55,6 +55,54 @@ void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
}
}
void BaseChatMesh::bootstrapRTCfromContacts() {
uint32_t latest = 0;
for (int i = 0; i < num_contacts; i++) {
if (contacts[i].lastmod > latest) {
latest = contacts[i].lastmod;
}
}
if (latest != 0) {
getRTCClock()->setCurrentTime(latest + 1);
}
}
ContactInfo* BaseChatMesh::allocateContactSlot() {
if (num_contacts < MAX_CONTACTS) {
return &contacts[num_contacts++];
} else if (shouldOverwriteWhenFull()) {
// Find oldest non-favourite contact by oldest lastmod timestamp
int oldest_idx = -1;
uint32_t oldest_lastmod = 0xFFFFFFFF;
for (int i = 0; i < num_contacts; i++) {
bool is_favourite = (contacts[i].flags & 0x01) != 0;
if (!is_favourite && contacts[i].lastmod < oldest_lastmod) {
oldest_lastmod = contacts[i].lastmod;
oldest_idx = i;
}
}
if (oldest_idx >= 0) {
onContactOverwrite(contacts[oldest_idx].id.pub_key);
return &contacts[oldest_idx];
}
}
return NULL; // no space, no overwrite or all contacts are all favourites
}
void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) {
memset(&ci, 0, sizeof(ci));
ci.id = id;
ci.out_path_len = OUT_PATH_UNKNOWN;
StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
ci.type = parser.getType();
if (parser.hasLatLon()) {
ci.gps_lat = parser.getIntLat();
ci.gps_lon = parser.getIntLon();
}
ci.last_advert_timestamp = timestamp;
ci.lastmod = getRTCClock()->getCurrentTime();
}
void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) {
AdvertDataParser parser(app_data, app_data_len);
if (!(parser.isValid() && parser.hasName())) {
@@ -83,53 +131,40 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
plen = packet->writeTo(temp_buf);
packet->header = save;
}
putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
bool is_new = false;
bool is_new = false; // true = not in contacts[], false = exists in contacts[]
if (from == NULL) {
if (!isAutoAddEnabled()) {
if (!shouldAutoAddContactType(parser.getType())) {
ContactInfo ci;
memset(&ci, 0, sizeof(ci));
ci.id = id;
ci.out_path_len = -1; // initially out_path is unknown
StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
ci.type = parser.getType();
if (parser.hasLatLon()) {
ci.gps_lat = parser.getIntLat();
ci.gps_lon = parser.getIntLon();
}
ci.last_advert_timestamp = timestamp;
ci.lastmod = getRTCClock()->getCurrentTime();
populateContactFromAdvert(ci, id, parser, timestamp);
onDiscoveredContact(ci, true, packet->path_len, packet->path); // let UI know
return;
}
is_new = true;
if (num_contacts < MAX_CONTACTS) {
from = &contacts[num_contacts++];
from->id = id;
from->out_path_len = -1; // initially out_path is unknown
from->gps_lat = 0; // initially unknown GPS loc
from->gps_lon = 0;
from->sync_since = 0;
// only need to calculate the shared_secret once, for better performance
self_id.calcSharedSecret(from->shared_secret, id);
} else {
MESH_DEBUG_PRINTLN("onAdvertRecv: contacts table is full!");
from = allocateContactSlot();
if (from == NULL) {
ContactInfo ci;
populateContactFromAdvert(ci, id, parser, timestamp);
onDiscoveredContact(ci, true, packet->path_len, packet->path);
onContactsFull();
MESH_DEBUG_PRINTLN("onAdvertRecv: unable to allocate contact slot for new contact");
return;
}
populateContactFromAdvert(*from, id, parser, timestamp);
from->sync_since = 0;
from->shared_secret_valid = false;
}
// update
StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
from->type = parser.getType();
if (parser.hasLatLon()) {
from->gps_lat = parser.getIntLat();
from->gps_lon = parser.getIntLon();
}
from->last_advert_timestamp = timestamp;
from->lastmod = getRTCClock()->getCurrentTime();
putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
from->type = parser.getType();
if (parser.hasLatLon()) {
from->gps_lat = parser.getIntLat();
from->gps_lon = parser.getIntLon();
}
from->last_advert_timestamp = timestamp;
from->lastmod = getRTCClock()->getCurrentTime();
onDiscoveredContact(*from, is_new, packet->path_len, packet->path); // let UI know
}
@@ -147,8 +182,7 @@ int BaseChatMesh::searchPeersByHash(const uint8_t* hash) {
void BaseChatMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
int i = matching_peer_indexes[peer_idx];
if (i >= 0 && i < num_contacts) {
// lookup pre-calculated shared_secret
memcpy(dest_secret, contacts[i].shared_secret, PUB_KEY_SIZE);
memcpy(dest_secret, contacts[i].getSharedSecret(self_id), PUB_KEY_SIZE);
} else {
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
}
@@ -166,7 +200,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) {
uint32_t timestamp;
memcpy(&timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint flags = data[4] >> 2; // message attempt number, and other flags
uint8_t flags = data[4] >> 2; // message attempt number, and other flags
// len can be > original length, but 'text' will be padded with zeroes
data[len] = 0; // need to make a C string again, with null terminator
@@ -229,7 +263,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len);
if (reply) {
if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT
if (from.out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY);
@@ -239,7 +273,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
}
} else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) {
onContactResponse(from, data, len);
if (packet->isRouteFlood() && from.out_path_len >= 0) {
if (packet->isRouteFlood() && from.out_path_len != OUT_PATH_UNKNOWN) {
// 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);
}
@@ -261,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) {
// 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'(?)
memcpy(from.out_path, out_path, from.out_path_len = out_path_len); // store a copy of path, for sendDirect()
from.out_path_len = mesh::Packet::copyPath(from.out_path, out_path, out_path_len); // store a copy of path, for sendDirect()
from.lastmod = getRTCClock()->getCurrentTime();
onContactPathUpdated(from);
@@ -283,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
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
if (packet->isRouteFlood() && from->out_path_len >= 0) {
if (packet->isRouteFlood() && from->out_path_len != OUT_PATH_UNKNOWN) {
// 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);
}
@@ -293,7 +327,7 @@ void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len) {
// NOTE: simplest impl is just to re-send a reciprocal return path to sender (DIRECTLY)
// override this method in various firmwares, if there's a better strategy
mesh::Packet* rpath = createPathReturn(contact.id, contact.shared_secret, path, path_len, 0, NULL, 0);
mesh::Packet* rpath = createPathReturn(contact.id, contact.getSharedSecret(self_id), path, path_len, 0, NULL, 0);
if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); // 3 second delay
}
@@ -342,7 +376,7 @@ mesh::Packet* BaseChatMesh::composeMsgPacket(const ContactInfo& recipient, uint3
temp[len++] = attempt; // hide attempt number at tail end of payload
}
return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.shared_secret, temp, len);
return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, len);
}
int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout) {
@@ -352,7 +386,7 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
int rc;
if (recipient.out_path_len < 0) {
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
rc = MSG_SEND_SENT_FLOOD;
@@ -373,12 +407,12 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest
temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2);
memcpy(&temp[5], text, text_len + 1);
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.shared_secret, temp, 5 + text_len);
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len);
if (pkt == NULL) return MSG_SEND_FAILED;
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
int rc;
if (recipient.out_path_len < 0) {
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
rc = MSG_SEND_SENT_FLOOD;
@@ -462,11 +496,36 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
tlen = 4 + len;
}
pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen);
pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, tlen);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len < 0) {
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
}
int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data, uint8_t len, uint32_t& tag, uint32_t& est_timeout) {
mesh::Packet* pkt;
{
uint8_t temp[MAX_PACKET_PAYLOAD];
tag = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &tag, 4); // tag to match later (also extra blob to help make packet_hash unique)
memcpy(&temp[4], data, len);
pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + len);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
@@ -489,11 +548,11 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_
memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique
memcpy(&temp[4], req_data, data_len);
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, 4 + data_len);
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + data_len);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len < 0) {
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
@@ -516,11 +575,11 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u
memset(&temp[5], 0, 4); // reserved (possibly for 'since' param)
getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp));
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, sizeof(temp));
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len < 0) {
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
@@ -624,7 +683,7 @@ void BaseChatMesh::checkConnections() {
MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact not found!");
continue;
}
if (contact->out_path_len < 0) {
if (contact->out_path_len == OUT_PATH_UNKNOWN) {
MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact, no out_path!");
continue;
}
@@ -639,7 +698,7 @@ void BaseChatMesh::checkConnections() {
// calc expected ACK reply
mesh::Utils::sha256((uint8_t *)&connections[i].expected_ack, 4, data, 9, self_id.pub_key, PUB_KEY_SIZE);
auto pkt = createDatagram(PAYLOAD_TYPE_REQ, contact->id, contact->shared_secret, data, 9);
auto pkt = createDatagram(PAYLOAD_TYPE_REQ, contact->id, contact->getSharedSecret(self_id), data, 9);
if (pkt) {
sendDirect(pkt, contact->out_path, contact->out_path_len);
}
@@ -651,7 +710,7 @@ void BaseChatMesh::checkConnections() {
}
void BaseChatMesh::resetPathTo(ContactInfo& recipient) {
recipient.out_path_len = -1;
recipient.out_path_len = OUT_PATH_UNKNOWN;
}
static ContactInfo* table; // pass via global :-(
@@ -699,13 +758,10 @@ ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int pre
}
bool BaseChatMesh::addContact(const ContactInfo& contact) {
if (num_contacts < MAX_CONTACTS) {
auto dest = &contacts[num_contacts++];
ContactInfo* dest = allocateContactSlot();
if (dest) {
*dest = contact;
// calc the ECDH shared secret (just once for performance)
self_id.calcSharedSecret(dest->shared_secret, contact.id);
dest->shared_secret_valid = false; // mark shared_secret as needing calculation
return true; // success
}
return false;

View File

@@ -88,10 +88,17 @@ protected:
memset(connections, 0, sizeof(connections));
}
void bootstrapRTCfromContacts();
void resetContacts() { num_contacts = 0; }
void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp);
ContactInfo* allocateContactSlot(); // helper to find slot for new contact
// 'UI' concepts, for sub-classes to implement
virtual bool isAutoAddEnabled() const { return true; }
virtual bool shouldAutoAddContactType(uint8_t type) const { return true; }
virtual void onContactsFull() {};
virtual bool shouldOverwriteWhenFull() const { return false; }
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 ContactInfo* processAck(const uint8_t *data) = 0;
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
@@ -141,6 +148,7 @@ public:
int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout);
bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len);
int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
int sendAnonReq(const ContactInfo& recipient, const uint8_t* data, uint8_t len, uint32_t& tag, uint32_t& est_timeout);
int sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout);
int sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout);
bool shareContactZeroHop(const ContactInfo& contact);

View File

@@ -11,7 +11,8 @@ static File openWrite(FILESYSTEM* _fs, const char* filename) {
#endif
}
void ClientACL::load(FILESYSTEM* _fs) {
void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
_fs = fs;
num_clients = 0;
if (_fs->exists("/s_contacts")) {
#if defined(RP2040_PLATFORM)
@@ -34,11 +35,12 @@ void ClientACL::load(FILESYSTEM* _fs) {
success = success && (file.read(unused, 2) == 2);
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below
if (!success) break; // EOF
c.id = mesh::Identity(pub_key);
self_id.calcSharedSecret(c.shared_secret, pub_key); // recalculate shared secrets in case our private key changed
if (num_clients < MAX_CLIENTS) {
clients[num_clients++] = c;
} else {
@@ -50,7 +52,8 @@ void ClientACL::load(FILESYSTEM* _fs) {
}
}
void ClientACL::save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)) {
void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
_fs = fs;
File file = openWrite(_fs, "/s_contacts");
if (file) {
uint8_t unused[2];
@@ -74,6 +77,16 @@ void ClientACL::save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)) {
}
}
bool ClientACL::clear() {
if (!_fs) return false; // no filesystem, nothing to clear
if (_fs->exists("/s_contacts")) {
_fs->remove("/s_contacts");
}
memset(clients, 0, sizeof(clients));
num_clients = 0;
return true;
}
ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
for (int i = 0; i < num_clients; i++) {
if (memcmp(pubkey, clients[i].id.pub_key, key_len) == 0) return &clients[i]; // already known
@@ -101,7 +114,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
memset(c, 0, sizeof(*c));
c->permissions = init_perms;
c->id = id;
c->out_path_len = -1; // initially out_path is unknown
c->out_path_len = OUT_PATH_UNKNOWN;
return c;
}

View File

@@ -10,10 +10,12 @@
#define PERM_ACL_READ_WRITE 2
#define PERM_ACL_ADMIN 3
#define OUT_PATH_UNKNOWN 0xFF
struct ClientInfo {
mesh::Identity id;
uint8_t permissions;
int8_t out_path_len;
uint8_t out_path_len;
uint8_t out_path[MAX_PATH_SIZE];
uint8_t shared_secret[PUB_KEY_SIZE];
uint32_t last_timestamp; // by THEIR clock (transient)
@@ -36,6 +38,7 @@ struct ClientInfo {
#endif
class ClientACL {
FILESYSTEM* _fs;
ClientInfo clients[MAX_CLIENTS];
int num_clients;
@@ -44,8 +47,9 @@ public:
memset(clients, 0, sizeof(clients));
num_clients = 0;
}
void load(FILESYSTEM* _fs);
void load(FILESYSTEM* _fs, const mesh::LocalIdentity& self_id);
void save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)=NULL);
bool clear();
ClientInfo* getClient(const uint8_t* pubkey, int key_len);
ClientInfo* putClient(const mesh::Identity& id, uint8_t init_perms);

View File

@@ -14,6 +14,14 @@ static uint32_t _atoi(const char* sp) {
return n;
}
static bool isValidName(const char *n) {
while (*n) {
if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
n++;
}
return true;
}
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
if (fs->exists("/com_prefs")) {
loadPrefsInt(fs, "/com_prefs"); // new filename
@@ -55,7 +63,8 @@ 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->bw, sizeof(_prefs->bw)); // 116
file.read((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
file.read(pad, 3); // 121
file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121
file.read(pad, 2); // 122
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->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
@@ -65,12 +74,15 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131
file.read((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135
file.read((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136
file.read(pad, 4); // 152
file.read((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152
file.read(pad, 3); // 153
file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
// 166
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
// 290
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -81,8 +93,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->bw = constrain(_prefs->bw, 7.8f, 500.0f);
_prefs->sf = constrain(_prefs->sf, 5, 12);
_prefs->cr = constrain(_prefs->cr, 5, 8);
_prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, 1, 30);
_prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, -9, 30);
_prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1);
_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
_prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
@@ -91,6 +105,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->bridge_baud = constrain(_prefs->bridge_baud, 9600, 115200);
_prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14);
_prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1);
_prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1);
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
@@ -133,7 +149,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
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->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
file.write(pad, 3); // 121
file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121
file.write(pad, 2); // 122
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->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
@@ -143,12 +160,15 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131
file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135
file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136
file.write(pad, 4); // 152
file.write((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152
file.write(pad, 3); // 153
file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
// 166
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
// 290
file.close();
}
@@ -179,8 +199,13 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) {
if (memcmp(command, "reboot", 6) == 0) {
_board->reboot(); // doesn't return
} else if (memcmp(command, "clkreboot", 9) == 0) {
// Reset clock
getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm
_board->reboot(); // doesn't return
} else if (memcmp(command, "advert", 6) == 0) {
_callbacks->sendSelfAdvertisement(1500); // longer delay, give CLI response time to be sent first
// send flood advert
_callbacks->sendSelfAdvertisement(1500, true); // longer delay, give CLI response time to be sent first
strcpy(reply, "OK - Advert sent");
} else if (memcmp(command, "clock sync", 10) == 0) {
uint32_t curr = getRTCClock()->getCurrentTime();
@@ -228,12 +253,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(tmp, &command[10]);
const char *parts[5];
int num = mesh::Utils::parseTextParts(tmp, parts, 5);
float freq = num > 0 ? atof(parts[0]) : 0.0f;
float bw = num > 1 ? atof(parts[1]) : 0.0f;
float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
int temp_timeout_mins = num > 4 ? atoi(parts[4]) : 0;
if (freq >= 300.0f && freq <= 2500.0f && sf >= 7 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f && temp_timeout_mins > 0) {
if (freq >= 300.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f && temp_timeout_mins > 0) {
_callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins);
sprintf(reply, "OK - temp params for %d mins", temp_timeout_mins);
} else {
@@ -284,7 +309,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else if (memcmp(config, "radio", 5) == 0) {
char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(_prefs->freq));
strcpy(bw, StrHelper::ftoa(_prefs->bw));
strcpy(bw, StrHelper::ftoa3(_prefs->bw));
sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
} else if (memcmp(config, "rxdelay", 7) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
@@ -294,8 +319,19 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
} else if (memcmp(config, "direct.txdelay", 14) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
} else if (memcmp(config, "owner.info", 10) == 0) {
*reply++ = '>';
*reply++ = ' ';
const char* sp = _prefs->owner_info;
while (*sp) {
*reply++ = (*sp == '\n') ? '|' : *sp; // translate newline back to orig '|'
sp++;
}
*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] == ' ')) {
sprintf(reply, "> %d", (uint32_t) _prefs->tx_power_dbm);
sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
} else if (memcmp(config, "freq", 4) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
} else if (memcmp(config, "public.key", 10) == 0) {
@@ -330,6 +366,51 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
} else if (memcmp(config, "bridge.secret", 13) == 0) {
sprintf(reply, "> %s", _prefs->bridge_secret);
#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) {
float adc_mult = _board->getAdcMultiplier();
if (adc_mult == 0.0f) {
strcpy(reply, "Error: unsupported by this board");
} else {
sprintf(reply, "> %.3f", adc_mult);
}
// Power management commands
} else if (memcmp(config, "pwrmgt.support", 14) == 0) {
#ifdef NRF52_POWER_MANAGEMENT
strcpy(reply, "> supported");
#else
strcpy(reply, "> unsupported");
#endif
} else if (memcmp(config, "pwrmgt.source", 13) == 0) {
#ifdef NRF52_POWER_MANAGEMENT
strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
#else
strcpy(reply, "ERROR: Power management not supported");
#endif
} else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
#ifdef NRF52_POWER_MANAGEMENT
sprintf(reply, "> Reset: %s; Shutdown: %s",
_board->getResetReasonString(_board->getResetReason()),
_board->getShutdownReasonString(_board->getShutdownReason()));
#else
strcpy(reply, "ERROR: Power management not supported");
#endif
} else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
#ifdef NRF52_POWER_MANAGEMENT
sprintf(reply, "> %u mV", _board->getBootVoltage());
#else
strcpy(reply, "ERROR: Power management not supported");
#endif
} else {
sprintf(reply, "??: %s", config);
@@ -361,8 +442,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(reply, "OK");
} else if (memcmp(config, "flood.advert.interval ", 22) == 0) {
int hours = _atoi(&config[22]);
if ((hours > 0 && hours < 3) || (hours > 48)) {
strcpy(reply, "Error: interval range is 3-48 hours");
if ((hours > 0 && hours < 3) || (hours > 168)) {
strcpy(reply, "Error: interval range is 3-168 hours");
} else {
_prefs->flood_advert_interval = (uint8_t)(hours);
_callbacks->updateFloodAdvertTimer();
@@ -383,22 +464,27 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
savePrefs();
strcpy(reply, "OK");
} else if (sender_timestamp == 0 &&
memcmp(config, "prv.key ", 8) == 0) { // from serial command line only
} else if (memcmp(config, "prv.key ", 8) == 0) {
uint8_t prv_key[PRV_KEY_SIZE];
bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
if (success) {
// only allow rekey if key is valid
if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) {
mesh::LocalIdentity new_id;
new_id.readFrom(prv_key, PRV_KEY_SIZE);
_callbacks->saveIdentity(new_id);
strcpy(reply, "OK");
strcpy(reply, "OK, reboot to apply! New pubkey: ");
mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE);
} else {
strcpy(reply, "Error, invalid key");
strcpy(reply, "Error, bad key");
}
} else if (memcmp(config, "name ", 5) == 0) {
StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
savePrefs();
strcpy(reply, "OK");
if (isValidName(&config[5])) {
StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, bad chars");
}
} else if (memcmp(config, "repeat ", 7) == 0) {
_prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
savePrefs();
@@ -407,11 +493,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(tmp, &config[6]);
const char *parts[4];
int num = mesh::Utils::parseTextParts(tmp, parts, 4);
float freq = num > 0 ? atof(parts[0]) : 0.0f;
float bw = num > 1 ? atof(parts[1]) : 0.0f;
float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
if (freq >= 300.0f && freq <= 2500.0f && sf >= 7 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
if (freq >= 300.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
_prefs->sf = sf;
_prefs->cr = cr;
_prefs->freq = freq;
@@ -465,6 +551,26 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else {
strcpy(reply, "Error, cannot be negative");
}
} else if (memcmp(config, "owner.info ", 11) == 0) {
config += 11;
char *dp = _prefs->owner_info;
while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) {
*dp++ = (*config == '|') ? '\n' : *config; // translate '|' to newline chars
config++;
}
*dp = 0;
savePrefs();
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) {
_prefs->tx_power_dbm = atoi(&config[3]);
savePrefs();
@@ -523,6 +629,19 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
savePrefs();
strcpy(reply, "OK");
#endif
} else if (memcmp(config, "adc.multiplier ", 15) == 0) {
_prefs->adc_multiplier = atof(&config[15]);
if (_board->setAdcMultiplier(_prefs->adc_multiplier)) {
savePrefs();
if (_prefs->adc_multiplier == 0.0f) {
strcpy(reply, "OK - using default board multiplier");
} else {
sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier);
}
} else {
_prefs->adc_multiplier = 0.0f;
strcpy(reply, "Error: unsupported by this board");
};
} else {
sprintf(reply, "unknown config: %s", config);
}
@@ -653,6 +772,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(reply, "Can't find GPS");
}
#endif
} else if (memcmp(command, "powersaving on", 14) == 0) {
_prefs->powersaving_enabled = 1;
savePrefs();
strcpy(reply, "ok"); // TODO: to return Not supported if required
} else if (memcmp(command, "powersaving off", 15) == 0) {
_prefs->powersaving_enabled = 0;
savePrefs();
strcpy(reply, "ok");
} else if (memcmp(command, "powersaving", 11) == 0) {
if (_prefs->powersaving_enabled) {
strcpy(reply, "on");
} else {
strcpy(reply, "off");
}
} else if (memcmp(command, "log start", 9) == 0) {
_callbacks->setLoggingOn(true);
strcpy(reply, " logging on");

View File

@@ -3,6 +3,7 @@
#include "Mesh.h"
#include <helpers/IdentityStore.h>
#include <helpers/SensorManager.h>
#include <helpers/ClientACL.h>
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
#define WITH_BRIDGE
@@ -18,7 +19,7 @@ struct NodePrefs { // persisted to file
double node_lat, node_lon;
char password[16];
float freq;
uint8_t tx_power_dbm;
int8_t tx_power_dbm;
uint8_t disable_fwd;
uint8_t advert_interval; // minutes / 2
uint8_t flood_advert_interval; // hours
@@ -42,11 +43,16 @@ struct NodePrefs { // persisted to file
uint32_t bridge_baud; // 9600, 19200, 38400, 57600, 115200 (default 115200)
uint8_t bridge_channel; // 1-14 (ESP-NOW only)
char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only)
// Power setting
uint8_t powersaving_enabled; // boolean
// Gps settings
uint8_t gps_enabled;
uint32_t gps_interval; // in seconds
uint8_t advert_loc_policy;
uint32_t discovery_mod_timestamp;
float adc_multiplier;
char owner_info[120];
uint8_t path_hash_mode; // which path mode to use when sending
};
class CommonCLICallbacks {
@@ -56,13 +62,13 @@ public:
virtual const char* getBuildDate() = 0;
virtual const char* getRole() = 0;
virtual bool formatFileSystem() = 0;
virtual void sendSelfAdvertisement(int delay_millis) = 0;
virtual void sendSelfAdvertisement(int delay_millis, bool flood) = 0;
virtual void updateAdvertTimer() = 0;
virtual void updateFloodAdvertTimer() = 0;
virtual void setLoggingOn(bool enable) = 0;
virtual void eraseLogFile() = 0;
virtual void dumpLogFile() = 0;
virtual void setTxPower(uint8_t power_dbm) = 0;
virtual void setTxPower(int8_t power_dbm) = 0;
virtual void formatNeighborsReply(char *reply) = 0;
virtual void removeNeighbor(const uint8_t* pubkey, int key_len) {
// no op by default
@@ -90,6 +96,7 @@ class CommonCLI {
CommonCLICallbacks* _callbacks;
mesh::MainBoard* _board;
SensorManager* _sensors;
ClientACL* _acl;
char tmp[PRV_KEY_SIZE*2 + 4];
mesh::RTCClock* getRTCClock() { return _rtc; }
@@ -97,8 +104,8 @@ class CommonCLI {
void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
public:
CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, NodePrefs* prefs, CommonCLICallbacks* callbacks)
: _board(&board), _rtc(&rtc), _sensors(&sensors), _prefs(prefs), _callbacks(callbacks) { }
CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks)
: _board(&board), _rtc(&rtc), _sensors(&sensors), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { }
void loadPrefs(FILESYSTEM* _fs);
void savePrefs(FILESYSTEM* _fs);

View File

@@ -3,16 +3,29 @@
#include <Arduino.h>
#include <Mesh.h>
#define OUT_PATH_UNKNOWN 0xFF
struct ContactInfo {
mesh::Identity id;
char name[32];
uint8_t type; // on of ADV_TYPE_*
uint8_t flags;
int8_t out_path_len;
uint8_t out_path_len;
mutable bool shared_secret_valid; // flag to indicate if shared_secret has been calculated
uint8_t out_path[MAX_PATH_SIZE];
uint32_t last_advert_timestamp; // by THEIR clock
uint8_t shared_secret[PUB_KEY_SIZE];
uint32_t lastmod; // by OUR clock
int32_t gps_lat, gps_lon; // 6 dec places
uint32_t sync_since;
const uint8_t* getSharedSecret(const mesh::LocalIdentity& self_id) const {
if (!shared_secret_valid) {
self_id.calcSharedSecret(shared_secret, id.pub_key);
shared_secret_valid = true;
}
return shared_secret;
}
private:
mutable uint8_t shared_secret[PUB_KEY_SIZE];
};

View File

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

View File

@@ -8,10 +8,12 @@
#include <rom/rtc.h>
#include <sys/time.h>
#include <Wire.h>
#include "driver/rtc_io.h"
class ESP32Board : public mesh::MainBoard {
protected:
uint8_t startup_reason;
bool inhibit_sleep = false;
public:
void begin() {
@@ -42,6 +44,39 @@ public:
#endif
}
// Temperature from ESP32 MCU
float getMCUTemperature() override {
uint32_t raw = 0;
// To get and average the temperature so it is more accurate, especially in low temperature
for (int i = 0; i < 4; i++) {
raw += temperatureRead();
}
return raw / 4;
}
void enterLightSleep(uint32_t secs) {
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(P_LORA_DIO_1) // Supported ESP32 variants
if (rtc_gpio_is_valid_gpio((gpio_num_t)P_LORA_DIO_1)) { // Only enter sleep mode if P_LORA_DIO_1 is RTC pin
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // To wake up when receiving a LoRa packet
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000); // To wake up every hour to do periodically jobs
}
esp_light_sleep_start(); // CPU enters light sleep
}
#endif
}
void sleep(uint32_t secs) override {
if (!inhibit_sleep) {
enterLightSleep(secs); // To wake up after "secs" seconds or when receiving a LoRa packet
}
}
uint8_t getStartupReason() const override { return startup_reason; }
#if defined(P_LORA_TX_LED)
@@ -87,6 +122,10 @@ public:
}
bool startOTAUpdate(const char* id, char reply[]) override;
void setInhibitSleep(bool inhibit) {
inhibit_sleep = inhibit;
}
};
class ESP32RTCClock : public mesh::RTCClock {

366
src/helpers/NRF52Board.cpp Normal file
View File

@@ -0,0 +1,366 @@
#if defined(NRF52_PLATFORM)
#include "NRF52Board.h"
#include <bluefruit.h>
#include <nrf_soc.h>
static BLEDfu bledfu;
static void connect_callback(uint16_t conn_handle) {
(void)conn_handle;
MESH_DEBUG_PRINTLN("BLE client connected");
}
static void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
(void)conn_handle;
(void)reason;
MESH_DEBUG_PRINTLN("BLE client disconnected");
}
void NRF52Board::begin() {
startup_reason = BD_STARTUP_NORMAL;
}
#ifdef NRF52_POWER_MANAGEMENT
#include "nrf.h"
// Power Management global variables
uint32_t g_nrf52_reset_reason = 0; // Reset/Startup reason
uint8_t g_nrf52_shutdown_reason = 0; // Shutdown reason
// Early constructor - runs before SystemInit() clears the registers
// Priority 101 ensures this runs before SystemInit (102) and before
// any C++ static constructors (default 65535)
static void __attribute__((constructor(101))) nrf52_early_reset_capture() {
g_nrf52_reset_reason = NRF_POWER->RESETREAS;
g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2;
}
void NRF52Board::initPowerMgr() {
// Copy early-captured register values
reset_reason = g_nrf52_reset_reason;
shutdown_reason = g_nrf52_shutdown_reason;
boot_voltage_mv = 0; // Will be set by checkBootVoltage()
// Clear registers for next boot
// Note: At this point SoftDevice may or may not be enabled
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_reset_reason_clr(0xFFFFFFFF);
sd_power_gpregret_clr(1, 0xFF);
} else {
NRF_POWER->RESETREAS = 0xFFFFFFFF; // Write 1s to clear
NRF_POWER->GPREGRET2 = 0;
}
// Log reset/shutdown info
if (shutdown_reason != SHUTDOWN_REASON_NONE) {
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX); Shutdown = %s (0x%02X)",
getResetReasonString(reset_reason), (unsigned long)reset_reason,
getShutdownReasonString(shutdown_reason), shutdown_reason);
} else {
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)",
getResetReasonString(reset_reason), (unsigned long)reset_reason);
}
}
bool NRF52Board::isExternalPowered() {
// Check if SoftDevice is enabled before using its API
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
uint32_t usb_status;
sd_power_usbregstatus_get(&usb_status);
return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
} else {
return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
}
}
const char* NRF52Board::getResetReasonString(uint32_t reason) {
if (reason & POWER_RESETREAS_RESETPIN_Msk) return "Reset Pin";
if (reason & POWER_RESETREAS_DOG_Msk) return "Watchdog";
if (reason & POWER_RESETREAS_SREQ_Msk) return "Soft Reset";
if (reason & POWER_RESETREAS_LOCKUP_Msk) return "CPU Lockup";
#ifdef POWER_RESETREAS_LPCOMP_Msk
if (reason & POWER_RESETREAS_LPCOMP_Msk) return "Wake from LPCOMP";
#endif
#ifdef POWER_RESETREAS_VBUS_Msk
if (reason & POWER_RESETREAS_VBUS_Msk) return "Wake from VBUS";
#endif
#ifdef POWER_RESETREAS_OFF_Msk
if (reason & POWER_RESETREAS_OFF_Msk) return "Wake from GPIO";
#endif
#ifdef POWER_RESETREAS_DIF_Msk
if (reason & POWER_RESETREAS_DIF_Msk) return "Debug Interface";
#endif
return "Cold Boot";
}
const char* NRF52Board::getShutdownReasonString(uint8_t reason) {
switch (reason) {
case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage";
case SHUTDOWN_REASON_USER: return "User Request";
case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection";
}
return "Unknown";
}
bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) {
initPowerMgr();
// Read boot voltage
boot_voltage_mv = getBattMilliVolts();
if (config->voltage_bootlock == 0) return true; // Protection disabled
// Skip check if externally powered
if (isExternalPowered()) {
MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (external power)");
boot_voltage_mv = getBattMilliVolts();
return true;
}
MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage = %u mV (threshold = %u mV)",
boot_voltage_mv, config->voltage_bootlock);
// Only trigger shutdown if reading is valid (>1000mV) AND below threshold
// This prevents spurious shutdowns on ADC glitches or uninitialized reads
if (boot_voltage_mv > 1000 && boot_voltage_mv < config->voltage_bootlock) {
MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage too low - entering protective shutdown");
initiateShutdown(SHUTDOWN_REASON_BOOT_PROTECT);
return false; // Should never reach this
}
return true;
}
void NRF52Board::initiateShutdown(uint8_t reason) {
enterSystemOff(reason);
}
void NRF52Board::enterSystemOff(uint8_t reason) {
MESH_DEBUG_PRINTLN("PWRMGT: Entering SYSTEMOFF (%s)", getShutdownReasonString(reason));
// Record shutdown reason in GPREGRET2
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_gpregret_clr(1, 0xFF);
sd_power_gpregret_set(1, reason);
} else {
NRF_POWER->GPREGRET2 = reason;
}
// Flush serial buffers
Serial.flush();
delay(100);
// Enter SYSTEMOFF
if (sd_enabled) {
uint32_t err = sd_power_system_off();
if (err == NRF_ERROR_SOFTDEVICE_NOT_ENABLED) { //SoftDevice not enabled
sd_enabled = 0;
}
}
if (!sd_enabled) {
// SoftDevice not available; write directly to POWER->SYSTEMOFF
NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter;
}
// If we get here, something went wrong. Reset to recover.
NVIC_SystemReset();
}
void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
// LPCOMP is not managed by SoftDevice - direct register access required
// Halt and disable before reconfiguration
NRF_LPCOMP->TASKS_STOP = 1;
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled;
// Select analog input (AIN0-7 maps to PSEL 0-7)
NRF_LPCOMP->PSEL = ((uint32_t)ain_channel << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk;
// Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk;
// Detect UP events (voltage rises above threshold for battery recovery)
NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up;
// Enable 50mV hysteresis for noise immunity
NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV;
// Clear stale events/interrupts before enabling wake
NRF_LPCOMP->EVENTS_READY = 0;
NRF_LPCOMP->EVENTS_DOWN = 0;
NRF_LPCOMP->EVENTS_UP = 0;
NRF_LPCOMP->EVENTS_CROSS = 0;
NRF_LPCOMP->INTENCLR = 0xFFFFFFFF;
NRF_LPCOMP->INTENSET = LPCOMP_INTENSET_UP_Msk;
// Enable LPCOMP
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled;
NRF_LPCOMP->TASKS_START = 1;
// Wait for comparator to settle before entering SYSTEMOFF
for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) {
delayMicroseconds(50);
}
if (refsel == 7) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel);
} else if (refsel <= 6) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)",
ain_channel, refsel + 1);
} else {
uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1);
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)",
ain_channel, ref_num);
}
// Configure VBUS (USB power) wake alongside LPCOMP
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_usbdetected_enable(1);
} else {
NRF_POWER->EVENTS_USBDETECTED = 0;
NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk;
}
MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured");
}
#endif
void NRF52BoardDCDC::begin() {
NRF52Board::begin();
// Enable DC/DC converter for improved power efficiency
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE);
} else {
NRF_POWER->DCDCEN = 1;
}
}
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
float NRF52Board::getMCUTemperature() {
NRF_TEMP->TASKS_START = 1; // Start temperature measurement
long startTime = millis();
while (NRF_TEMP->EVENTS_DATARDY == 0) { // Wait for completion. Should complete in 50us
if(millis() - startTime > 5) { // To wait 5ms just in case
NRF_TEMP->TASKS_STOP = 1;
return NAN;
}
}
NRF_TEMP->EVENTS_DATARDY = 0; // Clear event flag
int32_t temp = NRF_TEMP->TEMP; // In 0.25 *C units
NRF_TEMP->TASKS_STOP = 1;
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[]) {
// Config the peripheral connection with maximum bandwidth
// more SRAM required by SoftDevice
// Note: All config***() function must be called before begin()
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);
Bluefruit.begin(1, 0);
// Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
Bluefruit.setTxPower(4);
// Set the BLE device name
Bluefruit.setName(ota_name);
Bluefruit.Periph.setConnectCallback(connect_callback);
Bluefruit.Periph.setDisconnectCallback(disconnect_callback);
// To be consistent OTA DFU should be added first if it exists
bledfu.begin();
// Set up and start advertising
// Advertising packet
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addName();
/* Start Advertising
- Enable auto advertising if disconnected
- Interval: fast mode = 20 ms, slow mode = 152.5 ms
- Timeout for fast mode is 30 seconds
- Start(timeout) with timeout = 0 will advertise forever (until connected)
For recommended advertising interval
https://developer.apple.com/library/content/qa/qa1931/_index.html
*/
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
uint8_t mac_addr[6];
memset(mac_addr, 0, sizeof(mac_addr));
Bluefruit.getAddr(mac_addr);
sprintf(reply, "OK - mac: %02X:%02X:%02X:%02X:%02X:%02X", mac_addr[5], mac_addr[4], mac_addr[3],
mac_addr[2], mac_addr[1], mac_addr[0]);
return true;
}
#endif

79
src/helpers/NRF52Board.h Normal file
View File

@@ -0,0 +1,79 @@
#pragma once
#include <Arduino.h>
#include <MeshCore.h>
#if defined(NRF52_PLATFORM)
#ifdef NRF52_POWER_MANAGEMENT
// Shutdown Reason Codes (stored in GPREGRET before SYSTEMOFF)
#define SHUTDOWN_REASON_NONE 0x00
#define SHUTDOWN_REASON_LOW_VOLTAGE 0x4C // 'L' - Runtime low voltage threshold
#define SHUTDOWN_REASON_USER 0x55 // 'U' - User requested powerOff()
#define SHUTDOWN_REASON_BOOT_PROTECT 0x42 // 'B' - Boot voltage protection
// Boards provide this struct with their hardware-specific settings and callbacks.
struct PowerMgtConfig {
// LPCOMP wake configuration (for voltage recovery from SYSTEMOFF)
uint8_t lpcomp_ain_channel; // AIN0-7 for voltage sensing pin
uint8_t lpcomp_refsel; // REFSEL value: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16
// Boot protection voltage threshold (millivolts)
// Set to 0 to disable boot protection
uint16_t voltage_bootlock;
};
#endif
class NRF52Board : public mesh::MainBoard {
#ifdef NRF52_POWER_MANAGEMENT
void initPowerMgr();
#endif
protected:
uint8_t startup_reason;
char *ota_name;
#ifdef NRF52_POWER_MANAGEMENT
uint32_t reset_reason; // RESETREAS register value
uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF)
uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts)
bool checkBootVoltage(const PowerMgtConfig* config);
void enterSystemOff(uint8_t reason);
void configureVoltageWake(uint8_t ain_channel, uint8_t refsel);
virtual void initiateShutdown(uint8_t reason);
#endif
public:
NRF52Board(char *otaname) : ota_name(otaname) {}
virtual void begin();
virtual uint8_t getStartupReason() const override { return startup_reason; }
virtual float getMCUTemperature() override;
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 void sleep(uint32_t secs) override;
#ifdef NRF52_POWER_MANAGEMENT
bool isExternalPowered() override;
uint16_t getBootVoltage() override { return boot_voltage_mv; }
virtual uint32_t getResetReason() const override { return reset_reason; }
uint8_t getShutdownReason() const override { return shutdown_reason; }
const char* getResetReasonString(uint32_t reason) override;
const char* getShutdownReasonString(uint8_t reason) override;
#endif
};
/*
* The NRF52 has an internal DC/DC regulator that allows increased efficiency
* compared to the LDO regulator. For being able to use it, the module/board
* needs to have the required inductors and and capacitors populated. If the
* hardware requirements are met, this subclass can be used to enable the DC/DC
* regulator.
*/
class NRF52BoardDCDC : virtual public NRF52Board {
public:
NRF52BoardDCDC() {}
virtual void begin() override;
};
#endif

View File

@@ -2,6 +2,45 @@
#include <helpers/TxtDataHelpers.h>
#include <SHA256.h>
// helper class for region map exporter, we emulate Stream with a safe buffer writer.
class BufStream : public Stream {
public:
BufStream(char *buf, size_t max_len)
: _buf(buf), _max_len(max_len), _pos(0) {
if (_max_len > 0) _buf[0] = 0;
}
size_t write(uint8_t c) override {
if (_pos + 1 >= _max_len) return 0;
_buf[_pos++] = c;
_buf[_pos] = 0;
return 1;
}
size_t write(const uint8_t *buffer, size_t size) override {
size_t written = 0;
while (written < size) {
if (!write(buffer[written])) break;
written++;
}
return written;
}
int available() override { return 0; }
int read() override { return -1; }
int peek() override { return -1; }
void flush() override {}
size_t length() const { return _pos; }
private:
char *_buf;
size_t _max_len;
size_t _pos;
};
RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
next_id = 1; num_regions = 0; home_id = 0;
wildcard.id = wildcard.parent = 0;
@@ -9,8 +48,13 @@ RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
strcpy(wildcard.name, "*");
}
bool RegionMap::is_name_char(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '#';
bool RegionMap::is_name_char(uint8_t c) {
// accept all alpha-num or accented characters, but exclude most punctuation chars
return c == '-' || c == '$' || c == '#' || (c >= '0' && c <= '9') || c >= 'A';
}
static const char* skip_hash(const char* name) {
return *name == '#' ? name + 1 : name;
}
static File openWrite(FILESYSTEM* _fs, const char* filename) {
@@ -24,12 +68,12 @@ static File openWrite(FILESYSTEM* _fs, const char* filename) {
#endif
}
bool RegionMap::load(FILESYSTEM* _fs) {
if (_fs->exists("/regions2")) {
bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
if (_fs->exists(path ? path : "/regions2")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/regions2", "r");
File file = _fs->open(path ? path : "/regions2", "r");
#else
File file = _fs->open("/regions2");
File file = _fs->open(path ? path : "/regions2");
#endif
if (file) {
@@ -67,8 +111,8 @@ bool RegionMap::load(FILESYSTEM* _fs) {
return false; // failed
}
bool RegionMap::save(FILESYSTEM* _fs) {
File file = openWrite(_fs, "/regions2");
bool RegionMap::save(FILESYSTEM* _fs, const char* path) {
File file = openWrite(_fs, path ? path : "/regions2");
if (file) {
uint8_t pad[128];
memset(pad, 0, sizeof(pad));
@@ -126,11 +170,17 @@ RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param)
TransportKey keys[4];
int num;
if (region->name[0] == '#') { // auto hashtag region
if (region->name[0] == '$') { // private region
num = _store->loadKeysFor(region->id, keys, 4);
} else if (region->name[0] == '#') { // auto hashtag region
_store->getAutoKeyFor(region->id, region->name, keys[0]);
num = 1;
} else {
num = _store->loadKeysFor(region->id, keys, 4);
} else { // new: implicit auto hashtag region
char tmp[sizeof(region->name)];
tmp[0] = '#';
strcpy(&tmp[1], region->name);
_store->getAutoKeyFor(region->id, tmp, keys[0]);
num = 1;
}
for (int j = 0; j < num; j++) {
uint16_t code = keys[j].calcTransportCode(packet);
@@ -146,9 +196,10 @@ RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
RegionEntry* RegionMap::findByName(const char* name) {
if (strcmp(name, "*") == 0) return &wildcard;
if (*name == '#') { name++; } // ignore the '#' when matching by name
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(name, region->name) == 0) return region;
if (strcmp(name, skip_hash(region->name)) == 0) return region;
}
return NULL; // not found
}
@@ -156,11 +207,12 @@ RegionEntry* RegionMap::findByName(const char* name) {
RegionEntry* RegionMap::findByNamePrefix(const char* prefix) {
if (strcmp(prefix, "*") == 0) return &wildcard;
if (*prefix == '#') { prefix++; } // ignore the '#' when matching by name
RegionEntry* partial = NULL;
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(prefix, region->name) == 0) return region; // is a complete match, preference this one
if (memcmp(prefix, region->name, strlen(prefix)) == 0) {
if (strcmp(prefix, skip_hash(region->name)) == 0) return region; // is a complete match, preference this one
if (memcmp(prefix, skip_hash(region->name), strlen(prefix)) == 0) {
partial = region;
}
}
@@ -219,9 +271,9 @@ void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream&
}
if (parent->flags & REGION_DENY_FLOOD) {
out.printf("%s%s\n", parent->name, parent->id == home_id ? "^" : "");
out.printf("%s%s\n", skip_hash(parent->name), parent->id == home_id ? "^" : "");
} else {
out.printf("%s%s F\n", parent->name, parent->id == home_id ? "^" : "");
out.printf("%s%s F\n", skip_hash(parent->name), parent->id == home_id ? "^" : "");
}
for (int i = 0; i < num_regions; i++) {
@@ -235,3 +287,43 @@ void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream&
void RegionMap::exportTo(Stream& out) const {
printChildRegions(0, &wildcard, out); // recursive
}
size_t RegionMap::exportTo(char *dest, size_t max_len) const {
if (!dest || max_len == 0) return 0;
BufStream bs(dest, max_len);
exportTo(bs); // ← reuse existing logic
return bs.length();
}
int RegionMap::exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert) {
char *dp = dest;
// Check wildcard region
bool wildcard_matches = invert ? (wildcard.flags & mask) : !(wildcard.flags & mask);
if (wildcard_matches) {
*dp++ = '*';
*dp++ = ',';
}
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
// Check if region matches the filter criteria
bool region_matches = invert ? (region->flags & mask) : !(region->flags & mask);
if (region_matches) {
int len = strlen(skip_hash(region->name));
if ((dp - dest) + len + 2 < max_len) { // only append if name will fit
memcpy(dp, skip_hash(region->name), len);
dp += len;
*dp++ = ',';
}
}
}
if (dp > dest) { dp--; } // don't include trailing comma
*dp = 0; // set null terminator
return dp - dest; // return length
}

View File

@@ -30,10 +30,10 @@ class RegionMap {
public:
RegionMap(TransportKeyStore& store);
static bool is_name_char(char c);
static bool is_name_char(uint8_t c);
bool load(FILESYSTEM* _fs);
bool save(FILESYSTEM* _fs);
bool load(FILESYSTEM* _fs, const char* path=NULL);
bool save(FILESYSTEM* _fs, const char* path=NULL);
RegionEntry* putRegion(const char* name, uint16_t parent_id, uint16_t id = 0);
RegionEntry* findMatch(mesh::Packet* packet, uint8_t mask);
@@ -47,6 +47,11 @@ public:
bool clear();
void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; }
int getCount() const { return num_regions; }
const RegionEntry* getByIdx(int i) const { return &regions[i]; }
const RegionEntry* getRoot() const { return &wildcard; }
int exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert = false);
void exportTo(Stream& out) const;
void exportTo(Stream& out) const;
size_t exportTo(char *dest, size_t max_len) const;
};

View File

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

View File

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

View File

@@ -42,13 +42,14 @@ public:
uint32_t n_recv_flood,
uint32_t n_recv_direct) {
sprintf(reply,
"{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u}",
"{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u,\"recv_errors\":%u}",
driver.getPacketsRecv(),
driver.getPacketsSent(),
n_sent_flood,
n_sent_direct,
n_recv_flood,
n_recv_direct
n_recv_direct,
driver.getPacketsRecvErrors()
);
}
};

View File

@@ -140,6 +140,19 @@ const char* StrHelper::ftoa(float f) {
return tmp;
}
const char* StrHelper::ftoa3(float f) {
static char s[16];
int v = (int)(f * 1000.0f + (f >= 0 ? 0.5f : -0.5f)); // rounded ×1000
int w = v / 1000; // whole
int d = abs(v % 1000); // decimals
snprintf(s, sizeof(s), "%d.%03d", w, d);
for (int i = strlen(s) - 1; i > 0 && s[i] == '0'; i--)
s[i] = 0;
int L = strlen(s);
if (s[L - 1] == '.') s[L - 1] = 0;
return s;
}
uint32_t StrHelper::fromHex(const char* src) {
uint32_t n = 0;
while (*src) {

View File

@@ -12,6 +12,7 @@ public:
static void strncpy(char* dest, const char* src, size_t buf_sz);
static void strzcpy(char* dest, const char* src, size_t buf_sz); // pads with trailing nulls
static const char* ftoa(float f);
static const char* ftoa3(float f); //Converts float to string with 3 decimal places
static bool isBlank(const char* str);
static uint32_t fromHex(const char* src);
};

View File

@@ -16,7 +16,8 @@ void RS232Bridge::begin() {
#if defined(ESP32)
((HardwareSerial *)_serial)->setPins(WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
#elif defined(NRF52_PLATFORM)
((HardwareSerial *)_serial)->setPins(WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
// Tested with RAK_4631 and T114
((Uart *)_serial)->setPins(WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
#elif defined(RP2040_PLATFORM)
((SerialUART *)_serial)->setRX(WITH_RS232_BRIDGE_RX);
((SerialUART *)_serial)->setTX(WITH_RS232_BRIDGE_TX);
@@ -121,8 +122,7 @@ void RS232Bridge::sendPacket(mesh::Packet *packet) {
// Check if packet fits within our maximum payload size
if (len > (MAX_TRANS_UNIT + 1)) {
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", len,
MAX_TRANS_UNIT + 1);
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", len, MAX_TRANS_UNIT + 1);
return;
}

View File

@@ -40,7 +40,7 @@
* Platform Support:
* Different platforms require different pin configuration methods:
* - ESP32: Uses HardwareSerial::setPins(rx, tx)
* - NRF52: Uses HardwareSerial::setPins(rx, tx)
* - NRF52: Uses Uart::setPins(rx, tx)
* - RP2040: Uses SerialUART::setRX(rx) and SerialUART::setTX(tx)
* - STM32: Uses HardwareSerial::setRx(rx) and HardwareSerial::setTx(tx)
*/

View File

@@ -1,4 +1,5 @@
#include "SerialBLEInterface.h"
#include "esp_mac.h"
// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
@@ -9,11 +10,21 @@
#define ADVERT_RESTART_DELAY 1000 // millis
void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
_pin_code = pin_code;
if (strcmp(name, "@@MAC") == 0) {
uint8_t addr[8];
memset(addr, 0, sizeof(addr));
esp_efuse_mac_get_default(addr);
sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param)
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
}
char dev_name[32+16];
sprintf(dev_name, "%s%s", prefix, name);
// Create the BLE Device
BLEDevice::init(device_name);
BLEDevice::init(dev_name);
BLEDevice::setSecurityCallbacks(this);
BLEDevice::setMTU(MAX_FRAME_SIZE);

View File

@@ -61,7 +61,13 @@ public:
send_queue_len = recv_queue_len = 0;
}
void begin(const char* device_name, uint32_t pin_code);
/**
* init the BLE interface.
* @param prefix a prefix for the device name
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param pin_code the BLE security pin
*/
void begin(const char* prefix, char* name, uint32_t pin_code);
// BaseSerialInterface methods
void enable() override;

View File

@@ -43,6 +43,15 @@ bool SerialWifiInterface::isWriteBusy() const {
return false;
}
bool SerialWifiInterface::hasReceivedFrameHeader() {
return received_frame_header.type != 0 && received_frame_header.length != 0;
}
void SerialWifiInterface::resetReceivedFrameHeader() {
received_frame_header.type = 0;
received_frame_header.length = 0;
}
size_t SerialWifiInterface::checkRecvFrame(uint8_t dest[]) {
// check if new client connected
auto newClient = server.available();
@@ -54,6 +63,9 @@ size_t SerialWifiInterface::checkRecvFrame(uint8_t dest[]) {
// switch active connection to new client
client = newClient;
// forget received frame header
resetReceivedFrameHeader();
}
@@ -86,13 +98,69 @@ size_t SerialWifiInterface::checkRecvFrame(uint8_t dest[]) {
send_queue[i] = send_queue[i + 1];
}
} else {
int len = client.available();
if (len > 0) {
uint8_t buf[MAX_FRAME_SIZE + 4];
client.readBytes(buf, len);
memcpy(dest, buf+3, len-3); // remove header (don't even check ... problems are on the other dir)
return len-3;
// check if we are waiting for a frame header
if(!hasReceivedFrameHeader()){
// make sure we have received enough bytes for a frame header
// 3 bytes frame header = (1 byte frame type) + (2 bytes frame length as unsigned 16-bit little endian)
int frame_header_length = 3;
if(client.available() >= frame_header_length){
// read frame header
client.readBytes(&received_frame_header.type, 1);
client.readBytes((uint8_t*)&received_frame_header.length, 2);
}
}
// check if we have received a frame header
if(hasReceivedFrameHeader()){
// make sure we have received enough bytes for the required frame length
int available = client.available();
int frame_type = received_frame_header.type;
int frame_length = received_frame_header.length;
if(frame_length > available){
WIFI_DEBUG_PRINTLN("Waiting for %d more bytes", frame_length - available);
return 0;
}
// skip frames that are larger than MAX_FRAME_SIZE
if(frame_length > MAX_FRAME_SIZE){
WIFI_DEBUG_PRINTLN("Skipping frame: length=%d is larger than MAX_FRAME_SIZE=%d", frame_length, MAX_FRAME_SIZE);
while(frame_length > 0){
uint8_t skip[1];
int skipped = client.read(skip, 1);
frame_length -= skipped;
}
resetReceivedFrameHeader();
return 0;
}
// skip frames that are not expected type
// '<' is 0x3c which indicates a frame sent from app to radio
if(frame_type != '<'){
WIFI_DEBUG_PRINTLN("Skipping frame: type=0x%x is unexpected", frame_type);
while(frame_length > 0){
uint8_t skip[1];
int skipped = client.read(skip, 1);
frame_length -= skipped;
}
resetReceivedFrameHeader();
return 0;
}
// read frame data to provided buffer
client.readBytes(dest, frame_length);
// ready for next frame
resetReceivedFrameHeader();
return frame_length;
}
}
}

View File

@@ -12,11 +12,18 @@ class SerialWifiInterface : public BaseSerialInterface {
WiFiServer server;
WiFiClient client;
struct FrameHeader {
uint8_t type;
uint16_t length;
};
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
FrameHeader received_frame_header;
#define FRAME_QUEUE_SIZE 4
int recv_queue_len;
Frame recv_queue[FRAME_QUEUE_SIZE];
@@ -33,6 +40,8 @@ public:
_isEnabled = false;
_last_write = 0;
send_queue_len = recv_queue_len = 0;
received_frame_header.type = 0;
received_frame_header.length = 0;
}
void begin(int port);
@@ -47,6 +56,9 @@ public:
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
bool hasReceivedFrameHeader();
void resetReceivedFrameHeader();
};
#if WIFI_DEBUG_LOGGING && ARDUINO

View File

@@ -2,16 +2,7 @@
#if defined(TBEAM_SUPREME_SX1262) || defined(TBEAM_SX1262) || defined(TBEAM_SX1276)
#include <Wire.h>
#include <Arduino.h>
#include "XPowersLib.h"
#include "helpers/ESP32Board.h"
#include <driver/rtc_io.h>
//#include <RadioLib.h>
//#include <helpers/RadioLibWrappers.h>
//#include <helpers/CustomSX1262Wrapper.h>
//#include <helpers/CustomSX1276Wrapper.h>
// Define pin mappings BEFORE including ESP32Board.h so sleep() can use P_LORA_DIO_1
#ifdef TBEAM_SUPREME_SX1262
// LoRa radio module pins for TBeam S3 Supreme SX1262
#define P_LORA_DIO_0 -1 //NC
@@ -90,6 +81,13 @@
// SX1276
// };
// Include headers AFTER pin definitions so ESP32Board::sleep() can use P_LORA_DIO_1
#include <Wire.h>
#include <Arduino.h>
#include "XPowersLib.h"
#include "helpers/ESP32Board.h"
#include <driver/rtc_io.h>
class TBeamBoard : public ESP32Board {
XPowersLibInterface *PMU = NULL;
//PhysicalLayer * pl;

View File

@@ -1,193 +1,397 @@
#include "SerialBLEInterface.h"
#include <stdio.h>
#include <string.h>
#include "ble_gap.h"
#include "ble_hci.h"
static SerialBLEInterface* instance;
// Magic numbers came from actual testing
#define BLE_HEALTH_CHECK_INTERVAL 10000 // Advertising watchdog check every 10 seconds
#define BLE_RETRY_THROTTLE_MS 250 // Throttle retries to 250ms when queue buildup detected
// Connection parameters (units: interval=1.25ms, timeout=10ms)
#define BLE_MIN_CONN_INTERVAL 12 // 15ms
#define BLE_MAX_CONN_INTERVAL 24 // 30ms
#define BLE_SLAVE_LATENCY 4
#define BLE_CONN_SUP_TIMEOUT 200 // 2000ms
// Advertising parameters
#define BLE_ADV_INTERVAL_MIN 32 // 20ms (units: 0.625ms)
#define BLE_ADV_INTERVAL_MAX 244 // 152.5ms (units: 0.625ms)
#define BLE_ADV_FAST_TIMEOUT 30 // seconds
// RX drain buffer size for overflow protection
#define BLE_RX_DRAIN_BUF_SIZE 32
static SerialBLEInterface* instance = nullptr;
void SerialBLEInterface::onConnect(uint16_t connection_handle) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: connected");
// we now set _isDeviceConnected=true in onSecured callback instead
BLE_DEBUG_PRINTLN("SerialBLEInterface: connected handle=0x%04X", connection_handle);
if (instance) {
instance->_conn_handle = connection_handle;
instance->_isDeviceConnected = false;
instance->clearBuffers();
}
}
void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected reason=%d", reason);
if(instance){
instance->_isDeviceConnected = false;
instance->startAdv();
BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected handle=0x%04X reason=%u", connection_handle, reason);
if (instance) {
if (instance->_conn_handle == connection_handle) {
instance->_conn_handle = BLE_CONN_HANDLE_INVALID;
instance->_isDeviceConnected = false;
instance->clearBuffers();
}
}
}
void SerialBLEInterface::onSecured(uint16_t connection_handle) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: onSecured");
if(instance){
instance->_isDeviceConnected = true;
// no need to stop advertising on connect, as the ble stack does this automatically
BLE_DEBUG_PRINTLN("SerialBLEInterface: onSecured handle=0x%04X", connection_handle);
if (instance) {
if (instance->isValidConnection(connection_handle, true)) {
instance->_isDeviceConnected = true;
// Connection interval units: 1.25ms, supervision timeout units: 10ms
// Apple: "The product will not read or use the parameters in the Peripheral Preferred Connection Parameters characteristic."
// So we explicitly set it here to make Android & Apple match
ble_gap_conn_params_t conn_params;
conn_params.min_conn_interval = BLE_MIN_CONN_INTERVAL;
conn_params.max_conn_interval = BLE_MAX_CONN_INTERVAL;
conn_params.slave_latency = BLE_SLAVE_LATENCY;
conn_params.conn_sup_timeout = BLE_CONN_SUP_TIMEOUT;
uint32_t err_code = sd_ble_gap_conn_param_update(connection_handle, &conn_params);
if (err_code == NRF_SUCCESS) {
BLE_DEBUG_PRINTLN("Connection parameter update requested: %u-%ums interval, latency=%u, %ums timeout",
conn_params.min_conn_interval * 5 / 4, // convert to ms (1.25ms units)
conn_params.max_conn_interval * 5 / 4,
conn_params.slave_latency,
conn_params.conn_sup_timeout * 10); // convert to ms (10ms units)
} else {
BLE_DEBUG_PRINTLN("Failed to request connection parameter update: %lu", err_code);
}
} else {
BLE_DEBUG_PRINTLN("onSecured: ignoring stale/duplicate callback");
}
}
}
void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
bool SerialBLEInterface::onPairingPasskey(uint16_t connection_handle, uint8_t const passkey[6], bool match_request) {
(void)connection_handle;
(void)passkey;
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing passkey request match=%d", match_request);
return true;
}
void SerialBLEInterface::onPairingComplete(uint16_t connection_handle, uint8_t auth_status) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing complete handle=0x%04X status=%u", connection_handle, auth_status);
if (instance) {
if (instance->isValidConnection(connection_handle)) {
if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing successful");
} else {
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing failed, disconnecting");
instance->disconnect();
}
} else {
BLE_DEBUG_PRINTLN("onPairingComplete: ignoring stale callback");
}
}
}
void SerialBLEInterface::onBLEEvent(ble_evt_t* evt) {
if (!instance) return;
if (evt->header.evt_id == BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST) {
uint16_t conn_handle = evt->evt.gap_evt.conn_handle;
if (instance->isValidConnection(conn_handle)) {
BLE_DEBUG_PRINTLN("CONN_PARAM_UPDATE_REQUEST: handle=0x%04X, min_interval=%u, max_interval=%u, latency=%u, timeout=%u",
conn_handle,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.min_conn_interval,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.max_conn_interval,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.slave_latency,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.conn_sup_timeout);
uint32_t err_code = sd_ble_gap_conn_param_update(conn_handle, NULL);
if (err_code == NRF_SUCCESS) {
BLE_DEBUG_PRINTLN("Accepted CONN_PARAM_UPDATE_REQUEST (using PPCP)");
} else {
BLE_DEBUG_PRINTLN("ERROR: Failed to accept CONN_PARAM_UPDATE_REQUEST: 0x%08X", err_code);
}
} else {
BLE_DEBUG_PRINTLN("CONN_PARAM_UPDATE_REQUEST: ignoring stale callback for handle=0x%04X", conn_handle);
}
}
}
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
instance = this;
char charpin[20];
sprintf(charpin, "%d", pin_code);
snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code);
// If we want to control BLE LED ourselves, uncomment this:
// Bluefruit.autoConnLed(false);
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.configPrphConn(250, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); // increase MTU
Bluefruit.setTxPower(BLE_TX_POWER);
Bluefruit.begin();
Bluefruit.setName(device_name);
char dev_name[32+16];
if (strcmp(name, "@@MAC") == 0) {
ble_gap_addr_t addr;
if (sd_ble_gap_addr_get(&addr) == NRF_SUCCESS) {
sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param)
addr.addr[5], addr.addr[4], addr.addr[3], addr.addr[2], addr.addr[1], addr.addr[0]);
}
}
sprintf(dev_name, "%s%s", prefix, name);
// Connection interval units: 1.25ms, supervision timeout units: 10ms
ble_gap_conn_params_t ppcp_params;
ppcp_params.min_conn_interval = BLE_MIN_CONN_INTERVAL;
ppcp_params.max_conn_interval = BLE_MAX_CONN_INTERVAL;
ppcp_params.slave_latency = BLE_SLAVE_LATENCY;
ppcp_params.conn_sup_timeout = BLE_CONN_SUP_TIMEOUT;
uint32_t err_code = sd_ble_gap_ppcp_set(&ppcp_params);
if (err_code == NRF_SUCCESS) {
BLE_DEBUG_PRINTLN("PPCP set: %u-%ums interval, latency=%u, %ums timeout",
ppcp_params.min_conn_interval * 5 / 4, // convert to ms (1.25ms units)
ppcp_params.max_conn_interval * 5 / 4,
ppcp_params.slave_latency,
ppcp_params.conn_sup_timeout * 10); // convert to ms (10ms units)
} else {
BLE_DEBUG_PRINTLN("Failed to set PPCP: %lu", err_code);
}
Bluefruit.setTxPower(BLE_TX_POWER);
Bluefruit.setName(dev_name);
Bluefruit.Security.setMITM(true);
Bluefruit.Security.setPIN(charpin);
Bluefruit.Security.setIOCaps(true, false, false);
Bluefruit.Security.setPairPasskeyCallback(onPairingPasskey);
Bluefruit.Security.setPairCompleteCallback(onPairingComplete);
Bluefruit.Periph.setConnectCallback(onConnect);
Bluefruit.Periph.setDisconnectCallback(onDisconnect);
Bluefruit.Security.setSecuredCallback(onSecured);
// To be consistent OTA DFU should be added first if it exists
//bledfu.begin();
Bluefruit.setEventCallback(onBLEEvent);
// Configure and start the BLE Uart service
bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
bleuart.begin();
}
bleuart.setRxCallback(onBleUartRX);
void SerialBLEInterface::startAdv() {
BLE_DEBUG_PRINTLN("SerialBLEInterface: starting advertising");
// clean restart if already advertising
if(Bluefruit.Advertising.isRunning()){
BLE_DEBUG_PRINTLN("SerialBLEInterface: already advertising, stopping to allow clean restart");
Bluefruit.Advertising.stop();
}
Bluefruit.Advertising.clearData(); // clear advertising data
Bluefruit.ScanResponse.clearData(); // clear scan response data
// Advertising packet
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
// Include the BLE UART (AKA 'NUS') 128-bit UUID
Bluefruit.Advertising.addService(bleuart);
// Secondary Scan Response packet (optional)
// Since there is no room for 'Name' in Advertising packet
Bluefruit.ScanResponse.addName();
/* Start Advertising
* - Enable auto advertising if disconnected
* - Interval: fast mode = 20 ms, slow mode = 152.5 ms
* - Timeout for fast mode is 30 seconds
* - Start(timeout) with timeout = 0 will advertise forever (until connected)
*
* For recommended advertising interval
* https://developer.apple.com/library/content/qa/qa1931/_index.html
*/
Bluefruit.Advertising.restartOnDisconnect(false); // don't restart automatically as we handle it in onDisconnect
Bluefruit.Advertising.setInterval(32, 244);
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
Bluefruit.Advertising.setInterval(BLE_ADV_INTERVAL_MIN, BLE_ADV_INTERVAL_MAX);
Bluefruit.Advertising.setFastTimeout(BLE_ADV_FAST_TIMEOUT);
Bluefruit.Advertising.restartOnDisconnect(true);
}
void SerialBLEInterface::stopAdv() {
void SerialBLEInterface::clearBuffers() {
send_queue_len = 0;
recv_queue_len = 0;
_last_retry_attempt = 0;
bleuart.flush();
}
BLE_DEBUG_PRINTLN("SerialBLEInterface: stopping advertising");
// we only want to stop advertising if it's running, otherwise an invalid state error is logged by ble stack
if(!Bluefruit.Advertising.isRunning()){
return;
void SerialBLEInterface::shiftSendQueueLeft() {
if (send_queue_len > 0) {
send_queue_len--;
for (uint8_t i = 0; i < send_queue_len; i++) {
send_queue[i] = send_queue[i + 1];
}
}
// stop advertising
Bluefruit.Advertising.stop();
}
// ---------- public methods
void SerialBLEInterface::shiftRecvQueueLeft() {
if (recv_queue_len > 0) {
recv_queue_len--;
for (uint8_t i = 0; i < recv_queue_len; i++) {
recv_queue[i] = recv_queue[i + 1];
}
}
}
void SerialBLEInterface::enable() {
bool SerialBLEInterface::isValidConnection(uint16_t handle, bool requireWaitingForSecurity) const {
if (_conn_handle != handle) {
return false;
}
BLEConnection* conn = Bluefruit.Connection(handle);
if (conn == nullptr || !conn->connected()) {
return false;
}
if (requireWaitingForSecurity && _isDeviceConnected) {
return false;
}
return true;
}
bool SerialBLEInterface::isAdvertising() const {
ble_gap_addr_t adv_addr;
uint32_t err_code = sd_ble_gap_adv_addr_get(0, &adv_addr);
return (err_code == NRF_SUCCESS);
}
void SerialBLEInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
clearBuffers();
_last_health_check = millis();
// Start advertising
startAdv();
Bluefruit.Advertising.start(0);
}
void SerialBLEInterface::disconnect() {
if (_conn_handle != BLE_CONN_HANDLE_INVALID) {
sd_ble_gap_disconnect(_conn_handle, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
}
}
void SerialBLEInterface::disable() {
_isEnabled = false;
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
BLE_DEBUG_PRINTLN("SerialBLEInterface: disable");
#ifdef RAK_BOARD
Bluefruit.disconnect(Bluefruit.connHandle());
#else
uint16_t conn_id;
if (Bluefruit.getConnectedHandles(&conn_id, 1) > 0) {
Bluefruit.disconnect(conn_id);
}
#endif
Bluefruit.Advertising.restartOnDisconnect(false);
disconnect();
Bluefruit.Advertising.stop();
Bluefruit.Advertising.clearData();
stopAdv();
_last_health_check = 0;
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d", len);
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len);
return 0;
}
if (_isDeviceConnected && len > 0) {
bool connected = isConnected();
if (connected && len > 0) {
if (send_queue_len >= FRAME_QUEUE_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
}
send_queue[send_queue_len].len = len; // add to send queue
send_queue[send_queue_len].len = len;
memcpy(send_queue[send_queue_len].buf, src, len);
send_queue_len++;
return len;
}
return 0;
}
#define BLE_WRITE_MIN_INTERVAL 60
bool SerialBLEInterface::isWriteBusy() const {
return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write?
}
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
if (send_queue_len > 0 // first, check send queue
&& millis() >= _last_write + BLE_WRITE_MIN_INTERVAL // space the writes apart
) {
_last_write = millis();
bleuart.write(send_queue[0].buf, send_queue[0].len);
BLE_DEBUG_PRINTLN("writeBytes: sz=%d, hdr=%d", (uint32_t)send_queue[0].len, (uint32_t) send_queue[0].buf[0]);
if (send_queue_len > 0) {
if (!isConnected()) {
BLE_DEBUG_PRINTLN("writeBytes: connection invalid, clearing send queue");
send_queue_len = 0;
} else {
unsigned long now = millis();
bool throttle_active = (_last_retry_attempt > 0 && (now - _last_retry_attempt) < BLE_RETRY_THROTTLE_MS);
send_queue_len--;
for (int i = 0; i < send_queue_len; i++) { // delete top item from queue
send_queue[i] = send_queue[i + 1];
}
} else {
int len = bleuart.available();
if (len > 0) {
bleuart.readBytes(dest, len);
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]);
return len;
if (!throttle_active) {
Frame frame_to_send = send_queue[0];
size_t written = bleuart.write(frame_to_send.buf, frame_to_send.len);
if (written == frame_to_send.len) {
BLE_DEBUG_PRINTLN("writeBytes: sz=%u, hdr=%u", (unsigned)frame_to_send.len, (unsigned)frame_to_send.buf[0]);
_last_retry_attempt = 0;
shiftSendQueueLeft();
} else if (written > 0) {
BLE_DEBUG_PRINTLN("writeBytes: partial write, sent=%u of %u, dropping corrupted frame", (unsigned)written, (unsigned)frame_to_send.len);
_last_retry_attempt = 0;
shiftSendQueueLeft();
} else {
if (!isConnected()) {
BLE_DEBUG_PRINTLN("writeBytes failed: connection lost, dropping frame");
_last_retry_attempt = 0;
shiftSendQueueLeft();
} else {
BLE_DEBUG_PRINTLN("writeBytes failed (buffer full), keeping frame for retry");
_last_retry_attempt = now;
}
}
}
}
}
if (recv_queue_len > 0) {
size_t len = recv_queue[0].len;
memcpy(dest, recv_queue[0].buf, len);
BLE_DEBUG_PRINTLN("readBytes: sz=%u, hdr=%u", (unsigned)len, (unsigned)dest[0]);
shiftRecvQueueLeft();
return len;
}
// Advertising watchdog: periodically check if advertising is running, restart if not
// Only run when truly disconnected (no connection handle), not during connection establishment
unsigned long now = millis();
if (_isEnabled && !isConnected() && _conn_handle == BLE_CONN_HANDLE_INVALID) {
if (now - _last_health_check >= BLE_HEALTH_CHECK_INTERVAL) {
_last_health_check = now;
if (!isAdvertising()) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: advertising watchdog - advertising stopped, restarting");
Bluefruit.Advertising.start(0);
}
}
}
return 0;
}
bool SerialBLEInterface::isConnected() const {
return _isDeviceConnected;
void SerialBLEInterface::onBleUartRX(uint16_t conn_handle) {
if (!instance) {
return;
}
if (instance->_conn_handle != conn_handle || !instance->isConnected()) {
while (instance->bleuart.available() > 0) {
instance->bleuart.read();
}
return;
}
while (instance->bleuart.available() > 0) {
if (instance->recv_queue_len >= FRAME_QUEUE_SIZE) {
while (instance->bleuart.available() > 0) {
instance->bleuart.read();
}
BLE_DEBUG_PRINTLN("onBleUartRX: recv queue full, dropping data");
break;
}
int avail = instance->bleuart.available();
if (avail > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("onBleUartRX: WARN: BLE RX overflow, avail=%d, draining all", avail);
uint8_t drain_buf[BLE_RX_DRAIN_BUF_SIZE];
while (instance->bleuart.available() > 0) {
int chunk = instance->bleuart.available() > BLE_RX_DRAIN_BUF_SIZE ? BLE_RX_DRAIN_BUF_SIZE : instance->bleuart.available();
instance->bleuart.readBytes(drain_buf, chunk);
}
continue;
}
int read_len = avail;
instance->recv_queue[instance->recv_queue_len].len = read_len;
instance->bleuart.readBytes(instance->recv_queue[instance->recv_queue_len].buf, read_len);
instance->recv_queue_len++;
}
}
bool SerialBLEInterface::isConnected() const {
return _isDeviceConnected && Bluefruit.connected() > 0;
}
bool SerialBLEInterface::isWriteBusy() const {
return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3);
}

View File

@@ -11,41 +11,60 @@ class SerialBLEInterface : public BaseSerialInterface {
BLEUart bleuart;
bool _isEnabled;
bool _isDeviceConnected;
unsigned long _last_write;
uint16_t _conn_handle;
unsigned long _last_health_check;
unsigned long _last_retry_attempt;
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
#define FRAME_QUEUE_SIZE 4
int send_queue_len;
#define FRAME_QUEUE_SIZE 12
uint8_t send_queue_len;
Frame send_queue[FRAME_QUEUE_SIZE];
uint8_t recv_queue_len;
Frame recv_queue[FRAME_QUEUE_SIZE];
void clearBuffers() { send_queue_len = 0; }
void clearBuffers();
void shiftSendQueueLeft();
void shiftRecvQueueLeft();
bool isValidConnection(uint16_t handle, bool requireWaitingForSecurity = false) const;
bool isAdvertising() const;
static void onConnect(uint16_t connection_handle);
static void onDisconnect(uint16_t connection_handle, uint8_t reason);
static void onSecured(uint16_t connection_handle);
static bool onPairingPasskey(uint16_t connection_handle, uint8_t const passkey[6], bool match_request);
static void onPairingComplete(uint16_t connection_handle, uint8_t auth_status);
static void onBLEEvent(ble_evt_t* evt);
static void onBleUartRX(uint16_t conn_handle);
public:
SerialBLEInterface() {
_isEnabled = false;
_isDeviceConnected = false;
_last_write = 0;
_conn_handle = BLE_CONN_HANDLE_INVALID;
_last_health_check = 0;
_last_retry_attempt = 0;
send_queue_len = 0;
recv_queue_len = 0;
}
void startAdv();
void stopAdv();
void begin(const char* device_name, uint32_t pin_code);
/**
* init the BLE interface.
* @param prefix a prefix for the device name
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param pin_code the BLE security pin
*/
void begin(const char* prefix, char* name, uint32_t pin_code);
// BaseSerialInterface methods
void disconnect();
void enable() override;
void disable() override;
bool isEnabled() const override { return _isEnabled; }
bool isConnected() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;

View File

@@ -10,7 +10,7 @@ class CustomLR1110 : public LR1110 {
size_t getPacketLength(bool update) override {
size_t len = LR1110::getPacketLength(update);
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
// we've just recieved a corrupted packet
// we've just received a corrupted packet
// this may have triggered a bug causing subsequent packets to be shifted
// call standby() to return radio to known-good state
// recvRaw will call startReceive() to restart rx

View File

@@ -76,6 +76,14 @@ class CustomSX1262 : public SX1262 {
setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
#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
}

View File

@@ -19,4 +19,7 @@ public:
int sf = ((CustomSX1262 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
virtual void powerOff() override {
((CustomSX1262 *)_radio)->sleep(false);
}
};

View File

@@ -105,6 +105,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err);
len = 0;
n_recv_errors++;
} else {
// Serial.print(" readData() -> "); Serial.println(len);
n_recv++;
@@ -137,6 +138,7 @@ bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) {
}
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startTransmit(%d)", err);
idle(); // trigger another startRecv()
_board->onAfterTransmit();
return false;
}

View File

@@ -7,7 +7,7 @@ class RadioLibWrapper : public mesh::Radio {
protected:
PhysicalLayer* _radio;
mesh::MainBoard* _board;
uint32_t n_recv, n_sent;
uint32_t n_recv, n_sent, n_recv_errors;
int16_t _noise_floor, _threshold;
uint16_t _num_floor_samples;
int32_t _floor_sample_sum;
@@ -21,6 +21,7 @@ public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; }
void begin() override;
virtual void powerOff() { _radio->sleep(); }
int recvRaw(uint8_t* bytes, int sz) override;
uint32_t getEstAirtimeFor(int len_bytes) override;
bool startSendRaw(const uint8_t* bytes, int len) override;
@@ -44,8 +45,9 @@ public:
void loop() override;
uint32_t getPacketsRecv() const { return n_recv; }
uint32_t getPacketsRecvErrors() const { return n_recv_errors; }
uint32_t getPacketsSent() const { return n_sent; }
void resetStats() { n_recv = n_sent = 0; }
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
virtual float getLastRSSI() const override;
virtual float getLastSNR() const override;

View File

@@ -42,7 +42,7 @@ static Adafruit_BME280 BME280;
#endif
#define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level
#include <Adafruit_BMP280.h>
static Adafruit_BMP280 BMP280;
static Adafruit_BMP280 BMP280(TELEM_WIRE);
#endif
#if ENV_INCLUDE_SHTC3
@@ -58,6 +58,7 @@ static SensirionI2cSht4x SHT4X;
#if ENV_INCLUDE_LPS22HB
#include <Arduino_LPS22HB.h>
LPS22HBClass LPS22HB(*TELEM_WIRE);
#endif
#if ENV_INCLUDE_INA3221
@@ -178,10 +179,27 @@ bool EnvironmentSensorManager::begin() {
}
#endif
#if ENV_INCLUDE_BME680
if (BME680.begin(TELEM_BME680_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS);
BME680_initialized = true;
} else {
BME680_initialized = false;
MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESS);
}
#endif
#if ENV_INCLUDE_BME280
if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS);
MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID());
// Reduce self-heating: single-shot conversions, light oversampling, long standby.
BME280.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1, // temperature
Adafruit_BME280::SAMPLING_X1, // pressure
Adafruit_BME280::SAMPLING_X1, // humidity
Adafruit_BME280::FILTER_OFF,
Adafruit_BME280::STANDBY_MS_1000);
BME280_initialized = true;
} else {
BME280_initialized = false;
@@ -201,7 +219,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_SHTC3
if (SHTC3.begin()) {
if (SHTC3.begin(TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found sensor: SHTC3");
SHTC3_initialized = true;
} else {
@@ -226,7 +244,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_LPS22HB
if (BARO.begin()) {
if (LPS22HB.begin()) {
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
LPS22HB_initialized = true;
} else {
@@ -266,7 +284,7 @@ bool EnvironmentSensorManager::begin() {
INA260_initialized = true;
} else {
INA260_initialized = false;
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA260_ADDRESS);
}
#endif
@@ -301,16 +319,6 @@ bool EnvironmentSensorManager::begin() {
}
#endif
#if ENV_INCLUDE_BME680
if (BME680.begin(TELEM_BME680_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS);
BME680_initialized = true;
} else {
BME680_initialized = false;
MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESS);
}
#endif
#if ENV_INCLUDE_BMP085
// First argument is MODE (aka oversampling)
// choose ULTRALOWPOWER
@@ -344,12 +352,27 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
}
#endif
#if ENV_INCLUDE_BME680
if (BME680_initialized) {
if (BME680.performReading()) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME680.temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME680.humidity);
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME680.pressure / 100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, 44330.0 * (1.0 - pow((BME680.pressure / 100) / TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903)));
telemetry.addAnalogInput(next_available_channel, BME680.gas_resistance);
next_available_channel++;
}
}
#endif
#if ENV_INCLUDE_BME280
if (BME280_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME280.readTemperature());
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME280.readHumidity());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()/100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
if (BME280.takeForcedMeasurement()) { // trigger a fresh reading in forced mode
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME280.readTemperature());
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME280.readHumidity());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()/100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
}
}
#endif
@@ -385,8 +408,8 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
#if ENV_INCLUDE_LPS22HB
if (LPS22HB_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BARO.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BARO.readPressure());
telemetry.addTemperature(TELEM_CHANNEL_SELF, LPS22HB.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, LPS22HB.readPressure() * 10); // convert kPa to hPa
}
#endif
@@ -452,19 +475,6 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
}
#endif
#if ENV_INCLUDE_BME680
if (BME680_initialized) {
if (BME680.performReading()) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME680.temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME680.humidity);
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME680.pressure / 100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, 44330.0 * (1.0 - pow((BME680.pressure / 100) / TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903)));
telemetry.addAnalogInput(next_available_channel, BME680.gas_resistance);
next_available_channel++;
}
}
#endif
#if ENV_INCLUDE_BMP085
if (BMP085_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP085.readTemperature());
@@ -521,6 +531,15 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val
}
return true;
}
if (strcmp(name, "gps_interval") == 0) {
uint32_t interval_seconds = atoi(value);
if (interval_seconds > 0) {
gps_update_interval_sec = interval_seconds;
} else {
gps_update_interval_sec = 1; // Default to 1 second if 0
}
return true;
}
#endif
return false; // not supported
}
@@ -548,7 +567,11 @@ void EnvironmentSensorManager::initBasicGPS() {
delay(1000);
// We'll consider GPS detected if we see any data on Serial1
#ifdef ENV_SKIP_GPS_DETECT
gps_detected = true;
#else
gps_detected = (Serial1.available() > 0);
#endif
if (gps_detected) {
MESH_DEBUG_PRINTLN("GPS detected");
@@ -563,7 +586,7 @@ void EnvironmentSensorManager::initBasicGPS() {
gps_active = false; //Set GPS visibility off until setting is changed
}
// gps code for rak might be moved to MicroNMEALoactionProvider
// gps code for rak might be moved to MicroNMEALoactionProvider
// or make a new location provider ...
#ifdef RAK_WISBLOCK_GPS
void EnvironmentSensorManager::rakGPSInit(){
@@ -593,6 +616,7 @@ void EnvironmentSensorManager::rakGPSInit(){
MESH_DEBUG_PRINTLN("No GPS found");
gps_active = false;
gps_detected = false;
Serial1.end();
return;
}
@@ -631,8 +655,7 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
_location = &RAK12500_provider;
return true;
}
else if(Serial1){
} else if (Serial1.available()) {
MESH_DEBUG_PRINTLN("Serial GPS init correctly and is turned on");
if(PIN_GPS_EN){
gpsResetPin = PIN_GPS_EN;
@@ -642,6 +665,8 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
gps_detected = true;
return true;
}
pinMode(ioPin, INPUT);
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
return false;
}
@@ -683,8 +708,8 @@ void EnvironmentSensorManager::loop() {
#if ENV_INCLUDE_GPS
_location->loop();
if (millis() > next_gps_update) {
if(gps_active){
#ifdef RAK_WISBLOCK_GPS
if ((i2cGPSFlag || serialGPSFlag) && _location->isValid()) {
@@ -704,7 +729,7 @@ void EnvironmentSensorManager::loop() {
}
#endif
}
next_gps_update = millis() + 1000;
next_gps_update = millis() + (gps_update_interval_sec * 1000);
}
#endif
}

View File

@@ -25,6 +25,7 @@ protected:
bool gps_detected = false;
bool gps_active = false;
uint32_t gps_update_interval_sec = 1; // Default 1 second
#if ENV_INCLUDE_GPS
LocationProvider* _location;

View File

@@ -113,7 +113,7 @@ public:
return _pos <= _len;
}
bool readCurrent(float& amps) {
amps = getFloat(&_buf[_pos], 2, 1000, false); _pos += 2;
amps = getFloat(&_buf[_pos], 2, 1000, true); _pos += 2;
return _pos <= _len;
}
bool readPower(float& watts) {

View File

@@ -1,13 +1,26 @@
#include "GxEPDDisplay.h"
#ifdef EXP_PIN_BACKLIGHT
#include <PCA9557.h>
extern PCA9557 expander;
#endif
#ifndef DISPLAY_ROTATION
#define DISPLAY_ROTATION 3
#endif
#ifdef ESP32
SPIClass SPI1 = SPIClass(FSPI);
#endif
bool GxEPDDisplay::begin() {
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
#ifdef ESP32
SPI1.begin(PIN_DISPLAY_SCLK, PIN_DISPLAY_MISO, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS);
#else
SPI1.begin();
#endif
display.init(115200, true, 2, false);
display.setRotation(DISPLAY_ROTATION);
setTextSize(1); // Default to size 1
@@ -27,6 +40,8 @@ void GxEPDDisplay::turnOn() {
if (!_init) begin();
#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN)
digitalWrite(DISP_BACKLIGHT, HIGH);
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH);
#endif
_isOn = true;
}
@@ -34,6 +49,8 @@ void GxEPDDisplay::turnOn() {
void GxEPDDisplay::turnOff() {
#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN)
digitalWrite(DISP_BACKLIGHT, LOW);
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW);
#endif
_isOn = false;
}

View File

@@ -7,6 +7,10 @@ bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) {
}
bool SSD1306Display::begin() {
if (!_isOn) {
if (_peripher_power) _peripher_power->claim();
_isOn = true;
}
#ifdef DISPLAY_ROTATION
display.setRotation(DISPLAY_ROTATION);
#endif
@@ -14,13 +18,25 @@ bool SSD1306Display::begin() {
}
void SSD1306Display::turnOn() {
if (!_isOn) {
if (_peripher_power) _peripher_power->claim();
_isOn = true; // set before begin() to prevent double claim
if (_peripher_power) begin(); // re-init display after power was cut
}
display.ssd1306_command(SSD1306_DISPLAYON);
_isOn = true;
}
void SSD1306Display::turnOff() {
display.ssd1306_command(SSD1306_DISPLAYOFF);
_isOn = false;
if (_isOn) {
if (_peripher_power) {
#if PIN_OLED_RESET >= 0
digitalWrite(PIN_OLED_RESET, LOW);
#endif
_peripher_power->release();
}
_isOn = false;
}
}
void SSD1306Display::clear() {

View File

@@ -5,6 +5,7 @@
#include <Adafruit_GFX.h>
#define SSD1306_NO_SPLASH
#include <Adafruit_SSD1306.h>
#include <helpers/RefCountedDigitalPin.h>
#ifndef PIN_OLED_RESET
#define PIN_OLED_RESET 21 // Reset pin # (or -1 if sharing Arduino reset pin)
@@ -18,10 +19,16 @@ class SSD1306Display : public DisplayDriver {
Adafruit_SSD1306 display;
bool _isOn;
uint8_t _color;
RefCountedDigitalPin* _peripher_power;
bool i2c_probe(TwoWire& wire, uint8_t addr);
public:
SSD1306Display() : DisplayDriver(128, 64), display(128, 64, &Wire, PIN_OLED_RESET) { _isOn = false; }
SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
display(128, 64, &Wire, PIN_OLED_RESET),
_peripher_power(peripher_power)
{
_isOn = false;
}
bool begin();
bool isOn() override { return _isOn; }

View File

@@ -10,8 +10,13 @@
#define Y_OFFSET 1 // Vertical offset to prevent top row cutoff
#endif
#define SCALE_X 1.875f // 240 / 128
#define SCALE_Y 2.109375f // 135 / 64
#ifdef HELTEC_VISION_MASTER_T190
#define SCALE_X 2.5f // 320 / 128
#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() {
if(!_isOn) {

View File

@@ -23,12 +23,19 @@ bool ST7789LCDDisplay::begin() {
if (!_isOn) {
if (_peripher_power) _peripher_power->claim();
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
digitalWrite(PIN_TFT_RST, HIGH);
if (PIN_TFT_LEDA_CTL != -1) {
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
}
if (PIN_TFT_RST != -1) {
pinMode(PIN_TFT_RST, OUTPUT);
digitalWrite(PIN_TFT_RST, LOW);
delay(10);
digitalWrite(PIN_TFT_RST, HIGH);
}
// Im not sure if this is just a t-deck problem or not, if your display is slow try this.
#ifdef LILYGO_TDECK
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
displaySPI.begin(PIN_TFT_SCL, -1, PIN_TFT_SDA, PIN_TFT_CS);
#endif
@@ -54,9 +61,15 @@ void ST7789LCDDisplay::turnOn() {
void ST7789LCDDisplay::turnOff() {
if (_isOn) {
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
digitalWrite(PIN_TFT_RST, LOW);
digitalWrite(PIN_TFT_LEDA_CTL, LOW);
if (PIN_TFT_LEDA_CTL != -1) {
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
}
if (PIN_TFT_RST != -1) {
digitalWrite(PIN_TFT_RST, LOW);
}
if (PIN_TFT_LEDA_CTL != -1) {
digitalWrite(PIN_TFT_LEDA_CTL, LOW);
}
_isOn = false;
if (_peripher_power) _peripher_power->release();

View File

@@ -8,7 +8,7 @@
#include <helpers/RefCountedDigitalPin.h>
class ST7789LCDDisplay : public DisplayDriver {
#ifdef LILYGO_TDECK
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
SPIClass displaySPI;
#endif
Adafruit_ST7789 display;
@@ -25,7 +25,7 @@ public:
{
_isOn = false;
}
#elif LILYGO_TDECK
#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
displaySPI(HSPI),
display(&displaySPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST),

View File

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

View File

@@ -25,5 +25,5 @@ extern SensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(uint8_t dbm);
void radio_set_tx_power(int8_t dbm);
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);
}
void radio_set_tx_power(uint8_t dbm) {
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}

View File

@@ -17,5 +17,5 @@ extern SensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(uint8_t dbm);
void radio_set_tx_power(int8_t dbm);
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
}
void radio_set_tx_power(uint8_t dbm) {
void radio_set_tx_power(int8_t dbm) {
radio_driver.setTxPower(dbm);
}

View File

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

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