Compare commits

..

452 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
Florent
76711f54ce techo: let location_manager set clock 2025-08-31 21:45:47 +02:00
kelsey hudson
4b508136b4 Merge remote-tracking branch 'upstream/dev' into dev 2025-08-31 11:30:59 -07:00
Florent
fae3c284d3 techo: use EnvironmentSensor to get BME280 data 2025-08-31 18:09:05 +02:00
kelsey hudson
65be15e6be Merge remote-tracking branch 'upstream/dev' into dev 2025-08-30 17:54:42 -07:00
kelsey hudson
1b0999fc7e Merge remote-tracking branch 'upstream/dev' into dev 2025-08-27 00:39:35 -07:00
kelsey hudson
2e2e677b0a Ikoka Stick: Board IDs, LED behavior
Updates the manufacturer identifier with the EBYTE module. Makes the LED
behave properly. Turns the bright blue LED off after the first time you
transmit anything via LoRa.
2025-08-27 00:37:16 -07:00
João Brázio
7f142245e6 Merge remote-tracking branch 'origin/dev' into jbrazio/2025_3f11ad35 2025-08-22 23:00:35 +01:00
João Brázio
85273a6dc6 Merge remote-tracking branch 'origin/dev' into jbrazio/2025_3f11ad35 2025-07-29 00:31:52 +01:00
João Brázio
04042e3ca0 Refactor serial bridge handling 2025-07-09 11:03:35 +01:00
João Brázio
97b51900f8 More robust handling of pkt len 2025-07-08 21:45:49 +01:00
João Brázio
92ee1820c4 Add null check for packet allocation and clean up Dispatcher 2025-07-08 16:02:10 +01:00
João Brázio
ac056fb0b9 Remove serial bridge implementation and implement simplified version directly in the repeater source code. 2025-07-08 14:04:21 +01:00
João Brázio
3375389181 Merge remote-tracking branch 'upstream/dev' into jbrazio/2025_3f11ad35 2025-07-04 11:57:09 +01:00
João Brázio
2f77cef04b Add config flags to variants 2025-06-29 16:28:11 +01:00
João Brázio
4b70ee863d Serial bridge implementation 2025-06-27 20:16:14 +01:00
300 changed files with 15634 additions and 4302 deletions

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.
@@ -90,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 [MeshCore Discord](https://discord.gg/BMwCtwHj5V) to chat with the developers and get help from the community.
## RAK Wireless Board Support in PlatformIO
Before building/flashing the RAK4631 targets in this project, there is, unfortunately, some patching you have to do to your platformIO packages to make it work. There is a guide here on the process:
[RAK Wireless: How to Perform Installation of Board Support Package in PlatformIO](https://learn.rakwireless.com/hc/en-us/articles/26687276346775-How-To-Perform-Installation-of-Board-Support-Package-in-PlatformIO)
After building, you will need to convert the output firmware.hex file into a .uf2 file you can copy over to your RAK4631 device (after doing a full erase) by using the command `uf2conv.py -f 0xADA52840 -c firmware.hex` with the python script available from:
[GitHub: Microsoft - uf2](https://github.com/Microsoft/uf2/blob/master/utils/uf2conv.py)

View File

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

View File

@@ -0,0 +1,53 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"]
],
"usb_product": "HT-n5262",
"mcu": "nrf52840",
"variant": "heltec_mesh_pocket",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Heltec nrf (Adafruit BSP)",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://heltec.org/project/meshpocket/",
"vendor": "Heltec"
}

View File

@@ -0,0 +1,40 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_8MB.csv"
},
"core": "esp32",
"extra_flags": [
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_tracker_v2"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "heltec_tracker v2",
"upload": {
"flash_size": "8MB",
"maximum_ram_size": 327680,
"maximum_size": 8388608,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "heltec"
}

43
boards/heltec_v4.json Normal file
View File

@@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_qspi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "qspi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_v4"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "heltec_wifi_lora_32 v4 (16 MB FLASH, 2 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 2097152,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "heltec"
}

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

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

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

@@ -0,0 +1,38 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "LilyGo T-Deck (16M Flash 8M PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.lilygo.cc",
"vendor": "LilyGo"
}

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,12 +1,45 @@
#!/usr/bin/env bash
# usage
# sh build.sh build-firmware RAK_4631_Repeater
# sh build.sh build-firmwares
# sh build.sh build-matching-firmwares RAK_4631
# sh build.sh build-companion-firmwares
# sh build.sh build-repeater-firmwares
# sh build.sh build-room-server-firmwares
global_usage() {
cat - <<EOF
Usage:
sh build.sh <command> [target]
Commands:
help|usage|-h|--help: Shows this message.
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() {
@@ -24,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() {
@@ -47,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
@@ -60,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
@@ -85,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
@@ -96,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"
}
@@ -115,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"
}
@@ -127,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"
}
@@ -143,8 +195,11 @@ mkdir -p out
# handle script args
if [[ $1 == "build-firmware" ]]; then
if [ "$2" ]; then
build_firmware $2
TARGETS=${@:2}
if [ "$TARGETS" ]; then
for env in $TARGETS; do
build_firmware $env
done
else
echo "usage: $0 build-firmware <target>"
exit 1

31
create-uf2.py Normal file
View File

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

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/meshcore-dev/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)
@@ -61,21 +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.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code)
- [7.6. Q: How do I connect to the comnpanion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-comnpanion-via-wifi-eg-using-a-heltec-v3)
- [7.6. Q: How do I connect to the companion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-companion-via-wifi-eg-using-a-heltec-v3)
## 1. Introduction
@@ -90,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.
@@ -101,10 +99,10 @@ 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).
@@ -113,7 +111,7 @@ MeshCore is available on a variety of 433MHz, 868MHz and 915MHz LoRa devices. Fo
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.
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:
@@ -121,30 +119,30 @@ 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 or iOS MeshCore client over BLE
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.
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.
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.
@@ -159,7 +157,7 @@ The recommendation is to run repeater and room server on separate devices for th
If you have two supported devices, and there are not many MeshCore users near you, flash both to BLE Companion firmware so you can use your devices to communicate with your near-by friends and family.
If you have two supported devices, and there are other MeshcCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.
If you have two supported devices, and there are other MeshCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.
After you flashed the latest firmware onto your repeater device, keep the device connected to your computer via USB serial, use the console feature on the web flasher and set the frequency for your region or country, so your client can remote administer the repeater or room server over RF:
@@ -167,37 +165,32 @@ After you flashed the latest firmware onto your repeater device, keep the device
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://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".
@@ -213,7 +206,7 @@ As of Aug 20 2025, a pending PR on github will change the flood advert to 12 hou
### 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.
---
@@ -223,15 +216,14 @@ As of Aug 20 2025, a pending PR on github will change the flood advert to 12 hou
### 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.
@@ -240,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>`
@@ -270,14 +262,14 @@ You can get the latitude and longitude from Google Maps by right-clicking the lo
**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
**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.3. Q: Why is my T-Deck Plus not getting any satellite lock?
@@ -294,8 +286,8 @@ GPS on T-Deck is always enabled. You can skip the "GPS clock sync" and the T-De
**A:** Users have had no issues using 16GB or 32GB SD cards. Format the SD card to **FAT32**.
### 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
**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`
@@ -305,24 +297,24 @@ The smartphone app key is in hex:
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1354194409213792388)
### 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)
**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.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.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.
**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.10. Q: How to decipher the diagnostics screen on T-Deck?
@@ -330,17 +322,17 @@ Unlock page: <https://buymeacoffee.com/ripplebiz/e/249834>
**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:
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)
@@ -370,14 +362,30 @@ https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19
### 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)
@@ -385,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?
@@ -411,14 +419,14 @@ 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/meshcore-dev/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 [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>.
@@ -428,7 +436,7 @@ Support Liam Cottle's smartphone client development by unlocking the server admi
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:
@@ -448,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`
@@ -458,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
@@ -472,17 +480,17 @@ 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:**
**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`
@@ -501,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.
@@ -520,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:**
@@ -541,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:
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
@@ -569,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.`
@@ -638,16 +654,16 @@ Allow the browser user on it:
1. Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for `nrf dfu`, the app's full name is `nRF Device Firmware Update`
2. On flasher.meshcore.co.uk, download the **ZIP** version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao)
3. From the MeshCore app, login remotely to the repeater you want to update with admin priviledge
3. From the MeshCore app, login remotely to the repeater you want to update with admin privilege
4. Go to the Command Line tab, type `start ota` and hit enter.
5. you should see `OK` to confirm the repeater device is now in OTA mode
6. Run the DFU app,tab `Settings` on the top right corner
7. Enable `Packets receipt notifications`, and change `Number of Packets` to 10 for RAK, 8 for T114. 8 also works for RAK.
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.
@@ -655,17 +671,17 @@ Allow the browser user on it:
**A:** For ESP32-based devices (e.g. Heltec V3):
1. On flasher.meshcore.co.uk, download the **non-merged** version of the firmware for your ESP32 device (e.g. `Heltec_v3_repeater-v1.6.2-4449fd3.bin`, no `"merged"` in the file name)
2. From the MeshCore app, login remotely to the repeater you want to update with admin priviledge
2. From the MeshCore app, login remotely to the repeater you want to update with admin privilege
4. Go to the Command Line tab, type `start ota` and hit enter.
5. you should see `OK` to confirm the repeater device is now in OTA mode
6. The command `start ota` on an ESP32-based device starts a wifi hotspot named `MeshCore OTA`
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)?
**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.
@@ -677,7 +693,7 @@ Currently, the following boards are supported:
### 7.4. Q: are the MeshCore logo and font available?
**A:** Yes, it is on the MeshCore github repo here:
**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?
@@ -695,8 +711,8 @@ where `&type` is:
`room = 3`
`sensor = 4`
### 7.6. Q: How do I connect to the comnpanion via WIFI, e.g. using a heltec v3?
**A:**
### 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

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

View File

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

View File

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

View File

@@ -1,7 +1,13 @@
#include <Arduino.h>
#include "DataStore.h"
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _clock(&clock),
#if defined(EXTRAFS) || defined(QSPIFLASH)
#define MAX_BLOBRECS 100
#else
#define MAX_BLOBRECS 20
#endif
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
identity_store(fs, "")
#elif defined(RP2040_PLATFORM)
@@ -12,24 +18,45 @@ DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _clock(&
{
}
static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(EXTRAFS) || defined(QSPIFLASH)
DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
return _fs->open(filename, FILE_O_WRITE);
identity_store(fs, "")
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "w");
identity_store(fs, "/identity")
#else
return _fs->open(filename, "w", true);
identity_store(fs, "/identity")
#endif
{
}
#endif
static File openWrite(FILESYSTEM* fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
fs->remove(filename);
return fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return fs->open(filename, "w");
#else
return fs->open(filename, "w", true);
#endif
}
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
static uint32_t _ContactsChannelsTotalBlocks = 0;
#endif
void DataStore::begin() {
#if defined(RP2040_PLATFORM)
identity_store.begin();
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_ContactsChannelsTotalBlocks = _getContactsChannelsFS()->_getFS()->cfg->block_count;
checkAdvBlobFile();
#if defined(EXTRAFS) || defined(QSPIFLASH)
migrateToSecondaryFS();
#endif
#else
// init 'blob store' support
_fs->mkdir("/bl");
@@ -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];
@@ -203,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) {
@@ -236,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;
@@ -269,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;
@@ -296,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;
@@ -331,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();
@@ -344,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)) {
@@ -364,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;
@@ -411,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

@@ -50,6 +50,8 @@
#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
@@ -99,6 +101,7 @@
#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
@@ -175,15 +178,34 @@ void MyMesh::updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, co
}
}
bool MyMesh::Frame::isChannelMsg() const {
return buf[0] == RESP_CODE_CHANNEL_MSG_RECV || buf[0] == RESP_CODE_CHANNEL_MSG_RECV_V3;
}
void MyMesh::addToOfflineQueue(const uint8_t frame[], int len) {
if (offline_queue_len >= OFFLINE_QUEUE_SIZE) {
MESH_DEBUG_PRINTLN("ERROR: offline_queue is full!");
MESH_DEBUG_PRINTLN("WARN: offline_queue is full!");
int pos = 0;
while (pos < offline_queue_len) {
if (offline_queue[pos].isChannelMsg()) {
for (int i = pos; i < offline_queue_len - 1; i++) { // delete oldest channel msg from queue
offline_queue[i] = offline_queue[i + 1];
}
MESH_DEBUG_PRINTLN("INFO: removed oldest channel message from queue.");
offline_queue[offline_queue_len - 1].len = len;
memcpy(offline_queue[offline_queue_len - 1].buf, frame, len);
return;
}
pos++;
}
MESH_DEBUG_PRINTLN("INFO: no channel messages to remove from queue.");
} else {
offline_queue[offline_queue_len].len = len;
memcpy(offline_queue[offline_queue_len].buf, frame, len);
offline_queue_len++;
}
}
int MyMesh::getFromOfflineQueue(uint8_t frame[]) {
if (offline_queue_len > 0) { // check offline queue
size_t len = offline_queue[0].len; // take from top of queue
@@ -243,7 +265,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
}
} else {
#ifdef DISPLAY_CLASS
if (_ui) _ui->soundBuzzer(UIEventType::newContactMessage);
if (_ui) _ui->notify(UIEventType::newContactMessage);
#endif
}
@@ -294,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
@@ -306,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);
@@ -353,12 +375,41 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
if (should_display && _ui) {
_ui->newMsg(path_len, from.name, text, offline_queue_len);
if (!_serial->isConnected()) {
_ui->soundBuzzer(UIEventType::contactMessage);
_ui->notify(UIEventType::contactMessage);
}
}
#endif
}
bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
// REVISIT: try to determine which Region (from transport_codes[1]) that Sender is indicating for replies/responses
// if unknown, fallback to finding Region from transport_codes[0], the 'scope' used by Sender
return false;
}
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
@@ -412,7 +463,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
_serial->writeFrame(frame, 1);
} else {
#ifdef DISPLAY_CLASS
if (_ui) _ui->soundBuzzer(UIEventType::channelMessage);
if (_ui) _ui->notify(UIEventType::channelMessage);
#endif
}
#ifdef DISPLAY_CLASS
@@ -496,6 +547,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
memcpy(&out_frame[i], &tag, 4);
i += 4; // NEW: include server timestamp
out_frame[i++] = data[7]; // NEW (v7): ACL permissions
out_frame[i++] = data[12]; // FIRMWARE_VER_LEVEL
} else {
out_frame[i++] = PUSH_CODE_LOGIN_FAIL;
out_frame[i++] = 0; // reserved
@@ -576,6 +628,26 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len);
}
void MyMesh::onControlDataRecv(mesh::Packet *packet) {
if (packet->payload_len + 4 > sizeof(out_frame)) {
MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len);
return;
}
int i = 0;
out_frame[i++] = PUSH_CODE_CONTROL_DATA;
out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4);
out_frame[i++] = (int8_t)(_radio->getLastRSSI());
out_frame[i++] = packet->path_len;
memcpy(&out_frame[i], packet->payload, packet->payload_len);
i += packet->payload_len;
if (_serial->isConnected()) {
_serial->writeFrame(out_frame, i);
} else {
MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline");
}
}
void MyMesh::onRawDataRecv(mesh::Packet *packet) {
if (packet->payload_len + 4 > sizeof(out_frame)) {
MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len);
@@ -643,6 +715,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
sign_data = NULL;
dirty_contacts_expiry = 0;
memset(advert_paths, 0, sizeof(advert_paths));
memset(send_scope.key, 0, sizeof(send_scope.key));
// defaults
memset(&_prefs, 0, sizeof(_prefs));
@@ -686,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);
@@ -825,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;
}
@@ -1464,6 +1538,21 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
} else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE && len >= 2 && cmd_frame[1] == 0) {
if (len >= 2 + 16) {
memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set curr scope TransportKey
} else {
memset(send_scope.key, 0, sizeof(send_scope.key)); // set scope to null
}
writeOKFrame();
} else if (cmd_frame[0] == CMD_SEND_CONTROL_DATA && len >= 2 && (cmd_frame[1] & 0x80) != 0) {
auto resp = createControlData(&cmd_frame[1], len - 1);
if (resp) {
sendZeroHop(resp);
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);
@@ -1524,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
@@ -1567,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 {
@@ -1618,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
}
}

View File

@@ -5,14 +5,14 @@
#include "AbstractUITask.h"
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 7
#define FIRMWARE_VER_CODE 8
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "1 Sep 2025"
#define FIRMWARE_BUILD_DATE "13 Nov 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.8.1"
#define FIRMWARE_VERSION "v1.10.0"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -68,6 +68,7 @@
#endif
#include <helpers/BaseChatMesh.h>
#include <helpers/TransportKeyStore.h>
/* -------------------------------------------------------------------------------------- */
@@ -106,13 +107,17 @@ protected:
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);
@@ -128,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;
@@ -191,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;
@@ -198,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];
@@ -205,6 +215,7 @@ private:
struct AckTableEntry {
unsigned long msg_sent;
uint32_t ack;
ContactInfo* contact;
};
#define EXPECTED_ACK_TABLE_SIZE 8
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table

View File

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

View File

@@ -3,7 +3,9 @@
#include "../MyMesh.h"
#include "target.h"
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#ifndef AUTO_OFF_MILLIS
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#endif
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
#ifdef PIN_STATUS_LED
@@ -18,7 +20,11 @@
#define UI_RECENT_LIST_SIZE 4
#endif
#define PRESS_LABEL "long press"
#if UI_HAS_JOYSTICK
#define PRESS_LABEL "press Enter"
#else
#define PRESS_LABEL "long press"
#endif
#include "icons.h"
@@ -73,6 +79,12 @@ class HomeScreen : public UIScreen {
RADIO,
BLUETOOTH,
ADVERT,
#if ENV_INCLUDE_GPS == 1
GPS,
#endif
#if UI_SENSORS_PAGE == 1
SENSORS,
#endif
SHUTDOWN,
Count // keep as last
};
@@ -85,6 +97,7 @@ class HomeScreen : public UIScreen {
bool _shutdown_init;
AdvertPath recent[UI_RECENT_LIST_SIZE];
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
// Convert millivolts to percentage
const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
@@ -111,9 +124,37 @@ class HomeScreen : public UIScreen {
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) { }
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
_shutdown_init(false), sensors_lpp(200) { }
void poll() override {
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
@@ -124,17 +165,19 @@ public:
int render(DisplayDriver& display) override {
char tmp[80];
// node name
display.setCursor(0, 0);
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
display.print(_node_prefs->node_name);
char filtered_name[sizeof(_node_prefs->node_name)];
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
display.setCursor(0, 0);
display.print(filtered_name);
// battery voltage
renderBatteryIndicator(display, _task->getBattMilliVolts());
// curr page indicator
int y = 14;
int x = display.width() / 2 - 25;
int x = display.width() / 2 - 5 * (HomePage::Count-1);
for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) {
if (i == _page) {
display.fillRect(x-1, y-1, 3, 3);
@@ -166,8 +209,6 @@ public:
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) {
auto a = &recent[i];
if (a->name[0] == 0) continue; // empty slot
display.setCursor(0, y);
display.print(a->name);
int secs = _rtc->getCurrentTime() - a->recv_timestamp;
if (secs < 60) {
sprintf(tmp, "%ds", secs);
@@ -176,7 +217,14 @@ public:
} else {
sprintf(tmp, "%dh", secs / (60*60));
}
display.setCursor(display.width() - display.getTextWidth(tmp) - 1, y);
int timestamp_width = display.getTextWidth(tmp);
int max_name_width = display.width() - timestamp_width - 1;
char filtered_recent_name[sizeof(a->name)];
display.translateUTF8ToBlocks(filtered_recent_name, a->name, sizeof(filtered_recent_name));
display.drawTextEllipsized(0, y, max_name_width, filtered_recent_name);
display.setCursor(display.width() - timestamp_width - 1, y);
display.print(tmp);
}
} else if (_page == HomePage::RADIO) {
@@ -200,8 +248,8 @@ public:
display.print(tmp);
} else if (_page == HomePage::BLUETOOTH) {
display.setColor(DisplayDriver::GREEN);
display.drawXbm((display.width() - 32) / 2, 18,
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off,
display.drawXbm((display.width() - 32) / 2, 18,
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off,
32, 32);
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL);
@@ -209,6 +257,106 @@ public:
display.setColor(DisplayDriver::GREEN);
display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL);
#if ENV_INCLUDE_GPS == 1
} else if (_page == HomePage::GPS) {
LocationProvider* nmea = sensors.getLocationProvider();
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);
@@ -216,18 +364,18 @@ public:
display.drawTextCentered(display.width() / 2, 34, "hibernating...");
} else {
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate: " PRESS_LABEL);
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
}
}
return 5000; // next render after 5000 ms
}
bool handleInput(char c) override {
if (c == KEY_LEFT) {
if (c == KEY_LEFT || c == KEY_PREV) {
_page = (_page + HomePage::Count - 1) % HomePage::Count;
return true;
}
if (c == KEY_RIGHT || c == KEY_SELECT) {
if (c == KEY_NEXT || c == KEY_RIGHT) {
_page = (_page + 1) % HomePage::Count;
if (_page == HomePage::RECENT) {
_task->showAlert("Recent adverts", 800);
@@ -243,9 +391,7 @@ public:
return true;
}
if (c == KEY_ENTER && _page == HomePage::ADVERT) {
#ifdef PIN_BUZZER
_task->soundBuzzer(UIEventType::ack);
#endif
_task->notify(UIEventType::ack);
if (the_mesh.advert()) {
_task->showAlert("Advert sent!", 1000);
} else {
@@ -253,6 +399,19 @@ public:
}
return true;
}
#if ENV_INCLUDE_GPS == 1
if (c == KEY_ENTER && _page == HomePage::GPS) {
_task->toggleGPS();
return true;
}
#endif
#if UI_SENSORS_PAGE == 1
if (c == KEY_ENTER && _page == HomePage::SENSORS) {
_task->toggleGPS();
next_sensors_refresh=0;
return true;
}
#endif
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
_shutdown_init = true; // need to wait for button to be released
return true;
@@ -315,17 +474,25 @@ public:
display.setCursor(0, 14);
display.setColor(DisplayDriver::YELLOW);
display.print(p->origin);
char filtered_origin[sizeof(p->origin)];
display.translateUTF8ToBlocks(filtered_origin, p->origin, sizeof(filtered_origin));
display.print(filtered_origin);
display.setCursor(0, 25);
display.setColor(DisplayDriver::LIGHT);
display.printWordWrap(p->msg, display.width());
char filtered_msg[sizeof(p->msg)];
display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg));
display.printWordWrap(filtered_msg, display.width());
#if AUTO_OFF_MILLIS==0 // probably e-ink
return 10000; // 10 s
#else
return 1000; // next render after 1000 ms
#endif
}
bool handleInput(char c) override {
if (c == KEY_SELECT || c == KEY_RIGHT) {
if (c == KEY_NEXT || c == KEY_RIGHT) {
num_unread--;
if (num_unread == 0) {
_task->gotoHomeScreen();
@@ -367,6 +534,10 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
buzzer.begin();
#endif
#ifdef PIN_VIBRATION
vibration.begin();
#endif
ui_started_at = millis();
_alert_expiry = 0;
@@ -381,9 +552,9 @@ void UITask::showAlert(const char* text, int duration_millis) {
_alert_expiry = millis() + duration_millis;
}
void UITask::soundBuzzer(UIEventType bet) {
void UITask::notify(UIEventType t) {
#if defined(PIN_BUZZER)
switch(bet){
switch(t){
case UIEventType::contactMessage:
// gemini's pick
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7");
@@ -401,8 +572,16 @@ switch(bet){
break;
}
#endif
#ifdef PIN_VIBRATION
// Trigger vibration for all UI events except none
if (t != UIEventType::none) {
vibration.trigger();
}
#endif
}
void UITask::msgRead(int msgcount) {
_msgcount = msgcount;
if (msgcount == 0) {
@@ -419,42 +598,38 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
if (_display != NULL) {
if (!_display->isOn()) _display->turnOn();
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
_next_refresh = 0; // trigger refresh
_next_refresh = 100; // trigger refresh
}
}
void UITask::userLedHandler() {
#ifdef PIN_STATUS_LED
static int state = 0;
static int next_change = 0;
static int last_increment = 0;
int cur_time = millis();
if (cur_time > next_change) {
if (state == 0) {
state = 1;
if (cur_time > next_led_change) {
if (led_state == 0) {
led_state = 1;
if (_msgcount > 0) {
last_increment = LED_ON_MSG_MILLIS;
last_led_increment = LED_ON_MSG_MILLIS;
} else {
last_increment = LED_ON_MILLIS;
last_led_increment = LED_ON_MILLIS;
}
next_change = cur_time + last_increment;
next_led_change = cur_time + last_led_increment;
} else {
state = 0;
next_change = cur_time + LED_CYCLE_MILLIS - last_increment;
led_state = 0;
next_led_change = cur_time + LED_CYCLE_MILLIS - last_led_increment;
}
digitalWrite(PIN_STATUS_LED, state);
digitalWrite(PIN_STATUS_LED, led_state);
}
#endif
}
void UITask::setCurrScreen(UIScreen* c) {
curr = c;
_next_refresh = 0;
_next_refresh = 100;
}
/*
hardware-agnostic pre-shutdown activity should be done here
/*
hardware-agnostic pre-shutdown activity should be done here
*/
void UITask::shutdown(bool restart){
@@ -489,15 +664,13 @@ bool UITask::isButtonPressed() const {
void UITask::loop() {
char c = 0;
#if defined(PIN_USER_BTN)
#if UI_HAS_JOYSTICK
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_SELECT);
c = checkDisplayOn(KEY_ENTER);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
c = handleLongPress(KEY_ENTER); // REVISIT: could be mapped to different key code
}
#endif
#if defined(WIO_TRACKER_L1)
ev = joystick_left.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_LEFT);
@@ -510,20 +683,49 @@ void UITask::loop() {
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_RIGHT);
}
#endif
#if defined(PIN_USER_BTN_ANA)
ev = analog_btn.check();
ev = back_btn.check();
if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
#elif defined(PIN_USER_BTN)
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_SELECT);
c = checkDisplayOn(KEY_NEXT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
c = handleDoubleClick(KEY_PREV);
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
#endif
#if defined(PIN_USER_BTN_ANA)
if (abs(millis() - _analogue_pin_read_millis) > 10) {
ev = analog_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_NEXT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
c = handleDoubleClick(KEY_PREV);
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
}
_analogue_pin_read_millis = millis();
}
#endif
#if defined(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 = 0; // trigger refresh
_next_refresh = 100; // trigger refresh
}
userLedHandler();
@@ -553,11 +755,17 @@ void UITask::loop() {
}
_display->endFrame();
}
#if AUTO_OFF_MILLIS > 0
if (millis() > _auto_off) {
_display->turnOff();
}
#endif
}
#ifdef PIN_VIBRATION
vibration.loop();
#endif
#ifdef AUTO_SHUTDOWN_MILLIVOLTS
if (millis() > next_batt_chck) {
uint16_t milliVolts = getBattMilliVolts();
@@ -565,7 +773,7 @@ void UITask::loop() {
// show low battery shutdown alert
// we should only do this for eink displays, which will persist after power loss
#ifdef THINKNODE_M1
#if defined(THINKNODE_M1) || defined(LILYGO_TECHO)
if (_display != NULL) {
_display->startFrame();
_display->setTextSize(2);
@@ -604,20 +812,65 @@ char UITask::handleLongPress(char c) {
return c;
}
/*
void UITask::handleButtonTriplePress() {
MESH_DEBUG_PRINTLN("UITask: triple press triggered");
// Toggle buzzer quiet mode
char UITask::handleDoubleClick(char c) {
MESH_DEBUG_PRINTLN("UITask: double click triggered");
checkDisplayOn(c);
return c;
}
char UITask::handleTripleClick(char c) {
MESH_DEBUG_PRINTLN("UITask: triple click triggered");
checkDisplayOn(c);
toggleBuzzer();
c = 0;
return c;
}
bool UITask::getGPSState() {
if (_sensors != NULL) {
int num = _sensors->getNumSettings();
for (int i = 0; i < num; i++) {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
return !strcmp(_sensors->getSettingValue(i), "1");
}
}
}
return false;
}
void UITask::toggleGPS() {
if (_sensors != NULL) {
// toggle GPS on/off
int num = _sensors->getNumSettings();
for (int i = 0; i < num; i++) {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
_sensors->setSettingValue("gps", "0");
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);
soundBuzzer(UIEventType::ack);
showAlert("Buzzer: ON", 600);
notify(UIEventType::ack);
showAlert("Buzzer: ON", 800);
} else {
buzzer.quiet(true);
showAlert("Buzzer: OFF", 600);
showAlert("Buzzer: OFF", 800);
}
_next_refresh = 0; // trigger refresh
#endif
}
*/

View File

@@ -6,10 +6,14 @@
#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"
@@ -19,6 +23,9 @@ class UITask : public AbstractUITask {
SensorManager* _sensors;
#ifdef PIN_BUZZER
genericBuzzer buzzer;
#endif
#ifdef PIN_VIBRATION
GenericVibration vibration;
#endif
unsigned long _next_refresh, _auto_off;
NodePrefs* _node_prefs;
@@ -26,6 +33,16 @@ class UITask : public AbstractUITask {
unsigned long _alert_expiry;
int _msgcount;
unsigned long ui_started_at, next_batt_chck;
int next_backlight_btn_check = 0;
#ifdef PIN_STATUS_LED
int led_state = 0;
int next_led_change = 0;
int last_led_increment = 0;
#endif
#ifdef PIN_USER_BTN_ANA
unsigned long _analogue_pin_read_millis = millis();
#endif
UIScreen* splash;
UIScreen* home;
@@ -37,6 +54,8 @@ class UITask : public AbstractUITask {
// Button action handlers
char checkDisplayOn(char c);
char handleLongPress(char c);
char handleDoubleClick(char c);
char handleTripleClick(char c);
void setCurrScreen(UIScreen* c);
@@ -55,10 +74,15 @@ public:
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 soundBuzzer(UIEventType bet = UIEventType::none) override;
void notify(UIEventType t = UIEventType::none) override;
void loop() override;
void shutdown(bool restart = false);

View File

@@ -88,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");
@@ -108,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) {
@@ -370,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!");
@@ -388,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);
@@ -407,11 +407,11 @@ void UITask::handleButtonQuadruplePress() {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
_sensors->setSettingValue("gps", "0");
soundBuzzer(UIEventType::ack);
notify(UIEventType::ack);
sprintf(_alert, "GPS: Disabled");
} else {
_sensors->setSettingValue("gps", "1");
soundBuzzer(UIEventType::ack);
notify(UIEventType::ack);
sprintf(_alert, "GPS: Enabled");
}
break;

View File

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

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

@@ -46,6 +46,8 @@
/* ------------------------------ Code -------------------------------- */
#define FIRMWARE_VER_LEVEL 1
#define REQ_TYPE_LOGIN 0x00
#define REQ_TYPE_GET_STATUS 0x01
#define REQ_TYPE_KEEP_ALIVE 0x02
@@ -71,78 +73,6 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) {
#endif
}
static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
return _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "w");
#else
return _fs->open(filename, "w", true);
#endif
}
void SensorMesh::loadContacts() {
num_contacts = 0;
if (_fs->exists("/s_contacts")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/s_contacts", "r");
#else
File file = _fs->open("/s_contacts");
#endif
if (file) {
bool full = false;
while (!full) {
ContactInfo c;
uint8_t pub_key[32];
uint8_t unused[6];
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
success = success && (file.read(unused, 6) == 6);
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
c.last_timestamp = 0; // transient
c.last_activity = 0;
if (!success) break; // EOF
c.id = mesh::Identity(pub_key);
if (num_contacts < MAX_CONTACTS) {
contacts[num_contacts++] = c;
} else {
full = true;
}
}
file.close();
}
}
}
void SensorMesh::saveContacts() {
File file = openWrite(_fs, "/s_contacts");
if (file) {
uint8_t unused[5];
memset(unused, 0, sizeof(unused));
for (int i = 0; i < num_contacts; i++) {
auto c = &contacts[i];
if (c->permissions == 0) continue; // skip deleted entries
bool success = (file.write(c->id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *) &c->permissions, 1) == 1);
success = success && (file.write(unused, 6) == 6);
success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
success = success && (file.write(c->out_path, 64) == 64);
success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
if (!success) break; // write failed
}
file.close();
}
}
static uint8_t getDataSize(uint8_t type) {
switch (type) {
case LPP_GPS:
@@ -295,8 +225,8 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
uint8_t res2 = payload[1];
if (res1 == 0 && res2 == 0) {
uint8_t ofs = 4;
for (int i = 0; i < num_contacts && ofs + 7 <= sizeof(reply_data) - 4; i++) {
auto c = &contacts[i];
for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) {
auto c = acl.getClientByIdx(i);
if (c->permissions == 0) continue; // skip deleted entries
memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix
reply_data[ofs++] = c->permissions;
@@ -309,72 +239,12 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
mesh::Packet* SensorMesh::createSelfAdvert() {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
{
AdvertDataBuilder builder(ADV_TYPE_SENSOR, _prefs.node_name, _prefs.node_lat, _prefs.node_lon);
app_data_len = builder.encodeTo(app_data);
}
uint8_t app_data_len = _cli.buildAdvertData(ADV_TYPE_SENSOR, app_data);
return createAdvert(self_id, app_data, app_data_len);
}
ContactInfo* SensorMesh::getContact(const uint8_t* pubkey, int key_len) {
for (int i = 0; i < num_contacts; i++) {
if (memcmp(pubkey, contacts[i].id.pub_key, key_len) == 0) return &contacts[i]; // already known
}
return NULL; // not found
}
ContactInfo* SensorMesh::putContact(const mesh::Identity& id, uint8_t init_perms) {
uint32_t min_time = 0xFFFFFFFF;
ContactInfo* oldest = &contacts[MAX_CONTACTS - 1];
for (int i = 0; i < num_contacts; i++) {
if (id.matches(contacts[i].id)) return &contacts[i]; // already known
if (!contacts[i].isAdmin() && contacts[i].last_activity < min_time) {
oldest = &contacts[i];
min_time = oldest->last_activity;
}
}
ContactInfo* c;
if (num_contacts < MAX_CONTACTS) {
c = &contacts[num_contacts++];
} else {
c = oldest; // evict least active contact
}
memset(c, 0, sizeof(*c));
c->permissions = init_perms;
c->id = id;
c->out_path_len = -1; // initially out_path is unknown
return c;
}
bool SensorMesh::applyContactPermissions(const uint8_t* pubkey, int key_len, uint8_t perms) {
ContactInfo* c;
if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts
c = getContact(pubkey, key_len);
if (c == NULL) return false; // partial pubkey not found
num_contacts--; // delete from contacts[]
int i = c - contacts;
while (i < num_contacts) {
contacts[i] = contacts[i + 1];
i++;
}
} else {
if (key_len < PUB_KEY_SIZE) return false; // need complete pubkey when adding/modifying
mesh::Identity id(pubkey);
c = putContact(id, 0);
c->permissions = perms; // update their permissions
self_id.calcSharedSecret(c->shared_secret, pubkey);
}
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger saveContacts()
return true;
}
void SensorMesh::sendAlert(ContactInfo* c, Trigger* t) {
void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
int text_len = strlen(t->text);
uint8_t data[MAX_PACKET_PAYLOAD];
@@ -457,9 +327,9 @@ int SensorMesh::getAGCResetInterval() const {
}
uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) {
ContactInfo* client;
ClientInfo* client;
if (data[0] == 0) { // blank password, just check if sender is in ACL
client = getContact(sender.pub_key, PUB_KEY_SIZE);
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
if (client == NULL) {
#if MESH_DEBUG
MESH_DEBUG_PRINTLN("Login, sender not in ACL");
@@ -474,7 +344,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
return 0;
}
client = putContact(sender, PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO); // add to contacts (if not already known)
client = acl.putClient(sender, PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO); // add to contacts (if not already known)
if (sender_timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("Possible login replay attack!");
return 0; // FATAL: client table is full -OR- replay attack
@@ -492,12 +362,13 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
reply_data[4] = RESP_SERVER_LOGIN_OK;
reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16)
reply_data[5] = 0;
reply_data[6] = client->isAdmin() ? 1 : 0;
reply_data[7] = client->permissions;
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
reply_data[12] = FIRMWARE_VER_LEVEL;
return 12; // reply length
return 13; // reply length
}
void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
@@ -527,7 +398,8 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
uint8_t perms = atoi(sp);
if (applyContactPermissions(pubkey, hex_len / 2, perms)) {
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - invalid params");
@@ -538,8 +410,8 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
}
} else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
Serial.println("ACL:");
for (int i = 0; i < num_contacts; i++) {
auto c = &contacts[i];
for (int i = 0; i < acl.getNumClients(); i++) {
auto c = acl.getClientByIdx(i);
if (c->permissions == 0) continue; // skip deleted entries
Serial.printf("%02X ", c->permissions);
@@ -577,7 +449,14 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
memcpy(&timestamp, data, 4);
data[len] = 0; // ensure null terminator
uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
uint8_t reply_len;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
//} 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
@@ -595,8 +474,8 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
int SensorMesh::searchPeersByHash(const uint8_t* hash) {
int n = 0;
for (int i = 0; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) {
if (contacts[i].id.isHashMatch(hash)) {
for (int i = 0; i < acl.getNumClients() && n < MAX_SEARCH_RESULTS; i++) {
if (acl.getClientByIdx(i)->id.isHashMatch(hash)) {
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
}
}
@@ -605,15 +484,15 @@ int SensorMesh::searchPeersByHash(const uint8_t* hash) {
void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
int i = matching_peer_indexes[peer_idx];
if (i >= 0 && i < num_contacts) {
if (i >= 0 && i < acl.getNumClients()) {
// lookup pre-calculated shared_secret
memcpy(dest_secret, contacts[i].shared_secret, PUB_KEY_SIZE);
memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE);
} else {
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
}
}
void SensorMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
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);
@@ -632,34 +511,34 @@ void SensorMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= num_contacts) {
if (i < 0 || i >= acl.getNumClients()) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i);
return;
}
ContactInfo& from = contacts[i];
ClientInfo* from = acl.getClientByIdx(i);
if (type == PAYLOAD_TYPE_REQ) { // request (from a known contact)
uint32_t timestamp;
memcpy(&timestamp, data, 4);
if (timestamp > from.last_timestamp) { // prevent replay attacks
uint8_t reply_len = handleRequest(from.isAdmin() ? 0xFF : from.permissions, timestamp, data[4], &data[5], len - 5);
if (timestamp > from->last_timestamp) { // prevent replay attacks
uint8_t reply_len = handleRequest(from->isAdmin() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5);
if (reply_len == 0) return; // invalid command
from.last_timestamp = timestamp;
from.last_activity = getRTCClock()->getCurrentTime();
from->last_timestamp = timestamp;
from->last_activity = getRTCClock()->getCurrentTime();
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, reply_data, reply_len);
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
if (reply) {
if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
if (from->out_path_len >= 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);
}
@@ -668,30 +547,30 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
}
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && from.isAdmin()) { // a CLI command
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && from->isAdmin()) { // a CLI command
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint flags = (data[4] >> 2); // message attempt number, and other flags
if (sender_timestamp > from.last_timestamp) { // prevent replay attacks
if (sender_timestamp > from->last_timestamp) { // prevent replay attacks
if (flags == TXT_TYPE_PLAIN) {
bool handled = handleIncomingMsg(from, sender_timestamp, &data[5], flags, len - 5);
bool handled = handleIncomingMsg(*from, sender_timestamp, &data[5], flags, len - 5);
if (handled) { // if msg was handled then send an ack
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), from.id.pub_key, PUB_KEY_SIZE);
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), from->id.pub_key, PUB_KEY_SIZE);
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
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);
}
sendAckTo(*from, ack_hash);
}
}
} else if (flags == TXT_TYPE_CLI_DATA) {
from.last_timestamp = sender_timestamp;
from.last_activity = getRTCClock()->getCurrentTime();
from->last_timestamp = sender_timestamp;
from->last_activity = getRTCClock()->getCurrentTime();
// len can be > original length, but 'text' will be padded with zeroes
data[len] = 0; // need to make a C string again, with null terminator
@@ -711,12 +590,12 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (TXT_TYPE_CLI_DATA << 2);
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len);
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len);
if (reply) {
if (from.out_path_len < 0) {
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);
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
}
}
}
@@ -729,7 +608,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
}
}
bool SensorMesh::handleIncomingMsg(ContactInfo& from, uint32_t timestamp, uint8_t* data, uint flags, size_t len) {
bool SensorMesh::handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, 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);
@@ -738,23 +617,56 @@ bool SensorMesh::handleIncomingMsg(ContactInfo& from, uint32_t timestamp, uint8_
return false;
}
#define CTL_TYPE_NODE_DISCOVER_REQ 0x80
#define CTL_TYPE_NODE_DISCOVER_RESP 0x90
void SensorMesh::onControlDataRecv(mesh::Packet* packet) {
uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6) {
// TODO: apply rate limiting to these!
int i = 1;
uint8_t filter = packet->payload[i++];
uint32_t tag;
memcpy(&tag, &packet->payload[i], 4); i += 4;
uint32_t since;
if (packet->payload_len >= i+4) { // optional since field
memcpy(&since, &packet->payload[i], 4); i += 4;
} else {
since = 0;
}
if ((filter & (1 << ADV_TYPE_SENSOR)) != 0 && _prefs.discovery_mod_timestamp >= since) {
bool prefix_only = packet->payload[0] & 1;
uint8_t data[6 + PUB_KEY_SIZE];
data[0] = CTL_TYPE_NODE_DISCOVER_RESP | ADV_TYPE_SENSOR; // low 4-bits for node type
data[1] = packet->_snr; // let sender know the inbound SNR ( x 4)
memcpy(&data[2], &tag, 4); // include tag from request, for client to match to
memcpy(&data[6], self_id.pub_key, PUB_KEY_SIZE);
auto resp = createControlData(data, prefix_only ? 6 + 8 : 6 + PUB_KEY_SIZE);
if (resp) {
sendZeroHop(resp, getRetransmitDelay(resp)*4); // apply random delay (widened x4), as multiple nodes can respond to this
}
}
}
}
bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= num_contacts) {
if (i < 0 || i >= acl.getNumClients()) {
MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i);
return false;
}
ContactInfo& from = contacts[i];
ClientInfo* from = acl.getClientByIdx(i);
MESH_DEBUG_PRINTLN("PATH to contact, path_len=%d", (uint32_t) path_len);
// NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
memcpy(from.out_path, path, from.out_path_len = path_len); // store a copy of path, for sendDirect()
from.last_activity = getRTCClock()->getCurrentTime();
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()) {
if (from->isAdmin()) {
// only do saveContacts() (of this out_path change) if this is an admin
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
@@ -779,9 +691,8 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
_cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
_cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
{
num_contacts = 0;
next_local_advert = next_flood_advert = 0;
dirty_contacts_expiry = 0;
last_read_time = 0;
@@ -793,6 +704,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
_prefs.airtime_factor = 1.0; // one half
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
_prefs.tx_delay_factor = 0.5f; // was 0.25f
_prefs.direct_tx_delay_factor = 0.2f; // was zero
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
_prefs.node_lat = ADVERT_LAT;
_prefs.node_lon = ADVERT_LON;
@@ -807,6 +719,11 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
_prefs.disable_fwd = true;
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
// GPS defaults
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
}
void SensorMesh::begin(FILESYSTEM* fs) {
@@ -815,13 +732,17 @@ void SensorMesh::begin(FILESYSTEM* fs) {
// load persisted prefs
_cli.loadPrefs(_fs);
loadContacts();
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() {
@@ -889,6 +810,19 @@ 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();
@@ -967,13 +901,13 @@ void SensorMesh::loop() {
if (millisHasNowPassed(t->send_expiry)) { // next send needed?
if (t->attempt >= 4) { // max attempts reached, try next contact
t->curr_contact_idx++;
if (t->curr_contact_idx >= num_contacts) { // no more contacts to try?
if (t->curr_contact_idx >= acl.getNumClients()) { // no more contacts to try?
num_alert_tasks--; // remove t from queue
for (int i = 0; i < num_alert_tasks; i++) {
alert_tasks[i] = alert_tasks[i + 1];
}
} else {
auto c = &contacts[t->curr_contact_idx];
auto c = acl.getClientByIdx(t->curr_contact_idx);
uint16_t pri_mask = (t->pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO;
if (c->permissions & pri_mask) { // contact wants alert
@@ -986,8 +920,8 @@ void SensorMesh::loop() {
// next contact tested in next ::loop()
}
}
} else if (t->curr_contact_idx < num_contacts) {
auto c = &contacts[t->curr_contact_idx]; // send next attempt
} else if (t->curr_contact_idx < acl.getNumClients()) {
auto c = acl.getClientByIdx(t->curr_contact_idx); // send next attempt
sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry
} else {
// contact list has likely been modified while waiting for alert ACK, cancel this task
@@ -998,7 +932,7 @@ void SensorMesh::loop() {
// is there are pending dirty contacts write needed?
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
saveContacts();
acl.save(_fs);
dirty_contacts_expiry = 0;
}
}

View File

@@ -20,15 +20,11 @@
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/CommonCLI.h>
#include <helpers/StatsFormatHelper.h>
#include <helpers/ClientACL.h>
#include <RTClib.h>
#include <target.h>
#define PERM_ACL_ROLE_MASK 3 // lower 2 bits
#define PERM_ACL_GUEST 0
#define PERM_ACL_READ_ONLY 1
#define PERM_ACL_READ_WRITE 2
#define PERM_ACL_ADMIN 3
#define PERM_RESERVED1 (1 << 2)
#define PERM_RESERVED2 (1 << 3)
#define PERM_RESERVED3 (1 << 4)
@@ -36,30 +32,16 @@
#define PERM_RECV_ALERTS_LO (1 << 6) // low priority alerts
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
struct ContactInfo {
mesh::Identity id;
uint8_t permissions;
int8_t out_path_len;
uint8_t out_path[MAX_PATH_SIZE];
uint8_t shared_secret[PUB_KEY_SIZE];
uint32_t last_timestamp; // by THEIR clock (transient)
uint32_t last_activity; // by OUR clock (transient)
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
};
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "1 Sep 2025"
#define FIRMWARE_BUILD_DATE "13 Nov 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.8.1"
#define FIRMWARE_VERSION "v1.10.0"
#endif
#define FIRMWARE_ROLE "sensor"
#define MAX_CONTACTS 20
#define MAX_SEARCH_RESULTS 8
#define MAX_CONCURRENT_ALERTS 4
@@ -88,6 +70,9 @@ public:
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
mesh::LocalIdentity& getSelfId() override { return self_id; }
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override { }
@@ -140,17 +125,17 @@ protected:
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onControlDataRecv(mesh::Packet* packet) override;
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
virtual bool handleIncomingMsg(ContactInfo& from, uint32_t timestamp, uint8_t* data, uint flags, size_t len);
void sendAckTo(const ContactInfo& dest, uint32_t ack_hash);
virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, 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];
ContactInfo contacts[MAX_CONTACTS];
int num_contacts;
ClientACL acl;
unsigned long dirty_contacts_expiry;
CayenneLPP telemetry;
uint32_t last_read_time;
@@ -163,15 +148,15 @@ private:
uint8_t pending_sf;
uint8_t pending_cr;
void loadContacts();
void saveContacts();
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
mesh::Packet* createSelfAdvert();
ContactInfo* getContact(const uint8_t* pubkey, int key_len);
ContactInfo* putContact(const mesh::Identity& id, uint8_t init_perms);
bool applyContactPermissions(const uint8_t* pubkey, int key_len, uint8_t perms);
void sendAlert(ContactInfo* c, Trigger* t);
void sendAlert(const ClientInfo* c, Trigger* t);
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
}
#endif
};

View File

@@ -144,4 +144,5 @@ void loop() {
#ifdef DISPLAY_CLASS
ui_task.loop();
#endif
rtc_clock.tick();
}

View File

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

View File

@@ -18,11 +18,11 @@ 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
@@ -47,6 +47,7 @@ build_src_filter =
+<*.cpp>
+<helpers/*.cpp>
+<helpers/radiolib/*.cpp>
+<helpers/bridges/BridgeBase.cpp>
+<helpers/ui/MomentaryButton.cpp>
; ----------------- ESP32 ---------------------
@@ -66,6 +67,7 @@ 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/53.03.12/platform-espressif32.zip
@@ -75,10 +77,16 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/
[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
-D EXTRAFS=1
lib_deps =
${arduino_base.lib_deps}
https://github.com/oltaco/CustomLFS @ 0.2.1
; ----------------- RP2040 ---------------------
[rp2040_base]
@@ -103,6 +111,7 @@ build_src_filter = ${arduino_base.build_src_filter}
+<helpers/stm32>
lib_deps = ${arduino_base.lib_deps}
file://arch/stm32/Adafruit_LittleFS_stm32
adafruit/Adafruit BusIO @ 1.17.2
[sensor_base]
build_flags =
@@ -119,6 +128,8 @@ build_flags =
-D ENV_INCLUDE_INA260=1
-D ENV_INCLUDE_MLX90614=1
-D ENV_INCLUDE_VL53L0X=1
-D ENV_INCLUDE_BME680=1
-D ENV_INCLUDE_BMP085=1
lib_deps =
adafruit/Adafruit INA3221 Library @ ^1.0.1
adafruit/Adafruit INA219 @ ^1.2.3
@@ -133,3 +144,5 @@ lib_deps =
adafruit/Adafruit MLX90614 Library @ ^2.1.5
adafruit/Adafruit_VL53L0X @ ^1.2.4
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit BME680 Library @ ^2.0.4
adafruit/Adafruit BMP085 Library @ ^1.2.4

View File

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

@@ -68,6 +68,14 @@ 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 (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
@@ -90,6 +98,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard.
}
if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE;
DispatcherAction action = ACTION_RELEASE;
switch (pkt->getPayloadType()) {
@@ -201,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
@@ -587,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());
@@ -610,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;
@@ -645,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

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

View File

@@ -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
@@ -66,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

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,13 @@
#define TXT_ACK_DELAY 200
#endif
void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
sendFlood(pkt, delay_millis);
}
void BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
sendFlood(pkt, delay_millis);
}
mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name) {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
@@ -34,7 +41,7 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl
void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
if (dest.out_path_len < 0) {
mesh::Packet* ack = createAck(ack_hash);
if (ack) sendFlood(ack, TXT_ACK_DELAY);
if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY);
} else {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
@@ -68,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()) {
@@ -158,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
@@ -167,7 +182,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
if (path) sendFlood(path, TXT_ACK_DELAY);
if (path) sendFloodScoped(from, path, TXT_ACK_DELAY);
} else {
sendAckTo(from, ack_hash);
}
@@ -178,12 +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
@@ -193,7 +209,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
if (path) sendFlood(path, TXT_ACK_DELAY);
if (path) sendFloodScoped(from, path, TXT_ACK_DELAY);
} else {
sendAckTo(from, ack_hash);
}
@@ -209,20 +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);
}
}
}
@@ -248,7 +268,7 @@ bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
// also got an encoded ACK!
if (processAck(extra)) {
if (processAck(extra) != NULL) {
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
}
} else if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 0) {
@@ -258,12 +278,25 @@ bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_
}
void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
if (processAck((uint8_t *)&ack_crc)) {
ContactInfo* from;
if ((from = processAck((uint8_t *)&ack_crc)) != NULL) {
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
if (packet->isRouteFlood() && from->out_path_len >= 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;
@@ -320,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 {
@@ -346,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 {
@@ -372,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;
@@ -386,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
}
@@ -432,7 +467,7 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
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 {
@@ -459,7 +494,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_
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 {
@@ -486,7 +521,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u
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 {
@@ -550,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!
@@ -559,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

@@ -93,7 +93,7 @@ protected:
// '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;
@@ -105,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
@@ -127,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:

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->multi_acks, sizeof(_prefs->multi_acks)); // 115
file.read((uint8_t *) &_prefs->bw, sizeof(_prefs->bw)); // 116
file.read((uint8_t *) &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
file.read(pad, 3); // 121
file.read((uint8_t *) &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
file.read((uint8_t *) &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
file.read((uint8_t *) &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
file.read((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
file.read((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4
file.read(pad, 4); // 36
file.read((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
file.read((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
file.read((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56
file.read((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72
file.read((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
file.read((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
file.read((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
file.read((uint8_t *)pad, 1); // 79 was 'unused'
file.read((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
file.read((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
file.read((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
file.read((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
file.read(pad, 4); // 108
file.read((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112
file.read((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113
file.read((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
file.read((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
file.read((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116
file.read((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
file.read(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,12 +78,22 @@ 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();
}
}
@@ -88,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->multi_acks, sizeof(_prefs->multi_acks)); // 115
file.write((uint8_t *) &_prefs->bw, sizeof(_prefs->bw)); // 116
file.write((uint8_t *) &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
file.write(pad, 3); // 121
file.write((uint8_t *) &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
file.write((uint8_t *) &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
file.write((uint8_t *) &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
file.write((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
file.write((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4
file.write(pad, 4); // 36
file.write((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
file.write((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
file.write((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56
file.write((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72
file.write((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
file.write((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
file.write((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
file.write((uint8_t *)pad, 1); // 79 was 'unused'
file.write((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
file.write(pad, 4); // 108
file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112
file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113
file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
file.write((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116
file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
file.write(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();
}
@@ -128,6 +163,19 @@ void CommonCLI::savePrefs() {
_callbacks->savePrefs();
}
uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
if (_prefs->advert_loc_policy == ADVERT_LOC_NONE) {
AdvertDataBuilder builder(node_type, _prefs->node_name);
return builder.encodeTo(app_data);
} else if (_prefs->advert_loc_policy == ADVERT_LOC_SHARE) {
AdvertDataBuilder builder(node_type, _prefs->node_name, _sensors->node_lat, _sensors->node_lon);
return builder.encodeTo(app_data);
} else {
AdvertDataBuilder builder(node_type, _prefs->node_name, _prefs->node_lat, _prefs->node_lon);
return builder.encodeTo(app_data);
}
}
void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) {
if (memcmp(command, "reboot", 6) == 0) {
_board->reboot(); // doesn't return
@@ -199,6 +247,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else if (memcmp(command, "clear stats", 11) == 0) {
_callbacks->clearStats();
strcpy(reply, "(OK - stats reset)");
/*
* GET commands
*/
} else if (memcmp(command, "get ", 4) == 0) {
const char* config = &command[4];
if (memcmp(config, "af", 2) == 0) {
@@ -252,9 +303,40 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
} else if (memcmp(config, "role", 4) == 0) {
sprintf(reply, "> %s", _callbacks->getRole());
} else if (memcmp(config, "bridge.type", 11) == 0) {
sprintf(reply, "> %s",
#ifdef WITH_RS232_BRIDGE
"rs232"
#elif WITH_ESPNOW_BRIDGE
"espnow"
#else
"none"
#endif
);
#ifdef WITH_BRIDGE
} else if (memcmp(config, "bridge.enabled", 14) == 0) {
sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off");
} else if (memcmp(config, "bridge.delay", 12) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
} else if (memcmp(config, "bridge.source", 13) == 0) {
sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx");
#endif
#ifdef WITH_RS232_BRIDGE
} else if (memcmp(config, "bridge.baud", 11) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
#endif
#ifdef WITH_ESPNOW_BRIDGE
} else if (memcmp(config, "bridge.channel", 14) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
} else if (memcmp(config, "bridge.secret", 13) == 0) {
sprintf(reply, "> %s", _prefs->bridge_secret);
#endif
} else {
sprintf(reply, "??: %s", config);
}
/*
* SET commands
*/
} else if (memcmp(command, "set ", 4) == 0) {
const char* config = &command[4];
if (memcmp(config, "af ", 3) == 0) {
@@ -301,7 +383,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
savePrefs();
strcpy(reply, "OK");
} else if (sender_timestamp == 0 && memcmp(config, "prv.key ", 8) == 0) { // from serial command line only
} else if (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) {
@@ -391,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);
}
@@ -399,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");
@@ -411,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 multi_acks;
float bw;
uint8_t flood_max;
uint8_t interference_threshold;
uint8_t agc_reset_interval; // secs / 4
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
#define WITH_BRIDGE
#endif
#define ADVERT_LOC_NONE 0
#define ADVERT_LOC_SHARE 1
#define ADVERT_LOC_PREFS 2
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
double node_lat, node_lon;
char password[16];
float freq;
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 {
@@ -46,10 +67,21 @@ public:
virtual void removeNeighbor(const uint8_t* pubkey, int key_len) {
// no op by default
};
virtual void formatStatsReply(char *reply) = 0;
virtual void formatRadioStatsReply(char *reply) = 0;
virtual void formatPacketStatsReply(char *reply) = 0;
virtual mesh::LocalIdentity& getSelfId() = 0;
virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0;
virtual void clearStats() = 0;
virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0;
virtual void setBridgeState(bool enable) {
// no op by default
};
virtual void restartBridge() {
// no op by default
};
};
class CommonCLI {
@@ -57,6 +89,7 @@ class CommonCLI {
NodePrefs* _prefs;
CommonCLICallbacks* _callbacks;
mesh::MainBoard* _board;
SensorManager* _sensors;
char tmp[PRV_KEY_SIZE*2 + 4];
mesh::RTCClock* getRTCClock() { return _rtc; }
@@ -64,10 +97,11 @@ class CommonCLI {
void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
public:
CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, NodePrefs* prefs, CommonCLICallbacks* callbacks)
: _board(&board), _rtc(&rtc), _prefs(prefs), _callbacks(callbacks) { }
CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, 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

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

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*");
@@ -88,8 +88,6 @@ void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t
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) {

View File

@@ -4,10 +4,7 @@ static SerialBLEInterface* instance;
void SerialBLEInterface::onConnect(uint16_t connection_handle) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: connected");
if(instance){
instance->_isDeviceConnected = true;
// no need to stop advertising on connect, as the ble stack does this automatically
}
// we now set _isDeviceConnected=true in onSecured callback instead
}
void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) {
@@ -18,6 +15,14 @@ void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason
}
}
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;
@@ -27,8 +32,8 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t 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);
@@ -36,6 +41,7 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
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();
@@ -80,7 +86,7 @@ void SerialBLEInterface::startAdv() {
* https://developer.apple.com/library/content/qa/qa1931/_index.html
*/
Bluefruit.Advertising.restartOnDisconnect(false); // don't restart automatically as we handle it in onDisconnect
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
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

View File

@@ -3,6 +3,10 @@
#include "../BaseSerialInterface.h"
#include <bluefruit.h>
#ifndef BLE_TX_POWER
#define BLE_TX_POWER 4
#endif
class SerialBLEInterface : public BaseSerialInterface {
BLEUart bleuart;
bool _isEnabled;
@@ -21,6 +25,7 @@ class SerialBLEInterface : public BaseSerialInterface {
void clearBuffers() { send_queue_len = 0; }
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() {

View File

@@ -1,70 +1,28 @@
#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
#include "MeshCore.h"
class CustomLR1110 : public LR1110 {
public:
CustomLR1110(Module *mod) : LR1110(mod) { }
RadioLibTime_t getTimeOnAir(size_t len) override {
// calculate number of symbols
float N_symbol = 0;
if(this->codingRate <= RADIOLIB_LR11X0_LORA_CR_4_8_SHORT) {
// legacy coding rate - nice and simple
// get SF coefficients
float coeff1 = 0;
int16_t coeff2 = 0;
int16_t coeff3 = 0;
if(this->spreadingFactor < 7) {
// SF5, SF6
coeff1 = 6.25;
coeff2 = 4*this->spreadingFactor;
coeff3 = 4*this->spreadingFactor;
} else if(this->spreadingFactor < 11) {
// SF7. SF8, SF9, SF10
coeff1 = 4.25;
coeff2 = 4*this->spreadingFactor + 8;
coeff3 = 4*this->spreadingFactor;
} else {
// SF11, SF12
coeff1 = 4.25;
coeff2 = 4*this->spreadingFactor + 8;
coeff3 = 4*(this->spreadingFactor - 2);
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;
}
// get CRC length
int16_t N_bitCRC = 16;
if(this->crcTypeLoRa == RADIOLIB_LR11X0_LORA_CRC_DISABLED) {
N_bitCRC = 0;
}
// get header length
int16_t N_symbolHeader = 20;
if(this->headerType == RADIOLIB_LR11X0_LORA_HEADER_IMPLICIT) {
N_symbolHeader = 0;
}
// calculate number of LoRa preamble symbols - NO! Lora preamble is already in symbols
// uint32_t N_symbolPreamble = (this->preambleLengthLoRa & 0x0F) * (uint32_t(1) << ((this->preambleLengthLoRa & 0xF0) >> 4));
// calculate the number of symbols - nope
// N_symbol = (float)N_symbolPreamble + coeff1 + 8.0f + ceilf((float)RADIOLIB_MAX((int16_t)(8 * len + N_bitCRC - coeff2 + N_symbolHeader), (int16_t)0) / (float)coeff3) * (float)(this->codingRate + 4);
// calculate the number of symbols - using only preamblelora because it's already in symbols
N_symbol = (float)preambleLengthLoRa + coeff1 + 8.0f + ceilf((float)RADIOLIB_MAX((int16_t)(8 * len + N_bitCRC - coeff2 + N_symbolHeader), (int16_t)0) / (float)coeff3) * (float)(this->codingRate + 4);
} else {
// long interleaving - not needed for this modem
}
// get time-on-air in us
return(((uint32_t(1) << this->spreadingFactor) / this->bandwidthKhz) * N_symbol * 1000.0f);
}
bool isReceiving() {
uint16_t irq = getIrqStatus();
bool detected = ((irq & LR1110_IRQ_HEADER_VALID) || (irq & LR1110_IRQ_HAS_PREAMBLE));
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));
return detected;
}
};

View File

@@ -6,6 +6,21 @@
#define TELEM_WIRE &Wire // Use default I2C bus for Environment Sensors
#endif
#ifdef ENV_INCLUDE_BME680
#ifndef TELEM_BME680_ADDRESS
#define TELEM_BME680_ADDRESS 0x76
#endif
#define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25)
#include <Adafruit_BME680.h>
static Adafruit_BME680 BME680;
#endif
#ifdef ENV_INCLUDE_BMP085
#define TELEM_BMP085_SEALEVELPRESSURE_HPA (1013.25)
#include <Adafruit_BMP085.h>
static Adafruit_BMP085 BMP085;
#endif
#if ENV_INCLUDE_AHTX0
#define TELEM_AHTX_ADDRESS 0x38 // AHT10, AHT20 temperature and humidity sensor I2C address
#include <Adafruit_AHTX0.h>
@@ -66,11 +81,11 @@ static Adafruit_INA260 INA260;
#endif
#if ENV_INCLUDE_INA226
#define TELEM_INA226_ADDRESS 0x44
#define TELEM_INA226_SHUNT_VALUE 0.100
#define TELEM_INA226_ADDRESS 0x44
#define TELEM_INA226_SHUNT_VALUE 0.100
#define TELEM_INA226_MAX_AMP 0.8
#include <INA226.h>
static INA226 INA226(TELEM_INA226_ADDRESS);
static INA226 INA226(TELEM_INA226_ADDRESS, TELEM_WIRE);
#endif
#if ENV_INCLUDE_MLX90614
@@ -85,18 +100,57 @@ static Adafruit_MLX90614 MLX90614;
static Adafruit_VL53L0X VL53L0X;
#endif
#if ENV_INCLUDE_GPS && RAK_BOARD
#if ENV_INCLUDE_GPS && defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
#define RAK_WISBLOCK_GPS
#endif
#ifdef RAK_WISBLOCK_GPS
static uint32_t gpsResetPin = 0;
static bool i2cGPSFlag = false;
static bool serialGPSFlag = false;
#define TELEM_RAK12500_ADDRESS 0x42 //RAK12500 Ublox GPS via i2c
#include <SparkFun_u-blox_GNSS_Arduino_Library.h>
static SFE_UBLOX_GNSS ublox_GNSS;
class RAK12500LocationProvider : public LocationProvider {
long _lat = 0;
long _lng = 0;
long _alt = 0;
int _sats = 0;
long _epoch = 0;
bool _fix = false;
public:
long getLatitude() override { return _lat; }
long getLongitude() override { return _lng; }
long getAltitude() override { return _alt; }
long satellitesCount() override { return _sats; }
bool isValid() override { return _fix; }
long getTimestamp() override { return _epoch; }
void sendSentence(const char * sentence) override { }
void reset() override { }
void begin() override { }
void stop() override { }
void loop() override {
if (ublox_GNSS.getGnssFixOk(8)) {
_fix = true;
_lat = ublox_GNSS.getLatitude(2) / 10;
_lng = ublox_GNSS.getLongitude(2) / 10;
_alt = ublox_GNSS.getAltitude(2);
_sats = ublox_GNSS.getSIV(2);
} else {
_fix = false;
}
_epoch = ublox_GNSS.getUnixEpoch(2);
}
bool isEnabled() override { return true; }
};
static RAK12500LocationProvider RAK12500_provider;
#endif
bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_GPS
#if RAK_BOARD
#ifdef RAK_WISBLOCK_GPS
rakGPSInit(); //probe base board/sockets for GPS
#else
initBasicGPS();
@@ -104,7 +158,13 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_PIN_SDA && ENV_PIN_SCL
#ifdef NRF52_PLATFORM
Wire1.setPins(ENV_PIN_SDA, ENV_PIN_SCL);
Wire1.setClock(100000);
Wire1.begin();
#else
Wire1.begin(ENV_PIN_SDA, ENV_PIN_SCL, 100000);
#endif
MESH_DEBUG_PRINTLN("Second I2C initialized on pins SDA: %d SCL: %d", ENV_PIN_SDA, ENV_PIN_SCL);
#endif
@@ -241,6 +301,28 @@ bool EnvironmentSensorManager::begin() {
}
#endif
#if ENV_INCLUDE_BME680
if (BME680.begin(TELEM_BME680_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS);
BME680_initialized = true;
} else {
BME680_initialized = false;
MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESS);
}
#endif
#if ENV_INCLUDE_BMP085
// First argument is MODE (aka oversampling)
// choose ULTRALOWPOWER
if (BMP085.begin(0, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found sensor BMP085");
BMP085_initialized = true;
} else {
BMP085_initialized = false;
MESH_DEBUG_PRINTLN("BMP085 was not found at I2C address %02X", 0x77);
}
#endif
return true;
}
@@ -248,7 +330,7 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
next_available_channel = TELEM_CHANNEL_SELF + 1;
if (requester_permissions & TELEM_PERM_LOCATION && gps_active) {
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, 0.0f); // allow lat/lon via telemetry even if no GPS is detected
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); // allow lat/lon via telemetry even if no GPS is detected
}
if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
@@ -370,6 +452,27 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
}
#endif
#if ENV_INCLUDE_BME680
if (BME680_initialized) {
if (BME680.performReading()) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME680.temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME680.humidity);
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME680.pressure / 100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, 44330.0 * (1.0 - pow((BME680.pressure / 100) / TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903)));
telemetry.addAnalogInput(next_available_channel, BME680.gas_resistance);
next_available_channel++;
}
}
#endif
#if ENV_INCLUDE_BMP085
if (BMP085_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP085.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP085.readPressure() / 100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP085.readAltitude(TELEM_BMP085_SEALEVELPRESSURE_HPA * 100));
}
#endif
}
return true;
@@ -377,27 +480,34 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
int EnvironmentSensorManager::getNumSettings() const {
int settings = 0;
#if ENV_INCLUDE_GPS
return gps_detected ? 1 : 0; // only show GPS setting if GPS is detected
#else
return 0;
if (gps_detected) settings++; // only show GPS setting if GPS is detected
#endif
return settings;
}
const char* EnvironmentSensorManager::getSettingName(int i) const {
int settings = 0;
#if ENV_INCLUDE_GPS
return (gps_detected && i == 0) ? "gps" : NULL;
#else
return NULL;
if (gps_detected && i == settings++) {
return "gps";
}
#endif
// convenient way to add params (needed for some tests)
// if (i == settings++) return "param.2";
return NULL;
}
const char* EnvironmentSensorManager::getSettingValue(int i) const {
int settings = 0;
#if ENV_INCLUDE_GPS
if (gps_detected && i == 0) {
return gps_active ? "1" : "0";
}
if (gps_detected && i == settings++) {
return gps_active ? "1" : "0";
}
#endif
// convenient way to add params ...
// if (i == settings++) return "2";
return NULL;
}
@@ -427,10 +537,8 @@ void EnvironmentSensorManager::initBasicGPS() {
#endif
// Try to detect if GPS is physically connected to determine if we should expose the setting
#ifdef PIN_GPS_EN
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, HIGH); // Power on GPS
#endif
_location->begin();
_location->reset();
#ifndef PIN_GPS_EN
MESH_DEBUG_PRINTLN("No GPS wake/reset pin found for this board. Continuing on...");
@@ -451,13 +559,13 @@ void EnvironmentSensorManager::initBasicGPS() {
} else {
MESH_DEBUG_PRINTLN("No GPS detected");
}
#ifdef PIN_GPS_EN
digitalWrite(PIN_GPS_EN, LOW); // Power off GPS until the setting is changed
#endif
_location->stop();
gps_active = false; //Set GPS visibility off until setting is changed
}
#ifdef RAK_BOARD
// gps code for rak might be moved to MicroNMEALoactionProvider
// or make a new location provider ...
#ifdef RAK_WISBLOCK_GPS
void EnvironmentSensorManager::rakGPSInit(){
Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
@@ -506,12 +614,22 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
//Try to init RAK12500 on I2C
if (ublox_GNSS.begin(Wire) == true){
MESH_DEBUG_PRINTLN("RAK12500 GPS init correctly with pin %i",ioPin);
ublox_GNSS.setI2COutput(COM_TYPE_NMEA);
ublox_GNSS.setI2COutput(COM_TYPE_UBX);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GPS);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GALILEO);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GLONASS);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_SBAS);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_BEIDOU);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_IMES);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_QZSS);
ublox_GNSS.setMeasurementRate(1000);
ublox_GNSS.saveConfigSelective(VAL_CFG_SUBSEC_IOPORT);
gpsResetPin = ioPin;
i2cGPSFlag = true;
gps_active = true;
gps_detected = true;
_location = &RAK12500_provider;
return true;
}
else if(Serial1){
@@ -531,65 +649,63 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
void EnvironmentSensorManager::start_gps() {
gps_active = true;
#ifdef RAK_BOARD
#ifdef RAK_WISBLOCK_GPS
pinMode(gpsResetPin, OUTPUT);
digitalWrite(gpsResetPin, HIGH);
return;
#endif
#ifdef PIN_GPS_EN
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, HIGH);
return;
#endif
_location->begin();
_location->reset();
#ifndef PIN_GPS_RESET
MESH_DEBUG_PRINTLN("Start GPS is N/A on this board. Actual GPS state unchanged");
#endif
}
void EnvironmentSensorManager::stop_gps() {
gps_active = false;
#ifdef RAK_BOARD
#ifdef RAK_WISBLOCK_GPS
pinMode(gpsResetPin, OUTPUT);
digitalWrite(gpsResetPin, LOW);
return;
#endif
#ifdef PIN_GPS_EN
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, LOW);
return;
#endif
_location->stop();
#ifndef PIN_GPS_EN
MESH_DEBUG_PRINTLN("Stop GPS is N/A on this board. Actual GPS state unchanged");
#endif
}
void EnvironmentSensorManager::loop() {
static long next_gps_update = 0;
#if ENV_INCLUDE_GPS
_location->loop();
if (millis() > next_gps_update) {
if(gps_active){
#ifndef RAK_BOARD
#ifdef RAK_WISBLOCK_GPS
if ((i2cGPSFlag || serialGPSFlag) && _location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
node_altitude = ((double)_location->getAltitude()) / 1000.0;
MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
}
#else
if (_location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
node_altitude = ((double)_location->getAltitude()) / 1000.0;
MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
}
#else
if(i2cGPSFlag){
node_lat = ((double)ublox_GNSS.getLatitude())/10000000.;
node_lon = ((double)ublox_GNSS.getLongitude())/10000000.;
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
}
else if (serialGPSFlag && _location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
}
//else
//MESH_DEBUG_PRINTLN("No valid GPS data");
#endif
}
next_gps_update = millis() + 1000;
}
#endif
}
#endif

View File

@@ -20,6 +20,8 @@ protected:
bool MLX90614_initialized = false;
bool VL53L0X_initialized = false;
bool SHT4X_initialized = false;
bool BME680_initialized = false;
bool BMP085_initialized = false;
bool gps_detected = false;
bool gps_active = false;
@@ -39,6 +41,7 @@ protected:
public:
#if ENV_INCLUDE_GPS
EnvironmentSensorManager(LocationProvider &location): _location(&location){};
LocationProvider* getLocationProvider() { return _location; }
#else
EnvironmentSensorManager(){};
#endif

View File

@@ -0,0 +1,223 @@
#pragma once
#include <stdint.h>
#define LPP_DIGITAL_INPUT 0 // 1 byte
#define LPP_DIGITAL_OUTPUT 1 // 1 byte
#define LPP_ANALOG_INPUT 2 // 2 bytes, 0.01 signed
#define LPP_ANALOG_OUTPUT 3 // 2 bytes, 0.01 signed
#define LPP_GENERIC_SENSOR 100 // 4 bytes, unsigned
#define LPP_LUMINOSITY 101 // 2 bytes, 1 lux unsigned
#define LPP_PRESENCE 102 // 1 byte, bool
#define LPP_TEMPERATURE 103 // 2 bytes, 0.1°C signed
#define LPP_RELATIVE_HUMIDITY 104 // 1 byte, 0.5% unsigned
#define LPP_ACCELEROMETER 113 // 2 bytes per axis, 0.001G
#define LPP_BAROMETRIC_PRESSURE 115 // 2 bytes 0.1hPa unsigned
#define LPP_VOLTAGE 116 // 2 bytes 0.01V unsigned
#define LPP_CURRENT 117 // 2 bytes 0.001A unsigned
#define LPP_FREQUENCY 118 // 4 bytes 1Hz unsigned
#define LPP_PERCENTAGE 120 // 1 byte 1-100% unsigned
#define LPP_ALTITUDE 121 // 2 byte 1m signed
#define LPP_CONCENTRATION 125 // 2 bytes, 1 ppm unsigned
#define LPP_POWER 128 // 2 byte, 1W, unsigned
#define LPP_DISTANCE 130 // 4 byte, 0.001m, unsigned
#define LPP_ENERGY 131 // 4 byte, 0.001kWh, unsigned
#define LPP_DIRECTION 132 // 2 bytes, 1deg, unsigned
#define LPP_UNIXTIME 133 // 4 bytes, unsigned
#define LPP_GYROMETER 134 // 2 bytes per axis, 0.01 °/s
#define LPP_COLOUR 135 // 1 byte per RGB Color
#define LPP_GPS 136 // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
#define LPP_SWITCH 142 // 1 byte, 0/1
#define LPP_POLYLINE 240 // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
// Multipliers
#define LPP_DIGITAL_INPUT_MULT 1
#define LPP_DIGITAL_OUTPUT_MULT 1
#define LPP_ANALOG_INPUT_MULT 100
#define LPP_ANALOG_OUTPUT_MULT 100
#define LPP_GENERIC_SENSOR_MULT 1
#define LPP_LUMINOSITY_MULT 1
#define LPP_PRESENCE_MULT 1
#define LPP_TEMPERATURE_MULT 10
#define LPP_RELATIVE_HUMIDITY_MULT 2
#define LPP_ACCELEROMETER_MULT 1000
#define LPP_BAROMETRIC_PRESSURE_MULT 10
#define LPP_VOLTAGE_MULT 100
#define LPP_CURRENT_MULT 1000
#define LPP_FREQUENCY_MULT 1
#define LPP_PERCENTAGE_MULT 1
#define LPP_ALTITUDE_MULT 1
#define LPP_POWER_MULT 1
#define LPP_DISTANCE_MULT 1000
#define LPP_ENERGY_MULT 1000
#define LPP_DIRECTION_MULT 1
#define LPP_UNIXTIME_MULT 1
#define LPP_GYROMETER_MULT 100
#define LPP_GPS_LAT_LON_MULT 10000
#define LPP_GPS_ALT_MULT 100
#define LPP_SWITCH_MULT 1
#define LPP_CONCENTRATION_MULT 1
#define LPP_COLOUR_MULT 1
#define LPP_ERROR_OK 0
#define LPP_ERROR_OVERFLOW 1
#define LPP_ERROR_UNKOWN_TYPE 2
class LPPReader {
const uint8_t* _buf;
uint8_t _len;
uint8_t _pos;
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);
}
public:
LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { }
void reset() {
_pos = 0;
}
bool readHeader(uint8_t& channel, uint8_t& type) {
if (_pos + 2 < _len) {
channel = _buf[_pos++];
type = _buf[_pos++];
return channel != 0; // channel 0 is End-of-data
}
return false; // end-of-buffer
}
bool readGPS(float& lat, float& lon, float& alt) {
lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
return _pos <= _len;
}
bool readVoltage(float& voltage) {
voltage = getFloat(&_buf[_pos], 2, 100, false); _pos += 2;
return _pos <= _len;
}
bool readCurrent(float& amps) {
amps = getFloat(&_buf[_pos], 2, 1000, false); _pos += 2;
return _pos <= _len;
}
bool readPower(float& watts) {
watts = getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
return _pos <= _len;
}
bool readTemperature(float& degrees_c) {
degrees_c = getFloat(&_buf[_pos], 2, 10, true); _pos += 2;
return _pos <= _len;
}
bool readPressure(float& pa) {
pa = getFloat(&_buf[_pos], 2, 10, false); _pos += 2;
return _pos <= _len;
}
bool readRelativeHumidity(float& pct) {
pct = getFloat(&_buf[_pos], 1, 2, false); _pos += 1;
return _pos <= _len;
}
bool readAltitude(float& m) {
m = getFloat(&_buf[_pos], 2, 1, true); _pos += 2;
return _pos <= _len;
}
void skipData(uint8_t type) {
switch (type) {
case LPP_GPS:
_pos += 9; break;
case LPP_POLYLINE:
_pos += 8; break; // TODO: this is MINIMIUM
case LPP_GYROMETER:
case LPP_ACCELEROMETER:
_pos += 6; break;
case LPP_GENERIC_SENSOR:
case LPP_FREQUENCY:
case LPP_DISTANCE:
case LPP_ENERGY:
case LPP_UNIXTIME:
_pos += 4; break;
case LPP_COLOUR:
_pos += 3; break;
case LPP_ANALOG_INPUT:
case LPP_ANALOG_OUTPUT:
case LPP_LUMINOSITY:
case LPP_TEMPERATURE:
case LPP_CONCENTRATION:
case LPP_BAROMETRIC_PRESSURE:
case LPP_ALTITUDE:
case LPP_VOLTAGE:
case LPP_CURRENT:
case LPP_DIRECTION:
case LPP_POWER:
_pos += 2; break;
default:
_pos++;
}
}
};
class LPPWriter {
uint8_t* _buf;
uint8_t _max_len;
uint8_t _len;
void write(uint16_t value) {
_buf[_len++] = (value >> 8) & 0xFF; // MSB
_buf[_len++] = value & 0xFF; // LSB
}
public:
LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { }
bool writeVoltage(uint8_t channel, float voltage) {
if (_len + 4 <= _max_len) {
_buf[_len++] = channel;
_buf[_len++] = LPP_VOLTAGE;
uint16_t value = voltage * 100;
write(value);
return true;
}
return false;
}
bool writeGPS(uint8_t channel, float lat, float lon, float alt) {
if (_len + 11 <= _max_len) {
_buf[_len++] = channel;
_buf[_len++] = LPP_GPS;
int32_t lati = lat * 10000; // we lose some precision :-(
int32_t loni = lon * 10000;
int32_t alti = alt * 100;
_buf[_len++] = lati >> 16;
_buf[_len++] = lati >> 8;
_buf[_len++] = lati;
_buf[_len++] = loni >> 16;
_buf[_len++] = loni >> 8;
_buf[_len++] = loni;
_buf[_len++] = alti >> 16;
_buf[_len++] = alti >> 8;
_buf[_len++] = alti;
return true;
}
return false;
}
uint8_t length() { return _len; }
};

View File

@@ -17,8 +17,9 @@ public:
virtual bool isValid() = 0;
virtual long getTimestamp() = 0;
virtual void sendSentence(const char * sentence);
virtual void reset();
virtual void begin();
virtual void stop();
virtual void loop();
virtual void reset() = 0;
virtual void begin() = 0;
virtual void stop() = 0;
virtual void loop() = 0;
virtual bool isEnabled() = 0;
};

View File

@@ -3,17 +3,34 @@
#include "LocationProvider.h"
#include <MicroNMEA.h>
#include <RTClib.h>
#include <helpers/RefCountedDigitalPin.h>
#ifndef GPS_EN
#define GPS_EN (-1)
#ifdef PIN_GPS_EN
#define GPS_EN PIN_GPS_EN
#else
#define GPS_EN (-1)
#endif
#endif
#ifndef PIN_GPS_EN_ACTIVE
#define PIN_GPS_EN_ACTIVE HIGH
#endif
#ifndef GPS_RESET
#define GPS_RESET (-1)
#ifdef PIN_GPS_RESET
#define GPS_RESET PIN_GPS_RESET
#else
#define GPS_RESET (-1)
#endif
#endif
#ifndef GPS_RESET_FORCE
#define GPS_RESET_FORCE LOW
#ifdef PIN_GPS_RESET_ACTIVE
#define GPS_RESET_FORCE PIN_GPS_RESET_ACTIVE
#else
#define GPS_RESET_FORCE LOW
#endif
#endif
class MicroNMEALocationProvider : public LocationProvider {
@@ -21,14 +38,15 @@ class MicroNMEALocationProvider : public LocationProvider {
MicroNMEA nmea;
mesh::RTCClock* _clock;
Stream* _gps_serial;
RefCountedDigitalPin* _peripher_power;
int _pin_reset;
int _pin_en;
long next_check = 0;
long time_valid = 0;
public :
MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN) :
_gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en), _clock(clock) {
MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) :
_gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en), _clock(clock), _peripher_power(peripher_power) {
if (_pin_reset != -1) {
pinMode(_pin_reset, OUTPUT);
digitalWrite(_pin_reset, GPS_RESET_FORCE);
@@ -40,26 +58,38 @@ public :
}
void begin() override {
if (_peripher_power) _peripher_power->claim();
if (_pin_en != -1) {
digitalWrite(_pin_en, PIN_GPS_EN_ACTIVE);
}
if (_pin_reset != -1) {
digitalWrite(_pin_reset, !GPS_RESET_FORCE);
}
if (_pin_en != -1) {
digitalWrite(_pin_en, HIGH);
}
}
void reset() override {
if (_pin_reset != -1) {
digitalWrite(_pin_reset, GPS_RESET_FORCE);
delay(100);
delay(10);
digitalWrite(_pin_reset, !GPS_RESET_FORCE);
}
}
void stop() override {
if (_pin_en != -1) {
digitalWrite(_pin_en, LOW);
}
digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE);
}
if (_peripher_power) _peripher_power->release();
}
bool isEnabled() override {
// directly read the enable pin if present as gps can be
// activated/deactivated outside of here ...
if (_pin_en != -1) {
return digitalRead(_pin_en) == PIN_GPS_EN_ACTIVE;
} else {
return true; // no enable so must be active
}
}
void syncTime() override { nmea.clear(); LocationProvider::syncTime(); }
@@ -107,4 +137,4 @@ public :
}
}
}
};
};

View File

@@ -1,6 +1,7 @@
#pragma once
#include <stdint.h>
#include <string.h>
class DisplayDriver {
int _w, _h;
@@ -31,5 +32,69 @@ public:
setCursor(mid_x - w/2, y);
print(str);
}
virtual void drawTextRightAlign(int x_anch, int y, const char* str) {
int w = getTextWidth(str);
setCursor(x_anch - w, y);
print(str);
}
virtual void drawTextLeftAlign(int x_anch, int y, const char* str) {
setCursor(x_anch, y);
print(str);
}
// convert UTF-8 characters to displayable block characters for compatibility
virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {
size_t j = 0;
for (size_t i = 0; src[i] != 0 && j < dest_size - 1; i++) {
unsigned char c = (unsigned char)src[i];
if (c >= 32 && c <= 126) {
dest[j++] = c; // ASCII printable
} else if (c >= 0x80) {
dest[j++] = '\xDB'; // CP437 full block █
while (src[i+1] && (src[i+1] & 0xC0) == 0x80)
i++; // skip UTF-8 continuation bytes
}
}
dest[j] = 0;
}
// draw text with ellipsis if it exceeds max_width
virtual void drawTextEllipsized(int x, int y, int max_width, const char* str) {
char temp_str[256]; // reasonable buffer size
size_t len = strlen(str);
if (len >= sizeof(temp_str)) len = sizeof(temp_str) - 1;
memcpy(temp_str, str, len);
temp_str[len] = 0;
if (getTextWidth(temp_str) <= max_width) {
setCursor(x, y);
print(temp_str);
return;
}
// for variable-width fonts (GxEPD), add space after ellipsis
// for fixed-width fonts (OLED), keep tight spacing to save precious characters
const char* ellipsis;
// use a simple heuristic: if 'i' and 'l' have different widths, it's variable-width
int i_width = getTextWidth("i");
int l_width = getTextWidth("l");
if (i_width != l_width) {
ellipsis = "... "; // variable-width fonts: add space
} else {
ellipsis = "..."; // fixed-width fonts: no space
}
int ellipsis_width = getTextWidth(ellipsis);
int str_len = strlen(temp_str);
while (str_len > 0 && getTextWidth(temp_str) > max_width - ellipsis_width) {
temp_str[--str_len] = 0;
}
strcat(temp_str, ellipsis);
setCursor(x, y);
print(temp_str);
}
virtual void endFrame() = 0;
};

View File

@@ -0,0 +1,38 @@
#ifdef PIN_VIBRATION
#include "GenericVibration.h"
void GenericVibration::begin() {
pinMode(PIN_VIBRATION, OUTPUT);
digitalWrite(PIN_VIBRATION, LOW);
duration = 0;
}
void GenericVibration::trigger() {
duration = millis();
digitalWrite(PIN_VIBRATION, HIGH);
}
void GenericVibration::loop() {
if (isVibrating()) {
if ((millis() / 1000) % 2 == 0) {
digitalWrite(PIN_VIBRATION, LOW);
} else {
digitalWrite(PIN_VIBRATION, HIGH);
}
if (millis() - duration > VIBRATION_TIMEOUT) {
stop();
}
}
}
bool GenericVibration::isVibrating() {
return duration > 0;
}
void GenericVibration::stop() {
duration = 0;
digitalWrite(PIN_VIBRATION, LOW);
}
#endif // ifdef PIN_VIBRATION

View File

@@ -0,0 +1,33 @@
#pragma once
#ifdef PIN_VIBRATION
#include <Arduino.h>
/*
* Vibration motor control class
*
* Provides vibration feedback for events like new messages and new contacts
* Features:
* - 1-second vibration pulse
* - 5-second nag timeout (cooldown between vibrations)
* - Non-blocking operation
*/
#ifndef VIBRATION_TIMEOUT
#define VIBRATION_TIMEOUT 5000 // 5 seconds default
#endif
class GenericVibration {
public:
void begin(); // set up vibration pin
void trigger(); // trigger vibration if cooldown has passed
void loop(); // non-blocking timer handling
bool isVibrating(); // returns true if currently vibrating
void stop(); // stop vibration immediately
private:
unsigned long duration;
};
#endif // ifdef PIN_VIBRATION

View File

@@ -5,9 +5,6 @@
#define DISPLAY_ROTATION 3
#endif
#define SCALE_X 1.5625f // 200 / 128
#define SCALE_Y 1.5625f // 200 / 128
bool GxEPDDisplay::begin() {
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
SPI1.begin();
@@ -19,6 +16,7 @@ bool GxEPDDisplay::begin() {
display.fillScreen(GxEPD_WHITE);
display.display(true);
#if DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, LOW);
pinMode(DISP_BACKLIGHT, OUTPUT);
#endif
_init = true;
@@ -27,14 +25,14 @@ bool GxEPDDisplay::begin() {
void GxEPDDisplay::turnOn() {
if (!_init) begin();
#if DISP_BACKLIGHT
#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN)
digitalWrite(DISP_BACKLIGHT, HIGH);
#endif
_isOn = true;
}
void GxEPDDisplay::turnOff() {
#if DISP_BACKLIGHT
#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN)
digitalWrite(DISP_BACKLIGHT, LOW);
#endif
_isOn = false;
@@ -43,14 +41,17 @@ void GxEPDDisplay::turnOff() {
void GxEPDDisplay::clear() {
display.fillScreen(GxEPD_WHITE);
display.setTextColor(GxEPD_BLACK);
display_crc.reset();
}
void GxEPDDisplay::startFrame(Color bkg) {
display.fillScreen(GxEPD_WHITE);
display.setTextColor(_curr_color = GxEPD_BLACK);
display_crc.reset();
}
void GxEPDDisplay::setTextSize(int sz) {
display_crc.update<int>(sz);
switch(sz) {
case 1: // Small
display.setFont(&FreeSans9pt7b);
@@ -68,6 +69,7 @@ void GxEPDDisplay::setTextSize(int sz) {
}
void GxEPDDisplay::setColor(Color c) {
display_crc.update<Color> (c);
// colours need to be inverted for epaper displays
if (c == DARK) {
display.setTextColor(_curr_color = GxEPD_WHITE);
@@ -77,25 +79,41 @@ void GxEPDDisplay::setColor(Color c) {
}
void GxEPDDisplay::setCursor(int x, int y) {
display.setCursor(x*SCALE_X, (y+10)*SCALE_Y);
display_crc.update<int>(x);
display_crc.update<int>(y);
display.setCursor((x+offset_x)*scale_x, (y+offset_y)*scale_y);
}
void GxEPDDisplay::print(const char* str) {
display_crc.update<char>(str, strlen(str));
display.print(str);
}
void GxEPDDisplay::fillRect(int x, int y, int w, int h) {
display.fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _curr_color);
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display.fillRect(x*scale_x, y*scale_y, w*scale_x, h*scale_y, _curr_color);
}
void GxEPDDisplay::drawRect(int x, int y, int w, int h) {
display.drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _curr_color);
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display.drawRect(x*scale_x, y*scale_y, w*scale_x, h*scale_y, _curr_color);
}
void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display_crc.update<uint8_t>(bits, w * h / 8);
// Calculate the base position in display coordinates
uint16_t startX = x * SCALE_X;
uint16_t startY = y * SCALE_Y;
uint16_t startX = x * scale_x;
uint16_t startY = y * scale_y;
// Width in bytes for bitmap processing
uint16_t widthInBytes = (w + 7) / 8;
@@ -103,15 +121,15 @@ void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
// Process the bitmap row by row
for (uint16_t by = 0; by < h; by++) {
// Calculate the target y-coordinates for this logical row
int y1 = startY + (int)(by * SCALE_Y);
int y2 = startY + (int)((by + 1) * SCALE_Y);
int y1 = startY + (int)(by * scale_y);
int y2 = startY + (int)((by + 1) * scale_y);
int block_h = y2 - y1;
// Scan across the row bit by bit
for (uint16_t bx = 0; bx < w; bx++) {
// Calculate the target x-coordinates for this logical column
int x1 = startX + (int)(bx * SCALE_X);
int x2 = startX + (int)((bx + 1) * SCALE_X);
int x1 = startX + (int)(bx * scale_x);
int x2 = startX + (int)((bx + 1) * scale_x);
int block_w = x2 - x1;
// Get the current bit
@@ -132,9 +150,13 @@ uint16_t GxEPDDisplay::getTextWidth(const char* str) {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return ceil((w + 1) / SCALE_X);
return ceil((w + 1) / scale_x);
}
void GxEPDDisplay::endFrame() {
display.display(true);
uint32_t crc = display_crc.finalize();
if (crc != last_display_crc_value) {
display.display(true);
last_display_crc_value = crc;
}
}

View File

@@ -12,28 +12,37 @@
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
#define GxEPD2_DISPLAY_CLASS GxEPD2_BW
#define GxEPD2_DRIVER_CLASS GxEPD2_150_BN // DEPG0150BN 200x200, SSD1681, (FPC8101), TTGO T5 V2.4.1
#include <epd/GxEPD2_150_BN.h> // 1.54" b/w
#include <CRC32.h>
#include "DisplayDriver.h"
//GxEPD2_BW<GxEPD2_150_BN, 200> display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)); // DEPG0150BN 200x200, SSD1681, TTGO T5 V2.4.1
class GxEPDDisplay : public DisplayDriver {
#if defined(EINK_DISPLAY_MODEL)
GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> display;
const float scale_x = EINK_SCALE_X;
const float scale_y = EINK_SCALE_Y;
const float offset_x = EINK_X_OFFSET;
const float offset_y = EINK_Y_OFFSET;
#else
GxEPD2_BW<GxEPD2_150_BN, 200> display;
const float scale_x = 1.5625f;
const float scale_y = 1.5625f;
const float offset_x = 0;
const float offset_y = 10;
#endif
bool _init = false;
bool _isOn = false;
uint16_t _curr_color;
CRC32 display_crc;
int last_display_crc_value = 0;
public:
// there is a margin in y...
GxEPDDisplay() : DisplayDriver(128, 128), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {
}
#if defined(EINK_DISPLAY_MODEL)
GxEPDDisplay() : DisplayDriver(128, 128), display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
#else
GxEPDDisplay() : DisplayDriver(128, 128), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}
#endif
bool begin();

View File

@@ -0,0 +1,125 @@
#include "LGFXDisplay.h"
bool LGFXDisplay::begin() {
turnOn();
display->init();
display->setRotation(1);
display->setBrightness(64);
display->setColorDepth(8);
display->setTextColor(TFT_WHITE);
buffer.setColorDepth(8);
buffer.setPsram(true);
buffer.createSprite(width(), height());
return true;
}
void LGFXDisplay::turnOn() {
// display->wakeup();
if (!_isOn) {
display->wakeup();
}
_isOn = true;
}
void LGFXDisplay::turnOff() {
if (_isOn) {
display->sleep();
}
_isOn = false;
}
void LGFXDisplay::clear() {
// display->clearDisplay();
buffer.clearDisplay();
}
void LGFXDisplay::startFrame(Color bkg) {
// display->startWrite();
// display->getScanLine();
buffer.clearDisplay();
buffer.setTextColor(TFT_WHITE);
}
void LGFXDisplay::setTextSize(int sz) {
buffer.setTextSize(sz);
}
void LGFXDisplay::setColor(Color c) {
// _color = (c != 0) ? ILI9342_WHITE : ILI9342_BLACK;
switch (c) {
case DARK:
_color = TFT_BLACK;
break;
case LIGHT:
_color = TFT_WHITE;
break;
case RED:
_color = TFT_RED;
break;
case GREEN:
_color = TFT_GREEN;
break;
case BLUE:
_color = TFT_BLUE;
break;
case YELLOW:
_color = TFT_YELLOW;
break;
case ORANGE:
_color = TFT_ORANGE;
break;
default:
_color = TFT_WHITE;
}
buffer.setTextColor(_color);
}
void LGFXDisplay::setCursor(int x, int y) {
buffer.setCursor(x, y);
}
void LGFXDisplay::print(const char* str) {
buffer.println(str);
// Serial.println(str);
}
void LGFXDisplay::fillRect(int x, int y, int w, int h) {
buffer.fillRect(x, y, w, h, _color);
}
void LGFXDisplay::drawRect(int x, int y, int w, int h) {
buffer.drawRect(x, y, w, h, _color);
}
void LGFXDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
buffer.drawBitmap(x, y, bits, w, h, _color);
}
uint16_t LGFXDisplay::getTextWidth(const char* str) {
return buffer.textWidth(str);
}
void LGFXDisplay::endFrame() {
display->startWrite();
if (UI_ZOOM != 1) {
buffer.pushRotateZoom(display, display->width()/2, display->height()/2 , 0, UI_ZOOM, UI_ZOOM);
} else {
buffer.pushSprite(display, 0, 0);
}
display->endWrite();
}
bool LGFXDisplay::getTouch(int *x, int *y) {
lgfx::v1::touch_point_t point;
display->getTouch(&point);
if (UI_ZOOM != 1) {
*x = point.x / UI_ZOOM;
*y = point.y / UI_ZOOM;
} else {
*x = point.x;
*y = point.y;
}
return (*x >= 0) && (*y >= 0);
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include <helpers/ui/DisplayDriver.h>
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
#ifndef UI_ZOOM
#define UI_ZOOM 1
#endif
class LGFXDisplay : public DisplayDriver {
protected:
LGFX_Device* display;
LGFX_Sprite buffer;
bool _isOn = false;
int _color = TFT_WHITE;
public:
LGFXDisplay(int w, int h, LGFX_Device &disp)
: DisplayDriver(w/UI_ZOOM, h/UI_ZOOM), display(&disp) {}
bool begin();
bool isOn() override { return _isOn; }
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(Color bkg = DARK) override;
void setTextSize(int sz) override;
void setColor(Color c) override;
void setCursor(int x, int y) override;
void print(const char* str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
void endFrame() override;
virtual bool getTouch(int *x, int *y);
};

View File

@@ -1,6 +1,8 @@
#include "MomentaryButton.h"
MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup) {
#define MULTI_CLICK_WINDOW_MS 280
MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup, bool multiclick) {
_pin = pin;
_reverse = reverse;
_pull = pulldownup;
@@ -9,6 +11,10 @@ MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse
cancel = 0;
_long_millis = long_press_millis;
_threshold = 0;
_click_count = 0;
_last_click_time = 0;
_multi_click_window = multiclick ? MULTI_CLICK_WINDOW_MS : 0;
_pending_click = false;
}
MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, int analog_threshold) {
@@ -20,6 +26,10 @@ MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, int analog_t
cancel = 0;
_long_millis = long_press_millis;
_threshold = analog_threshold;
_click_count = 0;
_last_click_time = 0;
_multi_click_window = MULTI_CLICK_WINDOW_MS;
_pending_click = false;
}
void MomentaryButton::begin() {
@@ -35,6 +45,10 @@ bool MomentaryButton::isPressed() const {
void MomentaryButton::cancelClick() {
cancel = 1;
down_at = 0;
_click_count = 0;
_last_click_time = 0;
_pending_click = false;
}
bool MomentaryButton::isPressed(int level) const {
@@ -60,13 +74,20 @@ int MomentaryButton::check(bool repeat_click) {
// button UP
if (_long_millis > 0) {
if (down_at > 0 && (unsigned long)(millis() - down_at) < _long_millis) { // only a CLICK if still within the long_press millis
event = BUTTON_EVENT_CLICK;
_click_count++;
_last_click_time = millis();
_pending_click = true;
}
} else {
event = BUTTON_EVENT_CLICK; // any UP results in CLICK event when NOT using long_press feature
_click_count++;
_last_click_time = millis();
_pending_click = true;
}
if (event == BUTTON_EVENT_CLICK && cancel) {
event = BUTTON_EVENT_NONE;
_click_count = 0;
_last_click_time = 0;
_pending_click = false;
}
down_at = 0;
}
@@ -77,8 +98,16 @@ int MomentaryButton::check(bool repeat_click) {
}
if (_long_millis > 0 && down_at > 0 && (unsigned long)(millis() - down_at) >= _long_millis) {
event = BUTTON_EVENT_LONG_PRESS;
down_at = 0;
if (_pending_click) {
// long press during multi-click detection - cancel pending clicks
cancelClick();
} else {
event = BUTTON_EVENT_LONG_PRESS;
down_at = 0;
_click_count = 0;
_last_click_time = 0;
_pending_click = false;
}
}
if (down_at > 0 && repeat_click) {
unsigned long diff = (unsigned long)(millis() - down_at);
@@ -87,5 +116,30 @@ int MomentaryButton::check(bool repeat_click) {
}
}
if (_pending_click && (millis() - _last_click_time) >= _multi_click_window) {
if (down_at > 0) {
// still pressed - wait for button release before processing clicks
return event;
}
switch (_click_count) {
case 1:
event = BUTTON_EVENT_CLICK;
break;
case 2:
event = BUTTON_EVENT_DOUBLE_CLICK;
break;
case 3:
event = BUTTON_EVENT_TRIPLE_CLICK;
break;
default:
// For 4+ clicks, treat as triple click?
event = BUTTON_EVENT_TRIPLE_CLICK;
break;
}
_click_count = 0;
_last_click_time = 0;
_pending_click = false;
}
return event;
}

View File

@@ -5,6 +5,8 @@
#define BUTTON_EVENT_NONE 0
#define BUTTON_EVENT_CLICK 1
#define BUTTON_EVENT_LONG_PRESS 2
#define BUTTON_EVENT_DOUBLE_CLICK 3
#define BUTTON_EVENT_TRIPLE_CLICK 4
class MomentaryButton {
int8_t _pin;
@@ -13,11 +15,15 @@ class MomentaryButton {
int _long_millis;
int _threshold; // analog mode
unsigned long down_at;
uint8_t _click_count;
unsigned long _last_click_time;
int _multi_click_window;
bool _pending_click;
bool isPressed(int level) const;
public:
MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false);
MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true);
MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold);
void begin();
int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*

View File

@@ -24,14 +24,21 @@ bool ST7735Display::begin() {
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
digitalWrite(PIN_TFT_RST, HIGH);
#if defined(HELTEC_TRACKER_V2)
display.initR(INITR_MINI160x80);
display.setRotation(DISPLAY_ROTATION);
uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR
display.sendCommand(ST77XX_MADCTL, &madctl, 1);
#else
display.initR(INITR_MINI160x80_PLUGIN);
display.setRotation(DISPLAY_ROTATION);
#endif
display.setSPISpeed(40000000);
display.fillScreen(ST77XX_BLACK);
display.setTextColor(ST77XX_WHITE);
display.setTextSize(2);
display.cp437(true); // Use full 256 char 'Code Page 437' font
_isOn = true;
}
return true;

View File

@@ -0,0 +1,156 @@
#include "ST7789LCDDisplay.h"
#ifndef DISPLAY_ROTATION
#define DISPLAY_ROTATION 3
#endif
#ifndef DISPLAY_SCALE_X
#define DISPLAY_SCALE_X 2.5f // 320 / 128
#endif
#ifndef DISPLAY_SCALE_Y
#define DISPLAY_SCALE_Y 3.75f // 240 / 64
#endif
#define DISPLAY_WIDTH 240
#define DISPLAY_HEIGHT 320
bool ST7789LCDDisplay::i2c_probe(TwoWire& wire, uint8_t addr) {
return true;
}
bool ST7789LCDDisplay::begin() {
if (!_isOn) {
if (_peripher_power) _peripher_power->claim();
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
digitalWrite(PIN_TFT_RST, HIGH);
// Im not sure if this is just a t-deck problem or not, if your display is slow try this.
#ifdef LILYGO_TDECK
displaySPI.begin(PIN_TFT_SCL, -1, PIN_TFT_SDA, PIN_TFT_CS);
#endif
display.init(DISPLAY_WIDTH, DISPLAY_HEIGHT);
display.setRotation(DISPLAY_ROTATION);
display.setSPISpeed(40e6);
display.fillScreen(ST77XX_BLACK);
display.setTextColor(ST77XX_WHITE);
display.setTextSize(2 * DISPLAY_SCALE_X);
display.cp437(true); // Use full 256 char 'Code Page 437' font
_isOn = true;
}
return true;
}
void ST7789LCDDisplay::turnOn() {
ST7789LCDDisplay::begin();
}
void ST7789LCDDisplay::turnOff() {
if (_isOn) {
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
digitalWrite(PIN_TFT_RST, LOW);
digitalWrite(PIN_TFT_LEDA_CTL, LOW);
_isOn = false;
if (_peripher_power) _peripher_power->release();
}
}
void ST7789LCDDisplay::clear() {
display.fillScreen(ST77XX_BLACK);
}
void ST7789LCDDisplay::startFrame(Color bkg) {
display.fillScreen(ST77XX_BLACK);
display.setTextColor(ST77XX_WHITE);
display.setTextSize(1 * DISPLAY_SCALE_X); // This one affects size of Please wait... message
display.cp437(true); // Use full 256 char 'Code Page 437' font
}
void ST7789LCDDisplay::setTextSize(int sz) {
display.setTextSize(sz * DISPLAY_SCALE_X);
}
void ST7789LCDDisplay::setColor(Color c) {
switch (c) {
case DisplayDriver::DARK :
_color = ST77XX_BLACK;
break;
case DisplayDriver::LIGHT :
_color = ST77XX_WHITE;
break;
case DisplayDriver::RED :
_color = ST77XX_RED;
break;
case DisplayDriver::GREEN :
_color = ST77XX_GREEN;
break;
case DisplayDriver::BLUE :
_color = ST77XX_BLUE;
break;
case DisplayDriver::YELLOW :
_color = ST77XX_YELLOW;
break;
case DisplayDriver::ORANGE :
_color = ST77XX_ORANGE;
break;
default:
_color = ST77XX_WHITE;
break;
}
display.setTextColor(_color);
}
void ST7789LCDDisplay::setCursor(int x, int y) {
display.setCursor(x * DISPLAY_SCALE_X, y * DISPLAY_SCALE_Y);
}
void ST7789LCDDisplay::print(const char* str) {
display.print(str);
}
void ST7789LCDDisplay::fillRect(int x, int y, int w, int h) {
display.fillRect(x * DISPLAY_SCALE_X, y * DISPLAY_SCALE_Y, w * DISPLAY_SCALE_X, h * DISPLAY_SCALE_Y, _color);
}
void ST7789LCDDisplay::drawRect(int x, int y, int w, int h) {
display.drawRect(x * DISPLAY_SCALE_X, y * DISPLAY_SCALE_Y, w * DISPLAY_SCALE_X, h * DISPLAY_SCALE_Y, _color);
}
void ST7789LCDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
uint8_t byteWidth = (w + 7) / 8;
for (int j = 0; j < h; j++) {
for (int i = 0; i < w; i++) {
uint8_t byte = bits[j * byteWidth + i / 8];
bool pixelOn = byte & (0x80 >> (i & 7));
if (pixelOn) {
for (int dy = 0; dy < DISPLAY_SCALE_X; dy++) {
for (int dx = 0; dx < DISPLAY_SCALE_X; dx++) {
display.drawPixel(x * DISPLAY_SCALE_X + i * DISPLAY_SCALE_X + dx, y * DISPLAY_SCALE_Y + j * DISPLAY_SCALE_X + dy, _color);
}
}
}
}
}
}
uint16_t ST7789LCDDisplay::getTextWidth(const char* str) {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w / DISPLAY_SCALE_X;
}
void ST7789LCDDisplay::endFrame() {
// display.display();
}

View File

@@ -0,0 +1,60 @@
#pragma once
#include "DisplayDriver.h"
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <helpers/RefCountedDigitalPin.h>
class ST7789LCDDisplay : public DisplayDriver {
#ifdef LILYGO_TDECK
SPIClass displaySPI;
#endif
Adafruit_ST7789 display;
bool _isOn;
uint16_t _color;
RefCountedDigitalPin* _peripher_power;
bool i2c_probe(TwoWire& wire, uint8_t addr);
public:
#ifdef USE_PIN_TFT
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
display(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL, PIN_TFT_RST),
_peripher_power(peripher_power)
{
_isOn = false;
}
#elif LILYGO_TDECK
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
displaySPI(HSPI),
display(&displaySPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST),
_peripher_power(peripher_power)
{
_isOn = false;
}
#else
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
display(&SPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST),
_peripher_power(peripher_power)
{
_isOn = false;
}
#endif
bool begin();
bool isOn() override { return _isOn; }
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(Color bkg = DARK) override;
void setTextSize(int sz) override;
void setColor(Color c) override;
void setCursor(int x, int y) override;
void print(const char* str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
void endFrame() override;
};

View File

@@ -2,13 +2,17 @@
#include "DisplayDriver.h"
#define KEY_LEFT 0xB4
#define KEY_UP 0xB5
#define KEY_DOWN 0xB6
#define KEY_RIGHT 0xB7
#define KEY_SELECT 10
#define KEY_ENTER 13
#define KEY_BACK 27 // Esc
#define KEY_LEFT 0xB4
#define KEY_UP 0xB5
#define KEY_DOWN 0xB6
#define KEY_RIGHT 0xB7
#define KEY_SELECT 10
#define KEY_ENTER 13
#define KEY_CANCEL 27 // Esc
#define KEY_HOME 0xF0
#define KEY_NEXT 0xF1
#define KEY_PREV 0xF2
#define KEY_CONTEXT_MENU 0xF3
class UIScreen {
protected:

View File

@@ -0,0 +1,136 @@
[Ebyte_EoRa-S3]
extends = esp32_base
board = ebyte_eora-s3
build_flags =
${esp32_base.build_flags}
-I variants/ebyte_eora_s3
-D EBYTE_EORA_S3
-D P_LORA_DIO_1=33
-D P_LORA_NSS=7
-D P_LORA_RESET=8 ; RADIOLIB_NC
-D P_LORA_BUSY=34
-D P_LORA_SCLK=5
-D P_LORA_MISO=3
-D P_LORA_MOSI=6
-D P_LORA_TX_LED=37
-D PIN_VBAT_READ=1
-D PIN_USER_BTN=0
-D PIN_BOARD_SDA=18
-D PIN_BOARD_SCL=17
; SD_DAT0/MISO - GPIO2
; SD_DAT1 - GPIO4
; SD_CMD/MOSI - GPIO11
; SD_DAT2 - GPIO112
; SD_DAT3/CS - GPIO113
; SD_CLK - GPIO114
-D PIN_BOARD_SDA=18
-D PIN_BOARD_SCL=17
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_RX_BOOSTED_GAIN=1
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/ebyte_eora_s3>
lib_deps =
${esp32_base.lib_deps}
adafruit/Adafruit SSD1306 @ ^2.5.13
; === EByte EORA_S3 with SX1262 environments ===
[env:Ebyte_EoRa-S3_Repeater]
extends = Ebyte_EoRa-S3
build_flags =
${Ebyte_EoRa-S3.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"EORA_S3-1262 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Ebyte_EoRa-S3.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
lib_deps =
${Ebyte_EoRa-S3.lib_deps}
${esp32_ota.lib_deps}
[env:Ebyte_EoRa-S3_terminal_chat]
extends = Ebyte_EoRa-S3
build_flags =
${Ebyte_EoRa-S3.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Ebyte_EoRa-S3.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
lib_deps =
${Ebyte_EoRa-S3.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Ebyte_EoRa-S3_room_server]
extends = Ebyte_EoRa-S3
build_flags =
${Ebyte_EoRa-S3.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"EORA_S3-1262 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Ebyte_EoRa-S3.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_room_server>
lib_deps =
${Ebyte_EoRa-S3.lib_deps}
${esp32_ota.lib_deps}
[env:Ebyte_EoRa-S3_companion_radio_usb]
extends = Ebyte_EoRa-S3
build_flags =
${Ebyte_EoRa-S3.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${Ebyte_EoRa-S3.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Ebyte_EoRa-S3.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Ebyte_EoRa-S3_companion_radio_ble]
extends = Ebyte_EoRa-S3
build_flags =
${Ebyte_EoRa-S3.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Ebyte_EoRa-S3.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Ebyte_EoRa-S3.lib_deps}
densaugeo/base64 @ ~1.4.0

View File

@@ -0,0 +1,85 @@
#include <Arduino.h>
#include "target.h"
ESP32Board board;
#if defined(P_LORA_SCLK)
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
#ifdef SX126X_DIO3_TCXO_VOLTAGE
float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
#else
float tcxo = 1.6f;
#endif
#if defined(P_LORA_SCLK)
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
#endif
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo);
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
return false; // fail
}
radio.setCRC(1);
#if defined(SX126X_RXEN) && defined(SX126X_TXEN)
radio.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
#endif
#ifdef SX126X_CURRENT_LIMIT
radio.setCurrentLimit(SX126X_CURRENT_LIMIT);
#endif
#ifdef SX126X_DIO2_AS_RF_SWITCH
radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH);
#endif
#ifdef SX126X_RX_BOOSTED_GAIN
radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN);
#endif
return true; // success
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}

View File

@@ -0,0 +1,29 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/ESP32Board.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
#include <helpers/ui/MomentaryButton.h>
#endif
extern ESP32Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();

View File

@@ -30,7 +30,7 @@ lib_deps =
[env:Generic_E22_sx1262_repeater]
extends = Generic_E22
build_src_filter = ${Generic_E22.build_src_filter}
+<../examples/simple_repeater/main.cpp>
+<../examples/simple_repeater/*.cpp>
build_flags =
${Generic_E22.build_flags}
-D RADIO_CLASS=CustomSX1262
@@ -40,7 +40,55 @@ build_flags =
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=8
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps =
${Generic_E22.lib_deps}
${esp32_ota.lib_deps}
; [env:Generic_E22_sx1262_repeater_bridge_rs232]
; extends = Generic_E22
; build_src_filter = ${Generic_E22.build_src_filter}
; +<helpers/bridges/RS232Bridge.cpp>
; +<../examples/simple_repeater/*.cpp>
; build_flags =
; ${Generic_E22.build_flags}
; -D RADIO_CLASS=CustomSX1262
; -D WRAPPER_CLASS=CustomSX1262Wrapper
; -D LORA_TX_POWER=22
; -D ADVERT_NAME='"RS232 Bridge"'
; -D ADVERT_LAT=0.0
; -D ADVERT_LON=0.0
; -D ADMIN_PASSWORD='"password"'
; -D MAX_NEIGHBOURS=50
; -D WITH_RS232_BRIDGE=Serial2
; -D WITH_RS232_BRIDGE_RX=5
; -D WITH_RS232_BRIDGE_TX=6
; -D BRIDGE_DEBUG=1
; ; -D MESH_PACKET_LOGGING=1
; ; -D MESH_DEBUG=1
; lib_deps =
; ${Generic_E22.lib_deps}
; ${esp32_ota.lib_deps}
[env:Generic_E22_sx1262_repeater_bridge_espnow]
extends = Generic_E22
build_src_filter = ${Generic_E22.build_src_filter}
+<helpers/bridges/ESPNowBridge.cpp>
+<../examples/simple_repeater/*.cpp>
build_flags =
${Generic_E22.build_flags}
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D ADVERT_NAME='"ESPNow Bridge"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_ESPNOW_BRIDGE=1
; -D BRIDGE_DEBUG=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps =
@@ -50,7 +98,7 @@ lib_deps =
[env:Generic_E22_sx1268_repeater]
extends = Generic_E22
build_src_filter = ${Generic_E22.build_src_filter}
+<../examples/simple_repeater/main.cpp>
+<../examples/simple_repeater/*.cpp>
build_flags =
${Generic_E22.build_flags}
-D RADIO_CLASS=CustomSX1268
@@ -60,7 +108,55 @@ build_flags =
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=8
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps =
${Generic_E22.lib_deps}
${esp32_ota.lib_deps}
; [env:Generic_E22_sx1268_repeater_bridge_rs232]
; extends = Generic_E22
; build_src_filter = ${Generic_E22.build_src_filter}
; +<helpers/bridges/RS232Bridge.cpp>
; +<../examples/simple_repeater/*.cpp>
; build_flags =
; ${Generic_E22.build_flags}
; -D RADIO_CLASS=CustomSX1268
; -D WRAPPER_CLASS=CustomSX1268Wrapper
; -D LORA_TX_POWER=22
; -D ADVERT_NAME='"RS232 Bridge"'
; -D ADVERT_LAT=0.0
; -D ADVERT_LON=0.0
; -D ADMIN_PASSWORD='"password"'
; -D MAX_NEIGHBOURS=50
; -D WITH_RS232_BRIDGE=Serial2
; -D WITH_RS232_BRIDGE_RX=5
; -D WITH_RS232_BRIDGE_TX=6
; -D BRIDGE_DEBUG=1
; ; -D MESH_PACKET_LOGGING=1
; ; -D MESH_DEBUG=1
; lib_deps =
; ${Generic_E22.lib_deps}
; ${esp32_ota.lib_deps}
[env:Generic_E22_sx1268_repeater_bridge_espnow]
extends = Generic_E22
build_src_filter = ${Generic_E22.build_src_filter}
+<helpers/bridges/ESPNowBridge.cpp>
+<../examples/simple_repeater/*.cpp>
build_flags =
${Generic_E22.build_flags}
-D RADIO_CLASS=CustomSX1268
-D WRAPPER_CLASS=CustomSX1268Wrapper
-D LORA_TX_POWER=22
-D ADVERT_NAME='"ESPNow Bridge"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_ESPNOW_BRIDGE=1
; -D BRIDGE_DEBUG=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps =

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