Compare commits

..

819 Commits

Author SHA1 Message Date
Scott Powell
9405e8bee3 Merge branch 'dev'
# Conflicts:
#	docs/payloads.md
2025-11-13 20:47:52 +11: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
fdlamotte
8b68b5a689 Update README.md (RAK boards don't need pio patch) 2025-11-12 16:14:57 +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
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
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
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
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
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
Scott Powell
96e786fa9e * FIX: for divide by zero crash 2025-10-30 19:11:04 +11: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
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
recrof
87677fda76 allow spreading factor from 5 and bandwidth from 7.8kHz 2025-10-22 15:15:29 +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
Liam Cottle
b2dcb06197 Merge pull request #809 from tekstrand/fixup
Change source of truth to this repo, remove whitespace
2025-10-19 12:07:53 +13:00
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
ripplebiz
da5dbcd274 Merge pull request #871 from spacepc-de/fix-debug-log-field
Fix debug log: use c->extra.room.push_failures instead of c->push_failures
2025-10-07 09:45:11 +11:00
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
tekstrand
3e3fa5b443 trim trailing whitespace, clarify repeater gps, remove outdated instructions 2025-10-04 10:54:24 -05: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
f5f5886327 Merge branch 'dev' 2025-10-02 12:52:48 +10: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
Jonathan Stöcklmayer
6ee0b85195 Fix debug log: use c->extra.room.push_failures instead of non-existent c->push_failures 2025-10-01 09:50:41 +02:00
ripplebiz
86225cd24a Merge pull request #869 from LitBomb/patch-19
Update faq.md
2025-10-01 13:46:44 +10:00
uncle lit
f594f2c7e6 Update faq.md
added pyMC_core to meshcore projects
mentioned Cisien's meshcoretomqtt fork from Andrew-a-g
updated Coding Rate explanation and recommendation
updated radio presets and added how to update presets listed in the app
2025-09-30 16:01:11 -07:00
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
Liam Cottle
3dc04deabf Merge pull request #837 from silverphish-io/typo-fix
Typo fix
2025-09-29 10:42:23 +13:00
ripplebiz
c8a6bcf57f Update README.md 2025-09-28 21:43:30 +10:00
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
silverphish-io
4e886bfa90 Typo fix in faq and payloads 2025-09-25 15:01:39 +01:00
silverphish-io
816d4e2fa3 Update faq.md 2025-09-25 14:59:25 +01:00
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
Scott Powell
1130cf13ab Merge branch 'dev' 2025-09-01 19:32:54 +10:00
Scott Powell
637891b814 * ver bump 2025-09-01 19:32:15 +10:00
Liam Cottle
a4c2da9d50 Merge pull request #704 from liamcottle/fix/wismesh-pocket-user-button
Fix: WisMesh Pocket user button
2025-09-01 21:09:54 +12:00
liamcottle
3ad43431d9 fixed wismesh pocket user button 2025-09-01 21:04:41 +12:00
Scott Powell
74722c24b8 * MomentaryButton: added support for analog button (with threshold)
* RAK: support for PIN_USER_BTN_ANA
2025-09-01 17:11:55 +10:00
Scott Powell
b8223e9d07 * reverting HeltecV3 _CURRENT_LIMIT change 2025-09-01 16:28:53 +10:00
Scott Powell
81afd83099 * Meshadventurer companion USB build fix 2025-09-01 16:10:45 +10:00
Scott Powell
ee194a7b19 * added CommonCLI::saveIdentity() 2025-09-01 15:22:11 +10:00
Scott Powell
c28001d1e2 * ESP platform ver > 6.11.0 seems to break Github Actions 2025-09-01 14:29:40 +10:00
ripplebiz
7bc02296ff Merge pull request #698 from recrof/esp32_companion_max_contacts
raised companion max contacts for esp32 "classic" targets; changed WSL3 to 300.
2025-09-01 14:26:42 +10: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
fdlamotte
3aa57780f1 Update library.json version 2025-08-31 17:30:31 +02:00
recrof
489bcaffc9 raised max contacts for esp32 classic companions to 160 and 170; set to 300 for WSL3 2025-08-31 16:23:21 +02:00
Scott Powell
4413e5be95 Merge branch 'dev' 2025-08-31 23:43:33 +10:00
Scott Powell
8b3c16c497 * ver bump 2025-08-31 23:42:15 +10:00
Scott Powell
7c7faaab05 * agc.reset.interval rounding warning 2025-08-31 23:15:56 +10:00
ripplebiz
009ca6d6aa Merge pull request #696 from recrof/esp32c6_pioarduino_downgrade
downgrading pioarduino because build issues
2025-08-31 22:55:40 +10:00
ripplebiz
e1ac794a81 Merge pull request #695 from recrof/esp32_c3_c6_s3_max_contacts_300
set companion radios with esp32c3 esp32c6 and esp32s3 to 300 contacts max
2025-08-31 22:45:35 +10:00
Liam Cottle
f5c7d3dd80 Merge branch 'dev' into esp32_c3_c6_s3_max_contacts_300 2025-09-01 00:38:21 +12:00
recrof
7a00f3060e downgrading pioarduino because build issues 2025-08-31 14:33:49 +02:00
recrof
50cab44473 set companion radios with esp32c3 esp32c6 and esp32s3 boards to max 300 contacts 2025-08-31 14:27:44 +02:00
ripplebiz
8a39e80359 Merge pull request #694 from recrof/dev_xiao_s3_wio_serial_fix
fix: add xiao s3 wio serial companion to new ui
2025-08-31 21:00:01 +10:00
ripplebiz
615316f443 Merge pull request #693 from recrof/dev_meshadventurer_fix
fix: migrate meshadventurer to new ui
2025-08-31 20:59:13 +10:00
ripplebiz
e8d4ab5977 Merge pull request #692 from liamcottle/feature/thinknode-m1-poweroff
ThinkNode M1: low battery auto shutdown
2025-08-31 20:56:58 +10:00
recrof
7854244026 fix: add xiao s3 wio serial companion to new ui 2025-08-31 12:11:07 +02:00
recrof
0f23c0120a fix: migrate meshadventurer to new ui 2025-08-31 12:09:04 +02:00
Liam Cottle
d8c4fa456a Merge pull request #690 from mattzzw/main
Update faq.md, add information on how to configure wifi
2025-08-31 18:02:50 +12:00
kelsey hudson
65be15e6be Merge remote-tracking branch 'upstream/dev' into dev 2025-08-30 17:54:42 -07:00
Matthias Wientapper
b3adaa790a Update faq.md 2025-08-30 20:53:57 +02:00
liamcottle
18ef1ba804 add low battery shutdown alert for thinknode m1 2025-08-30 23:09:01 +12:00
liamcottle
6172537459 auto shutdown thinknode m1 at 3.3v 2025-08-30 21:56:00 +12:00
liamcottle
5a34bd5460 turn off tx led when powering off 2025-08-30 21:54:46 +12:00
ripplebiz
d4856a5275 Merge pull request #617 from Quency-D/dev-heltec_t190_board
Add heltec_vision_master_t190 board.
2025-08-29 20:11:24 +10:00
ripplebiz
e1c169b20a Merge pull request #687 from Quency-D/new-ui-heltec-paper
New UI for heltec paper
2025-08-29 19:59:40 +10:00
Quency-D
fee7975668 Remove duplicate rows. 2025-08-29 17:31:28 +08:00
ripplebiz
31006857fe Merge pull request #686 from Quency-D/new-ui-e213
New UI heltec e213.
2025-08-29 19:18:06 +10:00
ripplebiz
bc8c95fcb3 Merge pull request #685 from liamcottle/fix/wifi-companion-clients
WiFi Companion: always accept new connections
2025-08-29 19:16:52 +10:00
Quency-D
c5b28da41d Merge pull request #5 from Quency-D/dev
Merge Dev
2025-08-29 17:16:30 +08:00
Quency-D
4eaaeebfca Remove duplicate rows. 2025-08-29 17:12:35 +08:00
Quency-D
3e3e364dae Merge pull request #4 from Quency-D/dev
Merge Dev
2025-08-29 16:49:08 +08:00
ripplebiz
4785240608 Merge pull request #683 from taedryn/t114-power-consumption
Improving T114 power consumption
2025-08-29 17:50:30 +10:00
liamcottle
796b4c705c disconnect existing wifi client when new connection established 2025-08-29 19:50:05 +12:00
ripplebiz
4d6e33b365 Merge pull request #684 from liamcottle/new-ui/heltec-v3-wifi
Add new UI to Heltec v3 WiFi Companion
2025-08-29 17:48:02 +10:00
ripplebiz
980c3445d0 Merge pull request #679 from Quency-D/new-ui-e290
New UI heltec e290.
2025-08-29 17:06:15 +10:00
ripplebiz
a0b037ecd7 Merge pull request #680 from 446564/nano-g2-hibernate
add power off to nano g2
2025-08-29 17:04:17 +10:00
Scott Powell
6984d9f496 * minor compilation fix 2025-08-29 17:01:39 +10:00
liamcottle
09e45f25b7 add new ui to heltec v3 wifi companion 2025-08-29 18:58:54 +12:00
ripplebiz
d012dc7fd7 Merge pull request #663 from liamcottle/feature/remove-neighbour
Add CLI command to remove neighbour
2025-08-29 16:50:08 +10:00
liamcottle
9ee0152084 add default no op implementation for remove neighbor function 2025-08-29 18:17:01 +12:00
liamcottle
c49ecc121e use new instance of neighbour info object when removing neighbour 2025-08-29 18:08:06 +12:00
Laura J
19978d6b6e Merge branch 'dev' into t114-power-consumption 2025-08-28 19:42:08 -07:00
fdlamotte
f9f1c2e340 Merge pull request #673 from 446564/t1000e-roles
add remaining roles to t1000e
2025-08-28 18:14:06 +02:00
446564
13cd849fcd add power off to nano g2 2025-08-28 08:37:25 -07:00
446564
789629f5f8 remove display class from repeater and room 2025-08-28 08:13:33 -07:00
Quency-D
3fe2d48a61 Remove duplicate rows. 2025-08-28 19:20:11 +08:00
Quency-D
d93e03bb6e Merge pull request #3 from Quency-D/dev
Merge Dev
2025-08-28 18:56:56 +08:00
Quency-D
ec6d119900 Adapt the new UI for heltec t190. 2025-08-28 17:07:33 +08:00
Quency-D
bb63f8165d Merge pull request #2 from Quency-D/dev
Merging changes
2025-08-28 15:36:59 +08:00
Quency-D
2e49eef337 Adapt the new UI for heltec e290. 2025-08-28 15:00:27 +08:00
Quency-D
d5bec3d6a5 Remove duplicate rows. 2025-08-28 14:36:56 +08:00
Quency-D
1415792141 Adapt the new UI for heltec e213. 2025-08-28 14:29:56 +08:00
Quency-D
666b036d3d Adapt the new UI for heltec paper. 2025-08-28 14:03:20 +08:00
ripplebiz
3f5c772663 Merge pull request #671 from aveao/why2025-badge-pr
Add WHY2025 badge as a variant
2025-08-28 09:45:16 +10:00
ripplebiz
2d6e714ccd Merge pull request #675 from fdlamotte/sensormesh_additions
SensorMesh: handleIncomingMsg from admin
2025-08-28 09:25:13 +10:00
Florent
136f733df5 SensorMesh: add the possibility to receive msgs from admin 2025-08-27 21:19:12 +02:00
446564
c10b387e63 add remaining roles to t1000e
usb companion, repeater and room server
2025-08-27 10:30:56 -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
ave
6536e9931d add why2025 badge as a target 2025-08-27 08:34:38 +02:00
Florent
0959e64d11 lib_build: add UI_FLAVOR and some cleanup 2025-08-26 13:28:42 +02:00
Laura J
a16e011bd2 modified T114 code to disable many unused peripherals, and set up the LOWPWR power mode. all changes should be quite safe, but testing for a couple days just in case. 2025-08-25 22:10:18 -07:00
ripplebiz
24cc6a40a6 Merge pull request #665 from jbrazio/jbrazio/2025_9776d79d
Fix Heltec v3 433MHz crash while transmitting
2025-08-26 12:55:18 +10:00
ripplebiz
0ed8921153 Merge pull request #662 from fdlamotte/lib_build_display_support
lib_build: Support display and example build
2025-08-26 10:28:20 +10:00
ripplebiz
4a166078db Merge pull request #667 from LitBomb/patch-18
Update faq.md
2025-08-26 10:25:04 +10:00
uncle lit
02ad2bed4d Update faq.md
update instruction to add a node to the Internet Map from the app, and note that you can use the same companion radio to remove the nodes you added previously

add channel and contact QR code URL format
2025-08-25 17:20:13 -07:00
João Brázio
78fcb704bc Increase power limit for Heltec v3 433 MHz 2025-08-25 17:44:14 +01:00
Florent
df18dfb481 lib_build: exclude file from example 2025-08-25 18:34:50 +02:00
Florent
15249bb8d5 lib_build: include example code in build 2025-08-25 17:50:48 +02:00
Florent
033706adcf lib_build: fix ST7789 so we don't have to add helpers/ui in INC_DIRS 2025-08-25 16:33:34 +02:00
liamcottle
2d5016bac3 add ability to remove neighbour via cli 2025-08-25 23:45:50 +12:00
Florent
d0fb8d2f30 lib_build: support display 2025-08-25 12:11:39 +02:00
ripplebiz
08b49c3ac5 Merge pull request #660 from khudson/station_g2_logger
Station G2: Adds repeater variant with packet logging on
2025-08-25 12:05:22 +10:00
ripplebiz
0fd24c8e5d Merge pull request #656 from khudson/dev
Ikoka Stick: Adding variant support for different Ebyte S22 modules
2025-08-25 12:04:08 +10:00
ripplebiz
44fb4d1bce Merge pull request #659 from fdlamotte/use_as_lib
Use as lib
2025-08-25 11:58:00 +10:00
kelsey hudson
bbee057b03 Station G2: Adds repeater variant with packet logging on
Adds a variant to Station G2 which turns on mesh packet logging to
serial. Useful for telemetry logging. Sites such as map.w0z.is depend
on this functionality.
2025-08-24 15:54:05 -07:00
Florent
b88a360ada lib_build: add nrf52 and RP2040 2025-08-24 14:47:03 +02:00
Florent
228bac0add simplify src_filter creation 2025-08-24 13:09:55 +02:00
Florent
685f75234b let users compile their projects against meshcore as a lib 2025-08-24 13:06:19 +02:00
kelsey hudson
dedef49315 Ikoka Stick: Adding variant support for Ebyte S22 modules
The Ebyte S22 module comes in three variants, 22dBm, 30dBm, and 33dBm. Each
requires a different TX power max setting to avoid frying the PA frontend.

Breaks out variants and roles by module type.
2025-08-22 23:29:28 -07:00
Scott Powell
cb96503b92 * Companion: new-ui, power off now labelled "hibernate" 2025-08-23 14:45:18 +10:00
João Brázio
7f142245e6 Merge remote-tracking branch 'origin/dev' into jbrazio/2025_3f11ad35 2025-08-22 23:00:35 +01:00
ripplebiz
5cd1df48ad Merge pull request #651 from awolden/awolden/ina226
Add INA226 to environmental sensors
2025-08-22 17:58:04 +10:00
Alex Wolden
0c37eafd01 Fixed shunt value for ina226 2025-08-21 13:23:05 -07:00
ripplebiz
e0d548c71e Merge pull request #650 from LitBomb/patch-17
Update faq.md
2025-08-21 16:58:02 +10:00
Alex Wolden
c636536599 Add INA226 to rak 2025-08-20 22:23:54 -07:00
uncle lit
82184c5836 Update faq.md
update verbiage on room server's repeat functionality to discourage turning repeat on on room server.

Update repeater flood advert to 3 hours and mentioning pending PR to change that to 12 hours.
2025-08-20 22:08:19 -07:00
ripplebiz
5772756b30 Merge pull request #649 from LitBomb/patch-16
Update faq.md
2025-08-21 11:28:23 +10:00
uncle lit
f462113f4c Update faq.md
updated .mp3 file list for sound customization on t-deck
added instruction to take a screenshot on t-deck
Added link to MeshCore Ultra firmware user guide
update github link from /meshcore to /meshcore-dev
Update current hardware list to point to flasher.meshcore.co.uk
2025-08-20 12:20:06 -07:00
ripplebiz
412e9d4678 Merge pull request #645 from recrof/dev
set default flood advert interval to 12 hours
2025-08-20 22:01:07 +10:00
recrof
5dc930410c set default flood advert interval to 12 hours 2025-08-20 13:44:41 +02:00
Scott Powell
a9d4cf1d21 * various repeaters: fix for missing MomentaryButton module 2025-08-19 23:14:11 +10:00
ripplebiz
e846cc6798 Merge pull request #642 from fdlamotte/techo_power_off
techo_ui: implement poweroff + led fixes
2025-08-19 21:23:49 +10:00
Florent
ff03b041d0 techo_ui: implement poweroff + led fixes 2025-08-19 09:03:34 +02:00
ripplebiz
a825a3d4bc Merge pull request #638 from recrof/dev
fix RAK build errors
2025-08-18 22:12:31 +10:00
recrof
d145d5936d fix RAK build errors 2025-08-18 12:53:44 +02:00
ripplebiz
b8c4c75025 Merge pull request #636 from fdlamotte/t1000_wait_button_release_before_poweroff
t1000: wait for button release before powering off
2025-08-18 13:52:12 +10:00
Florent
b8f80afee9 t1000: wait for button release before powering off 2025-08-17 16:28:02 +02:00
ripplebiz
6e296e8db1 Merge pull request #633 from oltaco/dev
WioTrackerL1: add joystick left and right for new UI
2025-08-17 22:17:59 +10:00
ripplebiz
cb85600572 Merge pull request #634 from recrof/dev
station g2: switch to New UI
2025-08-17 22:15:01 +10:00
recrof
a4916f81eb station g2: switch to new_ui 2025-08-17 11:57:38 +02:00
taco
cb8ca91d27 WioTrackerL1: add joystick left and right for new UI 2025-08-17 19:01:28 +10:00
ripplebiz
3bb55b590c Merge pull request #632 from oltaco/dev
WioTrackerL1: add poweroff support
2025-08-17 18:13:14 +10:00
taco
8f1ccb65ae WioTrackerL1: add poweroff support 2025-08-17 18:10:58 +10:00
Scott Powell
e6152f9d6c * Xiao_s3_wio: ui-new 2025-08-17 17:52:05 +10:00
ripplebiz
ed3f52775a Merge pull request #629 from fdlamotte/techo_ui_tweaks
techo_ui: some tweaks
2025-08-17 17:48:47 +10:00
Scott Powell
7613b9455d * Xiao_nRF and Xiao_rp2040 build fixes 2025-08-17 17:36:35 +10:00
Scott Powell
76a53bf84d * Wio-e5-mini: ui-orig
* WioTrackerL1: ui-new
2025-08-17 17:23:28 +10:00
Florent
b332b06304 techo_ui: some tweaks 2025-08-17 08:52:57 +02:00
Scott Powell
fe376e8c35 * RAK_4631: ui-new 2025-08-17 16:49:08 +10:00
Scott Powell
f5ad1df103 * Minewsemi: ui-orig 2025-08-17 16:35:39 +10:00
Scott Powell
276a057693 * lilygo_tlora_v2_1: ui-new 2025-08-17 16:31:50 +10:00
Scott Powell
2477d60fae * TBeam variants: ui-new 2025-08-17 16:18:39 +10:00
Scott Powell
8f8830047b * T3S3 variants, ui-new 2025-08-17 16:06:26 +10:00
Scott Powell
c30a103baf * WSL3 fixes. Heltec V2 ui-new 2025-08-17 15:50:25 +10:00
ripplebiz
95c9d17dc5 Merge pull request #631 from khudson/new-companion-ui
Ikoka Stick: Use New Companion UI. Fix repeater & room server roles.
2025-08-17 08:24:09 +10:00
kelsey hudson
022bfc4f4b Ikoka Stick: Use new UI, make repeater & room server work
* Enable the new UI on the BLE and USB Companion roles.
* Fix compilation issues with the repeater and room server roles.
* Remove ESP32-related alternate pinout cruft from the NRF build tree.
* build.sh: add build-matching-firmwares command to allow e.g. building
  all roles for a given variant by passing the variant name.
2025-08-16 15:09:42 -07:00
kelsey hudson
0359df6cb5 Merge commit 'e9ffc3ea93d7912460c38cbaafb726f5455ad8cf' into new-companion-ui
Merging ikoka stick repeater/room server commit to simplify pull request later.
2025-08-16 12:26:34 -07:00
ripplebiz
f9284cdf4a Merge pull request #616 from haylinmoore/nix-add-python3
default.nix: add python3 to the shell
2025-08-17 00:32:41 +10:00
Scott Powell
37d7257f04 * Heltec tracker, now using /ui-new
* variants with no DISPLAY_CLASS .ini fixes
2025-08-16 21:53:36 +10:00
Scott Powell
e14b022a7c * original UITask now in /ui-orig folder 2025-08-16 21:09:35 +10:00
Scott Powell
acde9921b5 * Refactor of UITask, moved to /ui-new 2025-08-16 20:04:54 +10:00
ripplebiz
29fd5da5e8 Merge pull request #624 from oltaco/new-companion-ui
Support NewUI on WioTrackerL1
2025-08-16 13:51:18 +10:00
kelsey hudson
e9ffc3ea93 Ikoka Stick: repeater/room server functionality
Make Repeater and Room Server work as build targets.

Remove esp32-related alternate pinout cruft from the Ikoka Stick
NRF build tree.
2025-08-15 19:29:22 -07:00
taco
86671c0ff8 Support NewUI on WioTrackerL1 2025-08-16 09:29:48 +10:00
Quency-D
bd6bd065ac Add heltec_vision_master_t190 board. 2025-08-15 15:54:24 +08:00
Scott Powell
a5ebac6236 Merge branch 'dev' into new-companion-ui 2025-08-15 15:39:11 +10:00
ripplebiz
9108a709ee Merge pull request #612 from Quency-D/dec-heltec_e290
Add heltec_vision_master_e290 board.
2025-08-15 13:59:42 +10:00
ripplebiz
655d4a78f5 Merge pull request #615 from MikesAllotment/new-companion-ui
UI Refresh changes for HeltecV2 and Faketec ProMicro
2025-08-15 13:04:38 +10:00
Haylin Moore
24ef375fc7 default.nix: add python3 to the shell
There are many systems that chose to not have a global python install,
namely my own systems :p. I end up having to nix-shell -p python3 anytime
I want to run the build.sh as the last step uf2conv depends on python
2025-08-14 15:52:04 -07:00
MikesAllotment
d7c2293cb8 Added support for new EnhancedUI MomentaryButton
for Heltec V2 and Faketec ProMicro variants
2025-08-14 14:17:26 +01:00
ripplebiz
a7dcd112ac Merge pull request #608 from Quency-D/dev-heltec_e213
Add heltec_vision_master_e213 board.
2025-08-14 21:20:21 +10:00
Quency-D
aa7f9d8df6 Use the base class to optimize screen display code. 2025-08-14 17:43:46 +08:00
ripplebiz
82206fd281 Merge pull request #609 from 446564/nan-g2-usb
add nano g2 usb companion
2025-08-14 15:58:25 +10:00
ripplebiz
e47a1df67f Merge pull request #587 from alexbegoon/feature/xiao-c6-external-antenna-support
[Feature] Add external antenna support for XIAO ESP32C6
2025-08-14 15:41:09 +10:00
Quency-D
6d18e2c57b Add heltec_vision_master_e290 board. 2025-08-14 10:30:27 +08:00
Quency-D
fad4a7fb51 Modify the flash size to 16MB. 2025-08-14 10:26:26 +08:00
446564
dc9b4f8e84 add nano g2 usb companion 2025-08-13 09:47:01 -07:00
Quency-D
be243a2663 Add heltec_vision_master_e213 board. 2025-08-13 18:12:48 +08:00
Quency-D
dc6b830970 * Heltec CT62: sensor role (with 2 channel relay support + 1 digital …
* Heltec CT62: sensor role (with 2 channel relay support + 1 digital …
2025-08-13 17:00:17 +08:00
Scott Powell
86ec82fd06 * Heltec CT62: sensor role (with 2 channel relay support + 1 digital input) 2025-08-12 20:56:59 +10:00
Scott Powell
e84e3066ff * MomentaryButton: pullupdown param moved to constructor 2025-08-12 10:01:35 +10:00
ripplebiz
1897f51458 Merge pull request #601 from khudson/dev
Adding support for Ikoka Stick with Seeed Xiao nRF54 baseboard.
2025-08-11 11:46:11 +10:00
ripplebiz
a2eed714f5 Merge pull request #595 from gumbero/main
Added room option to T-Beam SX1276
2025-08-11 11:30:29 +10:00
kelsey hudson
bed311313a Adding support for Ikoka Stick with Seeed Xiao nRF54 baseboard.
Adds a new variant 'ikoka_stick_nrf' and associated support files. This is
based on the Xiao nRF54 code with pin numbers and functions changed to suit the
differences in hardware between the WIO SX1262 board and the Ikoka Stick.

Sets the default LoRa transmit power to 9dBm to avoid burning up the frontend
in Ikoka Sticks equipped with the Ebyte 33dBm S22 module on first boot.

Adds support for an SSD1306 display connected to the display header. Note the
display pinout is the same as the RAK4631 display header so make sure to use a
display wired accordingly (aliexpress etc. SSD1306s typically have Vcc & GND
reversed from what this board expects).

Adds support for display rotation to SSD1306Display via a platformIO define.
This support was added following the same paradigms found elsewhere in the code
for rotating a display.
2025-08-10 10:44:00 -07:00
gumbero
af7db5593b Update platformio.ini 2025-08-10 14:25:43 +02:00
ripplebiz
c8bbec6549 Merge pull request #598 from 446564/nano-g2-new-ui
add new UI to nano g2
2025-08-10 14:44:04 +10:00
446564
153051ab82 add new UI to nano g2 2025-08-09 20:47:35 -07:00
Scott Powell
67529d0cf3 Merge branch 'dev' into new-companion-ui 2025-08-10 12:28:25 +10:00
ripplebiz
72d13ca867 Merge pull request #594 from wel97459/dev
Changed waveshare_rp2040_lora radio_init function to use radio.std_init
2025-08-10 12:22:07 +10:00
ripplebiz
b9270aff5c Merge pull request #597 from 446564/ignore-clangd
add lsp ignores
2025-08-10 12:13:32 +10:00
446564
cdc762ada2 add lsp ignores
adds cache and ccls dirs and clangd files to git ignore
2025-08-09 17:37:51 -07:00
Tomas Gabor
6e26a6a78c Added room option to T-Beam SX1276 2025-08-09 21:28:31 +02:00
ripplebiz
cc065c84ba Merge pull request #589 from fdlamotte/techo_new_ui
TEcho: New companion UI
2025-08-09 18:53:51 +10:00
wel97459
331a29b082 Changed radio_init to use radio.std_init this also changes the preamble to 16 as before it was 8 2025-08-08 20:17:06 -07:00
Alexander Begoon
6902dd81fa Move variant specific code 2025-08-08 23:04:14 +02:00
fdlamotte
af72db6834 Merge branch 'new-companion-ui' into techo_new_ui 2025-08-08 15:40:19 +02:00
Florent
1e711f57f4 techo: initial support of new companion ui 2025-08-08 15:34:58 +02:00
ripplebiz
1b93ceaa30 Merge pull request #588 from liamcottle/feature/new-companion-ui-thinknode-m1
add support for new companion ui on thinknode m1
2025-08-08 22:24:55 +10:00
liamcottle
8d7a49867f add support for new companion ui on thinknode m1 2025-08-09 00:21:10 +12:00
Scott Powell
4b95c981bb * UI revamp for companion radios 2025-08-08 20:01:31 +10:00
Alexander Begoon
ce4e559c01 Add support for Xiao ESP32C6 with external antenna configuration 2025-08-07 22:36:40 +02:00
ripplebiz
a310a5c4d5 Merge pull request #584 from 446564/nano-g2-gps-fix
nano g2 gps fixes
2025-08-07 12:57:07 +10:00
Ded
4d97bee02a good oops 2025-08-06 18:52:57 -07:00
ripplebiz
6b2836ea07 Merge pull request #579 from alexbegoon/feature/add_meshimi_support
Add Meshimi support
2025-08-07 11:35:39 +10:00
446564
53c1f70412 various fixes for nano g2 gps
- baud rate changed to default
- switch state detected at boot to enable position
- small edge cases fixed
- more mesh debugging output added

Co-authored-by: LitBomb <lithangw@hotmail.com>
Co-authored-by: Nick from BOSTON
2025-08-06 11:41:37 -07:00
ripplebiz
4b653408a6 Merge pull request #575 from Quency-D/dev
Add HELTEC MeshSolar board.
2025-08-06 11:18:07 +10:00
Alexander Begoon
fa3500944b Add Meshimi configuration and environment setups in platformio.ini 2025-08-05 18:45:20 +02:00
Scott Powell
d1e13d0b9e Revert "disable led light on t-echo device"
This reverts commit 55453e1136.
2025-08-05 18:45:20 +02:00
TasmanDevil
6214b75e83 disable led light on t-echo device 2025-08-05 18:45:20 +02:00
Rastislav Vysoky
28360ba459 replaced Andy Kirby's discord with MeshCore discord. 2025-08-05 18:45:20 +02:00
Rastislav Vysoky
ca9687e212 faq: fix broken links, replace discord link from Andy's discord to MeshCore discord 2025-08-05 18:45:20 +02:00
Rastislav Vysoky
fc334a05c6 remove hardware compatibility list - it was outdated. 2025-08-05 18:45:19 +02:00
uncle lit
00dc193b0d Update faq.md
- rewrite Linux firmware update with more detailed instructions 
- fix T114 reset typo: double click twice, not once
- add link reference to MeshCore logo on github
- add public channel key for t-deck to the t-deck section
- reference the verbiage "what is meshcore?" on meshcore.co.uk homepage
2025-08-05 18:45:19 +02:00
Quency-D
cf9bcb5f4f refactor variants #393
refactor variants #393
2025-08-05 15:26:24 +08:00
Quency-D
fb8a4d12b1 refactor variants #393 2025-08-05 15:24:51 +08:00
Quency-D
612dde73e9 Add HELTEC MeshSolar board. 2025-08-04 14:29:49 +08:00
ripplebiz
2853708f38 Merge pull request #573 from recrof/sensor_template
platformio sensor template, use it for v3 and RAK.
2025-08-04 11:03:53 +10:00
ripplebiz
112e60a14a Merge pull request #569 from recrof/dev
esp32c6: pin the pioarduino version to last working one
2025-08-03 12:53:45 +10:00
recrof
6861b0702f create sensor template in platformio.ini, update heltec v3 and rak4631 to use new template 2025-08-02 21:40:56 +02:00
Rastislav Vysoky
a5c78f2f19 Merge branch 'ripplebiz:dev' into dev 2025-08-02 20:54:26 +02:00
recrof
8d3bdc6945 pin the pioarduino version to last working one 2025-08-02 16:26:21 +02:00
ripplebiz
d13dc10bf3 Merge pull request #564 from oltaco/fix-wiotracker-l1-board
WioTracker L1 various fixes
2025-08-02 19:36:57 +10:00
taco
a49b5aaba7 fix: wrong QSPI pins for wio tracker l1 2025-08-02 04:07:07 +10:00
Scott Powell
febc63d286 * fix: init rx_air_time to 0 2025-08-01 20:05:37 +10:00
ripplebiz
b17196828d Merge pull request #563 from Cisien/cisien/add-rx-time
Add a counter to track rx time for repeater stats
2025-08-01 19:59:20 +10:00
Scott Powell
28af68c187 * new CommonCLI commands: "get prv.key", "set prv.key {hex}" 2025-08-01 19:28:44 +10:00
ripplebiz
0a2d132d84 Merge pull request #547 from fdlamotte/sensors_gpio
sensors: gpio command
2025-08-01 19:06:44 +10:00
Scott Powell
2824fc31a4 * CMD_SEND_PATH_DISCOVERY_REQ: now force the sendRequest() to use flood mode 2025-07-31 14:38:11 +10:00
Scott Powell
32e8ce4130 * REQ_TYPE_GET_TELEMETRY_DATA, first reserved byte (of 4) is now inverse mask to apply to permissions 2025-07-31 13:45:53 +10:00
Scott Powell
fcdf342db6 * Companion: experimental CMD_SEND_DISCOVERY_REQ -> PUSH_CODE_DISCOVERY_RESPONSE 2025-07-31 13:04:16 +10:00
taco
1dfc0e6975 fix: Wio Tracker L1: use correct max ram and flash size for S140 v7.3.0 softdevice 2025-07-31 07:03:58 +10:00
Chris
ea2ce93c02 Add a counter to track rx time for repeater stats 2025-07-30 09:41:20 -07:00
Scott Powell
f87e856347 * companion: fix for CMD_IMPORT_PRIVATE_KEY, to re-calc shared secrets 2025-07-30 16:00:47 +10:00
Scott Powell
f66d900ae2 * companion: bug fix for CMD_ADD_UPDATE_CONTACT 2025-07-30 14:57:38 +10:00
ripplebiz
84eafe4752 Merge pull request #559 from 446564/wsl3-usb-comp
add wsl3 usb companion
2025-07-30 14:12:43 +10:00
446564
dca20ea994 add wsl3 usb companion 2025-07-29 15:56:07 -07:00
João Brázio
85273a6dc6 Merge remote-tracking branch 'origin/dev' into jbrazio/2025_3f11ad35 2025-07-29 00:31:52 +01:00
ripplebiz
131e7a5a23 Merge pull request #558 from mkudlacek/dev
Added support for TI INA260 and Sensirion SHT4x
2025-07-28 23:10:38 +10:00
Martin Kudlacek
822850b4d5 Support for Sensirion SHT4x 2025-07-28 10:02:31 +02:00
Martin Kudlacek
a96d1022a0 Added support for TI INA260 2025-07-28 09:41:06 +02:00
ripplebiz
5f9210b5db Merge pull request #554 from oltaco/esm-bmp280-altitude-fix
fix: use correct library for BMP280 altitude read
2025-07-28 15:02:42 +10:00
taco
ef58ef460b fix: BMP280 altitude was using incorrect library 2025-07-28 11:29:55 +10:00
Scott Powell
b0946b3f6b Revert "disable led light on t-echo device"
This reverts commit 55453e1136.
2025-07-27 20:38:20 +10:00
ripplebiz
16820c5289 Merge pull request #553 from ch4r13/main
Disable LED lights on t-echo device
2025-07-27 17:54:26 +10:00
ripplebiz
39eb5502af Merge pull request #552 from recrof/dev
promicro: added sensor role, removed non-functional llcc68 variant envs
2025-07-27 17:53:47 +10:00
TasmanDevil
55453e1136 disable led light on t-echo device 2025-07-25 22:53:56 +02:00
Rastislav Vysoky
12f5177229 Merge branch 'ripplebiz:dev' into dev 2025-07-25 15:10:15 +02:00
Scott Powell
ad19ac1ab3 * Companion: CMD_ADD_UPDATE_CONTACT now with optional extra 'last_mod' timestamp 2025-07-25 22:59:48 +10:00
recrof
2e346bc61c promicro: added sensor role 2025-07-24 17:37:35 +02:00
Scott Powell
c1041af5a1 Merge branch 'dev' 2025-07-24 20:31:13 +10:00
Scott Powell
365cb89634 * ver bump to 1.7.4 2025-07-24 20:30:35 +10:00
ripplebiz
048fa03784 Merge pull request #548 from recrof/dev
SenseCap Solar compilation fix: bad linker directory; added companion ble and usb roles
2025-07-24 19:40:05 +10:00
Rastislav Vysoky
3139d509c2 Merge branch 'ripplebiz:dev' into dev 2025-07-24 09:38:41 +02:00
Scott Powell
4689f9b425 * CommonCLI: reverted "set radio ..." command, added new "tempradio ..." command 2025-07-23 22:04:47 +10:00
Scott Powell
ea4aa93594 * CommonCLI: "set radio " now with optional 5th param timeout_mins, for applying temporary radio params for that many mins
* "advert" command now with longer delay, so that CLI reply is sent first
2025-07-23 21:40:37 +10:00
Rastislav Vysoky
9485488f6e Merge branch 'ripplebiz:dev' into dev 2025-07-23 12:18:05 +02:00
recrof
e48e64ae84 fix: bad linker directory; added companion ble and usb roles 2025-07-23 12:17:40 +02:00
Florent
479b8ed0ce sensors: gpio apply masks 2025-07-23 08:02:57 +02:00
ripplebiz
c2266026a0 Merge pull request #541 from recrof/dev
enable all ESM sensors on RAK4631
2025-07-23 13:44:23 +10:00
Florent
b5a8a1a883 sensors: gpio command 2025-07-22 21:08:15 +02:00
ripplebiz
e42d8f972e Merge pull request #540 from oltaco/wiotrackerl1-pinfix
Wio Tracker L1: fix incorrect joystick pin.
2025-07-21 23:47:41 +10:00
recrof
f88ebad604 enable all sensors on RAK4631 2025-07-21 15:30:14 +02:00
taco
296a1e45fb fix: Wio Tracker L1: correct joystick press button pin 2025-07-21 18:17:32 +10:00
ripplebiz
0a9da09a67 Merge pull request #539 from liamcottle/fix/missed-packets
Start receive immediately after packet received
2025-07-20 21:57:44 +10:00
liamcottle
599e3a187c start rx again immediately after receive 2025-07-20 23:27:54 +12:00
ripplebiz
c6d4b7513f Merge pull request #538 from oltaco/ct62-std-init
Heltec CT62: use radio.std_init()
2025-07-19 17:25:10 +10:00
taco
c5783660c4 radio.std_init() for heltec ct62 2025-07-19 16:09:54 +10:00
ripplebiz
a2e3e6607e Merge pull request #534 from mikenz/heltec-v3-sensor
Allow the SDA and SCL pins for Environment sensors to be configured independently of the OLED I2C
2025-07-18 20:33:48 +10:00
ripplebiz
64cc4cf60a Merge pull request #535 from fdlamotte/rak3x72_sensor
rak3x72: sensor target
2025-07-18 20:25:20 +10:00
Florent
9be28c2002 rak3x72: sensor target 2025-07-18 12:18:03 +02:00
Mike Cochrane
93802fe250 Add VL53L0X time-of-flight distance sensor to Heltec V3 Sensor 2025-07-18 22:01:47 +12:00
Mike Cochrane
9f2a77c92e Add Melexis Contact-less Infrared Sensor - MLX90614 to Heltec V3 Sensor 2025-07-18 18:51:00 +12:00
Mike Cochrane
e4f7b9e37f Allow the SDA and SCL pins for Environment sensors to be configured independantly. Add Heltec V3 Sensor. 2025-07-18 18:16:59 +12:00
Scott Powell
855e4831f5 * updates to packet/payload structure docs 2025-07-18 12:56:31 +10:00
ripplebiz
e9a8fcb1cd Merge pull request #531 from cod3doomy/dev
RAK4631 ESM Migration
2025-07-18 11:53:48 +10:00
cod3doomy
6b4592bfe2 Cleanup and fixes
-Added RAK_4631 define back
-Added includes for common RAK sensors that are currently supported in ESM
-Set global variables to static
-Reduced delay time within the RAK gps init sequence
2025-07-17 10:42:18 -07:00
cod3doomy
73b1ac5190 Merge branch 'ripplebiz:dev' into dev 2025-07-17 08:49:02 -07:00
ripplebiz
46d30f6bfe Merge pull request #521 from recrof/dev
cleanup: move radiolib wrappers to dedicated directory
2025-07-17 13:49:58 +10:00
cod3doomy
660ab0692f RAK4631 ESM Migration
Changes to migrate sensor code to the ESM.

Added a separate GPS init sequence for the RAK that scans I2C and Serial1 on the various sockets of the various base boards to find the RAK12500. (and soon the RAK12501)

Removed the GPS specific envs from platformio.ini and enabled GPS for all envs.

Verified working with RAK12500 on RAK19007 sockets A and D, as well as RAK19003.
2025-07-16 19:04:50 -07:00
Rastislav Vysoky
2c9dc8d351 Merge branch 'ripplebiz:dev' into dev 2025-07-16 20:05:21 +02:00
ripplebiz
6a6221f44e Merge pull request #529 from marcelverdult/patch-1
Fixed Barometric Pressure Reading for BMP280
2025-07-16 23:06:37 +10:00
ripplebiz
46fa3f2026 Merge pull request #527 from oltaco/wio-tracker-l1-radiofix
fix: Wio Tracker L1: add DIO2 as rfSwitch and correct TCXO voltage.
2025-07-16 23:05:47 +10:00
ripplebiz
122f5fa10a Merge pull request #526 from oltaco/uitask-add-gps-alert
add GPS UI alert to quad-press
2025-07-16 23:04:39 +10:00
marcelverdult
58cffa8f76 Fixed Barometric Pressure Reading for BMP280
Measurement has to be divided by 100 same as BME280
2025-07-16 13:50:23 +02:00
Scott Powell
3358783039 * sensor: "setperm {pubkey-hex} 0" command can now remove by partial pubkey
* sensor: login with blank password now just checks if sender is in ACL, and returns permissions (if so)
2025-07-16 21:16:05 +10:00
Scott Powell
5881b04a31 * companion: optional double ACKs, new prefs.multi_acks 2025-07-16 19:25:28 +10:00
Scott Powell
6bc8dd28d4 * CommonCLI: new "multi.acks" config setting 2025-07-16 18:51:18 +10:00
Scott Powell
3a0dfc1bf3 Merge branch 'dev' into double-acks 2025-07-16 18:02:41 +10:00
Scott Powell
d15b374c29 * Sensor permission levels renamed. Misc sensor fixes. 2025-07-16 14:18:05 +10:00
taco
3f996ef4fc fix: Wio Tracker L1: add DIO2 as rfSwitch and correct TCXO voltage. 2025-07-16 12:25:16 +10:00
taco
57f93a4196 add GPS UI alert to quad-press 2025-07-16 10:35:48 +10:00
Rastislav Vysoky
a6c8dc4866 Merge branch 'ripplebiz:dev' into dev 2025-07-15 12:19:51 +02:00
Scott Powell
c26418016b Merge branch 'dev' into double-acks 2025-07-15 17:02:20 +10:00
Scott Powell
c6b469fa47 Merge branch 'dev' 2025-07-15 16:00:11 +10:00
Scott Powell
f74819f8db * ver bump 2025-07-15 15:59:10 +10:00
Scott Powell
fccb3b6c39 * companion: added CMD_FACTORY_RESET (51) 2025-07-15 15:56:59 +10:00
Scott Powell
7947e8a2d8 * simple_sensor: redesigned permissions
* companion:  PUSH_CODE_LOGIN_SUCCESS now has extra byte in frame for ACL permissions
2025-07-15 15:05:38 +10:00
Scott Powell
da8bd717a4 * companion: serial protocol ver bump (FIRMWARE_VER_CODE) now 7 2025-07-14 13:09:22 +10:00
Scott Powell
1930dc347e * companion: reverted PUSH_CODE_TELEMETRY_RESPONSE, added new PUSH_CODE_BINARY_RESPONSE 2025-07-14 12:46:51 +10:00
Scott Powell
df33321bdc * companion: added CMD_SEND_BINARY_REQ (50) 2025-07-14 12:25:34 +10:00
ripplebiz
2c9a2ee18f Merge pull request #523 from recrof/patch-12
sensecap_solar: disable GPS until it's supported
2025-07-14 10:51:38 +10:00
Scott Powell
8c104b8a8f Merge commit '9117798a41ce1538dae824e18a54a1fa3a735126' into dev 2025-07-14 10:50:23 +10:00
ripplebiz
9117798a41 Merge pull request #522 from recrof/patch-11
Heltec Wireless Paper fix: radio init failed: -2
2025-07-14 10:49:12 +10:00
Scott Powell
4a2978736e * Sensor: "get acl" command 2025-07-14 10:12:27 +10:00
Rastislav Vysoky
3c92c6aa3b sensecap_solar: disable GPS until it's supported 2025-07-13 22:41:27 +02:00
Rastislav Vysoky
f9e595687e Heltec Wireless Paper fix: radio init failed: -2 2025-07-13 15:21:02 +02:00
Rastislav Vysoky
3adbb5042e Merge branch 'ripplebiz:dev' into dev 2025-07-13 11:39:02 +02:00
recrof
4fcbc00bea Merge branch 'dev' of github.com:recrof/MeshCore into dev 2025-07-13 11:37:58 +02:00
recrof
6be8e19a9f move radiolib wrappers to dedicated directory 2025-07-13 11:37:33 +02:00
Scott Powell
be68aaed20 * simple_sensor: new REQ_TYPE_GET_ACCESS_LIST 2025-07-13 18:50:52 +10:00
Scott Powell
339ee035aa * simple_sensor: handleCustomCommand() hook 2025-07-13 15:30:49 +10:00
ripplebiz
ced14d65db Merge pull request #517 from recrof/dev
platformio.ini cleanup: move rak to nrf52_core, remove nrf52840_core
2025-07-13 14:47:21 +10:00
recrof
854a8dfe2f move rak to nrf52_core, remove nrf52840_core 2025-07-12 20:06:56 +02:00
Scott Powell
0d1b5b17d3 * simple_sensor: added alert send queue, with retries, checks for ACKs, etc. Low pri alerts only 1 send attempt, otherwise 4 attempts 2025-07-12 12:26:16 +10:00
Scott Powell
d84feacc60 Merge branch 'dev' into double-acks 2025-07-12 10:36:03 +10:00
ripplebiz
fc541bdf42 Merge pull request #511 from oltaco/wio-tracker-l1
Seeed Wio Tracker L1: initial support
2025-07-11 21:31:39 +10:00
ripplebiz
fe2616d19c Merge pull request #514 from recrof/patch-10
replaced Andy Kirby's discord with MeshCore discord.
2025-07-11 21:22:35 +10:00
ripplebiz
7958b920fa Merge pull request #513 from recrof/patch-9
faq: fix broken links, replace discord link from Andy's discord to MeshCore discord
2025-07-11 21:21:06 +10:00
Rastislav Vysoky
10bb05c31a replaced Andy Kirby's discord with MeshCore discord. 2025-07-11 11:39:50 +02:00
Rastislav Vysoky
6aa41bd67d faq: fix broken links, replace discord link from Andy's discord to MeshCore discord 2025-07-11 11:37:01 +02:00
taco
78cd655789 Seeed Wio Tracker L1: initial support 2025-07-11 01:08:38 +10:00
ripplebiz
e8b1f317f3 Merge pull request #503 from fdlamotte/buzzer--disable-when-quiet
Buzzer  disable when quiet
2025-07-10 14:58:42 +10:00
ripplebiz
cd1cf71f39 Merge pull request #506 from 446564/cleanup-rak4631
move rak4631 specific files into variant folder
2025-07-10 14:55:05 +10:00
ripplebiz
55a259b0a1 Merge pull request #508 from ngavars/cleanup-minewsemi
Clean up Minewsemi ME25LS01 variant
2025-07-10 14:54:20 +10:00
ripplebiz
75486f5d41 Merge pull request #507 from ngavars/cleanup-promicro
Clean up Promicro variant
2025-07-10 14:53:47 +10:00
ripplebiz
90db5f7e39 Merge pull request #505 from fdlamotte/wio_e5_mini_sensor
wio-e5-mini: simple_sensor target
2025-07-10 14:51:48 +10:00
Normunds Gavars
ed7ca6fb60 393 clean up Minewsemi ME25LS01 variant 2025-07-10 00:55:38 +03:00
Normunds Gavars
cdd44212a1 393 clean up Promicro variant 2025-07-10 00:08:59 +03:00
Rob Loranger
9d0dd7947f move rak4631 specific files into variant folder 2025-07-09 10:21:24 -07:00
Florent
5f7bd0fe77 wio-e5-mini: simple_sensor target 2025-07-09 17:22:31 +02:00
Scott Powell
781f7e99f6 * companion: added CMD_GET_TUNING_PARAMS -> RESP_CODE_TUNING_PARAMS 2025-07-09 23:10:33 +10:00
João Brázio
04042e3ca0 Refactor serial bridge handling 2025-07-09 11:03:35 +01:00
Scott Powell
797ab85283 * sensor node: now have two alert priorities, LO, HI 2025-07-09 15:50:36 +10:00
Scott Powell
1f23632751 Merge branch 'dev' into double-acks 2025-07-09 14:59:25 +10:00
ripplebiz
91b911320b Merge pull request #504 from jaspersonneveldt/dev
Changed the Barometric Pressure value for the BME280 sensor since it was a factor 100 to high
2025-07-09 14:35:23 +10:00
João Brázio
97b51900f8 More robust handling of pkt len 2025-07-08 21:45:49 +01:00
jasper
7d47608985 Changed the Barometric Pressure value since it was a factor 100 to high 2025-07-08 21:16:03 +02:00
João Brázio
92ee1820c4 Add null check for packet allocation and clean up Dispatcher 2025-07-08 16:02:10 +01:00
Scott Powell
541cd8cfd9 * misc 2025-07-08 23:19:35 +10:00
Scott Powell
2715058eb2 * misc fixes 2025-07-08 23:19:35 +10:00
Scott Powell
112b360ef4 * implemented encoding responses to REQ_TYPE_GET_AVG_MIN_MAX 2025-07-08 23:19:35 +10:00
Scott Powell
29435342b0 * implemented getter methods for telemetry value types 2025-07-08 23:19:35 +10:00
Scott Powell
9cecbad2a7 * refactor: CommonCLI, processing of optional command prefix moved to handleCommand() call sites
* Sensor, anon_req now just for admin login (guest password now unused)
* special CLI command, "setperm {pubkey-hex} {permissions-int16}" for admin(s) to manage user access (permissions 0 = remove)
2025-07-08 23:19:35 +10:00
Scott Powell
ac834922de * simplified alertIf()
* refactored TimeSeriesData to top-level class
2025-07-08 23:19:35 +10:00
Scott Powell
de3e4bc27c * added REQ_TYPE_GET_AVG_MIN_MAX
* TimeSeriesData
* very basic SensorMesh::sendAlert()
2025-07-08 23:19:35 +10:00
Scott Powell
810b1f8fe7 * Mesh::onAnonDataRecv() slight optimisation, so that shared-secret calc doesn't need to be repeated
* SensporMesh: req_type now optionally encoded in anon_req payload (so can send various requests without a prior login)
2025-07-08 23:19:35 +10:00
Scott Powell
7fb7b69bbc * first cut of new simple_sensor sketch 2025-07-08 23:19:34 +10: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
Scott Powell
d3831821c7 * XiaoC3 custom, .ini fixes 2025-07-08 22:59:07 +10:00
Scott Powell
7bec45b3dd Merge branch 'dev' into double-acks 2025-07-08 22:14:48 +10:00
Florent
1c7c5ecb2b buzzer: disable when quiet 2025-07-08 14:01:31 +02:00
ripplebiz
58f4db1f19 Merge pull request #502 from jankowski-t/main
Migrate Meshadventurer to std_init()
2025-07-08 15:39:20 +10:00
jankowski-t
00ebb090e7 Migrate Meshadventurer to std_init() 2025-07-07 18:33:31 +02:00
ripplebiz
35374947ba Merge pull request #501 from fdlamotte/xiao_c3--small-fixups
xiao_c3: small fixups
2025-07-07 23:39:58 +10:00
Florent de Lamotte
d30412bf65 xiao_c3: small fixups 2025-07-07 10:41:29 +02:00
ripplebiz
02645be9df Merge pull request #500 from liamcottle/fix/nrf52-ble-refactor
Refactor nRF52 BLE to use connection callbacks
2025-07-07 17:00:24 +10:00
liamcottle
67f9204e88 refactor nrf52 ble to use callbacks 2025-07-07 16:36:55 +12:00
ripplebiz
992c8e49d4 Merge pull request #499 from jrkalf/dev
Xiao ESP32 C3 updates
2025-07-07 14:28:12 +10:00
ripplebiz
987c42409a Merge pull request #495 from recrof/dev
Seeed SenseCap Solar: correct LED behaviour: Lit when TX
2025-07-07 14:23:55 +10:00
ripplebiz
71f46ddaea Merge pull request #496 from recrof/patch-8
remove hardware compatibility list - it was outdated.
2025-07-07 14:20:08 +10:00
ripplebiz
0f2f1bc8be Merge pull request #490 from Noki/tbeam-sx1262-room
add room server role for TBeam SX1262
2025-07-07 14:19:33 +10:00
Jelle Kalf
5ec89dff5b Xiao ESP32 C3:
* Fixed pins for mainstream wio sx1262
* Moved previous sx1262 support to _custom version
* companion firmware added
2025-07-06 19:52:12 +02:00
Rastislav Vysoky
62f1ab4b06 remove hardware compatibility list - it was outdated. 2025-07-06 17:40:52 +02:00
recrof
88cbe3fddc Merge branch 'dev' of github.com:recrof/MeshCore into dev 2025-07-06 15:23:35 +02:00
recrof
e47755c8e9 Seeed SenseCap Solar: invert leds 2025-07-06 15:22:51 +02:00
ripplebiz
99e6b75743 Merge pull request #493 from liamcottle/fix/cli-messages
Don't show CLI replies on display
2025-07-06 17:54:18 +10:00
liamcottle
0914056a09 tidy logic for devices with display 2025-07-06 14:16:43 +12:00
liamcottle
7ea6a98513 dont show cli data replies on display 2025-07-06 14:07:56 +12:00
Tobias Schwarz
013787556d add room server role for TBeam SX1262 2025-07-05 16:08:49 +02:00
ripplebiz
54890421bb Merge pull request #475 from rightup/NRF52-BLE-FIX
Remove ScanResponse.addName() to fix re-advertising after disconnect
2025-07-05 22:02:27 +10:00
ripplebiz
0ddd3b9ade Merge pull request #474 from ngavars/dev
Add support for Minewsemi ME25LS01
2025-07-05 20:48:05 +10:00
ripplebiz
ae5e3588ba Merge pull request #484 from liquidraver/dev
LR's (corrected) calculation instead of SX and minor changes
2025-07-05 20:38:42 +10:00
Lloyd
d32fa5c004 Manually restart BLE advertising after disconnect to prevent stack freeze
Replaced use of `restartOnDisconnect(true)` with explicit (existing) manual re-advertising logic.
This avoids Bluetooth stack instability caused by overlapping advertising state,
Changes:
- Added explicit `Bluefruit.Advertising.stop()` and data clears in `startAdv()`
- Disabled automatic restart with `restartOnDisconnect(false)`
- Re-advertising now fully handled in `checkRecvFrame()` loop

Tested on: iPhone, Android, Windows, and Chrome – confirmed stable reconnects and name visibility.
2025-07-04 21:07:55 +01:00
Normunds Gavars
aa3c702ffd Read battery voltage on Minewsemi ME25LS01 2025-07-04 19:27:11 +03:00
liquidraver
fa481e832b LR's corrected calculation override (instead of SX) and minor changes according to radiolib's wiki 2025-07-04 16:40:19 +02:00
ripplebiz
ff9699c071 Merge pull request #482 from recrof/dev
initial support for Seeed Studio SenseCap Solar board
2025-07-05 00:31:23 +10:00
ripplebiz
2c1f61c03d Merge pull request #483 from fdlamotte/stm32_preamble
stm32 targets: set preamble to 16
2025-07-04 23:57:43 +10:00
Florent de Lamotte
71255e00f1 stm32 targets: set preamble to 16 2025-07-04 15:42:56 +02:00
recrof
2941388041 initial support for Seeed Studio SenseCap Solar board 2025-07-04 15:03:25 +02:00
Scott Powell
3d70a0d02c * added RADIOLIB_EXLUDE_'s for faster builds 2025-07-04 21:33:07 +10:00
João Brázio
3375389181 Merge remote-tracking branch 'upstream/dev' into jbrazio/2025_3f11ad35 2025-07-04 11:57:09 +01:00
ripplebiz
3d2404f249 Merge pull request #479 from 446564/cleanup-xio-nrf
cleanup xiao nrf52
2025-07-04 16:18:14 +10:00
ripplebiz
cf35daddc2 Merge pull request #481 from oltaco/heltecV3-gps_pin_fix
fix: Heltec V3 amended GPS pins
2025-07-04 15:49:08 +10:00
taco
2bb7e6dad4 fix: heltec v3: change gps enable pin 2025-07-04 14:12:57 +10:00
taco
74818d0594 fix: change GPS pins
Pin 45 and 46 are strapping pins on ESP32-S3, which can lead to unintended consequences on boot. I have amended the pins and added an enable pin as well.
2025-07-04 13:55:39 +10:00
ripplebiz
484b7b8144 Merge pull request #476 from WattleFoxxo/xiao-rp2040
Adds support for the XIAO-RP2040 board
2025-07-04 10:08:31 +10:00
ripplebiz
cb423bcb71 Merge pull request #471 from 446564/rak-usr-btn
move rak usr btn to companions
2025-07-04 10:03:26 +10:00
ripplebiz
837870169a Merge pull request #468 from recrof/dev
EnvironmentSensorManager: add support for SHTC3 and LPS22HB
2025-07-04 07:36:22 +10:00
Rob Loranger
90656e7d06 clean up xiao nrf52
move variant specific code out of src/helpers
redefine RXEN for alternate radio pinout
2025-07-03 09:18:26 -07:00
Rastislav Vysoky
d82b2a28e4 Merge branch 'ripplebiz:dev' into dev 2025-07-03 15:07:16 +02:00
Scott Powell
eb978f1b50 Merge branch 'LR1110-workaround' into dev 2025-07-03 19:38:20 +10:00
ripplebiz
910ec59887 Merge pull request #469 from jquatier/heltec-paper-include-fix
Fixing radio include and BT pin for Heltec Paper
2025-07-03 16:43:18 +10:00
WattleFoxxo
d23378cff6 Add XIAO RP2040 support 2025-07-03 11:42:53 +10:00
Lloyd
ec98d5f8a5 BLE: Remove ScanResponse.addName() to fix re-advertising after disconnect
Removed the call to Bluefruit.ScanResponse.addName() in startAdv(), as it was preventing BLE from reliably restarting advertising after a disconnect.

Hypothesis: adding the device name to the scan response exceeds internal buffer limits or causes a conflict with advertising timing, leading to the BLE stack silently failing to re-advertise.

Tested successfully (on T-1000) without this line, advertising now resumes correctly after disconnection (on Iphone)
2025-07-02 23:41:31 +01:00
JQ
ca422bbafb fix ble pin 2025-07-02 14:37:11 -07:00
Normunds Gavars
70a9990f45 Merge branch 'dev' into minewsemi-me25ls01 2025-07-03 00:14:39 +03:00
Normunds Gavars
6440bcaf48 Clean up pins in variant.h 2025-07-03 00:07:50 +03:00
Rob Loranger
ad2e015a5b move rak usr btn to companions
repeaters do not typically have user buttons and
there is only one analog pin available on most, if
not all, base boards. so this allows repeaters to
add custom peripherals or alternate battery signals
2025-07-02 10:24:45 -07:00
JQ
dcb7ffa92e fixing radio include order for heltec paper 2025-07-02 08:32:36 -07:00
recrof
539f99a90f removed unsupported(?) readAltitude 2025-07-02 16:50:47 +02:00
recrof
3832836eb2 EnvironmentSensorManager: add support for SHTC3 and LPS22HB 2025-07-02 16:42:35 +02:00
Normunds Gavars
af2628bb00 Use EnvironmentSensorManager in Minewsemi variant 2025-07-02 12:27:39 +03:00
Normunds Gavars
6f94c8148a Add Minewsemi ME25LS01 variant 2025-07-01 01:56:34 +03:00
Scott Powell
8c80c10d2a * CustomLR1110::getTimeOnAir(), copied from sx1262 2025-06-30 18:57:24 +10:00
João Brázio
2f77cef04b Add config flags to variants 2025-06-29 16:28:11 +01:00
Scott Powell
f7920114c5 Merge branch 'dev' into double-acks 2025-06-29 21:10:43 +10:00
João Brázio
4b70ee863d Serial bridge implementation 2025-06-27 20:16:14 +01:00
Scott Powell
127f3a7640 Merge branch 'dev' into double-acks 2025-06-25 13:52:11 +10:00
Scott Powell
70252b010c Merge branch 'dev' into double-acks 2025-06-24 13:07:36 +10:00
Scott Powell
b1ca3d1eb1 * new PAYLOAD_TYPE_MULTIPART
* experimental double ACK's (at each hop), sent 300ms apart (direct mode only)
2025-06-23 01:14:08 +10:00
405 changed files with 24364 additions and 4420 deletions

5
.gitignore vendored
View File

@@ -9,3 +9,8 @@ out/
.DS_Store
.vscode/settings.json
.vscode/extensions.json
.idea
cmake-*
.cache
.ccls
compile_commands.json

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.
@@ -74,17 +77,7 @@ They can also be managed via LoRa in the mobile app by using the Remote Manageme
## 🛠 Hardware Compatibility
MeshCore is designed for use with:
* Heltec V3 LoRa Boards
* RAK4631
* XiaoS3 WIO (sx1262 combo)
* XiaoC3 (plus external sx126x module)
* LilyGo T3S3
* Heltec T114
* Station G2
* Sensecap T1000e
* Heltec V2
* LilyGo TLora32 v1.6
MeshCore is designed for devices listed in the [MeshCore Flasher](https://flasher.meshcore.co.uk)
## 📜 License
@@ -100,17 +93,23 @@ Here are some general principals you should try to adhere to:
* 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
- [X] Repeater + Room Server: add ACL's (like Sensor Node has)
- [X] Standardise Bridge mode for repeaters
- [ ] Repeater/Bridge: Standardise the Transport Codes for zoning/filtering
- [X] 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 concensus 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.
- Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz).
- Join [Andy Kirby's Discord](https://discord.gg/GBxVx2JMAy) to chat with the developers and get help from the community.
## RAK Wireless Board Support in PlatformIO
Before building/flashing the RAK4631 targets in this project, there is, unfortunately, some patching you have to do to your platformIO packages to make it work. There is a guide here on the process:
[RAK Wireless: How to Perform Installation of Board Support Package in PlatformIO](https://learn.rakwireless.com/hc/en-us/articles/26687276346775-How-To-Perform-Installation-of-Board-Support-Package-in-PlatformIO)
After building, you will need to convert the output firmware.hex file into a .uf2 file you can copy over to your RAK4631 device (after doing a full erase) by using the command `uf2conv.py -f 0xADA52840 -c firmware.hex` with the python script available from:
[GitHub: Microsoft - uf2](https://github.com/Microsoft/uf2/blob/master/utils/uf2conv.py)
- Join [MeshCore Discord](https://discord.gg/BMwCtwHj5V) to chat with the developers and get help from the community.

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

44
boards/heltec_e213.json Normal file
View File

@@ -0,0 +1,44 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [
["0x303A", "0x1001"],
["0x303A", "0x0002"]
],
"mcu": "esp32s3",
"variant": "heltec_vision_master_e213"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "Heltec Vision Master E213",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 8388608,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/project/vision-master-e213/",
"vendor": "Heltec"
}

44
boards/heltec_e290.json Normal file
View File

@@ -0,0 +1,44 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [
["0x303A", "0x1001"],
["0x303A", "0x0002"]
],
"mcu": "esp32s3",
"variant": "heltec_vision_master_e290"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "Heltec Vision Master E290",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 8388608,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/project/vision-master-e290/",
"vendor": "Heltec"
}

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,61 @@
{
"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": "HT-n5262",
"mcu": "nrf52840",
"variant": "heltec_mesh_solar",
"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"
],
"name": "Heltec Mesh Solar 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://heltec.org/",
"vendor": "Heltec"
}

44
boards/heltec_t190.json Normal file
View File

@@ -0,0 +1,44 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [
["0x303A", "0x1001"],
["0x303A", "0x0002"]
],
"mcu": "esp32s3",
"variant": "heltec_vision_master_t190"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "Heltec Vision Master T190",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 8388608,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/project/vision-master-t190/",
"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"
}

View File

@@ -0,0 +1,59 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v7.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x8029"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x239A", "0x802A"]
],
"usb_product": "me25ls01-BOOT",
"mcu": "nrf52840",
"variant": "minewsemi_me25ls01",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "7.3.0",
"sd_fwid": "0x0123"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52.cfg"
},
"frameworks": ["arduino"],
"name": "Minewsemi ME25LS01",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink",
"cmsis-dap",
"blackmagic"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://en.minewsemi.com/lora-module/lr1110-nrf52840-me25LS01",
"vendor": "MINEWSEMI"
}

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

@@ -0,0 +1,62 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v7.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_SEEED_WIO_TRACKER_L1 -DNRF52840_XXAA -DSEEED_WIO_TRACKER_L1 ",
"f_cpu": "64000000L",
"hwids": [
[ "0x2886", "0x1667" ],
[ "0x2886", "0x1668" ]
],
"mcu": "nrf52840",
"variant": "Seeed_Wio_Tracker_L1",
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "7.3.0",
"sd_fwid": "0x0123"
},
"bsp": {
"name": "adafruit"
},
"bootloader": {
"settings_addr": "0xFF000"
},
"usb_product": "Seeed Wio Tracker L1"
},
"connectivity": [
"bluetooth"
],
"debug": {
"jlink_device": "nRF52840_xxAA",
"openocd_target": "nrf52.cfg",
"svd_path": "nrf52840.svd"
},
"frameworks": [
"arduino"
],
"name": "Seeed Wio Tracker L1",
"upload": {
"maximum_ram_size": 237568,
"maximum_size": 811008,
"protocol": "nrfutil",
"speed": 115200,
"protocols": [
"jlink",
"stlink",
"nrfjprog",
"nrfutil",
"cmsis-dap",
"sam-ba",
"blackmagic"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://wiki.seeedstudio.com/wio_tracker_l1_node/",
"vendor": "Seeed Studio"
}

View File

@@ -0,0 +1,60 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v7.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_Seeed_XIAO_nRF52840 -DNRF52840_XXAA -DSEEED_XIAO_NRF52840 ",
"f_cpu": "64000000L",
"hwids": [
[ "0x2886", "0x0059" ]
],
"mcu": "nrf52840",
"variant": "Seeed_XIAO_nRF52840",
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "7.3.0",
"sd_fwid": "0x0123"
},
"bsp": {
"name": "adafruit"
},
"bootloader": {
"settings_addr": "0xFF000"
},
"usb_product": "XIAO nRF52840"
},
"connectivity": [
"bluetooth"
],
"debug": {
"jlink_device": "nRF52840_xxAA",
"openocd_target": "nrf52.cfg",
"svd_path": "nrf52840.svd"
},
"frameworks": [
"arduino"
],
"name": "Seeed Studio XIAO nRF52840",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"protocol": "nrfutil",
"speed": 115200,
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"cmsis-dap",
"sam-ba",
"blackmagic"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://wiki.seeedstudio.com/meshtastic_solar_node/",
"vendor": "Seeed Studio"
}

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

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

View File

@@ -1,11 +1,45 @@
#!/usr/bin/env bash
# usage
# sh build.sh build-firmware RAK_4631_Repeater
# sh build.sh build-firmwares
# 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.
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
EOF
}
# Catch cries for help before doing anything else.
case $1 in
help|usage|-h|--help)
global_usage
exit 1
;;
esac
# get a list of pio env names that start with "env:"
get_pio_envs() {
@@ -23,6 +57,17 @@ 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
}
# build firmware for the provided pio env in $1
build_firmware() {
@@ -46,8 +91,8 @@ 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}\"'"
# build firmware target
pio run -e $1
@@ -59,7 +104,7 @@ build_firmware() {
# 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
python3 bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840
fi
# copy .bin, .uf2, and .zip to out folder
@@ -84,6 +129,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
@@ -95,7 +148,7 @@ build_repeater_firmwares() {
# build_firmware "RAK_4631_Repeater"
# build all repeater firmwares
build_all_firmwares_matching "repeater"
build_all_firmwares_by_suffix "_repeater"
}
@@ -114,8 +167,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"
}
@@ -126,7 +179,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"
}
@@ -142,8 +195,21 @@ mkdir -p out
# handle script args
if [[ $1 == "build-firmware" ]]; then
TARGETS=${@:2}
if [ "$TARGETS" ]; then
for env in $TARGETS; do
build_firmware $env
done
else
echo "usage: $0 build-firmware <target>"
exit 1
fi
elif [[ $1 == "build-matching-firmwares" ]]; then
if [ "$2" ]; then
build_firmware $2
build_all_firmwares_matching $2
else
echo "usage: $0 build-matching-firmwares <build-match-spec>"
exit 1
fi
elif [[ $1 == "build-firmwares" ]]; then
build_firmwares

64
build_as_lib.py Normal file
View File

@@ -0,0 +1,64 @@
from os.path import realpath
Import("env") # type: ignore
menv=env # type: ignore
src_filter = [
'+<*.cpp>',
'+<helpers/*.cpp>',
'+<helpers/sensors>',
'+<helpers/radiolib/*.cpp>',
'+<helpers/ui/MomentaryButton.cpp>',
'+<helpers/ui/buzzer.cpp>',
]
# add build and include dirs according to CPPDEFINES
for item in menv.get("CPPDEFINES", []):
# PLATFORM HANDLING
if item == "STM32_PLATFORM":
src_filter.append("+<helpers/stm32/*>")
elif item == "ESP32":
src_filter.append("+<helpers/esp32/*>")
elif item == "NRF52_PLATFORM":
src_filter.append("+<helpers/nrf52/*>")
elif item == "RP2040_PLATFORM":
src_filter.append("+<helpers/rp2040/*>")
# DISPLAY HANDLING
elif isinstance(item, tuple) and item[0] == "DISPLAY_CLASS":
display_class = item[1]
src_filter.append(f"+<helpers/ui/{display_class}.cpp>")
if (display_class == "ST7789Display") :
src_filter.append(f"+<helpers/ui/OLEDDisplay.cpp>")
src_filter.append(f"+<helpers/ui/OLEDDisplayFonts.cpp>")
# VARIANTS HANDLING
elif isinstance(item, tuple) and item[0] == "MC_VARIANT":
variant_name = item[1]
src_filter.append(f"+<../variants/{variant_name}>")
# INCLUDE EXAMPLE CODE IN BUILD (to provide your own support files without touching the tree)
elif isinstance(item, tuple) and item[0] == "BUILD_EXAMPLE":
example_name = item[1]
src_filter.append(f"+<../examples/{example_name}/*.cpp>")
# EXCLUDE A SOURCE FILE FROM AN EXAMPLE (must be placed after example name or boom)
elif isinstance(item, tuple) and item[0] == "EXCLUDE_FROM_EXAMPLE":
exclude_name = item[1]
if example_name is None:
print("***** PLEASE DEFINE EXAMPLE FIRST *****")
break
src_filter.append(f"-<../examples/{example_name}/{exclude_name}>")
# DEAL WITH UI VARIANT FOR AN EXAMPLE
elif isinstance(item, tuple) and item[0] == "MC_UI_FLAVOR":
ui_flavor = item[1]
if example_name is None:
print("***** PLEASE DEFINE EXAMPLE FIRST *****")
break
src_filter.append(f"+<../examples/{example_name}/{ui_flavor}/*.cpp>")
menv.Replace(SRC_FILTER=src_filter)
#print (menv.Dump())

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

View File

@@ -4,6 +4,7 @@ in
pkgs.mkShell {
buildInputs = [
pkgs.platformio
pkgs.python3
# optional: needed as a programmer i.e. for esp32
pkgs.avrdude
];

View File

@@ -1,10 +1,6 @@
**MeshCore-FAQ**<!-- omit from toc -->
A list of frequently-asked questions and answers for MeshCore
The current version of this MeshCore FAQ is at https://github.com/ripplebiz/MeshCore/blob/main/docs/faq.md.
This MeshCore FAQ is also mirrored at https://github.com/LitBomb/MeshCore-FAQ and might have newer updates if pull requests on Scott's MeshCore repo are not approved yet.
author: https://github.com/LitBomb<!-- omit from toc -->
---
- [1. Introduction](#1-introduction)
@@ -27,18 +23,20 @@ author: https://github.com/LitBomb<!-- omit from toc -->
- [3.3. Q: What is the password to administer a repeater or a room server?](#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server)
- [3.4. Q: What is the password to join a room server?](#34-q-what-is-the-password-to-join-a-room-server)
- [4. T-Deck Related](#4-t-deck-related)
- [4.1. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?](#41-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode)
- [4.2. Q: Why is my T-Deck Plus not getting any satellite lock?](#42-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock)
- [4.3. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?](#43-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock)
- [4.4. Q: What size of SD card does the T-Deck support?](#44-q-what-size-of-sd-card-does-the-t-deck-support)
- [4.5. Q: What is the public key for the default public channel?](#45-q-what-is-the-public-key-for-the-default-public-channel)
- [4.6. Q: How do I get maps on T-Deck?](#46-q-how-do-i-get-maps-on-t-deck)
- [4.7. Q: Where do the map tiles go?](#47-q-where-do-the-map-tiles-go)
- [4.8. Q: How to unlock deeper map zoom and server management features on T-Deck?](#48-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck)
- [4.9. Q: How to decipher the diagnostics screen on T-Deck?](#49-q-how-to-decipher-the-diagnostics-screen-on-t-deck)
- [4.10. Q: The T-Deck sound is too loud?](#410-q-the-t-deck-sound-is-too-loud)
- [4.11. Q: Can you customize the sound?](#411-q-can-you-customize-the-sound)
- [4.12. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?](#412-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts)
- [4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?](#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro)
- [4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?](#42-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode)
- [4.3. Q: Why is my T-Deck Plus not getting any satellite lock?](#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock)
- [4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?](#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock)
- [4.5. Q: What size of SD card does the T-Deck support?](#45-q-what-size-of-sd-card-does-the-t-deck-support)
- [4.6. Q: what is the public key for the default public channel?](#46-q-what-is-the-public-key-for-the-default-public-channel)
- [4.7. Q: How do I get maps on T-Deck?](#47-q-how-do-i-get-maps-on-t-deck)
- [4.8. Q: Where do the map tiles go?](#48-q-where-do-the-map-tiles-go)
- [4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?](#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck)
- [4.10. Q: How to decipher the diagnostics screen on T-Deck?](#410-q-how-to-decipher-the-diagnostics-screen-on-t-deck)
- [4.11. Q: The T-Deck sound is too loud?](#411-q-the-t-deck-sound-is-too-loud)
- [4.12. Q: Can you customize the sound?](#412-q-can-you-customize-the-sound)
- [4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?](#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts)
- [4.14. Q: How to capture a screenshot on T-Deck?](#414-q-how-to-capture-a-screenshot-on-t-deck)
- [5. General](#5-general)
- [5.1. Q: What are BW, SF, and CR?](#51-q-what-are-bw-sf-and-cr)
- [5.2. Q: Do MeshCore clients repeat?](#52-q-do-meshcore-clients-repeat)
@@ -59,19 +57,23 @@ author: https://github.com/LitBomb<!-- omit from toc -->
- [5.14.3. Python MeshCore](#5143-python-meshcore)
- [5.14.4. meshcore-cli](#5144-meshcore-cli)
- [5.14.5. meshcore.js](#5145-meshcorejs)
- [5.14.6. pyMC\_core](#5146-pymc_core)
- [6. Troubleshooting](#6-troubleshooting)
- [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago)
- [6.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?](#65-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code)
- [6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.](#66-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection)
- [6.7. Q: My RAK/T1000-E/xiao\_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?](#67-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh)
- [6.8. Q: WebFlasher fails on Linux with failed to open](#68-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.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 companion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-companion-via-wifi-eg-using-a-heltec-v3)
## 1. Introduction
@@ -86,7 +88,7 @@ MeshCore is free and open source:
* The T-Deck firmware is developed by Scott at Ripple Radios, the creator of MeshCore, is also free to flash on your devices and use
Some more advanced, but optional features are available on T-Deck if you register your device for a key to unlock. On the MeshCore smartphone clients for Android and iOS/iPadOS, you can unlock the wait timer for repeater and room server remote management over RF feature.
Some more advanced, but optional features are available on T-Deck if you register your device for a key to unlock. On the MeshCore smartphone clients for Android and iOS/iPadOS, you can unlock the wait timer for repeater and room server remote management over RF feature.
These features are completely optional and aren't needed for the core messaging experience. They're like super bonus features and to help the developers continue to work on these amazing features, they may charge a small fee for an unlock code to utilise the advanced features.
@@ -97,17 +99,19 @@ 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.
You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).
#### 1.2.1. Hardware
To use MeshCore without using a phone as the client interface, you can run MeshCore on a T-Deck or T-Deck Plus. It is a complete off-grid secure communication solution.
MeshCore is available on a variety of 433MHz, 868MHz and 915MHz LoRa devices. For example, Lilygo T-Deck, T-Pager, RAK Wireless WisBlock RAK4631 devices (e.g. 19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Nano G2 Ultra, Seeed Studio T1000-E. More devices are being added regularly.
MeshCore is also available on a variety of 868MHz and 915MHz LoRa devices. For example, RAK4631 devices (19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Seeed Studio T1000-E. More devices will be supported later.
For an up-to-date list of supported devices, please go to https://flasher.meshcore.co.uk/
To use MeshCore without using a phone as the client interface, you can run MeshCore on a LiLygo's T-Deck, T-Deck Plus, T-Pager, T-Watch, or T-Display Pro. MeshCore Ultra firmware running on these devices are a complete off-grid secure communication solution.
#### 1.2.2. Firmware
MeshCore has four firmware types that are not available on other LoRa systems. MeshCore has the following:
@@ -115,81 +119,78 @@ MeshCore has four firmware types that are not available on other LoRa systems. M
#### 1.2.3. Companion Radio Firmware
Companion radios are for connecting to the Android app or web app as a messenger client. There are two different companion radio firmware versions:
1. **BLE Companion**
BLE Companion firmware runs on a supported LoRa device and connects to a smart device running the Android MeshCore client over BLE (iOS MeshCore client will be available soon)
1. **BLE Companion**
BLE Companion firmware runs on a supported LoRa device and connects to a smart device running the Android or iOS MeshCore client over BLE
<https://meshcore.co.uk/apps.html>
2. **USB Serial Companion**
USB Serial Companion firmware runs on a supported LoRa device and connects to a smart device or a computer over USB Serial running the MeshCore web client
<https://meshcore.liamcottle.net/#/>
2. **USB Serial Companion**
USB Serial Companion firmware runs on a supported LoRa device and connects to a smart device or a computer over USB Serial running the MeshCore web client
<https://meshcore.liamcottle.net/#/>
<https://client.meshcore.co.uk/tabs/devices>
#### 1.2.4. Repeater
Repeaters are used to extend the range of a MeshCore network. Repeater firmware runs on the same devices that run client firmware. A repeater's job is to forward MeshCore packets to the destination device. It does **not** forward or retransmit every packet it receives, unlike other LoRa mesh systems.
Repeaters are used to extend the range of a MeshCore network. Repeater firmware runs on the same devices that run client firmware. A repeater's job is to forward MeshCore packets to the destination device. It does **not** forward or retransmit every packet it receives, unlike other LoRa mesh systems.
A repeater can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.
#### 1.2.5. Room Server
A room server is a simple BBS server for sharing posts. T-Deck devices running MeshCore firmware or a BLE Companion client connected to a smartphone running the MeshCore app can connect to a room server.
A room server is a simple BBS server for sharing posts. T-Deck devices running MeshCore firmware or a BLE Companion client connected to a smartphone running the MeshCore app can connect to a room server.
Room servers store message history on them and push the stored messages to users. Room servers allow roaming users to come back later and retrieve message history. With channels, messages are either received when it's sent, or not received and missed if the channel user is out of range. Room servers are different and more like email servers where you can come back later and get your emails from your mail server.
A room server can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.
A room server can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.
When a client logs into a room server, the client will receive the previously 32 unseen messages.
A room server can also take on the repeater role. To enable repeater role on a room server, use this command:
Although room server can also repeat with the command line command `set repeat on`, it is not recommended nor encouraged. A room server with repeat set to `on` lacks the full set of repeater and remote administration features that are only available in the repeater firmware.
The recommendation is to run repeater and room server on separate devices for the best experience.
`set repeat {on|off}`
---
## 2. Initial Setup
### 2.1. Q: How many devices do I need to start using MeshCore?
**A:** If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android client via Bluetooth (iOS client will be available later). You can start communicating with other MeshCore users near you.
**A:** If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android or iOS client via Bluetooth. You can start communicating with other MeshCore users near you.
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:
`set freq {frequency}`
The repeater and room server CLI reference is here: https://github.com/ripplebiz/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
The repeater and room server CLI reference is here: https://github.com/meshcore-dev/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
If you have more supported devices, you can use your additional devices with the room server firmware.
If you have more supported devices, you can use your additional devices with the room server firmware.
### 2.2. Q: Does MeshCore cost any money?
**A:** All radio firmware versions (e.g. for Heltec V3, RAK, T-1000E, etc) are free and open source developed by Scott at Ripple Radios.
**A:** All radio firmware versions (e.g. for Heltec V3, RAK, T-1000E, etc) are free and open source developed by Scott at Ripple Radios.
The native Android and iOS client uses the freemium model and is developed by Liam Cottle, developer of meshtastic map at [meshtastic.liamcottle.net](https://meshtastic.liamcottle.net) on [GitHub](https://github.com/liamcottle/meshtastic-map) and [reticulum-meshchat on github](https://github.com/liamcottle/reticulum-meshchat).
The native Android and iOS client uses the freemium model and is developed by Liam Cottle, developer of meshtastic map at [meshtastic.liamcottle.net](https://meshtastic.liamcottle.net) on [GitHub](https://github.com/liamcottle/meshtastic-map) and [reticulum-meshchat on github](https://github.com/liamcottle/reticulum-meshchat).
The T-Deck firmware is free to download and most features are available without cost. To support the firmware developer, you can pay for a registration key to unlock your T-Deck for deeper map zoom and remote server administration over RF using the T-Deck. You do not need to pay for the registration to use your T-Deck for direct messaging and connecting to repeaters and room servers.
The T-Deck firmware is free to download and most features are available without cost. To support the firmware developer, you can pay for a registration key to unlock your T-Deck for deeper map zoom and remote server administration over RF using the T-Deck. You do not need to pay for the registration to use your T-Deck for direct messaging and connecting to repeaters and room servers.
### 2.3. Q: What frequencies are supported by MeshCore?
**A:** It supports the 868MHz range in the UK/EU and the 915MHz range in New Zealand, Australia, and the USA. Countries and regions in these two frequency ranges are also supported. The firmware and client allow users to set their preferred frequency.
- Australia and New Zealand are on **915.8MHz**
- UK and EU are on **869.525MHz**
- Canada and USA are on **910.525MHz**
- For other regions and countries, please check your local LoRa frequency
**A:** It supports the 868MHz range in the UK/EU and the 915MHz range in New Zealand, Australia, and the USA. Countries and regions in these two frequency ranges are also supported.
In UK and EU, 867.5MHz is not allowed to use 250kHz bandwidth and it only allows 2.5% duty cycle for clients. 869.525Mhz allows an airtime of 10%, 250KHz bandwidth, and a higher EIRP, therefore MeshCore nodes can send more often and with more power. That is why this frequency is chosen for UK and EU. This is also why Meshtastic also uses this frequency.
Use the smartphone client or the repeater setup feature on there web flasher to set your radios' RF settings by choosing the preset for your regions.
[Source]([https://](https://discord.com/channels/826570251612323860/1330643963501351004/1356540643853209641))
Recently, as of October 2025, many regions have moved to the "narrow" setting, aka using BW62.5 and a lower SF number (instead of the original SF11). For example, USA/Canada (Recommended) preset is 910.525MHz, SF7, BW62.5, CR5.
After extensive testing, many regions have switched or about to switch over to BW62.5 and SF7, 8, or 9. Narrower bandwidth setting and lower SF setting allow MeshCore's radio signals to fit between interference in the ISM band, provide for a lower noise floor, better SNR, and faster transmissions.
If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the [#meshcore-app](https://discord.com/channels/1343693475589263471/1391681655911088241) channel on the [MeshCore Discord server ](https://discord.gg/cYtQNYCCRK) to let Liam Cottle know.
the rest of the radio settings are the same for all frequencies:
- Spread Factor (SF): 11
- Coding Rate (CR): 5
- Bandwidth (BW): 250.00
(Originally MeshCore started with SF 10. recently (as of late April 2025) the community has advocated SF 11 also a viable option for longer range but a little slower transmission. Currently there are MeshCore meshes with SF 10 and SF 11. Liam Cottle's smartphone app's presets now recommend SF 10 for Australia and SF 11 for all other regions and countries. EU and UK has SF 10 and SF 11 presets. Work with your local meshers on deciding with SF number is best for your use cases. In the future, there may be bridge nodes that can bridge SF 10 and SF 11 (or even different frequencies) traffic.)
### 2.4. Q: What is an "advert" in MeshCore?
**A:**
**A:**
Advert means to advertise yourself on the network. In Reticulum terms it would be to announce. In Meshtastic terms it would be the node sending its node info.
MeshCore allows you to manually broadcast your name, position and public encryption key, which is also signed to prevent spoofing. When you click the advert button, it broadcasts that data over LoRa. MeshCore calls that an Advert. There's two ways to advert, "zero hop" and "flood".
@@ -197,13 +198,15 @@ MeshCore allows you to manually broadcast your name, position and public encrypt
* Zero hop means your advert is broadcasted out to anyone that can hear it, and that's it.
* Flooded means it's broadcasted out and then repeated by all the repeaters that hear it.
MeshCore clients only advertise themselves when the user initiates it. A repeater (and room server?) advertises its presence once every 240 minutes. This interval can be configured using the following command:
MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 3 hours by default. This interval can be configured using the following command:
`set advert.interval {minutes}`
As of Aug 20 2025, a pending PR on github will change the flood advert to 12 hours to minimize airtime utilization caused by repeaters' flood adverts.
### 2.5. Q: Is there a hop limit?
**A:** Internally the firmware has maximum limit of 64 hops. In real world settings it will be difficult to get close to the limit due to the environments and timing as packets travel further and further. We want to hear how far your MeshCore conversations go.
**A:** Internally the firmware has maximum limit of 64 hops. In real world settings it will be difficult to get close to the limit due to the environments and timing as packets travel further and further. We want to hear how far your MeshCore conversations go.
---
@@ -213,15 +216,14 @@ MeshCore clients only advertise themselves when the user initiates it. A repeate
### 3.1. Q: How do you configure a repeater or a room server?
**A:** - When MeshCore is flashed onto a LoRa device is for the first time, it is necessary to set the server device's frequency to make it utilize the frequency that is legal in your country or region.
**A:** - When MeshCore is flashed onto a LoRa device is for the first time, it is necessary to set the server device's frequency to make it utilize the frequency that is legal in your country or region.
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
- Use a MeshCore smartphone clients to remotely administer servers via LoRa.
@@ -230,10 +232,10 @@ Repeater or room server can be administered with one of the options below:
<https://buymeacoffee.com/ripplebiz/e/249834>
### 3.2. Q: Do I need to set the location for a repeater?
**A:** With location set for a repeater, it can show up on a MeshCore map in the future. Set location with the following commands:
**A:** While not required, with location set for a repeater it will show up on the MeshCore map in the future. Set location with the following command:
`set lat <GPS Lat> set long <GPS Lon>`
@@ -255,33 +257,37 @@ You can get the latitude and longitude from Google Maps by right-clicking the lo
## 4. T-Deck Related
### 4.1. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?
**A:**
1. Device off
2. Connect USB cable to device
3. Hold down trackball (keep holding)
4. Turn on device
5. Hear USB connection sound
6. Release trackball
7. T-Deck in DFU mode now
### 4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?
**A:** Yes, it is available on https://buymeacoffee.com/ripplebiz/ultra-v7-7-guide-meshcore-users
### 4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?
**A:**
1. Device off
2. Connect USB cable to device
3. Hold down trackball (keep holding)
4. Turn on device
5. Hear USB connection sound
6. Release trackball
7. T-Deck in DFU mode now
8. At this point you can begin flashing using <https://flasher.meshcore.co.uk/>
### 4.2. Q: Why is my T-Deck Plus not getting any satellite lock?
### 4.3. Q: Why is my T-Deck Plus not getting any satellite lock?
**A:** For T-Deck Plus, the GPS baud rate should be set to **38400**. Also, some T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open the device to check the GPS orientation.
GPS on T-Deck is always enabled. You can skip the "GPS clock sync" and the T-Deck will continue to try to get a GPS lock. You can go to the `GPS Info` screen; you should see the `Sentences:` counter increasing if the baud rate is correct.
[Source]([https://](https://discord.com/channels/826570251612323860/1330643963501351004/1356609240302616689))
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1356609240302616689)
### 4.3. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?
### 4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?
**A:** The OG (non-Plus) T-Deck doesn't come with a GPS. If you added a GPS to your OG T-Deck, please refer to the manual of your GPS to see what baud rate it requires. Alternatively, you can try to set the baud rate from 9600, 19200, etc., and up to 115200 to see which one works.
### 4.4. Q: What size of SD card does the T-Deck support?
### 4.5. Q: What size of SD card does the T-Deck support?
**A:** Users have had no issues using 16GB or 32GB SD cards. Format the SD card to **FAT32**.
### 4.5. Q: what is the public key for the default public channel?
**A:**
T-Deck uses the same key the smartphone apps use but in base64
### 4.6. Q: what is the public key for the default public channel?
**A:**
T-Deck uses the same key the smartphone apps use but in base64
`izOH6cXN6mrJ5e26oRXNcg==`
The third character is the capital letter 'O', not zero `0`
@@ -290,69 +296,96 @@ The smartphone app key is in hex:
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1354194409213792388)
### 4.6. Q: How do I get maps on T-Deck?
**A:** You need map tiles. You can get pre-downloaded map tiles here (a good way to support development):
- <https://buymeacoffee.com/ripplebiz/e/342543> (Europe)
### 4.7. Q: How do I get maps on T-Deck?
**A:** You need map tiles. You can get pre-downloaded map tiles here (a good way to support development):
- <https://buymeacoffee.com/ripplebiz/e/342543> (Europe)
- <https://buymeacoffee.com/ripplebiz/e/342542> (US)
Another way to download map tiles is to use this Python script to get the tiles in the areas you want:
<https://github.com/fistulareffigy/MTD-Script>
Another way to download map tiles is to use this Python script to get the tiles in the areas you want:
<https://github.com/fistulareffigy/MTD-Script>
There is also a modified script that adds additional error handling and parallel downloads:
<https://discord.com/channels/826570251612323860/1330643963501351004/1338775811548905572>
There is also a modified script that adds additional error handling and parallel downloads:
<https://discord.com/channels/826570251612323860/1330643963501351004/1338775811548905572>
UK map tiles are available separately from Andy Kirby on his discord server:
UK map tiles are available separately from Andy Kirby on his discord server:
<https://discord.com/channels/826570251612323860/1330643963501351004/1331346597367386224>
### 4.7. Q: Where do the map tiles go?
### 4.8. Q: Where do the map tiles go?
Once you have the tiles downloaded, copy the `\tiles` folder to the root of your T-Deck's SD card.
### 4.8. Q: How to unlock deeper map zoom and server management features on T-Deck?
**A:** You can download, install, and use the T-Deck firmware for free, but it has some features (map zoom, server administration) that are enabled if you purchase an unlock code for \$10 per T-Deck device.
### 4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?
**A:** You can download, install, and use the T-Deck firmware for free, but it has some features (map zoom, server administration) that are enabled if you purchase an unlock code for \$10 per T-Deck device.
Unlock page: <https://buymeacoffee.com/ripplebiz/e/249834>
### 4.9. Q: How to decipher the diagnostics screen on T-Deck?
### 4.10. Q: How to decipher the diagnostics screen on T-Deck?
**A: ** Space is tight on T-Deck's screen, so the information is a bit cryptic. The format is :
`{hops} l:{packet-length}({payload-len}) t:{packet-type} snr:{n} rssi:{n}`
See here for packet-type: [https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19](https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19 "https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19")
See here for packet-type:
https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19
#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
#define PAYLOAD_TYPE_ACK 0x03 // a simple ack #define PAYLOAD_TYPE_ADVERT 0x04 // a node advertising its Identity
#define PAYLOAD_TYPE_GRP_TXT 0x05 // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
#define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
#define PAYLOAD_TYPE_ACK 0x03 // a simple ack #define PAYLOAD_TYPE_ADVERT 0x04 // a node advertising its Identity
#define PAYLOAD_TYPE_GRP_TXT 0x05 // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
#define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
#define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
[Source](https://discord.com/channels/1343693475589263471/1343693475589263474/1350611321040932966)
### 4.10. Q: The T-Deck sound is too loud?
### 4.11. Q: Can you customize the sound?
### 4.11. Q: The T-Deck sound is too loud?
### 4.12. Q: Can you customize the sound?
**A:** You can customise the sounds on the T-Deck, just by placing `.mp3` files onto the `root` dir of the SD card. `startup.mp3`, `alert.mp3` and `new-advert.mp3`
**A:** You can customise the sounds on the T-Deck, by placing `.mp3` files onto the `root` dir of the SD card. The files are:
### 4.12. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?
* `startup.mp3`
* `error.mp3`
* `alert.mp3`
* `new-advert.mp3`
* `existing-advert.mp3`
### 4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?
**A:** 'Import from Clipboard' is for importing a contact via a file named 'clipboard.txt' on the SD card. The opposite, is in the Identity screen, the 'Card to Clipboard' menu, which writes to 'clipboard.txt' so you can share yourself (call these 'biz cards', that start with "meshcore://...")
### 4.14. Q: How to capture a screenshot on T-Deck?
**A:** To capture a screenshot on a T-Deck, long press the top-left corner of the screen. The screenshot is saved to the microSD card, if one is inserted into the device.
---
## 5. General
### 5.1. Q: What are BW, SF, and CR?
**A:**
**A:**
**BW is bandwidth** - width of frequency spectrum that is used for transmission
**SF is spreading factor** - how much should the communication spread in time
**CR is coding rate** - https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate/
Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more bytes in the same time. Making the spreading factor 1 step lower (from SF10 to SF9) allows you to send 2x more bytes in the same time.
**CR is coding rate** - from: https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate/
TL;DR: default CR to 5 for good stable links. If it is not a solid link and is intermittent, change to CR to 7 or 8.
Forward Error Correction is a process of adding redundant bits to the data to be transmitted. During the transmission, data may get corrupted by interference (changes from 0 to 1 / 1 to 0). These error correction bits are used at the receivers for restoring corrupted bits.
The Code Rate of a forward error correction expresses the proportion of bits in a data stream that actually carry useful information.
There are 4 code rates used in LoRaWAN:
4/5
4/6
5/7
4/8
For example, if the code rate is 5/7, for every 5 bits of useful information, the coder generates a total of 7 bits of data, of which 2 bits are redundant.
Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more bytes in the same time. Making the spreading factor 1 step lower (from SF10 to SF9) allows you to send 2x more bytes in the same time.
Lowering the spreading factor makes it more difficult for the gateway to receive a transmission, as it will be more sensitive to noise. You could compare this to two people taking in a noisy place (a bar for example). If youre far from each other, you have to talk slow (SF10), but if youre close, you can talk faster (SF7)
@@ -360,14 +393,14 @@ So, it's balancing act between speed of the transmission and resistance to noise
things network is mainly focused on LoRaWAN, but the LoRa low-level stuff still checks out for any LoRa project
### 5.2. Q: Do MeshCore clients repeat?
**A:** No, MeshCore clients do not repeat. This is the core of MeshCore's messaging-first design. This is to avoid devices flooding the air ware and create endless collisions, so messages sent aren't received.
In MeshCore, only repeaters and room server with `set repeat on` repeat.
**A:** No, MeshCore clients do not repeat. This is the core of MeshCore's messaging-first design. This is to avoid devices flooding the air ware and create endless collisions, so messages sent aren't received.
In MeshCore, only repeaters and room server with `set repeat on` repeat.
### 5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?
**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?
@@ -386,24 +419,24 @@ Routes are stored in sender's contact list. When you send a message the first t
**A:** The smartphone app key is in hex:
` 8b3387e9c5cdea6ac9e5edbaa115cd72`
T-Deck uses the same key but in base64
T-Deck uses the same key but in base64
`izOH6cXN6mrJ5e26oRXNcg==`
The third character is the capital letter 'O', not zero `0`
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1354194409213792388)
### 5.7. Q: Is MeshCore open source?
**A:** Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps.
- Firmware repo: <https://github.com/ripplebiz/MeshCore>
**A:** Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps.
- Firmware repo: https://github.com/meshcore-dev/MeshCore
### 5.8. Q: How can I support MeshCore?
**A:** Provide your honest feedback on GitHub and on AndyKirby's Discord server <http://discord.com/invite/H62Re4DCeD>. Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at <https://buymeacoffee.com/ripplebiz>.
**A:** Provide your honest feedback on GitHub and on [MeshCore Discord server](https://discord.gg/BMwCtwHj5V). Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at <https://buymeacoffee.com/ripplebiz>.
Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase
Support Rastislav Vysoky (recrof)'s flasher web site and the map web site development through [PayPal](https://www.paypal.com/donate/?business=DREHF5HM265ES&no_recurring=0&item_name=If+you+enjoy+my+work%2C+you+can+support+me+here%3A&currency_code=EUR) or [Revolut](https://revolut.me/recrof)
### 5.9. Q: How do I build MeshCore firmware from source?
**A:** See instructions here:
**A:** See instructions here:
https://discord.com/channels/826570251612323860/1330643963501351004/1341826372120608769
Build instructions for MeshCore:
@@ -423,7 +456,7 @@ Then it should be the same for all platforms:
python3 -m venv meshcore
cd meshcore && source bin/activate
pip install -U platformio
git clone https://github.com/ripplebiz/MeshCore.git
git clone https://github.com/ripplebiz/MeshCore.git
cd MeshCore
```
open platformio.ini and in `[arduino_base]` edit the `LORA_FREQ=867.5`
@@ -433,13 +466,13 @@ pio run -e RAK_4631_Repeater
```
then you'll find `firmware.zip` in `.pio/build/RAK_4631_Repeater`
Andy also has a video on how to build using VS Code:
*How to build and flash Meshcore repeater firmware | Heltec V3*
Andy also has a video on how to build using VS Code:
*How to build and flash Meshcore repeater firmware | Heltec V3*
<https://www.youtube.com/watch?v=WJvg6dt13hk> *(Link referenced in the Discord post)*
### 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
@@ -447,20 +480,24 @@ 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
MeshCore clients would need to reset path constantly and flood traffic across the network which could lead to lots of collisions with something as chatty as ATAK.
MeshCore clients would need to reset path constantly and flood traffic across the network which could lead to lots of collisions with something as chatty as ATAK.
This could change in the future if MeshCore develops a client firmware that repeats.
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1354780032140054659)
### 5.12. Q: How do I add a node to the [MeshCore Map]([url](https://meshcore.co.uk/map.html))
**A:** From the smartphone app, connect to a BLE Companion radio
- To add the BLE Companion radio your smartphone is connected to to the map, tap the `advert` icon, then tap `Advert (To Clipboard)`.
- To add a Repeater or Room Server to the map, tap the 3 dots next to the Repeater or Room Server you want to add to the map, then tap `Share (To Clipboard)`.
- Go to the [MeshCore Map web site]([url](https://meshcore.co.uk/map.html)), tap the plus sign on the lower right corner and paste in the meshcore://... blob, then tap `Add Node`
**A:**
To add a BLE Companion radio, connect to the BLE Companion radio from the MeshCore smartphone app. In the app, tap the `3 dot` menu icon at the top right corner, then tap `Internet Map`. Tap the `3 dot` menu icon again and choose `Add me to the Map`
To add a Repeater or Room Server to the map, go to the Contact List, tap the `3 dot` next to the Repeater or Room Server you want to add to the Internet Map, tap `Share`, then tap `Upload to Internet Map`.
You can use the same companion (same public key) that you used to add your repeaters or room servers to remove them from the Internet Map.
### 5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?
** A:** Yes.
@@ -472,7 +509,7 @@ For ESP-based devices (e.g. Heltec V3) you need:
- Download firmware file from flasher.meshcore.co.uk
- Go to the web site on a browser, find the section that has the firmware up need
- Click the Download button, right click on the file you need, for example,
- `Heltec_V3_companion_radio_ble-v1.7.1-165fb33.bin`
- `Heltec_V3_companion_radio_ble-v1.7.1-165fb33.bin`
- Non-merged bin keeps the existing Bluetooth pairing database
- `Heltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.bin`
- Merged bin overwrites everything including the bootloader, existing Bluetooth pairing database, but keeps configurations.
@@ -491,7 +528,7 @@ For ESP-based devices (e.g. Heltec V3) you need:
- `esptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x10000 <non-merged_firmware>.bin`
- For merged bin:
- `esptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x00000 <merged_firmware>.bin`
**Instructions for nRF devices:**
@@ -512,24 +549,25 @@ For nRF devices (e.g. RAK, Heltec T114) you need the following:
- `pip install adafruit-nrfutil --break-system-packages`
- Use this command to flash the nRF device:
- `adafruit-nrfutil --verbose dfu serial --package RAK_4631_companion_radio_usb-v1.7.1-165fb33.zip -p /dev/ttyACM0 -b 115200 --singlebank --touch 1200`
To manage a repeater or room server connected to a Pi over USB serial using shell commands, you need to install `picocom`. To install `picocom`, run the following command:
- `sudo apt install picocom`
To start managing your USB serial-connected device using picocom, use the following command:
- `picocom -b 115200 /dev/ttyUSB0 --imap lfcrlf`
From here, reference repeater and room server command line commands on MeshCore github wiki here:
- https://github.com/ripplebiz/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
From here, reference repeater and room server command line commands on MeshCore github wiki here:
- https://github.com/meshcore-dev/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
### 5.14. Q: Are there are projects built around MeshCore?
**A:** Yes. See the following:
#### 5.14.1. meshcoremqtt
A Python script to send meshore debug and packet capture data to MQTT for analysis
A Python script to send meshcore debug and packet capture data to MQTT for analysis. Cisien's version is a fork of Andrew-a-g's and is being used to to collect data for https://map.w0z.is/messages and https://analyzer.letsme.sh/
https://github.com/Cisien/meshcoretomqtt
https://github.com/Andrew-a-g/meshcoretomqtt
#### 5.14.2. MeshCore for Home Assistant
@@ -540,59 +578,66 @@ https://github.com/awolden/meshcore-ha
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.
#### 5.14.4. meshcore-cli
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
A JavaScript library for interacting with a MeshCore device running the companion radio firmware
https://github.com/liamcottle/meshcore.js
#### 5.14.6. pyMC_core
pyMC_Core is a Python port of MeshCore, designed for Raspberry Pi and similar hardware, it talks to LoRa modules over SPI.
https://github.com/rightup/pyMC_core
---
## 6. Troubleshooting
### 6.1. 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.
**A:**
- If your client is a T-Deck, it may not have its time set (no GPS installed, no GPS lock, or wrong GPS baud rate).
- If you are using the Android or iOS client, the other client, repeater, or room server may have the wrong time.
**A:**
- If your client is a T-Deck, it may not have its time set (no GPS installed, no GPS lock, or wrong GPS baud rate).
- If you are using the Android or iOS client, the other client, repeater, or room server may have the wrong time.
You can get the epoch time on <https://www.epochconverter.com/> and use it to set your T-Deck clock. For a repeater and room server, the admin can use a T-Deck to remotely set their clock (clock sync), or use the `time` command in the USB serial console with the server device connected.
### 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:**
**A:**
1. Connect USB-C cable to your device, per your device's instruction, get it to flash mode:
- For RAK, click the reset button **TWICE**
- For T1000-e, quickly disconnect and reconnect the magnetic side of the cable from the device **TWICE**
- For Heltec T114, click the reset button **TWICE** (the bottom button)
- For Xiao nRF52, click the reset button once. If that doesn't work, quickly double click the reset button twice. If that doesn't work, disconnection the board from your PC and reconnect again ([seeed studio wiki](https://wiki.seeedstudio.com/XIAO_BLE/#access-the-swd-pins-for-debugging-and-reflashing-bootloader))
5. A new folder will appear on your computer's desktop
6. Download the `flash_erase*.uf2` file for your device on flasher.meshcore.co.uk
6. Download the `flash_erase*.uf2` file for your device on flasher.meshcore.co.uk
- RAK WisBlock and Heltec T114: `Flash_erase-nRF32_softdevice_v6.uf2`
- Seeed Studio Xiao nRF52 WIO: `Flash_erase-nRF52_softdevice_v7.uf2`
8. drag and drop the uf2 file for your device to the root of the new folder
9. Wait for the copy to complete. You might get an error dialog, you can ignore it
10. Go to https://flasher.meshcore.co.uk/, click `Console` and select the serial port for your connected device
10. Go to https://flasher.meshcore.co.uk/, click `Console` and select the serial port for your connected device
11. In the console, press enter. Your flash should now be erased
12. You may now flash the latest MeshCore firmware onto your device
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
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.`
@@ -603,40 +648,40 @@ Allow the browser user on it:
---
## 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?
### 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?
**A:** The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:
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.
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.
13. If it fails, try turning off and on Bluetooth on your phone. If that doesn't work, try rebooting your phone.
14. Wait for the update to complete. It can take a few minutes.
### 7.2 Q: How to update ESP32-based devices over the air?
### 7.2. Q: How to update ESP32-based devices over the air?
**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`
7. From your phone or computer connect to the 'MeshCore OTA' hotspot
7. From your phone or computer connect to the 'MeshCore OTA' hotspot
8. From a browser, go to http://192.168.4.1/update and upload the non-merged bin from the flasher
### 7.3 Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?
### 7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?
**A:** Yes, developer `che aporeps` has an enhanced OTA DFU bootloader for nRF52 based devices. With this bootloader, if it detects that the application firmware is invalid, it falls back to OTA DFU mode so you can attempt to flash again to recover. This bootloader has other changes to make the OTA DFU process more fault tolerant.
**A:** Yes, developer `che aporeps` has an enhanced OTA DFU bootloader for nRF52 based devices. With this bootloader, if it detects that the application firmware is invalid, it falls back to OTA DFU mode so you can attempt to flash again to recover. This bootloader has other changes to make the OTA DFU process more fault tolerant.
Refer to https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX for the latest information.
@@ -646,9 +691,29 @@ Currently, the following boards are supported:
- Seeed Studio XIAO nRF52840 BLE SENSE
- RAK 4631
### 7.4 Q are the MeshCore logo and font available?
### 7.4. Q: are the MeshCore logo and font available?
**A:** Yes, it is on the MeshCore github repo here: https://github.com/ripplebiz/MeshCore/tree/main/logo
**A:** Yes, it is on the MeshCore github repo here:
https://github.com/meshcore-dev/MeshCore/tree/main/logo
### 7.5. Q: What is the format of a contact or channel QR code?
**A:**
Channel:
`meshcore://channel/add?name=<name>&secret=<secret>`
Contact:
`meshcore://contact/add?name=<name>&public_key=<secret>&type=<type>`
where `&type` is:
`chat = 1`
`repeater = 2`
`room = 3`
`sensor = 4`
### 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.
---

View File

@@ -1,11 +1,12 @@
# Packet Structure
| Field | Size (bytes) | Description |
|----------|----------------------------------|-----------------------------------------------------------|
| header | 1 | Contains routing type, payload type, and payload version. |
| path_len | 1 | Length of the path field in bytes. |
| path | up to 64 (`MAX_PATH_SIZE`) | Stores the routing path if applicable. |
| payload | up to 184 (`MAX_PACKET_PAYLOAD`) | The actual data being transmitted. |
| Field | Size (bytes) | Description |
|-----------------|----------------------------------|-----------------------------------------------------------|
| header | 1 | Contains routing type, payload type, and payload version. |
| transport_codes | 4 (optional) | 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*) |
| path_len | 1 | Length of the path field in bytes. |
| path | up to 64 (`MAX_PATH_SIZE`) | Stores the routing path if applicable. |
| payload | up to 184 (`MAX_PACKET_PAYLOAD`) | The actual data being transmitted. |
Note: see the [payloads doc](./payloads.md) for more information about the content of payload.
@@ -42,6 +43,11 @@ bit 0 means the lowest bit (1s place)
| `0x07` | `PAYLOAD_TYPE_ANON_REQ` | Anonymous request. |
| `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

@@ -10,13 +10,17 @@ Inside of each [meshcore packet](./packet_structure.md) is a payload, identified
* Anonymous request.
* 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
This document defines the structure of each of these payload types.
NOTE: all 16 and 32-bit integer fields are Little Endian.
## Important concepts:
* Node/channel hash: the first byte of the node or channel's public key
* Node hash: the first byte of the node's public key
# Node advertisement
This kind of payload notifies receivers that a node exists, and gives information about the node
@@ -33,10 +37,10 @@ Appdata
| Field | Size (bytes) | Description |
|---------------|-----------------|-------------------------------------------------------|
| flags | 1 | specifies which of the fields are present, see below |
| latitude | 4 | decimal latitude multiplied by 1000000, integer |
| longitude | 4 | decimal longitude multiplied by 1000000, integer |
| feature 1 | 2 | reserved for future use |
| feature 2 | 2 | reserved for future use |
| latitude | 4 (optional) | decimal latitude multiplied by 1000000, integer |
| longitude | 4 (optional) | decimal longitude multiplied by 1000000, integer |
| feature 1 | 2 (optional) | reserved for future use |
| feature 2 | 2 (optional) | reserved for future use |
| name | rest of appdata | name of the node |
Appdata Flags
@@ -46,6 +50,7 @@ Appdata Flags
| `0x01` | is chat node | advert is for a chat node |
| `0x02` | is repeater | advert is for a repeater |
| `0x03` | is room server | advert is for a room server |
| `0x04` | is sensor | advert is for a sensor server |
| `0x10` | has location | appdata contains lat/long information |
| `0x20` | has feature 1 | Reserved for future use. |
| `0x40` | has feature 2 | Reserved for future use. |
@@ -53,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 |
|----------|--------------|------------------------------------------------------------|
@@ -92,13 +97,15 @@ Returned path messages provide a description of the route a packet took from the
Request type
| Value | Name | Description |
|--------|--------------------|---------------------------------------|
| `0x01` | get status | get status of repeater or room server |
| `0x02` | keepalive | TODO |
| `0x03` | get telemetry data | TODO |
| Value | Name | Description |
|--------|----------------------|---------------------------------------|
| `0x01` | get stats | get stats of repeater or room server |
| `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 |
### Get status
### Get stats
Gets information about the node, possibly including the following:
@@ -121,10 +128,6 @@ Gets information about the node, possibly including the following:
* Number posted (?)
* Number of post pushes (?)
### Keepalive
No-op request.
### Get telemetry data
Request data about sensors on the node, including battery level.
@@ -138,19 +141,19 @@ Request data about sensors on the node, including battery level.
## Plain text message
| Field | Size (bytes) | Description |
|--------------|-----------------|--------------------------------------------------------------|
| timestamp | 4 | send time (unix timestamp) |
| flags + TODO | 1 | first six bits are flags (see below), last two bits are TODO |
| 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 |
|--------|---------------------------|------------------------------------------------------------|
| `0x00` | plain text message | the plain text of the message |
| `0x01` | CLI command | the command text of the message |
| `0x02` | signed plain text message | two bytes of sender prefix, followed by plain text message |
| `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
# Anonymous request
@@ -161,27 +164,58 @@ 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 | for room server, otherwise absent: 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 |
# Group text message / datagram
| Field | Size (bytes) | Description |
|--------------|-----------------|--------------------------------------------|
| channel hash | 1 | the first byte of the channel's public key |
| channel hash | 1 | first byte of SHA256 of channel's shared key |
| cipher MAC | 2 | MAC for encrypted data in next field |
| ciphertext | rest of payload | encrypted message, see below for details |
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
Custom packets have no defined format.
Custom packets have no defined format.

View File

@@ -0,0 +1,46 @@
#pragma once
#include <MeshCore.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h>
#include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h>
#include <Arduino.h>
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
#include "NodePrefs.h"
enum class UIEventType {
none,
contactMessage,
channelMessage,
roomMessage,
newContactMessage,
ack
};
class AbstractUITask {
protected:
mesh::MainBoard* _board;
BaseSerialInterface* _serial;
bool _connected;
AbstractUITask(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial) {
_connected = false;
}
public:
void setHasConnection(bool connected) { _connected = connected; }
bool hasConnection() const { return _connected; }
uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); }
bool isSerialEnabled() const { return _serial->isEnabled(); }
void enableSerial() { _serial->enable(); }
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 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");
@@ -41,19 +68,33 @@ void DataStore::begin() {
#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 +108,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 +126,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,13 +144,31 @@ 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)
@@ -138,11 +197,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 +209,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((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1)); // 62
file.read(pad, 1); // 62
file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
@@ -163,7 +218,8 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
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(pad, 3); // 77
file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
file.read(pad, 2); // 78
file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.close();
@@ -184,7 +240,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((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1)); // 62
file.write(pad, 1); // 62
file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
@@ -193,7 +249,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
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(pad, 3); // 77
file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
file.write(pad, 2); // 78
file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.close();
@@ -201,12 +258,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
}
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) {
@@ -234,11 +286,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;
@@ -267,12 +318,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;
@@ -294,11 +340,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;
@@ -329,12 +374,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();
@@ -342,10 +387,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)) {
@@ -362,10 +514,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;
@@ -409,11 +559,7 @@ uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_b
sprintf(path, "/bl/%s", fname);
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();

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,16 @@ 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);
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

@@ -41,10 +41,17 @@
#define CMD_SEND_TRACE_PATH 36
#define CMD_SET_DEVICE_PIN 37
#define CMD_SET_OTHER_PARAMS 38
#define CMD_SEND_TELEMETRY_REQ 39
#define CMD_SEND_TELEMETRY_REQ 39 // can deprecate this
#define CMD_GET_CUSTOM_VARS 40
#define CMD_SET_CUSTOM_VAR 41
#define CMD_GET_ADVERT_PATH 42
#define CMD_GET_TUNING_PARAMS 43
// NOTE: CMD range 44..49 parked, potentially for WiFi operations
#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 RESP_CODE_OK 0
#define RESP_CODE_ERR 1
@@ -69,6 +76,7 @@
#define RESP_CODE_SIGNATURE 20
#define RESP_CODE_CUSTOM_VARS 21
#define RESP_CODE_ADVERT_PATH 22
#define RESP_CODE_TUNING_PARAMS 23
#define SEND_TIMEOUT_BASE_MILLIS 500
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
@@ -91,6 +99,9 @@
#define PUSH_CODE_TRACE_DATA 0x89
#define PUSH_CODE_NEW_ADVERT 0x8A
#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 ERR_CODE_UNSUPPORTED_CMD 1
#define ERR_CODE_NOT_FOUND 2
@@ -101,10 +112,6 @@
#define MAX_SIGN_DATA_LEN (8 * 1024) // 8K
#ifdef DISPLAY_CLASS
#include "UITask.h"
#endif
void MyMesh::writeOKFrame() {
uint8_t buf[1];
buf[0] = RESP_CODE_OK;
@@ -146,7 +153,7 @@ void MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact) {
_serial->writeFrame(out_frame, i);
}
void MyMesh::updateContactFromFrame(ContactInfo &contact, const uint8_t *frame, int len) {
void MyMesh::updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len) {
int i = 0;
uint8_t code = frame[i++]; // eg. CMD_ADD_UPDATE_CONTACT
memcpy(contact.id.pub_key, &frame[i], PUB_KEY_SIZE);
@@ -160,23 +167,45 @@ void MyMesh::updateContactFromFrame(ContactInfo &contact, const uint8_t *frame,
i += 32;
memcpy(&contact.last_advert_timestamp, &frame[i], 4);
i += 4;
if (i + 8 >= len) { // optional fields
if (len >= i + 8) { // optional fields
memcpy(&contact.gps_lat, &frame[i], 4);
i += 4;
memcpy(&contact.gps_lon, &frame[i], 4);
i += 4;
if (len >= i + 4) {
memcpy(&last_mod, &frame[i], 4);
}
}
}
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
@@ -204,6 +233,10 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
}
uint8_t MyMesh::getExtraAckTransmitCount() const {
return _prefs.multi_acks;
}
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
if (_serial->isConnected() && len + 3 <= MAX_FRAME_SIZE) {
int i = 0;
@@ -232,7 +265,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
}
} else {
#ifdef DISPLAY_CLASS
ui_task.soundBuzzer(UIEventType::newContactMessage);
if (_ui) _ui->notify(UIEventType::newContactMessage);
#endif
}
@@ -252,6 +285,7 @@ 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);
@@ -260,6 +294,20 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
static int sort_by_recent(const void *a, const void *b) {
return ((AdvertPath *) b)->recv_timestamp - ((AdvertPath *) a)->recv_timestamp;
}
int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) {
if (max_num > ADVERT_PATH_TABLE_SIZE) max_num = ADVERT_PATH_TABLE_SIZE;
qsort(advert_paths, ADVERT_PATH_TABLE_SIZE, sizeof(advert_paths[0]), sort_by_recent);
for (int i = 0; i < max_num; i++) {
dest[i] = advert_paths[i];
}
return max_num;
}
void MyMesh::onContactPathUpdated(const ContactInfo &contact) {
out_frame[0] = PUSH_CODE_PATH_UPDATED;
memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
@@ -268,7 +316,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
@@ -280,7 +328,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);
@@ -319,16 +367,49 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
_serial->writeFrame(frame, 1);
} else {
#ifdef DISPLAY_CLASS
ui_task.soundBuzzer(UIEventType::contactMessage);
#endif
}
#ifdef DISPLAY_CLASS
ui_task.newMsg(path_len, from.name, text, offline_queue_len);
// we only want to show text messages on display, not cli data
bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
if (should_display && _ui) {
_ui->newMsg(path_len, from.name, text, offline_queue_len);
if (!_serial->isConnected()) {
_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;
}
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);
} 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);
}
}
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);
} 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);
}
}
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
@@ -382,7 +463,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
_serial->writeFrame(frame, 1);
} else {
#ifdef DISPLAY_CLASS
ui_task.soundBuzzer(UIEventType::channelMessage);
if (_ui) _ui->notify(UIEventType::channelMessage);
#endif
}
#ifdef DISPLAY_CLASS
@@ -392,7 +473,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
if (getChannel(channel_idx, channel_details)) {
channel_name = channel_details.name;
}
ui_task.newMsg(path_len, channel_name, text, offline_queue_len);
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
#endif
}
@@ -420,6 +501,9 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim
permissions |= cp & TELEM_PERM_ENVIRONMENT;
}
uint8_t perm_mask = ~(data[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
permissions &= perm_mask;
if (permissions & TELEM_PERM_BASE) { // only respond if base permission bit is set
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
@@ -462,6 +546,8 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
i += 6; // pub_key_prefix
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
@@ -484,7 +570,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
memcpy(&out_frame[i], &data[4], len - 4);
i += (len - 4);
_serial->writeFrame(out_frame, i);
} else if (len > 4 && tag == pending_telemetry) { // check for telemetry response
} else if (len > 4 && tag == pending_telemetry) { // check for matching response tag
pending_telemetry = 0;
int i = 0;
@@ -495,6 +581,70 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
memcpy(&out_frame[i], &data[4], len - 4);
i += (len - 4);
_serial->writeFrame(out_frame, i);
} else if (len > 4 && tag == pending_req) { // check for matching response tag
pending_req = 0;
int i = 0;
out_frame[i++] = PUSH_CODE_BINARY_RESPONSE;
out_frame[i++] = 0; // reserved
memcpy(&out_frame[i], &tag, 4); // app needs to match this to RESP_CODE_SENT.tag
i += 4;
memcpy(&out_frame[i], &data[4], len - 4);
i += (len - 4);
_serial->writeFrame(out_frame, i);
}
}
bool MyMesh::onContactPathRecv(ContactInfo& contact, 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) {
if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) {
uint32_t tag;
memcpy(&tag, extra, 4);
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) {
MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len);
} else {
int i = 0;
out_frame[i++] = PUSH_CODE_PATH_DISCOVERY_RESPONSE;
out_frame[i++] = 0; // reserved
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;
out_frame[i++] = in_path_len;
memcpy(&out_frame[i], in_path, in_path_len);
i += in_path_len;
// NOTE: telemetry data in 'extra' is discarded at present
_serial->writeFrame(out_frame, i);
}
return false; // DON'T send reciprocal path!
}
}
// let base class handle received path and data
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");
}
}
@@ -553,18 +703,19 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t
void MyMesh::onSendTimeout() {}
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store)
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui)
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables),
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store) {
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) {
_iter_started = false;
_cli_rescue = false;
offline_queue_len = 0;
app_target_ver = 0;
pending_login = pending_status = pending_telemetry = 0;
clearPendingReqs();
next_ack_idx = 0;
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));
@@ -608,8 +759,8 @@ 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);
@@ -632,6 +783,7 @@ void MyMesh::begin(bool has_display) {
_active_ble_pin = 0;
#endif
resetContacts();
_store->loadContacts(this);
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
_store->loadChannels(this);
@@ -697,7 +849,7 @@ void MyMesh::handleCmdFrame(size_t len) {
i += 4;
memcpy(&out_frame[i], &lon, 4);
i += 4;
out_frame[i++] = 0; // reserved
out_frame[i++] = _prefs.multi_acks; // new v7+
out_frame[i++] = _prefs.advert_loc_policy;
out_frame[i++] = (_prefs.telemetry_mode_env << 4) | (_prefs.telemetry_mode_loc << 2) |
(_prefs.telemetry_mode_base); // v5+
@@ -746,6 +898,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;
}
@@ -870,15 +1023,16 @@ void MyMesh::handleCmdFrame(size_t len) {
} else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT && len >= 1 + 32 + 2 + 1) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
uint32_t last_mod = getRTCClock()->getCurrentTime(); // fallback value if not present in cmd_frame
if (recipient) {
updateContactFromFrame(*recipient, cmd_frame, len);
// recipient->lastmod = ?? shouldn't be needed, app already has this version of contact
updateContactFromFrame(*recipient, last_mod, cmd_frame, len);
recipient->lastmod = last_mod;
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
writeOKFrame();
} else {
ContactInfo contact;
updateContactFromFrame(contact, cmd_frame, len);
contact.lastmod = getRTCClock()->getCurrentTime();
updateContactFromFrame(contact, last_mod, cmd_frame, len);
contact.lastmod = last_mod;
contact.sync_since = 0;
if (addContact(contact)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
@@ -957,7 +1111,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if ((out_len = getFromOfflineQueue(out_frame)) > 0) {
_serial->writeFrame(out_frame, out_len);
#ifdef DISPLAY_CLASS
ui_task.msgRead(offline_queue_len);
if (_ui) _ui->msgRead(offline_queue_len);
#endif
} else {
out_frame[0] = RESP_CODE_NO_MORE_MESSAGES;
@@ -1012,6 +1166,13 @@ void MyMesh::handleCmdFrame(size_t len) {
_prefs.airtime_factor = ((float)af) / 1000.0f;
savePrefs();
writeOKFrame();
} else if (cmd_frame[0] == CMD_GET_TUNING_PARAMS) {
uint32_t rx = _prefs.rx_delay_base * 1000, af = _prefs.airtime_factor * 1000;
int i = 0;
out_frame[i++] = RESP_CODE_TUNING_PARAMS;
memcpy(&out_frame[i], &rx, 4); i += 4;
memcpy(&out_frame[i], &af, 4); i += 4;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) {
_prefs.manual_add_contacts = cmd_frame[1];
if (len >= 3) {
@@ -1021,6 +1182,9 @@ void MyMesh::handleCmdFrame(size_t len) {
if (len >= 4) {
_prefs.advert_loc_policy = cmd_frame[3];
if (len >= 5) {
_prefs.multi_acks = cmd_frame[4];
}
}
}
savePrefs();
@@ -1057,6 +1221,9 @@ void MyMesh::handleCmdFrame(size_t len) {
if (_store->saveMainIdentity(identity)) {
self_id = identity;
writeOKFrame();
// re-load contacts, to recalc shared secrets
resetContacts();
_store->loadContacts(this);
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
@@ -1090,7 +1257,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
pending_telemetry = pending_status = 0;
clearPendingReqs();
memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
@@ -1110,7 +1277,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
pending_telemetry = pending_login = 0;
clearPendingReqs();
// FUTURE: pending_status = tag; // match this in onContactResponse()
memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme
out_frame[0] = RESP_CODE_SENT;
@@ -1122,7 +1289,36 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
}
} else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) {
} else if (cmd_frame[0] == CMD_SEND_PATH_DISCOVERY_REQ && cmd_frame[1] == 0 && len >= 2 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[2];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
uint32_t tag, est_timeout;
// 'Path Discovery' is just a special case of flood + Telemetry req
uint8_t req_data[9];
req_data[0] = REQ_TYPE_GET_TELEMETRY_DATA;
req_data[1] = ~(TELEM_PERM_BASE); // NEW: inverse permissions mask (ie. we only want BASE telemetry)
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;
int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout);
recipient->out_path_len = save;
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
clearPendingReqs();
pending_discovery = tag; // match this in 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_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) { // can deprecate, in favour of CMD_SEND_BINARY_REQ
uint8_t *pub_key = &cmd_frame[4];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
@@ -1131,7 +1327,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
pending_status = pending_login = 0;
clearPendingReqs();
pending_telemetry = tag; // match this in onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
@@ -1157,6 +1353,27 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(&out_frame[i], telemetry.getBuffer(), tlen);
i += tlen;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_SEND_BINARY_REQ && len >= 2 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
uint8_t *req_data = &cmd_frame[1 + PUB_KEY_SIZE];
uint32_t tag, est_timeout;
int result = sendRequest(*recipient, req_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 in 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_HAS_CONNECTION && len >= 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
if (hasConnectionTo(pub_key)) {
@@ -1312,6 +1529,30 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_NOT_FOUND);
}
} else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) {
bool success = _store->formatFileSystem();
if (success) {
writeOKFrame();
delay(1000);
board.reboot(); // doesn't return
} 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 {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);
@@ -1372,33 +1613,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
@@ -1415,17 +1695,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 {
@@ -1466,8 +1759,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
}
}
@@ -1487,7 +1780,7 @@ void MyMesh::loop() {
}
#ifdef DISPLAY_CLASS
ui_task.setHasConnection(_serial->isConnected());
if (_ui) _ui->setHasConnection(_serial->isConnected());
#endif
}

View File

@@ -2,19 +2,17 @@
#include <Arduino.h>
#include <Mesh.h>
#ifdef DISPLAY_CLASS
#include "UITask.h"
#endif
#include "AbstractUITask.h"
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 6
#define FIRMWARE_VER_CODE 8
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "2 Jul 2025"
#define FIRMWARE_BUILD_DATE "13 Nov 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.7.2"
#define FIRMWARE_VERSION "v1.10.0"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -70,6 +68,7 @@
#endif
#include <helpers/BaseChatMesh.h>
#include <helpers/TransportKeyStore.h>
/* -------------------------------------------------------------------------------------- */
@@ -77,9 +76,17 @@
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
struct AdvertPath {
uint8_t pubkey_prefix[7];
uint8_t path_len;
char name[32];
uint32_t recv_timestamp;
uint8_t path[MAX_PATH_SIZE];
};
class MyMesh : public BaseChatMesh, public DataStoreHost {
public:
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store);
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
void begin(bool has_display);
void startInterface(BaseSerialInterface &serial);
@@ -93,16 +100,24 @@ public:
bool advert();
void enterCLIRescue();
int getRecentlyHeard(AdvertPath dest[], int max_num);
protected:
float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() const override;
int calcRxDelay(float score, uint32_t air_time) const override;
uint8_t getExtraAckTransmitCount() const override;
bool filterRecvFloodPacket(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 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);
@@ -118,6 +133,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;
@@ -132,12 +148,16 @@ protected:
bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) override { return setChannel(channel_idx, ch); }
bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) override { return getChannel(channel_idx, ch); }
void clearPendingReqs() {
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
}
private:
void writeOKFrame();
void writeErrFrame(uint8_t err_code);
void writeDisabledFrame();
void writeContactRespFrame(uint8_t code, const ContactInfo &contact);
void updateContactFromFrame(ContactInfo &contact, const uint8_t *frame, int len);
void updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len);
void addToOfflineQueue(const uint8_t frame[], int len);
int getFromOfflineQueue(uint8_t frame[]);
int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override {
@@ -160,8 +180,10 @@ private:
NodePrefs _prefs;
uint32_t pending_login;
uint32_t pending_status;
uint32_t pending_telemetry;
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
uint32_t pending_req; // pending _BINARY_REQ
BaseSerialInterface *_serial;
AbstractUITask* _ui;
ContactsIterator _iter;
uint32_t _iter_filter_since;
@@ -175,6 +197,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;
@@ -182,6 +206,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];
@@ -189,22 +215,14 @@ 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
int next_ack_idx;
struct AdvertPath {
uint8_t pubkey_prefix[7];
uint8_t path_len;
uint32_t recv_timestamp;
uint8_t path[MAX_PATH_SIZE];
};
#define ADVERT_PATH_TABLE_SIZE 16
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
};
extern MyMesh the_mesh;
#ifdef DISPLAY_CLASS
extern UITask ui_task;
#endif

View File

@@ -14,7 +14,7 @@ struct NodePrefs { // persisted to file
float freq;
uint8_t sf;
uint8_t cr;
uint8_t reserved1;
uint8_t multi_acks;
uint8_t manual_add_contacts;
float bw;
uint8_t tx_power_dbm;

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);
@@ -75,14 +86,19 @@ static uint32_t _atoi(const char* sp) {
#endif
/* GLOBAL OBJECTS */
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store);
#ifdef DISPLAY_CLASS
#include "UITask.h"
UITask ui_task(&board);
UITask ui_task(&board, &serial_interface);
#endif
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
#ifdef DISPLAY_CLASS
, &ui_task
#endif
);
/* END GLOBAL OBJECTS */
void halt() {
@@ -99,7 +115,10 @@ void setup() {
if (display.begin()) {
disp = &display;
disp->startFrame();
disp->print("Please wait...");
#ifdef ST7789
disp->setTextSize(2);
#endif
disp->drawTextCentered(disp->width() / 2, 28, "Loading...");
disp->endFrame();
}
#endif
@@ -110,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
@@ -196,4 +227,5 @@ void loop() {
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
rtc_clock.tick();
}

View File

@@ -0,0 +1,876 @@
#include "UITask.h"
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#include "target.h"
#ifndef AUTO_OFF_MILLIS
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#endif
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
#ifdef PIN_STATUS_LED
#define LED_ON_MILLIS 20
#define LED_ON_MSG_MILLIS 200
#define LED_CYCLE_MILLIS 4000
#endif
#define LONG_PRESS_MILLIS 1200
#ifndef UI_RECENT_LIST_SIZE
#define UI_RECENT_LIST_SIZE 4
#endif
#if UI_HAS_JOYSTICK
#define PRESS_LABEL "press Enter"
#else
#define PRESS_LABEL "long press"
#endif
#include "icons.h"
class SplashScreen : public UIScreen {
UITask* _task;
unsigned long dismiss_after;
char _version_info[12];
public:
SplashScreen(UITask* task) : _task(task) {
// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
const char *ver = FIRMWARE_VERSION;
const char *dash = strchr(ver, '-');
int len = dash ? dash - ver : strlen(ver);
if (len >= sizeof(_version_info)) len = sizeof(_version_info) - 1;
memcpy(_version_info, ver, len);
_version_info[len] = 0;
dismiss_after = millis() + BOOT_SCREEN_MILLIS;
}
int render(DisplayDriver& display) override {
// meshcore logo
display.setColor(DisplayDriver::BLUE);
int logoWidth = 128;
display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// version info
display.setColor(DisplayDriver::LIGHT);
display.setTextSize(2);
display.drawTextCentered(display.width()/2, 22, _version_info);
display.setTextSize(1);
display.drawTextCentered(display.width()/2, 42, FIRMWARE_BUILD_DATE);
return 1000;
}
void poll() override {
if (millis() >= dismiss_after) {
_task->gotoHomeScreen();
}
}
};
class HomeScreen : public UIScreen {
enum HomePage {
FIRST,
RECENT,
RADIO,
BLUETOOTH,
ADVERT,
#if ENV_INCLUDE_GPS == 1
GPS,
#endif
#if UI_SENSORS_PAGE == 1
SENSORS,
#endif
SHUTDOWN,
Count // keep as last
};
UITask* _task;
mesh::RTCClock* _rtc;
SensorManager* _sensors;
NodePrefs* _node_prefs;
uint8_t _page;
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)
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
// battery icon
int iconWidth = 24;
int iconHeight = 10;
int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner
int iconY = 0;
display.setColor(DisplayDriver::GREEN);
// battery outline
display.drawRect(iconX, iconY, iconWidth, iconHeight);
// battery "cap"
display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2);
// fill the battery based on the percentage
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
}
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), sensors_lpp(200) { }
void poll() override {
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
_task->shutdown();
}
}
int render(DisplayDriver& display) override {
char tmp[80];
// node name
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
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 - 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);
} else {
display.fillRect(x, y, 1, 1);
}
}
if (_page == HomePage::FIRST) {
display.setColor(DisplayDriver::YELLOW);
display.setTextSize(2);
sprintf(tmp, "MSG: %d", _task->getMsgCount());
display.drawTextCentered(display.width() / 2, 20, tmp);
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);
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
display.drawTextCentered(display.width() / 2, 43, tmp);
}
} else if (_page == HomePage::RECENT) {
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
display.setColor(DisplayDriver::GREEN);
int y = 20;
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) {
auto a = &recent[i];
if (a->name[0] == 0) continue; // empty slot
int secs = _rtc->getCurrentTime() - a->recv_timestamp;
if (secs < 60) {
sprintf(tmp, "%ds", secs);
} else if (secs < 60*60) {
sprintf(tmp, "%dm", secs / 60);
} else {
sprintf(tmp, "%dh", secs / (60*60));
}
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) {
display.setColor(DisplayDriver::YELLOW);
display.setTextSize(1);
// freq / sf
display.setCursor(0, 20);
sprintf(tmp, "FQ: %06.3f SF: %d", _node_prefs->freq, _node_prefs->sf);
display.print(tmp);
display.setCursor(0, 31);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
display.print(tmp);
// tx power, noise floor
display.setCursor(0, 42);
sprintf(tmp, "TX: %ddBm", _node_prefs->tx_power_dbm);
display.print(tmp);
display.setCursor(0, 53);
sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor());
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,
32, 32);
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL);
} else if (_page == HomePage::ADVERT) {
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();
int y = 18;
display.drawTextLeftAlign(0, y, _task->getGPSState() ? "gps on" : "gps off");
if (nmea == NULL) {
y = y + 12;
display.drawTextLeftAlign(0, y, "Can't access GPS");
} else {
char buf[50];
strcpy(buf, nmea->isValid()?"fix":"no fix");
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
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);
if (_shutdown_init) {
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);
}
}
return 5000; // next render after 5000 ms
}
bool handleInput(char c) override {
if (c == KEY_LEFT || c == KEY_PREV) {
_page = (_page + HomePage::Count - 1) % HomePage::Count;
return true;
}
if (c == KEY_NEXT || c == KEY_RIGHT) {
_page = (_page + 1) % HomePage::Count;
if (_page == HomePage::RECENT) {
_task->showAlert("Recent adverts", 800);
}
return true;
}
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) {
if (_task->isSerialEnabled()) { // toggle Bluetooth on/off
_task->disableSerial();
} else {
_task->enableSerial();
}
return true;
}
if (c == KEY_ENTER && _page == HomePage::ADVERT) {
_task->notify(UIEventType::ack);
if (the_mesh.advert()) {
_task->showAlert("Advert sent!", 1000);
} else {
_task->showAlert("Advert failed..", 1000);
}
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;
}
return false;
}
};
class MsgPreviewScreen : public UIScreen {
UITask* _task;
mesh::RTCClock* _rtc;
struct MsgEntry {
uint32_t timestamp;
char origin[62];
char msg[78];
};
#define MAX_UNREAD_MSGS 32
int num_unread;
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
auto p = &unread[num_unread++];
p->timestamp = _rtc->getCurrentTime();
if (path_len == 0xFF) {
sprintf(p->origin, "(D) %s:", from_name);
} else {
sprintf(p->origin, "(%d) %s:", (uint32_t) path_len, from_name);
}
StrHelper::strncpy(p->msg, msg, sizeof(p->msg));
}
int render(DisplayDriver& display) override {
char tmp[16];
display.setCursor(0, 0);
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
sprintf(tmp, "Unread: %d", num_unread);
display.print(tmp);
auto p = &unread[0];
int secs = _rtc->getCurrentTime() - p->timestamp;
if (secs < 60) {
sprintf(tmp, "%ds", secs);
} else if (secs < 60*60) {
sprintf(tmp, "%dm", secs / 60);
} else {
sprintf(tmp, "%dh", secs / (60*60));
}
display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0);
display.print(tmp);
display.drawRect(0, 11, display.width(), 1); // horiz line
display.setCursor(0, 14);
display.setColor(DisplayDriver::YELLOW);
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);
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_NEXT || c == KEY_RIGHT) {
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;
}
if (c == KEY_ENTER) {
num_unread = 0; // clear unread queue
_task->gotoHomeScreen();
return true;
}
return false;
}
};
void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) {
_display = display;
_sensors = sensors;
_auto_off = millis() + AUTO_OFF_MILLIS;
#if defined(PIN_USER_BTN)
user_btn.begin();
#endif
#if defined(PIN_USER_BTN_ANA)
analog_btn.begin();
#endif
_node_prefs = node_prefs;
if (_display != NULL) {
_display->turnOn();
}
#ifdef PIN_BUZZER
buzzer.begin();
#endif
#ifdef PIN_VIBRATION
vibration.begin();
#endif
ui_started_at = millis();
_alert_expiry = 0;
splash = new SplashScreen(this);
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
msg_preview = new MsgPreviewScreen(this, &rtc_clock);
setCurrScreen(splash);
}
void UITask::showAlert(const char* text, int duration_millis) {
strcpy(_alert, text);
_alert_expiry = millis() + duration_millis;
}
void UITask::notify(UIEventType t) {
#if defined(PIN_BUZZER)
switch(t){
case UIEventType::contactMessage:
// gemini's pick
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7");
break;
case UIEventType::channelMessage:
buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#");
break;
case UIEventType::ack:
buzzer.play("ack:d=32,o=8,b=120:c");
break;
case UIEventType::roomMessage:
case UIEventType::newContactMessage:
case UIEventType::none:
default:
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) {
gotoHomeScreen();
}
}
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
_msgcount = msgcount;
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
setCurrScreen(msg_preview);
if (_display != NULL) {
if (!_display->isOn()) _display->turnOn();
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
_next_refresh = 100; // trigger refresh
}
}
void UITask::userLedHandler() {
#ifdef PIN_STATUS_LED
int cur_time = millis();
if (cur_time > next_led_change) {
if (led_state == 0) {
led_state = 1;
if (_msgcount > 0) {
last_led_increment = LED_ON_MSG_MILLIS;
} else {
last_led_increment = LED_ON_MILLIS;
}
next_led_change = cur_time + last_led_increment;
} else {
led_state = 0;
next_led_change = cur_time + LED_CYCLE_MILLIS - last_led_increment;
}
digitalWrite(PIN_STATUS_LED, led_state);
}
#endif
}
void UITask::setCurrScreen(UIScreen* c) {
curr = c;
_next_refresh = 100;
}
/*
hardware-agnostic pre-shutdown activity should be done here
*/
void UITask::shutdown(bool restart){
#ifdef PIN_BUZZER
/* note: we have a choice here -
we can do a blocking buzzer.loop() with non-deterministic consequences
or we can set a flag and delay the shutdown for a couple of seconds
while a non-blocking buzzer.loop() plays out in UITask::loop()
*/
buzzer.shutdown();
uint32_t buzzer_timer = millis(); // fail-safe shutdown
while (buzzer.isPlaying() && (millis() - 2500) < buzzer_timer)
buzzer.loop();
#endif // PIN_BUZZER
if (restart) {
_board->reboot();
} else {
_display->turnOff();
_board->powerOff();
}
}
bool UITask::isButtonPressed() const {
#ifdef PIN_USER_BTN
return user_btn.isPressed();
#else
return false;
#endif
}
void UITask::loop() {
char c = 0;
#if UI_HAS_JOYSTICK
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_ENTER);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER); // REVISIT: could be mapped to different key code
}
ev = joystick_left.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_LEFT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_LEFT);
}
ev = joystick_right.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_RIGHT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_RIGHT);
}
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_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(DISP_BACKLIGHT) && defined(BACKLIGHT_BTN)
if (millis() > next_backlight_btn_check) {
bool touch_state = digitalRead(PIN_BUTTON2);
digitalWrite(DISP_BACKLIGHT, !touch_state);
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 = 100; // trigger refresh
}
userLedHandler();
#ifdef PIN_BUZZER
if (buzzer.isPlaying()) buzzer.loop();
#endif
if (curr) curr->poll();
if (_display != NULL && _display->isOn()) {
if (millis() >= _next_refresh && curr) {
_display->startFrame();
int delay_millis = curr->render(*_display);
if (millis() < _alert_expiry) { // render alert popup
_display->setTextSize(1);
int y = _display->height() / 3;
int p = _display->height() / 32;
_display->setColor(DisplayDriver::DARK);
_display->fillRect(p, y, _display->width() - p*2, y);
_display->setColor(DisplayDriver::LIGHT); // draw box border
_display->drawRect(p, y, _display->width() - p*2, y);
_display->drawTextCentered(_display->width() / 2, y + p*3, _alert);
_next_refresh = _alert_expiry; // will need refresh when alert is dismissed
} else {
_next_refresh = millis() + delay_millis;
}
_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();
if (milliVolts > 0 && milliVolts < AUTO_SHUTDOWN_MILLIVOLTS) {
// show low battery shutdown alert
// we should only do this for eink displays, which will persist after power loss
#if defined(THINKNODE_M1) || defined(LILYGO_TECHO)
if (_display != NULL) {
_display->startFrame();
_display->setTextSize(2);
_display->setColor(DisplayDriver::RED);
_display->drawTextCentered(_display->width() / 2, 20, "Low Battery.");
_display->drawTextCentered(_display->width() / 2, 40, "Shutting Down!");
_display->endFrame();
}
#endif
shutdown();
}
next_batt_chck = millis() + 8000;
}
#endif
}
char UITask::checkDisplayOn(char c) {
if (_display != NULL) {
if (!_display->isOn()) {
_display->turnOn(); // turn display on and consume event
c = 0;
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 0; // trigger refresh
}
return c;
}
char UITask::handleLongPress(char c) {
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
the_mesh.enterCLIRescue();
c = 0; // consume event
}
return c;
}
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");
notify(UIEventType::ack);
showAlert("GPS: Disabled", 800);
} else {
_sensors->setSettingValue("gps", "1");
notify(UIEventType::ack);
showAlert("GPS: Enabled", 800);
}
_next_refresh = 0;
break;
}
}
}
}
void UITask::toggleBuzzer() {
// Toggle buzzer quiet mode
#ifdef PIN_BUZZER
if (buzzer.isQuiet()) {
buzzer.quiet(false);
notify(UIEventType::ack);
showAlert("Buzzer: ON", 800);
} else {
buzzer.quiet(true);
showAlert("Buzzer: OFF", 800);
}
_next_refresh = 0; // trigger refresh
#endif
}

View File

@@ -0,0 +1,89 @@
#pragma once
#include <MeshCore.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h>
#include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h>
#include <Arduino.h>
#include <helpers/sensors/LPPDataHelpers.h>
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
#ifdef PIN_VIBRATION
#include <helpers/ui/GenericVibration.h>
#endif
#include "../AbstractUITask.h"
#include "../NodePrefs.h"
class UITask : public AbstractUITask {
DisplayDriver* _display;
SensorManager* _sensors;
#ifdef PIN_BUZZER
genericBuzzer buzzer;
#endif
#ifdef PIN_VIBRATION
GenericVibration vibration;
#endif
unsigned long _next_refresh, _auto_off;
NodePrefs* _node_prefs;
char _alert[80];
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;
UIScreen* msg_preview;
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);
public:
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
next_batt_chck = _next_refresh = 0;
ui_started_at = 0;
curr = NULL;
}
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
void gotoHomeScreen() { setCurrScreen(home); }
void showAlert(const char* text, int duration_millis);
int getMsgCount() const { return _msgcount; }
bool hasDisplay() const { return _display != NULL; }
bool isButtonPressed() const;
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 notify(UIEventType t = UIEventType::none) override;
void loop() override;
void shutdown(bool restart = false);
};

View File

@@ -0,0 +1,118 @@
#pragma once
#include <stdint.h>
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
static const uint8_t bluetooth_on[] = {
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x30, 0x00, 0x00,
0x00, 0x3C, 0x00, 0x00,
0x00, 0x3E, 0x00, 0x00,
0x00, 0x3F, 0x80, 0x00,
0x00, 0x3F, 0xC0, 0x00,
0x00, 0x3B, 0xE0, 0x00,
0x30, 0x38, 0xF8, 0x00,
0x3C, 0x38, 0x7C, 0x00,
0x3E, 0x38, 0x7C, 0x00,
0x1F, 0xB8, 0xF8, 0x70,
0x07, 0xF9, 0xF0, 0x78,
0x03, 0xFF, 0xC0, 0x78,
0x00, 0xFF, 0x80, 0x3C,
0x00, 0x7F, 0x07, 0x1C,
0x00, 0x7E, 0x07, 0x1C,
0x03, 0xFF, 0x82, 0x1C,
0x03, 0xFF, 0xC0, 0x78,
0x07, 0xFB, 0xE0, 0x78,
0x0F, 0xB8, 0xF8, 0x70,
0x3E, 0x38, 0x7C, 0x00,
0x3C, 0x38, 0x7C, 0x00,
0x38, 0x38, 0xF8, 0x00,
0x00, 0x39, 0xF0, 0x00,
0x00, 0x3F, 0xC0, 0x00,
0x00, 0x3F, 0x80, 0x00,
0x00, 0x3E, 0x00, 0x00,
0x00, 0x3C, 0x00, 0x00,
0x00, 0x38, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
};
static const uint8_t bluetooth_off[] = {
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0x80, 0x00,
0x00, 0x03, 0xC0, 0x00,
0x00, 0x03, 0xE0, 0x00,
0x38, 0x03, 0xF8, 0x00,
0x3C, 0x03, 0xFC, 0x00,
0x3E, 0x03, 0xBF, 0x00,
0x0F, 0x83, 0x8F, 0x80,
0x07, 0xC3, 0x87, 0xC0,
0x03, 0xF0, 0x03, 0xC0,
0x00, 0xF8, 0x0F, 0x80,
0x00, 0x7C, 0x0F, 0x00,
0x00, 0x1F, 0x0E, 0x00,
0x00, 0x0F, 0x80, 0x00,
0x00, 0x07, 0xE0, 0x00,
0x00, 0x07, 0xF0, 0x00,
0x00, 0x0F, 0xF8, 0x00,
0x00, 0x3F, 0xBE, 0x00,
0x00, 0x7F, 0x9F, 0x00,
0x00, 0xFB, 0x8F, 0xC0,
0x03, 0xE3, 0x83, 0xE0,
0x03, 0xC3, 0x87, 0xF0,
0x03, 0x83, 0x8F, 0xFC,
0x00, 0x03, 0xBF, 0x3C,
0x00, 0x03, 0xFC, 0x1C,
0x00, 0x03, 0xF8, 0x00,
0x00, 0x03, 0xE0, 0x00,
0x00, 0x03, 0xC0, 0x00,
0x00, 0x03, 0x80, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
};
static const uint8_t power_icon[] = {
0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00,
0x00, 0x33, 0xCC, 0x00, 0x00, 0xF3, 0xCF, 0x00, 0x01, 0xF3, 0xCF, 0x80,
0x03, 0xF3, 0xCF, 0xC0, 0x07, 0xF3, 0xCF, 0xE0, 0x0F, 0xE3, 0xC7, 0xF0,
0x1F, 0xC3, 0xC3, 0xF8, 0x1F, 0x83, 0xC1, 0xF8, 0x3F, 0x03, 0xC0, 0xFC,
0x3E, 0x03, 0xC0, 0x7C, 0x3E, 0x03, 0xC0, 0x7C, 0x7E, 0x01, 0x80, 0x7E,
0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E,
0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0x7C,
0x3E, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x80, 0x01, 0xF8,
0x1F, 0xC0, 0x03, 0xF8, 0x0F, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xF0,
0x07, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x00,
0x00, 0x3F, 0xFC, 0x00, 0x00, 0x0F, 0xF0, 0x00,
};
static const uint8_t advert_icon[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x30,
0x1C, 0x00, 0x00, 0x38, 0x18, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, 0x0C,
0x30, 0x60, 0x06, 0x0C, 0x60, 0xE0, 0x07, 0x06, 0x61, 0xC0, 0x03, 0x86,
0xE1, 0x81, 0x81, 0x87, 0xC3, 0x07, 0xE0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3,
0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3,
0xC3, 0x07, 0xE0, 0xC3, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x80, 0x01, 0x86,
0x60, 0xC0, 0x03, 0x06, 0x70, 0xE0, 0x07, 0x0E, 0x30, 0x40, 0x02, 0x0C,
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,
};

View File

@@ -1,8 +1,7 @@
#include "UITask.h"
#include <Arduino.h>
#include <helpers/TxtDataHelpers.h>
#include "NodePrefs.h"
#include "MyMesh.h"
#include "../MyMesh.h"
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
@@ -89,9 +88,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");
@@ -109,8 +108,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) {
@@ -371,7 +370,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!");
@@ -389,7 +388,7 @@ 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);
@@ -408,8 +407,12 @@ void UITask::handleButtonQuadruplePress() {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
_sensors->setSettingValue("gps", "0");
notify(UIEventType::ack);
sprintf(_alert, "GPS: Disabled");
} else {
_sensors->setSettingValue("gps", "1");
notify(UIEventType::ack);
sprintf(_alert, "GPS: Enabled");
}
break;
}

View File

@@ -9,28 +9,18 @@
#include <helpers/ui/buzzer.h>
#endif
#include "NodePrefs.h"
#include "../AbstractUITask.h"
#include "../NodePrefs.h"
#include "Button.h"
enum class UIEventType
{
none,
contactMessage,
channelMessage,
roomMessage,
newContactMessage,
ack
};
class UITask {
class UITask : public AbstractUITask {
DisplayDriver* _display;
mesh::MainBoard* _board;
SensorManager* _sensors;
#ifdef PIN_BUZZER
genericBuzzer buzzer;
#endif
unsigned long _next_refresh, _auto_off;
bool _connected;
NodePrefs* _node_prefs;
char _version_info[32];
char _origin[62];
@@ -64,19 +54,20 @@ class UITask {
public:
UITask(mesh::MainBoard* board) : _board(board), _display(NULL), _sensors(NULL) {
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
_next_refresh = 0;
ui_started_at = 0;
_connected = false;
}
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
void setHasConnection(bool connected) { _connected = connected; }
bool hasDisplay() const { return _display != NULL; }
void clearMsgPreview();
void msgRead(int msgcount);
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount);
void soundBuzzer(UIEventType bet = UIEventType::none);
// from AbstractUITask
void msgRead(int msgcount) override;
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
void notify(UIEventType t = UIEventType::none) override;
void loop() override;
void shutdown(bool restart = false);
void loop();
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,228 @@
#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;
};
#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 "13 Nov 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.10.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;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
ClientACL acl;
TransportKeyStore key_store;
RegionMap region_map, temp_map;
RegionEntry* load_stack[8];
RegionEntry* recv_pkt_region;
RateLimiter discover_limiter;
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);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, 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) 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(uint8_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
};

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,732 +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 "2 Jul 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.7.2"
#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;
};
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 1000
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;
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;
self_id.calcSharedSecret(oldest->secret, id); // calc ECDH shared secret
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();
memcpy(&reply_data[4], &stats, sizeof(stats));
return 4 + sizeof(stats); // reply_len
}
case REQ_TYPE_GET_TELEMETRY_DATA: {
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors(sender->is_admin ? 0xFF : 0x00, 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
}
void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override {
if (type == 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;
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];
const char *command = (const char *) &data[5];
char *reply = (char *) &temp[5];
if (is_retry) {
*reply = 0;
} else {
_cli.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;
_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 = 3; // 3 hours
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
}
CommonCLI* getCLI() { return &_cli; }
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);
}
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
}
const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; }
void clearStats() override {
radio_driver.resetStats();
resetStats();
((SimpleMeshTables *)getTables())->resetStats();
}
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
}
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
}
};
StdRNG fast_rng;
SimpleMeshTables tables;
@@ -736,7 +17,7 @@ void halt() {
while (1) ;
}
static char command[80];
static char command[160];
void setup() {
Serial.begin(115200);
@@ -747,12 +28,15 @@ void setup() {
#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()) {
halt();
}
fast_rng.begin(radio_get_rng_seed());
@@ -807,17 +91,19 @@ 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.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}
@@ -827,4 +113,8 @@ void loop() {
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
rtc_clock.tick();
}

View File

@@ -0,0 +1,885 @@
#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 < 0) {
sendFlood(reply);
client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD);
} else {
sendDirect(reply, client->out_path, client->out_path_len);
client->extra.room.ack_timeout =
futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (client->out_path_len + 1));
}
_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->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1);
}
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1);
}
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
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);
}
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);
} else {
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13);
if (reply) {
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply, SERVER_RESPONSE_DELAY);
}
}
}
}
}
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)
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 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 < 0) {
mesh::Packet *ack = createAck(ack_hash);
if (ack) sendFlood(ack, TXT_ACK_DELAY);
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 < 0) {
sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY);
} 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 >= 0) {
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);
} 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);
}
}
}
}
}
}
}
}
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);
memcpy(client->out_path, path, client->out_path_len = 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, &_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);
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
updateAdvertTimer();
updateFloodAdvertTimer();
#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) {
mesh::Packet *pkt = createSelfAdvert();
if (pkt) {
sendFlood(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(uint8_t power_dbm) {
radio_set_tx_power(power_dbm);
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
self_id = new_id;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
#elif defined(ESP32)
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 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 "13 Nov 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.10.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;
CommonCLI _cli;
ClientACL acl;
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) 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(uint8_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();
};

View File

@@ -1,915 +1,13 @@
#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 <RTClib.h>
#include <target.h>
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "2 Jul 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.7.2"
#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_CLIENTS
#define MAX_CLIENTS 32
#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
#include "MyMesh.h"
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(display);
#endif
#define FIRMWARE_ROLE "room_server"
#define PACKET_LOG_FILE "/packet_log"
/* ------------------------------ Code -------------------------------- */
enum RoomPermission {
ADMIN,
GUEST,
READ_ONLY
};
struct ClientInfo {
mesh::Identity id;
uint32_t last_timestamp; // by THEIR clock
uint32_t last_activity; // by OUR clock
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;
RoomPermission permission;
uint8_t push_failures;
uint8_t secret[PUB_KEY_SIZE];
int out_path_len;
uint8_t out_path[MAX_PATH_SIZE];
};
#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];
};
#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 CLIENT_KEEP_ALIVE_SECS 0 // Now Disabled (was 128)
#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 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;
};
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];
int num_clients;
ClientInfo known_clients[MAX_CLIENTS];
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;
ClientInfo* putClient(const mesh::Identity& id) {
for (int i = 0; i < num_clients; i++) {
if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known
}
ClientInfo* newClient;
if (num_clients < MAX_CLIENTS) {
newClient = &known_clients[num_clients++];
} else { // table is currently full
// evict least active client
uint32_t oldest_timestamp = 0xFFFFFFFF;
newClient = &known_clients[0];
for (int i = 0; i < num_clients; i++) {
auto c = &known_clients[i];
if (c->last_activity < oldest_timestamp) {
oldest_timestamp = c->last_activity;
newClient = c;
}
}
}
newClient->id = id;
newClient->out_path_len = -1; // initially out_path is unknown
newClient->last_timestamp = 0;
self_id.calcSharedSecret(newClient->secret, id); // calc ECDH shared secret
return newClient;
}
void evict(ClientInfo* client) {
client->last_activity = 0; // this slot will now be re-used (will be oldest)
memset(client->id.pub_key, 0, sizeof(client->id.pub_key));
memset(client->secret, 0, sizeof(client->secret));
client->pending_ack = 0;
}
void 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 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->pending_ack, 4, reply_data, len, client->id.pub_key, PUB_KEY_SIZE);
client->push_post_timestamp = post.post_timestamp;
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->secret, reply_data, len);
if (reply) {
if (client->out_path_len < 0) {
sendFlood(reply);
client->ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD);
} else {
sendDirect(reply, client->out_path, client->out_path_len);
client->ack_timeout = futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (client->out_path_len + 1));
}
_num_post_pushes++; // stats
} else {
client->pending_ack = 0;
MESH_DEBUG_PRINTLN("Unable to push post to client");
}
}
uint8_t getUnsyncedCount(ClientInfo* client) {
uint8_t count = 0;
for (int k = 0; k < MAX_UNSYNCED_POSTS; k++) {
if (posts[k].post_timestamp > client->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 processAck(const uint8_t *data) {
for (int i = 0; i < num_clients; i++) {
auto client = &known_clients[i];
if (client->pending_ack && memcmp(data, &client->pending_ack, 4) == 0) { // got an ACK from Client!
client->pending_ack = 0; // clear this, so next push can happen
client->push_failures = 0;
client->sync_since = client->push_post_timestamp; // advance Client's SINCE timestamp, to sync next post
return true;
}
}
return false;
}
mesh::Packet* createSelfAdvert() {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
{
AdvertDataBuilder builder(ADV_TYPE_ROOM, _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)
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 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: {
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 = _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.n_posted = _num_posted;
stats.n_post_push = _num_post_pushes;
memcpy(&reply_data[4], &stats, sizeof(stats));
return 4 + sizeof(stats);
}
case REQ_TYPE_GET_TELEMETRY_DATA: {
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors(sender->permission == RoomPermission::ADMIN ? 0xFF : 0x00, telemetry);
uint8_t tlen = telemetry.getSize();
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
return 4 + tlen; // reply_len
}
}
return 0; // unknown command
}
protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
}
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);
}
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;
}
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
}
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;
}
void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override {
if (type == 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
RoomPermission perm;
data[len] = 0; // ensure null terminator
if (strcmp((char *) &data[8], _prefs.password) == 0) { // check for valid admin password
perm = RoomPermission::ADMIN;
} else {
if (strcmp((char *) &data[8], _prefs.guest_password) == 0) { // check the room/public password
perm = RoomPermission::GUEST;
} else if (_prefs.allow_read_only) {
perm = RoomPermission::READ_ONLY;
} else {
MESH_DEBUG_PRINTLN("Incorrect room password");
return; // no response. Client will timeout
}
}
auto client = putClient(sender); // 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->permission = perm;
client->last_timestamp = sender_timestamp;
client->sync_since = sender_sync_since;
client->pending_ack = 0;
client->push_failures = 0;
uint32_t now = getRTCClock()->getCurrentTime();
client->last_activity = now;
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] = (CLIENT_KEEP_ALIVE_SECS >> 4); // NEW: recommended keep-alive interval (secs / 16)
reply_data[6] = (perm == RoomPermission::ADMIN ? 1 : (perm == RoomPermission::GUEST ? 0 : 2));
reply_data[7] = getUnsyncedCount(client); // NEW
memcpy(&reply_data[8], "OK", 2); // REVISIT: not really needed
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->secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, 8 + 2);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 8 + 2);
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 < num_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 < num_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 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 >= num_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_TXT_MSG && len > 5) { // a CLI command or new Post
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint flags = (data[4] >> 2); // message attempt number, and other flags
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->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->permission == RoomPermission::ADMIN) {
if (is_retry) {
temp[5] = 0; // no reply
} else {
_cli.handleCommand(sender_timestamp, (const 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->permission == RoomPermission::READ_ONLY) {
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) {
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);
}
}
delay_millis = TXT_ACK_DELAY + 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 < 0) {
sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY);
} 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->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->sync_since = forceSince; // force-update the 'sync since'
}
client->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 >= 0) {
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);
} 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);
}
}
}
}
}
}
}
}
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 < num_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);
}
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
// also got an encoded ACK!
processAck(extra);
}
// NOTE: no reciprocal path send!!
return false;
}
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override {
if (processAck((uint8_t *)&ack_crc)) {
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
}
}
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)
{
next_local_advert = next_flood_advert = 0;
_logging = false;
// 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;
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 = 3; // 3 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
num_clients = 0;
next_post_idx = 0;
next_client_idx = 0;
next_push = 0;
memset(posts, 0, sizeof(posts));
_num_posted = _num_post_pushes = 0;
}
CommonCLI* getCLI() { return &_cli; }
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);
}
bool formatFileSystem() override {
#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 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 {
strcpy(reply, "not supported");
}
const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; }
void clearStats() override {
radio_driver.resetStats();
resetStats();
((SimpleMeshTables *)getTables())->resetStats();
}
void loop() {
mesh::Mesh::loop();
if (millisHasNowPassed(next_push) && num_clients > 0) {
// check for ACK timeouts
for (int i = 0; i < num_clients; i++) {
auto c = &known_clients[i];
if (c->pending_ack && millisHasNowPassed(c->ack_timeout)) {
c->push_failures++;
c->pending_ack = 0; // reset (TODO: keep prev expected_ack's in a list, incase they arrive LATER, after we retry)
MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->push_failures);
}
}
// check next Round-Robin client, and sync next new post
auto client = &known_clients[next_client_idx];
bool did_push = false;
if (client->pending_ack == 0 && client->last_activity != 0 && client->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->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) % num_clients; // 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
}
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
// TODO: periodically check for OLD/inactive entries in known_clients[], and evict
}
};
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
@@ -929,6 +27,7 @@ void setup() {
#ifdef DISPLAY_CLASS
if (display.begin()) {
display.startFrame();
display.setCursor(0, 0);
display.print("Please wait...");
display.endFrame();
}
@@ -998,7 +97,7 @@ void loop() {
if (len > 0 && command[len - 1] == '\r') { // received complete line
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
the_mesh.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}
@@ -1008,4 +107,8 @@ void loop() {
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
rtc_clock.tick();
}

View File

@@ -217,18 +217,18 @@ protected:
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 {
@@ -548,7 +548,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) ;
@@ -587,4 +587,5 @@ void setup() {
void loop() {
the_mesh.loop();
rtc_clock.tick();
}

View File

@@ -0,0 +1,938 @@
#include "SensorMesh.h"
/* ------------------------------ Config -------------------------------- */
#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 "sensor"
#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
#ifndef SENSOR_READ_INTERVAL_SECS
#define SENSOR_READ_INTERVAL_SECS 60
#endif
/* ------------------------------ Code -------------------------------- */
#define FIRMWARE_VER_LEVEL 1
#define REQ_TYPE_LOGIN 0x00
#define REQ_TYPE_GET_STATUS 0x01
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
#define REQ_TYPE_GET_AVG_MIN_MAX 0x04
#define REQ_TYPE_GET_ACCESS_LIST 0x05
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
#define CLI_REPLY_DELAY_MILLIS 1000
#define LAZY_CONTACTS_WRITE_DELAY 5000
#define ALERT_ACK_EXPIRY_MILLIS 8000 // wait 8 secs for ACKs to alert messages
static File openAppend(FILESYSTEM* _fs, 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
}
static uint8_t getDataSize(uint8_t type) {
switch (type) {
case LPP_GPS:
return 9;
case LPP_POLYLINE:
return 8; // TODO: this is MINIMIUM
case LPP_GYROMETER:
case LPP_ACCELEROMETER:
return 6;
case LPP_GENERIC_SENSOR:
case LPP_FREQUENCY:
case LPP_DISTANCE:
case LPP_ENERGY:
case LPP_UNIXTIME:
return 4;
case LPP_COLOUR:
return 3;
case LPP_ANALOG_INPUT:
case LPP_ANALOG_OUTPUT:
case LPP_LUMINOSITY:
case LPP_TEMPERATURE:
case LPP_CONCENTRATION:
case LPP_BAROMETRIC_PRESSURE:
case LPP_RELATIVE_HUMIDITY:
case LPP_ALTITUDE:
case LPP_VOLTAGE:
case LPP_CURRENT:
case LPP_DIRECTION:
case LPP_POWER:
return 2;
}
return 1;
}
static uint32_t getMultiplier(uint8_t type) {
switch (type) {
case LPP_CURRENT:
case LPP_DISTANCE:
case LPP_ENERGY:
return 1000;
case LPP_VOLTAGE:
case LPP_ANALOG_INPUT:
case LPP_ANALOG_OUTPUT:
return 100;
case LPP_TEMPERATURE:
case LPP_BAROMETRIC_PRESSURE:
case LPP_RELATIVE_HUMIDITY:
return 10;
}
return 1;
}
static bool isSigned(uint8_t type) {
return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER ||
type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER;
}
static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
uint32_t value = 0;
for (uint8_t i = 0; i < size; i++) {
value = (value << 8) + buffer[i];
}
int sign = 1;
if (is_signed) {
uint32_t bit = 1ul << ((size * 8) - 1);
if ((value & bit) == bit) {
value = (bit << 1) - value;
sign = -1;
}
}
return sign * ((float) value / multiplier);
}
static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) {
// check sign
bool sign = value < 0;
if (sign) value = -value;
// get value to store
uint32_t v = value * multiplier;
// format an uint32_t as if it was an int32_t
if (is_signed & sign) {
uint32_t mask = (1 << (size * 8)) - 1;
v = v & mask;
if (sign) v = mask - v + 1;
}
// add bytes (MSB first)
for (uint8_t i=1; i<=size; i++) {
dest[size - i] = (v & 0xFF);
v >>= 8;
}
return size;
}
uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all
uint8_t perm_mask = ~(payload[0]); // 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(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
uint8_t tlen = telemetry.getSize();
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
return 4 + tlen; // reply_len
}
if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
uint32_t start_secs_ago, end_secs_ago;
memcpy(&start_secs_ago, &payload[0], 4);
memcpy(&end_secs_ago, &payload[4], 4);
uint8_t res1 = payload[8]; // reserved for future (extra query params)
uint8_t res2 = payload[9];
MinMaxAvg data[8];
int n;
if (res1 == 0 && res2 == 0) {
n = querySeriesData(start_secs_ago, end_secs_ago, data, 8);
} else {
n = 0;
}
uint8_t ofs = 4;
{
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(&reply_data[ofs], &now, 4); ofs += 4;
}
for (int i = 0; i < n; i++) {
auto d = &data[i];
reply_data[ofs++] = d->_channel;
reply_data[ofs++] = d->_lpp_type;
uint8_t sz = getDataSize(d->_lpp_type);
uint32_t mult = getMultiplier(d->_lpp_type);
bool is_signed = isSigned(d->_lpp_type);
ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
}
return ofs;
}
if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN) {
uint8_t res1 = payload[0]; // reserved for future (extra query params)
uint8_t res2 = payload[1];
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->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;
}
return ofs;
}
}
return 0; // unknown command
}
mesh::Packet* SensorMesh::createSelfAdvert() {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len = _cli.buildAdvertData(ADV_TYPE_SENSOR, app_data);
return createAdvert(self_id, app_data, app_data_len);
}
void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
int text_len = strlen(t->text);
uint8_t data[MAX_PACKET_PAYLOAD];
memcpy(data, &t->timestamp, 4);
data[4] = (TXT_TYPE_PLAIN << 2) | t->attempt; // attempt and flags
memcpy(&data[5], t->text, text_len);
// calc expected ACK reply
mesh::Utils::sha256((uint8_t *)&t->expected_acks[t->attempt], 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
t->attempt++;
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
sendDirect(pkt, c->out_path, c->out_path_len);
} else {
sendFlood(pkt);
}
}
t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
}
void SensorMesh::alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text) {
if (condition) {
if (!t.isTriggered() && num_alert_tasks < MAX_CONCURRENT_ALERTS) {
StrHelper::strncpy(t.text, text, sizeof(t.text));
t.pri = pri;
t.send_expiry = 0; // signal that initial send is needed
t.attempt = 4;
t.curr_contact_idx = -1; // start iterating thru contacts[]
alert_tasks[num_alert_tasks++] = &t; // add to queue
}
} else {
if (t.isTriggered()) {
t.text[0] = 0;
// remove 't' from alert queue
int i = 0;
while (i < num_alert_tasks && alert_tasks[i] != &t) i++;
if (i < num_alert_tasks) { // found, now delete from array
num_alert_tasks--;
while (i < num_alert_tasks) {
alert_tasks[i] = alert_tasks[i + 1];
i++;
}
}
}
}
}
float SensorMesh::getAirtimeBudgetFactor() const {
return _prefs.airtime_factor;
}
bool SensorMesh::allowPacketForward(const mesh::Packet* packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
return true;
}
int SensorMesh::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);
}
uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + 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);
return getRNG()->nextInt(0, 6)*t;
}
int SensorMesh::getInterferenceThreshold() const {
return _prefs.interference_threshold;
}
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) {
ClientInfo* client;
if (data[0] == 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
return 0;
}
} else {
if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password
#if MESH_DEBUG
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
#endif
return 0;
}
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
}
MESH_DEBUG_PRINTLN("Login success!");
client->last_timestamp = sender_timestamp;
client->last_activity = getRTCClock()->getCurrentTime();
client->permissions |= PERM_ACL_ADMIN;
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
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;
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 13; // reply length
}
void SensorMesh::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;
}
// first, see if this is a custom-handled CLI command (ie. in main.cpp)
if (handleCustomCommand(sender_timestamp, command, reply)) {
return; // command has been handled
}
// handle sensor-specific CLI 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 entries
Serial.printf("%02X ", c->permissions);
mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
Serial.printf("\n");
}
reply[0] = 0;
} else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read
if (command[2] == ' ') { // it's a write
uint32_t val;
uint32_t g = board.getGpio();
if (command[3] == 'r') { // reset bits
sscanf(&command[4], "%x", &val);
val = g & ~val;
} else if (command[3] == 's') { // set bits
sscanf(&command[4], "%x", &val);
val |= g;
} else if (command[3] == 't') { // toggle bits
sscanf(&command[4], "%x", &val);
val ^= g;
} else { // set value
sscanf(&command[3], "%x", &val);
}
board.setGpio(val);
}
sprintf(reply, "%x", board.getGpio());
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
}
void SensorMesh::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 timestamp;
memcpy(&timestamp, data, 4);
data[len] = 0; // ensure null terminator
uint8_t reply_len;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
//} 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
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, 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, sender, secret, reply_data, reply_len);
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
}
}
}
int SensorMesh::searchPeersByHash(const uint8_t* hash) {
int n = 0;
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)
}
}
return n;
}
void SensorMesh::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 SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash) {
if (dest.out_path_len < 0) {
mesh::Packet* ack = createAck(ack_hash);
if (ack) sendFlood(ack, TXT_ACK_DELAY);
} else {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
mesh::Packet* a1 = createMultiAck(ack_hash, 1);
if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d);
d += 300;
}
mesh::Packet* a2 = createAck(ack_hash);
if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d);
}
}
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 >= acl.getNumClients()) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i);
return;
}
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 (reply_len == 0) return; // invalid command
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,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
if (reply) {
if (from->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, from->out_path, from->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 && 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
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);
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);
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);
} else {
sendAckTo(*from, ack_hash);
}
}
} else if (flags == TXT_TYPE_CLI_DATA) {
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
uint8_t temp[166];
char *command = (char *) &data[5];
char *reply = (char *) &temp[5];
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);
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);
} else {
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
}
}
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
}
}
}
bool SensorMesh::handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint 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);
Serial.printf(": %s\n", data);
#endif
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 >= acl.getNumClients()) {
MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i);
return false;
}
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();
// REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes??
if (from->isAdmin()) {
// only do saveContacts() (of this out_path change) if this is an admin
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
// NOTE: no reciprocal path send!!
return false;
}
void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
if (num_alert_tasks > 0) {
auto t = alert_tasks[0]; // check current alert task
for (int i = 0; i < t->attempt; i++) {
if (ack_crc == t->expected_acks[i]) { // matching ACK!
t->attempt = 4; // signal to move to next contact
t->send_expiry = 0;
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
return;
}
}
}
}
SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
_cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
{
next_local_advert = next_flood_advert = 0;
dirty_contacts_expiry = 0;
last_read_time = 0;
num_alert_tasks = 0;
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; // 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;
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 = 0; // disabled
_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) {
mesh::Mesh::begin();
_fs = fs;
// load persisted prefs
_cli.loadPrefs(_fs);
acl.load(_fs);
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
updateAdvertTimer();
updateFloodAdvertTimer();
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
#endif
}
bool SensorMesh::formatFileSystem() {
#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 SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
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 SensorMesh::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
}
void SensorMesh::sendSelfAdvertisement(int delay_millis) {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) {
sendFlood(pkt, delay_millis);
} else {
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
}
}
void SensorMesh::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 SensorMesh::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 SensorMesh::setTxPower(uint8_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();
uint8_t i = 0;
while (i + 2 < size) {
// Get channel #
uint8_t ch = buf[i++];
// Get data type
uint8_t t = buf[i++];
uint8_t sz = getDataSize(t);
if (ch == channel && t == type) {
return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t));
}
i += sz; // skip
}
return 0.0f; // not found
}
bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
if (channel == TELEM_CHANNEL_SELF) {
lat = sensors.node_lat;
lon = sensors.node_lon;
alt = sensors.node_altitude;
return true;
}
// REVISIT: custom GPS channels??
return false;
}
void SensorMesh::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");
}
uint32_t curr = getRTCClock()->getCurrentTime();
if (curr >= last_read_time + SENSOR_READ_INTERVAL_SECS) {
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions
onSensorDataRead();
last_read_time = curr;
}
// check the alert send queue
if (num_alert_tasks > 0) {
auto t = alert_tasks[0]; // process head of queue
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 >= 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 = 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
// reset attempts
t->attempt = (t->pri == LOW_PRI_ALERT) ? 3 : 0; // Low pri alerts, start at attempt #3 (ie. only make ONE attempt)
t->timestamp = getRTCClock()->getCurrentTimeUnique(); // need unique timestamp per contact
sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry
} else {
// next contact tested in next ::loop()
}
}
} 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
t->attempt = 4; // next ::loop() will remove t from queue
}
}
}
// is there are pending dirty contacts write needed?
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
acl.save(_fs);
dirty_contacts_expiry = 0;
}
}

View File

@@ -0,0 +1,162 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include "TimeSeriesData.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 <helpers/StatsFormatHelper.h>
#include <helpers/ClientACL.h>
#include <RTClib.h>
#include <target.h>
#define PERM_RESERVED1 (1 << 2)
#define PERM_RESERVED2 (1 << 3)
#define PERM_RESERVED3 (1 << 4)
#define PERM_RESERVED4 (1 << 5)
#define PERM_RECV_ALERTS_LO (1 << 6) // low priority alerts
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "13 Nov 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.10.0"
#endif
#define FIRMWARE_ROLE "sensor"
#define MAX_SEARCH_RESULTS 8
#define MAX_CONCURRENT_ALERTS 4
class SensorMesh : public mesh::Mesh, public CommonCLICallbacks {
public:
SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
void begin(FILESYSTEM* fs);
void loop();
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
// CommonCLI callbacks
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); }
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis) 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 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 { }
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
float getTelemValue(uint8_t channel, uint8_t type);
protected:
// current telemetry data queries
float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); }
float getCurrent(uint8_t channel) { return getTelemValue(channel, LPP_CURRENT); }
float getPower(uint8_t channel) { return getTelemValue(channel, LPP_POWER); }
float getTemperature(uint8_t channel) { return getTelemValue(channel, LPP_TEMPERATURE); }
float getRelativeHumidity(uint8_t channel) { return getTelemValue(channel, LPP_RELATIVE_HUMIDITY); }
float getBarometricPressure(uint8_t channel) { return getTelemValue(channel, LPP_BAROMETRIC_PRESSURE); }
float getAltitude(uint8_t channel) { return getTelemValue(channel, LPP_ALTITUDE); }
bool getGPS(uint8_t channel, float& lat, float& lon, float& alt);
// alerts
enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT };
struct Trigger {
uint32_t timestamp;
AlertPriority pri;
uint32_t expected_acks[4];
int8_t curr_contact_idx;
uint8_t attempt;
unsigned long send_expiry;
char text[MAX_PACKET_PAYLOAD];
Trigger() { text[0] = 0; }
bool isTriggered() const { return text[0] != 0; }
};
void alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text);
virtual void onSensorDataRead() = 0; // for app to implement
virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0; // for app to implement
virtual bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) { return false; }
// Mesh overrides
float getAirtimeBudgetFactor() const override;
bool allowPacketForward(const mesh::Packet* packet) 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;
int getAGCResetInterval() const 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 onControlDataRecv(mesh::Packet* packet) override;
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint flags, size_t len);
void sendAckTo(const ClientInfo& dest, uint32_t ack_hash);
private:
FILESYSTEM* _fs;
unsigned long next_local_advert, next_flood_advert;
NodePrefs _prefs;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
ClientACL acl;
unsigned long dirty_contacts_expiry;
CayenneLPP telemetry;
uint32_t last_read_time;
int matching_peer_indexes[MAX_SEARCH_RESULTS];
int num_alert_tasks;
Trigger* alert_tasks[MAX_CONCURRENT_ALERTS];
unsigned long set_radio_at, revert_radio_at;
float pending_freq;
float pending_bw;
uint8_t pending_sf;
uint8_t pending_cr;
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
mesh::Packet* createSelfAdvert();
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

@@ -0,0 +1,45 @@
#include "TimeSeriesData.h"
void TimeSeriesData::recordData(mesh::RTCClock* clock, float value) {
uint32_t now = clock->getCurrentTime();
if (now >= last_timestamp + interval_secs) {
last_timestamp = now;
data[next] = value; // append to cycle table
next = (next + 1) % num_slots;
}
}
void TimeSeriesData::calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const {
int i = next, n = num_slots;
uint32_t ago = clock->getCurrentTime() - last_timestamp;
int num_values = 0;
float total = 0.0f;
dest->_channel = channel;
dest->_lpp_type = lpp_type;
// start at most recet recording, back-track through to oldest
while (n > 0) {
n--;
i = (i + num_slots - 1) % num_slots; // go back by one
if (ago >= end_secs_ago && ago < start_secs_ago) { // filter by the desired time range
float v = data[i];
num_values++;
total += v;
if (num_values == 1) {
dest->_max = dest->_min = v;
} else {
if (v < dest->_min) dest->_min = v;
if (v > dest->_max) dest->_max = v;
}
}
ago += interval_secs;
}
// calc average
if (num_values > 0) {
dest->_avg = total / num_values;
} else {
dest->_max = dest->_min = dest->_avg = NAN;
}
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <Arduino.h>
#include <Mesh.h>
struct MinMaxAvg {
float _min, _max, _avg;
uint8_t _lpp_type, _channel;
};
class TimeSeriesData {
float* data;
int num_slots, next;
uint32_t last_timestamp;
uint32_t interval_secs;
public:
TimeSeriesData(float* array, int num, uint32_t secs) : num_slots(num), data(array), last_timestamp(0), next(0), interval_secs(secs) {
memset(data, 0, sizeof(float)*num);
}
TimeSeriesData(int num, uint32_t secs) : num_slots(num), last_timestamp(0), next(0), interval_secs(secs) {
data = new float[num];
memset(data, 0, sizeof(float)*num);
}
void recordData(mesh::RTCClock* clock, float value);
void calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const;
};

View File

@@ -0,0 +1,114 @@
#include "UITask.h"
#include <Arduino.h>
#include <helpers/CommonCLI.h>
#define AUTO_OFF_MILLIS 20000 // 20 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
_node_prefs = node_prefs;
_display->turnOn();
// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
char *version = strdup(firmware_version);
char *dash = strchr(version, '-');
if(dash){
*dash = 0;
}
// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
}
void UITask::renderCurrScreen() {
char tmp[80];
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo
_display->setColor(DisplayDriver::BLUE);
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->print(_version_info);
// node type
const char* node_type = "< Sensor >";
uint16_t typeWidth = _display->getTextWidth(node_type);
_display->setCursor((_display->width() - typeWidth) / 2, 35);
_display->print(node_type);
} else { // home screen
// node name
_display->setCursor(0, 0);
_display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN);
_display->print(_node_prefs->node_name);
// freq / sf
_display->setCursor(0, 20);
_display->setColor(DisplayDriver::YELLOW);
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
_display->print(tmp);
// bw / cr
_display->setCursor(0, 30);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
_display->print(tmp);
}
}
void UITask::loop() {
#ifdef PIN_USER_BTN
if (millis() >= _next_read) {
int btnState = digitalRead(PIN_USER_BTN);
if (btnState != _prevBtnState) {
if (btnState == LOW) { // pressed?
if (_display->isOn()) {
// TODO: any action ?
} else {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
}
_prevBtnState = btnState;
}
_next_read = millis() + 200; // 5 reads per second
}
#endif
if (_display->isOn()) {
if (millis() >= _next_refresh) {
_display->startFrame();
renderCurrScreen();
_display->endFrame();
_next_refresh = millis() + 1000; // refresh every second
}
if (millis() > _auto_off) {
_display->turnOff();
}
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <helpers/ui/DisplayDriver.h>
#include <helpers/CommonCLI.h>
class UITask {
DisplayDriver* _display;
unsigned long _next_read, _next_refresh, _auto_off;
int _prevBtnState;
NodePrefs* _node_prefs;
char _version_info[32];
void renderCurrScreen();
public:
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
void loop();
};

View File

@@ -0,0 +1,148 @@
#include "SensorMesh.h"
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(display);
#endif
class MyMesh : public SensorMesh {
public:
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: SensorMesh(board, radio, ms, rng, rtc, tables),
battery_data(12*24, 5*60) // 24 hours worth of battery data, every 5 minutes
{
}
protected:
/* ========================== custom logic here ========================== */
Trigger low_batt, critical_batt;
TimeSeriesData battery_data;
void onSensorDataRead() override {
float batt_voltage = getVoltage(TELEM_CHANNEL_SELF);
battery_data.recordData(getRTCClock(), batt_voltage); // record battery
alertIf(batt_voltage < 3.4f, critical_batt, HIGH_PRI_ALERT, "Battery is critical!");
alertIf(batt_voltage < 3.6f, low_batt, LOW_PRI_ALERT, "Battery is low");
}
int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override {
battery_data.calcMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE);
return 1;
}
bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) override {
if (strcmp(command, "magic") == 0) { // example 'custom' command handling
strcpy(reply, "**Magic now done**");
return true; // handled
}
return false; // not handled
}
/* ======================================================================= */
};
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
void halt() {
while (1) ;
}
static char command[160];
void setup() {
Serial.begin(115200);
delay(1000);
board.begin();
#ifdef DISPLAY_CLASS
if (display.begin()) {
display.startFrame();
display.print("Please wait...");
display.endFrame();
}
#endif
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
fs = &InternalFS;
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
SPIFFS.begin(true);
fs = &SPIFFS;
IdentityStore store(SPIFFS, "/identity");
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
fs = &LittleFS;
IdentityStore store(LittleFS, "/identity");
store.begin();
#else
#error "need to define filesystem"
#endif
if (!store.load("_main", the_mesh.self_id)) {
MESH_DEBUG_PRINTLN("Generating new keypair");
the_mesh.self_id = radio_new_identity(); // create new random identity
int count = 0;
while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00 || the_mesh.self_id.pub_key[0] == 0xFF)) { // reserved id hashes
the_mesh.self_id = radio_new_identity(); count++;
}
store.save("_main", the_mesh.self_id);
}
Serial.print("Sensor ID: ");
mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
command[0] = 0;
sensors.begin();
the_mesh.begin(fs);
#ifdef DISPLAY_CLASS
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
// send out initial Advertisement to the mesh
the_mesh.sendSelfAdvertisement(16000);
}
void loop() {
int len = strlen(command);
while (Serial.available() && len < sizeof(command)-1) {
char c = Serial.read();
if (c != '\n') {
command[len++] = c;
command[len] = 0;
}
Serial.print(c);
}
if (len == sizeof(command)-1) { // command buffer full
command[sizeof(command)-1] = '\r';
}
if (len > 0 && command[len - 1] == '\r') { // received complete line
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!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}
command[0] = 0; // reset command buffer
}
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
rtc_clock.tick();
}

16
library.json Normal file
View File

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

View File

@@ -18,26 +18,43 @@ 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_PRIVATE_KEY_IMPORT=1 ; NOTE: comment these out for more secure firmware
-D ENABLE_PRIVATE_KEY_EXPORT=1
-D RADIOLIB_EXCLUDE_CC1101=1
-D RADIOLIB_EXCLUDE_RF69=1
-D RADIOLIB_EXCLUDE_SX1231=1
-D RADIOLIB_EXCLUDE_SI443X=1
-D RADIOLIB_EXCLUDE_RFM2X=1
-D RADIOLIB_EXCLUDE_SX128X=1
-D RADIOLIB_EXCLUDE_AFSK=1
-D RADIOLIB_EXCLUDE_AX25=1
-D RADIOLIB_EXCLUDE_HELLSCHREIBER=1
-D RADIOLIB_EXCLUDE_MORSE=1
-D RADIOLIB_EXCLUDE_APRS=1
-D RADIOLIB_EXCLUDE_BELL=1
-D RADIOLIB_EXCLUDE_RTTY=1
-D RADIOLIB_EXCLUDE_SSTV=1
build_src_filter =
+<*.cpp>
+<helpers/*.cpp>
+<helpers/radiolib/*.cpp>
+<helpers/bridges/BridgeBase.cpp>
+<helpers/ui/MomentaryButton.cpp>
; ----------------- ESP32 ---------------------
[esp32_base]
extends = arduino_base
platform = platformio/espressif32@^6.11.0
platform = platformio/espressif32@6.11.0
monitor_filters = esp32_exception_decoder
extra_scripts = merge-bin.py
build_flags = ${arduino_base.build_flags}
@@ -50,27 +67,26 @@ lib_deps =
file://arch/esp32/AsyncElegantOTA
; esp32c6 uses arduino framework 3.x
; WARNING: experimental. pioarduino on esp32c6 needs work - it's not considered stable and has issues.
[esp32c6_base]
extends = esp32_base
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip
; ----------------- NRF52 ---------------------
[nrf52_base]
extends = arduino_base
platform = nordicnrf52
platform_packages =
framework-arduinoadafruitnrf52 @ 1.10700.0
extra_scripts = create-uf2.py
build_flags = ${arduino_base.build_flags}
-D NRF52_PLATFORM
-D LFS_NO_ASSERT=1
[nrf52840_base]
extends = nrf52_base
build_flags = ${nrf52_base.build_flags}
-D EXTRAFS=1
lib_deps =
${nrf52_base.lib_deps}
rweather/Crypto @ ^0.4.0
https://github.com/adafruit/Adafruit_nRF52_Arduino
${arduino_base.lib_deps}
https://github.com/oltaco/CustomLFS @ 0.2.1
; ----------------- RP2040 ---------------------
[rp2040_base]
@@ -95,3 +111,38 @@ 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 =
-D ENV_INCLUDE_GPS=1
-D ENV_INCLUDE_AHTX0=1
-D ENV_INCLUDE_BME280=1
-D ENV_INCLUDE_BMP280=1
-D ENV_INCLUDE_SHTC3=1
-D ENV_INCLUDE_SHT4X=1
-D ENV_INCLUDE_LPS22HB=1
-D ENV_INCLUDE_INA3221=1
-D ENV_INCLUDE_INA219=1
-D ENV_INCLUDE_INA226=1
-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
robtillaart/INA226 @ ^0.6.4
adafruit/Adafruit INA260 Library @ ^1.5.3
adafruit/Adafruit AHTX0 @ ^2.0.5
adafruit/Adafruit BME280 Library @ ^2.3.0
adafruit/Adafruit BMP280 Library @ ^2.6.8
adafruit/Adafruit SHTC3 Library @ ^1.0.1
sensirion/Sensirion I2C SHT4x @ ^1.1.2
arduino-libraries/Arduino_LPS22HB @ ^1.0.2
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

@@ -159,6 +159,7 @@ void Dispatcher::checkRecv() {
pkt->_snr = _radio->getLastSNR() * 4.0f;
score = _radio->packetScore(_radio->getLastSNR(), len);
air_time = _radio->getEstAirtimeFor(len);
rx_air_time += air_time;
}
}
}
@@ -169,9 +170,9 @@ void Dispatcher::checkRecv() {
if (pkt) {
#if MESH_PACKET_LOGGING
Serial.print(getLogDateTime());
Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d",
Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%d",
pkt->getRawLength(), pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
(int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000));
(int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000), air_time);
static uint8_t packet_hash[MAX_HASH_SIZE];
pkt->calculatePacketHash(packet_hash);

View File

@@ -114,7 +114,7 @@ typedef uint32_t DispatcherAction;
*/
class Dispatcher {
Packet* outbound; // current outbound packet
unsigned long outbound_expiry, outbound_start, total_air_time;
unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time;
unsigned long next_tx_time;
unsigned long cad_busy_start;
unsigned long radio_nonrx_start;
@@ -134,7 +134,9 @@ protected:
Dispatcher(Radio& radio, MillisecondClock& ms, PacketManager& mgr)
: _radio(&radio), _ms(&ms), _mgr(&mgr)
{
outbound = NULL; total_air_time = 0; next_tx_time = 0;
outbound = NULL;
total_air_time = rx_air_time = 0;
next_tx_time = 0;
cad_busy_start = 0;
next_floor_calib_time = next_agc_reset_time = 0;
_err_flags = 0;
@@ -167,6 +169,7 @@ public:
void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0);
unsigned long getTotalAirTime() const { return total_air_time; } // in milliseconds
unsigned long getReceiveAirTime() const {return rx_air_time; }
uint32_t getNumSentFlood() const { return n_sent_flood; }
uint32_t getNumSentDirect() const { return n_sent_direct; }
uint32_t getNumRecvFlood() const { return n_recv_flood; }

View File

@@ -92,7 +92,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

@@ -64,14 +64,14 @@ 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;
bool readFrom(Stream& s);
bool writeTo(Stream& s) const;

View File

@@ -22,6 +22,9 @@ uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) {
uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) {
return 0; // by default, no delay
}
uint8_t Mesh::getExtraAckTransmitCount() const {
return 0;
}
uint32_t Mesh::getCADFailRetryDelay() const {
return _rng->nextInt(1, 4)*120;
@@ -65,28 +68,38 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
return ACTION_RELEASE;
}
if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) {
if (pkt->path_len == 0) {
onControlDataRecv(pkt);
}
// just zero-hop control packets allowed (for this subset of payloads)
return ACTION_RELEASE;
}
if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) {
if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) {
if (_tables->hasSeen(pkt)) return ACTION_RELEASE; // don't retransmit!
// remove our hash from 'path', then re-broadcast
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];
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
return forwardMultipartDirect(pkt);
} else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
if (!_tables->hasSeen(pkt)) { // don't retransmit!
removeSelfFromPath(pkt);
routeDirectRecvAcks(pkt, 0);
}
return ACTION_RELEASE;
}
#else
#error "need path remove impl"
#endif
uint32_t d = getDirectRetransmitDelay(pkt);
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
if (!_tables->hasSeen(pkt)) {
removeSelfFromPath(pkt);
uint32_t d = getDirectRetransmitDelay(pkt);
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
}
}
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()) {
@@ -181,7 +194,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i);
if (len > 0) { // success!
onAnonDataRecv(pkt, pkt->getPayloadType(), sender, data, len);
onAnonDataRecv(pkt, secret, sender, data, len);
pkt->markDoNotRetransmit();
}
}
@@ -198,9 +211,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
@@ -261,6 +274,32 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
}
break;
}
case PAYLOAD_TYPE_MULTIPART:
if (pkt->payload_len > 2) {
uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent
uint8_t type = pkt->payload[0] & 0x0F;
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.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
if (!_tables->hasSeen(&tmp)) {
uint32_t ack_crc;
memcpy(&ack_crc, tmp.payload, 4);
onAckRecv(&tmp, ack_crc);
//action = routeRecvPacket(&tmp); // NOTE: currently not needed, as multipart ACKs not sent Flood
}
} else {
// FUTURE: other multipart types??
}
}
break;
default:
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): unknown payload type, header: %d", getLogDateTime(), (int) pkt->header);
// Don't flood route unknown packet types! action = routeRecvPacket(pkt);
@@ -269,6 +308,20 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
return action;
}
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];
}
#else
#error "need path remove impl"
#endif
}
DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit()
&& packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) {
@@ -282,6 +335,54 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
return ACTION_RELEASE;
}
DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent
uint8_t type = pkt->payload[0] & 0x0F;
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.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
if (!_tables->hasSeen(&tmp)) { // don't retransmit!
removeSelfFromPath(&tmp);
routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2)
}
}
return ACTION_RELEASE;
}
void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
if (!packet->isMarkedDoNotRetransmit()) {
uint32_t crc;
memcpy(&crc, packet->payload, 4);
uint8_t extra = getExtraAckTransmitCount();
while (extra > 0) {
delay_millis += getDirectRetransmitDelay(packet) + 300;
auto a1 = createMultiAck(crc, extra);
if (a1) {
memcpy(a1->path, packet->path, a1->path_len = packet->path_len);
a1->header &= ~PH_ROUTE_MASK;
a1->header |= ROUTE_TYPE_DIRECT;
sendPacket(a1, 0, delay_millis);
}
extra--;
}
auto a2 = createAck(crc);
if (a2) {
memcpy(a2->path, packet->path, a2->path_len = packet->path_len);
a2->header &= ~PH_ROUTE_MASK;
a2->header |= ROUTE_TYPE_DIRECT;
sendPacket(a2, 0, delay_millis);
}
}
}
Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) {
if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL;
@@ -449,6 +550,21 @@ Packet* Mesh::createAck(uint32_t ack_crc) {
return packet;
}
Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK;
memcpy(&packet->payload[1], &ack_crc, 4);
packet->payload_len = 5;
return packet;
}
Packet* Mesh::createRawData(const uint8_t* data, size_t len) {
if (len > sizeof(Packet::payload)) return NULL; // invalid arg
@@ -481,6 +597,22 @@ Packet* Mesh::createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags) {
return packet;
}
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) {
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
@@ -504,6 +636,31 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) {
sendPacket(packet, pri, delay_millis);
}
void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis) {
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
return;
}
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD;
packet->transport_codes[0] = transport_codes[0];
packet->transport_codes[1] = transport_codes[1];
packet->path_len = 0;
_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::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis) {
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_DIRECT;
@@ -539,4 +696,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

@@ -28,6 +28,11 @@ class Mesh : public Dispatcher {
RNG* _rng;
MeshTables* _tables;
void removeSelfFromPath(Packet* packet);
void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis);
//void routeRecvAcks(Packet* packet, uint32_t delay_millis);
DispatcherAction forwardMultipartDirect(Packet* pkt);
protected:
DispatcherAction onRecvPacket(Packet* pkt) override;
@@ -38,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)
@@ -54,6 +65,11 @@ protected:
*/
virtual uint32_t getDirectRetransmitDelay(const Packet* packet);
/**
* \returns number of extra (Direct) ACK transmissions wanted.
*/
virtual uint8_t getExtraAckTransmitCount() const;
/**
* \brief Perform search of local DB of peers/contacts.
* \returns Number of peers with matching hash
@@ -107,10 +123,10 @@ protected:
/**
* \brief A (now decrypted) data packet has been received.
* NOTE: these can be received multiple times (per sender/contents), via different routes
* \param type one of: PAYLOAD_TYPE_ANON_REQ
* \param secret ECDH shared secret
* \param sender public key provided by sender
*/
virtual void onAnonDataRecv(Packet* packet, uint8_t type, const Identity& sender, uint8_t* data, size_t len) { }
virtual void onAnonDataRecv(Packet* packet, const uint8_t* secret, const Identity& sender, uint8_t* data, size_t len) { }
/**
* \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded)
@@ -118,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.
*/
@@ -165,16 +186,24 @@ public:
Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len);
Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len);
Packet* createAck(uint32_t ack_crc);
Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining);
Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
Packet* 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);
/**
* \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);
/**
* \brief send a locally-generated Packet with Direct routing
*/
@@ -185,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

@@ -28,6 +28,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
@@ -41,6 +47,8 @@ public:
virtual void onAfterTransmit() { }
virtual void reboot() = 0;
virtual void powerOff() { /* no op */ }
virtual uint32_t getGpio() { return 0; }
virtual void setGpio(uint32_t values) {}
virtual uint8_t getStartupReason() const = 0;
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
};
@@ -64,6 +72,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

@@ -26,6 +26,8 @@ namespace mesh {
#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
#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

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

@@ -8,7 +8,8 @@
#define ADV_TYPE_CHAT 1
#define ADV_TYPE_REPEATER 2
#define ADV_TYPE_ROOM 3
//FUTURE: 4..15
#define ADV_TYPE_SENSOR 4
//FUTURE: 5..15
#define ADV_LATLON_MASK 0x10
#define ADV_FEAT1_MASK 0x20 // FUTURE

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;
@@ -31,6 +38,23 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl
return createAdvert(self_id, app_data, app_data_len);
}
void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
if (dest.out_path_len < 0) {
mesh::Packet* ack = createAck(ack_hash);
if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY);
} else {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
mesh::Packet* a1 = createMultiAck(ack_hash, 1);
if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d);
d += 300;
}
mesh::Packet* a2 = createAck(ack_hash);
if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d);
}
}
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())) {
@@ -51,9 +75,16 @@ 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);
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;
}
putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
bool is_new = false;
if (from == NULL) {
if (!isAutoAddEnabled()) {
@@ -141,6 +172,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
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
@@ -150,16 +182,9 @@ 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 {
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (from.out_path_len < 0) {
sendFlood(ack, TXT_ACK_DELAY);
} else {
sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY);
}
}
sendAckTo(from, ack_hash);
}
} else if (flags == TXT_TYPE_CLI_DATA) {
onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
@@ -168,12 +193,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
@@ -183,16 +209,9 @@ 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 {
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (from.out_path_len < 0) {
sendFlood(ack, TXT_ACK_DELAY);
} else {
sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY);
}
}
sendAckTo(from, ack_hash);
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported message type: %u", (uint32_t) flags);
@@ -206,20 +225,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
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 >= 0) {
// 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);
}
}
}
@@ -232,16 +255,20 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui
ContactInfo& from = contacts[i];
// NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
return onContactPathRecv(from, packet->path, packet->path_len, path, path_len, extra_type, extra, extra_len);
}
bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
// NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
// 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()
memcpy(from.out_path, out_path, from.out_path_len = 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) {
@@ -251,12 +278,25 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui
}
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 >= 0) {
// 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.shared_secret, 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;
@@ -313,7 +353,7 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
int rc;
if (recipient.out_path_len < 0) {
sendFlood(pkt);
sendFloodScoped(recipient, pkt);
txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
rc = MSG_SEND_SENT_FLOOD;
} else {
@@ -339,7 +379,7 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
int rc;
if (recipient.out_path_len < 0) {
sendFlood(pkt);
sendFloodScoped(recipient, pkt);
txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
rc = MSG_SEND_SENT_FLOOD;
} else {
@@ -365,7 +405,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;
@@ -379,7 +419,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
}
@@ -403,26 +445,56 @@ bool BaseChatMesh::importContact(const uint8_t src_buf[], uint8_t len) {
}
int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout) {
int tlen;
uint8_t temp[24];
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique
if (recipient.type == ADV_TYPE_ROOM) {
memcpy(&temp[4], &recipient.sync_since, 4);
int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently
memcpy(&temp[8], password, len);
tlen = 8 + len;
} else {
int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently
memcpy(&temp[4], password, len);
tlen = 4 + len;
}
mesh::Packet* pkt;
{
int tlen;
uint8_t temp[24];
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique
if (recipient.type == ADV_TYPE_ROOM) {
memcpy(&temp[4], &recipient.sync_since, 4);
int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently
memcpy(&temp[8], password, len);
tlen = 8 + len;
} else {
int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently
memcpy(&temp[4], password, len);
tlen = 4 + len;
}
auto 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.shared_secret, temp, tlen);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len < 0) {
sendFlood(pkt);
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::sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout) {
if (data_len > MAX_PACKET_PAYLOAD - 16) return MSG_SEND_FAILED;
mesh::Packet* pkt;
{
uint8_t temp[MAX_PACKET_PAYLOAD];
tag = getRTCClock()->getCurrentTimeUnique();
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);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len < 0) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
@@ -435,18 +507,21 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
}
int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout) {
uint8_t temp[13];
tag = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = req_type;
memset(&temp[5], 0, 4); // reserved (possibly for 'since' param)
getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique
mesh::Packet* pkt;
{
uint8_t temp[13];
tag = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = req_type;
memset(&temp[5], 0, 4); // reserved (possibly for 'since' param)
getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique
auto pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp));
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp));
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len < 0) {
sendFlood(pkt);
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
@@ -510,7 +585,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!
@@ -519,10 +594,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() {

View File

@@ -72,6 +72,7 @@ class BaseChatMesh : public mesh::Mesh {
ConnectionInfo connections[MAX_CONNECTIONS];
mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack);
void sendAckTo(const ContactInfo& dest, uint32_t ack_hash);
protected:
BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
@@ -87,11 +88,14 @@ protected:
memset(connections, 0, sizeof(connections));
}
void resetContacts() { num_contacts = 0; }
// 'UI' concepts, for sub-classes to implement
virtual bool isAutoAddEnabled() const { return true; }
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;
virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0;
@@ -101,6 +105,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
@@ -123,7 +131,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:
@@ -134,6 +142,7 @@ public:
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 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);
uint8_t exportContact(const ContactInfo& contact, uint8_t dest_buf[]);
bool importContact(const uint8_t src_buf[], uint8_t len);

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

@@ -0,0 +1,130 @@
#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) {
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);
if (!success) break; // EOF
c.id = mesh::Identity(pub_key);
if (num_clients < MAX_CLIENTS) {
clients[num_clients++] = c;
} else {
full = true;
}
}
file.close();
}
}
}
void ClientACL::save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)) {
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();
}
}
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 = -1; // initially out_path is 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;
}

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

@@ -0,0 +1,56 @@
#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
struct ClientInfo {
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)
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 {
ClientInfo clients[MAX_CLIENTS];
int num_clients;
public:
ClientACL() {
memset(clients, 0, sizeof(clients));
num_clients = 0;
}
void load(FILESYSTEM* _fs);
void save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)=NULL);
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!
@@ -32,32 +33,44 @@ 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->reserved2, sizeof(_prefs->reserved2)); // 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(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->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(pad, 4); // 152
file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
// 166
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -65,10 +78,21 @@ 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->multi_acks = constrain(_prefs->multi_acks, 0, 1);
// 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->gps_enabled = constrain(_prefs->gps_enabled, 0, 1);
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
file.close();
}
@@ -87,32 +111,44 @@ 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->reserved2, sizeof(_prefs->reserved2)); // 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(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->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(pad, 4); // 152
file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
// 166
file.close();
}
@@ -120,25 +156,31 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
#define MIN_LOCAL_ADVERT_INTERVAL 60
void CommonCLI::checkAdvertInterval() {
void CommonCLI::savePrefs() {
if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) {
_prefs->advert_interval = 0; // turn it off, now that device has been manually configured
}
_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) {
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;
}
if (memcmp(command, "reboot", 6) == 0) {
_board->reboot(); // doesn't return
} else if (memcmp(command, "advert", 6) == 0) {
_callbacks->sendSelfAdvertisement(400);
_callbacks->sendSelfAdvertisement(1500); // 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();
@@ -171,15 +213,43 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
}
} else if (memcmp(command, "neighbors", 9) == 0) {
_callbacks->formatNeighborsReply(reply);
} else if (memcmp(command, "neighbor.remove ", 16) == 0) {
const char* hex = &command[16];
uint8_t pubkey[PUB_KEY_SIZE];
int hex_len = min((int)strlen(hex), PUB_KEY_SIZE*2);
int pubkey_len = hex_len / 2;
if (mesh::Utils::fromHex(pubkey, pubkey_len, hex)) {
_callbacks->removeNeighbor(pubkey, pubkey_len);
strcpy(reply, "OK");
} else {
strcpy(reply, "ERR: bad pubkey");
}
} else if (memcmp(command, "tempradio ", 10) == 0) {
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;
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) {
_callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins);
sprintf(reply, "OK - temp params for %d mins", temp_timeout_mins);
} else {
strcpy(reply, "Error, invalid params");
}
} else if (memcmp(command, "password ", 9) == 0) {
// change admin password
StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password));
checkAdvertInterval();
savePrefs();
sprintf(reply, "password now: %s", _prefs->password); // echo back just to let admin know for sure!!
} 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) {
@@ -188,6 +258,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "multi.acks", 10) == 0) {
sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
} else if (memcmp(config, "allow.read.only", 15) == 0) {
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
@@ -196,6 +268,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
} else if (memcmp(config, "guest.password", 14) == 0) {
sprintf(reply, "> %s", _prefs->guest_password);
} else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only
uint8_t prv_key[PRV_KEY_SIZE];
int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
mesh::Utils::toHex(tmp, prv_key, len);
sprintf(reply, "> %s", tmp);
} else if (memcmp(config, "name", 4) == 0) {
sprintf(reply, "> %s", _prefs->node_name);
} else if (memcmp(config, "repeat", 6) == 0) {
@@ -223,12 +300,43 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
} else if (memcmp(config, "public.key", 10) == 0) {
strcpy(reply, "> ");
mesh::Utils::toHex(&reply[2], _callbacks->getSelfIdPubKey(), PUB_KEY_SIZE);
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 {
sprintf(reply, "??: %s", config);
}
/*
* SET commands
*/
} else if (memcmp(command, "set ", 4) == 0) {
const char* config = &command[4];
if (memcmp(config, "af ", 3) == 0) {
@@ -242,6 +350,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
_prefs->agc_reset_interval = atoi(&config[19]) / 4;
savePrefs();
sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "multi.acks ", 11) == 0) {
_prefs->multi_acks = atoi(&config[11]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "allow.read.only ", 16) == 0) {
_prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
@@ -271,9 +383,20 @@ 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
uint8_t prv_key[PRV_KEY_SIZE];
bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
if (success) {
mesh::LocalIdentity new_id;
new_id.readFrom(prv_key, PRV_KEY_SIZE);
_callbacks->saveIdentity(new_id);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, invalid key");
}
} else if (memcmp(config, "name ", 5) == 0) {
StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
checkAdvertInterval();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "repeat ", 7) == 0) {
@@ -300,12 +423,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
}
} else if (memcmp(config, "lat ", 4) == 0) {
_prefs->node_lat = atof(&config[4]);
checkAdvertInterval();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "lon ", 4) == 0) {
_prefs->node_lon = atof(&config[4]);
checkAdvertInterval();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "rxdelay ", 8) == 0) {
@@ -353,6 +474,55 @@ 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 {
sprintf(reply, "unknown config: %s", config);
}
@@ -361,6 +531,128 @@ 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, "log start", 9) == 0) {
_callbacks->setLoggingOn(true);
strcpy(reply, " logging on");
@@ -373,6 +665,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,51 @@
#include "Mesh.h"
#include <helpers/IdentityStore.h>
#include <helpers/SensorManager.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 reserved2;
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;
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
// 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)
// Gps settings
uint8_t gps_enabled;
uint32_t gps_interval; // in seconds
uint8_t advert_loc_policy;
uint32_t discovery_mod_timestamp;
};
class CommonCLICallbacks {
@@ -43,8 +64,24 @@ public:
virtual void dumpLogFile() = 0;
virtual void setTxPower(uint8_t power_dbm) = 0;
virtual void formatNeighborsReply(char *reply) = 0;
virtual const uint8_t* getSelfIdPubKey() = 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 {
@@ -52,20 +89,19 @@ class CommonCLI {
NodePrefs* _prefs;
CommonCLICallbacks* _callbacks;
mesh::MainBoard* _board;
char tmp[80];
SensorManager* _sensors;
char tmp[PRV_KEY_SIZE*2 + 4];
mesh::RTCClock* getRTCClock() { return _rtc; }
void savePrefs() { _callbacks->savePrefs(); }
void checkAdvertInterval();
void savePrefs();
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, NodePrefs* prefs, CommonCLICallbacks* callbacks)
: _board(&board), _rtc(&rtc), _sensors(&sensors), _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

@@ -1,17 +0,0 @@
#pragma once
#include <RadioLib.h>
#define LR1110_IRQ_HAS_PREAMBLE 0b0000000100 // 4 4 valid LoRa header received
#define LR1110_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
class CustomLR1110 : public LR1110 {
public:
CustomLR1110(Module *mod) : LR1110(mod) { }
bool isReceiving() {
uint16_t irq = getIrqStatus();
bool detected = ((irq & LR1110_IRQ_HEADER_VALID) || (irq & LR1110_IRQ_HAS_PREAMBLE));
return detected;
}
};

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

View File

@@ -5,25 +5,25 @@
class RefCountedDigitalPin {
uint8_t _pin;
int8_t _claims = 0;
uint8_t _active = 0;
public:
RefCountedDigitalPin(uint8_t pin): _pin(pin) { }
RefCountedDigitalPin(uint8_t pin,uint8_t active=HIGH): _pin(pin), _active(active) { }
void begin() {
pinMode(_pin, OUTPUT);
digitalWrite(_pin, LOW); // initial state
digitalWrite(_pin, !_active); // initial state
}
void claim() {
_claims++;
if (_claims > 0) {
digitalWrite(_pin, HIGH);
digitalWrite(_pin, _active);
}
}
void release() {
_claims--;
if (_claims == 0) {
digitalWrite(_pin, LOW);
digitalWrite(_pin, !_active);
}
}
};

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

@@ -0,0 +1,237 @@
#include "RegionMap.h"
#include <helpers/TxtDataHelpers.h>
#include <SHA256.h>
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(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '#';
}
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) {
if (_fs->exists("/regions2")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/regions2", "r");
#else
File file = _fs->open("/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) {
File file = openWrite(_fs, "/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] == '#') { // auto hashtag region
_store->getAutoKeyFor(region->id, region->name, keys[0]);
num = 1;
} else {
num = _store->loadKeysFor(region->id, keys, 4);
}
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;
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(name, region->name) == 0) return region;
}
return NULL; // not found
}
RegionEntry* RegionMap::findByNamePrefix(const char* prefix) {
if (strcmp(prefix, "*") == 0) return &wildcard;
RegionEntry* partial = NULL;
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(prefix, region->name) == 0) return region; // is a complete match, preference this one
if (memcmp(prefix, region->name, strlen(prefix)) == 0) {
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", parent->name, parent->id == home_id ? "^" : "");
} else {
out.printf("%s%s F\n", 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
}

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

@@ -0,0 +1,52 @@
#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(char c);
bool load(FILESYSTEM* _fs);
bool save(FILESYSTEM* _fs);
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; }
void exportTo(Stream& out) 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

@@ -0,0 +1,54 @@
#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}",
driver.getPacketsRecv(),
driver.getPacketsSent(),
n_sent_flood,
n_sent_direct,
n_recv_flood,
n_recv_direct
);
}
};

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,23 @@ const char* StrHelper::ftoa(float f) {
}
return tmp;
}
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,6 @@ 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 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);
}
}

View File

@@ -0,0 +1,120 @@
#pragma once
#include "helpers/AbstractBridge.h"
#include "helpers/CommonCLI.h"
#include "helpers/SimpleMeshTables.h"
#include <RTClib.h>
/**
* @brief Base class implementing common bridge functionality
*
* This class provides common functionality used by different bridge implementations
* like packet tracking, checksum calculation, timestamping, and duplicate detection.
*
* Features:
* - Fletcher-16 checksum calculation for data integrity
* - Packet duplicate detection using SimpleMeshTables
* - Common timestamp formatting for debug logging
* - Shared packet management and queuing logic
*/
class BridgeBase : public AbstractBridge {
public:
virtual ~BridgeBase() = default;
/**
* @brief Gets the current state of the bridge.
*
* @return true if the bridge is initialized and running, false otherwise.
*/
bool isRunning() const override;
/**
* @brief Common magic number used by all bridge implementations for packet identification
*
* This magic number is placed at the beginning of bridge packets to identify
* them as mesh bridge packets and provide frame synchronization.
*/
static constexpr uint16_t BRIDGE_PACKET_MAGIC = 0xC03E;
/**
* @brief Common field sizes used by bridge implementations
*
* These constants define the size of common packet fields used across bridges.
* BRIDGE_MAGIC_SIZE is used by all bridges for packet identification.
* BRIDGE_LENGTH_SIZE is used by bridges that need explicit length fields (like RS232).
* BRIDGE_CHECKSUM_SIZE is used by all bridges for Fletcher-16 checksums.
*/
static constexpr uint16_t BRIDGE_MAGIC_SIZE = sizeof(BRIDGE_PACKET_MAGIC);
static constexpr uint16_t BRIDGE_LENGTH_SIZE = sizeof(uint16_t);
static constexpr uint16_t BRIDGE_CHECKSUM_SIZE = sizeof(uint16_t);
protected:
/** Tracks bridge state */
bool _initialized = false;
/** Packet manager for allocating and queuing mesh packets */
mesh::PacketManager *_mgr;
/** RTC clock for timestamping debug messages */
mesh::RTCClock *_rtc;
/** Node preferences for configuration settings */
NodePrefs *_prefs;
/** Tracks seen packets to prevent loops in broadcast communications */
SimpleMeshTables _seen_packets;
/**
* @brief Constructs a BridgeBase instance
*
* @param prefs Node preferences for configuration settings
* @param mgr PacketManager for allocating and queuing packets
* @param rtc RTCClock for timestamping debug messages
*/
BridgeBase(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
: _prefs(prefs), _mgr(mgr), _rtc(rtc) {}
/**
* @brief Gets formatted date/time string for logging
*
* Format: "HH:MM:SS - DD/MM/YYYY U"
*
* @return Formatted date/time string
*/
const char *getLogDateTime();
/**
* @brief Calculate Fletcher-16 checksum
*
* Based on: https://en.wikipedia.org/wiki/Fletcher%27s_checksum
* Used to verify data integrity of received packets
*
* @param data Pointer to data to calculate checksum for
* @param len Length of data in bytes
* @return Calculated Fletcher-16 checksum
*/
static uint16_t fletcher16(const uint8_t *data, size_t len);
/**
* @brief Validate received checksum against calculated checksum
*
* @param data Pointer to data to validate
* @param len Length of data in bytes
* @param received_checksum Checksum received with data
* @return true if checksum is valid, false otherwise
*/
bool validateChecksum(const uint8_t *data, size_t len, uint16_t received_checksum);
/**
* @brief Common packet handling for received packets
*
* Implements the standard pattern used by all bridges:
* - Check if packet was seen before using _seen_packets.hasSeen()
* - Queue packet for mesh processing if not seen before
* - Free packet if already seen to prevent duplicates
*
* @param packet The received mesh packet
*/
void handleReceivedPacket(mesh::Packet *packet);
};

View File

@@ -0,0 +1,219 @@
#include "ESPNowBridge.h"
#include <WiFi.h>
#include <esp_wifi.h>
#ifdef WITH_ESPNOW_BRIDGE
// Static member to handle callbacks
ESPNowBridge *ESPNowBridge::_instance = nullptr;
// Static callback wrappers
void ESPNowBridge::recv_cb(const uint8_t *mac, const uint8_t *data, int32_t len) {
if (_instance) {
_instance->onDataRecv(mac, data, len);
}
}
void ESPNowBridge::send_cb(const uint8_t *mac, esp_now_send_status_t status) {
if (_instance) {
_instance->onDataSent(mac, status);
}
}
ESPNowBridge::ESPNowBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
: BridgeBase(prefs, mgr, rtc), _rx_buffer_pos(0) {
_instance = this;
}
void ESPNowBridge::begin() {
BRIDGE_DEBUG_PRINTLN("Initializing...\n");
// Initialize WiFi in station mode
WiFi.mode(WIFI_STA);
// Set wifi channel
if (esp_wifi_set_channel(_prefs->bridge_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error setting WIFI channel to %d\n", _prefs->bridge_channel);
return;
}
// Initialize ESP-NOW
if (esp_now_init() != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error initializing ESP-NOW\n");
return;
}
// Register callbacks
esp_now_register_recv_cb(recv_cb);
esp_now_register_send_cb(send_cb);
// Add broadcast peer
esp_now_peer_info_t peerInfo = {};
memset(&peerInfo, 0, sizeof(peerInfo));
memset(peerInfo.peer_addr, 0xFF, ESP_NOW_ETH_ALEN); // Broadcast address
peerInfo.channel = _prefs->bridge_channel;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Failed to add broadcast peer\n");
return;
}
// Update bridge state
_initialized = true;
}
void ESPNowBridge::end() {
BRIDGE_DEBUG_PRINTLN("Stopping...\n");
// Remove broadcast peer
uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
if (esp_now_del_peer(broadcastAddress) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error removing broadcast peer\n");
}
// Unregister callbacks
esp_now_register_recv_cb(nullptr);
esp_now_register_send_cb(nullptr);
// Deinitialize ESP-NOW
if (esp_now_deinit() != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error deinitializing ESP-NOW\n");
}
// Turn off WiFi
WiFi.mode(WIFI_OFF);
// Update bridge state
_initialized = false;
}
void ESPNowBridge::loop() {
// Nothing to do here - ESP-NOW is callback based
}
void ESPNowBridge::xorCrypt(uint8_t *data, size_t len) {
size_t keyLen = strlen(_prefs->bridge_secret);
for (size_t i = 0; i < len; i++) {
data[i] ^= _prefs->bridge_secret[i % keyLen];
}
}
void ESPNowBridge::onDataRecv(const uint8_t *mac, const uint8_t *data, int32_t len) {
// Ignore packets that are too small to contain header + checksum
if (len < (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE)) {
BRIDGE_DEBUG_PRINTLN("RX packet too small, len=%d\n", len);
return;
}
// Validate total packet size
if (len > MAX_ESPNOW_PACKET_SIZE) {
BRIDGE_DEBUG_PRINTLN("RX packet too large, len=%d\n", len);
return;
}
// Check packet header magic
uint16_t received_magic = (data[0] << 8) | data[1];
if (received_magic != BRIDGE_PACKET_MAGIC) {
BRIDGE_DEBUG_PRINTLN("RX invalid magic 0x%04X\n", received_magic);
return;
}
// Make a copy we can decrypt
uint8_t decrypted[MAX_ESPNOW_PACKET_SIZE];
const size_t encryptedDataLen = len - BRIDGE_MAGIC_SIZE;
memcpy(decrypted, data + BRIDGE_MAGIC_SIZE, encryptedDataLen);
// Try to decrypt (checksum + payload)
xorCrypt(decrypted, encryptedDataLen);
// Validate checksum
uint16_t received_checksum = (decrypted[0] << 8) | decrypted[1];
const size_t payloadLen = encryptedDataLen - BRIDGE_CHECKSUM_SIZE;
if (!validateChecksum(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen, received_checksum)) {
// Failed to decrypt - likely from a different network
BRIDGE_DEBUG_PRINTLN("RX checksum mismatch, rcv=0x%04X\n", received_checksum);
return;
}
BRIDGE_DEBUG_PRINTLN("RX, payload_len=%d\n", payloadLen);
// Create mesh packet
mesh::Packet *pkt = _instance->_mgr->allocNew();
if (!pkt) return;
if (pkt->readFrom(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen)) {
_instance->onPacketReceived(pkt);
} else {
_instance->_mgr->free(pkt);
}
}
void ESPNowBridge::onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
// Could add transmission error handling here if needed
}
void ESPNowBridge::sendPacket(mesh::Packet *packet) {
// Guard against uninitialized state
if (_initialized == false) {
return;
}
// First validate the packet pointer
if (!packet) {
BRIDGE_DEBUG_PRINTLN("TX invalid packet pointer\n");
return;
}
if (!_seen_packets.hasSeen(packet)) {
// Create a temporary buffer just for size calculation and reuse for actual writing
uint8_t sizingBuffer[MAX_PAYLOAD_SIZE];
uint16_t meshPacketLen = packet->writeTo(sizingBuffer);
// Check if packet fits within our maximum payload size
if (meshPacketLen > MAX_PAYLOAD_SIZE) {
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", meshPacketLen,
MAX_PAYLOAD_SIZE);
return;
}
uint8_t buffer[MAX_ESPNOW_PACKET_SIZE];
// Write magic header (2 bytes)
buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF;
buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF;
// Write packet payload starting after magic header and checksum
const size_t packetOffset = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE;
memcpy(buffer + packetOffset, sizingBuffer, meshPacketLen);
// Calculate and add checksum (only of the payload)
uint16_t checksum = fletcher16(buffer + packetOffset, meshPacketLen);
buffer[2] = (checksum >> 8) & 0xFF; // High byte
buffer[3] = checksum & 0xFF; // Low byte
// Encrypt payload and checksum (not including magic header)
xorCrypt(buffer + BRIDGE_MAGIC_SIZE, meshPacketLen + BRIDGE_CHECKSUM_SIZE);
// Total packet size: magic header + checksum + payload
const size_t totalPacketSize = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE + meshPacketLen;
// Broadcast using ESP-NOW
uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
esp_err_t result = esp_now_send(broadcastAddress, buffer, totalPacketSize);
if (result == ESP_OK) {
BRIDGE_DEBUG_PRINTLN("TX, len=%d\n", meshPacketLen);
} else {
BRIDGE_DEBUG_PRINTLN("TX FAILED!\n");
}
}
}
void ESPNowBridge::onPacketReceived(mesh::Packet *packet) {
handleReceivedPacket(packet);
}
#endif

View File

@@ -0,0 +1,157 @@
#pragma once
#include "MeshCore.h"
#include "esp_now.h"
#include "helpers/bridges/BridgeBase.h"
#ifdef WITH_ESPNOW_BRIDGE
/**
* @brief Bridge implementation using ESP-NOW protocol for packet transport
*
* This bridge enables mesh packet transport over ESP-NOW, a connectionless communication
* protocol provided by Espressif that allows ESP32 devices to communicate directly
* without WiFi router infrastructure.
*
* Features:
* - Broadcast-based communication (all bridges receive all packets)
* - Network isolation using XOR encryption with shared secret
* - Duplicate packet detection using SimpleMeshTables tracking
* - Maximum packet size of 250 bytes (ESP-NOW limitation)
*
* Packet Structure:
* [2 bytes] Magic Header - Used to identify ESPNowBridge packets
* [2 bytes] Fletcher-16 checksum of encrypted payload (calculated over payload only)
* [246 bytes max] Encrypted payload containing the mesh packet
*
* The Fletcher-16 checksum is used to validate packet integrity and detect
* corrupted or tampered packets. It's calculated over the encrypted payload
* and provides a simple but effective way to verify packets are both
* uncorrupted and from the same network (since the checksum is calculated
* after encryption).
*
* Configuration:
* - Define WITH_ESPNOW_BRIDGE to enable this bridge
* - Define _prefs->bridge_secret with a string to set the network encryption key
*
* Network Isolation:
* Multiple independent mesh networks can coexist by using different
* _prefs->bridge_secret values. Packets encrypted with a different key will
* fail the checksum validation and be discarded.
*/
class ESPNowBridge : public BridgeBase {
private:
static ESPNowBridge *_instance;
static void recv_cb(const uint8_t *mac, const uint8_t *data, int32_t len);
static void send_cb(const uint8_t *mac, esp_now_send_status_t status);
/**
* ESP-NOW Protocol Structure:
* - ESP-NOW header: 20 bytes (handled by ESP-NOW protocol)
* - ESP-NOW payload: 250 bytes maximum
* Total ESP-NOW packet: 270 bytes
*
* Our Bridge Packet Structure (must fit in ESP-NOW payload):
* - Magic header: 2 bytes
* - Checksum: 2 bytes
* - Available payload: 246 bytes
*/
static const size_t MAX_ESPNOW_PACKET_SIZE = 250;
/**
* Size constants for packet parsing
*/
static const size_t MAX_PAYLOAD_SIZE = MAX_ESPNOW_PACKET_SIZE - (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE);
/** Buffer for receiving ESP-NOW packets */
uint8_t _rx_buffer[MAX_ESPNOW_PACKET_SIZE];
/** Current position in receive buffer */
size_t _rx_buffer_pos;
/**
* Performs XOR encryption/decryption of data
* Used to isolate different mesh networks
*
* Uses _prefs->bridge_secret as the key in a simple XOR operation.
* The same operation is used for both encryption and decryption.
* While not cryptographically secure, it provides basic network isolation.
*
* @param data Pointer to data to encrypt/decrypt
* @param len Length of data in bytes
*/
void xorCrypt(uint8_t *data, size_t len);
/**
* ESP-NOW receive callback
* Called by ESP-NOW when a packet is received
*
* @param mac Source MAC address
* @param data Received data
* @param len Length of received data
*/
void onDataRecv(const uint8_t *mac, const uint8_t *data, int32_t len);
/**
* ESP-NOW send callback
* Called by ESP-NOW after a transmission attempt
*
* @param mac_addr Destination MAC address
* @param status Transmission status
*/
void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status);
public:
/**
* Constructs an ESPNowBridge instance
*
* @param prefs Node preferences for configuration settings
* @param mgr PacketManager for allocating and queuing packets
* @param rtc RTCClock for timestamping debug messages
*/
ESPNowBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc);
/**
* Initializes the ESP-NOW bridge
*
* - Configures WiFi in station mode
* - Initializes ESP-NOW protocol
* - Registers callbacks
* - Sets up broadcast peer
*/
void begin() override;
/**
* Stops the ESP-NOW bridge
*
* - Removes broadcast peer
* - Unregisters callbacks
* - Deinitializes ESP-NOW protocol
* - Turns off WiFi to release radio resources
*/
void end() override;
/**
* Main loop handler
* ESP-NOW is callback-based, so this is currently empty
*/
void loop() override;
/**
* Called when a packet is received via ESP-NOW
* Queues the packet for mesh processing if not seen before
*
* @param packet The received mesh packet
*/
void onPacketReceived(mesh::Packet *packet) override;
/**
* Called when a packet needs to be transmitted via ESP-NOW
* Encrypts and broadcasts the packet if not seen before
*
* @param packet The mesh packet to transmit
*/
void sendPacket(mesh::Packet *packet) override;
};
#endif

View File

@@ -0,0 +1,151 @@
#include "RS232Bridge.h"
#include <HardwareSerial.h>
#ifdef WITH_RS232_BRIDGE
RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
: BridgeBase(prefs, mgr, rtc), _serial(&serial) {}
void RS232Bridge::begin() {
BRIDGE_DEBUG_PRINTLN("Initializing at %d baud...\n", _prefs->bridge_baud);
#if !defined(WITH_RS232_BRIDGE_RX) || !defined(WITH_RS232_BRIDGE_TX)
#error "WITH_RS232_BRIDGE_RX and WITH_RS232_BRIDGE_TX must be defined"
#endif
#if defined(ESP32)
((HardwareSerial *)_serial)->setPins(WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
#elif defined(NRF52_PLATFORM)
((HardwareSerial *)_serial)->setPins(WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
#elif defined(RP2040_PLATFORM)
((SerialUART *)_serial)->setRX(WITH_RS232_BRIDGE_RX);
((SerialUART *)_serial)->setTX(WITH_RS232_BRIDGE_TX);
#elif defined(STM32_PLATFORM)
((HardwareSerial *)_serial)->setRx(WITH_RS232_BRIDGE_RX);
((HardwareSerial *)_serial)->setTx(WITH_RS232_BRIDGE_TX);
#else
#error RS232Bridge was not tested on the current platform
#endif
((HardwareSerial *)_serial)->begin(_prefs->bridge_baud);
// Update bridge state
_initialized = true;
}
void RS232Bridge::end() {
BRIDGE_DEBUG_PRINTLN("Stopping...\n");
((HardwareSerial *)_serial)->end();
// Update bridge state
_initialized = false;
}
void RS232Bridge::loop() {
// Guard against uninitialized state
if (_initialized == false) {
return;
}
while (_serial->available()) {
uint8_t b = _serial->read();
if (_rx_buffer_pos < 2) {
// Waiting for magic word
if ((_rx_buffer_pos == 0 && b == ((BRIDGE_PACKET_MAGIC >> 8) & 0xFF)) ||
(_rx_buffer_pos == 1 && b == (BRIDGE_PACKET_MAGIC & 0xFF))) {
_rx_buffer[_rx_buffer_pos++] = b;
} else {
// Invalid magic byte, reset and start over
_rx_buffer_pos = 0;
// Check if this byte could be the start of a new magic word
if (b == ((BRIDGE_PACKET_MAGIC >> 8) & 0xFF)) {
_rx_buffer[_rx_buffer_pos++] = b;
}
}
} else {
// Reading length, payload, and checksum
_rx_buffer[_rx_buffer_pos++] = b;
if (_rx_buffer_pos >= 4) {
uint16_t len = (_rx_buffer[2] << 8) | _rx_buffer[3];
// Validate length field
if (len > (MAX_TRANS_UNIT + 1)) {
BRIDGE_DEBUG_PRINTLN("RX invalid length %d, resetting\n", len);
_rx_buffer_pos = 0; // Invalid length, reset
continue;
}
if (_rx_buffer_pos == len + SERIAL_OVERHEAD) { // Full packet received
uint16_t received_checksum = (_rx_buffer[4 + len] << 8) | _rx_buffer[5 + len];
if (validateChecksum(_rx_buffer + 4, len, received_checksum)) {
BRIDGE_DEBUG_PRINTLN("RX, len=%d crc=0x%04x\n", len, received_checksum);
mesh::Packet *pkt = _mgr->allocNew();
if (pkt) {
if (pkt->readFrom(_rx_buffer + 4, len)) {
onPacketReceived(pkt);
} else {
BRIDGE_DEBUG_PRINTLN("RX failed to parse packet\n");
_mgr->free(pkt);
}
} else {
BRIDGE_DEBUG_PRINTLN("RX failed to allocate packet\n");
}
} else {
BRIDGE_DEBUG_PRINTLN("RX checksum mismatch, rcv=0x%04x\n", received_checksum);
}
_rx_buffer_pos = 0; // Reset for next packet
}
}
}
}
}
void RS232Bridge::sendPacket(mesh::Packet *packet) {
// Guard against uninitialized state
if (_initialized == false) {
return;
}
// First validate the packet pointer
if (!packet) {
BRIDGE_DEBUG_PRINTLN("TX invalid packet pointer\n");
return;
}
if (!_seen_packets.hasSeen(packet)) {
uint8_t buffer[MAX_SERIAL_PACKET_SIZE];
uint16_t len = packet->writeTo(buffer + 4);
// Check if packet fits within our maximum payload size
if (len > (MAX_TRANS_UNIT + 1)) {
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", len,
MAX_TRANS_UNIT + 1);
return;
}
// Build packet header
buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF; // Magic high byte
buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF; // Magic low byte
buffer[2] = (len >> 8) & 0xFF; // Length high byte
buffer[3] = len & 0xFF; // Length low byte
// Calculate checksum over the payload
uint16_t checksum = fletcher16(buffer + 4, len);
buffer[4 + len] = (checksum >> 8) & 0xFF; // Checksum high byte
buffer[5 + len] = checksum & 0xFF; // Checksum low byte
// Send complete packet
_serial->write(buffer, len + SERIAL_OVERHEAD);
BRIDGE_DEBUG_PRINTLN("TX, len=%d crc=0x%04x\n", len, checksum);
}
}
void RS232Bridge::onPacketReceived(mesh::Packet *packet) {
handleReceivedPacket(packet);
}
#endif

View File

@@ -0,0 +1,148 @@
#pragma once
#include "helpers/bridges/BridgeBase.h"
#include <Stream.h>
#ifdef WITH_RS232_BRIDGE
/**
* @brief Bridge implementation using RS232/UART protocol for packet transport
*
* This bridge enables mesh packet transport over serial/UART connections,
* allowing nodes to communicate over wired serial links. It implements a simple
* packet framing protocol with checksums for reliable transfer.
*
* Features:
* - Point-to-point communication over hardware UART
* - Fletcher-16 checksum for data integrity verification
* - Magic header for packet synchronization and frame alignment
* - Duplicate packet detection using SimpleMeshTables tracking
* - Configurable RX/TX pins via build defines
* - Fixed baud rate at 115200 for consistent timing
*
* Packet Structure:
* [2 bytes] Magic Header (0xC03E) - Used to identify start of RS232Bridge packets
* [2 bytes] Payload Length - Length of the mesh packet payload
* [n bytes] Mesh Packet Payload - The actual mesh packet data
* [2 bytes] Fletcher-16 Checksum - Calculated over the payload for integrity verification
*
* The Fletcher-16 checksum is calculated over the mesh packet payload and provides
* error detection capabilities suitable for serial communication where electrical
* noise, timing issues, or hardware problems could corrupt data. The checksum
* validation ensures only valid packets are forwarded to the mesh.
*
* Configuration:
* - Define WITH_RS232_BRIDGE to enable this bridge
* - Define WITH_RS232_BRIDGE_RX with the RX pin number
* - Define WITH_RS232_BRIDGE_TX with the TX pin number
*
* Platform Support:
* Different platforms require different pin configuration methods:
* - ESP32: Uses HardwareSerial::setPins(rx, tx)
* - NRF52: Uses HardwareSerial::setPins(rx, tx)
* - RP2040: Uses SerialUART::setRX(rx) and SerialUART::setTX(tx)
* - STM32: Uses HardwareSerial::setRx(rx) and HardwareSerial::setTx(tx)
*/
class RS232Bridge : public BridgeBase {
public:
/**
* @brief Constructs an RS232Bridge instance
*
* @param prefs Node preferences for configuration settings
* @param serial The hardware serial port to use
* @param mgr PacketManager for allocating and queuing packets
* @param rtc RTCClock for timestamping debug messages
*/
RS232Bridge(NodePrefs *prefs, Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc);
/**
* Initializes the RS232 bridge
*
* - Validates that RX/TX pins are defined
* - Configures UART pins based on target platform
* - Sets baud rate to 115200 for consistent communication
* - Platform-specific pin configuration methods are used
*/
void begin() override;
/**
* Stops the RS232 bridge
*
*/
void end() override;
/**
* @brief Main loop handler for processing incoming serial data
*
* Implements a state machine for packet reception:
* 1. Searches for magic header bytes for packet synchronization
* 2. Reads length field to determine expected packet size
* 3. Validates packet length against maximum allowed size
* 4. Receives complete packet payload and checksum
* 5. Validates Fletcher-16 checksum for data integrity
* 6. Creates mesh packet and forwards if valid
*/
void loop() override;
/**
* @brief Called when a packet needs to be transmitted over serial
*
* Formats the mesh packet with RS232 framing protocol:
* - Adds magic header for synchronization
* - Includes payload length field
* - Calculates Fletcher-16 checksum over payload
* - Transmits complete framed packet
* - Uses duplicate detection to prevent retransmission
*
* @param packet The mesh packet to transmit
*/
void sendPacket(mesh::Packet *packet) override;
/**
* @brief Called when a complete valid packet has been received from serial
*
* Forwards the received packet to the mesh for processing.
* The packet has already been validated for checksum integrity
* and parsed successfully at this point.
*
* @param packet The received mesh packet ready for processing
*/
void onPacketReceived(mesh::Packet *packet) override;
private:
/**
* RS232 Protocol Structure:
* - Magic header: 2 bytes (packet identification)
* - Length field: 2 bytes (payload length)
* - Payload: variable bytes (mesh packet data)
* - Checksum: 2 bytes (Fletcher-16 over payload)
* Total overhead: 6 bytes
*/
/**
* @brief The total overhead of the serial protocol in bytes.
* Includes: MAGIC_WORD (2) + LENGTH (2) + CHECKSUM (2) = 6 bytes
*/
static constexpr uint16_t SERIAL_OVERHEAD = BRIDGE_MAGIC_SIZE + BRIDGE_LENGTH_SIZE + BRIDGE_CHECKSUM_SIZE;
/**
* @brief The maximum size of a complete packet on the serial line.
*
* This is calculated as the sum of:
* - MAX_TRANS_UNIT + 1 for the maximum mesh packet size
* - SERIAL_OVERHEAD for the framing (magic + length + checksum)
*/
static constexpr uint16_t MAX_SERIAL_PACKET_SIZE = (MAX_TRANS_UNIT + 1) + SERIAL_OVERHEAD;
/** Hardware serial port interface */
Stream *_serial;
/** Buffer for building received packets */
uint8_t _rx_buffer[MAX_SERIAL_PACKET_SIZE];
/** Current position in the receive buffer */
uint16_t _rx_buffer_pos = 0;
};
#endif

View File

@@ -66,7 +66,7 @@ bool SerialBLEInterface::onSecurityRequest() {
void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
if (cmpl.success) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success");
//deviceConnected = true;
deviceConnected = true;
} else {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure*");
@@ -83,12 +83,11 @@ void SerialBLEInterface::onConnect(BLEServer* pServer) {
void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", param->connect.conn_id, pServer->getPeerMTU(param->connect.conn_id));
last_conn_id = param->connect.conn_id;
}
void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) {
BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", pServer->getPeerMTU(param->mtu.conn_id));
deviceConnected = true;
}
void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
@@ -143,6 +142,7 @@ void SerialBLEInterface::disable() {
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
pServer->getAdvertising()->stop();
pServer->disconnect(last_conn_id);
pService->stop();
oldDeviceConnected = deviceConnected = false;
adv_restart_time = 0;

View File

@@ -13,6 +13,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
bool deviceConnected;
bool oldDeviceConnected;
bool _isEnabled;
uint16_t last_conn_id;
uint32_t _pin_code;
unsigned long _last_write;
unsigned long adv_restart_time;
@@ -56,6 +57,7 @@ public:
adv_restart_time = 0;
_isEnabled = false;
_last_write = 0;
last_conn_id = 0;
send_queue_len = recv_queue_len = 0;
}

View File

@@ -44,7 +44,18 @@ bool SerialWifiInterface::isWriteBusy() const {
}
size_t SerialWifiInterface::checkRecvFrame(uint8_t dest[]) {
if (!client) client = server.available();
// check if new client connected
auto newClient = server.available();
if (newClient) {
// disconnect existing client
deviceConnected = false;
client.stop();
// switch active connection to new client
client = newClient;
}
if (client.connected()) {
if (!deviceConnected) {

View File

@@ -1,23 +1,70 @@
#include "SerialBLEInterface.h"
static SerialBLEInterface* instance;
void SerialBLEInterface::onConnect(uint16_t connection_handle) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: connected");
// we now set _isDeviceConnected=true in onSecured callback instead
}
void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected reason=%d", reason);
if(instance){
instance->_isDeviceConnected = false;
instance->startAdv();
}
}
void SerialBLEInterface::onSecured(uint16_t connection_handle) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: onSecured");
if(instance){
instance->_isDeviceConnected = true;
// no need to stop advertising on connect, as the ble stack does this automatically
}
}
void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
instance = this;
char charpin[20];
sprintf(charpin, "%d", pin_code);
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.configPrphConn(250, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); // increase MTU
Bluefruit.setTxPower(BLE_TX_POWER);
Bluefruit.begin();
Bluefruit.setTxPower(4); // Check bluefruit.h for supported values
Bluefruit.setName(device_name);
Bluefruit.Security.setMITM(true);
Bluefruit.Security.setPIN(charpin);
Bluefruit.Periph.setConnectCallback(onConnect);
Bluefruit.Periph.setDisconnectCallback(onDisconnect);
Bluefruit.Security.setSecuredCallback(onSecured);
// To be consistent OTA DFU should be added first if it exists
//bledfu.begin();
// Configure and start the BLE Uart service
bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
bleuart.begin();
}
void SerialBLEInterface::startAdv() {
BLE_DEBUG_PRINTLN("SerialBLEInterface: starting advertising");
// clean restart if already advertising
if(Bluefruit.Advertising.isRunning()){
BLE_DEBUG_PRINTLN("SerialBLEInterface: already advertising, stopping to allow clean restart");
Bluefruit.Advertising.stop();
}
Bluefruit.Advertising.clearData(); // clear advertising data
Bluefruit.ScanResponse.clearData(); // clear scan response data
// Advertising packet
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
@@ -38,10 +85,25 @@ void SerialBLEInterface::startAdv() {
* 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.restartOnDisconnect(false); // don't restart automatically as we handle it in onDisconnect
Bluefruit.Advertising.setInterval(32, 244);
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
}
void SerialBLEInterface::stopAdv() {
BLE_DEBUG_PRINTLN("SerialBLEInterface: stopping advertising");
// we only want to stop advertising if it's running, otherwise an invalid state error is logged by ble stack
if(!Bluefruit.Advertising.isRunning()){
return;
}
// stop advertising
Bluefruit.Advertising.stop();
}
// ---------- public methods
@@ -52,25 +114,28 @@ void SerialBLEInterface::enable() {
_isEnabled = true;
clearBuffers();
// Configure and start the BLE Uart service
bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
bleuart.begin();
// Start advertising
startAdv();
checkAdvRestart = false;
}
void SerialBLEInterface::disable() {
_isEnabled = false;
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
Bluefruit.Advertising.stop();
#ifdef RAK_BOARD
Bluefruit.disconnect(Bluefruit.connHandle());
#else
uint16_t conn_id;
if (Bluefruit.getConnectedHandles(&conn_id, 1) > 0) {
Bluefruit.disconnect(conn_id);
}
#endif
oldDeviceConnected = deviceConnected = false;
checkAdvRestart = false;
Bluefruit.Advertising.restartOnDisconnect(false);
Bluefruit.Advertising.stop();
Bluefruit.Advertising.clearData();
stopAdv();
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
@@ -79,7 +144,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
return 0;
}
if (deviceConnected && len > 0) {
if (_isDeviceConnected && len > 0) {
if (send_queue_len >= FRAME_QUEUE_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
@@ -115,44 +180,14 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
} else {
int len = bleuart.available();
if (len > 0) {
deviceConnected = true; // should probably use the callback to monitor cx
bleuart.readBytes(dest, len);
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]);
return len;
}
}
if (Bluefruit.connected() == 0) deviceConnected = false;
if (deviceConnected != oldDeviceConnected) {
if (!deviceConnected) { // disconnecting
clearBuffers();
BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting...");
delay(500); // give the bluetooth stack the chance to get things ready
checkAdvRestart = true;
} else {
BLE_DEBUG_PRINTLN("SerialBLEInterface -> stopping advertising");
BLE_DEBUG_PRINTLN("SerialBLEInterface -> connecting...");
// connecting
// do stuff here on connecting
Bluefruit.Advertising.stop();
checkAdvRestart = false;
}
oldDeviceConnected = deviceConnected;
}
if (checkAdvRestart) {
if (Bluefruit.connected() == 0) {
BLE_DEBUG_PRINTLN("SerialBLEInterface -> re-starting advertising");
startAdv();
}
checkAdvRestart = false;
}
return 0;
}
bool SerialBLEInterface::isConnected() const {
return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0;
return _isDeviceConnected;
}

View File

@@ -3,12 +3,14 @@
#include "../BaseSerialInterface.h"
#include <bluefruit.h>
#ifndef BLE_TX_POWER
#define BLE_TX_POWER 4
#endif
class SerialBLEInterface : public BaseSerialInterface {
BLEUart bleuart;
bool deviceConnected;
bool oldDeviceConnected;
bool checkAdvRestart;
bool _isEnabled;
bool _isDeviceConnected;
unsigned long _last_write;
struct Frame {
@@ -21,18 +23,20 @@ class SerialBLEInterface : public BaseSerialInterface {
Frame send_queue[FRAME_QUEUE_SIZE];
void clearBuffers() { send_queue_len = 0; }
void startAdv();
static void onConnect(uint16_t connection_handle);
static void onDisconnect(uint16_t connection_handle, uint8_t reason);
static void onSecured(uint16_t connection_handle);
public:
SerialBLEInterface() {
deviceConnected = false;
oldDeviceConnected = false;
checkAdvRestart = false;
_isEnabled = false;
_isDeviceConnected = false;
_last_write = 0;
send_queue_len = 0;
}
void startAdv();
void stopAdv();
void begin(const char* device_name, uint32_t pin_code);
// BaseSerialInterface methods

View File

@@ -0,0 +1,28 @@
#pragma once
#include <RadioLib.h>
#include "MeshCore.h"
class CustomLR1110 : public LR1110 {
public:
CustomLR1110(Module *mod) : LR1110(mod) { }
size_t getPacketLength(bool update) override {
size_t len = LR1110::getPacketLength(update);
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
// we've just recieved a corrupted packet
// this may have triggered a bug causing subsequent packets to be shifted
// call standby() to return radio to known-good state
// recvRaw will call startReceive() to restart rx
MESH_DEBUG_PRINTLN("LR1110: got header err, calling standby()");
standby();
}
return len;
}
bool isReceiving() {
uint16_t irq = getIrqStatus();
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));
return detected;
}
};

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