Compare commits

..

389 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Tested successfully (on T-1000) without this line, advertising now resumes correctly after disconnection (on Iphone)
2025-07-02 23:41:31 +01:00
JQ
ca422bbafb fix ble pin 2025-07-02 14:37:11 -07:00
Normunds Gavars
70a9990f45 Merge branch 'dev' into minewsemi-me25ls01 2025-07-03 00:14:39 +03:00
Normunds Gavars
6440bcaf48 Clean up pins in variant.h 2025-07-03 00:07:50 +03:00
Rob Loranger
ad2e015a5b move rak usr btn to companions
repeaters do not typically have user buttons and
there is only one analog pin available on most, if
not all, base boards. so this allows repeaters to
add custom peripherals or alternate battery signals
2025-07-02 10:24:45 -07:00
JQ
dcb7ffa92e fixing radio include order for heltec paper 2025-07-02 08:32:36 -07:00
recrof
539f99a90f removed unsupported(?) readAltitude 2025-07-02 16:50:47 +02:00
recrof
3832836eb2 EnvironmentSensorManager: add support for SHTC3 and LPS22HB 2025-07-02 16:42:35 +02:00
Scott Powell
0963341f79 Merge branch 'dev' 2025-07-02 21:11:38 +10:00
Scott Powell
483b31665c * ver bump to v1.7.2 2025-07-02 21:11:07 +10:00
Normunds Gavars
af2628bb00 Use EnvironmentSensorManager in Minewsemi variant 2025-07-02 12:27:39 +03:00
ripplebiz
54fd7049df Merge pull request #467 from LitBomb/patch-14
Update faq.md
2025-07-02 15:26:48 +10:00
uncle lit
811ea175fa Update faq.md
- rewrite Linux firmware update with more detailed instructions 
- fix T114 reset typo: double click twice, not once
- add link reference to MeshCore logo on github
- add public channel key for t-deck to the t-deck section
- reference the verbiage "what is meshcore?" on meshcore.co.uk homepage
2025-07-01 22:04:16 -07:00
ripplebiz
79a75b8b0e Merge pull request #465 from fdlamotte/t1000e_preamble_16
t1000e: set preamble to 16 at init
2025-07-01 23:56:24 +10:00
Florent de Lamotte
b80d99edd1 t1000e: set preamble to 16 at init 2025-07-01 15:42:54 +02:00
ripplebiz
1d1bafb3eb Merge pull request #455 from jquatier/wireless-paper
Initial Support for Heltec Wireless Paper
2025-07-01 15:23:58 +10:00
ripplebiz
f8d277de83 Merge pull request #426 from 446564/feat-GH109
allows manual BLE pin even when device has display
2025-07-01 15:09:43 +10:00
ripplebiz
387e2c7e74 Merge pull request #464 from oltaco/lilygo_t3s3-radio.std_init
refactor: lilygo_t3s3 with CustomSX1262::std_init()
2025-07-01 12:26:43 +10:00
Normunds Gavars
6f94c8148a Add Minewsemi ME25LS01 variant 2025-07-01 01:56:34 +03:00
Scott Powell
3dc4607d89 * PAYLOAD_TYPE_PATH: reserving upper 4 bits if 'extra_type' field, for future use 2025-06-30 23:18:16 +10:00
Scott Powell
8c80c10d2a * CustomLR1110::getTimeOnAir(), copied from sx1262 2025-06-30 18:57:24 +10:00
Scott Powell
a72fafcbf1 Merge branch 'main' into dev 2025-06-30 17:07:01 +10:00
ripplebiz
d04fd377b6 Merge pull request #463 from jankowski-t/main
Support MeshAdventurer
2025-06-30 17:06:19 +10:00
ripplebiz
28a38e674b Merge pull request #462 from seagull9000/Refactor-Heltec-Tracker-radiolib-init
Refactor radiolib for Heltec Wireless Tracker
2025-06-30 17:01:55 +10:00
jankowski-t
c91356016b Fixed MeshAdventurer repeater build failing
Update build_src_filter in platformio.ini
2025-06-30 05:40:07 +02:00
jankowski-t
4541380632 Support MeshAdventurer
Added support for MeshAdventurer, including radio, display, button, GPS, voltage reading
2025-06-30 03:46:18 +02:00
taco
c56da5e6aa refactor: lilgo_t3s3 with CustomSX1262::std_init() 2025-06-30 08:51:18 +10:00
seagull9000
1bfa3d338c Refactor radiolib for Heltec Wireless Tracker
Made changes and tested transmission and reception.

Note:
TX to T1000E would not work direct, but always went through a repeater.

TX to two Thinknode M1's went through direct..
2025-06-30 09:48:18 +12:00
ripplebiz
eb4f81f9ae Merge pull request #459 from liquidraver/dev
change fixed preamble to match the new 16
2025-06-30 00:26:19 +10:00
liquidraver
ddbf27c245 change fixed preamble to match the new 16 2025-06-29 15:17:15 +02:00
Scott Powell
f7920114c5 Merge branch 'dev' into double-acks 2025-06-29 21:10:43 +10:00
JQ
ff3e888dfd formatting 2025-06-27 23:30:52 -07:00
JQ
e417c43c30 wireless paper board support 2025-06-27 22:57:49 -07:00
Scott Powell
127f3a7640 Merge branch 'dev' into double-acks 2025-06-25 13:52:11 +10:00
Scott Powell
70252b010c Merge branch 'dev' into double-acks 2025-06-24 13:07:36 +10:00
Scott Powell
b1ca3d1eb1 * new PAYLOAD_TYPE_MULTIPART
* experimental double ACK's (at each hop), sent 300ms apart (direct mode only)
2025-06-23 01:14:08 +10:00
Rob Loranger
f4463154cf allows manual BLE pin even when device has display
also updates remaining variants to use default of
123456 instead of 0 for random pin
2025-06-20 14:20:42 -07:00
254 changed files with 10988 additions and 1532 deletions

5
.gitignore vendored
View File

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

View File

@@ -74,17 +74,7 @@ They can also be managed via LoRa in the mobile app by using the Remote Manageme
## 🛠 Hardware Compatibility
MeshCore is designed for use with:
* Heltec V3 LoRa Boards
* RAK4631
* XiaoS3 WIO (sx1262 combo)
* XiaoC3 (plus external sx126x module)
* LilyGo T3S3
* Heltec T114
* Station G2
* Sensecap T1000e
* Heltec V2
* LilyGo TLora32 v1.6
MeshCore is designed for devices listed in the [MeshCore Flasher](https://flasher.meshcore.co.uk)
## 📜 License
@@ -104,7 +94,7 @@ Here are some general principals you should try to adhere to:
- Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page.
- Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz).
- Join [Andy Kirby's Discord](https://discord.gg/GBxVx2JMAy) to chat with the developers and get help from the community.
- Join [MeshCore Discord](https://discord.gg/BMwCtwHj5V) to chat with the developers and get help from the community.
## RAK Wireless Board Support in PlatformIO

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
# 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
@@ -144,6 +145,16 @@ mkdir -p out
if [[ $1 == "build-firmware" ]]; then
if [ "$2" ]; then
build_firmware $2
else
echo "usage: $0 build-firmware <target>"
exit 1
fi
elif [[ $1 == "build-matching-firmwares" ]]; then
if [ "$2" ]; then
build_all_firmwares_matching $2
else
echo "usage: $0 build-matching-firmwares <build-match-spec>"
exit 1
fi
elif [[ $1 == "build-firmwares" ]]; then
build_firmwares

64
build_as_lib.py Normal file
View File

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

View File

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

View File

@@ -1,7 +1,7 @@
**MeshCore-FAQ**<!-- omit from toc -->
A list of frequently-asked questions and answers for MeshCore
The current version of this MeshCore FAQ is at https://github.com/ripplebiz/MeshCore/blob/main/docs/faq.md.
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 -->
@@ -27,17 +27,20 @@ author: https://github.com/LitBomb<!-- omit from toc -->
- [3.3. Q: What is the password to administer a repeater or a room server?](#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server)
- [3.4. Q: What is the password to join a room server?](#34-q-what-is-the-password-to-join-a-room-server)
- [4. T-Deck Related](#4-t-deck-related)
- [4.1. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?](#41-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode)
- [4.2. Q: Why is my T-Deck Plus not getting any satellite lock?](#42-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock)
- [4.3. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?](#43-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock)
- [4.4. Q: What size of SD card does the T-Deck support?](#44-q-what-size-of-sd-card-does-the-t-deck-support)
- [4.5. Q: How do I get maps on T-Deck?](#45-q-how-do-i-get-maps-on-t-deck)
- [4.6. Q: Where do the map tiles go?](#46-q-where-do-the-map-tiles-go)
- [4.7. Q: How to unlock deeper map zoom and server management features on T-Deck?](#47-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck)
- [4.8. Q: How to decipher the diagnostics screen on T-Deck?](#48-q-how-to-decipher-the-diagnostics-screen-on-t-deck)
- [4.9. Q: The T-Deck sound is too loud?](#49-q-the-t-deck-sound-is-too-loud)
- [4.10. Q: Can you customize the sound?](#410-q-can-you-customize-the-sound)
- [4.11. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?](#411-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts)
- [4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?](#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro)
- [4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?](#42-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode)
- [4.3. Q: Why is my T-Deck Plus not getting any satellite lock?](#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock)
- [4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?](#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock)
- [4.5. Q: What size of SD card does the T-Deck support?](#45-q-what-size-of-sd-card-does-the-t-deck-support)
- [4.6. Q: what is the public key for the default public channel?](#46-q-what-is-the-public-key-for-the-default-public-channel)
- [4.7. Q: How do I get maps on T-Deck?](#47-q-how-do-i-get-maps-on-t-deck)
- [4.8. Q: Where do the map tiles go?](#48-q-where-do-the-map-tiles-go)
- [4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?](#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck)
- [4.10. Q: How to decipher the diagnostics screen on T-Deck?](#410-q-how-to-decipher-the-diagnostics-screen-on-t-deck)
- [4.11. Q: The T-Deck sound is too loud?](#411-q-the-t-deck-sound-is-too-loud)
- [4.12. Q: Can you customize the sound?](#412-q-can-you-customize-the-sound)
- [4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?](#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts)
- [4.14. Q: How to capture a screenshot on T-Deck?](#414-q-how-to-capture-a-screenshot-on-t-deck)
- [5. General](#5-general)
- [5.1. Q: What are BW, SF, and CR?](#51-q-what-are-bw-sf-and-cr)
- [5.2. Q: Do MeshCore clients repeat?](#52-q-do-meshcore-clients-repeat)
@@ -65,18 +68,22 @@ author: https://github.com/LitBomb<!-- omit from toc -->
- [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.7. Q: WebFlasher fails on Linux with failed to open](#67-q-webflasher-fails-on-linux-with-failed-to-open)
- [7. Other Questions:](#7-other-questions)
- [7.1 Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?](#71-q-how-to-update-nrf-rak-t114-seed-xiao-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app)
- [7.2 Q: How to update ESP32-based devices over the air?](#72-q-how-to-update-esp32-based-devices-over-the-air)
- [7.3 Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?](#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu)
- [7.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)
## 1. Introduction
### 1.1. Q: What is MeshCore?
**A:** MeshCore is free and open source
**A:** MeshCore is a multi platform system for enabling secure text based communications utilising LoRa radio hardware. It can be used for Off-Grid Communication, Emergency Response & Disaster Recovery, Outdoor Activities, Tactical Security including law enforcement and private security and also IoT sensor networks. ([source](https://meshcore.co.uk/))
MeshCore is free and open source:
* MeshCore is the routing and firmware etc, available on GitHub under MIT license
* There are clients made by the community, such as the web clients, these are free to use, and some are open source too
* The cross platform mobile app developed by [Liam Cottle](https://liamcottle.net) for Android/iOS/PC etc is free to download and use
@@ -102,9 +109,11 @@ Anyone is able to build anything they like on top of MeshCore without paying any
You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).
#### 1.2.1. Hardware
To use MeshCore without using a phone as the client interface, you can run MeshCore on a T-Deck or T-Deck Plus. It is a complete off-grid secure communication solution.
MeshCore is available on a variety of 433MHz, 868MHz and 915MHz LoRa devices. For example, Lilygo T-Deck, T-Pager, RAK Wireless WisBlock RAK4631 devices (e.g. 19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Nano G2 Ultra, Seeed Studio T1000-E. More devices are being added regularly.
MeshCore is also available on a variety of 868MHz and 915MHz LoRa devices. For example, RAK4631 devices (19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Seeed Studio T1000-E. More devices will be supported later.
For an up-to-date list of supported devices, please go to https://flasher.meshcore.co.uk/
To use MeshCore without using a phone as the client interface, you can run MeshCore on a LiLygo's T-Deck, T-Deck Plus, T-Pager, T-Watch, or T-Display Pro. MeshCore Ultra firmware running on these devices are a complete off-grid secure communication solution.
#### 1.2.2. Firmware
MeshCore has four firmware types that are not available on other LoRa systems. MeshCore has the following:
@@ -113,7 +122,7 @@ MeshCore has four firmware types that are not available on other LoRa systems. M
Companion radios are for connecting to the Android app or web app as a messenger client. There are two different companion radio firmware versions:
1. **BLE Companion**
BLE Companion firmware runs on a supported LoRa device and connects to a smart device running the Android MeshCore client over BLE (iOS MeshCore client will be available soon)
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**
@@ -135,16 +144,18 @@ A room server can be remotely administered using a T-Deck running the MeshCore f
When a client logs into a room server, the client will receive the previously 32 unseen messages.
A room server can also take on the repeater role. To enable repeater role on a room server, use this command:
Although room server can also repeat with the command line command `set repeat on`, it is not recommended nor encouraged. A room server with repeat set to `on` lacks the full set of repeater and remote administration features that are only available in the repeater firmware.
The recommendation is to run repeater and room server on separate devices for the best experience.
`set repeat {on|off}`
---
## 2. Initial Setup
### 2.1. Q: How many devices do I need to start using MeshCore?
**A:** If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android client via Bluetooth (iOS client will be available later). You can start communicating with other MeshCore users near you.
**A:** If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android or iOS client via Bluetooth. You can start communicating with other MeshCore users near you.
If you have two supported devices, and there are not many MeshCore users near you, flash both to BLE Companion firmware so you can use your devices to communicate with your near-by friends and family.
@@ -154,7 +165,7 @@ After you flashed the latest firmware onto your repeater device, keep the device
`set freq {frequency}`
The repeater and room server CLI reference is here: https://github.com/ripplebiz/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
The repeater and room server CLI reference is here: https://github.com/meshcore-dev/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
If you have more supported devices, you can use your additional devices with the room server firmware.
@@ -176,7 +187,7 @@ The T-Deck firmware is free to download and most features are available without
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.
[Source]([https://](https://discord.com/channels/826570251612323860/1330643963501351004/1356540643853209641))
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1356540643853209641)
the rest of the radio settings are the same for all frequencies:
- Spread Factor (SF): 11
@@ -194,10 +205,12 @@ MeshCore allows you to manually broadcast your name, position and public encrypt
* Zero hop means your advert is broadcasted out to anyone that can hear it, and that's it.
* Flooded means it's broadcasted out and then repeated by all the repeaters that hear it.
MeshCore clients only advertise themselves when the user initiates it. A repeater (and room server?) advertises its presence once every 240 minutes. This interval can be configured using the following command:
MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 3 hours by default. This interval can be configured using the following command:
`set advert.interval {minutes}`
As of Aug 20 2025, a pending PR on github will change the flood advert to 12 hours to minimize airtime utilization caused by repeaters' flood adverts.
### 2.5. Q: Is there a hop limit?
**A:** Internally the firmware has maximum limit of 64 hops. In real world settings it will be difficult to get close to the limit due to the environments and timing as packets travel further and further. We want to hear how far your MeshCore conversations go.
@@ -252,7 +265,11 @@ You can get the latitude and longitude from Google Maps by right-clicking the lo
## 4. T-Deck Related
### 4.1. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?
### 4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?
**A:** Yes, it is available on https://buymeacoffee.com/ripplebiz/ultra-v7-7-guide-meshcore-users
### 4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?
**A:**
1. Device off
2. Connect USB cable to device
@@ -263,20 +280,31 @@ You can get the latitude and longitude from Google Maps by right-clicking the lo
7. T-Deck in DFU mode now
8. At this point you can begin flashing using <https://flasher.meshcore.co.uk/>
### 4.2. Q: Why is my T-Deck Plus not getting any satellite lock?
### 4.3. Q: Why is my T-Deck Plus not getting any satellite lock?
**A:** For T-Deck Plus, the GPS baud rate should be set to **38400**. Also, some T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open the device to check the GPS orientation.
GPS on T-Deck is always enabled. You can skip the "GPS clock sync" and the T-Deck will continue to try to get a GPS lock. You can go to the `GPS Info` screen; you should see the `Sentences:` counter increasing if the baud rate is correct.
[Source]([https://](https://discord.com/channels/826570251612323860/1330643963501351004/1356609240302616689))
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1356609240302616689)
### 4.3. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?
### 4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?
**A:** The OG (non-Plus) T-Deck doesn't come with a GPS. If you added a GPS to your OG T-Deck, please refer to the manual of your GPS to see what baud rate it requires. Alternatively, you can try to set the baud rate from 9600, 19200, etc., and up to 115200 to see which one works.
### 4.4. Q: What size of SD card does the T-Deck support?
### 4.5. Q: What size of SD card does the T-Deck support?
**A:** Users have had no issues using 16GB or 32GB SD cards. Format the SD card to **FAT32**.
### 4.5. Q: How do I get maps on T-Deck?
### 4.6. Q: what is the public key for the default public channel?
**A:**
T-Deck uses the same key the smartphone apps use but in base64
`izOH6cXN6mrJ5e26oRXNcg==`
The third character is the capital letter 'O', not zero `0`
The smartphone app key is in hex:
` 8b3387e9c5cdea6ac9e5edbaa115cd72`
[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)
- <https://buymeacoffee.com/ripplebiz/e/342542> (US)
@@ -290,19 +318,20 @@ There is also a modified script that adds additional error handling and parallel
UK map tiles are available separately from Andy Kirby on his discord server:
<https://discord.com/channels/826570251612323860/1330643963501351004/1331346597367386224>
### 4.6. Q: Where do the map tiles go?
### 4.8. Q: Where do the map tiles go?
Once you have the tiles downloaded, copy the `\tiles` folder to the root of your T-Deck's SD card.
### 4.7. Q: How to unlock deeper map zoom and server management features on T-Deck?
### 4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?
**A:** You can download, install, and use the T-Deck firmware for free, but it has some features (map zoom, server administration) that are enabled if you purchase an unlock code for \$10 per T-Deck device.
Unlock page: <https://buymeacoffee.com/ripplebiz/e/249834>
### 4.8. Q: How to decipher the diagnostics screen on T-Deck?
### 4.10. Q: How to decipher the diagnostics screen on T-Deck?
**A: ** Space is tight on T-Deck's screen, so the information is a bit cryptic. The format is :
`{hops} l:{packet-length}({payload-len}) t:{packet-type} snr:{n} rssi:{n}`
See here for packet-type: [https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19](https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19 "https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19")
See here for packet-type:
https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19
#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
@@ -316,15 +345,25 @@ See here for packet-type: [https://github.com/ripplebiz/MeshCore/blob/main/src/P
[Source](https://discord.com/channels/1343693475589263471/1343693475589263474/1350611321040932966)
### 4.9. Q: The T-Deck sound is too loud?
### 4.10. Q: Can you customize the sound?
### 4.11. Q: The T-Deck sound is too loud?
### 4.12. Q: Can you customize the sound?
**A:** You can customise the sounds on the T-Deck, just by placing `.mp3` files onto the `root` dir of the SD card. `startup.mp3`, `alert.mp3` and `new-advert.mp3`
**A:** You can customise the sounds on the T-Deck, by placing `.mp3` files onto the `root` dir of the SD card. The files are:
### 4.11. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?
* `startup.mp3`
* `error.mp3`
* `alert.mp3`
* `new-advert.mp3`
* `existing-advert.mp3`
### 4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?
**A:** 'Import from Clipboard' is for importing a contact via a file named 'clipboard.txt' on the SD card. The opposite, is in the Identity screen, the 'Card to Clipboard' menu, which writes to 'clipboard.txt' so you can share yourself (call these 'biz cards', that start with "meshcore://...")
### 4.14. Q: How to capture a screenshot on T-Deck?
**A:** To capture a screenshot on a T-Deck, long press the top-left corner of the screen. The screenshot is saved to the microSD card, if one is inserted into the device.
---
## 5. General
@@ -379,10 +418,10 @@ The third character is the capital letter 'O', not zero `0`
### 5.7. Q: Is MeshCore open source?
**A:** Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps.
- Firmware repo: <https://github.com/ripplebiz/MeshCore>
- Firmware repo: https://github.com/meshcore-dev/MeshCore
### 5.8. Q: How can I support MeshCore?
**A:** Provide your honest feedback on GitHub and on AndyKirby's Discord server <http://discord.com/invite/H62Re4DCeD>. Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at <https://buymeacoffee.com/ripplebiz>.
**A:** Provide your honest feedback on GitHub and on [MeshCore Discord server](https://discord.gg/BMwCtwHj5V). Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at <https://buymeacoffee.com/ripplebiz>.
Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase
@@ -443,52 +482,76 @@ This could change in the future if MeshCore develops a client firmware that repe
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1354780032140054659)
### 5.12. Q: How do I add a node to the [MeshCore Map]([url](https://meshcore.co.uk/map.html))
**A:** From the smartphone app, connect to a BLE Companion radio
- To add the BLE Companion radio your smartphone is connected to to the map, tap the `advert` icon, then tap `Advert (To Clipboard)`.
- To add a Repeater or Room Server to the map, tap the 3 dots next to the Repeater or Room Server you want to add to the map, then tap `Share (To Clipboard)`.
- Go to the [MeshCore Map web site]([url](https://meshcore.co.uk/map.html)), tap the plus sign on the lower right corner and paste in the meshcore://... blob, then tap `Add Node`
**A:**
To add a BLE Companion radio, connect to the BLE Companion radio from the MeshCore smartphone app. In the app, tap the `3 dot` menu icon at the top right corner, then tap `Internet Map`. Tap the `3 dot` menu icon again and choose `Add me to the Map`
To add a Repeater or Room Server to the map, go to the Contact List, tap the `3 dot` next to the Repeater or Room Server you want to add to the Internet Map, tap `Share`, then tap `Upload to Internet Map`.
You can use the same companion (same public key) that you used to add your repeaters or room servers to remove them from the Internet Map.
### 5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?
** A:** Yes.
You will need to install picocom on the pi.
`sudo apt install picocom`
Below are the instructions to flash firmware onto a supported LoRa device using a Raspberry Pi over USB serial.
Then run the following commands to setup the repeater.
```
picocom -b 115200 /dev/ttyUSB0 --imap lfcrlf
set name your_repeater_name
time epoch_time
password your_unique_password
set advert.interval 240
advert
```
Note: If using a RAK the path will most likely be /dev/ttyACM0
> Instructions for nRF devices like RAK, T1000-E, T114 are immediately after the ESP instructions
Epoch time comes from https://www.epochconverter.com/
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`
- 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.
- Right click on the file name and copy the link and note it for later use here is an example: `https://flasher.meshcore.dev/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin`
- Run:
- `wget https://flasher.meshcore.dev/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin` to download the firmware file for your device type. or the version you need - USB, BLE, Repeater, Room Server, merged bin or non-merged bin
- If the above wget command only downloads a very small file (10K bytes instead of more than 100K byte, use this command instead:
- `wget --user-agent="Mozilla/5.0" --content-disposition "https://flasher.meshcore.dev/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_usb-v1.7.1-165fb33.bin"`
- Confirm the `ttyXXXX` device path on your Raspberry Pi:
- Go to `/dev` directory, run ls command to find confirm your device path
- They are usually `/dev/ttyUSB0` for ESP devices
- For ESP-based devices, install esptool from the shell:
- `pip install esptool --break-system-packages`
- To flash, use the following command:
- For non-merged bin:
- `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`
You can also flash the repeater using esptool. You will need to install esptool with the following command...
`pip install esptool --break-system-packages`
**Instructions for nRF devices:**
Then to flash the firmware to Heltec, obtain the .bin file from https://flasher.meshcore.co.uk/ (download all firmware link)
For nRF devices (e.g. RAK, Heltec T114) you need the following:
- 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
- You need the ZIP version for the adafruit flash tool (below)
- Click the Download button, right click on the ZIP file, for example:
- `RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip`
- Right click on the file name and copy the link and note it for later use here is an example: `https://flasher.meshcore.dev/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip`
- Run:
- `wget https://flasher.meshcore.dev/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip` to download the firmware file for your device type. or the version you need - USB, BLE, Repeater, Room Server, ZIP file only
- Confirm the `ttyXXXX` device path on your Raspberry Pi:
- Go to `/dev` directory, run ls command to find confirm your device path
- They are usually `/dev/ttyACM0` for nRF devices
- For nRF-based devices, install adafruit-nrfutil
- `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`
For Heltec:
`esptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x00000 firmware.bin`
To start managing your USB serial-connected device using picocom, use the following command:
- `picocom -b 115200 /dev/ttyUSB0 --imap lfcrlf`
If flashing a visual studio code build bin file, flash with the following offset:
`esptool.py -p /dev/ttyUSB0 --chip esp32-s3 write_flash 0x10000 firmware.bin`
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
For Pi
Download the zip from the online flasher website and use the following command:
Note: Requires adafruit-nrfutil command which can be installed as follows.
`pip install adafruit-nrfutil --break-system-packages`
```
adafruit-nrfutil --verbose dfu serial --package t1000_e_bootloader-0.9.1-5-g488711a_s140_7.3.0.zip -p /dev/ttyACM0 -b 115200 --singlebank --touch 1200
```
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1342120825251299388)
### 5.14. Q: Are there are projects built around MeshCore?
@@ -541,9 +604,9 @@ You can get the epoch time on <https://www.epochconverter.com/> and use it to se
**A:**
1. Connect USB-C cable to your device, per your device's instruction, get it to flash mode:
- For RAK, double click its reset button
- For T1000-e, quickly disconnect and reconnect the magnetic side of the cable from the device TWICE
- For Heltec T114, click the reset button once (the bottom button)
- 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
@@ -569,7 +632,7 @@ Allow the browser user on it:
---
## 7. Other Questions:
### 7.1 Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?
### 7.1. Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?
**A:** The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:
@@ -588,7 +651,7 @@ Allow the browser user on it:
14. Wait for the update to complete. It can take a few minutes.
### 7.2 Q: How to update ESP32-based devices over the air?
### 7.2. Q: How to update ESP32-based devices over the air?
**A:** For ESP32-based devices (e.g. Heltec V3):
1. On flasher.meshcore.co.uk, download the **non-merged** version of the firmware for your ESP32 device (e.g. `Heltec_v3_repeater-v1.6.2-4449fd3.bin`, no `"merged"` in the file name)
@@ -600,7 +663,7 @@ Allow the browser user on it:
8. From a browser, go to http://192.168.4.1/update and upload the non-merged bin from the flasher
### 7.3 Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?
### 7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?
**A:** Yes, developer `che aporeps` has an enhanced OTA DFU bootloader for nRF52 based devices. With this bootloader, if it detects that the application firmware is invalid, it falls back to OTA DFU mode so you can attempt to flash again to recover. This bootloader has other changes to make the OTA DFU process more fault tolerant.
@@ -612,7 +675,29 @@ Currently, the following boards are supported:
- Seeed Studio XIAO nRF52840 BLE SENSE
- RAK 4631
### 7.4. Q: are the MeshCore logo and font available?
**A:** Yes, it is on the MeshCore github repo here:
https://github.com/meshcore-dev/MeshCore/tree/main/logo
### 7.5. Q: What is the format of a contact or channel QR code?
**A:**
Channel:
`meshcore://channel/add?name=<name>&secret=<secret>`
Contact:
`meshcore://contact/add?name=<name>&public_key=<secret>&type=<type>`
where `&type` is:
`chat = 1`
`repeater = 2`
`room = 3`
`sensor = 4`
### 7.6. Q: How do I connect to the comnpanion via WIFI, e.g. using a heltec v3?
**A:**
WiFi firmware requires you to compile it yourself, as you need to set the wifi ssid and password.
Edit WIFI_SSID and WIFI_PWD in `./variants/heltec_v3/platformio.ini` and then flash it to your device.
---

View File

@@ -1,11 +1,12 @@
# Packet Structure
| Field | Size (bytes) | Description |
|----------|----------------------------------|-----------------------------------------------------------|
| header | 1 | Contains routing type, payload type, and payload version. |
| path_len | 1 | Length of the path field in bytes. |
| path | up to 64 (`MAX_PATH_SIZE`) | Stores the routing path if applicable. |
| payload | up to 184 (`MAX_PACKET_PAYLOAD`) | The actual data being transmitted. |
| Field | Size (bytes) | Description |
|-----------------|----------------------------------|-----------------------------------------------------------|
| header | 1 | Contains routing type, payload type, and payload version. |
| transport_codes | 4 (optional) | 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*) |
| path_len | 1 | Length of the path field in bytes. |
| path | up to 64 (`MAX_PATH_SIZE`) | Stores the routing path if applicable. |
| payload | up to 184 (`MAX_PACKET_PAYLOAD`) | The actual data being transmitted. |
Note: see the [payloads doc](./payloads.md) for more information about the content of payload.
@@ -42,6 +43,7 @@ bit 0 means the lowest bit (1s place)
| `0x07` | `PAYLOAD_TYPE_ANON_REQ` | Anonymous request. |
| `0x08` | `PAYLOAD_TYPE_PATH` | Returned path. |
| `0x09` | `PAYLOAD_TYPE_TRACE` | trace a path, collecting SNI for each hop. |
| `0x0A` | `PAYLOAD_TYPE_MULTIPART` | packet is part of a sequence of packets. |
| `0x0F` | `PAYLOAD_TYPE_RAW_CUSTOM` | Custom packet (raw bytes, custom encryption). |
## Payload Version Values

View File

@@ -10,13 +10,16 @@ Inside of each [meshcore packet](./packet_structure.md) is a payload, identified
* Anonymous request.
* Group text message (unverified).
* Group datagram (unverified).
* Multi-part packet
* Custom packet (raw bytes, custom encryption).
This document defines the structure of each of these payload types
This document defines the structure of each of these payload types.
NOTE: all 16 and 32-bit integer fields are Little Endian.
## Important concepts:
* Node/channel hash: the first byte of the node or channel's public key
* Node hash: the first byte of the node's public key
# Node advertisement
This kind of payload notifies receivers that a node exists, and gives information about the node
@@ -33,10 +36,10 @@ Appdata
| Field | Size (bytes) | Description |
|---------------|-----------------|-------------------------------------------------------|
| flags | 1 | specifies which of the fields are present, see below |
| latitude | 4 | decimal latitude multiplied by 1000000, integer |
| longitude | 4 | decimal longitude multiplied by 1000000, integer |
| feature 1 | 2 | reserved for future use |
| feature 2 | 2 | reserved for future use |
| latitude | 4 (optional) | decimal latitude multiplied by 1000000, integer |
| longitude | 4 (optional) | decimal longitude multiplied by 1000000, integer |
| feature 1 | 2 (optional) | reserved for future use |
| feature 2 | 2 (optional) | reserved for future use |
| name | rest of appdata | name of the node |
Appdata Flags
@@ -46,6 +49,7 @@ Appdata Flags
| `0x01` | is chat node | advert is for a chat node |
| `0x02` | is repeater | advert is for a repeater |
| `0x03` | is room server | advert is for a room server |
| `0x04` | is sensor | advert is for a sensor server |
| `0x10` | has location | appdata contains lat/long information |
| `0x20` | has feature 1 | Reserved for future use. |
| `0x40` | has feature 2 | Reserved for future use. |
@@ -92,13 +96,15 @@ Returned path messages provide a description of the route a packet took from the
Request type
| Value | Name | Description |
|--------|--------------------|---------------------------------------|
| `0x01` | get status | get status of repeater or room server |
| `0x02` | keepalive | TODO |
| `0x03` | get telemetry data | TODO |
| Value | Name | Description |
|--------|----------------------|---------------------------------------|
| `0x01` | get stats | get stats of repeater or room server |
| `0x02` | keepalive | (deprecated) |
| `0x03` | get telemetry data | TODO |
| `0x04` | get min,max,avg data | sensor nodes - get min, max, average for given time span |
| `0x05` | get access list | get node's approved access list |
### Get status
### Get stats
Gets information about the node, possibly including the following:
@@ -121,10 +127,6 @@ Gets information about the node, possibly including the following:
* Number posted (?)
* Number of post pushes (?)
### Keepalive
No-op request.
### Get telemetry data
Request data about sensors on the node, including battery level.
@@ -138,11 +140,11 @@ Request data about sensors on the node, including battery level.
## Plain text message
| Field | Size (bytes) | Description |
|--------------|-----------------|--------------------------------------------------------------|
| timestamp | 4 | send time (unix timestamp) |
| flags + TODO | 1 | first six bits are flags (see below), last two bits are TODO |
| message | rest of payload | the message content, see next table |
| Field | Size (bytes) | Description |
|-----------------|-----------------|--------------------------------------------------------------|
| timestamp | 4 | send time (unix timestamp) |
| 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 |
Flags
@@ -150,7 +152,7 @@ Flags
|--------|---------------------------|------------------------------------------------------------|
| `0x00` | plain text message | the plain text of the message |
| `0x01` | CLI command | the command text of the message |
| `0x02` | signed plain text message | two bytes of sender prefix, followed by plain text message |
| `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
# Anonymous request
@@ -166,14 +168,14 @@ Plaintext message
| Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------|
| timestamp | 4 | send time (unix timestamp) |
| sync timestamp | 4 | for room server, otherwise absent: sender's "sync messages SINCE x" timestamp |
| sync timestamp | 4 | NOTE: room server only! - sender's "sync messages SINCE x" timestamp |
| password | rest of message | password for repeater/room |
# Group text message / datagram
| Field | Size (bytes) | Description |
|--------------|-----------------|--------------------------------------------|
| channel hash | 1 | the first byte of the channel's public key |
| channel hash | 1 | first byte of SHA256 of channel's shared key |
| cipher MAC | 2 | MAC for encrypted data in next field |
| ciphertext | rest of payload | encrypted message, see below for details |

View File

@@ -0,0 +1,46 @@
#pragma once
#include <MeshCore.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h>
#include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h>
#include <Arduino.h>
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
#include "NodePrefs.h"
enum class UIEventType {
none,
contactMessage,
channelMessage,
roomMessage,
newContactMessage,
ack
};
class AbstractUITask {
protected:
mesh::MainBoard* _board;
BaseSerialInterface* _serial;
bool _connected;
AbstractUITask(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial) {
_connected = false;
}
public:
void setHasConnection(bool connected) { _connected = connected; }
bool hasConnection() const { return _connected; }
uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); }
bool isSerialEnabled() const { return _serial->isEnabled(); }
void enableSerial() { _serial->enable(); }
void disableSerial() { _serial->disable(); }
virtual void msgRead(int msgcount) = 0;
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0;
virtual void soundBuzzer(UIEventType bet = UIEventType::none) = 0;
virtual void loop() = 0;
};

View File

@@ -154,7 +154,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.read((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1)); // 62
file.read(pad, 1); // 62
file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
@@ -163,7 +163,8 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
file.read(pad, 3); // 77
file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
file.read(pad, 2); // 78
file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.close();
@@ -184,7 +185,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.write((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1)); // 62
file.write(pad, 1); // 62
file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
@@ -193,7 +194,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
file.write(pad, 3); // 77
file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
file.write(pad, 2); // 78
file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.close();

View File

@@ -41,10 +41,15 @@
#define CMD_SEND_TRACE_PATH 36
#define CMD_SET_DEVICE_PIN 37
#define CMD_SET_OTHER_PARAMS 38
#define CMD_SEND_TELEMETRY_REQ 39
#define CMD_SEND_TELEMETRY_REQ 39 // can deprecate this
#define CMD_GET_CUSTOM_VARS 40
#define CMD_SET_CUSTOM_VAR 41
#define CMD_GET_ADVERT_PATH 42
#define CMD_GET_TUNING_PARAMS 43
// NOTE: CMD range 44..49 parked, potentially for WiFi operations
#define CMD_SEND_BINARY_REQ 50
#define CMD_FACTORY_RESET 51
#define CMD_SEND_PATH_DISCOVERY_REQ 52
#define RESP_CODE_OK 0
#define RESP_CODE_ERR 1
@@ -69,6 +74,7 @@
#define RESP_CODE_SIGNATURE 20
#define RESP_CODE_CUSTOM_VARS 21
#define RESP_CODE_ADVERT_PATH 22
#define RESP_CODE_TUNING_PARAMS 23
#define SEND_TIMEOUT_BASE_MILLIS 500
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
@@ -91,6 +97,8 @@
#define PUSH_CODE_TRACE_DATA 0x89
#define PUSH_CODE_NEW_ADVERT 0x8A
#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B
#define PUSH_CODE_BINARY_RESPONSE 0x8C
#define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D
#define ERR_CODE_UNSUPPORTED_CMD 1
#define ERR_CODE_NOT_FOUND 2
@@ -101,10 +109,6 @@
#define MAX_SIGN_DATA_LEN (8 * 1024) // 8K
#ifdef DISPLAY_CLASS
#include "UITask.h"
#endif
void MyMesh::writeOKFrame() {
uint8_t buf[1];
buf[0] = RESP_CODE_OK;
@@ -146,7 +150,7 @@ void MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact) {
_serial->writeFrame(out_frame, i);
}
void MyMesh::updateContactFromFrame(ContactInfo &contact, const uint8_t *frame, int len) {
void MyMesh::updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len) {
int i = 0;
uint8_t code = frame[i++]; // eg. CMD_ADD_UPDATE_CONTACT
memcpy(contact.id.pub_key, &frame[i], PUB_KEY_SIZE);
@@ -160,11 +164,14 @@ void MyMesh::updateContactFromFrame(ContactInfo &contact, const uint8_t *frame,
i += 32;
memcpy(&contact.last_advert_timestamp, &frame[i], 4);
i += 4;
if (i + 8 >= len) { // optional fields
if (len >= i + 8) { // optional fields
memcpy(&contact.gps_lat, &frame[i], 4);
i += 4;
memcpy(&contact.gps_lon, &frame[i], 4);
i += 4;
if (len >= i + 4) {
memcpy(&last_mod, &frame[i], 4);
}
}
}
@@ -204,6 +211,10 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
}
uint8_t MyMesh::getExtraAckTransmitCount() const {
return _prefs.multi_acks;
}
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
if (_serial->isConnected() && len + 3 <= MAX_FRAME_SIZE) {
int i = 0;
@@ -232,7 +243,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
}
} else {
#ifdef DISPLAY_CLASS
ui_task.soundBuzzer(UIEventType::newContactMessage);
if (_ui) _ui->soundBuzzer(UIEventType::newContactMessage);
#endif
}
@@ -252,6 +263,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
}
memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix));
strcpy(p->name, contact.name);
p->recv_timestamp = getRTCClock()->getCurrentTime();
p->path_len = path_len;
memcpy(p->path, path, p->path_len);
@@ -260,6 +272,20 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
static int sort_by_recent(const void *a, const void *b) {
return ((AdvertPath *) b)->recv_timestamp - ((AdvertPath *) a)->recv_timestamp;
}
int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) {
if (max_num > ADVERT_PATH_TABLE_SIZE) max_num = ADVERT_PATH_TABLE_SIZE;
qsort(advert_paths, ADVERT_PATH_TABLE_SIZE, sizeof(advert_paths[0]), sort_by_recent);
for (int i = 0; i < max_num; i++) {
dest[i] = advert_paths[i];
}
return max_num;
}
void MyMesh::onContactPathUpdated(const ContactInfo &contact) {
out_frame[0] = PUSH_CODE_PATH_UPDATED;
memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
@@ -319,13 +345,17 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
_serial->writeFrame(frame, 1);
} else {
#ifdef DISPLAY_CLASS
ui_task.soundBuzzer(UIEventType::contactMessage);
#endif
}
#ifdef DISPLAY_CLASS
ui_task.newMsg(path_len, from.name, text, offline_queue_len);
// we only want to show text messages on display, not cli data
bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
if (should_display && _ui) {
_ui->newMsg(path_len, from.name, text, offline_queue_len);
if (!_serial->isConnected()) {
_ui->soundBuzzer(UIEventType::contactMessage);
}
}
#endif
}
@@ -382,7 +412,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
_serial->writeFrame(frame, 1);
} else {
#ifdef DISPLAY_CLASS
ui_task.soundBuzzer(UIEventType::channelMessage);
if (_ui) _ui->soundBuzzer(UIEventType::channelMessage);
#endif
}
#ifdef DISPLAY_CLASS
@@ -392,7 +422,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
if (getChannel(channel_idx, channel_details)) {
channel_name = channel_details.name;
}
ui_task.newMsg(path_len, channel_name, text, offline_queue_len);
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
#endif
}
@@ -420,6 +450,9 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim
permissions |= cp & TELEM_PERM_ENVIRONMENT;
}
uint8_t perm_mask = ~(data[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
permissions &= perm_mask;
if (permissions & TELEM_PERM_BASE) { // only respond if base permission bit is set
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
@@ -462,6 +495,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
i += 6; // pub_key_prefix
memcpy(&out_frame[i], &tag, 4);
i += 4; // NEW: include server timestamp
out_frame[i++] = data[7]; // NEW (v7): ACL permissions
} else {
out_frame[i++] = PUSH_CODE_LOGIN_FAIL;
out_frame[i++] = 0; // reserved
@@ -484,7 +518,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
memcpy(&out_frame[i], &data[4], len - 4);
i += (len - 4);
_serial->writeFrame(out_frame, i);
} else if (len > 4 && tag == pending_telemetry) { // check for telemetry response
} else if (len > 4 && tag == pending_telemetry) { // check for matching response tag
pending_telemetry = 0;
int i = 0;
@@ -495,9 +529,53 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
memcpy(&out_frame[i], &data[4], len - 4);
i += (len - 4);
_serial->writeFrame(out_frame, i);
} else if (len > 4 && tag == pending_req) { // check for matching response tag
pending_req = 0;
int i = 0;
out_frame[i++] = PUSH_CODE_BINARY_RESPONSE;
out_frame[i++] = 0; // reserved
memcpy(&out_frame[i], &tag, 4); // app needs to match this to RESP_CODE_SENT.tag
i += 4;
memcpy(&out_frame[i], &data[4], len - 4);
i += (len - 4);
_serial->writeFrame(out_frame, i);
}
}
bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) {
uint32_t tag;
memcpy(&tag, extra, 4);
if (tag == pending_discovery) { // check for matching response tag)
pending_discovery = 0;
if (in_path_len > MAX_PATH_SIZE || out_path_len > MAX_PATH_SIZE) {
MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len);
} else {
int i = 0;
out_frame[i++] = PUSH_CODE_PATH_DISCOVERY_RESPONSE;
out_frame[i++] = 0; // reserved
memcpy(&out_frame[i], contact.id.pub_key, 6);
i += 6; // pub_key_prefix
out_frame[i++] = out_path_len;
memcpy(&out_frame[i], out_path, out_path_len);
i += out_path_len;
out_frame[i++] = in_path_len;
memcpy(&out_frame[i], in_path, in_path_len);
i += in_path_len;
// NOTE: telemetry data in 'extra' is discarded at present
_serial->writeFrame(out_frame, i);
}
return false; // DON'T send reciprocal path!
}
}
// let base class handle received path and data
return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len);
}
void MyMesh::onRawDataRecv(mesh::Packet *packet) {
if (packet->payload_len + 4 > sizeof(out_frame)) {
MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len);
@@ -553,14 +631,14 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t
void MyMesh::onSendTimeout() {}
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store)
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui)
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables),
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store) {
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) {
_iter_started = false;
_cli_rescue = false;
offline_queue_len = 0;
app_target_ver = 0;
pending_login = pending_status = pending_telemetry = 0;
clearPendingReqs();
next_ack_idx = 0;
sign_data = NULL;
dirty_contacts_expiry = 0;
@@ -613,10 +691,10 @@ void MyMesh::begin(bool has_display) {
_prefs.cr = constrain(_prefs.cr, 5, 8);
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER);
#ifdef BLE_PIN_CODE
#ifdef BLE_PIN_CODE // 123456 by default
if (_prefs.ble_pin == 0) {
#ifdef DISPLAY_CLASS
if (has_display) {
if (has_display && BLE_PIN_CODE == 123456) {
StdRNG rng;
_active_ble_pin = rng.nextInt(100000, 999999); // random pin each session
} else {
@@ -632,6 +710,7 @@ void MyMesh::begin(bool has_display) {
_active_ble_pin = 0;
#endif
resetContacts();
_store->loadContacts(this);
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
_store->loadChannels(this);
@@ -697,7 +776,7 @@ void MyMesh::handleCmdFrame(size_t len) {
i += 4;
memcpy(&out_frame[i], &lon, 4);
i += 4;
out_frame[i++] = 0; // reserved
out_frame[i++] = _prefs.multi_acks; // new v7+
out_frame[i++] = _prefs.advert_loc_policy;
out_frame[i++] = (_prefs.telemetry_mode_env << 4) | (_prefs.telemetry_mode_loc << 2) |
(_prefs.telemetry_mode_base); // v5+
@@ -870,15 +949,16 @@ void MyMesh::handleCmdFrame(size_t len) {
} else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT && len >= 1 + 32 + 2 + 1) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
uint32_t last_mod = getRTCClock()->getCurrentTime(); // fallback value if not present in cmd_frame
if (recipient) {
updateContactFromFrame(*recipient, cmd_frame, len);
// recipient->lastmod = ?? shouldn't be needed, app already has this version of contact
updateContactFromFrame(*recipient, last_mod, cmd_frame, len);
recipient->lastmod = last_mod;
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
writeOKFrame();
} else {
ContactInfo contact;
updateContactFromFrame(contact, cmd_frame, len);
contact.lastmod = getRTCClock()->getCurrentTime();
updateContactFromFrame(contact, last_mod, cmd_frame, len);
contact.lastmod = last_mod;
contact.sync_since = 0;
if (addContact(contact)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
@@ -957,7 +1037,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if ((out_len = getFromOfflineQueue(out_frame)) > 0) {
_serial->writeFrame(out_frame, out_len);
#ifdef DISPLAY_CLASS
ui_task.msgRead(offline_queue_len);
if (_ui) _ui->msgRead(offline_queue_len);
#endif
} else {
out_frame[0] = RESP_CODE_NO_MORE_MESSAGES;
@@ -1012,6 +1092,13 @@ void MyMesh::handleCmdFrame(size_t len) {
_prefs.airtime_factor = ((float)af) / 1000.0f;
savePrefs();
writeOKFrame();
} else if (cmd_frame[0] == CMD_GET_TUNING_PARAMS) {
uint32_t rx = _prefs.rx_delay_base * 1000, af = _prefs.airtime_factor * 1000;
int i = 0;
out_frame[i++] = RESP_CODE_TUNING_PARAMS;
memcpy(&out_frame[i], &rx, 4); i += 4;
memcpy(&out_frame[i], &af, 4); i += 4;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) {
_prefs.manual_add_contacts = cmd_frame[1];
if (len >= 3) {
@@ -1021,6 +1108,9 @@ void MyMesh::handleCmdFrame(size_t len) {
if (len >= 4) {
_prefs.advert_loc_policy = cmd_frame[3];
if (len >= 5) {
_prefs.multi_acks = cmd_frame[4];
}
}
}
savePrefs();
@@ -1057,6 +1147,9 @@ void MyMesh::handleCmdFrame(size_t len) {
if (_store->saveMainIdentity(identity)) {
self_id = identity;
writeOKFrame();
// re-load contacts, to recalc shared secrets
resetContacts();
_store->loadContacts(this);
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
@@ -1090,7 +1183,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
pending_telemetry = pending_status = 0;
clearPendingReqs();
memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
@@ -1110,7 +1203,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
pending_telemetry = pending_login = 0;
clearPendingReqs();
// FUTURE: pending_status = tag; // match this in onContactResponse()
memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme
out_frame[0] = RESP_CODE_SENT;
@@ -1122,7 +1215,36 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
}
} else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) {
} else if (cmd_frame[0] == CMD_SEND_PATH_DISCOVERY_REQ && cmd_frame[1] == 0 && len >= 2 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[2];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
uint32_t tag, est_timeout;
// 'Path Discovery' is just a special case of flood + Telemetry req
uint8_t req_data[9];
req_data[0] = REQ_TYPE_GET_TELEMETRY_DATA;
req_data[1] = ~(TELEM_PERM_BASE); // NEW: inverse permissions mask (ie. we only want BASE telemetry)
memset(&req_data[2], 0, 3); // reserved
getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique
auto save = recipient->out_path_len; // temporarily force sendRequest() to flood
recipient->out_path_len = -1;
int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout);
recipient->out_path_len = save;
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
clearPendingReqs();
pending_discovery = tag; // match this in onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
memcpy(&out_frame[2], &tag, 4);
memcpy(&out_frame[6], &est_timeout, 4);
_serial->writeFrame(out_frame, 10);
}
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
}
} else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) { // can deprecate, in favour of CMD_SEND_BINARY_REQ
uint8_t *pub_key = &cmd_frame[4];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
@@ -1131,7 +1253,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
pending_status = pending_login = 0;
clearPendingReqs();
pending_telemetry = tag; // match this in onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
@@ -1157,6 +1279,27 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(&out_frame[i], telemetry.getBuffer(), tlen);
i += tlen;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_SEND_BINARY_REQ && len >= 2 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
uint8_t *req_data = &cmd_frame[1 + PUB_KEY_SIZE];
uint32_t tag, est_timeout;
int result = sendRequest(*recipient, req_data, len - (1 + PUB_KEY_SIZE), tag, est_timeout);
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
clearPendingReqs();
pending_req = tag; // match this in onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
memcpy(&out_frame[2], &tag, 4);
memcpy(&out_frame[6], &est_timeout, 4);
_serial->writeFrame(out_frame, 10);
}
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
}
} else if (cmd_frame[0] == CMD_HAS_CONNECTION && len >= 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
if (hasConnectionTo(pub_key)) {
@@ -1312,6 +1455,15 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_NOT_FOUND);
}
} else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) {
bool success = _store->formatFileSystem();
if (success) {
writeOKFrame();
delay(1000);
board.reboot(); // doesn't return
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);
@@ -1487,7 +1639,7 @@ void MyMesh::loop() {
}
#ifdef DISPLAY_CLASS
ui_task.setHasConnection(_serial->isConnected());
if (_ui) _ui->setHasConnection(_serial->isConnected());
#endif
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
#include "UITask.h"
#include <Arduino.h>
#include <helpers/TxtDataHelpers.h>
#include "NodePrefs.h"
#include "MyMesh.h"
#include "../MyMesh.h"
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
@@ -408,8 +407,12 @@ void UITask::handleButtonQuadruplePress() {
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
_sensors->setSettingValue("gps", "0");
soundBuzzer(UIEventType::ack);
sprintf(_alert, "GPS: Disabled");
} else {
_sensors->setSettingValue("gps", "1");
soundBuzzer(UIEventType::ack);
sprintf(_alert, "GPS: Enabled");
}
break;
}

View File

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

View File

@@ -22,11 +22,11 @@
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "29 Jun 2025"
#define FIRMWARE_BUILD_DATE "1 Sep 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.7.1"
#define FIRMWARE_VERSION "v1.8.1"
#endif
#ifndef LORA_FREQ
@@ -98,6 +98,7 @@ struct RepeaterStats {
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 {
@@ -120,7 +121,7 @@ struct NeighbourInfo {
int8_t snr; // multiplied by 4, user should divide to get float value
};
#define CLI_REPLY_DELAY_MILLIS 1000
#define CLI_REPLY_DELAY_MILLIS 600
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
FILESYSTEM* _fs;
@@ -134,6 +135,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
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;
@@ -149,12 +155,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
oldest->id = id;
oldest->out_path_len = -1; // initially out_path is unknown
oldest->last_timestamp = 0;
self_id.calcSharedSecret(oldest->secret, id); // calc ECDH shared secret
return oldest;
}
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr) {
#if MAX_NEIGHBOURS // check if neighbours enabled
#if MAX_NEIGHBOURS // check if neighbours enabled
// find existing neighbour, else use least recently updated
uint32_t oldest_timestamp = 0xFFFFFFFF;
NeighbourInfo* neighbour = &neighbours[0];
@@ -204,16 +209,19 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
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, telemetry);
sensors.querySensors((sender->is_admin ? 0xFF : 0x00) & perm_mask, telemetry);
uint8_t tlen = telemetry.getSize();
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
@@ -340,9 +348,12 @@ protected:
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, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override {
if (type == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
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);
@@ -369,6 +380,7 @@ protected:
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
@@ -500,12 +512,12 @@ protected:
}
uint8_t temp[166];
const char *command = (const char *) &data[5];
char *command = (char *) &data[5];
char *reply = (char *) &temp[5];
if (is_retry) {
*reply = 0;
} else {
_cli.handleCommand(sender_timestamp, command, reply);
handleCommand(sender_timestamp, command, reply);
}
int text_len = strlen(reply);
if (text_len > 0) {
@@ -555,6 +567,7 @@ public:
{
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
@@ -576,13 +589,11 @@ public:
_prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 3; // 3 hours
_prefs.flood_advert_interval = 12; // 12 hours
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
}
CommonCLI* getCLI() { return &_cli; }
void begin(FILESYSTEM* fs) {
mesh::Mesh::begin();
_fs = fs;
@@ -600,14 +611,24 @@ public:
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;
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();
@@ -698,7 +719,32 @@ public:
*dp = 0; // null terminator
}
const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; }
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();
@@ -706,6 +752,18 @@ public:
((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();
@@ -721,6 +779,19 @@ public:
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
@@ -736,7 +807,7 @@ void halt() {
while (1) ;
}
static char command[80];
static char command[160];
void setup() {
Serial.begin(115200);
@@ -817,7 +888,7 @@ void loop() {
if (len > 0 && command[len - 1] == '\r') { // received complete line
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
the_mesh.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}

View File

@@ -22,11 +22,11 @@
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "29 Jun 2025"
#define FIRMWARE_BUILD_DATE "1 Sep 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.7.1"
#define FIRMWARE_VERSION "v1.8.1"
#endif
#ifndef LORA_FREQ
@@ -165,6 +165,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
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++) {
@@ -188,7 +193,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
newClient->id = id;
newClient->out_path_len = -1; // initially out_path is unknown
newClient->last_timestamp = 0;
self_id.calcSharedSecret(newClient->secret, id); // calc ECDH shared secret
return newClient;
}
@@ -294,7 +298,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
// 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;
@@ -322,10 +326,12 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
}
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, telemetry);
sensors.querySensors((sender->permission == RoomPermission::ADMIN ? 0xFF : 0x00) & perm_mask, telemetry);
uint8_t tlen = telemetry.getSize();
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
@@ -334,7 +340,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
}
return 0; // unknown command
}
protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
@@ -425,6 +431,9 @@ protected:
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;
@@ -432,8 +441,8 @@ protected:
return true;
}
void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override {
if (type == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
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
@@ -465,6 +474,7 @@ protected:
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;
@@ -555,7 +565,7 @@ protected:
if (is_retry) {
temp[5] = 0; // no reply
} else {
_cli.handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]);
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;
@@ -578,15 +588,22 @@ protected:
uint32_t delay_millis;
if (send_ack) {
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (client->out_path_len < 0) {
sendFlood(ack, TXT_ACK_DELAY);
} else {
sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
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;
}
delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
} else {
delay_millis = 0;
}
@@ -711,6 +728,7 @@ public:
{
next_local_advert = next_flood_advert = 0;
_logging = false;
set_radio_at = revert_radio_at = 0;
// defaults
memset(&_prefs, 0, sizeof(_prefs));
@@ -728,9 +746,9 @@ public:
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.disable_fwd = 1;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 3; // 3 hours
_prefs.flood_advert_interval = 12; // 12 hours
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
_prefs.interference_threshold = 0; // disabled
#ifdef ROOM_PASSWORD
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
#endif
@@ -743,8 +761,6 @@ public:
_num_posted = _num_post_pushes = 0;
}
CommonCLI* getCLI() { return &_cli; }
void begin(FILESYSTEM* fs) {
mesh::Mesh::begin();
_fs = fs;
@@ -762,14 +778,24 @@ public:
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;
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();
@@ -837,7 +863,21 @@ public:
strcpy(reply, "not supported");
}
const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; }
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();
@@ -845,6 +885,18 @@ public:
((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();
@@ -902,6 +954,18 @@ public:
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
@@ -998,7 +1062,7 @@ void loop() {
if (len > 0 && command[len - 1] == '\r') { // received complete line
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
the_mesh.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include "TimeSeriesData.h"
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#include <helpers/ArduinoHelpers.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/IdentityStore.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/CommonCLI.h>
#include <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)
#define PERM_RESERVED4 (1 << 5)
#define PERM_RECV_ALERTS_LO (1 << 6) // low priority alerts
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
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"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.8.1"
#endif
#define FIRMWARE_ROLE "sensor"
#define MAX_CONTACTS 20
#define MAX_SEARCH_RESULTS 8
#define MAX_CONCURRENT_ALERTS 4
class SensorMesh : public mesh::Mesh, public CommonCLICallbacks {
public:
SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
void begin(FILESYSTEM* fs);
void loop();
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
// CommonCLI callbacks
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getRole() override { return FIRMWARE_ROLE; }
const char* getNodeName() { return _prefs.node_name; }
NodePrefs* getNodePrefs() { return &_prefs; }
void savePrefs() override { _cli.savePrefs(_fs); }
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;
void setLoggingOn(bool enable) override { }
void eraseLogFile() override { }
void dumpLogFile() override { }
void setTxPower(uint8_t power_dbm) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}
mesh::LocalIdentity& getSelfId() override { return self_id; }
void saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override { }
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
float getTelemValue(uint8_t channel, uint8_t type);
protected:
// current telemetry data queries
float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); }
float getCurrent(uint8_t channel) { return getTelemValue(channel, LPP_CURRENT); }
float getPower(uint8_t channel) { return getTelemValue(channel, LPP_POWER); }
float getTemperature(uint8_t channel) { return getTelemValue(channel, LPP_TEMPERATURE); }
float getRelativeHumidity(uint8_t channel) { return getTelemValue(channel, LPP_RELATIVE_HUMIDITY); }
float getBarometricPressure(uint8_t channel) { return getTelemValue(channel, LPP_BAROMETRIC_PRESSURE); }
float getAltitude(uint8_t channel) { return getTelemValue(channel, LPP_ALTITUDE); }
bool getGPS(uint8_t channel, float& lat, float& lon, float& alt);
// alerts
enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT };
struct Trigger {
uint32_t timestamp;
AlertPriority pri;
uint32_t expected_acks[4];
int8_t curr_contact_idx;
uint8_t attempt;
unsigned long send_expiry;
char text[MAX_PACKET_PAYLOAD];
Trigger() { text[0] = 0; }
bool isTriggered() const { return text[0] != 0; }
};
void alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text);
virtual void onSensorDataRead() = 0; // for app to implement
virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0; // for app to implement
virtual bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) { return false; }
// Mesh overrides
float getAirtimeBudgetFactor() const override;
bool allowPacketForward(const mesh::Packet* packet) override;
int calcRxDelay(float score, uint32_t air_time) const override;
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
int getInterferenceThreshold() const override;
int getAGCResetInterval() const override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void 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);
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;
unsigned long dirty_contacts_expiry;
CayenneLPP telemetry;
uint32_t last_read_time;
int matching_peer_indexes[MAX_SEARCH_RESULTS];
int num_alert_tasks;
Trigger* alert_tasks[MAX_CONCURRENT_ALERTS];
unsigned long set_radio_at, revert_radio_at;
float pending_freq;
float pending_bw;
uint8_t pending_sf;
uint8_t pending_cr;
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);
};

View File

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

View File

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

View File

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

View File

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

View File

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

16
library.json Normal file
View File

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

View File

@@ -29,15 +29,31 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-D LORA_SF=11
-D ENABLE_PRIVATE_KEY_IMPORT=1 ; NOTE: comment these out for more secure firmware
-D ENABLE_PRIVATE_KEY_EXPORT=1
-D RADIOLIB_EXCLUDE_CC1101=1
-D RADIOLIB_EXCLUDE_RF69=1
-D RADIOLIB_EXCLUDE_SX1231=1
-D RADIOLIB_EXCLUDE_SI443X=1
-D RADIOLIB_EXCLUDE_RFM2X=1
-D RADIOLIB_EXCLUDE_SX128X=1
-D RADIOLIB_EXCLUDE_AFSK=1
-D RADIOLIB_EXCLUDE_AX25=1
-D RADIOLIB_EXCLUDE_HELLSCHREIBER=1
-D RADIOLIB_EXCLUDE_MORSE=1
-D RADIOLIB_EXCLUDE_APRS=1
-D RADIOLIB_EXCLUDE_BELL=1
-D RADIOLIB_EXCLUDE_RTTY=1
-D RADIOLIB_EXCLUDE_SSTV=1
build_src_filter =
+<*.cpp>
+<helpers/*.cpp>
+<helpers/radiolib/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
; ----------------- ESP32 ---------------------
[esp32_base]
extends = arduino_base
platform = platformio/espressif32@^6.11.0
platform = platformio/espressif32@6.11.0
monitor_filters = esp32_exception_decoder
extra_scripts = merge-bin.py
build_flags = ${arduino_base.build_flags}
@@ -52,7 +68,7 @@ lib_deps =
; esp32c6 uses arduino framework 3.x
[esp32c6_base]
extends = esp32_base
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip
; ----------------- NRF52 ---------------------
@@ -63,14 +79,6 @@ build_flags = ${arduino_base.build_flags}
-D NRF52_PLATFORM
-D LFS_NO_ASSERT=1
[nrf52840_base]
extends = nrf52_base
build_flags = ${nrf52_base.build_flags}
lib_deps =
${nrf52_base.lib_deps}
rweather/Crypto @ ^0.4.0
https://github.com/adafruit/Adafruit_nRF52_Arduino
; ----------------- RP2040 ---------------------
[rp2040_base]
@@ -95,3 +103,33 @@ build_src_filter = ${arduino_base.build_src_filter}
+<helpers/stm32>
lib_deps = ${arduino_base.lib_deps}
file://arch/stm32/Adafruit_LittleFS_stm32
[sensor_base]
build_flags =
-D ENV_INCLUDE_GPS=1
-D ENV_INCLUDE_AHTX0=1
-D ENV_INCLUDE_BME280=1
-D ENV_INCLUDE_BMP280=1
-D ENV_INCLUDE_SHTC3=1
-D ENV_INCLUDE_SHT4X=1
-D ENV_INCLUDE_LPS22HB=1
-D ENV_INCLUDE_INA3221=1
-D ENV_INCLUDE_INA219=1
-D ENV_INCLUDE_INA226=1
-D ENV_INCLUDE_INA260=1
-D ENV_INCLUDE_MLX90614=1
-D ENV_INCLUDE_VL53L0X=1
lib_deps =
adafruit/Adafruit INA3221 Library @ ^1.0.1
adafruit/Adafruit INA219 @ ^1.2.3
robtillaart/INA226 @ ^0.6.4
adafruit/Adafruit INA260 Library @ ^1.5.3
adafruit/Adafruit AHTX0 @ ^2.0.5
adafruit/Adafruit BME280 Library @ ^2.3.0
adafruit/Adafruit BMP280 Library @ ^2.6.8
adafruit/Adafruit SHTC3 Library @ ^1.0.1
sensirion/Sensirion I2C SHT4x @ ^1.1.2
arduino-libraries/Arduino_LPS22HB @ ^1.0.2
adafruit/Adafruit MLX90614 Library @ ^2.1.5
adafruit/Adafruit_VL53L0X @ ^1.2.4
stevemarple/MicroNMEA @ ^2.0.6

View File

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

View File

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

View File

@@ -22,6 +22,9 @@ uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) {
uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) {
return 0; // by default, no delay
}
uint8_t Mesh::getExtraAckTransmitCount() const {
return 0;
}
uint32_t Mesh::getCADFailRetryDelay() const {
return _rng->nextInt(1, 4)*120;
@@ -67,22 +70,22 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) {
if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) {
if (_tables->hasSeen(pkt)) return ACTION_RELEASE; // don't retransmit!
// remove our hash from 'path', then re-broadcast
pkt->path_len -= PATH_HASH_SIZE;
#if 0
memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len);
#elif PATH_HASH_SIZE == 1
for (int k = 0; k < pkt->path_len; k++) { // shuffle bytes by 1
pkt->path[k] = pkt->path[k + 1];
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
return forwardMultipartDirect(pkt);
} else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
if (!_tables->hasSeen(pkt)) { // don't retransmit!
removeSelfFromPath(pkt);
routeDirectRecvAcks(pkt, 0);
}
return ACTION_RELEASE;
}
#else
#error "need path remove impl"
#endif
uint32_t d = getDirectRetransmitDelay(pkt);
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
if (!_tables->hasSeen(pkt)) {
removeSelfFromPath(pkt);
uint32_t d = getDirectRetransmitDelay(pkt);
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
}
}
return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard.
}
@@ -135,7 +138,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
int k = 0;
uint8_t path_len = data[k++];
uint8_t* path = &data[k]; k += path_len;
uint8_t extra_type = data[k++];
uint8_t extra_type = data[k++] & 0x0F; // upper 4 bits reserved for future use
uint8_t* extra = &data[k];
uint8_t extra_len = len - k; // remainder of packet (may be padded with zeroes!)
if (onPeerPathRecv(pkt, j, secret, path, path_len, extra_type, extra, extra_len)) {
@@ -181,7 +184,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i);
if (len > 0) { // success!
onAnonDataRecv(pkt, pkt->getPayloadType(), sender, data, len);
onAnonDataRecv(pkt, secret, sender, data, len);
pkt->markDoNotRetransmit();
}
}
@@ -261,6 +264,32 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
}
break;
}
case PAYLOAD_TYPE_MULTIPART:
if (pkt->payload_len > 2) {
uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent
uint8_t type = pkt->payload[0] & 0x0F;
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK
Packet tmp;
tmp.header = pkt->header;
tmp.path_len = pkt->path_len;
memcpy(tmp.path, pkt->path, pkt->path_len);
tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
if (!_tables->hasSeen(&tmp)) {
uint32_t ack_crc;
memcpy(&ack_crc, tmp.payload, 4);
onAckRecv(&tmp, ack_crc);
//action = routeRecvPacket(&tmp); // NOTE: currently not needed, as multipart ACKs not sent Flood
}
} else {
// FUTURE: other multipart types??
}
}
break;
default:
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): unknown payload type, header: %d", getLogDateTime(), (int) pkt->header);
// Don't flood route unknown packet types! action = routeRecvPacket(pkt);
@@ -269,6 +298,20 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
return action;
}
void Mesh::removeSelfFromPath(Packet* pkt) {
// remove our hash from 'path'
pkt->path_len -= PATH_HASH_SIZE;
#if 0
memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len);
#elif PATH_HASH_SIZE == 1
for (int k = 0; k < pkt->path_len; k++) { // shuffle bytes by 1
pkt->path[k] = pkt->path[k + 1];
}
#else
#error "need path remove impl"
#endif
}
DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit()
&& packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) {
@@ -282,6 +325,54 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
return ACTION_RELEASE;
}
DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent
uint8_t type = pkt->payload[0] & 0x0F;
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK
Packet tmp;
tmp.header = pkt->header;
tmp.path_len = pkt->path_len;
memcpy(tmp.path, pkt->path, pkt->path_len);
tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
if (!_tables->hasSeen(&tmp)) { // don't retransmit!
removeSelfFromPath(&tmp);
routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2)
}
}
return ACTION_RELEASE;
}
void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
if (!packet->isMarkedDoNotRetransmit()) {
uint32_t crc;
memcpy(&crc, packet->payload, 4);
uint8_t extra = getExtraAckTransmitCount();
while (extra > 0) {
delay_millis += getDirectRetransmitDelay(packet) + 300;
auto a1 = createMultiAck(crc, extra);
if (a1) {
memcpy(a1->path, packet->path, a1->path_len = packet->path_len);
a1->header &= ~PH_ROUTE_MASK;
a1->header |= ROUTE_TYPE_DIRECT;
sendPacket(a1, 0, delay_millis);
}
extra--;
}
auto a2 = createAck(crc);
if (a2) {
memcpy(a2->path, packet->path, a2->path_len = packet->path_len);
a2->header &= ~PH_ROUTE_MASK;
a2->header |= ROUTE_TYPE_DIRECT;
sendPacket(a2, 0, delay_millis);
}
}
}
Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) {
if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL;
@@ -449,6 +540,21 @@ Packet* Mesh::createAck(uint32_t ack_crc) {
return packet;
}
Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK;
memcpy(&packet->payload[1], &ack_crc, 4);
packet->payload_len = 5;
return packet;
}
Packet* Mesh::createRawData(const uint8_t* data, size_t len) {
if (len > sizeof(Packet::payload)) return NULL; // invalid arg

View File

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

View File

@@ -41,6 +41,8 @@ public:
virtual void onAfterTransmit() { }
virtual void reboot() = 0;
virtual void powerOff() { /* no op */ }
virtual uint32_t getGpio() { return 0; }
virtual void setGpio(uint32_t values) {}
virtual uint8_t getStartupReason() const = 0;
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
};

View File

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

View File

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

View File

@@ -31,6 +31,23 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl
return createAdvert(self_id, app_data, app_data_len);
}
void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
if (dest.out_path_len < 0) {
mesh::Packet* ack = createAck(ack_hash);
if (ack) sendFlood(ack, TXT_ACK_DELAY);
} else {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
mesh::Packet* a1 = createMultiAck(ack_hash, 1);
if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d);
d += 300;
}
mesh::Packet* a2 = createAck(ack_hash);
if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d);
}
}
void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) {
AdvertDataParser parser(app_data, app_data_len);
if (!(parser.isValid() && parser.hasName())) {
@@ -152,14 +169,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
if (path) sendFlood(path, TXT_ACK_DELAY);
} else {
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (from.out_path_len < 0) {
sendFlood(ack, TXT_ACK_DELAY);
} else {
sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY);
}
}
sendAckTo(from, ack_hash);
}
} else if (flags == TXT_TYPE_CLI_DATA) {
onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
@@ -185,14 +195,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
if (path) sendFlood(path, TXT_ACK_DELAY);
} else {
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (from.out_path_len < 0) {
sendFlood(ack, TXT_ACK_DELAY);
} else {
sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY);
}
}
sendAckTo(from, ack_hash);
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported message type: %u", (uint32_t) flags);
@@ -232,9 +235,13 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui
ContactInfo& from = contacts[i];
// NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
return onContactPathRecv(from, packet->path, packet->path_len, path, path_len, extra_type, extra, extra_len);
}
bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
// NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
memcpy(from.out_path, path, from.out_path_len = path_len); // store a copy of path, for sendDirect()
memcpy(from.out_path, out_path, from.out_path_len = out_path_len); // store a copy of path, for sendDirect()
from.lastmod = getRTCClock()->getCurrentTime();
onContactPathUpdated(from);
@@ -403,22 +410,52 @@ bool BaseChatMesh::importContact(const uint8_t src_buf[], uint8_t len) {
}
int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout) {
int tlen;
uint8_t temp[24];
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique
if (recipient.type == ADV_TYPE_ROOM) {
memcpy(&temp[4], &recipient.sync_since, 4);
int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently
memcpy(&temp[8], password, len);
tlen = 8 + len;
} else {
int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently
memcpy(&temp[4], password, len);
tlen = 4 + len;
}
mesh::Packet* pkt;
{
int tlen;
uint8_t temp[24];
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique
if (recipient.type == ADV_TYPE_ROOM) {
memcpy(&temp[4], &recipient.sync_since, 4);
int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently
memcpy(&temp[8], password, len);
tlen = 8 + len;
} else {
int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently
memcpy(&temp[4], password, len);
tlen = 4 + len;
}
auto pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen);
pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len < 0) {
sendFlood(pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
}
int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout) {
if (data_len > MAX_PACKET_PAYLOAD - 16) return MSG_SEND_FAILED;
mesh::Packet* pkt;
{
uint8_t temp[MAX_PACKET_PAYLOAD];
tag = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique
memcpy(&temp[4], req_data, data_len);
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, 4 + data_len);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len < 0) {
@@ -435,14 +472,17 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
}
int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout) {
uint8_t temp[13];
tag = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = req_type;
memset(&temp[5], 0, 4); // reserved (possibly for 'since' param)
getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique
mesh::Packet* pkt;
{
uint8_t temp[13];
tag = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = req_type;
memset(&temp[5], 0, 4); // reserved (possibly for 'since' param)
getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique
auto pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp));
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp));
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len < 0) {

View File

@@ -72,6 +72,7 @@ class BaseChatMesh : public mesh::Mesh {
ConnectionInfo connections[MAX_CONNECTIONS];
mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack);
void sendAckTo(const ContactInfo& dest, uint32_t ack_hash);
protected:
BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
@@ -87,11 +88,14 @@ protected:
memset(connections, 0, sizeof(connections));
}
void resetContacts() { num_contacts = 0; }
// 'UI' concepts, for sub-classes to implement
virtual bool isAutoAddEnabled() const { return true; }
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
virtual bool processAck(const uint8_t *data) = 0;
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len);
virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0;
@@ -134,6 +138,7 @@ public:
bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len);
int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
int sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout);
int sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout);
bool shareContactZeroHop(const ContactInfo& contact);
uint8_t exportContact(const ContactInfo& contact, uint8_t dest_buf[]);
bool importContact(const uint8_t src_buf[], uint8_t len);

View File

@@ -51,7 +51,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *) &_prefs->sf, sizeof(_prefs->sf)); // 112
file.read((uint8_t *) &_prefs->cr, sizeof(_prefs->cr)); // 113
file.read((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
file.read((uint8_t *) &_prefs->reserved2, sizeof(_prefs->reserved2)); // 115
file.read((uint8_t *) &_prefs->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
@@ -69,6 +69,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->sf = constrain(_prefs->sf, 7, 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);
file.close();
}
@@ -106,7 +107,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *) &_prefs->sf, sizeof(_prefs->sf)); // 112
file.write((uint8_t *) &_prefs->cr, sizeof(_prefs->cr)); // 113
file.write((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
file.write((uint8_t *) &_prefs->reserved2, sizeof(_prefs->reserved2)); // 115
file.write((uint8_t *) &_prefs->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
@@ -120,25 +121,18 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
#define MIN_LOCAL_ADVERT_INTERVAL 60
void CommonCLI::checkAdvertInterval() {
void CommonCLI::savePrefs() {
if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) {
_prefs->advert_interval = 0; // turn it off, now that device has been manually configured
}
_callbacks->savePrefs();
}
void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) {
while (*command == ' ') command++; // skip leading spaces
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
memcpy(reply, command, 3); // reflect the prefix back
reply += 3;
command += 3;
}
if (memcmp(command, "reboot", 6) == 0) {
_board->reboot(); // doesn't return
} else if (memcmp(command, "advert", 6) == 0) {
_callbacks->sendSelfAdvertisement(400);
_callbacks->sendSelfAdvertisement(1500); // longer delay, give CLI response time to be sent first
strcpy(reply, "OK - Advert sent");
} else if (memcmp(command, "clock sync", 10) == 0) {
uint32_t curr = getRTCClock()->getCurrentTime();
@@ -171,10 +165,35 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
}
} else if (memcmp(command, "neighbors", 9) == 0) {
_callbacks->formatNeighborsReply(reply);
} else if (memcmp(command, "neighbor.remove ", 16) == 0) {
const char* hex = &command[16];
uint8_t pubkey[PUB_KEY_SIZE];
int hex_len = min((int)strlen(hex), PUB_KEY_SIZE*2);
int pubkey_len = hex_len / 2;
if (mesh::Utils::fromHex(pubkey, pubkey_len, hex)) {
_callbacks->removeNeighbor(pubkey, pubkey_len);
strcpy(reply, "OK");
} else {
strcpy(reply, "ERR: bad pubkey");
}
} else if (memcmp(command, "tempradio ", 10) == 0) {
strcpy(tmp, &command[10]);
const char *parts[5];
int num = mesh::Utils::parseTextParts(tmp, parts, 5);
float freq = num > 0 ? atof(parts[0]) : 0.0f;
float bw = num > 1 ? atof(parts[1]) : 0.0f;
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
int temp_timeout_mins = num > 4 ? atoi(parts[4]) : 0;
if (freq >= 300.0f && freq <= 2500.0f && sf >= 7 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f && temp_timeout_mins > 0) {
_callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins);
sprintf(reply, "OK - temp params for %d mins", temp_timeout_mins);
} else {
strcpy(reply, "Error, invalid params");
}
} else if (memcmp(command, "password ", 9) == 0) {
// change admin password
StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password));
checkAdvertInterval();
savePrefs();
sprintf(reply, "password now: %s", _prefs->password); // echo back just to let admin know for sure!!
} else if (memcmp(command, "clear stats", 11) == 0) {
@@ -188,6 +207,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "multi.acks", 10) == 0) {
sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
} else if (memcmp(config, "allow.read.only", 15) == 0) {
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
@@ -196,6 +217,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
} else if (memcmp(config, "guest.password", 14) == 0) {
sprintf(reply, "> %s", _prefs->guest_password);
} else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only
uint8_t prv_key[PRV_KEY_SIZE];
int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
mesh::Utils::toHex(tmp, prv_key, len);
sprintf(reply, "> %s", tmp);
} else if (memcmp(config, "name", 4) == 0) {
sprintf(reply, "> %s", _prefs->node_name);
} else if (memcmp(config, "repeat", 6) == 0) {
@@ -223,7 +249,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
} else if (memcmp(config, "public.key", 10) == 0) {
strcpy(reply, "> ");
mesh::Utils::toHex(&reply[2], _callbacks->getSelfIdPubKey(), PUB_KEY_SIZE);
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
} else if (memcmp(config, "role", 4) == 0) {
sprintf(reply, "> %s", _callbacks->getRole());
} else {
@@ -242,6 +268,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
_prefs->agc_reset_interval = atoi(&config[19]) / 4;
savePrefs();
sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "multi.acks ", 11) == 0) {
_prefs->multi_acks = atoi(&config[11]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "allow.read.only ", 16) == 0) {
_prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
@@ -271,9 +301,19 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
savePrefs();
strcpy(reply, "OK");
} else if (sender_timestamp == 0 && memcmp(config, "prv.key ", 8) == 0) { // from serial command line only
uint8_t prv_key[PRV_KEY_SIZE];
bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
if (success) {
mesh::LocalIdentity new_id;
new_id.readFrom(prv_key, PRV_KEY_SIZE);
_callbacks->saveIdentity(new_id);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, invalid key");
}
} else if (memcmp(config, "name ", 5) == 0) {
StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
checkAdvertInterval();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "repeat ", 7) == 0) {
@@ -300,12 +340,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
}
} else if (memcmp(config, "lat ", 4) == 0) {
_prefs->node_lat = atof(&config[4]);
checkAdvertInterval();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "lon ", 4) == 0) {
_prefs->node_lon = atof(&config[4]);
checkAdvertInterval();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "rxdelay ", 8) == 0) {

View File

@@ -21,7 +21,7 @@ struct NodePrefs { // persisted to file
uint8_t sf;
uint8_t cr;
uint8_t allow_read_only;
uint8_t reserved2;
uint8_t multi_acks;
float bw;
uint8_t flood_max;
uint8_t interference_threshold;
@@ -43,8 +43,13 @@ public:
virtual void dumpLogFile() = 0;
virtual void setTxPower(uint8_t power_dbm) = 0;
virtual void formatNeighborsReply(char *reply) = 0;
virtual const uint8_t* getSelfIdPubKey() = 0;
virtual void removeNeighbor(const uint8_t* pubkey, int key_len) {
// no op by default
};
virtual 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;
};
class CommonCLI {
@@ -52,13 +57,10 @@ class CommonCLI {
NodePrefs* _prefs;
CommonCLICallbacks* _callbacks;
mesh::MainBoard* _board;
char tmp[80];
char tmp[PRV_KEY_SIZE*2 + 4];
mesh::RTCClock* getRTCClock() { return _rtc; }
void savePrefs() { _callbacks->savePrefs(); }
void checkAdvertInterval();
void savePrefs();
void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
public:

View File

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

View File

@@ -4,7 +4,7 @@
#include <helpers/RefCountedDigitalPin.h>
// LoRa radio module pins for Heltec V3
// Also for Heltec Wireless Tracker
// Also for Heltec Wireless Tracker/Paper
#define P_LORA_DIO_1 14
#define P_LORA_NSS 8
#define P_LORA_RESET RADIOLIB_NC
@@ -14,7 +14,9 @@
#define P_LORA_MOSI 10
// built-ins
#define PIN_VBAT_READ 1
#ifndef PIN_VBAT_READ // set in platformio.ini for boards like Heltec Wireless Paper (20)
#define PIN_VBAT_READ 1
#endif
#ifndef PIN_ADC_CTRL // set in platformio.ini for Heltec Wireless Tracker (2)
#define PIN_ADC_CTRL 37
#endif

View File

@@ -0,0 +1,81 @@
#pragma once
#include <Arduino.h>
// LoRa radio module pins for Meshadventurer
#define P_LORA_DIO_1 33
#define P_LORA_NSS 18
#define P_LORA_RESET 23
#define P_LORA_BUSY 32
#define P_LORA_SCLK 5
#define P_LORA_MISO 19
#define P_LORA_MOSI 27
#define PIN_VBAT_READ 35
#include "ESP32Board.h"
#include <driver/rtc_io.h>
class MeshadventurerBoard : public ESP32Board {
public:
void begin() {
ESP32Board::begin();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are held on required levels during deep sleep
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
} else {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
// Finally set ESP32 into sleep
esp_deep_sleep_start(); // CPU halts here and never returns!
}
void powerOff() override {
// TODO: re-enable this when there is a definite wake-up source pin:
// enterDeepSleep(0);
}
uint16_t getBattMilliVolts() override {
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < 4; i++) {
raw += analogReadMilliVolts(PIN_VBAT_READ);
}
raw = raw / 4;
return (2 * raw);
}
const char* getManufacturerName() const override {
return "Meshadventurer";
}
};

View File

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

View File

@@ -83,6 +83,7 @@ void SerialBLEInterface::onConnect(BLEServer* pServer) {
void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", param->connect.conn_id, pServer->getPeerMTU(param->connect.conn_id));
last_conn_id = param->connect.conn_id;
}
void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) {
@@ -143,6 +144,7 @@ void SerialBLEInterface::disable() {
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
pServer->getAdvertising()->stop();
pServer->disconnect(last_conn_id);
pService->stop();
oldDeviceConnected = deviceConnected = false;
adv_restart_time = 0;

View File

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

View File

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

View File

@@ -1,6 +1,27 @@
#include "SerialBLEInterface.h"
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
}
}
void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected reason=%d", reason);
if(instance){
instance->_isDeviceConnected = false;
instance->startAdv();
}
}
void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
instance = this;
char charpin[20];
sprintf(charpin, "%d", pin_code);
@@ -13,11 +34,31 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
Bluefruit.Security.setMITM(true);
Bluefruit.Security.setPIN(charpin);
Bluefruit.Periph.setConnectCallback(onConnect);
Bluefruit.Periph.setDisconnectCallback(onDisconnect);
// To be consistent OTA DFU should be added first if it exists
//bledfu.begin();
// Configure and start the BLE Uart service
bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
bleuart.begin();
}
void SerialBLEInterface::startAdv() {
BLE_DEBUG_PRINTLN("SerialBLEInterface: starting advertising");
// clean restart if already advertising
if(Bluefruit.Advertising.isRunning()){
BLE_DEBUG_PRINTLN("SerialBLEInterface: already advertising, stopping to allow clean restart");
Bluefruit.Advertising.stop();
}
Bluefruit.Advertising.clearData(); // clear advertising data
Bluefruit.ScanResponse.clearData(); // clear scan response data
// Advertising packet
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
@@ -38,10 +79,25 @@ void SerialBLEInterface::startAdv() {
* For recommended advertising interval
* https://developer.apple.com/library/content/qa/qa1931/_index.html
*/
Bluefruit.Advertising.restartOnDisconnect(true);
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.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
}
void SerialBLEInterface::stopAdv() {
BLE_DEBUG_PRINTLN("SerialBLEInterface: stopping advertising");
// we only want to stop advertising if it's running, otherwise an invalid state error is logged by ble stack
if(!Bluefruit.Advertising.isRunning()){
return;
}
// stop advertising
Bluefruit.Advertising.stop();
}
// ---------- public methods
@@ -52,25 +108,28 @@ void SerialBLEInterface::enable() {
_isEnabled = true;
clearBuffers();
// Configure and start the BLE Uart service
bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
bleuart.begin();
// Start advertising
startAdv();
checkAdvRestart = false;
}
void SerialBLEInterface::disable() {
_isEnabled = false;
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
Bluefruit.Advertising.stop();
#ifdef RAK_BOARD
Bluefruit.disconnect(Bluefruit.connHandle());
#else
uint16_t conn_id;
if (Bluefruit.getConnectedHandles(&conn_id, 1) > 0) {
Bluefruit.disconnect(conn_id);
}
#endif
oldDeviceConnected = deviceConnected = false;
checkAdvRestart = false;
Bluefruit.Advertising.restartOnDisconnect(false);
Bluefruit.Advertising.stop();
Bluefruit.Advertising.clearData();
stopAdv();
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
@@ -79,7 +138,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
return 0;
}
if (deviceConnected && len > 0) {
if (_isDeviceConnected && len > 0) {
if (send_queue_len >= FRAME_QUEUE_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
@@ -115,44 +174,14 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
} else {
int len = bleuart.available();
if (len > 0) {
deviceConnected = true; // should probably use the callback to monitor cx
bleuart.readBytes(dest, len);
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]);
return len;
}
}
if (Bluefruit.connected() == 0) deviceConnected = false;
if (deviceConnected != oldDeviceConnected) {
if (!deviceConnected) { // disconnecting
clearBuffers();
BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting...");
delay(500); // give the bluetooth stack the chance to get things ready
checkAdvRestart = true;
} else {
BLE_DEBUG_PRINTLN("SerialBLEInterface -> stopping advertising");
BLE_DEBUG_PRINTLN("SerialBLEInterface -> connecting...");
// connecting
// do stuff here on connecting
Bluefruit.Advertising.stop();
checkAdvRestart = false;
}
oldDeviceConnected = deviceConnected;
}
if (checkAdvRestart) {
if (Bluefruit.connected() == 0) {
BLE_DEBUG_PRINTLN("SerialBLEInterface -> re-starting advertising");
startAdv();
}
checkAdvRestart = false;
}
return 0;
}
bool SerialBLEInterface::isConnected() const {
return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0;
return _isDeviceConnected;
}

View File

@@ -5,10 +5,8 @@
class SerialBLEInterface : public BaseSerialInterface {
BLEUart bleuart;
bool deviceConnected;
bool oldDeviceConnected;
bool checkAdvRestart;
bool _isEnabled;
bool _isDeviceConnected;
unsigned long _last_write;
struct Frame {
@@ -21,18 +19,19 @@ class SerialBLEInterface : public BaseSerialInterface {
Frame send_queue[FRAME_QUEUE_SIZE];
void clearBuffers() { send_queue_len = 0; }
void startAdv();
static void onConnect(uint16_t connection_handle);
static void onDisconnect(uint16_t connection_handle, uint8_t reason);
public:
SerialBLEInterface() {
deviceConnected = false;
oldDeviceConnected = false;
checkAdvRestart = false;
_isEnabled = false;
_isDeviceConnected = false;
_last_write = 0;
send_queue_len = 0;
}
void startAdv();
void stopAdv();
void begin(const char* device_name, uint32_t pin_code);
// BaseSerialInterface methods

View File

@@ -26,6 +26,45 @@ void T114Board::begin() {
pinMode(PIN_VBAT_READ, INPUT);
// Enable SoftDevice low-power mode
sd_power_mode_set(NRF_POWER_MODE_LOWPWR);
// Enable DC/DC converter for better efficiency (REG1 stage)
NRF_POWER->DCDCEN = 1;
// Power down unused communication peripherals
// UART1 - Not used on T114
NRF_UARTE1->ENABLE = 0;
// SPIM2/SPIS2 - Not used (SPI is on SPIM0)
NRF_SPIM2->ENABLE = 0;
NRF_SPIS2->ENABLE = 0;
// TWI1 (I2C1) - Not used (I2C is on TWI0)
NRF_TWIM1->ENABLE = 0;
NRF_TWIS1->ENABLE = 0;
// PWM modules - Not used for standard T114 functions
NRF_PWM1->ENABLE = 0;
NRF_PWM2->ENABLE = 0;
NRF_PWM3->ENABLE = 0;
// PDM (Digital Microphone Interface) - Not used
NRF_PDM->ENABLE = 0;
// I2S - Not used
NRF_I2S->ENABLE = 0;
// QSPI - Not used (no external flash)
NRF_QSPI->ENABLE = 0;
// Disable unused analog peripherals
// SAADC channels - only keep what's needed for battery monitoring
NRF_SAADC->ENABLE = 0; // Re-enable only when needed for measurements
// COMP - Comparator not used
NRF_COMP->ENABLE = 0;
#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL);
#endif

View File

@@ -40,6 +40,9 @@ public:
uint16_t getBattMilliVolts() override {
int adcvalue = 0;
NRF_SAADC->ENABLE = 1;
analogReadResolution(12);
analogReference(AR_INTERNAL_3_0);
pinMode(PIN_BAT_CTL, OUTPUT); // battery adc can be read only ctrl pin 6 set to high
@@ -49,6 +52,8 @@ public:
adcvalue = analogRead(PIN_VBAT_READ);
digitalWrite(6, 0);
NRF_SAADC->ENABLE = 0;
return (uint16_t)((float)adcvalue * MV_LSB * 4.9);
}
@@ -60,5 +65,9 @@ public:
NVIC_SystemReset();
}
void powerOff() override {
sd_power_system_off();
}
bool startOTAUpdate(const char* id, char reply[]) override;
};

View File

@@ -43,6 +43,25 @@ public:
return "LilyGo T-Echo";
}
void powerOff() override {
#ifdef LED_RED
digitalWrite(LED_RED, LOW);
#endif
#ifdef LED_GREEN
digitalWrite(LED_GREEN, LOW);
#endif
#ifdef LED_BLUE
digitalWrite(LED_BLUE, LOW);
#endif
#ifdef DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, LOW);
#endif
#ifdef PIN_PWR_EN
digitalWrite(PIN_PWR_EN, LOW);
#endif
sd_power_system_off();
}
void reboot() override {
NVIC_SystemReset();
}

View File

@@ -55,4 +55,16 @@ public:
void reboot() override {
NVIC_SystemReset();
}
void powerOff() override {
// turn off all leds, sd_power_system_off will not do this for us
#ifdef P_LORA_TX_LED
digitalWrite(P_LORA_TX_LED, LOW);
#endif
// power off board
sd_power_system_off();
}
};

View File

@@ -0,0 +1,70 @@
#pragma once
#include <RadioLib.h>
#define LR1110_IRQ_HAS_PREAMBLE 0b0000000100 // 4 4 valid LoRa header received
#define LR1110_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
class CustomLR1110 : public LR1110 {
public:
CustomLR1110(Module *mod) : LR1110(mod) { }
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);
}
// 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));
return detected;
}
};

View File

@@ -17,7 +17,7 @@ public:
void onSendFinished() override {
RadioLibWrapper::onSendFinished();
_radio->setPreambleLength(8); // overcomes weird issues with small and big pkts
_radio->setPreambleLength(16); // overcomes weird issues with small and big pkts
}
float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); }

View File

@@ -96,8 +96,9 @@ bool RadioLibWrapper::isInRecvMode() const {
}
int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
int len = 0;
if (state & STATE_INT_READY) {
int len = _radio->getPacketLength();
len = _radio->getPacketLength();
if (len > 0) {
if (len > sz) { len = sz; }
int err = _radio->readData(bytes, len);
@@ -110,7 +111,6 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
}
}
state = STATE_IDLE; // need another startReceive()
return len;
}
if (state != STATE_RX) {
@@ -121,7 +121,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
}
}
return 0;
return len;
}
uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) {

View File

@@ -0,0 +1,30 @@
#include "XiaoRP2040Board.h"
#include <Arduino.h>
#include <Wire.h>
void XiaoRP2040Board::begin() {
// for future use, sub-classes SHOULD call this from their begin()
startup_reason = BD_STARTUP_NORMAL;
#ifdef P_LORA_TX_LED
pinMode(P_LORA_TX_LED, OUTPUT);
#endif
#ifdef PIN_VBAT_READ
pinMode(PIN_VBAT_READ, INPUT);
#endif
#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
Wire.setSDA(PIN_BOARD_SDA);
Wire.setSCL(PIN_BOARD_SCL);
#endif
Wire.begin();
delay(10); // give sx1262 some time to power up
}
bool XiaoRP2040Board::startOTAUpdate(const char *id, char reply[]) {
return false;
}

View File

@@ -0,0 +1,75 @@
#pragma once
#include <Arduino.h>
#include <MeshCore.h>
// LoRa radio module pins for the Xiao RP2040
// https://wiki.seeedstudio.com/XIAO-RP2040/
#define P_LORA_DIO_1 27 // D1
#define P_LORA_NSS 6 // D4
#define P_LORA_RESET 28 // D2
#define P_LORA_BUSY 29 // D3
#define P_LORA_TX_LED 17
#define SX126X_RXEN 7 // D5
#define SX126X_TXEN -1
#define SX126X_DIO2_AS_RF_SWITCH true
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
/*
* This board has no built-in way to read battery voltage.
* Nevertheless it's very easy to make it work, you only require two 1% resistors.
* If your using the WIO SX1262 Addon for xaio, make sure you dont connect D0!
*
* BAT+ -----+
* |
* VSYS --+ -/\/\/\/\- --+
* 200k |
* +-- D0
* |
* GND --+ -/\/\/\/\- --+
* | 100k
* BAT- -----+
*/
#define PIN_VBAT_READ 26 // D0
#define BATTERY_SAMPLES 8
#define ADC_MULTIPLIER (3.0f * 3.3f * 1000)
class XiaoRP2040Board : public mesh::MainBoard {
protected:
uint8_t startup_reason;
public:
void begin();
uint8_t getStartupReason() const override { return startup_reason; }
#ifdef P_LORA_TX_LED
void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); }
void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); }
#endif
uint16_t getBattMilliVolts() override {
#if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER)
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < BATTERY_SAMPLES; i++) {
raw += analogRead(PIN_VBAT_READ);
}
raw = raw / BATTERY_SAMPLES;
return (ADC_MULTIPLIER * raw) / 4096;
#else
return 0;
#endif
}
const char *getManufacturerName() const override { return "Xiao RP2040"; }
void reboot() override { rp2040.reboot(); }
bool startOTAUpdate(const char *id, char reply[]) override;
};

View File

@@ -1,5 +1,11 @@
#include "EnvironmentSensorManager.h"
#if ENV_PIN_SDA && ENV_PIN_SCL
#define TELEM_WIRE &Wire1 // Use Wire1 as the I2C bus for Environment Sensors
#else
#define TELEM_WIRE &Wire // Use default I2C bus for Environment Sensors
#endif
#if ENV_INCLUDE_AHTX0
#define TELEM_AHTX_ADDRESS 0x38 // AHT10, AHT20 temperature and humidity sensor I2C address
#include <Adafruit_AHTX0.h>
@@ -24,6 +30,20 @@ static Adafruit_BME280 BME280;
static Adafruit_BMP280 BMP280;
#endif
#if ENV_INCLUDE_SHTC3
#include <Adafruit_SHTC3.h>
static Adafruit_SHTC3 SHTC3;
#endif
#if ENV_INCLUDE_SHT4X
#define TELEM_SHT4X_ADDRESS 0x44 //0x44 - 0x46
#include <SensirionI2cSht4x.h>
static SensirionI2cSht4x SHT4X;
#endif
#if ENV_INCLUDE_LPS22HB
#include <Arduino_LPS22HB.h>
#endif
#if ENV_INCLUDE_INA3221
#define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address
@@ -39,13 +59,57 @@ static Adafruit_INA3221 INA3221;
static Adafruit_INA219 INA219(TELEM_INA219_ADDRESS);
#endif
#if ENV_INCLUDE_INA260
#define TELEM_INA260_ADDRESS 0x41 // INA260 single channel current sensor I2C address
#include <Adafruit_INA260.h>
static Adafruit_INA260 INA260;
#endif
#if ENV_INCLUDE_INA226
#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);
#endif
#if ENV_INCLUDE_MLX90614
#define TELEM_MLX90614_ADDRESS 0x5A // MLX90614 IR temperature sensor I2C address
#include <Adafruit_MLX90614.h>
static Adafruit_MLX90614 MLX90614;
#endif
#if ENV_INCLUDE_VL53L0X
#define TELEM_VL53L0X_ADDRESS 0x29 // VL53L0X time-of-flight distance sensor I2C address
#include <Adafruit_VL53L0X.h>
static Adafruit_VL53L0X VL53L0X;
#endif
#if ENV_INCLUDE_GPS && RAK_BOARD
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;
#endif
bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_GPS
#if RAK_BOARD
rakGPSInit(); //probe base board/sockets for GPS
#else
initBasicGPS();
#endif
#endif
#if ENV_PIN_SDA && ENV_PIN_SCL
Wire1.begin(ENV_PIN_SDA, ENV_PIN_SCL, 100000);
MESH_DEBUG_PRINTLN("Second I2C initialized on pins SDA: %d SCL: %d", ENV_PIN_SDA, ENV_PIN_SCL);
#endif
#if ENV_INCLUDE_AHTX0
if (AHTX0.begin(&Wire, 0, TELEM_AHTX_ADDRESS)) {
if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS);
AHTX0_initialized = true;
} else {
@@ -55,7 +119,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_BME280
if (BME280.begin(TELEM_BME280_ADDRESS, &Wire)) {
if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS);
MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID());
BME280_initialized = true;
@@ -76,28 +140,104 @@ bool EnvironmentSensorManager::begin() {
}
#endif
#if ENV_INCLUDE_INA3221
if (INA3221.begin(TELEM_INA3221_ADDRESS, &Wire)) {
MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS);
MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID());
for(int i = 0; i < 3; i++) {
INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE);
}
INA3221_initialized = true;
#if ENV_INCLUDE_SHTC3
if (SHTC3.begin()) {
MESH_DEBUG_PRINTLN("Found sensor: SHTC3");
SHTC3_initialized = true;
} else {
INA3221_initialized = false;
MESH_DEBUG_PRINTLN("INA3221 was not found at I2C address %02X", TELEM_INA3221_ADDRESS);
SHTC3_initialized = false;
MESH_DEBUG_PRINTLN("SHTC3 was not found at I2C address %02X", 0x70);
}
#endif
#if ENV_INCLUDE_SHT4X
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
uint32_t serialNumber = 0;
int16_t sht4x_error;
sht4x_error = SHT4X.serialNumber(serialNumber);
if (sht4x_error == 0) {
MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS);
SHT4X_initialized = true;
} else {
SHT4X_initialized = false;
MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS);
}
#endif
#if ENV_INCLUDE_LPS22HB
if (BARO.begin()) {
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
LPS22HB_initialized = true;
} else {
LPS22HB_initialized = false;
MESH_DEBUG_PRINTLN("LPS22HB was not found at I2C address %02X", 0x5C);
}
#endif
#if ENV_INCLUDE_INA3221
if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS);
MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID());
for(int i = 0; i < 3; i++) {
INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE);
}
INA3221_initialized = true;
} else {
INA3221_initialized = false;
MESH_DEBUG_PRINTLN("INA3221 was not found at I2C address %02X", TELEM_INA3221_ADDRESS);
}
#endif
#if ENV_INCLUDE_INA219
if (INA219.begin(&Wire)) {
MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS);
INA219_initialized = true;
if (INA219.begin(TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS);
INA219_initialized = true;
} else {
INA219_initialized = false;
MESH_DEBUG_PRINTLN("INA219 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
INA219_initialized = false;
MESH_DEBUG_PRINTLN("INA219 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
}
#endif
#if ENV_INCLUDE_INA260
if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS);
INA260_initialized = true;
} else {
INA260_initialized = false;
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
}
#endif
#if ENV_INCLUDE_INA226
if (INA226.begin()) {
MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS);
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
INA226_initialized = true;
} else {
INA226_initialized = false;
MESH_DEBUG_PRINTLN("INA226 was not found at I2C address %02X", TELEM_INA226_ADDRESS);
}
#endif
#if ENV_INCLUDE_MLX90614
if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS);
MLX90614_initialized = true;
} else {
MLX90614_initialized = false;
MESH_DEBUG_PRINTLN("MLX90614 was not found at I2C address %02X", TELEM_MLX90614_ADDRESS);
}
#endif
#if ENV_INCLUDE_VL53L0X
if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS);
VL53L0X_initialized = true;
} else {
VL53L0X_initialized = false;
MESH_DEBUG_PRINTLN("VL53L0X was not found at I2C address %02X", TELEM_VL53L0X_ADDRESS);
}
#endif
@@ -126,7 +266,7 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
if (BME280_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME280.readTemperature());
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME280.readHumidity());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()/100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
}
#endif
@@ -134,8 +274,37 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
#if ENV_INCLUDE_BMP280
if (BMP280_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP280.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure());
telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure()/100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP280.readAltitude(TELEM_BMP280_SEALEVELPRESSURE_HPA));
}
#endif
#if ENV_INCLUDE_SHTC3
if (SHTC3_initialized) {
sensors_event_t humidity, temp;
SHTC3.getEvent(&humidity, &temp);
telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity);
}
#endif
#if ENV_INCLUDE_SHT4X
if (SHT4X_initialized) {
float sht4x_humidity, sht4x_temperature;
int16_t sht4x_error;
sht4x_error = SHT4X.measureLowestPrecision(sht4x_temperature, sht4x_humidity);
if (sht4x_error == 0) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, sht4x_temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, sht4x_humidity);
}
}
#endif
#if ENV_INCLUDE_LPS22HB
if (LPS22HB_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BARO.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BARO.readPressure());
}
#endif
@@ -157,10 +326,47 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
#if ENV_INCLUDE_INA219
if (INA219_initialized) {
telemetry.addVoltage(next_available_channel, INA219.getBusVoltage_V());
telemetry.addCurrent(next_available_channel, INA219.getCurrent_mA() / 1000);
telemetry.addPower(next_available_channel, INA219.getPower_mW() / 1000);
next_available_channel++;
telemetry.addVoltage(next_available_channel, INA219.getBusVoltage_V());
telemetry.addCurrent(next_available_channel, INA219.getCurrent_mA() / 1000);
telemetry.addPower(next_available_channel, INA219.getPower_mW() / 1000);
next_available_channel++;
}
#endif
#if ENV_INCLUDE_INA260
if (INA260_initialized) {
telemetry.addVoltage(next_available_channel, INA260.readBusVoltage() / 1000);
telemetry.addCurrent(next_available_channel, INA260.readCurrent() / 1000);
telemetry.addPower(next_available_channel, INA260.readPower() / 1000);
next_available_channel++;
}
#endif
#if ENV_INCLUDE_INA226
if (INA226_initialized) {
telemetry.addVoltage(next_available_channel, INA226.getBusVoltage());
telemetry.addCurrent(next_available_channel, INA226.getCurrent_mA() / 1000.0);
telemetry.addPower(next_available_channel, INA226.getPower_mW() / 1000.0);
next_available_channel++;
}
#endif
#if ENV_INCLUDE_MLX90614
if (MLX90614_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, MLX90614.readObjectTempC());
telemetry.addTemperature(TELEM_CHANNEL_SELF + 1, MLX90614.readAmbientTempC());
}
#endif
#if ENV_INCLUDE_VL53L0X
if (VL53L0X_initialized) {
VL53L0X_RangingMeasurementData_t measure;
VL53L0X.rangingTest(&measure, false); // pass in 'true' to get debug data
if (measure.RangeStatus != 4) { // phase failures
telemetry.addDistance(TELEM_CHANNEL_SELF, measure.RangeMilliMeter / 1000.0f); // convert mm to m
} else {
telemetry.addDistance(TELEM_CHANNEL_SELF, 0.0f); // no valid measurement
}
}
#endif
@@ -251,8 +457,85 @@ void EnvironmentSensorManager::initBasicGPS() {
gps_active = false; //Set GPS visibility off until setting is changed
}
#ifdef RAK_BOARD
void EnvironmentSensorManager::rakGPSInit(){
Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
#ifdef GPS_BAUD_RATE
Serial1.begin(GPS_BAUD_RATE);
#else
Serial1.begin(9600);
#endif
//search for the correct IO standby pin depending on socket used
if(gpsIsAwake(WB_IO2)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19007/10");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket A");
}
else if(gpsIsAwake(WB_IO4)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19003/9");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket C");
}
else if(gpsIsAwake(WB_IO5)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19001/11");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket F");
}
else{
MESH_DEBUG_PRINTLN("No GPS found");
gps_active = false;
gps_detected = false;
return;
}
#ifndef FORCE_GPS_ALIVE // for use with repeaters, until GPS toggle is implimented
//Now that GPS is found and set up, set to sleep for initial state
stop_gps();
#endif
}
bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
//set initial waking state
pinMode(ioPin,OUTPUT);
digitalWrite(ioPin,LOW);
delay(500);
digitalWrite(ioPin,HIGH);
delay(500);
//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.saveConfigSelective(VAL_CFG_SUBSEC_IOPORT);
gpsResetPin = ioPin;
i2cGPSFlag = true;
gps_active = true;
gps_detected = true;
return true;
}
else if(Serial1){
MESH_DEBUG_PRINTLN("Serial GPS init correctly and is turned on");
if(PIN_GPS_EN){
gpsResetPin = PIN_GPS_EN;
}
serialGPSFlag = true;
gps_active = true;
gps_detected = true;
return true;
}
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
return false;
}
#endif
void EnvironmentSensorManager::start_gps() {
gps_active = true;
#ifdef RAK_BOARD
pinMode(gpsResetPin, OUTPUT);
digitalWrite(gpsResetPin, HIGH);
return;
#endif
#ifdef PIN_GPS_EN
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, HIGH);
@@ -264,6 +547,11 @@ void EnvironmentSensorManager::start_gps() {
void EnvironmentSensorManager::stop_gps() {
gps_active = false;
#ifdef RAK_BOARD
pinMode(gpsResetPin, OUTPUT);
digitalWrite(gpsResetPin, LOW);
return;
#endif
#ifdef PIN_GPS_EN
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, LOW);
@@ -279,12 +567,29 @@ void EnvironmentSensorManager::loop() {
_location->loop();
if (millis() > next_gps_update) {
if (gps_active && _location->isValid()) {
if(gps_active){
#ifndef RAK_BOARD
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);
}
#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

@@ -13,6 +13,13 @@ protected:
bool BMP280_initialized = false;
bool INA3221_initialized = false;
bool INA219_initialized = false;
bool INA260_initialized = false;
bool INA226_initialized = false;
bool SHTC3_initialized = false;
bool LPS22HB_initialized = false;
bool MLX90614_initialized = false;
bool VL53L0X_initialized = false;
bool SHT4X_initialized = false;
bool gps_detected = false;
bool gps_active = false;
@@ -22,6 +29,10 @@ protected:
void start_gps();
void stop_gps();
void initBasicGPS();
#ifdef RAK_BOARD
void rakGPSInit();
bool gpsIsAwake(uint8_t ioPin);
#endif
#endif

View File

@@ -21,9 +21,15 @@ public:
virtual void setColor(Color c) = 0;
virtual void setCursor(int x, int y) = 0;
virtual void print(const char* str) = 0;
virtual void printWordWrap(const char* str, int max_width) { print(str); } // fallback to basic print() if no override
virtual void fillRect(int x, int y, int w, int h) = 0;
virtual void drawRect(int x, int y, int w, int h) = 0;
virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0;
virtual uint16_t getTextWidth(const char* str) = 0;
virtual void drawTextCentered(int mid_x, int y, const char* str) { // helper method (override to optimise)
int w = getTextWidth(str);
setCursor(mid_x - w/2, y);
print(str);
}
virtual void endFrame() = 0;
};

View File

@@ -0,0 +1,164 @@
#include "E213Display.h"
#include "../../MeshCore.h"
BaseDisplay* E213Display::detectEInk()
{
// Test 1: Logic of BUSY pin
// Determines controller IC manufacturer
// Fitipower: busy when LOW
// Solomon Systech: busy when HIGH
// Force display BUSY by holding reset pin active
pinMode(DISP_RST, OUTPUT);
digitalWrite(DISP_RST, LOW);
delay(10);
// Read whether pin is HIGH or LOW while busy
pinMode(DISP_BUSY, INPUT);
bool busyLogic = digitalRead(DISP_BUSY);
// Test complete. Release pin
pinMode(DISP_RST, INPUT);
if (busyLogic == LOW) {
#ifdef VISION_MASTER_E213
return new EInkDisplay_VisionMasterE213 ;
#else
return new EInkDisplay_WirelessPaperV1_1 ;
#endif
} else {// busy HIGH
#ifdef VISION_MASTER_E213
return new EInkDisplay_VisionMasterE213V1_1 ;
#else
return new EInkDisplay_WirelessPaperV1_1_1 ;
#endif
}
}
bool E213Display::begin() {
if (_init) return true;
powerOn();
if(display==NULL) {
display = detectEInk();
}
display->begin();
// Set to landscape mode rotated 180 degrees
display->setRotation(3);
_init = true;
_isOn = true;
clear();
display->fastmodeOn(); // Enable fast mode for quicker (partial) updates
return true;
}
void E213Display::powerOn() {
#ifdef PIN_VEXT_EN
pinMode(PIN_VEXT_EN, OUTPUT);
#ifdef PIN_VEXT_EN_ACTIVE
digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE);
#else
digitalWrite(PIN_VEXT_EN, LOW); // Active low
#endif
delay(50); // Allow power to stabilize
#endif
}
void E213Display::powerOff() {
#ifdef PIN_VEXT_EN
#ifdef PIN_VEXT_EN_ACTIVE
digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE);
#else
digitalWrite(PIN_VEXT_EN, HIGH); // Turn off power
#endif
#endif
}
void E213Display::turnOn() {
if (!_init) begin();
powerOn();
_isOn = true;
}
void E213Display::turnOff() {
powerOff();
_isOn = false;
}
void E213Display::clear() {
display->clear();
}
void E213Display::startFrame(Color bkg) {
// Fill screen with white first to ensure clean background
display->fillRect(0, 0, width(), height(), WHITE);
if (bkg == LIGHT) {
// Fill with black if light background requested (inverted for e-ink)
display->fillRect(0, 0, width(), height(), BLACK);
}
}
void E213Display::setTextSize(int sz) {
// The library handles text size internally
display->setTextSize(sz);
}
void E213Display::setColor(Color c) {
// implemented in individual display methods
}
void E213Display::setCursor(int x, int y) {
display->setCursor(x, y);
}
void E213Display::print(const char *str) {
display->print(str);
}
void E213Display::fillRect(int x, int y, int w, int h) {
display->fillRect(x, y, w, h, BLACK);
}
void E213Display::drawRect(int x, int y, int w, int h) {
display->drawRect(x, y, w, h, BLACK);
}
void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
// Width in bytes for bitmap processing
uint16_t widthInBytes = (w + 7) / 8;
// Process the bitmap row by row
for (int by = 0; by < h; by++) {
// Scan across the row bit by bit
for (int bx = 0; bx < w; bx++) {
// Get the current bit using MSB ordering (like GxEPDDisplay)
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
uint8_t bitMask = 0x80 >> (bx & 7);
bool bitSet = bits[byteOffset] & bitMask;
// If the bit is set, draw the pixel
if (bitSet) {
display->drawPixel(x + bx, y + by, BLACK);
}
}
}
}
uint16_t E213Display::getTextWidth(const char *str) {
int16_t x1, y1;
uint16_t w, h;
display->getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w;
}
void E213Display::endFrame() {
display->update();
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include "DisplayDriver.h"
#include <SPI.h>
#include <Wire.h>
#include <heltec-eink-modules.h>
// Display driver for E213 e-ink display
class E213Display : public DisplayDriver {
BaseDisplay* display=NULL;
bool _init = false;
bool _isOn = false;
public:
E213Display() : DisplayDriver(250, 122) {}
~E213Display(){
if(display!=NULL) {
delete display;
}
}
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;
private:
BaseDisplay* detectEInk();
void powerOn();
void powerOff();
};

View File

@@ -0,0 +1,116 @@
#include "E290Display.h"
#include "../../MeshCore.h"
bool E290Display::begin() {
if (_init) return true;
powerOn();
display.begin();
// Set to landscape mode rotated 180 degrees
display.setRotation(3);
_init = true;
_isOn = true;
clear();
display.fastmodeOn(); // Enable fast mode for quicker (partial) updates
return true;
}
void E290Display::powerOn() {
#ifdef PIN_VEXT_EN
pinMode(PIN_VEXT_EN, OUTPUT);
digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE);
delay(50); // Allow power to stabilize
#endif
}
void E290Display::powerOff() {
#ifdef PIN_VEXT_EN
digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // Turn off power
#endif
}
void E290Display::turnOn() {
if (!_init) begin();
powerOn();
_isOn = true;
}
void E290Display::turnOff() {
powerOff();
_isOn = false;
}
void E290Display::clear() {
display.clear();
}
void E290Display::startFrame(Color bkg) {
// Fill screen with white first to ensure clean background
display.fillRect(0, 0, width(), height(), WHITE);
if (bkg == LIGHT) {
// Fill with black if light background requested (inverted for e-ink)
display.fillRect(0, 0, width(), height(), BLACK);
}
}
void E290Display::setTextSize(int sz) {
// The library handles text size internally
display.setTextSize(sz);
}
void E290Display::setColor(Color c) {
// implemented in individual display methods
}
void E290Display::setCursor(int x, int y) {
display.setCursor(x, y);
}
void E290Display::print(const char *str) {
display.print(str);
}
void E290Display::fillRect(int x, int y, int w, int h) {
display.fillRect(x, y, w, h, BLACK);
}
void E290Display::drawRect(int x, int y, int w, int h) {
display.drawRect(x, y, w, h, BLACK);
}
void E290Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
// Width in bytes for bitmap processing
uint16_t widthInBytes = (w + 7) / 8;
// Process the bitmap row by row
for (int by = 0; by < h; by++) {
// Scan across the row bit by bit
for (int bx = 0; bx < w; bx++) {
// Get the current bit using MSB ordering (like GxEPDDisplay)
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
uint8_t bitMask = 0x80 >> (bx & 7);
bool bitSet = bits[byteOffset] & bitMask;
// If the bit is set, draw the pixel
if (bitSet) {
display.drawPixel(x + bx, y + by, BLACK);
}
}
}
}
uint16_t E290Display::getTextWidth(const char *str) {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w;
}
void E290Display::endFrame() {
display.update();
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include "DisplayDriver.h"
#include <SPI.h>
#include <Wire.h>
#include <heltec-eink-modules.h>
// Display driver for E290 e-ink display
class E290Display : public DisplayDriver {
EInkDisplay_VisionMasterE290 display;
bool _init = false;
bool _isOn = false;
public:
E290Display() : DisplayDriver(296, 128) {}
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;
private:
void powerOn();
void powerOff();
};

View File

@@ -29,8 +29,8 @@ void GxEPDDisplay::turnOn() {
if (!_init) begin();
#if DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, HIGH);
_isOn = true;
#endif
_isOn = true;
}
void GxEPDDisplay::turnOff() {
@@ -47,6 +47,7 @@ void GxEPDDisplay::clear() {
void GxEPDDisplay::startFrame(Color bkg) {
display.fillScreen(GxEPD_WHITE);
display.setTextColor(_curr_color = GxEPD_BLACK);
}
void GxEPDDisplay::setTextSize(int sz) {
@@ -67,7 +68,12 @@ void GxEPDDisplay::setTextSize(int sz) {
}
void GxEPDDisplay::setColor(Color c) {
display.setTextColor(GxEPD_BLACK);
// colours need to be inverted for epaper displays
if (c == DARK) {
display.setTextColor(_curr_color = GxEPD_WHITE);
} else {
display.setTextColor(_curr_color = GxEPD_BLACK);
}
}
void GxEPDDisplay::setCursor(int x, int y) {
@@ -79,11 +85,11 @@ void GxEPDDisplay::print(const char* 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, GxEPD_BLACK);
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, GxEPD_BLACK);
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) {
@@ -116,7 +122,7 @@ void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
// If the bit is set, draw a block of pixels
if (bitSet) {
// Draw the block as a filled rectangle
display.fillRect(x1, y1, block_w, block_h, GxEPD_BLACK);
display.fillRect(x1, y1, block_w, block_h, _curr_color);
}
}
}
@@ -126,7 +132,7 @@ 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 w / SCALE_X;
return ceil((w + 1) / SCALE_X);
}
void GxEPDDisplay::endFrame() {

View File

@@ -28,6 +28,7 @@ class GxEPDDisplay : public DisplayDriver {
GxEPD2_BW<GxEPD2_150_BN, 200> display;
bool _init = false;
bool _isOn = false;
uint16_t _curr_color;
public:
// there is a margin in y...

View File

@@ -0,0 +1,91 @@
#include "MomentaryButton.h"
MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup) {
_pin = pin;
_reverse = reverse;
_pull = pulldownup;
down_at = 0;
prev = _reverse ? HIGH : LOW;
cancel = 0;
_long_millis = long_press_millis;
_threshold = 0;
}
MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, int analog_threshold) {
_pin = pin;
_reverse = false;
_pull = false;
down_at = 0;
prev = LOW;
cancel = 0;
_long_millis = long_press_millis;
_threshold = analog_threshold;
}
void MomentaryButton::begin() {
if (_pin >= 0 && _threshold == 0) {
pinMode(_pin, _pull ? (_reverse ? INPUT_PULLUP : INPUT_PULLDOWN) : INPUT);
}
}
bool MomentaryButton::isPressed() const {
int btn = _threshold > 0 ? (analogRead(_pin) < _threshold) : digitalRead(_pin);
return isPressed(btn);
}
void MomentaryButton::cancelClick() {
cancel = 1;
}
bool MomentaryButton::isPressed(int level) const {
if (_threshold > 0) {
return level;
}
if (_reverse) {
return level == LOW;
} else {
return level != LOW;
}
}
int MomentaryButton::check(bool repeat_click) {
if (_pin < 0) return BUTTON_EVENT_NONE;
int event = BUTTON_EVENT_NONE;
int btn = _threshold > 0 ? (analogRead(_pin) < _threshold) : digitalRead(_pin);
if (btn != prev) {
if (isPressed(btn)) {
down_at = millis();
} else {
// 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;
}
} else {
event = BUTTON_EVENT_CLICK; // any UP results in CLICK event when NOT using long_press feature
}
if (event == BUTTON_EVENT_CLICK && cancel) {
event = BUTTON_EVENT_NONE;
}
down_at = 0;
}
prev = btn;
}
if (!isPressed(btn) && cancel) { // always clear the pending 'cancel' once button is back in UP state
cancel = 0;
}
if (_long_millis > 0 && down_at > 0 && (unsigned long)(millis() - down_at) >= _long_millis) {
event = BUTTON_EVENT_LONG_PRESS;
down_at = 0;
}
if (down_at > 0 && repeat_click) {
unsigned long diff = (unsigned long)(millis() - down_at);
if (diff >= 700) {
event = BUTTON_EVENT_CLICK; // wait 700 millis before repeating the click events
}
}
return event;
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include <Arduino.h>
#define BUTTON_EVENT_NONE 0
#define BUTTON_EVENT_CLICK 1
#define BUTTON_EVENT_LONG_PRESS 2
class MomentaryButton {
int8_t _pin;
int8_t prev, cancel;
bool _reverse, _pull;
int _long_millis;
int _threshold; // analog mode
unsigned long down_at;
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, int analog_threshold);
void begin();
int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*
void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state)
uint8_t getPin() { return _pin; }
bool isPressed() const;
};

View File

@@ -7,6 +7,9 @@ bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) {
}
bool SSD1306Display::begin() {
#ifdef DISPLAY_ROTATION
display.setRotation(DISPLAY_ROTATION);
#endif
return display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS);
}

View File

@@ -18,7 +18,11 @@ bool ST7789Display::begin() {
pinMode(PIN_TFT_VDD_CTL, OUTPUT);
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
digitalWrite(PIN_TFT_VDD_CTL, LOW);
#ifdef PIN_TFT_LEDA_CTL_ACTIVE
digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE);
#else
digitalWrite(PIN_TFT_LEDA_CTL, LOW);
#endif
digitalWrite(PIN_TFT_RST, HIGH);
display.init();
@@ -43,15 +47,22 @@ void ST7789Display::turnOn() {
delay(20);
// Now turn on the backlight
#ifdef PIN_TFT_LEDA_CTL_ACTIVE
digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE);
#else
digitalWrite(PIN_TFT_LEDA_CTL, LOW);
#endif
_isOn = true;
}
}
void ST7789Display::turnOff() {
digitalWrite(PIN_TFT_VDD_CTL, HIGH);
#ifdef PIN_TFT_LEDA_CTL_ACTIVE
digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE);
#else
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
#endif
digitalWrite(PIN_TFT_RST, LOW);
_isOn = false;
}
@@ -62,6 +73,9 @@ void ST7789Display::clear() {
void ST7789Display::startFrame(Color bkg) {
display.clear();
_color = ST77XX_WHITE;
display.setRGB(_color);
display.setFont(ArialMT_Plain_16);
}
void ST7789Display::setTextSize(int sz) {
@@ -81,7 +95,9 @@ void ST7789Display::setColor(Color c) {
switch (c) {
case DisplayDriver::DARK :
_color = ST77XX_BLACK;
display.setColor(OLEDDISPLAY_COLOR::BLACK);
break;
#if 0
case DisplayDriver::LIGHT :
_color = ST77XX_WHITE;
break;
@@ -100,8 +116,10 @@ void ST7789Display::setColor(Color c) {
case DisplayDriver::ORANGE :
_color = ST77XX_ORANGE;
break;
#endif
default:
_color = ST77XX_WHITE;
display.setColor(OLEDDISPLAY_COLOR::WHITE);
break;
}
display.setRGB(_color);
@@ -116,6 +134,10 @@ void ST7789Display::print(const char* str) {
display.drawString(_x, _y, str);
}
void ST7789Display::printWordWrap(const char* str, int max_width) {
display.drawStringMaxWidth(_x, _y, max_width*SCALE_X, str);
}
void ST7789Display::fillRect(int x, int y, int w, int h) {
display.fillRect(x*SCALE_X + X_OFFSET, y*SCALE_Y + Y_OFFSET, w*SCALE_X, h*SCALE_Y);
}

View File

@@ -4,7 +4,7 @@
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <ST7789Spi.h>
#include "ST7789Spi.h"
class ST7789Display : public DisplayDriver {
ST7789Spi display;
@@ -14,8 +14,11 @@ class ST7789Display : public DisplayDriver {
bool i2c_probe(TwoWire& wire, uint8_t addr);
public:
#ifdef HELTEC_VISION_MASTER_T190
ST7789Display() : DisplayDriver(128, 64), display(&SPI, PIN_TFT_RST, PIN_TFT_DC, PIN_TFT_CS, GEOMETRY_RAWMODE, 320, 170,PIN_TFT_SDA,-1,PIN_TFT_SCL) {_isOn = false;}
#else
ST7789Display() : DisplayDriver(128, 64), display(&SPI1, PIN_TFT_RST, PIN_TFT_DC, PIN_TFT_CS, GEOMETRY_RAWMODE, 240, 135) {_isOn = false;}
#endif
bool begin();
bool isOn() override { return _isOn; }
@@ -27,6 +30,7 @@ public:
void setColor(Color c) override;
void setCursor(int x, int y) override;
void print(const char* str) override;
void printWordWrap(const char* str, int max_width) 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;

21
src/helpers/ui/UIScreen.h Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#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
class UIScreen {
protected:
UIScreen() { }
public:
virtual int render(DisplayDriver& display) =0; // return value is number of millis until next render
virtual bool handleInput(char c) { return false; }
virtual void poll() { }
};

View File

@@ -46,6 +46,13 @@ void genericBuzzer::shutdown() {
void genericBuzzer::quiet(bool buzzer_state) {
_is_quiet = buzzer_state;
#ifdef PIN_BUZZER_EN
if (_is_quiet) {
digitalWrite(PIN_BUZZER_EN, LOW);
} else {
digitalWrite(PIN_BUZZER_EN, HIGH);
}
#endif
}
bool genericBuzzer::isQuiet() {

View File

@@ -2,10 +2,10 @@
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/RadioLibWrappers.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/ESP32Board.h>
#include <helpers/CustomSX1262Wrapper.h>
#include <helpers/CustomSX1268Wrapper.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/radiolib/CustomSX1268Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>

View File

@@ -26,7 +26,7 @@ build_src_filter = ${esp32_base.build_src_filter}
extends = Generic_ESPNOW
build_flags =
${Generic_ESPNOW.build_flags}
-D MAX_CONTACTS=100
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=1
build_src_filter = ${Generic_ESPNOW.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
@@ -54,13 +54,13 @@ lib_deps =
extends = Generic_ESPNOW
build_flags =
${Generic_ESPNOW.build_flags}
-D MAX_CONTACTS=100
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=8
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
; NOTE: DO NOT ENABLE --> -D ESPNOW_DEBUG_LOGGING=1
build_src_filter = ${Generic_ESPNOW.build_src_filter}
+<../examples/companion_radio>
+<../examples/companion_radio/*.cpp>
lib_deps =
${Generic_ESPNOW.lib_deps}
densaugeo/base64 @ ~1.4.0

View File

@@ -8,9 +8,35 @@
#include <helpers/ESP32Board.h>
class Heltec_CT62_Board : public ESP32Board {
public:
uint32_t gpio_state = 0;
uint16_t getBattMilliVolts() override {
public:
void begin() {
ESP32Board::begin();
#if defined(PIN_BOARD_RELAY_CH1) && defined(PIN_BOARD_RELAY_CH2)
pinMode(PIN_BOARD_RELAY_CH1, OUTPUT);
pinMode(PIN_BOARD_RELAY_CH2, OUTPUT);
#endif
#if defined(PIN_BOARD_DIGITAL_IN)
pinMode(PIN_BOARD_DIGITAL_IN, INPUT);
#endif
}
uint32_t getGpio() override {
#if defined(PIN_BOARD_DIGITAL_IN)
return gpio_state | (digitalRead(PIN_BOARD_DIGITAL_IN) ? 1 : 0);
#else
return 0;
#endif
}
void setGpio(uint32_t values) override {
#if defined(PIN_BOARD_RELAY_CH1) && defined(PIN_BOARD_RELAY_CH2)
gpio_state = values;
digitalWrite(PIN_BOARD_RELAY_CH1, values & 2);
digitalWrite(PIN_BOARD_RELAY_CH2, values & 4);
#endif
}
uint16_t getBattMilliVolts() override {
#ifdef PIN_VBAT_READ
analogReadResolution(12); // ESP32-C3 ADC is 12-bit - 3.3/4096 (ref voltage/max counts)
uint32_t raw = 0;

View File

@@ -55,13 +55,13 @@ build_flags =
${Heltec_ct62.build_flags}
; -D ARDUINO_USB_MODE=1
; -D ARDUINO_USB_CDC_ON_BOOT=1
-D MAX_CONTACTS=100
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=8
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_ct62.build_src_filter}
+<../examples/companion_radio>
+<../examples/companion_radio/*.cpp>
lib_deps =
${Heltec_ct62.lib_deps}
${esp32_ota.lib_deps}
@@ -73,16 +73,37 @@ build_flags =
${Heltec_ct62.build_flags}
; -D ARDUINO_USB_MODE=1
; -D ARDUINO_USB_CDC_ON_BOOT=1
-D MAX_CONTACTS=100
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=8
-D OFFLINE_QUEUE_SIZE=256
-D BLE_PIN_CODE=123456
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_ct62.build_src_filter}
+<../examples/companion_radio>
+<../examples/companion_radio/*.cpp>
+<helpers/esp32/SerialBLEInterface.cpp>
lib_deps =
${Heltec_ct62.lib_deps}
${esp32_ota.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_ct62_sensor]
extends = Heltec_ct62
build_flags =
${Heltec_ct62.build_flags}
-D ADVERT_NAME='"HT-CT62 Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D PIN_BOARD_SDA=-1
-D PIN_BOARD_SCL=-1
-D PIN_BOARD_RELAY_CH1=0
-D PIN_BOARD_RELAY_CH2=1
-D PIN_BOARD_DIGITAL_IN=19
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_ct62.build_src_filter}
+<../examples/simple_sensor>
lib_deps =
${Heltec_ct62.lib_deps}
${esp32_ota.lib_deps}

View File

@@ -10,43 +10,10 @@ ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
#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);
#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
rtc_clock.begin(Wire);
return radio.std_init(&SPI);
}
uint32_t radio_get_rng_seed() {

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