Compare commits

..

121 Commits

Author SHA1 Message Date
Scott Powell
295f67d4fa Merge branch 'dev'
Some checks failed
Build and deploy Docs site to GitHub Pages / github-pages (push) Failing after 49s
2026-02-15 17:25:07 +11:00
Scott Powell
e2571accbe * ver 1.13.0 2026-02-15 17:24:37 +11:00
Liam Cottle
603bd0e446 Merge pull request #1346 from mboelen/patch-1
Update faq.md
2026-02-15 18:20:00 +13:00
ripplebiz
88452c412e Merge pull request #1603 from oltaco/fix-build.sh-for-RP2040-and-STM32
Add RP2040 and STM32 support to build.sh
2026-02-15 16:09:46 +11:00
ripplebiz
2220eca4f3 Merge pull request #1669 from Azuresword/fix/wio-tracker-l1-grove-sensor-v2
Fix WioTrackerL1 BLE companion: route sensors to Grove I2C bus (Wire1)
2026-02-15 15:55:25 +11:00
ripplebiz
a6e741e30e Merge pull request #1672 from ChaoticLeah/feature/mute-buzzer-icon
Add muted icon to show when buzzer is muted
2026-02-15 15:54:33 +11:00
Scott Powell
0abac35744 * client_repeat state now in _DEVICE_INFO response 2026-02-14 16:45:41 +11:00
Scott Powell
564a19d125 * companion client repeat mode support 2026-02-14 15:50:06 +11:00
taco
5df139f3d6 update build.sh to support RP2040 and STM32 2026-02-13 12:43:04 +11:00
taco
77675ab496 add -D ESP32_PLATFORM to esp32_base 2026-02-13 12:01:04 +11:00
Liam Cottle
246d681b2b Merge pull request #1678 from liamcottle/main
Some checks failed
Build and deploy Docs site to GitHub Pages / github-pages (push) Failing after 13s
Docs: Update Packet Formats
2026-02-12 17:13:51 +13:00
liamcottle
4224ddff0b refactor packet format docs 2026-02-12 17:12:23 +13:00
Liam Cottle
5ccae4bddc Merge pull request #1671 from recrof/rak3112-fix
fix: usb and ui for rak 3112
2026-02-12 11:39:27 +13:00
Leah
fb025fb67e Add muted icon to show when buzzer is muted 2026-02-11 10:00:20 +01:00
Rastislav Vysoky
beff18c53b fix usb and build for rak 3112 2026-02-11 09:34:41 +01:00
dylan
f720338c03 Fix WioTrackerL1 BLE companion: route sensors to Grove I2C bus (Wire1)
Sensors connected via the Grove I2C connector (D18/D17) were not detected
because the firmware scanned the OLED I2C bus (Wire, D14/D15) by default.
Adding ENV_PIN_SDA/SCL flags directs EnvironmentSensorManager to use Wire1,
matching the physical Grove connector pinout.

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

This patch needs to be reverted because it boot freezes t114 433Mhz variant.
2026-01-31 13:45:58 +00:00
taco
8d5eaf500d add makeBlobPath inline helper for esp32 2026-02-01 00:02:30 +11:00
taco
31ba971c60 only store advblob when adding/updating contacts 2026-02-01 00:02:30 +11:00
taco
e6e1b810f8 add DataStore::deleteBlobByKey() 2026-02-01 00:02:29 +11:00
taco
c5b1d30280 t114: remove extra DCDC enable 2026-01-31 23:48:28 +11:00
ViezeVingertjes
c786cfe613 Add KISS Modem firmware 2026-01-31 10:22:32 +01:00
Liam Cottle
06a83c0453 Merge pull request #1531 from agessaman/add-recv-errors-stats
Add recv_errors to CMD_GET_STATS STATS_TYPE_PACKETS response
2026-01-30 20:57:07 +13:00
agessaman
019bbf74d3 Add recv_errors to CMD_GET_STATS STATS_TYPE_PACKETS response
Append uint32_t recv_errors (RadioLib receive/CRC errors) to packet stats
binary frame. Frame size 26 -> 30 bytes. Update stats_binary_frames.md and
Python/TypeScript parsing examples for backward compatibility (accept >=26).
2026-01-29 20:44:11 -08:00
taco
c7eea3915d fix: remove esp_wifi.h from esp32board.h
saves ~500 bytes of dram and allows Tbeam to compile again
2026-01-30 15:07:40 +11:00
ripplebiz
cf7d95c6de Merge pull request #1509 from stevenlafl/tbeam-1w
Add LilyGO T-Beam 1W Support
2026-01-30 14:55:17 +11:00
ripplebiz
e60fb14e88 Merge pull request #1500 from Meshcore-Portugal/jbrazio/2026_03a6aa94
Allow usage of "/" in radio names
2026-01-30 12:09:08 +11:00
Liam Cottle
e50fe31fd6 Merge pull request #1528 from recrof/revert-1437-dev
Revert "Remove _serial->isConnected() logic from buzzer notifications"
2026-01-30 12:16:12 +13:00
Rastislav Vysoky
c345f1da8e Revert "Remove _serial->isConnected() logic from buzzer notifications" 2026-01-30 00:12:04 +01:00
SoCalix
31fbb679d5 Merge branch 'dev' into heltecv4-register1 2026-01-29 09:12:35 -06:00
fdlamotte
acca73f57e Merge pull request #1524 from recrof/thinknode-m3-m6-fixes
Elecrow ThinkNode M3, M6 build errors fix after NRF52Board base class migration
2026-01-29 11:07:32 -04:00
Rastislav Vysoky
2a321b53eb renamed board files 2026-01-29 16:00:19 +01:00
Rastislav Vysoky
3a7ccc085d fixed build errors and typos/inconsistencies 2026-01-29 15:32:51 +01:00
Steven Linn
a9a8299e14 Set LilyGO T-Beam 1W to use TX0 3.0V (within reference +2.85V~+3.15V) 2026-01-28 22:24:57 -07:00
Steven Linn
44e7c092c8 Add battery min/max voltage parameter support 2026-01-28 22:24:49 -07:00
Steven Linn
f7e54ea797 Add LilyGO T-Beam 1W Support 2026-01-28 22:24:43 -07:00
Max Litruv Boonzaayer
dd2a9044f3 Refactor display scaling definitions for HELTEC_VISION_MASTER_T190 2026-01-29 08:05:26 +11:00
liamcottle
706b5a39c6 allow manual deploy 2026-01-28 21:48:39 +13:00
liamcottle
c35c1961de add docs branch for testing 2026-01-28 21:48:39 +13:00
liamcottle
132c8961e8 add workflow to build and deploy docs to github pages 2026-01-28 21:48:39 +13:00
liamcottle
a87c0fe2d6 separate table of contents 2026-01-28 21:48:39 +13:00
liamcottle
0c2da8ce1e add support for mkdocs 2026-01-28 21:48:39 +13:00
João Brázio
edeafde51c Fix: Correct validation logic in isValidName function 2026-01-27 19:36:12 +00:00
Piotr Małek
3845a1c021 Fix incorrect INA260 address in debug message 2026-01-27 16:29:31 +01:00
Socalix
46e4cc06e3 Revert boosted gain flag to original 2026-01-21 21:12:54 -06:00
Socalix
4575800e40 Turn on register 0x8B5 LSB for improved RX, turn off boosted gain 2026-01-14 17:52:15 -06:00
Michael Boelen
599e15fd0c Update faq.md
Fix of a typo, change second sentence to start with a capital (like Q 3.6), adding missing dot in 3.8, and turn 3.8 into a question.

The file on dev branch is outdated, so submitting this on main.
2026-01-08 15:20:05 +00:00
212 changed files with 3142 additions and 996 deletions

36
.github/workflows/github-pages.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Build and deploy Docs site to GitHub Pages
on:
workflow_dispatch:
push:
branches:
- main
permissions:
contents: write
jobs:
github-pages:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
ruby-version: 3.x
- name: Build
run: |
pip install mkdocs-material
mkdocs build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
cname: docs.meshcore.nz
publish_dir: ./site
publish_branch: 'gh-pages'

1
CNAME Normal file
View File

@@ -0,0 +1 @@
docs.meshcore.nz

View File

@@ -39,9 +39,11 @@ For developers;
- Clone and open the MeshCore repository in Visual Studio Code.
- See the example applications you can modify and run:
- [Companion Radio](./examples/companion_radio) - For use with an external chat app, over BLE, USB or WiFi.
- [KISS Modem](./examples/kiss_modem) - Serial KISS protocol bridge for host applications. ([protocol docs](./docs/kiss_modem_protocol.md))
- [Simple Repeater](./examples/simple_repeater) - Extends network coverage by relaying messages.
- [Simple Room Server](./examples/simple_room_server) - A simple BBS server for shared Posts.
- [Simple Secure Chat](./examples/simple_secure_chat) - Secure terminal based text communication between devices.
- [Simple Sensor](./examples/simple_sensor) - Remote sensor node with telemetry and alerting.
The Simple Secure Chat example can be interacted with through the Serial Monitor in Visual Studio Code, or with a Serial USB Terminal on Android.

50
boards/t_beam_1w.json Normal file
View File

@@ -0,0 +1,50 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DLILYGO_TBEAM_1W",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [
[
"0x303A",
"0x1001"
]
],
"mcu": "esp32s3",
"variant": "lilygo_tbeam_1w"
},
"connectivity": [
"wifi",
"bluetooth",
"lora"
],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino"
],
"name": "LilyGo TBeam-1W",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"url": "http://www.lilygo.cn/",
"vendor": "LilyGo"
}

View File

@@ -7,6 +7,7 @@ sh build.sh <command> [target]
Commands:
help|usage|-h|--help: Shows this message.
list|-l: List firmwares available to build.
build-firmware <target>: Build the firmware for the given build target.
build-firmwares: Build all firmwares for all targets.
build-matching-firmwares <build-match-spec>: Build all firmwares for build targets containing the string given for <build-match-spec>.
@@ -46,19 +47,25 @@ $ sh build.sh build-firmware RAK_4631_repeater
EOF
}
# get a list of pio env names that start with "env:"
get_pio_envs() {
pio project config | grep 'env:' | sed 's/env://'
}
# Catch cries for help before doing anything else.
case $1 in
help|usage|-h|--help)
global_usage
exit 1
;;
list|-l)
get_pio_envs
exit 0
;;
esac
# get a list of pio env names that start with "env:"
get_pio_envs() {
echo $(pio project config | grep 'env:' | sed 's/env://')
}
# cache project config json for use in get_platform_for_env()
PIO_CONFIG_JSON=$(pio project config --json-output)
# $1 should be the string to find (case insensitive)
get_pio_envs_containing_string() {
@@ -82,6 +89,25 @@ get_pio_envs_ending_with_string() {
done
}
# get platform flag for a given environment
# $1 should be the environment name
get_platform_for_env() {
local env_name=$1
echo "$PIO_CONFIG_JSON" | python3 -c "
import sys, json, re
data = json.load(sys.stdin)
for section, options in data:
if section == 'env:$env_name':
for key, value in options:
if key == 'build_flags':
for flag in value:
match = re.search(r'(ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM)', flag)
if match:
print(match.group(1))
sys.exit(0)
"
}
# disable all debug logging flags if DISABLE_DEBUG=1 is set
disable_debug_flags() {
if [ "$DISABLE_DEBUG" == "1" ]; then
@@ -91,6 +117,8 @@ disable_debug_flags() {
# build firmware for the provided pio env in $1
build_firmware() {
# get env platform for post build actions
ENV_PLATFORM=($(get_platform_for_env $1))
# get git commit sha
COMMIT_HASH=$(git rev-parse --short HEAD)
@@ -121,27 +149,31 @@ build_firmware() {
# build firmware target
pio run -e $1
# build merge-bin for esp32 fresh install
if [ -f .pio/build/$1/firmware.bin ]; then
# build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin)
if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then
pio run -t mergebin -e $1
cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true
cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true
fi
# build .uf2 for nrf52 boards
if [[ -f .pio/build/$1/firmware.zip && -f .pio/build/$1/firmware.hex ]]; then
# build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2)
if [ "$ENV_PLATFORM" == "NRF52_PLATFORM" ]; then
python3 bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840
cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true
fi
# copy .bin, .uf2, and .zip to out folder
# e.g: Heltec_v3_room_server-v1.0.0-SHA.bin
# e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2
# for stm32, copy .bin and .hex to out folder
if [ "$ENV_PLATFORM" == "STM32_PLATFORM" ]; then
cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true
cp .pio/build/$1/firmware.hex out/${FIRMWARE_FILENAME}.hex 2>/dev/null || true
fi
# copy .bin for esp32 boards
cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true
cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true
# copy .zip and .uf2 of nrf52 boards
cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true
# for rp2040, copy .bin and .uf2 to out folder
if [ "$ENV_PLATFORM" == "RP2040_PLATFORM" ]; then
cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true
cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
fi
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 139 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M3.232,3.582C2.789,3.582 2.368,3.934 2.289,4.369L0.013,16.964C-0.066,17.399 0.229,17.751 0.671,17.751L3.087,17.751C3.529,17.751 3.951,17.399 4.03,16.964L4.935,11.951L6.592,17.293C6.672,17.572 6.923,17.751 7.235,17.751L10.434,17.751C10.746,17.751 11.062,17.572 11.243,17.293L14.835,11.925L13.924,16.964C13.844,17.399 14.14,17.751 14.583,17.751L16.998,17.751C17.44,17.751 17.862,17.399 17.941,16.964L20.217,4.369C20.298,3.934 20.002,3.582 19.56,3.582L16.46,3.582C16.147,3.582 15.831,3.761 15.65,4.04L9.76,12.872C9.668,13.013 9.446,12.975 9.397,12.81L6.976,4.04C6.895,3.761 6.645,3.582 6.332,3.582L3.232,3.582Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M20.853,17.751C20.853,17.751 32.797,17.751 32.797,17.751C33.063,17.751 33.317,17.538 33.364,17.278L33.863,14.504C33.91,14.242 33.733,14.031 33.467,14.031L25.166,14.031C25.077,14.031 25.019,13.96 25.034,13.873L25.281,12.508C25.296,12.421 25.38,12.35 25.469,12.35L32.146,12.35C32.411,12.35 32.665,12.137 32.712,11.877L33.157,9.421C33.204,9.159 33.027,8.949 32.761,8.949L26.085,8.949C25.996,8.949 25.938,8.877 25.953,8.79L26.216,7.328C26.232,7.241 26.316,7.17 26.405,7.17L34.706,7.17C34.971,7.17 35.226,6.957 35.272,6.695L35.756,4.021C35.804,3.761 35.627,3.548 35.361,3.548L23.417,3.548C22.975,3.548 22.551,3.902 22.473,4.337L20.191,16.962C20.114,17.397 20.409,17.751 20.853,17.751Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M45.291,17.749L45.291,17.751L45.705,17.751C47.783,17.751 49.767,16.095 50.136,14.052L50.375,12.727C50.744,10.685 49.359,9.029 47.28,9.029L40.882,9.029C40.617,9.029 40.44,8.818 40.487,8.556L40.649,7.664C40.696,7.402 40.95,7.191 41.215,7.191L49.87,7.191C50.313,7.191 50.735,6.839 50.814,6.404L51.183,4.368C51.262,3.931 50.966,3.579 50.523,3.579L41.063,3.579C38.985,3.579 37,5.235 36.631,7.278L36.37,8.723C36.001,10.767 37.386,12.422 39.465,12.422L45.863,12.422C46.128,12.422 46.305,12.633 46.258,12.895L46.138,13.565C46.091,13.826 45.837,14.037 45.571,14.037L36.675,14.037C36.233,14.037 35.811,14.389 35.732,14.824L35.346,16.962C35.267,17.397 35.562,17.749 36.005,17.749L45.291,17.749Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M67.068,3.575C67.068,3.575 64.393,3.575 64.393,3.575C63.951,3.575 63.529,3.927 63.45,4.361L62.654,8.766C62.639,8.853 62.554,8.923 62.466,8.923L57.282,8.923C57.193,8.923 57.135,8.853 57.15,8.766L57.946,4.361C58.023,3.927 57.73,3.575 57.287,3.575L54.613,3.575C54.17,3.575 53.748,3.927 53.669,4.361L51.392,16.964C51.313,17.399 51.608,17.751 52.05,17.751L54.725,17.751C55.168,17.751 55.589,17.399 55.668,16.964L56.48,12.478C56.495,12.392 56.58,12.32 56.668,12.32L61.852,12.32C61.941,12.32 61.999,12.39 61.984,12.478L61.174,16.964C61.096,17.399 61.391,17.751 61.834,17.751L64.508,17.751C64.951,17.751 65.372,17.399 65.451,16.964L67.729,4.361C67.804,3.927 67.511,3.575 67.068,3.575Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M71.102,17.751C71.102,17.751 78.96,17.751 78.96,17.751C79.402,17.751 79.824,17.399 79.903,16.964L80.288,14.824C80.367,14.389 80.072,14.037 79.629,14.037L72.808,14.037C72.542,14.037 72.365,13.826 72.412,13.565L73.48,7.686C73.527,7.426 73.781,7.213 74.045,7.213L80.866,7.213C81.309,7.213 81.73,6.861 81.81,6.427L82.188,4.335C82.267,3.9 81.971,3.548 81.529,3.548L73.691,3.548C71.618,3.548 69.638,5.197 69.265,7.234L68.011,14.046C67.639,16.091 69.022,17.751 71.102,17.751Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M95.833,3.529C95.833,3.529 87.654,3.529 87.654,3.529C85.576,3.529 83.592,5.186 83.223,7.228L81.99,14.052C81.621,16.094 83.006,17.751 85.084,17.751L93.263,17.751C95.341,17.751 97.326,16.095 97.695,14.052L98.928,7.228C99.297,5.186 97.911,3.529 95.833,3.529ZM93.488,13.567C93.44,13.828 93.186,14.039 92.921,14.039L86.762,14.039C86.496,14.039 86.319,13.828 86.366,13.567L87.434,7.663C87.481,7.402 87.735,7.191 88,7.191L94.157,7.191C94.423,7.191 94.6,7.402 94.553,7.663L93.488,13.567Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M99.884,17.751L102.557,17.751C102.999,17.751 103.421,17.399 103.5,16.965L103.973,14.348C103.988,14.261 104.073,14.19 104.161,14.19L107.397,14.186C107.557,14.186 107.69,14.265 107.756,14.395L109.281,17.37C109.458,17.722 109.78,17.764 110.751,17.749C111.32,17.756 112.184,17.713 113.577,17.713C114.025,17.713 114.3,17.244 114.079,16.853L112.413,13.953C112.37,13.876 112.417,13.772 112.509,13.727C113.795,13.102 114.814,11.889 115.068,10.487L115.649,7.262C116.02,5.218 114.635,3.562 112.557,3.562L102.448,3.562C102.006,3.562 101.584,3.914 101.505,4.349L99.225,16.964C99.146,17.399 99.442,17.751 99.884,17.751L99.884,17.751ZM105.255,7.268C105.27,7.181 105.354,7.11 105.443,7.11L110.674,7.11C111.069,7.11 111.331,7.424 111.261,7.812L110.877,9.933C110.806,10.319 110.431,10.634 110.038,10.634L104.806,10.634C104.718,10.634 104.66,10.564 104.675,10.475L105.255,7.268Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M116.642,17.751C116.642,17.751 128.586,17.751 128.586,17.751C128.851,17.751 129.105,17.538 129.152,17.278L129.651,14.504C129.698,14.242 129.521,14.031 129.256,14.031L120.955,14.031C120.866,14.031 120.808,13.96 120.823,13.873L121.069,12.508C121.084,12.421 121.169,12.35 121.257,12.35L127.934,12.35C128.2,12.35 128.454,12.137 128.501,11.877L128.945,9.421C128.992,9.159 128.815,8.949 128.55,8.949L121.873,8.949C121.785,8.949 121.726,8.877 121.741,8.79L122.005,7.328C122.02,7.241 122.105,7.17 122.193,7.17L130.495,7.17C130.76,7.17 131.014,6.957 131.061,6.695L131.545,4.021C131.592,3.761 131.415,3.548 131.15,3.548L119.206,3.548C118.763,3.548 118.34,3.902 118.261,4.337L115.98,16.962C115.902,17.397 116.198,17.751 116.642,17.751Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M134.674,0C134.674,0 132.059,0 132.059,0C131.965,0 131.877,0.074 131.86,0.166L131.783,0.594C131.766,0.686 131.828,0.76 131.921,0.76L132.745,0.76C132.764,0.76 132.776,0.775 132.773,0.793L132.406,2.819C132.39,2.91 132.452,2.984 132.545,2.984L133.108,2.984C133.201,2.984 133.29,2.91 133.307,2.819L133.673,0.793C133.676,0.775 133.694,0.76 133.713,0.76L134.536,0.76C134.629,0.76 134.718,0.686 134.735,0.594L134.812,0.166C134.828,0.074 134.767,0 134.674,0Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M135.278,0.002C135.185,0.002 135.096,0.076 135.079,0.167L134.6,2.819C134.583,2.91 134.646,2.984 134.739,2.984L135.247,2.984C135.34,2.984 135.429,2.91 135.446,2.819L135.636,1.763L135.985,2.888C136.002,2.947 136.055,2.984 136.121,2.984L136.794,2.984C136.86,2.984 136.926,2.947 136.964,2.888L137.72,1.758L137.528,2.819C137.512,2.91 137.574,2.984 137.667,2.984L138.176,2.984C138.269,2.984 138.358,2.91 138.374,2.819L138.853,0.167C138.87,0.076 138.808,0.002 138.715,0.002L138.062,0.002C137.997,0.002 137.93,0.039 137.892,0.098L136.652,1.957C136.633,1.987 136.586,1.979 136.575,1.944L136.066,0.098C136.049,0.039 135.996,0.002 135.93,0.002L135.278,0.002Z" style="fill:white;fill-rule:nonzero;"/>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1,16 @@
:root {
--md-primary-fg-color: #1F2937;
--md-primary-fg-color--light: #1F2937;
--md-primary-fg-color--dark: #1F2937;
--md-accent-fg-color: #1F2937;
}
/* hide git repo version */
.md-source__fact--version {
display: none;
}
/* underline links */
.md-typeset a {
text-decoration: underline;
}

View File

@@ -1,4 +1,6 @@
# MeshCore Repeater & Room Server CLI Commands
# CLI Commands
This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.
## Navigation
@@ -642,7 +644,7 @@
**Usage:**
- `region`
**Serial Only:** Yes
**Serial Only:** For firmware older than 1.12.0
---

View File

@@ -1,26 +1,42 @@
# MeshCore Device Communication Protocol Guide
# Companion Protocol
This document provides a comprehensive guide for communicating with MeshCore devices over Bluetooth Low Energy (BLE). It is platform-agnostic and can be used for Android, iOS, Python, JavaScript, or any other platform that supports BLE.
- **Last Updated**: 2026-01-03
- **Protocol Version**: Companion Firmware v1.12.0+
## ⚠️ Important Security Note
> NOTE: This document is still in development. Some information may be inaccurate.
**All secrets, hashes, and cryptographic values shown in this guide are EXAMPLE VALUES ONLY and are NOT real secrets.**
This document provides a comprehensive guide for communicating with MeshCore devices over Bluetooth Low Energy (BLE).
- The secret `9b647d242d6e1c5883fde0c5cf5c4c5e` used in examples is a made-up example value
- All hex values, public keys, and hashes in examples are for demonstration purposes only
- **Never use example secrets in production** - always generate new cryptographically secure random secrets
- This guide is for protocol documentation only - implement proper security practices in your actual implementation
It is platform-agnostic and can be used for Android, iOS, Python, JavaScript, or any other platform that supports BLE.
## Official Libraries
Please see the following repos for existing MeshCore Companion Protocol libraries.
- JavaScript: [https://github.com/meshcore-dev/meshcore.js](https://github.com/meshcore-dev/meshcore.js)
- Python: [https://github.com/meshcore-dev/meshcore_py](https://github.com/meshcore-dev/meshcore_py)
## Important Security Note
All secrets, hashes, and cryptographic values shown in this guide are example values only.
- All hex values, public keys and hashes are for demonstration purposes only
- Never use example secrets in production
- Always generate new cryptographically secure random secrets
- Please implement proper security practices in your implementation
- This guide is for protocol documentation only
## Table of Contents
1. [BLE Connection](#ble-connection)
2. [Protocol Overview](#protocol-overview)
2. [Packet Structure](#packet-structure)
3. [Commands](#commands)
4. [Channel Management](#channel-management)
5. [Secret Generation and QR Codes](#secret-generation-and-qr-codes)
6. [Message Handling](#message-handling)
7. [Response Parsing](#response-parsing)
8. [Example Implementation Flow](#example-implementation-flow)
5. [Message Handling](#message-handling)
6. [Response Parsing](#response-parsing)
7. [Example Implementation Flow](#example-implementation-flow)
8. [Best Practices](#best-practices)
9. [Troubleshooting](#troubleshooting)
---
@@ -28,181 +44,111 @@ This document provides a comprehensive guide for communicating with MeshCore dev
### Service and Characteristics
MeshCore devices expose a BLE service with the following UUIDs:
MeshCore Companion devices expose a BLE service with the following UUIDs:
- **Service UUID**: `0000ff00-0000-1000-8000-00805f9b34fb`
- **RX Characteristic** (Device → Client): `0000ff01-0000-1000-8000-00805f9b34fb`
- **TX Characteristic** (Client → Device): `0000ff02-0000-1000-8000-00805f9b34fb`
- **Service UUID**: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
- **RX Characteristic** (App → Firmware): `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`
- **TX Characteristic** (Firmware → App): `6E400003-B5A3-F393-E0A9-E50E24DCCA9E`
### Connection Steps
1. **Scan for Devices**
- Scan for BLE devices advertising the MeshCore service UUID
- Filter by device name (typically contains "MeshCore" or similar)
- Note the device MAC address for reconnection
- Scan for BLE devices advertising the MeshCore Service UUID
- Optionally filter by device name (typically contains "MeshCore" prefix)
- Note the device MAC address for reconnection
2. **Connect to GATT**
- Connect to the device using the discovered MAC address
- Wait for connection to be established
- Connect to the device using the discovered MAC address
- Wait for connection to be established
3. **Discover Services and Characteristics**
- Discover the service with UUID `0000ff00-0000-1000-8000-00805f9b34fb`
- Discover RX characteristic (`0000ff01-...`) for receiving data
- Discover TX characteristic (`0000ff02-...`) for sending commands
- Discover the service with UUID `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
- Discover the RX characteristic `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`
- Your app writes to this, the firmware reads from this
- Discover the TX characteristic `6E400003-B5A3-F393-E0A9-E50E24DCCA9E`
- The firmware writes to this, your app reads from this
4. **Enable Notifications**
- Subscribe to notifications on the RX characteristic
- Enable notifications/indications to receive data from the device
- On some platforms, you may need to write to a descriptor (e.g., `0x2902`) with value `0x01` or `0x02`
- Subscribe to notifications on the TX characteristic to receive data from the firmware
5. **Send AppStart Command**
- Send the app start command (see [Commands](#commands)) to initialize communication
- Wait for OK response before sending other commands
### Connection State Management
- **Disconnected**: No connection established
- **Connecting**: Connection attempt in progress
- **Connected**: GATT connection established, ready for commands
- **Error**: Connection failed or lost
5. **Send Initial Commands**
- Send `CMD_APP_START` to identify your app to firmware and get radio settings
- Send `CMD_DEVICE_QEURY` to fetch device info and negotiate supported protocol versions
- Send `CMD_SET_DEVICE_TIME` to set the firmware clock
- Send `CMD_GET_CONTACTS` to fetch all contacts
- Send `CMD_GET_CHANNEL` multiple times to fetch all channel slots
- Send `CMD_SYNC_NEXT_MESSAGE` to fetch the next message stored in firmware
- Setup listeners for push codes, such as `PUSH_CODE_MSG_WAITING` or `PUSH_CODE_ADVERT`
- See [Commands](#commands) section for information on other commands
**Note**: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.
### BLE Write Type
When writing commands to the TX characteristic, specify the write type:
When writing commands to the RX characteristic, specify the write type:
- **Write with Response** (default): Waits for acknowledgment from device
- **Write without Response**: Faster but no acknowledgment
**Platform-specific**:
- **Android**: Use `BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT` or `WRITE_TYPE_NO_RESPONSE`
- **iOS**: Use `CBCharacteristicWriteType.withResponse` or `.withoutResponse`
- **Python (bleak)**: Use `write_gatt_char()` with `response=True` or `False`
**Recommendation**: Use write with response for reliability, especially for critical commands like `SET_CHANNEL`.
**Recommendation**: Use write with response for reliability.
### MTU (Maximum Transmission Unit)
The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (66 bytes), you may need to:
1. **Request Larger MTU**: Request MTU of 512 bytes if supported
- Android: `gatt.requestMtu(512)`
- iOS: `peripheral.maximumWriteValueLength(for:)`
- Python (bleak): MTU is negotiated automatically
- Android: `gatt.requestMtu(512)`
- iOS: `peripheral.maximumWriteValueLength(for:)`
- Python (bleak): MTU is negotiated automatically
2. **Handle Chunking**: If MTU is small, commands may be split automatically by the BLE stack
- Ensure all chunks are sent before waiting for response
- Responses may also arrive in chunks - buffer until complete
### Command Sequencing and Timing
### Command Sequencing
**Critical**: Commands must be sent in the correct sequence:
1. **After Connection**:
- Wait for GATT connection established
- Wait for services/characteristics discovered
- Wait for notifications enabled (descriptor write complete)
- **Wait 200-1000ms** for device to be ready (some devices need initialization time)
- Send `APP_START` command
- **Wait for `PACKET_OK` response** before sending any other commands
- Wait for BLE connection to be established
- Wait for services/characteristics to be discovered
- Wait for notifications to be enabled
- Now you can safely send commands to the firmware
2. **Command-Response Matching**:
- Send one command at a time
- Wait for response before sending next command
- Use timeout (typically 5 seconds)
- Match response to command by:
- Command type (e.g., `GET_CHANNEL``PACKET_CHANNEL_INFO`)
- Sequence number (if implemented)
- First-in-first-out queue
3. **Timing Considerations**:
- Minimum delay between commands: 50-100ms
- After `APP_START`: Wait 200-500ms before next command
- After `SET_CHANNEL`: Wait 500-1000ms for channel to be created
- After enabling notifications: Wait 200ms before sending commands
**Example Flow**:
```python
# 1. Connect and discover
await connect_to_device(device)
await discover_services()
await enable_notifications()
await asyncio.sleep(0.2) # Wait for device ready
# 2. Send AppStart
send_command(build_app_start())
response = await wait_for_response(PACKET_OK, timeout=5.0)
if response.type != PACKET_OK:
raise Exception("AppStart failed")
# 3. Now safe to send other commands
await asyncio.sleep(0.1) # Small delay between commands
send_command(build_device_query())
response = await wait_for_response(PACKET_DEVICE_INFO, timeout=5.0)
```
- Send one command at a time
- Wait for a response before sending another command
- Use a timeout (typically 5 seconds)
- Match response to command by type (e.g: `CMD_GET_CHANNEL``RESP_CODE_CHANNEL_INFO`)
### Command Queue Management
For reliable operation, implement a command queue:
For reliable operation, implement a command queue.
1. **Queue Structure**:
- Maintain a queue of pending commands
- Track which command is currently waiting for response
- Only send next command after receiving response or timeout
**Queue Structure**:
2. **Implementation**:
```python
class CommandQueue:
def __init__(self):
self.queue = []
self.waiting_for_response = False
self.current_command = None
async def send_command(self, command, expected_response_type, timeout=5.0):
if self.waiting_for_response:
# Queue the command
self.queue.append((command, expected_response_type, timeout))
return
self.waiting_for_response = True
self.current_command = (command, expected_response_type, timeout)
# Send command
await write_to_tx_characteristic(command)
# Wait for response
response = await wait_for_response(expected_response_type, timeout)
self.waiting_for_response = False
self.current_command = None
# Process next queued command
if self.queue:
next_cmd, next_type, next_timeout = self.queue.pop(0)
await self.send_command(next_cmd, next_type, next_timeout)
return response
```
- Maintain a queue of pending commands
- Track which command is currently waiting for a response
- Only send next command after receiving response or timeout
3. **Error Handling**:
- On timeout: Clear current command, process next in queue
- On error: Log error, clear current command, process next
- Don't block queue on single command failure
**Error Handling**:
- On timeout, clear current command, process next in queue
- On error, log error, clear current command, process next
---
## Protocol Overview
## Packet Structure
The MeshCore protocol uses a binary format with the following structure:
- **Commands**: Sent from client to device via TX characteristic
- **Responses**: Received from device via RX characteristic (notifications)
- **All multi-byte integers**: Little-endian byte order
- **Commands**: Sent from app to firmware via RX characteristic
- **Responses**: Received from firmware via TX characteristic notifications
- **All multi-byte integers**: Little-endian byte order (except CayenneLPP which is Big-endian)
- **All strings**: UTF-8 encoding
### Packet Structure
Most packets follow this format:
```
[Packet Type (1 byte)] [Data (variable length)]
@@ -283,7 +229,7 @@ Byte 1: Channel Index (0-7)
Byte 0: 0x20
Byte 1: Channel Index (0-7)
Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)
Bytes 34-65: Secret (32 bytes, see [Secret Generation](#secret-generation))
Bytes 34-65: Secret (32 bytes)
```
**Total Length**: 66 bytes
@@ -298,7 +244,7 @@ Bytes 34-65: Secret (32 bytes, see [Secret Generation](#secret-generation))
- Padded with null bytes (0x00) if shorter
**Secret Field** (32 bytes):
- For **private channels**: 32-byte secret (see [Secret Generation](#secret-generation))
- For **private channels**: 32-byte secret
- For **public channels**: All zeros (0x00)
**Example** (create channel "YourChannelName" at index 1 with secret):
@@ -380,170 +326,33 @@ Byte 0: 0x14
### Channel Types
1. **Public Channels** (Index 0)
- No secret required
- Anyone with the channel name can join
- Use for open communication
2. **Private Channels** (Indices 1-7)
- Require a 16-byte secret
- Secret is expanded to 32 bytes using SHA-512 (see [Secret Generation](#secret-generation))
- Only devices with the secret can access the channel
1. **Public Channel**
- Uses a publicly known 16-byte key: `8b3387e9c5cdea6ac9e5edbaa115cd72`
- Anyone can join this channel, messages should be considered public
- Used as the default public group chat
2. **Hashtag Channels**
- Uses a secret key derived from the channel name
- It is the first 16 bytes of `sha256("#test")`
- For example hashtag channel `#test` has the key: `9cd8fcf22a47333b591d96a2b848b73f`
- Used as a topic based public group chat, separate from the default public channel
3. **Private Channels**
- Uses a randomly generated 16-byte secret key
- Messages should be considered private between those that know the secret
- Users should keep the key secret, and only share with those you want to communicate with
- Used as a secure private group chat
### Channel Lifecycle
1. **Create Channel**:
- Choose an available index (1-7 for private channels)
- Generate or provide a 16-byte secret
- Send `SET_CHANNEL` command with name and secret
- **Store the secret locally** (device does not return it)
2. **Query Channel**:
- Send `GET_CHANNEL` command with channel index
- Parse `PACKET_CHANNEL_INFO` response
- Note: Secret will be null in response (security feature)
1. **Set Channel**:
- Fetch all channel slots, and find one with empty name and all-zero secret
- Generate or provide a 16-byte secret
- Send `CMD_SET_CHANNEL` with name and secret
2. **Get Channel**:
- Send `CMD_GET_CHANNEL` with channel index
- Parse `RESP_CODE_CHANNEL_INFO` response
3. **Delete Channel**:
- Send `SET_CHANNEL` command with empty name and all-zero secret
- Or overwrite with a new channel
### Channel Index Management
- **Index 0**: Reserved for public channels
- **Indices 1-7**: Available for private channels
- If a channel exists at index 0 but should be private, migrate it to index 1-7
---
## Secret Generation and QR Codes
### Secret Generation
For private channels, generate a cryptographically secure 16-byte secret:
**Pseudocode**:
```python
import secrets
# Generate 16 random bytes
secret_bytes = secrets.token_bytes(16)
# Convert to hex string for storage/sharing
secret_hex = secret_bytes.hex() # 32 hex characters
```
**Important**: Use a cryptographically secure random number generator (CSPRNG). Do not use predictable values.
### Secret Expansion
When sending the secret to the device via `SET_CHANNEL`, the 16-byte secret must be expanded to 32 bytes:
**Process**:
1. Take the 16-byte secret
2. Compute SHA-512 hash: `hash = SHA-512(secret)`
3. Use the first 32 bytes of the hash as the secret field in the command
**Pseudocode**:
```python
import hashlib
secret_16_bytes = ... # Your 16-byte secret
sha512_hash = hashlib.sha512(secret_16_bytes).digest() # 64 bytes
secret_32_bytes = sha512_hash[:32] # First 32 bytes
```
This matches MeshCore's ED25519 key expansion method.
### QR Code Format
QR codes for sharing channel secrets use the following format:
**URL Scheme**:
```
meshcore://channel/add?name=<ChannelName>&secret=<32HexChars>
```
**Parameters**:
- `name`: Channel name (URL-encoded if needed)
- `secret`: 32-character hexadecimal representation of the 16-byte secret
**Example** (using example secret - NOT a real secret):
```
meshcore://channel/add?name=YourChannelName&secret=9b647d242d6e1c5883fde0c5cf5c4c5e
```
**Alternative Formats** (for backward compatibility):
1. **JSON Format**:
```json
{
"name": "YourChannelName",
"secret": "9b647d242d6e1c5883fde0c5cf5c4c5e"
}
```
*Note: The secret value above is an example only - generate your own secure random secret.*
2. **Plain Hex** (32 hex characters):
```
9b647d242d6e1c5883fde0c5cf5c4c5e
```
*Note: This is an example hex value - always generate your own cryptographically secure random secret.*
### QR Code Generation
**Steps**:
1. Generate or use existing 16-byte secret
2. Convert to 32-character hex string (lowercase)
3. URL-encode the channel name
4. Construct the `meshcore://` URL
5. Generate QR code from the URL string
**Example** (Python with `qrcode` library):
```python
import qrcode
from urllib.parse import quote
import secrets
channel_name = "YourChannelName"
# Generate a real cryptographically secure secret (NOT the example value)
secret_bytes = secrets.token_bytes(16)
secret_hex = secret_bytes.hex() # This will be a different value each time
# Example value shown in documentation: "9b647d242d6e1c5883fde0c5cf5c4c5e"
# DO NOT use the example value - always generate your own!
url = f"meshcore://channel/add?name={quote(channel_name)}&secret={secret_hex}"
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save("channel_qr.png")
```
### QR Code Scanning
When scanning a QR code:
1. **Parse URL Format**:
- Extract `name` and `secret` query parameters
- Validate secret is 32 hex characters
2. **Parse JSON Format**:
- Parse JSON object
- Extract `name` and `secret` fields
3. **Parse Plain Hex**:
- Extract only hex characters (0-9, a-f, A-F)
- Validate length is 32 characters
- Convert to lowercase
4. **Validate Secret**:
- Must be exactly 32 hex characters (16 bytes)
- Convert hex string to bytes
5. **Create Channel**:
- Use extracted name and secret
- Send `SET_CHANNEL` command
- Send `CMD_SET_CHANNEL` with empty name and all-zero secret
- Or overwrite with a new channel
---
@@ -693,28 +502,28 @@ Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)).
### Packet Types
| Value | Name | Description |
|-------|------|-------------|
| 0x00 | PACKET_OK | Command succeeded |
| 0x01 | PACKET_ERROR | Command failed |
| 0x02 | PACKET_CONTACT_START | Start of contact list |
| 0x03 | PACKET_CONTACT | Contact information |
| 0x04 | PACKET_CONTACT_END | End of contact list |
| 0x05 | PACKET_SELF_INFO | Device self-information |
| 0x06 | PACKET_MSG_SENT | Message sent confirmation |
| 0x07 | PACKET_CONTACT_MSG_RECV | Contact message (standard) |
| 0x08 | PACKET_CHANNEL_MSG_RECV | Channel message (standard) |
| 0x09 | PACKET_CURRENT_TIME | Current time response |
| 0x0A | PACKET_NO_MORE_MSGS | No more messages available |
| 0x0C | PACKET_BATTERY | Battery level |
| 0x0D | PACKET_DEVICE_INFO | Device information |
| 0x10 | PACKET_CONTACT_MSG_RECV_V3 | Contact message (V3 with SNR) |
| 0x11 | PACKET_CHANNEL_MSG_RECV_V3 | Channel message (V3 with SNR) |
| 0x12 | PACKET_CHANNEL_INFO | Channel information |
| 0x80 | PACKET_ADVERTISEMENT | Advertisement packet |
| 0x82 | PACKET_ACK | Acknowledgment |
| 0x83 | PACKET_MESSAGES_WAITING | Messages waiting notification |
| 0x88 | PACKET_LOG_DATA | RF log data (can be ignored) |
| Value | Name | Description |
|-------|----------------------------|-------------------------------|
| 0x00 | PACKET_OK | Command succeeded |
| 0x01 | PACKET_ERROR | Command failed |
| 0x02 | PACKET_CONTACT_START | Start of contact list |
| 0x03 | PACKET_CONTACT | Contact information |
| 0x04 | PACKET_CONTACT_END | End of contact list |
| 0x05 | PACKET_SELF_INFO | Device self-information |
| 0x06 | PACKET_MSG_SENT | Message sent confirmation |
| 0x07 | PACKET_CONTACT_MSG_RECV | Contact message (standard) |
| 0x08 | PACKET_CHANNEL_MSG_RECV | Channel message (standard) |
| 0x09 | PACKET_CURRENT_TIME | Current time response |
| 0x0A | PACKET_NO_MORE_MSGS | No more messages available |
| 0x0C | PACKET_BATTERY | Battery level |
| 0x0D | PACKET_DEVICE_INFO | Device information |
| 0x10 | PACKET_CONTACT_MSG_RECV_V3 | Contact message (V3 with SNR) |
| 0x11 | PACKET_CHANNEL_MSG_RECV_V3 | Channel message (V3 with SNR) |
| 0x12 | PACKET_CHANNEL_INFO | Channel information |
| 0x80 | PACKET_ADVERTISEMENT | Advertisement packet |
| 0x82 | PACKET_ACK | Acknowledgment |
| 0x83 | PACKET_MESSAGES_WAITING | Messages waiting notification |
| 0x88 | PACKET_LOG_DATA | RF log data (can be ignored) |
### Parsing Responses
@@ -1081,33 +890,6 @@ def on_notification_received(data):
send_command(tx_char, build_get_message())
```
### QR Code Sharing
```python
import secrets
from urllib.parse import quote
# 1. Generate QR code data
channel_name = "YourChannelName"
# Generate a real secret (NOT the example value from documentation)
secret_bytes = secrets.token_bytes(16)
secret_hex = secret_bytes.hex()
# Example value in documentation: "9b647d242d6e1c5883fde0c5cf5c4c5e"
# DO NOT use example values - always generate your own secure random secrets!
url = f"meshcore://channel/add?name={quote(channel_name)}&secret={secret_hex}"
# 2. Generate QR code image
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
# 3. Display or save QR code
img.save("channel_qr.png")
```
---
## Best Practices
@@ -1121,81 +903,37 @@ img.save("channel_qr.png")
- Always use cryptographically secure random number generators
- Store secrets securely (encrypted storage)
- Never log or transmit secrets in plain text
- Device does not return secrets - you must store them locally
3. **Message Handling**:
- Poll `GET_MESSAGE` periodically or when `PACKET_MESSAGES_WAITING` is received
- Handle message chunking for long messages (>133 characters)
- Implement message deduplication to avoid processing the same message twice
- Send `CMD_SYNC_NEXT_MESSAGE` when `PUSH_CODE_MSG_WAITING` is received
- Implement message deduplication to avoid display the same message twice
4. **Error Handling**:
4. **Channel Management**:
- Fetch all channel slots even if you encounter an empty slot
- Ideally save new channels into the first empty slot
5. **Error Handling**:
- Implement timeouts for all commands (typically 5 seconds)
- Handle `PACKET_ERROR` responses appropriately
- Log errors for debugging but don't expose sensitive information
5. **Channel Management**:
- Avoid using channel index 0 for private channels
- Migrate channels from index 0 to 1-7 if needed
- Query channels after connection to discover existing channels
---
## Platform-Specific Notes
### Android
- Use `BluetoothGatt` API
- Request `BLUETOOTH_CONNECT` and `BLUETOOTH_SCAN` permissions (Android 12+)
- Enable notifications by writing to descriptor `0x2902` with value `0x01` or `0x02`
### iOS
- Use `CoreBluetooth` framework
- Implement `CBPeripheralDelegate` for notifications
- Request Bluetooth permissions in Info.plist
### Python
- Use `bleak` library for cross-platform BLE support
- Handle async/await for BLE operations
- Use `asyncio` for command-response patterns
### JavaScript/Node.js
- Use `noble` or `@abandonware/noble` for BLE
- Handle callbacks or promises for async operations
- Use `Buffer` for binary data manipulation
- Handle `RESP_CODE_ERR` responses appropriately
---
## Troubleshooting
### Connection Issues
- **Device not found**: Ensure device is powered on and advertising
- **Connection timeout**: Check Bluetooth permissions and device proximity
- **GATT errors**: Ensure proper service/characteristic discovery
### Command Issues
- **No response**: Verify notifications are enabled, check connection state
- **Error responses**: Verify command format, check channel index validity
- **Timeout**: Increase timeout value or check device responsiveness
- **Error responses**: Verify command format and check error code
- **Timeout**: Increase timeout value or try again
### Message Issues
- **Messages not received**: Poll `GET_MESSAGE` command periodically
- **Duplicate messages**: Implement message deduplication using timestamps/hashes
- **Message truncation**: Split long messages into chunks
### Secret/Channel Issues
- **Secret not working**: Verify secret expansion (SHA-512) is correct
- **Channel not found**: Query channels after connection to discover existing channels
- **Channel index 0**: Migrate to index 1-7 for private channels
---
## References
- MeshCore Python implementation: `meshcore_py-main/src/meshcore/`
- BLE GATT Specification: Bluetooth SIG Core Specification
- ED25519 Key Expansion: RFC 8032
---
**Last Updated**: 2025-01-01
**Protocol Version**: Based on MeshCore v1.36.0+
- **Duplicate messages**: Implement message deduplication using timestamp/content as a unique id
- **Message truncation**: Send long messages as separate shorter messages

13
docs/docs.md Normal file
View File

@@ -0,0 +1,13 @@
# Local Documentation
This document explains how to build and view the MeshCore documentation locally.
## Building and viewing Docs
```
pip install mkdocs
pip install mkdocs-material
```
- `mkdocs serve` - Start the live-reloading docs server.
- `mkdocs build` - Build the documentation site.

View File

@@ -1,12 +1,7 @@
**MeshCore-FAQ**<!-- omit from toc -->
# Frequently Asked Questions
A list of frequently-asked questions and answers for MeshCore
The current version of this MeshCore FAQ is at https://github.com/meshcore-dev/MeshCore/blob/main/docs/faq.md.
This MeshCore FAQ is also mirrored at https://github.com/LitBomb/MeshCore-FAQ and might have newer updates if pull requests on Scott's MeshCore repo are not approved yet.
author: https://github.com/LitBomb<!-- omit from toc -->
---
- [1. Introduction](#1-introduction)
- [1.1. Q: What is MeshCore?](#11-q-what-is-meshcore)
- [1.2. Q: What do you need to start using MeshCore?](#12-q-what-do-you-need-to-start-using-meshcore)
@@ -28,8 +23,8 @@ author: https://github.com/LitBomb<!-- omit from toc -->
- [3.4. Q: What is the password to join a room server?](#34-q-what-is-the-password-to-join-a-room-server)
- [3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?](#35-q-can-i-retrieve-a-repeaters-private-key-or-set-a-repeaters-private-key)
- [3.6. Q: The first byte of my repeater's public key collides with an exisitng repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?](#36-q-the-first-byte-of-my-repeaters-public-key-collides-with-an-exisitng-repeater-on-the-mesh--how-do-i-get-a-new-private-key-with-a-matching-public-key-that-has-its-first-byte-of-my-choosing)
- [3.7. Q: My repeater maybe suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. what can I do?](#37-q-my-repeater-maybe-suffering-from-deafness-due-to-high-power-interference-near-my-meshs-frequency-it-is-not-hearing-other-in-range-meshcore-radios--what-can-i-do)
- [3.8 Q: How do I make my repeater an observer on the mesh](#38-q-how-do-i-make-my-repeater-an-observer-on-the-mesh)
- [3.7. Q: My repeater maybe suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?](#37-q-my-repeater-maybe-suffering-from-deafness-due-to-high-power-interference-near-my-meshs-frequency-it-is-not-hearing-other-in-range-meshcore-radios--what-can-i-do)
- [3.8. Q: How do I make my repeater an observer on the mesh?](#38-q-how-do-i-make-my-repeater-an-observer-on-the-mesh)
- [4. T-Deck Related](#4-t-deck-related)
- [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)
@@ -112,15 +107,15 @@ Anyone is able to build anything they like on top of MeshCore without paying any
### 1.2. Q: What do you need to start using MeshCore?
**A:** Everything you need for MeshCore is available at:
Main web site: [https://meshcore.co.uk/](https://meshcore.co.uk/)
Firmware Flasher: https://flasher.meshcore.co.uk/
Phone Client Applications: https://meshcore.co.uk/apps.html
MeshCore Firmware GitHub: https://github.com/ripplebiz/MeshCore
NOTE: Andy Kirby has a very useful [intro video](https://www.youtube.com/watch?v=t1qne8uJBAc) for beginners.
- Main web site: [https://meshcore.co.uk](https://meshcore.co.uk)
- Firmware Flasher: [https://flasher.meshcore.co.uk](https://flasher.meshcore.co.uk)
- MeshCore Firmware on GitHub: [https://github.com/meshcore-dev/MeshCore](https://github.com/meshcore-dev/MeshCore)
- MeshCore Companion App: [https://meshcore.nz](https://meshcore.nz)
- MeshCore Map: [https://meshcore.co.uk/map.html](https://meshcore.co.uk/map.html)
- Andy Kirby has a very useful [intro video](https://www.youtube.com/watch?v=t1qne8uJBAc) for beginners.
You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).
You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).
#### 1.2.1. Hardware
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.
@@ -282,9 +277,9 @@ Reboot the repeater after `set prv.key <hex>` command for the new private key to
**A:** You can generate a new private key and specific the first byte of its public key here: https://gessaman.com/mc-keygen/
### 3.7. Q: My repeater maybe suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. what can I do?
### 3.7. Q: My repeater maybe suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?
**A:** This may be due to the SX1262 radio's auto gain control feature. You can use this command to preiodically reset its AGC.
**A:** This may be due to the SX1262 radio's auto gain control feature. You can use this command to periodically reset its AGC.
`set agc.reset.interval <number>`
@@ -293,9 +288,9 @@ The `<number>` unit is in seconds and is incremented by 4. `set agc.reset.inter
This is a very low cost operation. AGC reset is done by simply setting `state = STATE_IDLE;` in function `RadioLibWrapper::resetAGC()` in `RadioLibWrappers.cpp`
### 3.8 Q: How do I make my repeater an observer on the mesh
### 3.8. Q: How do I make my repeater an observer on the mesh?
**A:** The observer instruction is available here: https://analyzer.letsme.sh/observer/onboard
**A:** The observer instruction is available here: https://analyzer.letsmesh.net/observer/onboard
---
@@ -535,7 +530,7 @@ MeshCore clients would need to reset path constantly and flood traffic across th
This could change in the future if MeshCore develops a client firmware that repeats.
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1354780032140054659)
### 5.12. Q: How do I add a node to the [MeshCore Map]([url](https://meshcore.co.uk/map.html))
### 5.12. Q: How do I add a node to the [MeshCore Map](https://meshcore.co.uk/map.html)
**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`
@@ -612,7 +607,7 @@ From here, reference repeater and room server command line commands on MeshCore
**A:** Yes. See the following:
#### 5.14.1. meshcoremqtt
A Python script to send meshcore debug and packet capture data to MQTT for analysis. Cisien's version is a fork of Andrew-a-g's and is being used to to collect data for https://map.w0z.is/messages and https://analyzer.letsme.sh/
A Python script to send meshcore debug and packet capture data to MQTT for analysis. Cisien's version is a fork of Andrew-a-g's and is being used to to collect data for https://map.w0z.is/messages and https://analyzer.letsmesh.net/
https://github.com/Cisien/meshcoretomqtt
https://github.com/Andrew-a-g/meshcoretomqtt
@@ -637,7 +632,7 @@ pyMC_Core is a Python port of MeshCore, designed for Raspberry Pi and similar ha
https://github.com/rightup/pyMC_core
#### 5.14.7. MeshCore Packet Decoder
A TypeScript library for decoding MeshCore mesh networking packets with full cryptographic support. Uses WebAssembly (WASM) for Ed25519 key derivation through the orlp/ed25519 library. It powers the [MeshCore Packet Analyzer](https://analyzer.letsme.sh/packets).
A TypeScript library for decoding MeshCore mesh networking packets with full cryptographic support. Uses WebAssembly (WASM) for Ed25519 key derivation through the orlp/ed25519 library. It powers the [MeshCore Packet Analyzer](https://analyzer.letsmesh.net/packets).
https://github.com/michaelhart/meshcore-decoder
#### 5.14.8. meshcore-pi

15
docs/index.md Normal file
View File

@@ -0,0 +1,15 @@
# Introduction
Welcome to the MeshCore documentation.
Below are a few quick start guides.
- [Frequently Asked Questions](./faq.md)
- [CLI Commands](./cli_commands.md)
- [Companion Protocol](./companion_protocol.md)
- [Packet Format](./packet_format.md)
- [QR Codes](./qr_codes.md)
If you find a mistake in any of our documentation, or find something is missing, please feel free to open a pull request for us to review.
- [Documentation Source](https://github.com/meshcore-dev/MeshCore/tree/main/docs)

282
docs/kiss_modem_protocol.md Normal file
View File

@@ -0,0 +1,282 @@
# MeshCore KISS Modem Protocol
Standard KISS TNC firmware for MeshCore LoRa radios. Compatible with any KISS client (Direwolf, APRSdroid, YAAC, etc.) for sending and receiving raw packets. MeshCore-specific extensions (cryptography, radio configuration, telemetry) are available through the standard SetHardware (0x06) command.
## Serial Configuration
115200 baud, 8N1, no flow control.
## Frame Format
Standard KISS framing per the KA9Q/K3MC specification.
| Byte | Name | Description |
|------|------|-------------|
| `0xC0` | FEND | Frame delimiter |
| `0xDB` | FESC | Escape character |
| `0xDC` | TFEND | Escaped FEND (FESC + TFEND = 0xC0) |
| `0xDD` | TFESC | Escaped FESC (FESC + TFESC = 0xDB) |
```
┌──────┬───────────┬──────────────┬──────┐
│ FEND │ Type Byte │ Data (escaped)│ FEND │
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │
└──────┴───────────┴──────────────┴──────┘
```
### Type Byte
The type byte is split into two nibbles:
| Bits | Field | Description |
|------|-------|-------------|
| 7-4 | Port | Port number (0 for single-port TNC) |
| 3-0 | Command | Command number |
Maximum unescaped frame size: 512 bytes.
## Standard KISS Commands
### Host to TNC
| Command | Value | Data | Description |
|---------|-------|------|-------------|
| Data | `0x00` | Raw packet | Queue packet for transmission |
| TXDELAY | `0x01` | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) |
| Persistence | `0x02` | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) |
| SlotTime | `0x03` | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) |
| TXtail | `0x04` | Delay (1 byte) | Post-TX hold time in 10ms units (default: 0) |
| FullDuplex | `0x05` | Mode (1 byte) | 0 = half duplex, nonzero = full duplex (default: 0) |
| SetHardware | `0x06` | Sub-command + data | MeshCore extensions (see below) |
| Return | `0xFF` | - | Exit KISS mode (no-op) |
### TNC to Host
| Type | Value | Data | Description |
|------|-------|------|-------------|
| Data | `0x00` | Raw packet | Received packet from radio |
Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes.
### CSMA Behavior
The TNC implements p-persistent CSMA for half-duplex operation:
1. When a packet is queued, monitor carrier detect
2. When the channel clears, generate a random value 0-255
3. If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
4. Otherwise, wait SlotTime and repeat from step 1
In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.
## SetHardware Extensions (0x06)
MeshCore-specific functionality uses the standard KISS SetHardware command. The first byte of SetHardware data is a sub-command. Standard KISS clients ignore these frames.
### Frame Format
```
┌──────┬──────┬─────────────┬──────────────┬──────┐
│ FEND │ 0x06 │ Sub-command │ Data (escaped)│ FEND │
│ 0xC0 │ │ 1 byte │ variable │ 0xC0 │
└──────┴──────┴─────────────┴──────────────┴──────┘
```
### Request Sub-commands (Host to TNC)
| Sub-command | Value | Data |
|-------------|-------|------|
| GetIdentity | `0x01` | - |
| GetRandom | `0x02` | Length (1 byte, 1-64) |
| VerifySignature | `0x03` | PubKey (32) + Signature (64) + Data |
| SignData | `0x04` | Data to sign |
| EncryptData | `0x05` | Key (32) + Plaintext |
| DecryptData | `0x06` | Key (32) + MAC (2) + Ciphertext |
| KeyExchange | `0x07` | Remote PubKey (32) |
| Hash | `0x08` | Data to hash |
| SetRadio | `0x09` | Freq (4) + BW (4) + SF (1) + CR (1) |
| SetTxPower | `0x0A` | Power dBm (1) |
| GetRadio | `0x0B` | - |
| GetTxPower | `0x0C` | - |
| GetCurrentRssi | `0x0D` | - |
| IsChannelBusy | `0x0E` | - |
| GetAirtime | `0x0F` | Packet length (1) |
| GetNoiseFloor | `0x10` | - |
| GetVersion | `0x11` | - |
| GetStats | `0x12` | - |
| GetBattery | `0x13` | - |
| GetMCUTemp | `0x14` | - |
| GetSensors | `0x15` | Permissions (1) |
| GetDeviceName | `0x16` | - |
| Ping | `0x17` | - |
| Reboot | `0x18` | - |
| SetSignalReport | `0x19` | Enable (1): 0x00=disable, nonzero=enable |
| GetSignalReport | `0x1A` | - |
### Response Sub-commands (TNC to Host)
Response codes use the high-bit convention: `response = command | 0x80`. Generic and unsolicited responses use the `0xF0`+ range.
| Sub-command | Value | Data |
|-------------|-------|------|
| Identity | `0x81` | PubKey (32) |
| Random | `0x82` | Random bytes (1-64) |
| Verify | `0x83` | Result (1): 0x00=invalid, 0x01=valid |
| Signature | `0x84` | Signature (64) |
| Encrypted | `0x85` | MAC (2) + Ciphertext |
| Decrypted | `0x86` | Plaintext |
| SharedSecret | `0x87` | Shared secret (32) |
| Hash | `0x88` | SHA-256 hash (32) |
| Radio | `0x8B` | Freq (4) + BW (4) + SF (1) + CR (1) |
| TxPower | `0x8C` | Power dBm (1) |
| CurrentRssi | `0x8D` | RSSI dBm (1, signed) |
| ChannelBusy | `0x8E` | Result (1): 0x00=clear, 0x01=busy |
| Airtime | `0x8F` | Milliseconds (4) |
| NoiseFloor | `0x90` | dBm (2, signed) |
| Version | `0x91` | Version (1) + Reserved (1) |
| Stats | `0x92` | RX (4) + TX (4) + Errors (4) |
| Battery | `0x93` | Millivolts (2) |
| MCUTemp | `0x94` | Temperature (2, signed) |
| Sensors | `0x95` | CayenneLPP payload |
| DeviceName | `0x96` | Name (variable, UTF-8) |
| Pong | `0x97` | - |
| SignalReport | `0x9A` | Status (1): 0x00=disabled, 0x01=enabled |
| OK | `0xF0` | - |
| Error | `0xF1` | Error code (1) |
| TxDone | `0xF8` | Result (1): 0x00=failed, 0x01=success |
| RxMeta | `0xF9` | SNR (1) + RSSI (1) |
### Error Codes
| Code | Value | Description |
|------|-------|-------------|
| InvalidLength | `0x01` | Request data too short |
| InvalidParam | `0x02` | Invalid parameter value |
| NoCallback | `0x03` | Feature not available |
| MacFailed | `0x04` | MAC verification failed |
| UnknownCmd | `0x05` | Unknown sub-command |
| EncryptFailed | `0x06` | Encryption failed |
### Unsolicited Events
The TNC sends these SetHardware frames without a preceding request:
**TxDone (0xF8)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure.
**RxMeta (0xF9)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame.
## Data Formats
### Radio Parameters (SetRadio / Radio response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| Frequency | 4 bytes | Hz (e.g., 869618000) |
| Bandwidth | 4 bytes | Hz (e.g., 62500) |
| SF | 1 byte | Spreading factor (5-12) |
| CR | 1 byte | Coding rate (5-8) |
### Version (Version response)
| Field | Size | Description |
|-------|------|-------------|
| Version | 1 byte | Firmware version |
| Reserved | 1 byte | Always 0 |
### Encrypted (Encrypted response)
| Field | Size | Description |
|-------|------|-------------|
| MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes |
| Ciphertext | variable | AES-128-CBC encrypted data |
### Airtime (Airtime response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| Airtime | 4 bytes | uint32_t, estimated air time in milliseconds |
### Noise Floor (NoiseFloor response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| Noise floor | 2 bytes | int16_t, dBm (signed) |
The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.
### Stats (Stats response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| RX | 4 bytes | Packets received |
| TX | 4 bytes | Packets transmitted |
| Errors | 4 bytes | Receive errors |
### Battery (Battery response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| Millivolts | 2 bytes | uint16_t, battery voltage in mV |
### MCU Temperature (MCUTemp response)
All values little-endian.
| Field | Size | Description |
|-------|------|-------------|
| Temperature | 2 bytes | int16_t, tenths of °C (e.g., 253 = 25.3°C) |
Returns `NoCallback` error if the board does not support temperature readings.
### Device Name (DeviceName response)
| Field | Size | Description |
|-------|------|-------------|
| Name | variable | UTF-8 string, no null terminator |
### Reboot
Sends an `OK` response, flushes serial, then reboots the device. The host should expect the connection to drop.
### Sensor Permissions (GetSensors)
| Bit | Value | Description |
|-----|-------|-------------|
| 0 | `0x01` | Base (battery) |
| 1 | `0x02` | Location (GPS) |
| 2 | `0x04` | Environment (temp, humidity, pressure) |
Use `0x07` for all permissions.
### Sensor Data (Sensors response)
Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.mydevices.com/docs/lorawan/cayenne-lpp) for parsing.
## Cryptographic Algorithms
| Operation | Algorithm |
|-----------|-----------|
| Identity / Signing / Verification | Ed25519 |
| Key Exchange | X25519 (ECDH) |
| Encryption | AES-128-CBC + HMAC-SHA256 (MAC truncated to 2 bytes) |
| Hashing | SHA-256 |
## Notes
- Data payload limit (255 bytes) matches MeshCore MAX_TRANS_UNIT; no change needed for KISS “1024+ recommended” (that applies to general TNCs, not MeshCore)
- Modem generates identity on first boot (stored in flash)
- All multi-byte values are little-endian unless stated otherwise
- SNR values in RxMeta are multiplied by 4 for 0.25 dB precision
- TxDone is sent as a SetHardware event after each transmission
- Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames
- See [packet_structure.md](./packet_structure.md) for packet format

120
docs/packet_format.md Normal file
View File

@@ -0,0 +1,120 @@
# Packet Format
This document describes the MeshCore packet format.
- `0xYY` indicates `YY` in hex notation.
- `0bYY` indicates `YY` in binary notation.
- Bit 0 indicates the bit furthest to the right: `0000000X`
- Bit 7 indicates the bit furthest to the left: `X0000000`
## Version 1 Packet Format
This is the protocol level packet structure used in MeshCore firmware v1.12.0
```
[header][transport_codes(optional)][path_length][path][payload]
```
- [header](#header-format) - 1 byte
- 8-bit Format: `0bVVPPPPRR` - `V=Version` - `P=PayloadType` - `R=RouteType`
- Bits 0-1 - 2-bits - [Route Type](#route-types)
- `0x00`/`0b00` - `ROUTE_TYPE_TRANSPORT_FLOOD` - Flood Routing + Transport Codes
- `0x01`/`0b01` - `ROUTE_TYPE_FLOOD` - Flood Routing
- `0x02`/`0b10` - `ROUTE_TYPE_DIRECT` - Direct Routing
- `0x03`/`0b11` - `ROUTE_TYPE_TRANSPORT_DIRECT` - Direct Routing + Transport Codes
- Bits 2-5 - 4-bits - [Payload Type](#payload-types)
- `0x00`/`0b0000` - `PAYLOAD_TYPE_REQ` - Request (destination/source hashes + MAC)
- `0x01`/`0b0001` - `PAYLOAD_TYPE_RESPONSE` - Response to `REQ` or `ANON_REQ`
- `0x02`/`0b0010` - `PAYLOAD_TYPE_TXT_MSG` - Plain text message
- `0x03`/`0b0011` - `PAYLOAD_TYPE_ACK` - Acknowledgment
- `0x04`/`0b0100` - `PAYLOAD_TYPE_ADVERT` - Node advertisement
- `0x05`/`0b0101` - `PAYLOAD_TYPE_GRP_TXT` - Group text message (unverified)
- `0x06`/`0b0110` - `PAYLOAD_TYPE_GRP_DATA` - Group datagram (unverified)
- `0x07`/`0b0111` - `PAYLOAD_TYPE_ANON_REQ` - Anonymous request
- `0x08`/`0b1000` - `PAYLOAD_TYPE_PATH` - Returned path
- `0x09`/`0b1001` - `PAYLOAD_TYPE_TRACE` - Trace a path, collecting SNR for each hop
- `0x0A`/`0b1010` - `PAYLOAD_TYPE_MULTIPART` - Packet is part of a sequence of packets
- `0x0B`/`0b1011` - `PAYLOAD_TYPE_CONTROL` - Control packet data (unencrypted)
- `0x0C`/`0b1100` - reserved
- `0x0D`/`0b1101` - reserved
- `0x0E`/`0b1110` - reserved
- `0x0F`/`0b1111` - `PAYLOAD_TYPE_RAW_CUSTOM` - Custom packet (raw bytes, custom encryption)
- Bits 6-7 - 2-bits - [Payload Version](#payload-versions)
- `0x00`/`0b00` - v1 - 1-byte src/dest hashes, 2-byte MAC
- `0x01`/`0b01` - v2 - Future version (e.g., 2-byte hashes, 4-byte MAC)
- `0x02`/`0b10` - v3 - Future version
- `0x03`/`0b11` - v4 - Future version
- `transport_codes` - 4 bytes (optional)
- Only present for `ROUTE_TYPE_TRANSPORT_FLOOD` and `ROUTE_TYPE_TRANSPORT_DIRECT`
- `transport_code_1` - 2 bytes - `uint16_t` - calculated from region scope
- `transport_code_2` - 2 bytes - `uint16_t` - reserved
- `path_length` - 1 byte - Length of the path field in bytes
- `path` - size provided by `path_length` - Path to use for Direct Routing
- Up to a maximum of 64 bytes, defined by `MAX_PATH_SIZE`
- v1.12.0 firmware and older drops packets with `path_length` [larger than 64](https://github.com/meshcore-dev/MeshCore/blob/e812632235274ffd2382adf5354168aec765d416/src/Dispatcher.cpp#L144)
- `payload` - variable length - Payload Data
- Up to a maximum 184 bytes, defined by `MAX_PACKET_PAYLOAD`
- Generally this is the remainder of the raw packet data
- The firmware parses this data based on the provided Payload Type
- v1.12.0 firmware and older drops packets with `payload` sizes [larger than 184](https://github.com/meshcore-dev/MeshCore/blob/e812632235274ffd2382adf5354168aec765d416/src/Dispatcher.cpp#L152)
### Packet Format
| 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_length | 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`) | Data for the provided Payload Type |
> NOTE: see the [Payloads](./payloads.md) documentation for more information about the content of specific payload types.
### Header Format
Bit 0 means the lowest bit (1s place)
| Bits | Mask | Field | Description |
|------|--------|-----------------|----------------------------------|
| 0-1 | `0x03` | Route Type | Flood, Direct, etc |
| 2-5 | `0x3C` | Payload Type | Request, Response, ACK, etc |
| 6-7 | `0xC0` | Payload Version | Versioning of the payload format |
### Route Types
| Value | Name | Description |
|--------|-------------------------------|----------------------------------|
| `0x00` | `ROUTE_TYPE_TRANSPORT_FLOOD` | Flood Routing + Transport Codes |
| `0x01` | `ROUTE_TYPE_FLOOD` | Flood Routing |
| `0x02` | `ROUTE_TYPE_DIRECT` | Direct Routing |
| `0x03` | `ROUTE_TYPE_TRANSPORT_DIRECT` | Direct Routing + Transport Codes |
### Payload Types
| Value | Name | Description |
|--------|---------------------------|----------------------------------------------|
| `0x00` | `PAYLOAD_TYPE_REQ` | Request (destination/source hashes + MAC) |
| `0x01` | `PAYLOAD_TYPE_RESPONSE` | Response to `REQ` or `ANON_REQ` |
| `0x02` | `PAYLOAD_TYPE_TXT_MSG` | Plain text message |
| `0x03` | `PAYLOAD_TYPE_ACK` | Acknowledgment |
| `0x04` | `PAYLOAD_TYPE_ADVERT` | Node advertisement |
| `0x05` | `PAYLOAD_TYPE_GRP_TXT` | Group text message (unverified) |
| `0x06` | `PAYLOAD_TYPE_GRP_DATA` | Group datagram (unverified) |
| `0x07` | `PAYLOAD_TYPE_ANON_REQ` | Anonymous request |
| `0x08` | `PAYLOAD_TYPE_PATH` | Returned path |
| `0x09` | `PAYLOAD_TYPE_TRACE` | Trace a path, collecting SNR for each hop |
| `0x0A` | `PAYLOAD_TYPE_MULTIPART` | Packet is part of a sequence of packets |
| `0x0B` | `PAYLOAD_TYPE_CONTROL` | Control packet data (unencrypted) |
| `0x0C` | reserved | reserved |
| `0x0D` | reserved | reserved |
| `0x0E` | reserved | reserved |
| `0x0F` | `PAYLOAD_TYPE_RAW_CUSTOM` | Custom packet (raw bytes, custom encryption) |
### Payload Versions
| Value | Version | Description |
|--------|---------|--------------------------------------------------|
| `0x00` | 1 | 1-byte src/dest hashes, 2-byte MAC |
| `0x01` | 2 | Future version (e.g., 2-byte hashes, 4-byte MAC) |
| `0x02` | 3 | Future version |
| `0x03` | 4 | Future version |

View File

@@ -1,60 +0,0 @@
# Packet Structure
| 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.
## Header Breakdown
bit 0 means the lowest bit (1s place)
| Bits | Mask | Field | Description |
|-------|--------|-----------------|-----------------------------------------------|
| 0-1 | `0x03` | Route Type | Flood, Direct, Reserved - see below. |
| 2-5 | `0x3C` | Payload Type | Request, Response, ACK, etc. - see below. |
| 6-7 | `0xC0` | Payload Version | Versioning of the payload format - see below. |
## Route Type Values
| Value | Name | Description |
|--------|-------------------------------|--------------------------------------|
| `0x00` | `ROUTE_TYPE_TRANSPORT_FLOOD` | Flood routing mode + transport codes |
| `0x01` | `ROUTE_TYPE_FLOOD` | Flood routing mode (builds up path). |
| `0x02` | `ROUTE_TYPE_DIRECT` | Direct route (path is supplied). |
| `0x03` | `ROUTE_TYPE_TRANSPORT_DIRECT` | direct route + transport codes |
## Payload Type Values
| Value | Name | Description |
|--------|---------------------------|-----------------------------------------------|
| `0x00` | `PAYLOAD_TYPE_REQ` | Request (destination/source hashes + MAC). |
| `0x01` | `PAYLOAD_TYPE_RESPONSE` | Response to REQ or ANON_REQ. |
| `0x02` | `PAYLOAD_TYPE_TXT_MSG` | Plain text message. |
| `0x03` | `PAYLOAD_TYPE_ACK` | Acknowledgment. |
| `0x04` | `PAYLOAD_TYPE_ADVERT` | Node advertisement. |
| `0x05` | `PAYLOAD_TYPE_GRP_TXT` | Group text message (unverified). |
| `0x06` | `PAYLOAD_TYPE_GRP_DATA` | Group datagram (unverified). |
| `0x07` | `PAYLOAD_TYPE_ANON_REQ` | Anonymous request. |
| `0x08` | `PAYLOAD_TYPE_PATH` | Returned path. |
| `0x09` | `PAYLOAD_TYPE_TRACE` | trace a path, collecting SNI for each hop. |
| `0x0A` | `PAYLOAD_TYPE_MULTIPART` | packet is part of a sequence of packets. |
| `0x0B` | `PAYLOAD_TYPE_CONTROL` | control packet data (unencrypted) |
| `0x0C` | . | reserved |
| `0x0D` | . | reserved |
| `0x0E` | . | reserved |
| `0x0F` | `PAYLOAD_TYPE_RAW_CUSTOM` | Custom packet (raw bytes, custom encryption). |
## Payload Version Values
| Value | Version | Description |
|--------|---------|---------------------------------------------------|
| `0x00` | 1 | 1-byte src/dest hashes, 2-byte MAC. |
| `0x01` | 2 | Future version (e.g., 2-byte hashes, 4-byte MAC). |
| `0x02` | 3 | Future version. |
| `0x03` | 4 | Future version. |

View File

@@ -1,5 +1,6 @@
# Meshcore payloads
Inside of each [meshcore packet](./packet_structure.md) is a payload, identified by the payload type in the packet header. The types of payloads are:
# Payload Format
Inside each [MeshCore Packet](./packet_format.md) is a payload, identified by the payload type in the packet header. The types of payloads are:
* Node advertisement.
* Acknowledgment.
@@ -80,12 +81,12 @@ Returned path, request, response, and plain text messages are all formatted in t
Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message.
| Field | Size (bytes) | Description |
|-------------|--------------|----------------------------------------------------------------------------------------------|
| path length | 1 | length of next field |
| path | see above | a list of node hashes (one byte each) |
| extra type | 1 | extra, bundled payload type, eg., acknowledgement or response. Same values as in [packet structure](./packet_structure.md) |
| extra | rest of data | extra, bundled payload content, follows same format as main content defined by this document |
| Field | Size (bytes) | Description |
|-------------|--------------|----------------------------------------------------------------------------------------------------------------------|
| path length | 1 | length of next field |
| path | see above | a list of node hashes (one byte each) |
| extra type | 1 | extra, bundled payload type, eg., acknowledgement or response. Same values as in [Packet Format](./packet_format.md) |
| extra | rest of data | extra, bundled payload content, follows same format as main content defined by this document |
## Request

34
docs/qr_codes.md Normal file
View File

@@ -0,0 +1,34 @@
# QR Codes
This document provides an overview of QR Code formats that can be used for sharing MeshCore channels and contacts. The formats described below are supported by the MeshCore mobile app.
## Add Channel
**Example URL**:
```
meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72
```
**Parameters**:
- `name`: Channel name (URL-encoded if needed)
- `secret`: 16-byte secret represented as 32 hex characters
## Add Contact
**Example URL**:
```
meshcore://contact/add?name=Example+Contact&public_key=9cd8fcf22a47333b591d96a2b848b73f457b1bb1a3ea2453a885f9e5787765b1&type=1
```
**Parameters**:
- `name`: Contact name (URL-encoded if needed)
- `public_key`: 32-byte public key represented as 64 hex characters
- `type`: numeric contact type
- `1`: Companion
- `2`: Repeater
- `3`: Room Server
- `4`: Sensor

View File

@@ -94,7 +94,7 @@ struct StatsRadio {
## RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)
**Total Frame Size:** 26 bytes
**Total Frame Size:** 26 bytes (legacy) or 30 bytes (includes `recv_errors`)
| Offset | Size | Type | Field Name | Description | Range/Notes |
|--------|------|------|------------|-------------|-------------|
@@ -106,12 +106,14 @@ struct StatsRadio {
| 14 | 4 | uint32_t | direct_tx | Packets sent via direct routing | 0 - 4,294,967,295 |
| 18 | 4 | uint32_t | flood_rx | Packets received via flood routing | 0 - 4,294,967,295 |
| 22 | 4 | uint32_t | direct_rx | Packets received via direct routing | 0 - 4,294,967,295 |
| 26 | 4 | uint32_t | recv_errors | Receive/CRC errors (RadioLib); present only in 30-byte frame | 0 - 4,294,967,295 |
### Notes
- Counters are cumulative from boot and may wrap.
- `recv = flood_rx + direct_rx`
- `sent = flood_tx + direct_tx`
- Clients should accept frame length ≥ 26; if length ≥ 30, parse `recv_errors` at offset 26.
### Example Structure (C/C++)
@@ -125,6 +127,7 @@ struct StatsPackets {
uint32_t direct_tx;
uint32_t flood_rx;
uint32_t direct_rx;
uint32_t recv_errors; // present when frame size is 30
} __attribute__((packed));
```
@@ -183,11 +186,12 @@ def parse_stats_radio(frame):
}
def parse_stats_packets(frame):
"""Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 bytes)"""
"""Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)"""
assert len(frame) >= 26, "STATS_TYPE_PACKETS frame too short"
response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \
struct.unpack('<B B I I I I I I', frame)
struct.unpack('<B B I I I I I I', frame[:26])
assert response_code == 24 and stats_type == 2, "Invalid response type"
return {
result = {
'recv': recv,
'sent': sent,
'flood_tx': flood_tx,
@@ -195,6 +199,10 @@ def parse_stats_packets(frame):
'flood_rx': flood_rx,
'direct_rx': direct_rx
}
if len(frame) >= 30:
(recv_errors,) = struct.unpack('<I', frame[26:30])
result['recv_errors'] = recv_errors
return result
```
---
@@ -251,6 +259,7 @@ interface StatsPackets {
direct_tx: number;
flood_rx: number;
direct_rx: number;
recv_errors?: number; // present when frame is 30 bytes
}
function parseStatsCore(buffer: ArrayBuffer): StatsCore {
@@ -286,12 +295,15 @@ function parseStatsRadio(buffer: ArrayBuffer): StatsRadio {
function parseStatsPackets(buffer: ArrayBuffer): StatsPackets {
const view = new DataView(buffer);
if (buffer.byteLength < 26) {
throw new Error('STATS_TYPE_PACKETS frame too short');
}
const response_code = view.getUint8(0);
const stats_type = view.getUint8(1);
if (response_code !== 24 || stats_type !== 2) {
throw new Error('Invalid response type');
}
return {
const result: StatsPackets = {
recv: view.getUint32(2, true),
sent: view.getUint32(6, true),
flood_tx: view.getUint32(10, true),
@@ -299,6 +311,10 @@ function parseStatsPackets(buffer: ArrayBuffer): StatsPackets {
flood_rx: view.getUint32(18, true),
direct_rx: view.getUint32(22, true)
};
if (buffer.byteLength >= 30) {
result.recv_errors = view.getUint32(26, true);
}
return result;
}
```

96
docs/terminal_chat_cli.md Normal file
View File

@@ -0,0 +1,96 @@
# Terminal Chat CLI
Below are the commands you can enter into the Terminal Chat clients:
```
set freq {frequency}
```
Set the LoRa frequency. Example: set freq 915.8
```
set tx {tx-power-dbm}
```
Sets LoRa transmit power in dBm.
```
set name {name}
```
Sets your advertisement name.
```
set lat {latitude}
```
Sets your advertisement map latitude. (decimal degrees)
```
set lon {longitude}
```
Sets your advertisement map longitude. (decimal degrees)
```
set af {air-time-factor}
```
Sets the transmit air-time-factor.
```
time {epoch-secs}
```
Set the device clock using UNIX epoch seconds. Example: time 1738242833
```
advert
```
Sends an advertisement packet
```
clock
```
Displays current time per device's clock.
```
ver
```
Shows the device version and firmware build date.
```
card
```
Displays *your* 'business card', for other to manually _import_
```
import {card}
```
Imports the given card to your contacts.
```
list {n}
```
List all contacts by most recent. (optional {n}, is the last n by advertisement date)
```
to
```
Shows the name of current recipient contact. (for subsequent 'send' commands)
```
to {name-prefix}
```
Sets the recipient to the _first_ matching contact (in 'list') by the name prefix. (ie. you don't have to type whole name)
```
send {text}
```
Sends the text message (as DM) to current recipient.
```
reset path
```
Resets the path to current recipient, for new path discovery.
```
public {text}
```
Sends the text message to the built-in 'public' group channel

View File

@@ -212,7 +212,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.read(pad, 1); // 62
file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62
file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
@@ -247,7 +247,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.write(pad, 1); // 62
file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62
file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
@@ -560,14 +560,20 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src
}
return false; // error
}
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
return true; // this is just a stub on NRF52/STM32 platforms
}
#else
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
char path[64];
inline void makeBlobPath(const uint8_t key[], int key_len, char* path, size_t path_size) {
char fname[18];
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
mesh::Utils::toHex(fname, key, key_len);
sprintf(path, "/bl/%s", fname);
}
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
char path[64];
makeBlobPath(key, key_len, path, sizeof(path));
if (_fs->exists(path)) {
File f = openRead(_fs, path);
@@ -582,11 +588,7 @@ uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_b
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
char path[64];
char fname[18];
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
mesh::Utils::toHex(fname, key, key_len);
sprintf(path, "/bl/%s", fname);
makeBlobPath(key, key_len, path, sizeof(path));
File f = openWrite(_fs, path);
if (f) {
@@ -598,4 +600,13 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src
}
return false; // error
}
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
char path[64];
makeBlobPath(key, key_len, path, sizeof(path));
_fs->remove(path);
return true; // return true even if file did not exist
}
#endif

View File

@@ -42,6 +42,7 @@ public:
void migrateToSecondaryFS();
uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]);
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len);
bool deleteBlobByKey(const uint8_t key[], int key_len);
File openRead(const char* filename);
File openRead(FILESYSTEM* fs, const char* filename);
bool removeFile(const char* filename);

View File

@@ -56,6 +56,7 @@
#define CMD_SEND_ANON_REQ 57
#define CMD_SET_AUTOADD_CONFIG 58
#define CMD_GET_AUTOADD_CONFIG 59
#define CMD_GET_ALLOWED_REPEAT_FREQ 60
// Stats sub-types for CMD_GET_STATS
#define STATS_TYPE_CORE 0
@@ -88,6 +89,7 @@
#define RESP_CODE_TUNING_PARAMS 23
#define RESP_CODE_STATS 24 // v8+, second byte is stats type
#define RESP_CODE_AUTOADD_CONFIG 25
#define RESP_ALLOWED_REPEAT_FREQ 26
#define SEND_TIMEOUT_BASE_MILLIS 500
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
@@ -307,6 +309,7 @@ bool MyMesh::shouldOverwriteWhenFull() const {
}
void MyMesh::onContactOverwrite(const uint8_t* pub_key) {
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage
if (_serial->isConnected()) {
out_frame[0] = PUSH_CODE_CONTACT_DELETED;
memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE);
@@ -330,10 +333,11 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
_serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
}
}
} else {
#ifdef DISPLAY_CLASS
if (_ui && !_prefs.buzzer_quiet) _ui->notify(UIEventType::newContactMessage); //buzz if enabled
if (_ui) _ui->notify(UIEventType::newContactMessage);
#endif
}
// add inbound-path to mem cache
if (path && path_len <= sizeof(AdvertPath::path)) { // check path is valid
@@ -440,7 +444,9 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
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 (!_prefs.buzzer_quiet) _ui->notify(UIEventType::contactMessage); //buzz if enabled
if (!_serial->isConnected()) {
_ui->notify(UIEventType::contactMessage);
}
}
#endif
}
@@ -451,6 +457,10 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
return false;
}
bool MyMesh::allowPacketForward(const mesh::Packet* packet) {
return _prefs.client_repeat != 0;
}
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: dynamic send_scope, depending on recipient and current 'home' Region
if (send_scope.isNull()) {
@@ -525,8 +535,11 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
_serial->writeFrame(frame, 1);
} else {
#ifdef DISPLAY_CLASS
if (_ui) _ui->notify(UIEventType::channelMessage);
#endif
}
#ifdef DISPLAY_CLASS
// Get the channel name from the channel index
const char *channel_name = "Unknown";
@@ -534,10 +547,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
if (getChannel(channel_idx, channel_details)) {
channel_name = channel_details.name;
}
if (_ui) {
_ui->newMsg(path_len, channel_name, text, offline_queue_len);
if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::channelMessage); //buzz if enabled
}
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
#endif
}
@@ -796,7 +806,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.bw = LORA_BW;
_prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.buzzer_quiet = 0;
_prefs.gps_enabled = 0; // GPS disabled by default
_prefs.gps_interval = 0; // No automatic GPS updates by default
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed
@@ -835,8 +844,7 @@ void MyMesh::begin(bool has_display) {
_prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f);
_prefs.sf = constrain(_prefs.sf, 5, 12);
_prefs.cr = constrain(_prefs.cr, 5, 8);
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER);
_prefs.buzzer_quiet = constrain(_prefs.buzzer_quiet, 0, 1); // Ensure boolean 0 or 1
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER);
_prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours
@@ -879,6 +887,24 @@ uint32_t MyMesh::getBLEPin() {
return _active_ble_pin;
}
struct FreqRange {
uint32_t lower_freq, upper_freq;
};
static FreqRange repeat_freq_ranges[] = {
{ 433000, 433000 },
{ 869000, 869000 },
{ 918000, 918000 }
};
bool MyMesh::isValidClientRepeatFreq(uint32_t f) const {
for (int i = 0; i < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]); i++) {
auto r = &repeat_freq_ranges[i];
if (f >= r->lower_freq && f <= r->upper_freq) return true;
}
return false;
}
void MyMesh::startInterface(BaseSerialInterface &serial) {
_serial = &serial;
serial.enable();
@@ -902,6 +928,7 @@ void MyMesh::handleCmdFrame(size_t len) {
i += 40;
StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20);
i += 20;
out_frame[i++] = _prefs.client_repeat; // v9+
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_APP_START &&
len >= 8) { // sent when app establishes connection, respond with node ID
@@ -1123,6 +1150,7 @@ void MyMesh::handleCmdFrame(size_t len) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient && removeContact(*recipient)) {
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE);
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
writeOKFrame();
} else {
@@ -1205,13 +1233,20 @@ void MyMesh::handleCmdFrame(size_t len) {
i += 4;
uint8_t sf = cmd_frame[i++];
uint8_t cr = cmd_frame[i++];
uint8_t repeat = 0; // default - false
if (len > i) {
repeat = cmd_frame[i++]; // FIRMWARE_VER_CODE 9+
}
if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 &&
if (repeat && !isValidClientRepeatFreq(freq)) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 &&
bw <= 500000) {
_prefs.sf = sf;
_prefs.cr = cr;
_prefs.freq = (float)freq / 1000.0;
_prefs.bw = (float)bw / 1000.0;
_prefs.client_repeat = repeat;
savePrefs();
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
@@ -1225,10 +1260,11 @@ void MyMesh::handleCmdFrame(size_t len) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else if (cmd_frame[0] == CMD_SET_RADIO_TX_POWER) {
if (cmd_frame[1] > MAX_LORA_TX_POWER) {
int8_t power = (int8_t)cmd_frame[1];
if (power < -9 || power > MAX_LORA_TX_POWER) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
_prefs.tx_power_dbm = cmd_frame[1];
_prefs.tx_power_dbm = power;
savePrefs();
radio_set_tx_power(_prefs.tx_power_dbm);
writeOKFrame();
@@ -1564,7 +1600,7 @@ void MyMesh::handleCmdFrame(size_t len) {
sendDirect(pkt, &cmd_frame[10], path_len);
uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2);
uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len);
uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len >> path_sz);
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = 0;
@@ -1688,12 +1724,14 @@ void MyMesh::handleCmdFrame(size_t len) {
uint32_t n_sent_direct = getNumSentDirect();
uint32_t n_recv_flood = getNumRecvFlood();
uint32_t n_recv_direct = getNumRecvDirect();
uint32_t n_recv_errors = radio_driver.getPacketsRecvErrors();
memcpy(&out_frame[i], &recv, 4); i += 4;
memcpy(&out_frame[i], &sent, 4); i += 4;
memcpy(&out_frame[i], &n_sent_flood, 4); i += 4;
memcpy(&out_frame[i], &n_sent_direct, 4); i += 4;
memcpy(&out_frame[i], &n_recv_flood, 4); i += 4;
memcpy(&out_frame[i], &n_recv_direct, 4); i += 4;
memcpy(&out_frame[i], &n_recv_errors, 4); i += 4;
_serial->writeFrame(out_frame, i);
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type
@@ -1735,6 +1773,15 @@ void MyMesh::handleCmdFrame(size_t len) {
out_frame[i++] = RESP_CODE_AUTOADD_CONFIG;
out_frame[i++] = _prefs.autoadd_config;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ) {
int i = 0;
out_frame[i++] = RESP_ALLOWED_REPEAT_FREQ;
for (int k = 0; k < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]) && i + 8 < sizeof(out_frame); k++) {
auto r = &repeat_freq_ranges[k];
memcpy(&out_frame[i], &r->lower_freq, 4); i += 4;
memcpy(&out_frame[i], &r->upper_freq, 4); i += 4;
}
_serial->writeFrame(out_frame, i);
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);

View File

@@ -5,14 +5,14 @@
#include "AbstractUITask.h"
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 8
#define FIRMWARE_VER_CODE 9
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "29 Jan 2026"
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.12.0"
#define FIRMWARE_VERSION "v1.13.0"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -108,6 +108,7 @@ protected:
int calcRxDelay(float score, uint32_t air_time) const override;
uint8_t getExtraAckTransmitCount() const override;
bool filterRecvFloodPacket(mesh::Packet* packet) override;
bool allowPacketForward(const mesh::Packet* packet) override;
void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override;
@@ -176,6 +177,7 @@ private:
void checkCLIRescueCmd();
void checkSerialInterface();
bool isValidClientRepeatFreq(uint32_t f) const;
// helpers, short-cuts
void saveChannels() { _store->saveChannels(this); }

View File

@@ -17,7 +17,7 @@ struct NodePrefs { // persisted to file
uint8_t multi_acks;
uint8_t manual_add_contacts;
float bw;
uint8_t tx_power_dbm;
int8_t tx_power_dbm;
uint8_t telemetry_mode_base;
uint8_t telemetry_mode_loc;
uint8_t telemetry_mode_env;
@@ -28,4 +28,5 @@ struct NodePrefs { // persisted to file
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval; // GPS read interval in seconds
uint8_t autoadd_config; // bitmask for auto-add contacts config
uint8_t client_repeat;
};

View File

@@ -194,6 +194,7 @@ void setup() {
);
#ifdef WIFI_SSID
board.setInhibitSleep(true); // prevent sleep when WiFi is active
WiFi.begin(WIFI_SSID, WIFI_PWD);
serial_interface.begin(TCP_PORT);
#elif defined(BLE_PIN_CODE)

View File

@@ -103,8 +103,14 @@ class HomeScreen : public UIScreen {
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
// Convert millivolts to percentage
const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
#ifndef BATT_MIN_MILLIVOLTS
#define BATT_MIN_MILLIVOLTS 3000
#endif
#ifndef BATT_MAX_MILLIVOLTS
#define BATT_MAX_MILLIVOLTS 4200
#endif
const int minMilliVolts = BATT_MIN_MILLIVOLTS;
const int maxMilliVolts = BATT_MAX_MILLIVOLTS;
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
@@ -125,6 +131,14 @@ class HomeScreen : public UIScreen {
// fill the battery based on the percentage
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
// show muted icon if buzzer is muted
#ifdef PIN_BUZZER
if (_task->isBuzzerQuiet()) {
display.setColor(DisplayDriver::RED);
display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8);
}
#endif
}
CayenneLPP sensors_lpp;
@@ -452,15 +466,17 @@ class MsgPreviewScreen : public UIScreen {
};
#define MAX_UNREAD_MSGS 32
int num_unread;
int head = MAX_UNREAD_MSGS - 1; // index of latest unread message
MsgEntry unread[MAX_UNREAD_MSGS];
public:
MsgPreviewScreen(UITask* task, mesh::RTCClock* rtc) : _task(task), _rtc(rtc) { num_unread = 0; }
void addPreview(uint8_t path_len, const char* from_name, const char* msg) {
if (num_unread >= MAX_UNREAD_MSGS) return; // full
head = (head + 1) % MAX_UNREAD_MSGS;
if (num_unread < MAX_UNREAD_MSGS) num_unread++;
auto p = &unread[num_unread++];
auto p = &unread[head];
p->timestamp = _rtc->getCurrentTime();
if (path_len == 0xFF) {
sprintf(p->origin, "(D) %s:", from_name);
@@ -478,7 +494,7 @@ public:
sprintf(tmp, "Unread: %d", num_unread);
display.print(tmp);
auto p = &unread[0];
auto p = &unread[head];
int secs = _rtc->getCurrentTime() - p->timestamp;
if (secs < 60) {
@@ -514,14 +530,10 @@ public:
bool handleInput(char c) override {
if (c == KEY_NEXT || c == KEY_RIGHT) {
head = (head + MAX_UNREAD_MSGS - 1) % MAX_UNREAD_MSGS;
num_unread--;
if (num_unread == 0) {
_task->gotoHomeScreen();
} else {
// delete first/curr item from unread queue
for (int i = 0; i < num_unread; i++) {
unread[i] = unread[i + 1];
}
}
return true;
}

View File

@@ -78,6 +78,14 @@ public:
bool hasDisplay() const { return _display != NULL; }
bool isButtonPressed() const;
bool isBuzzerQuiet() {
#ifdef PIN_BUZZER
return buzzer.isQuiet();
#else
return true;
#endif
}
void toggleBuzzer();
bool getGPSState();
void toggleGPS();

View File

@@ -115,4 +115,8 @@ static const uint8_t advert_icon[] = {
0x38, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x30,
0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const uint8_t muted_icon[] = {
0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20
};

View File

@@ -149,8 +149,14 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) {
// Convert millivolts to percentage
const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
#ifndef BATT_MIN_MILLIVOLTS
#define BATT_MIN_MILLIVOLTS 3000
#endif
#ifndef BATT_MAX_MILLIVOLTS
#define BATT_MAX_MILLIVOLTS 4200
#endif
const int minMilliVolts = BATT_MIN_MILLIVOLTS;
const int maxMilliVolts = BATT_MAX_MILLIVOLTS;
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%

View File

@@ -0,0 +1,581 @@
#include "KissModem.h"
#include <CayenneLPP.h>
KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng,
mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors)
: _serial(serial), _identity(identity), _rng(rng), _radio(radio), _board(board), _sensors(sensors) {
_rx_len = 0;
_rx_escaped = false;
_rx_active = false;
_has_pending_tx = false;
_pending_tx_len = 0;
_txdelay = KISS_DEFAULT_TXDELAY;
_persistence = KISS_DEFAULT_PERSISTENCE;
_slottime = KISS_DEFAULT_SLOTTIME;
_txtail = 0;
_fullduplex = 0;
_tx_state = TX_IDLE;
_tx_timer = 0;
_setRadioCallback = nullptr;
_setTxPowerCallback = nullptr;
_getCurrentRssiCallback = nullptr;
_getStatsCallback = nullptr;
_config = {0, 0, 0, 0, 0};
_signal_report_enabled = true;
}
void KissModem::begin() {
_rx_len = 0;
_rx_escaped = false;
_rx_active = false;
_has_pending_tx = false;
_tx_state = TX_IDLE;
}
void KissModem::writeByte(uint8_t b) {
if (b == KISS_FEND) {
_serial.write(KISS_FESC);
_serial.write(KISS_TFEND);
} else if (b == KISS_FESC) {
_serial.write(KISS_FESC);
_serial.write(KISS_TFESC);
} else {
_serial.write(b);
}
}
void KissModem::writeFrame(uint8_t type, const uint8_t* data, uint16_t len) {
_serial.write(KISS_FEND);
writeByte(type);
for (uint16_t i = 0; i < len; i++) {
writeByte(data[i]);
}
_serial.write(KISS_FEND);
}
void KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
_serial.write(KISS_FEND);
writeByte(KISS_CMD_SETHARDWARE);
writeByte(sub_cmd);
for (uint16_t i = 0; i < len; i++) {
writeByte(data[i]);
}
_serial.write(KISS_FEND);
}
void KissModem::writeHardwareError(uint8_t error_code) {
writeHardwareFrame(HW_RESP_ERROR, &error_code, 1);
}
void KissModem::loop() {
while (_serial.available()) {
uint8_t b = _serial.read();
if (b == KISS_FEND) {
if (_rx_active && _rx_len > 0) {
processFrame();
}
_rx_len = 0;
_rx_escaped = false;
_rx_active = true;
continue;
}
if (!_rx_active) continue;
if (b == KISS_FESC) {
_rx_escaped = true;
continue;
}
if (_rx_escaped) {
_rx_escaped = false;
if (b == KISS_TFEND) b = KISS_FEND;
else if (b == KISS_TFESC) b = KISS_FESC;
else continue;
}
if (_rx_len < KISS_MAX_FRAME_SIZE) {
_rx_buf[_rx_len++] = b;
} else {
/* Buffer full with no FEND; reset so we don't stay stuck ignoring input. */
_rx_len = 0;
_rx_escaped = false;
_rx_active = false;
}
}
processTx();
}
void KissModem::processFrame() {
if (_rx_len < 1) return;
uint8_t type_byte = _rx_buf[0];
if (type_byte == KISS_CMD_RETURN) return;
uint8_t port = (type_byte >> 4) & 0x0F;
uint8_t cmd = type_byte & 0x0F;
if (port != 0) return;
const uint8_t* data = &_rx_buf[1];
uint16_t data_len = _rx_len - 1;
switch (cmd) {
case KISS_CMD_DATA:
if (data_len > 0 && data_len <= KISS_MAX_PACKET_SIZE && !_has_pending_tx) {
memcpy(_pending_tx, data, data_len);
_pending_tx_len = data_len;
_has_pending_tx = true;
}
break;
case KISS_CMD_TXDELAY:
if (data_len >= 1) _txdelay = data[0];
break;
case KISS_CMD_PERSISTENCE:
if (data_len >= 1) _persistence = data[0];
break;
case KISS_CMD_SLOTTIME:
if (data_len >= 1) _slottime = data[0];
break;
case KISS_CMD_TXTAIL:
if (data_len >= 1) _txtail = data[0];
break;
case KISS_CMD_FULLDUPLEX:
if (data_len >= 1) _fullduplex = data[0];
break;
case KISS_CMD_SETHARDWARE:
if (data_len >= 1) {
handleHardwareCommand(data[0], data + 1, data_len - 1);
}
break;
default:
break;
}
}
void KissModem::handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
switch (sub_cmd) {
case HW_CMD_GET_IDENTITY:
handleGetIdentity();
break;
case HW_CMD_GET_RANDOM:
handleGetRandom(data, len);
break;
case HW_CMD_VERIFY_SIGNATURE:
handleVerifySignature(data, len);
break;
case HW_CMD_SIGN_DATA:
handleSignData(data, len);
break;
case HW_CMD_ENCRYPT_DATA:
handleEncryptData(data, len);
break;
case HW_CMD_DECRYPT_DATA:
handleDecryptData(data, len);
break;
case HW_CMD_KEY_EXCHANGE:
handleKeyExchange(data, len);
break;
case HW_CMD_HASH:
handleHash(data, len);
break;
case HW_CMD_SET_RADIO:
handleSetRadio(data, len);
break;
case HW_CMD_SET_TX_POWER:
handleSetTxPower(data, len);
break;
case HW_CMD_GET_RADIO:
handleGetRadio();
break;
case HW_CMD_GET_TX_POWER:
handleGetTxPower();
break;
case HW_CMD_GET_VERSION:
handleGetVersion();
break;
case HW_CMD_GET_CURRENT_RSSI:
handleGetCurrentRssi();
break;
case HW_CMD_IS_CHANNEL_BUSY:
handleIsChannelBusy();
break;
case HW_CMD_GET_AIRTIME:
handleGetAirtime(data, len);
break;
case HW_CMD_GET_NOISE_FLOOR:
handleGetNoiseFloor();
break;
case HW_CMD_GET_STATS:
handleGetStats();
break;
case HW_CMD_GET_BATTERY:
handleGetBattery();
break;
case HW_CMD_PING:
handlePing();
break;
case HW_CMD_GET_SENSORS:
handleGetSensors(data, len);
break;
case HW_CMD_GET_MCU_TEMP:
handleGetMCUTemp();
break;
case HW_CMD_REBOOT:
handleReboot();
break;
case HW_CMD_GET_DEVICE_NAME:
handleGetDeviceName();
break;
case HW_CMD_SET_SIGNAL_REPORT:
handleSetSignalReport(data, len);
break;
case HW_CMD_GET_SIGNAL_REPORT:
handleGetSignalReport();
break;
default:
writeHardwareError(HW_ERR_UNKNOWN_CMD);
break;
}
}
void KissModem::processTx() {
switch (_tx_state) {
case TX_IDLE:
if (_has_pending_tx) {
if (_fullduplex) {
_tx_timer = millis();
_tx_state = TX_DELAY;
} else {
_tx_state = TX_WAIT_CLEAR;
}
}
break;
case TX_WAIT_CLEAR:
if (!_radio.isReceiving()) {
uint8_t rand_val;
_rng.random(&rand_val, 1);
if (rand_val <= _persistence) {
_tx_timer = millis();
_tx_state = TX_DELAY;
} else {
_tx_timer = millis();
_tx_state = TX_SLOT_WAIT;
}
}
break;
case TX_SLOT_WAIT:
if (millis() - _tx_timer >= (uint32_t)_slottime * 10) {
_tx_state = TX_WAIT_CLEAR;
}
break;
case TX_DELAY:
if (millis() - _tx_timer >= (uint32_t)_txdelay * 10) {
_radio.startSendRaw(_pending_tx, _pending_tx_len);
_tx_state = TX_SENDING;
}
break;
case TX_SENDING:
if (_radio.isSendComplete()) {
_radio.onSendFinished();
uint8_t result = 0x01;
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
}
break;
}
}
void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) {
writeFrame(KISS_CMD_DATA, packet, len);
if (_signal_report_enabled) {
uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi };
writeHardwareFrame(HW_RESP_RX_META, meta, 2);
}
}
void KissModem::handleGetIdentity() {
writeHardwareFrame(HW_RESP(HW_CMD_GET_IDENTITY), _identity.pub_key, PUB_KEY_SIZE);
}
void KissModem::handleGetRandom(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t requested = data[0];
if (requested < 1 || requested > 64) {
writeHardwareError(HW_ERR_INVALID_PARAM);
return;
}
uint8_t buf[64];
_rng.random(buf, requested);
writeHardwareFrame(HW_RESP(HW_CMD_GET_RANDOM), buf, requested);
}
void KissModem::handleVerifySignature(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE + SIGNATURE_SIZE + 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
mesh::Identity signer(data);
const uint8_t* signature = data + PUB_KEY_SIZE;
const uint8_t* msg = data + PUB_KEY_SIZE + SIGNATURE_SIZE;
uint16_t msg_len = len - PUB_KEY_SIZE - SIGNATURE_SIZE;
uint8_t result = signer.verify(signature, msg, msg_len) ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_VERIFY_SIGNATURE), &result, 1);
}
void KissModem::handleSignData(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t signature[SIGNATURE_SIZE];
_identity.sign(signature, data, len);
writeHardwareFrame(HW_RESP(HW_CMD_SIGN_DATA), signature, SIGNATURE_SIZE);
}
void KissModem::handleEncryptData(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE + 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
const uint8_t* key = data;
const uint8_t* plaintext = data + PUB_KEY_SIZE;
uint16_t plaintext_len = len - PUB_KEY_SIZE;
uint8_t buf[KISS_MAX_FRAME_SIZE];
int encrypted_len = mesh::Utils::encryptThenMAC(key, buf, plaintext, plaintext_len);
if (encrypted_len > 0) {
writeHardwareFrame(HW_RESP(HW_CMD_ENCRYPT_DATA), buf, encrypted_len);
} else {
writeHardwareError(HW_ERR_ENCRYPT_FAILED);
}
}
void KissModem::handleDecryptData(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE + CIPHER_MAC_SIZE + 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
const uint8_t* key = data;
const uint8_t* ciphertext = data + PUB_KEY_SIZE;
uint16_t ciphertext_len = len - PUB_KEY_SIZE;
uint8_t buf[KISS_MAX_FRAME_SIZE];
int decrypted_len = mesh::Utils::MACThenDecrypt(key, buf, ciphertext, ciphertext_len);
if (decrypted_len > 0) {
writeHardwareFrame(HW_RESP(HW_CMD_DECRYPT_DATA), buf, decrypted_len);
} else {
writeHardwareError(HW_ERR_MAC_FAILED);
}
}
void KissModem::handleKeyExchange(const uint8_t* data, uint16_t len) {
if (len < PUB_KEY_SIZE) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t shared_secret[PUB_KEY_SIZE];
_identity.calcSharedSecret(shared_secret, data);
writeHardwareFrame(HW_RESP(HW_CMD_KEY_EXCHANGE), shared_secret, PUB_KEY_SIZE);
}
void KissModem::handleHash(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t hash[32];
mesh::Utils::sha256(hash, 32, data, len);
writeHardwareFrame(HW_RESP(HW_CMD_HASH), hash, 32);
}
void KissModem::handleSetRadio(const uint8_t* data, uint16_t len) {
if (len < 10) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
if (!_setRadioCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
memcpy(&_config.freq_hz, data, 4);
memcpy(&_config.bw_hz, data + 4, 4);
_config.sf = data[8];
_config.cr = data[9];
_setRadioCallback(_config.freq_hz / 1000000.0f, _config.bw_hz / 1000.0f, _config.sf, _config.cr);
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
}
void KissModem::handleSetTxPower(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
if (!_setTxPowerCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
_config.tx_power = data[0];
_setTxPowerCallback(data[0]);
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
}
void KissModem::handleGetRadio() {
uint8_t buf[10];
memcpy(buf, &_config.freq_hz, 4);
memcpy(buf + 4, &_config.bw_hz, 4);
buf[8] = _config.sf;
buf[9] = _config.cr;
writeHardwareFrame(HW_RESP(HW_CMD_GET_RADIO), buf, 10);
}
void KissModem::handleGetTxPower() {
writeHardwareFrame(HW_RESP(HW_CMD_GET_TX_POWER), &_config.tx_power, 1);
}
void KissModem::handleGetVersion() {
uint8_t buf[2];
buf[0] = KISS_FIRMWARE_VERSION;
buf[1] = 0;
writeHardwareFrame(HW_RESP(HW_CMD_GET_VERSION), buf, 2);
}
void KissModem::handleGetCurrentRssi() {
if (!_getCurrentRssiCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
float rssi = _getCurrentRssiCallback();
int8_t rssi_byte = (int8_t)rssi;
writeHardwareFrame(HW_RESP(HW_CMD_GET_CURRENT_RSSI), (uint8_t*)&rssi_byte, 1);
}
void KissModem::handleIsChannelBusy() {
uint8_t busy = _radio.isReceiving() ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_IS_CHANNEL_BUSY), &busy, 1);
}
void KissModem::handleGetAirtime(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t packet_len = data[0];
uint32_t airtime = _radio.getEstAirtimeFor(packet_len);
writeHardwareFrame(HW_RESP(HW_CMD_GET_AIRTIME), (uint8_t*)&airtime, 4);
}
void KissModem::handleGetNoiseFloor() {
int16_t noise_floor = _radio.getNoiseFloor();
writeHardwareFrame(HW_RESP(HW_CMD_GET_NOISE_FLOOR), (uint8_t*)&noise_floor, 2);
}
void KissModem::handleGetStats() {
if (!_getStatsCallback) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
uint32_t rx, tx, errors;
_getStatsCallback(&rx, &tx, &errors);
uint8_t buf[12];
memcpy(buf, &rx, 4);
memcpy(buf + 4, &tx, 4);
memcpy(buf + 8, &errors, 4);
writeHardwareFrame(HW_RESP(HW_CMD_GET_STATS), buf, 12);
}
void KissModem::handleGetBattery() {
uint16_t mv = _board.getBattMilliVolts();
writeHardwareFrame(HW_RESP(HW_CMD_GET_BATTERY), (uint8_t*)&mv, 2);
}
void KissModem::handlePing() {
writeHardwareFrame(HW_RESP(HW_CMD_PING), nullptr, 0);
}
void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
uint8_t permissions = data[0];
CayenneLPP telemetry(255);
if (_sensors.querySensors(permissions, telemetry)) {
writeHardwareFrame(HW_RESP(HW_CMD_GET_SENSORS), telemetry.getBuffer(), telemetry.getSize());
} else {
writeHardwareFrame(HW_RESP(HW_CMD_GET_SENSORS), nullptr, 0);
}
}
void KissModem::handleGetMCUTemp() {
float temp = _board.getMCUTemperature();
if (isnan(temp)) {
writeHardwareError(HW_ERR_NO_CALLBACK);
return;
}
int16_t temp_tenths = (int16_t)(temp * 10.0f);
writeHardwareFrame(HW_RESP(HW_CMD_GET_MCU_TEMP), (uint8_t*)&temp_tenths, 2);
}
void KissModem::handleReboot() {
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
_serial.flush();
delay(50);
_board.reboot();
}
void KissModem::handleGetDeviceName() {
const char* name = _board.getManufacturerName();
writeHardwareFrame(HW_RESP(HW_CMD_GET_DEVICE_NAME), (const uint8_t*)name, strlen(name));
}
void KissModem::handleSetSignalReport(const uint8_t* data, uint16_t len) {
if (len < 1) {
writeHardwareError(HW_ERR_INVALID_LENGTH);
return;
}
_signal_report_enabled = (data[0] != 0x00);
uint8_t val = _signal_report_enabled ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_GET_SIGNAL_REPORT), &val, 1);
}
void KissModem::handleGetSignalReport() {
uint8_t val = _signal_report_enabled ? 0x01 : 0x00;
writeHardwareFrame(HW_RESP(HW_CMD_GET_SIGNAL_REPORT), &val, 1);
}

View File

@@ -0,0 +1,183 @@
#pragma once
#include <Arduino.h>
#include <Identity.h>
#include <Utils.h>
#include <Mesh.h>
#include <helpers/SensorManager.h>
#define KISS_FEND 0xC0
#define KISS_FESC 0xDB
#define KISS_TFEND 0xDC
#define KISS_TFESC 0xDD
#define KISS_MAX_FRAME_SIZE 512
#define KISS_MAX_PACKET_SIZE 255
#define KISS_CMD_DATA 0x00
#define KISS_CMD_TXDELAY 0x01
#define KISS_CMD_PERSISTENCE 0x02
#define KISS_CMD_SLOTTIME 0x03
#define KISS_CMD_TXTAIL 0x04
#define KISS_CMD_FULLDUPLEX 0x05
#define KISS_CMD_SETHARDWARE 0x06
#define KISS_CMD_RETURN 0xFF
#define KISS_DEFAULT_TXDELAY 50
#define KISS_DEFAULT_PERSISTENCE 63
#define KISS_DEFAULT_SLOTTIME 10
#define HW_CMD_GET_IDENTITY 0x01
#define HW_CMD_GET_RANDOM 0x02
#define HW_CMD_VERIFY_SIGNATURE 0x03
#define HW_CMD_SIGN_DATA 0x04
#define HW_CMD_ENCRYPT_DATA 0x05
#define HW_CMD_DECRYPT_DATA 0x06
#define HW_CMD_KEY_EXCHANGE 0x07
#define HW_CMD_HASH 0x08
#define HW_CMD_SET_RADIO 0x09
#define HW_CMD_SET_TX_POWER 0x0A
#define HW_CMD_GET_RADIO 0x0B
#define HW_CMD_GET_TX_POWER 0x0C
#define HW_CMD_GET_CURRENT_RSSI 0x0D
#define HW_CMD_IS_CHANNEL_BUSY 0x0E
#define HW_CMD_GET_AIRTIME 0x0F
#define HW_CMD_GET_NOISE_FLOOR 0x10
#define HW_CMD_GET_VERSION 0x11
#define HW_CMD_GET_STATS 0x12
#define HW_CMD_GET_BATTERY 0x13
#define HW_CMD_GET_MCU_TEMP 0x14
#define HW_CMD_GET_SENSORS 0x15
#define HW_CMD_GET_DEVICE_NAME 0x16
#define HW_CMD_PING 0x17
#define HW_CMD_REBOOT 0x18
#define HW_CMD_SET_SIGNAL_REPORT 0x19
#define HW_CMD_GET_SIGNAL_REPORT 0x1A
/* Response code = command code | 0x80. Generic / unsolicited use 0xF0+. */
#define HW_RESP(cmd) ((cmd) | 0x80)
/* Generic responses (shared by multiple commands) */
#define HW_RESP_OK 0xF0
#define HW_RESP_ERROR 0xF1
/* Unsolicited notifications (no corresponding request) */
#define HW_RESP_TX_DONE 0xF8
#define HW_RESP_RX_META 0xF9
#define HW_ERR_INVALID_LENGTH 0x01
#define HW_ERR_INVALID_PARAM 0x02
#define HW_ERR_NO_CALLBACK 0x03
#define HW_ERR_MAC_FAILED 0x04
#define HW_ERR_UNKNOWN_CMD 0x05
#define HW_ERR_ENCRYPT_FAILED 0x06
#define KISS_FIRMWARE_VERSION 1
typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr);
typedef void (*SetTxPowerCallback)(uint8_t power);
typedef float (*GetCurrentRssiCallback)();
typedef void (*GetStatsCallback)(uint32_t* rx, uint32_t* tx, uint32_t* errors);
struct RadioConfig {
uint32_t freq_hz;
uint32_t bw_hz;
uint8_t sf;
uint8_t cr;
uint8_t tx_power;
};
enum TxState {
TX_IDLE,
TX_WAIT_CLEAR,
TX_SLOT_WAIT,
TX_DELAY,
TX_SENDING
};
class KissModem {
Stream& _serial;
mesh::LocalIdentity& _identity;
mesh::RNG& _rng;
mesh::Radio& _radio;
mesh::MainBoard& _board;
SensorManager& _sensors;
uint8_t _rx_buf[KISS_MAX_FRAME_SIZE];
uint16_t _rx_len;
bool _rx_escaped;
bool _rx_active;
uint8_t _pending_tx[KISS_MAX_PACKET_SIZE];
uint16_t _pending_tx_len;
bool _has_pending_tx;
uint8_t _txdelay;
uint8_t _persistence;
uint8_t _slottime;
uint8_t _txtail;
uint8_t _fullduplex;
TxState _tx_state;
uint32_t _tx_timer;
SetRadioCallback _setRadioCallback;
SetTxPowerCallback _setTxPowerCallback;
GetCurrentRssiCallback _getCurrentRssiCallback;
GetStatsCallback _getStatsCallback;
RadioConfig _config;
bool _signal_report_enabled;
void writeByte(uint8_t b);
void writeFrame(uint8_t type, const uint8_t* data, uint16_t len);
void writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
void writeHardwareError(uint8_t error_code);
void processFrame();
void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
void processTx();
void handleGetIdentity();
void handleGetRandom(const uint8_t* data, uint16_t len);
void handleVerifySignature(const uint8_t* data, uint16_t len);
void handleSignData(const uint8_t* data, uint16_t len);
void handleEncryptData(const uint8_t* data, uint16_t len);
void handleDecryptData(const uint8_t* data, uint16_t len);
void handleKeyExchange(const uint8_t* data, uint16_t len);
void handleHash(const uint8_t* data, uint16_t len);
void handleSetRadio(const uint8_t* data, uint16_t len);
void handleSetTxPower(const uint8_t* data, uint16_t len);
void handleGetRadio();
void handleGetTxPower();
void handleGetVersion();
void handleGetCurrentRssi();
void handleIsChannelBusy();
void handleGetAirtime(const uint8_t* data, uint16_t len);
void handleGetNoiseFloor();
void handleGetStats();
void handleGetBattery();
void handlePing();
void handleGetSensors(const uint8_t* data, uint16_t len);
void handleGetMCUTemp();
void handleReboot();
void handleGetDeviceName();
void handleSetSignalReport(const uint8_t* data, uint16_t len);
void handleGetSignalReport();
public:
KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng,
mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors);
void begin();
void loop();
void setRadioCallback(SetRadioCallback cb) { _setRadioCallback = cb; }
void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; }
void setGetCurrentRssiCallback(GetCurrentRssiCallback cb) { _getCurrentRssiCallback = cb; }
void setGetStatsCallback(GetStatsCallback cb) { _getStatsCallback = cb; }
void onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len);
bool isTxBusy() const { return _tx_state != TX_IDLE; }
/** True only when radio is actually transmitting; use to skip recvRaw in main loop. */
bool isActuallyTransmitting() const { return _tx_state == TX_SENDING; }
};

View File

@@ -0,0 +1,146 @@
#include <Arduino.h>
#include <target.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/IdentityStore.h>
#include "KissModem.h"
#if defined(NRF52_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#if defined(KISS_UART_RX) && defined(KISS_UART_TX)
#include <HardwareSerial.h>
#endif
#define NOISE_FLOOR_CALIB_INTERVAL_MS 2000
#define AGC_RESET_INTERVAL_MS 30000
StdRNG rng;
mesh::LocalIdentity identity;
KissModem* modem;
static uint32_t next_noise_floor_calib_ms = 0;
static uint32_t next_agc_reset_ms = 0;
void halt() {
while (1) ;
}
void loadOrCreateIdentity() {
#if defined(NRF52_PLATFORM)
InternalFS.begin();
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
SPIFFS.begin(true);
IdentityStore store(SPIFFS, "/identity");
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
IdentityStore store(LittleFS, "/identity");
store.begin();
#else
#error "Filesystem not defined"
#endif
if (!store.load("_main", identity)) {
identity = radio_new_identity();
while (identity.pub_key[0] == 0x00 || identity.pub_key[0] == 0xFF) {
identity = radio_new_identity();
}
store.save("_main", identity);
}
}
void onSetRadio(float freq, float bw, uint8_t sf, uint8_t cr) {
radio_set_params(freq, bw, sf, cr);
}
void onSetTxPower(uint8_t power) {
radio_set_tx_power(power);
}
float onGetCurrentRssi() {
return radio_driver.getCurrentRSSI();
}
void onGetStats(uint32_t* rx, uint32_t* tx, uint32_t* errors) {
*rx = radio_driver.getPacketsRecv();
*tx = radio_driver.getPacketsSent();
*errors = radio_driver.getPacketsRecvErrors();
}
void setup() {
board.begin();
if (!radio_init()) {
halt();
}
radio_driver.begin();
rng.begin(radio_get_rng_seed());
loadOrCreateIdentity();
sensors.begin();
#if defined(KISS_UART_RX) && defined(KISS_UART_TX)
#if defined(ESP32)
Serial1.setPins(KISS_UART_RX, KISS_UART_TX);
Serial1.begin(115200);
#elif defined(NRF52_PLATFORM)
((Uart *)&Serial1)->setPins(KISS_UART_RX, KISS_UART_TX);
Serial1.begin(115200);
#elif defined(RP2040_PLATFORM)
((SerialUART *)&Serial1)->setRX(KISS_UART_RX);
((SerialUART *)&Serial1)->setTX(KISS_UART_TX);
Serial1.begin(115200);
#elif defined(STM32_PLATFORM)
((HardwareSerial *)&Serial1)->setRx(KISS_UART_RX);
((HardwareSerial *)&Serial1)->setTx(KISS_UART_TX);
Serial1.begin(115200);
#else
#error "KISS UART not supported on this platform"
#endif
modem = new KissModem(Serial1, identity, rng, radio_driver, board, sensors);
#else
Serial.begin(115200);
uint32_t start = millis();
while (!Serial && millis() - start < 3000) delay(10);
delay(100);
modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors);
#endif
modem->setRadioCallback(onSetRadio);
modem->setTxPowerCallback(onSetTxPower);
modem->setGetCurrentRssiCallback(onGetCurrentRssi);
modem->setGetStatsCallback(onGetStats);
modem->begin();
}
void loop() {
modem->loop();
if (!modem->isActuallyTransmitting()) {
if (!modem->isTxBusy()) {
if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) {
radio_driver.resetAGC();
next_agc_reset_ms = millis();
}
}
uint8_t rx_buf[256];
int rx_len = radio_driver.recvRaw(rx_buf, sizeof(rx_buf));
if (rx_len > 0) {
int8_t snr = (int8_t)(radio_driver.getLastSNR() * 4);
int8_t rssi = (int8_t)radio_driver.getLastRSSI();
modem->onPacketReceived(snr, rssi, rx_buf, rx_len);
}
}
if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) {
radio_driver.triggerNoiseFloorCalibrate(0);
next_noise_floor_calib_ms = millis();
}
radio_driver.loop();
}

View File

@@ -899,7 +899,7 @@ void MyMesh::dumpLogFile() {
}
}
void MyMesh::setTxPower(uint8_t power_dbm) {
void MyMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}
@@ -1219,5 +1219,8 @@ void MyMesh::loop() {
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep
#endif
return _mgr->getOutboundCount(0xFFFFFFFF) > 0;
}

View File

@@ -69,11 +69,11 @@ struct NeighbourInfo {
};
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "29 Jan 2026"
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.12.0"
#define FIRMWARE_VERSION "v1.13.0"
#endif
#define FIRMWARE_ROLE "repeater"
@@ -198,7 +198,7 @@ public:
}
void dumpLogFile() override;
void setTxPower(uint8_t power_dbm) override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override;
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
void formatStatsReply(char *reply) override;

View File

@@ -29,6 +29,12 @@ void setup() {
board.begin();
#if defined(MESH_DEBUG) && defined(NRF52_PLATFORM)
// give some extra time for serial to settle so
// boot debug messages can be seen on terminal
delay(5000);
#endif
// For power saving
lastActive = millis(); // mark last active time since boot
@@ -42,6 +48,7 @@ void setup() {
#endif
if (!radio_init()) {
MESH_DEBUG_PRINTLN("Radio init failed!");
halt();
}
@@ -127,14 +134,17 @@ void loop() {
#endif
rtc_clock.tick();
if (the_mesh.getNodePrefs()->powersaving_enabled && // To check if power saving is enabled
the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep
if (!the_mesh.hasPendingWork()) { // No pending work. Safe to sleep
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible
#else
if (the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep
board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet
lastActive = millis();
nextSleepinSecs = 5; // Default: To work for 5s and sleep again
} else {
nextSleepinSecs += 5; // When there is pending work, to work another 5s
}
#endif
}
}

View File

@@ -719,7 +719,7 @@ void MyMesh::dumpLogFile() {
}
}
void MyMesh::setTxPower(uint8_t power_dbm) {
void MyMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}

View File

@@ -26,11 +26,11 @@
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "29 Jan 2026"
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.12.0"
#define FIRMWARE_VERSION "v1.13.0"
#endif
#ifndef LORA_FREQ
@@ -188,7 +188,7 @@ public:
}
void dumpLogFile() override;
void setTxPower(uint8_t power_dbm) override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");

View File

@@ -66,7 +66,7 @@ struct NodePrefs { // persisted to file
char node_name[32];
double node_lat, node_lon;
float freq;
uint8_t tx_power_dbm;
int8_t tx_power_dbm;
uint8_t unused[3];
};
@@ -290,7 +290,7 @@ public:
}
float getFreqPref() const { return _prefs.freq; }
uint8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }
int8_t getTxPowerPref() const { return _prefs.tx_power_dbm; }
void begin(FILESYSTEM& fs) {
_fs = &fs;

View File

@@ -815,7 +815,7 @@ void SensorMesh::updateFloodAdvertTimer() {
}
}
void SensorMesh::setTxPower(uint8_t power_dbm) {
void SensorMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}

View File

@@ -33,11 +33,11 @@
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "29 Jan 2026"
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.12.0"
#define FIRMWARE_VERSION "v1.13.0"
#endif
#define FIRMWARE_ROLE "sensor"
@@ -66,7 +66,7 @@ public:
void setLoggingOn(bool enable) override { }
void eraseLogFile() override { }
void dumpLogFile() override { }
void setTxPower(uint8_t power_dbm) override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}

19
mkdocs.yml Normal file
View File

@@ -0,0 +1,19 @@
site_name: MeshCore Docs
site_url: https://meshcore-dev.github.io/meshcore/
site_description: Documentation for the open source MeshCore firmware
repo_name: meshcore-dev/meshcore
repo_url: https://github.com/meshcore-dev/meshcore/
edit_uri: edit/main/docs/
theme:
name: material
logo: _assets/meshcore_tm.svg
features:
- content.action.edit
- content.code.copy
- search.highlight
- search.suggest
extra_css:
- _stylesheets/extra.css

View File

@@ -59,6 +59,7 @@ platform = platformio/espressif32@6.11.0
monitor_filters = esp32_exception_decoder
extra_scripts = merge-bin.py
build_flags = ${arduino_base.build_flags}
-D ESP32_PLATFORM
; -D ESP32_CPU_FREQ=80 ; change it to your need
build_src_filter = ${arduino_base.build_src_filter}
@@ -68,10 +69,10 @@ lib_deps =
file://arch/esp32/AsyncElegantOTA
; esp32c6 uses arduino framework 3.x
; WARNING: experimental. pioarduino on esp32c6 needs work - it's not considered stable and has issues.
; WARNING: experimental. May not work as stable as other platforms.
[esp32c6_base]
extends = esp32_base
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13-1/platform-espressif32.zip
; ----------------- NRF52 ---------------------
@@ -80,7 +81,7 @@ extends = arduino_base
platform = nordicnrf52
platform_packages =
framework-arduinoadafruitnrf52 @ 1.10700.0
extra_scripts =
extra_scripts =
create-uf2.py
arch/nrf52/extra_scripts/patch_bluefruit.py
build_flags = ${arduino_base.build_flags}

View File

@@ -131,7 +131,6 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
plen = packet->writeTo(temp_buf);
packet->header = save;
}
putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
bool is_new = false; // true = not in contacts[], false = exists in contacts[]
if (from == NULL) {
@@ -157,6 +156,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
from->shared_secret_valid = false;
}
// update
putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
from->type = parser.getType();
if (parser.hasLatLon()) {

View File

@@ -16,7 +16,7 @@ static uint32_t _atoi(const char* sp) {
static bool isValidName(const char *n) {
while (*n) {
if (*n == '[' || *n == ']' || *n == '/' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
n++;
}
return true;
@@ -92,7 +92,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->bw = constrain(_prefs->bw, 7.8f, 500.0f);
_prefs->sf = constrain(_prefs->sf, 5, 12);
_prefs->cr = constrain(_prefs->cr, 5, 8);
_prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, 1, 30);
_prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, -9, 30);
_prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1);
_prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f);
@@ -326,7 +326,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
}
*reply = 0; // set null terminator
} else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
sprintf(reply, "> %d", (uint32_t) _prefs->tx_power_dbm);
sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
} else if (memcmp(config, "freq", 4) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
} else if (memcmp(config, "public.key", 10) == 0) {

View File

@@ -19,7 +19,7 @@ struct NodePrefs { // persisted to file
double node_lat, node_lon;
char password[16];
float freq;
uint8_t tx_power_dbm;
int8_t tx_power_dbm;
uint8_t disable_fwd;
uint8_t advert_interval; // minutes / 2
uint8_t flood_advert_interval; // hours
@@ -67,7 +67,7 @@ public:
virtual void setLoggingOn(bool enable) = 0;
virtual void eraseLogFile() = 0;
virtual void dumpLogFile() = 0;
virtual void setTxPower(uint8_t power_dbm) = 0;
virtual void setTxPower(int8_t power_dbm) = 0;
virtual void formatNeighborsReply(char *reply) = 0;
virtual void removeNeighbor(const uint8_t* pubkey, int key_len) {
// no op by default

View File

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

View File

@@ -8,12 +8,12 @@
#include <rom/rtc.h>
#include <sys/time.h>
#include <Wire.h>
#include "esp_wifi.h"
#include "driver/rtc_io.h"
class ESP32Board : public mesh::MainBoard {
protected:
uint8_t startup_reason;
bool inhibit_sleep = false;
public:
void begin() {
@@ -72,11 +72,7 @@ public:
}
void sleep(uint32_t secs) override {
// To check for WiFi status to see if there is active OTA
wifi_mode_t mode;
esp_err_t err = esp_wifi_get_mode(&mode);
if (err != ESP_OK) { // WiFi is off ~ No active OTA, safe to go to sleep
if (!inhibit_sleep) {
enterLightSleep(secs); // To wake up after "secs" seconds or when receiving a LoRa packet
}
}
@@ -126,6 +122,10 @@ public:
}
bool startOTAUpdate(const char* id, char reply[]) override;
void setInhibitSleep(bool inhibit) {
inhibit_sleep = inhibit;
}
};
class ESP32RTCClock : public mesh::RTCClock {

View File

@@ -251,6 +251,32 @@ void NRF52BoardDCDC::begin() {
}
}
void NRF52Board::sleep(uint32_t secs) {
// Clear FPU interrupt flags to avoid insomnia
// see errata 87 for details https://docs.nordicsemi.com/bundle/errata_nRF52840_Rev3/page/ERR/nRF52840/Rev3/latest/anomaly_840_87.html
#if (__FPU_USED == 1)
__set_FPSCR(__get_FPSCR() & ~(0x0000009F));
(void) __get_FPSCR();
NVIC_ClearPendingIRQ(FPU_IRQn);
#endif
// On nRF52, we use event-driven sleep instead of timed sleep
// The 'secs' parameter is ignored - we wake on any interrupt
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
// first call processes pending softdevice events, second call sleeps.
sd_app_evt_wait();
sd_app_evt_wait();
} else {
// softdevice is disabled, use raw WFE
__SEV();
__WFE();
__WFE();
}
}
// Temperature from NRF52 MCU
float NRF52Board::getMCUTemperature() {
NRF_TEMP->TASKS_START = 1; // Start temperature measurement

View File

@@ -51,6 +51,7 @@ public:
virtual float getMCUTemperature() override;
virtual void reboot() override { NVIC_SystemReset(); }
virtual bool startOTAUpdate(const char *id, char reply[]) override;
virtual void sleep(uint32_t secs) override;
#ifdef NRF52_POWER_MANAGEMENT
bool isExternalPowered() override;

View File

@@ -76,6 +76,14 @@ class CustomSX1262 : public SX1262 {
setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
#endif
// for improved RX with Heltec v4
#ifdef SX126X_REGISTER_PATCH
uint8_t r_data = 0;
readRegister(0x8B5, &r_data, 1);
r_data |= 0x01;
writeRegister(0x8B5, &r_data, 1);
#endif
return true; // success
}

View File

@@ -284,7 +284,7 @@ bool EnvironmentSensorManager::begin() {
INA260_initialized = true;
} else {
INA260_initialized = false;
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA260_ADDRESS);
}
#endif

View File

@@ -18,17 +18,23 @@ bool SSD1306Display::begin() {
}
void SSD1306Display::turnOn() {
display.ssd1306_command(SSD1306_DISPLAYON);
if (!_isOn) {
if (_peripher_power) _peripher_power->claim();
_isOn = true;
_isOn = true; // set before begin() to prevent double claim
if (_peripher_power) begin(); // re-init display after power was cut
}
display.ssd1306_command(SSD1306_DISPLAYON);
}
void SSD1306Display::turnOff() {
display.ssd1306_command(SSD1306_DISPLAYOFF);
if (_isOn) {
if (_peripher_power) _peripher_power->release();
if (_peripher_power) {
#if PIN_OLED_RESET >= 0
digitalWrite(PIN_OLED_RESET, LOW);
#endif
_peripher_power->release();
}
_isOn = false;
}
}

View File

@@ -10,8 +10,13 @@
#define Y_OFFSET 1 // Vertical offset to prevent top row cutoff
#endif
#define SCALE_X 1.875f // 240 / 128
#define SCALE_Y 2.109375f // 135 / 64
#ifdef HELTEC_VISION_MASTER_T190
#define SCALE_X 2.5f // 320 / 128
#define SCALE_Y 2.65625f // 170 / 64
#else
#define SCALE_X 1.875f // 240 / 128
#define SCALE_Y 2.109375f // 135 / 64
#endif
bool ST7789Display::begin() {
if(!_isOn) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
// no-op
}
void radio_set_tx_power(uint8_t dbm) {
void radio_set_tx_power(int8_t dbm) {
radio_driver.setTxPower(dbm);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,6 @@ void T114Board::initiateShutdown(uint8_t reason) {
void T114Board::begin() {
NRF52Board::begin();
NRF_POWER->DCDCEN = 1;
pinMode(PIN_VBAT_READ, INPUT);

View File

@@ -6,6 +6,7 @@ extends = nrf52_base
board = heltec_t114
board_build.ldscript = boards/nrf52840_s140_v6.ld
build_flags = ${nrf52_base.build_flags}
${sensor_base.build_flags}
-I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-I variants/heltec_t114
@@ -28,20 +29,20 @@ build_flags = ${nrf52_base.build_flags}
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D DISPLAY_CLASS=NullDisplayDriver
-D ST7789
-D PIN_GPS_RX=39
-D PIN_GPS_TX=37
-D PIN_GPS_EN=21
-D PIN_GPS_RESET=38
-D PIN_GPS_RESET_ACTIVE=LOW
-D ENV_PIN_SDA=PIN_WIRE1_SDA
-D ENV_PIN_SCL=PIN_WIRE1_SCL
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors>
+<../variants/heltec_t114>
lib_deps =
${nrf52_base.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit GFX Library @ ^1.12.1
${sensor_base.lib_deps}
debug_tool = jlink
upload_protocol = nrfutil
@@ -100,6 +101,7 @@ board_upload.maximum_size = 712704
build_flags =
${Heltec_t114.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=NullDisplayDriver
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
@@ -122,6 +124,7 @@ board_upload.maximum_size = 712704
build_flags =
${Heltec_t114.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=NullDisplayDriver
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; -D BLE_PIN_CODE=123456
@@ -144,6 +147,7 @@ extends = Heltec_t114
board = heltec_t114
board_build.ldscript = boards/nrf52840_s140_v6.ld
build_flags = ${Heltec_t114.build_flags}
-D ST7789
-D HELTEC_T114_WITH_DISPLAY
-D DISPLAY_CLASS=ST7789Display
build_src_filter = ${Heltec_t114.build_src_filter}
@@ -153,6 +157,7 @@ build_src_filter = ${Heltec_t114.build_src_filter}
+<helpers/ui/OLEDDisplayFonts.cpp>
lib_deps =
${Heltec_t114.lib_deps}
adafruit/Adafruit SSD1306 @ ^2.5.13
debug_tool = jlink
upload_protocol = nrfutil

View File

@@ -1,28 +1,46 @@
#include <Arduino.h>
#include "target.h"
#include <Arduino.h>
#include <helpers/ArduinoHelpers.h>
#ifdef ENV_INCLUDE_GPS
#include <helpers/sensors/MicroNMEALocationProvider.h>
#endif
T114Board board;
#if defined(P_LORA_SCLK)
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
T114SensorManager sensors = T114SensorManager(nmea);
#if ENV_INCLUDE_GPS
#include <helpers/sensors/MicroNMEALocationProvider.h>
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors;
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {
rtc_clock.begin(Wire);
#if defined(P_LORA_SCLK)
return radio.std_init(&SPI);
#else
return radio.std_init();
#endif
}
uint32_t radio_get_rng_seed() {
@@ -36,98 +54,11 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setCodingRate(cr);
}
void radio_set_tx_power(uint8_t dbm) {
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
void T114SensorManager::start_gps() {
if (!gps_active) {
gps_active = true;
_location->begin();
}
}
void T114SensorManager::stop_gps() {
if (gps_active) {
gps_active = false;
_location->stop();
}
}
bool T114SensorManager::begin() {
Serial1.begin(9600);
// Try to detect if GPS is physically connected to determine if we should expose the setting
pinMode(GPS_EN, OUTPUT);
digitalWrite(GPS_EN, HIGH); // Power on GPS
// Give GPS a moment to power up and send data
delay(1500);
// We'll consider GPS detected if we see any data on Serial1
gps_detected = (Serial1.available() > 0);
if (gps_detected) {
MESH_DEBUG_PRINTLN("GPS detected");
} else {
MESH_DEBUG_PRINTLN("No GPS detected");
}
digitalWrite(GPS_EN, LOW); // Power off GPS until the setting is changed
return true;
}
bool T114SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
return true;
}
void T114SensorManager::loop() {
static long next_gps_update = 0;
_location->loop();
if (millis() > next_gps_update) {
if (_location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
node_altitude = ((double)_location->getAltitude()) / 1000.0;
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
}
next_gps_update = millis() + 1000;
}
}
int T114SensorManager::getNumSettings() const {
return gps_detected ? 1 : 0; // only show GPS setting if GPS is detected
}
const char* T114SensorManager::getSettingName(int i) const {
return (gps_detected && i == 0) ? "gps" : NULL;
}
const char* T114SensorManager::getSettingValue(int i) const {
if (gps_detected && i == 0) {
return gps_active ? "1" : "0";
}
return NULL;
}
bool T114SensorManager::setSettingValue(const char* name, const char* value) {
if (gps_detected && strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
return true;
}
return false; // not supported
return mesh::LocalIdentity(&rng); // create new random identity
}

View File

@@ -2,11 +2,11 @@
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <T114Board.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#include <helpers/sensors/LocationProvider.h>
#ifdef DISPLAY_CLASS
@@ -18,37 +18,18 @@
#endif
#endif
class T114SensorManager : public SensorManager {
bool gps_active = false;
bool gps_detected = false;
LocationProvider* _location;
void start_gps();
void stop_gps();
public:
T114SensorManager(LocationProvider &location): _location(&location) { }
bool begin() override;
bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
void loop() override;
LocationProvider* getLocationProvider() override { return gps_detected ? _location : NULL; }
int getNumSettings() const override;
const char* getSettingName(int i) const override;
const char* getSettingValue(int i) const override;
bool setSettingValue(const char* name, const char* value) override;
};
extern T114Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern T114SensorManager sensors;
extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(uint8_t dbm);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

View File

@@ -14,7 +14,7 @@
#define USE_LFXO // 32.768 kHz crystal oscillator
#define VARIANT_MCK (64000000ul)
#define WIRE_INTERFACES_COUNT (1)
#define WIRE_INTERFACES_COUNT (2)
////////////////////////////////////////////////////////////////////////////////
// Power
@@ -58,8 +58,11 @@
////////////////////////////////////////////////////////////////////////////////
// I2C pin definition
#define PIN_WIRE_SDA (16) // P0.16
#define PIN_WIRE_SCL (13) // P0.13
#define PIN_WIRE_SDA (26) // P0.26
#define PIN_WIRE_SCL (27) // P0.27
#define PIN_WIRE1_SDA (7) // P0.8
#define PIN_WIRE1_SCL (8) // P0.7
////////////////////////////////////////////////////////////////////////////////
// SPI pin definition

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,6 +26,7 @@ build_flags =
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D SX126X_REGISTER_PATCH=1
-D PIN_BOARD_SDA=5
-D PIN_BOARD_SCL=6
-D PIN_USER_BTN=0

View File

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

View File

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

View File

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

View File

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

View File

@@ -367,3 +367,12 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter}
lib_deps =
${Heltec_lora32_v3.lib_deps}
${esp32_ota.lib_deps}
[env:Heltec_v3_kiss_modem]
extends = Heltec_lora32_v3
build_flags =
${Heltec_lora32_v3.build_flags}
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+<../examples/kiss_modem/>
lib_deps =
${Heltec_lora32_v3.lib_deps}

View File

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

View File

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

View File

@@ -17,18 +17,19 @@ build_flags =
-D P_LORA_SCLK=9
-D P_LORA_MISO=11
-D P_LORA_MOSI=10
-D P_LORA_PA_POWER=7 ;power en
-D P_LORA_PA_EN=2
-D P_LORA_PA_TX_EN=46 ;enable tx
-D P_LORA_PA_POWER=7 ; VFEM_Ctrl - Power on GC1109
-D P_LORA_PA_EN=2 ; PA CSD - Enable GC1109
-D P_LORA_PA_TX_EN=46 ; PA CPS - GC1109 TX PA full(High) / bypass(Low)
-D PIN_USER_BTN=0
-D PIN_VEXT_EN=36
-D PIN_VEXT_EN_ACTIVE=LOW
-D LORA_TX_POWER=10 ;If it is configured as 10 here, the final output will be 22 dbm.
-D MAX_LORA_TX_POWER=22 ; Max SX1262 output
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX
-D SX126X_DIO2_AS_RF_SWITCH=true ; GC1109 CTX is controlled by SX1262 DIO2
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D SX126X_RX_BOOSTED_GAIN=1 ; In some cases, commenting this out will improve RX
-D PIN_GPS_RX=38
-D PIN_GPS_TX=39
-D PIN_GPS_RESET=42
@@ -52,6 +53,7 @@ build_flags =
-D HELTEC_LORA_V4_OLED
-D PIN_BOARD_SDA=17
-D PIN_BOARD_SCL=18
-D PIN_OLED_RESET=21
-D ENV_PIN_SDA=4
-D ENV_PIN_SCL=3
build_src_filter= ${Heltec_lora32_v4.build_src_filter}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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