mirror of
https://github.com/meshcore-dev/MeshCore.git
synced 2026-06-11 14:31:38 +00:00
Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e70e6e859f | |||
| b941958b52 | |||
| a35b1bbbfd | |||
| bffd17a188 | |||
| 23f48e025f | |||
| efaf95d685 | |||
| 7c8d84ffb5 | |||
| 26c641bc63 | |||
| 9f683975c1 | |||
| 8540c98947 | |||
| 5058415fa3 | |||
| 2462cf6c97 | |||
| 1e740d30f2 | |||
| bfdceae16e | |||
| 683299079f | |||
| a130a95a0d | |||
| 0a13f6bcba | |||
| f03662af11 | |||
| b92fcbed10 | |||
| 63fe3d42a2 | |||
| 717142abd6 | |||
| 2eb747d504 | |||
| f6e6fdaa05 | |||
| 0cf4d422a8 | |||
| 39b333656b | |||
| 79caba5bab | |||
| cf9bf1d7fe | |||
| e1616ff29d | |||
| 973321d9b1 | |||
| 6325a85336 | |||
| 0db7715430 | |||
| f4be34a997 | |||
| 555745700e | |||
| c588540b1b | |||
| 12a37a224a | |||
| 68363d9e4d | |||
| b0b87fd709 | |||
| c4523f71a9 | |||
| 2fdbfbdbf6 | |||
| 96bbed225a | |||
| a49ee6ebe9 | |||
| 242c45f4a3 | |||
| 3cdcb3ef84 | |||
| 6d3b71eed9 | |||
| 716ba8ee5e | |||
| 3eacc49489 | |||
| e7ea2fc563 | |||
| e7e97ec438 | |||
| a3e1930176 | |||
| 75fb07fc2c | |||
| 77bd994a61 | |||
| 56d4671c2c | |||
| 6e2e9cc33c | |||
| 841db56595 | |||
| 5a6fd4c16a | |||
| ffa76016ff | |||
| b612ba49f2 | |||
| 0800f8d677 | |||
| 4b6b8abe7a | |||
| a0cc3bab4b | |||
| 51665881a2 | |||
| 6979326340 | |||
| bf733f00a6 | |||
| b75cefbfb4 | |||
| 314d777b42 | |||
| 3bde089bdb | |||
| 6b3df327e9 | |||
| 68ab882cd5 | |||
| 19ebd8c795 | |||
| f9ddd9da72 | |||
| 00afb06768 | |||
| 235706a22a | |||
| e56c1b3d58 | |||
| e727fd543b | |||
| 679fb9c0ab | |||
| 5a509752a7 | |||
| 0a8a0a4904 | |||
| ccda0a99d8 | |||
| 9b1ca6b10b | |||
| 0be082b5f9 | |||
| c7c400ed5d | |||
| d7e3a9ddba | |||
| ffbf73dc5c | |||
| 5c651b35a0 | |||
| bd90475d72 | |||
| 9ce6b49cd3 | |||
| 2b0f74a7e9 | |||
| c5bf23f4b1 | |||
| daa50e9469 | |||
| 2522492cd7 | |||
| 293a8a9a84 | |||
| db8a002c0e | |||
| 2f6ca731c8 | |||
| 696aae6ed1 | |||
| 5fbd473298 | |||
| 0265851621 | |||
| 5beef490a0 | |||
| 66009069cd | |||
| f7d8fa3420 | |||
| 04b69e86be | |||
| d8f3d59d82 | |||
| b963681830 | |||
| 12d9cc3752 | |||
| cb81d34b1a | |||
| e111f71064 | |||
| 3cd40902b3 | |||
| 5f75b90ff9 | |||
| c8d81bc04c | |||
| 0396cf6771 | |||
| 470ad43313 | |||
| 70212fb5cc | |||
| 9c8eb301a4 | |||
| b6d0b7a5dd | |||
| cb669a26f1 | |||
| b91f127e4e | |||
| 40d7fcfc26 | |||
| 835b0e9114 | |||
| 7f38e3f145 | |||
| b6a3aa1bfe | |||
| 5e03c00397 | |||
| 9a1be5386d | |||
| b705d5489f | |||
| 277331381e | |||
| ecd0cfc1c1 | |||
| 5372388e6e | |||
| dc548578ff | |||
| d3b457681a | |||
| f1f1361fd5 | |||
| bdae60498c | |||
| 66fe9ac2db | |||
| cc6139bc23 | |||
| d516cd8a9e | |||
| 07671cf1fe | |||
| d532481f34 | |||
| 28b1586fd7 | |||
| 3d982711a6 | |||
| db7baa7bd7 | |||
| e8907a3108 | |||
| 3fcf5b548b | |||
| dee3e26ac0 | |||
| 3751785400 | |||
| 49b37d5622 | |||
| 35b5548c0c | |||
| 54a48da6b5 | |||
| 0899f66034 | |||
| acdb41fcf9 | |||
| 352fe5f4ae | |||
| c7be216f27 | |||
| cfe4b0b9a5 | |||
| 992dddeee5 | |||
| 77d737beb9 | |||
| 96a16c238a | |||
| 4f9764b1b4 | |||
| d7a3d41843 | |||
| b898e7a04e | |||
| 958204c7f1 | |||
| 230c9af87b | |||
| e6c6282d40 | |||
| 3a9e1086af | |||
| 9f680d1744 | |||
| bd05eca9f5 | |||
| 49221fc159 | |||
| 748f9cfdda | |||
| 7e4d5f723a | |||
| 3ff8b17660 | |||
| 7d8d66c3e6 | |||
| 8056344b86 | |||
| a9de5bfc0c | |||
| 612aa66fc8 | |||
| f2b61f1ce5 | |||
| c92cd219d1 | |||
| d360931aa7 | |||
| bfd4800f59 | |||
| 9ec08228e5 | |||
| 057542625b | |||
| 0a13ac7fc7 | |||
| a5ae89aac0 | |||
| c0252ab82c | |||
| 8ede764104 | |||
| a9b55f58f1 | |||
| 73fc967388 | |||
| 633db082d1 | |||
| 30963231ce | |||
| ea6ec53709 | |||
| f0ec5d2ae7 | |||
| 7268d7d0d3 | |||
| aad1cf81da | |||
| bfdd5fb571 | |||
| 4b25f0a3df | |||
| 50e9456f28 | |||
| 3843c00f54 | |||
| 31007d9790 | |||
| 2f6046d7ba | |||
| dcf5785cc4 | |||
| a8783b372e | |||
| 913a27da20 | |||
| 6978fc4bc9 | |||
| a434f9eff5 | |||
| 6fd8c28522 | |||
| a61add2e6a | |||
| 534a0c97a7 | |||
| 82dd0cd996 | |||
| 4e9c229093 |
@@ -31,6 +31,6 @@ jobs:
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cname: docs.meshcore.nz
|
||||
cname: docs.meshcore.io
|
||||
publish_dir: ./site
|
||||
publish_branch: 'gh-pages'
|
||||
|
||||
@@ -39,6 +39,10 @@ jobs:
|
||||
- wio-e5-mini_repeater
|
||||
# ESP32-C6
|
||||
- LilyGo_Tlora_C6_repeater_
|
||||
# LR1110 (nRF52)
|
||||
- wio_wm1110_repeater
|
||||
# SX1276 (ESP32)
|
||||
- Tbeam_SX1276_repeater
|
||||
|
||||
steps:
|
||||
- name: Clone Repo
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Run Unit Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Clone Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Build Environment
|
||||
uses: ./.github/actions/setup-build-environment
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: pio test -e native -vv
|
||||
|
||||
- name: Upload Test Results
|
||||
# Upload test results even if the test step failed.
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results
|
||||
path: .pio/build/native/
|
||||
@@ -29,7 +29,8 @@ MeshCore provides the ability to create wireless mesh networks, similar to Mesht
|
||||
## 🚀 How to Get Started
|
||||
|
||||
- Watch the [MeshCore Intro Video](https://www.youtube.com/watch?v=t1qne8uJBAc) by Andy Kirby.
|
||||
- Read through our [Frequently Asked Questions](./docs/faq.md) section.
|
||||
- Watch the [MeshCore Technical Presentation](https://www.youtube.com/watch?v=OwmkVkZQTf4) by Liam Cottle.
|
||||
- Read through our [Frequently Asked Questions](./docs/faq.md) and [Documentation](https://docs.meshcore.io).
|
||||
- Flash the MeshCore firmware on a supported device.
|
||||
- Connect with a supported client.
|
||||
|
||||
@@ -51,7 +52,7 @@ The Simple Secure Chat example can be interacted with through the Serial Monitor
|
||||
|
||||
We have prebuilt firmware ready to flash on supported devices.
|
||||
|
||||
- Launch https://flasher.meshcore.co.uk
|
||||
- Launch https://meshcore.io/flasher
|
||||
- Select a supported device
|
||||
- Flash one of the firmware types:
|
||||
- Companion, Repeater or Room Server
|
||||
@@ -73,13 +74,13 @@ The companion firmware can be connected to via BLE, USB or WiFi depending on the
|
||||
|
||||
The repeater and room server firmwares can be setup via USB in the web config tool.
|
||||
|
||||
- https://config.meshcore.dev
|
||||
- https://config.meshcore.io
|
||||
|
||||
They can also be managed via LoRa in the mobile app by using the Remote Management feature.
|
||||
|
||||
## 🛠 Hardware Compatibility
|
||||
|
||||
MeshCore is designed for devices listed in the [MeshCore Flasher](https://flasher.meshcore.co.uk)
|
||||
MeshCore is designed for devices listed in the [MeshCore Flasher](https://meshcore.io/flasher)
|
||||
|
||||
## 📜 License
|
||||
|
||||
@@ -97,6 +98,14 @@ Here are some general principals you should try to adhere to:
|
||||
|
||||
Help us prioritize! Please react with thumbs-up to issues/PRs you care about most. We look at reaction counts when planning work.
|
||||
|
||||
### Running unit tests
|
||||
|
||||
To run unit tests, run the following command:
|
||||
|
||||
```bash
|
||||
pio test --environment native --verbose
|
||||
```
|
||||
|
||||
## Road-Map / To-Do
|
||||
|
||||
There are a number of fairly major features in the pipeline, with no particular time-frames attached yet. In very rough chronological order:
|
||||
@@ -116,4 +125,4 @@ There are a number of fairly major features in the pipeline, with no particular
|
||||
|
||||
- Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page.
|
||||
- Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz).
|
||||
- Join [MeshCore Discord](https://discord.gg/BMwCtwHj5V) to chat with the developers and get help from the community.
|
||||
- Join [MeshCore Discord](https://meshcore.gg) to chat with the developers and get help from the community.
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
#include "WiFi.h"
|
||||
#include "AsyncTCP.h"
|
||||
#include "Update.h"
|
||||
#include "esp_int_wdt.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#endif
|
||||
|
||||
#include "Hash.h"
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
"""
|
||||
Bluefruit BLE Patch Script
|
||||
|
||||
Patches Bluefruit library to fix semaphore leak bug that causes device lockup
|
||||
when BLE central disconnects unexpectedly (e.g., going out of range, supervision timeout).
|
||||
|
||||
Patches applied:
|
||||
1. BLEConnection.h: Add _hvn_qsize member to track semaphore queue size
|
||||
2. BLEConnection.cpp: Store hvn_qsize and restore semaphore on disconnect
|
||||
|
||||
Bug description:
|
||||
- When a BLE central disconnects unexpectedly (reason=8 supervision timeout),
|
||||
the BLE_GATTS_EVT_HVN_TX_COMPLETE event may never fire
|
||||
- This leaves the _hvn_sem counting semaphore in a decremented state
|
||||
- Since BLEConnection objects are reused (destructor never called), the
|
||||
semaphore count is never restored
|
||||
- Eventually all semaphore counts are exhausted and notify() blocks/fails
|
||||
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
Import("env") # pylint: disable=undefined-variable
|
||||
|
||||
|
||||
def _patch_ble_connection_header(source: Path) -> bool:
|
||||
"""
|
||||
Add _hvn_qsize member variable to BLEConnection class.
|
||||
|
||||
This is needed to restore the semaphore to its correct count on disconnect.
|
||||
|
||||
Returns True if patch was applied or already applied, False on error.
|
||||
"""
|
||||
try:
|
||||
content = source.read_text()
|
||||
|
||||
# Check if already patched
|
||||
if "_hvn_qsize" in content:
|
||||
return True # Already patched
|
||||
|
||||
# Find the location to insert - after _phy declaration
|
||||
original_pattern = ''' uint8_t _phy;
|
||||
|
||||
uint8_t _role;'''
|
||||
|
||||
patched_pattern = ''' uint8_t _phy;
|
||||
uint8_t _hvn_qsize;
|
||||
|
||||
uint8_t _role;'''
|
||||
|
||||
if original_pattern not in content:
|
||||
print("Bluefruit patch: WARNING - BLEConnection.h pattern not found")
|
||||
return False
|
||||
|
||||
content = content.replace(original_pattern, patched_pattern)
|
||||
source.write_text(content)
|
||||
|
||||
# Verify
|
||||
if "_hvn_qsize" not in source.read_text():
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Bluefruit patch: ERROR patching BLEConnection.h: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _patch_ble_connection_source(source: Path) -> bool:
|
||||
"""
|
||||
Patch BLEConnection.cpp to:
|
||||
1. Store hvn_qsize in constructor
|
||||
2. Restore _hvn_sem semaphore to full count on disconnect
|
||||
|
||||
Returns True if patch was applied or already applied, False on error.
|
||||
"""
|
||||
try:
|
||||
content = source.read_text()
|
||||
|
||||
# Check if already patched (look for the restore loop)
|
||||
if "uxSemaphoreGetCount(_hvn_sem)" in content:
|
||||
return True # Already patched
|
||||
|
||||
# Patch 1: Store queue size in constructor
|
||||
constructor_original = ''' _hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);'''
|
||||
|
||||
constructor_patched = ''' _hvn_qsize = hvn_qsize;
|
||||
_hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);'''
|
||||
|
||||
if constructor_original not in content:
|
||||
print("Bluefruit patch: WARNING - BLEConnection.cpp constructor pattern not found")
|
||||
return False
|
||||
|
||||
content = content.replace(constructor_original, constructor_patched)
|
||||
|
||||
# Patch 2: Restore semaphore on disconnect
|
||||
disconnect_original = ''' case BLE_GAP_EVT_DISCONNECTED:
|
||||
// mark as disconnected
|
||||
_connected = false;
|
||||
break;'''
|
||||
|
||||
disconnect_patched = ''' case BLE_GAP_EVT_DISCONNECTED:
|
||||
// Restore notification semaphore to full count
|
||||
// This fixes lockup when disconnect occurs with notifications in flight
|
||||
while (uxSemaphoreGetCount(_hvn_sem) < _hvn_qsize) {
|
||||
xSemaphoreGive(_hvn_sem);
|
||||
}
|
||||
// Release indication semaphore if waiting
|
||||
if (_hvc_sem) {
|
||||
_hvc_received = false;
|
||||
xSemaphoreGive(_hvc_sem);
|
||||
}
|
||||
// mark as disconnected
|
||||
_connected = false;
|
||||
break;'''
|
||||
|
||||
if disconnect_original not in content:
|
||||
print("Bluefruit patch: WARNING - BLEConnection.cpp disconnect pattern not found")
|
||||
return False
|
||||
|
||||
content = content.replace(disconnect_original, disconnect_patched)
|
||||
source.write_text(content)
|
||||
|
||||
# Verify
|
||||
verify_content = source.read_text()
|
||||
if "uxSemaphoreGetCount(_hvn_sem)" not in verify_content:
|
||||
return False
|
||||
if "_hvn_qsize = hvn_qsize" not in verify_content:
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Bluefruit patch: ERROR patching BLEConnection.cpp: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _apply_bluefruit_patches(target, source, env): # pylint: disable=unused-argument
|
||||
framework_path = env.get("PLATFORMFW_DIR")
|
||||
if not framework_path:
|
||||
framework_path = env.PioPlatform().get_package_dir("framework-arduinoadafruitnrf52")
|
||||
|
||||
if not framework_path:
|
||||
print("Bluefruit patch: ERROR - framework directory not found")
|
||||
env.Exit(1)
|
||||
return
|
||||
|
||||
framework_dir = Path(framework_path)
|
||||
bluefruit_lib = framework_dir / "libraries" / "Bluefruit52Lib" / "src"
|
||||
patch_failed = False
|
||||
|
||||
# Patch BLEConnection.h
|
||||
conn_header = bluefruit_lib / "BLEConnection.h"
|
||||
if conn_header.exists():
|
||||
before = conn_header.read_text()
|
||||
success = _patch_ble_connection_header(conn_header)
|
||||
after = conn_header.read_text()
|
||||
|
||||
if success:
|
||||
if before != after:
|
||||
print("Bluefruit patch: OK - Applied BLEConnection.h fix (added _hvn_qsize member)")
|
||||
else:
|
||||
print("Bluefruit patch: OK - BLEConnection.h already patched")
|
||||
else:
|
||||
print("Bluefruit patch: FAILED - BLEConnection.h")
|
||||
patch_failed = True
|
||||
else:
|
||||
print(f"Bluefruit patch: ERROR - BLEConnection.h not found at {conn_header}")
|
||||
patch_failed = True
|
||||
|
||||
# Patch BLEConnection.cpp
|
||||
conn_source = bluefruit_lib / "BLEConnection.cpp"
|
||||
if conn_source.exists():
|
||||
before = conn_source.read_text()
|
||||
success = _patch_ble_connection_source(conn_source)
|
||||
after = conn_source.read_text()
|
||||
|
||||
if success:
|
||||
if before != after:
|
||||
print("Bluefruit patch: OK - Applied BLEConnection.cpp fix (restore semaphore on disconnect)")
|
||||
else:
|
||||
print("Bluefruit patch: OK - BLEConnection.cpp already patched")
|
||||
else:
|
||||
print("Bluefruit patch: FAILED - BLEConnection.cpp")
|
||||
patch_failed = True
|
||||
else:
|
||||
print(f"Bluefruit patch: ERROR - BLEConnection.cpp not found at {conn_source}")
|
||||
patch_failed = True
|
||||
|
||||
if patch_failed:
|
||||
print("Bluefruit patch: CRITICAL - Patch failed! Build aborted.")
|
||||
env.Exit(1)
|
||||
|
||||
|
||||
# Register the patch to run before build
|
||||
bluefruit_action = env.VerboseAction(_apply_bluefruit_patches, "Applying Bluefruit BLE patches...")
|
||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", bluefruit_action)
|
||||
|
||||
# Also run immediately to patch before any compilation
|
||||
_apply_bluefruit_patches(None, None, env)
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x239A","0x8029"],
|
||||
["0x239A","0x0029"],
|
||||
["0x239A","0x002A"],
|
||||
["0x239A","0x802A"]
|
||||
],
|
||||
"usb_product": "HT-n5262T1",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "heltec_t1",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": [
|
||||
"bluetooth"
|
||||
],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino"
|
||||
],
|
||||
"name": "Heltec Mesh Node T1 Board",
|
||||
"upload": {
|
||||
"maximum_ram_size": 235520,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": [
|
||||
"jlink",
|
||||
"nrfjprog",
|
||||
"nrfutil",
|
||||
"stlink"
|
||||
],
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "https://heltec.org/",
|
||||
"vendor": "Heltec"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino":{
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DARDUINO_NRF52840_T_IMPULSE_PLUS -DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
[
|
||||
"0x239A",
|
||||
"0x8029"
|
||||
]
|
||||
],
|
||||
"usb_product": "T-Impulse-Plus-nRF52840",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "t_impulse_plus_nrf52840",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": [
|
||||
"bluetooth"
|
||||
],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"onboard_tools": [
|
||||
"jlink"
|
||||
],
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino"
|
||||
],
|
||||
"name": "Lilygo T-Impulse-Plus-nRF52840",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"require_upload_port": true,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": [
|
||||
"jlink",
|
||||
"nrfjprog",
|
||||
"stlink",
|
||||
"cmsis-dap",
|
||||
"blackmagic"
|
||||
],
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "https://www.lilygo.cc/",
|
||||
"vendor": "Lilygo"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=0"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": [
|
||||
"wifi"
|
||||
],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "Station G3 ESP32",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://wiki.bqvoy.com/en/devkits/station-g3",
|
||||
"vendor": "BQ Consulting"
|
||||
}
|
||||
+5
-2
@@ -53,11 +53,14 @@
|
||||
"protocols": [
|
||||
"jlink",
|
||||
"nrfjprog",
|
||||
"nrfutil",
|
||||
"stlink",
|
||||
"cmsis-dap",
|
||||
"blackmagic"
|
||||
]
|
||||
],
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "https://os.mbed.com/platforms/Nordic-nRF52840-DK/",
|
||||
"vendor": "Nordic"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?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 134 15" 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.277,0.053C2.829,0.053 2.401,0.41 2.321,0.851L0.013,13.623C-0.067,14.064 0.232,14.421 0.681,14.421L3.13,14.421C3.578,14.421 4.006,14.064 4.086,13.623L5.004,8.54L6.684,13.957C6.766,14.239 7.02,14.421 7.337,14.421L10.58,14.421C10.897,14.421 11.217,14.239 11.401,13.957L15.043,8.513L14.119,13.623C14.038,14.064 14.338,14.421 14.787,14.421L17.236,14.421C17.684,14.421 18.112,14.064 18.192,13.623L20.5,0.851C20.582,0.41 20.283,0.053 19.834,0.053L16.69,0.053C16.373,0.053 16.053,0.235 15.87,0.517L9.897,9.473C9.803,9.616 9.578,9.578 9.528,9.41L7.074,0.517C6.992,0.235 6.738,0.053 6.421,0.053L3.277,0.053Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M21.146,14.421C21.146,14.421 33.257,14.421 33.257,14.421C33.526,14.421 33.784,14.205 33.831,13.942L34.337,11.128C34.385,10.863 34.206,10.649 33.936,10.649L25.519,10.649C25.429,10.649 25.37,10.576 25.385,10.488L25.635,9.105C25.65,9.017 25.736,8.944 25.826,8.944L32.596,8.944C32.865,8.944 33.123,8.728 33.171,8.465L33.621,5.974C33.669,5.709 33.49,5.495 33.221,5.495L26.45,5.495C26.361,5.495 26.301,5.423 26.317,5.335L26.584,3.852C26.599,3.764 26.685,3.691 26.775,3.691L35.192,3.691C35.462,3.691 35.719,3.476 35.767,3.21L36.258,0.498C36.306,0.235 36.126,0.019 35.857,0.019L23.746,0.019C23.297,0.019 22.867,0.378 22.788,0.819L20.474,13.621C20.396,14.062 20.695,14.421 21.146,14.421Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M45.926,14.419L45.926,14.421L46.346,14.421C48.453,14.421 50.465,12.742 50.839,10.67L51.081,9.327C51.456,7.256 50.05,5.576 47.943,5.576L41.455,5.576C41.186,5.576 41.007,5.363 41.054,5.097L41.218,4.192C41.266,3.927 41.524,3.713 41.793,3.713L50.569,3.713C51.018,3.713 51.446,3.356 51.526,2.915L51.9,0.85C51.98,0.407 51.68,0.05 51.232,0.05L41.638,0.05C39.531,0.05 37.519,1.73 37.145,3.801L36.88,5.267C36.505,7.339 37.91,9.018 40.018,9.018L46.506,9.018C46.775,9.018 46.954,9.231 46.907,9.497L46.785,10.176C46.737,10.441 46.479,10.655 46.21,10.655L37.189,10.655C36.741,10.655 36.313,11.012 36.233,11.453L35.841,13.621C35.761,14.062 36.061,14.419 36.51,14.419L45.926,14.419Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M68.008,0.046C68.008,0.046 65.296,0.046 65.296,0.046C64.847,0.046 64.42,0.403 64.34,0.844L63.532,5.31C63.517,5.398 63.431,5.469 63.341,5.469L58.085,5.469C57.995,5.469 57.936,5.398 57.951,5.31L58.758,0.844C58.837,0.403 58.539,0.046 58.09,0.046L55.378,0.046C54.93,0.046 54.502,0.403 54.422,0.844L52.112,13.623C52.032,14.064 52.331,14.421 52.78,14.421L55.492,14.421C55.941,14.421 56.369,14.064 56.449,13.623L57.272,9.074C57.287,8.986 57.373,8.914 57.462,8.914L62.719,8.914C62.809,8.914 62.868,8.985 62.853,9.074L62.032,13.623C61.952,14.064 62.252,14.421 62.7,14.421L65.413,14.421C65.861,14.421 66.289,14.064 66.369,13.623L68.678,0.844C68.755,0.403 68.457,0.046 68.008,0.046Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M72.099,14.421C72.099,14.421 80.066,14.421 80.066,14.421C80.515,14.421 80.943,14.064 81.022,13.623L81.414,11.453C81.494,11.012 81.194,10.655 80.746,10.655L73.828,10.655C73.559,10.655 73.38,10.441 73.427,10.176L74.51,4.215C74.558,3.951 74.815,3.736 75.082,3.736L82,3.736C82.448,3.736 82.876,3.379 82.956,2.938L83.34,0.817C83.42,0.376 83.12,0.019 82.672,0.019L74.724,0.019C72.622,0.019 70.614,1.691 70.236,3.757L68.965,10.665C68.587,12.738 69.99,14.421 72.099,14.421Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M97.176,-0C97.176,0 88.882,0 88.882,0C86.775,0 84.763,1.68 84.389,3.751L83.139,10.67C82.765,12.741 84.169,14.421 86.277,14.421L94.571,14.421C96.678,14.421 98.69,12.741 99.064,10.67L100.314,3.751C100.689,1.68 99.284,-0 97.176,-0ZM94.798,10.178C94.75,10.443 94.492,10.657 94.223,10.657L87.978,10.657C87.709,10.657 87.529,10.443 87.577,10.178L88.659,4.192C88.707,3.927 88.964,3.713 89.234,3.713L95.477,3.713C95.747,3.713 95.926,3.927 95.878,4.192L94.798,10.178Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M101.284,14.421L103.995,14.421C104.443,14.421 104.871,14.065 104.951,13.624L105.43,10.97C105.446,10.882 105.531,10.81 105.621,10.81L108.902,10.806C109.064,10.806 109.2,10.886 109.267,11.018L110.813,14.035C110.992,14.392 111.319,14.434 112.303,14.419C112.88,14.426 113.756,14.382 115.169,14.382C115.623,14.382 115.902,13.907 115.678,13.51L113.989,10.569C113.945,10.491 113.993,10.386 114.086,10.34C115.39,9.707 116.423,8.477 116.681,7.055L117.27,3.785C117.646,1.713 116.242,0.033 114.134,0.033L103.884,0.033C103.436,0.033 103.008,0.39 102.928,0.831L100.616,13.623C100.536,14.064 100.836,14.421 101.284,14.421L101.284,14.421ZM106.73,3.791C106.745,3.703 106.831,3.631 106.921,3.631L112.225,3.631C112.626,3.631 112.891,3.949 112.821,4.343L112.431,6.494C112.359,6.885 111.979,7.204 111.58,7.204L106.276,7.204C106.186,7.204 106.127,7.133 106.142,7.043L106.73,3.791Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M118.277,14.421C118.277,14.421 130.388,14.421 130.388,14.421C130.657,14.421 130.915,14.205 130.963,13.942L131.468,11.128C131.516,10.863 131.337,10.649 131.068,10.649L122.65,10.649C122.56,10.649 122.501,10.576 122.516,10.488L122.766,9.105C122.781,9.017 122.867,8.944 122.957,8.944L129.728,8.944C129.997,8.944 130.254,8.728 130.302,8.465L130.753,5.974C130.801,5.709 130.621,5.495 130.352,5.495L123.581,5.495C123.492,5.495 123.432,5.423 123.448,5.335L123.715,3.852C123.73,3.764 123.816,3.691 123.906,3.691L132.324,3.691C132.593,3.691 132.851,3.476 132.898,3.21L133.389,0.498C133.437,0.235 133.257,0.019 132.988,0.019L120.877,0.019C120.428,0.019 119.999,0.378 119.919,0.819L117.605,13.621C117.527,14.062 117.827,14.421 118.277,14.421Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
+28
-4
@@ -102,7 +102,9 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
- `neighbor.remove <pubkey_prefix>`
|
||||
|
||||
**Parameters:**
|
||||
- `pubkey_prefix`: The public key of the node to remove from the neighbors list
|
||||
- `pubkey_prefix`: The public key of the node to remove from the neighbors list. This can be a short prefix or the full key. All neighbors matching the provided prefix will be removed.
|
||||
|
||||
**Note:** You can remove all neighbors by sending a space character as the prefix. The space indicates an empty prefix, which matches all existing neighbors.
|
||||
|
||||
---
|
||||
|
||||
@@ -217,6 +219,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
|
||||
---
|
||||
|
||||
#### View or change the boosted receive gain mode
|
||||
**Usage:**
|
||||
- `get radio.rxgain`
|
||||
- `set radio.rxgain <state>`
|
||||
|
||||
**Parameters:**
|
||||
- `state`: `on`|`off`
|
||||
|
||||
**Default:** `off`
|
||||
|
||||
**Note:** Only available on SX1262 and SX1268 based boards.
|
||||
|
||||
---
|
||||
|
||||
#### Change the radio parameters for a set duration
|
||||
**Usage:**
|
||||
- `tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>`
|
||||
@@ -440,7 +456,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
|
||||
**Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14
|
||||
|
||||
**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are suppored. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.
|
||||
**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.
|
||||
|
||||
---
|
||||
|
||||
@@ -474,6 +490,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
|
||||
**Default:** `0.5`
|
||||
|
||||
**Note:** When multiple nearby repeaters all hear the same flood packet, each waits a random amount of time before retransmitting to avoid simultaneous collisions. This factor scales the size of that random window. Higher values reduce collision risk at the cost of added latency. `0` disables the window entirely.
|
||||
|
||||
---
|
||||
|
||||
#### View or change the retransmit delay factor for direct traffic
|
||||
@@ -486,6 +504,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
|
||||
**Default:** `0.2`
|
||||
|
||||
**Note:** Same collision-avoidance random window as `txdelay`, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them.
|
||||
|
||||
---
|
||||
|
||||
#### [Experimental] View or change the processing delay for received traffic
|
||||
@@ -498,6 +518,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
|
||||
**Default:** `0.0`
|
||||
|
||||
**Note:** When enabled, repeaters that received a flood packet with a weak signal are held in a delay queue before processing, while those that received it with a strong signal process it immediately. This gives strong-signal paths forwarding priority. By the time weak-signal nodes process their copy, the packet may have already propagated and will be suppressed as a duplicate, reducing redundant retransmissions.
|
||||
|
||||
---
|
||||
|
||||
#### View or change the duty cycle limit
|
||||
@@ -866,7 +888,9 @@ region save
|
||||
|
||||
**Default:** `off`
|
||||
|
||||
**Note:** Output format: `{status}, {fix}, {sat count}` (when enabled)
|
||||
**Note:** Output format:
|
||||
- `off` when the GPS hardware is disabled
|
||||
- `on, {active|deactivated}, {fix|no fix}, {sat count} sats` when the GPS hardware is enabled
|
||||
|
||||
---
|
||||
|
||||
@@ -909,7 +933,7 @@ region save
|
||||
|
||||
---
|
||||
|
||||
#### View or change thevalue of a sensor
|
||||
#### View or change the value of a sensor
|
||||
**Usage:**
|
||||
- `sensor get <key>`
|
||||
- `sensor set <key> <value>`
|
||||
|
||||
+115
-29
@@ -283,32 +283,110 @@ Bytes 7+: Message Text (UTF-8, variable length)
|
||||
|
||||
### 6. Send Channel Data Datagram
|
||||
|
||||
**Purpose**: Send binary datagram data to a channel.
|
||||
**Purpose**: Send a binary datagram to a channel. Unlike channel text messages, datagrams carry no built-in sender identity and no timestamp — applications needing either must encode them inside the binary payload.
|
||||
|
||||
**Command Format**:
|
||||
```
|
||||
Byte 0: 0x3E
|
||||
Bytes 1-2: Data Type (`data_type`, 16-bit little-endian)
|
||||
Byte 3: Channel Index (0-7)
|
||||
Bytes 4+: Binary payload bytes (variable length)
|
||||
Byte 0: 0x3E
|
||||
Byte 1: Channel Index (0-7)
|
||||
Byte 2: Path Length (0xFF = flood, otherwise actual path length)
|
||||
Bytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF)
|
||||
Next 2 bytes (little-endian): Data Type (`data_type`, uint16)
|
||||
Remaining bytes: Binary payload (variable length)
|
||||
```
|
||||
|
||||
**Example** (flood, `DATA_TYPE_DEV`, payload `A1 B2 C3`, channel 1):
|
||||
```
|
||||
3E 01 FF FF FF A1 B2 C3
|
||||
```
|
||||
|
||||
**Data Type / Transport Mapping**:
|
||||
- `0x0000` is invalid for this command.
|
||||
- `0x0000` (`DATA_TYPE_RESERVED`) is invalid and rejected with `PACKET_ERROR`.
|
||||
- `0xFFFF` (`DATA_TYPE_DEV`) is the developer namespace for experimenting and developing apps.
|
||||
- Other non-zero values can be used as assigned application/community namespaces.
|
||||
|
||||
**Note**: Applications that need a timestamp should encode it inside the binary payload.
|
||||
- Values `0x0001`–`0xFFFE` are available for registered application/community namespaces. See the [Registered data_type values](#registered-data_type-values) table below.
|
||||
|
||||
**Limits**:
|
||||
- Maximum payload length is `163` bytes.
|
||||
- Larger payloads are rejected with `PACKET_ERROR`.
|
||||
- Maximum payload length is `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163` bytes.
|
||||
- Larger payloads are rejected with `PACKET_ERROR` (`ERR_CODE_ILLEGAL_ARG`).
|
||||
|
||||
**Response**: `PACKET_OK` (0x00) on success
|
||||
**Response**: `PACKET_OK` (0x00) on success, or `PACKET_ERROR` (0x01) with one of:
|
||||
- `ERR_CODE_NOT_FOUND` (2) — unknown `channel_idx`
|
||||
- `ERR_CODE_ILLEGAL_ARG` (6) — invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_CHANNEL_DATA_LENGTH`
|
||||
- `ERR_CODE_TABLE_FULL` (3) — outbound send queue is full; retry later
|
||||
|
||||
**Inbound datagrams** are delivered to the host via `RESP_CODE_CHANNEL_DATA_RECV` (0x1B); see [Receive Channel Data Datagram](#receive-channel-data-datagram).
|
||||
|
||||
#### Registered `data_type` values
|
||||
|
||||
`data_type` is an **application identifier**, not a payload-format identifier. Each registered value identifies an application that owns its own internal payload schemas. The firmware does not inspect payload contents — `data_type` is transported opaquely.
|
||||
|
||||
| Value | Constant | Purpose |
|
||||
|-----------------|----------------------|--------------------------------------------------------------------------|
|
||||
| 0x0000 | `DATA_TYPE_RESERVED` | Reserved; invalid on send |
|
||||
| 0x0001 – 0x00FF | — | Reserved for internal use |
|
||||
| 0x0100 – 0xFEFF | — | Registered application namespaces (see [number_allocations.md](number_allocations.md)) |
|
||||
| 0xFF00 – 0xFFFE | — | Testing/development; no registration required |
|
||||
| 0xFFFF | `DATA_TYPE_DEV` | Developer/experimental namespace |
|
||||
|
||||
To register a new application, submit a PR adding a row to the table in [docs/number_allocations.md](number_allocations.md). Internal sub-formats within an allocated application ID are owned by that application and are not tracked in MeshCore firmware or this document.
|
||||
|
||||
---
|
||||
|
||||
### 6. Get Message
|
||||
### Receive Channel Data Datagram
|
||||
|
||||
Inbound group datagrams (radio-level `PAYLOAD_TYPE_GRP_DATA`, 0x06) are forwarded to the host as `RESP_CODE_CHANNEL_DATA_RECV` notifications.
|
||||
|
||||
**Frame Format** (`RESP_CODE_CHANNEL_DATA_RECV`, 0x1B):
|
||||
```
|
||||
Byte 0: 0x1B (packet type)
|
||||
Byte 1: SNR (signed int8, scaled ×4 — divide by 4.0 to recover dB)
|
||||
Bytes 2-3: Reserved (clients MUST ignore)
|
||||
Byte 4: Channel Index (0-7)
|
||||
Byte 5: Path Length (actual path length when flooded, otherwise 0xFF for direct)
|
||||
Bytes 6-7: Data Type (uint16 little-endian)
|
||||
Byte 8: Data Length
|
||||
Bytes 9 .. 8+data_len: Payload
|
||||
```
|
||||
|
||||
**Path bytes are not forwarded**: Only `path_len` is reported in the receive frame — the path itself is not copied to the host. There are no path bytes between byte 5 and the data_type field at bytes 6–7, regardless of `path_len`.
|
||||
|
||||
**Path Length semantics differ between send and receive**:
|
||||
|
||||
| Direction | `path_len = 0xFF` | `path_len ≠ 0xFF` |
|
||||
|-----------|---------------------------------|-------------------------------------------------------------------------------------|
|
||||
| Send | Flood the network | Direct route; the encoded path follows (low 6 bits = hash count, top 2 bits + 1 = hash size; on-wire byte count = `hash_count × hash_size`) |
|
||||
| Receive | Packet arrived via direct route | Packet was flooded; this is the encoded `pkt->path_len` field as observed (no path bytes follow) |
|
||||
|
||||
In other words, the meaning of `0xFF` is inverted between the two directions, and on receive the field carries metadata only — never a routable path. `path_len` is an encoded byte (see `Packet::isValidPathLen` / `Packet::writePath` in `src/Packet.cpp`), not a raw byte count.
|
||||
|
||||
**Note**: The device may also emit `PACKET_MESSAGES_WAITING` (0x83) to notify the host that datagrams are queued; poll with `CMD_SYNC_NEXT_MESSAGE` (0x0A) to retrieve them.
|
||||
|
||||
**Parsing Pseudocode**:
|
||||
```python
|
||||
def parse_channel_data_recv(data):
|
||||
if len(data) < 9:
|
||||
return None
|
||||
snr_byte = data[1]
|
||||
snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0
|
||||
channel_idx = data[4]
|
||||
path_len = data[5]
|
||||
data_type = int.from_bytes(data[6:8], 'little')
|
||||
data_len = data[8]
|
||||
if 9 + data_len > len(data):
|
||||
return None
|
||||
payload = data[9:9 + data_len]
|
||||
return {
|
||||
'snr': snr,
|
||||
'channel_idx': channel_idx,
|
||||
'path_len': path_len,
|
||||
'data_type': data_type,
|
||||
'payload': bytes(payload),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Get Message
|
||||
|
||||
**Purpose**: Request the next queued message from the device.
|
||||
|
||||
@@ -325,13 +403,14 @@ Byte 0: 0x0A
|
||||
**Response**:
|
||||
- `PACKET_CHANNEL_MSG_RECV` (0x08) or `PACKET_CHANNEL_MSG_RECV_V3` (0x11) for channel messages
|
||||
- `PACKET_CONTACT_MSG_RECV` (0x07) or `PACKET_CONTACT_MSG_RECV_V3` (0x10) for contact messages
|
||||
- `PACKET_CHANNEL_DATA_RECV` (0x1B) for channel data datagrams
|
||||
- `PACKET_NO_MORE_MSGS` (0x0A) if no messages available
|
||||
|
||||
**Note**: Poll this command periodically to retrieve queued messages. The device may also send `PACKET_MESSAGES_WAITING` (0x83) as a notification when messages are available.
|
||||
|
||||
---
|
||||
|
||||
### 7. Get Battery and Storage
|
||||
### 8. Get Battery and Storage
|
||||
|
||||
**Purpose**: Query device battery voltage and storage usage.
|
||||
|
||||
@@ -527,6 +606,15 @@ Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)).
|
||||
|
||||
## Response Parsing
|
||||
|
||||
### Terminology
|
||||
|
||||
This document uses a spec-level naming convention (`PACKET_*`) for bytes the firmware sends back to the host. In the firmware source these same values are split across two `#define` families by purpose:
|
||||
|
||||
- `RESP_CODE_*` — direct replies to a command (e.g. `RESP_CODE_CHANNEL_DATA_RECV` = `PACKET_CHANNEL_DATA_RECV` = 0x1B).
|
||||
- `PUSH_CODE_*` — asynchronous notifications not tied to a specific command (e.g. `PUSH_CODE_MSG_WAITING` = `PACKET_MESSAGES_WAITING` = 0x83).
|
||||
|
||||
Byte values are authoritative; names are aliases. When reading firmware source, `RESP_CODE_X` / `PUSH_CODE_X` correspond to this doc's `PACKET_X` of the same numeric value.
|
||||
|
||||
### Packet Types
|
||||
|
||||
| Value | Name | Description |
|
||||
@@ -547,6 +635,7 @@ Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)).
|
||||
| 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 |
|
||||
| 0x1B | PACKET_CHANNEL_DATA_RECV | Channel data datagram |
|
||||
| 0x80 | PACKET_ADVERTISEMENT | Advertisement packet |
|
||||
| 0x82 | PACKET_ACK | Acknowledgment |
|
||||
| 0x83 | PACKET_MESSAGES_WAITING | Messages waiting notification |
|
||||
@@ -718,22 +807,18 @@ Bytes 1-6: ACK Code (6 bytes, hex)
|
||||
|
||||
### Error Codes
|
||||
|
||||
**PACKET_ERROR** (0x01) may include an error code in byte 1:
|
||||
`PACKET_ERROR` (0x01) carries a single-byte error code in byte 1. Values match the `ERR_CODE_*` constants defined in `examples/companion_radio/MyMesh.cpp`:
|
||||
|
||||
| Error Code | Description |
|
||||
|------------|-------------|
|
||||
| 0x00 | Generic error (no specific code) |
|
||||
| 0x01 | Invalid command |
|
||||
| 0x02 | Invalid parameter |
|
||||
| 0x03 | Channel not found |
|
||||
| 0x04 | Channel already exists |
|
||||
| 0x05 | Channel index out of range |
|
||||
| 0x06 | Secret mismatch |
|
||||
| 0x07 | Message too long |
|
||||
| 0x08 | Device busy |
|
||||
| 0x09 | Not enough storage |
|
||||
| Code | Constant (firmware) | Description |
|
||||
|------|----------------------------|------------------------------------------------------------------------------|
|
||||
| 1 | `ERR_CODE_UNSUPPORTED_CMD` | Unknown or unsupported command byte / sub-command |
|
||||
| 2 | `ERR_CODE_NOT_FOUND` | Target not found (channel, contact, message, etc.) |
|
||||
| 3 | `ERR_CODE_TABLE_FULL` | Internal queue or table is full — retry later |
|
||||
| 4 | `ERR_CODE_BAD_STATE` | Operation not valid in current device state (e.g. iterator already running) |
|
||||
| 5 | `ERR_CODE_FILE_IO_ERROR` | Filesystem or storage I/O failure |
|
||||
| 6 | `ERR_CODE_ILLEGAL_ARG` | Invalid argument (bad length, out-of-range value, reserved field, etc.) |
|
||||
|
||||
**Note**: Error codes may vary by firmware version. Always check byte 1 of `PACKET_ERROR` response.
|
||||
**Note**: Error codes may vary by firmware version. Always check byte 1 of `PACKET_ERROR` response, and treat unknown codes as generic errors.
|
||||
|
||||
### Frame Handling
|
||||
|
||||
@@ -765,7 +850,8 @@ BLE implementations enqueue and deliver one protocol frame per BLE write/notific
|
||||
- `GET_CHANNEL` → `PACKET_CHANNEL_INFO`
|
||||
- `SET_CHANNEL` → `PACKET_OK` or `PACKET_ERROR`
|
||||
- `SEND_CHANNEL_MESSAGE` → `PACKET_MSG_SENT`
|
||||
- `GET_MESSAGE` → `PACKET_CHANNEL_MSG_RECV`, `PACKET_CONTACT_MSG_RECV`, or `PACKET_NO_MORE_MSGS`
|
||||
- `GET_MESSAGE` → `PACKET_CHANNEL_MSG_RECV`, `PACKET_CONTACT_MSG_RECV`, `PACKET_CHANNEL_DATA_RECV`, or `PACKET_NO_MORE_MSGS`
|
||||
- `SEND_CHANNEL_DATA` → `PACKET_OK` or `PACKET_ERROR`
|
||||
- `GET_BATTERY` → `PACKET_BATTERY`
|
||||
|
||||
4. **Timeout Handling**:
|
||||
|
||||
+198
-234
@@ -2,107 +2,93 @@
|
||||
|
||||
A list of frequently-asked questions and answers for MeshCore
|
||||
|
||||
- [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)
|
||||
- [1.2.1. Hardware](#121-hardware)
|
||||
- [1.2.2. Firmware](#122-firmware)
|
||||
- [1.2.3. Companion Radio Firmware](#123-companion-radio-firmware)
|
||||
- [1.2.4. Repeater](#124-repeater)
|
||||
- [1.2.5. Room Server](#125-room-server)
|
||||
- [2. Initial Setup](#2-initial-setup)
|
||||
- [2.1. Q: How many devices do I need to start using MeshCore?](#21-q-how-many-devices-do-i-need-to-start-using-meshcore)
|
||||
- [2.2. Q: Does MeshCore cost any money?](#22-q-does-meshcore-cost-any-money)
|
||||
- [2.3. Q: What frequencies are supported by MeshCore?](#23-q-what-frequencies-are-supported-by-meshcore)
|
||||
- [2.4. Q: What is an "advert" in MeshCore?](#24-q-what-is-an-advert-in-meshcore)
|
||||
- [2.5. Q: Is there a hop limit?](#25-q-is-there-a-hop-limit)
|
||||
- [3. Server Administration](#3-server-administration)
|
||||
- [3.1. Q: How do you configure a repeater or a room server?](#31-q-how-do-you-configure-a-repeater-or-a-room-server)
|
||||
- [3.2. Q: Do I need to set the location for a repeater?](#32-q-do-i-need-to-set-the-location-for-a-repeater)
|
||||
- [3.3. Q: What is the password to administer a repeater or a room server?](#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server)
|
||||
- [3.4. Q: What is the password to join a room server?](#34-q-what-is-the-password-to-join-a-room-server)
|
||||
- [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)
|
||||
- [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)
|
||||
- [4.3. Q: Why is my T-Deck Plus not getting any satellite lock?](#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock)
|
||||
- [4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?](#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock)
|
||||
- [4.5. Q: What size of SD card does the T-Deck support?](#45-q-what-size-of-sd-card-does-the-t-deck-support)
|
||||
- [4.6. Q: what is the public key for the default public channel?](#46-q-what-is-the-public-key-for-the-default-public-channel)
|
||||
- [4.7. Q: How do I get maps on T-Deck?](#47-q-how-do-i-get-maps-on-t-deck)
|
||||
- [4.8. Q: Where do the map tiles go?](#48-q-where-do-the-map-tiles-go)
|
||||
- [4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?](#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck)
|
||||
- [4.10. Q: How to decipher the diagnostics screen on T-Deck?](#410-q-how-to-decipher-the-diagnostics-screen-on-t-deck)
|
||||
- [4.11. Q: The T-Deck sound is too loud?](#411-q-the-t-deck-sound-is-too-loud)
|
||||
- [4.12. Q: Can you customize the sound?](#412-q-can-you-customize-the-sound)
|
||||
- [4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?](#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts)
|
||||
- [4.14. Q: How to capture a screenshot on T-Deck?](#414-q-how-to-capture-a-screenshot-on-t-deck)
|
||||
- [5. General](#5-general)
|
||||
- [5.1. Q: What are BW, SF, and CR?](#51-q-what-are-bw-sf-and-cr)
|
||||
- [5.2. Q: Do MeshCore clients repeat?](#52-q-do-meshcore-clients-repeat)
|
||||
- [5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?](#53-q-what-happens-when-a-node-learns-a-route-via-a-mobile-repeater-and-that-repeater-is-gone)
|
||||
- [5.4. Q: How does a node discovery a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?](#54-q-how-does-a-node-discovery-a-path-to-its-destination-and-then-use-it-to-send-messages-in-the-future-instead-of-flooding-every-message-it-sends-like-meshtastic)
|
||||
- [5.5. Q: Do public channels always flood? Do private channels always flood?](#55-q-do-public-channels-always-flood-do-private-channels-always-flood)
|
||||
- [5.6. Q: what is the public key for the default public channel?](#56-q-what-is-the-public-key-for-the-default-public-channel)
|
||||
- [5.7. Q: Is MeshCore open source?](#57-q-is-meshcore-open-source)
|
||||
- [5.8. Q: How can I support MeshCore?](#58-q-how-can-i-support-meshcore)
|
||||
- [5.9. Q: How do I build MeshCore firmware from source?](#59-q-how-do-i-build-meshcore-firmware-from-source)
|
||||
- [5.10. Q: Are there other MeshCore related open source projects?](#510-q-are-there-other-meshcore-related-open-source-projects)
|
||||
- [5.11. Q: Does MeshCore support ATAK](#511-q-does-meshcore-support-atak)
|
||||
- [5.12. Q: How do I add a node to the MeshCore Map](#512-q-how-do-i-add-a-node-to-the-meshcore-map)
|
||||
- [5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?](#513-q-can-i-use-a-raspberry-pi-to-update-a-meshcore-radio)
|
||||
- [5.14. Q: Are there are projects built around MeshCore?](#514-q-are-there-are-projects-built-around-meshcore)
|
||||
- [5.14.1. overview](#5141-overview)
|
||||
- [5.14.1.1. awesome-meshcore](#51411-awesome-meshcore)
|
||||
- [5.14.2. programming libraries, command line software](#5142-programming-libraries-command-line-software)
|
||||
- [5.14.2.1. meshcoremqtt](#51421-meshcoremqtt)
|
||||
- [5.14.2.2. MeshCore for Home Assistant](#51422-meshcore-for-home-assistant)
|
||||
- [5.14.2.3. Python MeshCore](#51423-python-meshcore)
|
||||
- [5.14.2.4. meshcore-cli](#51424-meshcore-cli)
|
||||
- [5.14.2.5. meshcore.js](#51425-meshcorejs)
|
||||
- [5.14.2.6. pyMC\_core](#51426-pymc_core)
|
||||
- [5.14.2.7. MeshCore Packet Decoder](#51427-meshcore-packet-decoder)
|
||||
- [5.14.2.8. meshcore-pi](#51428-meshcore-pi)
|
||||
- [5.14.2.9. pyMC\_Repeater](#51429-pymc_repeater)
|
||||
- [5.14.2.10. MeshCore map auto uploader](#514210-MeshCore-map-auto-uploader)
|
||||
- [5.14.3. apps, graphical software](#5143-apps-graphical-software)
|
||||
- [5.14.3.1. meshcore-open](#51431-meshcore-open)
|
||||
- [5.14.4. firmwares](#5144-firmwares)
|
||||
- [5.14.4.1. MeshCore-Cardputer-ADV](#51441-MeshCore-Cardputer-ADV)
|
||||
- [5.14.4.2. LunarCore](#51442-LunarCore)
|
||||
- [5.14.4.3. MC-Term](#51443-MC-Term)
|
||||
- [5.14.4.4. Meck](#51444-Meck)
|
||||
- [5.14.4.5. Meshcore for Wio Tracker L1 Pro](#51445-Meshcore-for-Wio-Tracker-L1-Pro)
|
||||
- [5.14.5. online services](#5145-online-services)
|
||||
- [5.15. Q: Are there client applications for Windows or Mac?](#515-q-are-there-client-applications-for-windows-or-mac)
|
||||
- [5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?](#516-q-are-there-any-resources-that-compare-meshcore-to-other-lora-systems)
|
||||
- [6. Troubleshooting](#6-troubleshooting)
|
||||
- [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago)
|
||||
- [6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.](#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed)
|
||||
- [6.3. Q: How to connect to a repeater via BLE (Bluetooth)?](#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth)
|
||||
- [6.4. Q: My companion isn't showing up over Bluetooth?](#64-q-my-companion-isnt-showing-up-over-bluetooth)
|
||||
- [6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#65-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code)
|
||||
- [6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.](#66-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection)
|
||||
- [6.7. Q: My RAK/T1000-E/xiao\_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?](#67-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh)
|
||||
- [6.8. Q: WebFlasher fails on Linux with failed to open](#68-q-webflasher-fails-on-linux-with-failed-to-open)
|
||||
- [7. Other Questions:](#7-other-questions)
|
||||
- [7.1. Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?](#71-q-how-to-update-nrf-rak-t114-seed-xiao-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app)
|
||||
- [7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?](#711-q-can-i-update-seeed-studio-wio-tracker-l1-pro-using-ota)
|
||||
- [7.2. Q: How to update ESP32-based devices over the air?](#72-q-how-to-update-esp32-based-devices-over-the-air)
|
||||
- [7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?](#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu)
|
||||
- [7.4. Q: are the MeshCore logo and font available?](#74-q-are-the-meshcore-logo-and-font-available)
|
||||
- [7.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code)
|
||||
- [7.6. Q: How do I connect to the companion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-companion-via-wifi-eg-using-a-heltec-v3)
|
||||
- [7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with a EByte E22-900M30S or a E22-900M33S module, what should their transmit power be set to?](#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-a-ebyte-e22-900m30s-or-a-e22-900m33s-module-what-should-their-transmit-power-be-set-to)
|
||||
- [Frequently Asked Questions](#frequently-asked-questions)
|
||||
- [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)
|
||||
- [1.2.1. Hardware](#121-hardware)
|
||||
- [1.2.2. Firmware](#122-firmware)
|
||||
- [1.2.3. Companion Radio Firmware](#123-companion-radio-firmware)
|
||||
- [1.2.4. Repeater](#124-repeater)
|
||||
- [1.2.5. Room Server](#125-room-server)
|
||||
- [2. Initial Setup](#2-initial-setup)
|
||||
- [2.1. Q: How many devices do I need to start using MeshCore?](#21-q-how-many-devices-do-i-need-to-start-using-meshcore)
|
||||
- [2.2. Q: Does MeshCore cost any money?](#22-q-does-meshcore-cost-any-money)
|
||||
- [2.3. Q: What frequencies are supported by MeshCore?](#23-q-what-frequencies-are-supported-by-meshcore)
|
||||
- [2.4. Q: What is an "advert" in MeshCore?](#24-q-what-is-an-advert-in-meshcore)
|
||||
- [2.5. Q: Is there a hop limit?](#25-q-is-there-a-hop-limit)
|
||||
- [3. Server Administration](#3-server-administration)
|
||||
- [3.1. Q: How do you configure a repeater or a room server?](#31-q-how-do-you-configure-a-repeater-or-a-room-server)
|
||||
- [3.2. Q: Do I need to set the location for a repeater?](#32-q-do-i-need-to-set-the-location-for-a-repeater)
|
||||
- [3.3. Q: What is the password to administer a repeater or a room server?](#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server)
|
||||
- [3.4. Q: What is the password to join a room server?](#34-q-what-is-the-password-to-join-a-room-server)
|
||||
- [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 existing 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-existing-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 may be 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-may-be-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.9. Q: What is multi-byte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean?](#39-q-what-is-multi-byte-support--what-do-1-byte-2-byte-3-byte-adverts-and-messages-mean)
|
||||
- [3.9.1. Q: **What path hash sizes will my repeater forward?**](#391-q-what-path-hash-sizes-will-my-repeater-forward)
|
||||
- [3.9.2. Q: **What determines a packet's path hash size?**](#392-q-what-determines-a-packets-path-hash-size)
|
||||
- [3.9.3. Q: **How do I change my companion's path hash size?**](#393-q-how-do-i-change-my-companions-path-hash-size)
|
||||
- [3.9.4. Q: **What does the CLI command `path.hash.mode` do on a repeater?**](#394-q-what-does-the-cli-command-pathhashmode-do-on-a-repeater)
|
||||
- [3.9.5. Q: **Why use 2- or 3-byte path hash for adverts?**](#395-q-why-use-2--or-3-byte-path-hash-for-adverts)
|
||||
- [3.9.6. Q: **When can we move away from 1-byte path hash for channel and direct messages?**](#396-q-when-can-we-move-away-from-1-byte-path-hash-for-channel-and-direct-messages)
|
||||
- [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)
|
||||
- [4.3. Q: Why is my T-Deck Plus not getting any satellite lock?](#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock)
|
||||
- [4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?](#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock)
|
||||
- [4.5. Q: What size of SD card does the T-Deck support?](#45-q-what-size-of-sd-card-does-the-t-deck-support)
|
||||
- [4.6. Q: What is the public key for the default public channel?](#46-q-what-is-the-public-key-for-the-default-public-channel)
|
||||
- [4.7. Q: How do I get maps on T-Deck?](#47-q-how-do-i-get-maps-on-t-deck)
|
||||
- [4.8. Q: Where do the map tiles go?](#48-q-where-do-the-map-tiles-go)
|
||||
- [4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?](#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck)
|
||||
- [4.10. Q: How to decipher the diagnostics screen on T-Deck?](#410-q-how-to-decipher-the-diagnostics-screen-on-t-deck)
|
||||
- [4.11. Q: The T-Deck sound is too loud?](#411-q-the-t-deck-sound-is-too-loud)
|
||||
- [4.12. Q: Can you customize the sound?](#412-q-can-you-customize-the-sound)
|
||||
- [4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?](#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts)
|
||||
- [4.14. Q: How to capture a screenshot on T-Deck?](#414-q-how-to-capture-a-screenshot-on-t-deck)
|
||||
- [5. General](#5-general)
|
||||
- [5.1. Q: What are BW, SF, and CR?](#51-q-what-are-bw-sf-and-cr)
|
||||
- [5.2. Q: Do MeshCore clients repeat?](#52-q-do-meshcore-clients-repeat)
|
||||
- [5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?](#53-q-what-happens-when-a-node-learns-a-route-via-a-mobile-repeater-and-that-repeater-is-gone)
|
||||
- [5.4. Q: How does a node discover a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?](#54-q-how-does-a-node-discover-a-path-to-its-destination-and-then-use-it-to-send-messages-in-the-future-instead-of-flooding-every-message-it-sends-like-meshtastic)
|
||||
- [5.5. Q: Do public channels always flood? Do private channels always flood?](#55-q-do-public-channels-always-flood-do-private-channels-always-flood)
|
||||
- [5.6. Q: What is the public key for the default public channel?](#56-q-what-is-the-public-key-for-the-default-public-channel)
|
||||
- [5.7. Q: Is MeshCore open source?](#57-q-is-meshcore-open-source)
|
||||
- [5.8. Q: How can I support MeshCore?](#58-q-how-can-i-support-meshcore)
|
||||
- [5.9. Q: How do I build MeshCore firmware from source?](#59-q-how-do-i-build-meshcore-firmware-from-source)
|
||||
- [5.10. Q: Are there other MeshCore related open source projects?](#510-q-are-there-other-meshcore-related-open-source-projects)
|
||||
- [5.11. Q: Does MeshCore support ATAK](#511-q-does-meshcore-support-atak)
|
||||
- [5.12. Q: How do I add a node to the MeshCore Map](#512-q-how-do-i-add-a-node-to-the-meshcore-map)
|
||||
- [5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?](#513-q-can-i-use-a-raspberry-pi-to-update-a-meshcore-radio)
|
||||
- [5.14. Q: Are there projects built around MeshCore?](#514-q-are-there-projects-built-around-meshcore)
|
||||
- [5.15. Q: Are there client applications for Windows or Mac?](#515-q-are-there-client-applications-for-windows-or-mac)
|
||||
- [5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?](#516-q-are-there-any-resources-that-compare-meshcore-to-other-lora-systems)
|
||||
- [6. Troubleshooting](#6-troubleshooting)
|
||||
- [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago)
|
||||
- [6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.](#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed)
|
||||
- [6.3. Q: How to connect to a repeater via BLE (Bluetooth)?](#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth)
|
||||
- [6.4. Q: My companion isn't showing up over Bluetooth?](#64-q-my-companion-isnt-showing-up-over-bluetooth)
|
||||
- [6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#65-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code)
|
||||
- [6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.](#66-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection)
|
||||
- [6.7. Q: My RAK/T1000-E/xiao\_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?](#67-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh)
|
||||
- [6.8. Q: WebFlasher fails on Linux with failed to open](#68-q-webflasher-fails-on-linux-with-failed-to-open)
|
||||
- [7. Other Questions:](#7-other-questions)
|
||||
- [7.1. Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?](#71-q-how-to-update-nrf-rak-t114-seed-xiao-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app)
|
||||
- [7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?](#711-q-can-i-update-seeed-studio-wio-tracker-l1-pro-using-ota)
|
||||
- [7.2. Q: How to update ESP32-based devices over the air?](#72-q-how-to-update-esp32-based-devices-over-the-air)
|
||||
- [7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?](#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu)
|
||||
- [7.4. Q: are the MeshCore logo and font available?](#74-q-are-the-meshcore-logo-and-font-available)
|
||||
- [7.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code)
|
||||
- [7.6. Q: How do I connect to the companion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-companion-via-wifi-eg-using-a-heltec-v3)
|
||||
- [7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with a EByte E22-900M30S or a E22-900M33S module, what should their transmit power be set to?](#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-a-ebyte-e22-900m30s-or-a-e22-900m33s-module-what-should-their-transmit-power-be-set-to)
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
### 1.1. Q: What is MeshCore?
|
||||
|
||||
**A:** MeshCore is a multi platform system for enabling secure text based communications utilising LoRa radio hardware. It can be used for Off-Grid Communication, Emergency Response & Disaster Recovery, Outdoor Activities, Tactical Security including law enforcement and private security and also IoT sensor networks. ([source](https://meshcore.co.uk/))
|
||||
**A:** MeshCore is a multi platform system for enabling secure text based communications utilising LoRa radio hardware. It can be used for Off-Grid Communication, Emergency Response & Disaster Recovery, Outdoor Activities, Tactical Security including law enforcement and private security and also IoT sensor networks. ([source](https://meshcore.io/))
|
||||
|
||||
MeshCore is free and open source:
|
||||
* MeshCore is the routing and firmware etc, available on GitHub under MIT license
|
||||
@@ -120,19 +106,20 @@ 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](https://flasher.meshcore.co.uk)
|
||||
- Main Website: [https://meshcore.io](https://meshcore.io)
|
||||
- Firmware Flasher: [https://flasher.meshcore.io](https://flasher.meshcore.io)
|
||||
- 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.
|
||||
- MeshCore Companion Web App: [https://app.meshcore.nz](https://app.meshcore.nz)
|
||||
- MeshCore Map: [https://map.meshcore.io](https://map.meshcore.io)
|
||||
- Andy Kirby's [MeshCore Intro Video](https://www.youtube.com/watch?v=t1qne8uJBAc)
|
||||
- Liam Cottle's [MeshCore Technical Presentation](https://www.youtube.com/watch?v=OwmkVkZQTf4)
|
||||
|
||||
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.
|
||||
|
||||
For an up-to-date list of supported devices, please go to https://flasher.meshcore.co.uk/
|
||||
For an up-to-date list of supported devices, please go to https://flasher.meshcore.io
|
||||
|
||||
To use MeshCore without using a phone as the client interface, you can run MeshCore on a LiLygo's T-Deck, T-Deck Plus, T-Pager, T-Watch, or T-Display Pro. MeshCore Ultra firmware running on these devices are a complete off-grid secure communication solution.
|
||||
|
||||
@@ -144,12 +131,11 @@ Companion radios are for connecting to the Android app or web app as a messenger
|
||||
|
||||
1. **BLE Companion**
|
||||
BLE Companion firmware runs on a supported LoRa device and connects to a smart device running the Android or iOS MeshCore client over BLE
|
||||
<https://meshcore.co.uk/apps.html>
|
||||
<https://meshcore.io/>
|
||||
|
||||
2. **USB Serial Companion**
|
||||
USB Serial Companion firmware runs on a supported LoRa device and connects to a smart device or a computer over USB Serial running the MeshCore web client
|
||||
<https://meshcore.liamcottle.net/#/>
|
||||
<https://client.meshcore.co.uk/tabs/devices>
|
||||
<https://app.meshcore.nz/>
|
||||
|
||||
#### 1.2.4. Repeater
|
||||
Repeaters are used to extend the range of a MeshCore network. Repeater firmware runs on the same devices that run client firmware. A repeater's job is to forward MeshCore packets to the destination device. It does **not** forward or retransmit every packet it receives, unlike other LoRa mesh systems.
|
||||
@@ -202,13 +188,13 @@ The T-Deck firmware is free to download and most features are available without
|
||||
### 2.3. Q: What frequencies are supported by MeshCore?
|
||||
**A:** It supports the 868MHz range in the UK/EU and the 915MHz range in New Zealand, Australia, and the USA. Countries and regions in these two frequency ranges are also supported.
|
||||
|
||||
Use the smartphone client or the repeater setup feature on there web flasher to set your radios' RF settings by choosing the preset for your regions.
|
||||
Use the smartphone client or the repeater setup feature on the web flasher to set your radios' RF settings by choosing the preset for your regions.
|
||||
|
||||
Recently, as of October 2025, many regions have moved to the "narrow" setting, aka using BW62.5 and a lower SF number (instead of the original SF11). For example, USA/Canada (Recommended) preset is 910.525MHz, SF7, BW62.5, CR5.
|
||||
|
||||
After extensive testing, many regions have switched or about to switch over to BW62.5 and SF7, 8, or 9. Narrower bandwidth setting and lower SF setting allow MeshCore's radio signals to fit between interference in the ISM band, provide for a lower noise floor, better SNR, and faster transmissions.
|
||||
|
||||
If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the [#meshcore-app](https://discord.com/channels/1343693475589263471/1391681655911088241) channel on the [MeshCore Discord server ](https://discord.gg/cYtQNYCCRK) to let Liam Cottle know.
|
||||
If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the [#meshcore-app](https://discord.com/channels/1343693475589263471/1391681655911088241) channel on the [MeshCore Discord server ](https://meshcore.gg) to let Liam Cottle know.
|
||||
|
||||
|
||||
|
||||
@@ -243,11 +229,11 @@ The separate `set advert.interval {minutes}` command controls the local zero-hop
|
||||
|
||||
Repeater or room server can be administered with one of the options below:
|
||||
|
||||
- After a repeater or room server firmware is flashed on to a LoRa device, go to <https://config.meshcore.dev> and use the web user interface to connect to the LoRa device via USB serial. From there you can set the name of the server, its frequency and other related settings, location, passwords etc.
|
||||
- After a repeater or room server firmware is flashed on to a LoRa device, go to <https://config.meshcore.io> and use the web user interface to connect to the LoRa device via USB serial. From there you can set the name of the server, its frequency and other related settings, location, passwords etc.
|
||||
|
||||

|
||||
|
||||
- Connect the server device using a USB cable to a computer running Chrome on https://flasher.meshcore.co.uk/, then use the `console` feature to connect to the device
|
||||
- Connect the server device using a USB cable to a computer running Chrome on https://flasher.meshcore.io, then use the `console` feature to connect to the device
|
||||
|
||||
- Use a MeshCore smartphone clients to remotely administer servers via LoRa.
|
||||
|
||||
@@ -286,12 +272,16 @@ You can get the latitude and longitude from Google Maps by right-clicking the lo
|
||||
|
||||
Reboot the repeater after `set prv.key <hex>` command for the new private key to take effect.
|
||||
|
||||
### 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?
|
||||
### 3.6. Q: The first byte of my repeater's public key collides with an existing 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?
|
||||
|
||||
**A:** You can generate a new private key and specific the first byte of its public key here: https://gessaman.com/mc-keygen/
|
||||
|
||||
Having multiple repeaters with the same first byte ID does not negatively affect the mesh or its functionality. Flood and pathed packets will still reach their destinations. First byte ID collision makes traceroute and path analysis harder because these tools don't know exactly which of the two (or more) colliding repeaters is the one in the path.
|
||||
|
||||
### 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?
|
||||
Best practice is when you set up a new repeater, choose a public key that is not in use. If it is not possible to find a unique first byte for your repeater's public key, choose one that is unique within about 10 miles (16 km) to minimize collision with nearby repeaters.
|
||||
|
||||
|
||||
### 3.7. Q: My repeater may be 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 periodically reset its AGC.
|
||||
|
||||
@@ -306,6 +296,58 @@ This is a very low cost operation. AGC reset is done by simply setting `state =
|
||||
|
||||
**A:** The observer instruction is available here: https://analyzer.letsmesh.net/observer/onboard
|
||||
|
||||
### 3.9. Q: What is multi-byte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean?
|
||||
|
||||
**A:**
|
||||
The original MeshCore protocol design uses the first byte of a repeater's public key to denote the repeater in a path. And with 1 byte for each repeater in the path, MeshCore packets can travel as many as 64 hops.
|
||||
|
||||
However, with 1 byte, there are only 254 unique IDs (exclude 00 and FF which are reserved). Many meshes group have multiple repeaters with the same first byte in their public keys. Packets continue to pass through repeaters and the mesh is not harmed in anyway. It does make it harder for tools to analyze paths with duplicated repeater IDs.
|
||||
|
||||
Firmware version 1.14 and newer introduces the ability for repeaters to advert with 1-, 2-, or 3-byte adverts. Companions can also send out channel and direct messages with 1-, 2-, or 3-byte path. Adverts and messages sent in 1-byte path is compatible with repeater firmware older or newer than 1.14. They will travel up to 64 hops. 2-byte adverts and messages will travel up to 32 hops. 3-byte adverts and messages will travel up to 21 hops.
|
||||
|
||||
### 3.9.1. Q: **What path hash sizes will my repeater forward?**
|
||||
|
||||
Repeaters running firmware 1.14+ repeat packets sent with 1-, 2-, or 3-byte path hash. Repeaters on firmware older than 1.14 only repeat 1-byte path hash packets and silently drop 2- and 3-byte packets.
|
||||
|
||||
### 3.9.2. Q: **What determines a packet's path hash size?**
|
||||
|
||||
The original packet sender determines the path hash size. The most common original sender is a companion app. The other common original sender is a repeater, when it broadcasts its advert.
|
||||
|
||||
### 3.9.3. Q: **How do I change my companion's path hash size?**
|
||||
|
||||
As of firmware version 1.14 and MeshCore app version 1.41.0, in the MeshCore app, you can set your companion's message path hash size in `Settings (gear icon)`, `Experimental Settings`.
|
||||
|
||||
Until your regional mesh has the vast majority of the repeaters updated to 1.14+ firmware, it is recommended to keep your companion at the default 1-byte because pre-1.14 repeaters will silently drop messages with larger path hashes.
|
||||
|
||||
### 3.9.4. Q: **What does the CLI command `path.hash.mode` do on a repeater?**
|
||||
|
||||
This CLI command `path.hash.mode` *only* controls the path hash size used in a repeater's own advert broadcasts. It does **NOT** affect which packets the repeater forwards. A repeater with firmware 1.14+ always forward 1-, 2-, and 3-byte packets regardless of this setting.
|
||||
|
||||
Usage: `set path.hash.mode {0|1|2}`:
|
||||
|
||||
```
|
||||
┌────────────────┬───────────────────────┐
|
||||
│ path.hash.mode │ Advert path hash size │
|
||||
├────────────────┼───────────────────────┤
|
||||
│ 0 │ 1 byte (default) │
|
||||
├────────────────┼───────────────────────┤
|
||||
│ 1 │ 2 bytes │
|
||||
├────────────────┼───────────────────────┤
|
||||
│ 2 │ 3 bytes │
|
||||
└────────────────┴───────────────────────┘
|
||||
```
|
||||
|
||||
It is safe to set your 1.14+ repeaters to mode 1 or 2.
|
||||
|
||||
### 3.9.5. Q: **Why use 2- or 3-byte path hash for adverts?**
|
||||
|
||||
A longer path hash helps tools like the LetsMesh.net Analyzer and MeshMapper disambiguate repeaters more reliably. With only 1 byte, the chance of different repeaters having the same first byte in their public key is high, making it harder to tell them apart in mesh network analysis. Since this only affects adverts, there's no downside. 2- and 3-byte adverts don't travel as far as 1-byte adverts, but it is not important for MeshCore nodes to hear a repeater's advert that are 21 or 32 hops away.
|
||||
|
||||
### 3.9.6. Q: **When can we move away from 1-byte path hash for channel and direct messages?**
|
||||
|
||||
You should move to send 2-byte or 3-byte channel and direct messages when the vast majority of the repeaters in your regional mesh are updated to firmware version 1.14 or newer. Setting your repeater's `path.hash.mode` to 1 (for 2-byte path hash) or 2 (for 3-byte path hash) now helps the community gauge to how many repeaters have updated to 1.14+. Please work with your MeshCore community together to decide when to switch to 2-byte path or 3-byte path for channel and direct messages.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. T-Deck Related
|
||||
@@ -323,7 +365,7 @@ This is a very low cost operation. AGC reset is done by simply setting `state =
|
||||
5. Hear USB connection sound
|
||||
6. Release trackball
|
||||
7. T-Deck in DFU mode now
|
||||
8. At this point you can begin flashing using <https://flasher.meshcore.co.uk/>
|
||||
8. At this point you can begin flashing using <https://flasher.meshcore.io>
|
||||
|
||||
### 4.3. Q: Why is my T-Deck Plus not getting any satellite lock?
|
||||
**A:** For T-Deck Plus, the GPS baud rate should be set to **38400**. Also, some T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open the device to check the GPS orientation.
|
||||
@@ -338,7 +380,7 @@ GPS on T-Deck is always enabled. You can skip the "GPS clock sync" and the T-De
|
||||
### 4.5. Q: What size of SD card does the T-Deck support?
|
||||
**A:** Users have had no issues using 16GB or 32GB SD cards. Format the SD card to **FAT32**.
|
||||
|
||||
### 4.6. Q: what is the public key for the default public channel?
|
||||
### 4.6. Q: What is the public key for the default public channel?
|
||||
**A:**
|
||||
T-Deck uses the same key the smartphone apps use but in base64
|
||||
`izOH6cXN6mrJ5e26oRXNcg==`
|
||||
@@ -457,7 +499,7 @@ In MeshCore, only repeaters and room server with `set repeat on` repeat.
|
||||
|
||||
In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to re-establish a path.
|
||||
|
||||
### 5.4. Q: How does a node discovery a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?
|
||||
### 5.4. Q: How does a node discover a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?
|
||||
|
||||
Routes are stored in sender's contact list. When you send a message the first time, the message first gets to your destination by flood routing. When your destination node gets the message, it will send back a delivery report to the sender with all repeaters that the original message went through. This delivery report is flood-routed back to you the sender and is a basis for future direct path. When you send the next message, the path will get embedded into the packet and be evaluated by repeaters. If the hop and address of the repeater matches, it will retransmit the message, otherwise it will not retransmit, hence minimizing utilization.
|
||||
|
||||
@@ -470,7 +512,7 @@ Routes are stored in sender's contact list. When you send a message the first t
|
||||
[Source](https://discord.com/channels/1343693475589263471/1343693475589263474/1350023009527664672)
|
||||
|
||||
|
||||
### 5.6. Q: what is the public key for the default public channel?
|
||||
### 5.6. Q: What is the public key for the default public channel?
|
||||
**A:** The smartphone app key is in hex:
|
||||
` 8b3387e9c5cdea6ac9e5edbaa115cd72`
|
||||
|
||||
@@ -484,7 +526,7 @@ The third character is the capital letter 'O', not zero `0`
|
||||
- Firmware repo: https://github.com/meshcore-dev/MeshCore
|
||||
|
||||
### 5.8. Q: How can I support MeshCore?
|
||||
**A:** Provide your honest feedback on GitHub and on [MeshCore Discord server](https://discord.gg/BMwCtwHj5V). Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at <https://buymeacoffee.com/ripplebiz>.
|
||||
**A:** Provide your honest feedback on GitHub and on [MeshCore Discord server](https://meshcore.gg). Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at <https://buymeacoffee.com/ripplebiz>.
|
||||
|
||||
Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase
|
||||
|
||||
@@ -544,7 +586,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](https://meshcore.co.uk/map.html)
|
||||
### 5.12. Q: How do I add a node to the [MeshCore Map](https://map.meshcore.io)
|
||||
**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`
|
||||
@@ -561,18 +603,18 @@ Below are the instructions to flash firmware onto a supported LoRa device using
|
||||
> Instructions for nRF devices like RAK, T1000-E, T114 are immediately after the ESP instructions
|
||||
|
||||
For ESP-based devices (e.g. Heltec V3) you need:
|
||||
- Download firmware file from flasher.meshcore.co.uk
|
||||
- Download firmware file from https://flasher.meshcore.io
|
||||
- Go to the web site on a browser, find the section that has the firmware up need
|
||||
- Click the Download button, right click on the file you need, for example,
|
||||
- `Heltec_V3_companion_radio_ble-v1.7.1-165fb33.bin`
|
||||
- Non-merged bin keeps the existing Bluetooth pairing database
|
||||
- `Heltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.bin`
|
||||
- Merged bin overwrites everything including the bootloader, existing Bluetooth pairing database, but keeps configurations.
|
||||
- Right click on the file name and copy the link and note it for later use here is an example: `https://flasher.meshcore.dev/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin`
|
||||
- Right click on the file name and copy the link and note it for later use here is an example: `https://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin`
|
||||
- Run:
|
||||
- `wget https://flasher.meshcore.dev/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin` to download the firmware file for your device type. or the version you need - USB, BLE, Repeater, Room Server, merged bin or non-merged bin
|
||||
- `wget https://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin` to download the firmware file for your device type. or the version you need - USB, BLE, Repeater, Room Server, merged bin or non-merged bin
|
||||
- If the above wget command only downloads a very small file (10K bytes instead of more than 100K byte, use this command instead:
|
||||
- `wget --user-agent="Mozilla/5.0" --content-disposition "https://flasher.meshcore.dev/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_usb-v1.7.1-165fb33.bin"`
|
||||
- `wget --user-agent="Mozilla/5.0" --content-disposition "https://flasher.meshcore.io/releases/download/companion-v1.7.1/Heltec_v3_companion_radio_usb-v1.7.1-165fb33.bin"`
|
||||
- Confirm the `ttyXXXX` device path on your Raspberry Pi:
|
||||
- Go to `/dev` directory, run ls command to find confirm your device path
|
||||
- They are usually `/dev/ttyUSB0` for ESP devices
|
||||
@@ -589,14 +631,14 @@ For ESP-based devices (e.g. Heltec V3) you need:
|
||||
**Instructions for nRF devices:**
|
||||
|
||||
For nRF devices (e.g. RAK, Heltec T114) you need the following:
|
||||
- Download firmware file from flasher.meshcore.co.uk
|
||||
- Download firmware file from https://flasher.meshcore.io
|
||||
- Go to the web site on a browser, find the section that has the firmware up need
|
||||
- You need the ZIP version for the adafruit flash tool (below)
|
||||
- Click the Download button, right click on the ZIP file, for example:
|
||||
- `RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip`
|
||||
- Right click on the file name and copy the link and note it for later use here is an example: `https://flasher.meshcore.dev/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip`
|
||||
- Right click on the file name and copy the link and note it for later use here is an example: `https://flasher.meshcore.io/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip`
|
||||
- Run:
|
||||
- `wget https://flasher.meshcore.dev/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip` to download the firmware file for your device type. or the version you need - USB, BLE, Repeater, Room Server, ZIP file only
|
||||
- `wget https://flasher.meshcore.io/releases/download/companion-v1.7.1/RAK_4631_companion_radio_ble-v1.7.1-165fb33.zip` to download the firmware file for your device type. or the version you need - USB, BLE, Repeater, Room Server, ZIP file only
|
||||
- Confirm the `ttyXXXX` device path on your Raspberry Pi:
|
||||
- Go to `/dev` directory, run ls command to find confirm your device path
|
||||
- They are usually `/dev/ttyACM0` for nRF devices
|
||||
@@ -616,103 +658,18 @@ From here, reference repeater and room server command line commands on MeshCore
|
||||
- https://github.com/meshcore-dev/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
|
||||
|
||||
|
||||
### 5.14. Q: Are there are projects built around MeshCore?
|
||||
### 5.14. Q: Are there projects built around MeshCore?
|
||||
|
||||
**A:** Yes. Some of them are listed below.
|
||||
**A:** Yes, there are many. MeshCore's protocol is open source using the MIT license. The MIT license and the open source protocol makes it very easy for the MeshCore community to build new firmware for radios, applications on mobile devices, map tools, and analysis tools, and integration with other projects like Home Assistant.
|
||||
|
||||
#### 5.14.1. overview
|
||||
|
||||
Some resources that by themselves give overviews about MeshCore related projects:
|
||||
|
||||
##### 5.14.1.1. awesome-meshcore
|
||||
A meta website/ git-repository collecting many projects related to MeshCore, grouped by type. See
|
||||
https://github.com/samuk/awesome-meshcore.
|
||||
|
||||
#### 5.14.2. programming libraries, command line software
|
||||
|
||||
##### 5.14.2.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.letsmesh.net/
|
||||
https://github.com/Cisien/meshcoretomqtt
|
||||
https://github.com/Andrew-a-g/meshcoretomqtt
|
||||
|
||||
##### 5.14.2.2. MeshCore for Home Assistant
|
||||
A custom Home Assistant integration for MeshCore mesh radio nodes. It allows you to monitor and control MeshCore nodes via USB, BLE, or TCP connections.
|
||||
https://github.com/awolden/meshcore-ha
|
||||
|
||||
##### 5.14.2.3. Python MeshCore
|
||||
Bindings to access your MeshCore companion radio nodes in python.
|
||||
https://github.com/fdlamotte/meshcore_py
|
||||
|
||||
##### 5.14.2.4. meshcore-cli
|
||||
CLI interface to MeshCore companion radio over BLE, TCP, or serial. Uses Python MeshCore above.
|
||||
https://github.com/fdlamotte/meshcore-cli
|
||||
|
||||
##### 5.14.2.5. meshcore.js
|
||||
A JavaScript library for interacting with a MeshCore device running the companion radio firmware
|
||||
https://github.com/liamcottle/meshcore.js
|
||||
|
||||
##### 5.14.2.6. pyMC_core
|
||||
pyMC_Core is a Python port of MeshCore, designed for Raspberry Pi and similar hardware, it talks to LoRa modules over SPI.
|
||||
https://github.com/rightup/pyMC_core
|
||||
|
||||
##### 5.14.2.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.letsmesh.net/packets).
|
||||
https://github.com/michaelhart/meshcore-decoder
|
||||
|
||||
##### 5.14.2.8. meshcore-pi
|
||||
meshcore-pi is another Python port of MeshCore, designed for Raspberry Pi and similar hardware, it talks to LoRa modules over SPI or GPIO.
|
||||
https://github.com/brianwiddas/meshcore-pi
|
||||
|
||||
##### 5.14.2.9. pyMC_Repeater
|
||||
pyMC_Repeater is a repeater daemon in Python built on top of the [`pymc_core`](#5146-pymc_core) library.
|
||||
https://github.com/rightup/pyMC_Repeater
|
||||
|
||||
##### 5.14.2.10. MeshCore map auto uploader
|
||||
A Node.js software that will upload every repeater or room server to [map.meshcore.dev](https://map.meshcore.dev/) when a connected companion hears new advert.
|
||||
https://github.com/recrof/map.meshcore.dev-uploader
|
||||
|
||||
#### 5.14.3. apps, graphical software
|
||||
|
||||
##### 5.14.3.1. meshcore-open
|
||||
Open Source companion app for Android, iOS, GNU/Linux (and maybe other Unixes), Windows, macOS, chromium-based browsers.
|
||||
https://github.com/zjs81/meshcore-open
|
||||
|
||||
#### 5.14.4. firmwares
|
||||
|
||||
##### 5.14.4.1. MeshCore-Cardputer-ADV
|
||||
Standalone client firmware for the "[M5Stack Cardputer ADV](https://docs.m5stack.com/en/core/Cardputer-Adv)" with the "[M5Stack Cap LoRa-1262](https://docs.m5stack.com/en/cap/Cap_LoRa-1262)" module.
|
||||
|
||||
There are two variants:
|
||||
|
||||
* https://github.com/Stachugit/MeshCore-Cardputer-ADV,
|
||||
* https://github.com/sosprz/meshcore-cardputer-adv.
|
||||
|
||||
##### 5.14.4.2. LunarCore
|
||||
Multi-protocol mesh firmware for ESP32-S3 LoRa devices (MeshCore, Meshtastic, RNode/KISS (Reticulum)). Protocol is auto-detected from the first bytes over serial or BLE.
|
||||
https://github.com/STCisGOOD/lunarcore
|
||||
|
||||
##### 5.14.4.3. MC-Term
|
||||
(Soon to be) Open Source companion firmware for [LilyGO T-Deck (Plus)](https://lilygo.cc/en-us/products/t-deck-plus-1) and [Seeed Studio SenseCap Indicator (TFT / D1Pro)](https://www.seeedstudio.com/SenseCAP-Indicator-D1Pro-p-5644.html), that can be used both standalone and together with a companion app.
|
||||
https://github.com/dabeani/meshcore
|
||||
|
||||
##### 5.14.4.4. Meck
|
||||
Companion firmware for [LilyGo T-Deck Pro](https://lilygo.cc/products/t-deck-pro) that allows standalone operation and connection to a companion app via Bluetooth Low Energy (BLE).
|
||||
https://github.com/pelgraine/Meck
|
||||
|
||||
##### 5.14.4.5. Meshcore for Wio Tracker L1 Pro
|
||||
Companion firmware for [Seeed Studio Wio Tracker L1 Pro](https://www.seeedstudio.com/Wio-Tracker-L1-Pro-p-6454.html) with specific UI adjustments that can be used standalone.
|
||||
https://github.com/sosprz/Meshcore-Wio-Tracker-L1-Pro
|
||||
|
||||
#### 5.14.5. online services
|
||||
|
||||
*(None yet listed here. See [overview ressources](#5141-overview).)*
|
||||
As new MeshCore community projects become available on a weekly basis, we have stopped tracking them here in this FAQ. [samuk](https://github.com/samuk) maintains a very exhaustive list of MeshCore community project at https://github.com/samuk/awesome-meshcore/blob/main/README.md. samuk accepts PRs and merges them regularly.
|
||||
|
||||
|
||||
### 5.15. Q: Are there client applications for Windows or Mac?
|
||||
**A:** Yes, the same iOS and Android client is also available for Windows and Intel Mac (sorry, not available for ARM-based Mac yet). You can find them together with the Android APK here:
|
||||
**A:** Yes, the same iOS and Android client is also available for Windows and Mac. You can find them together with the Android APK here:
|
||||
https://files.liamcottle.net/MeshCore
|
||||
|
||||
Both the Windows and Intel Mac versions of the client app are fully unlocked and are free to use.
|
||||
Both the Windows and Mac versions of the client app are fully unlocked and are free to use.
|
||||
|
||||
### 5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?
|
||||
|
||||
@@ -759,18 +716,18 @@ You can get the epoch time on <https://www.epochconverter.com/> and use it to se
|
||||
- For RAK, click the reset button **TWICE**
|
||||
- For T1000-e, quickly disconnect and reconnect the magnetic side of the cable from the device **TWICE**
|
||||
- For Heltec T114, click the reset button **TWICE** (the bottom button)
|
||||
- For Xiao nRF52, click the reset button once. If that doesn't work, quickly double click the reset button twice. If that doesn't work, disconnection the board from your PC and reconnect again ([seeed studio wiki](https://wiki.seeedstudio.com/XIAO_BLE/#access-the-swd-pins-for-debugging-and-reflashing-bootloader))
|
||||
- For Xiao nRF52, click the reset button once. If that doesn't work, quickly double click the reset button twice. If that doesn't work, disconnect the board from your PC and reconnect again ([seeed studio wiki](https://wiki.seeedstudio.com/XIAO_BLE/#access-the-swd-pins-for-debugging-and-reflashing-bootloader))
|
||||
5. A new folder will appear on your computer's desktop
|
||||
6. Download the `flash_erase*.uf2` file for your device on flasher.meshcore.co.uk
|
||||
6. Download the `flash_erase*.uf2` file for your device on https://flasher.meshcore.io
|
||||
- RAK WisBlock and Heltec T114: `Flash_erase-nRF32_softdevice_v6.uf2`
|
||||
- Seeed Studio Xiao nRF52 WIO: `Flash_erase-nRF52_softdevice_v7.uf2`
|
||||
8. drag and drop the uf2 file for your device to the root of the new folder
|
||||
9. Wait for the copy to complete. You might get an error dialog, you can ignore it
|
||||
10. Go to https://flasher.meshcore.co.uk/, click `Console` and select the serial port for your connected device
|
||||
10. Go to https://flasher.meshcore.io, click `Console` and select the serial port for your connected device
|
||||
11. In the console, press enter. Your flash should now be erased
|
||||
12. You may now flash the latest MeshCore firmware onto your device
|
||||
|
||||
Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by hold down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on flasher.meshcore.co.uk
|
||||
Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by hold down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on https://flasher.meshcore.io
|
||||
|
||||
### 6.8. Q: WebFlasher fails on Linux with failed to open
|
||||
|
||||
@@ -783,12 +740,12 @@ Allow the browser user on it:
|
||||
---
|
||||
## 7. Other Questions:
|
||||
|
||||
### 7.1. Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?
|
||||
### 7.1. Q: How to update nRF (RAK, T114, Seed XIAO) companion, repeater and room server firmware over the air using the new simpler DFU app?
|
||||
|
||||
**A:** The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:
|
||||
|
||||
1. Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for `nrf dfu`, the app's full name is `nRF Device Firmware Update`
|
||||
2. On flasher.meshcore.co.uk, download the **ZIP** version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao)
|
||||
2. On https://flasher.meshcore.io, download the **ZIP** version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao)
|
||||
3. From the MeshCore app, login remotely to the repeater you want to update with admin privilege
|
||||
4. Go to the Command Line tab, type `start ota` and hit enter.
|
||||
5. you should see `OK` to confirm the repeater device is now in OTA mode
|
||||
@@ -798,8 +755,14 @@ Allow the browser user on it:
|
||||
10. Select the device you want to update. If the device you want to update is not on the list, try enabling`OTA` on the device again
|
||||
11. If the device is not found, enable `Force Scanning` in the DFU app
|
||||
12. Tab the `Upload` to begin OTA update
|
||||
13. If it fails, try turning off and on Bluetooth on your phone. If that doesn't work, try rebooting your phone.
|
||||
14. Wait for the update to complete. It can take a few minutes.
|
||||
13. If it fails, try turning off and on Bluetooth on your phone. If that doesn't work, try rebooting your phone. If you keep getting failures at the "Enabling Bootloader" step, try forgetting the NRF board in your IOS or Andriod device's bluetooth settings and re-pair it through the DFU app.
|
||||
14. Wait for the update to complete. It can take a few minutes.
|
||||
15. It is strongly recommended that you install and use the OTAFIX bootloader at https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX.
|
||||
16. To update a companion node over OTA, it must be running companion firmware v1.15 or greater.
|
||||
17. Please see the Meshcore Blog for additional information on OTA firmware flashing:
|
||||
- https://blog.meshcore.io/2026/04/06/otafix-bootloader
|
||||
- https://blog.meshcore.io/2026/04/02/nrf-ota-update
|
||||
|
||||
|
||||
#### 7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?
|
||||
**A:** You can flash this safer bootloader to the Wio Tracker L1 Pro
|
||||
@@ -811,7 +774,7 @@ After this bootloader is flashed onto the device, you can trigger over the air u
|
||||
### 7.2. Q: How to update ESP32-based devices over the air?
|
||||
|
||||
**A:** For ESP32-based devices (e.g. Heltec V3):
|
||||
1. On flasher.meshcore.co.uk, download the **non-merged** version of the firmware for your ESP32 device (e.g. `Heltec_v3_repeater-v1.6.2-4449fd3.bin`, no `"merged"` in the file name)
|
||||
1. On https://flasher.meshcore.io, download the **non-merged** version of the firmware for your ESP32 device (e.g. `Heltec_v3_repeater-v1.6.2-4449fd3.bin`, no `"merged"` in the file name)
|
||||
2. From the MeshCore app, login remotely to the repeater you want to update with admin privilege
|
||||
4. Go to the Command Line tab, type `start ota` and hit enter.
|
||||
5. you should see `OK` to confirm the repeater device is now in OTA mode
|
||||
@@ -833,7 +796,7 @@ Currently, the following boards are supported:
|
||||
- Seeed Studio Wio Tracker L1
|
||||
- Seeed Studio XIAO nRF52840 BLE
|
||||
- Seeed Studio XIAO nRF52840 BLE SENSE
|
||||
- RAK 4631 (See note)
|
||||
- RAK 4631
|
||||
- RAK WisMesh Tag (new 28/11/2025)
|
||||
|
||||
### 7.4. Q: are the MeshCore logo and font available?
|
||||
@@ -863,20 +826,21 @@ Edit WIFI_SSID and WIFI_PWD in `./variants/heltec_v3/platformio.ini` and then fl
|
||||
|
||||
### 7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with a EByte E22-900M30S or a E22-900M33S module, what should their transmit power be set to?
|
||||
**A:**
|
||||
For companion radios, you can set these radios' transmit power in the smartphone app. For repeater and room server radios, you can set their transmit power using the command line command `set tx`. You can get their current value using command line comand `get tx`
|
||||
For companion radios, you can set these radios' transmit power in the smartphone app. For repeater and room server radios, you can set their transmit power using the command line command `set tx`. You can get their current value using command line command `get tx`
|
||||
|
||||
|
||||
> ### ⚠️ **WARNING: Set these values at your own risk. Incorrect power settings can permanently damage your radio hardware.**
|
||||
⚠️ **WARNING: Set these values at your own risk. Incorrect power settings can permanently damage your radio hardware.**
|
||||
|
||||
| Device / Model | Region / Description | In-App Setting (dBm) | Target Radio Output | Notes |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **Station G2** <br> [Reference](https://wiki.uniteng.com/en/meshtastic/station-g2) | US915 Max Output | 19 dBm | 36.5 dBm (4.46W) | |
|
||||
| | US915 Recommended Max | 16 dBm | 35 dBm (3.16W) | 1dB compression point |
|
||||
| | EU868 Recommended Max | 15 dBm | 34.5 dBm (2.82W) | 1dB compression point |
|
||||
| | US915 1W Output | 10 dBm | 1W | |
|
||||
| | EU868 1W Output | 9 dBm | 1W | |
|
||||
| **Ikoka Stick E22-900M30S** | 1W Model | 19 dBm | 1W | **DO NOT EXCEED** (Risk of burn out) |
|
||||
| **Ikoka Stick E22-900M33S** | 2W Model | 9 dBm | 2W | **DO NOT EXCEED** (Risk of burn out) |
|
||||
| **Heltec V4** | Standard Output | 10 dBm | 22 dBm | |
|
||||
| | High Output | 22 dBm | 28 dBm | |
|
||||
| | US915 Max at 1dB compression point | 16 dBm | 35 dBm (3.16W) | 1dB compression point |
|
||||
| | EU868 Max at 1dB compression point | 15 dBm | 34.5 dBm (2.82W) | 1dB compression point |
|
||||
| | US915 1W Output | 10 dBm | 1W | Refer to your local government's requirements |
|
||||
| | EU868 1W Output | 9 dBm | 1W |Refer to your local government's requirements |
|
||||
| **Ikoka Stick E22-900M30S** | 1W Model | 19 dBm | 1W | **DO NOT EXCEED** (Risk of burn out) [data sheet](https://www.cdebyte.com/pdf-down.aspx?id=4216) |
|
||||
| **Ikoka Stick E22-900M33S** | 2W Model | 9 dBm | 2W | **DO NOT EXCEED** (Risk of burn out) [data sheet](https://www.cdebyte.com/pdf-down.aspx?id=4216) Refer to your local government's requirements |
|
||||
| **Heltec V4** | Standard Output | 10 dBm | 22 dBm (~0.15W) | |
|
||||
| | High Output | 22 dBm | 28 dBm (~0.5W to 0.6W) | |
|
||||
|
||||
---
|
||||
|
||||
@@ -156,6 +156,7 @@ Response codes use the high-bit convention: `response = command | 0x80`. Generic
|
||||
| MacFailed | `0x04` | MAC verification failed |
|
||||
| UnknownCmd | `0x05` | Unknown sub-command |
|
||||
| EncryptFailed | `0x06` | Encryption failed |
|
||||
| TxBusy | `0x07` | Transmit busy |
|
||||
|
||||
### Unsolicited Events
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ Once you have a working app/project, you need to be able to demonstrate it exist
|
||||
| Data-Type range | App name | Contact |
|
||||
|-----------------|-----------------------------|------------------------------------------------------|
|
||||
| 0000 - 00FF | -reserved for internal use- | |
|
||||
| 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open |
|
||||
| FF00 - FFFF | -reserved for testing/dev- | |
|
||||
|
||||
(add rows, inside the range 0100 - FEFF for custom apps)
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
#define CMD_SEND_CHANNEL_DATA 62
|
||||
#define CMD_SET_DEFAULT_FLOOD_SCOPE 63
|
||||
#define CMD_GET_DEFAULT_FLOOD_SCOPE 64
|
||||
#define CMD_SEND_RAW_PACKET 65
|
||||
|
||||
// Stats sub-types for CMD_GET_STATS
|
||||
#define STATS_TYPE_CORE 0
|
||||
@@ -495,19 +496,27 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3
|
||||
|
||||
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||
// TODO: dynamic send_scope, depending on recipient and current 'home' Region
|
||||
TransportKey default_scope;
|
||||
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
|
||||
if (send_unscoped) {
|
||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped
|
||||
} else {
|
||||
TransportKey default_scope;
|
||||
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
|
||||
|
||||
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
|
||||
sendFloodScoped(*scope, pkt, delay_millis);
|
||||
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
|
||||
sendFloodScoped(*scope, pkt, delay_millis);
|
||||
}
|
||||
}
|
||||
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||
// TODO: have per-channel send_scope
|
||||
TransportKey default_scope;
|
||||
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
|
||||
if (send_unscoped) {
|
||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped
|
||||
} else {
|
||||
TransportKey default_scope;
|
||||
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
|
||||
|
||||
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
|
||||
sendFloodScoped(*scope, pkt, delay_millis);
|
||||
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
|
||||
sendFloodScoped(*scope, pkt, delay_millis);
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
@@ -856,6 +865,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
dirty_contacts_expiry = 0;
|
||||
memset(advert_paths, 0, sizeof(advert_paths));
|
||||
memset(send_scope.key, 0, sizeof(send_scope.key));
|
||||
send_unscoped = false;
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
@@ -951,8 +961,8 @@ void MyMesh::begin(bool has_display) {
|
||||
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
|
||||
_store->loadChannels(this);
|
||||
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_set_tx_power(_prefs.tx_power_dbm);
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||
@@ -973,9 +983,13 @@ struct FreqRange {
|
||||
};
|
||||
|
||||
static FreqRange repeat_freq_ranges[] = {
|
||||
#ifdef ALLOWED_REPEAT_FREQ_RANGE
|
||||
ALLOWED_REPEAT_FREQ_RANGE
|
||||
#else
|
||||
{ 433000, 433000 },
|
||||
{ 869000, 869000 },
|
||||
{ 918000, 918000 }
|
||||
#endif
|
||||
};
|
||||
|
||||
bool MyMesh::isValidClientRepeatFreq(uint32_t f) const {
|
||||
@@ -1374,7 +1388,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
_prefs.client_repeat = repeat;
|
||||
savePrefs();
|
||||
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf,
|
||||
(uint32_t)cr);
|
||||
|
||||
@@ -1391,7 +1405,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
} else {
|
||||
_prefs.tx_power_dbm = power;
|
||||
savePrefs();
|
||||
radio_set_tx_power(_prefs.tx_power_dbm);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
writeOKFrame();
|
||||
}
|
||||
} else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) {
|
||||
@@ -1885,10 +1899,14 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
}
|
||||
} else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE_KEY && len >= 2 && cmd_frame[1] == 0) {
|
||||
if (len >= 2 + 16) {
|
||||
memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set curr scope TransportKey
|
||||
memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set scope override TransportKey
|
||||
} else {
|
||||
memset(send_scope.key, 0, sizeof(send_scope.key)); // set scope to null
|
||||
memset(send_scope.key, 0, sizeof(send_scope.key)); // reset scope override
|
||||
}
|
||||
send_unscoped = false;
|
||||
writeOKFrame();
|
||||
} else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE_KEY && len >= 2 && cmd_frame[1] == 1) { // ver 12+
|
||||
send_unscoped = true;
|
||||
writeOKFrame();
|
||||
} else if (cmd_frame[0] == CMD_SET_DEFAULT_FLOOD_SCOPE && len >= 1) {
|
||||
if (len >= 1+31+16) {
|
||||
@@ -1946,6 +1964,19 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
memcpy(&out_frame[i], &r->upper_freq, 4); i += 4;
|
||||
}
|
||||
_serial->writeFrame(out_frame, i);
|
||||
} else if (cmd_frame[0] == CMD_SEND_RAW_PACKET && len >= 4) {
|
||||
auto pkt = obtainNewPacket();
|
||||
if (pkt) {
|
||||
uint8_t priority = cmd_frame[1];
|
||||
if (tryParsePacket(pkt, &cmd_frame[2], len - 2)) {
|
||||
sendPacket(pkt, priority, 0);
|
||||
writeOKFrame();
|
||||
} else {
|
||||
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
||||
}
|
||||
} else {
|
||||
writeErrFrame(ERR_CODE_TABLE_FULL);
|
||||
}
|
||||
} else {
|
||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
|
||||
MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
#include "AbstractUITask.h"
|
||||
|
||||
/*------------ Frame Protocol --------------*/
|
||||
#define FIRMWARE_VER_CODE 11
|
||||
#define FIRMWARE_VER_CODE 12
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "20 Mar 2026"
|
||||
#define FIRMWARE_BUILD_DATE "19 Apr 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.14.1"
|
||||
#define FIRMWARE_VERSION "v1.15.0"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
@@ -215,6 +215,7 @@ private:
|
||||
uint32_t _active_ble_pin;
|
||||
bool _iter_started;
|
||||
bool _cli_rescue;
|
||||
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
|
||||
char cli_command[80];
|
||||
uint8_t app_target_ver;
|
||||
uint8_t *sign_data;
|
||||
|
||||
@@ -105,6 +105,12 @@ void halt() {
|
||||
while (1) ;
|
||||
}
|
||||
|
||||
/* WIFI RECONNECT TRACKERS */
|
||||
#if defined(ESP32) && defined(WIFI_SSID)
|
||||
bool wifi_needs_reconnect = false;
|
||||
unsigned long last_wifi_reconnect_attempt = 0;
|
||||
#endif
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
@@ -125,7 +131,7 @@ void setup() {
|
||||
|
||||
if (!radio_init()) { halt(); }
|
||||
|
||||
fast_rng.begin(radio_get_rng_seed());
|
||||
fast_rng.begin(radio_driver.getRngSeed());
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
InternalFS.begin();
|
||||
@@ -195,6 +201,18 @@ void setup() {
|
||||
|
||||
#ifdef WIFI_SSID
|
||||
board.setInhibitSleep(true); // prevent sleep when WiFi is active
|
||||
WiFi.setAutoReconnect(true);
|
||||
|
||||
WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){
|
||||
if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) {
|
||||
WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect...");
|
||||
wifi_needs_reconnect = true;
|
||||
} else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
|
||||
WIFI_DEBUG_PRINTLN("WiFi connected successfully!");
|
||||
wifi_needs_reconnect = false;
|
||||
}
|
||||
});
|
||||
|
||||
WiFi.begin(WIFI_SSID, WIFI_PWD);
|
||||
serial_interface.begin(TCP_PORT);
|
||||
#elif defined(BLE_PIN_CODE)
|
||||
@@ -229,4 +247,14 @@ void loop() {
|
||||
ui_task.loop();
|
||||
#endif
|
||||
rtc_clock.tick();
|
||||
|
||||
#if defined(ESP32) && defined(WIFI_SSID)
|
||||
// Safely attempt to reconnect every 10 seconds if flagged
|
||||
if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) {
|
||||
WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect...");
|
||||
WiFi.disconnect();
|
||||
WiFi.reconnect();
|
||||
last_wifi_reconnect_attempt = millis();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -57,13 +57,21 @@ public:
|
||||
int logoWidth = 128;
|
||||
display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
|
||||
|
||||
// meshcore website
|
||||
const char* website = "https://meshcore.io";
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
uint16_t websiteWidth = display.getTextWidth(website);
|
||||
display.setCursor((display.width() - websiteWidth) / 2, 22);
|
||||
display.print(website);
|
||||
|
||||
// version info
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(2);
|
||||
display.drawTextCentered(display.width()/2, 22, _version_info);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width()/2, 35, _version_info);
|
||||
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width()/2, 42, FIRMWARE_BUILD_DATE);
|
||||
display.drawTextCentered(display.width()/2, 48, FIRMWARE_BUILD_DATE);
|
||||
|
||||
return 1000;
|
||||
}
|
||||
@@ -146,7 +154,7 @@ class HomeScreen : public UIScreen {
|
||||
bool sensors_scroll = false;
|
||||
int sensors_scroll_offset = 0;
|
||||
int next_sensors_refresh = 0;
|
||||
|
||||
|
||||
void refresh_sensors() {
|
||||
if (millis() > next_sensors_refresh) {
|
||||
sensors_lpp.reset();
|
||||
@@ -170,7 +178,7 @@ class HomeScreen : public UIScreen {
|
||||
|
||||
public:
|
||||
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
|
||||
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
|
||||
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
|
||||
_shutdown_init(false), sensors_lpp(200) { }
|
||||
|
||||
void poll() override {
|
||||
@@ -213,7 +221,7 @@ public:
|
||||
IPAddress ip = WiFi.localIP();
|
||||
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, 54, tmp);
|
||||
display.drawTextCentered(display.width() / 2, 54, tmp);
|
||||
#endif
|
||||
if (_task->hasConnection()) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
@@ -241,10 +249,10 @@ public:
|
||||
} else {
|
||||
sprintf(tmp, "%dh", secs / (60*60));
|
||||
}
|
||||
|
||||
|
||||
int timestamp_width = display.getTextWidth(tmp);
|
||||
int max_name_width = display.width() - timestamp_width - 1;
|
||||
|
||||
|
||||
char filtered_recent_name[sizeof(a->name)];
|
||||
display.translateUTF8ToBlocks(filtered_recent_name, a->name, sizeof(filtered_recent_name));
|
||||
display.drawTextEllipsized(0, y, max_name_width, filtered_recent_name);
|
||||
@@ -310,7 +318,7 @@ public:
|
||||
display.drawTextRightAlign(display.width()-1, y, buf);
|
||||
y = y + 12;
|
||||
display.drawTextLeftAlign(0, y, "pos");
|
||||
sprintf(buf, "%.4f %.4f",
|
||||
sprintf(buf, "%.4f %.4f",
|
||||
nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.);
|
||||
display.drawTextRightAlign(display.width()-1, y, buf);
|
||||
y = y + 12;
|
||||
@@ -567,6 +575,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
#ifdef PIN_BUZZER
|
||||
buzzer.begin();
|
||||
buzzer.quiet(_node_prefs->buzzer_quiet);
|
||||
buzzer.startup();
|
||||
#endif
|
||||
|
||||
#ifdef PIN_VIBRATION
|
||||
@@ -741,7 +750,7 @@ void UITask::loop() {
|
||||
#endif
|
||||
#if defined(PIN_USER_BTN_ANA)
|
||||
if (abs(millis() - _analogue_pin_read_millis) > 10) {
|
||||
ev = analog_btn.check();
|
||||
int ev = analog_btn.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_NEXT);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
@@ -878,7 +887,7 @@ bool UITask::getGPSState() {
|
||||
return !strcmp(_sensors->getSettingValue(i), "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
#ifdef PIN_BUZZER
|
||||
buzzer.begin();
|
||||
buzzer.quiet(_node_prefs->buzzer_quiet);
|
||||
buzzer.startup();
|
||||
#endif
|
||||
|
||||
// Initialize digital button if available
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
|
||||
|
||||
#ifndef STATUS_BAR_SCROLL_MS
|
||||
#define STATUS_BAR_SCROLL_MS 80
|
||||
#endif
|
||||
|
||||
#ifndef STATUS_BAR_SEPARATOR
|
||||
#define STATUS_BAR_SEPARATOR " | "
|
||||
#endif
|
||||
|
||||
#ifndef STATUS_BAR_UPDATE_MS
|
||||
#define STATUS_BAR_UPDATE_MS 2000 // rebuild status string every 2s
|
||||
#endif
|
||||
|
||||
class ScrollingStatusBar {
|
||||
char _status[160];
|
||||
int _text_width;
|
||||
int _scroll_x;
|
||||
int _display_width;
|
||||
unsigned long _next_scroll;
|
||||
unsigned long _next_update;
|
||||
bool _needs_redraw;
|
||||
|
||||
// cached state for change detection
|
||||
char _last_name[32];
|
||||
uint16_t _last_batt_mv;
|
||||
bool _last_buzzer_quiet;
|
||||
bool _last_gps_on;
|
||||
bool _last_ble_on;
|
||||
|
||||
public:
|
||||
ScrollingStatusBar() : _text_width(0), _scroll_x(0), _display_width(72),
|
||||
_next_scroll(0), _next_update(0), _needs_redraw(true),
|
||||
_last_batt_mv(0), _last_buzzer_quiet(false),
|
||||
_last_gps_on(false), _last_ble_on(false) {
|
||||
_status[0] = 0;
|
||||
_last_name[0] = 0;
|
||||
}
|
||||
|
||||
void begin(int display_width) {
|
||||
_display_width = display_width;
|
||||
_scroll_x = 0;
|
||||
_next_scroll = 0;
|
||||
_next_update = 0;
|
||||
}
|
||||
|
||||
// Call periodically to update the status string content.
|
||||
// Only rebuilds if values have changed or update interval has elapsed.
|
||||
void update(DisplayDriver& display, const char* node_name, uint16_t batt_millivolts,
|
||||
bool buzzer_quiet, bool gps_on, bool ble_on) {
|
||||
|
||||
bool changed = (batt_millivolts != _last_batt_mv)
|
||||
|| (buzzer_quiet != _last_buzzer_quiet)
|
||||
|| (gps_on != _last_gps_on)
|
||||
|| (ble_on != _last_ble_on)
|
||||
|| (strcmp(node_name, _last_name) != 0);
|
||||
|
||||
if (!changed) return;
|
||||
|
||||
// cache current values
|
||||
strncpy(_last_name, node_name, sizeof(_last_name) - 1);
|
||||
_last_name[sizeof(_last_name) - 1] = 0;
|
||||
_last_batt_mv = batt_millivolts;
|
||||
_last_buzzer_quiet = buzzer_quiet;
|
||||
_last_gps_on = gps_on;
|
||||
_last_ble_on = ble_on;
|
||||
|
||||
float volts = batt_millivolts / 1000.0f;
|
||||
|
||||
snprintf(_status, sizeof(_status),
|
||||
"%s" STATUS_BAR_SEPARATOR
|
||||
"%.2fV" STATUS_BAR_SEPARATOR
|
||||
"BUZ:%s" STATUS_BAR_SEPARATOR
|
||||
"GPS:%s" STATUS_BAR_SEPARATOR
|
||||
"BLE:%s"
|
||||
" - ", // trailing gap before the text loops
|
||||
node_name,
|
||||
volts,
|
||||
buzzer_quiet ? "OFF" : "ON",
|
||||
gps_on ? "ON" : "OFF",
|
||||
ble_on ? "ON" : "OFF"
|
||||
);
|
||||
|
||||
display.setTextSize(1);
|
||||
_text_width = display.getTextWidth(_status);
|
||||
_next_update = millis() + STATUS_BAR_UPDATE_MS;
|
||||
_needs_redraw = true;
|
||||
}
|
||||
|
||||
// Returns true if the status bar needs a redraw this frame.
|
||||
bool needsRedraw() {
|
||||
if (_text_width <= _display_width) return _needs_redraw; // static, no scrolling
|
||||
return millis() >= _next_scroll;
|
||||
}
|
||||
|
||||
// Render the status bar via DisplayDriver.
|
||||
// U8g2 full-buffer mode clips to display bounds automatically,
|
||||
// and the font height stays within STATUS_BAR_HEIGHT, so no
|
||||
// explicit clip window is needed.
|
||||
void render(DisplayDriver& display) {
|
||||
if (_status[0] == 0) return;
|
||||
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
|
||||
// if (_needs_redraw) {
|
||||
// _text_width = display.getTextWidth(_status);
|
||||
// }
|
||||
|
||||
// static text: no scrolling needed
|
||||
if (_text_width <= _display_width) {
|
||||
display.setCursor(0, 0);
|
||||
display.print(_status);
|
||||
_needs_redraw = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int x = _scroll_x;
|
||||
do {
|
||||
display.setCursor(x, 0);
|
||||
display.print(_status);
|
||||
x += _text_width;
|
||||
} while (x < _display_width);
|
||||
|
||||
|
||||
// advance scroll position
|
||||
_scroll_x--;
|
||||
if (_scroll_x <= -_text_width) _scroll_x = 0;
|
||||
|
||||
_next_scroll = millis() + STATUS_BAR_SCROLL_MS;
|
||||
_needs_redraw = false;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,819 @@
|
||||
#include "UITask.h"
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include "../MyMesh.h"
|
||||
#include "target.h"
|
||||
#include "u8g2_icons.h"
|
||||
|
||||
#ifdef WIFI_SSID
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#ifndef AUTO_OFF_MILLIS
|
||||
#define AUTO_OFF_MILLIS 15000 // 15 seconds
|
||||
#endif
|
||||
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
|
||||
|
||||
#ifdef PIN_STATUS_LED
|
||||
#define LED_ON_MILLIS 20
|
||||
#define LED_ON_MSG_MILLIS 200
|
||||
#define LED_CYCLE_MILLIS 4000
|
||||
#endif
|
||||
|
||||
#define LONG_PRESS_MILLIS 1200
|
||||
|
||||
#ifndef UI_RECENT_LIST_SIZE
|
||||
#define UI_RECENT_LIST_SIZE 4
|
||||
#endif
|
||||
|
||||
#if UI_HAS_JOYSTICK
|
||||
#define PRESS_LABEL "press Enter"
|
||||
#else
|
||||
#define PRESS_LABEL "long press"
|
||||
#endif
|
||||
|
||||
class SplashScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
unsigned long dismiss_after;
|
||||
unsigned long version_after;
|
||||
char _version_info[12];
|
||||
|
||||
public:
|
||||
SplashScreen(UITask* task) : _task(task) {
|
||||
// strip off dash and commit hash by changing dash to null terminator
|
||||
// e.g: v1.2.3-abcdef -> v1.2.3
|
||||
const char *ver = FIRMWARE_VERSION;
|
||||
const char *dash = strchr(ver, '-');
|
||||
|
||||
int len = dash ? dash - ver : strlen(ver);
|
||||
if (len >= sizeof(_version_info)) len = sizeof(_version_info) - 1;
|
||||
memcpy(_version_info, ver, len);
|
||||
_version_info[len] = 0;
|
||||
|
||||
version_after = millis() + BOOT_SCREEN_MILLIS / 2;
|
||||
dismiss_after = millis() + BOOT_SCREEN_MILLIS;
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
if (millis() < version_after) {
|
||||
// meshcore logo
|
||||
display.setColor(DisplayDriver::BLUE);
|
||||
int logoWidth = 72;
|
||||
display.drawXbm(0, 0, meshcore_logo, 72, 36);
|
||||
} else {
|
||||
|
||||
// meshcore website
|
||||
const char* website = "meshcore.io";
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
uint16_t websiteWidth = display.getTextWidth(website);
|
||||
display.setCursor((display.width() - websiteWidth) / 2, 9);
|
||||
display.print(website);
|
||||
|
||||
// version info
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width()/2, 18, _version_info);
|
||||
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width()/2, 27, FIRMWARE_BUILD_DATE);
|
||||
}
|
||||
return 1000;
|
||||
}
|
||||
|
||||
void poll() override {
|
||||
if (millis() >= dismiss_after) {
|
||||
_task->gotoHomeScreen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class HomeScreen : public UIScreen {
|
||||
enum HomePage {
|
||||
FIRST,
|
||||
RECENT,
|
||||
RADIO,
|
||||
BLUETOOTH,
|
||||
ADVERT,
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
GPS,
|
||||
#endif
|
||||
#if UI_SENSORS_PAGE == 1
|
||||
SENSORS,
|
||||
#endif
|
||||
SHUTDOWN,
|
||||
Count // keep as last
|
||||
};
|
||||
|
||||
UITask* _task;
|
||||
mesh::RTCClock* _rtc;
|
||||
SensorManager* _sensors;
|
||||
NodePrefs* _node_prefs;
|
||||
uint8_t _page;
|
||||
bool _shutdown_init;
|
||||
AdvertPath recent[UI_RECENT_LIST_SIZE];
|
||||
|
||||
CayenneLPP sensors_lpp;
|
||||
int sensors_nb = 0;
|
||||
bool sensors_scroll = false;
|
||||
int sensors_scroll_offset = 0;
|
||||
int next_sensors_refresh = 0;
|
||||
|
||||
void refresh_sensors() {
|
||||
if (millis() > next_sensors_refresh) {
|
||||
sensors_lpp.reset();
|
||||
sensors_nb = 0;
|
||||
sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
sensors.querySensors(0xFF, sensors_lpp);
|
||||
LPPReader reader (sensors_lpp.getBuffer(), sensors_lpp.getSize());
|
||||
uint8_t channel, type;
|
||||
while(reader.readHeader(channel, type)) {
|
||||
reader.skipData(type);
|
||||
sensors_nb ++;
|
||||
}
|
||||
sensors_scroll = sensors_nb > UI_RECENT_LIST_SIZE;
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec
|
||||
#else
|
||||
next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
|
||||
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
|
||||
_shutdown_init(false), sensors_lpp(200) { }
|
||||
|
||||
void poll() override {
|
||||
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
|
||||
_task->shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
char tmp[80];
|
||||
|
||||
if (_page == HomePage::FIRST) {
|
||||
// // node name
|
||||
// display.setTextSize(1);
|
||||
// display.setColor(DisplayDriver::GREEN);
|
||||
// char filtered_name[sizeof(_node_prefs->node_name)];
|
||||
// display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
|
||||
// display.setCursor(0, 0);
|
||||
// display.print(filtered_name);
|
||||
|
||||
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setTextSize(2);
|
||||
sprintf(tmp, "MSG: %d", _task->getMsgCount());
|
||||
display.setCursor(0, 10);
|
||||
display.print(tmp);
|
||||
|
||||
sprintf(tmp, "BATT: %.2fV", _task->getCachedBattMV() / 1000.0f);
|
||||
display.setCursor(0, 19);
|
||||
display.print(tmp);
|
||||
|
||||
#ifdef WIFI_SSID
|
||||
IPAddress ip = WiFi.localIP();
|
||||
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, 54, tmp);
|
||||
#endif
|
||||
if (_task->hasConnection()) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, display.height()-8, "< Connected >");
|
||||
|
||||
} else if (the_mesh.getBLEPin() != 0) { // BT pin
|
||||
display.setColor(DisplayDriver::RED);
|
||||
display.setTextSize(2);
|
||||
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
|
||||
display.drawTextCentered(display.width() / 2, display.height()-8, tmp);
|
||||
}
|
||||
} else if (_page == HomePage::RECENT) {
|
||||
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
int y = 8;
|
||||
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) {
|
||||
auto a = &recent[i];
|
||||
if (a->name[0] == 0) continue; // empty slot
|
||||
int secs = _rtc->getCurrentTime() - a->recv_timestamp;
|
||||
if (secs < 60) {
|
||||
sprintf(tmp, "%ds", secs);
|
||||
} else if (secs < 60*60) {
|
||||
sprintf(tmp, "%dm", secs / 60);
|
||||
} else {
|
||||
sprintf(tmp, "%dh", secs / (60*60));
|
||||
}
|
||||
|
||||
int timestamp_width = display.getTextWidth(tmp);
|
||||
int max_name_width = display.width() - timestamp_width - 1;
|
||||
|
||||
char filtered_recent_name[sizeof(a->name)];
|
||||
display.translateUTF8ToBlocks(filtered_recent_name, a->name, sizeof(filtered_recent_name));
|
||||
display.drawTextEllipsized(0, y, max_name_width, filtered_recent_name);
|
||||
display.setCursor(display.width() - timestamp_width - 1, y);
|
||||
display.print(tmp);
|
||||
}
|
||||
} else if (_page == HomePage::RADIO) {
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setTextSize(1);
|
||||
// frequency and spreading factor
|
||||
display.setCursor(0, 8);
|
||||
sprintf(tmp, "FQ %06.3f", _node_prefs->freq);
|
||||
display.print(tmp);
|
||||
sprintf(tmp, "SF%d", _node_prefs->sf);
|
||||
display.drawTextRightAlign(display.width(), 8, tmp);
|
||||
// bandwidth and coding rate
|
||||
display.setCursor(0, 17);
|
||||
sprintf(tmp, "BW %03.2f", _node_prefs->bw);
|
||||
display.print(tmp);
|
||||
sprintf(tmp, "CR%d", _node_prefs->cr);
|
||||
display.drawTextRightAlign(display.width(), 17, tmp);
|
||||
// tx power and noise floor
|
||||
display.setCursor(0, 26);
|
||||
sprintf(tmp, "NF %ddB", radio_driver.getNoiseFloor());
|
||||
display.print(tmp);
|
||||
sprintf(tmp, "TX%d", _node_prefs->tx_power_dbm);
|
||||
display.drawTextRightAlign(display.width(), 26, tmp);
|
||||
|
||||
} else if (_page == HomePage::BLUETOOTH) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.drawXbm((display.width() - 32) / 2, 8,
|
||||
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off,
|
||||
32, 32);
|
||||
display.setTextSize(1);
|
||||
// display.drawTextCentered(display.width() / 2, 40 - 11, "toggle: " PRESS_LABEL);
|
||||
} else if (_page == HomePage::ADVERT) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.drawXbm((display.width() - 32) / 2, 8, advert_icon, 32, 32);
|
||||
// display.drawTextCentered(display.width() / 2, 40 - 11, "advert: " PRESS_LABEL);
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
} else if (_page == HomePage::GPS) {
|
||||
LocationProvider* nmea = sensors.getLocationProvider();
|
||||
char buf[50];
|
||||
int y = 8;
|
||||
bool gps_state = _task->getGPSState();
|
||||
#ifdef PIN_GPS_SWITCH
|
||||
bool hw_gps_state = digitalRead(PIN_GPS_SWITCH);
|
||||
if (gps_state != hw_gps_state) {
|
||||
strcpy(buf, gps_state ? "gps off(hw)" : "gps off(sw)");
|
||||
} else {
|
||||
strcpy(buf, gps_state ? "gps on" : "gps off");
|
||||
}
|
||||
#else
|
||||
strcpy(buf, gps_state ? "gps on" : "gps off");
|
||||
#endif
|
||||
display.drawTextLeftAlign(0, y, buf);
|
||||
if (nmea == NULL) {
|
||||
// y = y + 8;
|
||||
display.drawTextLeftAlign(0, y, "Can't access GPS");
|
||||
} else {
|
||||
if (!gps_state || !nmea->isValid()) {
|
||||
strcpy(buf, "no fix");
|
||||
} else {
|
||||
sprintf(buf, "%d sat", nmea->satellitesCount());
|
||||
}
|
||||
display.drawTextRightAlign(display.width()-1, y, buf);
|
||||
y = y + 8;
|
||||
sprintf(buf, "lat %.4f",
|
||||
nmea->getLatitude()/1000000.);
|
||||
display.drawTextLeftAlign(0, y, buf);
|
||||
y = y + 8;
|
||||
sprintf(buf, "lon %.4f",
|
||||
nmea->getLongitude()/1000000.);
|
||||
display.drawTextLeftAlign(0, y, buf);
|
||||
y = y + 8;
|
||||
sprintf(buf, "alt %.1f", nmea->getAltitude()/1000.);
|
||||
display.drawTextLeftAlign(0, y, buf);
|
||||
}
|
||||
#endif
|
||||
#if UI_SENSORS_PAGE == 1
|
||||
} else if (_page == HomePage::SENSORS) {
|
||||
int y = 8;
|
||||
refresh_sensors();
|
||||
char buf[30];
|
||||
char name[30];
|
||||
LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize());
|
||||
|
||||
for (int i = 0; i < sensors_scroll_offset; i++) {
|
||||
uint8_t channel, type;
|
||||
r.readHeader(channel, type);
|
||||
r.skipData(type);
|
||||
}
|
||||
|
||||
for (int i = 0; i < (sensors_scroll?UI_RECENT_LIST_SIZE:sensors_nb); i++) {
|
||||
uint8_t channel, type;
|
||||
if (!r.readHeader(channel, type)) { // reached end, reset
|
||||
r.reset();
|
||||
r.readHeader(channel, type);
|
||||
}
|
||||
|
||||
display.setCursor(0, y);
|
||||
float v;
|
||||
switch (type) {
|
||||
case LPP_GPS: // GPS
|
||||
float lat, lon, alt;
|
||||
r.readGPS(lat, lon, alt);
|
||||
strcpy(name, "gps"); sprintf(buf, "%.4f %.4f", lat, lon);
|
||||
break;
|
||||
case LPP_VOLTAGE:
|
||||
r.readVoltage(v);
|
||||
strcpy(name, "voltage"); sprintf(buf, "%6.2f", v);
|
||||
break;
|
||||
case LPP_CURRENT:
|
||||
r.readCurrent(v);
|
||||
strcpy(name, "current"); sprintf(buf, "%.3f", v);
|
||||
break;
|
||||
case LPP_TEMPERATURE:
|
||||
r.readTemperature(v);
|
||||
strcpy(name, "temperature"); sprintf(buf, "%.2f", v);
|
||||
break;
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
r.readRelativeHumidity(v);
|
||||
strcpy(name, "humidity"); sprintf(buf, "%.2f", v);
|
||||
break;
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
r.readPressure(v);
|
||||
strcpy(name, "pressure"); sprintf(buf, "%.2f", v);
|
||||
break;
|
||||
case LPP_ALTITUDE:
|
||||
r.readAltitude(v);
|
||||
strcpy(name, "altitude"); sprintf(buf, "%.0f", v);
|
||||
break;
|
||||
case LPP_POWER:
|
||||
r.readPower(v);
|
||||
strcpy(name, "power"); sprintf(buf, "%6.2f", v);
|
||||
break;
|
||||
default:
|
||||
r.skipData(type);
|
||||
strcpy(name, "unk"); sprintf(buf, "");
|
||||
}
|
||||
display.setCursor(0, y);
|
||||
display.print(name);
|
||||
display.setCursor(
|
||||
display.width()-display.getTextWidth(buf)-1, y
|
||||
);
|
||||
display.print(buf);
|
||||
y = y + 12;
|
||||
}
|
||||
if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb;
|
||||
else sensors_scroll_offset = 0;
|
||||
#endif
|
||||
} else if (_page == HomePage::SHUTDOWN) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setTextSize(1);
|
||||
if (_shutdown_init) {
|
||||
display.drawTextCentered(display.width() / 2, 20, "hibernating...");
|
||||
} else {
|
||||
display.drawXbm((display.width() - 32) / 2, 8, power_icon, 32, 32);
|
||||
// display.drawTextCentered(display.width() / 2, 40 - 11, "hibernate:" PRESS_LABEL);
|
||||
}
|
||||
}
|
||||
return 5000; // next render after 5000 ms
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_LEFT || c == KEY_PREV) {
|
||||
_page = (_page + HomePage::Count - 1) % HomePage::Count;
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_NEXT || c == KEY_RIGHT) {
|
||||
_page = (_page + 1) % HomePage::Count;
|
||||
if (_page == HomePage::RECENT) {
|
||||
_task->showAlert("Recent adverts", 800);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) {
|
||||
if (_task->isSerialEnabled()) { // toggle Bluetooth on/off
|
||||
_task->disableSerial();
|
||||
} else {
|
||||
_task->enableSerial();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_ENTER && _page == HomePage::ADVERT) {
|
||||
_task->notify(UIEventType::ack);
|
||||
if (the_mesh.advert()) {
|
||||
_task->showAlert("Advert sent!", 1000);
|
||||
} else {
|
||||
_task->showAlert("Advert failed..", 1000);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
if (c == KEY_ENTER && _page == HomePage::GPS) {
|
||||
_task->toggleGPS();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#if UI_SENSORS_PAGE == 1
|
||||
if (c == KEY_ENTER && _page == HomePage::SENSORS) {
|
||||
_task->toggleGPS();
|
||||
next_sensors_refresh=0;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
|
||||
_shutdown_init = true; // need to wait for button to be released
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) {
|
||||
_display = display;
|
||||
_sensors = sensors;
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_cached_batt_mv = getBattMilliVolts();
|
||||
|
||||
#if defined(PIN_USER_BTN)
|
||||
user_btn.begin();
|
||||
#endif
|
||||
#if defined(PIN_USER_BTN_ANA)
|
||||
analog_btn.begin();
|
||||
#endif
|
||||
|
||||
_node_prefs = node_prefs;
|
||||
|
||||
if (_display != NULL) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_statusBar.begin(_display->width());
|
||||
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
buzzer.begin();
|
||||
buzzer.quiet(_node_prefs->buzzer_quiet);
|
||||
buzzer.startup();
|
||||
#endif
|
||||
|
||||
#ifdef PIN_VIBRATION
|
||||
vibration.begin();
|
||||
#endif
|
||||
|
||||
ui_started_at = millis();
|
||||
_alert_expiry = 0;
|
||||
|
||||
splash = new SplashScreen(this);
|
||||
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
||||
setCurrScreen(splash);
|
||||
}
|
||||
|
||||
void UITask::showAlert(const char* text, int duration_millis) {
|
||||
strcpy(_alert, text);
|
||||
_alert_expiry = millis() + duration_millis;
|
||||
}
|
||||
|
||||
void UITask::notify(UIEventType t) {
|
||||
#if defined(PIN_BUZZER)
|
||||
switch(t){
|
||||
case UIEventType::contactMessage:
|
||||
// gemini's pick
|
||||
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7");
|
||||
break;
|
||||
case UIEventType::channelMessage:
|
||||
buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#");
|
||||
break;
|
||||
case UIEventType::ack:
|
||||
buzzer.play("ack:d=32,o=8,b=120:c");
|
||||
break;
|
||||
case UIEventType::roomMessage:
|
||||
case UIEventType::newContactMessage:
|
||||
case UIEventType::none:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef PIN_VIBRATION
|
||||
// Trigger vibration for all UI events except none
|
||||
if (t != UIEventType::none) {
|
||||
vibration.trigger();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void UITask::msgRead(int msgcount) {
|
||||
_msgcount = msgcount;
|
||||
if (msgcount == 0) {
|
||||
gotoHomeScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
|
||||
_msgcount = msgcount;
|
||||
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn() && !hasConnection()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
if (_display->isOn()) {
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
||||
_next_refresh = 100; // trigger refresh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::userLedHandler() {
|
||||
#ifdef PIN_STATUS_LED
|
||||
int cur_time = millis();
|
||||
if (cur_time > next_led_change) {
|
||||
if (led_state == 0) {
|
||||
led_state = 1;
|
||||
if (_msgcount > 0) {
|
||||
last_led_increment = LED_ON_MSG_MILLIS;
|
||||
} else {
|
||||
last_led_increment = LED_ON_MILLIS;
|
||||
}
|
||||
next_led_change = cur_time + last_led_increment;
|
||||
} else {
|
||||
led_state = 0;
|
||||
next_led_change = cur_time + LED_CYCLE_MILLIS - last_led_increment;
|
||||
}
|
||||
digitalWrite(PIN_STATUS_LED, led_state == LED_STATE_ON);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void UITask::setCurrScreen(UIScreen* c) {
|
||||
curr = c;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
/*
|
||||
hardware-agnostic pre-shutdown activity should be done here
|
||||
*/
|
||||
void UITask::shutdown(bool restart){
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
/* note: we have a choice here -
|
||||
we can do a blocking buzzer.loop() with non-deterministic consequences
|
||||
or we can set a flag and delay the shutdown for a couple of seconds
|
||||
while a non-blocking buzzer.loop() plays out in UITask::loop()
|
||||
*/
|
||||
buzzer.shutdown();
|
||||
uint32_t buzzer_timer = millis(); // fail-safe shutdown
|
||||
while (buzzer.isPlaying() && (millis() - 2500) < buzzer_timer)
|
||||
buzzer.loop();
|
||||
|
||||
#endif // PIN_BUZZER
|
||||
|
||||
if (restart) {
|
||||
_board->reboot();
|
||||
} else {
|
||||
_display->turnOff();
|
||||
radio_driver.powerOff();
|
||||
_board->powerOff();
|
||||
}
|
||||
}
|
||||
|
||||
bool UITask::isButtonPressed() const {
|
||||
#ifdef PIN_USER_BTN
|
||||
return user_btn.isPressed();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void UITask::loop() {
|
||||
char c = 0;
|
||||
#if UI_HAS_JOYSTICK
|
||||
int ev = user_btn.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_ENTER);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
c = handleLongPress(KEY_ENTER); // REVISIT: could be mapped to different key code
|
||||
}
|
||||
ev = joystick_left.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_LEFT);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
c = handleLongPress(KEY_LEFT);
|
||||
}
|
||||
ev = joystick_right.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_RIGHT);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
c = handleLongPress(KEY_RIGHT);
|
||||
}
|
||||
ev = back_btn.check();
|
||||
if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||
c = handleTripleClick(KEY_SELECT);
|
||||
}
|
||||
#elif defined(PIN_USER_BTN)
|
||||
int ev = user_btn.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_NEXT);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
c = handleLongPress(KEY_ENTER);
|
||||
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
||||
c = handleDoubleClick(KEY_PREV);
|
||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||
c = handleTripleClick(KEY_SELECT);
|
||||
}
|
||||
#endif
|
||||
#if defined(PIN_USER_BTN_ANA)
|
||||
if (abs(millis() - _analogue_pin_read_millis) > 10) {
|
||||
int ev = analog_btn.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_NEXT);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
c = handleLongPress(KEY_ENTER);
|
||||
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
||||
c = handleDoubleClick(KEY_PREV);
|
||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||
c = handleTripleClick(KEY_SELECT);
|
||||
}
|
||||
_analogue_pin_read_millis = millis();
|
||||
}
|
||||
#endif
|
||||
#if defined(BACKLIGHT_BTN)
|
||||
if (millis() > next_backlight_btn_check) {
|
||||
bool touch_state = digitalRead(PIN_BUTTON2);
|
||||
#if defined(DISP_BACKLIGHT)
|
||||
digitalWrite(DISP_BACKLIGHT, !touch_state);
|
||||
#elif defined(EXP_PIN_BACKLIGHT)
|
||||
expander.digitalWrite(EXP_PIN_BACKLIGHT, !touch_state);
|
||||
#endif
|
||||
next_backlight_btn_check = millis() + 300;
|
||||
}
|
||||
#endif
|
||||
#if defined(HAS_TORCH)
|
||||
ev = back_btn.check();
|
||||
if (ev == BUTTON_EVENT_CLICK && c == 0) {
|
||||
c = checkDisplayOn(KEY_PREV);
|
||||
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
||||
board.toggleTorch();
|
||||
c = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (c != 0 && curr) {
|
||||
curr->handleInput(c);
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
_next_refresh = 100; // trigger refresh
|
||||
}
|
||||
|
||||
userLedHandler();
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
if (buzzer.isPlaying()) buzzer.loop();
|
||||
#endif
|
||||
|
||||
if (curr) curr->poll();
|
||||
|
||||
if (_display != NULL && _display->isOn()) {
|
||||
_statusBar.update(*_display,
|
||||
_node_prefs->node_name,
|
||||
_cached_batt_mv,
|
||||
isBuzzerQuiet(),
|
||||
getGPSState(),
|
||||
isSerialEnabled());
|
||||
|
||||
bool status_dirty = _statusBar.needsRedraw();
|
||||
bool content_dirty = (millis() >= _next_refresh && curr);
|
||||
|
||||
if (status_dirty || content_dirty) {
|
||||
_display->startFrame();
|
||||
_statusBar.render(*_display);
|
||||
|
||||
if (curr) {
|
||||
int delay_millis = curr->render(*_display);
|
||||
if (content_dirty) {
|
||||
_next_refresh = millis() + delay_millis;
|
||||
}
|
||||
}
|
||||
|
||||
if (millis() < _alert_expiry) { // render alert popup
|
||||
_display->setTextSize(1);
|
||||
int y = _display->height() / 3;
|
||||
int p = _display->height() / 32;
|
||||
_display->setColor(DisplayDriver::DARK);
|
||||
_display->fillRect(p, y, _display->width() - p*2, y);
|
||||
_display->setColor(DisplayDriver::LIGHT); // draw box border
|
||||
_display->drawRect(p, y, _display->width() - p*2, y);
|
||||
_display->drawTextCentered(_display->width() / 2, y + p*3, _alert);
|
||||
_next_refresh = _alert_expiry; // will need refresh when alert is dismissed
|
||||
}
|
||||
|
||||
_display->endFrame();
|
||||
}
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
if (millis() > _auto_off) {
|
||||
_display->turnOff();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef PIN_VIBRATION
|
||||
vibration.loop();
|
||||
#endif
|
||||
|
||||
#ifdef AUTO_SHUTDOWN_MILLIVOLTS
|
||||
if (millis() > next_batt_chck) {
|
||||
_cached_batt_mv = getBattMilliVolts();
|
||||
if (_cached_batt_mv > 0 && _cached_batt_mv < AUTO_SHUTDOWN_MILLIVOLTS) {
|
||||
|
||||
shutdown();
|
||||
|
||||
}
|
||||
next_batt_chck = millis() + 8000;
|
||||
}
|
||||
#else
|
||||
if (_display != NULL && _display->isOn() && millis() >= next_batt_chck) {
|
||||
_cached_batt_mv = getBattMilliVolts();
|
||||
next_batt_chck = millis() + 8000;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
char UITask::checkDisplayOn(char c) {
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn()) {
|
||||
_display->turnOn(); // turn display on and consume event
|
||||
c = 0;
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
_next_refresh = 0; // trigger refresh
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
char UITask::handleLongPress(char c) {
|
||||
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
|
||||
the_mesh.enterCLIRescue();
|
||||
c = 0; // consume event
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
char UITask::handleDoubleClick(char c) {
|
||||
MESH_DEBUG_PRINTLN("UITask: double click triggered");
|
||||
checkDisplayOn(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
char UITask::handleTripleClick(char c) {
|
||||
MESH_DEBUG_PRINTLN("UITask: triple click triggered");
|
||||
checkDisplayOn(c);
|
||||
toggleBuzzer();
|
||||
c = 0;
|
||||
return c;
|
||||
}
|
||||
|
||||
bool UITask::getGPSState() {
|
||||
if (_sensors != NULL) {
|
||||
int num = _sensors->getNumSettings();
|
||||
for (int i = 0; i < num; i++) {
|
||||
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
|
||||
return !strcmp(_sensors->getSettingValue(i), "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void UITask::toggleGPS() {
|
||||
if (_sensors != NULL) {
|
||||
// toggle GPS on/off
|
||||
int num = _sensors->getNumSettings();
|
||||
for (int i = 0; i < num; i++) {
|
||||
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
|
||||
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
|
||||
_sensors->setSettingValue("gps", "0");
|
||||
_node_prefs->gps_enabled = 0;
|
||||
notify(UIEventType::ack);
|
||||
} else {
|
||||
_sensors->setSettingValue("gps", "1");
|
||||
_node_prefs->gps_enabled = 1;
|
||||
notify(UIEventType::ack);
|
||||
}
|
||||
the_mesh.savePrefs();
|
||||
showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800);
|
||||
_next_refresh = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::toggleBuzzer() {
|
||||
// Toggle buzzer quiet mode
|
||||
#ifdef PIN_BUZZER
|
||||
if (buzzer.isQuiet()) {
|
||||
buzzer.quiet(false);
|
||||
notify(UIEventType::ack);
|
||||
} else {
|
||||
buzzer.quiet(true);
|
||||
}
|
||||
_node_prefs->buzzer_quiet = buzzer.isQuiet();
|
||||
the_mesh.savePrefs();
|
||||
showAlert(buzzer.isQuiet() ? "Buzzer: OFF" : "Buzzer: ON", 800);
|
||||
_next_refresh = 0; // trigger refresh
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/BaseSerialInterface.h>
|
||||
#include <Arduino.h>
|
||||
#include <helpers/sensors/LPPDataHelpers.h>
|
||||
|
||||
#include "ScrollingStatusBar.h"
|
||||
|
||||
|
||||
#ifndef LED_STATE_ON
|
||||
#define LED_STATE_ON 1
|
||||
#endif
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
#include <helpers/ui/buzzer.h>
|
||||
#endif
|
||||
#ifdef PIN_VIBRATION
|
||||
#include <helpers/ui/GenericVibration.h>
|
||||
#endif
|
||||
|
||||
#include "../AbstractUITask.h"
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
class UITask : public AbstractUITask {
|
||||
DisplayDriver* _display;
|
||||
SensorManager* _sensors;
|
||||
ScrollingStatusBar _statusBar;
|
||||
#ifdef PIN_BUZZER
|
||||
genericBuzzer buzzer;
|
||||
#endif
|
||||
#ifdef PIN_VIBRATION
|
||||
GenericVibration vibration;
|
||||
#endif
|
||||
unsigned long _next_refresh, _auto_off;
|
||||
NodePrefs* _node_prefs;
|
||||
char _alert[80];
|
||||
unsigned long _alert_expiry;
|
||||
int _msgcount;
|
||||
unsigned long ui_started_at, next_batt_chck;
|
||||
int next_backlight_btn_check = 0;
|
||||
uint16_t _cached_batt_mv;
|
||||
#ifdef PIN_STATUS_LED
|
||||
int led_state = 0;
|
||||
int next_led_change = 0;
|
||||
int last_led_increment = 0;
|
||||
#endif
|
||||
|
||||
#ifdef PIN_USER_BTN_ANA
|
||||
unsigned long _analogue_pin_read_millis = millis();
|
||||
#endif
|
||||
|
||||
UIScreen* splash;
|
||||
UIScreen* home;
|
||||
// UIScreen* msg_preview;
|
||||
UIScreen* curr;
|
||||
|
||||
|
||||
void userLedHandler();
|
||||
|
||||
// Button action handlers
|
||||
char checkDisplayOn(char c);
|
||||
char handleLongPress(char c);
|
||||
char handleDoubleClick(char c);
|
||||
char handleTripleClick(char c);
|
||||
|
||||
void setCurrScreen(UIScreen* c);
|
||||
|
||||
public:
|
||||
|
||||
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
|
||||
next_batt_chck = _next_refresh = 0;
|
||||
_cached_batt_mv = 0;
|
||||
ui_started_at = 0;
|
||||
curr = NULL;
|
||||
}
|
||||
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
|
||||
|
||||
void gotoHomeScreen() { setCurrScreen(home); }
|
||||
void showAlert(const char* text, int duration_millis);
|
||||
int getMsgCount() const { return _msgcount; }
|
||||
uint16_t getCachedBattMV() const { return _cached_batt_mv; }
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
bool isButtonPressed() const;
|
||||
|
||||
bool isBuzzerQuiet() {
|
||||
#ifdef PIN_BUZZER
|
||||
return buzzer.isQuiet();
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void toggleBuzzer();
|
||||
bool getGPSState();
|
||||
void toggleGPS();
|
||||
|
||||
|
||||
// from AbstractUITask
|
||||
void msgRead(int msgcount) override;
|
||||
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
|
||||
void notify(UIEventType t = UIEventType::none) override;
|
||||
void loop() override;
|
||||
|
||||
void shutdown(bool restart = false);
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
// icons converted for use with U8g2 which needs a different format of xbm data.
|
||||
|
||||
// 'meshcore', 72x36px
|
||||
static const uint8_t meshcore_logo [] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0xf0, 0x00, 0x3e, 0xfe, 0x3f, 0xfe, 0x3f, 0x1e, 0x78,
|
||||
0xf8, 0x00, 0x1f, 0xff, 0x3f, 0xff, 0x3f, 0x1e, 0x78, 0xf8, 0x01, 0x1f,
|
||||
0xff, 0x9f, 0xff, 0x1f, 0x0e, 0x78, 0xf8, 0x81, 0x1f, 0x0f, 0x80, 0x07,
|
||||
0x00, 0x0f, 0x38, 0xf8, 0xc1, 0x1f, 0x0f, 0x80, 0x07, 0x00, 0x0f, 0x3c,
|
||||
0xf8, 0xc3, 0x1f, 0xff, 0x87, 0xff, 0x07, 0xff, 0x3f, 0xf8, 0xe3, 0x1f,
|
||||
0xff, 0x87, 0xff, 0x0f, 0xff, 0x3f, 0xfc, 0xf3, 0x8f, 0xff, 0x07, 0xff,
|
||||
0x1f, 0xff, 0x3f, 0xfc, 0xf3, 0x8f, 0x07, 0x00, 0x00, 0x9e, 0x0f, 0x1e,
|
||||
0xbc, 0x7f, 0x8f, 0x07, 0x00, 0x00, 0x9e, 0x07, 0x1e, 0x9c, 0x3f, 0x8f,
|
||||
0x07, 0x00, 0x00, 0x9f, 0x07, 0x1e, 0x9c, 0x3f, 0x8f, 0xff, 0xcf, 0xff,
|
||||
0x8f, 0x07, 0x1e, 0x1e, 0x1f, 0xc7, 0xff, 0xcf, 0xff, 0x87, 0x07, 0x1e,
|
||||
0x1e, 0x0f, 0xc7, 0xff, 0xc7, 0xff, 0x83, 0x03, 0x0e, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xc0, 0xff, 0xf0, 0xff, 0xe0, 0xff, 0xc1, 0xff, 0x07, 0xf0, 0xff, 0xf8,
|
||||
0xff, 0xf1, 0xff, 0xc3, 0xff, 0x07, 0xf0, 0xff, 0xfc, 0xff, 0xf1, 0xff,
|
||||
0xc7, 0xff, 0x07, 0x78, 0x00, 0x3c, 0xe0, 0xf1, 0xc0, 0xe7, 0x01, 0x00,
|
||||
0x78, 0x00, 0x1e, 0xe0, 0xf1, 0x80, 0xe7, 0x01, 0x00, 0x78, 0x00, 0x1e,
|
||||
0xe0, 0xf1, 0xc0, 0xe3, 0x01, 0x00, 0x78, 0x00, 0x1e, 0xe0, 0x71, 0xc0,
|
||||
0xe3, 0xff, 0x00, 0x3c, 0x00, 0x1e, 0xe0, 0xf9, 0xff, 0xe3, 0xff, 0x00,
|
||||
0x3c, 0x00, 0x1e, 0xe0, 0xf8, 0xff, 0xf1, 0xff, 0x00, 0x3c, 0x00, 0x0e,
|
||||
0xf0, 0xf8, 0xff, 0xf0, 0x00, 0x00, 0x3c, 0x00, 0x1f, 0xf0, 0x78, 0x7c,
|
||||
0xf0, 0x00, 0x00, 0xfc, 0x3f, 0xff, 0xff, 0x38, 0xf8, 0xf0, 0xff, 0x01,
|
||||
0xfc, 0x3f, 0xfe, 0x7f, 0x3c, 0xf0, 0xf0, 0xff, 0x01, 0xf8, 0x3f, 0xfe,
|
||||
0x3f, 0x3c, 0xf0, 0xf1, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
|
||||
};
|
||||
|
||||
|
||||
// bluetooth on icon, 32x32px, horizontal
|
||||
static const uint8_t bluetooth_on[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x0c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00,
|
||||
0x00, 0xfc, 0x01, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xdc, 0x07, 0x00,
|
||||
0x0c, 0x1c, 0x1f, 0x00, 0x3c, 0x1c, 0x3e, 0x00, 0x7c, 0x1c, 0x3e, 0x00,
|
||||
0xf8, 0x1d, 0x1f, 0x0e, 0xe0, 0x9f, 0x0f, 0x1e, 0xc0, 0xff, 0x03, 0x1e,
|
||||
0x00, 0xff, 0x01, 0x3c, 0x00, 0xfe, 0xe0, 0x38, 0x00, 0x7e, 0xe0, 0x38,
|
||||
0xc0, 0xff, 0x41, 0x38, 0xc0, 0xff, 0x03, 0x1e, 0xe0, 0xdf, 0x07, 0x1e,
|
||||
0xf0, 0x1d, 0x1f, 0x0e, 0x7c, 0x1c, 0x3e, 0x00, 0x3c, 0x1c, 0x3e, 0x00,
|
||||
0x1c, 0x1c, 0x1f, 0x00, 0x00, 0x9c, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00,
|
||||
0x00, 0xfc, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00,
|
||||
0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
|
||||
|
||||
// bluetooth off icon, 32x32px, horizontal
|
||||
static const uint8_t bluetooth_off[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00,
|
||||
0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x1c, 0xc0, 0x1f, 0x00,
|
||||
0x3c, 0xc0, 0x3f, 0x00, 0x7c, 0xc0, 0xfd, 0x00, 0xf0, 0xc1, 0xf1, 0x01,
|
||||
0xe0, 0xc3, 0xe1, 0x03, 0xc0, 0x0f, 0xc0, 0x03, 0x00, 0x1f, 0xf0, 0x01,
|
||||
0x00, 0x3e, 0xf0, 0x00, 0x00, 0xf8, 0x70, 0x00, 0x00, 0xf0, 0x01, 0x00,
|
||||
0x00, 0xe0, 0x07, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0xf0, 0x1f, 0x00,
|
||||
0x00, 0xfc, 0x7d, 0x00, 0x00, 0xfe, 0xf9, 0x00, 0x00, 0xdf, 0xf1, 0x03,
|
||||
0xc0, 0xc7, 0xc1, 0x07, 0xc0, 0xc3, 0xe1, 0x0f, 0xc0, 0xc1, 0xf1, 0x3f,
|
||||
0x00, 0xc0, 0xfd, 0x3c, 0x00, 0xc0, 0x3f, 0x38, 0x00, 0xc0, 0x1f, 0x00,
|
||||
0x00, 0xc0, 0x07, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
|
||||
|
||||
// power icon, 32x32px, horizontal
|
||||
static const uint8_t power_icon[] = {
|
||||
0x00, 0x80, 0x01, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x03, 0x00,
|
||||
0x00, 0xcc, 0x33, 0x00, 0x00, 0xcf, 0xf3, 0x00, 0x80, 0xcf, 0xf3, 0x01,
|
||||
0xc0, 0xcf, 0xf3, 0x03, 0xe0, 0xcf, 0xf3, 0x07, 0xf0, 0xc7, 0xe3, 0x0f,
|
||||
0xf8, 0xc3, 0xc3, 0x1f, 0xf8, 0xc1, 0x83, 0x1f, 0xfc, 0xc0, 0x03, 0x3f,
|
||||
0x7c, 0xc0, 0x03, 0x3e, 0x7c, 0xc0, 0x03, 0x3e, 0x7e, 0x80, 0x01, 0x7e,
|
||||
0x3e, 0x00, 0x00, 0x7c, 0x3e, 0x00, 0x00, 0x7c, 0x3e, 0x00, 0x00, 0x7c,
|
||||
0x3e, 0x00, 0x00, 0x7c, 0x3e, 0x00, 0x00, 0x7c, 0x7c, 0x00, 0x00, 0x3e,
|
||||
0x7c, 0x00, 0x00, 0x3e, 0xfc, 0x00, 0x00, 0x3f, 0xf8, 0x01, 0x80, 0x1f,
|
||||
0xf8, 0x03, 0xc0, 0x1f, 0xf0, 0x07, 0xe0, 0x0f, 0xf0, 0x1f, 0xf8, 0x0f,
|
||||
0xe0, 0xff, 0xff, 0x07, 0xc0, 0xff, 0xff, 0x03, 0x00, 0xff, 0xff, 0x00,
|
||||
0x00, 0xfc, 0x3f, 0x00, 0x00, 0xf0, 0x0f, 0x00 };
|
||||
|
||||
|
||||
|
||||
|
||||
// 'advert', 32x32px, horizontal
|
||||
static const uint8_t advert_icon[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x0c,
|
||||
0x38, 0x00, 0x00, 0x1c, 0x18, 0x00, 0x00, 0x18, 0x0c, 0x00, 0x00, 0x30,
|
||||
0x0c, 0x06, 0x60, 0x30, 0x06, 0x07, 0xe0, 0x60, 0x86, 0x03, 0xc0, 0x61,
|
||||
0x87, 0x81, 0x81, 0xe1, 0xc3, 0xe0, 0x07, 0xc3, 0xc3, 0xf0, 0x0f, 0xc3,
|
||||
0xc3, 0xf0, 0x0f, 0xc3, 0xc3, 0xf0, 0x0f, 0xc3, 0xc3, 0xf0, 0x0f, 0xc3,
|
||||
0xc3, 0xe0, 0x07, 0xc3, 0x83, 0xc1, 0x83, 0xc1, 0x86, 0x01, 0x80, 0x61,
|
||||
0x06, 0x03, 0xc0, 0x60, 0x0e, 0x07, 0xe0, 0x70, 0x0c, 0x02, 0x40, 0x30,
|
||||
0x1c, 0x00, 0x00, 0x38, 0x18, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, 0x0c,
|
||||
0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
|
||||
|
||||
|
||||
// 'muted, 8x8px, horizontal
|
||||
static const uint8_t muted_icon[] = {
|
||||
0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20 };
|
||||
@@ -129,6 +129,8 @@ void KissModem::processFrame() {
|
||||
memcpy(_pending_tx, data, data_len);
|
||||
_pending_tx_len = data_len;
|
||||
_has_pending_tx = true;
|
||||
} else if (_has_pending_tx) {
|
||||
writeHardwareError(HW_ERR_TX_BUSY);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -257,6 +259,7 @@ void KissModem::processTx() {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_DELAY;
|
||||
} else {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_WAIT_CLEAR;
|
||||
}
|
||||
}
|
||||
@@ -273,19 +276,30 @@ void KissModem::processTx() {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_SLOT_WAIT;
|
||||
}
|
||||
} else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(KISS_MAX_PACKET_SIZE) * KISS_TX_TIMEOUT_FACTOR) {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_DELAY;
|
||||
}
|
||||
break;
|
||||
|
||||
case TX_SLOT_WAIT:
|
||||
if (millis() - _tx_timer >= (uint32_t)_slottime * 10) {
|
||||
_tx_timer = millis();
|
||||
_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;
|
||||
if (_radio.startSendRaw(_pending_tx, _pending_tx_len)) {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_SENDING;
|
||||
} else {
|
||||
uint8_t result = 0x00;
|
||||
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
|
||||
_has_pending_tx = false;
|
||||
_tx_state = TX_IDLE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -296,6 +310,12 @@ void KissModem::processTx() {
|
||||
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
|
||||
_has_pending_tx = false;
|
||||
_tx_state = TX_IDLE;
|
||||
} else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(_pending_tx_len) * KISS_TX_TIMEOUT_FACTOR) {
|
||||
_radio.onSendFinished();
|
||||
uint8_t result = 0x00;
|
||||
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
|
||||
_has_pending_tx = false;
|
||||
_tx_state = TX_IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#define KISS_DEFAULT_TXDELAY 50
|
||||
#define KISS_DEFAULT_PERSISTENCE 63
|
||||
#define KISS_DEFAULT_SLOTTIME 10
|
||||
#define KISS_TX_TIMEOUT_FACTOR 3/2 // 1.5x estimated airtime
|
||||
|
||||
#define HW_CMD_GET_IDENTITY 0x01
|
||||
#define HW_CMD_GET_RANDOM 0x02
|
||||
@@ -71,6 +72,7 @@
|
||||
#define HW_ERR_MAC_FAILED 0x04
|
||||
#define HW_ERR_UNKNOWN_CMD 0x05
|
||||
#define HW_ERR_ENCRYPT_FAILED 0x06
|
||||
#define HW_ERR_TX_BUSY 0x07
|
||||
|
||||
#define KISS_FIRMWARE_VERSION 1
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
#include <LittleFS.h>
|
||||
#elif defined(ESP32)
|
||||
#include <SPIFFS.h>
|
||||
#else
|
||||
#include <InternalFileSystem.h>
|
||||
#endif
|
||||
|
||||
#if defined(KISS_UART_RX) && defined(KISS_UART_TX)
|
||||
#include <HardwareSerial.h>
|
||||
#endif
|
||||
@@ -29,7 +32,7 @@ void halt() {
|
||||
}
|
||||
|
||||
void loadOrCreateIdentity() {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
InternalFS.begin();
|
||||
IdentityStore store(InternalFS, "");
|
||||
#elif defined(ESP32)
|
||||
@@ -53,11 +56,11 @@ void loadOrCreateIdentity() {
|
||||
}
|
||||
|
||||
void onSetRadio(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
radio_set_params(freq, bw, sf, cr);
|
||||
radio_driver.setParams(freq, bw, sf, cr);
|
||||
}
|
||||
|
||||
void onSetTxPower(uint8_t power) {
|
||||
radio_set_tx_power(power);
|
||||
radio_driver.setTxPower(power);
|
||||
}
|
||||
|
||||
float onGetCurrentRssi() {
|
||||
@@ -79,7 +82,7 @@ void setup() {
|
||||
|
||||
radio_driver.begin();
|
||||
|
||||
rng.begin(radio_get_rng_seed());
|
||||
rng.begin(radio_driver.getRngSeed());
|
||||
loadOrCreateIdentity();
|
||||
|
||||
sensors.begin();
|
||||
|
||||
@@ -953,8 +953,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
}
|
||||
#endif
|
||||
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_set_tx_power(_prefs.tx_power_dbm);
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||
@@ -1050,7 +1050,7 @@ void MyMesh::dumpLogFile() {
|
||||
}
|
||||
|
||||
void MyMesh::setTxPower(int8_t power_dbm) {
|
||||
radio_set_tx_power(power_dbm);
|
||||
radio_driver.setTxPower(power_dbm);
|
||||
}
|
||||
|
||||
#if defined(USE_SX1262) || defined(USE_SX1268)
|
||||
@@ -1279,13 +1279,13 @@ void MyMesh::loop() {
|
||||
|
||||
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
|
||||
set_radio_at = 0; // clear timer
|
||||
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
|
||||
radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr);
|
||||
MESH_DEBUG_PRINTLN("Temp radio params");
|
||||
}
|
||||
|
||||
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
|
||||
revert_radio_at = 0; // clear timer
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
MESH_DEBUG_PRINTLN("Radio params restored");
|
||||
}
|
||||
|
||||
|
||||
@@ -69,11 +69,11 @@ struct NeighbourInfo {
|
||||
};
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "20 Mar 2026"
|
||||
#define FIRMWARE_BUILD_DATE "19 Apr 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.14.1"
|
||||
#define FIRMWARE_VERSION "v1.15.0"
|
||||
#endif
|
||||
|
||||
#define FIRMWARE_ROLE "repeater"
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
#include <Arduino.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
|
||||
#ifndef USER_BTN_PRESSED
|
||||
#define USER_BTN_PRESSED LOW
|
||||
#endif
|
||||
|
||||
#define AUTO_OFF_MILLIS 20000 // 20 seconds
|
||||
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
|
||||
|
||||
@@ -48,17 +52,25 @@ void UITask::renderCurrScreen() {
|
||||
int logoWidth = 128;
|
||||
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
|
||||
|
||||
// meshcore website
|
||||
const char* website = "https://meshcore.io";
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setTextSize(1);
|
||||
uint16_t websiteWidth = _display->getTextWidth(website);
|
||||
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
|
||||
_display->print(website);
|
||||
|
||||
// version info
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setTextSize(1);
|
||||
uint16_t versionWidth = _display->getTextWidth(_version_info);
|
||||
_display->setCursor((_display->width() - versionWidth) / 2, 22);
|
||||
_display->setCursor((_display->width() - versionWidth) / 2, 35);
|
||||
_display->print(_version_info);
|
||||
|
||||
// node type
|
||||
const char* node_type = "< Repeater >";
|
||||
uint16_t typeWidth = _display->getTextWidth(node_type);
|
||||
_display->setCursor((_display->width() - typeWidth) / 2, 35);
|
||||
_display->setCursor((_display->width() - typeWidth) / 2, 48);
|
||||
_display->print(node_type);
|
||||
} else { // home screen
|
||||
// node name
|
||||
@@ -85,7 +97,7 @@ void UITask::loop() {
|
||||
if (millis() >= _next_read) {
|
||||
int btnState = digitalRead(PIN_USER_BTN);
|
||||
if (btnState != _prevBtnState) {
|
||||
if (btnState == LOW) { // pressed?
|
||||
if (btnState == USER_BTN_PRESSED) { // pressed?
|
||||
if (_display->isOn()) {
|
||||
// TODO: any action ?
|
||||
} else {
|
||||
|
||||
@@ -57,7 +57,7 @@ void setup() {
|
||||
halt();
|
||||
}
|
||||
|
||||
fast_rng.begin(radio_get_rng_seed());
|
||||
fast_rng.begin(radio_driver.getRngSeed());
|
||||
|
||||
FILESYSTEM* fs;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
|
||||
@@ -691,8 +691,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
}
|
||||
}
|
||||
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_set_tx_power(_prefs.tx_power_dbm);
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
@@ -796,7 +796,7 @@ void MyMesh::dumpLogFile() {
|
||||
}
|
||||
|
||||
void MyMesh::setTxPower(int8_t power_dbm) {
|
||||
radio_set_tx_power(power_dbm);
|
||||
radio_driver.setTxPower(power_dbm);
|
||||
}
|
||||
|
||||
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
|
||||
@@ -999,13 +999,13 @@ void MyMesh::loop() {
|
||||
|
||||
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
|
||||
set_radio_at = 0; // clear timer
|
||||
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
|
||||
radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr);
|
||||
MESH_DEBUG_PRINTLN("Temp radio params");
|
||||
}
|
||||
|
||||
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
|
||||
revert_radio_at = 0; // clear timer
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
MESH_DEBUG_PRINTLN("Radio params restored");
|
||||
}
|
||||
|
||||
|
||||
@@ -27,11 +27,11 @@
|
||||
/* ------------------------------ Config -------------------------------- */
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "20 Mar 2026"
|
||||
#define FIRMWARE_BUILD_DATE "19 Apr 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.14.1"
|
||||
#define FIRMWARE_VERSION "v1.15.0"
|
||||
#endif
|
||||
|
||||
#ifndef LORA_FREQ
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
#include <Arduino.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
|
||||
#ifndef USER_BTN_PRESSED
|
||||
#define USER_BTN_PRESSED LOW
|
||||
#endif
|
||||
|
||||
#define AUTO_OFF_MILLIS 20000 // 20 seconds
|
||||
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
|
||||
|
||||
@@ -48,17 +52,25 @@ void UITask::renderCurrScreen() {
|
||||
int logoWidth = 128;
|
||||
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
|
||||
|
||||
// meshcore website
|
||||
const char* website = "https://meshcore.io";
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setTextSize(1);
|
||||
uint16_t websiteWidth = _display->getTextWidth(website);
|
||||
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
|
||||
_display->print(website);
|
||||
|
||||
// version info
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setTextSize(1);
|
||||
uint16_t versionWidth = _display->getTextWidth(_version_info);
|
||||
_display->setCursor((_display->width() - versionWidth) / 2, 22);
|
||||
_display->setCursor((_display->width() - versionWidth) / 2, 35);
|
||||
_display->print(_version_info);
|
||||
|
||||
// node type
|
||||
const char* node_type = "< Room Server >";
|
||||
uint16_t typeWidth = _display->getTextWidth(node_type);
|
||||
_display->setCursor((_display->width() - typeWidth) / 2, 35);
|
||||
_display->setCursor((_display->width() - typeWidth) / 2, 48);
|
||||
_display->print(node_type);
|
||||
} else { // home screen
|
||||
// node name
|
||||
@@ -85,7 +97,7 @@ void UITask::loop() {
|
||||
if (millis() >= _next_read) {
|
||||
int btnState = digitalRead(PIN_USER_BTN);
|
||||
if (btnState != _prevBtnState) {
|
||||
if (btnState == LOW) { // pressed?
|
||||
if (btnState == USER_BTN_PRESSED) { // pressed?
|
||||
if (_display->isOn()) {
|
||||
// TODO: any action ?
|
||||
} else {
|
||||
|
||||
@@ -35,7 +35,7 @@ void setup() {
|
||||
|
||||
if (!radio_init()) { halt(); }
|
||||
|
||||
fast_rng.begin(radio_get_rng_seed());
|
||||
fast_rng.begin(radio_driver.getRngSeed());
|
||||
|
||||
FILESYSTEM* fs;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
|
||||
@@ -562,7 +562,7 @@ void setup() {
|
||||
|
||||
if (!radio_init()) { halt(); }
|
||||
|
||||
fast_rng.begin(radio_get_rng_seed());
|
||||
fast_rng.begin(radio_driver.getRngSeed());
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
InternalFS.begin();
|
||||
@@ -577,8 +577,8 @@ void setup() {
|
||||
#error "need to define filesystem"
|
||||
#endif
|
||||
|
||||
radio_set_params(the_mesh.getFreqPref(), LORA_BW, LORA_SF, LORA_CR);
|
||||
radio_set_tx_power(the_mesh.getTxPowerPref());
|
||||
radio_driver.setParams(the_mesh.getFreqPref(), LORA_BW, LORA_SF, LORA_CR);
|
||||
radio_driver.setTxPower(the_mesh.getTxPowerPref());
|
||||
|
||||
the_mesh.showWelcome();
|
||||
|
||||
|
||||
@@ -764,8 +764,8 @@ void SensorMesh::begin(FILESYSTEM* fs) {
|
||||
}
|
||||
}
|
||||
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_set_tx_power(_prefs.tx_power_dbm);
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
@@ -842,7 +842,7 @@ void SensorMesh::updateFloodAdvertTimer() {
|
||||
}
|
||||
|
||||
void SensorMesh::setTxPower(int8_t power_dbm) {
|
||||
radio_set_tx_power(power_dbm);
|
||||
radio_driver.setTxPower(power_dbm);
|
||||
}
|
||||
|
||||
void SensorMesh::formatStatsReply(char *reply) {
|
||||
@@ -908,13 +908,13 @@ void SensorMesh::loop() {
|
||||
|
||||
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
|
||||
set_radio_at = 0; // clear timer
|
||||
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
|
||||
radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr);
|
||||
MESH_DEBUG_PRINTLN("Temp radio params");
|
||||
}
|
||||
|
||||
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
|
||||
revert_radio_at = 0; // clear timer
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
MESH_DEBUG_PRINTLN("Radio params restored");
|
||||
}
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "20 Mar 2026"
|
||||
#define FIRMWARE_BUILD_DATE "19 Apr 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.14.1"
|
||||
#define FIRMWARE_VERSION "v1.15.0"
|
||||
#endif
|
||||
|
||||
#define FIRMWARE_ROLE "sensor"
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
#include <Arduino.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
|
||||
#ifndef USER_BTN_PRESSED
|
||||
#define USER_BTN_PRESSED LOW
|
||||
#endif
|
||||
|
||||
#define AUTO_OFF_MILLIS 20000 // 20 seconds
|
||||
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
|
||||
|
||||
@@ -48,17 +52,25 @@ void UITask::renderCurrScreen() {
|
||||
int logoWidth = 128;
|
||||
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
|
||||
|
||||
// meshcore website
|
||||
const char* website = "https://meshcore.io";
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setTextSize(1);
|
||||
uint16_t websiteWidth = _display->getTextWidth(website);
|
||||
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
|
||||
_display->print(website);
|
||||
|
||||
// version info
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setTextSize(1);
|
||||
uint16_t versionWidth = _display->getTextWidth(_version_info);
|
||||
_display->setCursor((_display->width() - versionWidth) / 2, 22);
|
||||
_display->setCursor((_display->width() - versionWidth) / 2, 35);
|
||||
_display->print(_version_info);
|
||||
|
||||
// node type
|
||||
const char* node_type = "< Sensor >";
|
||||
uint16_t typeWidth = _display->getTextWidth(node_type);
|
||||
_display->setCursor((_display->width() - typeWidth) / 2, 35);
|
||||
_display->setCursor((_display->width() - typeWidth) / 2, 48);
|
||||
_display->print(node_type);
|
||||
} else { // home screen
|
||||
// node name
|
||||
@@ -85,7 +97,7 @@ void UITask::loop() {
|
||||
if (millis() >= _next_read) {
|
||||
int btnState = digitalRead(PIN_USER_BTN);
|
||||
if (btnState != _prevBtnState) {
|
||||
if (btnState == LOW) { // pressed?
|
||||
if (btnState == USER_BTN_PRESSED) { // pressed?
|
||||
if (_display->isOn()) {
|
||||
// TODO: any action ?
|
||||
} else {
|
||||
|
||||
@@ -68,7 +68,7 @@ void setup() {
|
||||
|
||||
if (!radio_init()) { halt(); }
|
||||
|
||||
fast_rng.begin(radio_get_rng_seed());
|
||||
fast_rng.begin(radio_driver.getRngSeed());
|
||||
|
||||
FILESYSTEM* fs;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ edit_uri: edit/main/docs/
|
||||
|
||||
theme:
|
||||
name: material
|
||||
logo: _assets/meshcore_tm.svg
|
||||
logo: _assets/meshcore.svg
|
||||
features:
|
||||
- content.action.edit
|
||||
- content.code.copy
|
||||
|
||||
+23
-8
@@ -10,8 +10,8 @@
|
||||
|
||||
[platformio]
|
||||
extra_configs =
|
||||
variants/*/platformio.ini
|
||||
platformio.local.ini
|
||||
variants/*/platformio.ini
|
||||
platformio.local.ini
|
||||
|
||||
[arduino_base]
|
||||
framework = arduino
|
||||
@@ -66,7 +66,7 @@ build_src_filter = ${arduino_base.build_src_filter}
|
||||
|
||||
[esp32_ota]
|
||||
lib_deps =
|
||||
me-no-dev/ESPAsyncWebServer @ ^3.6.0
|
||||
ESP32Async/ESPAsyncWebServer @ 3.10.3
|
||||
file://arch/esp32/AsyncElegantOTA
|
||||
|
||||
; esp32c6 uses arduino framework 3.x
|
||||
@@ -81,17 +81,18 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/
|
||||
extends = arduino_base
|
||||
platform = nordicnrf52
|
||||
platform_packages =
|
||||
framework-arduinoadafruitnrf52 @ 1.10700.0
|
||||
extra_scripts =
|
||||
create-uf2.py
|
||||
arch/nrf52/extra_scripts/patch_bluefruit.py
|
||||
; use internal fork that includes patch to ble stack to prevent firmware lockup during rapid connect/disconnect
|
||||
; https://github.com/meshcore-dev/MeshCore/pull/1177
|
||||
; https://github.com/meshcore-dev/MeshCore/pull/1295
|
||||
framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301
|
||||
extra_scripts = create-uf2.py
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
-D NRF52_PLATFORM
|
||||
-D LFS_NO_ASSERT=1
|
||||
-D EXTRAFS=1
|
||||
lib_deps =
|
||||
${arduino_base.lib_deps}
|
||||
https://github.com/oltaco/CustomLFS @ 0.2.1
|
||||
https://github.com/oltaco/CustomLFS#0.2.2
|
||||
; ----------------- RP2040 ---------------------
|
||||
|
||||
[rp2040_base]
|
||||
@@ -151,3 +152,17 @@ lib_deps =
|
||||
stevemarple/MicroNMEA @ ^2.0.6
|
||||
adafruit/Adafruit BME680 Library @ ^2.0.4
|
||||
adafruit/Adafruit BMP085 Library @ ^1.2.4
|
||||
|
||||
; ----------------- TESTING ---------------------
|
||||
|
||||
[env:native]
|
||||
platform = native
|
||||
build_flags = -std=c++17
|
||||
-I src
|
||||
-I test/mocks
|
||||
test_build_src = yes
|
||||
build_src_filter =
|
||||
-<*>
|
||||
+<../src/Utils.cpp>
|
||||
lib_deps =
|
||||
google/googletest @ 1.17.0
|
||||
|
||||
+2
-1
@@ -193,8 +193,9 @@ public:
|
||||
bool millisHasNowPassed(unsigned long timestamp) const;
|
||||
unsigned long futureMillis(int millis_from_now) const;
|
||||
|
||||
private:
|
||||
bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len);
|
||||
|
||||
private:
|
||||
void checkRecv();
|
||||
void checkSend();
|
||||
};
|
||||
|
||||
+8
-11
@@ -361,13 +361,10 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
|
||||
|
||||
void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
|
||||
if (!packet->isMarkedDoNotRetransmit()) {
|
||||
uint32_t crc;
|
||||
memcpy(&crc, packet->payload, 4);
|
||||
|
||||
uint8_t extra = getExtraAckTransmitCount();
|
||||
while (extra > 0) {
|
||||
delay_millis += getDirectRetransmitDelay(packet) + 300;
|
||||
auto a1 = createMultiAck(crc, extra);
|
||||
auto a1 = createMultiAck(packet->payload, packet->payload_len, extra);
|
||||
if (a1) {
|
||||
a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len);
|
||||
a1->header &= ~PH_ROUTE_MASK;
|
||||
@@ -377,7 +374,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
|
||||
extra--;
|
||||
}
|
||||
|
||||
auto a2 = createAck(crc);
|
||||
auto a2 = createAck(packet->payload, packet->payload_len);
|
||||
if (a2) {
|
||||
a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len);
|
||||
a2->header &= ~PH_ROUTE_MASK;
|
||||
@@ -543,7 +540,7 @@ Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, con
|
||||
return packet;
|
||||
}
|
||||
|
||||
Packet* Mesh::createAck(uint32_t ack_crc) {
|
||||
Packet* Mesh::createAck(const uint8_t* ack, uint8_t len) {
|
||||
Packet* packet = obtainNewPacket();
|
||||
if (packet == NULL) {
|
||||
MESH_DEBUG_PRINTLN("%s Mesh::createAck(): error, packet pool empty", getLogDateTime());
|
||||
@@ -551,13 +548,13 @@ Packet* Mesh::createAck(uint32_t ack_crc) {
|
||||
}
|
||||
packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
|
||||
|
||||
memcpy(packet->payload, &ack_crc, 4);
|
||||
packet->payload_len = 4;
|
||||
memcpy(packet->payload, ack, len);
|
||||
packet->payload_len = len;
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
|
||||
Packet* Mesh::createMultiAck(const uint8_t* ack, uint8_t len, uint8_t remaining) {
|
||||
Packet* packet = obtainNewPacket();
|
||||
if (packet == NULL) {
|
||||
MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime());
|
||||
@@ -566,8 +563,8 @@ Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
|
||||
packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
|
||||
|
||||
packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK;
|
||||
memcpy(&packet->payload[1], &ack_crc, 4);
|
||||
packet->payload_len = 5;
|
||||
memcpy(&packet->payload[1], ack, len);
|
||||
packet->payload_len = 1 + len;
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
+4
-2
@@ -185,8 +185,10 @@ public:
|
||||
Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len);
|
||||
Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len);
|
||||
Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len);
|
||||
Packet* createAck(uint32_t ack_crc);
|
||||
Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining);
|
||||
Packet* createAck(const uint8_t* ack, uint8_t len);
|
||||
Packet* createAck(uint32_t ack_crc) { return createAck((uint8_t *) &ack_crc, 4); }
|
||||
Packet* createMultiAck(const uint8_t* ack, uint8_t len, uint8_t remaining);
|
||||
Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining) { return createMultiAck((uint8_t *)&ack_crc, 4, remaining); }
|
||||
Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
|
||||
Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
|
||||
Packet* createRawData(const uint8_t* data, size_t len);
|
||||
|
||||
@@ -27,9 +27,11 @@ bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) {
|
||||
}
|
||||
|
||||
void AutoDiscoverRTCClock::begin(TwoWire& wire) {
|
||||
#if !defined(DISABLE_DS3231_PROBE)
|
||||
if (i2c_probe(wire, DS3231_ADDRESS)) {
|
||||
ds3231_success = rtc_3231.begin(&wire);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (i2c_probe(wire, RV3028_ADDRESS)) {
|
||||
rtc_rv3028.initI2C(wire);
|
||||
|
||||
@@ -38,19 +38,19 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl
|
||||
return createAdvert(self_id, app_data, app_data_len);
|
||||
}
|
||||
|
||||
void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
|
||||
void BaseChatMesh::sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, uint8_t ack_len) {
|
||||
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
|
||||
mesh::Packet* ack = createAck(ack_hash);
|
||||
mesh::Packet* ack = createAck(ack_hash, ack_len);
|
||||
if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY);
|
||||
} else {
|
||||
uint32_t d = TXT_ACK_DELAY;
|
||||
if (getExtraAckTransmitCount() > 0) {
|
||||
mesh::Packet* a1 = createMultiAck(ack_hash, 1);
|
||||
mesh::Packet* a1 = createMultiAck(ack_hash, ack_len, 1);
|
||||
if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d);
|
||||
d += 300;
|
||||
}
|
||||
|
||||
mesh::Packet* a2 = createAck(ack_hash);
|
||||
mesh::Packet* a2 = createAck(ack_hash, ack_len);
|
||||
if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d);
|
||||
}
|
||||
}
|
||||
@@ -218,16 +218,20 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
||||
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
|
||||
onMessageRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
|
||||
|
||||
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
|
||||
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), from.id.pub_key, PUB_KEY_SIZE);
|
||||
int text_len = strlen((char *)&data[5]);
|
||||
uint8_t ack_hash[6]; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
|
||||
mesh::Utils::sha256(ack_hash, 4, data, 5 + text_len, from.id.pub_key, PUB_KEY_SIZE);
|
||||
// NEW: append (potential) extended attempt byte (to make packethash unique)
|
||||
ack_hash[4] = data[5 + text_len + 1];
|
||||
getRNG()->random(&ack_hash[5], 1); // make 6th byte random
|
||||
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
|
||||
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
|
||||
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 6);
|
||||
if (path) sendFloodScoped(from, path, TXT_ACK_DELAY);
|
||||
} else {
|
||||
sendAckTo(from, ack_hash);
|
||||
sendAckTo(from, ack_hash, 6);
|
||||
}
|
||||
} else if (flags == TXT_TYPE_CLI_DATA) {
|
||||
onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
|
||||
@@ -254,7 +258,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
||||
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
|
||||
if (path) sendFloodScoped(from, path, TXT_ACK_DELAY);
|
||||
} else {
|
||||
sendAckTo(from, ack_hash);
|
||||
sendAckTo(from, (uint8_t *) &ack_hash);
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported message type: %u", (uint32_t) flags);
|
||||
|
||||
@@ -72,7 +72,7 @@ class BaseChatMesh : public mesh::Mesh {
|
||||
ConnectionInfo connections[MAX_CONNECTIONS];
|
||||
|
||||
mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack);
|
||||
void sendAckTo(const ContactInfo& dest, uint32_t ack_hash);
|
||||
void sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, uint8_t ack_len=4);
|
||||
|
||||
protected:
|
||||
BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "CommonCLI.h"
|
||||
#include "TxtDataHelpers.h"
|
||||
#include "AdvertDataHelpers.h"
|
||||
#include "TxtDataHelpers.h"
|
||||
#include <RTClib.h>
|
||||
|
||||
#ifndef BRIDGE_MAX_BAUD
|
||||
@@ -285,7 +286,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
// change admin password
|
||||
StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password));
|
||||
savePrefs();
|
||||
sprintf(reply, "password now: %s", _prefs->password); // echo back just to let admin know for sure!!
|
||||
sprintf(reply, "password now: ");
|
||||
StrHelper::strncpy(&reply[14], _prefs->password, 160-15); // echo back just to let admin know for sure!!
|
||||
} else if (memcmp(command, "clear stats", 11) == 0) {
|
||||
_callbacks->clearStats();
|
||||
strcpy(reply, "(OK - stats reset)");
|
||||
@@ -580,21 +582,21 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(config, "rxdelay ", 8) == 0) {
|
||||
float db = atof(&config[8]);
|
||||
if (db >= 0) {
|
||||
if (db >= 0 && db <= 20.0f) {
|
||||
_prefs->rx_delay_base = db;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, cannot be negative");
|
||||
strcpy(reply, "Error, must be 0-20");
|
||||
}
|
||||
} else if (memcmp(config, "txdelay ", 8) == 0) {
|
||||
float f = atof(&config[8]);
|
||||
if (f >= 0) {
|
||||
if (f >= 0 && f <= 2.0f) {
|
||||
_prefs->tx_delay_factor = f;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, cannot be negative");
|
||||
strcpy(reply, "Error, must be 0-2");
|
||||
}
|
||||
} else if (memcmp(config, "flood.max ", 10) == 0) {
|
||||
uint8_t m = atoi(&config[10]);
|
||||
@@ -607,12 +609,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
}
|
||||
} else if (memcmp(config, "direct.txdelay ", 15) == 0) {
|
||||
float f = atof(&config[15]);
|
||||
if (f >= 0) {
|
||||
if (f >= 0 && f <= 2.0f) {
|
||||
_prefs->direct_tx_delay_factor = f;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, cannot be negative");
|
||||
strcpy(reply, "Error, must be 0-2");
|
||||
}
|
||||
} else if (memcmp(config, "owner.info ", 11) == 0) {
|
||||
config += 11;
|
||||
@@ -726,7 +728,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
strcpy(reply, "Error: unsupported by this board");
|
||||
};
|
||||
} else {
|
||||
sprintf(reply, "unknown config: %s", config);
|
||||
strcpy(reply, "unknown config: ");
|
||||
StrHelper::strncpy(&reply[16], config, 160-17);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,10 +787,11 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else if (memcmp(config, "direct.txdelay", 14) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
|
||||
} else if (memcmp(config, "owner.info", 10) == 0) {
|
||||
auto start = reply;
|
||||
*reply++ = '>';
|
||||
*reply++ = ' ';
|
||||
const char* sp = _prefs->owner_info;
|
||||
while (*sp) {
|
||||
while (*sp && reply - start < 159) {
|
||||
*reply++ = (*sp == '\n') ? '|' : *sp; // translate newline back to orig '|'
|
||||
sp++;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
#include <MeshCore.h>
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifndef USER_BTN_PRESSED
|
||||
#define USER_BTN_PRESSED LOW
|
||||
#endif
|
||||
|
||||
#if defined(ESP_PLATFORM)
|
||||
|
||||
#include <rom/rtc.h>
|
||||
|
||||
@@ -67,7 +67,7 @@ public:
|
||||
/*
|
||||
* The NRF52 has an internal DC/DC regulator that allows increased efficiency
|
||||
* compared to the LDO regulator. For being able to use it, the module/board
|
||||
* needs to have the required inductors and and capacitors populated. If the
|
||||
* needs to have the required inductors and capacitors populated. If the
|
||||
* hardware requirements are met, this subclass can be used to enable the DC/DC
|
||||
* regulator.
|
||||
*/
|
||||
|
||||
@@ -176,7 +176,7 @@ int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[],
|
||||
_store->getAutoKeyFor(src.id, src.name, dest[0]);
|
||||
num = 1;
|
||||
} else { // new: implicit auto hashtag region
|
||||
char tmp[sizeof(src.name)];
|
||||
char tmp[sizeof(src.name)+1];
|
||||
tmp[0] = '#';
|
||||
strcpy(&tmp[1], src.name);
|
||||
_store->getAutoKeyFor(src.id, tmp, dest[0]);
|
||||
|
||||
@@ -6,22 +6,17 @@
|
||||
#include <FS.h>
|
||||
#endif
|
||||
|
||||
#define MAX_PACKET_HASHES 128
|
||||
#define MAX_PACKET_ACKS 64
|
||||
#define MAX_PACKET_HASHES (128+32)
|
||||
|
||||
class SimpleMeshTables : public mesh::MeshTables {
|
||||
uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE];
|
||||
int _next_idx;
|
||||
uint32_t _acks[MAX_PACKET_ACKS];
|
||||
int _next_ack_idx;
|
||||
uint32_t _direct_dups, _flood_dups;
|
||||
|
||||
public:
|
||||
SimpleMeshTables() {
|
||||
memset(_hashes, 0, sizeof(_hashes));
|
||||
_next_idx = 0;
|
||||
memset(_acks, 0, sizeof(_acks));
|
||||
_next_ack_idx = 0;
|
||||
_direct_dups = _flood_dups = 0;
|
||||
}
|
||||
|
||||
@@ -29,37 +24,14 @@ public:
|
||||
void restoreFrom(File f) {
|
||||
f.read(_hashes, sizeof(_hashes));
|
||||
f.read((uint8_t *) &_next_idx, sizeof(_next_idx));
|
||||
f.read((uint8_t *) &_acks[0], sizeof(_acks));
|
||||
f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx));
|
||||
}
|
||||
void saveTo(File f) {
|
||||
f.write(_hashes, sizeof(_hashes));
|
||||
f.write((const uint8_t *) &_next_idx, sizeof(_next_idx));
|
||||
f.write((const uint8_t *) &_acks[0], sizeof(_acks));
|
||||
f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx));
|
||||
}
|
||||
#endif
|
||||
|
||||
bool hasSeen(const mesh::Packet* packet) override {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
|
||||
uint32_t ack;
|
||||
memcpy(&ack, packet->payload, 4);
|
||||
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
|
||||
if (ack == _acks[i]) {
|
||||
if (packet->isRouteDirect()) {
|
||||
_direct_dups++; // keep some stats
|
||||
} else {
|
||||
_flood_dups++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_acks[_next_ack_idx] = ack;
|
||||
_next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; // cyclic table
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t hash[MAX_HASH_SIZE];
|
||||
packet->calculatePacketHash(hash);
|
||||
|
||||
@@ -81,25 +53,14 @@ public:
|
||||
}
|
||||
|
||||
void clear(const mesh::Packet* packet) override {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
|
||||
uint32_t ack;
|
||||
memcpy(&ack, packet->payload, 4);
|
||||
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
|
||||
if (ack == _acks[i]) {
|
||||
_acks[i] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uint8_t hash[MAX_HASH_SIZE];
|
||||
packet->calculatePacketHash(hash);
|
||||
uint8_t hash[MAX_HASH_SIZE];
|
||||
packet->calculatePacketHash(hash);
|
||||
|
||||
uint8_t* sp = _hashes;
|
||||
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
|
||||
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
|
||||
memset(sp, 0, MAX_HASH_SIZE);
|
||||
break;
|
||||
}
|
||||
uint8_t* sp = _hashes;
|
||||
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
|
||||
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
|
||||
memset(sp, 0, MAX_HASH_SIZE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ void ESPNOWRadio::init() {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ESPNOWRadio::getRngSeed() {
|
||||
return millis() + intID(); // TODO: where to get some entropy?
|
||||
}
|
||||
|
||||
void ESPNOWRadio::setTxPower(uint8_t dbm) {
|
||||
esp_wifi_set_max_tx_power(dbm * 4);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,13 @@ protected:
|
||||
public:
|
||||
ESPNOWRadio() { n_recv = n_sent = n_recv_errors = 0; }
|
||||
|
||||
uint32_t getRngSeed();
|
||||
|
||||
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
// no-op
|
||||
}
|
||||
void powerOff() { /* no-op */ }
|
||||
|
||||
void init();
|
||||
int recvRaw(uint8_t* bytes, int sz) override;
|
||||
uint32_t getEstAirtimeFor(int len_bytes) override;
|
||||
|
||||
@@ -181,6 +181,13 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
|
||||
bleuart.begin();
|
||||
bleuart.setRxCallback(onBleUartRX);
|
||||
|
||||
|
||||
|
||||
// Register DFU on the main BLE stack so paired clients can discover it
|
||||
// without switching the device into a separate OTA-only BLE mode first.
|
||||
bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
|
||||
bledfu.begin();
|
||||
|
||||
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
|
||||
Bluefruit.Advertising.addTxPower();
|
||||
Bluefruit.Advertising.addService(bleuart);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#endif
|
||||
|
||||
class SerialBLEInterface : public BaseSerialInterface {
|
||||
BLEDfu bledfu;
|
||||
BLEUart bleuart;
|
||||
bool _isEnabled;
|
||||
bool _isDeviceConnected;
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
class CustomLLCC68Wrapper : public RadioLibWrapper {
|
||||
public:
|
||||
CustomLLCC68Wrapper(CustomLLCC68& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
((CustomLLCC68 *)_radio)->setFrequency(freq);
|
||||
((CustomLLCC68 *)_radio)->setSpreadingFactor(sf);
|
||||
((CustomLLCC68 *)_radio)->setBandwidth(bw);
|
||||
((CustomLLCC68 *)_radio)->setCodingRate(cr);
|
||||
updatePreamble(sf);
|
||||
}
|
||||
|
||||
bool isReceivingPacket() override {
|
||||
return ((CustomLLCC68 *)_radio)->isReceiving();
|
||||
}
|
||||
@@ -20,6 +29,7 @@ public:
|
||||
int sf = ((CustomLLCC68 *)_radio)->spreadingFactor;
|
||||
return packetScoreInt(snr, sf, packet_len);
|
||||
}
|
||||
uint8_t getSpreadingFactor() const override { return ((CustomLLCC68 *)_radio)->spreadingFactor; }
|
||||
|
||||
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
|
||||
|
||||
|
||||
@@ -36,4 +36,6 @@ class CustomLR1110 : public LR1110 {
|
||||
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));
|
||||
return detected;
|
||||
}
|
||||
|
||||
uint8_t getSpreadingFactor() const { return spreadingFactor; }
|
||||
};
|
||||
@@ -7,6 +7,15 @@
|
||||
class CustomLR1110Wrapper : public RadioLibWrapper {
|
||||
public:
|
||||
CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
((CustomLR1110 *)_radio)->setFrequency(freq);
|
||||
((CustomLR1110 *)_radio)->setSpreadingFactor(sf);
|
||||
((CustomLR1110 *)_radio)->setBandwidth(bw);
|
||||
((CustomLR1110 *)_radio)->setCodingRate(cr);
|
||||
updatePreamble(sf);
|
||||
}
|
||||
|
||||
void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); }
|
||||
bool isReceivingPacket() override {
|
||||
return ((CustomLR1110 *)_radio)->isReceiving();
|
||||
@@ -19,12 +28,14 @@ public:
|
||||
|
||||
void onSendFinished() override {
|
||||
RadioLibWrapper::onSendFinished();
|
||||
_radio->setPreambleLength(16); // overcomes weird issues with small and big pkts
|
||||
_radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts
|
||||
}
|
||||
|
||||
float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); }
|
||||
float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); }
|
||||
|
||||
uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); }
|
||||
|
||||
void setRxBoostedGainMode(bool en) override {
|
||||
((CustomLR1110 *)_radio)->setRxBoostedGainMode(en);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
class CustomSTM32WLxWrapper : public RadioLibWrapper {
|
||||
public:
|
||||
CustomSTM32WLxWrapper(CustomSTM32WLx& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
((CustomSTM32WLx *)_radio)->setFrequency(freq);
|
||||
((CustomSTM32WLx *)_radio)->setSpreadingFactor(sf);
|
||||
((CustomSTM32WLx *)_radio)->setBandwidth(bw);
|
||||
((CustomSTM32WLx *)_radio)->setCodingRate(cr);
|
||||
updatePreamble(sf);
|
||||
}
|
||||
|
||||
bool isReceivingPacket() override {
|
||||
return ((CustomSTM32WLx *)_radio)->isReceiving();
|
||||
}
|
||||
@@ -21,6 +30,7 @@ public:
|
||||
int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor;
|
||||
return packetScoreInt(snr, sf, packet_len);
|
||||
}
|
||||
uint8_t getSpreadingFactor() const override { return ((CustomSTM32WLx *)_radio)->spreadingFactor; }
|
||||
|
||||
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
|
||||
};
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
class CustomSX1262Wrapper : public RadioLibWrapper {
|
||||
public:
|
||||
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
((CustomSX1262 *)_radio)->setFrequency(freq);
|
||||
((CustomSX1262 *)_radio)->setSpreadingFactor(sf);
|
||||
((CustomSX1262 *)_radio)->setBandwidth(bw);
|
||||
((CustomSX1262 *)_radio)->setCodingRate(cr);
|
||||
updatePreamble(sf);
|
||||
}
|
||||
|
||||
bool isReceivingPacket() override {
|
||||
return ((CustomSX1262 *)_radio)->isReceiving();
|
||||
}
|
||||
@@ -24,6 +33,7 @@ public:
|
||||
int sf = ((CustomSX1262 *)_radio)->spreadingFactor;
|
||||
return packetScoreInt(snr, sf, packet_len);
|
||||
}
|
||||
uint8_t getSpreadingFactor() const override { return ((CustomSX1262 *)_radio)->spreadingFactor; }
|
||||
virtual void powerOff() override {
|
||||
((CustomSX1262 *)_radio)->sleep(false);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
class CustomSX1268Wrapper : public RadioLibWrapper {
|
||||
public:
|
||||
CustomSX1268Wrapper(CustomSX1268& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
((CustomSX1268 *)_radio)->setFrequency(freq);
|
||||
((CustomSX1268 *)_radio)->setSpreadingFactor(sf);
|
||||
((CustomSX1268 *)_radio)->setBandwidth(bw);
|
||||
((CustomSX1268 *)_radio)->setCodingRate(cr);
|
||||
updatePreamble(sf);
|
||||
}
|
||||
|
||||
bool isReceivingPacket() override {
|
||||
return ((CustomSX1268 *)_radio)->isReceiving();
|
||||
}
|
||||
@@ -24,6 +33,7 @@ public:
|
||||
int sf = ((CustomSX1268 *)_radio)->spreadingFactor;
|
||||
return packetScoreInt(snr, sf, packet_len);
|
||||
}
|
||||
uint8_t getSpreadingFactor() const override { return ((CustomSX1268 *)_radio)->spreadingFactor; }
|
||||
|
||||
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
|
||||
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
class CustomSX1276Wrapper : public RadioLibWrapper {
|
||||
public:
|
||||
CustomSX1276Wrapper(CustomSX1276& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
((CustomSX1276 *)_radio)->setFrequency(freq);
|
||||
((CustomSX1276 *)_radio)->setSpreadingFactor(sf);
|
||||
((CustomSX1276 *)_radio)->setBandwidth(bw);
|
||||
((CustomSX1276 *)_radio)->setCodingRate(cr);
|
||||
updatePreamble(sf);
|
||||
}
|
||||
|
||||
bool isReceivingPacket() override {
|
||||
return ((CustomSX1276 *)_radio)->isReceiving();
|
||||
}
|
||||
@@ -23,4 +32,5 @@ public:
|
||||
int sf = ((CustomSX1276 *)_radio)->spreadingFactor;
|
||||
return packetScoreInt(snr, sf, packet_len);
|
||||
}
|
||||
uint8_t getSpreadingFactor() const override { return ((CustomSX1276 *)_radio)->spreadingFactor; }
|
||||
};
|
||||
|
||||
@@ -26,6 +26,8 @@ void setFlag(void) {
|
||||
|
||||
void RadioLibWrapper::begin() {
|
||||
_radio->setPacketReceivedAction(setFlag); // this is also SentComplete interrupt
|
||||
_preamble_sf = getSpreadingFactor();
|
||||
_radio->setPreambleLength(preambleLengthForSF(_preamble_sf)); // longer preamble for lower SF improves reliability
|
||||
state = STATE_IDLE;
|
||||
|
||||
if (_board->getStartupReason() == BD_STARTUP_RX_PACKET) { // received a LoRa packet (while in deep sleep)
|
||||
@@ -40,6 +42,14 @@ void RadioLibWrapper::begin() {
|
||||
_floor_sample_sum = 0;
|
||||
}
|
||||
|
||||
uint32_t RadioLibWrapper::getRngSeed() {
|
||||
return _radio->random(0x7FFFFFFF);
|
||||
}
|
||||
|
||||
void RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||
_radio->setOutputPower(dbm);
|
||||
}
|
||||
|
||||
void RadioLibWrapper::idle() {
|
||||
_radio->standby();
|
||||
state = STATE_IDLE; // need another startReceive()
|
||||
|
||||
@@ -11,6 +11,7 @@ protected:
|
||||
int16_t _noise_floor, _threshold;
|
||||
uint16_t _num_floor_samples;
|
||||
int32_t _floor_sample_sum;
|
||||
uint8_t _preamble_sf;
|
||||
|
||||
void idle();
|
||||
void startRecv();
|
||||
@@ -19,7 +20,7 @@ protected:
|
||||
virtual void doResetAGC();
|
||||
|
||||
public:
|
||||
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; }
|
||||
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; }
|
||||
|
||||
void begin() override;
|
||||
virtual void powerOff() { _radio->sleep(); }
|
||||
@@ -37,7 +38,14 @@ public:
|
||||
return isChannelActive();
|
||||
}
|
||||
|
||||
virtual void setParams(float freq, float bw, uint8_t sf, uint8_t cr) = 0;
|
||||
uint32_t getRngSeed();
|
||||
void setTxPower(int8_t dbm);
|
||||
|
||||
virtual float getCurrentRSSI() =0;
|
||||
virtual uint8_t getSpreadingFactor() const { return LORA_SF; }
|
||||
static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; }
|
||||
void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); }
|
||||
|
||||
int getNoiseFloor() const override { return _noise_floor; }
|
||||
void triggerNoiseFloorCalibrate(int threshold) override;
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
#include "EnvironmentSensorManager.h"
|
||||
|
||||
#include <Wire.h>
|
||||
|
||||
#if ENV_PIN_SDA && ENV_PIN_SCL
|
||||
#define TELEM_WIRE &Wire1 // Use Wire1 as the I2C bus for Environment Sensors
|
||||
#else
|
||||
#define TELEM_WIRE &Wire // Use default I2C bus for Environment Sensors
|
||||
#endif
|
||||
|
||||
// ============================================================
|
||||
// Sensor library includes and static driver instances
|
||||
// ============================================================
|
||||
|
||||
#ifdef ENV_INCLUDE_BME680
|
||||
#ifndef TELEM_BME680_ADDRESS
|
||||
#define TELEM_BME680_ADDRESS 0x76
|
||||
@@ -31,7 +37,7 @@ static Adafruit_AHTX0 AHTX0;
|
||||
#ifndef TELEM_BME280_ADDRESS
|
||||
#define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address
|
||||
#endif
|
||||
#define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level
|
||||
#define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level
|
||||
#include <Adafruit_BME280.h>
|
||||
static Adafruit_BME280 BME280;
|
||||
#endif
|
||||
@@ -40,7 +46,7 @@ static Adafruit_BME280 BME280;
|
||||
#ifndef TELEM_BMP280_ADDRESS
|
||||
#define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address
|
||||
#endif
|
||||
#define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level
|
||||
#define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level
|
||||
#include <Adafruit_BMP280.h>
|
||||
static Adafruit_BMP280 BMP280(TELEM_WIRE);
|
||||
#endif
|
||||
@@ -51,7 +57,7 @@ static Adafruit_SHTC3 SHTC3;
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_SHT4X
|
||||
#define TELEM_SHT4X_ADDRESS 0x44 //0x44 - 0x46
|
||||
#define TELEM_SHT4X_ADDRESS 0x44
|
||||
#include <SensirionI2cSht4x.h>
|
||||
static SensirionI2cSht4x SHT4X;
|
||||
#endif
|
||||
@@ -62,9 +68,15 @@ LPS22HBClass LPS22HB(*TELEM_WIRE);
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA3221
|
||||
#define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address
|
||||
#ifndef TELEM_INA3221_ADDRESS
|
||||
#define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address
|
||||
#endif
|
||||
#ifndef TELEM_INA3221_SHUNT_VALUE
|
||||
#define TELEM_INA3221_SHUNT_VALUE 0.100 // most variants will have a 0.1 ohm shunts
|
||||
#endif
|
||||
#ifndef TELEM_INA3221_NUM_CHANNELS
|
||||
#define TELEM_INA3221_NUM_CHANNELS 3
|
||||
#endif
|
||||
#include <Adafruit_INA3221.h>
|
||||
static Adafruit_INA3221 INA3221;
|
||||
#endif
|
||||
@@ -82,9 +94,9 @@ static Adafruit_INA260 INA260;
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA226
|
||||
#define TELEM_INA226_ADDRESS 0x44
|
||||
#define TELEM_INA226_ADDRESS 0x44
|
||||
#define TELEM_INA226_SHUNT_VALUE 0.100
|
||||
#define TELEM_INA226_MAX_AMP 0.8
|
||||
#define TELEM_INA226_MAX_AMP 0.8
|
||||
#include <INA226.h>
|
||||
static INA226 INA226(TELEM_INA226_ADDRESS, TELEM_WIRE);
|
||||
#endif
|
||||
@@ -155,10 +167,352 @@ public:
|
||||
static RAK12500LocationProvider RAK12500_provider;
|
||||
#endif
|
||||
|
||||
// ============================================================
|
||||
// I2C bus scanner
|
||||
// Probes every valid address and records which ones ACK.
|
||||
// This runs before any sensor library is touched, so a missing
|
||||
// or misbehaving device cannot stall or crash the boot sequence.
|
||||
// ============================================================
|
||||
|
||||
static void scanI2CBus(TwoWire* wire, bool found[128]) {
|
||||
for (uint8_t addr = 0x08; addr < 0x78; addr++) {
|
||||
wire->beginTransmission(addr);
|
||||
found[addr] = (wire->endTransmission() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Per-sensor init and query functions
|
||||
//
|
||||
// init(wire, address) — called only when the address was seen
|
||||
// on the bus. Returns 0 on failure, or the number of
|
||||
// telemetry channels the sensor will consume (1 for all
|
||||
// single-output sensors; INA3221 returns one per enabled
|
||||
// hardware channel; MLX90614 and RAK12035+calibration
|
||||
// return 2).
|
||||
//
|
||||
// query(channel, sub_channel, lpp) — called once per active
|
||||
// sensor entry during querySensors(). sub_channel is always
|
||||
// 0 for single-output sensors.
|
||||
// ============================================================
|
||||
|
||||
#if ENV_INCLUDE_AHTX0
|
||||
static uint8_t init_ahtx0(TwoWire* wire, uint8_t addr) {
|
||||
return AHTX0.begin(wire, 0, addr) ? 1 : 0;
|
||||
}
|
||||
static void query_ahtx0(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
sensors_event_t humidity, temp;
|
||||
AHTX0.getEvent(&humidity, &temp);
|
||||
lpp.addTemperature(ch, temp.temperature);
|
||||
lpp.addRelativeHumidity(ch, humidity.relative_humidity);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENV_INCLUDE_BME680
|
||||
static uint8_t init_bme680(TwoWire*, uint8_t addr) {
|
||||
// Wire was set in the static constructor; begin() takes address only.
|
||||
return BME680.begin(addr) ? 1 : 0;
|
||||
}
|
||||
static void query_bme680(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
if (BME680.performReading()) {
|
||||
lpp.addTemperature(ch, BME680.temperature);
|
||||
lpp.addRelativeHumidity(ch, BME680.humidity);
|
||||
lpp.addBarometricPressure(ch, BME680.pressure / 100);
|
||||
lpp.addAltitude(ch, 44330.0 * (1.0 - pow((BME680.pressure / 100) / TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903)));
|
||||
lpp.addAnalogInput(ch, BME680.gas_resistance);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BME280
|
||||
static uint8_t init_bme280(TwoWire* wire, uint8_t addr) {
|
||||
if (!BME280.begin(addr, wire)) return 0;
|
||||
BME280.setSampling(Adafruit_BME280::MODE_FORCED,
|
||||
Adafruit_BME280::SAMPLING_X1,
|
||||
Adafruit_BME280::SAMPLING_X1,
|
||||
Adafruit_BME280::SAMPLING_X1,
|
||||
Adafruit_BME280::FILTER_OFF,
|
||||
Adafruit_BME280::STANDBY_MS_1000);
|
||||
return 1;
|
||||
}
|
||||
static void query_bme280(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
if (BME280.takeForcedMeasurement()) {
|
||||
lpp.addTemperature(ch, BME280.readTemperature());
|
||||
lpp.addRelativeHumidity(ch, BME280.readHumidity());
|
||||
lpp.addBarometricPressure(ch, BME280.readPressure() / 100);
|
||||
lpp.addAltitude(ch, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BMP280
|
||||
static uint8_t init_bmp280(TwoWire*, uint8_t addr) {
|
||||
// BMP280 static instance was constructed with TELEM_WIRE; begin() uses it.
|
||||
return BMP280.begin(addr) ? 1 : 0;
|
||||
}
|
||||
static void query_bmp280(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
lpp.addTemperature(ch, BMP280.readTemperature());
|
||||
lpp.addBarometricPressure(ch, BMP280.readPressure() / 100);
|
||||
lpp.addAltitude(ch, BMP280.readAltitude(TELEM_BMP280_SEALEVELPRESSURE_HPA));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_SHTC3
|
||||
static uint8_t init_shtc3(TwoWire* wire, uint8_t) {
|
||||
// Adafruit_SHTC3::begin() does not accept an address (fixed at 0x70).
|
||||
return SHTC3.begin(wire) ? 1 : 0;
|
||||
}
|
||||
static void query_shtc3(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
sensors_event_t humidity, temp;
|
||||
SHTC3.getEvent(&humidity, &temp);
|
||||
lpp.addTemperature(ch, temp.temperature);
|
||||
lpp.addRelativeHumidity(ch, humidity.relative_humidity);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_SHT4X
|
||||
static uint8_t init_sht4x(TwoWire* wire, uint8_t addr) {
|
||||
// SensirionI2cSht4x::begin() does not probe the hardware; use serialNumber()
|
||||
// as the actual presence check since it performs a real I2C transaction.
|
||||
SHT4X.begin(*wire, addr);
|
||||
uint32_t serial = 0;
|
||||
return (SHT4X.serialNumber(serial) == 0) ? 1 : 0;
|
||||
}
|
||||
static void query_sht4x(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
float temperature, humidity;
|
||||
if (SHT4X.measureLowestPrecision(temperature, humidity) == 0) {
|
||||
lpp.addTemperature(ch, temperature);
|
||||
lpp.addRelativeHumidity(ch, humidity);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_LPS22HB
|
||||
static uint8_t init_lps22hb(TwoWire*, uint8_t) {
|
||||
// LPS22HBClass is constructed with the wire reference; begin() uses it.
|
||||
return LPS22HB.begin() ? 1 : 0;
|
||||
}
|
||||
static void query_lps22hb(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
lpp.addTemperature(ch, LPS22HB.readTemperature());
|
||||
lpp.addBarometricPressure(ch, LPS22HB.readPressure() * 10); // convert kPa to hPa
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA3221
|
||||
static uint8_t init_ina3221(TwoWire* wire, uint8_t addr) {
|
||||
if (!INA3221.begin(addr, wire)) return 0;
|
||||
for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
|
||||
INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE);
|
||||
}
|
||||
// Each enabled hardware channel becomes its own telemetry channel.
|
||||
uint8_t enabled = 0;
|
||||
for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
|
||||
if (INA3221.isChannelEnabled(i)) enabled++;
|
||||
}
|
||||
return enabled > 0 ? enabled : 1;
|
||||
}
|
||||
static void query_ina3221(uint8_t ch, uint8_t sub_ch, CayenneLPP& lpp) {
|
||||
// sub_ch is the index of the nth enabled hardware channel.
|
||||
uint8_t seen = 0;
|
||||
for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
|
||||
if (INA3221.isChannelEnabled(i)) {
|
||||
if (seen == sub_ch) {
|
||||
float v = INA3221.getBusVoltage(i);
|
||||
float c = INA3221.getCurrentAmps(i);
|
||||
lpp.addVoltage(ch, v);
|
||||
lpp.addCurrent(ch, c);
|
||||
lpp.addPower(ch, v * c);
|
||||
return;
|
||||
}
|
||||
seen++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA219
|
||||
static uint8_t init_ina219(TwoWire* wire, uint8_t) {
|
||||
// INA219 static instance was constructed with the address; begin() uses it.
|
||||
return INA219.begin(wire) ? 1 : 0;
|
||||
}
|
||||
static void query_ina219(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
lpp.addVoltage(ch, INA219.getBusVoltage_V());
|
||||
lpp.addCurrent(ch, INA219.getCurrent_mA() / 1000.0f);
|
||||
lpp.addPower(ch, INA219.getPower_mW() / 1000.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA260
|
||||
static uint8_t init_ina260(TwoWire* wire, uint8_t addr) {
|
||||
return INA260.begin(addr, wire) ? 1 : 0;
|
||||
}
|
||||
static void query_ina260(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
lpp.addVoltage(ch, INA260.readBusVoltage() / 1000.0f);
|
||||
lpp.addCurrent(ch, INA260.readCurrent() / 1000.0f);
|
||||
lpp.addPower(ch, INA260.readPower() / 1000.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA226
|
||||
static uint8_t init_ina226(TwoWire*, uint8_t) {
|
||||
// INA226 static instance was constructed with address and wire.
|
||||
if (!INA226.begin()) return 0;
|
||||
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
|
||||
return 1;
|
||||
}
|
||||
static void query_ina226(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
lpp.addVoltage(ch, INA226.getBusVoltage());
|
||||
lpp.addCurrent(ch, INA226.getCurrent_mA() / 1000.0f);
|
||||
lpp.addPower(ch, INA226.getPower_mW() / 1000.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_MLX90614
|
||||
static uint8_t init_mlx90614(TwoWire* wire, uint8_t addr) {
|
||||
return MLX90614.begin(addr, wire) ? 2 : 0; // 2 channels: object temp, ambient temp
|
||||
}
|
||||
static void query_mlx90614(uint8_t ch, uint8_t sub_ch, CayenneLPP& lpp) {
|
||||
if (sub_ch == 0)
|
||||
lpp.addTemperature(ch, MLX90614.readObjectTempC());
|
||||
else
|
||||
lpp.addTemperature(ch, MLX90614.readAmbientTempC());
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_VL53L0X
|
||||
static uint8_t init_vl53l0x(TwoWire* wire, uint8_t addr) {
|
||||
return VL53L0X.begin(addr, false, wire) ? 1 : 0;
|
||||
}
|
||||
static void query_vl53l0x(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
VL53L0X_RangingMeasurementData_t measure;
|
||||
VL53L0X.rangingTest(&measure, false);
|
||||
lpp.addDistance(ch, measure.RangeStatus != 4 ? measure.RangeMilliMeter / 1000.0f : 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENV_INCLUDE_BMP085
|
||||
static uint8_t init_bmp085(TwoWire* wire, uint8_t) {
|
||||
return BMP085.begin(0, wire) ? 1 : 0; // mode 0 = ULTRALOWPOWER
|
||||
}
|
||||
static void query_bmp085(uint8_t ch, uint8_t, CayenneLPP& lpp) {
|
||||
lpp.addTemperature(ch, BMP085.readTemperature());
|
||||
lpp.addBarometricPressure(ch, BMP085.readPressure() / 100);
|
||||
lpp.addAltitude(ch, BMP085.readAltitude(TELEM_BMP085_SEALEVELPRESSURE_HPA * 100));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_RAK12035
|
||||
static uint8_t init_rak12035(TwoWire* wire, uint8_t addr) {
|
||||
// RAK12035 requires setup() before begin().
|
||||
RAK12035.setup(*wire);
|
||||
if (!RAK12035.begin(addr)) return 0;
|
||||
#ifdef ENABLE_RAK12035_CALIBRATION
|
||||
return 2; // moisture channel + calibration channel
|
||||
#else
|
||||
return 1;
|
||||
#endif
|
||||
}
|
||||
static void query_rak12035(uint8_t ch, uint8_t sub_ch, CayenneLPP& lpp) {
|
||||
if (sub_ch == 0) {
|
||||
lpp.addTemperature(ch, RAK12035.get_sensor_temperature());
|
||||
lpp.addPercentage(ch, RAK12035.get_sensor_moisture());
|
||||
} else {
|
||||
#ifdef ENABLE_RAK12035_CALIBRATION
|
||||
float cap = RAK12035.get_sensor_capacitance();
|
||||
float wet = RAK12035.get_humidity_full();
|
||||
float dry = RAK12035.get_humidity_zero();
|
||||
lpp.addFrequency(ch, cap);
|
||||
lpp.addTemperature(ch, wet);
|
||||
lpp.addPower(ch, dry);
|
||||
if (cap > dry) RAK12035.set_humidity_zero(cap);
|
||||
if (cap < wet) RAK12035.set_humidity_full(cap);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// ============================================================
|
||||
// Sensor descriptor table
|
||||
//
|
||||
// Each entry maps an I2C address to a sensor's init and query
|
||||
// functions. Only entries whose ENV_INCLUDE_* guard is defined
|
||||
// are compiled in. The sentinel at the end keeps the array
|
||||
// non-empty regardless of which sensors are enabled.
|
||||
//
|
||||
// Ordering here determines channel assignment at runtime:
|
||||
// the first detected+initialized sensor gets channel 2, the
|
||||
// next gets channel 3, and so on.
|
||||
// ============================================================
|
||||
|
||||
struct SensorDef {
|
||||
uint8_t address;
|
||||
const char* name;
|
||||
uint8_t (*init)(TwoWire* wire, uint8_t address);
|
||||
void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
|
||||
};
|
||||
|
||||
static const SensorDef SENSOR_TABLE[] = {
|
||||
#if ENV_INCLUDE_AHTX0
|
||||
{ TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0 },
|
||||
#endif
|
||||
#ifdef ENV_INCLUDE_BME680
|
||||
{ TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 },
|
||||
#endif
|
||||
#if ENV_INCLUDE_BME280
|
||||
{ TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 },
|
||||
#endif
|
||||
#if ENV_INCLUDE_BMP280
|
||||
{ TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 },
|
||||
#endif
|
||||
#if ENV_INCLUDE_SHTC3
|
||||
{ 0x70, "SHTC3", init_shtc3, query_shtc3 },
|
||||
#endif
|
||||
#if ENV_INCLUDE_SHT4X
|
||||
{ TELEM_SHT4X_ADDRESS, "SHT4X", init_sht4x, query_sht4x },
|
||||
#endif
|
||||
#if ENV_INCLUDE_LPS22HB
|
||||
{ 0x5C, "LPS22HB", init_lps22hb, query_lps22hb },
|
||||
#endif
|
||||
#if ENV_INCLUDE_INA3221
|
||||
{ TELEM_INA3221_ADDRESS, "INA3221", init_ina3221, query_ina3221 },
|
||||
#endif
|
||||
#if ENV_INCLUDE_INA219
|
||||
{ TELEM_INA219_ADDRESS, "INA219", init_ina219, query_ina219 },
|
||||
#endif
|
||||
#if ENV_INCLUDE_INA260
|
||||
{ TELEM_INA260_ADDRESS, "INA260", init_ina260, query_ina260 },
|
||||
#endif
|
||||
#if ENV_INCLUDE_INA226
|
||||
{ TELEM_INA226_ADDRESS, "INA226", init_ina226, query_ina226 },
|
||||
#endif
|
||||
#if ENV_INCLUDE_MLX90614
|
||||
{ TELEM_MLX90614_ADDRESS,"MLX90614", init_mlx90614, query_mlx90614 },
|
||||
#endif
|
||||
#if ENV_INCLUDE_VL53L0X
|
||||
{ TELEM_VL53L0X_ADDRESS, "VL53L0X", init_vl53l0x, query_vl53l0x },
|
||||
#endif
|
||||
#ifdef ENV_INCLUDE_BMP085
|
||||
{ 0x77, "BMP085", init_bmp085, query_bmp085 },
|
||||
#endif
|
||||
#if ENV_INCLUDE_RAK12035
|
||||
{ TELEM_RAK12035_ADDRESS,"RAK12035", init_rak12035, query_rak12035 },
|
||||
#endif
|
||||
{ 0, nullptr, nullptr, nullptr } // sentinel — keeps the array non-empty
|
||||
};
|
||||
|
||||
static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TABLE[0])) - 1;
|
||||
|
||||
// ============================================================
|
||||
// begin() — scan the I2C bus, then initialize only what was
|
||||
// found. A sensor whose address does not ACK during the scan
|
||||
// is never touched by a library call, preventing hangs or
|
||||
// crashes caused by absent or misbehaving hardware.
|
||||
// ============================================================
|
||||
|
||||
bool EnvironmentSensorManager::begin() {
|
||||
#if ENV_INCLUDE_GPS
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
rakGPSInit(); //probe base board/sockets for GPS
|
||||
rakGPSInit();
|
||||
#else
|
||||
initBasicGPS();
|
||||
#endif
|
||||
@@ -175,361 +529,52 @@ bool EnvironmentSensorManager::begin() {
|
||||
MESH_DEBUG_PRINTLN("Second I2C initialized on pins SDA: %d SCL: %d", ENV_PIN_SDA, ENV_PIN_SCL);
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_AHTX0
|
||||
if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
|
||||
MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS);
|
||||
AHTX0_initialized = true;
|
||||
} else {
|
||||
AHTX0_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("AHT10/AHT20 was not found at I2C address %02X", TELEM_AHTX_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
// Scan the I2C bus before touching any sensor library.
|
||||
bool detected[128] = {};
|
||||
scanI2CBus(TELEM_WIRE, detected);
|
||||
|
||||
#if ENV_INCLUDE_BME680
|
||||
if (BME680.begin(TELEM_BME680_ADDRESS)) {
|
||||
MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS);
|
||||
BME680_initialized = true;
|
||||
} else {
|
||||
BME680_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BME280
|
||||
if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS);
|
||||
MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID());
|
||||
// Reduce self-heating: single-shot conversions, light oversampling, long standby.
|
||||
BME280.setSampling(Adafruit_BME280::MODE_FORCED,
|
||||
Adafruit_BME280::SAMPLING_X1, // temperature
|
||||
Adafruit_BME280::SAMPLING_X1, // pressure
|
||||
Adafruit_BME280::SAMPLING_X1, // humidity
|
||||
Adafruit_BME280::FILTER_OFF,
|
||||
Adafruit_BME280::STANDBY_MS_1000);
|
||||
BME280_initialized = true;
|
||||
} else {
|
||||
BME280_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("BME280 was not found at I2C address %02X", TELEM_BME280_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BMP280
|
||||
if (BMP280.begin(TELEM_BMP280_ADDRESS)) {
|
||||
MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESS);
|
||||
MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID());
|
||||
BMP280_initialized = true;
|
||||
} else {
|
||||
BMP280_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("BMP280 was not found at I2C address %02X", TELEM_BMP280_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_SHTC3
|
||||
if (SHTC3.begin(TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found sensor: SHTC3");
|
||||
SHTC3_initialized = true;
|
||||
} else {
|
||||
SHTC3_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("SHTC3 was not found at I2C address %02X", 0x70);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#if ENV_INCLUDE_SHT4X
|
||||
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
|
||||
uint32_t serialNumber = 0;
|
||||
int16_t sht4x_error;
|
||||
sht4x_error = SHT4X.serialNumber(serialNumber);
|
||||
if (sht4x_error == 0) {
|
||||
MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS);
|
||||
SHT4X_initialized = true;
|
||||
} else {
|
||||
SHT4X_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_LPS22HB
|
||||
if (LPS22HB.begin()) {
|
||||
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
|
||||
LPS22HB_initialized = true;
|
||||
} else {
|
||||
LPS22HB_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("LPS22HB was not found at I2C address %02X", 0x5C);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA3221
|
||||
if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS);
|
||||
MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID());
|
||||
|
||||
for(int i = 0; i < 3; i++) {
|
||||
INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE);
|
||||
// Walk the sensor table and initialize only detected devices.
|
||||
_active_sensor_count = 0;
|
||||
for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; i++) {
|
||||
const SensorDef& def = SENSOR_TABLE[i];
|
||||
if (!detected[def.address]) {
|
||||
MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, def.address);
|
||||
continue;
|
||||
}
|
||||
uint8_t n = def.init(TELEM_WIRE, def.address);
|
||||
if (n == 0) {
|
||||
MESH_DEBUG_PRINTLN("%s found at %02X but failed to initialize", def.name, def.address);
|
||||
continue;
|
||||
}
|
||||
MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, def.address);
|
||||
for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) {
|
||||
_active_sensors[_active_sensor_count++] = { def.query, sub };
|
||||
}
|
||||
INA3221_initialized = true;
|
||||
} else {
|
||||
INA3221_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("INA3221 was not found at I2C address %02X", TELEM_INA3221_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA219
|
||||
if (INA219.begin(TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS);
|
||||
INA219_initialized = true;
|
||||
} else {
|
||||
INA219_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("INA219 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA260
|
||||
if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS);
|
||||
INA260_initialized = true;
|
||||
} else {
|
||||
INA260_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA260_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA226
|
||||
if (INA226.begin()) {
|
||||
MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS);
|
||||
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
|
||||
INA226_initialized = true;
|
||||
} else {
|
||||
INA226_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("INA226 was not found at I2C address %02X", TELEM_INA226_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_MLX90614
|
||||
if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS);
|
||||
MLX90614_initialized = true;
|
||||
} else {
|
||||
MLX90614_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("MLX90614 was not found at I2C address %02X", TELEM_MLX90614_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_VL53L0X
|
||||
if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS);
|
||||
VL53L0X_initialized = true;
|
||||
} else {
|
||||
VL53L0X_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("VL53L0X was not found at I2C address %02X", TELEM_VL53L0X_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BMP085
|
||||
// First argument is MODE (aka oversampling)
|
||||
// choose ULTRALOWPOWER
|
||||
if (BMP085.begin(0, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found sensor BMP085");
|
||||
BMP085_initialized = true;
|
||||
} else {
|
||||
BMP085_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("BMP085 was not found at I2C address %02X", 0x77);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_RAK12035
|
||||
RAK12035.setup(*TELEM_WIRE);
|
||||
if (RAK12035.begin(TELEM_RAK12035_ADDRESS)) {
|
||||
MESH_DEBUG_PRINTLN("Found sensor RAK12035 at address: %02X", TELEM_RAK12035_ADDRESS);
|
||||
RAK12035_initialized = true;
|
||||
} else {
|
||||
RAK12035_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("RAK12035 was not found at I2C address %02X", TELEM_RAK12035_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// querySensors() — GPS stays on channel 1; each active sensor
|
||||
// gets the next available channel in the order it was
|
||||
// initialized.
|
||||
// ============================================================
|
||||
|
||||
bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
|
||||
next_available_channel = TELEM_CHANNEL_SELF + 1;
|
||||
|
||||
if (requester_permissions & TELEM_PERM_LOCATION && gps_active) {
|
||||
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); // allow lat/lon via telemetry even if no GPS is detected
|
||||
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
|
||||
}
|
||||
|
||||
if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
|
||||
|
||||
#if ENV_INCLUDE_AHTX0
|
||||
if (AHTX0_initialized) {
|
||||
sensors_event_t humidity, temp;
|
||||
AHTX0.getEvent(&humidity, &temp);
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature);
|
||||
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BME680
|
||||
if (BME680_initialized) {
|
||||
if (BME680.performReading()) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME680.temperature);
|
||||
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME680.humidity);
|
||||
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME680.pressure / 100);
|
||||
telemetry.addAltitude(TELEM_CHANNEL_SELF, 44330.0 * (1.0 - pow((BME680.pressure / 100) / TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903)));
|
||||
telemetry.addAnalogInput(next_available_channel, BME680.gas_resistance);
|
||||
next_available_channel++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BME280
|
||||
if (BME280_initialized) {
|
||||
if (BME280.takeForcedMeasurement()) { // trigger a fresh reading in forced mode
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME280.readTemperature());
|
||||
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME280.readHumidity());
|
||||
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()/100);
|
||||
telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BMP280
|
||||
if (BMP280_initialized) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP280.readTemperature());
|
||||
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure()/100);
|
||||
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP280.readAltitude(TELEM_BMP280_SEALEVELPRESSURE_HPA));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_SHTC3
|
||||
if (SHTC3_initialized) {
|
||||
sensors_event_t humidity, temp;
|
||||
SHTC3.getEvent(&humidity, &temp);
|
||||
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature);
|
||||
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_SHT4X
|
||||
if (SHT4X_initialized) {
|
||||
float sht4x_humidity, sht4x_temperature;
|
||||
int16_t sht4x_error;
|
||||
sht4x_error = SHT4X.measureLowestPrecision(sht4x_temperature, sht4x_humidity);
|
||||
if (sht4x_error == 0) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, sht4x_temperature);
|
||||
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, sht4x_humidity);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_LPS22HB
|
||||
if (LPS22HB_initialized) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, LPS22HB.readTemperature());
|
||||
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, LPS22HB.readPressure() * 10); // convert kPa to hPa
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA3221
|
||||
if (INA3221_initialized) {
|
||||
for(int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
|
||||
// add only enabled INA3221 channels to telemetry
|
||||
if (INA3221.isChannelEnabled(i)) {
|
||||
float voltage = INA3221.getBusVoltage(i);
|
||||
float current = INA3221.getCurrentAmps(i);
|
||||
telemetry.addVoltage(next_available_channel, voltage);
|
||||
telemetry.addCurrent(next_available_channel, current);
|
||||
telemetry.addPower(next_available_channel, voltage * current);
|
||||
next_available_channel++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA219
|
||||
if (INA219_initialized) {
|
||||
telemetry.addVoltage(next_available_channel, INA219.getBusVoltage_V());
|
||||
telemetry.addCurrent(next_available_channel, INA219.getCurrent_mA() / 1000);
|
||||
telemetry.addPower(next_available_channel, INA219.getPower_mW() / 1000);
|
||||
for (int i = 0; i < _active_sensor_count; i++) {
|
||||
_active_sensors[i].query(next_available_channel, _active_sensors[i].sub_channel, telemetry);
|
||||
next_available_channel++;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA260
|
||||
if (INA260_initialized) {
|
||||
telemetry.addVoltage(next_available_channel, INA260.readBusVoltage() / 1000);
|
||||
telemetry.addCurrent(next_available_channel, INA260.readCurrent() / 1000);
|
||||
telemetry.addPower(next_available_channel, INA260.readPower() / 1000);
|
||||
next_available_channel++;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA226
|
||||
if (INA226_initialized) {
|
||||
telemetry.addVoltage(next_available_channel, INA226.getBusVoltage());
|
||||
telemetry.addCurrent(next_available_channel, INA226.getCurrent_mA() / 1000.0);
|
||||
telemetry.addPower(next_available_channel, INA226.getPower_mW() / 1000.0);
|
||||
next_available_channel++;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_MLX90614
|
||||
if (MLX90614_initialized) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, MLX90614.readObjectTempC());
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF + 1, MLX90614.readAmbientTempC());
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_VL53L0X
|
||||
if (VL53L0X_initialized) {
|
||||
VL53L0X_RangingMeasurementData_t measure;
|
||||
VL53L0X.rangingTest(&measure, false); // pass in 'true' to get debug data
|
||||
if (measure.RangeStatus != 4) { // phase failures
|
||||
telemetry.addDistance(TELEM_CHANNEL_SELF, measure.RangeMilliMeter / 1000.0f); // convert mm to m
|
||||
} else {
|
||||
telemetry.addDistance(TELEM_CHANNEL_SELF, 0.0f); // no valid measurement
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BMP085
|
||||
if (BMP085_initialized) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP085.readTemperature());
|
||||
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP085.readPressure() / 100);
|
||||
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP085.readAltitude(TELEM_BMP085_SEALEVELPRESSURE_HPA * 100));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_RAK12035
|
||||
if (RAK12035_initialized) {
|
||||
|
||||
// RAK12035 Telemetry is Channel 2
|
||||
telemetry.addTemperature(2, RAK12035.get_sensor_temperature());
|
||||
telemetry.addPercentage(2, RAK12035.get_sensor_moisture());
|
||||
|
||||
// RAK12035 CALIBRATION Telemetry is Channel 3, if enabled
|
||||
|
||||
#ifdef ENABLE_RAK12035_CALIBRATION
|
||||
// Calibration Data Screen is Channel 3
|
||||
float cap = RAK12035.get_sensor_capacitance();
|
||||
float _wet = RAK12035.get_humidity_full();
|
||||
float _dry = RAK12035.get_humidity_zero();
|
||||
|
||||
telemetry.addFrequency(3, cap);
|
||||
telemetry.addTemperature(3, _wet);
|
||||
telemetry.addPower(3, _dry);
|
||||
|
||||
if(cap > _dry){
|
||||
RAK12035.set_humidity_zero(cap);
|
||||
}
|
||||
|
||||
if(cap < _wet){
|
||||
RAK12035.set_humidity_full(cap);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -549,8 +594,6 @@ const char* EnvironmentSensorManager::getSettingName(int i) const {
|
||||
return "gps";
|
||||
}
|
||||
#endif
|
||||
// convenient way to add params (needed for some tests)
|
||||
// if (i == settings++) return "param.2";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -561,8 +604,6 @@ const char* EnvironmentSensorManager::getSettingValue(int i) const {
|
||||
return gps_active ? "1" : "0";
|
||||
}
|
||||
#endif
|
||||
// convenient way to add params ...
|
||||
// if (i == settings++) return "2";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -578,11 +619,7 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val
|
||||
}
|
||||
if (strcmp(name, "gps_interval") == 0) {
|
||||
uint32_t interval_seconds = atoi(value);
|
||||
if (interval_seconds > 0) {
|
||||
gps_update_interval_sec = interval_seconds;
|
||||
} else {
|
||||
gps_update_interval_sec = 1; // Default to 1 second if 0
|
||||
}
|
||||
gps_update_interval_sec = interval_seconds > 0 ? interval_seconds : 1;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -646,16 +683,10 @@ void EnvironmentSensorManager::rakGPSInit(){
|
||||
|
||||
//search for the correct IO standby pin depending on socket used
|
||||
if(gpsIsAwake(WB_IO2)){
|
||||
// MESH_DEBUG_PRINTLN("RAK base board is RAK19007/10");
|
||||
// MESH_DEBUG_PRINTLN("GPS is installed on Socket A");
|
||||
}
|
||||
else if(gpsIsAwake(WB_IO4)){
|
||||
// MESH_DEBUG_PRINTLN("RAK base board is RAK19003/9");
|
||||
// MESH_DEBUG_PRINTLN("GPS is installed on Socket C");
|
||||
}
|
||||
else if(gpsIsAwake(WB_IO5)){
|
||||
// MESH_DEBUG_PRINTLN("RAK base board is RAK19001/11");
|
||||
// MESH_DEBUG_PRINTLN("GPS is installed on Socket F");
|
||||
}
|
||||
else{
|
||||
MESH_DEBUG_PRINTLN("No GPS found");
|
||||
@@ -702,15 +733,17 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
|
||||
return true;
|
||||
} else if (Serial1.available()) {
|
||||
MESH_DEBUG_PRINTLN("Serial GPS init correctly and is turned on");
|
||||
#ifdef PIN_GPS_EN
|
||||
if(PIN_GPS_EN){
|
||||
gpsResetPin = PIN_GPS_EN;
|
||||
}
|
||||
#endif
|
||||
serialGPSFlag = true;
|
||||
gps_active = true;
|
||||
gps_detected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
pinMode(ioPin, INPUT);
|
||||
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
|
||||
return false;
|
||||
|
||||
@@ -6,27 +6,22 @@
|
||||
|
||||
class EnvironmentSensorManager : public SensorManager {
|
||||
protected:
|
||||
int next_available_channel = TELEM_CHANNEL_SELF + 1;
|
||||
static const int MAX_ACTIVE_SENSORS = 16;
|
||||
|
||||
bool AHTX0_initialized = false;
|
||||
bool BME280_initialized = false;
|
||||
bool BMP280_initialized = false;
|
||||
bool INA3221_initialized = false;
|
||||
bool INA219_initialized = false;
|
||||
bool INA260_initialized = false;
|
||||
bool INA226_initialized = false;
|
||||
bool SHTC3_initialized = false;
|
||||
bool LPS22HB_initialized = false;
|
||||
bool MLX90614_initialized = false;
|
||||
bool VL53L0X_initialized = false;
|
||||
bool SHT4X_initialized = false;
|
||||
bool BME680_initialized = false;
|
||||
bool BMP085_initialized = false;
|
||||
bool RAK12035_initialized = false;
|
||||
// Query function pointer + sub-channel index (for multi-channel sensors like INA3221).
|
||||
// Sub-channel is 0 for all single-output sensors.
|
||||
struct ActiveSensor {
|
||||
void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
|
||||
uint8_t sub_channel;
|
||||
};
|
||||
|
||||
bool gps_detected = false;
|
||||
bool gps_active = false;
|
||||
uint32_t gps_update_interval_sec = 1; // Default 1 second
|
||||
ActiveSensor _active_sensors[MAX_ACTIVE_SENSORS];
|
||||
int _active_sensor_count = 0;
|
||||
uint8_t next_available_channel = TELEM_CHANNEL_SELF + 1;
|
||||
|
||||
bool gps_detected = false;
|
||||
bool gps_active = false;
|
||||
uint32_t gps_update_interval_sec = 1;
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
LocationProvider* _location;
|
||||
@@ -39,7 +34,6 @@ protected:
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
public:
|
||||
#if ENV_INCLUDE_GPS
|
||||
EnvironmentSensorManager(LocationProvider &location): _location(&location){};
|
||||
|
||||
@@ -67,10 +67,10 @@ void RAK12035_SoilMoisture::setup(TwoWire &i2c)
|
||||
// setup() and that the bus has been initialized externally (Wire.begin()).
|
||||
// It uses the passed in I2C Address (default 0x20)
|
||||
//
|
||||
// *** This code does not supprt three sensors ***
|
||||
// *** This code does not support three sensors ***
|
||||
// The RAK12023 has three connectors, but each of the sensors attached must
|
||||
// all have a different I2C addresses.
|
||||
// This code has a function to set the I2C adress of a sensor
|
||||
// This code has a function to set the I2C address of a sensor
|
||||
// and currently only supports one address 0x20 (the default).
|
||||
// To support three sensors, EnvironmentSensorManager would need to be modified
|
||||
// to support multiple instances of the RAK12035_SoilMoisture class,
|
||||
@@ -78,7 +78,7 @@ void RAK12035_SoilMoisture::setup(TwoWire &i2c)
|
||||
// The begin() function would need to be modified to loop through the three addresses
|
||||
//
|
||||
// DEBUG STATEMENTS: Can be enabled by uncommenting or adding:
|
||||
// File: varients/rak4631 platformio.ini
|
||||
// File: variants/rak4631 platformio.ini
|
||||
// Section example: [env:RAK_4631_companion_radio_ble]
|
||||
// Enable Debug statements: -D MESH_DEBUG=1
|
||||
//
|
||||
@@ -107,7 +107,7 @@ bool RAK12035_SoilMoisture::begin(uint8_t addr)
|
||||
* Change the value to 1 in the RAK12035_SoilMoisture.h file
|
||||
*
|
||||
* Calibration Procedure:
|
||||
* 1) Flash the the Calibration version of the firmware.
|
||||
* 1) Flash the Calibration version of the firmware.
|
||||
* 2) Leave the sensor dry, power up the device.
|
||||
* 3) After detecting the RAK12035 this firmware will display calibration data on Channel 3
|
||||
*
|
||||
@@ -431,7 +431,7 @@ bool RAK12035_SoilMoisture::sensor_on()
|
||||
delay(10); // Wait for the sensor code to complete initialization
|
||||
*/
|
||||
uint8_t v = 0;
|
||||
time_t timeout = millis();
|
||||
uint32_t timeout = millis();
|
||||
while ((!query_sensor())) //Wait for sensor to respond to I2C commands,
|
||||
{ //indicating it is ready
|
||||
if ((millis() - timeout) > 50){ //0.5 second timeout for sensor to respond
|
||||
|
||||
@@ -846,7 +846,7 @@ void OLEDDisplay::drawLogBuffer(uint16_t xMove, uint16_t yMove) {
|
||||
if (this->logBuffer[i] == 10) {
|
||||
length++;
|
||||
// Draw string on line `line` from lastPos to length
|
||||
// Passing 0 as the lenght because we are in TEXT_ALIGN_LEFT
|
||||
// Passing 0 as the length because we are in TEXT_ALIGN_LEFT
|
||||
drawStringInternal(xMove, yMove + (line++) * lineHeight, &this->logBuffer[lastPos], length, 0, false);
|
||||
// Remember last pos
|
||||
lastPos = i;
|
||||
|
||||
@@ -60,7 +60,7 @@ private:
|
||||
};
|
||||
|
||||
#else
|
||||
#error "Unkown operating system"
|
||||
#error "Unknown operating system"
|
||||
#endif
|
||||
|
||||
#include "OLEDDisplayFonts.h"
|
||||
@@ -160,7 +160,7 @@ class OLEDDisplay : public Print {
|
||||
#elif __MBED__
|
||||
class OLEDDisplay : public Stream {
|
||||
#else
|
||||
#error "Unkown operating system"
|
||||
#error "Unknown operating system"
|
||||
#endif
|
||||
|
||||
public:
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "SSD1315Display.h"
|
||||
|
||||
#ifndef SCREEN_XOFFSET
|
||||
#define SCREEN_XOFFSET 0
|
||||
#endif
|
||||
|
||||
#ifndef SCREEN_YOFFSET
|
||||
#define SCREEN_YOFFSET 0
|
||||
#endif
|
||||
|
||||
bool SSD1315Display::i2c_probe(TwoWire& wire, uint8_t addr) {
|
||||
wire.beginTransmission(addr);
|
||||
uint8_t error = wire.endTransmission();
|
||||
return (error == 0);
|
||||
}
|
||||
|
||||
bool SSD1315Display::begin() {
|
||||
if (!_isOn) {
|
||||
if (_peripher_power) _peripher_power->claim();
|
||||
_isOn = true;
|
||||
}
|
||||
#ifdef DISPLAY_ROTATION
|
||||
display.setRotation(DISPLAY_ROTATION);
|
||||
#endif
|
||||
return display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS);
|
||||
}
|
||||
|
||||
void SSD1315Display::turnOn() {
|
||||
if (!_isOn) {
|
||||
if (_peripher_power) _peripher_power->claim();
|
||||
_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 SSD1315Display::turnOff() {
|
||||
display.ssd1306_command(SSD1306_DISPLAYOFF);
|
||||
if (_isOn) {
|
||||
if (_peripher_power) {
|
||||
#if PIN_OLED_RESET >= 0
|
||||
digitalWrite(PIN_OLED_RESET, LOW);
|
||||
#endif
|
||||
_peripher_power->release();
|
||||
}
|
||||
_isOn = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SSD1315Display::clear() {
|
||||
display.clearDisplay();
|
||||
display.display();
|
||||
}
|
||||
|
||||
void SSD1315Display::startFrame(Color bkg) {
|
||||
display.clearDisplay(); // TODO: apply 'bkg'
|
||||
_color = SSD1306_WHITE;
|
||||
display.setTextColor(_color);
|
||||
display.setTextSize(1);
|
||||
display.setTextWrap(false);
|
||||
display.cp437(true); // Use full 256 char 'Code Page 437' font
|
||||
}
|
||||
|
||||
void SSD1315Display::setTextSize(int sz) {
|
||||
display.setTextSize(1); // only small allowed
|
||||
}
|
||||
|
||||
void SSD1315Display::setColor(Color c) {
|
||||
_color = (c != 0) ? SSD1306_WHITE : SSD1306_BLACK;
|
||||
display.setTextColor(_color);
|
||||
}
|
||||
|
||||
void SSD1315Display::setCursor(int x, int y) {
|
||||
display.setCursor(x + SCREEN_XOFFSET, y + SCREEN_YOFFSET);
|
||||
}
|
||||
|
||||
void SSD1315Display::print(const char* str) {
|
||||
display.print(str);
|
||||
}
|
||||
|
||||
void SSD1315Display::fillRect(int x, int y, int w, int h) {
|
||||
display.fillRect(x + SCREEN_XOFFSET, y + SCREEN_YOFFSET, w, h, _color);
|
||||
}
|
||||
|
||||
void SSD1315Display::drawRect(int x, int y, int w, int h) {
|
||||
display.drawRect(x + SCREEN_XOFFSET, y + SCREEN_YOFFSET, w, h, _color);
|
||||
}
|
||||
|
||||
void SSD1315Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
||||
// Width in bytes for bitmap processing
|
||||
uint16_t widthInBytes = (w + 7) / 8;
|
||||
|
||||
x += SCREEN_XOFFSET;
|
||||
y += SCREEN_YOFFSET;
|
||||
// Process the bitmap row by row
|
||||
for (int by = 0; by < h; by++) {
|
||||
// Scan across the row bit by bit
|
||||
for (int bx = 0; bx < w; bx++) {
|
||||
// Get the current bit using MSB ordering (like GxEPDDisplay)
|
||||
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
|
||||
uint8_t bitMask = 1 << (bx & 7);
|
||||
bool bitSet = bits[byteOffset] & bitMask;
|
||||
|
||||
// If the bit is set, draw the pixel
|
||||
if (bitSet) {
|
||||
display.drawPixel(x + bx, y + by, SSD1306_WHITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
// wrong bit order:
|
||||
// display.drawBitmap(x + SCREEN_XOFFSET, y + SCREEN_YOFFSET, bits, w, h, SSD1306_WHITE);
|
||||
}
|
||||
|
||||
uint16_t SSD1315Display::getTextWidth(const char* str) {
|
||||
int16_t x1, y1;
|
||||
uint16_t w, h;
|
||||
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
return w;
|
||||
}
|
||||
|
||||
void SSD1315Display::endFrame() {
|
||||
display.display();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "DisplayDriver.h"
|
||||
#include <Wire.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#define SSD1306_NO_SPLASH
|
||||
#include <Adafruit_SSD1306.h>
|
||||
#include <helpers/RefCountedDigitalPin.h>
|
||||
|
||||
#ifndef PIN_OLED_RESET
|
||||
#define PIN_OLED_RESET 21 // Reset pin # (or -1 if sharing Arduino reset pin)
|
||||
#endif
|
||||
|
||||
#ifndef DISPLAY_ADDRESS
|
||||
#define DISPLAY_ADDRESS 0x3C
|
||||
#endif
|
||||
|
||||
class SSD1315Display : public DisplayDriver {
|
||||
Adafruit_SSD1306 display;
|
||||
bool _isOn;
|
||||
uint8_t _color;
|
||||
RefCountedDigitalPin* _peripher_power;
|
||||
|
||||
bool i2c_probe(TwoWire& wire, uint8_t addr);
|
||||
public:
|
||||
SSD1315Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(64, 32),
|
||||
display(128, 64, &Wire, PIN_OLED_RESET),
|
||||
_peripher_power(peripher_power)
|
||||
{
|
||||
_isOn = false;
|
||||
}
|
||||
bool begin();
|
||||
|
||||
bool isOn() override { return _isOn; }
|
||||
void turnOn() override;
|
||||
void turnOff() override;
|
||||
void clear() override;
|
||||
void startFrame(Color bkg = DARK) override;
|
||||
void setTextSize(int sz) override;
|
||||
void setColor(Color c) override;
|
||||
void setCursor(int x, int y) override;
|
||||
void print(const char* str) override;
|
||||
void fillRect(int x, int y, int w, int h) override;
|
||||
void drawRect(int x, int y, int w, int h) override;
|
||||
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||
uint16_t getTextWidth(const char* str) override;
|
||||
void endFrame() override;
|
||||
};
|
||||
@@ -28,7 +28,10 @@ bool ST7735Display::begin() {
|
||||
#endif
|
||||
digitalWrite(PIN_TFT_RST, HIGH);
|
||||
|
||||
#if defined(HELTEC_TRACKER_V2) || defined(HELTEC_T096)
|
||||
#if defined(HELTEC_T1)
|
||||
display.initR(INITR_MINI160x80);
|
||||
display.setRotation(DISPLAY_ROTATION);
|
||||
#elif defined(HELTEC_TRACKER_V2) || defined(HELTEC_T096)
|
||||
display.initR(INITR_MINI160x80);
|
||||
display.setRotation(DISPLAY_ROTATION);
|
||||
uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR
|
||||
|
||||
@@ -27,12 +27,6 @@ bool ST7789LCDDisplay::begin() {
|
||||
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
|
||||
}
|
||||
if (PIN_TFT_RST != -1) {
|
||||
pinMode(PIN_TFT_RST, OUTPUT);
|
||||
digitalWrite(PIN_TFT_RST, LOW);
|
||||
delay(10);
|
||||
digitalWrite(PIN_TFT_RST, HIGH);
|
||||
}
|
||||
|
||||
// Im not sure if this is just a t-deck problem or not, if your display is slow try this.
|
||||
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
|
||||
@@ -166,4 +160,4 @@ uint16_t ST7789LCDDisplay::getTextWidth(const char* str) {
|
||||
|
||||
void ST7789LCDDisplay::endFrame() {
|
||||
// display.display();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#pragma once
|
||||
|
||||
#include "DisplayDriver.h"
|
||||
#include <U8g2lib.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#ifndef DISPLAY_ADDRESS
|
||||
#define DISPLAY_ADDRESS 0x3C
|
||||
#endif
|
||||
|
||||
#ifndef OLED_WIDTH
|
||||
#define OLED_WIDTH 72
|
||||
#endif
|
||||
|
||||
#ifndef OLED_HEIGHT
|
||||
#define OLED_HEIGHT 40
|
||||
#endif
|
||||
|
||||
class U8g2Display : public DisplayDriver {
|
||||
// U8g2 constructor for SSD1306/SSD1315 72×40 panel — handles all
|
||||
// GDDRAM column/page offsets, SETMULTIPLEX, SETDISPLAYOFFSET internally
|
||||
U8G2_SSD1306_72X40_ER_F_HW_I2C _u8g2;
|
||||
bool _isOn;
|
||||
uint8_t _drawColor;
|
||||
|
||||
// Font metrics for current font (cached on setTextSize)
|
||||
uint8_t _fontAscent;
|
||||
uint8_t _fontHeight;
|
||||
|
||||
void applyFont(int sz) {
|
||||
if (sz >= 2) {
|
||||
_u8g2.setFont(u8g2_font_6x10_mr); // slightly larger font for better readability. TODO: more font sizes?
|
||||
} else {
|
||||
_u8g2.setFont(u8g2_font_5x7_mr);
|
||||
}
|
||||
_fontAscent = _u8g2.getAscent();
|
||||
_fontHeight = _u8g2.getAscent() - _u8g2.getDescent();
|
||||
}
|
||||
|
||||
public:
|
||||
U8g2Display() : DisplayDriver(OLED_WIDTH, OLED_HEIGHT),
|
||||
_u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE),
|
||||
_isOn(false), _drawColor(1), _fontAscent(5), _fontHeight(6) {}
|
||||
|
||||
bool begin() {
|
||||
// Wire must already be initialised by board.begin() before this is called
|
||||
bool ok = _u8g2.begin();
|
||||
if (ok) {
|
||||
_u8g2.setI2CAddress(DISPLAY_ADDRESS * 2); // U8g2 uses 8-bit address
|
||||
_u8g2.setFontPosTop(); // y coordinate = top of text, not baseline
|
||||
_u8g2.setFontMode(1); // transparent background
|
||||
applyFont(1); // default to compact font
|
||||
_isOn = true;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool isOn() override { return _isOn; }
|
||||
|
||||
void turnOn() override {
|
||||
_u8g2.setPowerSave(0);
|
||||
_isOn = true;
|
||||
}
|
||||
|
||||
void turnOff() override {
|
||||
_u8g2.setPowerSave(1);
|
||||
_isOn = false;
|
||||
}
|
||||
|
||||
void clear() override {
|
||||
_u8g2.clearBuffer();
|
||||
_u8g2.sendBuffer();
|
||||
}
|
||||
|
||||
void startFrame(Color bkg = DARK) override {
|
||||
_u8g2.clearBuffer();
|
||||
_drawColor = 1;
|
||||
_u8g2.setDrawColor(1);
|
||||
applyFont(1);
|
||||
}
|
||||
|
||||
void setTextSize(int sz) override {
|
||||
applyFont(sz);
|
||||
}
|
||||
|
||||
void setColor(Color c) override {
|
||||
_drawColor = (c != DARK) ? 1 : 0;
|
||||
_u8g2.setDrawColor(_drawColor);
|
||||
}
|
||||
|
||||
void setCursor(int x, int y) override {
|
||||
_cursorX = x;
|
||||
_cursorY = y;
|
||||
}
|
||||
|
||||
void print(const char* str) override {
|
||||
_u8g2.setDrawColor(_drawColor);
|
||||
_u8g2.drawStr(_cursorX, _cursorY, str);
|
||||
}
|
||||
|
||||
void fillRect(int x, int y, int w, int h) override {
|
||||
_u8g2.setDrawColor(_drawColor);
|
||||
_u8g2.drawBox(x, y, w, h);
|
||||
}
|
||||
|
||||
void drawRect(int x, int y, int w, int h) override {
|
||||
_u8g2.setDrawColor(_drawColor);
|
||||
_u8g2.drawFrame(x, y, w, h);
|
||||
}
|
||||
|
||||
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override {
|
||||
_u8g2.setDrawColor(1);
|
||||
_u8g2.drawXBM(x, y, w, h, bits);
|
||||
}
|
||||
|
||||
uint16_t getTextWidth(const char* str) override {
|
||||
return _u8g2.getStrWidth(str);
|
||||
}
|
||||
|
||||
void endFrame() override {
|
||||
_u8g2.sendBuffer();
|
||||
}
|
||||
|
||||
private:
|
||||
int _cursorX = 0;
|
||||
int _cursorY = 0;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "Arduino.h"
|
||||
#ifdef PIN_BUZZER
|
||||
#include "buzzer.h"
|
||||
|
||||
@@ -12,7 +13,6 @@ void genericBuzzer::begin() {
|
||||
quiet(false);
|
||||
pinMode(PIN_BUZZER, OUTPUT);
|
||||
digitalWrite(PIN_BUZZER, LOW); // need to pull low by default to avoid extreme power draw
|
||||
startup();
|
||||
}
|
||||
|
||||
void genericBuzzer::play(const char *melody) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Mock AES128 class for testing
|
||||
// Provides minimal interface to allow Utils.cpp to compile
|
||||
class AES128 {
|
||||
public:
|
||||
void setKey(const uint8_t* key, size_t keySize) {}
|
||||
void encryptBlock(uint8_t* output, const uint8_t* input) {}
|
||||
void decryptBlock(uint8_t* output, const uint8_t* input) {}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Mock SHA256 class for testing
|
||||
// Provides minimal interface to allow Utils.cpp to compile
|
||||
class SHA256 {
|
||||
public:
|
||||
void update(const uint8_t* data, size_t len) {}
|
||||
void finalize(uint8_t* hash, size_t hashLen) {}
|
||||
void resetHMAC(const uint8_t* key, size_t keyLen) {}
|
||||
void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) {}
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
// Mock Stream class for native testing
|
||||
// Provides minimal interface needed by Utils.h
|
||||
|
||||
class Stream {
|
||||
public:
|
||||
virtual void print(char c) {}
|
||||
virtual void print(const char* str) {}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "Utils.h"
|
||||
|
||||
using namespace mesh;
|
||||
|
||||
#define HEX_BUFFER_SIZE(input) (sizeof(input) * 2 + 1)
|
||||
|
||||
TEST(UtilsToHex, ConvertSingleByte) {
|
||||
uint8_t input[] = {0xAB};
|
||||
char output[HEX_BUFFER_SIZE(input)];
|
||||
|
||||
Utils::toHex(output, input, sizeof(input));
|
||||
|
||||
EXPECT_STREQ("AB", output);
|
||||
}
|
||||
|
||||
TEST(UtilsToHex, ConvertMultipleBytes) {
|
||||
uint8_t input[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};
|
||||
char output[HEX_BUFFER_SIZE(input)];
|
||||
|
||||
Utils::toHex(output, input, sizeof(input));
|
||||
|
||||
EXPECT_STREQ("0123456789ABCDEF", output);
|
||||
}
|
||||
|
||||
TEST(UtilsToHex, ConvertZeroByte) {
|
||||
uint8_t input[] = {0x00};
|
||||
char output[HEX_BUFFER_SIZE(input)];
|
||||
|
||||
Utils::toHex(output, input, sizeof(input));
|
||||
|
||||
EXPECT_STREQ("00", output);
|
||||
}
|
||||
|
||||
TEST(UtilsToHex, ConvertMaxByte) {
|
||||
uint8_t input[] = {0xFF};
|
||||
char output[HEX_BUFFER_SIZE(input)];
|
||||
|
||||
Utils::toHex(output, input, sizeof(input));
|
||||
|
||||
EXPECT_STREQ("FF", output);
|
||||
}
|
||||
|
||||
TEST(UtilsToHex, NullTerminatesOnEmptyInput) {
|
||||
uint8_t input[] = {0xAB};
|
||||
char output[] = "X"; // Pre-fill with X.
|
||||
|
||||
Utils::toHex(output, input, 0);
|
||||
|
||||
// Should just null-terminate at position 0
|
||||
EXPECT_EQ('\0', output[0]);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -135,3 +135,8 @@ build_src_filter = ${Ebyte_EoRa-S3.build_src_filter}
|
||||
lib_deps =
|
||||
${Ebyte_EoRa-S3.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
[env:Ebyte_EoRa-S3_kiss_modem]
|
||||
extends = Ebyte_EoRa-S3
|
||||
build_src_filter = ${Ebyte_EoRa-S3.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
|
||||
@@ -64,21 +64,6 @@ bool radio_init() {
|
||||
return true; // success
|
||||
}
|
||||
|
||||
uint32_t radio_get_rng_seed() {
|
||||
return radio.random(0x7FFFFFFF);
|
||||
}
|
||||
|
||||
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
radio.setFrequency(freq);
|
||||
radio.setSpreadingFactor(sf);
|
||||
radio.setBandwidth(bw);
|
||||
radio.setCodingRate(cr);
|
||||
}
|
||||
|
||||
void radio_set_tx_power(int8_t dbm) {
|
||||
radio.setOutputPower(dbm);
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
|
||||
@@ -23,7 +23,4 @@ extern SensorManager sensors;
|
||||
#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(int8_t dbm);
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
|
||||
@@ -112,3 +112,7 @@ lib_deps =
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
end2endzone/NonBlockingRTTTL@^1.3.0
|
||||
|
||||
[env:GAT562_30S_Mesh_Kit_kiss_modem]
|
||||
extends = GAT562_30S_Mesh_Kit
|
||||
build_src_filter = ${GAT562_30S_Mesh_Kit.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
|
||||
@@ -38,21 +38,6 @@ bool radio_init() {
|
||||
return radio.std_init(&SPI);
|
||||
}
|
||||
|
||||
uint32_t radio_get_rng_seed() {
|
||||
return radio.random(0x7FFFFFFF);
|
||||
}
|
||||
|
||||
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
radio.setFrequency(freq);
|
||||
radio.setSpreadingFactor(sf);
|
||||
radio.setBandwidth(bw);
|
||||
radio.setCodingRate(cr);
|
||||
}
|
||||
|
||||
void radio_set_tx_power(int8_t dbm) {
|
||||
radio.setOutputPower(dbm);
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
|
||||
@@ -24,7 +24,4 @@ extern AutoDiscoverRTCClock rtc_clock;
|
||||
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(int8_t dbm);
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
|
||||
@@ -50,3 +50,8 @@ build_flags =
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${GAT562_Mesh_EVB_Pro.build_src_filter}
|
||||
+<../examples/simple_room_server>
|
||||
|
||||
[env:GAT562_Mesh_EVB_Pro_kiss_modem]
|
||||
extends = GAT562_Mesh_EVB_Pro
|
||||
build_src_filter = ${GAT562_Mesh_EVB_Pro.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
|
||||
@@ -37,21 +37,6 @@ bool radio_init() {
|
||||
return radio.std_init(&SPI);
|
||||
}
|
||||
|
||||
uint32_t radio_get_rng_seed() {
|
||||
return radio.random(0x7FFFFFFF);
|
||||
}
|
||||
|
||||
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
radio.setFrequency(freq);
|
||||
radio.setSpreadingFactor(sf);
|
||||
radio.setBandwidth(bw);
|
||||
radio.setCodingRate(cr);
|
||||
}
|
||||
|
||||
void radio_set_tx_power(int8_t dbm) {
|
||||
radio.setOutputPower(dbm);
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
|
||||
@@ -23,7 +23,4 @@ extern AutoDiscoverRTCClock rtc_clock;
|
||||
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(int8_t dbm);
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
|
||||
@@ -106,3 +106,7 @@ lib_deps =
|
||||
${GAT562_Mesh_Tracker_Pro.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
[env:GAT562_Mesh_Tracker_Pro_kiss_modem]
|
||||
extends = GAT562_Mesh_Tracker_Pro
|
||||
build_src_filter = ${GAT562_Mesh_Tracker_Pro.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
|
||||
@@ -38,21 +38,6 @@ bool radio_init() {
|
||||
return radio.std_init(&SPI);
|
||||
}
|
||||
|
||||
uint32_t radio_get_rng_seed() {
|
||||
return radio.random(0x7FFFFFFF);
|
||||
}
|
||||
|
||||
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
radio.setFrequency(freq);
|
||||
radio.setSpreadingFactor(sf);
|
||||
radio.setBandwidth(bw);
|
||||
radio.setCodingRate(cr);
|
||||
}
|
||||
|
||||
void radio_set_tx_power(int8_t dbm) {
|
||||
radio.setOutputPower(dbm);
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
|
||||
@@ -24,7 +24,4 @@ extern AutoDiscoverRTCClock rtc_clock;
|
||||
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(int8_t dbm);
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
|
||||
@@ -87,3 +87,7 @@ lib_deps =
|
||||
${GAT562_Mesh_Watch13.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
[env:GAT562_Mesh_Watch13_kiss_modem]
|
||||
extends = GAT562_Mesh_Watch13
|
||||
build_src_filter = ${GAT562_Mesh_Watch13.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
|
||||
@@ -29,21 +29,6 @@ bool radio_init() {
|
||||
return radio.std_init(&SPI);
|
||||
}
|
||||
|
||||
uint32_t radio_get_rng_seed() {
|
||||
return radio.random(0x7FFFFFFF);
|
||||
}
|
||||
|
||||
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
radio.setFrequency(freq);
|
||||
radio.setSpreadingFactor(sf);
|
||||
radio.setBandwidth(bw);
|
||||
radio.setCodingRate(cr);
|
||||
}
|
||||
|
||||
void radio_set_tx_power(int8_t dbm) {
|
||||
radio.setOutputPower(dbm);
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
|
||||
@@ -25,7 +25,4 @@ extern AutoDiscoverRTCClock rtc_clock;
|
||||
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(int8_t dbm);
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
|
||||
@@ -95,6 +95,11 @@ lib_deps =
|
||||
${Generic_E22.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
|
||||
[env:Generic_E22_kiss_modem]
|
||||
extends = Generic_E22
|
||||
build_src_filter = ${Generic_E22.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
|
||||
[env:Generic_E22_sx1268_repeater]
|
||||
extends = Generic_E22
|
||||
build_src_filter = ${Generic_E22.build_src_filter}
|
||||
|
||||
@@ -27,21 +27,6 @@ bool radio_init() {
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t radio_get_rng_seed() {
|
||||
return radio.random(0x7FFFFFFF);
|
||||
}
|
||||
|
||||
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
radio.setFrequency(freq);
|
||||
radio.setSpreadingFactor(sf);
|
||||
radio.setBandwidth(bw);
|
||||
radio.setCodingRate(cr);
|
||||
}
|
||||
|
||||
void radio_set_tx_power(int8_t dbm) {
|
||||
radio.setOutputPower(dbm);
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
|
||||
@@ -15,8 +15,5 @@ extern AutoDiscoverRTCClock rtc_clock;
|
||||
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(int8_t dbm);
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user