diff --git a/boards/meshtiny.json b/boards/meshtiny.json new file mode 100644 index 00000000..0418dc3b --- /dev/null +++ b/boards/meshtiny.json @@ -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" +} diff --git a/boards/rak3401.json b/boards/rak3401.json new file mode 100644 index 00000000..a2816a63 --- /dev/null +++ b/boards/rak3401.json @@ -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" +} diff --git a/build.sh b/build.sh index f2127941..b7f95dd7 100755 --- a/build.sh +++ b/build.sh @@ -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 diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md new file mode 100644 index 00000000..ebe9bbbe --- /dev/null +++ b/docs/nrf52_power_management.md @@ -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) diff --git a/docs/payloads.md b/docs/payloads.md index 5a41e69c..4742bfbb 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -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 | diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 4faac975..f61f53ae 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -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(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 7689708c..2dad7866 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -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]); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 1fcc5697..a2b0033f 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -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; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index e9db5444..62cd4164 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -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 }; \ No newline at end of file diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 82c8c21d..7e636ace 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -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); diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 33e32a68..6d957cc0 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -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 - ??"); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 343aa44f..60d22902 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -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; diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 8c745613..d55d6118 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -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() { diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 60dd1840..22a3d208 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -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 diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index e7f1fee8..4f3ed0e4 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -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; diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 1a3b4d6e..825fb007 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -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() { diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index da1bac5b..018ec2a2 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -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() { diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 4995c55f..8e27323e 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -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!"); } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index c320eb44..eb2d90c5 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -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; diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index a5fcc148..330adcc2 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -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() { diff --git a/platformio.ini b/platformio.ini index 75d37e86..743e357a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/src/Identity.cpp b/src/Identity.cpp index 83298928..ea546274 100644 --- a/src/Identity.cpp +++ b/src/Identity.cpp @@ -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); diff --git a/src/Identity.h b/src/Identity.h index 60e8783b..c3ffcd75 100644 --- a/src/Identity.h +++ b/src/Identity.h @@ -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; diff --git a/src/MeshCore.h b/src/MeshCore.h index 718660d3..f194cdeb 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -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"; } }; /** diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 597444fa..aebfc1b6 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -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 } diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 76b0dd1c..fd391b98 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -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); diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 4ea19fd2..55b70ca5 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -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 diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 1b650edd..dfbc3fce 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -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); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 78e1b5e0..42198b49 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -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(); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 642a4cce..8661d1e6 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -3,6 +3,7 @@ #include "Mesh.h" #include #include +#include #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); diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 8f60823c..6915c856 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -2,6 +2,7 @@ #include "NRF52Board.h" #include +#include 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(); diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index f464b79a..1c70d8f0 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -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 }; /* diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 36844615..2cc47e1d 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -2,6 +2,45 @@ #include #include +// 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 = ®ions[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 = ®ions[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 = ®ions[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 +} diff --git a/src/helpers/RegionMap.h b/src/helpers/RegionMap.h index 50513be1..3ebff1ba 100644 --- a/src/helpers/RegionMap.h +++ b/src/helpers/RegionMap.h @@ -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 ®ions[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; + }; diff --git a/src/helpers/StatsFormatHelper.h b/src/helpers/StatsFormatHelper.h index d0107f3b..5aa01da9 100644 --- a/src/helpers/StatsFormatHelper.h +++ b/src/helpers/StatsFormatHelper.h @@ -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() ); } }; diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 7ec93723..eccfeca6 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -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); diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 29ad897a..965e90fd 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -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; diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index eb1e90bb..5648707e 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -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); diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index 25968d78..e2fc6cb9 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -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; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index e3407821..cf3e1266 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -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++; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 3c26d372..9ac1bbae 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -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; diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index b7238def..8471d80d 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -42,7 +42,7 @@ static Adafruit_BME280 BME280; #endif #define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level #include -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 +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 diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index c9da0cf8..4e7fd10a 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -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() { diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index 1a3a9602..d843da85 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -5,6 +5,7 @@ #include #define SSD1306_NO_SPLASH #include +#include #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; } diff --git a/src/helpers/ui/ST7789LCDDisplay.cpp b/src/helpers/ui/ST7789LCDDisplay.cpp index 97d82f42..9fd0b23d 100644 --- a/src/helpers/ui/ST7789LCDDisplay.cpp +++ b/src/helpers/ui/ST7789LCDDisplay.cpp @@ -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 diff --git a/src/helpers/ui/ST7789LCDDisplay.h b/src/helpers/ui/ST7789LCDDisplay.h index a8077148..5b960ca1 100644 --- a/src/helpers/ui/ST7789LCDDisplay.h +++ b/src/helpers/ui/ST7789LCDDisplay.h @@ -8,7 +8,7 @@ #include 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), diff --git a/variants/heltec_t114/T114Board.cpp b/variants/heltec_t114/T114Board.cpp index 4995e7de..2a36bd90 100644 --- a/variants/heltec_t114/T114Board.cpp +++ b/variants/heltec_t114/T114Board.cpp @@ -3,6 +3,35 @@ #include #include +#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 } \ No newline at end of file diff --git a/variants/heltec_t114/T114Board.h b/variants/heltec_t114/T114Board.h index 340cfa97..f27dc291 100644 --- a/variants/heltec_t114/T114Board.h +++ b/variants/heltec_t114/T114Board.h @@ -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(); } }; diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index 7b6f5cee..20f5e8fe 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -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 diff --git a/variants/heltec_t114/variant.h b/variants/heltec_t114/variant.h index b3f760bb..ac9dbbe6 100644 --- a/variants/heltec_t114/variant.h +++ b/variants/heltec_t114/variant.h @@ -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 diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index dcb2873c..6b61eff5 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -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 diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index f143db36..92f93437 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -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 } diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index ecfd7889..ba759009 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -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} + +<../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} + + +<../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} + +<../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} + + +<../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} + + + +<../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} + + + +<../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} + +<../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} + + + +<../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} + + + + + +<../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} + + + +<../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} + + + + + +<../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} + + + + + + + +<../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} + + + + + + + +<../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} + + + +<../examples/simple_sensor> +lib_deps = + ${heltec_v4_tft.lib_deps} ${esp32_ota.lib_deps} diff --git a/variants/heltec_v4/target.cpp b/variants/heltec_v4/target.cpp index 015c3a8e..0d2bd497 100644 --- a/variants/heltec_v4/target.cpp +++ b/variants/heltec_v4/target.cpp @@ -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 diff --git a/variants/heltec_v4/target.h b/variants/heltec_v4/target.h index a153b2af..00d2adab 100644 --- a/variants/heltec_v4/target.h +++ b/variants/heltec_v4/target.h @@ -9,7 +9,11 @@ #include #include #ifdef DISPLAY_CLASS - #include +#ifdef HELTEC_LORA_V4_OLED + #include +#elif defined(HELTEC_LORA_V4_TFT) + #include +#endif #include #endif diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 9cf76153..f0bca860 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -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} \ No newline at end of file + ${esp32_ota.lib_deps} diff --git a/variants/ikoka_handheld_nrf/platformio.ini b/variants/ikoka_handheld_nrf/platformio.ini index e4643c38..d2bbeffe 100644 --- a/variants/ikoka_handheld_nrf/platformio.ini +++ b/variants/ikoka_handheld_nrf/platformio.ini @@ -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> + +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} + +<../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} + [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} + [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> diff --git a/variants/ikoka_nano_nrf/platformio.ini b/variants/ikoka_nano_nrf/platformio.ini index abfbcf67..08b1101b 100644 --- a/variants/ikoka_nano_nrf/platformio.ini +++ b/variants/ikoka_nano_nrf/platformio.ini @@ -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} + + + +<../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} + + + +<../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} + + + +<../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} + +<../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} + +<../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 diff --git a/variants/ikoka_stick_nrf/platformio.ini b/variants/ikoka_stick_nrf/platformio.ini index 9ced2bbb..2e43b700 100644 --- a/variants/ikoka_stick_nrf/platformio.ini +++ b/variants/ikoka_stick_nrf/platformio.ini @@ -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} + + + @@ -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} + + + @@ -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} + + + @@ -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} + +<../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} + +<../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} + +<../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 diff --git a/variants/lilygo_t3s3_sx1276/platformio.ini b/variants/lilygo_t3s3_sx1276/platformio.ini index f544be11..5a7ece2c 100644 --- a/variants/lilygo_t3s3_sx1276/platformio.ini +++ b/variants/lilygo_t3s3_sx1276/platformio.ini @@ -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} + + diff --git a/variants/meshtiny/MeshtinyBoard.cpp b/variants/meshtiny/MeshtinyBoard.cpp new file mode 100644 index 00000000..a2cdcb08 --- /dev/null +++ b/variants/meshtiny/MeshtinyBoard.cpp @@ -0,0 +1,44 @@ +#include "MeshtinyBoard.h" + +#include +#include +#include + +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 +} + + diff --git a/variants/meshtiny/MeshtinyBoard.h b/variants/meshtiny/MeshtinyBoard.h new file mode 100644 index 00000000..a73c9ea3 --- /dev/null +++ b/variants/meshtiny/MeshtinyBoard.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include + +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(); + } + +}; diff --git a/variants/meshtiny/platformio.ini b/variants/meshtiny/platformio.ini new file mode 100644 index 00000000..14e5c60d --- /dev/null +++ b/variants/meshtiny/platformio.ini @@ -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> + + + + + + +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} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Meshtiny.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/meshtiny/target.cpp b/variants/meshtiny/target.cpp new file mode 100644 index 00000000..5fc60eae --- /dev/null +++ b/variants/meshtiny/target.cpp @@ -0,0 +1,47 @@ +#include "target.h" + +#include +#include + +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 +} diff --git a/variants/meshtiny/target.h b/variants/meshtiny/target.h new file mode 100644 index 00000000..8ee3ee86 --- /dev/null +++ b/variants/meshtiny/target.h @@ -0,0 +1,33 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS +#include +#include +#endif +#include + +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(); diff --git a/variants/meshtiny/variant.cpp b/variants/meshtiny/variant.cpp new file mode 100644 index 00000000..7cec7dec --- /dev/null +++ b/variants/meshtiny/variant.cpp @@ -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); +} diff --git a/variants/meshtiny/variant.h b/variants/meshtiny/variant.h new file mode 100644 index 00000000..daa8eff5 --- /dev/null +++ b/variants/meshtiny/variant.h @@ -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_ diff --git a/variants/rak3112/RAK3112Board.h b/variants/rak3112/RAK3112Board.h new file mode 100644 index 00000000..8ba3197c --- /dev/null +++ b/variants/rak3112/RAK3112Board.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include + +// 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 + +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"; + } +}; diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini new file mode 100644 index 00000000..29ebdff2 --- /dev/null +++ b/variants/rak3112/platformio.ini @@ -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> + + +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} + + + +<../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} + + + + + +<../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} + + + + + +<../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} + + + +<../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} + + + + + +<../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} + + + + + + + +<../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} + + + + + + + +<../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} + + + +<../examples/simple_sensor> +lib_deps = + ${rak3112.lib_deps} + ${esp32_ota.lib_deps} diff --git a/variants/rak3112/target.cpp b/variants/rak3112/target.cpp new file mode 100644 index 00000000..634573b8 --- /dev/null +++ b/variants/rak3112/target.cpp @@ -0,0 +1,60 @@ +#include +#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 + 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 +} diff --git a/variants/rak3112/target.h b/variants/rak3112/target.h new file mode 100644 index 00000000..eae90900 --- /dev/null +++ b/variants/rak3112/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include + #include +#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(); diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp new file mode 100644 index 00000000..b9431c92 --- /dev/null +++ b/variants/rak3401/RAK3401Board.cpp @@ -0,0 +1,32 @@ +#include +#include + +#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 +} \ No newline at end of file diff --git a/variants/rak3401/RAK3401Board.h b/variants/rak3401/RAK3401Board.h new file mode 100644 index 00000000..609393c3 --- /dev/null +++ b/variants/rak3401/RAK3401Board.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include + +// 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 +}; diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini new file mode 100644 index 00000000..30d35d0b --- /dev/null +++ b/variants/rak3401/platformio.ini @@ -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> + + + + + + +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} + + + +<../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} + + + +<../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} + + + +<../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} + + + +<../examples/simple_sensor> \ No newline at end of file diff --git a/variants/rak3401/target.cpp b/variants/rak3401/target.cpp new file mode 100644 index 00000000..52f3a3d5 --- /dev/null +++ b/variants/rak3401/target.cpp @@ -0,0 +1,58 @@ +#include +#include "target.h" +#include + +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 + 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 +} diff --git a/variants/rak3401/target.h b/variants/rak3401/target.h new file mode 100644 index 00000000..32f17cd1 --- /dev/null +++ b/variants/rak3401/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include + +#ifdef DISPLAY_CLASS + #include + extern DISPLAY_CLASS display; + #include + 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(); diff --git a/variants/rak3401/variant.cpp b/variants/rak3401/variant.cpp new file mode 100644 index 00000000..d562189f --- /dev/null +++ b/variants/rak3401/variant.cpp @@ -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); +} diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h new file mode 100644 index 00000000..9c182247 --- /dev/null +++ b/variants/rak3401/variant.h @@ -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 diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 65c54711..9fb47b43 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -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 } \ No newline at end of file diff --git a/variants/rak4631/RAK4631Board.h b/variants/rak4631/RAK4631Board.h index a253e142..ff4a5b7d 100644 --- a/variants/rak4631/RAK4631Board.h +++ b/variants/rak4631/RAK4631Board.h @@ -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(); diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 7293b4d4..9a9ab2dd 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -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 diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index e83d1339..b18335f8 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -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) diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 1428221d..91ef5f7a 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -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 diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index 555b182f..ac929308 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -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} + + diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index ade487e9..397bf8e3 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -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} diff --git a/variants/thinknode_m1/target.cpp b/variants/thinknode_m1/target.cpp index 2b04d7c6..c3b1abc2 100644 --- a/variants/thinknode_m1/target.cpp +++ b/variants/thinknode_m1/target.cpp @@ -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 diff --git a/variants/thinknode_m1/target.h b/variants/thinknode_m1/target.h index 1e4e1381..8425369d 100644 --- a/variants/thinknode_m1/target.h +++ b/variants/thinknode_m1/target.h @@ -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; diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index 31c6bcb0..75651d69 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -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} diff --git a/variants/xiao_nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp index b7b60dc6..42ee6a87 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.cpp +++ b/variants/xiao_nrf52/XiaoNrf52Board.cpp @@ -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 \ No newline at end of file diff --git a/variants/xiao_nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h index bf3115d3..bd0fd9b1 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -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"; diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index edbf6275..6e96018b 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -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 diff --git a/variants/xiao_nrf52/variant.h b/variants/xiao_nrf52/variant.h index 3f4d7afe..25619b9e 100644 --- a/variants/xiao_nrf52/variant.h +++ b/variants/xiao_nrf52/variant.h @@ -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;