Merge branch 'dev' into nrf-dcdc

This commit is contained in:
fdlamotte 2026-01-27 08:42:58 -04:00 committed by GitHub
commit 3f3978c7d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 3339 additions and 393 deletions

74
boards/meshtiny.json Normal file
View file

@ -0,0 +1,74 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
[
"0x239A",
"0x8029"
],
[
"0x239A",
"0x0029"
],
[
"0x239A",
"0x002A"
],
[
"0x239A",
"0x802A"
]
],
"usb_product": "Meshtiny",
"mcu": "nrf52840",
"variant": "meshtiny",
"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": "nrf52840-mdk-rs"
},
"frameworks": [
"arduino",
"freertos"
],
"name": "Meshtiny",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://shop.mtoolstec.com/product/meshtiny",
"vendor": "MTools Tec"
}

72
boards/rak3401.json Normal file
View file

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

View file

@ -29,6 +29,20 @@ $ sh build.sh build-repeater-firmwares
Build all chat room server firmwares
$ sh build.sh build-room-server-firmwares
Environment Variables:
DISABLE_DEBUG=1: Disables all debug logging flags (MESH_DEBUG, MESH_PACKET_LOGGING, etc.)
If not set, debug flags from variant platformio.ini files are used.
Examples:
Build without debug logging:
$ export FIRMWARE_VERSION=v1.0.0
$ export DISABLE_DEBUG=1
$ sh build.sh build-firmware RAK_4631_repeater
Build with debug logging (default, uses flags from variant files):
$ export FIRMWARE_VERSION=v1.0.0
$ sh build.sh build-firmware RAK_4631_repeater
EOF
}
@ -68,6 +82,13 @@ get_pio_envs_ending_with_string() {
done
}
# disable all debug logging flags if DISABLE_DEBUG=1 is set
disable_debug_flags() {
if [ "$DISABLE_DEBUG" == "1" ]; then
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG -UBLE_DEBUG_LOGGING -UWIFI_DEBUG_LOGGING -UBRIDGE_DEBUG -UGPS_NMEA_DEBUG -UCORE_DEBUG_LEVEL -UESPNOW_DEBUG_LOGGING -UDEBUG_RP2040_WIRE -UDEBUG_RP2040_SPI -UDEBUG_RP2040_CORE -UDEBUG_RP2040_PORT -URADIOLIB_DEBUG_SPI -UCFG_DEBUG -URADIOLIB_DEBUG_BASIC -URADIOLIB_DEBUG_PROTOCOL"
fi
}
# build firmware for the provided pio env in $1
build_firmware() {
@ -94,6 +115,9 @@ build_firmware() {
# add firmware version info to end of existing platformio build flags in environment vars
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'"
# disable debug flags if requested
disable_debug_flags
# build firmware target
pio run -e $1

View file

@ -0,0 +1,213 @@
# nRF52 Power Management
## Overview
The nRF52 Power Management module provides battery protection features to prevent over-discharge, minimise likelihood of brownout and flash corruption conditions existing, and enable safe voltage-based recovery.
## Features
### Boot Voltage Protection
- Checks battery voltage immediately after boot and before mesh operations commence
- If voltage is below a configurable threshold (e.g., 3300mV), the device configures voltage wake (LPCOMP + VBUS) and enters protective shutdown (SYSTEMOFF)
- Prevents boot loops when battery is critically low
- Skipped when external power (USB VBUS) is detected
### Voltage Wake (LPCOMP + VBUS)
- Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF
- Enables USB VBUS detection so external power can wake the device
- Device automatically wakes when battery voltage rises above recovery threshold or when VBUS is detected
### Early Boot Register Capture
- Captures RESETREAS (reset reason) and GPREGRET2 (shutdown reason) before SystemInit() clears them
- Allows firmware to determine why it booted (cold boot, watchdog, LPCOMP wake, etc.)
- Allows firmware to determine why it last shut down (user request, low voltage, boot protection)
### Shutdown Reason Tracking
Shutdown reason codes (stored in GPREGRET2):
| Code | Name | Description |
|------|------|-------------|
| 0x00 | NONE | Normal boot / no previous shutdown |
| 0x4C | LOW_VOLTAGE | Runtime low voltage threshold reached |
| 0x55 | USER | User requested powerOff() |
| 0x42 | BOOT_PROTECT | Boot voltage protection triggered |
## Supported Boards
| Board | Implemented | LPCOMP wake | VBUS wake |
|-------|-------------|-------------|-----------|
| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | Yes | Yes |
| RAK4631 (`rak4631`) | Yes | Yes | Yes |
| Heltec T114 (`heltec_t114`) | Yes | Yes | Yes |
| Promicro nRF52840 | No | No | No |
| RAK WisMesh Tag | No | No | No |
| Heltec Mesh Solar | No | No | No |
| LilyGo T-Echo / T-Echo Lite | No | No | No |
| SenseCAP Solar | No | No | No |
| WIO Tracker L1 / L1 E-Ink | No | No | No |
| WIO WM1110 | No | No | No |
| Mesh Pocket | No | No | No |
| Nano G2 Ultra | No | No | No |
| ThinkNode M1/M3/M6 | No | No | No |
| T1000-E | No | No | No |
| Ikoka Nano/Stick/Handheld (nRF) | No | No | No |
| Keepteen LT1 | No | No | No |
| Minewsemi ME25LS01 | No | No | No |
Notes:
- "Implemented" reflects Phase 1 (boot lockout + shutdown reason capture).
- User power-off on Heltec T114 does not enable LPCOMP wake.
- VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52.
## Technical Details
### Architecture
The power management functionality is integrated into the `NRF52Board` base class in `src/helpers/NRF52Board.cpp`. Board variants provide hardware-specific configuration via a `PowerMgtConfig` struct and override `initiateShutdown(uint8_t reason)` to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).
### Early Boot Capture
A static constructor with priority 101 in `NRF52Board.cpp` captures the RESETREAS and GPREGRET2 registers before:
- SystemInit() (priority 102) - which clears RESETREAS
- Static C++ constructors (default priority 65535)
This ensures we capture the true reset reason before any initialisation code runs.
### Board Implementation
To enable power management on a board variant:
1. **Enable in platformio.ini**:
```ini
-D NRF52_POWER_MANAGEMENT
```
2. **Define configuration in variant.h**:
```c
#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV)
#define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing
#define PWRMGT_LPCOMP_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
```
3. **Implement in board .cpp file**:
```cpp
#ifdef NRF52_POWER_MANAGEMENT
const PowerMgtConfig power_config = {
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
};
void MyBoard::initiateShutdown(uint8_t reason) {
// Board-specific shutdown preparation (e.g., disable peripherals)
bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
reason == SHUTDOWN_REASON_BOOT_PROTECT);
if (enable_lpcomp) {
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
}
enterSystemOff(reason);
}
#endif
void MyBoard::begin() {
NRF52Board::begin(); // or NRF52BoardDCDC::begin()
// ... board setup ...
#ifdef NRF52_POWER_MANAGEMENT
checkBootVoltage(&power_config);
#endif
}
```
For user-initiated shutdowns, `powerOff()` remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).
4. **Declare override in board .h file**:
```cpp
#ifdef NRF52_POWER_MANAGEMENT
void initiateShutdown(uint8_t reason) override;
#endif
```
### Voltage Wake Configuration
The LPCOMP (Low Power Comparator) is configured to:
- Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31)
- Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
- Detect UP events (voltage rising above threshold)
- Use 50mV hysteresis for noise immunity
- Wake the device from SYSTEMOFF when triggered
VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever `configureVoltageWake()` is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).
**LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL)**:
| REFSEL | Fraction | VBAT @ 1M/1M divider (VDD=3.0-3.3) | VBAT @ 1.5M/1M divider (VDD=3.0-3.3) |
|--------|----------|------------------------------------|--------------------------------------|
| 0 | 1/8 | 0.75-0.82 V | 0.94-1.03 V |
| 1 | 2/8 | 1.50-1.65 V | 1.88-2.06 V |
| 2 | 3/8 | 2.25-2.47 V | 2.81-3.09 V |
| 3 | 4/8 | 3.00-3.30 V | 3.75-4.12 V |
| 4 | 5/8 | 3.75-4.12 V | 4.69-5.16 V |
| 5 | 6/8 | 4.50-4.95 V | 5.62-6.19 V |
| 6 | 7/8 | 5.25-5.77 V | 6.56-7.22 V |
| 7 | ARef | - | - |
| 8 | 1/16 | 0.38-0.41 V | 0.47-0.52 V |
| 9 | 3/16 | 1.12-1.24 V | 1.41-1.55 V |
| 10 | 5/16 | 1.88-2.06 V | 2.34-2.58 V |
| 11 | 7/16 | 2.62-2.89 V | 3.28-3.61 V |
| 12 | 9/16 | 3.38-3.71 V | 4.22-4.64 V |
| 13 | 11/16 | 4.12-4.54 V | 5.16-5.67 V |
| 14 | 13/16 | 4.88-5.36 V | 6.09-6.70 V |
| 15 | 15/16 | 5.62-6.19 V | 7.03-7.73 V |
**Important**: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use:
`VBAT_threshold ≈ (VDD * fraction) * divider_scale`, where `divider_scale = (Rtop + Rbottom) / Rbottom` (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).
### SoftDevice Compatibility
The power management code checks whether SoftDevice is enabled and uses the appropriate API:
- When SD enabled: `sd_power_*` functions
- When SD disabled: Direct register access (NRF_POWER->*)
This ensures compatibility regardless of BLE stack state.
## CLI Commands
Power management status can be queried via the CLI:
| Command | Description |
|---------|-------------|
| `get pwrmgt.support` | Returns "supported" or "unsupported" |
| `get pwrmgt.source` | Returns current power source - "battery" or "external" (5V/USB power) |
| `get pwrmgt.bootreason` | Returns reset and shutdown reason strings |
| `get pwrmgt.bootmv` | Returns boot voltage in millivolts |
On boards without power management enabled, all commands except `get pwrmgt.support` return:
```
ERROR: Power management not supported
```
## Debug Output
When `MESH_DEBUG=1` is enabled, the power management module outputs:
```
DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)
DEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)
DEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)
```
## Phase 2 (Planned)
- Runtime voltage monitoring
- Voltage state machine (Normal -> Warning -> Critical -> Shutdown)
- Configurable thresholds
- Load shedding callbacks for power reduction
- Deep sleep integration
- Scheduled wake-up
- Extended sleep with periodic monitoring
## References
- [nRF52840 Product Specification - POWER](https://infocenter.nordicsemi.com/topic/ps_nrf52840/power.html)
- [nRF52840 Product Specification - LPCOMP](https://infocenter.nordicsemi.com/topic/ps_nrf52840/lpcomp.html)
- [SoftDevice S140 API - Power Management](https://infocenter.nordicsemi.com/topic/sdk_nrf5_v17.1.0/group__nrf__sdm__api.html)

View file

@ -103,7 +103,9 @@ Request type
| `0x02` | keepalive | (deprecated) |
| `0x03` | get telemetry data | TODO |
| `0x04` | get min,max,avg data | sensor nodes - get min, max, average for given time span |
| `0x05` | get access list | get node's approved access list |
| `0x05` | get access list | get node's approved access list |
| `0x06` | get neighbors | get repeater node's neighbors |
| `0x07` | get owner info | get repeater firmware-ver/name/owner info |
### Get stats
@ -132,6 +134,27 @@ Gets information about the node, possibly including the following:
Request data about sensors on the node, including battery level.
### Get Telemetry
TODO
### Get Min/Max/Ave (Sensor nodes)
TODO
### Get Access List
TODO
### Get Neighors
TODO
### Get Owner Info
TODO
## Response
| Field | Size (bytes) | Description |
@ -179,6 +202,34 @@ txt_type
| timestamp | 4 | sender time (unix timestamp) |
| password | rest of message | password for repeater/sensor |
## Repeater - Regions request
| Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------|
| timestamp | 4 | sender time (unix timestamp) |
| req type | 1 | 0x01 (request sub type) |
| reply path len | 1 | path len for reply |
| reply path | (variable) | reply path |
## Repeater - Owner info request
| Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------|
| timestamp | 4 | sender time (unix timestamp) |
| req type | 1 | 0x02 (request sub type) |
| reply path len | 1 | path len for reply |
| reply path | (variable) | reply path |
## Repeater - Clock and status request
| Field | Size (bytes) | Description |
|----------------|-----------------|-------------------------------------------------------------------------------|
| timestamp | 4 | sender time (unix timestamp) |
| req type | 1 | 0x03 (request sub type) |
| reply path len | 1 | path len for reply |
| reply path | (variable) | reply path |
# Group text message / datagram
| Field | Size (bytes) | Description |

View file

@ -227,6 +227,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.close();
}
@ -261,6 +262,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.close();
}

View file

@ -53,6 +53,9 @@
#define CMD_SET_FLOOD_SCOPE 54 // v8+
#define CMD_SEND_CONTROL_DATA 55 // v8+
#define CMD_GET_STATS 56 // v8+, second byte is stats type
#define CMD_SEND_ANON_REQ 57
#define CMD_SET_AUTOADD_CONFIG 58
#define CMD_GET_AUTOADD_CONFIG 59
// Stats sub-types for CMD_GET_STATS
#define STATS_TYPE_CORE 0
@ -84,6 +87,7 @@
#define RESP_CODE_ADVERT_PATH 22
#define RESP_CODE_TUNING_PARAMS 23
#define RESP_CODE_STATS 24 // v8+, second byte is stats type
#define RESP_CODE_AUTOADD_CONFIG 25
#define SEND_TIMEOUT_BASE_MILLIS 500
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
@ -109,6 +113,8 @@
#define PUSH_CODE_BINARY_RESPONSE 0x8C
#define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D
#define PUSH_CODE_CONTROL_DATA 0x8E // v8+
#define PUSH_CODE_CONTACT_DELETED 0x8F // used to notify client app of deleted contact when overwriting oldest
#define PUSH_CODE_CONTACTS_FULL 0x90 // used to notify client app that contacts storage is full
#define ERR_CODE_UNSUPPORTED_CMD 1
#define ERR_CODE_NOT_FOUND 2
@ -119,6 +125,15 @@
#define MAX_SIGN_DATA_LEN (8 * 1024) // 8K
// Auto-add config bitmask
// Bit 0: If set, overwrite oldest non-favourite contact when contacts file is full
// Bits 1-4: these indicate which contact types to auto-add when manual_contact_mode = 0x01
#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0) // 0x01 - overwrite oldest non-favourite when full
#define AUTO_ADD_CHAT (1 << 1) // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
#define AUTO_ADD_REPEATER (1 << 2) // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
#define AUTO_ADD_ROOM_SERVER (1 << 3) // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
#define AUTO_ADD_SENSOR (1 << 4) // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
void MyMesh::writeOKFrame() {
uint8_t buf[1];
buf[0] = RESP_CODE_OK;
@ -261,20 +276,64 @@ bool MyMesh::isAutoAddEnabled() const {
return (_prefs.manual_add_contacts & 1) == 0;
}
bool MyMesh::shouldAutoAddContactType(uint8_t contact_type) const {
if ((_prefs.manual_add_contacts & 1) == 0) {
return true;
}
uint8_t type_bit = 0;
switch (contact_type) {
case ADV_TYPE_CHAT:
type_bit = AUTO_ADD_CHAT;
break;
case ADV_TYPE_REPEATER:
type_bit = AUTO_ADD_REPEATER;
break;
case ADV_TYPE_ROOM:
type_bit = AUTO_ADD_ROOM_SERVER;
break;
case ADV_TYPE_SENSOR:
type_bit = AUTO_ADD_SENSOR;
break;
default:
return false; // Unknown type, don't auto-add
}
return (_prefs.autoadd_config & type_bit) != 0;
}
bool MyMesh::shouldOverwriteWhenFull() const {
return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0;
}
void MyMesh::onContactOverwrite(const uint8_t* pub_key) {
if (_serial->isConnected()) {
out_frame[0] = PUSH_CODE_CONTACT_DELETED;
memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE);
_serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
}
}
void MyMesh::onContactsFull() {
if (_serial->isConnected()) {
out_frame[0] = PUSH_CODE_CONTACTS_FULL;
_serial->writeFrame(out_frame, 1);
}
}
void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) {
if (_serial->isConnected()) {
if (!isAutoAddEnabled() && is_new) {
if (is_new) {
writeContactRespFrame(PUSH_CODE_NEW_ADVERT, contact);
} else {
out_frame[0] = PUSH_CODE_ADVERT;
memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
_serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
}
} else {
}
#ifdef DISPLAY_CLASS
if (_ui) _ui->notify(UIEventType::newContactMessage);
if (_ui && !_prefs.buzzer_quiet) _ui->notify(UIEventType::newContactMessage); //buzz if enabled
#endif
}
// add inbound-path to mem cache
if (path && path_len <= sizeof(AdvertPath::path)) { // check path is valid
@ -298,7 +357,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
memcpy(p->path, path, p->path_len);
}
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]
}
static int sort_by_recent(const void *a, const void *b) {
@ -381,9 +440,7 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
if (should_display && _ui) {
_ui->newMsg(path_len, from.name, text, offline_queue_len);
if (!_serial->isConnected()) {
_ui->notify(UIEventType::contactMessage);
}
if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::contactMessage); //buzz if enabled
}
#endif
}
@ -468,11 +525,8 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
_serial->writeFrame(frame, 1);
} else {
#ifdef DISPLAY_CLASS
if (_ui) _ui->notify(UIEventType::channelMessage);
#endif
}
#ifdef DISPLAY_CLASS
// Get the channel name from the channel index
const char *channel_name = "Unknown";
@ -480,7 +534,10 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
if (getChannel(channel_idx, channel_details)) {
channel_name = channel_details.name;
}
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
if (_ui) {
_ui->newMsg(path_len, channel_name, text, offline_queue_len);
if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::channelMessage); //buzz if enabled
}
#endif
}
@ -739,6 +796,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.bw = LORA_BW;
_prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.buzzer_quiet = 0;
_prefs.gps_enabled = 0; // GPS disabled by default
_prefs.gps_interval = 0; // No automatic GPS updates by default
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed
@ -757,14 +815,14 @@ void MyMesh::begin(bool has_display) {
_store->saveMainIdentity(self_id);
}
// if name is provided as a build flag, use that as default node name instead
#ifdef ADVERT_NAME
strcpy(_prefs.node_name, ADVERT_NAME);
#else
// use hex of first 4 bytes of identity public key as default node name
char pub_key_hex[10];
mesh::Utils::toHex(pub_key_hex, self_id.pub_key, 4);
strcpy(_prefs.node_name, pub_key_hex);
// if name is provided as a build flag, use that as default node name instead
#ifdef ADVERT_NAME
strcpy(_prefs.node_name, ADVERT_NAME);
#endif
// load persisted prefs
@ -778,6 +836,7 @@ void MyMesh::begin(bool has_display) {
_prefs.sf = constrain(_prefs.sf, 5, 12);
_prefs.cr = constrain(_prefs.cr, 5, 8);
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER);
_prefs.buzzer_quiet = constrain(_prefs.buzzer_quiet, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours
@ -802,6 +861,7 @@ void MyMesh::begin(bool has_display) {
resetContacts();
_store->loadContacts(this);
bootstrapRTCfromContacts();
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
_store->loadChannels(this);
@ -1234,16 +1294,20 @@ void MyMesh::handleCmdFrame(size_t len) {
#endif
} else if (cmd_frame[0] == CMD_IMPORT_PRIVATE_KEY && len >= 65) {
#if ENABLE_PRIVATE_KEY_IMPORT
mesh::LocalIdentity identity;
identity.readFrom(&cmd_frame[1], 64);
if (_store->saveMainIdentity(identity)) {
self_id = identity;
writeOKFrame();
// re-load contacts, to invalidate ecdh shared_secrets
resetContacts();
_store->loadContacts(this);
if (!mesh::LocalIdentity::validatePrivateKey(&cmd_frame[1])) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid key
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
mesh::LocalIdentity identity;
identity.readFrom(&cmd_frame[1], 64);
if (_store->saveMainIdentity(identity)) {
self_id = identity;
writeOKFrame();
// re-load contacts, to invalidate ecdh shared_secrets
resetContacts();
_store->loadContacts(this);
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
}
#else
writeDisabledFrame();
@ -1286,6 +1350,27 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
}
} else if (cmd_frame[0] == CMD_SEND_ANON_REQ && len > 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
uint8_t *data = &cmd_frame[1 + PUB_KEY_SIZE];
if (recipient) {
uint32_t tag, est_timeout;
int result = sendAnonReq(*recipient, data, len - (1 + PUB_KEY_SIZE), tag, est_timeout);
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
clearPendingReqs();
pending_req = tag; // match this to onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
memcpy(&out_frame[2], &tag, 4);
memcpy(&out_frame[6], &est_timeout, 4);
_serial->writeFrame(out_frame, 10);
}
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
}
} else if (cmd_frame[0] == CMD_SEND_STATUS_REQ && len >= 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
@ -1641,6 +1726,15 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
} else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) {
_prefs.autoadd_config = cmd_frame[1];
savePrefs();
writeOKFrame();
} else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) {
int i = 0;
out_frame[i++] = RESP_CODE_AUTOADD_CONFIG;
out_frame[i++] = _prefs.autoadd_config;
_serial->writeFrame(out_frame, i);
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);

View file

@ -114,6 +114,10 @@ protected:
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
bool isAutoAddEnabled() const override;
bool shouldAutoAddContactType(uint8_t type) const override;
bool shouldOverwriteWhenFull() const override;
void onContactsFull() override;
void onContactOverwrite(const uint8_t* pub_key) override;
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
void onContactPathUpdated(const ContactInfo &contact) override;

View file

@ -27,4 +27,5 @@ struct NodePrefs { // persisted to file
uint8_t buzzer_quiet;
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval; // GPS read interval in seconds
uint8_t autoadd_config; // bitmask for auto-add contacts config
};

View file

@ -151,9 +151,7 @@ void setup() {
);
#ifdef BLE_PIN_CODE
char dev_name[32+16];
sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName());
serial_interface.begin(dev_name, the_mesh.getBLEPin());
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#else
serial_interface.begin(Serial);
#endif
@ -199,9 +197,7 @@ void setup() {
WiFi.begin(WIFI_SSID, WIFI_PWD);
serial_interface.begin(TCP_PORT);
#elif defined(BLE_PIN_CODE)
char dev_name[32+16];
sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName());
serial_interface.begin(dev_name, the_mesh.getBLEPin());
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#elif defined(SERIAL_RX)
companion_serial.setPins(SERIAL_RX, SERIAL_TX);
companion_serial.begin(115200);

View file

@ -41,16 +41,21 @@
#define TXT_ACK_DELAY 200
#endif
#define FIRMWARE_VER_LEVEL 1
#define FIRMWARE_VER_LEVEL 2
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
#define REQ_TYPE_GET_ACCESS_LIST 0x05
#define REQ_TYPE_GET_NEIGHBOURS 0x06
#define REQ_TYPE_GET_OWNER_INFO 0x07 // FIRMWARE_VER_LEVEL >= 2
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
#define ANON_REQ_TYPE_REGIONS 0x01
#define ANON_REQ_TYPE_OWNER 0x02
#define ANON_REQ_TYPE_BASIC 0x03 // just remote clock
#define CLI_REPLY_DELAY_MILLIS 600
#define LAZY_CONTACTS_WRITE_DELAY 5000
@ -139,6 +144,64 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
return 13; // reply length
}
uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path}
reply_path_len = *data++ & 0x3F;
memcpy(reply_path, data, reply_path_len);
// data += reply_path_len;
memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness)
return 8 + region_map.exportNamesTo((char *) &reply_data[8], sizeof(reply_data) - 12, REGION_DENY_FLOOD); // reply length
}
return 0;
}
uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path}
reply_path_len = *data++ & 0x3F;
memcpy(reply_path, data, reply_path_len);
// data += reply_path_len;
memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness)
sprintf((char *) &reply_data[8], "%s\n%s", _prefs.node_name, _prefs.owner_info);
return 8 + strlen((char *) &reply_data[8]); // reply length
}
return 0;
}
uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
// request data has: {reply-path-len}{reply-path}
reply_path_len = *data++ & 0x3F;
memcpy(reply_path, data, reply_path_len);
// data += reply_path_len;
memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag
uint32_t now = getRTCClock()->getCurrentTime();
memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness)
reply_data[8] = 0; // features
#ifdef WITH_RS232_BRIDGE
reply_data[8] |= 0x01; // is bridge, type UART
#elif WITH_ESPNOW_BRIDGE
reply_data[8] |= 0x03; // is bridge, type ESP-NOW
#endif
if (_prefs.disable_fwd) { // is this repeater currently disabled
reply_data[8] |= 0x80; // is disabled
}
// TODO: add some kind of moving-window utilisation metric, so can query 'how busy' is this repeater
return 9; // reply length
}
return 0;
}
int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len) {
// uint32_t now = getRTCClock()->getCurrentTimeUnique();
// memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
@ -163,7 +226,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
stats.total_rx_air_time_secs = getReceiveAirTime() / 1000;
stats.n_recv_errors = radio_driver.getPacketsRecvErrors();
memcpy(&reply_data[4], &stats, sizeof(stats));
return 4 + sizeof(stats); // reply_len
@ -296,6 +359,9 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
return reply_offset;
}
} else if (payload[0] == REQ_TYPE_GET_OWNER_INFO) {
sprintf((char *) &reply_data[4], "%s\n%s\n%s", FIRMWARE_VERSION, _prefs.node_name, _prefs.owner_info);
return 4 + strlen((char *) &reply_data[4]);
}
return 0; // unknown command
}
@ -448,12 +514,18 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
data[len] = 0; // ensure null terminator
uint8_t reply_len;
reply_path_len = -1;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood());
//} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes
// TODO
} else if (data[4] == ANON_REQ_TYPE_REGIONS && packet->isRouteDirect()) {
reply_len = handleAnonRegionsReq(sender, timestamp, &data[5]);
} else if (data[4] == ANON_REQ_TYPE_OWNER && packet->isRouteDirect()) {
reply_len = handleAnonOwnerReq(sender, timestamp, &data[5]);
} else if (data[4] == ANON_REQ_TYPE_BASIC && packet->isRouteDirect()) {
reply_len = handleAnonClockReq(sender, timestamp, &data[5]);
} else {
reply_len = 0; // unknown request type
reply_len = 0; // unknown/invalid request type
}
if (reply_len == 0) return; // invalid request
@ -463,9 +535,12 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else {
} else if (reply_path_len < 0) {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY);
}
}
}
@ -637,7 +712,9 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
void MyMesh::onControlDataRecv(mesh::Packet* packet) {
uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6 && discover_limiter.allow(rtc_clock.getCurrentTime())) {
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6
&& !_prefs.disable_fwd && discover_limiter.allow(rtc_clock.getCurrentTime())
) {
int i = 1;
uint8_t filter = packet->payload[i++];
uint32_t tag;
@ -667,8 +744,9 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) {
MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
mesh::RTCClock &rtc, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
_cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store),
discover_limiter(4, 120) // max 4 every 2 minutes
_cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store),
discover_limiter(4, 120), // max 4 every 2 minutes
anon_limiter(4, 180) // max 4 every 3 minutes
#if defined(WITH_RS232_BRIDGE)
, bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc)
#endif
@ -730,7 +808,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
_fs = fs;
// load persisted prefs
_cli.loadPrefs(_fs);
acl.load(_fs);
acl.load(_fs, self_id);
// TODO: key_store.begin();
region_map.load(_fs);
@ -776,10 +854,14 @@ bool MyMesh::formatFileSystem() {
#endif
}
void MyMesh::sendSelfAdvertisement(int delay_millis) {
void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet *pkt = createSelfAdvert();
if (pkt) {
sendFlood(pkt, delay_millis);
if (flood) {
sendFlood(pkt, delay_millis);
} else {
sendZeroHop(pkt, delay_millis);
}
} else {
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
}
@ -890,7 +972,6 @@ void MyMesh::formatPacketStatsReply(char *reply) {
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
self_id = new_id;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
#elif defined(ESP32)
@ -900,7 +981,7 @@ void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#else
#error "need to define saveIdentity()"
#endif
store.save("_main", self_id);
store.save("_main", new_id);
}
void MyMesh::clearStats() {
@ -991,8 +1072,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
const char* parts[4];
int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
if (n == 1 && sender_timestamp == 0) {
region_map.exportTo(Serial);
if (n == 1) {
region_map.exportTo(reply, 160);
} else if (n >= 2 && strcmp(parts[1], "load") == 0) {
temp_map.resetFrom(region_map); // rebuild regions in a temp instance
memset(load_stack, 0, sizeof(load_stack));
@ -1065,6 +1146,25 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
} else {
strcpy(reply, "Err - not found");
}
} else if (n >= 3 && strcmp(parts[1], "list") == 0) {
uint8_t mask = 0;
bool invert = false;
if (strcmp(parts[2], "allowed") == 0) {
mask = REGION_DENY_FLOOD;
invert = false; // list regions that DON'T have DENY flag
} else if (strcmp(parts[2], "denied") == 0) {
mask = REGION_DENY_FLOOD;
invert = true; // list regions that DO have DENY flag
} else {
strcpy(reply, "Err - use 'allowed' or 'denied'");
return;
}
int len = region_map.exportNamesTo(reply, 160, mask, invert);
if (len == 0) {
strcpy(reply, "-none-");
}
} else {
strcpy(reply, "Err - ??");
}

View file

@ -54,6 +54,7 @@ struct RepeaterStats {
int16_t last_snr; // x 4
uint16_t n_direct_dups, n_flood_dups;
uint32_t total_rx_air_time_secs;
uint32_t n_recv_errors;
};
#ifndef MAX_CLIENTS
@ -86,14 +87,16 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
unsigned long next_local_advert, next_flood_advert;
bool _logging;
NodePrefs _prefs;
ClientACL acl;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
ClientACL acl;
uint8_t reply_path[MAX_PATH_SIZE];
int8_t reply_path_len;
TransportKeyStore key_store;
RegionMap region_map, temp_map;
RegionEntry* load_stack[8];
RegionEntry* recv_pkt_region;
RateLimiter discover_limiter;
RateLimiter discover_limiter, anon_limiter;
bool region_load_active;
unsigned long dirty_contacts_expiry;
#if MAX_NEIGHBOURS
@ -114,6 +117,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
mesh::Packet* createSelfAdvert();
@ -181,7 +187,7 @@ public:
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis) override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;

View file

@ -87,8 +87,10 @@ void setup() {
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
// send out initial Advertisement to the mesh
the_mesh.sendSelfAdvertisement(16000);
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
}
void loop() {

View file

@ -587,7 +587,7 @@ void MyMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) {
MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
mesh::RTCClock &rtc, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
_cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
_cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
last_millis = 0;
uptime_millis = 0;
next_local_advert = next_flood_advert = 0;
@ -637,7 +637,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
// load persisted prefs
_cli.loadPrefs(_fs);
acl.load(_fs);
acl.load(_fs, self_id);
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
@ -675,10 +675,14 @@ bool MyMesh::formatFileSystem() {
#endif
}
void MyMesh::sendSelfAdvertisement(int delay_millis) {
void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet *pkt = createSelfAdvert();
if (pkt) {
sendFlood(pkt, delay_millis);
if (flood) {
sendFlood(pkt, delay_millis);
} else {
sendZeroHop(pkt, delay_millis);
}
} else {
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
}
@ -720,7 +724,6 @@ void MyMesh::setTxPower(uint8_t power_dbm) {
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
self_id = new_id;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
#elif defined(ESP32)
@ -730,7 +733,7 @@ void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#else
#error "need to define saveIdentity()"
#endif
store.save("_main", self_id);
store.save("_main", new_id);
}
void MyMesh::clearStats() {
@ -815,7 +818,7 @@ void MyMesh::loop() {
if (c->extra.room.pending_ack && millisHasNowPassed(c->extra.room.ack_timeout)) {
c->extra.room.push_failures++;
c->extra.room.pending_ack = 0; // reset (TODO: keep prev expected_ack's in a list, incase they arrive LATER, after we retry)
MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->push_failures);
MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->extra.room.push_failures);
}
}
// check next Round-Robin client, and sync next new post

View file

@ -94,8 +94,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
unsigned long next_local_advert, next_flood_advert;
bool _logging;
NodePrefs _prefs;
CommonCLI _cli;
ClientACL acl;
CommonCLI _cli;
unsigned long dirty_contacts_expiry;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
unsigned long next_push;
@ -177,7 +177,7 @@ public:
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis) override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;

View file

@ -76,8 +76,10 @@ void setup() {
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
// send out initial Advertisement to the mesh
the_mesh.sendSelfAdvertisement(16000);
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
}
void loop() {

View file

@ -582,7 +582,9 @@ void setup() {
the_mesh.showWelcome();
// send out initial Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvert(1200); // add slight delay
#endif
}
void loop() {

View file

@ -695,7 +695,7 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
_cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
_cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
{
next_local_advert = next_flood_advert = 0;
dirty_contacts_expiry = 0;
@ -736,7 +736,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {
// load persisted prefs
_cli.loadPrefs(_fs);
acl.load(_fs);
acl.load(_fs, self_id);
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
@ -765,7 +765,6 @@ bool SensorMesh::formatFileSystem() {
}
void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
self_id = new_id;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
#elif defined(ESP32)
@ -775,7 +774,7 @@ void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
#else
#error "need to define saveIdentity()"
#endif
store.save("_main", self_id);
store.save("_main", new_id);
}
void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
@ -788,10 +787,14 @@ void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t
revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params
}
void SensorMesh::sendSelfAdvertisement(int delay_millis) {
void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) {
sendFlood(pkt, delay_millis);
if (flood) {
sendFlood(pkt, delay_millis);
} else {
sendZeroHop(pkt, delay_millis);
}
} else {
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
}

View file

@ -60,7 +60,7 @@ public:
NodePrefs* getNodePrefs() { return &_prefs; }
void savePrefs() override { _cli.savePrefs(_fs); }
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis) override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;
void setLoggingOn(bool enable) override { }
@ -133,9 +133,9 @@ private:
FILESYSTEM* _fs;
unsigned long next_local_advert, next_flood_advert;
NodePrefs _prefs;
ClientACL acl;
CommonCLI _cli;
uint8_t reply_data[MAX_PACKET_PAYLOAD];
ClientACL acl;
unsigned long dirty_contacts_expiry;
CayenneLPP telemetry;
uint32_t last_read_time;

View file

@ -110,8 +110,10 @@ void setup() {
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
// send out initial Advertisement to the mesh
the_mesh.sendSelfAdvertisement(16000);
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
}
void loop() {

View file

@ -27,6 +27,7 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-D LORA_FREQ=869.525
-D LORA_BW=250
-D LORA_SF=11
-D ENABLE_ADVERT_ON_BOOT=1
-D ENABLE_PRIVATE_KEY_IMPORT=1 ; NOTE: comment these out for more secure firmware
-D ENABLE_PRIVATE_KEY_EXPORT=1
-D RADIOLIB_EXCLUDE_CC1101=1

View file

@ -48,6 +48,50 @@ LocalIdentity::LocalIdentity(RNG* rng) {
ed25519_create_keypair(pub_key, prv_key, seed);
}
bool LocalIdentity::validatePrivateKey(const uint8_t prv[64]) {
uint8_t pub[32];
ed25519_derive_pub(pub, prv); // derive public key from given private key
// disallow 00 or FF prefixed public keys
if (pub[0] == 0x00 || pub[0] == 0xFF) return false;
// known good test client keypair
const uint8_t test_client_prv[64] = {
0x70, 0x65, 0xe1, 0x8f, 0xd9, 0xfa, 0xbb, 0x70,
0xc1, 0xed, 0x90, 0xdc, 0xa1, 0x99, 0x07, 0xde,
0x69, 0x8c, 0x88, 0xb7, 0x09, 0xea, 0x14, 0x6e,
0xaf, 0xd9, 0x3d, 0x9b, 0x83, 0x0c, 0x7b, 0x60,
0xc4, 0x68, 0x11, 0x93, 0xc7, 0x9b, 0xbc, 0x39,
0x94, 0x5b, 0xa8, 0x06, 0x41, 0x04, 0xbb, 0x61,
0x8f, 0x8f, 0xd7, 0xa8, 0x4a, 0x0a, 0xf6, 0xf5,
0x70, 0x33, 0xd6, 0xe8, 0xdd, 0xcd, 0x64, 0x71
};
const uint8_t test_client_pub[32] = {
0x1e, 0xc7, 0x71, 0x75, 0xb0, 0x91, 0x8e, 0xd2,
0x06, 0xf9, 0xae, 0x04, 0xec, 0x13, 0x6d, 0x6d,
0x5d, 0x43, 0x15, 0xbb, 0x26, 0x30, 0x54, 0x27,
0xf6, 0x45, 0xb4, 0x92, 0xe9, 0x35, 0x0c, 0x10
};
uint8_t ss1[32], ss2[32];
// shared secret we calculte from test client pubkey and given private key
ed25519_key_exchange(ss1, test_client_pub, prv);
// shared secret they calculate from our derived public key and test client private key
ed25519_key_exchange(ss2, pub, test_client_prv);
// check that both shared secrets match
if (memcmp(ss1, ss2, 32) != 0) return false;
// reject all-zero shared secret
for (int i = 0; i < 32; i++) {
if (ss1[i] != 0) return true;
}
return false;
}
bool LocalIdentity::readFrom(Stream& s) {
bool success = (s.readBytes(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE);
success = success && (s.readBytes(prv_key, PRV_KEY_SIZE) == PRV_KEY_SIZE);

View file

@ -76,6 +76,13 @@ public:
*/
void calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const;
/**
* \brief Validates that a given private key can be used for ECDH / shared-secret operations.
* \param prv IN - the private key to validate (must be PRV_KEY_SIZE bytes)
* \returns true, if the private key is valid for login.
*/
static bool validatePrivateKey(const uint8_t prv[64]);
bool readFrom(Stream& s);
bool writeTo(Stream& s) const;
void printTo(Stream& s) const;

View file

@ -56,6 +56,14 @@ public:
virtual void setGpio(uint32_t values) {}
virtual uint8_t getStartupReason() const = 0;
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
// Power management interface (boards with power management override these)
virtual bool isExternalPowered() { return false; }
virtual uint16_t getBootVoltage() { return 0; }
virtual uint32_t getResetReason() const { return 0; }
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
virtual uint8_t getShutdownReason() const { return 0; }
virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; }
};
/**

View file

@ -55,6 +55,54 @@ void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
}
}
void BaseChatMesh::bootstrapRTCfromContacts() {
uint32_t latest = 0;
for (int i = 0; i < num_contacts; i++) {
if (contacts[i].lastmod > latest) {
latest = contacts[i].lastmod;
}
}
if (latest != 0) {
getRTCClock()->setCurrentTime(latest + 1);
}
}
ContactInfo* BaseChatMesh::allocateContactSlot() {
if (num_contacts < MAX_CONTACTS) {
return &contacts[num_contacts++];
} else if (shouldOverwriteWhenFull()) {
// Find oldest non-favourite contact by oldest lastmod timestamp
int oldest_idx = -1;
uint32_t oldest_lastmod = 0xFFFFFFFF;
for (int i = 0; i < num_contacts; i++) {
bool is_favourite = (contacts[i].flags & 0x01) != 0;
if (!is_favourite && contacts[i].lastmod < oldest_lastmod) {
oldest_lastmod = contacts[i].lastmod;
oldest_idx = i;
}
}
if (oldest_idx >= 0) {
onContactOverwrite(contacts[oldest_idx].id.pub_key);
return &contacts[oldest_idx];
}
}
return NULL; // no space, no overwrite or all contacts are all favourites
}
void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) {
memset(&ci, 0, sizeof(ci));
ci.id = id;
ci.out_path_len = -1; // initially out_path is unknown
StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
ci.type = parser.getType();
if (parser.hasLatLon()) {
ci.gps_lat = parser.getIntLat();
ci.gps_lon = parser.getIntLon();
}
ci.last_advert_timestamp = timestamp;
ci.lastmod = getRTCClock()->getCurrentTime();
}
void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) {
AdvertDataParser parser(app_data, app_data_len);
if (!(parser.isValid() && parser.hasName())) {
@ -85,50 +133,38 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
}
putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
bool is_new = false;
bool is_new = false; // true = not in contacts[], false = exists in contacts[]
if (from == NULL) {
if (!isAutoAddEnabled()) {
if (!shouldAutoAddContactType(parser.getType())) {
ContactInfo ci;
memset(&ci, 0, sizeof(ci));
ci.id = id;
ci.out_path_len = -1; // initially out_path is unknown
StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
ci.type = parser.getType();
if (parser.hasLatLon()) {
ci.gps_lat = parser.getIntLat();
ci.gps_lon = parser.getIntLon();
}
ci.last_advert_timestamp = timestamp;
ci.lastmod = getRTCClock()->getCurrentTime();
populateContactFromAdvert(ci, id, parser, timestamp);
onDiscoveredContact(ci, true, packet->path_len, packet->path); // let UI know
return;
}
is_new = true;
if (num_contacts < MAX_CONTACTS) {
from = &contacts[num_contacts++];
from->id = id;
from->out_path_len = -1; // initially out_path is unknown
from->gps_lat = 0; // initially unknown GPS loc
from->gps_lon = 0;
from->sync_since = 0;
from->shared_secret_valid = false; // ecdh shared_secret will be calculated later on demand
} else {
MESH_DEBUG_PRINTLN("onAdvertRecv: contacts table is full!");
from = allocateContactSlot();
if (from == NULL) {
ContactInfo ci;
populateContactFromAdvert(ci, id, parser, timestamp);
onDiscoveredContact(ci, true, packet->path_len, packet->path);
onContactsFull();
MESH_DEBUG_PRINTLN("onAdvertRecv: unable to allocate contact slot for new contact");
return;
}
populateContactFromAdvert(*from, id, parser, timestamp);
from->sync_since = 0;
from->shared_secret_valid = false;
}
// update
StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
from->type = parser.getType();
if (parser.hasLatLon()) {
from->gps_lat = parser.getIntLat();
from->gps_lon = parser.getIntLon();
}
from->last_advert_timestamp = timestamp;
from->lastmod = getRTCClock()->getCurrentTime();
StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
from->type = parser.getType();
if (parser.hasLatLon()) {
from->gps_lat = parser.getIntLat();
from->gps_lon = parser.getIntLon();
}
from->last_advert_timestamp = timestamp;
from->lastmod = getRTCClock()->getCurrentTime();
onDiscoveredContact(*from, is_new, packet->path_len, packet->path); // let UI know
}
@ -477,6 +513,31 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
return MSG_SEND_FAILED;
}
int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data, uint8_t len, uint32_t& tag, uint32_t& est_timeout) {
mesh::Packet* pkt;
{
uint8_t temp[MAX_PACKET_PAYLOAD];
tag = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &tag, 4); // tag to match later (also extra blob to help make packet_hash unique)
memcpy(&temp[4], data, len);
pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + len);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len < 0) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
}
int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout) {
if (data_len > MAX_PACKET_PAYLOAD - 16) return MSG_SEND_FAILED;
@ -697,10 +758,9 @@ ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int pre
}
bool BaseChatMesh::addContact(const ContactInfo& contact) {
if (num_contacts < MAX_CONTACTS) {
auto dest = &contacts[num_contacts++];
ContactInfo* dest = allocateContactSlot();
if (dest) {
*dest = contact;
dest->shared_secret_valid = false; // mark shared_secret as needing calculation
return true; // success
}

View file

@ -88,10 +88,17 @@ protected:
memset(connections, 0, sizeof(connections));
}
void bootstrapRTCfromContacts();
void resetContacts() { num_contacts = 0; }
void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp);
ContactInfo* allocateContactSlot(); // helper to find slot for new contact
// 'UI' concepts, for sub-classes to implement
virtual bool isAutoAddEnabled() const { return true; }
virtual bool shouldAutoAddContactType(uint8_t type) const { return true; }
virtual void onContactsFull() {};
virtual bool shouldOverwriteWhenFull() const { return false; }
virtual void onContactOverwrite(const uint8_t* pub_key) {};
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
virtual ContactInfo* processAck(const uint8_t *data) = 0;
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
@ -141,6 +148,7 @@ public:
int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout);
bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len);
int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
int sendAnonReq(const ContactInfo& recipient, const uint8_t* data, uint8_t len, uint32_t& tag, uint32_t& est_timeout);
int sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout);
int sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout);
bool shareContactZeroHop(const ContactInfo& contact);

View file

@ -11,7 +11,8 @@ static File openWrite(FILESYSTEM* _fs, const char* filename) {
#endif
}
void ClientACL::load(FILESYSTEM* _fs) {
void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
_fs = fs;
num_clients = 0;
if (_fs->exists("/s_contacts")) {
#if defined(RP2040_PLATFORM)
@ -34,11 +35,12 @@ void ClientACL::load(FILESYSTEM* _fs) {
success = success && (file.read(unused, 2) == 2);
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below
if (!success) break; // EOF
c.id = mesh::Identity(pub_key);
self_id.calcSharedSecret(c.shared_secret, pub_key); // recalculate shared secrets in case our private key changed
if (num_clients < MAX_CLIENTS) {
clients[num_clients++] = c;
} else {
@ -50,7 +52,8 @@ void ClientACL::load(FILESYSTEM* _fs) {
}
}
void ClientACL::save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)) {
void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
_fs = fs;
File file = openWrite(_fs, "/s_contacts");
if (file) {
uint8_t unused[2];
@ -74,6 +77,16 @@ void ClientACL::save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)) {
}
}
bool ClientACL::clear() {
if (!_fs) return false; // no filesystem, nothing to clear
if (_fs->exists("/s_contacts")) {
_fs->remove("/s_contacts");
}
memset(clients, 0, sizeof(clients));
num_clients = 0;
return true;
}
ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
for (int i = 0; i < num_clients; i++) {
if (memcmp(pubkey, clients[i].id.pub_key, key_len) == 0) return &clients[i]; // already known

View file

@ -36,6 +36,7 @@ struct ClientInfo {
#endif
class ClientACL {
FILESYSTEM* _fs;
ClientInfo clients[MAX_CLIENTS];
int num_clients;
@ -44,8 +45,9 @@ public:
memset(clients, 0, sizeof(clients));
num_clients = 0;
}
void load(FILESYSTEM* _fs);
void load(FILESYSTEM* _fs, const mesh::LocalIdentity& self_id);
void save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)=NULL);
bool clear();
ClientInfo* getClient(const uint8_t* pubkey, int key_len);
ClientInfo* putClient(const mesh::Identity& id, uint8_t init_perms);

View file

@ -14,6 +14,14 @@ static uint32_t _atoi(const char* sp) {
return n;
}
static bool isValidName(const char *n) {
while (*n) {
if (*n == '[' || *n == ']' || *n == '/' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
n++;
}
return true;
}
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
if (fs->exists("/com_prefs")) {
loadPrefsInt(fs, "/com_prefs"); // new filename
@ -72,7 +80,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
// 170
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
// 290
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@ -155,7 +164,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
// 170
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
// 290
file.close();
}
@ -186,8 +196,13 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) {
if (memcmp(command, "reboot", 6) == 0) {
_board->reboot(); // doesn't return
} else if (memcmp(command, "clkreboot", 9) == 0) {
// Reset clock
getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm
_board->reboot(); // doesn't return
} else if (memcmp(command, "advert", 6) == 0) {
_callbacks->sendSelfAdvertisement(1500); // longer delay, give CLI response time to be sent first
// send flood advert
_callbacks->sendSelfAdvertisement(1500, true); // longer delay, give CLI response time to be sent first
strcpy(reply, "OK - Advert sent");
} else if (memcmp(command, "clock sync", 10) == 0) {
uint32_t curr = getRTCClock()->getCurrentTime();
@ -301,6 +316,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
} 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) {
*reply++ = '>';
*reply++ = ' ';
const char* sp = _prefs->owner_info;
while (*sp) {
*reply++ = (*sp == '\n') ? '|' : *sp; // translate newline back to orig '|'
sp++;
}
*reply = 0; // set null terminator
} else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
sprintf(reply, "> %d", (uint32_t) _prefs->tx_power_dbm);
} else if (memcmp(config, "freq", 4) == 0) {
@ -345,6 +369,33 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else {
sprintf(reply, "> %.3f", adc_mult);
}
// Power management commands
} else if (memcmp(config, "pwrmgt.support", 14) == 0) {
#ifdef NRF52_POWER_MANAGEMENT
strcpy(reply, "> supported");
#else
strcpy(reply, "> unsupported");
#endif
} else if (memcmp(config, "pwrmgt.source", 13) == 0) {
#ifdef NRF52_POWER_MANAGEMENT
strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
#else
strcpy(reply, "ERROR: Power management not supported");
#endif
} else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
#ifdef NRF52_POWER_MANAGEMENT
sprintf(reply, "> Reset: %s; Shutdown: %s",
_board->getResetReasonString(_board->getResetReason()),
_board->getShutdownReasonString(_board->getShutdownReason()));
#else
strcpy(reply, "ERROR: Power management not supported");
#endif
} else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
#ifdef NRF52_POWER_MANAGEMENT
sprintf(reply, "> %u mV", _board->getBootVoltage());
#else
strcpy(reply, "ERROR: Power management not supported");
#endif
} else {
sprintf(reply, "??: %s", config);
}
@ -375,8 +426,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
strcpy(reply, "OK");
} else if (memcmp(config, "flood.advert.interval ", 22) == 0) {
int hours = _atoi(&config[22]);
if ((hours > 0 && hours < 3) || (hours > 48)) {
strcpy(reply, "Error: interval range is 3-48 hours");
if ((hours > 0 && hours < 3) || (hours > 168)) {
strcpy(reply, "Error: interval range is 3-168 hours");
} else {
_prefs->flood_advert_interval = (uint8_t)(hours);
_callbacks->updateFloodAdvertTimer();
@ -397,22 +448,27 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
savePrefs();
strcpy(reply, "OK");
} else if (sender_timestamp == 0 &&
memcmp(config, "prv.key ", 8) == 0) { // from serial command line only
} else if (memcmp(config, "prv.key ", 8) == 0) {
uint8_t prv_key[PRV_KEY_SIZE];
bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
if (success) {
// only allow rekey if key is valid
if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) {
mesh::LocalIdentity new_id;
new_id.readFrom(prv_key, PRV_KEY_SIZE);
_callbacks->saveIdentity(new_id);
strcpy(reply, "OK");
strcpy(reply, "OK, reboot to apply! New pubkey: ");
mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE);
} else {
strcpy(reply, "Error, invalid key");
strcpy(reply, "Error, bad key");
}
} else if (memcmp(config, "name ", 5) == 0) {
StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
savePrefs();
strcpy(reply, "OK");
if (isValidName(&config[5])) {
StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, bad chars");
}
} else if (memcmp(config, "repeat ", 7) == 0) {
_prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
savePrefs();
@ -479,6 +535,16 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else {
strcpy(reply, "Error, cannot be negative");
}
} else if (memcmp(config, "owner.info ", 11) == 0) {
config += 11;
char *dp = _prefs->owner_info;
while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) {
*dp++ = (*config == '|') ? '\n' : *config; // translate '|' to newline chars
config++;
}
*dp = 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "tx ", 3) == 0) {
_prefs->tx_power_dbm = atoi(&config[3]);
savePrefs();

View file

@ -3,6 +3,7 @@
#include "Mesh.h"
#include <helpers/IdentityStore.h>
#include <helpers/SensorManager.h>
#include <helpers/ClientACL.h>
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
#define WITH_BRIDGE
@ -50,6 +51,7 @@ struct NodePrefs { // persisted to file
uint8_t advert_loc_policy;
uint32_t discovery_mod_timestamp;
float adc_multiplier;
char owner_info[120];
};
class CommonCLICallbacks {
@ -59,7 +61,7 @@ public:
virtual const char* getBuildDate() = 0;
virtual const char* getRole() = 0;
virtual bool formatFileSystem() = 0;
virtual void sendSelfAdvertisement(int delay_millis) = 0;
virtual void sendSelfAdvertisement(int delay_millis, bool flood) = 0;
virtual void updateAdvertTimer() = 0;
virtual void updateFloodAdvertTimer() = 0;
virtual void setLoggingOn(bool enable) = 0;
@ -93,6 +95,7 @@ class CommonCLI {
CommonCLICallbacks* _callbacks;
mesh::MainBoard* _board;
SensorManager* _sensors;
ClientACL* _acl;
char tmp[PRV_KEY_SIZE*2 + 4];
mesh::RTCClock* getRTCClock() { return _rtc; }
@ -100,8 +103,8 @@ class CommonCLI {
void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
public:
CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, NodePrefs* prefs, CommonCLICallbacks* callbacks)
: _board(&board), _rtc(&rtc), _sensors(&sensors), _prefs(prefs), _callbacks(callbacks) { }
CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks)
: _board(&board), _rtc(&rtc), _sensors(&sensors), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { }
void loadPrefs(FILESYSTEM* _fs);
void savePrefs(FILESYSTEM* _fs);

View file

@ -2,6 +2,7 @@
#include "NRF52Board.h"
#include <bluefruit.h>
#include <nrf_soc.h>
static BLEDfu bledfu;
@ -21,6 +22,222 @@ void NRF52Board::begin() {
startup_reason = BD_STARTUP_NORMAL;
}
#ifdef NRF52_POWER_MANAGEMENT
#include "nrf.h"
// Power Management global variables
uint32_t g_nrf52_reset_reason = 0; // Reset/Startup reason
uint8_t g_nrf52_shutdown_reason = 0; // Shutdown reason
// Early constructor - runs before SystemInit() clears the registers
// Priority 101 ensures this runs before SystemInit (102) and before
// any C++ static constructors (default 65535)
static void __attribute__((constructor(101))) nrf52_early_reset_capture() {
g_nrf52_reset_reason = NRF_POWER->RESETREAS;
g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2;
}
void NRF52Board::initPowerMgr() {
// Copy early-captured register values
reset_reason = g_nrf52_reset_reason;
shutdown_reason = g_nrf52_shutdown_reason;
boot_voltage_mv = 0; // Will be set by checkBootVoltage()
// Clear registers for next boot
// Note: At this point SoftDevice may or may not be enabled
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_reset_reason_clr(0xFFFFFFFF);
sd_power_gpregret_clr(1, 0xFF);
} else {
NRF_POWER->RESETREAS = 0xFFFFFFFF; // Write 1s to clear
NRF_POWER->GPREGRET2 = 0;
}
// Log reset/shutdown info
if (shutdown_reason != SHUTDOWN_REASON_NONE) {
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX); Shutdown = %s (0x%02X)",
getResetReasonString(reset_reason), (unsigned long)reset_reason,
getShutdownReasonString(shutdown_reason), shutdown_reason);
} else {
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)",
getResetReasonString(reset_reason), (unsigned long)reset_reason);
}
}
bool NRF52Board::isExternalPowered() {
// Check if SoftDevice is enabled before using its API
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
uint32_t usb_status;
sd_power_usbregstatus_get(&usb_status);
return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
} else {
return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
}
}
const char* NRF52Board::getResetReasonString(uint32_t reason) {
if (reason & POWER_RESETREAS_RESETPIN_Msk) return "Reset Pin";
if (reason & POWER_RESETREAS_DOG_Msk) return "Watchdog";
if (reason & POWER_RESETREAS_SREQ_Msk) return "Soft Reset";
if (reason & POWER_RESETREAS_LOCKUP_Msk) return "CPU Lockup";
#ifdef POWER_RESETREAS_LPCOMP_Msk
if (reason & POWER_RESETREAS_LPCOMP_Msk) return "Wake from LPCOMP";
#endif
#ifdef POWER_RESETREAS_VBUS_Msk
if (reason & POWER_RESETREAS_VBUS_Msk) return "Wake from VBUS";
#endif
#ifdef POWER_RESETREAS_OFF_Msk
if (reason & POWER_RESETREAS_OFF_Msk) return "Wake from GPIO";
#endif
#ifdef POWER_RESETREAS_DIF_Msk
if (reason & POWER_RESETREAS_DIF_Msk) return "Debug Interface";
#endif
return "Cold Boot";
}
const char* NRF52Board::getShutdownReasonString(uint8_t reason) {
switch (reason) {
case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage";
case SHUTDOWN_REASON_USER: return "User Request";
case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection";
}
return "Unknown";
}
bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) {
initPowerMgr();
// Read boot voltage
boot_voltage_mv = getBattMilliVolts();
if (config->voltage_bootlock == 0) return true; // Protection disabled
// Skip check if externally powered
if (isExternalPowered()) {
MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (external power)");
boot_voltage_mv = getBattMilliVolts();
return true;
}
MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage = %u mV (threshold = %u mV)",
boot_voltage_mv, config->voltage_bootlock);
// Only trigger shutdown if reading is valid (>1000mV) AND below threshold
// This prevents spurious shutdowns on ADC glitches or uninitialized reads
if (boot_voltage_mv > 1000 && boot_voltage_mv < config->voltage_bootlock) {
MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage too low - entering protective shutdown");
initiateShutdown(SHUTDOWN_REASON_BOOT_PROTECT);
return false; // Should never reach this
}
return true;
}
void NRF52Board::initiateShutdown(uint8_t reason) {
enterSystemOff(reason);
}
void NRF52Board::enterSystemOff(uint8_t reason) {
MESH_DEBUG_PRINTLN("PWRMGT: Entering SYSTEMOFF (%s)", getShutdownReasonString(reason));
// Record shutdown reason in GPREGRET2
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_gpregret_clr(1, 0xFF);
sd_power_gpregret_set(1, reason);
} else {
NRF_POWER->GPREGRET2 = reason;
}
// Flush serial buffers
Serial.flush();
delay(100);
// Enter SYSTEMOFF
if (sd_enabled) {
uint32_t err = sd_power_system_off();
if (err == NRF_ERROR_SOFTDEVICE_NOT_ENABLED) { //SoftDevice not enabled
sd_enabled = 0;
}
}
if (!sd_enabled) {
// SoftDevice not available; write directly to POWER->SYSTEMOFF
NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter;
}
// If we get here, something went wrong. Reset to recover.
NVIC_SystemReset();
}
void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
// LPCOMP is not managed by SoftDevice - direct register access required
// Halt and disable before reconfiguration
NRF_LPCOMP->TASKS_STOP = 1;
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled;
// Select analog input (AIN0-7 maps to PSEL 0-7)
NRF_LPCOMP->PSEL = ((uint32_t)ain_channel << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk;
// Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk;
// Detect UP events (voltage rises above threshold for battery recovery)
NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up;
// Enable 50mV hysteresis for noise immunity
NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV;
// Clear stale events/interrupts before enabling wake
NRF_LPCOMP->EVENTS_READY = 0;
NRF_LPCOMP->EVENTS_DOWN = 0;
NRF_LPCOMP->EVENTS_UP = 0;
NRF_LPCOMP->EVENTS_CROSS = 0;
NRF_LPCOMP->INTENCLR = 0xFFFFFFFF;
NRF_LPCOMP->INTENSET = LPCOMP_INTENSET_UP_Msk;
// Enable LPCOMP
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled;
NRF_LPCOMP->TASKS_START = 1;
// Wait for comparator to settle before entering SYSTEMOFF
for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) {
delayMicroseconds(50);
}
if (refsel == 7) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel);
} else if (refsel <= 6) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)",
ain_channel, refsel + 1);
} else {
uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1);
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)",
ain_channel, ref_num);
}
// Configure VBUS (USB power) wake alongside LPCOMP
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_usbdetected_enable(1);
} else {
NRF_POWER->EVENTS_USBDETECTED = 0;
NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk;
}
MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured");
}
#endif
void NRF52BoardDCDC::begin() {
NRF52Board::begin();

View file

@ -5,11 +5,45 @@
#if defined(NRF52_PLATFORM)
#ifdef NRF52_POWER_MANAGEMENT
// Shutdown Reason Codes (stored in GPREGRET before SYSTEMOFF)
#define SHUTDOWN_REASON_NONE 0x00
#define SHUTDOWN_REASON_LOW_VOLTAGE 0x4C // 'L' - Runtime low voltage threshold
#define SHUTDOWN_REASON_USER 0x55 // 'U' - User requested powerOff()
#define SHUTDOWN_REASON_BOOT_PROTECT 0x42 // 'B' - Boot voltage protection
// Boards provide this struct with their hardware-specific settings and callbacks.
struct PowerMgtConfig {
// LPCOMP wake configuration (for voltage recovery from SYSTEMOFF)
uint8_t lpcomp_ain_channel; // AIN0-7 for voltage sensing pin
uint8_t lpcomp_refsel; // REFSEL value: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16
// Boot protection voltage threshold (millivolts)
// Set to 0 to disable boot protection
uint16_t voltage_bootlock;
};
#endif
class NRF52Board : public mesh::MainBoard {
#ifdef NRF52_POWER_MANAGEMENT
void initPowerMgr();
#endif
protected:
uint8_t startup_reason;
char *ota_name;
#ifdef NRF52_POWER_MANAGEMENT
uint32_t reset_reason; // RESETREAS register value
uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF)
uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts)
bool checkBootVoltage(const PowerMgtConfig* config);
void enterSystemOff(uint8_t reason);
void configureVoltageWake(uint8_t ain_channel, uint8_t refsel);
virtual void initiateShutdown(uint8_t reason);
#endif
public:
NRF52Board(char *otaname) : ota_name(otaname) {}
virtual void begin();
@ -17,6 +51,15 @@ public:
virtual float getMCUTemperature() override;
virtual void reboot() override { NVIC_SystemReset(); }
virtual bool startOTAUpdate(const char *id, char reply[]) override;
#ifdef NRF52_POWER_MANAGEMENT
bool isExternalPowered() override;
uint16_t getBootVoltage() override { return boot_voltage_mv; }
virtual uint32_t getResetReason() const override { return reset_reason; }
uint8_t getShutdownReason() const override { return shutdown_reason; }
const char* getResetReasonString(uint32_t reason) override;
const char* getShutdownReasonString(uint8_t reason) override;
#endif
};
/*

View file

@ -2,6 +2,45 @@
#include <helpers/TxtDataHelpers.h>
#include <SHA256.h>
// helper class for region map exporter, we emulate Stream with a safe buffer writer.
class BufStream : public Stream {
public:
BufStream(char *buf, size_t max_len)
: _buf(buf), _max_len(max_len), _pos(0) {
if (_max_len > 0) _buf[0] = 0;
}
size_t write(uint8_t c) override {
if (_pos + 1 >= _max_len) return 0;
_buf[_pos++] = c;
_buf[_pos] = 0;
return 1;
}
size_t write(const uint8_t *buffer, size_t size) override {
size_t written = 0;
while (written < size) {
if (!write(buffer[written])) break;
written++;
}
return written;
}
int available() override { return 0; }
int read() override { return -1; }
int peek() override { return -1; }
void flush() override {}
size_t length() const { return _pos; }
private:
char *_buf;
size_t _max_len;
size_t _pos;
};
RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
next_id = 1; num_regions = 0; home_id = 0;
wildcard.id = wildcard.parent = 0;
@ -9,8 +48,13 @@ RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
strcpy(wildcard.name, "*");
}
bool RegionMap::is_name_char(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '#';
bool RegionMap::is_name_char(uint8_t c) {
// accept all alpha-num or accented characters, but exclude most punctuation chars
return c == '-' || c == '$' || c == '#' || (c >= '0' && c <= '9') || c >= 'A';
}
static const char* skip_hash(const char* name) {
return *name == '#' ? name + 1 : name;
}
static File openWrite(FILESYSTEM* _fs, const char* filename) {
@ -24,12 +68,12 @@ static File openWrite(FILESYSTEM* _fs, const char* filename) {
#endif
}
bool RegionMap::load(FILESYSTEM* _fs) {
if (_fs->exists("/regions2")) {
bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
if (_fs->exists(path ? path : "/regions2")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/regions2", "r");
File file = _fs->open(path ? path : "/regions2", "r");
#else
File file = _fs->open("/regions2");
File file = _fs->open(path ? path : "/regions2");
#endif
if (file) {
@ -67,8 +111,8 @@ bool RegionMap::load(FILESYSTEM* _fs) {
return false; // failed
}
bool RegionMap::save(FILESYSTEM* _fs) {
File file = openWrite(_fs, "/regions2");
bool RegionMap::save(FILESYSTEM* _fs, const char* path) {
File file = openWrite(_fs, path ? path : "/regions2");
if (file) {
uint8_t pad[128];
memset(pad, 0, sizeof(pad));
@ -126,11 +170,17 @@ RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param)
TransportKey keys[4];
int num;
if (region->name[0] == '#') { // auto hashtag region
if (region->name[0] == '$') { // private region
num = _store->loadKeysFor(region->id, keys, 4);
} else if (region->name[0] == '#') { // auto hashtag region
_store->getAutoKeyFor(region->id, region->name, keys[0]);
num = 1;
} else {
num = _store->loadKeysFor(region->id, keys, 4);
} else { // new: implicit auto hashtag region
char tmp[sizeof(region->name)];
tmp[0] = '#';
strcpy(&tmp[1], region->name);
_store->getAutoKeyFor(region->id, tmp, keys[0]);
num = 1;
}
for (int j = 0; j < num; j++) {
uint16_t code = keys[j].calcTransportCode(packet);
@ -146,9 +196,10 @@ RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
RegionEntry* RegionMap::findByName(const char* name) {
if (strcmp(name, "*") == 0) return &wildcard;
if (*name == '#') { name++; } // ignore the '#' when matching by name
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(name, region->name) == 0) return region;
if (strcmp(name, skip_hash(region->name)) == 0) return region;
}
return NULL; // not found
}
@ -156,11 +207,12 @@ RegionEntry* RegionMap::findByName(const char* name) {
RegionEntry* RegionMap::findByNamePrefix(const char* prefix) {
if (strcmp(prefix, "*") == 0) return &wildcard;
if (*prefix == '#') { prefix++; } // ignore the '#' when matching by name
RegionEntry* partial = NULL;
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(prefix, region->name) == 0) return region; // is a complete match, preference this one
if (memcmp(prefix, region->name, strlen(prefix)) == 0) {
if (strcmp(prefix, skip_hash(region->name)) == 0) return region; // is a complete match, preference this one
if (memcmp(prefix, skip_hash(region->name), strlen(prefix)) == 0) {
partial = region;
}
}
@ -219,9 +271,9 @@ void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream&
}
if (parent->flags & REGION_DENY_FLOOD) {
out.printf("%s%s\n", parent->name, parent->id == home_id ? "^" : "");
out.printf("%s%s\n", skip_hash(parent->name), parent->id == home_id ? "^" : "");
} else {
out.printf("%s%s F\n", parent->name, parent->id == home_id ? "^" : "");
out.printf("%s%s F\n", skip_hash(parent->name), parent->id == home_id ? "^" : "");
}
for (int i = 0; i < num_regions; i++) {
@ -235,3 +287,43 @@ void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream&
void RegionMap::exportTo(Stream& out) const {
printChildRegions(0, &wildcard, out); // recursive
}
size_t RegionMap::exportTo(char *dest, size_t max_len) const {
if (!dest || max_len == 0) return 0;
BufStream bs(dest, max_len);
exportTo(bs); // ← reuse existing logic
return bs.length();
}
int RegionMap::exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert) {
char *dp = dest;
// Check wildcard region
bool wildcard_matches = invert ? (wildcard.flags & mask) : !(wildcard.flags & mask);
if (wildcard_matches) {
*dp++ = '*';
*dp++ = ',';
}
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
// Check if region matches the filter criteria
bool region_matches = invert ? (region->flags & mask) : !(region->flags & mask);
if (region_matches) {
int len = strlen(skip_hash(region->name));
if ((dp - dest) + len + 2 < max_len) { // only append if name will fit
memcpy(dp, skip_hash(region->name), len);
dp += len;
*dp++ = ',';
}
}
}
if (dp > dest) { dp--; } // don't include trailing comma
*dp = 0; // set null terminator
return dp - dest; // return length
}

View file

@ -30,10 +30,10 @@ class RegionMap {
public:
RegionMap(TransportKeyStore& store);
static bool is_name_char(char c);
static bool is_name_char(uint8_t c);
bool load(FILESYSTEM* _fs);
bool save(FILESYSTEM* _fs);
bool load(FILESYSTEM* _fs, const char* path=NULL);
bool save(FILESYSTEM* _fs, const char* path=NULL);
RegionEntry* putRegion(const char* name, uint16_t parent_id, uint16_t id = 0);
RegionEntry* findMatch(mesh::Packet* packet, uint8_t mask);
@ -47,6 +47,11 @@ public:
bool clear();
void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; }
int getCount() const { return num_regions; }
const RegionEntry* getByIdx(int i) const { return &regions[i]; }
const RegionEntry* getRoot() const { return &wildcard; }
int exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert = false);
void exportTo(Stream& out) const;
void exportTo(Stream& out) const;
size_t exportTo(char *dest, size_t max_len) const;
};

View file

@ -42,13 +42,14 @@ public:
uint32_t n_recv_flood,
uint32_t n_recv_direct) {
sprintf(reply,
"{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u}",
"{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u,\"recv_errors\":%u}",
driver.getPacketsRecv(),
driver.getPacketsSent(),
n_sent_flood,
n_sent_direct,
n_recv_flood,
n_recv_direct
n_recv_direct,
driver.getPacketsRecvErrors()
);
}
};

View file

@ -9,11 +9,21 @@
#define ADVERT_RESTART_DELAY 1000 // millis
void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
_pin_code = pin_code;
if (strcmp(name, "@@MAC") == 0) {
uint8_t addr[8];
memset(addr, 0, sizeof(addr));
esp_efuse_mac_get_default(addr);
sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param)
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
}
char dev_name[32+16];
sprintf(dev_name, "%s%s", prefix, name);
// Create the BLE Device
BLEDevice::init(device_name);
BLEDevice::init(dev_name);
BLEDevice::setSecurityCallbacks(this);
BLEDevice::setMTU(MAX_FRAME_SIZE);

View file

@ -61,7 +61,13 @@ public:
send_queue_len = recv_queue_len = 0;
}
void begin(const char* device_name, uint32_t pin_code);
/**
* init the BLE interface.
* @param prefix a prefix for the device name
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param pin_code the BLE security pin
*/
void begin(const char* prefix, char* name, uint32_t pin_code);
// BaseSerialInterface methods
void enable() override;

View file

@ -123,7 +123,7 @@ void SerialBLEInterface::onBLEEvent(ble_evt_t* evt) {
}
}
void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
instance = this;
char charpin[20];
@ -133,7 +133,17 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
// Bluefruit.autoConnLed(false);
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.begin();
char dev_name[32+16];
if (strcmp(name, "@@MAC") == 0) {
ble_gap_addr_t addr;
if (sd_ble_gap_addr_get(&addr) == NRF_SUCCESS) {
sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param)
addr.addr[5], addr.addr[4], addr.addr[3], addr.addr[2], addr.addr[1], addr.addr[0]);
}
}
sprintf(dev_name, "%s%s", prefix, name);
// Connection interval units: 1.25ms, supervision timeout units: 10ms
ble_gap_conn_params_t ppcp_params;
ppcp_params.min_conn_interval = BLE_MIN_CONN_INTERVAL;
@ -153,7 +163,7 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
}
Bluefruit.setTxPower(BLE_TX_POWER);
Bluefruit.setName(device_name);
Bluefruit.setName(dev_name);
Bluefruit.Security.setMITM(true);
Bluefruit.Security.setPIN(charpin);

View file

@ -52,7 +52,14 @@ public:
recv_queue_len = 0;
}
void begin(const char* device_name, uint32_t pin_code);
/**
* init the BLE interface.
* @param prefix a prefix for the device name
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param pin_code the BLE security pin
*/
void begin(const char* prefix, char* name, uint32_t pin_code);
void disconnect();
void enable() override;
void disable() override;

View file

@ -105,6 +105,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err);
len = 0;
n_recv_errors++;
} else {
// Serial.print(" readData() -> "); Serial.println(len);
n_recv++;

View file

@ -7,7 +7,7 @@ class RadioLibWrapper : public mesh::Radio {
protected:
PhysicalLayer* _radio;
mesh::MainBoard* _board;
uint32_t n_recv, n_sent;
uint32_t n_recv, n_sent, n_recv_errors;
int16_t _noise_floor, _threshold;
uint16_t _num_floor_samples;
int32_t _floor_sample_sum;
@ -45,8 +45,9 @@ public:
void loop() override;
uint32_t getPacketsRecv() const { return n_recv; }
uint32_t getPacketsRecvErrors() const { return n_recv_errors; }
uint32_t getPacketsSent() const { return n_sent; }
void resetStats() { n_recv = n_sent = 0; }
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
virtual float getLastRSSI() const override;
virtual float getLastSNR() const override;

View file

@ -42,7 +42,7 @@ static Adafruit_BME280 BME280;
#endif
#define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level
#include <Adafruit_BMP280.h>
static Adafruit_BMP280 BMP280;
static Adafruit_BMP280 BMP280(TELEM_WIRE);
#endif
#if ENV_INCLUDE_SHTC3
@ -58,6 +58,7 @@ static SensirionI2cSht4x SHT4X;
#if ENV_INCLUDE_LPS22HB
#include <Arduino_LPS22HB.h>
LPS22HBClass LPS22HB(*TELEM_WIRE);
#endif
#if ENV_INCLUDE_INA3221
@ -218,7 +219,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_SHTC3
if (SHTC3.begin()) {
if (SHTC3.begin(TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found sensor: SHTC3");
SHTC3_initialized = true;
} else {
@ -243,7 +244,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_LPS22HB
if (BARO.begin()) {
if (LPS22HB.begin()) {
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
LPS22HB_initialized = true;
} else {
@ -407,8 +408,8 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
#if ENV_INCLUDE_LPS22HB
if (LPS22HB_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BARO.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BARO.readPressure() * 10); // convert kPa to hPa
telemetry.addTemperature(TELEM_CHANNEL_SELF, LPS22HB.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, LPS22HB.readPressure() * 10); // convert kPa to hPa
}
#endif

View file

@ -7,6 +7,10 @@ bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) {
}
bool SSD1306Display::begin() {
if (!_isOn) {
if (_peripher_power) _peripher_power->claim();
_isOn = true;
}
#ifdef DISPLAY_ROTATION
display.setRotation(DISPLAY_ROTATION);
#endif
@ -15,12 +19,18 @@ bool SSD1306Display::begin() {
void SSD1306Display::turnOn() {
display.ssd1306_command(SSD1306_DISPLAYON);
_isOn = true;
if (!_isOn) {
if (_peripher_power) _peripher_power->claim();
_isOn = true;
}
}
void SSD1306Display::turnOff() {
display.ssd1306_command(SSD1306_DISPLAYOFF);
_isOn = false;
if (_isOn) {
if (_peripher_power) _peripher_power->release();
_isOn = false;
}
}
void SSD1306Display::clear() {

View file

@ -5,6 +5,7 @@
#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)
@ -18,10 +19,16 @@ class SSD1306Display : public DisplayDriver {
Adafruit_SSD1306 display;
bool _isOn;
uint8_t _color;
RefCountedDigitalPin* _peripher_power;
bool i2c_probe(TwoWire& wire, uint8_t addr);
public:
SSD1306Display() : DisplayDriver(128, 64), display(128, 64, &Wire, PIN_OLED_RESET) { _isOn = false; }
SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
display(128, 64, &Wire, PIN_OLED_RESET),
_peripher_power(peripher_power)
{
_isOn = false;
}
bool begin();
bool isOn() override { return _isOn; }

View file

@ -28,11 +28,14 @@ bool ST7789LCDDisplay::begin() {
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.
#ifdef LILYGO_TDECK
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
displaySPI.begin(PIN_TFT_SCL, -1, PIN_TFT_SDA, PIN_TFT_CS);
#endif

View file

@ -8,7 +8,7 @@
#include <helpers/RefCountedDigitalPin.h>
class ST7789LCDDisplay : public DisplayDriver {
#ifdef LILYGO_TDECK
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
SPIClass displaySPI;
#endif
Adafruit_ST7789 display;
@ -25,7 +25,7 @@ public:
{
_isOn = false;
}
#elif LILYGO_TDECK
#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
displaySPI(HSPI),
display(&displaySPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST),

View file

@ -3,6 +3,35 @@
#include <Arduino.h>
#include <Wire.h>
#ifdef NRF52_POWER_MANAGEMENT
// Static configuration for power management
// Values come from variant.h defines
const PowerMgtConfig power_config = {
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
};
void T114Board::initiateShutdown(uint8_t reason) {
#if ENV_INCLUDE_GPS == 1
pinMode(GPS_EN, OUTPUT);
digitalWrite(GPS_EN, LOW);
#endif
digitalWrite(SX126X_POWER_EN, LOW);
bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
reason == SHUTDOWN_REASON_BOOT_PROTECT);
pinMode(PIN_BAT_CTL, OUTPUT);
digitalWrite(PIN_BAT_CTL, enable_lpcomp ? HIGH : LOW);
if (enable_lpcomp) {
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
}
enterSystemOff(reason);
}
#endif // NRF52_POWER_MANAGEMENT
void T114Board::begin() {
NRF52Board::begin();
NRF_POWER->DCDCEN = 1;
@ -21,6 +50,11 @@ void T114Board::begin() {
#endif
pinMode(SX126X_POWER_EN, OUTPUT);
#ifdef NRF52_POWER_MANAGEMENT
// Boot voltage protection check (may not return if voltage too low)
// We need to call this after we configure SX126X_POWER_EN as output but before we pull high
checkBootVoltage(&power_config);
#endif
digitalWrite(SX126X_POWER_EN, HIGH);
delay(10); // give sx1262 some time to power up
}

View file

@ -10,6 +10,11 @@
#define MV_LSB (3000.0F / 4096.0F) // 12-bit ADC with 3.0V input range
class T114Board : public NRF52BoardDCDC {
protected:
#ifdef NRF52_POWER_MANAGEMENT
void initiateShutdown(uint8_t reason) override;
#endif
public:
T114Board() : NRF52Board("T114_OTA") {}
void begin();
@ -42,13 +47,13 @@ public:
}
void powerOff() override {
#ifdef LED_PIN
#ifdef LED_PIN
digitalWrite(LED_PIN, HIGH);
#endif
#if ENV_INCLUDE_GPS == 1
#endif
#if ENV_INCLUDE_GPS == 1
pinMode(GPS_EN, OUTPUT);
digitalWrite(GPS_EN, LOW);
#endif
#endif
sd_power_system_off();
}
};

View file

@ -11,6 +11,7 @@ build_flags = ${nrf52_base.build_flags}
-I variants/heltec_t114
-I src/helpers/ui
-D HELTEC_T114
-D NRF52_POWER_MANAGEMENT
-D P_LORA_DIO_1=20
-D P_LORA_NSS=24
-D P_LORA_RESET=25

View file

@ -30,6 +30,14 @@
#define AREF_VOLTAGE (3.0)
// Power management boot protection threshold (millivolts)
// Set to 0 to disable boot protection
#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV)
// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
// AIN2 = P0.04 = BATTERY_PIN / PIN_VBAT_READ
#define PWRMGT_LPCOMP_AIN 2
#define PWRMGT_LPCOMP_REFSEL 1 // 2/8 VDD (~3.68-4.04V)
////////////////////////////////////////////////////////////////////////////////
// Number of pins
@ -50,8 +58,8 @@
////////////////////////////////////////////////////////////////////////////////
// I2C pin definition
#define PIN_WIRE_SDA (26) // P0.26
#define PIN_WIRE_SCL (27) // P0.27
#define PIN_WIRE_SDA (16) // P0.16
#define PIN_WIRE_SCL (13) // P0.13
////////////////////////////////////////////////////////////////////////////////
// SPI pin definition

View file

@ -323,7 +323,7 @@ lib_deps =
extends = Heltec_lora32_v3
build_flags =
${Heltec_lora32_v3.build_flags}
-D MAX_CONTACTS=140
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1

View file

@ -86,5 +86,9 @@ void HeltecV4Board::begin() {
}
const char* HeltecV4Board::getManufacturerName() const {
return "Heltec V4";
#ifdef HELTEC_LORA_V4_TFT
return "Heltec V4 TFT";
#else
return "Heltec V4 OLED";
#endif
}

View file

@ -20,11 +20,9 @@ build_flags =
-D P_LORA_PA_POWER=7 ;power en
-D P_LORA_PA_EN=2
-D P_LORA_PA_TX_EN=46 ;enable tx
-D PIN_BOARD_SDA=17
-D PIN_BOARD_SCL=18
-D PIN_USER_BTN=0
-D PIN_VEXT_EN=36
-D PIN_VEXT_EN_ACTIVE=HIGH
-D PIN_VEXT_EN_ACTIVE=LOW
-D LORA_TX_POWER=10 ;If it is configured as 10 here, the final output will be 22 dbm.
-D MAX_LORA_TX_POWER=22 ; Max SX1262 output
-D SX126X_DIO2_AS_RF_SWITCH=true
@ -47,10 +45,44 @@ lib_deps =
${esp32_base.lib_deps}
${sensor_base.lib_deps}
[env:heltec_v4_repeater]
[heltec_v4_oled]
extends = Heltec_lora32_v4
build_flags =
${Heltec_lora32_v4.build_flags}
-D HELTEC_LORA_V4_OLED
-D PIN_BOARD_SDA=17
-D PIN_BOARD_SCL=18
-D ENV_PIN_SDA=4
-D ENV_PIN_SCL=3
build_src_filter= ${Heltec_lora32_v4.build_src_filter}
lib_deps = ${Heltec_lora32_v4.lib_deps}
[heltec_v4_tft]
extends = Heltec_lora32_v4
build_flags =
${Heltec_lora32_v4.build_flags}
-D HELTEC_LORA_V4_TFT
-D PIN_BOARD_SDA=4
-D PIN_BOARD_SCL=3
-D DISPLAY_SCALE_X=2.5
-D DISPLAY_SCALE_Y=3.75
-D PIN_TFT_RST=18
-D PIN_TFT_VDD_CTL=-1
-D PIN_TFT_LEDA_CTL=21
-D PIN_TFT_LEDA_CTL_ACTIVE=HIGH
-D PIN_TFT_CS=15
-D PIN_TFT_DC=16
-D PIN_TFT_SCL=17
-D PIN_TFT_SDA=33
build_src_filter= ${Heltec_lora32_v4.build_src_filter}
lib_deps =
${Heltec_lora32_v4.lib_deps}
adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
[env:heltec_v4_repeater]
extends = heltec_v4_oled
build_flags =
${heltec_v4_oled.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"Heltec Repeater"'
-D ADVERT_LAT=0.0
@ -59,18 +91,18 @@ build_flags =
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v4.build_src_filter}
build_src_filter = ${heltec_v4_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
lib_deps =
${Heltec_lora32_v4.lib_deps}
${heltec_v4_oled.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:heltec_v4_repeater_bridge_espnow]
extends = Heltec_lora32_v4
extends = heltec_v4_oled
build_flags =
${Heltec_lora32_v4.build_flags}
${heltec_v4_oled.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"ESPNow Bridge"'
-D ADVERT_LAT=0.0
@ -81,18 +113,18 @@ build_flags =
; -D BRIDGE_DEBUG=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v4.build_src_filter}
build_src_filter = ${heltec_v4_oled.build_src_filter}
+<helpers/bridges/ESPNowBridge.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
lib_deps =
${Heltec_lora32_v4.lib_deps}
${heltec_v4_oled.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_v4_room_server]
extends = Heltec_lora32_v4
extends = heltec_v4_oled
build_flags =
${Heltec_lora32_v4.build_flags}
${heltec_v4_oled.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"Heltec Room"'
-D ADVERT_LAT=0.0
@ -101,50 +133,50 @@ build_flags =
-D ROOM_PASSWORD='"hello"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v4.build_src_filter}
build_src_filter = ${heltec_v4_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_room_server>
lib_deps =
${Heltec_lora32_v4.lib_deps}
${heltec_v4_oled.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_v4_terminal_chat]
extends = Heltec_lora32_v4
extends = heltec_v4_oled
build_flags =
${Heltec_lora32_v4.build_flags}
${heltec_v4_oled.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v4.build_src_filter}
build_src_filter = ${heltec_v4_oled.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
lib_deps =
${Heltec_lora32_v4.lib_deps}
${heltec_v4_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_companion_radio_usb]
extends = Heltec_lora32_v4
extends = heltec_v4_oled
build_flags =
${Heltec_lora32_v4.build_flags}
${heltec_v4_oled.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=SSD1306Display
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v4.build_src_filter}
build_src_filter = ${heltec_v4_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_lora32_v4.lib_deps}
${heltec_v4_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_companion_radio_ble]
extends = Heltec_lora32_v4
extends = heltec_v4_oled
build_flags =
${Heltec_lora32_v4.build_flags}
${heltec_v4_oled.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
@ -155,20 +187,20 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v4.build_src_filter}
build_src_filter = ${heltec_v4_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_lora32_v4.lib_deps}
${heltec_v4_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_companion_radio_wifi]
extends = Heltec_lora32_v4
extends = heltec_v4_oled
build_flags =
${Heltec_lora32_v4.build_flags}
${heltec_v4_oled.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
@ -176,24 +208,23 @@ build_flags =
-D WIFI_DEBUG_LOGGING=1
-D WIFI_SSID='"myssid"'
-D WIFI_PWD='"mypwd"'
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v4.build_src_filter}
build_src_filter = ${heltec_v4_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_lora32_v4.lib_deps}
${heltec_v4_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_sensor]
extends = Heltec_lora32_v4
extends = heltec_v4_oled
build_flags =
${Heltec_lora32_v4.build_flags}
-D ADVERT_NAME='"Heltec v3 Sensor"'
${heltec_v4_oled.build_flags}
-D ADVERT_NAME='"Heltec v4 Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
@ -202,9 +233,172 @@ build_flags =
-D DISPLAY_CLASS=SSD1306Display
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v4.build_src_filter}
build_src_filter = ${heltec_v4_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_sensor>
lib_deps =
${Heltec_lora32_v4.lib_deps}
${heltec_v4_oled.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_v4_tft_repeater]
extends = heltec_v4_tft
build_flags =
${heltec_v4_tft.build_flags}
-D DISPLAY_CLASS=ST7789LCDDisplay
-D ADVERT_NAME='"Heltec Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${heltec_v4_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<../examples/simple_repeater>
lib_deps =
${heltec_v4_tft.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:heltec_v4_tft_repeater_bridge_espnow]
extends = heltec_v4_tft
build_flags =
${heltec_v4_tft.build_flags}
-D DISPLAY_CLASS=ST7789LCDDisplay
-D ADVERT_NAME='"ESPNow Bridge"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_ESPNOW_BRIDGE=1
; -D BRIDGE_DEBUG=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${heltec_v4_tft.build_src_filter}
+<helpers/bridges/ESPNowBridge.cpp>
+<helpers/ui/ST7789LCDDisplay.cpp>
+<../examples/simple_repeater>
lib_deps =
${heltec_v4_tft.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_v4_tft_room_server]
extends = heltec_v4_tft
build_flags =
${heltec_v4_tft.build_flags}
-D DISPLAY_CLASS=ST7789LCDDisplay
-D ADVERT_NAME='"Heltec Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${heltec_v4_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<../examples/simple_room_server>
lib_deps =
${heltec_v4_tft.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_v4_tft_terminal_chat]
extends = heltec_v4_tft
build_flags =
${heltec_v4_tft.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${heltec_v4_tft.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
lib_deps =
${heltec_v4_tft.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_tft_companion_radio_usb]
extends = heltec_v4_tft
build_flags =
${heltec_v4_tft.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=ST7789LCDDisplay
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${heltec_v4_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${heltec_v4_tft.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_tft_companion_radio_ble]
extends = heltec_v4_tft
build_flags =
${heltec_v4_tft.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=ST7789LCDDisplay
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${heltec_v4_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${heltec_v4_tft.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_tft_companion_radio_wifi]
extends = heltec_v4_tft
build_flags =
${heltec_v4_tft.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=ST7789LCDDisplay
-D WIFI_DEBUG_LOGGING=1
-D WIFI_SSID='"myssid"'
-D WIFI_PWD='"mypwd"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${heltec_v4_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${heltec_v4_tft.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_tft_sensor]
extends = heltec_v4_tft
build_flags =
${heltec_v4_tft.build_flags}
-D ADVERT_NAME='"Heltec v4 Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ENV_PIN_SDA=3
-D ENV_PIN_SCL=4
-D DISPLAY_CLASS=ST7789LCDDisplay
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${heltec_v4_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<../examples/simple_sensor>
lib_deps =
${heltec_v4_tft.lib_deps}
${esp32_ota.lib_deps}

View file

@ -24,7 +24,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
DISPLAY_CLASS display(&(board.periph_power));
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif

View file

@ -9,7 +9,11 @@
#include <helpers/SensorManager.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
#ifdef HELTEC_LORA_V4_OLED
#include <helpers/ui/SSD1306Display.h>
#elif defined(HELTEC_LORA_V4_TFT)
#include <helpers/ui/ST7789LCDDisplay.h>
#endif
#include <helpers/ui/MomentaryButton.h>
#endif

View file

@ -5,7 +5,7 @@ build_flags =
${esp32_base.build_flags}
-I variants/heltec_wireless_paper
-D HELTEC_WIRELESS_PAPER
-D ARDUINO_USB_CDC_ON_BOOT=1 ; need for Serial
;-D ARDUINO_USB_CDC_ON_BOOT=1 ; this breaks Serial
-D P_LORA_DIO_1=14
-D P_LORA_NSS=8
-D P_LORA_RESET=RADIOLIB_NC
@ -17,8 +17,8 @@ build_flags =
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D P_LORA_TX_LED=18
-D PIN_BOARD_SDA=17
-D PIN_BOARD_SCL=18
;-D PIN_BOARD_SDA=17
;-D PIN_BOARD_SCL=18 ; same GPIO as P_LORA_TX_LED
-D PIN_USER_BTN=0
-D PIN_VEXT_EN=45
-D PIN_VBAT_READ=20
@ -139,4 +139,4 @@ build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter}
+<../examples/simple_room_server>
lib_deps =
${Heltec_Wireless_Paper_base.lib_deps}
${esp32_ota.lib_deps}
${esp32_ota.lib_deps}

View file

@ -1,8 +1,5 @@
[ikoka_nrf52]
extends = Xiao_nrf52
lib_deps = ${nrf52_base.lib_deps}
${sensor_base.lib_deps}
densaugeo/base64 @ ~1.4.0
[ikoka_handheld_nrf]
extends = nrf52_base
build_flags = ${nrf52_base.build_flags}
${sensor_base.build_flags}
-I lib/nrf52/s140_nrf52_7.3.0_API/include
@ -26,12 +23,15 @@ build_flags = ${nrf52_base.build_flags}
build_src_filter = ${nrf52_base.build_src_filter}
+<../variants/ikoka_handheld_nrf>
+<helpers/sensors>
lib_deps = ${nrf52_base.lib_deps}
${sensor_base.lib_deps}
densaugeo/base64 @ ~1.4.0
# larger screen has a different driver, this is for the 0.96 inch
[ikoka_nrf52_ssd1306_companion]
lib_deps = ${ikoka_nrf52.lib_deps}
[ikoka_handheld_nrf_ssd1306_companion]
lib_deps = ${ikoka_handheld_nrf.lib_deps}
adafruit/Adafruit SSD1306 @ ^2.5.13
build_flags = ${ikoka_nrf52.build_flags}
build_flags = ${ikoka_handheld_nrf.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D DISPLAY_ROTATION=0
-D PIN_WIRE_SCL=D6
@ -42,62 +42,62 @@ build_flags = ${ikoka_nrf52.build_flags}
-D OFFLINE_QUEUE_SIZE=256
-D QSPIFLASH=1
-I examples/companion_radio/ui-new
build_src_filter = ${ikoka_nrf52.build_src_filter}
build_src_filter = ${ikoka_handheld_nrf.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/companion_radio/ui-new/UITask.cpp>
+<../examples/companion_radio/*.cpp>
[env:ikoka_handheld_nrf_e22_30dbm_096_companion_radio_ble]
extends = ikoka_nrf52
build_flags = ${ikoka_nrf52_ssd1306_companion.build_flags}
build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
-D BLE_PIN_CODE=123456
-D LORA_TX_POWER=20
build_src_filter = ${ikoka_nrf52_ssd1306_companion.build_src_filter}
build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
[env:ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_ble]
extends = ikoka_nrf52
build_flags = ${ikoka_nrf52_ssd1306_companion.build_flags}
build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
-D BLE_PIN_CODE=123456
-D LORA_TX_POWER=20
-D DISPLAY_ROTATION=2
build_src_filter = ${ikoka_nrf52_ssd1306_companion.build_src_filter}
-D DISPLAY_ROTATION=2
build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
[env:ikoka_handheld_nrf_e22_30dbm_096_companion_radio_usb]
extends = ikoka_nrf52
build_flags = ${ikoka_nrf52_ssd1306_companion.build_flags}
build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
-D LORA_TX_POWER=20
build_src_filter = ${ikoka_nrf52_ssd1306_companion.build_src_filter}
build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
[env:ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_usb]
extends = ikoka_nrf52
build_flags = ${ikoka_nrf52_ssd1306_companion.build_flags}
build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags}
-D LORA_TX_POWER=20
-D DISPLAY_ROTATION=2
build_src_filter = ${ikoka_nrf52_ssd1306_companion.build_src_filter}
build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
[env:ikoka_handheld_nrf_e22_30dbm_repeater]
extends = ikoka_nrf52
build_flags =
${ikoka_nrf52.build_flags}
${ikoka_handheld_nrf.build_flags}
-D ADVERT_NAME='"ikoka_handheld Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D LORA_TX_POWER=20
build_src_filter = ${ikoka_nrf52.build_src_filter}
-D LORA_TX_POWER=20
build_src_filter = ${ikoka_handheld_nrf.build_src_filter}
+<../examples/simple_repeater/*.cpp>
[env:ikoka_handheld_nrf_e22_30dbm_room_server]
extends = ikoka_nrf52
build_flags =
${ikoka_nrf52.build_flags}
${ikoka_handheld_nrf.build_flags}
-D ADVERT_NAME='"ikoka_handheld Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D LORA_TX_POWER=20
build_src_filter = ${ikoka_nrf52.build_src_filter}
-D LORA_TX_POWER=20
build_src_filter = ${ikoka_handheld_nrf.build_src_filter}
+<../examples/simple_room_server/*.cpp>

View file

@ -1,151 +1,124 @@
[nrf52840_xiao]
[ikoka_nano_nrf]
extends = nrf52_base
platform_packages =
toolchain-gccarmnoneeabi@~1.100301.0
framework-arduinoadafruitnrf52
board = seeed-xiao-afruitnrf52-nrf52840
board_build.ldscript = boards/nrf52840_s140_v7.ld
build_flags = ${nrf52_base.build_flags}
${sensor_base.build_flags}
-D NRF52_PLATFORM -D XIAO_NRF52
-I lib/nrf52/s140_nrf52_7.3.0_API/include
-I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
lib_ignore =
BluetoothOTA
lvgl
lib5b4
lib_deps =
${nrf52_base.lib_deps}
rweather/Crypto @ ^0.4.0
adafruit/Adafruit INA3221 Library @ ^1.0.1
adafruit/Adafruit INA219 @ ^1.2.3
adafruit/Adafruit AHTX0 @ ^2.0.5
adafruit/Adafruit BME280 Library @ ^2.3.0
adafruit/Adafruit SSD1306 @ ^2.5.13
[ikoka_nano_nrf_baseboard]
extends = nrf52840_xiao
;board_build.ldscript = boards/nrf52840_s140_v7.ld
build_flags = ${nrf52840_xiao.build_flags}
-D P_LORA_TX_LED=11
-I variants/ikoka_nano_nrf
-I src/helpers/nrf52
-D P_LORA_TX_LED=11
-D DISPLAY_CLASS=NullDisplayDriver
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D P_LORA_DIO_1=D1
; -D P_LORA_BUSY=D3
-D P_LORA_BUSY=D2 ; specific to ikoka nano variant.
; -D P_LORA_RESET=D2
-D P_LORA_RESET=D3 ; specific to ikoka nano variant.
; -D P_LORA_NSS=D4
-D P_LORA_NSS=D0 ; specific to ikoka nano variant.
; -D SX126X_RXEN=D5
-D P_LORA_BUSY=D2
-D P_LORA_RESET=D3
-D P_LORA_NSS=D0
-D SX126X_RXEN=D7
-D SX126X_TXEN=RADIOLIB_NC
-D SX126X_DIO2_AS_RF_SWITCH=1
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D PIN_WIRE_SCL=5 ; specific to ikoka nano variant.
-D PIN_WIRE_SDA=4 ; specific to ikoka nano variant.
-D ENV_INCLUDE_AHTX0=1
-D ENV_INCLUDE_BME280=1
-D ENV_INCLUDE_INA3221=1
-D ENV_INCLUDE_INA219=1
-D PIN_WIRE_SCL=5
-D PIN_WIRE_SDA=4
-UENV_INCLUDE_GPS
debug_tool = jlink
upload_protocol = nrfutil
;;; abstracted hardware variants
lib_deps = ${nrf52_base.lib_deps}
${sensor_base.lib_deps}
[ikoka_nano_nrf_e22_22dbm]
extends = ikoka_nano_nrf_baseboard
extends = ikoka_nano_nrf
; No PA in this model, full 22dBm
build_flags =
${ikoka_nano_nrf_baseboard.build_flags}
${ikoka_nano_nrf.build_flags}
-D MANUFACTURER_STRING='"Ikoka Nano-E22-22dBm (Xiao_nrf52)"'
-D LORA_TX_POWER=22
build_src_filter = ${nrf52840_xiao.build_src_filter}
build_src_filter = ${ikoka_nano_nrf.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors>
+<helpers/ui/NullDisplayDriver.cpp>
+<../variants/ikoka_nano_nrf>
[ikoka_nano_nrf_e22_30dbm]
extends = ikoka_nano_nrf_baseboard
extends = ikoka_nano_nrf
; limit txpower to 20dBm on E22-900M30S. Anything higher will
; cause distortion in the PA output. 20dBm in -> 30dBm out
build_flags =
${ikoka_nano_nrf_baseboard.build_flags}
${ikoka_nano_nrf.build_flags}
-D MANUFACTURER_STRING='"Ikoka Nano-E22-30dBm (Xiao_nrf52)"'
-D LORA_TX_POWER=20
build_src_filter = ${nrf52840_xiao.build_src_filter}
build_src_filter = ${ikoka_nano_nrf.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors>
+<helpers/ui/NullDisplayDriver.cpp>
+<../variants/ikoka_nano_nrf>
[ikoka_nano_nrf_e22_33dbm]
extends = ikoka_nano_nrf_baseboard
extends = ikoka_nano_nrf
; limit txpower to 9dBm on E22-900M33S to avoid hardware damage
; to the rf amplifier frontend. 9dBm in -> 33dBm out
build_flags =
${ikoka_nano_nrf_baseboard.build_flags}
${ikoka_nano_nrf.build_flags}
-D MANUFACTURER_STRING='"Ikoka Nano-E22-33dBm (Xiao_nrf52)"'
-D LORA_TX_POWER=9
build_src_filter = ${nrf52840_xiao.build_src_filter}
build_src_filter = ${ikoka_nano_nrf.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors>
+<helpers/ui/NullDisplayDriver.cpp>
+<../variants/ikoka_nano_nrf>
;;; abstracted firmware roles
[ikoka_nano_nrf_companion_radio_ble]
extends = ikoka_nano_nrf_baseboard
extends = ikoka_nano_nrf
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
board_upload.maximum_size = 708608
build_flags =
${ikoka_nano_nrf_baseboard.build_flags}
${ikoka_nano_nrf.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
-I examples/companion_radio/ui-new
-D QSPIFLASH=1
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
build_src_filter = ${ikoka_nano_nrf.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${ikoka_nano_nrf_baseboard.lib_deps}
${ikoka_nano_nrf.lib_deps}
densaugeo/base64 @ ~1.4.0
[ikoka_nano_nrf_companion_radio_usb]
extends = ikoka_nano_nrf_baseboard
extends = ikoka_nano_nrf
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
board_upload.maximum_size = 708608
build_flags =
${ikoka_nano_nrf_baseboard.build_flags}
${ikoka_nano_nrf.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-I examples/companion_radio/ui-new
-D QSPIFLASH=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
build_src_filter = ${ikoka_nano_nrf.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${ikoka_nano_nrf_baseboard.lib_deps}
${ikoka_nano_nrf.lib_deps}
densaugeo/base64 @ ~1.4.0
[ikoka_nano_nrf_repeater]
extends = ikoka_nano_nrf_baseboard
extends = ikoka_nano_nrf
build_flags =
${ikoka_nano_nrf_baseboard.build_flags}
${ikoka_nano_nrf.build_flags}
-D ADVERT_NAME='"Ikoka Nano Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
@ -153,26 +126,23 @@ build_flags =
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
build_src_filter = ${ikoka_nano_nrf.build_src_filter}
+<../examples/simple_repeater/*.cpp>
[ikoka_nano_nrf_room_server]
extends = ikoka_nano_nrf_baseboard
extends = ikoka_nano_nrf
build_flags =
${ikoka_nano_nrf_baseboard.build_flags}
${ikoka_nano_nrf.build_flags}
-D ADVERT_NAME='"Ikoka Nano Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
build_src_filter = ${ikoka_nano_nrf.build_src_filter}
+<../examples/simple_room_server/*.cpp>
;;; hardware + firmware variants
;;; 22dBm EBYTE E22-900M22 variants
[env:ikoka_nano_nrf_22dbm_companion_radio_usb]
extends =
ikoka_nano_nrf_e22_22dbm
@ -219,7 +189,6 @@ build_src_filter =
;;; 30dBm EBYTE E22-900M30 variants
[env:ikoka_nano_nrf_30dbm_companion_radio_usb]
extends =
ikoka_nano_nrf_e22_30dbm
@ -266,7 +235,6 @@ build_src_filter =
;;; 33dBm EBYTE E22-900M33 variants
[env:ikoka_nano_nrf_33dbm_companion_radio_usb]
extends =
ikoka_nano_nrf_e22_33dbm

View file

@ -1,34 +1,15 @@
[nrf52840_xiao]
[ikoka_stick_nrf]
extends = nrf52_base
platform_packages =
toolchain-gccarmnoneeabi@~1.100301.0
framework-arduinoadafruitnrf52
board = seeed-xiao-afruitnrf52-nrf52840
board_build.ldscript = boards/nrf52840_s140_v7.ld
build_flags = ${nrf52_base.build_flags}
${sensor_base.build_flags}
-D NRF52_PLATFORM -D XIAO_NRF52
-I lib/nrf52/s140_nrf52_7.3.0_API/include
-I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
lib_ignore =
BluetoothOTA
lvgl
lib5b4
lib_deps =
${nrf52_base.lib_deps}
rweather/Crypto @ ^0.4.0
adafruit/Adafruit INA3221 Library @ ^1.0.1
adafruit/Adafruit INA219 @ ^1.2.3
adafruit/Adafruit AHTX0 @ ^2.0.5
adafruit/Adafruit BME280 Library @ ^2.3.0
adafruit/Adafruit SSD1306 @ ^2.5.13
[ikoka_stick_nrf_baseboard]
extends = nrf52840_xiao
;board_build.ldscript = boards/nrf52840_s140_v7.ld
build_flags = ${nrf52840_xiao.build_flags}
-D P_LORA_TX_LED=11
-I variants/ikoka_stick_nrf
-I src/helpers/nrf52
-D P_LORA_TX_LED=11
-D DISPLAY_CLASS=SSD1306Display
-D DISPLAY_ROTATION=2
-D RADIO_CLASS=CustomSX1262
@ -46,24 +27,18 @@ build_flags = ${nrf52840_xiao.build_flags}
-D PIN_USER_BTN=0
-D PIN_WIRE_SCL=7
-D PIN_WIRE_SDA=6
-D ENV_INCLUDE_AHTX0=1
-D ENV_INCLUDE_BME280=1
-D ENV_INCLUDE_INA3221=1
-D ENV_INCLUDE_INA219=1
debug_tool = jlink
upload_protocol = nrfutil
;;; abstracted hardware variants
-UENV_INCLUDE_GPS
lib_deps = ${nrf52_base.lib_deps}
${sensor_base.lib_deps}
[ikoka_stick_nrf_e22_22dbm]
extends = ikoka_stick_nrf_baseboard
extends = ikoka_stick_nrf
; No PA in this model, full 22dBm
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
${ikoka_stick_nrf.build_flags}
-D MANUFACTURER_STRING='"Ikoka Stick-E22-22dBm (Xiao_nrf52)"'
-D LORA_TX_POWER=22
build_src_filter = ${nrf52840_xiao.build_src_filter}
build_src_filter = ${ikoka_stick_nrf.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors>
+<helpers/ui/MomentaryButton.cpp>
@ -71,14 +46,14 @@ build_src_filter = ${nrf52840_xiao.build_src_filter}
+<../variants/ikoka_stick_nrf>
[ikoka_stick_nrf_e22_30dbm]
extends = ikoka_stick_nrf_baseboard
extends = ikoka_stick_nrf
; limit txpower to 20dBm on E22-900M30S. Anything higher will
; cause distortion in the PA output. 20dBm in -> 30dBm out
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
${ikoka_stick_nrf.build_flags}
-D MANUFACTURER_STRING='"Ikoka Stick-E22-30dBm (Xiao_nrf52)"'
-D LORA_TX_POWER=20
build_src_filter = ${nrf52840_xiao.build_src_filter}
build_src_filter = ${ikoka_stick_nrf.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors>
+<helpers/ui/MomentaryButton.cpp>
@ -86,14 +61,14 @@ build_src_filter = ${nrf52840_xiao.build_src_filter}
+<../variants/ikoka_stick_nrf>
[ikoka_stick_nrf_e22_33dbm]
extends = ikoka_stick_nrf_baseboard
extends = ikoka_stick_nrf
; limit txpower to 9dBm on E22-900M33S to avoid hardware damage
; to the rf amplifier frontend. 9dBm in -> 33dBm out
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
${ikoka_stick_nrf.build_flags}
-D MANUFACTURER_STRING='"Ikoka Stick-E22-33dBm (Xiao_nrf52)"'
-D LORA_TX_POWER=9
build_src_filter = ${nrf52840_xiao.build_src_filter}
build_src_filter = ${ikoka_stick_nrf.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors>
+<helpers/ui/MomentaryButton.cpp>
@ -103,50 +78,52 @@ build_src_filter = ${nrf52840_xiao.build_src_filter}
;;; abstracted firmware roles
[ikoka_stick_nrf_companion_radio_ble]
extends = ikoka_stick_nrf_baseboard
extends = ikoka_stick_nrf
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
board_upload.maximum_size = 708608
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
${ikoka_stick_nrf.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
-I examples/companion_radio/ui-new
-D QSPIFLASH=1
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
build_src_filter = ${ikoka_stick_nrf.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${ikoka_stick_nrf_baseboard.lib_deps}
${ikoka_stick_nrf.lib_deps}
densaugeo/base64 @ ~1.4.0
[ikoka_stick_nrf_companion_radio_usb]
extends = ikoka_stick_nrf_baseboard
extends = ikoka_stick_nrf
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
board_upload.maximum_size = 708608
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
${ikoka_stick_nrf.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-I examples/companion_radio/ui-new
-D QSPIFLASH=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
build_src_filter = ${ikoka_stick_nrf.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${ikoka_stick_nrf_baseboard.lib_deps}
${ikoka_stick_nrf.lib_deps}
densaugeo/base64 @ ~1.4.0
[ikoka_stick_nrf_repeater]
extends = ikoka_stick_nrf_baseboard
extends = ikoka_stick_nrf
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
${ikoka_stick_nrf.build_flags}
-D ADVERT_NAME='"Ikoka Stick Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
@ -154,21 +131,21 @@ build_flags =
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
build_src_filter = ${ikoka_stick_nrf.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater/*.cpp>
[ikoka_stick_nrf_room_server]
extends = ikoka_stick_nrf_baseboard
extends = ikoka_stick_nrf
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
${ikoka_stick_nrf.build_flags}
-D ADVERT_NAME='"Ikoka Stick Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
build_src_filter = ${ikoka_stick_nrf.build_src_filter}
+<../examples/simple_room_server/*.cpp>
;;; hardware + firmware variants

View file

@ -138,8 +138,6 @@ build_flags =
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D MESH_PACKET_LOGGING=1
-D MESH_DEBUG=1
build_src_filter = ${LilyGo_T3S3_sx1276.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>

View file

@ -0,0 +1,44 @@
#include "MeshtinyBoard.h"
#include <Arduino.h>
#include <Wire.h>
#include <bluefruit.h>
static BLEDfu bledfu;
static void connect_callback(uint16_t conn_handle) {
(void)conn_handle;
MESH_DEBUG_PRINTLN("BLE client connected");
}
static void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
(void)conn_handle;
(void)reason;
MESH_DEBUG_PRINTLN("BLE client disconnected");
}
void MeshtinyBoard::begin() {
NRF52BoardDCDC::begin();
btn_prev_state = HIGH;
pinMode(PIN_VBAT_READ, INPUT); // VBAT ADC input
// Set all button pins to INPUT_PULLUP
pinMode(PIN_BUTTON1, INPUT_PULLUP);
pinMode(PIN_BUTTON2, INPUT_PULLUP);
pinMode(PIN_BUTTON3, INPUT_PULLUP);
pinMode(PIN_BUTTON4, INPUT_PULLUP);
#if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL)
Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL);
#endif
Wire.begin();
pinMode(SX126X_POWER_EN, OUTPUT);
digitalWrite(SX126X_POWER_EN, HIGH);
delay(10); // give sx1262 some time to power up
}

View file

@ -0,0 +1,66 @@
#pragma once
#include <Arduino.h>
#include <MeshCore.h>
#include <helpers/NRF52Board.h>
class MeshtinyBoard : public NRF52BoardDCDC, public NRF52BoardOTA {
protected:
uint8_t btn_prev_state;
public:
MeshtinyBoard() : NRF52BoardOTA("Meshtiny OTA") {}
void begin();
#if defined(P_LORA_TX_LED)
void onBeforeTransmit() override {
digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on
}
void onAfterTransmit() override {
digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off
}
#endif
uint16_t getBattMilliVolts() override {
int adcvalue = 0;
analogReadResolution(12);
analogReference(AR_INTERNAL_3_0);
delay(10);
adcvalue = analogRead(PIN_VBAT_READ);
return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
}
const char *getManufacturerName() const override { return "Meshtiny"; }
void reboot() override { NVIC_SystemReset(); }
void powerOff() override {
#ifdef PIN_USER_BTN
while (digitalRead(PIN_USER_BTN) == LOW) {
delay(10);
}
#endif
#ifdef PIN_3V3_EN
pinMode(PIN_3V3_EN, OUTPUT);
digitalWrite(PIN_3V3_EN, LOW);
#endif
#ifdef PIN_LED1
digitalWrite(PIN_LED1, LOW);
#endif
#ifdef PIN_LED2
digitalWrite(PIN_LED2, LOW);
#endif
#ifdef PIN_USER_BTN
nrf_gpio_cfg_sense_input(g_ADigitalPinMap[PIN_USER_BTN], NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
#endif
sd_power_system_off();
}
};

View file

@ -0,0 +1,68 @@
[Meshtiny]
extends = nrf52_base
board = meshtiny
board_build.ldscript = boards/nrf52840_s140_v6.ld
build_flags = ${nrf52_base.build_flags}
-I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-I variants/meshtiny
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D PIN_3V3_EN=34
-D MESHTINY
-D UI_HAS_JOYSTICK
build_src_filter = ${nrf52_base.build_src_filter}
+<../variants/meshtiny>
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/buzzer.cpp>
+<helpers/sensors>
lib_deps =
${nrf52_base.lib_deps}
adafruit/Adafruit SSD1306 @ ^2.5.13
end2endzone/NonBlockingRTTTL@^1.3.0
[env:Meshtiny_companion_radio_usb]
extends = Meshtiny
build_flags =
${Meshtiny.build_flags}
-I examples/companion_radio/ui-new
-D MESHTINY
-D PIN_BUZZER=30
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Meshtiny.build_src_filter}
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Meshtiny.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Meshtiny_companion_radio_ble]
extends = Meshtiny
build_flags =
${Meshtiny.build_flags}
-I examples/companion_radio/ui-new
-D MESHTINY
-D PIN_BUZZER=30
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
; -D BLE_DEBUG_LOGGING=1
build_src_filter = ${Meshtiny.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Meshtiny.lib_deps}
densaugeo/base64 @ ~1.4.0

View file

@ -0,0 +1,47 @@
#include "target.h"
#include <Arduino.h>
#include <helpers/ArduinoHelpers.h>
MeshtinyBoard board;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
EnvironmentSensorManager sensors = EnvironmentSensorManager();
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(ENCODER_PRESS, 1000, true, true);
MomentaryButton joystick_left(ENCODER_LEFT, 1000, true, true);
MomentaryButton joystick_right(ENCODER_RIGHT, 1000, true, true);
MomentaryButton back_btn(PIN_SIDE_BUTTON, 1000, true, true);
#endif
bool radio_init() {
rtc_clock.begin(Wire);
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(uint8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}

View file

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

View file

@ -0,0 +1,51 @@
/*
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
Copyright (c) 2016 Sandeep Mistry All right reserved.
Copyright (c) 2018, Adafruit Industries (adafruit.com)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "variant.h"
#include "nrf.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
const uint32_t g_ADigitalPinMap[] = {
// P0
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
29, 30, 31,
// P1
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47
};
void initVariant() {
// LED1 & LED2
#ifdef PIN_LED1
pinMode(PIN_LED1, OUTPUT);
ledOff(PIN_LED1);
#endif
#ifdef PIN_LED2
pinMode(PIN_LED2, OUTPUT);
ledOff(PIN_LED2);
#endif
// 3V3 Power Rail - nothing connected on meshtiny
pinMode(PIN_3V3_EN, OUTPUT);
digitalWrite(PIN_3V3_EN, LOW);
}

View file

@ -0,0 +1,98 @@
#ifndef _MESHTINY_H_
#define _MESHTINY_H_
/** Master clock frequency */
#define VARIANT_MCK (64000000ul)
#define USE_LFXO // Board uses 32khz crystal for LF
/*----------------------------------------------------------------------------
* Headers
*----------------------------------------------------------------------------*/
#include "WVariant.h"
#define PINS_COUNT (48)
#define NUM_DIGITAL_PINS (48)
#define NUM_ANALOG_INPUTS (6)
#define NUM_ANALOG_OUTPUTS (0)
// LEDs
#define PIN_LED1 (35) // Green LED
#define PIN_LED2 (36) // Blue LED
#define LED_RED (-1)
#define LED_GREEN PIN_LED1
#define LED_BLUE (-1) // Disable annoying flashing caused by Bluefruit
#define P_LORA_TX_LED PIN_LED2 // Blue LED
// #define PIN_STATUS_LED LED_GREEN // disable status led.
#define LED_BUILTIN LED_GREEN
#define PIN_LED LED_BUILTIN
#define LED_PIN LED_BUILTIN
#define LED_STATE_ON HIGH
// Buttons
#define PIN_BUTTON1 (9) // side button
#define PIN_BUTTON2 (4) // encoder left
#define PIN_BUTTON3 (26) // encoder right
#define PIN_BUTTON4 (28) // encoder press
#define PIN_SIDE_BUTTON PIN_BUTTON1
#define ENCODER_LEFT PIN_BUTTON2
#define ENCODER_RIGHT PIN_BUTTON3
#define ENCODER_PRESS PIN_BUTTON4
#define PIN_USER_BTN PIN_SIDE_BUTTON
// VBAT sensing
#define PIN_VBAT_READ (5)
#define BATTERY_SENSE_RESOLUTION_BITS 12
#define BATTERY_SENSE_RESOLUTION 4096.0
#define AREF_VOLTAGE 3.0
#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
#define ADC_MULTIPLIER 1.73
#define ADC_RESOLUTION 14
// Serial interfaces
#define PIN_SERIAL1_RX (15)
#define PIN_SERIAL1_TX (16)
#define PIN_SERIAL2_RX (8) // Connected to Jlink CDC
#define PIN_SERIAL2_TX (6)
// SPI Interfaces
#define SPI_INTERFACES_COUNT 2
#define PIN_SPI_MISO (45)
#define PIN_SPI_MOSI (44)
#define PIN_SPI_SCK (43)
#define PIN_SPI1_MISO (29)
#define PIN_SPI1_MOSI (30)
#define PIN_SPI1_SCK (3)
// LoRa SX1262 module pins
#define P_LORA_SCLK PIN_SPI_SCK
#define P_LORA_MISO PIN_SPI_MISO
#define P_LORA_MOSI PIN_SPI_MOSI
#define P_LORA_DIO_1 (47)
#define P_LORA_RESET (38)
#define P_LORA_BUSY (46)
#define P_LORA_NSS (42)
#define SX126X_POWER_EN (37)
#define SX126X_RXEN RADIOLIB_NC
#define SX126X_TXEN RADIOLIB_NC
#define SX126X_DIO2_AS_RF_SWITCH true
#define SX126X_DIO3_TCXO_VOLTAGE (1.8f)
// Wire Interfaces
#define WIRE_INTERFACES_COUNT 1
#define PIN_WIRE_SDA (13)
#define PIN_WIRE_SCL (14)
#define PIN_BOARD_SDA (13)
#define PIN_BOARD_SCL (14)
// Power control
#define PIN_3V3_EN (34) // nothing connected on meshtiny board
#endif // _MESHTINY_H_

View file

@ -0,0 +1,96 @@
#pragma once
#include <Arduino.h>
#include <helpers/RefCountedDigitalPin.h>
#include <helpers/ESP32Board.h>
// built-ins
#ifndef PIN_VBAT_READ
#define PIN_VBAT_READ 1
#endif
#ifndef PIN_ADC_CTRL
#define PIN_ADC_CTRL 36
#endif
#define PIN_ADC_CTRL_ACTIVE LOW
#define PIN_ADC_CTRL_INACTIVE HIGH
#define ADC_MULTIPLIER (3 * 1.73 * 1.187 * 1000)
#define BATTERY_SAMPLES 8
#include <driver/rtc_io.h>
class RAK3112Board : public ESP32Board {
private:
bool adc_active_state;
public:
RefCountedDigitalPin periph_power;
RAK3112Board() : periph_power(PIN_VEXT_EN) { }
void begin() {
ESP32Board::begin();
// Auto-detect correct ADC_CTRL pin polarity (different for boards >3.2)
pinMode(PIN_ADC_CTRL, INPUT);
adc_active_state = !digitalRead(PIN_ADC_CTRL);
pinMode(PIN_ADC_CTRL, OUTPUT);
digitalWrite(PIN_ADC_CTRL, !adc_active_state); // Initially inactive
periph_power.begin();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
} else {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
// Finally set ESP32 into sleep
esp_deep_sleep_start(); // CPU halts here and never returns!
}
void powerOff() override {
enterDeepSleep(0);
}
uint16_t getBattMilliVolts() override {
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < BATTERY_SAMPLES; i++) {
raw += analogRead(PIN_VBAT_READ);
}
raw = raw / BATTERY_SAMPLES;
return (ADC_MULTIPLIER * raw) / 4096;
}
const char* getManufacturerName() const override {
return "RAK 3112";
}
};

View file

@ -0,0 +1,223 @@
[rak3112]
extends = esp32_base
board = esp32-s3-devkitc-1
build_flags =
${esp32_base.build_flags}
${sensor_base.build_flags}
-I variants/rak3112
-D RAK_3112=1
-D ESP32_CPU_FREQ=80
-D P_LORA_DIO_1=47
-D P_LORA_NSS=7
-D P_LORA_RESET=8
-D P_LORA_BUSY=48
-D P_LORA_SCLK=5
-D P_LORA_MISO=3
-D P_LORA_MOSI=6
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D P_LORA_TX_LED=46
-D PIN_BOARD_SDA=9
-D PIN_BOARD_SCL=40
-D PIN_USER_BTN=-1
-D PIN_VEXT_EN=14
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D PIN_GPS_RX=43
-D PIN_GPS_TX=44
; -D PIN_GPS_EN=26
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/rak3112>
+<helpers/sensors>
lib_deps =
${esp32_base.lib_deps}
${sensor_base.lib_deps}
[env:RAK3112_repeater]
extends = rak3112
build_flags =
${rak3112.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"RAK3112 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${rak3112.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
lib_deps =
${rak3112.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:RAK3112_repeater_bridge_rs232]
extends = rak3112
build_flags =
${rak3112.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"RS232 Bridge"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_RS232_BRIDGE=Serial2
-D WITH_RS232_BRIDGE_RX=5
-D WITH_RS232_BRIDGE_TX=6
; -D BRIDGE_DEBUG=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${rak3112.build_src_filter}
+<helpers/bridges/RS232Bridge.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
lib_deps =
${rak3112.lib_deps}
${esp32_ota.lib_deps}
[env:RAK3112_repeater_bridge_espnow]
extends = rak3112
build_flags =
${rak3112.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"ESPNow Bridge"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_ESPNOW_BRIDGE=1
; -D BRIDGE_DEBUG=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${rak3112.build_src_filter}
+<helpers/bridges/ESPNowBridge.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
lib_deps =
${rak3112.lib_deps}
${esp32_ota.lib_deps}
[env:RAK3112_room_server]
extends = rak3112
build_flags =
${rak3112.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"RAK3112 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${rak3112.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_room_server>
lib_deps =
${rak3112.lib_deps}
${esp32_ota.lib_deps}
[env:RAK3112_terminal_chat]
extends = rak3112
build_flags =
${rak3112.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${rak3112.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
lib_deps =
${rak3112.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:RAK3112_companion_radio_usb]
extends = rak3112
build_flags =
${rak3112.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=SSD1306Display
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${rak3112.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${rak3112.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:RAK3112_companion_radio_ble]
extends = rak3112
build_flags =
${rak3112.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=SSD1306Display
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${rak3112.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${rak3112.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:RAK3112_companion_radio_wifi]
extends = rak3112
build_flags =
${rak3112.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=SSD1306Display
-D WIFI_DEBUG_LOGGING=1
-D WIFI_SSID='"myssid"'
-D WIFI_PWD='"mypwd"'
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${rak3112.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${rak3112.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:RAK3112_sensor]
extends = rak3112
build_flags =
${rak3112.build_flags}
-D ADVERT_NAME='"RAK3112 v3 Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ENV_PIN_SDA=33
-D ENV_PIN_SCL=34
-D DISPLAY_CLASS=SSD1306Display
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${rak3112.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_sensor>
lib_deps =
${rak3112.lib_deps}
${esp32_ota.lib_deps}

View file

@ -0,0 +1,60 @@
#include <Arduino.h>
#include "target.h"
RAK3112Board board;
#if defined(P_LORA_SCLK)
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
#if ENV_INCLUDE_GPS
#include <helpers/sensors/MicroNMEALocationProvider.h>
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors;
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
#else
return radio.std_init();
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}

30
variants/rak3112/target.h Normal file
View file

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

View file

@ -0,0 +1,32 @@
#include <Arduino.h>
#include <Wire.h>
#include "RAK3401Board.h"
void RAK3401Board::begin() {
NRF52BoardDCDC::begin();
pinMode(PIN_VBAT_READ, INPUT);
#ifdef PIN_USER_BTN
pinMode(PIN_USER_BTN, INPUT_PULLUP);
#endif
#ifdef PIN_USER_BTN_ANA
pinMode(PIN_USER_BTN_ANA, INPUT_PULLUP);
#endif
#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL);
#endif
Wire.begin();
pinMode(PIN_3V3_EN, OUTPUT);
digitalWrite(PIN_3V3_EN, HIGH);
#ifdef P_LORA_PA_EN
// Initialize RAK13302 1W LoRa transceiver module PA control pin
pinMode(P_LORA_PA_EN, OUTPUT);
digitalWrite(P_LORA_PA_EN, LOW); // Start with PA disabled
delay(10); // Allow PA module to initialize
#endif
}

View file

@ -0,0 +1,70 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
#include <helpers/NRF52Board.h>
// LoRa radio module pins for RAK13302
#define P_LORA_SCLK 3
#define P_LORA_MISO 29
#define P_LORA_MOSI 30
#define P_LORA_NSS 26
#define P_LORA_DIO_1 10
#define P_LORA_BUSY 9
#define P_LORA_RESET 4
#ifndef P_LORA_PA_EN
#define P_LORA_PA_EN 31
#endif
//#define PIN_GPS_SDA 13 //GPS SDA pin (output option)
//#define PIN_GPS_SCL 14 //GPS SCL pin (output option)
// #define PIN_GPS_TX 16 //GPS TX pin
// #define PIN_GPS_RX 15 //GPS RX pin
#define PIN_GPS_1PPS 17 //GPS PPS pin
#define GPS_BAUD_RATE 9600
#define GPS_ADDRESS 0x42 //i2c address for GPS
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
// built-ins
#define PIN_VBAT_READ 5
#define ADC_MULTIPLIER (3 * 1.73 * 1.187 * 1000)
#define PIN_3V3_EN (34)
#define WB_IO2 PIN_3V3_EN
class RAK3401Board : public NRF52BoardDCDC, public NRF52BoardOTA {
public:
RAK3401Board() : NRF52BoardOTA("RAK3401_OTA") {}
void begin();
#define BATTERY_SAMPLES 8
uint16_t getBattMilliVolts() override {
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < BATTERY_SAMPLES; i++) {
raw += analogRead(PIN_VBAT_READ);
}
raw = raw / BATTERY_SAMPLES;
return (ADC_MULTIPLIER * raw) / 4096;
}
const char* getManufacturerName() const override {
return "RAK 3401";
}
#ifdef P_LORA_PA_EN
void onBeforeTransmit() override {
digitalWrite(P_LORA_PA_EN, HIGH); // Enable PA before transmission
}
void onAfterTransmit() override {
digitalWrite(P_LORA_PA_EN, LOW); // Disable PA after transmission to save power
}
#endif
};

View file

@ -0,0 +1,127 @@
[rak3401]
extends = nrf52_base
board = rak3401
board_check = true
build_flags = ${nrf52_base.build_flags}
${sensor_base.build_flags}
-I variants/rak3401
-D RAK_3401
-D RAK13302
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
build_src_filter = ${nrf52_base.build_src_filter}
+<../variants/rak3401>
+<helpers/sensors>
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
lib_deps =
${nrf52_base.lib_deps}
${sensor_base.lib_deps}
adafruit/Adafruit SSD1306 @ ^2.5.13
sparkfun/SparkFun u-blox GNSS Arduino Library@^2.2.27
[env:RAK_3401_repeater]
extends = rak3401
build_flags =
${rak3401.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"RAK3401 1W Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
;-D MESH_PACKET_LOGGING=1
;-D MESH_DEBUG=1
build_src_filter = ${rak3401.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
[env:RAK_3401_room_server]
extends = rak3401
build_flags =
${rak3401.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"Test Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
;-D MESH_PACKET_LOGGING=1
;-D MESH_DEBUG=1
build_src_filter = ${rak3401.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_room_server>
[env:RAK_3401_companion_radio_usb]
extends = rak3401
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${rak3401.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${rak3401.build_src_filter}
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${rak3401.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:RAK_3401_companion_radio_ble]
extends = rak3401
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${rak3401.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
;-D MESH_PACKET_LOGGING=1
;-D MESH_DEBUG=1
build_src_filter = ${rak3401.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${rak3401.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:RAK_3401_terminal_chat]
extends = rak3401
build_flags =
${rak3401.build_flags}
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=1
;-D MESH_PACKET_LOGGING=1
;-D MESH_DEBUG=1
build_src_filter = ${rak3401.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
lib_deps =
${rak3401.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:RAK_3401_sensor]
extends = rak3401
build_flags =
${rak3401.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"RAK3401 Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
;-D MESH_PACKET_LOGGING=1
;-D MESH_DEBUG=1
build_src_filter = ${rak3401.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_sensor>

View file

@ -0,0 +1,58 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
RAK3401Board board;
#ifndef PIN_USER_BTN
#define PIN_USER_BTN (-1)
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true);
#if defined(PIN_USER_BTN_ANA)
MomentaryButton analog_btn(PIN_USER_BTN_ANA, 1000, 20);
#endif
#endif
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
#if ENV_INCLUDE_GPS
#include <helpers/sensors/MicroNMEALocationProvider.h>
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors;
#endif
bool radio_init() {
rtc_clock.begin(Wire);
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(uint8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}

30
variants/rak3401/target.h Normal file
View file

@ -0,0 +1,30 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <RAK3401Board.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
extern DISPLAY_CLASS display;
#include <helpers/ui/MomentaryButton.h>
extern MomentaryButton user_btn;
#if defined(PIN_USER_BTN_ANA)
extern MomentaryButton analog_btn;
#endif
#endif
extern RAK3401Board board;
extern WRAPPER_CLASS radio_driver;
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(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();

View file

@ -0,0 +1,52 @@
/*
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
Copyright (c) 2016 Sandeep Mistry All right reserved.
Copyright (c) 2018, Adafruit Industries (adafruit.com)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "variant.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
#include "nrf.h"
const uint32_t g_ADigitalPinMap[] =
{
// P0
0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
8 , 9 , 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31,
// P1
32, 33, 34, 35, 36, 37, 38, 39,
40, 41, 42, 43, 44, 45, 46, 47
};
void initVariant()
{
// LED1 & LED2
pinMode(PIN_LED1, OUTPUT);
ledOff(PIN_LED1);
pinMode(PIN_LED2, OUTPUT);
ledOff(PIN_LED2);
// 3V3 Power Rail
pinMode(PIN_3V3_EN, OUTPUT);
digitalWrite(PIN_3V3_EN, HIGH);
}

199
variants/rak3401/variant.h Normal file
View file

@ -0,0 +1,199 @@
/*
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
Copyright (c) 2016 Sandeep Mistry All right reserved.
Copyright (c) 2018, Adafruit Industries (adafruit.com)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _VARIANT_RAK3401_
#define _VARIANT_RAK3401_
#define RAK4630
/** Master clock frequency */
#define VARIANT_MCK (64000000ul)
#define USE_LFXO // Board uses 32khz crystal for LF
// define USE_LFRC // Board uses RC for LF
/*----------------------------------------------------------------------------
* Headers
*----------------------------------------------------------------------------*/
#include "WVariant.h"
#ifdef __cplusplus
extern "C"
{
#endif // __cplusplus
// Number of pins defined in PinDescription array
#define PINS_COUNT (48)
#define NUM_DIGITAL_PINS (48)
#define NUM_ANALOG_INPUTS (6)
#define NUM_ANALOG_OUTPUTS (0)
// LEDs
#define PIN_LED1 (35)
#define PIN_LED2 (36)
#define LED_BUILTIN PIN_LED1
#define LED_CONN PIN_LED2
#define LED_GREEN PIN_LED1
#define LED_BLUE PIN_LED2
#define LED_STATE_ON 1 // State when LED is litted
/*
* Analog pins
*/
#define PIN_A0 (5) //(3)
#define PIN_A1 (31) //(4)
#define PIN_A2 (28)
#define PIN_A3 (29)
#define PIN_A4 (30)
#define PIN_A5 (31)
#define PIN_A6 (0xff)
#define PIN_A7 (0xff)
static const uint8_t A0 = PIN_A0;
static const uint8_t A1 = PIN_A1;
static const uint8_t A2 = PIN_A2;
static const uint8_t A3 = PIN_A3;
static const uint8_t A4 = PIN_A4;
static const uint8_t A5 = PIN_A5;
static const uint8_t A6 = PIN_A6;
static const uint8_t A7 = PIN_A7;
#define ADC_RESOLUTION 14
// Other pins
#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT
#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT
#define PIN_AREF (2)
#define PIN_NFC1 (9)
#define WB_IO5 PIN_NFC1
#define WB_IO4 (4)
#define PIN_NFC2 (10)
static const uint8_t AREF = PIN_AREF;
/*
* Serial interfaces
*/
// TXD1 RXD1 on Base Board
#define PIN_SERIAL1_RX (15)
#define PIN_SERIAL1_TX (16)
// Connected to Jlink CDC
#define PIN_SERIAL2_RX (8)
#define PIN_SERIAL2_TX (6)
/*
* SPI Interfaces
*/
#define SPI_INTERFACES_COUNT 2
#define PIN_SPI_MISO (45)
#define PIN_SPI_MOSI (44)
#define PIN_SPI_SCK (43)
#define PIN_SPI1_MISO (29)
#define PIN_SPI1_MOSI (30)
#define PIN_SPI1_SCK (3)
static const uint8_t SS = 42;
static const uint8_t MOSI = PIN_SPI_MOSI;
static const uint8_t MISO = PIN_SPI_MISO;
static const uint8_t SCK = PIN_SPI_SCK;
/*
* Wire Interfaces
*/
#define WIRE_INTERFACES_COUNT 1
#define PIN_WIRE_SDA (WB_I2C1_SDA)
#define PIN_WIRE_SCL (WB_I2C1_SCL)
// QSPI Pins
// QSPI occupied by GPIO's
#define PIN_QSPI_SCK 3
#define PIN_QSPI_CS 26
#define PIN_QSPI_IO0 30
#define PIN_QSPI_IO1 29
#define PIN_QSPI_IO2 28
#define PIN_QSPI_IO3 2
// On-board QSPI Flash
// No onboard flash
#define EXTERNAL_FLASH_DEVICES IS25LP080D
#define EXTERNAL_FLASH_USE_QSPI
#define P_LORA_SCK PIN_SPI1_SCK
#define P_LORA_MISO PIN_SPI1_MISO
#define P_LORA_MOSI PIN_SPI1_MOSI
#define P_LORA_CS 26
#define USE_SX1262
#define SX126X_CS (26)
#define SX126X_DIO1 (10)
#define SX126X_BUSY (9)
#define SX126X_RESET (4)
#define SX126X_POWER_EN (21)
// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
// enables 3.3V periphery like GPS or IO Module
// Do not toggle this for GPS power savings
#define PIN_3V3_EN (34)
#define WB_IO2 PIN_3V3_EN
// RAK1910 GPS module
// If using the wisblock GPS module and pluged into Port A on WisBlock base
// IO1 is hooked to PPS (pin 12 on header) = gpio 17
// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on).
// Therefore must be 1 to keep peripherals powered
// Power is on the controllable 3V3_S rail
#define PIN_GPS_PPS (17) // Pulse per second input from the GPS
#define PIN_GPS_RX PIN_SERIAL1_RX
#define PIN_GPS_TX PIN_SERIAL1_TX
// Battery
// The battery sense is hooked to pin A0 (5)
#define BATTERY_PIN PIN_A0
// and has 12 bit resolution
#define BATTERY_SENSE_RESOLUTION_BITS 12
#define BATTERY_SENSE_RESOLUTION 4096.0
#undef AREF_VOLTAGE
#define AREF_VOLTAGE 3.0
#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
#define ADC_MULTIPLIER 1.73
#define HAS_RTC 1
#define RAK_4631 1
#ifdef __cplusplus
}
#endif
/*----------------------------------------------------------------------------
* Arduino objects - C++ only
*----------------------------------------------------------------------------*/
#endif

View file

@ -3,6 +3,28 @@
#include "RAK4631Board.h"
#ifdef NRF52_POWER_MANAGEMENT
// Static configuration for power management
// Values set in variant.h defines
const PowerMgtConfig power_config = {
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
};
void RAK4631Board::initiateShutdown(uint8_t reason) {
// Disable LoRa module power before shutdown
digitalWrite(SX126X_POWER_EN, LOW);
if (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
reason == SHUTDOWN_REASON_BOOT_PROTECT) {
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
}
enterSystemOff(reason);
}
#endif // NRF52_POWER_MANAGEMENT
void RAK4631Board::begin() {
NRF52BoardDCDC::begin();
pinMode(PIN_VBAT_READ, INPUT);
@ -21,6 +43,11 @@ void RAK4631Board::begin() {
Wire.begin();
pinMode(SX126X_POWER_EN, OUTPUT);
#ifdef NRF52_POWER_MANAGEMENT
// Boot voltage protection check (may not return if voltage too low)
// We need to call this after we configure SX126X_POWER_EN as output but before we pull high
checkBootVoltage(&power_config);
#endif
digitalWrite(SX126X_POWER_EN, HIGH);
delay(10); // give sx1262 some time to power up
}

View file

@ -30,6 +30,11 @@
#define ADC_MULTIPLIER (3 * 1.73 * 1.187 * 1000)
class RAK4631Board : public NRF52BoardDCDC {
protected:
#ifdef NRF52_POWER_MANAGEMENT
void initiateShutdown(uint8_t reason) override;
#endif
public:
RAK4631Board() : NRF52Board("RAK4631_OTA") {}
void begin();

View file

@ -7,6 +7,7 @@ build_flags = ${nrf52_base.build_flags}
-I variants/rak4631
-D RAK_4631
-D RAK_BOARD
-D NRF52_POWER_MANAGEMENT
-D PIN_BOARD_SCL=14
-D PIN_BOARD_SDA=13
-D PIN_GPS_TX=PIN_SERIAL1_RX

View file

@ -104,6 +104,14 @@ extern "C"
static const uint8_t A7 = PIN_A7;
#define ADC_RESOLUTION 14
// Power management boot protection threshold (millivolts)
// Set to 0 to disable boot protection
#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV)
// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
// AIN3 = P0.05 = PIN_A0 / PIN_VBAT_READ
#define PWRMGT_LPCOMP_AIN 3
#define PWRMGT_LPCOMP_REFSEL 4 // 5/8 VDD (~3.13-3.44V)
// Other pins
#define PIN_AREF (2)
#define PIN_NFC1 (9)

View file

@ -16,7 +16,8 @@ build_flags =
-D P_LORA_SCLK=12
-D P_LORA_MISO=14
-D P_LORA_MOSI=13
-D LORA_TX_POWER=19
-D LORA_TX_POWER=7 ; configured as 7dbm, because the final output will be ~27dbm (~0.5w) if the PA is enabled.
-D MAX_LORA_TX_POWER=19 ; max output without burning out the PA
; -D P_LORA_TX_LED=35
-D PIN_BOARD_SDA=5
-D PIN_BOARD_SCL=6

View file

@ -107,6 +107,7 @@ build_flags = ${t1000-e.build_flags}
-D DISPLAY_CLASS=NullDisplayDriver
-D PIN_BUZZER=25
-D PIN_BUZZER_EN=37 ; P1/5 - required for T1000-E
-D ADVERT_NAME='"@@MAC"'
build_src_filter = ${t1000-e.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/ui/buzzer.cpp>

View file

@ -83,6 +83,7 @@ build_flags =
-D PIN_BUZZER=6
-D AUTO_SHUTDOWN_MILLIVOLTS=3300
-D QSPIFLASH=1
-D ENV_INCLUDE_GPS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ThinkNode_M1.build_src_filter}

View file

@ -11,7 +11,7 @@ WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
ThinkNodeM1SensorManager sensors = ThinkNodeM1SensorManager(nmea);
#ifdef DISPLAY_CLASS

View file

@ -22,6 +22,7 @@ class ThinkNodeM1SensorManager : public SensorManager {
void stop_gps();
public:
ThinkNodeM1SensorManager(LocationProvider &location): _location(&location) { }
LocationProvider* getLocationProvider() override { return _location; }
bool begin() override;
bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
void loop() override;

View file

@ -95,6 +95,7 @@ build_flags = ${WioTrackerL1.build_flags}
-D UI_HAS_JOYSTICK=1
-D PIN_BUZZER=12
-D QSPIFLASH=1
-D ADVERT_NAME='"@@MAC"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${WioTrackerL1.build_src_filter}

View file

@ -5,12 +5,40 @@
#include "XiaoNrf52Board.h"
#ifdef NRF52_POWER_MANAGEMENT
// Static configuration for power management
// Values set in variant.h defines
const PowerMgtConfig power_config = {
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
};
void XiaoNrf52Board::initiateShutdown(uint8_t reason) {
bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
reason == SHUTDOWN_REASON_BOOT_PROTECT);
pinMode(VBAT_ENABLE, OUTPUT);
digitalWrite(VBAT_ENABLE, enable_lpcomp ? LOW : HIGH);
if (enable_lpcomp) {
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
}
enterSystemOff(reason);
}
#endif // NRF52_POWER_MANAGEMENT
void XiaoNrf52Board::begin() {
NRF52BoardDCDC::begin();
// Configure battery voltage ADC
pinMode(PIN_VBAT, INPUT);
pinMode(VBAT_ENABLE, OUTPUT);
digitalWrite(VBAT_ENABLE, HIGH);
digitalWrite(VBAT_ENABLE, LOW); // Enable VBAT divider for reading
analogReadResolution(12);
analogReference(AR_INTERNAL_3_0);
delay(50); // Allow ADC to settle
#ifdef PIN_USER_BTN
pinMode(PIN_USER_BTN, INPUT_PULLUP);
@ -27,9 +55,20 @@ void XiaoNrf52Board::begin() {
digitalWrite(P_LORA_TX_LED, HIGH);
#endif
// pinMode(SX126X_POWER_EN, OUTPUT);
// digitalWrite(SX126X_POWER_EN, HIGH);
delay(10); // give sx1262 some time to power up
#ifdef NRF52_POWER_MANAGEMENT
// Boot voltage protection check (may not return if voltage too low)
checkBootVoltage(&power_config);
#endif
delay(10); // Give sx1262 some time to power up
}
uint16_t XiaoNrf52Board::getBattMilliVolts() {
// https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
// VBAT_ENABLE must be LOW to read battery voltage
digitalWrite(VBAT_ENABLE, LOW);
int adcvalue = analogRead(PIN_VBAT);
return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
}
#endif

View file

@ -7,6 +7,11 @@
#ifdef XIAO_NRF52
class XiaoNrf52Board : public NRF52BoardDCDC {
protected:
#if NRF52_POWER_MANAGEMENT
void initiateShutdown(uint8_t reason) override;
#endif
public:
XiaoNrf52Board() : NRF52Board("XIAO_NRF52_OTA") {}
void begin();
@ -20,21 +25,7 @@ public:
}
#endif
uint16_t getBattMilliVolts() override {
// Please read befor going further ;)
// https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
// We can't drive VBAT_ENABLE to HIGH as long
// as we don't know wether we are charging or not ...
// this is a 3mA loss (4/1500)
digitalWrite(VBAT_ENABLE, LOW);
int adcvalue = 0;
analogReadResolution(12);
analogReference(AR_INTERNAL_3_0);
delay(10);
adcvalue = analogRead(PIN_VBAT);
return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
}
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override {
return "Seeed Xiao-nrf52";

View file

@ -9,6 +9,7 @@ build_flags = ${nrf52_base.build_flags}
-I variants/xiao_nrf52
-UENV_INCLUDE_GPS
-D NRF52_PLATFORM
-D NRF52_POWER_MANAGEMENT
-D XIAO_NRF52
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper

View file

@ -75,6 +75,21 @@ static const uint8_t D10 = 10;
#define AREF_VOLTAGE (3.0)
#define ADC_MULTIPLIER (3.0F) // 1M, 512k divider bridge
// Power management boot protection threshold (millivolts)
// Set to 0 to disable boot protection
#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage
// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
#define PWRMGT_LPCOMP_AIN 7 // AIN7 = P0.31 = PIN_VBAT
// IMPORTANT: The XIAO exposes battery via a resistor divider (ADC_MULTIPLIER = 3.0).
// LPCOMP measures the divided voltage, not the battery voltage directly.
// Vpin = VDD * (REFSEL fraction), and VBAT ≈ Vpin * ADC_MULTIPLIER.
//
// Using 3/8 VDD gives a wake threshold above the boot protection point:
// - If VDD ≈ 3.0V: VBAT ≈ (3.0 * 3/8) * 3 ≈ 3375mV
// - If VDD ≈ 3.3V: VBAT ≈ (3.3 * 3/8) * 3 ≈ 3712mV
#define PWRMGT_LPCOMP_REFSEL 2 // 3/8 VDD (~3.38-3.71V)
static const uint8_t A0 = PIN_A0;
static const uint8_t A1 = PIN_A1;
static const uint8_t A2 = PIN_A2;