Compare commits

...

870 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
Scott Powell
91e9fcea4b * ver 1.10.0 2025-11-13 20:45:38 +11:00
fdlamotte
750e955f19 Update library.json to latest libs and version 2025-11-13 10:39:20 +01:00
ripplebiz
a5cdc88fe2 Merge pull request #1064 from recrof/esp_contacts_350_channels_40
set max contacts to 350 and channels to 40 for esp32c3, s3 and c6
2025-11-12 00:49:05 +11:00
ripplebiz
ba6b8535c9 Merge pull request #971 from fdlamotte/remove_set_setting_by_key
SensorManager: remove setSettingByKey
2025-11-11 23:40:13 +11:00
liquidraver
b0ce00652f Fix RAK4631 GPS UART pin macros 2025-11-11 13:00:27 +01:00
Florent
90e26129ee Merge branch 'dev' into remove_set_setting_by_key 2025-11-11 12:23:12 +01:00
Scott Powell
b59d1999e6 * Sensor: DISCOVER_REQ, prefix_only support 2025-11-11 20:08:05 +11:00
ripplebiz
74f136ba7a Merge pull request #1068 from fdlamotte/sensor_control_data
sensor: copy control data code from repeater
2025-11-11 20:03:33 +11: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
Scott Powell
ab0721d6df * fix: repeater and room server telemetry requests now return all telemetry for _READ & _WRITE ACL permissions. 2025-11-09 16:36:23 +11:00
Scott Powell
b31d3e7b5f * added StrHelper::fromHex() 2025-11-09 16:17:49 +11: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
Scott Powell
1520f4d28e * repeater, DISCOVER_REQ, flags lowest bit now for 'prefix_only' responses 2025-11-07 19:31:09 +11:00
Scott Powell
62d7ce110b * packet format docs updated 2025-11-07 15:49:01 +11:00
Scott Powell
28b90c18cf Merge branch 'transportcodes' into dev 2025-11-07 14:52:11 +11:00
Scott Powell
963290ea15 * repeater: various "region" CLI changes
* transport codes 0000 and FFFF reserved
2025-11-07 14:42:06 +11:00
Florent
06825030e5 sensor: copy control data code from repeater 2025-11-06 22:36:37 +01:00
Scott Powell
2e63499ae5 * companion: protocol ver bumped to 8. 2025-11-06 22:51:17 +11:00
Scott Powell
4a5404d997 * companion: added CMD_SEND_CONTROL_DATA, and PUSH_CODE_CONTROL_DATA 2025-11-06 22:10:20 +11:00
Scott Powell
ddac13ae80 * repeater: CLI, added "region put" and "region remove" commands 2025-11-06 21:40:52 +11:00
Scott Powell
256848208d * repeater: onAnonDataRecv(), future code check bug fix (offset 4)
* sensor: onAnonDataRecv(), future request code provision
2025-11-06 20:22:40 +11:00
Scott Powell
09eab330a2 * repeater: onAnonDataRecv(), now rejecting non-ASCII password (preparing for future request codes)
* repeater: DISCOVER requests now with a simple RateLimiter (max 4, every 2 minutes)
2025-11-06 20:15:01 +11:00
Scott Powell
cf547da857 * RegionMap: get/set Home Region
* repeater: admin CLI, changed "allowf *", "denyf *", added "home"
2025-11-06 17:28:45 +11:00
ripplebiz
a9d245fe68 Merge pull request #1038 from adam2872/Analogue-button-fix-for-new-UI
Analogue user button fix for new UI
2025-11-06 15:41:45 +11:00
ripplebiz
23783b27c8 Merge pull request #1058 from dotdavid/dev
Fix Xiao S3 WIO board name
2025-11-06 13:45:12 +11:00
Scott Powell
7419ed71f7 * region filtering now applied in allowPacketForward() 2025-11-06 12:27:25 +11:00
Scott Powell
82b4c1e6b0 * new PAYLOAD_TYPE_CONTROL (11)
* repeater: onControlDataRecv(), now responds to new CTL_TYPE_NODE_DISCOVER_REQ (zero hop only)
* node prefs: new discovery_mod_timestamp  (will be set to affect when node should respond to DISCOVERY_REQ's )
2025-11-06 00:56:54 +11:00
Scott Powell
3ef53e64a1 * is_name_char() bug fix 2025-11-05 15:34:23 +11:00
Scott Powell
937865c8fd * companion: new CMD_SET_FLOOD_SCOPE (54) 2025-11-05 14:56:18 +11:00
Scott Powell
9ebeb477aa * RegionMap: inverted 'flags' to _deny_ bits
* Mesh: new filterRecvFloodPacket() for overriding
* repeater CLI: 'allow' -> 'allowf' or 'denyf'
2025-11-05 14:34:44 +11:00
recrof
04c0c40b39 set max contacts to 350 and channels to 40 for esp32c3, s3 and c6 2025-11-04 23:58:32 +01:00
David Hall
c3dbec41ba Fix manufacturer name on Seeed Xiao S3 WIO 2025-11-03 21:02:08 +00:00
David Hall
5c80334dbd Fix manufacturer name on Seeed Xiao S3 WIO 2025-11-03 21:00:43 +00: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
Scott Powell
d9ff3a4d02 * Mesh: new sendFlood() overload with transport codes.
* BaseChatMesh:  sendFloodScoped(), for overriding with some outbound 'scope' / TransportKey
* companion: new 'send_scope' variable.
2025-11-04 01:21:56 +11:00
Scott Powell
ecd30f4d36 * new CLI commands: region, region load, region save, region get, region allow 2025-11-03 22:53:14 +11:00
Scott Powell
f797744f7c * misc RegionMap and key store methods 2025-11-03 18:14:44 +11:00
Scott Powell
03fc949014 * setting up framework for Regions, TransportKeys, etc 2025-11-03 14:23:32 +11:00
ripplebiz
5b4544b9fe Merge pull request #889 from fdlamotte/sensecap_indicator
Sensecap indicator
2025-11-03 11:09:17 +11:00
ripplebiz
920ac51c8c Merge pull request #998 from tahnok/bmp085-sensor
Add support for bmp085/bmp180 temperature/pressure sensor
2025-11-03 10:58:22 +11:00
Liam Cottle
0b9f055860 Merge pull request #1047 from aqua/build-name-fix
Fix the sample RAK repeater build target name
2025-11-01 19:19:49 +13:00
Devin Carraway
d0caa3be04 Fix the sample RAK repeater build target name
The actual target doesn't capitalize the 'r' in repeater.
2025-10-31 22:11:24 -07:00
ViezeVingertjes
ff4fa7be31 Add ESP32-S3-Zero board configuration and Nibble Screen Connect variant 2025-10-31 14:42:16 +01:00
Adam Mealings
c13b4ae481 Analogue button delay based on millis 2025-10-31 13:04:59 +00:00
Scott Powell
7755400a35 * Companion: Now using transport codes { 0, 0 } when Share contact zero hop.
* Repeater: onAdvertRecv(), adverts via Share now NOT added to neighbours table
2025-10-31 20:40:22 +11:00
ripplebiz
ef752926c9 Merge pull request #1036 from oltaco/datastore-refactor
Refactor DataStore to use openRead() and openWrite()
2025-10-31 17:10:03 +11:00
ripplebiz
228b073006 Merge pull request #982 from ViezeVingertjes/feat/wio-wm1110-variant
Add Seeed Wio WM1110 Dev Board variant
2025-10-31 17:02:47 +11:00
ripplebiz
7ad45d113c Merge pull request #993 from recrof/allow_lower_bw_sf
allow saving spreading factor from 5 and bandwidth from 7.8kHz
2025-10-31 16:58:35 +11:00
Scott Powell
7abe6c9693 * Upping max channel hash conflicts to 4 (was 2) 2025-10-31 16:54:58 +11:00
taco
52a3df4977 revert pubBlobByKey() change 2025-10-31 15:06:29 +11:00
taco
0b8159c6e5 refactor DataStore to use openRead() and openWrite()
refactored loadPrefsInt(), loadContacts(), loadChannels(), getBlobByKey() and putBlobByKey() to use openRead() and openWrite()
2025-10-31 13:17:22 +11:00
ViezeVingertjes
5088444f85 Update Wio WM1110 configuration to disable GPS and clean up location provider code 2025-10-30 16:33:02 +01:00
liquidraver
07e58d8ab5 Merge branch 'dev' into devt114 2025-10-30 10:10:06 +01:00
Scott Powell
96e786fa9e * FIX: for divide by zero crash 2025-10-30 19:11:04 +11:00
liquidraver
f3b20d5e70 t114 gps 2025-10-30 08:35:01 +01:00
Scott Powell
3d9378d91e * Fix for VolatileRTCClock wrapping around to initial synced time every 49 days 2025-10-30 16:45:50 +11:00
ripplebiz
c4e99a841a Merge pull request #1023 from WattleFoxxo/dev
Update xiao rp2040 to use new radio standard init
2025-10-30 12:43:20 +11:00
Scott Powell
80f0405600 * direct.txdelay default now 0.2 (was zero) 2025-10-30 00:03:10 +11:00
Scott Powell
886878c70a Merge commit 'cc002404fa89a2b0139a1394f78b4a72988846f8' into dev 2025-10-29 23:36:07 +11:00
Scott Powell
8cbcd2271d * experimental: retransmit delay, removing the 6 'slots' 2025-10-29 23:35:46 +11:00
ripplebiz
cc002404fa Merge pull request #1026 from recrof/disable_esp32c6
esp32c6: disable releases because of issues with pioarduino(arduino 3.0)
2025-10-29 23:29:06 +11:00
ripplebiz
ac37a37b18 Merge pull request #1025 from recrof/disable_vision_master
heltec vision master: remove boards from build process
2025-10-29 23:28:26 +11:00
recrof
4aef696620 missed one definition 2025-10-29 13:27:26 +01:00
recrof
377f9ff67d renamed esp32c6 variants, so they are not included in release. added disclaimer about pioarduino builds 2025-10-29 13:22:11 +01:00
recrof
1c052d8ad2 use different strategy in renaming the envs in order to avoid building 2025-10-29 13:14:27 +01:00
recrof
1bbc2151f1 remove vision master boards because of issues with display drivers 2025-10-29 10:32:39 +01:00
fdlamotte
1d2a115b26 Merge pull request #900 from michaelhart/dev
Add stats to serial CLI
2025-10-29 08:50:17 +01:00
Michael Hart
81ab944682 Adds serial commands to get stats
- Added formatStatsReply, formatRadioStatsReply, and formatPacketStatsReply methods in MyMesh for both simple_repeater, simple_room_server, and simple_sensor.
- Updated CommonCLI to handle new stats commands.
2025-10-28 23:55:49 -07:00
WattleFoxxo
d4eb04d6e9 Switch xiao rp2040 to std init 2025-10-29 15:20:31 +11:00
Matthias Wientapper
f339c74bb4 * Add #ifdef, reuse variable 2025-10-27 17:58:29 +01:00
ripplebiz
cb4468bd5d Merge pull request #977 from tpp-at-idx/thinknode_m2
Support for Elecrow Thinknode M2
2025-10-27 13:31:09 +11:00
ripplebiz
9aa11a87ab Merge pull request #1000 from kallanreed/enable_wismesh_tag_gps
Add PIN_GPS_EN build flag for wismesh tag companion
2025-10-26 15:36:48 +11:00
ripplebiz
a2f5432818 Merge pull request #1018 from Woodie-07/dev
LR1110 IRQ fixes
2025-10-26 14:54:27 +11:00
Woodie-07
0e259a63ed lr1110 irq fixes
fix incorrect irqs used in isReceiving. also remove getTimeOnAir override as fixed upstream
2025-10-25 22:12:30 +01:00
fdlamotte
6d6db10ac5 Merge pull request #1012 from Woodie-07/dev
New workaround for LR1110 shift issue
2025-10-25 09:14:11 +02:00
Woodie-07
2981fc70e1 new workaround 2025-10-24 20:12:02 +01:00
ripplebiz
61cd01db27 Merge pull request #1003 from liamcottle/feature/increased-max-uptime
Increase max uptime stats from 49 days to 136 years
2025-10-23 23:30:10 +11:00
Scott Powell
63c3342f7d Merge commit '6288a5d11a8786c9883d6ce0c8a404430150a664' into dev 2025-10-23 21:49:11 +11:00
Scott Powell
dfb4497c7a * T114: enabled GPS page in UITask 2025-10-23 21:44:52 +11:00
liamcottle
273a54f104 increase room server max uptime from 49 days to 136 years 2025-10-23 23:29:08 +13:00
liamcottle
f1824e68b9 increase repeater max uptime from 49 days to 136 years 2025-10-23 23:24:40 +13:00
Liam Cottle
6288a5d11a Merge pull request #1002 from wel97459/dev-CayenneLPP
Updated CayenneLPP to 1.6.1
2025-10-23 20:24:49 +13:00
Winston Lowe
2e249e24dc Updated CayenneLPP to 1.6.1 2025-10-22 23:55:51 -07:00
kallanreed
8ca3ed28cf set PIN_GPS_EN in wismesh tag companion 2025-10-22 16:16:43 -07:00
Wesley Ellis
4cfbd3bad5 Switch BMP085 mode to 0 for ULTRALOWPOWER 2025-10-22 16:53:11 -04:00
Wesley Ellis
ac15131296 Add support for bmp085/bmp180 temperature/pressure sensor 2025-10-22 16:17:06 -04:00
Matthias Wientapper
a38418e09a * Add display of IP address to companion screen 2025-10-22 20:01:15 +02:00
recrof
87677fda76 allow spreading factor from 5 and bandwidth from 7.8kHz 2025-10-22 15:15:29 +02:00
liquidraver
0920dc6663 Fix reversed GPS PINs on G2 and enable timesync 2025-10-21 12:23:45 +02:00
ripplebiz
f27e8ba6b2 Merge pull request #981 from oltaco/revert-heltecT114-powersaving
Revert Heltec T114 power savings
2025-10-21 17:20:02 +11:00
ViezeVingertjes
ec05d40b3c Add Seeed Wio WM1110 Dev Board variant 2025-10-20 21:40:59 +02:00
taco
5d495d505a Revert Heltec T114 power savings
As discussed on discord with @recrof people are having issues, possibly due to these changes. See https://github.com/meshcore-dev/MeshCore/issues/746

This reverts commit a16e011bd2.
2025-10-21 00:34:57 +11:00
ripplebiz
4687ab74e3 Merge pull request #973 from recrof/rak_platform_update
equalize RAK with all other nrf52 variants and use newer platform with all important fixes
2025-10-20 20:07:49 +11:00
ripplebiz
292305c5e1 Merge pull request #972 from recrof/Adafruit_nRF52_Arduino-1.7.0
all nrf52 devices: force framework-arduinoadafruitnrf52 version to 1.10700.0
2025-10-20 20:04:36 +11:00
Tomas P
31b8f7252a Support for Elecrow Thinknode M2 2025-10-19 20:44:27 +02:00
ripplebiz
99e44f499e Merge pull request #968 from fdlamotte/uitask_back3_disables_buzzer
uitask: bring back buzzer toggle on tracker l1
2025-10-19 14:34:42 +11:00
ripplebiz
dab44a1bb0 Merge pull request #967 from fdlamotte/remove_target_h_dep
CommonCli: Remove dependency on target.h
2025-10-19 14:25:20 +11:00
ripplebiz
53a2ae97ea Merge pull request #923 from recrof/tlora_1.6_cleanup
TLora V2.1 1.6 cleanup
2025-10-19 14:15:55 +11:00
ripplebiz
798725d450 Merge pull request #922 from recrof/station_g2_cleanup
station g2 cleanup
2025-10-19 14:11:20 +11:00
ripplebiz
a222578041 Merge pull request #921 from recrof/heltec_v2_cleanup
heltec v2 cleanup
2025-10-19 14:07:16 +11:00
ripplebiz
ebf4599c92 Merge pull request #920 from recrof/heltec_v3_cleanup
heltec v3 cleanup
2025-10-19 14:01:50 +11:00
Liam Cottle
79d0989702 Merge pull request #962 from haxwithaxe/dev
Added more polished build.sh usage
2025-10-19 12:10:58 +13:00
recrof
a5070077ba equalize RAK with all other nrf52 variants and use newer platform with all important fixes 2025-10-19 00:02:38 +02:00
recrof
a421215e84 all nrf52 devices: force framework-arduinoadafruitnrf52 version to 1.10700.0 2025-10-18 23:42:28 +02:00
Florent
37dc715a8e SensorManager: remove setSettingByKey 2025-10-18 23:37:58 +02:00
Florent
ce70792309 lgfx_display: better handle display class construction 2025-10-18 14:03:27 +02:00
Florent
7d62a27836 uitask: bring back buzzer toggle on tracker l1 2025-10-18 13:40:01 +02:00
Florent
f085a9d6c5 tracker_l1_eink: set UI_HAS_JOYSTICK 2025-10-18 13:11:18 +02:00
Florent
3210475f35 CommonCli: Remove dependency on target.h 2025-10-18 12:33:43 +02:00
ripplebiz
666447eafc Merge pull request #955 from liquidraver/dev
Add simple BME680 support to RAK (RAK1906)
2025-10-18 15:06:05 +11:00
haxwithaxe
006af52776 Added more polished build.sh usage 2025-10-17 14:20:55 -04:00
fdlamotte
ece40716da Merge pull request #956 from recrof/uf2_pio_task
added custom pio task "Create UF2 file"
2025-10-17 17:24:32 +02:00
recrof
24ed5b377f added custom pio task "Create UF2 file" 2025-10-17 16:25:58 +02:00
fdlamotte
15ecf186fa Merge pull request #953 from Woodie-07/dev
LR1110 packet shift issue workaround
2025-10-17 15:26:09 +02:00
Woodie-07
02351abc2d change println to debug macro in lr1110 patch 2025-10-16 16:25:18 +01:00
liquidraver
3c48f01601 BME680 library doesn't have altitude calculation, we can add it here to match other sensors' 2025-10-16 11:29:22 +02:00
liquidraver
0e7486552d Add simple BME680 support to RAK with adafruit library 2025-10-16 10:17:23 +02:00
Scott Powell
cd920693ec * UITask: new UI_HAS_JOYSTICK
* MomentaryButton: new constructor 'multiclick' param
* WIoTrackerL1: now just use joystick, joystick press for KEY_ENTER, no multi-click for snappier UI
2025-10-16 17:33:22 +11:00
Scott Powell
d3be6afccb * fix for non-RAK targets 2025-10-15 22:51:05 +11:00
Scott Powell
fa8c31be88 * fix for RAK12500 GPS (I2C) 2025-10-15 22:47:55 +11:00
ripplebiz
34b9a1c9dc Merge pull request #916 from Quency-D/dev-heltec_tracker_v2
add heltec tracker v2
2025-10-14 12:45:15 +11:00
ripplebiz
ca5dcf22dd Merge pull request #911 from bplein/ikoka-nano-support
Ikoka Nano Variant
2025-10-14 12:39:35 +11:00
ripplebiz
86ecf97d33 Merge pull request #912 from recrof/patch-1
heltec wireless tracker: use `-D ARDUINO_USB_CDC_ON_BOOT=1` with all envs
2025-10-14 12:35:11 +11:00
Scott Powell
c6b4a58449 * repeater and room server: enable downgrading permissions on guest login 2025-10-14 12:31:43 +11:00
fdlamotte
633538d9c7 Merge pull request #918 from recrof/xiao_c3_refresh
xiao c3 cleanup
2025-10-12 19:10:47 +02:00
recrof
c6e5d5021e fix: remove VL53L0X because it causes bootloops on esp32c3 2025-10-12 17:16:45 +02:00
Woodie-07
8426fddcb7 workaround for LR1110 shift issue
it seems that if the LR1110 radio hears a packet corrupted in a specific way, it'll report a packet of 0 length and with the header error IRQ set. every packet received afterwards will then be shifted to the right by 4 bytes on top of the radio's reported offset. this can occur multiple times with the shift increasing by 4 bytes each time. thus, this patch will read from an additional offset after hearing the trigger packet.
transmitting seems to reset the shift - unsure exactly what operation resets it but standby() is called after tx so patch assumes shift is 0 after standby(). more investigation may be needed here.
2025-10-12 16:09:57 +01:00
Scott Powell
93c0180740 * Refactor: advert_loc_policy now applied in new method CommonCLI::buildAdvertData() 2025-10-12 12:49:26 +11:00
Scott Powell
837e7dcbdb * Advert type fix
* GPS pref defaults tidy
2025-10-12 12:33:20 +11:00
ripplebiz
487b7c6576 Merge pull request #890 from fdlamotte/CommonCLI--gps-management
CommonCLI: gps management commands
2025-10-12 12:21:47 +11:00
recrof
69cddbca4e move LilyGoTLoraBoard.h to variants, use template in platformio.ini, cleanup 2025-10-12 00:32:26 +02:00
recrof
7cb2e0863a move StationG2Board.h to variants, enable ESM, add companion wifi, cleanup 2025-10-12 00:13:34 +02:00
recrof
1979517381 heltec v2 cleanup 2025-10-11 23:35:10 +02:00
recrof
c4a2b13930 moved HeltecV3Board.h to variant folder 2025-10-11 21:52:48 +02:00
Florent
bf1da43d7d gps_cli: gps advert to control advert location policy 2025-10-11 19:00:02 +02:00
recrof
4dc3dda2d8 xiao c3: migrated to esm, added missing roles, cleanup 2025-10-11 18:32:02 +02:00
Florent
f6064b41e9 gps_cli: set node location based on gps 2025-10-11 18:00:57 +02:00
Florent
76dcfbb23a gpsCli: use parseTextParts 2025-10-11 15:29:17 +02:00
Quency-D
ad2894a039 delete PSRAM. 2025-10-11 18:03:15 +08:00
Quency-D
70ac820594 add heltec tracker v2 board. 2025-10-11 18:01:26 +08:00
Rastislav Vysoky
8a2e4721d1 heltec wireless tracker: use -D ARDUINO_USB_CDC_ON_BOOT=1 with all envs
repeater and room server envs did not have arduino cdc flag enabled which resulted in broken serial.
2025-10-10 16:01:48 +02:00
ripplebiz
da52d08168 Merge pull request #898 from syssi/wsl3-wifi-build
Introduce Heltec_WSL3_companion_radio_wifi target
2025-10-10 13:30:55 +11:00
ripplebiz
b47ace5d10 Merge pull request #908 from ViezeVingertjes/feat/meshpocket-poweroff
Add hibernation support for MeshPocket
2025-10-10 11:38:07 +11:00
Bill Plein
b588e3f1e3 Ikoka Nano Variant
Created as a fork of the ikoka stick variant.

- Updated for Ikoka Nano legacy pinout
- Removed display support
- Removed user button support
- Retains I2C sensor support

Tested with the ebytes E22 30W module, companion-ble and repeater firmware versions, with an I2C INA3221 power sensor.
2025-10-09 17:31:32 -05:00
ViezeVingertjes
da7b8ad669 Add powerOff support for MeshPocket 2025-10-09 20:30:25 +02:00
ripplebiz
27e5f6e7df Merge pull request #885 from ViezeVingertjes/fix/heltec-v4-max-tx-power
Add MAX_LORA_TX_POWER build flag for Heltec V4 configuration
2025-10-09 12:39:21 +11:00
Sebastian Muszynski
601479e572 Introduce Heltec_WSL3_companion_radio_wifi target 2025-10-07 11:17:19 +02:00
Florent de Lamotte
9e3c2fc9d9 gps_cli: gps also restored on sensors and rooms 2025-10-06 15:30:18 +02:00
Florent de Lamotte
6ed8e9d514 gps_cli: gps state is now saved and restored upon reboot 2025-10-06 15:12:03 +02:00
Florent de Lamotte
c9fd1827ef Merge branch 'dev' into CommonCLI--gps-management 2025-10-06 14:26:05 +02:00
fdlamotte
5f31979e1e Merge pull request #831 from Meshcore-Portugal/jbrazio/2025_87fe0ad8
Add bridge management CLI
2025-10-06 14:23:35 +02:00
Florent de Lamotte
341b69e3c9 sensor list command 2025-10-06 14:08:16 +02:00
João Brázio
13a0202062 Add BRIDGE_DEBUG flag 2025-10-06 12:57:32 +01:00
João Brázio
fb46e5cc8a Refactor debug logging across bridge implementations 2025-10-06 12:57:04 +01:00
Florent de Lamotte
7be65c148e cli_gps: remove callbacks and add generic sensor set/get. 2025-10-06 10:25:10 +02:00
Florent
e4f2d63b0a cli_gps: use sensormanger to toggle gps on/off to keep state coherent 2025-10-05 20:31:25 +02:00
Florent
0502bc370d CommonCLI: gps management commands 2025-10-05 19:23:52 +02:00
Florent
45ab0e8cf7 sensecap_indicator: initial espnow support 2025-10-05 13:58:25 +02:00
João Brázio
9b4d93d112 Add bridge type command to CLI for reporting bridge configuration 2025-10-05 11:48:05 +01:00
ripplebiz
5ae574b426 Merge pull request #878 from WattleFoxxo/tdeck
Scaling fixes for TDeck
2025-10-05 15:18:51 +11:00
ViezeVingertjes
c568edc8d0 Add MAX_LORA_TX_POWER build flag for Heltec V4 configuration 2025-10-04 23:04:47 +02:00
WattleFoxxo
69e6d69798 Fix font and icon scaling issues for TDeck 2025-10-03 22:55:32 +10:00
ripplebiz
54675ed1b2 Merge pull request #812 from khudson/source-cleanup
Ikoka Stick: Naming Convention, LED behavior, device strings
2025-10-03 14:04:00 +10:00
João Brázio
e48f3a58ae Remove WITH_ESPNOW_BRIDGE_SECRET definition from platformio.ini files and update documentation to use _prefs->bridge_secret 2025-10-03 00:23:09 +01:00
João Brázio
8edcb46a28 Bridge: enhance CLI configuration options 2025-10-03 00:20:09 +01:00
Florent
262e9864e7 stm32: upd repeater targets 2025-10-02 12:18:47 +02:00
ripplebiz
3912bbdf7d Merge pull request #821 from fdlamotte/ui_gps_page
ui_task: initial gps page
2025-10-02 18:54:23 +10:00
João Brázio
aa946bbe36 WITH_BRIDGE was not implementing setBridgeState() 2025-10-02 09:47:00 +01:00
Scott Powell
8d8b9a6141 * ver 1.9.1 2025-10-02 12:52:19 +10:00
Florent de Lamotte
18bfc2d81a DisplayDriver: introduce drawTextRightAlign and drawTextLeftAlign 2025-10-01 18:07:59 +02:00
ripplebiz
219297172a Merge pull request #863 from liamcottle/fix/repeater-login
Fix: repeater login
2025-09-29 21:38:54 +10:00
liamcottle
6a1f8d65c9 add missing null terminator for login payload 2025-09-30 00:31:10 +13:00
ripplebiz
b82f5ea7cd Merge pull request #859 from liamcottle/feature/board-cli-command
Add CLI Command: board
2025-09-29 13:28:21 +10:00
liamcottle
ec48e6acfc added 'board' cli command to get board name 2025-09-29 15:24:25 +13:00
Liam Cottle
e381f03bc2 Merge pull request #854 from liamcottle/fix/missing-build-flags
Fix: add missing build flags
2025-09-29 13:49:57 +13:00
Liam Cottle
8ac6dcb644 Merge pull request #858 from csrutil/fix/tiny-relay-compile
Fix: add missing build flags for tiny_relay
2025-09-29 13:49:31 +13:00
csrutil
fc0cf5f370 🔧 chore: update tiny_relay platformio.ini configuration
- Add ADVERT_LAT and ADVERT_LON definitions for both repeater and sensor variants
- Set MAX_NEIGHBOURS to 50 for improved network capacity
- Fix repeater build source filter path to include entire directory
2025-09-29 07:48:26 +08:00
liamcottle
914001344f add missing build flags for failed builds 2025-09-28 23:32:04 +13:00
Scott Powell
b92d9bd972 * ver 1.9.0 2025-09-28 19:24:00 +10:00
Scott Powell
3335b49d9f Merge branch 'main' into dev
# Conflicts:
#	variants/heltec_vision_master_e290/platformio.ini
2025-09-28 19:21:48 +10:00
ripplebiz
e5de6e6600 Merge pull request #820 from fdlamotte/gps_reset_fix
ESM: delegate gps management to LocationProvider
2025-09-28 19:08:35 +10:00
ripplebiz
cd7e7d9bbe Merge pull request #852 from liamcottle/increase-max-neighbours
Increase MAX_NEIGHBOURS to 50
2025-09-28 18:32:10 +10:00
ripplebiz
4bb16ef5a7 Merge pull request #850 from liamcottle/fix/legacy-neighbours-cli
Backwards Compatibility: Legacy Neighbours CLI
2025-09-28 18:30:11 +10:00
Liam Cottle
70ec996c08 Merge pull request #853 from liamcottle/fix-calc-shared-secret
fix multiple candidates warning
2025-09-28 21:05:03 +13:00
liamcottle
3f4f9eff17 fix multiple candidates warning 2025-09-28 21:01:41 +13:00
Florent
db7635102d gps_page: enable if gps enabled 2025-09-28 09:43:28 +02:00
fdlamotte
0767fc49e5 Merge pull request #843 from dotdavid/main
Create Xiao_S3_WIO_companion_radio_usb profile
2025-09-28 09:24:23 +02:00
fdlamotte
c83abbeff6 ESM: add gps reset after begin 2025-09-28 09:20:59 +02:00
fdlamotte
030f0d5d82 location provider: reduce reset delay 2025-09-28 09:16:45 +02:00
liamcottle
0307b6119e increase MAX_NEIGHBOURS from 8 to 50 2025-09-28 16:11:58 +13:00
Liam Cottle
2e92137d10 Merge pull request #851 from liamcottle/build-script-suffix
build script should check for firmware type suffix
2025-09-28 15:04:22 +13:00
liamcottle
58ed14d971 build script should check for firmware type suffix 2025-09-28 15:00:45 +13:00
liamcottle
f8f5f00549 admin cli neighbors command should sort newest to oldest 2025-09-28 14:38:13 +13:00
Liam Cottle
f9b2613e57 Merge pull request #844 from liamcottle/refactor-variant-suffixes
refactor variants to use standard firmware type suffixes
2025-09-28 14:20:04 +13:00
liamcottle
f3b9c06646 refactor variants to use standard firmware type suffixes 2025-09-27 23:45:33 +12:00
Liam Cottle
2992062bbe Merge pull request #770 from Meshcore-Portugal/jbrazio/2025_44aa3add
Normalize repeater target names
2025-09-27 23:15:23 +12:00
ripplebiz
0beaa323ed Merge pull request #842 from liamcottle/feature/custom-build-flags
Build Script Improvements
2025-09-27 20:32:20 +10:00
David Hall
cc822c029b Create Xiao_S3_WIO_companion_radio_usb profile
Create Xiao_S3_WIO_companion_radio_usb profile from Xiao_S3_WIO_companion_radio_ble profile
2025-09-27 11:14:28 +01:00
Scott Powell
95e533d60b * repeater & room server fix for blank guest password 2025-09-27 01:56:27 +10:00
liamcottle
e49eef5588 allow building multiple specific targets at same time 2025-09-26 22:59:20 +12:00
liamcottle
3fbdaf7ce6 don't overwrite existing platformio build flags in build script 2025-09-26 22:46:38 +12:00
ripplebiz
7bcf1f1b47 Merge pull request #828 from recrof/meshadventurer-build-fix
fixed meshadventurer failing build
2025-09-26 20:05:34 +10:00
fdlamotte
84feb63ed5 Merge pull request #835 from oltaco/wio-L1-revert-pins
fix: revert ENV_PIN_SDA for Wio Tracker L1 non-eink
2025-09-26 07:19:40 +02:00
taco
a3e6b79c2f Revert addition of ENV_PIN_SDA 2025-09-25 20:08:18 +10:00
ripplebiz
74e1b6c75b Merge pull request #833 from liamcottle/feature/binary-neighbours-request
Implement binary request/response for Repeater Neighbours
2025-09-25 14:04:12 +10:00
liamcottle
418ae08b4d add FIRMWARE_VER_LEVEL to companion PUSH_CODE_LOGIN_SUCCESS 2025-09-25 15:21:58 +12:00
liamcottle
b8394a4e62 use pointer array 2025-09-25 14:55:36 +12:00
liamcottle
1c7a0ce2bd use uint16_t to allow fetching up to 65535 neighbours 2025-09-25 14:55:36 +12:00
liamcottle
02c178dae7 implement new binary request/response for paginated neighbours 2025-09-25 14:55:36 +12:00
Scott Powell
a5af1b5bcd * companion: disabled processing/sending of keep_alive packets (deprecated)
* FIRMWARE_VER_LEVEL now moved to end of response payloads
2025-09-25 09:39:11 +10:00
Scott Powell
e988531f6a Merge commit '3bc8ec2006917670695b3a74e7bb7df2c764e9e5' into dev 2025-09-25 09:14:10 +10:00
Scott Powell
76be66313e * repeater: reduce FS writes on login 2025-09-25 09:11:48 +10:00
Scott Powell
c21596341a * Login response payload: now includes FIRMWARE_VER_LEVEL 2025-09-25 09:07:59 +10:00
fdlamotte
3bc8ec2006 Merge pull request #830 from SoulOfNoob/feat/add_t-echo-lite_variant
Feat: add `T-Echo-Lite` Variant to MeshCore
2025-09-24 20:59:58 +02:00
João Brázio
2297d24013 Minor fixes 2025-09-24 16:46:03 +01:00
João Brázio
1d45c7ec66 Add bridge management CLI 2025-09-24 16:30:00 +01:00
Jan Ryklikas
088b8fd98c fix: revert to orignal default scaling and fix it in variant config 2025-09-24 15:10:51 +02:00
Jan Ryklikas
128119fe40 refactor: remove redundant import statement 2025-09-24 14:45:40 +02:00
recrof
f2cff53b0e fixed meshadventurer failing build 2025-09-24 09:04:16 +02:00
Jan Ryklikas
20b0fd1dc9 refactor: readability 2025-09-23 22:28:54 +02:00
Jan Ryklikas
f85db18242 refactor: use macro from ttgo repo for readability 2025-09-23 22:18:04 +02:00
Jan Ryklikas
955b7321e8 chore: cleanup 2025-09-23 22:10:27 +02:00
Jan Ryklikas
e2fa70d6f3 chore: refactor to use GxEPD2 fork 2025-09-23 21:57:35 +02:00
Jan Ryklikas
b11f08422e add T-Echo-Lite Device Variant 2025-09-23 19:39:11 +02:00
Jan Ryklikas
db40a9cea6 import missing eInk display 2025-09-23 19:38:45 +02:00
Florent
76aa7cf488 ui_task: initial gps page 2025-09-23 12:08:34 +02:00
Florent
c1915a1133 ESM: delegate gps management to LocationProvider 2025-09-23 11:12:07 +02:00
ripplebiz
ea13fa899e Merge pull request #814 from WattleFoxxo/tdeck
LilyGo TDeck support
2025-09-23 16:00:10 +10:00
ripplebiz
4aa58ade8a Merge pull request #811 from fdlamotte/tracker_l1_environment_sensors
Tracker l1: environment sensors
2025-09-23 15:34:22 +10:00
ripplebiz
3885d47ec9 Merge pull request #818 from silverphish-io/faq-spellcheck
Updated some typos and spelling mistakes in FAQ
2025-09-23 15:15:24 +10:00
silverphish-io
adecd1e58d Updated some typos and spelling mistakes in FAQ 2025-09-22 21:49:56 +01:00
Florent
611d61b6c6 tracker_l1: fix bme226 init in ESM to include all sensors 2025-09-22 19:10:01 +02:00
WattleFoxxo
f100894882 LillyGo TDeck support 2025-09-22 23:48:46 +10:00
ripplebiz
4579a1bcaf Merge pull request #813 from Quency-D/dev-heltec_v4
add heltec v4 board.
2025-09-22 22:32:01 +10:00
Quency-D
669bea04a0 add heltec_v4 board. 2025-09-22 19:58:27 +08:00
Quency-D
881396eeaf Merge pull request #10 from meshcore-dev/dev
merge dev
2025-09-22 19:49:49 +08:00
Florent
0cb34740d2 tracker-l1: correct bad definition for PIN_GPS_EN 2025-09-22 12:06:05 +02:00
Florent
c9b060aefb Merge branch 'dev' into tracker_l1_environment_sensors 2025-09-22 07:30:42 +02:00
ripplebiz
d85d364431 Merge pull request #808 from fdlamotte/wio-l1-eink
wio-l1-eink initial support
2025-09-22 15:07:22 +10:00
Scott Powell
52d5cc6068 * tidy and minor fix for offline queue deletion 2025-09-22 15:01:28 +10:00
ripplebiz
28d673ee15 Merge pull request #796 from 446564/mutable-queue
make offline queue channel messages mutable
2025-09-22 14:54:09 +10:00
kelsey hudson
9e460560bf Merge branch 'source-cleanup' of github.com:khudson/MeshCore into source-cleanup 2025-09-21 14:14:28 -07:00
kelsey hudson
9d009074da Ikoka Stick: Move to unified code naming conventions 2025-09-21 14:12:15 -07:00
Florent
f9543bb7bb tracker_l1: support for EnvironmentSensorManager 2025-09-21 22:14:22 +02:00
kelsey hudson
7b3a0bba97 Merge remote-tracking branch 'upstream/dev' into dev 2025-09-21 08:28:16 -07:00
Florent
59ea6cdb89 wio-l1-eink initial support 2025-09-20 21:45:13 +02:00
ripplebiz
695473f842 Merge pull request #805 from csrutil/tiny-relay-variant
Tiny relay variant
2025-09-21 00:03:58 +10:00
ripplebiz
4daad75f7d Merge pull request #806 from oltaco/safer-lfs-traverse
Safer _countLfsBlock / _getLfsUsedBlockCount
2025-09-21 00:00:19 +10:00
taco
2922b62888 add bounds check to _countLfsBlock / _getLfsUsedBlockCount 2025-09-20 17:36:52 +10:00
Florent
757ff9fb55 stm32: force the use of Adafruit BusIO v1.17.2 as 1.17.3 won't compile on this platform 2025-09-20 08:54:30 +02:00
csrutil
a1622bad75 🔗 fix: update tiny_relay board URL to proper STM32WLE5CC documentation link 2025-09-20 14:07:22 +08:00
csrutil
b3af4d9c72 feat: add tiny_relay board configuration
Add board configuration for BB-STM32WL tiny relay variant with STM32WLE5CC MCU support including debug and upload protocols.
2025-09-20 10:59:36 +08:00
csrutil
736118fe6b Add tiny_relay variant files
- platformio.ini: Build configuration for tiny relay variant
- target.cpp/h: Hardware-specific implementation
- variant.h: Variant identification header
2025-09-20 10:58:02 +08:00
ripplebiz
b464f5c640 Merge pull request #801 from recrof/sensecap_solar_env_manager
use sensor_base for seeed sensecap solar
2025-09-19 13:47:26 +10:00
recrof
985b290d02 use sensor_base for seeed sensecap solar 2025-09-18 09:15:01 +02:00
Scott Powell
384b02bec4 * GenericVibration: code style refactor 2025-09-18 13:19:54 +10:00
ripplebiz
b3e9fd76ce Merge pull request #708 from csrutil/feature/vibration-feedback
 feat: add vibration feedback system
2025-09-18 13:12:36 +10:00
ripplebiz
f77fd15707 Merge pull request #730 from michaelhart/node-displayname-improvements
Adds name filtering and text truncation for display in HomeScreen
2025-09-18 12:42:10 +10:00
ripplebiz
e35e4bb23e Merge pull request #745 from ViezeVingertjes/fix-pin-display
Fix: Set device as connected after successful authentication
2025-09-18 12:02:08 +10:00
ripplebiz
8ddabfcffa Merge pull request #783 from sschueller/eora-s3
feat: Added EByte EoRa-S3-XXXTB Support #740
2025-09-18 11:13:40 +10:00
Scott Powell
9ba8d6f23f Merge branch 'rep-room-acl' into dev 2025-09-17 17:25:26 +10:00
csrutil
6f8ce425d8 remove the unnecessary blank line 2025-09-17 09:19:18 +08:00
csrutil
043f37a08e ♻️ refactor: unify UI notification methods into single notify() function
Consolidates soundBuzzer() and triggerVibration() into a unified notify() method
that handles both audio and haptic feedback based on UIEventType.
2025-09-17 08:56:18 +08:00
csrutil
2da50882c0 feat: add vibration feedback support for UI events
- Add genericVibration class with 5-second cooldown and 1-second pulse
- Integrate vibration triggers for new messages and contact discoveries
- Add conditional compilation support with PIN_VIBRATION guard
- Implement abstract interface for vibration in UITask system
2025-09-17 08:56:18 +08:00
Michael Hart
bd6aa983a3 feat: add DisplayDriver methods for UTF-8 filtering and text ellipsis
- Add translateUTF8ToBlocks() method to convert UTF-8 characters to displayable blocks
- Add drawTextEllipsized() method for text truncation with ellipsis
- Apply UTF-8 filtering to node names, recent contacts, and message content
- Use ellipsized text rendering for recent contacts to prevent overflow
- Addresses PR feedback by moving text processing to DisplayDriver level
2025-09-16 17:17:15 -07:00
446564
fca16f1b71 make offline queue channel messages mutable
older channel messages can be overwritten, keeping other mssagage types

this allows a user to be away for a long time and still get the most recent
channel messages without losing any direct messages for exampe
2025-09-16 15:40:21 -07:00
Liam Cottle
47c57a52cc Merge pull request #795 from tahnok/python3-build-sh
Use python3 not python in build.sh
2025-09-17 10:26:09 +12:00
Wesley Ellis
19fb7aae63 Use python3 not python in build.sh
Since the bin/uf2conv/uf2conf.py script uses python3, use python3 as the command instead of python.
On my ubuntu 24.04 machine, I don't have a python command in my path by default
2025-09-16 18:15:14 -04:00
ripplebiz
d86851b881 Merge pull request #787 from recrof/rak-wishmesh-tag-fix
fix building errors for wismesh tag companion ble
2025-09-16 13:09:30 +10:00
Scott Powell
98b524bfcf Merge branch 'dev' into rep-room-acl 2025-09-16 13:07:14 +10:00
Scott Powell
a288ac06a6 Merge branch 'dev' into reciprocal-path-retry 2025-09-16 13:03:41 +10:00
fdlamotte
88786a906f Merge pull request #786 from recrof/xiao-nrf52-cleanup
tidy up xiao nrf52 variant
2025-09-15 15:45:38 +02:00
recrof
845a497604 fix compilation errors for wismesh tag 2025-09-15 14:56:04 +02:00
recrof
81180bbf8c xiao nrf52: add all available sensors, remove *_alt envs, cleanup 2025-09-15 14:46:10 +02:00
ripplebiz
f9428b7d27 Merge pull request #785 from liamcottle/feature/new-message-timestamps
Update lastmod when a new message is received
2025-09-15 19:34:26 +10:00
Scott Powell
fa3e4f9715 Merge branch 'dev' into reciprocal-path-retry 2025-09-15 18:34:39 +10:00
ripplebiz
d377ffd393 Merge pull request #784 from liamcottle/fix/ble-advertising-interval
revert unexpected change to ble advertising interval on nrf52
2025-09-15 11:42:48 +10:00
liamcottle
400e09f318 revert unexpected change to ble advertising interval on nrf52 2025-09-15 13:06:35 +12:00
liamcottle
561dbea30f update lastmod when a new message is received 2025-09-15 12:28:26 +12:00
Kelsey Hudson
2536fa6bcf Merge remote-tracking branch 'upstream/dev' into dev 2025-09-14 14:51:05 -07:00
Stefan Schüller
ded81780a4 fix: removed display reset (NC), set SDA and SCL for display 2025-09-14 13:53:45 +02:00
Stefan Schüller
21ea63bcd9 feat: Added EByte EoRa Pi 2025-09-14 13:53:38 +02:00
Scott Powell
5ccacb2a91 * bug fix 2025-09-14 21:51:32 +10:00
Scott Powell
ce08db6238 * room server: ClientACL added 2025-09-14 21:22:12 +10:00
ripplebiz
5377d7cc17 Merge pull request #782 from askpatrickw/patch-1
Update FAQ with new server administration screenshot
2025-09-14 14:13:14 +10:00
ripplebiz
3ef2aa6a95 Merge pull request #776 from liamcottle/fix/nrf52-ble-pin-display
Fix: BLE pin disappearing too quickly on nrf52 devices
2025-09-14 13:23:10 +10:00
Patrick
9b2dbf51cb fix markdown 2025-09-13 14:05:57 -07:00
Patrick
a6a0183d44 Update FAQ with new server administration screenshot 2025-09-13 14:04:31 -07:00
Scott Powell
de2e0cf59c * repeater now using ClientACL class 2025-09-13 19:37:15 +10:00
Scott Powell
c69d78b62e Merge branch 'dev' into reciprocal-path-retry 2025-09-13 18:48:24 +10:00
Scott Powell
9df6e8a6b6 Merge branch 'dev' into rep-room-acl 2025-09-13 18:43:02 +10:00
Liam Cottle
5cd0470879 Merge pull request #777 from bryantkelley/docs/add-ble-firmware-troubleshooting-q-a
[Docs] Add companion not showing up over Bluetooth to FAQ
2025-09-13 14:23:51 +12:00
Bryant Kelley
b5820b1ce0 Add companion not showing up over BLE to FAQ 2025-09-12 11:31:05 -07:00
liamcottle
25ea953cc3 don't mark as connected until connection secured 2025-09-12 20:23:21 +12:00
Scott Powell
281591f147 * refactor: moved ACL out of SensorMesh -> ClientACL 2025-09-12 15:35:31 +10:00
ripplebiz
d929d32569 Merge pull request #768 from 446564/fix/nano-g2-notification
fix nano g2 notification
2025-09-12 12:07:36 +10:00
João Brázio
510472bfa0 Normalize repeater target names 2025-09-10 23:56:07 +01:00
446564
e42ecc3bb3 fix nano g2 notification
revert change to disable buzzer before hibernate

needs more work as the buzzer pin is a macro and can't be changed at runtime
2025-09-10 09:44:58 -07:00
ripplebiz
95d1f052c2 Merge pull request #762 from oltaco/new-ldscript-for-extrafs
New linker scripts for NRF52 companion envs
2025-09-10 18:25:07 +10:00
ripplebiz
ce39df599c Merge pull request #763 from csrutil/fix-environment-sensor-node-altitude
Fix node_altitude not being set in EnvironmentSensorManager
2025-09-10 17:45:06 +10:00
Scott Powell
3b82224db6 Merge branch 'rep-room-acl' into dev 2025-09-10 17:35:05 +10:00
Scott Powell
c8a10cc3b3 * RAK wishmesh tag: build fix 2025-09-10 17:34:06 +10:00
ripplebiz
1257c6b181 Merge pull request #739 from fdlamotte/ui_sensors_page
ui: sensors page
2025-09-10 17:23:34 +10:00
Scott Powell
65ef6c2fd0 * repeater and room server build_src_filter fixes 2025-09-10 17:04:58 +10:00
Liam Cottle
f35e259fd6 Merge pull request #767 from liamcottle/fix/wismeshtag-poweroff-wakeup
Fix: WisMeshTag power off and wake up
2025-09-10 17:44:37 +12:00
liamcottle
80d5e2d8bc fix wismesh tag power off and wake up 2025-09-10 17:04:03 +12:00
Florent de Lamotte
d83cdc501f ui: use LPPDataHelper and conditionals for sensors page 2025-09-09 16:32:41 +02:00
Florent de Lamotte
2d4b77c998 Merge remote-tracking branch 'upstream/dev' into ui_sensors_page 2025-09-09 15:36:14 +02:00
csrutil
cf93109cd5 feat: add altitude support to environment sensor node telemetry
- Include actual node altitude in GPS telemetry instead of hardcoded 0.0f
- Extract altitude data from both ublox_GNSS and serial GPS sources
- Update debug logging to display altitude alongside lat/lon coordinates
2025-09-09 19:20:39 +08:00
Scott Powell
3666cd72e5 * room refactor: extracted MyMesh class 2025-09-09 20:52:19 +10:00
Scott Powell
e35183ae41 Merge branch 'dev' into rep-room-acl
# Conflicts:
#	examples/simple_repeater/main.cpp
2025-09-09 19:02:23 +10:00
Scott Powell
5344f04d89 * Repeater: slight refactor of 'bridge' instantiation 2025-09-09 18:46:30 +10:00
Scott Powell
08f91f8d95 Merge branch 'dev' into rep-room-acl
# Conflicts:
#	examples/simple_repeater/main.cpp
2025-09-09 18:02:05 +10:00
Scott Powell
18d6d54c07 Merge branch 'dev' into reciprocal-path-retry 2025-09-09 17:51:55 +10:00
taco
f92bd0db9e fix inconsistencies across nrf companion roles 2025-09-09 17:00:29 +10:00
taco
e8314c9c8c new ldscript for extrafs nrf companion envs 2025-09-09 16:55:46 +10:00
fdlamotte
ea33f39557 Merge pull request #454 from jbrazio/jbrazio/2025_3f11ad35
RS232/ESP-NOW Bridge/cross repeater implementation
2025-09-09 07:34:31 +02:00
ripplebiz
ecd2e12894 Merge pull request #760 from fschrempf/readme-repeat-clarification
README.md: Explain that companion nodes do not repeat messages
2025-09-09 14:25:37 +10:00
ripplebiz
bb29b66b29 Update README.md 2025-09-09 14:05:07 +10:00
Frieder Schrempf
0dfd2bcbb8 README.md: Explain that companion nodes do not repeat messages
This is a key difference compared to other systems and I see people
asking this a lot. It is mentioned in the FAQ but let's make it more
prominent in the README.
2025-09-08 23:04:32 +02:00
João Brázio
a55fa8d8ec Add BRIDGE_DELAY as a buffer to prevent immediate processing conflicts in the mesh network 2025-09-08 20:21:33 +01:00
João Brázio
1c93c162a1 Add ESPNow bridge configurations for all ESP32 targets 2025-09-08 18:49:33 +01:00
João Brázio
1d25c87c57 Refactor bridge packet handling to use common magic number and size constants 2025-09-08 18:16:50 +01:00
ripplebiz
c44d84ca9b Merge pull request #756 from oltaco/correct-max-contacts-channels
Set correct new MAX_CONTACTS and MAX_GROUP_CHANNELS for some NRF devices
2025-09-08 23:37:43 +10:00
ripplebiz
adaad00b19 Merge pull request #755 from recrof/wismesh_rak_customlfs
rak wismesh: set the `MAX_CONTACTS` and `MAX_GROUP_CHANNELS` in line with earlier CustomLFS changes
2025-09-08 23:37:24 +10:00
taco
a0e7b47e29 correct max contacts and channels for some nrf devices 2025-09-08 22:06:15 +10:00
Scott Powell
f2e8fb0259 * refactor: MyMesh class extracted 2025-09-08 21:46:19 +10:00
recrof
a44b8e626a set the max_contacts and max_group channels in line with other nrf52 targets 2025-09-08 13:26:19 +02:00
Scott Powell
74dea260e5 * proposed change for re-trying reciprocal path transmit 2025-09-08 19:22:59 +10:00
ripplebiz
6a9dedf0b4 Merge pull request #751 from fdlamotte/t1000e_revert_gps_resetb
T1000e revert gps resetb
2025-09-08 15:35:10 +10:00
kelsey hudson
7b08acf56d Ikoka Stick: Move to unified code naming conventions 2025-09-07 21:29:10 -07:00
João Brázio
7fca20475a Merge remote-tracking branch 'upstream/dev' into jbrazio/2025_3f11ad35 2025-09-08 02:04:14 +01:00
João Brázio
0051ccef26 Refactor bridge implementations to inherit from BridgeBase 2025-09-08 02:03:08 +01:00
João Brázio
537449e6af Refactor ESPNowBridge packet handling to use 2-byte magic header and improve packet size validation 2025-09-08 01:20:54 +01:00
João Brázio
04e70829a4 Rename RS232 bridge environments 2025-09-07 21:46:51 +01:00
João Brázio
5b9d11ac8f Support ESPNow and improve documentation 2025-09-07 21:39:54 +01:00
Florent
006605ce1d t1000e: revert GPS_RESETB as an INPUT 2025-09-07 19:48:02 +02:00
fdlamotte
73b49ea14d Merge pull request #736 from ViezeVingertjes/t1000e-low-power
Introduce BLE low-power mode and enable DC/DC converter
2025-09-07 16:01:22 +02:00
ViezeVingertjes
5370667bd8 Replaced BLE_LOW_POWER with BLE_TX_POWER & updated usages. 2025-09-07 15:44:24 +02:00
ViezeVingertjes
7363a4f67d Few adjustments after testing. 2025-09-07 14:08:53 +02:00
fdlamotte
f6f0cfd603 Merge pull request #744 from ViezeVingertjes/fix-t1000e-sleep
T1000-E: ensure rails off and radio idle before system off; fix button wake pin
2025-09-07 12:33:40 +02:00
ripplebiz
b0c7ea45c0 Merge pull request #741 from recrof/rak_wismesh_tag
new variant: RAK WisMesh Tag
2025-09-07 20:28:53 +10:00
ripplebiz
0088509df4 Merge pull request #749 from oltaco/thinknode-m1-companion-usb
Add companion usb to ThinkNode M1
2025-09-07 20:17:38 +10:00
ripplebiz
ea4ed2abec Merge pull request #748 from oltaco/t-echo-qspi-pins
Add QSPI pins for Lilygo T-Echo
2025-09-07 20:14:44 +10:00
ripplebiz
6da6504b80 Merge pull request #747 from oltaco/customlfs-versionbump
CustomLFS version bump
2025-09-07 20:13:31 +10:00
taco
18be92615b add QSPI pins to Lilygo T-Echo 2025-09-07 20:00:44 +10:00
taco
acf6110001 add companion usb to ThinkNode M1 2025-09-07 19:59:01 +10:00
taco
8521b0eb08 new version of CustomLFS lib 2025-09-07 19:54:42 +10:00
kelsey hudson
951d2dfdbb Merge remote-tracking branch 'upstream/dev' into dev 2025-09-06 15:13:58 -07:00
ViezeVingertjes
c10c010736 fix: only hide pin after successful authentication in SerialBLEInterface 2025-09-06 22:06:47 +02:00
ViezeVingertjes
ac8ec172ef T1000-E: refactor GPS initialization; set GPS_RESETB pin as OUTPUT and remove redundant pin settings 2025-09-06 20:42:11 +02:00
ViezeVingertjes
132ca72735 T1000-E: ensure rails off and radio idle before system off; fix button wake pin 2025-09-06 20:10:09 +02:00
ripplebiz
84623938c3 Merge pull request #732 from jbrazio/jbrazio/2025_b5813561
Heltec T114 without display
2025-09-06 22:46:46 +10:00
ripplebiz
1c0154279a Merge pull request #727 from recrof/waveshare_rp2040_lora_refactor
tidy up waveshare rp2040 lora variant
2025-09-06 22:39:42 +10:00
ripplebiz
605210dd07 Merge pull request #726 from recrof/xiao_rp2040_refactor
tidy up xiao rp2040 variant
2025-09-06 22:38:07 +10:00
ripplebiz
5b8c8b0bf6 Merge pull request #653 from oltaco/CustomLFS
Extra filesystem support for NRF52 (CustomLFS)
2025-09-06 17:45:20 +10:00
taco
bcfc8d3771 improved RescueCLI for dual FS 2025-09-06 14:15:40 +10:00
taco
3d83556829 refactor: use _getContactsChannelsFS() instead of ifdefs 2025-09-06 14:15:40 +10:00
taco
accd1e0a97 nrf52 targets: increase limits for contacts and channels 2025-09-06 14:15:40 +10:00
taco
2b24c575c7 support dual filsystems on nrf52
store identity and prefs in UserData and contacts, channels and adv_blobs in ExtraData
2025-09-06 14:15:40 +10:00
taco
bdfe9ad27b switch to using QSPI bus for external flash 2025-09-06 14:15:40 +10:00
taco
c5180d4588 initial commit: CustomLFS 2025-09-06 14:15:40 +10:00
João Brázio
2ef38422e9 Delete the variant-specific NullDisplayDriver.h and update target.h to use the shared implementation from #735 2025-09-05 17:59:59 +01:00
João Brázio
808214d7b5 Merge remote-tracking branch 'upstream/dev' into jbrazio/2025_b5813561 2025-09-05 17:54:45 +01:00
recrof
d59724acd0 new variant: RAK WisMesh Tag 2025-09-05 16:21:19 +02:00
fdlamotte
0ebca4b88e Merge pull request #734 from recrof/lilygo_techo_refactor
lilygo t-echo enhancements and cleanup
2025-09-05 16:11:04 +02:00
fdlamotte
ec332c442b Merge pull request #735 from recrof/t1000_refactor
t1000 cleanup + move NullDisplayDriver.h out of t1000e folder
2025-09-05 15:58:59 +02:00
João Brázio
cb99eb4ae8 Remove retransmit check for RS232 bridge in logTx
Since the flag is preserved and respected by the mesh processing on the receiving end, there's no risk of these packets being retransmitted endlessly.
2025-09-05 14:49:06 +01:00
Florent de Lamotte
8fdaaceb1c ui: refresh sensors on gps toggle 2025-09-05 15:35:04 +02:00
Florent de Lamotte
f974cb2a4f ui: ENTER on SENSORS page toggles gps 2025-09-05 15:32:02 +02:00
Florent de Lamotte
2d651221c4 ui: sensors page 2025-09-05 15:20:52 +02:00
João Brázio
5843a12c71 Rename SerialBridge to RS232Bridge 2025-09-05 11:28:40 +01:00
ripplebiz
6fae950814 Merge pull request #738 from recrof/lilygo_tlora_c6_new_radio_init
tlora_c6 to use new radio init
2025-09-05 19:55:29 +10:00
ripplebiz
8f3c0a3ad2 Merge pull request #737 from recrof/generic_e22_radio_init
variant generic-e22 to use new radio init
2025-09-05 19:53:29 +10:00
recrof
24b2953861 tlora_c6 to use new radio init 2025-09-05 11:33:48 +02:00
recrof
8549696e4d generic e22 uses new radio init 2025-09-05 11:17:57 +02:00
recrof
c9e6ae9e6c fix typo in pin configuration 2025-09-05 11:12:17 +02:00
ripplebiz
2aa6835064 Merge pull request #725 from recrof/rpi_picow_refactor
tidy up rpi picow variant
2025-09-05 19:04:21 +10:00
ViezeVingertjes
963556f9ba Updated BLE functionality for low power mode in SerialBLEInterface. Updated platformio.ini to enable low power mode and added DC/DC converter support in T1000eBoard for improved power efficiency. 2025-09-05 10:46:51 +02:00
João Brázio
375093f78d Add nRF52 support and refactor packet handling
This commit introduces several improvements to the SerialBridge helper:

- Adds support for the nRF52 platform by implementing the `setPins` configuration.
- Corrects the type cast for the RP2040 platform from `HardwareSerial` to `SerialUART`.
- Refactors packet deserialization to use a new `Packet::readFrom()` method instead of a direct `memcpy`, improving encapsulation.
- Updates the packet length validation to use the more appropriate `MAX_TRANS_UNIT` constant.
2025-09-05 09:22:06 +01:00
ripplebiz
0e3933f18a Merge pull request #731 from oltaco/tracker-l1-platformio-tidy
Wio Tracker L1: correct platformio.ini defines
2025-09-05 13:44:58 +10:00
ripplebiz
c396ed9a05 Merge pull request #706 from recrof/patch-5
fixed max_contacts to 300 for heltec v3 companion_ble
2025-09-05 13:22:52 +10:00
João Brázio
77ab19153e Add serial logging for TX/RX packets 2025-09-05 02:07:26 +01:00
João Brázio
2b920dfed3 Rework packet serialization and parsing 2025-09-05 01:50:50 +01:00
João Brázio
ee3c4baea5 Prevent packet loops and duplicates
Implement a "seen packets" table to track packets that have already been processed by the serial bridge.

This prevents packets from being re-transmitted over the serial link if they have already been seen, and it stops inbound packets from serial from being re-injected into the mesh if they are duplicates.

Duplicate inbound packets are now freed to prevent memory leaks.
2025-09-04 23:50:13 +01:00
João Brázio
1948d284a0 Extract serial bridge into dedicated classes
This commit refactors the serial bridge functionality out of the `simple_repeater` example and into a more reusable, object-oriented structure.

An `AbstractBridge` interface has been introduced, along with a concrete `SerialBridge` implementation. This encapsulates all the logic for packet framing, checksum calculation, and serial communication, cleaning up the main example file significantly.

The `simple_repeater` example now instantiates and uses the `SerialBridge` class, resulting in better separation of concerns and improved code organization.
2025-09-04 23:43:05 +01:00
recrof
9b9c7289e6 moved pindefs from board to platformio.ini 2025-09-04 23:31:05 +02:00
recrof
816bbf925f t1000 cleanup + move NullDisplayDriver.h to helpers/ui for other variants to use 2025-09-04 23:12:57 +02:00
recrof
5b2c1715f4 lilygo t-echo cleanup, add AUTO_SHUTDOWN_MILLIVOLTS 2025-09-04 21:45:42 +02:00
João Brázio
d8f80f259a Refactor display driver inclusion for Heltec T114 to support configurations without a display 2025-09-04 13:26:48 +01:00
taco
1f20722f51 fix: wio tracker L1: tidy platformio.ini 2025-09-04 19:59:33 +10:00
ripplebiz
f9079985b6 Merge pull request #724 from recrof/thinknode_m1_refactor
tidy up thinknode_m1 variant
2025-09-04 19:33:49 +10:00
ripplebiz
46b3910d81 Merge pull request #713 from Quency-D/dev-meshpocket
add heltec meshpocket board.
2025-09-04 19:31:40 +10:00
Liam Cottle
a3aa66ac16 Merge pull request #729 from liamcottle/fix/thinknodem1
ThinkNode M1: add missing crc lib dep
2025-09-04 20:42:15 +12:00
liamcottle
d56b725256 add missing crc32 libdep after gxepd display driver changes 2025-09-04 20:07:37 +12:00
Quency-D
8fa31e00aa -D DISABLE_DIAGNOSTIC_OUTPUT this one will make GxEPD less verbose ;) 2025-09-04 15:39:08 +08:00
Quency-D
f4df94a20e Delete the sensor part and adapt to the latest crc display. 2025-09-04 14:04:00 +08:00
Quency-D
6e6c59d2ce Merge pull request #9 from meshcore-dev/dev
Dev
2025-09-04 13:50:09 +08:00
ripplebiz
a9fef1aefa Merge pull request #723 from recrof/heltec_t114_refactor
tidy up heltec_t114 variant
2025-09-04 13:52:23 +10:00
Quency-D
13d046892a Merge branch 'dev' into dev-meshpocket 2025-09-04 11:47:55 +08:00
ripplebiz
5782c2e799 Merge pull request #720 from oltaco/newui-multiclick-toggles
new-ui: add double/triple clicks, buzzer and gps toggle functions
2025-09-04 13:41:54 +10:00
ripplebiz
3e7459ae2e Merge pull request #719 from recrof/vision_master_refactor
renamend and refactored vision master, added usb roles
2025-09-04 13:33:48 +10:00
ripplebiz
6334971e2b Merge pull request #722 from fdlamotte/techo_epd_damage
Techo epd damage
2025-09-04 13:25:09 +10:00
recrof
c2fc70047a waveshare rp2040 lora cleanup 2025-09-03 21:37:07 +02:00
recrof
72b267092f xiao rp2040 cleanup 2025-09-03 21:28:46 +02:00
recrof
cbf3a03d2e rpi picow cleanup 2025-09-03 20:52:58 +02:00
recrof
d610b7be86 thinknode m1 refactor 2025-09-03 20:17:55 +02:00
recrof
1c91298b3a tidy up heltec_t114 variant 2025-09-03 19:38:38 +02:00
Florent
9f97edcb21 gxepd: use a crc to track damage ! 2025-09-03 18:17:37 +02:00
Florent
cb3049e706 cleanups (remove statics and typos) 2025-09-03 17:41:05 +02:00
taco
96a71bb21b alter keycode keycode handling 2025-09-03 16:28:58 +10:00
taco
afbfc6c6ed add new keycodes 2025-09-03 15:48:50 +10:00
taco
a9ab1f072a increase gps/buzzer alert times
600 is a bit short for eink
2025-09-03 14:02:35 +10:00
taco
9f185303b4 long press cancels multi click 2025-09-03 12:29:20 +10:00
taco
5de0dc1fd6 sliding multiclick window 2025-09-03 12:03:31 +10:00
taco
43c3105bf1 wake screen on double and triple clicks 2025-09-03 08:31:38 +10:00
taco
ce31fd7c57 multi click support including buzzer toggle 2025-09-03 08:25:59 +10:00
recrof
ddc900c8c8 renamend and refactored vision master to play better with build system 2025-09-02 22:23:32 +02:00
ripplebiz
a93a0fecba Merge pull request #717 from oltaco/promicro-hibernate
fix: promicro: add powerOff
2025-09-02 21:38:37 +10:00
taco
03358b33c2 fix: promicro: add powerOff 2025-09-02 21:30:51 +10:00
Scott Powell
90cb1e73f9 * HeltecV3: powerOff() fix 2025-09-02 21:18:05 +10:00
Florent de Lamotte
3cdf2f9b4d techo: display backlight behavior 2025-09-02 11:43:48 +02:00
Quency-D
c9671d7d8d add heltec meshpocket board. 2025-09-02 13:56:24 +08:00
Quency-D
88fbb41016 Merge pull request #7 from Quency-D/dev
merge Dev
2025-09-02 13:43:15 +08:00
ripplebiz
1a41da6bf2 Merge pull request #700 from fdlamotte/techo_env_sensors
techo: use EnvironmentSensor to get BME280 data
2025-09-02 14:58:57 +10:00
ripplebiz
2546a5da07 Merge pull request #711 from oltaco/heltec-vision-master-rename-companion-target
fix: Heltec Vision Master E290: rename companion target
2025-09-02 14:57:38 +10:00
Quency-D
b863a1a673 Merge pull request #6 from Quency-D/dev
merge Dev
2025-09-02 11:25:36 +08:00
taco
b64e78b7eb fix: Heltec Vision Master E290: rename companion target 2025-09-02 08:06:43 +10:00
ripplebiz
c3fb3bcefe Update README.md 2025-09-01 22:14:21 +10:00
ripplebiz
4849b863e9 Update README.md 2025-09-01 22:07:36 +10:00
ripplebiz
f3c52d84db Update README.md 2025-09-01 22:00:06 +10:00
Rastislav Vysoky
accacd9d74 fixed max_contacts to 300 for v3 2025-09-01 12:21:03 +02:00
João Brázio
9fd7e9427a Add bridge support for WSL3 board 2025-09-01 10:53:51 +01:00
João Brázio
cf4720bd34 Merge remote-tracking branch 'upstream/dev' into jbrazio/2025_3f11ad35 2025-09-01 10:47:19 +01:00
Florent
76711f54ce techo: let location_manager set clock 2025-08-31 21:45:47 +02:00
kelsey hudson
4b508136b4 Merge remote-tracking branch 'upstream/dev' into dev 2025-08-31 11:30:59 -07:00
Florent
fae3c284d3 techo: use EnvironmentSensor to get BME280 data 2025-08-31 18:09:05 +02:00
kelsey hudson
65be15e6be Merge remote-tracking branch 'upstream/dev' into dev 2025-08-30 17:54:42 -07:00
kelsey hudson
1b0999fc7e Merge remote-tracking branch 'upstream/dev' into dev 2025-08-27 00:39:35 -07:00
kelsey hudson
2e2e677b0a Ikoka Stick: Board IDs, LED behavior
Updates the manufacturer identifier with the EBYTE module. Makes the LED
behave properly. Turns the bright blue LED off after the first time you
transmit anything via LoRa.
2025-08-27 00:37:16 -07:00
João Brázio
7f142245e6 Merge remote-tracking branch 'origin/dev' into jbrazio/2025_3f11ad35 2025-08-22 23:00:35 +01:00
João Brázio
85273a6dc6 Merge remote-tracking branch 'origin/dev' into jbrazio/2025_3f11ad35 2025-07-29 00:31:52 +01:00
João Brázio
04042e3ca0 Refactor serial bridge handling 2025-07-09 11:03:35 +01:00
João Brázio
97b51900f8 More robust handling of pkt len 2025-07-08 21:45:49 +01:00
João Brázio
92ee1820c4 Add null check for packet allocation and clean up Dispatcher 2025-07-08 16:02:10 +01:00
João Brázio
ac056fb0b9 Remove serial bridge implementation and implement simplified version directly in the repeater source code. 2025-07-08 14:04:21 +01:00
João Brázio
3375389181 Merge remote-tracking branch 'upstream/dev' into jbrazio/2025_3f11ad35 2025-07-04 11:57:09 +01:00
João Brázio
2f77cef04b Add config flags to variants 2025-06-29 16:28:11 +01:00
João Brázio
4b70ee863d Serial bridge implementation 2025-06-27 20:16:14 +01:00
482 changed files with 28351 additions and 6119 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

@@ -9,7 +9,10 @@ MeshCore provides the ability to create wireless mesh networks, similar to Mesht
## ⚡ Key Features
* Multi-Hop Packet Routing Devices can forward messages across multiple nodes, extending range beyond a single radio's reach. MeshCore supports up to a configurable number of hops to balance network efficiency and prevent excessive traffic.
* Multi-Hop Packet Routing
* Devices can forward messages across multiple nodes, extending range beyond a single radio's reach.
* Supports up to a configurable number of hops to balance network efficiency and prevent excessive traffic.
* Nodes use fixed roles where "Companion" nodes are not repeating messages at all to prevent adverse routing paths from being used.
* Supports LoRa Radios Works with Heltec, RAK Wireless, and other LoRa-based hardware.
* Decentralized & Resilient No central server or internet required; the network is self-healing.
* Low Power Consumption Ideal for battery-powered or solar-powered devices.
@@ -36,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.
@@ -86,10 +91,25 @@ 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)
## Road-Map / To-Do
There are a number of fairly major features in the pipeline, with no particular time-frames attached yet. In very rough chronological order:
- [X] Companion radio: UI redesign
- [ ] Repeater + Room Server: add ACL's (like Sensor Node has)
- [ ] Standardise Bridge mode for repeaters
- [ ] Repeater/Bridge: Standardise the Transport Codes for zoning/filtering
- [ ] Core + Repeater: enhanced zero-hop neighbour discovery
- [ ] Core: round-trip manual path support
- [ ] Companion + Apps: support for multiple sub-meshes (and 'off-grid' client repeat mode)
- [ ] 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 consensus around V2 packet protocol, including path hashes, new encryption specs, etc
## 📞 Get Support
- Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page.

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)

View File

@@ -0,0 +1,39 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld"
},
"core": "esp32",
"extra_flags": [
"-D ARDUINO_USB_CDC_ON_BOOT=0",
"-D ARDUINO_USB_MSC_ON_BOOT=0",
"-D ARDUINO_USB_DFU_ON_BOOT=0",
"-D ARDUINO_USB_MODE=0",
"-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": "ESP32-S3-WROOM-1-N4"
},
"connectivity": ["wifi", "bluetooth"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "ESP32-S3-WROOM-1-N4 (4 MB Flash, No PSRAM)",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 524288,
"maximum_size": 4194304,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.espressif.com/sites/default/files/documentation/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf",
"vendor": "Espressif"
}

45
boards/ebyte_eora-s3.json Normal file
View File

@@ -0,0 +1,45 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default.csv",
"memory_type": "qio_qspi"
},
"core": "esp32",
"extra_flags": [
"-DARDUINO_LILYGO_T3_S3_V1_X",
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1",
"-DARDUINO_USB_MODE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": [
"wifi"
],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Ebyte EoRa-S3-XXXTB Radio",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 460800
},
"url": "https://www.cdebyte.com/products/EoRa-S3-900TB",
"vendor": "Chengdu Ebyte Electronic Technology Co., Ltd"
}

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"
}

View File

@@ -0,0 +1,53 @@
{
"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": "HT-n5262",
"mcu": "nrf52840",
"variant": "heltec_mesh_pocket",
"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",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Heltec nrf (Adafruit BSP)",
"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://heltec.org/project/meshpocket/",
"vendor": "Heltec"
}

View File

@@ -0,0 +1,40 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_8MB.csv"
},
"core": "esp32",
"extra_flags": [
"-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",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_tracker_v2"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "heltec_tracker v2",
"upload": {
"flash_size": "8MB",
"maximum_ram_size": 327680,
"maximum_size": 8388608,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "heltec"
}

43
boards/heltec_v4.json Normal file
View File

@@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_qspi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-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": "qspi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_v4"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "heltec_wifi_lora_32 v4 (16 MB FLASH, 2 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 2097152,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "heltec"
}

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"
}

View File

@@ -0,0 +1,38 @@
/* Linker script to configure memory regions. */
SEARCH_DIR(.)
GROUP(-lgcc -lc -lnosys)
MEMORY
{
FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xD4000 - 0x26000
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
* - Max ATT MTU
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
}
SECTIONS
{
. = ALIGN(4);
.svc_data :
{
PROVIDE(__start_svc_data = .);
KEEP(*(.svc_data))
PROVIDE(__stop_svc_data = .);
} > RAM
.fs_data :
{
PROVIDE(__start_fs_data = .);
KEEP(*(.fs_data))
PROVIDE(__stop_fs_data = .);
} > RAM
} INSERT AFTER .data;
INCLUDE "nrf52_common.ld"

View File

@@ -0,0 +1,38 @@
/* Linker script to configure memory regions. */
SEARCH_DIR(.)
GROUP(-lgcc -lc -lnosys)
MEMORY
{
FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xD4000 - 0x27000
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
* - Max ATT MTU
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
}
SECTIONS
{
. = ALIGN(4);
.svc_data :
{
PROVIDE(__start_svc_data = .);
KEEP(*(.svc_data))
PROVIDE(__stop_svc_data = .);
} > RAM
.fs_data :
{
PROVIDE(__start_fs_data = .);
KEEP(*(.fs_data))
PROVIDE(__stop_fs_data = .);
} > RAM
} INSERT AFTER .data;
INCLUDE "nrf52_common.ld"

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"
}

72
boards/rak4631.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 RAK4631 Board",
"mcu": "nrf52840",
"variant": "WisCore_RAK4631_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 RAK4631 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"
}

View File

@@ -46,6 +46,7 @@
"speed": 115200,
"protocols": [
"jlink",
"stlink",
"nrfjprog",
"nrfutil",
"cmsis-dap",

38
boards/t-deck.json Normal file
View File

@@ -0,0 +1,38 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_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": "LilyGo T-Deck (16M Flash 8M PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.lilygo.cc",
"vendor": "LilyGo"
}

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"
}

33
boards/tiny_relay.json Normal file
View File

@@ -0,0 +1,33 @@
{
"build": {
"arduino": {
"variant_h": "variant_RAK3172_MODULE.h"
},
"core": "stm32",
"cpu": "cortex-m4",
"extra_flags": "-DSTM32WL -DSTM32WLxx -DSTM32WLE5xx",
"framework_extra_flags": {
"arduino": "-DUSE_CM4_STARTUP_FILE -DARDUINO_RAK3172_MODULE"
},
"f_cpu": "48000000L",
"mcu": "stm32wle5ccu",
"product_line": "STM32WLE5xx",
"variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U"
},
"debug": {
"default_tools": ["stlink"],
"jlink_device": "STM32WLE5CC",
"openocd_target": "stm32wlx",
"svd_path": "STM32WLE5_CM4.svd"
},
"frameworks": ["arduino"],
"name": "BB-STM32WL",
"upload": {
"maximum_ram_size": 65536,
"maximum_size": 262144,
"protocol": "stlink",
"protocols": ["stlink", "jlink"]
},
"url": "https://www.st.com/en/microcontrollers-microprocessors/stm32wle5cc.html",
"vendor": "YAOYAO"
}

173
build.sh
View File

@@ -1,18 +1,72 @@
#!/usr/bin/env bash
# usage
# sh build.sh build-firmware RAK_4631_Repeater
# sh build.sh build-firmwares
# sh build.sh build-matching-firmwares RAK_4631
# sh build.sh build-companion-firmwares
# sh build.sh build-repeater-firmwares
# sh build.sh build-room-server-firmwares
global_usage() {
cat - <<EOF
Usage:
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>.
build-companion-firmwares: Build all companion firmwares for all build targets.
build-repeater-firmwares: Build all repeater firmwares for all build targets.
build-room-server-firmwares: Build all chat room server firmwares for all build targets.
Examples:
Build firmware for the "RAK_4631_repeater" device target
$ sh build.sh build-firmware RAK_4631_repeater
Build all firmwares for device targets containing the string "RAK_4631"
$ sh build.sh build-matching-firmwares <build-match-spec>
Build all companion firmwares
$ sh build.sh build-companion-firmwares
Build all repeater firmwares
$ 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() {
echo $(pio project config | grep 'env:' | sed 's/env://')
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
# 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() {
shopt -s nocasematch
@@ -24,8 +78,47 @@ get_pio_envs_containing_string() {
done
}
# $1 should be the string to find (case insensitive)
get_pio_envs_ending_with_string() {
shopt -s nocasematch
envs=($(get_pio_envs))
for env in "${envs[@]}"; do
if [[ "$env" == *${1} ]]; then
echo $env
fi
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)
@@ -47,33 +140,40 @@ build_firmware() {
# e.g: RAK_4631_Repeater-v1.0.0-SHA
FIRMWARE_FILENAME="$1-${FIRMWARE_VERSION_STRING}"
# export build flags for pio so we can inject firmware version info
export PLATFORMIO_BUILD_FLAGS="-DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'"
# 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
python bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840
# 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
}
@@ -85,6 +185,14 @@ build_all_firmwares_matching() {
done
}
# firmwares ending with $1 will be built
build_all_firmwares_by_suffix() {
envs=($(get_pio_envs_ending_with_string "$1"))
for env in "${envs[@]}"; do
build_firmware $env
done
}
build_repeater_firmwares() {
# # build specific repeater firmwares
@@ -96,7 +204,7 @@ build_repeater_firmwares() {
# build_firmware "RAK_4631_Repeater"
# build all repeater firmwares
build_all_firmwares_matching "repeater"
build_all_firmwares_by_suffix "_repeater"
}
@@ -115,8 +223,8 @@ build_companion_firmwares() {
# build_firmware "t1000e_companion_radio_ble"
# build all companion firmwares
build_all_firmwares_matching "companion_radio_usb"
build_all_firmwares_matching "companion_radio_ble"
build_all_firmwares_by_suffix "_companion_radio_usb"
build_all_firmwares_by_suffix "_companion_radio_ble"
}
@@ -127,7 +235,7 @@ build_room_server_firmwares() {
# build_firmware "RAK_4631_room_server"
# build all room server firmwares
build_all_firmwares_matching "room_server"
build_all_firmwares_by_suffix "_room_server"
}
@@ -143,8 +251,11 @@ mkdir -p out
# handle script args
if [[ $1 == "build-firmware" ]]; then
if [ "$2" ]; then
build_firmware $2
TARGETS=${@:2}
if [ "$TARGETS" ]; then
for env in $TARGETS; do
build_firmware $env
done
else
echo "usage: $0 build-firmware <target>"
exit 1

31
create-uf2.py Normal file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/python3
# Adds PlatformIO post-processing to convert hex files to uf2 files
import os
Import("env")
firmware_hex = "${BUILD_DIR}/${PROGNAME}.hex"
uf2_file = os.environ.get("UF2_FILE_PATH", "${BUILD_DIR}/${PROGNAME}.uf2")
def create_uf2_action(source, target, env):
uf2_cmd = " ".join(
[
'"$PYTHONEXE"',
'"$PROJECT_DIR/bin/uf2conv/uf2conv.py"',
'-f', '0xADA52840',
'-c', firmware_hex,
'-o', uf2_file,
]
)
env.Execute(uf2_cmd)
env.AddCustomTarget(
name="create_uf2",
dependencies=firmware_hex,
actions=create_uf2_action,
title="Create UF2 file",
description="Use uf2conv to convert hex binary into uf2",
always_build=True,
)

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
---

View File

@@ -65,17 +65,18 @@ author: https://github.com/LitBomb<!-- omit from toc -->
- [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago)
- [6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.](#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed)
- [6.3. Q: How to connect to a repeater via BLE (Bluetooth)?](#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth)
- [6.4. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#64-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code)
- [6.5. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.](#65-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection)
- [6.6. Q: My RAK/T1000-E/xiao\_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?](#66-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh)
- [6.7. Q: WebFlasher fails on Linux with failed to open](#67-q-webflasher-fails-on-linux-with-failed-to-open)
- [6.4. Q: My companion isn't showing up over Bluetooth?](#64-q-my-companion-isnt-showing-up-over-bluetooth)
- [6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#64-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code)
- [6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.](#65-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection)
- [6.7. Q: My RAK/T1000-E/xiao\_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?](#66-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh)
- [6.8. Q: WebFlasher fails on Linux with failed to open](#67-q-webflasher-fails-on-linux-with-failed-to-open)
- [7. Other Questions:](#7-other-questions)
- [7.1. Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?](#71-q-how-to-update-nrf-rak-t114-seed-xiao-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app)
- [7.2. Q: How to update ESP32-based devices over the air?](#72-q-how-to-update-esp32-based-devices-over-the-air)
- [7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?](#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu)
- [7.4. Q: are the MeshCore logo and font available?](#74-q-are-the-meshcore-logo-and-font-available)
- [7.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code)
- [7.6. Q: How do I connect to the comnpanion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-comnpanion-via-wifi-eg-using-a-heltec-v3)
- [7.6. Q: How do I connect to the companion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-comnpanion-via-wifi-eg-using-a-heltec-v3)
## 1. Introduction
@@ -101,7 +102,7 @@ Anyone is able to build anything they like on top of MeshCore without paying any
Main web site: [https://meshcore.co.uk/](https://meshcore.co.uk/)
Firmware Flasher: https://flasher.meshcore.co.uk/
Phone Client Applications: https://meshcore.co.uk/apps.html
MeshCore Fimrware GitHub: https://github.com/ripplebiz/MeshCore
MeshCore Firmware GitHub: https://github.com/ripplebiz/MeshCore
NOTE: Andy Kirby has a very useful [intro video](https://www.youtube.com/watch?v=t1qne8uJBAc) for beginners.
@@ -159,7 +160,7 @@ The recommendation is to run repeater and room server on separate devices for th
If you have two supported devices, and there are not many MeshCore users near you, flash both to BLE Companion firmware so you can use your devices to communicate with your near-by friends and family.
If you have two supported devices, and there are other MeshcCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.
If you have two supported devices, and there are other MeshCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.
After you flashed the latest firmware onto your repeater device, keep the device connected to your computer via USB serial, use the console feature on the web flasher and set the frequency for your region or country, so your client can remote administer the repeater or room server over RF:
@@ -229,8 +230,7 @@ Repeater or room server can be administered with one of the options below:
- After a repeater or room server firmware is flashed on to a LoRa device, go to <https://config.meshcore.dev> and use the web user interface to connect to the LoRa device via USB serial. From there you can set the name of the server, its frequency and other related settings, location, passwords etc.
![image](https://github.com/user-attachments/assets/bec28ff3-a7d6-4a1e-8602-cb6b290dd150)
![image](https://github.com/user-attachments/assets/2a9d9894-e34d-4dbe-b57c-fc3c250a2d34)
- Connect the server device using a USB cable to a computer running Chrome on https://flasher.meshcore.co.uk/, then use the `console` feature to connect to the device
@@ -392,7 +392,7 @@ In MeshCore, only repeaters and room server with `set repeat on` repeat.
**A:** If you used to reach a node through a repeater and the repeater is no longer reachable, the client will send the message using the existing (but now broken) known path, the message will fail after 3 retries, and the app will reset the path and send the message as flood on the last retry by default. This can be turned off in settings. If the destination is reachable directly or through another repeater, the new path will be used going forward. Or you can set the path manually if you know a specific repeater to use to reach that destination.
In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to reestablish a path.
In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to re-establish a path.
### 5.4. Q: How does a node discovery a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?
@@ -464,7 +464,7 @@ Andy also has a video on how to build using VS Code:
### 5.10. Q: Are there other MeshCore related open source projects?
**A:** [Liam Cottle](https://liamcottle.net)'s MeshCore web client and MeshCore Javascript libary are open source under MIT license.
**A:** [Liam Cottle](https://liamcottle.net)'s MeshCore web client and MeshCore Javascript library are open source under MIT license.
Web client: https://github.com/liamcottle/meshcore-web
Javascript: https://github.com/liamcottle/meshcore.js
@@ -472,7 +472,7 @@ Javascript: https://github.com/liamcottle/meshcore.js
### 5.11. Q: Does MeshCore support ATAK
**A:** ATAK is not currently on MeshCore's roadmap.
Meshcore would not be best suited to ATAK because MeshCore:
Meshcore would not be best suited to ATAK because MeshCore:
clients do not repeat and therefore you would need a network of repeaters in place
will not have a stable path where all clients are constantly moving between repeaters
@@ -570,7 +570,7 @@ Bindings to access your MeshCore companion radio nodes in python.
https://github.com/fdlamotte/meshcore_py
#### 5.14.4. meshcore-cli
CLI interface to MeshCore companion radio over BLE, TCP, or serial. Uses Pyton MeshCore above.
CLI interface to MeshCore companion radio over BLE, TCP, or serial. Uses Python MeshCore above.
https://github.com/fdlamotte/meshcore-cli
#### 5.14.5. meshcore.js
@@ -592,15 +592,19 @@ You can get the epoch time on <https://www.epochconverter.com/> and use it to se
### 6.3. Q: How to connect to a repeater via BLE (Bluetooth)?
**A:** You can't connect to a device running repeater firmware via Bluetooth. Devices running the BLE companion firmware you can connect to it via Bluetooth using the android app
### 6.4. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?
### 6.4. Q: My companion isn't showing up over Bluetooth?
**A:** make sure that you flashed the Bluetooth companion firmware and not the USB-only companion firmware.
### 6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?
**A:** the default Bluetooth pairing code is `123456`
### 6.5. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.
### 6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.
**A:** Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification.
### 6.6. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?
### 6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?
**A:**
1. Connect USB-C cable to your device, per your device's instruction, get it to flash mode:
@@ -620,8 +624,7 @@ You can get the epoch time on <https://www.epochconverter.com/> and use it to se
Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by hold down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on flasher.meshcore.co.uk
### 6.7. Q: WebFlasher fails on Linux with failed to open
### 6.8. Q: WebFlasher fails on Linux with failed to open
**A:** If the usb port doesn't have the right ownership for this task, the process fails with the following error:
`NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.`
@@ -638,13 +641,13 @@ Allow the browser user on it:
1. Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for `nrf dfu`, the app's full name is `nRF Device Firmware Update`
2. On flasher.meshcore.co.uk, download the **ZIP** version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao)
3. From the MeshCore app, login remotely to the repeater you want to update with admin priviledge
3. From the MeshCore app, login remotely to the repeater you want to update with admin privilege
4. Go to the Command Line tab, type `start ota` and hit enter.
5. you should see `OK` to confirm the repeater device is now in OTA mode
6. Run the DFU app,tab `Settings` on the top right corner
7. Enable `Packets receipt notifications`, and change `Number of Packets` to 10 for RAK, 8 for T114. 8 also works for RAK.
9. Select the firmware zip file you downloaded
10. Select the device you want to update. If the device you want to updat is not on the list, try enabling`OTA` on the device again
10. Select the device you want to update. If the device you want to update is not on the list, try enabling`OTA` on the device again
11. If the device is not found, enable `Force Scanning` in the DFU app
12. Tab the `Upload` to begin OTA update
13. If it fails, try turning off and on Bluetooth on your phone. If that doesn't work, try rebooting your phone.
@@ -655,7 +658,7 @@ Allow the browser user on it:
**A:** For ESP32-based devices (e.g. Heltec V3):
1. On flasher.meshcore.co.uk, download the **non-merged** version of the firmware for your ESP32 device (e.g. `Heltec_v3_repeater-v1.6.2-4449fd3.bin`, no `"merged"` in the file name)
2. From the MeshCore app, login remotely to the repeater you want to update with admin priviledge
2. From the MeshCore app, login remotely to the repeater you want to update with admin privilege
4. Go to the Command Line tab, type `start ota` and hit enter.
5. you should see `OK` to confirm the repeater device is now in OTA mode
6. The command `start ota` on an ESP32-based device starts a wifi hotspot named `MeshCore OTA`
@@ -695,7 +698,7 @@ where `&type` is:
`room = 3`
`sensor = 4`
### 7.6. Q: How do I connect to the comnpanion via WIFI, e.g. using a heltec v3?
### 7.6. Q: How do I connect to the companion via WIFI, e.g. using a heltec v3?
**A:**
WiFi firmware requires you to compile it yourself, as you need to set the wifi ssid and password.
Edit WIFI_SSID and WIFI_PWD in `./variants/heltec_v3/platformio.ini` and then flash it to your device.

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

@@ -44,6 +44,10 @@ bit 0 means the lowest bit (1s place)
| `0x08` | `PAYLOAD_TYPE_PATH` | Returned path. |
| `0x09` | `PAYLOAD_TYPE_TRACE` | trace a path, collecting SNI for each hop. |
| `0x0A` | `PAYLOAD_TYPE_MULTIPART` | packet is part of a sequence of packets. |
| `0x0B` | `PAYLOAD_TYPE_CONTROL` | control packet data (unencrypted) |
| `0x0C` | . | reserved |
| `0x0D` | . | reserved |
| `0x0E` | . | reserved |
| `0x0F` | `PAYLOAD_TYPE_RAW_CUSTOM` | Custom packet (raw bytes, custom encryption). |
## Payload Version Values

View File

@@ -11,6 +11,7 @@ Inside of each [meshcore packet](./packet_structure.md) is a payload, identified
* Group text message (unverified).
* Group datagram (unverified).
* Multi-part packet
* Control data packet
* Custom packet (raw bytes, custom encryption).
This document defines the structure of each of these payload types.
@@ -57,7 +58,7 @@ Appdata Flags
# Acknowledgement
An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement will be sent in the "extra" payload (see [Returned Path](#returned-path)) and not as a discrete ackowledgement. CLI commands do not require an acknowledgement, neither discrete nor extra.
An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the "extra" payload (see [Returned Path](#returned-path)) instead of as a separate ackowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra.
| Field | Size (bytes) | Description |
|----------|--------------|------------------------------------------------------------|
@@ -102,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
@@ -131,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 |
@@ -140,13 +164,13 @@ Request data about sensors on the node, including battery level.
## Plain text message
| Field | Size (bytes) | Description |
|-----------------|-----------------|--------------------------------------------------------------|
| timestamp | 4 | send time (unix timestamp) |
| flags + attempt | 1 | upper six bits are flags (see below), lower two bits are attempt number (0..3) |
| message | rest of payload | the message content, see next table |
| Field | Size (bytes) | Description |
|--------------------|-----------------|--------------------------------------------------------------|
| timestamp | 4 | send time (unix timestamp) |
| txt_type + attempt | 1 | upper six bits are txt_type (see below), lower two bits are attempt number (0..3) |
| message | rest of payload | the message content, see next table |
Flags
txt_type
| Value | Description | Message content |
|--------|---------------------------|------------------------------------------------------------|
@@ -163,13 +187,48 @@ Flags
| cipher MAC | 2 | MAC for encrypted data in next field |
| ciphertext | rest of payload | encrypted message, see below for details |
Plaintext message
## Room server login
| Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------|
| timestamp | 4 | send time (unix timestamp) |
| sync timestamp | 4 | NOTE: room server only! - sender's "sync messages SINCE x" timestamp |
| password | rest of message | password for repeater/room |
| timestamp | 4 | sender time (unix timestamp) |
| sync timestamp | 4 | sender's "sync messages SINCE x" timestamp |
| password | rest of message | password for room |
## Repeater/Sensor login
| Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------|
| 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
@@ -182,7 +241,31 @@ Plaintext message
The plaintext contained in the ciphertext matches the format described in [plain text message](#plain-text-message). Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be `0x00` because it is a "plain text message". The message will be of the form `<sender name>: <message body>` (eg., `user123: I'm on my way`).
TODO: describe what datagram looks like
# Control data
| Field | Size (bytes) | Description |
|--------------|-----------------|--------------------------------------------|
| flags | 1 | upper 4 bits is sub_type |
| data | rest of payload | typically unencrypted data |
## DISCOVER_REQ (sub_type)
| Field | Size (bytes) | Description |
|--------------|-----------------|----------------------------------------------|
| flags | 1 | 0x8 (upper 4 bits), prefix_only (lowest bit) |
| type_filter | 1 | bit for each ADV_TYPE_* |
| tag | 4 | randomly generate by sender |
| since | 4 | (optional) epoch timestamp (0 by default) |
## DISCOVER_RESP (sub_type)
| Field | Size (bytes) | Description |
|--------------|-----------------|--------------------------------------------|
| flags | 1 | 0x9 (upper 4 bits), node_type (lower 4) |
| snr | 1 | signed, SNR*4 |
| tag | 4 | reflected back from DISCOVER_REQ |
| pubkey | 8 or 32 | node's ID (or prefix) |
# Custom packet

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

@@ -41,6 +41,6 @@ public:
void disableSerial() { _serial->disable(); }
virtual void msgRead(int msgcount) = 0;
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0;
virtual void soundBuzzer(UIEventType bet = UIEventType::none) = 0;
virtual void notify(UIEventType t = UIEventType::none) = 0;
virtual void loop() = 0;
};

View File

@@ -1,7 +1,13 @@
#include <Arduino.h>
#include "DataStore.h"
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _clock(&clock),
#if defined(EXTRAFS) || defined(QSPIFLASH)
#define MAX_BLOBRECS 100
#else
#define MAX_BLOBRECS 20
#endif
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
identity_store(fs, "")
#elif defined(RP2040_PLATFORM)
@@ -12,24 +18,45 @@ DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _clock(&
{
}
static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(EXTRAFS) || defined(QSPIFLASH)
DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
return _fs->open(filename, FILE_O_WRITE);
identity_store(fs, "")
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "w");
identity_store(fs, "/identity")
#else
return _fs->open(filename, "w", true);
identity_store(fs, "/identity")
#endif
{
}
#endif
static File openWrite(FILESYSTEM* fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
fs->remove(filename);
return fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return fs->open(filename, "w");
#else
return fs->open(filename, "w", true);
#endif
}
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
static uint32_t _ContactsChannelsTotalBlocks = 0;
#endif
void DataStore::begin() {
#if defined(RP2040_PLATFORM)
identity_store.begin();
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_ContactsChannelsTotalBlocks = _getContactsChannelsFS()->_getFS()->cfg->block_count;
checkAdvBlobFile();
#if defined(EXTRAFS) || defined(QSPIFLASH)
migrateToSecondaryFS();
#endif
#else
// init 'blob store' support
_fs->mkdir("/bl");
@@ -38,22 +65,37 @@ 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)
#include <InternalFileSystem.h>
#if defined(QSPIFLASH)
#include <CustomLFS_QSPIFlash.h>
#elif defined(EXTRAFS)
#include <CustomLFS.h>
#else
#include <InternalFileSystem.h>
#endif
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
int _countLfsBlock(void *p, lfs_block_t block){
if (block > _ContactsChannelsTotalBlocks) {
MESH_DEBUG_PRINTLN("ERROR: Block %d exceeds filesystem bounds - CORRUPTION DETECTED!", block);
return LFS_ERR_CORRUPT; // return error to abort lfs_traverse() gracefully
}
lfs_size_t *size = (lfs_size_t*) p;
*size += 1;
return 0;
return 0;
}
lfs_ssize_t _getLfsUsedBlockCount() {
lfs_ssize_t _getLfsUsedBlockCount(FILESYSTEM* fs) {
lfs_size_t size = 0;
lfs_traverse(InternalFS._getFS(), _countLfsBlock, &size);
int err = lfs_traverse(fs->_getFS(), _countLfsBlock, &size);
if (err) {
MESH_DEBUG_PRINTLN("ERROR: lfs_traverse() error: %d", err);
return 0;
}
return size;
}
#endif
@@ -67,8 +109,8 @@ uint32_t DataStore::getStorageUsedKb() const {
_fs->info(info);
return info.usedBytes / 1024;
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
const lfs_config* config = InternalFS._getFS()->cfg;
int usedBlockCount = _getLfsUsedBlockCount();
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
int usedBlockCount = _getLfsUsedBlockCount(_getContactsChannelsFS());
int usedBytes = config->block_size * usedBlockCount;
return usedBytes / 1024;
#else
@@ -85,7 +127,7 @@ uint32_t DataStore::getStorageTotalKb() const {
_fs->info(info);
return info.totalBytes / 1024;
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
const lfs_config* config = InternalFS._getFS()->cfg;
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
int totalBytes = config->block_size * config->block_count;
return totalBytes / 1024;
#else
@@ -103,17 +145,37 @@ File DataStore::openRead(const char* filename) {
#endif
}
File DataStore::openRead(FILESYSTEM* fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
return fs->open(filename, FILE_O_READ);
#elif defined(RP2040_PLATFORM)
return fs->open(filename, "r");
#else
return fs->open(filename, "r", false);
#endif
}
bool DataStore::removeFile(const char* filename) {
return _fs->remove(filename);
}
bool DataStore::removeFile(FILESYSTEM* fs, const char* filename) {
return fs->remove(filename);
}
bool DataStore::formatFileSystem() {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
return _fs->format();
if (_fsExtra == nullptr) {
return _fs->format();
} else {
return _fs->format() && _fsExtra->format();
}
#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
@@ -138,11 +200,7 @@ void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon)
}
void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) {
#if defined(RP2040_PLATFORM)
File file = _fs->open(filename, "r");
#else
File file = _fs->open(filename);
#endif
File file = openRead(_fs, filename);
if (file) {
uint8_t pad[8];
@@ -154,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
@@ -164,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();
}
@@ -185,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
@@ -195,20 +258,20 @@ 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();
}
}
void DataStore::loadContacts(DataStoreHost* host) {
if (_fs->exists("/contacts3")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/contacts3", "r");
#else
File file = _fs->open("/contacts3");
#endif
File file = openRead(_getContactsChannelsFS(), "/contacts3");
if (file) {
bool full = false;
while (!full) {
@@ -236,11 +299,10 @@ void DataStore::loadContacts(DataStoreHost* host) {
}
file.close();
}
}
}
void DataStore::saveContacts(DataStoreHost* host) {
File file = openWrite(_fs, "/contacts3");
File file = openWrite(_getContactsChannelsFS(), "/contacts3");
if (file) {
uint32_t idx = 0;
ContactInfo c;
@@ -269,12 +331,7 @@ void DataStore::saveContacts(DataStoreHost* host) {
}
void DataStore::loadChannels(DataStoreHost* host) {
if (_fs->exists("/channels2")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/channels2", "r");
#else
File file = _fs->open("/channels2");
#endif
File file = openRead(_getContactsChannelsFS(), "/channels2");
if (file) {
bool full = false;
uint8_t channel_idx = 0;
@@ -296,11 +353,10 @@ void DataStore::loadChannels(DataStoreHost* host) {
}
file.close();
}
}
}
void DataStore::saveChannels(DataStoreHost* host) {
File file = openWrite(_fs, "/channels2");
File file = openWrite(_getContactsChannelsFS(), "/channels2");
if (file) {
uint8_t channel_idx = 0;
ChannelDetails ch;
@@ -331,12 +387,12 @@ struct BlobRec {
};
void DataStore::checkAdvBlobFile() {
if (!_fs->exists("/adv_blobs")) {
File file = openWrite(_fs, "/adv_blobs");
if (!_getContactsChannelsFS()->exists("/adv_blobs")) {
File file = openWrite(_getContactsChannelsFS(), "/adv_blobs");
if (file) {
BlobRec zeroes;
memset(&zeroes, 0, sizeof(zeroes));
for (int i = 0; i < 20; i++) { // pre-allocate to fixed size
for (int i = 0; i < MAX_BLOBRECS; i++) { // pre-allocate to fixed size
file.write((uint8_t *) &zeroes, sizeof(zeroes));
}
file.close();
@@ -344,10 +400,117 @@ void DataStore::checkAdvBlobFile() {
}
}
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
File file = _fs->open("/adv_blobs");
uint8_t len = 0; // 0 = not found
void DataStore::migrateToSecondaryFS() {
// migrate old adv_blobs, contacts3 and channels2 files to secondary FS if they don't already exist
if (!_fsExtra->exists("/adv_blobs")) {
if (_fs->exists("/adv_blobs")) {
File oldAdvBlobs = openRead(_fs, "/adv_blobs");
File newAdvBlobs = openWrite(_fsExtra, "/adv_blobs");
if (oldAdvBlobs && newAdvBlobs) {
BlobRec rec;
size_t count = 0;
// Copy 20 BlobRecs from old to new
while (count < 20 && oldAdvBlobs.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) {
newAdvBlobs.seek(count * sizeof(BlobRec));
newAdvBlobs.write((uint8_t *)&rec, sizeof(rec));
count++;
}
}
if (oldAdvBlobs) oldAdvBlobs.close();
if (newAdvBlobs) newAdvBlobs.close();
_fs->remove("/adv_blobs");
}
}
if (!_fsExtra->exists("/contacts3")) {
if (_fs->exists("/contacts3")) {
File oldFile = openRead(_fs, "/contacts3");
File newFile = openWrite(_fsExtra, "/contacts3");
if (oldFile && newFile) {
uint8_t buf[64];
int n;
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
newFile.write(buf, n);
}
}
if (oldFile) oldFile.close();
if (newFile) newFile.close();
_fs->remove("/contacts3");
}
}
if (!_fsExtra->exists("/channels2")) {
if (_fs->exists("/channels2")) {
File oldFile = openRead(_fs, "/channels2");
File newFile = openWrite(_fsExtra, "/channels2");
if (oldFile && newFile) {
uint8_t buf[64];
int n;
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
newFile.write(buf, n);
}
}
if (oldFile) oldFile.close();
if (newFile) newFile.close();
_fs->remove("/channels2");
}
}
// cleanup nodes which have been testing the extra fs, copy _main.id and new_prefs back to primary
if (_fsExtra->exists("/_main.id")) {
if (_fs->exists("/_main.id")) {_fs->remove("/_main.id");}
File oldFile = openRead(_fsExtra, "/_main.id");
File newFile = openWrite(_fs, "/_main.id");
if (oldFile && newFile) {
uint8_t buf[64];
int n;
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
newFile.write(buf, n);
}
}
if (oldFile) oldFile.close();
if (newFile) newFile.close();
_fsExtra->remove("/_main.id");
}
if (_fsExtra->exists("/new_prefs")) {
if (_fs->exists("/new_prefs")) {_fs->remove("/new_prefs");}
File oldFile = openRead(_fsExtra, "/new_prefs");
File newFile = openWrite(_fs, "/new_prefs");
if (oldFile && newFile) {
uint8_t buf[64];
int n;
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
newFile.write(buf, n);
}
}
if (oldFile) oldFile.close();
if (newFile) newFile.close();
_fsExtra->remove("/new_prefs");
}
// remove files from where they should not be anymore
if (_fs->exists("/adv_blobs")) {
_fs->remove("/adv_blobs");
}
if (_fs->exists("/contacts3")) {
_fs->remove("/contacts3");
}
if (_fs->exists("/channels2")) {
_fs->remove("/channels2");
}
if (_fsExtra->exists("/_main.id")) {
_fsExtra->remove("/_main.id");
}
if (_fsExtra->exists("/new_prefs")) {
_fsExtra->remove("/new_prefs");
}
}
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
File file = openRead(_getContactsChannelsFS(), "/adv_blobs");
uint8_t len = 0; // 0 = not found
if (file) {
BlobRec tmp;
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
@@ -364,10 +527,8 @@ 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) {
if (len < PUB_KEY_SIZE+4+SIGNATURE_SIZE || len > MAX_ADVERT_PKT_LEN) return false;
checkAdvBlobFile();
File file = _fs->open("/adv_blobs", FILE_O_WRITE);
File file = _getContactsChannelsFS()->open("/adv_blobs", FILE_O_WRITE);
if (file) {
uint32_t pos = 0, found_pos = 0;
uint32_t min_timestamp = 0xFFFFFFFF;
@@ -401,21 +562,23 @@ 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)) {
#if defined(RP2040_PLATFORM)
File f = _fs->open(path, "r");
#else
File f = _fs->open(path);
#endif
File f = openRead(_fs, path);
if (f) {
int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!!
f.close();
@@ -427,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) {
@@ -443,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

@@ -15,6 +15,7 @@ public:
class DataStore {
FILESYSTEM* _fs;
FILESYSTEM* _fsExtra;
mesh::RTCClock* _clock;
IdentityStore identity_store;
@@ -25,8 +26,11 @@ class DataStore {
public:
DataStore(FILESYSTEM& fs, mesh::RTCClock& clock);
DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock);
void begin();
bool formatFileSystem();
FILESYSTEM* getPrimaryFS() const { return _fs; }
FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
bool loadMainIdentity(mesh::LocalIdentity &identity);
bool saveMainIdentity(const mesh::LocalIdentity &identity);
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon);
@@ -35,10 +39,17 @@ public:
void saveContacts(DataStoreHost* host);
void loadChannels(DataStoreHost* host);
void saveChannels(DataStoreHost* host);
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);
bool removeFile(FILESYSTEM* fs, const char* filename);
uint32_t getStorageUsedKb() const;
uint32_t getStorageTotalKb() const;
private:
FILESYSTEM* _getContactsChannelsFS() const { if (_fsExtra) return _fsExtra; return _fs;};
};

View File

@@ -50,6 +50,19 @@
#define CMD_SEND_BINARY_REQ 50
#define CMD_FACTORY_RESET 51
#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
@@ -75,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
@@ -99,6 +115,9 @@
#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B
#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
@@ -109,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;
@@ -175,15 +203,34 @@ void MyMesh::updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, co
}
}
bool MyMesh::Frame::isChannelMsg() const {
return buf[0] == RESP_CODE_CHANNEL_MSG_RECV || buf[0] == RESP_CODE_CHANNEL_MSG_RECV_V3;
}
void MyMesh::addToOfflineQueue(const uint8_t frame[], int len) {
if (offline_queue_len >= OFFLINE_QUEUE_SIZE) {
MESH_DEBUG_PRINTLN("ERROR: offline_queue is full!");
MESH_DEBUG_PRINTLN("WARN: offline_queue is full!");
int pos = 0;
while (pos < offline_queue_len) {
if (offline_queue[pos].isChannelMsg()) {
for (int i = pos; i < offline_queue_len - 1; i++) { // delete oldest channel msg from queue
offline_queue[i] = offline_queue[i + 1];
}
MESH_DEBUG_PRINTLN("INFO: removed oldest channel message from queue.");
offline_queue[offline_queue_len - 1].len = len;
memcpy(offline_queue[offline_queue_len - 1].buf, frame, len);
return;
}
pos++;
}
MESH_DEBUG_PRINTLN("INFO: no channel messages to remove from queue.");
} else {
offline_queue[offline_queue_len].len = len;
memcpy(offline_queue[offline_queue_len].buf, frame, len);
offline_queue_len++;
}
}
int MyMesh::getFromOfflineQueue(uint8_t frame[]) {
if (offline_queue_len > 0) { // check offline queue
size_t len = offline_queue[0].len; // take from top of queue
@@ -211,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;
}
@@ -232,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;
@@ -243,12 +345,12 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
}
} else {
#ifdef DISPLAY_CLASS
if (_ui) _ui->soundBuzzer(UIEventType::newContactMessage);
if (_ui) _ui->notify(UIEventType::newContactMessage);
#endif
}
// 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
@@ -265,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) {
@@ -294,7 +395,7 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
bool MyMesh::processAck(const uint8_t *data) {
ContactInfo* MyMesh::processAck(const uint8_t *data) {
// see if matches any in a table
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) {
if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient
@@ -306,7 +407,7 @@ bool MyMesh::processAck(const uint8_t *data) {
// NOTE: the same ACK can be received multiple times!
expected_ack_table[i].ack = 0; // clear expected hash, now that we have received ACK
return true;
return expected_ack_table[i].contact;
}
}
return checkConnectionsAck(data);
@@ -353,12 +454,45 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
if (should_display && _ui) {
_ui->newMsg(path_len, from.name, text, offline_queue_len);
if (!_serial->isConnected()) {
_ui->soundBuzzer(UIEventType::contactMessage);
_ui->notify(UIEventType::contactMessage);
}
}
#endif
}
bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
// REVISIT: try to determine which Region (from transport_codes[1]) that Sender is indicating for replies/responses
// if unknown, fallback to finding Region from transport_codes[0], the 'scope' used by Sender
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, _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, _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, _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, _prefs.path_hash_mode + 1);
}
}
void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) {
markConnectionActive(from); // in case this is from a server, and we have a connection
@@ -412,7 +546,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
_serial->writeFrame(frame, 1);
} else {
#ifdef DISPLAY_CLASS
if (_ui) _ui->soundBuzzer(UIEventType::channelMessage);
if (_ui) _ui->notify(UIEventType::channelMessage);
#endif
}
#ifdef DISPLAY_CLASS
@@ -496,6 +630,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
memcpy(&out_frame[i], &tag, 4);
i += 4; // NEW: include server timestamp
out_frame[i++] = data[7]; // NEW (v7): ACL permissions
out_frame[i++] = data[12]; // FIRMWARE_VER_LEVEL
} else {
out_frame[i++] = PUSH_CODE_LOGIN_FAIL;
out_frame[i++] = 0; // reserved
@@ -551,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;
@@ -560,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);
@@ -576,6 +709,26 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len);
}
void MyMesh::onControlDataRecv(mesh::Packet *packet) {
if (packet->payload_len + 4 > sizeof(out_frame)) {
MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len);
return;
}
int i = 0;
out_frame[i++] = PUSH_CODE_CONTROL_DATA;
out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4);
out_frame[i++] = (int8_t)(_radio->getLastRSSI());
out_frame[i++] = packet->path_len;
memcpy(&out_frame[i], packet->payload, packet->payload_len);
i += packet->payload_len;
if (_serial->isConnected()) {
_serial->writeFrame(out_frame, i);
} else {
MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline");
}
}
void MyMesh::onRawDataRecv(mesh::Packet *packet) {
if (packet->payload_len + 4 > sizeof(out_frame)) {
MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len);
@@ -598,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
@@ -609,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()) {
@@ -624,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() {}
@@ -643,6 +803,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
sign_data = NULL;
dirty_contacts_expiry = 0;
memset(advert_paths, 0, sizeof(advert_paths));
memset(send_scope.key, 0, sizeof(send_scope.key));
// defaults
memset(&_prefs, 0, sizeof(_prefs));
@@ -653,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
}
@@ -669,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
@@ -686,10 +849,12 @@ void MyMesh::begin(bool has_display) {
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
_prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f);
_prefs.freq = constrain(_prefs.freq, 400.0f, 2500.0f);
_prefs.bw = constrain(_prefs.bw, 62.5f, 500.0f);
_prefs.sf = constrain(_prefs.sf, 7, 12);
_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) {
@@ -712,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);
@@ -729,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();
@@ -752,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
@@ -813,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 {
@@ -825,6 +1012,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (expected_ack) {
expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); // add to circular table
expected_ack_table[next_ack_idx].ack = expected_ack;
expected_ack_table[next_ack_idx].contact = recipient;
next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE;
}
@@ -927,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);
}
@@ -939,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();
@@ -971,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 {
@@ -1053,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);
@@ -1073,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();
@@ -1115,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();
@@ -1142,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();
@@ -1194,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);
@@ -1227,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) {
@@ -1372,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) {
@@ -1428,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);
@@ -1447,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();
@@ -1464,6 +1768,39 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
} else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE && len >= 2 && cmd_frame[1] == 0) {
if (len >= 2 + 16) {
memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set curr scope TransportKey
} else {
memset(send_scope.key, 0, sizeof(send_scope.key)); // set scope to null
}
writeOKFrame();
} else if (cmd_frame[0] == CMD_SEND_CONTROL_DATA && len >= 2 && (cmd_frame[1] & 0x80) != 0) {
auto resp = createControlData(&cmd_frame[1], len - 1);
if (resp) {
sendZeroHop(resp);
writeOKFrame();
} 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]);
@@ -1524,33 +1861,72 @@ void MyMesh::checkCLIRescueCmd() {
// get path from command e.g: "ls /adafruit"
const char *path = &cli_command[3];
bool is_fs2 = false;
if (memcmp(path, "UserData/", 9) == 0) {
path += 8; // skip "UserData"
} else if (memcmp(path, "ExtraFS/", 8) == 0) {
path += 7; // skip "ExtraFS"
is_fs2 = true;
}
Serial.printf("Listing files in %s\n", path);
// log each file and directory
File root = _store->openRead(path);
if(root){
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.printf("[dir] %s\n", file.name());
} else {
Serial.printf("[file] %s (%d bytes)\n", file.name(), file.size());
if (is_fs2 == false) {
if (root) {
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.printf("[dir] UserData%s/%s\n", path, file.name());
} else {
Serial.printf("[file] UserData%s/%s (%d bytes)\n", path, file.name(), file.size());
}
// move to next file
file = root.openNextFile();
}
// move to next file
file = root.openNextFile();
root.close();
}
root.close();
}
if (is_fs2 == true || strlen(path) == 0 || strcmp(path, "/") == 0) {
if (_store->getSecondaryFS() != nullptr) {
File root2 = _store->openRead(_store->getSecondaryFS(), path);
File file = root2.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.printf("[dir] ExtraFS%s/%s\n", path, file.name());
} else {
Serial.printf("[file] ExtraFS%s/%s (%d bytes)\n", path, file.name(), file.size());
}
// move to next file
file = root2.openNextFile();
}
root2.close();
}
}
} else if (memcmp(cli_command, "cat", 3) == 0) {
// get path from command e.g: "cat /contacts3"
const char *path = &cli_command[4];
bool is_fs2 = false;
if (memcmp(path, "UserData/", 9) == 0) {
path += 8; // skip "UserData"
} else if (memcmp(path, "ExtraFS/", 8) == 0) {
path += 7; // skip "ExtraFS"
is_fs2 = true;
} else {
Serial.println("Invalid path provided, must start with UserData/ or ExtraFS/");
cli_command[0] = 0;
return;
}
// log file content as hex
File file = _store->openRead(path);
if (is_fs2 == true) {
file = _store->openRead(_store->getSecondaryFS(), path);
}
if(file){
// get file content
@@ -1567,17 +1943,30 @@ void MyMesh::checkCLIRescueCmd() {
}
} else if (memcmp(cli_command, "rm ", 3) == 0) {
// get path from command e.g: "rm /adv_blobs"
const char *path = &cli_command[4];
const char *path = &cli_command[3];
MESH_DEBUG_PRINTLN("Removing file: %s", path);
// ensure path is not empty, or root dir
if(!path || strlen(path) == 0 || strcmp(path, "/") == 0){
Serial.println("Invalid path provided");
} else {
bool is_fs2 = false;
if (memcmp(path, "UserData/", 9) == 0) {
path += 8; // skip "UserData"
} else if (memcmp(path, "ExtraFS/", 8) == 0) {
path += 7; // skip "ExtraFS"
is_fs2 = true;
}
// remove file
bool removed = _store->removeFile(path);
bool removed;
if (is_fs2) {
MESH_DEBUG_PRINTLN("Removing file from ExtraFS: %s", path);
removed = _store->removeFile(_store->getSecondaryFS(), path);
} else {
MESH_DEBUG_PRINTLN("Removing file from UserData: %s", path);
removed = _store->removeFile(path);
}
if(removed){
Serial.println("File removed");
} else {
@@ -1618,8 +2007,8 @@ void MyMesh::checkSerialInterface() {
_serial->writeFrame(out_frame, 5);
_iter_started = false;
}
} else if (!_serial->isWriteBusy()) {
checkConnections();
//} else if (!_serial->isWriteBusy()) {
// checkConnections(); // TODO - deprecate the 'Connections' stuff
}
}
@@ -1656,4 +2045,4 @@ bool MyMesh::advert() {
} else {
return false;
}
}
}

View File

@@ -5,14 +5,14 @@
#include "AbstractUITask.h"
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 7
#define FIRMWARE_VER_CODE 10
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "1 Sep 2025"
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.8.1"
#define FIRMWARE_VERSION "v1.13.0"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -68,6 +68,7 @@
#endif
#include <helpers/BaseChatMesh.h>
#include <helpers/TransportKeyStore.h>
/* -------------------------------------------------------------------------------------- */
@@ -105,14 +106,25 @@ 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;
bool processAck(const uint8_t *data) override;
ContactInfo* processAck(const uint8_t *data) override;
void queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *extra, int extra_len, const char *text);
@@ -128,6 +140,7 @@ protected:
uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
uint8_t len, uint8_t *reply) override;
void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) override;
void onControlDataRecv(mesh::Packet *packet) override;
void onRawDataRecv(mesh::Packet *packet) override;
void 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) override;
@@ -146,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);
@@ -163,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;
@@ -191,6 +206,8 @@ private:
uint32_t sign_data_len;
unsigned long dirty_contacts_expiry;
TransportKey send_scope;
uint8_t cmd_frame[MAX_FRAME_SIZE + 1];
uint8_t out_frame[MAX_FRAME_SIZE + 1];
CayenneLPP telemetry;
@@ -198,6 +215,8 @@ private:
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
bool isChannelMsg() const;
};
int offline_queue_len;
Frame offline_queue[OFFLINE_QUEUE_SIZE];
@@ -205,6 +224,7 @@ private:
struct AckTableEntry {
unsigned long msg_sent;
uint32_t ack;
ContactInfo* contact;
};
#define EXPECTED_ACK_TABLE_SIZE 8
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table

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

@@ -14,7 +14,18 @@ static uint32_t _atoi(const char* sp) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
DataStore store(InternalFS, rtc_clock);
#if defined(QSPIFLASH)
#include <CustomLFS_QSPIFlash.h>
DataStore store(InternalFS, QSPIFlash, rtc_clock);
#else
#if defined(EXTRAFS)
#include <CustomLFS.h>
CustomLFS ExtraFS(0xD4000, 0x19000, 128);
DataStore store(InternalFS, ExtraFS, rtc_clock);
#else
DataStore store(InternalFS, rtc_clock);
#endif
#endif
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
DataStore store(LittleFS, rtc_clock);
@@ -118,6 +129,18 @@ void setup() {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
#if defined(QSPIFLASH)
if (!QSPIFlash.begin()) {
// debug output might not be available at this point, might be too early. maybe should fall back to InternalFS here?
MESH_DEBUG_PRINTLN("CustomLFS_QSPIFlash: failed to initialize");
} else {
MESH_DEBUG_PRINTLN("CustomLFS_QSPIFlash: initialized successfully");
}
#else
#if defined(EXTRAFS)
ExtraFS.begin();
#endif
#endif
store.begin();
the_mesh.begin(
#ifdef DISPLAY_CLASS
@@ -128,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
@@ -173,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);
@@ -204,4 +224,5 @@ void loop() {
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
rtc_clock.tick();
}

View File

@@ -2,8 +2,13 @@
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#include "target.h"
#ifdef WIFI_SSID
#include <WiFi.h>
#endif
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#ifndef AUTO_OFF_MILLIS
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#endif
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
#ifdef PIN_STATUS_LED
@@ -18,7 +23,11 @@
#define UI_RECENT_LIST_SIZE 4
#endif
#define PRESS_LABEL "long press"
#if UI_HAS_JOYSTICK
#define PRESS_LABEL "press Enter"
#else
#define PRESS_LABEL "long press"
#endif
#include "icons.h"
@@ -73,6 +82,12 @@ class HomeScreen : public UIScreen {
RADIO,
BLUETOOTH,
ADVERT,
#if ENV_INCLUDE_GPS == 1
GPS,
#endif
#if UI_SENSORS_PAGE == 1
SENSORS,
#endif
SHUTDOWN,
Count // keep as last
};
@@ -85,10 +100,17 @@ class HomeScreen : public UIScreen {
bool _shutdown_init;
AdvertPath recent[UI_RECENT_LIST_SIZE];
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%
@@ -109,11 +131,47 @@ 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;
int sensors_nb = 0;
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();
sensors_nb = 0;
sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
sensors.querySensors(0xFF, sensors_lpp);
LPPReader reader (sensors_lpp.getBuffer(), sensors_lpp.getSize());
uint8_t channel, type;
while(reader.readHeader(channel, type)) {
reader.skipData(type);
sensors_nb ++;
}
sensors_scroll = sensors_nb > UI_RECENT_LIST_SIZE;
#if AUTO_OFF_MILLIS > 0
next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec
#else
next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min
#endif
}
}
public:
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), _shutdown_init(false) { }
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
_shutdown_init(false), sensors_lpp(200) { }
void poll() override {
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
@@ -124,17 +182,19 @@ public:
int render(DisplayDriver& display) override {
char tmp[80];
// node name
display.setCursor(0, 0);
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
display.print(_node_prefs->node_name);
char filtered_name[sizeof(_node_prefs->node_name)];
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
display.setCursor(0, 0);
display.print(filtered_name);
// battery voltage
renderBatteryIndicator(display, _task->getBattMilliVolts());
// curr page indicator
int y = 14;
int x = display.width() / 2 - 25;
int x = display.width() / 2 - 5 * (HomePage::Count-1);
for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) {
if (i == _page) {
display.fillRect(x-1, y-1, 3, 3);
@@ -149,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);
@@ -166,8 +233,6 @@ public:
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) {
auto a = &recent[i];
if (a->name[0] == 0) continue; // empty slot
display.setCursor(0, y);
display.print(a->name);
int secs = _rtc->getCurrentTime() - a->recv_timestamp;
if (secs < 60) {
sprintf(tmp, "%ds", secs);
@@ -176,7 +241,14 @@ public:
} else {
sprintf(tmp, "%dh", secs / (60*60));
}
display.setCursor(display.width() - display.getTextWidth(tmp) - 1, y);
int timestamp_width = display.getTextWidth(tmp);
int max_name_width = display.width() - timestamp_width - 1;
char filtered_recent_name[sizeof(a->name)];
display.translateUTF8ToBlocks(filtered_recent_name, a->name, sizeof(filtered_recent_name));
display.drawTextEllipsized(0, y, max_name_width, filtered_recent_name);
display.setCursor(display.width() - timestamp_width - 1, y);
display.print(tmp);
}
} else if (_page == HomePage::RADIO) {
@@ -200,8 +272,8 @@ public:
display.print(tmp);
} else if (_page == HomePage::BLUETOOTH) {
display.setColor(DisplayDriver::GREEN);
display.drawXbm((display.width() - 32) / 2, 18,
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off,
display.drawXbm((display.width() - 32) / 2, 18,
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off,
32, 32);
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL);
@@ -209,6 +281,117 @@ public:
display.setColor(DisplayDriver::GREEN);
display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL);
#if ENV_INCLUDE_GPS == 1
} else if (_page == HomePage::GPS) {
LocationProvider* nmea = sensors.getLocationProvider();
char buf[50];
int y = 18;
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 {
strcpy(buf, nmea->isValid()?"fix":"no fix");
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
display.drawTextLeftAlign(0, y, "sat");
sprintf(buf, "%d", nmea->satellitesCount());
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
display.drawTextLeftAlign(0, y, "pos");
sprintf(buf, "%.4f %.4f",
nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.);
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
display.drawTextLeftAlign(0, y, "alt");
sprintf(buf, "%.2f", nmea->getAltitude()/1000.);
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
}
#endif
#if UI_SENSORS_PAGE == 1
} else if (_page == HomePage::SENSORS) {
int y = 18;
refresh_sensors();
char buf[30];
char name[30];
LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize());
for (int i = 0; i < sensors_scroll_offset; i++) {
uint8_t channel, type;
r.readHeader(channel, type);
r.skipData(type);
}
for (int i = 0; i < (sensors_scroll?UI_RECENT_LIST_SIZE:sensors_nb); i++) {
uint8_t channel, type;
if (!r.readHeader(channel, type)) { // reached end, reset
r.reset();
r.readHeader(channel, type);
}
display.setCursor(0, y);
float v;
switch (type) {
case LPP_GPS: // GPS
float lat, lon, alt;
r.readGPS(lat, lon, alt);
strcpy(name, "gps"); sprintf(buf, "%.4f %.4f", lat, lon);
break;
case LPP_VOLTAGE:
r.readVoltage(v);
strcpy(name, "voltage"); sprintf(buf, "%6.2f", v);
break;
case LPP_CURRENT:
r.readCurrent(v);
strcpy(name, "current"); sprintf(buf, "%.3f", v);
break;
case LPP_TEMPERATURE:
r.readTemperature(v);
strcpy(name, "temperature"); sprintf(buf, "%.2f", v);
break;
case LPP_RELATIVE_HUMIDITY:
r.readRelativeHumidity(v);
strcpy(name, "humidity"); sprintf(buf, "%.2f", v);
break;
case LPP_BAROMETRIC_PRESSURE:
r.readPressure(v);
strcpy(name, "pressure"); sprintf(buf, "%.2f", v);
break;
case LPP_ALTITUDE:
r.readAltitude(v);
strcpy(name, "altitude"); sprintf(buf, "%.0f", v);
break;
case LPP_POWER:
r.readPower(v);
strcpy(name, "power"); sprintf(buf, "%6.2f", v);
break;
default:
r.skipData(type);
strcpy(name, "unk"); sprintf(buf, "");
}
display.setCursor(0, y);
display.print(name);
display.setCursor(
display.width()-display.getTextWidth(buf)-1, y
);
display.print(buf);
y = y + 12;
}
if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb;
else sensors_scroll_offset = 0;
#endif
} else if (_page == HomePage::SHUTDOWN) {
display.setColor(DisplayDriver::GREEN);
display.setTextSize(1);
@@ -216,18 +399,18 @@ public:
display.drawTextCentered(display.width() / 2, 34, "hibernating...");
} else {
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate: " PRESS_LABEL);
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
}
}
return 5000; // next render after 5000 ms
}
bool handleInput(char c) override {
if (c == KEY_LEFT) {
if (c == KEY_LEFT || c == KEY_PREV) {
_page = (_page + HomePage::Count - 1) % HomePage::Count;
return true;
}
if (c == KEY_RIGHT || c == KEY_SELECT) {
if (c == KEY_NEXT || c == KEY_RIGHT) {
_page = (_page + 1) % HomePage::Count;
if (_page == HomePage::RECENT) {
_task->showAlert("Recent adverts", 800);
@@ -243,9 +426,7 @@ public:
return true;
}
if (c == KEY_ENTER && _page == HomePage::ADVERT) {
#ifdef PIN_BUZZER
_task->soundBuzzer(UIEventType::ack);
#endif
_task->notify(UIEventType::ack);
if (the_mesh.advert()) {
_task->showAlert("Advert sent!", 1000);
} else {
@@ -253,6 +434,19 @@ public:
}
return true;
}
#if ENV_INCLUDE_GPS == 1
if (c == KEY_ENTER && _page == HomePage::GPS) {
_task->toggleGPS();
return true;
}
#endif
#if UI_SENSORS_PAGE == 1
if (c == KEY_ENTER && _page == HomePage::SENSORS) {
_task->toggleGPS();
next_sensors_refresh=0;
return true;
}
#endif
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
_shutdown_init = true; // need to wait for button to be released
return true;
@@ -272,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);
@@ -298,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) {
@@ -315,25 +511,29 @@ public:
display.setCursor(0, 14);
display.setColor(DisplayDriver::YELLOW);
display.print(p->origin);
char filtered_origin[sizeof(p->origin)];
display.translateUTF8ToBlocks(filtered_origin, p->origin, sizeof(filtered_origin));
display.print(filtered_origin);
display.setCursor(0, 25);
display.setColor(DisplayDriver::LIGHT);
display.printWordWrap(p->msg, display.width());
char filtered_msg[sizeof(p->msg)];
display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg));
display.printWordWrap(filtered_msg, display.width());
#if AUTO_OFF_MILLIS==0 // probably e-ink
return 10000; // 10 s
#else
return 1000; // next render after 1000 ms
#endif
}
bool handleInput(char c) override {
if (c == KEY_SELECT || c == KEY_RIGHT) {
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;
}
@@ -359,12 +559,30 @@ 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
vibration.begin();
#endif
ui_started_at = millis();
@@ -381,9 +599,9 @@ void UITask::showAlert(const char* text, int duration_millis) {
_alert_expiry = millis() + duration_millis;
}
void UITask::soundBuzzer(UIEventType bet) {
void UITask::notify(UIEventType t) {
#if defined(PIN_BUZZER)
switch(bet){
switch(t){
case UIEventType::contactMessage:
// gemini's pick
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7");
@@ -401,8 +619,16 @@ switch(bet){
break;
}
#endif
#ifdef PIN_VIBRATION
// Trigger vibration for all UI events except none
if (t != UIEventType::none) {
vibration.trigger();
}
#endif
}
void UITask::msgRead(int msgcount) {
_msgcount = msgcount;
if (msgcount == 0) {
@@ -417,44 +643,44 @@ 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 = 0; // trigger refresh
_next_refresh = 100; // trigger refresh
}
}
}
void UITask::userLedHandler() {
#ifdef PIN_STATUS_LED
static int state = 0;
static int next_change = 0;
static int last_increment = 0;
int cur_time = millis();
if (cur_time > next_change) {
if (state == 0) {
state = 1;
if (cur_time > next_led_change) {
if (led_state == 0) {
led_state = 1;
if (_msgcount > 0) {
last_increment = LED_ON_MSG_MILLIS;
last_led_increment = LED_ON_MSG_MILLIS;
} else {
last_increment = LED_ON_MILLIS;
last_led_increment = LED_ON_MILLIS;
}
next_change = cur_time + last_increment;
next_led_change = cur_time + last_led_increment;
} else {
state = 0;
next_change = cur_time + LED_CYCLE_MILLIS - last_increment;
led_state = 0;
next_led_change = cur_time + LED_CYCLE_MILLIS - last_led_increment;
}
digitalWrite(PIN_STATUS_LED, state);
digitalWrite(PIN_STATUS_LED, led_state == LED_STATE_ON);
}
#endif
}
void UITask::setCurrScreen(UIScreen* c) {
curr = c;
_next_refresh = 0;
_next_refresh = 100;
}
/*
hardware-agnostic pre-shutdown activity should be done here
/*
hardware-agnostic pre-shutdown activity should be done here
*/
void UITask::shutdown(bool restart){
@@ -475,6 +701,7 @@ void UITask::shutdown(bool restart){
_board->reboot();
} else {
_display->turnOff();
radio_driver.powerOff();
_board->powerOff();
}
}
@@ -489,15 +716,13 @@ bool UITask::isButtonPressed() const {
void UITask::loop() {
char c = 0;
#if defined(PIN_USER_BTN)
#if UI_HAS_JOYSTICK
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_SELECT);
c = checkDisplayOn(KEY_ENTER);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
c = handleLongPress(KEY_ENTER); // REVISIT: could be mapped to different key code
}
#endif
#if defined(WIO_TRACKER_L1)
ev = joystick_left.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_LEFT);
@@ -510,20 +735,53 @@ void UITask::loop() {
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_RIGHT);
}
#endif
#if defined(PIN_USER_BTN_ANA)
ev = analog_btn.check();
ev = back_btn.check();
if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
#elif defined(PIN_USER_BTN)
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_SELECT);
c = checkDisplayOn(KEY_NEXT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
c = handleDoubleClick(KEY_PREV);
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
#endif
#if defined(PIN_USER_BTN_ANA)
if (abs(millis() - _analogue_pin_read_millis) > 10) {
ev = analog_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_NEXT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
c = handleDoubleClick(KEY_PREV);
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
_analogue_pin_read_millis = millis();
}
#endif
#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
if (c != 0 && curr) {
curr->handleInput(c);
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 0; // trigger refresh
_next_refresh = 100; // trigger refresh
}
userLedHandler();
@@ -553,11 +811,17 @@ void UITask::loop() {
}
_display->endFrame();
}
#if AUTO_OFF_MILLIS > 0
if (millis() > _auto_off) {
_display->turnOff();
}
#endif
}
#ifdef PIN_VIBRATION
vibration.loop();
#endif
#ifdef AUTO_SHUTDOWN_MILLIVOLTS
if (millis() > next_batt_chck) {
uint16_t milliVolts = getBattMilliVolts();
@@ -565,7 +829,7 @@ void UITask::loop() {
// show low battery shutdown alert
// we should only do this for eink displays, which will persist after power loss
#ifdef THINKNODE_M1
#if defined(THINKNODE_M1) || defined(LILYGO_TECHO)
if (_display != NULL) {
_display->startFrame();
_display->setTextSize(2);
@@ -604,20 +868,68 @@ char UITask::handleLongPress(char c) {
return c;
}
/*
void UITask::handleButtonTriplePress() {
MESH_DEBUG_PRINTLN("UITask: triple press triggered");
// Toggle buzzer quiet mode
char UITask::handleDoubleClick(char c) {
MESH_DEBUG_PRINTLN("UITask: double click triggered");
checkDisplayOn(c);
return c;
}
char UITask::handleTripleClick(char c) {
MESH_DEBUG_PRINTLN("UITask: triple click triggered");
checkDisplayOn(c);
toggleBuzzer();
c = 0;
return c;
}
bool UITask::getGPSState() {
if (_sensors != NULL) {
int num = _sensors->getNumSettings();
for (int i = 0; i < num; i++) {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
return !strcmp(_sensors->getSettingValue(i), "1");
}
}
}
return false;
}
void UITask::toggleGPS() {
if (_sensors != NULL) {
// toggle GPS on/off
int num = _sensors->getNumSettings();
for (int i = 0; i < num; i++) {
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);
} else {
_sensors->setSettingValue("gps", "1");
_node_prefs->gps_enabled = 1;
notify(UIEventType::ack);
}
the_mesh.savePrefs();
showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800);
_next_refresh = 0;
break;
}
}
}
}
void UITask::toggleBuzzer() {
// Toggle buzzer quiet mode
#ifdef PIN_BUZZER
if (buzzer.isQuiet()) {
buzzer.quiet(false);
soundBuzzer(UIEventType::ack);
showAlert("Buzzer: ON", 600);
notify(UIEventType::ack);
} else {
buzzer.quiet(true);
showAlert("Buzzer: OFF", 600);
}
_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

@@ -6,10 +6,18 @@
#include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h>
#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
#ifdef PIN_VIBRATION
#include <helpers/ui/GenericVibration.h>
#endif
#include "../AbstractUITask.h"
#include "../NodePrefs.h"
@@ -19,6 +27,9 @@ class UITask : public AbstractUITask {
SensorManager* _sensors;
#ifdef PIN_BUZZER
genericBuzzer buzzer;
#endif
#ifdef PIN_VIBRATION
GenericVibration vibration;
#endif
unsigned long _next_refresh, _auto_off;
NodePrefs* _node_prefs;
@@ -26,6 +37,16 @@ class UITask : public AbstractUITask {
unsigned long _alert_expiry;
int _msgcount;
unsigned long ui_started_at, next_batt_chck;
int next_backlight_btn_check = 0;
#ifdef PIN_STATUS_LED
int led_state = 0;
int next_led_change = 0;
int last_led_increment = 0;
#endif
#ifdef PIN_USER_BTN_ANA
unsigned long _analogue_pin_read_millis = millis();
#endif
UIScreen* splash;
UIScreen* home;
@@ -33,10 +54,12 @@ class UITask : public AbstractUITask {
UIScreen* curr;
void userLedHandler();
// Button action handlers
char checkDisplayOn(char c);
char handleLongPress(char c);
char handleDoubleClick(char c);
char handleTripleClick(char c);
void setCurrScreen(UIScreen* c);
@@ -55,10 +78,23 @@ 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();
// from AbstractUITask
void msgRead(int msgcount) override;
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
void soundBuzzer(UIEventType bet = UIEventType::none) override;
void notify(UIEventType t = UIEventType::none) override;
void loop() override;
void shutdown(bool restart = false);

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
@@ -88,9 +89,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
ui_started_at = millis();
}
void UITask::soundBuzzer(UIEventType bet) {
void UITask::notify(UIEventType t) {
#if defined(PIN_BUZZER)
switch(bet){
switch(t){
case UIEventType::contactMessage:
// gemini's pick
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7");
@@ -108,8 +109,8 @@ switch(bet){
break;
}
#endif
// Serial.print("DBG: Buzzzzzz -> ");
// Serial.println((int) bet);
// Serial.print("DBG: Alert user -> ");
// Serial.println((int) t);
}
void UITask::msgRead(int msgcount) {
@@ -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() {
@@ -370,7 +383,7 @@ void UITask::handleButtonDoublePress() {
MESH_DEBUG_PRINTLN("UITask: double press triggered, sending advert");
// ADVERT
#ifdef PIN_BUZZER
soundBuzzer(UIEventType::ack);
notify(UIEventType::ack);
#endif
if (the_mesh.advert()) {
MESH_DEBUG_PRINTLN("Advert sent!");
@@ -388,12 +401,14 @@ void UITask::handleButtonTriplePress() {
#ifdef PIN_BUZZER
if (buzzer.isQuiet()) {
buzzer.quiet(false);
soundBuzzer(UIEventType::ack);
notify(UIEventType::ack);
sprintf(_alert, "Buzzer: ON");
} else {
buzzer.quiet(true);
sprintf(_alert, "Buzzer: OFF");
}
_node_prefs->buzzer_quiet = buzzer.isQuiet();
the_mesh.savePrefs();
_need_refresh = true;
#endif
}
@@ -407,11 +422,11 @@ void UITask::handleButtonQuadruplePress() {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
_sensors->setSettingValue("gps", "0");
soundBuzzer(UIEventType::ack);
notify(UIEventType::ack);
sprintf(_alert, "GPS: Disabled");
} else {
_sensors->setSettingValue("gps", "1");
soundBuzzer(UIEventType::ack);
notify(UIEventType::ack);
sprintf(_alert, "GPS: Enabled");
}
break;

View File

@@ -66,7 +66,7 @@ public:
// from AbstractUITask
void msgRead(int msgcount) override;
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
void soundBuzzer(UIEventType bet = UIEventType::none) override;
void notify(UIEventType t = UIEventType::none) override;
void loop() override;
void shutdown(bool restart = false);

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();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,241 @@
#pragma once
#include <Arduino.h>
#include <Mesh.h>
#include <RTClib.h>
#include <target.h>
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#ifdef WITH_RS232_BRIDGE
#include "helpers/bridges/RS232Bridge.h"
#define WITH_BRIDGE
#endif
#ifdef WITH_ESPNOW_BRIDGE
#include "helpers/bridges/ESPNowBridge.h"
#define WITH_BRIDGE
#endif
#include <helpers/AdvertDataHelpers.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/ClientACL.h>
#include <helpers/CommonCLI.h>
#include <helpers/IdentityStore.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/StatsFormatHelper.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/RegionMap.h>
#include "RateLimiter.h"
#ifdef WITH_BRIDGE
extern AbstractBridge* bridge;
#endif
struct RepeaterStats {
uint16_t batt_milli_volts;
uint16_t curr_tx_queue_len;
int16_t noise_floor;
int16_t last_rssi;
uint32_t n_packets_recv;
uint32_t n_packets_sent;
uint32_t total_air_time_secs;
uint32_t total_up_time_secs;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
uint16_t err_events; // was 'n_full_events'
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
#define MAX_CLIENTS 32
#endif
struct NeighbourInfo {
mesh::Identity id;
uint32_t advert_timestamp;
uint32_t heard_timestamp;
int8_t snr; // multiplied by 4, user should divide to get float value
};
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.13.0"
#endif
#define FIRMWARE_ROLE "repeater"
#define PACKET_LOG_FILE "/packet_log"
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
FILESYSTEM* _fs;
uint32_t last_millis;
uint64_t uptime_millis;
unsigned long next_local_advert, next_flood_advert;
bool _logging;
NodePrefs _prefs;
ClientACL acl;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
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, anon_limiter;
uint32_t pending_discover_tag;
unsigned long pending_discover_until;
bool region_load_active;
unsigned long dirty_contacts_expiry;
#if MAX_NEIGHBOURS
NeighbourInfo neighbours[MAX_NEIGHBOURS];
#endif
CayenneLPP telemetry;
unsigned long set_radio_at, revert_radio_at;
float pending_freq;
float pending_bw;
uint8_t pending_sf;
uint8_t pending_cr;
int matching_peer_indexes[MAX_CLIENTS];
#if defined(WITH_RS232_BRIDGE)
RS232Bridge bridge;
#elif defined(WITH_ESPNOW_BRIDGE)
ESPNowBridge bridge;
#endif
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
void sendNodeDiscoverReq();
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t 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();
File openAppend(const char* fname);
protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
}
bool allowPacketForward(const mesh::Packet* packet) override;
const char* getLogDateTime() override;
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
void logRx(mesh::Packet* pkt, int len, float score) override;
void logTx(mesh::Packet* pkt, int len) override;
void logTxFail(mesh::Packet* pkt, int len) 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;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
}
int getAGCResetInterval() const override {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
uint8_t getExtraAckTransmitCount() const override {
return _prefs.multi_acks;
}
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
}
#endif
bool filterRecvFloodPacket(mesh::Packet* pkt) override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len);
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
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;
public:
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
void begin(FILESYSTEM* fs);
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getRole() override { return FIRMWARE_ROLE; }
const char* getNodeName() { return _prefs.node_name; }
NodePrefs* getNodePrefs() {
return &_prefs;
}
void savePrefs() override {
_cli.savePrefs(_fs);
}
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;
void setLoggingOn(bool enable) override { _logging = enable; }
void eraseLogFile() override {
_fs->remove(PACKET_LOG_FILE);
}
void dumpLogFile() 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;
void formatRadioStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
mesh::LocalIdentity& getSelfId() override { return self_id; }
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override;
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
void loop();
#if defined(WITH_BRIDGE)
void setBridgeState(bool enable) override {
if (enable == bridge.isRunning()) return;
if (enable)
{
bridge.begin();
}
else
{
bridge.end();
}
}
void restartBridge() override {
if (!bridge.isRunning()) return;
bridge.end();
bridge.begin();
}
#endif
// To check if there is pending work
bool hasPendingWork() const;
};

View File

@@ -0,0 +1,23 @@
#pragma once
#include <stdint.h>
class RateLimiter {
uint32_t _start_timestamp;
uint32_t _secs;
uint16_t _maximum, _count;
public:
RateLimiter(uint16_t maximum, uint32_t secs): _maximum(maximum), _secs(secs), _start_timestamp(0), _count(0) { }
bool allow(uint32_t now) {
if (now < _start_timestamp + _secs) {
_count++;
if (_count > _maximum) return false; // deny
} else { // time window now expired
_start_timestamp = now;
_count = 1;
}
return true;
}
};

View File

@@ -1,803 +1,13 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#include <helpers/ArduinoHelpers.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/IdentityStore.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/CommonCLI.h>
#include <RTClib.h>
#include <target.h>
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "1 Sep 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.8.1"
#endif
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#endif
#ifndef LORA_BW
#define LORA_BW 250
#endif
#ifndef LORA_SF
#define LORA_SF 10
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifndef LORA_TX_POWER
#define LORA_TX_POWER 20
#endif
#ifndef ADVERT_NAME
#define ADVERT_NAME "repeater"
#endif
#ifndef ADVERT_LAT
#define ADVERT_LAT 0.0
#endif
#ifndef ADVERT_LON
#define ADVERT_LON 0.0
#endif
#ifndef ADMIN_PASSWORD
#define ADMIN_PASSWORD "password"
#endif
#ifndef SERVER_RESPONSE_DELAY
#define SERVER_RESPONSE_DELAY 300
#endif
#ifndef TXT_ACK_DELAY
#define TXT_ACK_DELAY 200
#endif
#include "MyMesh.h"
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(display);
#endif
#define FIRMWARE_ROLE "repeater"
#define PACKET_LOG_FILE "/packet_log"
/* ------------------------------ Code -------------------------------- */
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
struct RepeaterStats {
uint16_t batt_milli_volts;
uint16_t curr_tx_queue_len;
int16_t noise_floor;
int16_t last_rssi;
uint32_t n_packets_recv;
uint32_t n_packets_sent;
uint32_t total_air_time_secs;
uint32_t total_up_time_secs;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
uint16_t err_events; // was 'n_full_events'
int16_t last_snr; // x 4
uint16_t n_direct_dups, n_flood_dups;
uint32_t total_rx_air_time_secs;
};
struct ClientInfo {
mesh::Identity id;
uint32_t last_timestamp, last_activity;
uint8_t secret[PUB_KEY_SIZE];
bool is_admin;
int8_t out_path_len;
uint8_t out_path[MAX_PATH_SIZE];
};
#ifndef MAX_CLIENTS
#define MAX_CLIENTS 32
#endif
struct NeighbourInfo {
mesh::Identity id;
uint32_t advert_timestamp;
uint32_t heard_timestamp;
int8_t snr; // multiplied by 4, user should divide to get float value
};
#define CLI_REPLY_DELAY_MILLIS 600
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
FILESYSTEM* _fs;
unsigned long next_local_advert, next_flood_advert;
bool _logging;
NodePrefs _prefs;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
ClientInfo known_clients[MAX_CLIENTS];
#if MAX_NEIGHBOURS
NeighbourInfo neighbours[MAX_NEIGHBOURS];
#endif
CayenneLPP telemetry;
unsigned long set_radio_at, revert_radio_at;
float pending_freq;
float pending_bw;
uint8_t pending_sf;
uint8_t pending_cr;
ClientInfo* putClient(const mesh::Identity& id) {
uint32_t min_time = 0xFFFFFFFF;
ClientInfo* oldest = &known_clients[0];
for (int i = 0; i < MAX_CLIENTS; i++) {
if (known_clients[i].last_activity < min_time) {
oldest = &known_clients[i];
min_time = oldest->last_activity;
}
if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known
}
oldest->id = id;
oldest->out_path_len = -1; // initially out_path is unknown
oldest->last_timestamp = 0;
return oldest;
}
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr) {
#if MAX_NEIGHBOURS // check if neighbours enabled
// find existing neighbour, else use least recently updated
uint32_t oldest_timestamp = 0xFFFFFFFF;
NeighbourInfo* neighbour = &neighbours[0];
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
// if neighbour already known, we should update it
if (id.matches(neighbours[i].id)) {
neighbour = &neighbours[i];
break;
}
// otherwise we should update the least recently updated neighbour
if (neighbours[i].heard_timestamp < oldest_timestamp) {
neighbour = &neighbours[i];
oldest_timestamp = neighbour->heard_timestamp;
}
}
// update neighbour info
neighbour->id = id;
neighbour->advert_timestamp = timestamp;
neighbour->heard_timestamp = getRTCClock()->getCurrentTime();
neighbour->snr = (int8_t) (snr * 4);
#endif
}
int 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
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
switch (payload[0]) {
case REQ_TYPE_GET_STATUS: { // guests can also access this now
RepeaterStats stats;
stats.batt_milli_volts = board.getBattMilliVolts();
stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF);
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
stats.last_rssi = (int16_t) radio_driver.getLastRSSI();
stats.n_packets_recv = radio_driver.getPacketsRecv();
stats.n_packets_sent = radio_driver.getPacketsSent();
stats.total_air_time_secs = getTotalAirTime() / 1000;
stats.total_up_time_secs = _ms->getMillis() / 1000;
stats.n_sent_flood = getNumSentFlood();
stats.n_sent_direct = getNumSentDirect();
stats.n_recv_flood = getNumRecvFlood();
stats.n_recv_direct = getNumRecvDirect();
stats.err_events = _err_flags;
stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4);
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
stats.total_rx_air_time_secs = getReceiveAirTime() / 1000;
memcpy(&reply_data[4], &stats, sizeof(stats));
return 4 + sizeof(stats); // reply_len
}
case REQ_TYPE_GET_TELEMETRY_DATA: {
uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors((sender->is_admin ? 0xFF : 0x00) & perm_mask, telemetry);
uint8_t tlen = telemetry.getSize();
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
return 4 + tlen; // reply_len
}
}
return 0; // unknown command
}
mesh::Packet* createSelfAdvert() {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
{
AdvertDataBuilder builder(ADV_TYPE_REPEATER, _prefs.node_name, _prefs.node_lat, _prefs.node_lon);
app_data_len = builder.encodeTo(app_data);
}
return createAdvert(self_id, app_data, app_data_len);
}
File openAppend(const char* fname) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
return _fs->open(fname, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return _fs->open(fname, "a");
#else
return _fs->open(fname, "a", true);
#endif
}
protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
}
bool allowPacketForward(const mesh::Packet* packet) override {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
return true;
}
const char* getLogDateTime() override {
static char tmp[32];
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), dt.year());
return tmp;
}
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override {
#if MESH_PACKET_LOGGING
Serial.print(getLogDateTime());
Serial.print(" RAW: ");
mesh::Utils::printHex(Serial, raw, len);
Serial.println();
#endif
}
void logRx(mesh::Packet* pkt, int len, float score) override {
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d",
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
(int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score*1000));
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
} else {
f.printf("\n");
}
f.close();
}
}
}
void logTx(mesh::Packet* pkt, int len) override {
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)",
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
} else {
f.printf("\n");
}
f.close();
}
}
}
void logTxFail(mesh::Packet* pkt, int len) override {
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n",
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
f.close();
}
}
}
int calcRxDelay(float score, uint32_t air_time) const override {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
}
uint32_t getRetransmitDelay(const mesh::Packet* packet) override {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
}
int getAGCResetInterval() const override {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
uint8_t getExtraAckTransmitCount() const override {
return _prefs.multi_acks;
}
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override {
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
uint32_t timestamp;
memcpy(&timestamp, data, 4);
bool is_admin;
data[len] = 0; // ensure null terminator
if (strcmp((char *) &data[4], _prefs.password) == 0) { // check for valid password
is_admin = true;
} else if (strcmp((char *) &data[4], _prefs.guest_password) == 0) { // check guest password
is_admin = false;
} else {
#if MESH_DEBUG
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
#endif
return;
}
auto client = putClient(sender); // add to known clients (if not already known)
if (timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("Possible login replay attack!");
return; // FATAL: client table is full -OR- replay attack
}
MESH_DEBUG_PRINTLN("Login success!");
client->last_timestamp = timestamp;
client->last_activity = getRTCClock()->getCurrentTime();
client->is_admin = is_admin;
memcpy(client->secret, secret, PUB_KEY_SIZE);
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
#if 0
memcpy(&reply_data[4], "OK", 2); // legacy response
#else
reply_data[4] = RESP_SERVER_LOGIN_OK;
reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16)
reply_data[6] = is_admin ? 1 : 0;
reply_data[7] = 0; // FUTURE: reserved
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
#endif
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(sender, client->secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, 12);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 12);
if (reply) {
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply, SERVER_RESPONSE_DELAY);
}
}
}
}
}
int matching_peer_indexes[MAX_CLIENTS];
int searchPeersByHash(const uint8_t* hash) override {
int n = 0;
for (int i = 0; i < MAX_CLIENTS; i++) {
if (known_clients[i].id.isHashMatch(hash)) {
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
}
}
return n;
}
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override {
int i = matching_peer_indexes[peer_idx];
if (i >= 0 && i < MAX_CLIENTS) {
// lookup pre-calculated shared_secret
memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE);
} else {
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
}
}
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) {
mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl
// if this a zero hop advert, add it to neighbours
if (packet->path_len == 0) {
AdvertDataParser parser(app_data, app_data_len);
if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters
putNeighbour(id, timestamp, packet->getSNR());
}
}
}
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context)
MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i);
return;
}
auto client = &known_clients[i];
if (type == PAYLOAD_TYPE_REQ) { // request (from a Known admin client!)
uint32_t timestamp;
memcpy(&timestamp, data, 4);
if (timestamp > client->last_timestamp) { // prevent replay attacks
int reply_len = handleRequest(client, timestamp, &data[4], len - 4);
if (reply_len == 0) return; // invalid command
client->last_timestamp = timestamp;
client->last_activity = getRTCClock()->getCurrentTime();
if (packet->isRouteFlood()) {
// 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);
} 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
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply, SERVER_RESPONSE_DELAY);
}
}
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
}
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->is_admin) { // 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
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks
bool is_retry = (sender_timestamp == client->last_timestamp);
client->last_timestamp = sender_timestamp;
client->last_activity = getRTCClock()->getCurrentTime();
// 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
if (flags == TXT_TYPE_PLAIN) { // for legacy CLI, send Acks
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key, PUB_KEY_SIZE);
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (client->out_path_len < 0) {
sendFlood(ack, TXT_ACK_DELAY);
} else {
sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
}
}
}
uint8_t temp[166];
char *command = (char *) &data[5];
char *reply = (char *) &temp[5];
if (is_retry) {
*reply = 0;
} else {
handleCommand(sender_timestamp, command, reply);
}
int text_len = strlen(reply);
if (text_len > 0) {
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
if (timestamp == sender_timestamp) {
// WORKAROUND: the two timestamps need to be different, in the CLI view
timestamp++;
}
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (TXT_TYPE_CLI_DATA << 2); // NOTE: legacy was: TXT_TYPE_PLAIN
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);
} else {
sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS);
}
}
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
}
}
}
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 {
// TODO: prevent replay attacks
int i = matching_peer_indexes[sender_idx];
if (i >= 0 && i < MAX_CLIENTS) { // 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 = &known_clients[i];
memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
} else {
MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
}
// NOTE: no reciprocal path send!!
return false;
}
public:
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, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
{
memset(known_clients, 0, sizeof(known_clients));
next_local_advert = next_flood_advert = 0;
set_radio_at = revert_radio_at = 0;
_logging = false;
#if MAX_NEIGHBOURS
memset(neighbours, 0, sizeof(neighbours));
#endif
// defaults
memset(&_prefs, 0, sizeof(_prefs));
_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
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
_prefs.node_lat = ADVERT_LAT;
_prefs.node_lon = ADVERT_LON;
StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password));
_prefs.freq = LORA_FREQ;
_prefs.sf = LORA_SF;
_prefs.bw = LORA_BW;
_prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 12; // 12 hours
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
}
void begin(FILESYSTEM* fs) {
mesh::Mesh::begin();
_fs = fs;
// load persisted prefs
_cli.loadPrefs(_fs);
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
updateAdvertTimer();
updateFloodAdvertTimer();
}
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getRole() override { return FIRMWARE_ROLE; }
const char* getNodeName() { return _prefs.node_name; }
NodePrefs* getNodePrefs() {
return &_prefs;
}
void savePrefs() override {
_cli.savePrefs(_fs);
}
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override {
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
pending_freq = freq;
pending_bw = bw;
pending_sf = sf;
pending_cr = cr;
revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params
}
bool formatFileSystem() override {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
return InternalFS.format();
#elif defined(RP2040_PLATFORM)
return LittleFS.format();
#elif defined(ESP32)
return SPIFFS.format();
#else
#error "need to implement file system erase"
return false;
#endif
}
void sendSelfAdvertisement(int delay_millis) override {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) {
sendFlood(pkt, delay_millis);
} else {
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
}
}
void updateAdvertTimer() override {
if (_prefs.advert_interval > 0) { // schedule local advert timer
next_local_advert = futureMillis( ((uint32_t)_prefs.advert_interval) * 2 * 60 * 1000);
} else {
next_local_advert = 0; // stop the timer
}
}
void updateFloodAdvertTimer() override {
if (_prefs.flood_advert_interval > 0) { // schedule flood advert timer
next_flood_advert = futureMillis( ((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000);
} else {
next_flood_advert = 0; // stop the timer
}
}
void setLoggingOn(bool enable) override { _logging = enable; }
void eraseLogFile() override {
_fs->remove(PACKET_LOG_FILE);
}
void dumpLogFile() override {
#if defined(RP2040_PLATFORM)
File f = _fs->open(PACKET_LOG_FILE, "r");
#else
File f = _fs->open(PACKET_LOG_FILE);
#endif
if (f) {
while (f.available()) {
int c = f.read();
if (c < 0) break;
Serial.print((char)c);
}
f.close();
}
}
void setTxPower(uint8_t power_dbm) override {
radio_set_tx_power(power_dbm);
}
void formatNeighborsReply(char *reply) override {
char *dp = reply;
#if MAX_NEIGHBOURS
for (int i = 0; i < MAX_NEIGHBOURS && dp - reply < 134; i++) {
NeighbourInfo* neighbour = &neighbours[i];
if (neighbour->heard_timestamp == 0) continue; // skip empty slots
// add new line if not first item
if (i > 0) *dp++ = '\n';
char hex[10];
// get 4 bytes of neighbour id as hex
mesh::Utils::toHex(hex, neighbour->id.pub_key, 4);
// add next neighbour
uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
sprintf(dp, "%s:%d:%d", hex, secs_ago, neighbour->snr);
while (*dp) dp++; // find end of string
}
#endif
if (dp == reply) { // no neighbours, need empty response
strcpy(dp, "-none-"); dp += 6;
}
*dp = 0; // null terminator
}
void removeNeighbor(const uint8_t* pubkey, int key_len) override {
#if MAX_NEIGHBOURS
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
NeighbourInfo* neighbour = &neighbours[i];
if(memcmp(neighbour->id.pub_key, pubkey, key_len) == 0){
neighbours[i] = NeighbourInfo(); // clear neighbour entry
}
}
#endif
}
mesh::LocalIdentity& getSelfId() override { return self_id; }
void saveIdentity(const mesh::LocalIdentity& new_id) override {
self_id = new_id;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
#elif defined(ESP32)
IdentityStore store(*_fs, "/identity");
#elif defined(RP2040_PLATFORM)
IdentityStore store(*_fs, "/identity");
#else
#error "need to define saveIdentity()"
#endif
store.save("_main", self_id);
}
void clearStats() override {
radio_driver.resetStats();
resetStats();
((SimpleMeshTables *)getTables())->resetStats();
}
void handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
while (*command == ' ') command++; // skip leading spaces
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
memcpy(reply, command, 3); // reflect the prefix back
reply += 3;
command += 3;
}
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
void loop() {
mesh::Mesh::loop();
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) sendFlood(pkt);
updateFloodAdvertTimer(); // schedule next flood advert
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
} else if (next_local_advert && millisHasNowPassed(next_local_advert)) {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) sendZeroHop(pkt);
updateAdvertTimer(); // schedule next local advert
}
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
set_radio_at = 0; // clear timer
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
MESH_DEBUG_PRINTLN("Temp radio params");
}
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
}
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
}
};
StdRNG fast_rng;
SimpleMeshTables tables;
@@ -809,21 +19,38 @@ 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();
display.setCursor(0, 0);
display.print("Please wait...");
display.endFrame();
}
#endif
if (!radio_init()) { halt(); }
if (!radio_init()) {
MESH_DEBUG_PRINTLN("Radio init failed!");
halt();
}
fast_rng.begin(radio_get_rng_seed());
@@ -867,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() {
@@ -878,14 +107,16 @@ void loop() {
if (c != '\n') {
command[len++] = c;
command[len] = 0;
Serial.print(c);
}
Serial.print(c);
if (c == '\r') break;
}
if (len == sizeof(command)-1) { // command buffer full
command[sizeof(command)-1] = '\r';
}
if (len > 0 && command[len - 1] == '\r') { // received complete line
Serial.print('\n');
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
@@ -898,4 +129,22 @@ void loop() {
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
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

@@ -0,0 +1,896 @@
#include "MyMesh.h"
#define REPLY_DELAY_MILLIS 1500
#define PUSH_NOTIFY_DELAY_MILLIS 2000
#define SYNC_PUSH_INTERVAL 1200
#define PUSH_ACK_TIMEOUT_FLOOD 12000
#define PUSH_TIMEOUT_BASE 4000
#define PUSH_ACK_TIMEOUT_FACTOR 2000
#define POST_SYNC_DELAY_SECS 6
#define FIRMWARE_VER_LEVEL 1
#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 RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
#define LAZY_CONTACTS_WRITE_DELAY 5000
struct ServerStats {
uint16_t batt_milli_volts;
uint16_t curr_tx_queue_len;
int16_t noise_floor;
int16_t last_rssi;
uint32_t n_packets_recv;
uint32_t n_packets_sent;
uint32_t total_air_time_secs;
uint32_t total_up_time_secs;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
uint16_t err_events; // was 'n_full_events'
int16_t last_snr; // x 4
uint16_t n_direct_dups, n_flood_dups;
uint16_t n_posted, n_post_push;
};
void MyMesh::addPost(ClientInfo *client, const char *postData) {
// TODO: suggested postData format: <title>/<descrption>
posts[next_post_idx].author = client->id; // add to cyclic queue
StrHelper::strncpy(posts[next_post_idx].text, postData, MAX_POST_TEXT_LEN);
posts[next_post_idx].post_timestamp = getRTCClock()->getCurrentTimeUnique();
next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS;
next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS);
_num_posted++; // stats
}
void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) {
int len = 0;
memcpy(&reply_data[len], &post.post_timestamp, 4);
len += 4; // this is a PAST timestamp... but should be accepted by client
uint8_t attempt;
getRNG()->random(&attempt, 1); // need this for re-tries, so packet hash (and ACK) will be different
reply_data[len++] = (TXT_TYPE_SIGNED_PLAIN << 2) | (attempt & 3); // 'signed' plain text
// encode prefix of post.author.pub_key
memcpy(&reply_data[len], post.author.pub_key, 4);
len += 4; // just first 4 bytes
int text_len = strlen(post.text);
memcpy(&reply_data[len], post.text, text_len);
len += text_len;
// calc expected ACK reply
mesh::Utils::sha256((uint8_t *)&client->extra.room.pending_ack, 4, reply_data, len, client->id.pub_key, PUB_KEY_SIZE);
client->extra.room.push_post_timestamp = post.post_timestamp;
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len);
if (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);
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 {
client->extra.room.pending_ack = 0;
MESH_DEBUG_PRINTLN("Unable to push post to client");
}
}
uint8_t MyMesh::getUnsyncedCount(ClientInfo *client) {
uint8_t count = 0;
for (int k = 0; k < MAX_UNSYNCED_POSTS; k++) {
if (posts[k].post_timestamp > client->extra.room.sync_since // is new post for this Client?
&& !posts[k].author.matches(client->id)) { // don't push posts to the author
count++;
}
}
return count;
}
bool MyMesh::processAck(const uint8_t *data) {
for (int i = 0; i < acl.getNumClients(); i++) {
auto client = acl.getClientByIdx(i);
if (client->extra.room.pending_ack && memcmp(data, &client->extra.room.pending_ack, 4) == 0) { // got an ACK from Client!
client->extra.room.pending_ack = 0; // clear this, so next push can happen
client->extra.room.push_failures = 0;
client->extra.room.sync_since = client->extra.room.push_post_timestamp; // advance Client's SINCE timestamp, to sync next post
return true;
}
}
return false;
}
mesh::Packet *MyMesh::createSelfAdvert() {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len = _cli.buildAdvertData(ADV_TYPE_ROOM, app_data);
return createAdvert(self_id, app_data, app_data_len);
}
File MyMesh::openAppend(const char *fname) {
#if defined(NRF52_PLATFORM)
return _fs->open(fname, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return _fs->open(fname, "a");
#else
return _fs->open(fname, "a", true);
#endif
}
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
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
if (payload[0] == REQ_TYPE_GET_STATUS) {
ServerStats stats;
stats.batt_milli_volts = board.getBattMilliVolts();
stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF);
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
stats.last_rssi = (int16_t)radio_driver.getLastRSSI();
stats.n_packets_recv = radio_driver.getPacketsRecv();
stats.n_packets_sent = radio_driver.getPacketsSent();
stats.total_air_time_secs = getTotalAirTime() / 1000;
stats.total_up_time_secs = uptime_millis / 1000;
stats.n_sent_flood = getNumSentFlood();
stats.n_sent_direct = getNumSentDirect();
stats.n_recv_flood = getNumRecvFlood();
stats.n_recv_direct = getNumRecvDirect();
stats.err_events = _err_flags;
stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4);
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
stats.n_posted = _num_posted;
stats.n_post_push = _num_post_pushes;
memcpy(&reply_data[4], &stats, sizeof(stats));
return 4 + sizeof(stats);
}
if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA) {
uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
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);
uint8_t tlen = telemetry.getSize();
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
return 4 + tlen; // reply_len
}
if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) {
uint8_t res1 = payload[1]; // reserved for future (extra query params)
uint8_t res2 = payload[2];
if (res1 == 0 && res2 == 0) {
uint8_t ofs = 4;
for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) {
auto c = acl.getClientByIdx(i);
if (!c->isAdmin()) continue; // skip non-Admin entries
memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix
reply_data[ofs++] = c->permissions;
}
return ofs;
}
}
return 0; // unknown command
}
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
#if MESH_PACKET_LOGGING
Serial.print(getLogDateTime());
Serial.print(" RAW: ");
mesh::Utils::printHex(Serial, raw, len);
Serial.println();
#endif
}
void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d", len,
pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
(int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score * 1000));
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ ||
pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
} else {
f.printf("\n");
}
f.close();
}
}
}
void MyMesh::logTx(mesh::Packet *pkt, int len) {
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", len, pkt->getPayloadType(),
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ ||
pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
} else {
f.printf("\n");
}
f.close();
}
}
}
void MyMesh::logTxFail(mesh::Packet *pkt, int len) {
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n", len, pkt->getPayloadType(),
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
f.close();
}
}
}
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
}
const char *MyMesh::getLogDateTime() {
static char tmp[32];
uint32_t now = getRTCClock()->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(),
dt.year());
return tmp;
}
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
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->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->getPathHashCount() >= _prefs.flood_max) return false;
return true;
}
void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender,
uint8_t *data, size_t len) {
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin
// client (unknown at this stage)
uint32_t sender_timestamp, sender_sync_since;
memcpy(&sender_timestamp, data, 4);
memcpy(&sender_sync_since, &data[4], 4); // sender's "sync messags SINCE x" timestamp
data[len] = 0; // ensure null terminator
ClientInfo* client = NULL;
if (data[8] == 0) { // blank password, just check if sender is in ACL
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
if (client == NULL) {
#if MESH_DEBUG
MESH_DEBUG_PRINTLN("Login, sender not in ACL");
#endif
}
}
if (client == NULL) {
uint8_t perm;
if (strcmp((char *)&data[8], _prefs.password) == 0) { // check for valid admin password
perm = PERM_ACL_ADMIN;
} else {
if (strcmp((char *)&data[8], _prefs.guest_password) == 0) { // check the room/public password
perm = PERM_ACL_READ_WRITE;
} else if (_prefs.allow_read_only) {
perm = PERM_ACL_GUEST;
} else {
MESH_DEBUG_PRINTLN("Incorrect room password");
return; // no response. Client will timeout
}
}
client = acl.putClient(sender, 0); // add to known clients (if not already known)
if (sender_timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("possible replay attack!");
return;
}
MESH_DEBUG_PRINTLN("Login success!");
client->last_timestamp = sender_timestamp;
client->extra.room.sync_since = sender_sync_since;
client->extra.room.pending_ack = 0;
client->extra.room.push_failures = 0;
client->last_activity = getRTCClock()->getCurrentTime();
client->permissions &= ~0x03;
client->permissions |= perm;
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
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?
reply_data[4] = RESP_SERVER_LOGIN_OK;
reply_data[5] = 0; // Legacy: was recommended keep-alive interval (secs / 16)
reply_data[6] = (client->isAdmin() ? 1 : (client->permissions == 0 ? 2 : 0));
// LEGACY: reply_data[7] = getUnsyncedCount(client);
reply_data[7] = client->permissions; // NEW
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
reply_data[12] = FIRMWARE_VER_LEVEL; // New field
next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); // delay next push, give RESPONSE packet time to arrive first
if (packet->isRouteFlood()) {
// 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, packet->getPathHashSize());
} else {
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13);
if (reply) {
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, packet->getPathHashSize());
}
}
}
}
}
int MyMesh::searchPeersByHash(const uint8_t *hash) {
int n = 0;
for (int i = 0; i < acl.getNumClients(); i++) {
if (acl.getClientByIdx(i)->id.isHashMatch(hash)) {
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
}
}
return n;
}
void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) {
int i = matching_peer_indexes[peer_idx];
if (i >= 0 && i < acl.getNumClients()) {
// lookup pre-calculated shared_secret
memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE);
} else {
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
}
}
void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret,
uint8_t *data, size_t len) {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context)
MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i);
return;
}
auto client = acl.getClientByIdx(i);
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)
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);
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries
bool is_retry = (sender_timestamp == client->last_timestamp);
client->last_timestamp = sender_timestamp;
uint32_t now = getRTCClock()->getCurrentTimeUnique();
client->last_activity = now;
client->extra.room.push_failures = 0; // reset so push can resume (if prev failed)
// 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
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to
// sender that we got it
mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key,
PUB_KEY_SIZE);
uint8_t temp[166];
bool send_ack;
if (flags == TXT_TYPE_CLI_DATA) {
if (client->isAdmin()) {
if (is_retry) {
temp[5] = 0; // no reply
} else {
handleCommand(sender_timestamp, (char *)&data[5], (char *)&temp[5]);
temp[4] = (TXT_TYPE_CLI_DATA << 2); // attempt and flags, (NOTE: legacy was: TXT_TYPE_PLAIN)
}
send_ack = false;
} else {
temp[5] = 0; // no reply
send_ack = false; // and no ACK... user shoudn't be sending these
}
} else { // TXT_TYPE_PLAIN
if ((client->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) {
temp[5] = 0; // no reply
send_ack = false; // no ACK
} else {
if (!is_retry) {
addPost(client, (const char *)&data[5]);
}
temp[5] = 0; // no reply (ACK is enough)
send_ack = true;
}
}
uint32_t delay_millis;
if (send_ack) {
if (client->out_path_len == OUT_PATH_UNKNOWN) {
mesh::Packet *ack = createAck(ack_hash);
if (ack) sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize());
delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
} else {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
mesh::Packet *a1 = createMultiAck(ack_hash, 1);
if (a1) sendDirect(a1, client->out_path, client->out_path_len, d);
d += 300;
}
mesh::Packet *a2 = createAck(ack_hash);
if (a2) sendDirect(a2, client->out_path, client->out_path_len, d);
delay_millis = d + REPLY_DELAY_MILLIS;
}
} else {
delay_millis = 0;
}
int text_len = strlen((char *)&temp[5]);
if (text_len > 0) {
if (now == sender_timestamp) {
// WORKAROUND: the two timestamps need to be different, in the CLI view
now++;
}
memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique
// calc expected ACK reply
// mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key,
// PUB_KEY_SIZE);
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
if (reply) {
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);
}
}
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
}
} else if (type == PAYLOAD_TYPE_REQ && len >= 5) {
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
if (sender_timestamp < client->last_timestamp) { // prevent replay attacks
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
} else {
client->last_timestamp = sender_timestamp;
uint32_t now = getRTCClock()->getCurrentTime();
client->last_activity = now; // <-- THIS will keep client connection alive
client->extra.room.push_failures = 0; // reset so push can resume (if prev failed)
if (data[4] == REQ_TYPE_KEEP_ALIVE && packet->isRouteDirect()) { // request type
uint32_t forceSince = 0;
if (len >= 9) { // optional - last post_timestamp client received
memcpy(&forceSince, &data[5], 4); // NOTE: this may be 0, if part of decrypted PADDING!
} else {
memcpy(&data[5], &forceSince, 4); // make sure there are zeroes in payload (for ack_hash calc below)
}
if (forceSince > 0) {
client->extra.room.sync_since = forceSince; // force-update the 'sync since'
}
client->extra.room.pending_ack = 0;
// TODO: Throttle KEEP_ALIVE requests!
// if client sends too quickly, evict()
// RULE: only send keep_alive response DIRECT!
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);
auto reply = createAck(ack_hash);
if (reply) {
reply->payload[reply->payload_len++] = getUnsyncedCount(client); // NEW: add unsynced counter to end of ACK packet
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
}
}
} else {
int reply_len = handleRequest(client, sender_timestamp, &data[4], len - 4);
if (reply_len > 0) { // valid command
if (packet->isRouteFlood()) {
// 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, packet->getPathHashSize());
} else {
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
if (reply) {
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, packet->getPathHashSize());
}
}
}
}
}
}
}
}
bool MyMesh::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) {
// TODO: prevent replay attacks
int i = matching_peer_indexes[sender_idx];
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);
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);
}
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
// also got an encoded ACK!
processAck(extra);
}
// NOTE: no reciprocal path send!!
return false;
}
void MyMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) {
if (processAck((uint8_t *)&ack_crc)) {
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
}
}
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, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
last_millis = 0;
uptime_millis = 0;
next_local_advert = next_flood_advert = 0;
dirty_contacts_expiry = 0;
_logging = false;
set_radio_at = revert_radio_at = 0;
// defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0; // one half
_prefs.rx_delay_base = 0.0f; // off by default, was 10.0
_prefs.tx_delay_factor = 0.5f; // was 0.25f;
_prefs.direct_tx_delay_factor = 0.2f; // was zero
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
_prefs.node_lat = ADVERT_LAT;
_prefs.node_lon = ADVERT_LON;
StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password));
_prefs.freq = LORA_FREQ;
_prefs.sf = LORA_SF;
_prefs.bw = LORA_BW;
_prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.disable_fwd = 1;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 12; // 12 hours
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
#ifdef ROOM_PASSWORD
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
#endif
// GPS defaults
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
next_post_idx = 0;
next_client_idx = 0;
next_push = 0;
memset(posts, 0, sizeof(posts));
_num_posted = _num_post_pushes = 0;
}
void MyMesh::begin(FILESYSTEM *fs) {
mesh::Mesh::begin();
_fs = fs;
// load persisted prefs
_cli.loadPrefs(_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);
updateAdvertTimer();
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
#endif
}
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
pending_freq = freq;
pending_bw = bw;
pending_sf = sf;
pending_cr = cr;
revert_radio_at = futureMillis(2000 + timeout_mins * 60 * 1000); // schedule when to revert radio params
}
bool MyMesh::formatFileSystem() {
#if defined(NRF52_PLATFORM)
return InternalFS.format();
#elif defined(RP2040_PLATFORM)
return LittleFS.format();
#elif defined(ESP32)
return SPIFFS.format();
#else
#error "need to implement file system erase"
return false;
#endif
}
void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet *pkt = createSelfAdvert();
if (pkt) {
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!");
}
}
void MyMesh::updateAdvertTimer() {
if (_prefs.advert_interval > 0) { // schedule local advert timer
next_local_advert = futureMillis((uint32_t)_prefs.advert_interval * 2 * 60 * 1000);
} else {
next_local_advert = 0; // stop the timer
}
}
void MyMesh::updateFloodAdvertTimer() {
if (_prefs.flood_advert_interval > 0) { // schedule flood advert timer
next_flood_advert = futureMillis(((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000);
} else {
next_flood_advert = 0; // stop the timer
}
}
void MyMesh::dumpLogFile() {
#if defined(RP2040_PLATFORM)
File f = _fs->open(PACKET_LOG_FILE, "r");
#else
File f = _fs->open(PACKET_LOG_FILE);
#endif
if (f) {
while (f.available()) {
int c = f.read();
if (c < 0) break;
Serial.print((char)c);
}
f.close();
}
}
void MyMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
#elif defined(ESP32)
IdentityStore store(*_fs, "/identity");
#elif defined(RP2040_PLATFORM)
IdentityStore store(*_fs, "/identity");
#else
#error "need to define saveIdentity()"
#endif
store.save("_main", new_id);
}
void MyMesh::clearStats() {
radio_driver.resetStats();
resetStats();
((SimpleMeshTables *)getTables())->resetStats();
}
void MyMesh::formatStatsReply(char *reply) {
StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
}
void MyMesh::formatRadioStatsReply(char *reply) {
StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime());
}
void MyMesh::formatPacketStatsReply(char *reply) {
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
getNumRecvFlood(), getNumRecvDirect());
}
void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
while (*command == ' ')
command++; // skip leading spaces
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
memcpy(reply, command, 3); // reflect the prefix back
reply += 3;
command += 3;
}
// handle ACL related commands
if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8}
char* hex = &command[8];
char* sp = strchr(hex, ' '); // look for separator char
if (sp == NULL) {
strcpy(reply, "Err - bad params");
} else {
*sp++ = 0; // replace space with null terminator
uint8_t pubkey[PUB_KEY_SIZE];
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
uint8_t perms = atoi(sp);
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - invalid params");
}
} else {
strcpy(reply, "Err - bad pubkey");
}
}
} else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
Serial.println("ACL:");
for (int i = 0; i < acl.getNumClients(); i++) {
auto c = acl.getClientByIdx(i);
if (c->permissions == 0) continue; // skip deleted (or guest) entries
Serial.printf("%02X ", c->permissions);
mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
Serial.printf("\n");
}
reply[0] = 0;
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
}
bool MyMesh::saveFilter(ClientInfo* client) {
return client->isAdmin(); // only save Admins
}
void MyMesh::loop() {
mesh::Mesh::loop();
if (millisHasNowPassed(next_push) && acl.getNumClients() > 0) {
// check for ACK timeouts
for (int i = 0; i < acl.getNumClients(); i++) {
auto c = acl.getClientByIdx(i);
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->extra.room.push_failures);
}
}
// check next Round-Robin client, and sync next new post
auto client = acl.getClientByIdx(next_client_idx);
bool did_push = false;
if (client->extra.room.pending_ack == 0 && client->last_activity != 0 &&
client->extra.room.push_failures < 3) { // not already waiting for ACK, AND not evicted, AND retries not max
MESH_DEBUG_PRINTLN("loop - checking for client %02X", (uint32_t)client->id.pub_key[0]);
uint32_t now = getRTCClock()->getCurrentTime();
for (int k = 0, idx = next_post_idx; k < MAX_UNSYNCED_POSTS; k++) {
auto p = &posts[idx];
if (now >= p->post_timestamp + POST_SYNC_DELAY_SECS &&
p->post_timestamp > client->extra.room.sync_since // is new post for this Client?
&& !p->author.matches(client->id)) { // don't push posts to the author
// push this post to Client, then wait for ACK
pushPostToClient(client, *p);
did_push = true;
MESH_DEBUG_PRINTLN("loop - pushed to client %02X: %s", (uint32_t)client->id.pub_key[0], p->text);
break;
}
idx = (idx + 1) % MAX_UNSYNCED_POSTS; // wrap to start of cyclic queue
}
} else {
MESH_DEBUG_PRINTLN("loop - skipping busy (or evicted) client %02X", (uint32_t)client->id.pub_key[0]);
}
next_client_idx = (next_client_idx + 1) % acl.getNumClients(); // round robin polling for each client
if (did_push) {
next_push = futureMillis(SYNC_PUSH_INTERVAL);
} else {
// were no unsynced posts for curr client, so proccess next client much quicker! (in next loop())
next_push = futureMillis(SYNC_PUSH_INTERVAL / 8);
}
}
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
mesh::Packet *pkt = createSelfAdvert();
if (pkt) sendFlood(pkt);
updateFloodAdvertTimer(); // schedule next flood advert
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
} else if (next_local_advert && millisHasNowPassed(next_local_advert)) {
mesh::Packet *pkt = createSelfAdvert();
if (pkt) sendZeroHop(pkt);
updateAdvertTimer(); // schedule next local advert
}
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
set_radio_at = 0; // clear timer
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
MESH_DEBUG_PRINTLN("Temp radio params");
}
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
}
// is pending dirty contacts write needed?
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
acl.save(_fs, MyMesh::saveFilter);
dirty_contacts_expiry = 0;
}
// TODO: periodically check for OLD/inactive entries in known_clients[], and evict
// update uptime
uint32_t now = millis();
uptime_millis += now - last_millis;
last_millis = now;
}

View File

@@ -0,0 +1,208 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#if defined(NRF52_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#include <helpers/ArduinoHelpers.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/IdentityStore.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/CommonCLI.h>
#include <helpers/StatsFormatHelper.h>
#include <helpers/ClientACL.h>
#include <RTClib.h>
#include <target.h>
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.13.0"
#endif
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#endif
#ifndef LORA_BW
#define LORA_BW 250
#endif
#ifndef LORA_SF
#define LORA_SF 10
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifndef LORA_TX_POWER
#define LORA_TX_POWER 20
#endif
#ifndef ADVERT_NAME
#define ADVERT_NAME "Test BBS"
#endif
#ifndef ADVERT_LAT
#define ADVERT_LAT 0.0
#endif
#ifndef ADVERT_LON
#define ADVERT_LON 0.0
#endif
#ifndef ADMIN_PASSWORD
#define ADMIN_PASSWORD "password"
#endif
#ifndef MAX_UNSYNCED_POSTS
#define MAX_UNSYNCED_POSTS 32
#endif
#ifndef SERVER_RESPONSE_DELAY
#define SERVER_RESPONSE_DELAY 300
#endif
#ifndef TXT_ACK_DELAY
#define TXT_ACK_DELAY 200
#endif
#define FIRMWARE_ROLE "room_server"
#define PACKET_LOG_FILE "/packet_log"
#define MAX_POST_TEXT_LEN (160-9)
struct PostInfo {
mesh::Identity author;
uint32_t post_timestamp; // by OUR clock
char text[MAX_POST_TEXT_LEN+1];
};
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
FILESYSTEM* _fs;
uint32_t last_millis;
uint64_t uptime_millis;
unsigned long next_local_advert, next_flood_advert;
bool _logging;
NodePrefs _prefs;
ClientACL acl;
CommonCLI _cli;
unsigned long dirty_contacts_expiry;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
unsigned long next_push;
uint16_t _num_posted, _num_post_pushes;
int next_client_idx; // for round-robin polling
int next_post_idx;
PostInfo posts[MAX_UNSYNCED_POSTS]; // cyclic queue
CayenneLPP telemetry;
unsigned long set_radio_at, revert_radio_at;
float pending_freq;
float pending_bw;
uint8_t pending_sf;
uint8_t pending_cr;
int matching_peer_indexes[MAX_CLIENTS];
void addPost(ClientInfo* client, const char* postData);
void pushPostToClient(ClientInfo* client, PostInfo& post);
uint8_t getUnsyncedCount(ClientInfo* client);
bool processAck(const uint8_t *data);
mesh::Packet* createSelfAdvert();
File openAppend(const char* fname);
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
}
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
void logRx(mesh::Packet* pkt, int len, float score) override;
void logTx(mesh::Packet* pkt, int len) override;
void logTxFail(mesh::Packet* pkt, int len) override;
int calcRxDelay(float score, uint32_t air_time) const override;
const char* getLogDateTime() override;
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
}
int getAGCResetInterval() const override {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
uint8_t getExtraAckTransmitCount() const override {
return _prefs.multi_acks;
}
bool allowPacketForward(const mesh::Packet* packet) override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override ;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
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 onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
}
#endif
public:
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
void begin(FILESYSTEM* fs);
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getRole() override { return FIRMWARE_ROLE; }
const char* getNodeName() { return _prefs.node_name; }
NodePrefs* getNodePrefs() {
return &_prefs;
}
void savePrefs() override {
_cli.savePrefs(_fs);
}
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;
void setLoggingOn(bool enable) override { _logging = enable; }
void eraseLogFile() override {
_fs->remove(PACKET_LOG_FILE);
}
void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
mesh::LocalIdentity& getSelfId() override { return self_id; }
static bool saveFilter(ClientInfo* client);
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override;
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
void loop();
};

File diff suppressed because it is too large Load Diff

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,22 +213,22 @@ 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();
}
bool processAck(const uint8_t *data) override {
ContactInfo* processAck(const uint8_t *data) override {
if (memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient
Serial.printf(" Got ACK! (round trip: %d millis)\n", _ms->getMillis() - last_msg_sent);
// NOTE: the same ACK can be received multiple times!
expected_ack_crc = 0; // reset our expected hash, now that we have received ACK
return true;
return NULL; // TODO: really should return ContactInfo pointer
}
//uint32_t crc;
//memcpy(&crc, data, 4);
//MESH_DEBUG_PRINTLN("unknown ACK received: %08X (expected: %08X)", crc, expected_ack_crc);
return false;
return NULL;
}
void onMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override {
@@ -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;
@@ -548,7 +549,7 @@ public:
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(radio_driver, fast_rng, *new VolatileRTCClock(), tables); // TODO: test with 'rtc_clock' in target.cpp
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables);
void halt() {
while (1) ;
@@ -582,9 +583,12 @@ 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() {
the_mesh.loop();
rtc_clock.tick();
}

View File

@@ -46,6 +46,8 @@
/* ------------------------------ Code -------------------------------- */
#define FIRMWARE_VER_LEVEL 1
#define REQ_TYPE_LOGIN 0x00
#define REQ_TYPE_GET_STATUS 0x01
#define REQ_TYPE_KEEP_ALIVE 0x02
@@ -71,78 +73,6 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) {
#endif
}
static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
return _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "w");
#else
return _fs->open(filename, "w", true);
#endif
}
void SensorMesh::loadContacts() {
num_contacts = 0;
if (_fs->exists("/s_contacts")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/s_contacts", "r");
#else
File file = _fs->open("/s_contacts");
#endif
if (file) {
bool full = false;
while (!full) {
ContactInfo c;
uint8_t pub_key[32];
uint8_t unused[6];
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
success = success && (file.read(unused, 6) == 6);
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);
c.last_timestamp = 0; // transient
c.last_activity = 0;
if (!success) break; // EOF
c.id = mesh::Identity(pub_key);
if (num_contacts < MAX_CONTACTS) {
contacts[num_contacts++] = c;
} else {
full = true;
}
}
file.close();
}
}
}
void SensorMesh::saveContacts() {
File file = openWrite(_fs, "/s_contacts");
if (file) {
uint8_t unused[5];
memset(unused, 0, sizeof(unused));
for (int i = 0; i < num_contacts; i++) {
auto c = &contacts[i];
if (c->permissions == 0) continue; // skip deleted entries
bool success = (file.write(c->id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *) &c->permissions, 1) == 1);
success = success && (file.write(unused, 6) == 6);
success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
success = success && (file.write(c->out_path, 64) == 64);
success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
if (!success) break; // write failed
}
file.close();
}
}
static uint8_t getDataSize(uint8_t type) {
switch (type) {
case LPP_GPS:
@@ -295,8 +225,8 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
uint8_t res2 = payload[1];
if (res1 == 0 && res2 == 0) {
uint8_t ofs = 4;
for (int i = 0; i < num_contacts && ofs + 7 <= sizeof(reply_data) - 4; i++) {
auto c = &contacts[i];
for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) {
auto c = acl.getClientByIdx(i);
if (c->permissions == 0) continue; // skip deleted entries
memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix
reply_data[ofs++] = c->permissions;
@@ -309,72 +239,12 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
mesh::Packet* SensorMesh::createSelfAdvert() {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
{
AdvertDataBuilder builder(ADV_TYPE_SENSOR, _prefs.node_name, _prefs.node_lat, _prefs.node_lon);
app_data_len = builder.encodeTo(app_data);
}
uint8_t app_data_len = _cli.buildAdvertData(ADV_TYPE_SENSOR, app_data);
return createAdvert(self_id, app_data, app_data_len);
}
ContactInfo* SensorMesh::getContact(const uint8_t* pubkey, int key_len) {
for (int i = 0; i < num_contacts; i++) {
if (memcmp(pubkey, contacts[i].id.pub_key, key_len) == 0) return &contacts[i]; // already known
}
return NULL; // not found
}
ContactInfo* SensorMesh::putContact(const mesh::Identity& id, uint8_t init_perms) {
uint32_t min_time = 0xFFFFFFFF;
ContactInfo* oldest = &contacts[MAX_CONTACTS - 1];
for (int i = 0; i < num_contacts; i++) {
if (id.matches(contacts[i].id)) return &contacts[i]; // already known
if (!contacts[i].isAdmin() && contacts[i].last_activity < min_time) {
oldest = &contacts[i];
min_time = oldest->last_activity;
}
}
ContactInfo* c;
if (num_contacts < MAX_CONTACTS) {
c = &contacts[num_contacts++];
} else {
c = oldest; // evict least active contact
}
memset(c, 0, sizeof(*c));
c->permissions = init_perms;
c->id = id;
c->out_path_len = -1; // initially out_path is unknown
return c;
}
bool SensorMesh::applyContactPermissions(const uint8_t* pubkey, int key_len, uint8_t perms) {
ContactInfo* c;
if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts
c = getContact(pubkey, key_len);
if (c == NULL) return false; // partial pubkey not found
num_contacts--; // delete from contacts[]
int i = c - contacts;
while (i < num_contacts) {
contacts[i] = contacts[i + 1];
i++;
}
} else {
if (key_len < PUB_KEY_SIZE) return false; // need complete pubkey when adding/modifying
mesh::Identity id(pubkey);
c = putContact(id, 0);
c->permissions = perms; // update their permissions
self_id.calcSharedSecret(c->shared_secret, pubkey);
}
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger saveContacts()
return true;
}
void SensorMesh::sendAlert(ContactInfo* c, Trigger* t) {
void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
int text_len = strlen(t->text);
uint8_t data[MAX_PACKET_PAYLOAD];
@@ -388,10 +258,11 @@ void SensorMesh::sendAlert(ContactInfo* 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);
@@ -432,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;
}
@@ -442,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 {
@@ -456,10 +327,10 @@ 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) {
ContactInfo* client;
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 = getContact(sender.pub_key, PUB_KEY_SIZE);
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
if (client == NULL) {
#if MESH_DEBUG
MESH_DEBUG_PRINTLN("Login, sender not in ACL");
@@ -474,7 +345,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
return 0;
}
client = putContact(sender, PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO); // add to contacts (if not already known)
client = acl.putClient(sender, PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO); // add to contacts (if not already known)
if (sender_timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("Possible login replay attack!");
return 0; // FATAL: client table is full -OR- replay attack
@@ -489,15 +360,20 @@ 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;
reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16)
reply_data[5] = 0;
reply_data[6] = client->isAdmin() ? 1 : 0;
reply_data[7] = client->permissions;
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
reply_data[12] = FIRMWARE_VER_LEVEL;
return 12; // reply length
return 13; // reply length
}
void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
@@ -527,7 +403,8 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
uint8_t perms = atoi(sp);
if (applyContactPermissions(pubkey, hex_len / 2, perms)) {
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - invalid params");
@@ -538,8 +415,8 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
}
} else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
Serial.println("ACL:");
for (int i = 0; i < num_contacts; i++) {
auto c = &contacts[i];
for (int i = 0; i < acl.getNumClients(); i++) {
auto c = acl.getClientByIdx(i);
if (c->permissions == 0) continue; // skip deleted entries
Serial.printf("%02X ", c->permissions);
@@ -577,7 +454,14 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
memcpy(&timestamp, data, 4);
data[len] = 0; // ensure null terminator
uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
uint8_t reply_len;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood());
//} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes
// TODO
} else {
reply_len = 0; // unknown request type
}
if (reply_len == 0) return; // invalid request
@@ -585,18 +469,18 @@ 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());
}
}
}
int SensorMesh::searchPeersByHash(const uint8_t* hash) {
int n = 0;
for (int i = 0; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) {
if (contacts[i].id.isHashMatch(hash)) {
for (int i = 0; i < acl.getNumClients() && n < MAX_SEARCH_RESULTS; i++) {
if (acl.getClientByIdx(i)->id.isHashMatch(hash)) {
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
}
}
@@ -605,18 +489,18 @@ int SensorMesh::searchPeersByHash(const uint8_t* hash) {
void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
int i = matching_peer_indexes[peer_idx];
if (i >= 0 && i < num_contacts) {
if (i >= 0 && i < acl.getNumClients()) {
// lookup pre-calculated shared_secret
memcpy(dest_secret, contacts[i].shared_secret, PUB_KEY_SIZE);
memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE);
} else {
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
}
}
void SensorMesh::sendAckTo(const ContactInfo& 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) {
@@ -632,66 +516,66 @@ void SensorMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= num_contacts) {
if (i < 0 || i >= acl.getNumClients()) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i);
return;
}
ContactInfo& from = contacts[i];
ClientInfo* from = acl.getClientByIdx(i);
if (type == PAYLOAD_TYPE_REQ) { // request (from a known contact)
uint32_t timestamp;
memcpy(&timestamp, data, 4);
if (timestamp > from.last_timestamp) { // prevent replay attacks
uint8_t reply_len = handleRequest(from.isAdmin() ? 0xFF : from.permissions, timestamp, data[4], &data[5], len - 5);
if (timestamp > from->last_timestamp) { // prevent replay attacks
uint8_t reply_len = handleRequest(from->isAdmin() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5);
if (reply_len == 0) return; // invalid command
from.last_timestamp = timestamp;
from.last_activity = getRTCClock()->getCurrentTime();
from->last_timestamp = timestamp;
from->last_activity = getRTCClock()->getCurrentTime();
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
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);
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
sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
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());
}
}
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
}
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && from.isAdmin()) { // a CLI command
} 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 (sender_timestamp > from->last_timestamp) { // prevent replay attacks
if (flags == TXT_TYPE_PLAIN) {
bool handled = handleIncomingMsg(from, sender_timestamp, &data[5], flags, len - 5);
bool handled = handleIncomingMsg(*from, sender_timestamp, &data[5], flags, len - 5);
if (handled) { // if msg was handled then send an ack
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), from.id.pub_key, PUB_KEY_SIZE);
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), from->id.pub_key, PUB_KEY_SIZE);
if (packet->isRouteFlood()) {
// 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);
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, packet->getPathHashSize());
} else {
sendAckTo(from, ack_hash);
}
sendAckTo(*from, ack_hash, packet->getPathHashSize());
}
}
} else if (flags == TXT_TYPE_CLI_DATA) {
from.last_timestamp = sender_timestamp;
from.last_activity = getRTCClock()->getCurrentTime();
from->last_timestamp = sender_timestamp;
from->last_activity = getRTCClock()->getCurrentTime();
// 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
@@ -711,12 +595,12 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (TXT_TYPE_CLI_DATA << 2);
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len);
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len);
if (reply) {
if (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);
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
}
}
}
@@ -729,7 +613,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
}
}
bool SensorMesh::handleIncomingMsg(ContactInfo& 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);
@@ -738,23 +622,56 @@ bool SensorMesh::handleIncomingMsg(ContactInfo& from, uint32_t timestamp, uint8_
return false;
}
#define CTL_TYPE_NODE_DISCOVER_REQ 0x80
#define CTL_TYPE_NODE_DISCOVER_RESP 0x90
void SensorMesh::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) {
// TODO: apply rate limiting to these!
int i = 1;
uint8_t filter = packet->payload[i++];
uint32_t tag;
memcpy(&tag, &packet->payload[i], 4); i += 4;
uint32_t since;
if (packet->payload_len >= i+4) { // optional since field
memcpy(&since, &packet->payload[i], 4); i += 4;
} else {
since = 0;
}
if ((filter & (1 << ADV_TYPE_SENSOR)) != 0 && _prefs.discovery_mod_timestamp >= since) {
bool prefix_only = packet->payload[0] & 1;
uint8_t data[6 + PUB_KEY_SIZE];
data[0] = CTL_TYPE_NODE_DISCOVER_RESP | ADV_TYPE_SENSOR; // low 4-bits for node type
data[1] = packet->_snr; // let sender know the inbound SNR ( x 4)
memcpy(&data[2], &tag, 4); // include tag from request, for client to match to
memcpy(&data[6], self_id.pub_key, PUB_KEY_SIZE);
auto resp = createControlData(data, prefix_only ? 6 + 8 : 6 + PUB_KEY_SIZE);
if (resp) {
sendZeroHop(resp, getRetransmitDelay(resp)*4); // apply random delay (widened x4), as multiple nodes can respond to this
}
}
}
}
bool SensorMesh::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) {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= num_contacts) {
if (i < 0 || i >= acl.getNumClients()) {
MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i);
return false;
}
ContactInfo& from = contacts[i];
ClientInfo* from = acl.getClientByIdx(i);
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.last_activity = getRTCClock()->getCurrentTime();
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??
if (from.isAdmin()) {
if (from->isAdmin()) {
// only do saveContacts() (of this out_path change) if this is an admin
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
@@ -779,9 +696,8 @@ 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, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
_cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
{
num_contacts = 0;
next_local_advert = next_flood_advert = 0;
dirty_contacts_expiry = 0;
last_read_time = 0;
@@ -793,6 +709,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
_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
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
_prefs.node_lat = ADVERT_LAT;
_prefs.node_lon = ADVERT_LON;
@@ -807,6 +724,11 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
_prefs.disable_fwd = true;
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
// GPS defaults
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
}
void SensorMesh::begin(FILESYSTEM* fs) {
@@ -815,13 +737,19 @@ void SensorMesh::begin(FILESYSTEM* fs) {
// load persisted prefs
_cli.loadPrefs(_fs);
loadContacts();
acl.load(_fs, self_id);
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
updateAdvertTimer();
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
#endif
}
bool SensorMesh::formatFileSystem() {
@@ -838,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)
@@ -848,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) {
@@ -861,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!");
}
@@ -885,10 +816,23 @@ void SensorMesh::updateFloodAdvertTimer() {
}
}
void SensorMesh::setTxPower(uint8_t power_dbm) {
void SensorMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}
void SensorMesh::formatStatsReply(char *reply) {
StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
}
void SensorMesh::formatRadioStatsReply(char *reply) {
StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime());
}
void SensorMesh::formatPacketStatsReply(char *reply) {
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
getNumRecvFlood(), getNumRecvDirect());
}
float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
auto buf = telemetry.getBuffer();
uint8_t size = telemetry.getSize();
@@ -925,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)
@@ -967,13 +912,13 @@ void SensorMesh::loop() {
if (millisHasNowPassed(t->send_expiry)) { // next send needed?
if (t->attempt >= 4) { // max attempts reached, try next contact
t->curr_contact_idx++;
if (t->curr_contact_idx >= num_contacts) { // no more contacts to try?
if (t->curr_contact_idx >= acl.getNumClients()) { // no more contacts to try?
num_alert_tasks--; // remove t from queue
for (int i = 0; i < num_alert_tasks; i++) {
alert_tasks[i] = alert_tasks[i + 1];
}
} else {
auto c = &contacts[t->curr_contact_idx];
auto c = acl.getClientByIdx(t->curr_contact_idx);
uint16_t pri_mask = (t->pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO;
if (c->permissions & pri_mask) { // contact wants alert
@@ -986,8 +931,8 @@ void SensorMesh::loop() {
// next contact tested in next ::loop()
}
}
} else if (t->curr_contact_idx < num_contacts) {
auto c = &contacts[t->curr_contact_idx]; // send next attempt
} else if (t->curr_contact_idx < acl.getNumClients()) {
auto c = acl.getClientByIdx(t->curr_contact_idx); // send next attempt
sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry
} else {
// contact list has likely been modified while waiting for alert ACK, cancel this task
@@ -998,7 +943,7 @@ void SensorMesh::loop() {
// is there are pending dirty contacts write needed?
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
saveContacts();
acl.save(_fs);
dirty_contacts_expiry = 0;
}
}

View File

@@ -20,15 +20,11 @@
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/CommonCLI.h>
#include <helpers/StatsFormatHelper.h>
#include <helpers/ClientACL.h>
#include <RTClib.h>
#include <target.h>
#define PERM_ACL_ROLE_MASK 3 // lower 2 bits
#define PERM_ACL_GUEST 0
#define PERM_ACL_READ_ONLY 1
#define PERM_ACL_READ_WRITE 2
#define PERM_ACL_ADMIN 3
#define PERM_RESERVED1 (1 << 2)
#define PERM_RESERVED2 (1 << 3)
#define PERM_RESERVED3 (1 << 4)
@@ -36,30 +32,16 @@
#define PERM_RECV_ALERTS_LO (1 << 6) // low priority alerts
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
struct ContactInfo {
mesh::Identity id;
uint8_t permissions;
int8_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)
uint32_t last_activity; // by OUR clock (transient)
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
};
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "1 Sep 2025"
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.8.1"
#define FIRMWARE_VERSION "v1.13.0"
#endif
#define FIRMWARE_ROLE "sensor"
#define MAX_CONTACTS 20
#define MAX_SEARCH_RESULTS 8
#define MAX_CONCURRENT_ALERTS 4
@@ -78,16 +60,19 @@ 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");
}
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
mesh::LocalIdentity& getSelfId() override { return self_id; }
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override { }
@@ -140,17 +125,17 @@ protected:
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
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(ContactInfo& from, uint32_t timestamp, uint8_t* data, uint flags, size_t len);
void sendAckTo(const ContactInfo& 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];
ContactInfo contacts[MAX_CONTACTS];
int num_contacts;
unsigned long dirty_contacts_expiry;
CayenneLPP telemetry;
uint32_t last_read_time;
@@ -163,15 +148,15 @@ private:
uint8_t pending_sf;
uint8_t pending_cr;
void loadContacts();
void saveContacts();
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();
ContactInfo* getContact(const uint8_t* pubkey, int key_len);
ContactInfo* putContact(const mesh::Identity& id, uint8_t init_perms);
bool applyContactPermissions(const uint8_t* pubkey, int key_len, uint8_t perms);
void sendAlert(ContactInfo* c, Trigger* t);
void sendAlert(const ClientInfo* c, Trigger* t);
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
}
#endif
};

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() {
@@ -144,4 +146,5 @@ void loop() {
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
rtc_clock.tick();
}

View File

@@ -1,14 +1,14 @@
{
"name": "MeshCore",
"version" : "1.8.0",
"version" : "1.10.0",
"dependencies": {
"SPI": "*",
"Wire": "*",
"jgromes/RadioLib": "^7.1.2",
"jgromes/RadioLib": "^7.3.0",
"rweather/Crypto": "^0.4.0",
"adafruit/RTClib": "^2.1.3",
"melopero/Melopero RV3028": "^1.1.0",
"electroniccats/CayenneLPP": "1.4.0"
"electroniccats/CayenneLPP": "1.6.1"
},
"build": {
"extraScript": "build_as_lib.py"

View File

@@ -18,15 +18,16 @@ monitor_speed = 115200
lib_deps =
SPI
Wire
jgromes/RadioLib @ ^7.1.2
jgromes/RadioLib @ ^7.3.0
rweather/Crypto @ ^0.4.0
adafruit/RTClib @ ^2.1.3
melopero/Melopero RV3028 @ ^1.1.0
electroniccats/CayenneLPP @ 1.4.0
electroniccats/CayenneLPP @ 1.6.1
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
@@ -47,6 +48,7 @@ build_src_filter =
+<*.cpp>
+<helpers/*.cpp>
+<helpers/radiolib/*.cpp>
+<helpers/bridges/BridgeBase.cpp>
+<helpers/ui/MomentaryButton.cpp>
; ----------------- ESP32 ---------------------
@@ -57,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}
@@ -66,19 +69,28 @@ lib_deps =
file://arch/esp32/AsyncElegantOTA
; esp32c6 uses arduino framework 3.x
; 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 ---------------------
[nrf52_base]
extends = arduino_base
platform = nordicnrf52
platform_packages =
framework-arduinoadafruitnrf52 @ 1.10700.0
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
-D EXTRAFS=1
lib_deps =
${arduino_base.lib_deps}
https://github.com/oltaco/CustomLFS @ 0.2.1
; ----------------- RP2040 ---------------------
[rp2040_base]
@@ -103,6 +115,7 @@ build_src_filter = ${arduino_base.build_src_filter}
+<helpers/stm32>
lib_deps = ${arduino_base.lib_deps}
file://arch/stm32/Adafruit_LittleFS_stm32
adafruit/Adafruit BusIO @ 1.17.2
[sensor_base]
build_flags =
@@ -119,6 +132,8 @@ build_flags =
-D ENV_INCLUDE_INA260=1
-D ENV_INCLUDE_MLX90614=1
-D ENV_INCLUDE_VL53L0X=1
-D ENV_INCLUDE_BME680=1
-D ENV_INCLUDE_BMP085=1
lib_deps =
adafruit/Adafruit INA3221 Library @ ^1.0.1
adafruit/Adafruit INA219 @ ^1.2.3
@@ -133,3 +148,5 @@ lib_deps =
adafruit/Adafruit MLX90614 Library @ ^2.1.5
adafruit/Adafruit_VL53L0X @ ^1.2.4
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit BME680 Library @ ^2.0.4
adafruit/Adafruit BMP085 Library @ ^1.2.4

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);
@@ -92,7 +136,7 @@ void LocalIdentity::sign(uint8_t* sig, const uint8_t* message, int msg_len) cons
ed25519_sign(sig, message, msg_len, pub_key, prv_key);
}
void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) {
void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const {
ed25519_key_exchange(secret, other_pub_key, prv_key);
}

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.
@@ -64,14 +71,21 @@ public:
* \param secret OUT - the 'shared secret' (must be PUB_KEY_SIZE bytes)
* \param other IN - the second party in the exchange.
*/
void calcSharedSecret(uint8_t* secret, const Identity& other) { calcSharedSecret(secret, other.pub_key); }
void calcSharedSecret(uint8_t* secret, const Identity& other) const { calcSharedSecret(secret, other.pub_key); }
/**
* \brief the ECDH key exhange, with Ed25519 public key transposed to Ex25519.
* \param secret OUT - the 'shared secret' (must be PUB_KEY_SIZE bytes)
* \param other_pub_key IN - the public key of second party in the exchange (must be PUB_KEY_SIZE bytes)
*/
void calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key);
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;

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?
@@ -68,8 +64,26 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
return ACTION_RELEASE;
}
if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) {
if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) {
if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 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->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) {
@@ -90,6 +104,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard.
}
if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE;
DispatcherAction action = ACTION_RELEASE;
switch (pkt->getPayloadType()) {
@@ -137,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!)
@@ -201,9 +219,9 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (i + 2 >= pkt->payload_len) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime());
} else if (!_tables->hasSeen(pkt)) {
// scan channels DB, for all matching hashes of 'channel_hash' (max 2 matches supported ATM)
GroupChannel channels[2];
int num = searchChannelsByHash(&channel_hash, channels, 2);
// scan channels DB, for all matching hashes of 'channel_hash' (max 4 matches supported ATM)
GroupChannel channels[4];
int num = searchChannelsByHash(&channel_hash, channels, 4);
// for each matching channel, try to decrypt data
for (int j = 0; j < num; j++) {
// decrypt, checking MAC is valid
@@ -272,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);
@@ -300,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;
}
@@ -332,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);
@@ -355,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);
@@ -365,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);
@@ -418,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) {
@@ -436,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;
@@ -587,15 +604,64 @@ Packet* Mesh::createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags) {
return packet;
}
void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) {
Packet* Mesh::createControlData(const uint8_t* data, size_t len) {
if (len > sizeof(Packet::payload)) return NULL; // invalid arg
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createControlData(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (PAYLOAD_TYPE_CONTROL << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
memcpy(packet->payload, data, len);
packet->payload_len = len;
return packet;
}
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
uint8_t pri;
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 2;
} else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) {
pri = 3; // de-prioritie these
} else {
pri = 1;
}
sendPacket(packet, pri, 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->setPathHashSizeAndCount(path_hash_size, 0);
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
@@ -617,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 {
@@ -645,4 +711,17 @@ void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) {
sendPacket(packet, 0, delay_millis);
}
void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis) {
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_TRANSPORT_DIRECT;
packet->transport_codes[0] = transport_codes[0];
packet->transport_codes[1] = transport_codes[1];
packet->path_len = 0; // path_len of zero means Zero Hop
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
sendPacket(packet, 0, delay_millis);
}
}

View File

@@ -43,6 +43,12 @@ protected:
*/
DispatcherAction routeRecvPacket(Packet* packet);
/**
* \brief Called _before_ the packet is dispatched to the on..Recv() methods.
* \returns true, if given packet should be NOT be processed.
*/
virtual bool filterRecvFloodPacket(Packet* packet) { return false; }
/**
* \brief Check whether this packet should be forwarded (re-transmitted) or not.
* Is sub-classes responsibility to make sure given packet is only transmitted ONCE (by this node)
@@ -128,6 +134,11 @@ protected:
*/
virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { }
/**
* \brief A control packet has been received.
*/
virtual void onControlDataRecv(Packet* packet) { }
/**
* \brief A packet with PAYLOAD_TYPE_RAW_CUSTOM has been received.
*/
@@ -180,11 +191,18 @@ public:
Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
Packet* createRawData(const uint8_t* data, size_t len);
Packet* createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0);
Packet* createControlData(const uint8_t* data, size_t len);
/**
* \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, uint8_t path_hash_size=1);
/**
* \brief send a locally-generated Packet with Direct routing
@@ -196,6 +214,12 @@ public:
*/
void sendZeroHop(Packet* packet, uint32_t delay_millis=0);
/**
* \brief send a locally-generated Packet to just neigbor nodes (zero hops), with specific transort codes
* \param transport_codes array of 2 codes to attach to packet
*/
void sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0);
};
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include <stdint.h>
#include <math.h>
#define MAX_HASH_SIZE 8
#define PUB_KEY_SIZE 32
@@ -28,6 +29,12 @@
#define MESH_DEBUG_PRINTLN(...) {}
#endif
#if BRIDGE_DEBUG && ARDUINO
#define BRIDGE_DEBUG_PRINTLN(F, ...) Serial.printf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__)
#else
#define BRIDGE_DEBUG_PRINTLN(...) {}
#endif
namespace mesh {
#define BD_STARTUP_NORMAL 0 // getStartupReason() codes
@@ -36,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"; }
};
/**
@@ -66,6 +86,11 @@ public:
*/
virtual void setCurrentTime(uint32_t time) = 0;
/**
* override in classes that need to periodically update internal state
*/
virtual void tick() { /* no op */}
uint32_t getCurrentTimeUnique() {
uint32_t t = getCurrentTime();
if (t <= last_unique) {

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

@@ -27,6 +27,7 @@ namespace mesh {
#define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
#define PAYLOAD_TYPE_TRACE 0x09 // trace a path, collecting SNI for each hop
#define PAYLOAD_TYPE_MULTIPART 0x0A // packet is one of a set of packets
#define PAYLOAD_TYPE_CONTROL 0x0B // a control/discovery packet
//...
#define PAYLOAD_TYPE_RAW_CUSTOM 0x0F // custom packet as raw bytes, for applications with custom encryption, payloads, etc
@@ -75,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

@@ -0,0 +1,46 @@
#pragma once
#include <Mesh.h>
class AbstractBridge {
public:
virtual ~AbstractBridge() {}
/**
* @brief Initializes the bridge.
*/
virtual void begin() = 0;
/**
* @brief Stops the bridge.
*/
virtual void end() = 0;
/**
* @brief Gets the current state of the bridge.
*
* @return true if the bridge is initialized and running, false otherwise.
*/
virtual bool isRunning() const = 0;
/**
* @brief A method to be called on every main loop iteration.
* Used for tasks like checking for incoming data.
*/
virtual void loop() = 0;
/**
* @brief A callback that is triggered when the mesh transmits a packet.
* The bridge can use this to forward the packet.
*
* @param packet The packet that was transmitted.
*/
virtual void sendPacket(mesh::Packet* packet) = 0;
/**
* @brief Processes a received packet from the bridge's medium.
*
* @param packet The packet that was received.
*/
virtual void onPacketReceived(mesh::Packet* packet) = 0;
};

View File

@@ -4,11 +4,19 @@
#include <Arduino.h>
class VolatileRTCClock : public mesh::RTCClock {
long millis_offset;
uint32_t base_time;
uint64_t accumulator;
unsigned long prev_millis;
public:
VolatileRTCClock() { millis_offset = 1715770351; } // 15 May 2024, 8:50pm
uint32_t getCurrentTime() override { return (millis()/1000 + millis_offset); }
void setCurrentTime(uint32_t time) override { millis_offset = time - millis()/1000; }
VolatileRTCClock() { base_time = 1715770351; accumulator = 0; prev_millis = millis(); } // 15 May 2024, 8:50pm
uint32_t getCurrentTime() override { return base_time + accumulator/1000; }
void setCurrentTime(uint32_t time) override { base_time = time; accumulator = 0; prev_millis = millis(); }
void tick() override {
unsigned long now = millis();
accumulator += (now - prev_millis);
prev_millis = now;
}
};
class ArduinoMillis : public mesh::MillisecondClock {

View File

@@ -14,4 +14,8 @@ public:
void begin(TwoWire& wire);
uint32_t getCurrentTime() override;
void setCurrentTime(uint32_t time) override;
void tick() override {
_fallback->tick(); // is typically VolatileRTCClock, which now needs tick()
}
};

View File

@@ -9,6 +9,13 @@
#define TXT_ACK_DELAY 200
#endif
void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
sendFlood(pkt, delay_millis);
}
void BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
sendFlood(pkt, delay_millis);
}
mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name) {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
@@ -32,9 +39,9 @@ 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) sendFlood(ack, TXT_ACK_DELAY);
if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY);
} else {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
@@ -48,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())) {
@@ -68,54 +123,48 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
}
// save a copy of raw advert packet (to support "Share..." function)
int plen = packet->writeTo(temp_buf);
putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
bool is_new = false;
int plen;
{
uint8_t save = packet->header;
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_FLOOD; // make sure transport codes are NOT saved
plen = packet->writeTo(temp_buf);
packet->header = save;
}
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
}
@@ -133,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);
}
@@ -152,12 +200,13 @@ 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
if (flags == TXT_TYPE_PLAIN) {
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
onMessageRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
@@ -167,7 +216,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
// 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) sendFloodScoped(from, path, TXT_ACK_DELAY);
} else {
sendAckTo(from, ack_hash);
}
@@ -178,12 +227,13 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra)
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0);
if (path) sendFlood(path);
if (path) sendFloodScoped(from, path);
}
} else if (flags == TXT_TYPE_SIGNED_PLAIN) {
if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date
from.sync_since = timestamp;
}
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
onSignedMessageRecv(from, packet, timestamp, &data[5], (const char *) &data[9]); // let UI know
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + OUR pub_key, to prove to sender that we got it
@@ -193,7 +243,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
// 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) sendFloodScoped(from, path, TXT_ACK_DELAY);
} else {
sendAckTo(from, ack_hash);
}
@@ -209,20 +259,24 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
// 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, temp_buf, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
if (path) sendFloodScoped(from, path, SERVER_RESPONSE_DELAY);
} 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 {
sendFlood(reply, SERVER_RESPONSE_DELAY);
sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY);
}
}
}
}
} else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) {
onContactResponse(from, data, len);
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);
}
}
}
@@ -241,14 +295,14 @@ 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);
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
// also got an encoded ACK!
if (processAck(extra)) {
if (processAck(extra) != NULL) {
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
}
} else if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 0) {
@@ -258,12 +312,25 @@ bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_
}
void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
if (processAck((uint8_t *)&ack_crc)) {
ContactInfo* from;
if ((from = processAck((uint8_t *)&ack_crc)) != NULL) {
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 != 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);
}
}
}
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.getSharedSecret(self_id), path, path_len, 0, NULL, 0);
if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); // 3 second delay
}
#ifdef MAX_GROUP_CHANNELS
int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) {
int n = 0;
@@ -309,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) {
@@ -319,8 +386,8 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
int rc;
if (recipient.out_path_len < 0) {
sendFlood(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
rc = MSG_SEND_SENT_FLOOD;
} else {
@@ -340,13 +407,13 @@ 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) {
sendFlood(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
rc = MSG_SEND_SENT_FLOOD;
} else {
@@ -372,7 +439,7 @@ bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& chan
auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len);
if (pkt) {
sendFlood(pkt);
sendFloodScoped(channel, pkt);
return true;
}
return false;
@@ -386,7 +453,9 @@ bool BaseChatMesh::shareContactZeroHop(const ContactInfo& contact) {
if (packet == NULL) return false; // no Packets available
packet->readFrom(temp_buf, plen); // restore Packet from 'blob'
sendZeroHop(packet);
uint16_t codes[2];
codes[0] = codes[1] = 0; // { 0, 0 } means 'send this nowhere'
sendZeroHop(packet, codes);
return true; // success
}
@@ -427,12 +496,37 @@ 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) {
sendFlood(pkt);
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;
} else {
@@ -454,12 +548,12 @@ 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) {
sendFlood(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
@@ -481,12 +575,12 @@ 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) {
sendFlood(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
@@ -550,7 +644,7 @@ void BaseChatMesh::markConnectionActive(const ContactInfo& contact) {
}
}
bool BaseChatMesh::checkConnectionsAck(const uint8_t* data) {
ContactInfo* BaseChatMesh::checkConnectionsAck(const uint8_t* data) {
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (connections[i].keep_alive_millis > 0 && memcmp(&connections[i].expected_ack, data, 4) == 0) {
// yes, got an ack for our keep_alive request!
@@ -559,10 +653,12 @@ bool BaseChatMesh::checkConnectionsAck(const uint8_t* data) {
// re-schedule next KEEP_ALIVE, now that we have heard from server
connections[i].next_ping = futureMillis(connections[i].keep_alive_millis);
return true; // yes, a match
auto id = &connections[i].server_id;
return lookupContactByPubKey(id->pub_key, PUB_KEY_SIZE); // yes, a match
}
}
return false; /// no match
return NULL; /// no match
}
void BaseChatMesh::checkConnections() {
@@ -587,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;
}
@@ -602,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);
}
@@ -614,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 :-(
@@ -662,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,12 +88,19 @@ 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 bool processAck(const uint8_t *data) = 0;
virtual ContactInfo* processAck(const uint8_t *data) = 0;
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
virtual 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);
virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
@@ -105,6 +112,10 @@ protected:
virtual void onChannelMessageRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t timestamp, const char *text) = 0;
virtual uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) = 0;
virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0;
virtual void handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len);
virtual void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0);
virtual void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0);
// storage concepts, for sub-classes to override/implement
virtual int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { return 0; } // not implemented
@@ -127,7 +138,7 @@ protected:
void stopConnection(const uint8_t* pub_key);
bool hasConnectionTo(const uint8_t* pub_key);
void markConnectionActive(const ContactInfo& contact);
bool checkConnectionsAck(const uint8_t* data);
ContactInfo* checkConnectionsAck(const uint8_t* data);
void checkConnections();
public:
@@ -137,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);

143
src/helpers/ClientACL.cpp Normal file
View File

@@ -0,0 +1,143 @@
#include "ClientACL.h"
static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
return _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "w");
#else
return _fs->open(filename, "w", true);
#endif
}
void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
_fs = fs;
num_clients = 0;
if (_fs->exists("/s_contacts")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/s_contacts", "r");
#else
File file = _fs->open("/s_contacts");
#endif
if (file) {
bool full = false;
while (!full) {
ClientInfo c;
uint8_t pub_key[32];
uint8_t unused[2];
memset(&c, 0, sizeof(c));
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
success = success && (file.read((uint8_t *) &c.extra.room.sync_since, 4) == 4);
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); // 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 {
full = true;
}
}
file.close();
}
}
}
void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
_fs = fs;
File file = openWrite(_fs, "/s_contacts");
if (file) {
uint8_t unused[2];
memset(unused, 0, sizeof(unused));
for (int i = 0; i < num_clients; i++) {
auto c = &clients[i];
if (c->permissions == 0 || (filter && !filter(c))) continue; // skip deleted entries, or by filter function
bool success = (file.write(c->id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *) &c->permissions, 1) == 1);
success = success && (file.write((uint8_t *) &c->extra.room.sync_since, 4) == 4);
success = success && (file.write(unused, 2) == 2);
success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
success = success && (file.write(c->out_path, 64) == 64);
success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
if (!success) break; // write failed
}
file.close();
}
}
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
}
return NULL; // not found
}
ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
uint32_t min_time = 0xFFFFFFFF;
ClientInfo* oldest = &clients[MAX_CLIENTS - 1];
for (int i = 0; i < num_clients; i++) {
if (id.matches(clients[i].id)) return &clients[i]; // already known
if (!clients[i].isAdmin() && clients[i].last_activity < min_time) {
oldest = &clients[i];
min_time = oldest->last_activity;
}
}
ClientInfo* c;
if (num_clients < MAX_CLIENTS) {
c = &clients[num_clients++];
} else {
c = oldest; // evict least active contact
}
memset(c, 0, sizeof(*c));
c->permissions = init_perms;
c->id = id;
c->out_path_len = OUT_PATH_UNKNOWN;
return c;
}
bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms) {
ClientInfo* c;
if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts
c = getClient(pubkey, key_len);
if (c == NULL) return false; // partial pubkey not found
num_clients--; // delete from contacts[]
int i = c - clients;
while (i < num_clients) {
clients[i] = clients[i + 1];
i++;
}
} else {
if (key_len < PUB_KEY_SIZE) return false; // need complete pubkey when adding/modifying
mesh::Identity id(pubkey);
c = putClient(id, 0);
c->permissions = perms; // update their permissions
self_id.calcSharedSecret(c->shared_secret, pubkey);
}
return true;
}

60
src/helpers/ClientACL.h Normal file
View File

@@ -0,0 +1,60 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/IdentityStore.h>
#define PERM_ACL_ROLE_MASK 3 // lower 2 bits
#define PERM_ACL_GUEST 0
#define PERM_ACL_READ_ONLY 1
#define PERM_ACL_READ_WRITE 2
#define PERM_ACL_ADMIN 3
#define OUT_PATH_UNKNOWN 0xFF
struct ClientInfo {
mesh::Identity id;
uint8_t permissions;
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)
uint32_t last_activity; // by OUR clock (transient)
union {
struct {
uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock)
uint32_t pending_ack;
uint32_t push_post_timestamp;
unsigned long ack_timeout;
uint8_t push_failures;
} room;
} extra;
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
};
#ifndef MAX_CLIENTS
#define MAX_CLIENTS 20
#endif
class ClientACL {
FILESYSTEM* _fs;
ClientInfo clients[MAX_CLIENTS];
int num_clients;
public:
ClientACL() {
memset(clients, 0, sizeof(clients));
num_clients = 0;
}
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);
bool applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms);
int getNumClients() const { return num_clients; }
ClientInfo* getClientByIdx(int idx) { return &clients[idx]; }
};

View File

@@ -1,6 +1,7 @@
#include <Arduino.h>
#include "CommonCLI.h"
#include "TxtDataHelpers.h"
#include "AdvertDataHelpers.h"
#include <RTClib.h>
// Believe it or not, this std C function is busted on some platforms!
@@ -13,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
@@ -32,32 +41,48 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
if (file) {
uint8_t pad[8];
file.read((uint8_t *) &_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
file.read((uint8_t *) &_prefs->node_name, sizeof(_prefs->node_name)); // 4
file.read(pad, 4); // 36
file.read((uint8_t *) &_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
file.read((uint8_t *) &_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
file.read((uint8_t *) &_prefs->password[0], sizeof(_prefs->password)); // 56
file.read((uint8_t *) &_prefs->freq, sizeof(_prefs->freq)); // 72
file.read((uint8_t *) &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
file.read((uint8_t *) &_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
file.read((uint8_t *) &_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
file.read((uint8_t *) pad, 1); // 79 was 'unused'
file.read((uint8_t *) &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
file.read((uint8_t *) &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
file.read((uint8_t *) &_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
file.read((uint8_t *) &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
file.read(pad, 4); // 108
file.read((uint8_t *) &_prefs->sf, sizeof(_prefs->sf)); // 112
file.read((uint8_t *) &_prefs->cr, sizeof(_prefs->cr)); // 113
file.read((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
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->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
file.read((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
file.read((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4
file.read(pad, 4); // 36
file.read((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
file.read((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
file.read((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56
file.read((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72
file.read((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
file.read((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
file.read((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
file.read((uint8_t *)pad, 1); // 79 was 'unused'
file.read((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
file.read((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
file.read((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
file.read((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
file.read(pad, 4); // 108
file.read((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112
file.read((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113
file.read((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
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((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
file.read((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127
file.read((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128
file.read((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130
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((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
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);
@@ -65,11 +90,25 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->direct_tx_delay_factor = constrain(_prefs->direct_tx_delay_factor, 0, 2.0f);
_prefs->airtime_factor = constrain(_prefs->airtime_factor, 0, 9.0f);
_prefs->freq = constrain(_prefs->freq, 400.0f, 2500.0f);
_prefs->bw = constrain(_prefs->bw, 62.5f, 500.0f);
_prefs->sf = constrain(_prefs->sf, 7, 12);
_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);
_prefs->bridge_delay = constrain(_prefs->bridge_delay, 0, 10000);
_prefs->bridge_pkt_src = constrain(_prefs->bridge_pkt_src, 0, 1);
_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);
file.close();
}
@@ -88,32 +127,48 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
uint8_t pad[8];
memset(pad, 0, sizeof(pad));
file.write((uint8_t *) &_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
file.write((uint8_t *) &_prefs->node_name, sizeof(_prefs->node_name)); // 4
file.write(pad, 4); // 36
file.write((uint8_t *) &_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
file.write((uint8_t *) &_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
file.write((uint8_t *) &_prefs->password[0], sizeof(_prefs->password)); // 56
file.write((uint8_t *) &_prefs->freq, sizeof(_prefs->freq)); // 72
file.write((uint8_t *) &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
file.write((uint8_t *) &_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
file.write((uint8_t *) &_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
file.write((uint8_t *) pad, 1); // 79 was 'unused'
file.write((uint8_t *) &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
file.write((uint8_t *) &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
file.write((uint8_t *) &_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
file.write((uint8_t *) &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
file.write(pad, 4); // 108
file.write((uint8_t *) &_prefs->sf, sizeof(_prefs->sf)); // 112
file.write((uint8_t *) &_prefs->cr, sizeof(_prefs->cr)); // 113
file.write((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
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->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
file.write((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
file.write((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4
file.write(pad, 4); // 36
file.write((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
file.write((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
file.write((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56
file.write((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72
file.write((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
file.write((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
file.write((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
file.write((uint8_t *)pad, 1); // 79 was 'unused'
file.write((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
file.write(pad, 4); // 108
file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112
file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113
file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
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((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
file.write((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127
file.write((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128
file.write((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130
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((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
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();
}
@@ -128,11 +183,29 @@ void CommonCLI::savePrefs() {
_callbacks->savePrefs();
}
uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
if (_prefs->advert_loc_policy == ADVERT_LOC_NONE) {
AdvertDataBuilder builder(node_type, _prefs->node_name);
return builder.encodeTo(app_data);
} else if (_prefs->advert_loc_policy == ADVERT_LOC_SHARE) {
AdvertDataBuilder builder(node_type, _prefs->node_name, _sensors->node_lat, _sensors->node_lon);
return builder.encodeTo(app_data);
} else {
AdvertDataBuilder builder(node_type, _prefs->node_name, _prefs->node_lat, _prefs->node_lon);
return builder.encodeTo(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();
@@ -180,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 {
@@ -199,6 +272,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else if (memcmp(command, "clear stats", 11) == 0) {
_callbacks->clearStats();
strcpy(reply, "(OK - stats reset)");
/*
* GET commands
*/
} else if (memcmp(command, "get ", 4) == 0) {
const char* config = &command[4];
if (memcmp(config, "af", 2) == 0) {
@@ -233,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));
@@ -243,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) {
@@ -252,9 +339,85 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
} else if (memcmp(config, "role", 4) == 0) {
sprintf(reply, "> %s", _callbacks->getRole());
} else if (memcmp(config, "bridge.type", 11) == 0) {
sprintf(reply, "> %s",
#ifdef WITH_RS232_BRIDGE
"rs232"
#elif WITH_ESPNOW_BRIDGE
"espnow"
#else
"none"
#endif
);
#ifdef WITH_BRIDGE
} else if (memcmp(config, "bridge.enabled", 14) == 0) {
sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off");
} else if (memcmp(config, "bridge.delay", 12) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
} else if (memcmp(config, "bridge.source", 13) == 0) {
sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx");
#endif
#ifdef WITH_RS232_BRIDGE
} else if (memcmp(config, "bridge.baud", 11) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
#endif
#ifdef WITH_ESPNOW_BRIDGE
} else if (memcmp(config, "bridge.channel", 14) == 0) {
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);
}
/*
* SET commands
*/
} else if (memcmp(command, "set ", 4) == 0) {
const char* config = &command[4];
if (memcmp(config, "af ", 3) == 0) {
@@ -279,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();
@@ -301,21 +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();
@@ -324,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;
@@ -382,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();
@@ -391,6 +580,68 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
_prefs->freq = atof(&config[5]);
savePrefs();
strcpy(reply, "OK - reboot to apply");
#ifdef WITH_BRIDGE
} else if (memcmp(config, "bridge.enabled ", 15) == 0) {
_prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0;
_callbacks->setBridgeState(_prefs->bridge_enabled);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "bridge.delay ", 13) == 0) {
int delay = _atoi(&config[13]);
if (delay >= 0 && delay <= 10000) {
_prefs->bridge_delay = (uint16_t)delay;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: delay must be between 0-10000 ms");
}
} else if (memcmp(config, "bridge.source ", 14) == 0) {
_prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0;
savePrefs();
strcpy(reply, "OK");
#endif
#ifdef WITH_RS232_BRIDGE
} else if (memcmp(config, "bridge.baud ", 12) == 0) {
uint32_t baud = atoi(&config[12]);
if (baud >= 9600 && baud <= 115200) {
_prefs->bridge_baud = (uint32_t)baud;
_callbacks->restartBridge();
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: baud rate must be between 9600-115200");
}
#endif
#ifdef WITH_ESPNOW_BRIDGE
} else if (memcmp(config, "bridge.channel ", 15) == 0) {
int ch = atoi(&config[15]);
if (ch > 0 && ch < 15) {
_prefs->bridge_channel = (uint8_t)ch;
_callbacks->restartBridge();
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: channel must be between 1-14");
}
} else if (memcmp(config, "bridge.secret ", 14) == 0) {
StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret));
_callbacks->restartBridge();
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);
}
@@ -399,6 +650,142 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "File system erase: %s", s ? "OK" : "Err");
} else if (memcmp(command, "ver", 3) == 0) {
sprintf(reply, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate());
} else if (memcmp(command, "board", 5) == 0) {
sprintf(reply, "%s", _board->getManufacturerName());
} else if (memcmp(command, "sensor get ", 11) == 0) {
const char* key = command + 11;
const char* val = _sensors->getSettingByKey(key);
if (val != NULL) {
sprintf(reply, "> %s", val);
} else {
strcpy(reply, "null");
}
} else if (memcmp(command, "sensor set ", 11) == 0) {
strcpy(tmp, &command[11]);
const char *parts[2];
int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' ');
const char *key = (num > 0) ? parts[0] : "";
const char *value = (num > 1) ? parts[1] : "null";
if (_sensors->setSettingValue(key, value)) {
strcpy(reply, "ok");
} else {
strcpy(reply, "can't find custom var");
}
} else if (memcmp(command, "sensor list", 11) == 0) {
char* dp = reply;
int start = 0;
int end = _sensors->getNumSettings();
if (strlen(command) > 11) {
start = _atoi(command+12);
}
if (start >= end) {
strcpy(reply, "no custom var");
} else {
sprintf(dp, "%d vars\n", end);
dp = strchr(dp, 0);
int i;
for (i = start; i < end && (dp-reply < 134); i++) {
sprintf(dp, "%s=%s\n",
_sensors->getSettingName(i),
_sensors->getSettingValue(i));
dp = strchr(dp, 0);
}
if (i < end) {
sprintf(dp, "... next:%d", i);
} else {
*(dp-1) = 0; // remove last CR
}
}
#if ENV_INCLUDE_GPS == 1
} else if (memcmp(command, "gps on", 6) == 0) {
if (_sensors->setSettingValue("gps", "1")) {
_prefs->gps_enabled = 1;
savePrefs();
strcpy(reply, "ok");
} else {
strcpy(reply, "gps toggle not found");
}
} else if (memcmp(command, "gps off", 7) == 0) {
if (_sensors->setSettingValue("gps", "0")) {
_prefs->gps_enabled = 0;
savePrefs();
strcpy(reply, "ok");
} else {
strcpy(reply, "gps toggle not found");
}
} else if (memcmp(command, "gps sync", 8) == 0) {
LocationProvider * l = _sensors->getLocationProvider();
if (l != NULL) {
l->syncTime();
}
} else if (memcmp(command, "gps setloc", 10) == 0) {
_prefs->node_lat = _sensors->node_lat;
_prefs->node_lon = _sensors->node_lon;
savePrefs();
strcpy(reply, "ok");
} else if (memcmp(command, "gps advert", 10) == 0) {
if (strlen(command) == 10) {
switch (_prefs->advert_loc_policy) {
case ADVERT_LOC_NONE:
strcpy(reply, "> none");
break;
case ADVERT_LOC_PREFS:
strcpy(reply, "> prefs");
break;
case ADVERT_LOC_SHARE:
strcpy(reply, "> share");
break;
default:
strcpy(reply, "error");
}
} else if (memcmp(command+11, "none", 4) == 0) {
_prefs->advert_loc_policy = ADVERT_LOC_NONE;
savePrefs();
strcpy(reply, "ok");
} else if (memcmp(command+11, "share", 5) == 0) {
_prefs->advert_loc_policy = ADVERT_LOC_SHARE;
savePrefs();
strcpy(reply, "ok");
} else if (memcmp(command+11, "prefs", 4) == 0) {
_prefs->advert_loc_policy = ADVERT_LOC_PREFS;
savePrefs();
strcpy(reply, "ok");
} else {
strcpy(reply, "error");
}
} else if (memcmp(command, "gps", 3) == 0) {
LocationProvider * l = _sensors->getLocationProvider();
if (l != NULL) {
bool enabled = l->isEnabled(); // is EN pin on ?
bool fix = l->isValid(); // has fix ?
int sats = l->satellitesCount();
bool active = !strcmp(_sensors->getSettingByKey("gps"), "1");
if (enabled) {
sprintf(reply, "on, %s, %s, %d sats",
active?"active":"deactivated",
fix?"fix":"no fix",
sats);
} else {
strcpy(reply, "off");
}
} else {
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");
@@ -411,6 +798,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else if (sender_timestamp == 0 && memcmp(command, "log", 3) == 0) {
_callbacks->dumpLogFile();
strcpy(reply, " EOF");
} else if (sender_timestamp == 0 && memcmp(command, "stats-packets", 13) == 0 && (command[13] == 0 || command[13] == ' ')) {
_callbacks->formatPacketStatsReply(reply);
} else if (sender_timestamp == 0 && memcmp(command, "stats-radio", 11) == 0 && (command[11] == 0 || command[11] == ' ')) {
_callbacks->formatRadioStatsReply(reply);
} else if (sender_timestamp == 0 && memcmp(command, "stats-core", 10) == 0 && (command[10] == 0 || command[10] == ' ')) {
_callbacks->formatStatsReply(reply);
} else {
strcpy(reply, "Unknown command");
}

View File

@@ -2,30 +2,57 @@
#include "Mesh.h"
#include <helpers/IdentityStore.h>
#include <helpers/SensorManager.h>
#include <helpers/ClientACL.h>
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
double node_lat, node_lon;
char password[16];
float freq;
uint8_t tx_power_dbm;
uint8_t disable_fwd;
uint8_t advert_interval; // minutes / 2
uint8_t flood_advert_interval; // hours
float rx_delay_base;
float tx_delay_factor;
char guest_password[16];
float direct_tx_delay_factor;
uint32_t guard;
uint8_t sf;
uint8_t cr;
uint8_t allow_read_only;
uint8_t multi_acks;
float bw;
uint8_t flood_max;
uint8_t interference_threshold;
uint8_t agc_reset_interval; // secs / 4
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
#define WITH_BRIDGE
#endif
#define ADVERT_LOC_NONE 0
#define ADVERT_LOC_SHARE 1
#define ADVERT_LOC_PREFS 2
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
double node_lat, node_lon;
char password[16];
float freq;
int8_t tx_power_dbm;
uint8_t disable_fwd;
uint8_t advert_interval; // minutes / 2
uint8_t flood_advert_interval; // hours
float rx_delay_base;
float tx_delay_factor;
char guest_password[16];
float direct_tx_delay_factor;
uint32_t guard;
uint8_t sf;
uint8_t cr;
uint8_t allow_read_only;
uint8_t multi_acks;
float bw;
uint8_t flood_max;
uint8_t interference_threshold;
uint8_t agc_reset_interval; // secs / 4
// Bridge settings
uint8_t bridge_enabled; // boolean
uint16_t bridge_delay; // milliseconds (default 500 ms)
uint8_t bridge_pkt_src; // 0 = logTx, 1 = logRx (default logTx)
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 {
@@ -35,21 +62,32 @@ 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
};
virtual void formatStatsReply(char *reply) = 0;
virtual void formatRadioStatsReply(char *reply) = 0;
virtual void formatPacketStatsReply(char *reply) = 0;
virtual mesh::LocalIdentity& getSelfId() = 0;
virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0;
virtual void clearStats() = 0;
virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0;
virtual void setBridgeState(bool enable) {
// no op by default
};
virtual void restartBridge() {
// no op by default
};
};
class CommonCLI {
@@ -57,6 +95,8 @@ class CommonCLI {
NodePrefs* _prefs;
CommonCLICallbacks* _callbacks;
mesh::MainBoard* _board;
SensorManager* _sensors;
ClientACL* _acl;
char tmp[PRV_KEY_SIZE*2 + 4];
mesh::RTCClock* getRTCClock() { return _rtc; }
@@ -64,10 +104,11 @@ class CommonCLI {
void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
public:
CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, NodePrefs* prefs, CommonCLICallbacks* callbacks)
: _board(&board), _rtc(&rtc), _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);
void handleCommand(uint32_t sender_timestamp, const char* command, char* reply);
uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data);
};

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 {

View File

@@ -9,7 +9,6 @@
using namespace Adafruit_LittleFS_Namespace;
#endif
#include <Identity.h>
class IdentityStore {
@@ -18,7 +17,8 @@ class IdentityStore {
public:
IdentityStore(FILESYSTEM& fs, const char* dir): _fs(&fs), _dir(dir) { }
void begin() { if (_dir && _dir[0] == '/') { _fs->mkdir(_dir); } }
void begin() {
if (_dir && _dir[0] == '/') { _fs->mkdir(_dir); } }
bool load(const char *name, mesh::LocalIdentity& id);
bool load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz);
bool save(const char *name, const mesh::LocalIdentity& id);

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

329
src/helpers/RegionMap.cpp Normal file
View File

@@ -0,0 +1,329 @@
#include "RegionMap.h"
#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;
wildcard.flags = 0; // default behaviour, allow flood and direct
strcpy(wildcard.name, "*");
}
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) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
return _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "w");
#else
return _fs->open(filename, "w", true);
#endif
}
bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
if (_fs->exists(path ? path : "/regions2")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open(path ? path : "/regions2", "r");
#else
File file = _fs->open(path ? path : "/regions2");
#endif
if (file) {
uint8_t pad[128];
num_regions = 0; next_id = 1; home_id = 0;
bool success = file.read(pad, 5) == 5; // reserved header
success = success && file.read((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
success = success && file.read((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
success = success && file.read((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
if (success) {
while (num_regions < MAX_REGION_ENTRIES) {
auto r = &regions[num_regions];
success = file.read((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id);
success = success && file.read((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent);
success = success && file.read((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name);
success = success && file.read((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags);
success = success && file.read(pad, sizeof(pad)) == sizeof(pad);
if (!success) break; // EOF
if (r->id >= next_id) { // make sure next_id is valid
next_id = r->id + 1;
}
num_regions++;
}
}
file.close();
return true;
}
}
return false; // failed
}
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));
bool success = file.write(pad, 5) == 5; // reserved header
success = success && file.write((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
success = success && file.write((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
success = success && file.write((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
if (success) {
for (int i = 0; i < num_regions; i++) {
auto r = &regions[i];
success = file.write((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id);
success = success && file.write((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent);
success = success && file.write((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name);
success = success && file.write((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags);
success = success && file.write(pad, sizeof(pad)) == sizeof(pad);
if (!success) break; // write failed
}
}
file.close();
return true;
}
return false; // failed
}
RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t id) {
const char* sp = name; // check for illegal name chars
while (*sp) {
if (!is_name_char(*sp)) return NULL; // error
sp++;
}
auto region = findByName(name);
if (region) {
if (region->id == parent_id) return NULL; // ERROR: invalid parent!
region->parent = parent_id; // re-parent / move this region in the hierarchy
} else {
if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return NULL; // full!
region = &regions[num_regions++]; // alloc new RegionEntry
region->flags = REGION_DENY_FLOOD; // DENY by default
region->id = id == 0 ? next_id++ : id;
StrHelper::strncpy(region->name, name, sizeof(region->name));
region->parent = parent_id;
}
return region;
}
RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param)
TransportKey keys[4];
int num;
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 { // 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);
if (packet->transport_codes[0] == code) { // a match!!
return region;
}
}
}
}
return NULL; // no matches
}
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, skip_hash(region->name)) == 0) return region;
}
return NULL; // not found
}
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, 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;
}
}
return partial;
}
RegionEntry* RegionMap::findById(uint16_t id) {
if (id == 0) return &wildcard; // special root Region
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (region->id == id) return region;
}
return NULL; // not found
}
RegionEntry* RegionMap::getHomeRegion() {
return findById(home_id);
}
void RegionMap::setHomeRegion(const RegionEntry* home) {
home_id = home ? home->id : 0;
}
bool RegionMap::removeRegion(const RegionEntry& region) {
if (region.id == 0) return false; // failed (cannot remove the wildcard Region)
int i; // first check region has no child regions
for (i = 0; i < num_regions; i++) {
if (regions[i].parent == region.id) return false; // failed (must remove child Regions first)
}
i = 0;
while (i < num_regions) {
if (region.id == regions[i].id) break;
i++;
}
if (i >= num_regions) return false; // failed (not found)
num_regions--; // remove from regions array
while (i < num_regions) {
regions[i] = regions[i + 1];
i++;
}
return true; // success
}
bool RegionMap::clear() {
num_regions = 0;
return true; // success
}
void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream& out) const {
for (int i = 0; i < indent; i++) {
out.print(' ');
}
if (parent->flags & REGION_DENY_FLOOD) {
out.printf("%s%s\n", skip_hash(parent->name), parent->id == home_id ? "^" : "");
} else {
out.printf("%s%s F\n", skip_hash(parent->name), parent->id == home_id ? "^" : "");
}
for (int i = 0; i < num_regions; i++) {
auto r = &regions[i];
if (r->parent == parent->id) {
printChildRegions(indent + 1, r, out);
}
}
}
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
}

57
src/helpers/RegionMap.h Normal file
View File

@@ -0,0 +1,57 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Packet.h>
#include "TransportKeyStore.h"
#ifndef MAX_REGION_ENTRIES
#define MAX_REGION_ENTRIES 32
#endif
#define REGION_DENY_FLOOD 0x01
#define REGION_DENY_DIRECT 0x02 // reserved for future
struct RegionEntry {
uint16_t id;
uint16_t parent;
uint8_t flags;
char name[31];
};
class RegionMap {
TransportKeyStore* _store;
uint16_t next_id, home_id;
uint16_t num_regions;
RegionEntry regions[MAX_REGION_ENTRIES];
RegionEntry wildcard;
void printChildRegions(int indent, const RegionEntry* parent, Stream& out) const;
public:
RegionMap(TransportKeyStore& store);
static bool is_name_char(uint8_t c);
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);
RegionEntry& getWildcard() { return wildcard; }
RegionEntry* findByName(const char* name);
RegionEntry* findByNamePrefix(const char* prefix);
RegionEntry* findById(uint16_t id);
RegionEntry* getHomeRegion(); // NOTE: can be NULL
void setHomeRegion(const RegionEntry* home);
bool removeRegion(const RegionEntry& region);
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;
size_t exportTo(char *dest, size_t max_len) const;
};

View File

@@ -1,6 +1,7 @@
#pragma once
#include <CayenneLPP.h>
#include "sensors/LocationProvider.h"
#define TELEM_PERM_BASE 0x01 // 'base' permission includes battery
#define TELEM_PERM_LOCATION 0x02
@@ -21,4 +22,16 @@ public:
virtual const char* getSettingName(int i) const { return NULL; }
virtual const char* getSettingValue(int i) const { return NULL; }
virtual bool setSettingValue(const char* name, const char* value) { return false; }
virtual LocationProvider* getLocationProvider() { return NULL; }
// Helper functions to manage setting by keys (useful in many places ...)
const char* getSettingByKey(const char* key) {
int num = getNumSettings();
for (int i = 0; i < num; i++) {
if (strcmp(getSettingName(i), key) == 0) {
return getSettingValue(i);
}
}
return NULL;
}
};

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

@@ -0,0 +1,55 @@
#pragma once
#include "Mesh.h"
class StatsFormatHelper {
public:
static void formatCoreStats(char* reply,
mesh::MainBoard& board,
mesh::MillisecondClock& ms,
uint16_t err_flags,
mesh::PacketManager* mgr) {
sprintf(reply,
"{\"battery_mv\":%u,\"uptime_secs\":%u,\"errors\":%u,\"queue_len\":%u}",
board.getBattMilliVolts(),
ms.getMillis() / 1000,
err_flags,
mgr->getOutboundCount(0xFFFFFFFF)
);
}
template<typename RadioDriverType>
static void formatRadioStats(char* reply,
mesh::Radio* radio,
RadioDriverType& driver,
uint32_t total_air_time_ms,
uint32_t total_rx_air_time_ms) {
sprintf(reply,
"{\"noise_floor\":%d,\"last_rssi\":%d,\"last_snr\":%.2f,\"tx_air_secs\":%u,\"rx_air_secs\":%u}",
(int16_t)radio->getNoiseFloor(),
(int16_t)driver.getLastRSSI(),
driver.getLastSNR(),
total_air_time_ms / 1000,
total_rx_air_time_ms / 1000
);
}
template<typename RadioDriverType>
static void formatPacketStats(char* reply,
RadioDriverType& driver,
uint32_t n_sent_flood,
uint32_t n_sent_direct,
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_errors\":%u}",
driver.getPacketsRecv(),
driver.getPacketsSent(),
n_sent_flood,
n_sent_direct,
n_recv_flood,
n_recv_direct,
driver.getPacketsRecvErrors()
);
}
};

View File

@@ -0,0 +1,92 @@
#include "TransportKeyStore.h"
#include <SHA256.h>
uint16_t TransportKey::calcTransportCode(const mesh::Packet* packet) const {
uint16_t code;
SHA256 sha;
sha.resetHMAC(key, sizeof(key));
uint8_t type = packet->getPayloadType();
sha.update(&type, 1);
sha.update(packet->payload, packet->payload_len);
sha.finalizeHMAC(key, sizeof(key), &code, 2);
if (code == 0) { // reserve codes 0000 and FFFF
code++;
} else if (code == 0xFFFF) {
code--;
}
return code;
}
bool TransportKey::isNull() const {
for (int i = 0; i < sizeof(key); i++) {
if (key[i]) return false;
}
return true; // key is all zeroes
}
void TransportKeyStore::putCache(uint16_t id, const TransportKey& key) {
if (num_cache < MAX_TKS_ENTRIES) {
cache_ids[num_cache] = id;
cache_keys[num_cache] = key;
num_cache++;
} else {
// TODO: evict oldest cache entry
}
}
void TransportKeyStore::getAutoKeyFor(uint16_t id, const char* name, TransportKey& dest) {
for (int i = 0; i < num_cache; i++) { // first, check cache
if (cache_ids[i] == id) { // cache hit!
dest = cache_keys[i];
return;
}
}
// calc key for publicly-known hashtag region name
SHA256 sha;
sha.update(name, strlen(name));
sha.finalize(&dest.key, sizeof(dest.key));
putCache(id, dest);
}
int TransportKeyStore::loadKeysFor(uint16_t id, TransportKey keys[], int max_num) {
int n = 0;
for (int i = 0; i < num_cache && n < max_num; i++) { // first, check cache
if (cache_ids[i] == id) {
keys[n++] = cache_keys[i];
}
}
if (n > 0) return n; // cache hit!
// TODO: retrieve from difficult-to-copy keystore
// store in cache (if room)
for (int i = 0; i < n; i++) {
putCache(id, keys[i]);
}
return n;
}
bool TransportKeyStore::saveKeysFor(uint16_t id, const TransportKey keys[], int num) {
invalidateCache();
// TODO: update hardware keystore
return false; // failed
}
bool TransportKeyStore::removeKeys(uint16_t id) {
invalidateCache();
// TODO: remove from hardware keystore
return false; // failed
}
bool TransportKeyStore::clear() {
invalidateCache();
// TODO: clear hardware keystore
return false; // failed
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Packet.h>
#include <helpers/IdentityStore.h>
struct TransportKey {
uint8_t key[16];
uint16_t calcTransportCode(const mesh::Packet* packet) const;
bool isNull() const;
};
#define MAX_TKS_ENTRIES 16
class TransportKeyStore {
uint16_t cache_ids[MAX_TKS_ENTRIES];
TransportKey cache_keys[MAX_TKS_ENTRIES];
int num_cache;
void putCache(uint16_t id, const TransportKey& key);
void invalidateCache() { num_cache = 0; }
public:
TransportKeyStore() { num_cache = 0; }
void getAutoKeyFor(uint16_t id, const char* name, TransportKey& dest);
int loadKeysFor(uint16_t id, TransportKey keys[], int max_num);
bool saveKeysFor(uint16_t id, const TransportKey keys[], int num);
bool removeKeys(uint16_t id);
bool clear();
};

View File

@@ -19,6 +19,13 @@ void StrHelper::strzcpy(char* dest, const char* src, size_t buf_sz) {
}
}
bool StrHelper::isBlank(const char* str) {
while (*str) {
if (*str++ != ' ') return false;
}
return true;
}
#include <Arduino.h>
union int32_Float_t
@@ -132,3 +139,36 @@ 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) {
if (*src >= '0' && *src <= '9') {
n <<= 4;
n |= (*src - '0');
} else if (*src >= 'A' && *src <= 'F') {
n <<= 4;
n |= (*src - 'A' + 10);
} else if (*src >= 'a' && *src <= 'f') {
n <<= 4;
n |= (*src - 'a' + 10);
} else {
break; // non-hex char encountered, stop parsing
}
src++;
}
return n;
}

View File

@@ -12,4 +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

@@ -0,0 +1,48 @@
#include "BridgeBase.h"
#include <Arduino.h>
bool BridgeBase::isRunning() const {
return _initialized;
}
const char *BridgeBase::getLogDateTime() {
static char tmp[32];
uint32_t now = _rtc->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(),
dt.year());
return tmp;
}
uint16_t BridgeBase::fletcher16(const uint8_t *data, size_t len) {
uint8_t sum1 = 0, sum2 = 0;
for (size_t i = 0; i < len; i++) {
sum1 = (sum1 + data[i]) % 255;
sum2 = (sum2 + sum1) % 255;
}
return (sum2 << 8) | sum1;
}
bool BridgeBase::validateChecksum(const uint8_t *data, size_t len, uint16_t received_checksum) {
uint16_t calculated_checksum = fletcher16(data, len);
return received_checksum == calculated_checksum;
}
void BridgeBase::handleReceivedPacket(mesh::Packet *packet) {
// Guard against uninitialized state
if (_initialized == false) {
BRIDGE_DEBUG_PRINTLN("RX packet received before initialization\n");
_mgr->free(packet);
return;
}
if (!_seen_packets.hasSeen(packet)) {
// bridge_delay provides a buffer to prevent immediate processing conflicts in the mesh network.
_mgr->queueInbound(packet, millis() + _prefs->bridge_delay);
} else {
_mgr->free(packet);
}
}

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