From 8c80c10d2a0bc1dff1853252610154eae0192635 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 30 Jun 2025 18:57:24 +1000 Subject: [PATCH 01/60] * CustomLR1110::getTimeOnAir(), copied from sx1262 --- src/helpers/CustomLR1110.h | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/helpers/CustomLR1110.h b/src/helpers/CustomLR1110.h index 3451aac1..d431dac1 100644 --- a/src/helpers/CustomLR1110.h +++ b/src/helpers/CustomLR1110.h @@ -9,6 +9,35 @@ class CustomLR1110 : public LR1110 { public: CustomLR1110(Module *mod) : LR1110(mod) { } + RadioLibTime_t getTimeOnAir(size_t len) override { + uint32_t symbolLength_us = ((uint32_t)(1000 * 10) << this->spreadingFactor) / (this->bandwidthKhz * 10) ; + uint8_t sfCoeff1_x4 = 17; // (4.25 * 4) + uint8_t sfCoeff2 = 8; + if(this->spreadingFactor == 5 || this->spreadingFactor == 6) { + sfCoeff1_x4 = 25; // 6.25 * 4 + sfCoeff2 = 0; + } + uint8_t sfDivisor = 4*this->spreadingFactor; + if(symbolLength_us >= 16000) { + sfDivisor = 4*(this->spreadingFactor - 2); + } + const int8_t bitsPerCrc = 16; + const int8_t N_symbol_header = this->headerType == RADIOLIB_SX126X_LORA_HEADER_EXPLICIT ? 20 : 0; + + // numerator of equation in section 6.1.4 of SX1268 datasheet v1.1 (might not actually be bitcount, but it has len * 8) + int16_t bitCount = (int16_t) 8 * len + this->crcTypeLoRa * bitsPerCrc - 4 * this->spreadingFactor + sfCoeff2 + N_symbol_header; + if(bitCount < 0) { + bitCount = 0; + } + // add (sfDivisor) - 1 to the numerator to give integer CEIL(...) + uint16_t nPreCodedSymbols = (bitCount + (sfDivisor - 1)) / (sfDivisor); + + // preamble can be 65k, therefore nSymbol_x4 needs to be 32 bit + uint32_t nSymbol_x4 = (this->preambleLengthLoRa + 8) * 4 + sfCoeff1_x4 + nPreCodedSymbols * (this->codingRate + 4) * 4; + + return((symbolLength_us * nSymbol_x4) / 4); + } + bool isReceiving() { uint16_t irq = getIrqStatus(); bool detected = ((irq & LR1110_IRQ_HEADER_VALID) || (irq & LR1110_IRQ_HAS_PREAMBLE)); From 6f94c8148a54c6f31d85303e973e64d8686e7285 Mon Sep 17 00:00:00 2001 From: Normunds Gavars Date: Tue, 1 Jul 2025 01:56:34 +0300 Subject: [PATCH 02/60] Add Minewsemi ME25LS01 variant --- boards/minewsemi_me25ls01.json | 59 +++++ src/helpers/nrf52/MinewsemiME25LS01Board.cpp | 92 ++++++++ src/helpers/nrf52/MinewsemiME25LS01Board.h | 112 ++++++++++ .../minewsemi_me25ls01/NullDisplayDriver.h | 24 ++ variants/minewsemi_me25ls01/platformio.ini | 184 +++++++++++++++ variants/minewsemi_me25ls01/target.cpp | 209 ++++++++++++++++++ variants/minewsemi_me25ls01/target.h | 46 ++++ variants/minewsemi_me25ls01/variant.cpp | 98 ++++++++ variants/minewsemi_me25ls01/variant.h | 147 ++++++++++++ 9 files changed, 971 insertions(+) create mode 100644 boards/minewsemi_me25ls01.json create mode 100644 src/helpers/nrf52/MinewsemiME25LS01Board.cpp create mode 100644 src/helpers/nrf52/MinewsemiME25LS01Board.h create mode 100644 variants/minewsemi_me25ls01/NullDisplayDriver.h create mode 100644 variants/minewsemi_me25ls01/platformio.ini create mode 100644 variants/minewsemi_me25ls01/target.cpp create mode 100644 variants/minewsemi_me25ls01/target.h create mode 100644 variants/minewsemi_me25ls01/variant.cpp create mode 100644 variants/minewsemi_me25ls01/variant.h diff --git a/boards/minewsemi_me25ls01.json b/boards/minewsemi_me25ls01.json new file mode 100644 index 00000000..4c943158 --- /dev/null +++ b/boards/minewsemi_me25ls01.json @@ -0,0 +1,59 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "me25ls01-BOOT", + "mcu": "nrf52840", + "variant": "minewsemi_me25ls01", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": ["arduino"], + "name": "Minewsemi ME25LS01", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://en.minewsemi.com/lora-module/lr1110-nrf52840-me25LS01", + "vendor": "MINEWSEMI" +} diff --git a/src/helpers/nrf52/MinewsemiME25LS01Board.cpp b/src/helpers/nrf52/MinewsemiME25LS01Board.cpp new file mode 100644 index 00000000..f402a0fd --- /dev/null +++ b/src/helpers/nrf52/MinewsemiME25LS01Board.cpp @@ -0,0 +1,92 @@ +#include +#include "MinewsemiME25LS01Board.h" +#include + +#include + +void MinewsemiME25LS01Board::begin() { + // for future use, sub-classes SHOULD call this from their begin() + startup_reason = BD_STARTUP_NORMAL; + btn_prev_state = HIGH; + + sd_power_mode_set(NRF_POWER_MODE_LOWPWR); + +#ifdef BUTTON_PIN + // pinMode(BATTERY_PIN, INPUT); + pinMode(BUTTON_PIN, INPUT); + pinMode(LED_PIN, OUTPUT); +#endif + +#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); +#endif + + Wire.begin(); + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); +#endif + + delay(10); // give sx1262 some time to power up +} + +#if 0 +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"); +} + + +bool TrackerT1000eBoard::startOTAUpdate(const char* id, char reply[]) { + // Config the peripheral connection with maximum bandwidth + // more SRAM required by SoftDevice + // Note: All config***() function must be called before begin() + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); + + Bluefruit.begin(1, 0); + // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 + Bluefruit.setTxPower(4); + // Set the BLE device name + Bluefruit.setName("T1000E_OTA"); + + Bluefruit.Periph.setConnectCallback(connect_callback); + Bluefruit.Periph.setDisconnectCallback(disconnect_callback); + + // To be consistent OTA DFU should be added first if it exists + bledfu.begin(); + + // Set up and start advertising + // Advertising packet + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + Bluefruit.Advertising.addName(); + + /* Start Advertising + - Enable auto advertising if disconnected + - Interval: fast mode = 20 ms, slow mode = 152.5 ms + - Timeout for fast mode is 30 seconds + - Start(timeout) with timeout = 0 will advertise forever (until connected) + + For recommended advertising interval + https://developer.apple.com/library/content/qa/qa1931/_index.html + */ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + + strcpy(reply, "OK - started"); + return true; +} +#endif \ No newline at end of file diff --git a/src/helpers/nrf52/MinewsemiME25LS01Board.h b/src/helpers/nrf52/MinewsemiME25LS01Board.h new file mode 100644 index 00000000..4b8c8293 --- /dev/null +++ b/src/helpers/nrf52/MinewsemiME25LS01Board.h @@ -0,0 +1,112 @@ +#pragma once + +#include +#include + +// LoRa and SPI pins + +#define P_LORA_DIO_1 (32 + 12) // P1.12 +#define P_LORA_NSS (32 + 13) // P1.13 +#define P_LORA_RESET (32 + 11) // P1.11 +#define P_LORA_BUSY (32 + 10) // P1.10 +#define P_LORA_SCLK (32 + 15) // P1.15 +#define P_LORA_MISO (0 + 29) // P0.29 +#define P_LORA_MOSI (0 + 2) // P0.2 + +#define LR11X0_DIO_AS_RF_SWITCH true +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 + +// built-ins +//#define PIN_VBAT_READ 5 +//#define ADC_MULTIPLIER (3 * 1.73 * 1000) + +class MinewsemiME25LS01Board : public mesh::MainBoard { +protected: + uint8_t startup_reason; + uint8_t btn_prev_state; + +public: + void begin(); + + uint16_t getBattMilliVolts() override { + #ifdef BATTERY_PIN + #ifdef PIN_3V3_EN + digitalWrite(PIN_3V3_EN, HIGH); + #endif + analogReference(AR_INTERNAL_3_0); + analogReadResolution(12); + delay(10); + float volts = (analogRead(BATTERY_PIN) * ADC_MULTIPLIER * AREF_VOLTAGE) / 4096; + #ifdef PIN_3V3_EN + digitalWrite(PIN_3V3_EN, LOW); + #endif + + analogReference(AR_DEFAULT); // put back to default + analogReadResolution(10); + + return volts * 1000; + #else + return 0; + #endif + } + + uint8_t getStartupReason() const override { return startup_reason; } + + const char* getManufacturerName() const override { + return "m25ls01"; + } + + int buttonStateChanged() { + #ifdef BUTTON_PIN + uint8_t v = digitalRead(BUTTON_PIN); + if (v != btn_prev_state) { + btn_prev_state = v; + return (v == LOW) ? 1 : -1; + } + #endif + return 0; + } + + void powerOff() override { + #ifdef HAS_GPS + digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(GPS_RESET, LOW); + digitalWrite(GPS_SLEEP_INT, LOW); + digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RESETB, OUTPUT); + digitalWrite(GPS_RESETB, LOW); + #endif + + #ifdef BUZZER_EN + digitalWrite(BUZZER_EN, LOW); + #endif + + #ifdef PIN_3V3_EN + digitalWrite(PIN_3V3_EN, LOW); + #endif + + #ifdef LED_PIN + digitalWrite(LED_PIN, LOW); + #endif + #ifdef BUTTON_PIN + nrf_gpio_cfg_sense_input(digitalPinToInterrupt(BUTTON_PIN), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_HIGH); + #endif + sd_power_system_off(); + } + + #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 + + + void reboot() override { + NVIC_SystemReset(); + } + +// bool startOTAUpdate(const char* id, char reply[]) override; +}; \ No newline at end of file diff --git a/variants/minewsemi_me25ls01/NullDisplayDriver.h b/variants/minewsemi_me25ls01/NullDisplayDriver.h new file mode 100644 index 00000000..38bf93f1 --- /dev/null +++ b/variants/minewsemi_me25ls01/NullDisplayDriver.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +class NullDisplayDriver : public DisplayDriver { +public: + NullDisplayDriver() : DisplayDriver(128, 64) { } + bool begin() { return false; } // not present + + bool isOn() override { return false; } + void turnOn() override { } + void turnOff() override { } + void clear() override { } + void startFrame(Color bkg = DARK) override { } + void setTextSize(int sz) override { } + void setColor(Color c) override { } + void setCursor(int x, int y) override { } + void print(const char* str) override { } + void fillRect(int x, int y, int w, int h) override { } + void drawRect(int x, int y, int w, int h) override { } + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { } + uint16_t getTextWidth(const char* str) override { return 0; } + void endFrame() { } +}; diff --git a/variants/minewsemi_me25ls01/platformio.ini b/variants/minewsemi_me25ls01/platformio.ini new file mode 100644 index 00000000..4775e289 --- /dev/null +++ b/variants/minewsemi_me25ls01/platformio.ini @@ -0,0 +1,184 @@ +; ----------------- NRF52 me25ls01--------------------- +[nrf52840_me25ls01] +extends = nrf52_base +platform_packages = framework-arduinoadafruitnrf52 +build_flags = ${nrf52_base.build_flags} + -I src/helpers/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 + +[me25ls01] +extends = nrf52840_me25ls01 +board = minewsemi_me25ls01 +board_build.ldscript = boards/nrf52840_s140_v7.ld +build_flags = ${nrf52840_me25ls01.build_flags} + -I variants/minewsemi_me25ls01 + -D me25ls01 + -D PIN_USER_BTN=27 + -D USER_BTN_PRESSED=HIGH + -D PIN_STATUS_LED=39 + -D P_LORA_TX_LED=22 + -D RADIO_CLASS=CustomLR1110 + -D WRAPPER_CLASS=CustomLR1110Wrapper + -D LORA_TX_POWER=22 + -D ENV_INCLUDE_GPS=0 + -D ENV_INCLUDE_AHTX0=1 + -D ENV_INCLUDE_BME280=1 + -D ENV_INCLUDE_INA3221=1 + -D ENV_INCLUDE_INA219=1 +build_src_filter = ${nrf52840_me25ls01.build_src_filter} + + + + + +<../variants/minewsemi_me25ls01> + + +debug_tool = jlink +upload_protocol = nrfutil +lib_deps = ${nrf52840_me25ls01.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 + 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 + +[env:Minewsemi_me25ls01_companion_radio_ble] +extends = me25ls01 +build_flags = ${me25ls01.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 + -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D RX_BOOSTED_GAIN=true + -D RF_SWITCH_TABLE + -D DISPLAY_CLASS=NullDisplayDriver + ;-D PIN_BUZZER=25 + ;-D PIN_BUZZER_EN=37 +build_src_filter = ${me25ls01.build_src_filter} + + + ;+ + +<../examples/companion_radio/*.cpp> +lib_deps = ${me25ls01.lib_deps} + adafruit/RTClib @ ^2.1.3 + + +[env:Minewsemi_me25ls01_repeater] +extends = me25ls01 +build_flags = ${me25ls01.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 + -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D RX_BOOSTED_GAIN=true + -D RF_SWITCH_TABLE + -D ADVERT_NAME='"ME25LS01 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${me25ls01.build_src_filter} + ;+ + ;+ + ;+<../examples/companion_radio/*.cpp> + +<../examples/simple_repeater> +lib_deps = ${me25ls01.lib_deps} + adafruit/RTClib @ ^2.1.3 + + + +[env:Minewsemi_me25ls01_room_server] +extends = me25ls01 +build_flags = ${me25ls01.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 +; -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D RX_BOOSTED_GAIN=true + -D RF_SWITCH_TABLE + -D ADVERT_NAME='"ME25LS01 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=8 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${me25ls01.build_src_filter} + ;+ + ;+ + ;+<../examples/companion_radio/*.cpp> + ;+<../examples/simple_repeater> + +<../examples/simple_room_server> +lib_deps = ${me25ls01.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:Minewsemi_me25ls01_terminal_chat] +extends = me25ls01 +build_flags = ${me25ls01.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 + -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D RX_BOOSTED_GAIN=true + -D RF_SWITCH_TABLE + -D ADVERT_NAME='"ME25LS01 Chat"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=8 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${me25ls01.build_src_filter} + ;+ + ;+ + ;+<../examples/companion_radio/*.cpp> + ;+<../examples/simple_repeater> + ;+<../examples/simple_room_server> + +<../examples/simple_secure_chat/main.cpp> +lib_deps = ${me25ls01.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:Minewsemi_me25ls01_companion_radio_usb] +extends = me25ls01 +build_flags = ${me25ls01.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + ;-D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 + -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D RX_BOOSTED_GAIN=true + -D RF_SWITCH_TABLE + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${me25ls01.build_src_filter} + + + ;+ + ;+ + ;+<../examples/companion_radio/*.cpp> + ;+<../examples/simple_repeater> + ;+<../examples/simple_room_server> + +<../examples/companion_radio> +lib_deps = ${me25ls01.lib_deps} + adafruit/RTClib @ ^2.1.3 + diff --git a/variants/minewsemi_me25ls01/target.cpp b/variants/minewsemi_me25ls01/target.cpp new file mode 100644 index 00000000..b6b1b585 --- /dev/null +++ b/variants/minewsemi_me25ls01/target.cpp @@ -0,0 +1,209 @@ +#include +//#include "t1000e_sensors.h" +#include "target.h" +#include + +MinewsemiME25LS01Board 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 rtc_clock; +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +//T1000SensorManager sensors = T1000SensorManager(nmea); +me25ls01SensorManager sensors = me25ls01SensorManager(nmea); + +#ifdef DISPLAY_CLASS + NullDisplayDriver display; +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +#ifdef RF_SWITCH_TABLE +static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_LR11X0_DIO7, + RADIOLIB_LR11X0_DIO8, + RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 DIO7 DIO8 + { LR11x0::MODE_STBY, {LOW, LOW, LOW, LOW }}, + { LR11x0::MODE_RX, {HIGH, LOW, LOW, HIGH }}, + { LR11x0::MODE_TX, {HIGH, HIGH, LOW, HIGH }}, + { LR11x0::MODE_TX_HP, {LOW, HIGH, LOW, HIGH }}, + { LR11x0::MODE_TX_HF, {LOW, LOW, LOW, LOW }}, + { LR11x0::MODE_GNSS, {LOW, LOW, HIGH, LOW }}, + { LR11x0::MODE_WIFI, {LOW, LOW, LOW, LOW }}, + END_OF_MODE_TABLE, +}; +#endif + +bool radio_init() { + //rtc_clock.begin(Wire); + +#ifdef LR11X0_DIO3_TCXO_VOLTAGE + float tcxo = LR11X0_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(1); + +#ifdef RF_SWITCH_TABLE + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); +#endif +#ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} + +void me25ls01SensorManager::start_gps() { + gps_active = false; + //_nmea->begin(); + // this init sequence should be better + // comes from seeed examples and deals with all gps pins + // pinMode(GPS_EN, OUTPUT); + // digitalWrite(GPS_EN, HIGH); + // delay(10); + // pinMode(GPS_VRTC_EN, OUTPUT); + // digitalWrite(GPS_VRTC_EN, HIGH); + // delay(10); + + // pinMode(GPS_RESET, OUTPUT); + // digitalWrite(GPS_RESET, HIGH); + // delay(10); + // digitalWrite(GPS_RESET, LOW); + + // pinMode(GPS_SLEEP_INT, OUTPUT); + // digitalWrite(GPS_SLEEP_INT, HIGH); + // pinMode(GPS_RTC_INT, OUTPUT); + // digitalWrite(GPS_RTC_INT, LOW); + // pinMode(GPS_RESETB, INPUT_PULLUP); +} + +void me25ls01SensorManager::sleep_gps() { + gps_active = false; + // digitalWrite(GPS_VRTC_EN, HIGH); + // digitalWrite(GPS_EN, LOW); + // digitalWrite(GPS_RESET, HIGH); + // digitalWrite(GPS_SLEEP_INT, HIGH); + // digitalWrite(GPS_RTC_INT, LOW); + // pinMode(GPS_RESETB, OUTPUT); + // digitalWrite(GPS_RESETB, LOW); + //_nmea->stop(); +} + +void me25ls01SensorManager::stop_gps() { + gps_active = false; + // digitalWrite(GPS_VRTC_EN, LOW); + // digitalWrite(GPS_EN, LOW); + // digitalWrite(GPS_RESET, HIGH); + // digitalWrite(GPS_SLEEP_INT, HIGH); + // digitalWrite(GPS_RTC_INT, LOW); + // pinMode(GPS_RESETB, OUTPUT); + // digitalWrite(GPS_RESETB, LOW); + // //_nmea->stop(); +} + + +bool me25ls01SensorManager::begin() { + // init GPS + Serial1.begin(115200); + + // make sure gps pin are off + // digitalWrite(GPS_VRTC_EN, LOW); + // digitalWrite(GPS_RESET, LOW); + // digitalWrite(GPS_SLEEP_INT, LOW); + // digitalWrite(GPS_RTC_INT, LOW); + // pinMode(GPS_RESETB, OUTPUT); + // digitalWrite(GPS_RESETB, LOW); + + return true; +} + +bool me25ls01SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { + if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission? + //telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); + } + if (requester_permissions & TELEM_PERM_ENVIRONMENT) { + //telemetry.addLuminosity(TELEM_CHANNEL_SELF, t1000e_get_light()); + //telemetry.addTemperature(TELEM_CHANNEL_SELF, t1000e_get_temperature()); + } + return true; +} + +void me25ls01SensorManager::loop() { + static long next_gps_update = 0; + + //_nmea->loop(); + + // if (millis() > next_gps_update) { + // if (_nmea->isValid()) { + // node_lat = ((double)_nmea->getLatitude())/1000000.; + // node_lon = ((double)_nmea->getLongitude())/1000000.; + // node_altitude = ((double)_nmea->getAltitude()) / 1000.0; + // //Serial.printf("lat %f lon %f\r\n", _lat, _lon); + // } + // next_gps_update = millis() + 1000; + //} +} + +int me25ls01SensorManager::getNumSettings() const { return 1; } // just one supported: "gps" (power switch) + +const char* me25ls01SensorManager::getSettingName(int i) const { + return i == 0 ? "gps" : NULL; +} +const char* me25ls01SensorManager::getSettingValue(int i) const { + if (i == 0) { + return gps_active ? "1" : "0"; + } + return NULL; +} +bool me25ls01SensorManager::setSettingValue(const char* name, const char* value) { + if (strcmp(name, "gps") == 0) { + // if (strcmp(value, "0") == 0) { + // sleep_gps(); // sleep for faster fix ! + // } else { + // start_gps(); + // } + return true; + } + return false; // not supported +} diff --git a/variants/minewsemi_me25ls01/target.h b/variants/minewsemi_me25ls01/target.h new file mode 100644 index 00000000..e5354a10 --- /dev/null +++ b/variants/minewsemi_me25ls01/target.h @@ -0,0 +1,46 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include "NullDisplayDriver.h" +#endif + +class me25ls01SensorManager: public SensorManager { + bool gps_active = false; + LocationProvider * _nmea; + + void start_gps(); + void sleep_gps(); + void stop_gps(); +public: + me25ls01SensorManager(LocationProvider &nmea): _nmea(&nmea) { } + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; + void loop() override; + int getNumSettings() const override; + const char* getSettingName(int i) const override; + const char* getSettingValue(int i) const override; + bool setSettingValue(const char* name, const char* value) override; +}; + +#ifdef DISPLAY_CLASS + extern NullDisplayDriver display; +#endif + +extern MinewsemiME25LS01Board board; +extern WRAPPER_CLASS radio_driver; +extern VolatileRTCClock rtc_clock; +extern me25ls01SensorManager 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/minewsemi_me25ls01/variant.cpp b/variants/minewsemi_me25ls01/variant.cpp new file mode 100644 index 00000000..5440e600 --- /dev/null +++ b/variants/minewsemi_me25ls01/variant.cpp @@ -0,0 +1,98 @@ +/* + * variant.cpp + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = +{ + 0, // P0.00 + 1, // P0.01 + 2, // P0.02, AIN0 BATTERY_PIN + 3, // P0.03 + 4, // P0.04, SENSOR_EN + 5, // P0.05, EXT_PWR_DETEC + 6, // P0.06, PIN_BUTTON1 + 7, // P0.07, LORA_BUSY + 8, // P0.08, GPS_VRTC_EN + 9, // P0.09 + 10, // P0.10 + 11, // P0.11, PIN_SPI_SCK + 12, // P0.12, PIN_SPI_NSS + 13, // P0.13, PIN_SERIAL1_TX + 14, // P0.14, PIN_SERIAL1_RX + 15, // P0.15, GPS_RTC_INT + 16, // P0.16, PIN_SERIAL2_TX + 17, // P0.17, PIN_SERIAL2_RX + 18, // P0.18 + 19, // P0.19 + 20, // P0.20 + 21, // P0.21 + 22, // P0.22 + 23, // P0.23 + 24, // P0.24, LED_GREEN + 25, // P0.25, BUZZER_PIN + 26, // P0.26, PIN_WIRE_SDA + 27, // P0.27, PIN_WIRE_SCL + 28, // P0.28 + 29, // P0.29, AIN5, LUX_SENSOR + 30, // P0.30 + 31, // P0.31, AIN7, TEMP_SENSOR + 32, // P1.00 + 33, // P1.01, LORA_DIO_1 + 34, // P1.02 + 35, // P1.03, EXT_CHRG_DETECT + 36, // P1.04 + 37, // P1.05, LR1110_EN + 38, // P1.06, 3V3_EN PWR TO SENSORS + 39, // P1.07, PIN_3V3_ACC_EN + 40, // P1.08, PIN_SPI_MISO + 41, // P1.09, PIN_SPI_MOSI + 42, // P1.10, LORA_RESET + 43, // P1.11, GPS_EN + 44, // P1.12, GPS_SLEEP_INT + 45, // P1.13 + 46, // P1.14, GPS_RESETB + 47, // P1.15, PIN_GPS_RESET + 255, // NRFX_SPIM_PIN_NOT_USED +}; + +void initVariant() +{ + // All pins output HIGH by default. + // https://github.com/Seeed-Studio/Adafruit_nRF52_Arduino/blob/fab7d30a997a1dfeef9d1d59bfb549adda73815a/cores/nRF5/wiring.c#L65-L69 + + pinMode(BATTERY_PIN, INPUT); + pinMode(EXT_CHRG_DETECT, INPUT); + pinMode(EXT_PWR_DETECT, INPUT); + // pinMode(GPS_RESETB, INPUT); + pinMode(PIN_BUTTON1, INPUT); + + pinMode(PIN_3V3_EN, OUTPUT); + pinMode(PIN_3V3_ACC_EN, OUTPUT); + // pinMode(BUZZER_EN, OUTPUT); + // pinMode(SENSOR_EN, OUTPUT); + // pinMode(GPS_EN, OUTPUT); + // pinMode(GPS_RESET, OUTPUT); + // pinMode(GPS_VRTC_EN, OUTPUT); + // pinMode(GPS_SLEEP_INT, OUTPUT); + // pinMode(GPS_RTC_INT, OUTPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(P_LORA_TX_LED, OUTPUT); + + // digitalWrite(PIN_3V3_EN, LOW); + // digitalWrite(PIN_3V3_ACC_EN, LOW); + // digitalWrite(BUZZER_EN, LOW); + // digitalWrite(SENSOR_EN, LOW); + // digitalWrite(GPS_EN, LOW); + // digitalWrite(GPS_RESET, LOW); + // digitalWrite(GPS_VRTC_EN, LOW); + // digitalWrite(GPS_SLEEP_INT, HIGH); + // digitalWrite(GPS_RTC_INT, LOW); + digitalWrite(LED_PIN, HIGH); + digitalWrite(P_LORA_TX_LED, LOW); +} diff --git a/variants/minewsemi_me25ls01/variant.h b/variants/minewsemi_me25ls01/variant.h new file mode 100644 index 00000000..d085a4b9 --- /dev/null +++ b/variants/minewsemi_me25ls01/variant.h @@ -0,0 +1,147 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) +// #define USE_LFRC // 32.768 kHz RC oscillator + +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define NRF_APM // detect usb power +#define PIN_3V3_EN (32 + 5) // P1.6 Power to Sensors +#define PIN_3V3_ACC_EN -1 + +#define BATTERY_PIN (-1) // P0.2/AIN0 +#define BATTERY_IMMUTABLE +#define ADC_MULTIPLIER (2.0F) + +#define EXT_CHRG_DETECT (-1) // P1.3 +#define EXT_PWR_DETECT (-1) // P0.5 + +#define ADC_RESOLUTION (14) +#define BATTERY_SENSE_RES (12) + +#define AREF_VOLTAGE (3.0) + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX (14) // P0.14 - checked +#define PIN_SERIAL1_TX (13) // P0.13 - checked + +#define PIN_SERIAL2_RX (17) // P0.17 - checked +#define PIN_SERIAL2_TX (16) // P0.16 - checked + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define HAS_WIRE (1) // checked +#define WIRE_INTERFACES_COUNT (1) // checked + +#define PIN_WIRE_SDA (15) // P0.26 - checked +#define PIN_WIRE_SCL (17) // P0.27 - checked +#define I2C_NO_RESCAN +// #define HAS_QMA6100P +// #define QMA_6100P_INT_PIN (-1) // P1.2 + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (0 + 29) // P0.29 +#define PIN_SPI_MOSI (0 + 2) // P0.2 +#define PIN_SPI_SCK (32 + 15) // P1.15 +#define PIN_SPI_NSS (32 + 13) // P1.13 + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define LED_BUILTIN (-1) +#define LED_RED (32 + 5) // P1.5 +#define LED_BLUE (32 + 7) // P1.7 +#define LED_PIN LED_BLUE +#define P_LORA_TX_LED LED_RED + +#define LED_STATE_ON HIGH + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (0 + 27) // P0.6 +#define BUTTON_PIN PIN_BUTTON1 + +//////////////////////////////////////////////////////////////////////////////// +// LR1110 + +#define LORA_DIO_1 (32 + 12) // P1.12 +#define LORA_DIO_2 (32 + 10) // P1.10 +#define LORA_NSS (PIN_SPI_NSS) // P1.13 +#define LORA_RESET (32 + 11) // P1.11 +#define LORA_BUSY (32 + 10) // P1.10 +#define LORA_SCLK (PIN_SPI_SCK) // P1.15 +#define LORA_MISO (PIN_SPI_MISO) // P0.29 +#define LORA_MOSI (PIN_SPI_MOSI) // P0.2 +#define LORA_CS PIN_SPI_NSS // P1.13 + +#define LR11X0_DIO_AS_RF_SWITCH true +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 + +#define LR1110_IRQ_PIN LORA_DIO_1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO_2 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCLK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO +//////////////////////////////////////////////////////////////////////////////// +// GPS + +//#define HAS_GPS 0 +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +#define GPS_EN (-1) // P1.11 +#define GPS_RESET (-1) // P1.15 + +#define GPS_VRTC_EN (-1) // P0.8 +#define GPS_SLEEP_INT (-1) // P1.12 +#define GPS_RTC_INT (-1) // P0.15 +#define GPS_RESETB (-1) // P1.14 + +//////////////////////////////////////////////////////////////////////////////// +// Temp+Lux Sensor + +#define SENSOR_EN (-1) // P0.4 +#define TEMP_SENSOR (-1) // P0.31/AIN7 +#define LUX_SENSOR (-1) // P0.29/AIN5 + +//////////////////////////////////////////////////////////////////////////////// +// Accelerometer (I2C addr : ??? ) + +#define PIN_3V3_ACC_EN (-1) // P1.7 + +//////////////////////////////////////////////////////////////////////////////// +// Buzzer + +#define BUZZER_EN (-1) // P1.5 +#define BUZZER_PIN (0 + 25) // P0.25 \ No newline at end of file From af2628bb00ec8306b15520438c23c0bc39608257 Mon Sep 17 00:00:00 2001 From: Normunds Gavars Date: Wed, 2 Jul 2025 12:27:39 +0300 Subject: [PATCH 03/60] Use EnvironmentSensorManager in Minewsemi variant --- variants/minewsemi_me25ls01/target.cpp | 129 ++---------------------- variants/minewsemi_me25ls01/target.h | 21 +--- variants/minewsemi_me25ls01/variant.cpp | 6 -- variants/minewsemi_me25ls01/variant.h | 6 -- 4 files changed, 11 insertions(+), 151 deletions(-) diff --git a/variants/minewsemi_me25ls01/target.cpp b/variants/minewsemi_me25ls01/target.cpp index b6b1b585..13306762 100644 --- a/variants/minewsemi_me25ls01/target.cpp +++ b/variants/minewsemi_me25ls01/target.cpp @@ -1,7 +1,5 @@ #include -//#include "t1000e_sensors.h" #include "target.h" -#include MinewsemiME25LS01Board board; @@ -10,9 +8,14 @@ RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BU WRAPPER_CLASS radio_driver(radio, board); VolatileRTCClock rtc_clock; -MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); -//T1000SensorManager sensors = T1000SensorManager(nmea); -me25ls01SensorManager sensors = me25ls01SensorManager(nmea); +extern EnvironmentSensorManager sensors; +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif #ifdef DISPLAY_CLASS NullDisplayDriver display; @@ -92,118 +95,4 @@ void radio_set_tx_power(uint8_t dbm) { mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); // create new random identity -} - -void me25ls01SensorManager::start_gps() { - gps_active = false; - //_nmea->begin(); - // this init sequence should be better - // comes from seeed examples and deals with all gps pins - // pinMode(GPS_EN, OUTPUT); - // digitalWrite(GPS_EN, HIGH); - // delay(10); - // pinMode(GPS_VRTC_EN, OUTPUT); - // digitalWrite(GPS_VRTC_EN, HIGH); - // delay(10); - - // pinMode(GPS_RESET, OUTPUT); - // digitalWrite(GPS_RESET, HIGH); - // delay(10); - // digitalWrite(GPS_RESET, LOW); - - // pinMode(GPS_SLEEP_INT, OUTPUT); - // digitalWrite(GPS_SLEEP_INT, HIGH); - // pinMode(GPS_RTC_INT, OUTPUT); - // digitalWrite(GPS_RTC_INT, LOW); - // pinMode(GPS_RESETB, INPUT_PULLUP); -} - -void me25ls01SensorManager::sleep_gps() { - gps_active = false; - // digitalWrite(GPS_VRTC_EN, HIGH); - // digitalWrite(GPS_EN, LOW); - // digitalWrite(GPS_RESET, HIGH); - // digitalWrite(GPS_SLEEP_INT, HIGH); - // digitalWrite(GPS_RTC_INT, LOW); - // pinMode(GPS_RESETB, OUTPUT); - // digitalWrite(GPS_RESETB, LOW); - //_nmea->stop(); -} - -void me25ls01SensorManager::stop_gps() { - gps_active = false; - // digitalWrite(GPS_VRTC_EN, LOW); - // digitalWrite(GPS_EN, LOW); - // digitalWrite(GPS_RESET, HIGH); - // digitalWrite(GPS_SLEEP_INT, HIGH); - // digitalWrite(GPS_RTC_INT, LOW); - // pinMode(GPS_RESETB, OUTPUT); - // digitalWrite(GPS_RESETB, LOW); - // //_nmea->stop(); -} - - -bool me25ls01SensorManager::begin() { - // init GPS - Serial1.begin(115200); - - // make sure gps pin are off - // digitalWrite(GPS_VRTC_EN, LOW); - // digitalWrite(GPS_RESET, LOW); - // digitalWrite(GPS_SLEEP_INT, LOW); - // digitalWrite(GPS_RTC_INT, LOW); - // pinMode(GPS_RESETB, OUTPUT); - // digitalWrite(GPS_RESETB, LOW); - - return true; -} - -bool me25ls01SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { - if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission? - //telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); - } - if (requester_permissions & TELEM_PERM_ENVIRONMENT) { - //telemetry.addLuminosity(TELEM_CHANNEL_SELF, t1000e_get_light()); - //telemetry.addTemperature(TELEM_CHANNEL_SELF, t1000e_get_temperature()); - } - return true; -} - -void me25ls01SensorManager::loop() { - static long next_gps_update = 0; - - //_nmea->loop(); - - // if (millis() > next_gps_update) { - // if (_nmea->isValid()) { - // node_lat = ((double)_nmea->getLatitude())/1000000.; - // node_lon = ((double)_nmea->getLongitude())/1000000.; - // node_altitude = ((double)_nmea->getAltitude()) / 1000.0; - // //Serial.printf("lat %f lon %f\r\n", _lat, _lon); - // } - // next_gps_update = millis() + 1000; - //} -} - -int me25ls01SensorManager::getNumSettings() const { return 1; } // just one supported: "gps" (power switch) - -const char* me25ls01SensorManager::getSettingName(int i) const { - return i == 0 ? "gps" : NULL; -} -const char* me25ls01SensorManager::getSettingValue(int i) const { - if (i == 0) { - return gps_active ? "1" : "0"; - } - return NULL; -} -bool me25ls01SensorManager::setSettingValue(const char* name, const char* value) { - if (strcmp(name, "gps") == 0) { - // if (strcmp(value, "0") == 0) { - // sleep_gps(); // sleep for faster fix ! - // } else { - // start_gps(); - // } - return true; - } - return false; // not supported -} +} \ No newline at end of file diff --git a/variants/minewsemi_me25ls01/target.h b/variants/minewsemi_me25ls01/target.h index e5354a10..aad55757 100644 --- a/variants/minewsemi_me25ls01/target.h +++ b/variants/minewsemi_me25ls01/target.h @@ -8,28 +8,11 @@ #include #include #include +#include #ifdef DISPLAY_CLASS #include "NullDisplayDriver.h" #endif -class me25ls01SensorManager: public SensorManager { - bool gps_active = false; - LocationProvider * _nmea; - - void start_gps(); - void sleep_gps(); - void stop_gps(); -public: - me25ls01SensorManager(LocationProvider &nmea): _nmea(&nmea) { } - bool begin() override; - bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; - void loop() override; - int getNumSettings() const override; - const char* getSettingName(int i) const override; - const char* getSettingValue(int i) const override; - bool setSettingValue(const char* name, const char* value) override; -}; - #ifdef DISPLAY_CLASS extern NullDisplayDriver display; #endif @@ -37,7 +20,7 @@ public: extern MinewsemiME25LS01Board board; extern WRAPPER_CLASS radio_driver; extern VolatileRTCClock rtc_clock; -extern me25ls01SensorManager sensors; +extern EnvironmentSensorManager sensors; bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/minewsemi_me25ls01/variant.cpp b/variants/minewsemi_me25ls01/variant.cpp index 5440e600..a6b6eaab 100644 --- a/variants/minewsemi_me25ls01/variant.cpp +++ b/variants/minewsemi_me25ls01/variant.cpp @@ -1,9 +1,3 @@ -/* - * variant.cpp - * Copyright (C) 2023 Seeed K.K. - * MIT License - */ - #include "variant.h" #include "wiring_constants.h" #include "wiring_digital.h" diff --git a/variants/minewsemi_me25ls01/variant.h b/variants/minewsemi_me25ls01/variant.h index d085a4b9..de57a65e 100644 --- a/variants/minewsemi_me25ls01/variant.h +++ b/variants/minewsemi_me25ls01/variant.h @@ -1,9 +1,3 @@ -/* - * variant.h - * Copyright (C) 2023 Seeed K.K. - * MIT License - */ - #pragma once #include "WVariant.h" From 3832836eb24093133b86609cb0d677f8d0c5d8c8 Mon Sep 17 00:00:00 2001 From: recrof Date: Wed, 2 Jul 2025 16:42:35 +0200 Subject: [PATCH 04/60] EnvironmentSensorManager: add support for SHTC3 and LPS22HB --- .../sensors/EnvironmentSensorManager.cpp | 78 +++++++++++++++---- .../sensors/EnvironmentSensorManager.h | 2 + 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index b672cdd4..f0480a87 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -24,6 +24,14 @@ static Adafruit_BME280 BME280; static Adafruit_BMP280 BMP280; #endif +#if ENV_INCLUDE_SHTC3 +#include +static Adafruit_SHTC3 SHTC3; +#endif + +#if ENV_INCLUDE_LPS22HB +#include +#endif #if ENV_INCLUDE_INA3221 #define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address @@ -76,28 +84,48 @@ bool EnvironmentSensorManager::begin() { } #endif + #if ENV_INCLUDE_SHTC3 + if (SHTC3.begin()) { + MESH_DEBUG_PRINTLN("Found sensor: SHTC3"); + SHTC3_initialized = true; + } else { + SHTC3_initialized = false; + MESH_DEBUG_PRINTLN("SHTC3 was not found at I2C address %02X", 0x70); + } + #endif + + #if ENV_INCLUDE_LPS22HB + if (BARO.begin()) { + MESH_DEBUG_PRINTLN("Found sensor: LPS22HB"); + LPS22HB_initialized = true; + } else { + LPS22HB_initialized = false; + MESH_DEBUG_PRINTLN("LPS22HB was not found at I2C address %02X", 0x5C); + } + #endif + #if ENV_INCLUDE_INA3221 if (INA3221.begin(TELEM_INA3221_ADDRESS, &Wire)) { - MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS); - MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID()); + MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS); + MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID()); - for(int i = 0; i < 3; i++) { - INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE); - } - INA3221_initialized = true; + for(int i = 0; i < 3; i++) { + INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE); + } + INA3221_initialized = true; } else { - INA3221_initialized = false; - MESH_DEBUG_PRINTLN("INA3221 was not found at I2C address %02X", TELEM_INA3221_ADDRESS); + INA3221_initialized = false; + MESH_DEBUG_PRINTLN("INA3221 was not found at I2C address %02X", TELEM_INA3221_ADDRESS); } #endif #if ENV_INCLUDE_INA219 if (INA219.begin(&Wire)) { - MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS); - INA219_initialized = true; + MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS); + INA219_initialized = true; } else { - INA219_initialized = false; - MESH_DEBUG_PRINTLN("INA219 was not found at I2C address %02X", TELEM_INA219_ADDRESS); + INA219_initialized = false; + MESH_DEBUG_PRINTLN("INA219 was not found at I2C address %02X", TELEM_INA219_ADDRESS); } #endif @@ -139,6 +167,24 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen } #endif + #if ENV_INCLUDE_SHTC3 + if (SHTC3_initialized) { + sensors_event_t humidity, temp; + SHTC3.getEvent(&humidity, &temp); + + telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature); + telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity); + } + #endif + + #if ENV_INCLUDE_LPS22HB + if (LPS22HB_initialized) { + telemetry.addTemperature(TELEM_CHANNEL_SELF, BARO.readTemperature()); + telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BARO.readPressure()); + telemetry.addAltitude(TELEM_CHANNEL_SELF, BARO.readAltitude()); + } + #endif + #if ENV_INCLUDE_INA3221 if (INA3221_initialized) { for(int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) { @@ -157,10 +203,10 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen #if ENV_INCLUDE_INA219 if (INA219_initialized) { - telemetry.addVoltage(next_available_channel, INA219.getBusVoltage_V()); - telemetry.addCurrent(next_available_channel, INA219.getCurrent_mA() / 1000); - telemetry.addPower(next_available_channel, INA219.getPower_mW() / 1000); - next_available_channel++; + telemetry.addVoltage(next_available_channel, INA219.getBusVoltage_V()); + telemetry.addCurrent(next_available_channel, INA219.getCurrent_mA() / 1000); + telemetry.addPower(next_available_channel, INA219.getPower_mW() / 1000); + next_available_channel++; } #endif diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 76bffc48..f7804431 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -13,6 +13,8 @@ protected: bool BMP280_initialized = false; bool INA3221_initialized = false; bool INA219_initialized = false; + bool SHTC3_initialized = false; + bool LPS22HB_initialized = false; bool gps_detected = false; bool gps_active = false; From 539f99a90f0b9ff32034e8aa3dbce95d4b7791af Mon Sep 17 00:00:00 2001 From: recrof Date: Wed, 2 Jul 2025 16:50:47 +0200 Subject: [PATCH 05/60] removed unsupported(?) readAltitude --- src/helpers/sensors/EnvironmentSensorManager.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index f0480a87..a424c46b 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -181,7 +181,6 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen if (LPS22HB_initialized) { telemetry.addTemperature(TELEM_CHANNEL_SELF, BARO.readTemperature()); telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BARO.readPressure()); - telemetry.addAltitude(TELEM_CHANNEL_SELF, BARO.readAltitude()); } #endif From dcb7ffa92ee470896380eb191730c4a45b983716 Mon Sep 17 00:00:00 2001 From: JQ Date: Wed, 2 Jul 2025 08:32:36 -0700 Subject: [PATCH 06/60] fixing radio include order for heltec paper --- variants/heltec_wireless_paper/target.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/variants/heltec_wireless_paper/target.h b/variants/heltec_wireless_paper/target.h index f06eead3..7d901c01 100644 --- a/variants/heltec_wireless_paper/target.h +++ b/variants/heltec_wireless_paper/target.h @@ -2,10 +2,10 @@ #define RADIOLIB_STATIC_ONLY 1 #include -#include -#include -#include #include +#include +#include +#include #include #ifdef DISPLAY_CLASS #include From ad2e015a5b35a5f9e4a0aa7584111f728897555a Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 2 Jul 2025 10:24:45 -0700 Subject: [PATCH 07/60] move rak usr btn to companions repeaters do not typically have user buttons and there is only one analog pin available on most, if not all, base boards. so this allows repeaters to add custom peripherals or alternate battery signals --- variants/rak4631/platformio.ini | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 9391f99b..86879ed7 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -6,8 +6,6 @@ board_check = true build_flags = ${nrf52840_base.build_flags} -I variants/rak4631 -D RAK_4631 - -D PIN_USER_BTN=9 - -D PIN_USER_BTN_ANA=31 -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 @@ -84,6 +82,8 @@ build_src_filter = ${rak4631.build_src_filter} extends = rak4631 build_flags = ${rak4631.build_flags} + -D PIN_USER_BTN=9 + -D PIN_USER_BTN_ANA=31 -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 @@ -100,6 +100,8 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} + -D PIN_USER_BTN=9 + -D PIN_USER_BTN_ANA=31 -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 @@ -120,6 +122,8 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} + -D PIN_USER_BTN=9 + -D PIN_USER_BTN_ANA=31 -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 @@ -145,6 +149,8 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} + -D PIN_USER_BTN=9 + -D PIN_USER_BTN_ANA=31 -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=1 ; -D MESH_PACKET_LOGGING=1 From 6440bcaf48ddfb37d0f281928127f565d064c5c2 Mon Sep 17 00:00:00 2001 From: Normunds Gavars Date: Thu, 3 Jul 2025 00:07:50 +0300 Subject: [PATCH 08/60] Clean up pins in variant.h --- variants/minewsemi_me25ls01/variant.cpp | 64 ++++++++----------------- variants/minewsemi_me25ls01/variant.h | 60 ++++------------------- 2 files changed, 31 insertions(+), 93 deletions(-) diff --git a/variants/minewsemi_me25ls01/variant.cpp b/variants/minewsemi_me25ls01/variant.cpp index a6b6eaab..15f30d14 100644 --- a/variants/minewsemi_me25ls01/variant.cpp +++ b/variants/minewsemi_me25ls01/variant.cpp @@ -6,44 +6,44 @@ const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = { 0, // P0.00 1, // P0.01 - 2, // P0.02, AIN0 BATTERY_PIN + 2, // P0.02 3, // P0.03 - 4, // P0.04, SENSOR_EN - 5, // P0.05, EXT_PWR_DETEC - 6, // P0.06, PIN_BUTTON1 - 7, // P0.07, LORA_BUSY - 8, // P0.08, GPS_VRTC_EN + 4, // P0.04 + 5, // P0.05 + 6, // P0.06 + 7, // P0.07 + 8, // P0.08 9, // P0.09 10, // P0.10 - 11, // P0.11, PIN_SPI_SCK - 12, // P0.12, PIN_SPI_NSS + 11, // P0.11 + 12, // P0.12 13, // P0.13, PIN_SERIAL1_TX 14, // P0.14, PIN_SERIAL1_RX - 15, // P0.15, GPS_RTC_INT - 16, // P0.16, PIN_SERIAL2_TX - 17, // P0.17, PIN_SERIAL2_RX + 15, // P0.15, PIN_SERIAL2_RX + 16, // P0.16, PIN_WIRE_SCL + 17, // P0.17, PIN_SERIAL2_TX 18, // P0.18 19, // P0.19 20, // P0.20 - 21, // P0.21 + 21, // P0.21, PIN_WIRE_SDA 22, // P0.22 23, // P0.23 - 24, // P0.24, LED_GREEN - 25, // P0.25, BUZZER_PIN - 26, // P0.26, PIN_WIRE_SDA - 27, // P0.27, PIN_WIRE_SCL + 24, // P0.24, + 25, // P0.25, + 26, // P0.26, + 27, // P0.27, 28, // P0.28 - 29, // P0.29, AIN5, LUX_SENSOR + 29, // P0.29, 30, // P0.30 - 31, // P0.31, AIN7, TEMP_SENSOR + 31, // P0.31, 32, // P1.00 33, // P1.01, LORA_DIO_1 34, // P1.02 - 35, // P1.03, EXT_CHRG_DETECT + 35, // P1.03, 36, // P1.04 37, // P1.05, LR1110_EN - 38, // P1.06, 3V3_EN PWR TO SENSORS - 39, // P1.07, PIN_3V3_ACC_EN + 38, // P1.06, + 39, // P1.07, 40, // P1.08, PIN_SPI_MISO 41, // P1.09, PIN_SPI_MOSI 42, // P1.10, LORA_RESET @@ -57,36 +57,14 @@ const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = void initVariant() { - // All pins output HIGH by default. - // https://github.com/Seeed-Studio/Adafruit_nRF52_Arduino/blob/fab7d30a997a1dfeef9d1d59bfb549adda73815a/cores/nRF5/wiring.c#L65-L69 - pinMode(BATTERY_PIN, INPUT); - pinMode(EXT_CHRG_DETECT, INPUT); - pinMode(EXT_PWR_DETECT, INPUT); - // pinMode(GPS_RESETB, INPUT); pinMode(PIN_BUTTON1, INPUT); pinMode(PIN_3V3_EN, OUTPUT); pinMode(PIN_3V3_ACC_EN, OUTPUT); - // pinMode(BUZZER_EN, OUTPUT); - // pinMode(SENSOR_EN, OUTPUT); - // pinMode(GPS_EN, OUTPUT); - // pinMode(GPS_RESET, OUTPUT); - // pinMode(GPS_VRTC_EN, OUTPUT); - // pinMode(GPS_SLEEP_INT, OUTPUT); - // pinMode(GPS_RTC_INT, OUTPUT); pinMode(LED_PIN, OUTPUT); pinMode(P_LORA_TX_LED, OUTPUT); - // digitalWrite(PIN_3V3_EN, LOW); - // digitalWrite(PIN_3V3_ACC_EN, LOW); - // digitalWrite(BUZZER_EN, LOW); - // digitalWrite(SENSOR_EN, LOW); - // digitalWrite(GPS_EN, LOW); - // digitalWrite(GPS_RESET, LOW); - // digitalWrite(GPS_VRTC_EN, LOW); - // digitalWrite(GPS_SLEEP_INT, HIGH); - // digitalWrite(GPS_RTC_INT, LOW); digitalWrite(LED_PIN, HIGH); digitalWrite(P_LORA_TX_LED, LOW); } diff --git a/variants/minewsemi_me25ls01/variant.h b/variants/minewsemi_me25ls01/variant.h index de57a65e..bbf15ff6 100644 --- a/variants/minewsemi_me25ls01/variant.h +++ b/variants/minewsemi_me25ls01/variant.h @@ -9,10 +9,7 @@ #define VARIANT_MCK (64000000ul) // #define USE_LFRC // 32.768 kHz RC oscillator -//////////////////////////////////////////////////////////////////////////////// // Power - -#define NRF_APM // detect usb power #define PIN_3V3_EN (32 + 5) // P1.6 Power to Sensors #define PIN_3V3_ACC_EN -1 @@ -20,46 +17,33 @@ #define BATTERY_IMMUTABLE #define ADC_MULTIPLIER (2.0F) -#define EXT_CHRG_DETECT (-1) // P1.3 -#define EXT_PWR_DETECT (-1) // P0.5 - #define ADC_RESOLUTION (14) #define BATTERY_SENSE_RES (12) #define AREF_VOLTAGE (3.0) -//////////////////////////////////////////////////////////////////////////////// // Number of pins - #define PINS_COUNT (48) #define NUM_DIGITAL_PINS (48) #define NUM_ANALOG_INPUTS (6) #define NUM_ANALOG_OUTPUTS (0) -//////////////////////////////////////////////////////////////////////////////// // UART pin definition +#define PIN_SERIAL1_RX (14) // P0.14 +#define PIN_SERIAL1_TX (13) // P0.13 -#define PIN_SERIAL1_RX (14) // P0.14 - checked -#define PIN_SERIAL1_TX (13) // P0.13 - checked +#define PIN_SERIAL2_RX (15) // P0.15 +#define PIN_SERIAL2_TX (17) // P0.17 -#define PIN_SERIAL2_RX (17) // P0.17 - checked -#define PIN_SERIAL2_TX (16) // P0.16 - checked - -//////////////////////////////////////////////////////////////////////////////// // I2C pin definition +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (1) -#define HAS_WIRE (1) // checked -#define WIRE_INTERFACES_COUNT (1) // checked - -#define PIN_WIRE_SDA (15) // P0.26 - checked -#define PIN_WIRE_SCL (17) // P0.27 - checked +#define PIN_WIRE_SDA (21) // P0.21 +#define PIN_WIRE_SCL (16) // P0.16 #define I2C_NO_RESCAN -// #define HAS_QMA6100P -// #define QMA_6100P_INT_PIN (-1) // P1.2 -//////////////////////////////////////////////////////////////////////////////// // SPI pin definition - #define SPI_INTERFACES_COUNT (1) #define PIN_SPI_MISO (0 + 29) // P0.29 @@ -67,9 +51,7 @@ #define PIN_SPI_SCK (32 + 15) // P1.15 #define PIN_SPI_NSS (32 + 13) // P1.13 -//////////////////////////////////////////////////////////////////////////////// // Builtin LEDs - #define LED_BUILTIN (-1) #define LED_RED (32 + 5) // P1.5 #define LED_BLUE (32 + 7) // P1.7 @@ -78,15 +60,12 @@ #define LED_STATE_ON HIGH -//////////////////////////////////////////////////////////////////////////////// // Builtin buttons #define PIN_BUTTON1 (0 + 27) // P0.6 #define BUTTON_PIN PIN_BUTTON1 -//////////////////////////////////////////////////////////////////////////////// // LR1110 - #define LORA_DIO_1 (32 + 12) // P1.12 #define LORA_DIO_2 (32 + 10) // P1.10 #define LORA_NSS (PIN_SPI_NSS) // P1.13 @@ -107,10 +86,9 @@ #define LR1110_SPI_SCK_PIN LORA_SCLK #define LR1110_SPI_MOSI_PIN LORA_MOSI #define LR1110_SPI_MISO_PIN LORA_MISO -//////////////////////////////////////////////////////////////////////////////// -// GPS -//#define HAS_GPS 0 +// GPS +#define HAS_GPS 0 #define GPS_RX_PIN PIN_SERIAL1_RX #define GPS_TX_PIN PIN_SERIAL1_TX @@ -121,21 +99,3 @@ #define GPS_SLEEP_INT (-1) // P1.12 #define GPS_RTC_INT (-1) // P0.15 #define GPS_RESETB (-1) // P1.14 - -//////////////////////////////////////////////////////////////////////////////// -// Temp+Lux Sensor - -#define SENSOR_EN (-1) // P0.4 -#define TEMP_SENSOR (-1) // P0.31/AIN7 -#define LUX_SENSOR (-1) // P0.29/AIN5 - -//////////////////////////////////////////////////////////////////////////////// -// Accelerometer (I2C addr : ??? ) - -#define PIN_3V3_ACC_EN (-1) // P1.7 - -//////////////////////////////////////////////////////////////////////////////// -// Buzzer - -#define BUZZER_EN (-1) // P1.5 -#define BUZZER_PIN (0 + 25) // P0.25 \ No newline at end of file From ca422bbafbebea98083d08fadf58902a77363fc3 Mon Sep 17 00:00:00 2001 From: JQ Date: Wed, 2 Jul 2025 14:37:11 -0700 Subject: [PATCH 09/60] fix ble pin --- variants/heltec_wireless_paper/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 0b9ac38e..513ba4b9 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -40,7 +40,7 @@ build_flags = -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D DISPLAY_CLASS=E213Display - -D BLE_PIN_CODE=0 ; dynamic, random PIN + -D BLE_PIN_CODE=123456 ; dynamic, random PIN -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter} From ec98d5f8a51dc497259610a4038ac25a35dbe112 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Wed, 2 Jul 2025 23:41:31 +0100 Subject: [PATCH 10/60] BLE: Remove ScanResponse.addName() to fix re-advertising after disconnect Removed the call to Bluefruit.ScanResponse.addName() in startAdv(), as it was preventing BLE from reliably restarting advertising after a disconnect. Hypothesis: adding the device name to the scan response exceeds internal buffer limits or causes a conflict with advertising timing, leading to the BLE stack silently failing to re-advertise. Tested successfully (on T-1000) without this line, advertising now resumes correctly after disconnection (on Iphone) --- src/helpers/nrf52/SerialBLEInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index f43a3767..c6c99d5b 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -27,7 +27,7 @@ void SerialBLEInterface::startAdv() { // Secondary Scan Response packet (optional) // Since there is no room for 'Name' in Advertising packet - Bluefruit.ScanResponse.addName(); + // Bluefruit.ScanResponse.addName(); /* Start Advertising * - Enable auto advertising if disconnected From d23378cff692f0be9ca4245104978536d8729f63 Mon Sep 17 00:00:00 2001 From: WattleFoxxo Date: Thu, 3 Jul 2025 11:42:53 +1000 Subject: [PATCH 11/60] Add XIAO RP2040 support --- src/helpers/rp2040/XiaoRP2040Board.cpp | 30 +++++++ src/helpers/rp2040/XiaoRP2040Board.h | 75 ++++++++++++++++++ variants/xiao_rp2040/platformio.ini | 104 +++++++++++++++++++++++++ variants/xiao_rp2040/target.cpp | 71 +++++++++++++++++ variants/xiao_rp2040/target.h | 21 +++++ 5 files changed, 301 insertions(+) create mode 100644 src/helpers/rp2040/XiaoRP2040Board.cpp create mode 100644 src/helpers/rp2040/XiaoRP2040Board.h create mode 100644 variants/xiao_rp2040/platformio.ini create mode 100644 variants/xiao_rp2040/target.cpp create mode 100644 variants/xiao_rp2040/target.h diff --git a/src/helpers/rp2040/XiaoRP2040Board.cpp b/src/helpers/rp2040/XiaoRP2040Board.cpp new file mode 100644 index 00000000..bb439706 --- /dev/null +++ b/src/helpers/rp2040/XiaoRP2040Board.cpp @@ -0,0 +1,30 @@ +#include "XiaoRP2040Board.h" + +#include +#include + +void XiaoRP2040Board::begin() { + // for future use, sub-classes SHOULD call this from their begin() + startup_reason = BD_STARTUP_NORMAL; + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); +#endif + +#ifdef PIN_VBAT_READ + pinMode(PIN_VBAT_READ, INPUT); +#endif + +#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + Wire.setSDA(PIN_BOARD_SDA); + Wire.setSCL(PIN_BOARD_SCL); +#endif + + Wire.begin(); + + delay(10); // give sx1262 some time to power up +} + +bool XiaoRP2040Board::startOTAUpdate(const char *id, char reply[]) { + return false; +} diff --git a/src/helpers/rp2040/XiaoRP2040Board.h b/src/helpers/rp2040/XiaoRP2040Board.h new file mode 100644 index 00000000..c9353906 --- /dev/null +++ b/src/helpers/rp2040/XiaoRP2040Board.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +// LoRa radio module pins for the Xiao RP2040 +// https://wiki.seeedstudio.com/XIAO-RP2040/ + +#define P_LORA_DIO_1 27 // D1 +#define P_LORA_NSS 6 // D4 +#define P_LORA_RESET 28 // D2 +#define P_LORA_BUSY 29 // D3 +#define P_LORA_TX_LED 17 + +#define SX126X_RXEN 7 // D5 +#define SX126X_TXEN -1 + +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +/* + * This board has no built-in way to read battery voltage. + * Nevertheless it's very easy to make it work, you only require two 1% resistors. + * If your using the WIO SX1262 Addon for xaio, make sure you dont connect D0! + * + * BAT+ -----+ + * | + * VSYS --+ -/\/\/\/\- --+ + * 200k | + * +-- D0 + * | + * GND --+ -/\/\/\/\- --+ + * | 100k + * BAT- -----+ + */ +#define PIN_VBAT_READ 26 // D0 +#define BATTERY_SAMPLES 8 +#define ADC_MULTIPLIER (3.0f * 3.3f * 1000) + +class XiaoRP2040Board : public mesh::MainBoard { +protected: + uint8_t startup_reason; + +public: + void begin(); + uint8_t getStartupReason() const override { return startup_reason; } + +#ifdef P_LORA_TX_LED + void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); } + void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } +#endif + + + uint16_t getBattMilliVolts() override { +#if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER) + analogReadResolution(12); + + uint32_t raw = 0; + for (int i = 0; i < BATTERY_SAMPLES; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / BATTERY_SAMPLES; + + return (ADC_MULTIPLIER * raw) / 4096; +#else + return 0; +#endif + } + + const char *getManufacturerName() const override { return "Xiao RP2040"; } + + void reboot() override { rp2040.reboot(); } + + bool startOTAUpdate(const char *id, char reply[]) override; +}; diff --git a/variants/xiao_rp2040/platformio.ini b/variants/xiao_rp2040/platformio.ini new file mode 100644 index 00000000..960fdbba --- /dev/null +++ b/variants/xiao_rp2040/platformio.ini @@ -0,0 +1,104 @@ +[Xiao_rp2040] +extends = rp2040_base + +board = seeed_xiao_rp2040 +board_build.filesystem_size = 0.5m + +build_flags = ${rp2040_base.build_flags} + -I variants/xiao_rp2040 + -D SX126X_CURRENT_LIMIT=140 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_RX_BOOSTED_GAIN=1 +; Debug options + ; -D DEBUG_RP2040_WIRE=1 + ; -D DEBUG_RP2040_SPI=1 + ; -D DEBUG_RP2040_CORE=1 + ; -D RADIOLIB_DEBUG_SPI=1 + ; -D DEBUG_RP2040_PORT=Serial + +build_src_filter = ${rp2040_base.build_src_filter} + + + +<../variants/xiao_rp2040> + +lib_deps = ${rp2040_base.lib_deps} + +[env:Xiao_rp2040_Repeater] +extends = Xiao_rp2040 +build_flags = ${Xiao_rp2040.build_flags} + -D ADVERT_NAME='"Xiao Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 + -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 +build_src_filter = ${Xiao_rp2040.build_src_filter} + +<../examples/simple_repeater> + +[env:Xiao_rp2040_room_server] +extends = Xiao_rp2040 +build_flags = ${Xiao_rp2040.build_flags} + -D ADVERT_NAME='"Xiao 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 = ${Xiao_rp2040.build_src_filter} + +<../examples/simple_room_server> + +[env:Xiao_rp2040_companion_radio_usb] +extends = Xiao_rp2040 +build_flags = ${Xiao_rp2040.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${Xiao_rp2040.build_src_filter} + +<../examples/companion_radio> +lib_deps = ${Xiao_rp2040.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; [env:Xiao_rp2040_companion_radio_ble] +; extends = Xiao_rp2040 +; build_flags = ${Xiao_rp2040.build_flags} +; -D MAX_CONTACTS=100 +; -D MAX_GROUP_CHANNELS=8 +; -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 +; ; -D MESH_PACKET_LOGGING=1 +; ; -D MESH_DEBUG=1 +; build_src_filter = ${Xiao_rp2040.build_src_filter} +; +<../examples/companion_radio> +; lib_deps = ${Xiao_rp2040.lib_deps} +; densaugeo/base64 @ ~1.4.0 + +; [env:Xiao_rp2040_companion_radio_wifi] +; extends = Xiao_rp2040 +; build_flags = ${Xiao_rp2040.build_flags} +; -D MAX_CONTACTS=100 +; -D MAX_GROUP_CHANNELS=8 +; -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 = ${Xiao_rp2040.build_src_filter} +; +<../examples/companion_radio> +; lib_deps = ${Xiao_rp2040.lib_deps} +; densaugeo/base64 @ ~1.4.0 + +[env:Xiao_rp2040_terminal_chat] +extends = Xiao_rp2040 +build_flags = ${Xiao_rp2040.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_rp2040.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = ${Xiao_rp2040.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/xiao_rp2040/target.cpp b/variants/xiao_rp2040/target.cpp new file mode 100644 index 00000000..a801aae8 --- /dev/null +++ b/variants/xiao_rp2040/target.cpp @@ -0,0 +1,71 @@ +#include "target.h" + +#include +#include + +XiaoRP2040Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifndef LORA_CR +#define LORA_CR 5 +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + +#ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); + + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(1); + +#ifdef SX126X_CURRENT_LIMIT + radio.setCurrentLimit(SX126X_CURRENT_LIMIT); +#endif + +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif + +#ifdef SX126X_RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +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/xiao_rp2040/target.h b/variants/xiao_rp2040/target.h new file mode 100644 index 00000000..6a3c192b --- /dev/null +++ b/variants/xiao_rp2040/target.h @@ -0,0 +1,21 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 + +#include +#include +#include +#include +#include +#include + +extern XiaoRP2040Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); From 90656e7d06ecf53f320149f1b9ce98e4a02cdf08 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 3 Jul 2025 09:18:26 -0700 Subject: [PATCH 12/60] clean up xiao nrf52 move variant specific code out of src/helpers redefine RXEN for alternate radio pinout --- .../nrf52 => variants/xiao_nrf52}/XiaoNrf52Board.cpp | 0 .../helpers/nrf52 => variants/xiao_nrf52}/XiaoNrf52Board.h | 7 +++---- variants/xiao_nrf52/platformio.ini | 3 +-- variants/xiao_nrf52/target.h | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) rename {src/helpers/nrf52 => variants/xiao_nrf52}/XiaoNrf52Board.cpp (100%) rename {src/helpers/nrf52 => variants/xiao_nrf52}/XiaoNrf52Board.h (92%) diff --git a/src/helpers/nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp similarity index 100% rename from src/helpers/nrf52/XiaoNrf52Board.cpp rename to variants/xiao_nrf52/XiaoNrf52Board.cpp diff --git a/src/helpers/nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h similarity index 92% rename from src/helpers/nrf52/XiaoNrf52Board.h rename to variants/xiao_nrf52/XiaoNrf52Board.h index e6c8e7f7..60b9f5bb 100644 --- a/src/helpers/nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -5,20 +5,19 @@ #ifdef XIAO_NRF52 -// LoRa radio module pins for Seeed Xiao-nrf52 +// redefine lora pins if using the S3 variant of SX1262 board #ifdef SX1262_XIAO_S3_VARIANT #undef P_LORA_DIO_1 #undef P_LORA_BUSY #undef P_LORA_RESET #undef P_LORA_NSS + #undef SX126X_RXEN #define P_LORA_DIO_1 D0 #define P_LORA_BUSY D1 #define P_LORA_RESET D2 #define P_LORA_NSS D3 + #define SX126X_RXEN D4 #endif -//#define SX126X_POWER_EN 37 - - class XiaoNrf52Board : public mesh::MainBoard { protected: diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index c4934e04..bba3e632 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -50,8 +50,7 @@ build_flags = ${nrf52840_xiao.build_flags} -D ENV_INCLUDE_INA219=1 build_src_filter = ${nrf52840_xiao.build_src_filter} + - + - + + + +<../variants/xiao_nrf52> debug_tool = jlink upload_protocol = nrfutil diff --git a/variants/xiao_nrf52/target.h b/variants/xiao_nrf52/target.h index eb299006..c8c6a42a 100644 --- a/variants/xiao_nrf52/target.h +++ b/variants/xiao_nrf52/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include From 74818d0594b9b5e45e5ffa9b264080497ba76439 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 4 Jul 2025 13:55:39 +1000 Subject: [PATCH 13/60] fix: change GPS pins Pin 45 and 46 are strapping pins on ESP32-S3, which can lead to unintended consequences on boot. I have amended the pins and added an enable pin as well. --- variants/heltec_v3/platformio.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index c39e9c76..c3bb27b7 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -23,9 +23,9 @@ build_flags = -D ENV_INCLUDE_INA3221=1 -D ENV_INCLUDE_INA219=1 -D ENV_INCLUDE_GPS=1 - -D PIN_GPS_RX=45 - -D PIN_GPS_TX=46 - -D PIN_GPS_EN=-1 + -D PIN_GPS_RX=47 + -D PIN_GPS_TX=48 + -D PIN_GPS_EN=46 build_src_filter = ${esp32_base.build_src_filter} +<../variants/heltec_v3> + From 2bb7e6dad4e1e622577edb4febbc9bdf10a3eb0a Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 4 Jul 2025 14:12:57 +1000 Subject: [PATCH 14/60] fix: heltec v3: change gps enable pin --- variants/heltec_v3/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index c3bb27b7..a4b6bf6f 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -25,7 +25,7 @@ build_flags = -D ENV_INCLUDE_GPS=1 -D PIN_GPS_RX=47 -D PIN_GPS_TX=48 - -D PIN_GPS_EN=46 + -D PIN_GPS_EN=26 build_src_filter = ${esp32_base.build_src_filter} +<../variants/heltec_v3> + From 3d70a0d02cc877c6bba1620696f352c1968e8edd Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 4 Jul 2025 21:33:07 +1000 Subject: [PATCH 15/60] * added RADIOLIB_EXLUDE_'s for faster builds --- platformio.ini | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/platformio.ini b/platformio.ini index 90e7cfb0..d20508a8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -29,6 +29,20 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1 -D LORA_SF=11 -D ENABLE_PRIVATE_KEY_IMPORT=1 ; NOTE: comment these out for more secure firmware -D ENABLE_PRIVATE_KEY_EXPORT=1 + -D RADIOLIB_EXCLUDE_CC1101=1 + -D RADIOLIB_EXCLUDE_RF69=1 + -D RADIOLIB_EXCLUDE_SX1231=1 + -D RADIOLIB_EXCLUDE_SI443X=1 + -D RADIOLIB_EXCLUDE_RFM2X=1 + -D RADIOLIB_EXCLUDE_SX128X=1 + -D RADIOLIB_EXCLUDE_AFSK=1 + -D RADIOLIB_EXCLUDE_AX25=1 + -D RADIOLIB_EXCLUDE_HELLSCHREIBER=1 + -D RADIOLIB_EXCLUDE_MORSE=1 + -D RADIOLIB_EXCLUDE_APRS=1 + -D RADIOLIB_EXCLUDE_BELL=1 + -D RADIOLIB_EXCLUDE_RTTY=1 + -D RADIOLIB_EXCLUDE_SSTV=1 build_src_filter = +<*.cpp> + From 294138804151046a6798f472933df607ebd1b4c1 Mon Sep 17 00:00:00 2001 From: recrof Date: Fri, 4 Jul 2025 15:03:25 +0200 Subject: [PATCH 16/60] initial support for Seeed Studio SenseCap Solar board --- boards/seeed_sensecap_solar.json | 60 +++++++++++++ .../sensecap_solar/SenseCapSolarBoard.cpp | 81 ++++++++++++++++++ variants/sensecap_solar/SenseCapSolarBoard.h | 42 +++++++++ variants/sensecap_solar/platformio.ini | 75 ++++++++++++++++ variants/sensecap_solar/target.cpp | 39 +++++++++ variants/sensecap_solar/target.h | 21 +++++ variants/sensecap_solar/variant.cpp | 69 +++++++++++++++ variants/sensecap_solar/variant.h | 85 +++++++++++++++++++ 8 files changed, 472 insertions(+) create mode 100644 boards/seeed_sensecap_solar.json create mode 100644 variants/sensecap_solar/SenseCapSolarBoard.cpp create mode 100644 variants/sensecap_solar/SenseCapSolarBoard.h create mode 100644 variants/sensecap_solar/platformio.ini create mode 100644 variants/sensecap_solar/target.cpp create mode 100644 variants/sensecap_solar/target.h create mode 100644 variants/sensecap_solar/variant.cpp create mode 100644 variants/sensecap_solar/variant.h diff --git a/boards/seeed_sensecap_solar.json b/boards/seeed_sensecap_solar.json new file mode 100644 index 00000000..d6630d82 --- /dev/null +++ b/boards/seeed_sensecap_solar.json @@ -0,0 +1,60 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_Seeed_XIAO_nRF52840 -DNRF52840_XXAA -DSEEED_XIAO_NRF52840 ", + "f_cpu": "64000000L", + "hwids": [ + [ "0x2886", "0x0059" ] + ], + "mcu": "nrf52840", + "variant": "Seeed_XIAO_nRF52840", + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bsp": { + "name": "adafruit" + }, + "bootloader": { + "settings_addr": "0xFF000" + }, + "usb_product": "XIAO nRF52840" + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "openocd_target": "nrf52.cfg", + "svd_path": "nrf52840.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "Seeed Studio XIAO nRF52840", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "protocol": "nrfutil", + "speed": 115200, + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "cmsis-dap", + "sam-ba", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://wiki.seeedstudio.com/meshtastic_solar_node/", + "vendor": "Seeed Studio" +} \ No newline at end of file diff --git a/variants/sensecap_solar/SenseCapSolarBoard.cpp b/variants/sensecap_solar/SenseCapSolarBoard.cpp new file mode 100644 index 00000000..eb903f59 --- /dev/null +++ b/variants/sensecap_solar/SenseCapSolarBoard.cpp @@ -0,0 +1,81 @@ +#include +#include "SenseCapSolarBoard.h" + +#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 SenseCapSolarBoard::begin() { + // for future use, sub-classes SHOULD call this from their begin() + startup_reason = BD_STARTUP_NORMAL; + +#if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) + Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL); +#endif + + Wire.begin(); + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, HIGH); +#endif + + delay(10); // give sx1262 some time to power up +} + +bool SenseCapSolarBoard::startOTAUpdate(const char* id, char reply[]) { + // Config the peripheral connection with maximum bandwidth + // more SRAM required by SoftDevice + // Note: All config***() function must be called before begin() + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); + + Bluefruit.begin(1, 0); + // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 + Bluefruit.setTxPower(4); + // Set the BLE device name + Bluefruit.setName("SENSECAP_SOLAR_OTA"); + + Bluefruit.Periph.setConnectCallback(connect_callback); + Bluefruit.Periph.setDisconnectCallback(disconnect_callback); + + // To be consistent OTA DFU should be added first if it exists + bledfu.begin(); + + // Set up and start advertising + // Advertising packet + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + Bluefruit.Advertising.addName(); + + /* Start Advertising + - Enable auto advertising if disconnected + - Interval: fast mode = 20 ms, slow mode = 152.5 ms + - Timeout for fast mode is 30 seconds + - Start(timeout) with timeout = 0 will advertise forever (until connected) + + For recommended advertising interval + https://developer.apple.com/library/content/qa/qa1931/_index.html + */ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + + strcpy(reply, "OK - started"); + + return true; +} diff --git a/variants/sensecap_solar/SenseCapSolarBoard.h b/variants/sensecap_solar/SenseCapSolarBoard.h new file mode 100644 index 00000000..b0956644 --- /dev/null +++ b/variants/sensecap_solar/SenseCapSolarBoard.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +class SenseCapSolarBoard : public mesh::MainBoard { +protected: + uint8_t startup_reason; + +public: + void begin(); + uint8_t getStartupReason() const override { return startup_reason; } + +#if defined(P_LORA_TX_LED) + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off + } +#endif + + uint16_t getBattMilliVolts() override { + digitalWrite(VBAT_ENABLE, LOW); + int adcvalue = 0; + analogReadResolution(12); + analogReference(AR_INTERNAL_3_0); + delay(10); + adcvalue = analogRead(BATTERY_PIN); + return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return "Seeed SenseCap Solar"; + } + + void reboot() override { + NVIC_SystemReset(); + } + + bool startOTAUpdate(const char* id, char reply[]) override; +}; diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini new file mode 100644 index 00000000..281f6688 --- /dev/null +++ b/variants/sensecap_solar/platformio.ini @@ -0,0 +1,75 @@ +[SenseCap_Solar] +extends = nrf52_base +board = seeed_sensecap_solar +board_build.ldscript = boards/nrf52840_s140_v7.ld +build_flags = ${nrf52_base.build_flags} + -I lib/nrf52/s140_nrf52_7.3.0_API/include + -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 + -I variants/sensecap_solar + -I src/helpers/nrf52 + -D NRF52_PLATFORM=1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_TX_LED=12 + -D P_LORA_DIO_1=1 + -D P_LORA_RESET=2 + -D P_LORA_BUSY=3 + -D P_LORA_NSS=4 + -D LORA_TX_POWER=22 + -D SX126X_RXEN=5 + -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 ENV_INCLUDE_AHTX0=1 + -D ENV_INCLUDE_BME280=1 + -D ENV_INCLUDE_BMP280=1 + -D ENV_INCLUDE_SHTC3=1 + -D ENV_INCLUDE_LPS22HB=1 + -D ENV_INCLUDE_INA3221=1 + -D ENV_INCLUDE_INA219=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + + + +<../variants/SenseCap_Solar> +debug_tool = jlink +upload_protocol = nrfutil +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 BMP280 Library @ ^2.6.8 + adafruit/Adafruit SHTC3 Library @ ^1.0.1 + arduino-libraries/Arduino_LPS22HB @ ^1.0.2 + +[env:SenseCap_Solar_repeater] +extends = SenseCap_Solar +build_flags = + ${SenseCap_Solar.build_flags} + -D ADVERT_NAME='"SenseCap_Solar Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${SenseCap_Solar.build_src_filter} + +<../examples/simple_repeater/main.cpp> + +[env:SenseCap_Solar_room_server] +extends = SenseCap_Solar +build_flags = + ${SenseCap_Solar.build_flags} + -D ADVERT_NAME='"SenseCap_Solar 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 = ${SenseCap_Solar.build_src_filter} + +<../examples/simple_room_server/main.cpp> \ No newline at end of file diff --git a/variants/sensecap_solar/target.cpp b/variants/sensecap_solar/target.cpp new file mode 100644 index 00000000..6bd7d31a --- /dev/null +++ b/variants/sensecap_solar/target.cpp @@ -0,0 +1,39 @@ +#include +#include "target.h" +#include + +SenseCapSolarBoard 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; + +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/sensecap_solar/target.h b/variants/sensecap_solar/target.h new file mode 100644 index 00000000..63506951 --- /dev/null +++ b/variants/sensecap_solar/target.h @@ -0,0 +1,21 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include + +extern SenseCapSolarBoard 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/sensecap_solar/variant.cpp b/variants/sensecap_solar/variant.cpp new file mode 100644 index 00000000..2b3ca305 --- /dev/null +++ b/variants/sensecap_solar/variant.cpp @@ -0,0 +1,69 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" +#include "nrf.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 2, // D0 P0.02 (A0) GNSS_WAKEUP + 3, // D1 P0.03 (A1) LORA_DIO1 + 28, // D2 P0.28 (A2) LORA_RESET + 29, // D3 P0.29 (A3) LORA_BUSY + 4, // D4 P0.04 (A4/SDA) LORA_CS + 5, // D5 P0.05 (A5/SCL) LORA_SW + 43, // D6 P1.11 (UART_TX) GNSS_TX + 44, // D7 P1.12 (UART_RX) GNSS_RX + 45, // D8 P1.13 (SPI_SCK) LORA_SCK + 46, // D9 P1.14 (SPI_MISO) LORA_MISO + 47, // D10 P1.15 (SPI_MOSI) LORA_MOSI + + // D11-D12 - LED outputs + 15, // D11 P0.15 User LED + 19, // D12 P0.19 Breathing LED + + // D13 - User input + 33, // D13 P1.01 User Button + + // D14-D15 - Grove/NFC interface + 9, // D14 P0.09 NFC1/GROVE_D1 + 10, // D15 P0.10 NFC2/GROVE_D0 + + // D16 - Power management + // 31, // D16 P0.31 VBAT_ADC (Battery voltage) + 31, // D16 P0.31 VBAT_ADC (Battery voltage) + // D17 - GNSS control + 35, // D17 P1.03 GNSS_RESET + + 37, // D18 P1.05 GNSS_ENABLE + 14, // D19 P0.14 BAT_READ + 39, // D20 P1.07 USER_BUTTON + + // + 21, // D21 P0.21 (QSPI_SCK) + 25, // D22 P0.25 (QSPI_CSN) + 20, // D23 P0.20 (QSPI_SIO_0 DI) + 24, // D24 P0.24 (QSPI_SIO_1 DO) + 22, // D25 P0.22 (QSPI_SIO_2 WP) + 23, // D26 P0.23 (QSPI_SIO_3 HOLD) +}; + +void initVariant() { + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, LOW); + + pinMode(BATTERY_PIN, INPUT); + pinMode(VBAT_ENABLE, OUTPUT); + digitalWrite(VBAT_ENABLE, LOW); + + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + + pinMode(LED_GREEN, OUTPUT); + digitalWrite(LED_GREEN, LOW); + + pinMode(LED_BLUE, OUTPUT); + digitalWrite(LED_BLUE, LOW); + + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, HIGH); +} diff --git a/variants/sensecap_solar/variant.h b/variants/sensecap_solar/variant.h new file mode 100644 index 00000000..76494f48 --- /dev/null +++ b/variants/sensecap_solar/variant.h @@ -0,0 +1,85 @@ +#ifndef _SEEED_SENSECAP_SOLAR_H_ +#define _SEEED_SENSECAP_SOLAR_H_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#define PINS_COUNT (33) +#define NUM_DIGITAL_PINS (33) +#define NUM_ANALOG_INPUTS (8) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED (12) +#define LED_PWR (PINS_COUNT) + +#define LED_BUILTIN (PIN_LED) + +#define LED_RED (PINS_COUNT) +#define LED_GREEN (12) +#define LED_BLUE (11) + +#define LED_STATE_ON (1) // State when LED is litted + +// Buttons +#define PIN_BUTTON1 (13) +#define PIN_BUTTON2 (20) + +#define VBAT_ENABLE (19) // Output LOW to enable reading of the BAT voltage. + +// Analog pins +#define BATTERY_PIN (16) // Read the BAT voltage. +#define AREF_VOLTAGE (3.0F) +#define ADC_MULTIPLIER (3.0F) // 1M, 512k divider bridge +#define ADC_RESOLUTION (12) + +// Serial interfaces +#define PIN_SERIAL1_RX (7) +#define PIN_SERIAL1_TX (6) + +// SPI Interfaces +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (9) +#define PIN_SPI_MOSI (10) +#define PIN_SPI_SCK (8) + +// Lora SPI is on SPI0 +#define P_LORA_SCLK PIN_SPI_SCK +#define P_LORA_MISO PIN_SPI_MISO +#define P_LORA_MOSI PIN_SPI_MOSI + +// Wire Interfaces +#define WIRE_INTERFACES_COUNT (1) + +#define PIN_WIRE_SDA (14) +#define PIN_WIRE_SCL (15) + +// GPS L76KB +#define GPS_BAUDRATE 9600 +#define GPS_THREAD_INTERVAL 50 +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_STANDBY (0) +#define GPS_EN (18) + +// QSPI Pins +#define PIN_QSPI_SCK (21) +#define PIN_QSPI_CS (22) +#define PIN_QSPI_IO0 (23) +#define PIN_QSPI_IO1 (24) +#define PIN_QSPI_IO2 (25) +#define PIN_QSPI_IO3 (26) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +#endif \ No newline at end of file From 71255e00f173f7552a210ac96a508e6898dd2902 Mon Sep 17 00:00:00 2001 From: Florent de Lamotte Date: Fri, 4 Jul 2025 15:42:56 +0200 Subject: [PATCH 17/60] stm32 targets: set preamble to 16 --- variants/rak3x72/target.cpp | 2 +- variants/wio-e5-dev/target.cpp | 2 +- variants/wio-e5-mini/target.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/variants/rak3x72/target.cpp b/variants/rak3x72/target.cpp index d7070eae..446783aa 100644 --- a/variants/rak3x72/target.cpp +++ b/variants/rak3x72/target.cpp @@ -38,7 +38,7 @@ bool radio_init() { radio.setRfSwitchTable(rfswitch_pins, rfswitch_table); - int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, STM32WL_TCXO_VOLTAGE, 0); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, STM32WL_TCXO_VOLTAGE, 0); if (status != RADIOLIB_ERR_NONE) { Serial.print("ERROR: radio init failed: "); diff --git a/variants/wio-e5-dev/target.cpp b/variants/wio-e5-dev/target.cpp index 8ccbe384..42e900e4 100644 --- a/variants/wio-e5-dev/target.cpp +++ b/variants/wio-e5-dev/target.cpp @@ -35,7 +35,7 @@ bool radio_init() { radio.setRfSwitchTable(rfswitch_pins, rfswitch_table); - int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, 1.7, 0); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, 1.7, 0); if (status != RADIOLIB_ERR_NONE) { Serial.print("ERROR: radio init failed: "); diff --git a/variants/wio-e5-mini/target.cpp b/variants/wio-e5-mini/target.cpp index 6c045dd5..0e2358b8 100644 --- a/variants/wio-e5-mini/target.cpp +++ b/variants/wio-e5-mini/target.cpp @@ -33,7 +33,7 @@ bool radio_init() { radio.setRfSwitchTable(rfswitch_pins, rfswitch_table); - int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, 1.7, 0); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, 1.7, 0); if (status != RADIOLIB_ERR_NONE) { Serial.print("ERROR: radio init failed: "); From fa481e832b4360593f2a1a8340e2f59944940882 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:40:19 +0200 Subject: [PATCH 18/60] LR's corrected calculation override (instead of SX) and minor changes according to radiolib's wiki --- src/helpers/CustomLR1110.h | 76 ++++++++++++++++++++++++------------- variants/t1000-e/target.cpp | 3 +- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/helpers/CustomLR1110.h b/src/helpers/CustomLR1110.h index d431dac1..e82f48f5 100644 --- a/src/helpers/CustomLR1110.h +++ b/src/helpers/CustomLR1110.h @@ -10,34 +10,58 @@ class CustomLR1110 : public LR1110 { CustomLR1110(Module *mod) : LR1110(mod) { } RadioLibTime_t getTimeOnAir(size_t len) override { - uint32_t symbolLength_us = ((uint32_t)(1000 * 10) << this->spreadingFactor) / (this->bandwidthKhz * 10) ; - uint8_t sfCoeff1_x4 = 17; // (4.25 * 4) - uint8_t sfCoeff2 = 8; - if(this->spreadingFactor == 5 || this->spreadingFactor == 6) { - sfCoeff1_x4 = 25; // 6.25 * 4 - sfCoeff2 = 0; - } - uint8_t sfDivisor = 4*this->spreadingFactor; - if(symbolLength_us >= 16000) { - sfDivisor = 4*(this->spreadingFactor - 2); - } - const int8_t bitsPerCrc = 16; - const int8_t N_symbol_header = this->headerType == RADIOLIB_SX126X_LORA_HEADER_EXPLICIT ? 20 : 0; - - // numerator of equation in section 6.1.4 of SX1268 datasheet v1.1 (might not actually be bitcount, but it has len * 8) - int16_t bitCount = (int16_t) 8 * len + this->crcTypeLoRa * bitsPerCrc - 4 * this->spreadingFactor + sfCoeff2 + N_symbol_header; - if(bitCount < 0) { - bitCount = 0; - } - // add (sfDivisor) - 1 to the numerator to give integer CEIL(...) - uint16_t nPreCodedSymbols = (bitCount + (sfDivisor - 1)) / (sfDivisor); - - // preamble can be 65k, therefore nSymbol_x4 needs to be 32 bit - uint32_t nSymbol_x4 = (this->preambleLengthLoRa + 8) * 4 + sfCoeff1_x4 + nPreCodedSymbols * (this->codingRate + 4) * 4; - - return((symbolLength_us * nSymbol_x4) / 4); + // calculate number of symbols + float N_symbol = 0; + if(this->codingRate <= RADIOLIB_LR11X0_LORA_CR_4_8_SHORT) { + // legacy coding rate - nice and simple + // get SF coefficients + float coeff1 = 0; + int16_t coeff2 = 0; + int16_t coeff3 = 0; + if(this->spreadingFactor < 7) { + // SF5, SF6 + coeff1 = 6.25; + coeff2 = 4*this->spreadingFactor; + coeff3 = 4*this->spreadingFactor; + } else if(this->spreadingFactor < 11) { + // SF7. SF8, SF9, SF10 + coeff1 = 4.25; + coeff2 = 4*this->spreadingFactor + 8; + coeff3 = 4*this->spreadingFactor; + } else { + // SF11, SF12 + coeff1 = 4.25; + coeff2 = 4*this->spreadingFactor + 8; + coeff3 = 4*(this->spreadingFactor - 2); } + // get CRC length + int16_t N_bitCRC = 16; + if(this->crcTypeLoRa == RADIOLIB_LR11X0_LORA_CRC_DISABLED) { + N_bitCRC = 0; + } + + // get header length + int16_t N_symbolHeader = 20; + if(this->headerType == RADIOLIB_LR11X0_LORA_HEADER_IMPLICIT) { + N_symbolHeader = 0; + } + + // calculate number of LoRa preamble symbols - NO! Lora preamble is already in symbols + // uint32_t N_symbolPreamble = (this->preambleLengthLoRa & 0x0F) * (uint32_t(1) << ((this->preambleLengthLoRa & 0xF0) >> 4)); + + // calculate the number of symbols - nope + // N_symbol = (float)N_symbolPreamble + coeff1 + 8.0f + ceilf((float)RADIOLIB_MAX((int16_t)(8 * len + N_bitCRC - coeff2 + N_symbolHeader), (int16_t)0) / (float)coeff3) * (float)(this->codingRate + 4); + // calculate the number of symbols - using only preamblelora because it's already in symbols + N_symbol = (float)preambleLengthLoRa + coeff1 + 8.0f + ceilf((float)RADIOLIB_MAX((int16_t)(8 * len + N_bitCRC - coeff2 + N_symbolHeader), (int16_t)0) / (float)coeff3) * (float)(this->codingRate + 4); + } else { + // long interleaving - not needed for this modem + } + + // get time-on-air in us + return(((uint32_t(1) << this->spreadingFactor) / this->bandwidthKhz) * N_symbol * 1000.0f); +} + bool isReceiving() { uint16_t irq = getIrqStatus(); bool detected = ((irq & LR1110_IRQ_HEADER_VALID) || (irq & LR1110_IRQ_HAS_PREAMBLE)); diff --git a/variants/t1000-e/target.cpp b/variants/t1000-e/target.cpp index f6fb1f04..06509c4f 100644 --- a/variants/t1000-e/target.cpp +++ b/variants/t1000-e/target.cpp @@ -61,7 +61,8 @@ bool radio_init() { return false; // fail } - radio.setCRC(1); + radio.setCRC(2); + radio.explicitHeader(); #ifdef RF_SWITCH_TABLE radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); From aa3c702ffdbe06fce30735813488484d00cb4f71 Mon Sep 17 00:00:00 2001 From: Normunds Gavars Date: Fri, 4 Jul 2025 19:27:11 +0300 Subject: [PATCH 19/60] Read battery voltage on Minewsemi ME25LS01 --- src/helpers/nrf52/MinewsemiME25LS01Board.cpp | 11 ++--- src/helpers/nrf52/MinewsemiME25LS01Board.h | 50 +++++--------------- variants/minewsemi_me25ls01/platformio.ini | 19 -------- variants/minewsemi_me25ls01/variant.cpp | 6 +-- variants/minewsemi_me25ls01/variant.h | 9 +--- 5 files changed, 22 insertions(+), 73 deletions(-) diff --git a/src/helpers/nrf52/MinewsemiME25LS01Board.cpp b/src/helpers/nrf52/MinewsemiME25LS01Board.cpp index f402a0fd..c41a6bc0 100644 --- a/src/helpers/nrf52/MinewsemiME25LS01Board.cpp +++ b/src/helpers/nrf52/MinewsemiME25LS01Board.cpp @@ -8,11 +8,12 @@ void MinewsemiME25LS01Board::begin() { // for future use, sub-classes SHOULD call this from their begin() startup_reason = BD_STARTUP_NORMAL; btn_prev_state = HIGH; + + pinMode(PIN_VBAT_READ, INPUT); sd_power_mode_set(NRF_POWER_MODE_LOWPWR); #ifdef BUTTON_PIN - // pinMode(BATTERY_PIN, INPUT); pinMode(BUTTON_PIN, INPUT); pinMode(LED_PIN, OUTPUT); #endif @@ -31,7 +32,6 @@ void MinewsemiME25LS01Board::begin() { delay(10); // give sx1262 some time to power up } -#if 0 static BLEDfu bledfu; static void connect_callback(uint16_t conn_handle) { @@ -47,7 +47,7 @@ static void disconnect_callback(uint16_t conn_handle, uint8_t reason) { } -bool TrackerT1000eBoard::startOTAUpdate(const char* id, char reply[]) { +bool MinewsemiME25LS01Board::startOTAUpdate(const char* id, char reply[]) { // Config the peripheral connection with maximum bandwidth // more SRAM required by SoftDevice // Note: All config***() function must be called before begin() @@ -58,7 +58,7 @@ bool TrackerT1000eBoard::startOTAUpdate(const char* id, char reply[]) { // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 Bluefruit.setTxPower(4); // Set the BLE device name - Bluefruit.setName("T1000E_OTA"); + Bluefruit.setName("Minewsemi_OTA"); Bluefruit.Periph.setConnectCallback(connect_callback); Bluefruit.Periph.setDisconnectCallback(disconnect_callback); @@ -88,5 +88,4 @@ bool TrackerT1000eBoard::startOTAUpdate(const char* id, char reply[]) { strcpy(reply, "OK - started"); return true; -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/helpers/nrf52/MinewsemiME25LS01Board.h b/src/helpers/nrf52/MinewsemiME25LS01Board.h index 4b8c8293..777606a6 100644 --- a/src/helpers/nrf52/MinewsemiME25LS01Board.h +++ b/src/helpers/nrf52/MinewsemiME25LS01Board.h @@ -16,9 +16,9 @@ #define LR11X0_DIO_AS_RF_SWITCH true #define LR11X0_DIO3_TCXO_VOLTAGE 1.6 -// built-ins -//#define PIN_VBAT_READ 5 -//#define ADC_MULTIPLIER (3 * 1.73 * 1000) +#define PIN_VBAT_READ BATTERY_PIN +#define ADC_MULTIPLIER (1.815f) // dependent on voltage divider resistors. TODO: more accurate battery tracking + class MinewsemiME25LS01Board : public mesh::MainBoard { protected: @@ -28,43 +28,23 @@ protected: public: void begin(); +#define BATTERY_SAMPLES 8 + uint16_t getBattMilliVolts() override { - #ifdef BATTERY_PIN - #ifdef PIN_3V3_EN - digitalWrite(PIN_3V3_EN, HIGH); - #endif - analogReference(AR_INTERNAL_3_0); analogReadResolution(12); - delay(10); - float volts = (analogRead(BATTERY_PIN) * ADC_MULTIPLIER * AREF_VOLTAGE) / 4096; - #ifdef PIN_3V3_EN - digitalWrite(PIN_3V3_EN, LOW); - #endif - analogReference(AR_DEFAULT); // put back to default - analogReadResolution(10); - - return volts * 1000; - #else - return 0; - #endif + 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); } uint8_t getStartupReason() const override { return startup_reason; } const char* getManufacturerName() const override { - return "m25ls01"; - } - - int buttonStateChanged() { - #ifdef BUTTON_PIN - uint8_t v = digitalRead(BUTTON_PIN); - if (v != btn_prev_state) { - btn_prev_state = v; - return (v == LOW) ? 1 : -1; - } - #endif - return 0; + return "Minewsemi"; } void powerOff() override { @@ -81,10 +61,6 @@ public: digitalWrite(BUZZER_EN, LOW); #endif - #ifdef PIN_3V3_EN - digitalWrite(PIN_3V3_EN, LOW); - #endif - #ifdef LED_PIN digitalWrite(LED_PIN, LOW); #endif @@ -108,5 +84,5 @@ public: NVIC_SystemReset(); } -// bool startOTAUpdate(const char* id, char reply[]) override; + bool startOTAUpdate(const char* id, char reply[]) override; }; \ No newline at end of file diff --git a/variants/minewsemi_me25ls01/platformio.ini b/variants/minewsemi_me25ls01/platformio.ini index 4775e289..302e695f 100644 --- a/variants/minewsemi_me25ls01/platformio.ini +++ b/variants/minewsemi_me25ls01/platformio.ini @@ -8,7 +8,6 @@ build_flags = ${nrf52_base.build_flags} -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 lib_ignore = BluetoothOTA - lvgl lib5b4 lib_deps = ${nrf52_base.lib_deps} @@ -67,7 +66,6 @@ build_flags = ${me25ls01.build_flags} ;-D PIN_BUZZER_EN=37 build_src_filter = ${me25ls01.build_src_filter} + - ;+ +<../examples/companion_radio/*.cpp> lib_deps = ${me25ls01.lib_deps} adafruit/RTClib @ ^2.1.3 @@ -92,9 +90,6 @@ build_flags = ${me25ls01.build_flags} -D MAX_NEIGHBOURS=8 -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} - ;+ - ;+ - ;+<../examples/companion_radio/*.cpp> +<../examples/simple_repeater> lib_deps = ${me25ls01.lib_deps} adafruit/RTClib @ ^2.1.3 @@ -121,10 +116,6 @@ build_flags = ${me25ls01.build_flags} -D MAX_NEIGHBOURS=8 -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} - ;+ - ;+ - ;+<../examples/companion_radio/*.cpp> - ;+<../examples/simple_repeater> +<../examples/simple_room_server> lib_deps = ${me25ls01.lib_deps} adafruit/RTClib @ ^2.1.3 @@ -149,11 +140,6 @@ build_flags = ${me25ls01.build_flags} -D MAX_NEIGHBOURS=8 -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} - ;+ - ;+ - ;+<../examples/companion_radio/*.cpp> - ;+<../examples/simple_repeater> - ;+<../examples/simple_room_server> +<../examples/simple_secure_chat/main.cpp> lib_deps = ${me25ls01.lib_deps} adafruit/RTClib @ ^2.1.3 @@ -173,11 +159,6 @@ build_flags = ${me25ls01.build_flags} -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} + - ;+ - ;+ - ;+<../examples/companion_radio/*.cpp> - ;+<../examples/simple_repeater> - ;+<../examples/simple_room_server> +<../examples/companion_radio> lib_deps = ${me25ls01.lib_deps} adafruit/RTClib @ ^2.1.3 diff --git a/variants/minewsemi_me25ls01/variant.cpp b/variants/minewsemi_me25ls01/variant.cpp index 15f30d14..5dbac9d3 100644 --- a/variants/minewsemi_me25ls01/variant.cpp +++ b/variants/minewsemi_me25ls01/variant.cpp @@ -35,7 +35,7 @@ const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = 28, // P0.28 29, // P0.29, 30, // P0.30 - 31, // P0.31, + 31, // P0.31, BATTERY_PIN 32, // P1.00 33, // P1.01, LORA_DIO_1 34, // P1.02 @@ -60,8 +60,8 @@ void initVariant() pinMode(BATTERY_PIN, INPUT); pinMode(PIN_BUTTON1, INPUT); - pinMode(PIN_3V3_EN, OUTPUT); - pinMode(PIN_3V3_ACC_EN, OUTPUT); + // pinMode(PIN_3V3_EN, OUTPUT); + // pinMode(PIN_3V3_ACC_EN, OUTPUT); pinMode(LED_PIN, OUTPUT); pinMode(P_LORA_TX_LED, OUTPUT); diff --git a/variants/minewsemi_me25ls01/variant.h b/variants/minewsemi_me25ls01/variant.h index bbf15ff6..a8bbbe3a 100644 --- a/variants/minewsemi_me25ls01/variant.h +++ b/variants/minewsemi_me25ls01/variant.h @@ -2,26 +2,19 @@ #include "WVariant.h" -//////////////////////////////////////////////////////////////////////////////// // Low frequency clock source - #define USE_LFXO // 32.768 kHz crystal oscillator #define VARIANT_MCK (64000000ul) // #define USE_LFRC // 32.768 kHz RC oscillator // Power -#define PIN_3V3_EN (32 + 5) // P1.6 Power to Sensors -#define PIN_3V3_ACC_EN -1 - -#define BATTERY_PIN (-1) // P0.2/AIN0 +#define BATTERY_PIN (31) #define BATTERY_IMMUTABLE #define ADC_MULTIPLIER (2.0F) #define ADC_RESOLUTION (14) #define BATTERY_SENSE_RES (12) -#define AREF_VOLTAGE (3.0) - // Number of pins #define PINS_COUNT (48) #define NUM_DIGITAL_PINS (48) From d32fa5c00476013f3a12183277a15aaa5d6795e9 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Fri, 4 Jul 2025 21:07:55 +0100 Subject: [PATCH 20/60] Manually restart BLE advertising after disconnect to prevent stack freeze MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced use of `restartOnDisconnect(true)` with explicit (existing) manual re-advertising logic. This avoids Bluetooth stack instability caused by overlapping advertising state, Changes: - Added explicit `Bluefruit.Advertising.stop()` and data clears in `startAdv()` - Disabled automatic restart with `restartOnDisconnect(false)` - Re-advertising now fully handled in `checkRecvFrame()` loop Tested on: iPhone, Android, Windows, and Chrome – confirmed stable reconnects and name visibility. --- src/helpers/nrf52/SerialBLEInterface.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index c6c99d5b..af5b425e 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -18,6 +18,10 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) { } void SerialBLEInterface::startAdv() { + Bluefruit.Advertising.stop(); // always clean restart + Bluefruit.Advertising.clearData(); // clear advertising data + Bluefruit.ScanResponse.clearData(); // clear scan response data + // Advertising packet Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); Bluefruit.Advertising.addTxPower(); @@ -27,7 +31,7 @@ void SerialBLEInterface::startAdv() { // Secondary Scan Response packet (optional) // Since there is no room for 'Name' in Advertising packet - // Bluefruit.ScanResponse.addName(); + Bluefruit.ScanResponse.addName(); /* Start Advertising * - Enable auto advertising if disconnected @@ -38,7 +42,7 @@ void SerialBLEInterface::startAdv() { * For recommended advertising interval * https://developer.apple.com/library/content/qa/qa1931/_index.html */ - Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.restartOnDisconnect(false); // don't restart automatically as already beeing done in checkRecvFrame() Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds From 013787556d8572486fa1268ff163f58ca2d4e308 Mon Sep 17 00:00:00 2001 From: Tobias Schwarz Date: Sat, 5 Jul 2025 16:08:49 +0200 Subject: [PATCH 21/60] add room server role for TBeam SX1262 --- variants/lilygo_tbeam_SX1262/platformio.ini | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini index 8bf2c5ab..eac899f0 100644 --- a/variants/lilygo_tbeam_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -69,4 +69,21 @@ build_src_filter = ${LilyGo_TBeam_SX1262.build_src_filter} +<../examples/simple_repeater> lib_deps = ${LilyGo_TBeam_SX1262.lib_deps} - ${esp32_ota.lib_deps} \ No newline at end of file + ${esp32_ota.lib_deps} + +[env:Tbeam_SX1262_room_server] +extends = LilyGo_TBeam_SX1262 +build_flags = + ${LilyGo_TBeam_SX1262.build_flags} + -D ADVERT_NAME='"Tbeam SX1262 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 = ${LilyGo_TBeam_SX1262.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_TBeam_SX1262.lib_deps} + ${esp32_ota.lib_deps} From 7ea6a9851362546ee181123b810935cc4d26dac6 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 6 Jul 2025 14:07:56 +1200 Subject: [PATCH 22/60] dont show cli data replies on display --- examples/companion_radio/MyMesh.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 879200b7..c141f9d6 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -315,17 +315,24 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe i += tlen; addToOfflineQueue(out_frame, i); + // we only want to show text messages on display, not cli data + bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; + if (_serial->isConnected()) { uint8_t frame[1]; frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); } else { #ifdef DISPLAY_CLASS - ui_task.soundBuzzer(UIEventType::contactMessage); + if(should_display){ + ui_task.soundBuzzer(UIEventType::contactMessage); + } #endif } #ifdef DISPLAY_CLASS - ui_task.newMsg(path_len, from.name, text, offline_queue_len); + if(should_display){ + ui_task.newMsg(path_len, from.name, text, offline_queue_len); + } #endif } From 0914056a09244b850f43595d27303e9d5ecf500f Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 6 Jul 2025 14:16:43 +1200 Subject: [PATCH 23/60] tidy logic for devices with display --- examples/companion_radio/MyMesh.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index c141f9d6..6ddc19d6 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -315,23 +315,20 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe i += tlen; addToOfflineQueue(out_frame, i); - // we only want to show text messages on display, not cli data - bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; - if (_serial->isConnected()) { uint8_t frame[1]; frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); - } else { + } + #ifdef DISPLAY_CLASS - if(should_display){ + // we only want to show text messages on display, not cli data + bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; + if (should_display) { + ui_task.newMsg(path_len, from.name, text, offline_queue_len); + if (!_serial->isConnected()) { ui_task.soundBuzzer(UIEventType::contactMessage); } -#endif - } -#ifdef DISPLAY_CLASS - if(should_display){ - ui_task.newMsg(path_len, from.name, text, offline_queue_len); } #endif } From e47755c8e977007cdd6bb37afeef7f3890a65916 Mon Sep 17 00:00:00 2001 From: recrof Date: Sun, 6 Jul 2025 15:22:51 +0200 Subject: [PATCH 24/60] Seeed SenseCap Solar: invert leds --- variants/sensecap_solar/SenseCapSolarBoard.cpp | 2 +- variants/sensecap_solar/SenseCapSolarBoard.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/variants/sensecap_solar/SenseCapSolarBoard.cpp b/variants/sensecap_solar/SenseCapSolarBoard.cpp index eb903f59..d6c044d1 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.cpp +++ b/variants/sensecap_solar/SenseCapSolarBoard.cpp @@ -30,7 +30,7 @@ void SenseCapSolarBoard::begin() { #ifdef P_LORA_TX_LED pinMode(P_LORA_TX_LED, OUTPUT); - digitalWrite(P_LORA_TX_LED, HIGH); + digitalWrite(P_LORA_TX_LED, LOW); #endif delay(10); // give sx1262 some time to power up diff --git a/variants/sensecap_solar/SenseCapSolarBoard.h b/variants/sensecap_solar/SenseCapSolarBoard.h index b0956644..b1e5f8f1 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.h +++ b/variants/sensecap_solar/SenseCapSolarBoard.h @@ -13,10 +13,10 @@ public: #if defined(P_LORA_TX_LED) void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off } #endif From 5ec89dff5b8fbcb0d9ca9951c8555da322e37cf5 Mon Sep 17 00:00:00 2001 From: Jelle Kalf Date: Sun, 6 Jul 2025 19:52:12 +0200 Subject: [PATCH 25/60] Xiao ESP32 C3: * Fixed pins for mainstream wio sx1262 * Moved previous sx1262 support to _custom version * companion firmware added --- variants/xiao_c3/platformio.ini | 86 +++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 55baf2b8..3643c612 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -1,6 +1,28 @@ [Xiao_esp32_C3] extends = esp32_base board = seeed_xiao_esp32c3 +build_flags = + ${esp32_base.build_flags} + -I variants/xiao_c3 + -D ESP32_CPU_FREQ=80 + ; -D LORA_TX_BOOST_PIN=D3 + ; -D P_LORA_TX_LED=D5 + -D PIN_VBAT_READ=D0 + -D P_LORA_DIO_1=D1 + -D P_LORA_NSS=D4 + -D P_LORA_RESET=RADIOLIB_NC + -D P_LORA_BUSY=D3 + -D PIN_BOARD_SDA=D6 + -D PIN_BOARD_SCL=D7 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/xiao_c3> + +[Xiao_esp32_C3_custom] +extends = esp32_base +board = seeed_xiao_esp32c3 build_flags = ${esp32_base.build_flags} -I variants/xiao_c3 @@ -60,3 +82,67 @@ build_flags = lib_deps = ${Xiao_esp32_C3.lib_deps} ${esp32_ota.lib_deps} + +[env:Xiao_C3_sx1262_companion_radio_ble] +extends = Xiao_esp32_C3 +build_src_filter = ${Xiao_esp32_C3.build_src_filter} + +<../examples/companion_radio> + + +build_flags = + ${Xiao_esp32_C3.build_flags} + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RX_BOOSTED_GAIN=1 + -D LORA_TX_POWER=22 + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 + ; -D BLE_DEBUG_LOGGING=1 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_esp32_C3.lib_deps} + ${esp32_ota.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Xiao_C3_Repeater_sx1262_custom] +extends = Xiao_esp32_C3_custom +build_src_filter = ${Xiao_esp32_C3.build_src_filter} + +<../examples/simple_repeater/main.cpp> +build_flags = + ${Xiao_esp32_C3.build_flags} + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RX_BOOSTED_GAIN=1 + -D LORA_TX_POWER=22 + -D ADVERT_NAME='"Xiao Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_esp32_C3.lib_deps} + ${esp32_ota.lib_deps} + +[env:Xiao_C3_Repeater_sx1268_custom] +extends = Xiao_esp32_C3_custom +build_src_filter = ${Xiao_esp32_C3.build_src_filter} + +<../examples/simple_repeater/main.cpp> +build_flags = + ${Xiao_esp32_C3.build_flags} + -D RADIO_CLASS=CustomSX1268 + -D WRAPPER_CLASS=CustomSX1268Wrapper + -D LORA_TX_POWER=22 + -D ADVERT_NAME='"Xiao Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_esp32_C3.lib_deps} + ${esp32_ota.lib_deps} \ No newline at end of file From 67f9204e88e3583797323384a30d846cdf8a3790 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Mon, 7 Jul 2025 16:36:55 +1200 Subject: [PATCH 26/60] refactor nrf52 ble to use callbacks --- src/helpers/nrf52/SerialBLEInterface.cpp | 105 +++++++++++++---------- src/helpers/nrf52/SerialBLEInterface.h | 13 ++- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index af5b425e..a8c11d97 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -1,6 +1,27 @@ #include "SerialBLEInterface.h" +static SerialBLEInterface* instance; + +void SerialBLEInterface::onConnect(uint16_t connection_handle) { + BLE_DEBUG_PRINTLN("SerialBLEInterface: connected"); + if(instance){ + instance->_isDeviceConnected = true; + // no need to stop advertising on connect, as the ble stack does this automatically + } +} + +void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) { + BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected reason=%d", reason); + if(instance){ + instance->_isDeviceConnected = false; + instance->startAdv(); + } +} + void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) { + + instance = this; + char charpin[20]; sprintf(charpin, "%d", pin_code); @@ -13,12 +34,28 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) { Bluefruit.Security.setMITM(true); Bluefruit.Security.setPIN(charpin); + Bluefruit.Periph.setConnectCallback(onConnect); + Bluefruit.Periph.setDisconnectCallback(onDisconnect); + // To be consistent OTA DFU should be added first if it exists //bledfu.begin(); + + // Configure and start the BLE Uart service + bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); + bleuart.begin(); + } void SerialBLEInterface::startAdv() { - Bluefruit.Advertising.stop(); // always clean restart + + BLE_DEBUG_PRINTLN("SerialBLEInterface: starting advertising"); + + // clean restart if already advertising + if(Bluefruit.Advertising.isRunning()){ + BLE_DEBUG_PRINTLN("SerialBLEInterface: already advertising, stopping to allow clean restart"); + Bluefruit.Advertising.stop(); + } + Bluefruit.Advertising.clearData(); // clear advertising data Bluefruit.ScanResponse.clearData(); // clear scan response data @@ -42,10 +79,25 @@ void SerialBLEInterface::startAdv() { * For recommended advertising interval * https://developer.apple.com/library/content/qa/qa1931/_index.html */ - Bluefruit.Advertising.restartOnDisconnect(false); // don't restart automatically as already beeing done in checkRecvFrame() + Bluefruit.Advertising.restartOnDisconnect(false); // don't restart automatically as we handle it in onDisconnect Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode - Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + +} + +void SerialBLEInterface::stopAdv() { + + BLE_DEBUG_PRINTLN("SerialBLEInterface: stopping advertising"); + + // we only want to stop advertising if it's running, otherwise an invalid state error is logged by ble stack + if(!Bluefruit.Advertising.isRunning()){ + return; + } + + // stop advertising + Bluefruit.Advertising.stop(); + } // ---------- public methods @@ -56,25 +108,14 @@ void SerialBLEInterface::enable() { _isEnabled = true; clearBuffers(); - // Configure and start the BLE Uart service - bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); - bleuart.begin(); - // Start advertising startAdv(); - - checkAdvRestart = false; } void SerialBLEInterface::disable() { _isEnabled = false; - BLE_DEBUG_PRINTLN("SerialBLEInterface::disable"); - - Bluefruit.Advertising.stop(); - - oldDeviceConnected = deviceConnected = false; - checkAdvRestart = false; + stopAdv(); } size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { @@ -83,7 +124,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { return 0; } - if (deviceConnected && len > 0) { + if (_isDeviceConnected && len > 0) { if (send_queue_len >= FRAME_QUEUE_SIZE) { BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); return 0; @@ -119,44 +160,14 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { } else { int len = bleuart.available(); if (len > 0) { - deviceConnected = true; // should probably use the callback to monitor cx bleuart.readBytes(dest, len); BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]); return len; } } - - if (Bluefruit.connected() == 0) deviceConnected = false; - - if (deviceConnected != oldDeviceConnected) { - if (!deviceConnected) { // disconnecting - clearBuffers(); - - BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting..."); - delay(500); // give the bluetooth stack the chance to get things ready - - checkAdvRestart = true; - } else { - BLE_DEBUG_PRINTLN("SerialBLEInterface -> stopping advertising"); - BLE_DEBUG_PRINTLN("SerialBLEInterface -> connecting..."); - // connecting - // do stuff here on connecting - Bluefruit.Advertising.stop(); - checkAdvRestart = false; - } - oldDeviceConnected = deviceConnected; - } - - if (checkAdvRestart) { - if (Bluefruit.connected() == 0) { - BLE_DEBUG_PRINTLN("SerialBLEInterface -> re-starting advertising"); - startAdv(); - } - checkAdvRestart = false; - } return 0; } bool SerialBLEInterface::isConnected() const { - return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0; + return _isDeviceConnected; } diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index d5555f56..12a4f46a 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -5,10 +5,8 @@ class SerialBLEInterface : public BaseSerialInterface { BLEUart bleuart; - bool deviceConnected; - bool oldDeviceConnected; - bool checkAdvRestart; bool _isEnabled; + bool _isDeviceConnected; unsigned long _last_write; struct Frame { @@ -21,18 +19,19 @@ class SerialBLEInterface : public BaseSerialInterface { Frame send_queue[FRAME_QUEUE_SIZE]; void clearBuffers() { send_queue_len = 0; } - void startAdv(); + static void onConnect(uint16_t connection_handle); + static void onDisconnect(uint16_t connection_handle, uint8_t reason); public: SerialBLEInterface() { - deviceConnected = false; - oldDeviceConnected = false; - checkAdvRestart = false; _isEnabled = false; + _isDeviceConnected = false; _last_write = 0; send_queue_len = 0; } + void startAdv(); + void stopAdv(); void begin(const char* device_name, uint32_t pin_code); // BaseSerialInterface methods From d30412bf65e603c3d5a1da01ba2253ee03919624 Mon Sep 17 00:00:00 2001 From: Florent de Lamotte Date: Mon, 7 Jul 2025 10:41:29 +0200 Subject: [PATCH 27/60] xiao_c3: small fixups --- variants/xiao_c3/platformio.ini | 51 +++++++++++++++++---------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 3643c612..b5bf1e16 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -5,15 +5,14 @@ build_flags = ${esp32_base.build_flags} -I variants/xiao_c3 -D ESP32_CPU_FREQ=80 - ; -D LORA_TX_BOOST_PIN=D3 - ; -D P_LORA_TX_LED=D5 -D PIN_VBAT_READ=D0 -D P_LORA_DIO_1=D1 -D P_LORA_NSS=D4 - -D P_LORA_RESET=RADIOLIB_NC + -D P_LORA_RESET=D2 -D P_LORA_BUSY=D3 -D PIN_BOARD_SDA=D6 -D PIN_BOARD_SCL=D7 + -D SX126X_RXEN=D5 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 @@ -52,7 +51,7 @@ build_flags = -D WRAPPER_CLASS=CustomSX1262Wrapper -D SX126X_RX_BOOSTED_GAIN=1 -D LORA_TX_POWER=22 - -D ADVERT_NAME='"Xiao Repeater"' + -D ADVERT_NAME='"Xiao C3 Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' @@ -63,27 +62,7 @@ lib_deps = ${Xiao_esp32_C3.lib_deps} ${esp32_ota.lib_deps} -[env:Xiao_C3_Repeater_sx1268] -extends = Xiao_esp32_C3 -build_src_filter = ${Xiao_esp32_C3.build_src_filter} - +<../examples/simple_repeater/main.cpp> -build_flags = - ${Xiao_esp32_C3.build_flags} - -D RADIO_CLASS=CustomSX1268 - -D WRAPPER_CLASS=CustomSX1268Wrapper - -D LORA_TX_POWER=22 - -D ADVERT_NAME='"Xiao Repeater"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - -D MAX_NEIGHBOURS=8 - ; -D MESH_PACKET_LOGGING=1 - ; -D MESH_DEBUG=1 -lib_deps = - ${Xiao_esp32_C3.lib_deps} - ${esp32_ota.lib_deps} - -[env:Xiao_C3_sx1262_companion_radio_ble] +[env:Xiao_C3_companion_radio_ble] extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} +<../examples/companion_radio> @@ -106,6 +85,28 @@ lib_deps = ${esp32_ota.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Xiao_C3_companion_radio_usb] +extends = Xiao_esp32_C3 +build_src_filter = ${Xiao_esp32_C3.build_src_filter} + +<../examples/companion_radio> + + +build_flags = + ${Xiao_esp32_C3.build_flags} + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RX_BOOSTED_GAIN=1 + -D LORA_TX_POWER=22 + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D OFFLINE_QUEUE_SIZE=256 + ; -D BLE_DEBUG_LOGGING=1 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_esp32_C3.lib_deps} + ${esp32_ota.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Xiao_C3_Repeater_sx1262_custom] extends = Xiao_esp32_C3_custom build_src_filter = ${Xiao_esp32_C3.build_src_filter} From 00ebb090e76f1836b38ed48b4e29041e23849b46 Mon Sep 17 00:00:00 2001 From: jankowski-t Date: Mon, 7 Jul 2025 18:33:31 +0200 Subject: [PATCH 28/60] Migrate Meshadventurer to std_init() --- variants/meshadventurer/target.cpp | 43 ++++-------------------------- 1 file changed, 5 insertions(+), 38 deletions(-) diff --git a/variants/meshadventurer/target.cpp b/variants/meshadventurer/target.cpp index 43e171c0..a1d6dcad 100644 --- a/variants/meshadventurer/target.cpp +++ b/variants/meshadventurer/target.cpp @@ -5,13 +5,8 @@ MeshadventurerBoard 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 - +static SPIClass spi; +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); ESP32RTCClock fallback_clock; @@ -30,40 +25,12 @@ MASensorManager sensors = MASensorManager(nmea); bool radio_init() { fallback_clock.begin(); rtc_clock.begin(Wire); - -#ifdef SX126X_DIO3_TCXO_VOLTAGE - float tcxo = SX126X_DIO3_TCXO_VOLTAGE; -#else - float tcxo = 1.6f; -#endif #if defined(P_LORA_SCLK) - spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + return radio.std_init(&spi); +#else + return radio.std_init(); #endif - int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); - if (status != RADIOLIB_ERR_NONE) { - Serial.print("ERROR: radio init failed: "); - Serial.println(status); - return false; // fail - } - - radio.setCRC(1); - -#if defined(SX126X_RXEN) && defined(SX126X_TXEN) - radio.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); -#endif - -#ifdef SX126X_CURRENT_LIMIT - radio.setCurrentLimit(SX126X_CURRENT_LIMIT); -#endif -#ifdef SX126X_DIO2_AS_RF_SWITCH - radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); -#endif -#ifdef SX126X_RX_BOOSTED_GAIN - radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); -#endif - - return true; // success } uint32_t radio_get_rng_seed() { From 1c7c5ecb2bfed03f7608097fd35fa05f3b4f2883 Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 8 Jul 2025 14:01:31 +0200 Subject: [PATCH 29/60] buzzer: disable when quiet --- src/helpers/ui/buzzer.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/helpers/ui/buzzer.cpp b/src/helpers/ui/buzzer.cpp index c8e5cfcc..ca469d17 100644 --- a/src/helpers/ui/buzzer.cpp +++ b/src/helpers/ui/buzzer.cpp @@ -46,6 +46,13 @@ void genericBuzzer::shutdown() { void genericBuzzer::quiet(bool buzzer_state) { _is_quiet = buzzer_state; +#ifdef PIN_BUZZER_EN + if (_is_quiet) { + digitalWrite(PIN_BUZZER_EN, LOW); + } else { + digitalWrite(PIN_BUZZER_EN, HIGH); + } +#endif } bool genericBuzzer::isQuiet() { From d3831821c7a9d020b1fea6c2ba56281b6f1d12cd Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 8 Jul 2025 22:59:07 +1000 Subject: [PATCH 30/60] * XiaoC3 custom, .ini fixes --- variants/xiao_c3/platformio.ini | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index b5bf1e16..3e4bfdb4 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -109,10 +109,10 @@ lib_deps = [env:Xiao_C3_Repeater_sx1262_custom] extends = Xiao_esp32_C3_custom -build_src_filter = ${Xiao_esp32_C3.build_src_filter} +build_src_filter = ${Xiao_esp32_C3_custom.build_src_filter} +<../examples/simple_repeater/main.cpp> build_flags = - ${Xiao_esp32_C3.build_flags} + ${Xiao_esp32_C3_custom.build_flags} -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D SX126X_RX_BOOSTED_GAIN=1 @@ -125,15 +125,15 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = - ${Xiao_esp32_C3.lib_deps} + ${Xiao_esp32_C3_custom.lib_deps} ${esp32_ota.lib_deps} [env:Xiao_C3_Repeater_sx1268_custom] extends = Xiao_esp32_C3_custom -build_src_filter = ${Xiao_esp32_C3.build_src_filter} +build_src_filter = ${Xiao_esp32_C3_custom.build_src_filter} +<../examples/simple_repeater/main.cpp> build_flags = - ${Xiao_esp32_C3.build_flags} + ${Xiao_esp32_C3_custom.build_flags} -D RADIO_CLASS=CustomSX1268 -D WRAPPER_CLASS=CustomSX1268Wrapper -D LORA_TX_POWER=22 @@ -145,5 +145,5 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = - ${Xiao_esp32_C3.lib_deps} + ${Xiao_esp32_C3_custom.lib_deps} ${esp32_ota.lib_deps} \ No newline at end of file From 7fb7b69bbc4130ad5b787a2b7997d7132df15504 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 7 Jul 2025 14:21:19 +1000 Subject: [PATCH 31/60] * first cut of new simple_sensor sketch --- examples/simple_sensor/SensorMesh.cpp | 590 ++++++++++++++++++++++++++ examples/simple_sensor/SensorMesh.h | 134 ++++++ examples/simple_sensor/UITask.cpp | 114 +++++ examples/simple_sensor/UITask.h | 19 + examples/simple_sensor/main.cpp | 128 ++++++ src/helpers/AdvertDataHelpers.h | 3 +- variants/heltec_v3/platformio.ini | 17 + 7 files changed, 1004 insertions(+), 1 deletion(-) create mode 100644 examples/simple_sensor/SensorMesh.cpp create mode 100644 examples/simple_sensor/SensorMesh.h create mode 100644 examples/simple_sensor/UITask.cpp create mode 100644 examples/simple_sensor/UITask.h create mode 100644 examples/simple_sensor/main.cpp diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp new file mode 100644 index 00000000..3b5b04cc --- /dev/null +++ b/examples/simple_sensor/SensorMesh.cpp @@ -0,0 +1,590 @@ +#include "SensorMesh.h" + +/* ------------------------------ Config -------------------------------- */ + +#ifndef LORA_FREQ + #define LORA_FREQ 915.0 +#endif +#ifndef LORA_BW + #define LORA_BW 250 +#endif +#ifndef LORA_SF + #define LORA_SF 10 +#endif +#ifndef LORA_CR + #define LORA_CR 5 +#endif +#ifndef LORA_TX_POWER + #define LORA_TX_POWER 20 +#endif + +#ifndef ADVERT_NAME + #define ADVERT_NAME "sensor" +#endif +#ifndef ADVERT_LAT + #define ADVERT_LAT 0.0 +#endif +#ifndef ADVERT_LON + #define ADVERT_LON 0.0 +#endif + +#ifndef ADMIN_PASSWORD + #define ADMIN_PASSWORD "password" +#endif + +#ifndef SERVER_RESPONSE_DELAY + #define SERVER_RESPONSE_DELAY 300 +#endif + +#ifndef TXT_ACK_DELAY + #define TXT_ACK_DELAY 200 +#endif + +#ifndef SENSOR_READ_INTERVAL_SECS + #define SENSOR_READ_INTERVAL_SECS 60 +#endif + +/* ------------------------------ Code -------------------------------- */ + +#define REQ_TYPE_GET_STATUS 0x01 +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 + +#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ + +#define CLI_REPLY_DELAY_MILLIS 1000 + +#define LAZY_CONTACTS_WRITE_DELAY 5000 + +static File openAppend(FILESYSTEM* _fs, const char* fname) { + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return _fs->open(fname, FILE_O_WRITE); + #elif defined(RP2040_PLATFORM) + return _fs->open(fname, "a"); + #else + return _fs->open(fname, "a", true); + #endif +} + +static File openWrite(FILESYSTEM* _fs, const char* filename) { + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _fs->remove(filename); + return _fs->open(filename, FILE_O_WRITE); + #elif defined(RP2040_PLATFORM) + return _fs->open(filename, "w"); + #else + return _fs->open(filename, "w", true); + #endif +} + +void SensorMesh::loadContacts() { + num_contacts = 0; + if (_fs->exists("/s_contacts")) { + #if defined(RP2040_PLATFORM) + File file = _fs->open("/s_contacts", "r"); + #else + File file = _fs->open("/s_contacts"); + #endif + if (file) { + bool full = false; + while (!full) { + ContactInfo c; + uint8_t pub_key[32]; + uint8_t unused; + + bool success = (file.read(pub_key, 32) == 32); + success = success && (file.read(&c.type, 1) == 1); + success = success && (file.read(&c.flags, 1) == 1); + success = success && (file.read(&unused, 1) == 1); + 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); + c.last_timestamp = 0; // transient + c.last_activity = 0; + + if (!success) break; // EOF + + c.id = mesh::Identity(pub_key); + if (num_contacts < MAX_CONTACTS) { + contacts[num_contacts++] = c; + } else { + full = true; + } + } + file.close(); + } + } +} + +void SensorMesh::saveContacts() { + File file = openWrite(_fs, "/s_contacts"); + if (file) { + uint8_t unused = 0; + + for (int i = 0; i < num_contacts; i++) { + auto c = &contacts[i]; + if (c->type == 0) continue; // don't persist guest contacts + + bool success = (file.write(c->id.pub_key, 32) == 32); + success = success && (file.write(&c->type, 1) == 1); + success = success && (file.write(&c->flags, 1) == 1); + success = success && (file.write(&unused, 1) == 1); + success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1); + success = success && (file.write(c->out_path, 64) == 64); + success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); + + if (!success) break; // write failed + } + file.close(); + } +} + +int SensorMesh::handleRequest(ContactInfo& 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 + memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') + + switch (payload[0]) { + case REQ_TYPE_GET_TELEMETRY_DATA: { + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions for admin or guest + + uint8_t tlen = telemetry.getSize(); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + return 4 + tlen; // reply_len + } + } + return 0; // unknown command +} + +mesh::Packet* SensorMesh::createSelfAdvert() { + uint8_t app_data[MAX_ADVERT_DATA_SIZE]; + uint8_t app_data_len; + { + AdvertDataBuilder builder(ADV_TYPE_SENSOR, _prefs.node_name, _prefs.node_lat, _prefs.node_lon); + app_data_len = builder.encodeTo(app_data); + } + + return createAdvert(self_id, app_data, app_data_len); +} + +ContactInfo* SensorMesh::putContact(const mesh::Identity& id) { + uint32_t min_time = 0xFFFFFFFF; + ContactInfo* oldest = &contacts[MAX_CONTACTS - 1]; + for (int i = 0; i < num_contacts; i++) { + if (id.matches(contacts[i].id)) return &contacts[i]; // already known + if (!contacts[i].isAdmin() && contacts[i].last_activity < min_time) { + oldest = &contacts[i]; + min_time = oldest->last_activity; + } + } + + ContactInfo* c; + if (num_contacts < MAX_CONTACTS) { + c = &contacts[num_contacts++]; + } else { + c = oldest; // evict least active contact + } + memset(c, 0, sizeof(*c)); + c->id = id; + c->out_path_len = -1; // initially out_path is unknown + return c; +} + +void SensorMesh::alertIfLow(Trigger& t, float value, float threshold, const char* text) { + if (value < threshold) { + if (!t.triggered) { + t.triggered = true; + t.time = getRTCClock()->getCurrentTime(); + sendAlert(text); + } + } else { + if (t.triggered) { + t.triggered = false; + // TODO: apply debounce logic + } + } +} + +void SensorMesh::alertIfHigh(Trigger& t, float value, float threshold, const char* text) { + if (value > threshold) { + if (!t.triggered) { + t.triggered = true; + t.time = getRTCClock()->getCurrentTime(); + sendAlert(text); + } + } else { + if (t.triggered) { + t.triggered = false; + // TODO: apply debounce logic + } + } +} + +float SensorMesh::getAirtimeBudgetFactor() const { + return _prefs.airtime_factor; +} + +bool SensorMesh::allowPacketForward(const mesh::Packet* packet) { + if (_prefs.disable_fwd) return false; + if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; + return true; +} + +int SensorMesh::calcRxDelay(float score, uint32_t air_time) const { + if (_prefs.rx_delay_base <= 0.0f) return 0; + return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); +} + +uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) { + uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor); + return getRNG()->nextInt(0, 6)*t; +} +uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) { + uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); + return getRNG()->nextInt(0, 6)*t; +} +int SensorMesh::getInterferenceThreshold() const { + return _prefs.interference_threshold; +} +int SensorMesh::getAGCResetInterval() const { + return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds +} + +void SensorMesh::onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) { + if (type == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) + uint32_t timestamp; + memcpy(×tamp, data, 4); + + bool is_admin; + data[len] = 0; // ensure null terminator + if (strcmp((char *) &data[4], _prefs.password) == 0) { // check for valid password + is_admin = true; + } else if (strcmp((char *) &data[4], _prefs.guest_password) == 0) { // check guest password + is_admin = false; + } else { + #if MESH_DEBUG + MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); + #endif + return; + } + + auto client = putContact(sender); // add to contacts (if not already known) + if (timestamp <= client->last_timestamp) { + MESH_DEBUG_PRINTLN("Possible login replay attack!"); + return; // FATAL: client table is full -OR- replay attack + } + + MESH_DEBUG_PRINTLN("Login success!"); + client->last_timestamp = timestamp; + client->last_activity = getRTCClock()->getCurrentTime(); + client->type = is_admin ? 1 : 0; + self_id.calcSharedSecret(client->shared_secret, client->id); // calc ECDH shared secret + + if (is_admin) { + // only need to saveContacts() if this is an admin + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + } + + uint32_t now = getRTCClock()->getCurrentTimeUnique(); + memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + reply_data[4] = RESP_SERVER_LOGIN_OK; + reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) + reply_data[6] = client->type; + reply_data[7] = 0; // FUTURE: reserved + getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness + + if (packet->isRouteFlood()) { + // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response + mesh::Packet* path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, 12); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + } else { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 12); + if (reply) { + if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); + } else { + sendFlood(reply, SERVER_RESPONSE_DELAY); + } + } + } + } +} + +int SensorMesh::searchPeersByHash(const uint8_t* hash) { + int n = 0; + for (int i = 0; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) { + if (contacts[i].id.isHashMatch(hash)) { + matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) + } + } + return n; +} + +void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { + int i = matching_peer_indexes[peer_idx]; + if (i >= 0 && i < num_contacts) { + // lookup pre-calculated shared_secret + memcpy(dest_secret, contacts[i].shared_secret, PUB_KEY_SIZE); + } else { + MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i); + } +} + +void SensorMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) { + mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl +#if 0 + // if this a zero hop advert, add it to neighbours + if (packet->path_len == 0) { + AdvertDataParser parser(app_data, app_data_len); + if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters + putNeighbour(id, timestamp, packet->getSNR()); + } + } +#endif +} + +void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) { + int i = matching_peer_indexes[sender_idx]; + if (i < 0 || i >= num_contacts) { + MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i); + return; + } + + ContactInfo& from = contacts[i]; + + if (type == PAYLOAD_TYPE_REQ) { // request (from a known contact) + uint32_t timestamp; + memcpy(×tamp, data, 4); + + if (timestamp > from.last_timestamp) { // prevent replay attacks + int reply_len = handleRequest(from, timestamp, &data[4], len - 4); + if (reply_len == 0) return; // invalid command + + from.last_timestamp = timestamp; + from.last_activity = getRTCClock()->getCurrentTime(); + + if (packet->isRouteFlood()) { + // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response + mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + } else { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, reply_data, reply_len); + if (reply) { + if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY); + } else { + sendFlood(reply, SERVER_RESPONSE_DELAY); + } + } + } + } else { + MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); + } + } else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && from.isAdmin()) { // a CLI command + uint32_t sender_timestamp; + memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) + uint flags = (data[4] >> 2); // message attempt number, and other flags + + if (!(flags == TXT_TYPE_CLI_DATA)) { + MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags); + } else if (sender_timestamp > from.last_timestamp) { // prevent replay attacks + from.last_timestamp = sender_timestamp; + from.last_activity = getRTCClock()->getCurrentTime(); + + // len can be > original length, but 'text' will be padded with zeroes + data[len] = 0; // need to make a C string again, with null terminator + + uint8_t temp[166]; + const char *command = (const char *) &data[5]; + char *reply = (char *) &temp[5]; + _cli.handleCommand(sender_timestamp, command, reply); + + int text_len = strlen(reply); + if (text_len > 0) { + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + if (timestamp == sender_timestamp) { + // WORKAROUND: the two timestamps need to be different, in the CLI view + timestamp++; + } + memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique + temp[4] = (TXT_TYPE_CLI_DATA << 2); + + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len); + if (reply) { + if (from.out_path_len < 0) { + sendFlood(reply, CLI_REPLY_DELAY_MILLIS); + } else { + sendDirect(reply, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS); + } + } + } + } else { + MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); + } + } +} + +bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { + int i = matching_peer_indexes[sender_idx]; + if (i < 0 || i >= num_contacts) { + MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i); + return false; + } + + ContactInfo& from = contacts[i]; + + MESH_DEBUG_PRINTLN("PATH to contact, path_len=%d", (uint32_t) path_len); + // NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path. + // FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?) + memcpy(from.out_path, path, from.out_path_len = path_len); // store a copy of path, for sendDirect() + from.last_activity = getRTCClock()->getCurrentTime(); + + if (from.isAdmin()) { + // only need to saveContacts() if this is an admin + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + } + + // NOTE: no reciprocal path send!! + return false; +} + +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, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) +{ + num_contacts = 0; + next_local_advert = next_flood_advert = 0; + dirty_contacts_expiry = 0; + last_read_time = 0; + + // defaults + memset(&_prefs, 0, sizeof(_prefs)); + _prefs.airtime_factor = 1.0; // one half + _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; + _prefs.tx_delay_factor = 0.5f; // was 0.25f + StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); + _prefs.node_lat = ADVERT_LAT; + _prefs.node_lon = ADVERT_LON; + StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password)); + _prefs.freq = LORA_FREQ; + _prefs.sf = LORA_SF; + _prefs.bw = LORA_BW; + _prefs.cr = LORA_CR; + _prefs.tx_power_dbm = LORA_TX_POWER; + _prefs.advert_interval = 1; // default to 2 minutes for NEW installs + _prefs.flood_advert_interval = 3; // 3 hours + _prefs.disable_fwd = true; + _prefs.flood_max = 64; + _prefs.interference_threshold = 0; // disabled +} + +void SensorMesh::begin(FILESYSTEM* fs) { + mesh::Mesh::begin(); + _fs = fs; + // load persisted prefs + _cli.loadPrefs(_fs); + + loadContacts(); + + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + radio_set_tx_power(_prefs.tx_power_dbm); + + updateAdvertTimer(); + updateFloodAdvertTimer(); +} + +bool SensorMesh::formatFileSystem() { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return InternalFS.format(); +#elif defined(RP2040_PLATFORM) + return LittleFS.format(); +#elif defined(ESP32) + return SPIFFS.format(); +#else + #error "need to implement file system erase" + return false; +#endif +} + +void SensorMesh::sendSelfAdvertisement(int delay_millis) { + mesh::Packet* pkt = createSelfAdvert(); + if (pkt) { + sendFlood(pkt, delay_millis); + } else { + MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!"); + } +} + +void SensorMesh::updateAdvertTimer() { + if (_prefs.advert_interval > 0) { // schedule local advert timer + next_local_advert = futureMillis( ((uint32_t)_prefs.advert_interval) * 2 * 60 * 1000); + } else { + next_local_advert = 0; // stop the timer + } +} +void SensorMesh::updateFloodAdvertTimer() { + if (_prefs.flood_advert_interval > 0) { // schedule flood advert timer + next_flood_advert = futureMillis( ((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000); + } else { + next_flood_advert = 0; // stop the timer + } +} + +void SensorMesh::setTxPower(uint8_t power_dbm) { + radio_set_tx_power(power_dbm); +} + +void SensorMesh::loop() { + mesh::Mesh::loop(); + + if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { + mesh::Packet* pkt = createSelfAdvert(); + if (pkt) sendFlood(pkt); + + updateFloodAdvertTimer(); // schedule next flood advert + updateAdvertTimer(); // also schedule local advert (so they don't overlap) + } else if (next_local_advert && millisHasNowPassed(next_local_advert)) { + mesh::Packet* pkt = createSelfAdvert(); + if (pkt) sendZeroHop(pkt); + + updateAdvertTimer(); // schedule next local advert + } + + uint32_t curr = getRTCClock()->getCurrentTime(); + if (curr >= last_read_time + SENSOR_READ_INTERVAL_SECS) { + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions + + checkForAlerts(); + + // save telemetry to time-series datastore + File file = openAppend(_fs, "/s_data"); + if (file) { + file.write((uint8_t *)&curr, 4); // start record with RTC timestamp + uint8_t tlen = telemetry.getSize(); + file.write(&tlen, 1); + file.write(telemetry.getBuffer(), tlen); + uint8_t zero = 0; + while (tlen < MAX_PACKET_PAYLOAD - 4) { // pad with zeroes, for fixed record length + file.write(&zero, 1); + tlen++; + } + file.close(); + } + + last_read_time = curr; + } + + // is there are pending dirty contacts write needed? + if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { + saveContacts(); + dirty_contacts_expiry = 0; + } +} diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h new file mode 100644 index 00000000..051d6644 --- /dev/null +++ b/examples/simple_sensor/SensorMesh.h @@ -0,0 +1,134 @@ +#pragma once + +#include // needed for PlatformIO +#include + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#include +#elif defined(RP2040_PLATFORM) +#include +#elif defined(ESP32) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct ContactInfo { + mesh::Identity id; + uint8_t type; // 1 = admin, 0 = guest + uint8_t flags; + int8_t out_path_len; + uint8_t out_path[MAX_PATH_SIZE]; + uint8_t shared_secret[PUB_KEY_SIZE]; + uint32_t last_timestamp; // by THEIR clock (transient) + uint32_t last_activity; // by OUR clock (transient) + + bool isAdmin() const { return type != 0; } +}; + +#ifndef FIRMWARE_BUILD_DATE + #define FIRMWARE_BUILD_DATE "2 Jul 2025" +#endif + +#ifndef FIRMWARE_VERSION + #define FIRMWARE_VERSION "v1.7.2" +#endif + +#define FIRMWARE_ROLE "sensor" + +#ifndef MAX_CONTACTS + #define MAX_CONTACTS 32 +#endif + +#define MAX_SEARCH_RESULTS 8 + +class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { +public: + SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); + void begin(FILESYSTEM* fs); + CommonCLI* getCLI() { return &_cli; } + void loop(); + + // CommonCLI callbacks + const char* getFirmwareVer() override { return FIRMWARE_VERSION; } + const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } + const char* getRole() override { return FIRMWARE_ROLE; } + const char* getNodeName() { return _prefs.node_name; } + NodePrefs* getNodePrefs() { return &_prefs; } + void savePrefs() override { _cli.savePrefs(_fs); } + bool formatFileSystem() override; + void sendSelfAdvertisement(int delay_millis) override; + void updateAdvertTimer() override; + void updateFloodAdvertTimer() override; + void setLoggingOn(bool enable) override { } + void eraseLogFile() override { } + void dumpLogFile() override { } + void setTxPower(uint8_t power_dbm) override; + void formatNeighborsReply(char *reply) override { + strcpy(reply, "not supported"); + } + const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; } + void clearStats() override { } + +protected: + // telemetry data queries + float getVoltage(uint8_t channel) { return 0.0f; } // TODO: extract from curr telemetry buffer + + // alerts + struct Trigger { + bool triggered; + uint32_t time; + + Trigger() { triggered = false; time = 0; } + }; + + void alertIfLow(Trigger& t, float value, float threshold, const char* text); + void alertIfHigh(Trigger& t, float value, float threshold, const char* text); + + virtual void checkForAlerts() = 0; // for app to implement + + // Mesh overrides + float getAirtimeBudgetFactor() const override; + bool allowPacketForward(const mesh::Packet* packet) override; + int calcRxDelay(float score, uint32_t air_time) const override; + uint32_t getRetransmitDelay(const mesh::Packet* packet) override; + uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; + int getInterferenceThreshold() const override; + int getAGCResetInterval() const override; + void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override; + int searchPeersByHash(const uint8_t* hash) override; + void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; + void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len); + void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override; + bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; + +private: + FILESYSTEM* _fs; + unsigned long next_local_advert, next_flood_advert; + NodePrefs _prefs; + CommonCLI _cli; + uint8_t reply_data[MAX_PACKET_PAYLOAD]; + ContactInfo contacts[MAX_CONTACTS]; + int num_contacts; + unsigned long dirty_contacts_expiry; + CayenneLPP telemetry; + uint32_t last_read_time; + int matching_peer_indexes[MAX_SEARCH_RESULTS]; + + void loadContacts(); + void saveContacts(); + int handleRequest(ContactInfo& sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); + mesh::Packet* createSelfAdvert(); + ContactInfo* putContact(const mesh::Identity& id); + + void sendAlert(const char* text) { } // TODO + +}; diff --git a/examples/simple_sensor/UITask.cpp b/examples/simple_sensor/UITask.cpp new file mode 100644 index 00000000..0694bc3c --- /dev/null +++ b/examples/simple_sensor/UITask.cpp @@ -0,0 +1,114 @@ +#include "UITask.h" +#include +#include + +#define AUTO_OFF_MILLIS 20000 // 20 seconds +#define BOOT_SCREEN_MILLIS 4000 // 4 seconds + +// 'meshcore', 128x13px +static const uint8_t meshcore_logo [] PROGMEM = { + 0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, + 0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe, + 0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc, + 0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00, + 0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00, + 0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0, + 0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00, + 0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00, + 0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, +}; + +void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { + _prevBtnState = HIGH; + _auto_off = millis() + AUTO_OFF_MILLIS; + _node_prefs = node_prefs; + _display->turnOn(); + + // strip off dash and commit hash by changing dash to null terminator + // e.g: v1.2.3-abcdef -> v1.2.3 + char *version = strdup(firmware_version); + char *dash = strchr(version, '-'); + if(dash){ + *dash = 0; + } + + // v1.2.3 (1 Jan 2025) + sprintf(_version_info, "%s (%s)", version, build_date); +} + +void UITask::renderCurrScreen() { + char tmp[80]; + if (millis() < BOOT_SCREEN_MILLIS) { // boot screen + // meshcore logo + _display->setColor(DisplayDriver::BLUE); + int logoWidth = 128; + _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); + + // version info + _display->setColor(DisplayDriver::LIGHT); + _display->setTextSize(1); + uint16_t versionWidth = _display->getTextWidth(_version_info); + _display->setCursor((_display->width() - versionWidth) / 2, 22); + _display->print(_version_info); + + // node type + const char* node_type = "< Sensor >"; + uint16_t typeWidth = _display->getTextWidth(node_type); + _display->setCursor((_display->width() - typeWidth) / 2, 35); + _display->print(node_type); + } else { // home screen + // node name + _display->setCursor(0, 0); + _display->setTextSize(1); + _display->setColor(DisplayDriver::GREEN); + _display->print(_node_prefs->node_name); + + // freq / sf + _display->setCursor(0, 20); + _display->setColor(DisplayDriver::YELLOW); + sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); + _display->print(tmp); + + // bw / cr + _display->setCursor(0, 30); + sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); + _display->print(tmp); + } +} + +void UITask::loop() { +#ifdef PIN_USER_BTN + if (millis() >= _next_read) { + int btnState = digitalRead(PIN_USER_BTN); + if (btnState != _prevBtnState) { + if (btnState == LOW) { // pressed? + if (_display->isOn()) { + // TODO: any action ? + } else { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + } + _prevBtnState = btnState; + } + _next_read = millis() + 200; // 5 reads per second + } +#endif + + if (_display->isOn()) { + if (millis() >= _next_refresh) { + _display->startFrame(); + renderCurrScreen(); + _display->endFrame(); + + _next_refresh = millis() + 1000; // refresh every second + } + if (millis() > _auto_off) { + _display->turnOff(); + } + } +} diff --git a/examples/simple_sensor/UITask.h b/examples/simple_sensor/UITask.h new file mode 100644 index 00000000..a27259f1 --- /dev/null +++ b/examples/simple_sensor/UITask.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +class UITask { + DisplayDriver* _display; + unsigned long _next_read, _next_refresh, _auto_off; + int _prevBtnState; + NodePrefs* _node_prefs; + char _version_info[32]; + + void renderCurrScreen(); +public: + UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } + void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); + + void loop(); +}; \ No newline at end of file diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp new file mode 100644 index 00000000..63122e9a --- /dev/null +++ b/examples/simple_sensor/main.cpp @@ -0,0 +1,128 @@ +#include "SensorMesh.h" + +#ifdef DISPLAY_CLASS + #include "UITask.h" + static UITask ui_task(display); +#endif + +class MyMesh : public SensorMesh { +public: + MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) + : SensorMesh(board, radio, ms, rng, rtc, tables) { } + +protected: + /* ========================== custom alert logic here ========================== */ + Trigger low_batt; + + void checkForAlerts() override { + alertIfLow(low_batt, getVoltage(TELEM_CHANNEL_SELF), 3.4f, "Battery low!"); + // alertIf ... + // alertIf ... + } + /* ============================================================================= */ +}; + +StdRNG fast_rng; +SimpleMeshTables tables; + +MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables); + +void halt() { + while (1) ; +} + +static char command[80]; + +void setup() { + Serial.begin(115200); + delay(1000); + + board.begin(); + +#ifdef DISPLAY_CLASS + if (display.begin()) { + display.startFrame(); + display.print("Please wait..."); + display.endFrame(); + } +#endif + + if (!radio_init()) { halt(); } + + fast_rng.begin(radio_get_rng_seed()); + + FILESYSTEM* fs; +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + InternalFS.begin(); + fs = &InternalFS; + IdentityStore store(InternalFS, ""); +#elif defined(ESP32) + SPIFFS.begin(true); + fs = &SPIFFS; + IdentityStore store(SPIFFS, "/identity"); +#elif defined(RP2040_PLATFORM) + LittleFS.begin(); + fs = &LittleFS; + IdentityStore store(LittleFS, "/identity"); + store.begin(); +#else + #error "need to define filesystem" +#endif + if (!store.load("_main", the_mesh.self_id)) { + MESH_DEBUG_PRINTLN("Generating new keypair"); + the_mesh.self_id = radio_new_identity(); // create new random identity + int count = 0; + while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00 || the_mesh.self_id.pub_key[0] == 0xFF)) { // reserved id hashes + the_mesh.self_id = radio_new_identity(); count++; + } + store.save("_main", the_mesh.self_id); + } + + Serial.print("Sensor ID: "); + mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); + + command[0] = 0; + + sensors.begin(); + + the_mesh.begin(fs); + +#ifdef DISPLAY_CLASS + ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); +#endif + + // send out initial Advertisement to the mesh + the_mesh.sendSelfAdvertisement(16000); +} + +void loop() { + int len = strlen(command); + while (Serial.available() && len < sizeof(command)-1) { + char c = Serial.read(); + if (c != '\n') { + command[len++] = c; + command[len] = 0; + } + Serial.print(c); + } + if (len == sizeof(command)-1) { // command buffer full + command[sizeof(command)-1] = '\r'; + } + + if (len > 0 && command[len - 1] == '\r') { // received complete line + command[len - 1] = 0; // replace newline with C string null terminator + char reply[160]; + the_mesh.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + if (reply[0]) { + Serial.print(" -> "); Serial.println(reply); + } + + command[0] = 0; // reset command buffer + } + + the_mesh.loop(); + sensors.loop(); +#ifdef DISPLAY_CLASS + ui_task.loop(); +#endif +} diff --git a/src/helpers/AdvertDataHelpers.h b/src/helpers/AdvertDataHelpers.h index 6da18802..abe14cbd 100644 --- a/src/helpers/AdvertDataHelpers.h +++ b/src/helpers/AdvertDataHelpers.h @@ -8,7 +8,8 @@ #define ADV_TYPE_CHAT 1 #define ADV_TYPE_REPEATER 2 #define ADV_TYPE_ROOM 3 -//FUTURE: 4..15 +#define ADV_TYPE_SENSOR 4 +//FUTURE: 5..15 #define ADV_LATLON_MASK 0x10 #define ADV_FEAT1_MASK 0x20 // FUTURE diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index a4b6bf6f..edc683d2 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -198,3 +198,20 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter} lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 + +[env:Heltec_WSL3_sensor] +extends = Heltec_lora32_v3 +build_flags = + ${Heltec_lora32_v3.build_flags} + -D ADVERT_NAME='"Heltec 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 = ${Heltec_lora32_v3.build_src_filter} + +<../examples/simple_sensor> +lib_deps = + ${Heltec_lora32_v3.lib_deps} + ${esp32_ota.lib_deps} + From 810b1f8fe7a9d0d90a63fc8b775a38897f7f0d96 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 7 Jul 2025 20:41:28 +1000 Subject: [PATCH 32/60] * Mesh::onAnonDataRecv() slight optimisation, so that shared-secret calc doesn't need to be repeated * SensporMesh: req_type now optionally encoded in anon_req payload (so can send various requests without a prior login) --- examples/simple_repeater/main.cpp | 6 +- examples/simple_room_server/main.cpp | 6 +- examples/simple_sensor/SensorMesh.cpp | 111 +++++++++++++++----------- examples/simple_sensor/SensorMesh.h | 7 +- src/Mesh.cpp | 2 +- src/Mesh.h | 4 +- 6 files changed, 76 insertions(+), 60 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 71c9f24a..d8f1cde1 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -149,7 +149,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { oldest->id = id; oldest->out_path_len = -1; // initially out_path is unknown oldest->last_timestamp = 0; - self_id.calcSharedSecret(oldest->secret, id); // calc ECDH shared secret return oldest; } @@ -341,8 +340,8 @@ protected: return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } - void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override { - if (type == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) + void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override { + if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) uint32_t timestamp; memcpy(×tamp, data, 4); @@ -369,6 +368,7 @@ protected: client->last_timestamp = timestamp; client->last_activity = getRTCClock()->getCurrentTime(); client->is_admin = is_admin; + memcpy(client->secret, secret, PUB_KEY_SIZE); uint32_t now = getRTCClock()->getCurrentTimeUnique(); memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index c394f3e5..b0ee472f 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -188,7 +188,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { newClient->id = id; newClient->out_path_len = -1; // initially out_path is unknown newClient->last_timestamp = 0; - self_id.calcSharedSecret(newClient->secret, id); // calc ECDH shared secret return newClient; } @@ -432,8 +431,8 @@ protected: return true; } - void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override { - if (type == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) + void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override { + if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) uint32_t sender_timestamp, sender_sync_since; memcpy(&sender_timestamp, data, 4); memcpy(&sender_sync_since, &data[4], 4); // sender's "sync messags SINCE x" timestamp @@ -465,6 +464,7 @@ protected: client->sync_since = sender_sync_since; client->pending_ack = 0; client->push_failures = 0; + memcpy(client->secret, secret, PUB_KEY_SIZE); uint32_t now = getRTCClock()->getCurrentTime(); client->last_activity = now; diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 3b5b04cc..92a57f83 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -46,6 +46,7 @@ /* ------------------------------ Code -------------------------------- */ +#define REQ_TYPE_LOGIN 0x00 #define REQ_TYPE_GET_STATUS 0x01 #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 @@ -139,12 +140,10 @@ void SensorMesh::saveContacts() { } } -int SensorMesh::handleRequest(ContactInfo& 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 +uint8_t SensorMesh::handleRequest(bool is_admin, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') - switch (payload[0]) { + switch (req_type) { case REQ_TYPE_GET_TELEMETRY_DATA: { telemetry.reset(); telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); @@ -253,63 +252,79 @@ int SensorMesh::getAGCResetInterval() const { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } -void SensorMesh::onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) { - if (type == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) +uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) { + bool is_admin; + if (strcmp((char *) data, _prefs.password) == 0) { // check for valid password + is_admin = true; + } else if (strcmp((char *) data, _prefs.guest_password) == 0) { // check guest password + is_admin = false; + } else { + #if MESH_DEBUG + MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); + #endif + return 0; + } + + auto client = putContact(sender); // add to contacts (if not already known) + if (sender_timestamp <= client->last_timestamp) { + MESH_DEBUG_PRINTLN("Possible login replay attack!"); + return 0; // FATAL: client table is full -OR- replay attack + } + + MESH_DEBUG_PRINTLN("Login success!"); + client->last_timestamp = sender_timestamp; + client->last_activity = getRTCClock()->getCurrentTime(); + client->type = is_admin ? 1 : 0; + memcpy(client->shared_secret, secret, PUB_KEY_SIZE); + + if (is_admin) { + // only need to saveContacts() if this is an admin + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + } + + uint32_t now = getRTCClock()->getCurrentTimeUnique(); + memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + reply_data[4] = RESP_SERVER_LOGIN_OK; + reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) + reply_data[6] = client->type; + reply_data[7] = 0; // FUTURE: reserved + getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness + + return 12; // reply length +} + +void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) { + if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) uint32_t timestamp; memcpy(×tamp, data, 4); - bool is_admin; data[len] = 0; // ensure null terminator - if (strcmp((char *) &data[4], _prefs.password) == 0) { // check for valid password - is_admin = true; - } else if (strcmp((char *) &data[4], _prefs.guest_password) == 0) { // check guest password - is_admin = false; + + uint8_t req_code; + uint8_t i = 4; + if (data[4] < 32) { // non-print char, is a request code + req_code = data[i++]; } else { - #if MESH_DEBUG - MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); - #endif - return; + req_code = REQ_TYPE_LOGIN; } - auto client = putContact(sender); // add to contacts (if not already known) - if (timestamp <= client->last_timestamp) { - MESH_DEBUG_PRINTLN("Possible login replay attack!"); - return; // FATAL: client table is full -OR- replay attack + uint8_t reply_len; + if (req_code == REQ_TYPE_LOGIN) { + reply_len = handleLoginReq(sender, secret, timestamp, &data[i]); + } else { + reply_len = handleRequest(false, timestamp, req_code, &data[i], len - i); } - MESH_DEBUG_PRINTLN("Login success!"); - client->last_timestamp = timestamp; - client->last_activity = getRTCClock()->getCurrentTime(); - client->type = is_admin ? 1 : 0; - self_id.calcSharedSecret(client->shared_secret, client->id); // calc ECDH shared secret - - if (is_admin) { - // only need to saveContacts() if this is an admin - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - } - - uint32_t now = getRTCClock()->getCurrentTimeUnique(); - memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp - reply_data[4] = RESP_SERVER_LOGIN_OK; - reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) - reply_data[6] = client->type; - reply_data[7] = 0; // FUTURE: reserved - getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness + if (reply_len == 0) return; // invalid request if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response - mesh::Packet* path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, 12); + 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 { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 12); - if (reply) { - if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); - } else { - sendFlood(reply, SERVER_RESPONSE_DELAY); - } - } + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY); } } } @@ -361,7 +376,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i memcpy(×tamp, data, 4); if (timestamp > from.last_timestamp) { // prevent replay attacks - int reply_len = handleRequest(from, timestamp, &data[4], len - 4); + uint8_t reply_len = handleRequest(from.isAdmin(), timestamp, data[4], &data[5], len - 5); if (reply_len == 0) return; // invalid command from.last_timestamp = timestamp; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 051d6644..6293b21e 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -103,10 +103,10 @@ protected: uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; int getInterferenceThreshold() const override; int getAGCResetInterval() const override; - void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override; + void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; - void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len); + void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override; void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override; bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; @@ -125,7 +125,8 @@ private: void loadContacts(); void saveContacts(); - int handleRequest(ContactInfo& sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); + uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data); + uint8_t handleRequest(bool is_admin, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); ContactInfo* putContact(const mesh::Identity& id); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 87f99878..f34f6f77 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -181,7 +181,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint8_t data[MAX_PACKET_PAYLOAD]; int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); if (len > 0) { // success! - onAnonDataRecv(pkt, pkt->getPayloadType(), sender, data, len); + onAnonDataRecv(pkt, secret, sender, data, len); pkt->markDoNotRetransmit(); } } diff --git a/src/Mesh.h b/src/Mesh.h index 9649187c..b2047ac1 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -107,10 +107,10 @@ protected: /** * \brief A (now decrypted) data packet has been received. * NOTE: these can be received multiple times (per sender/contents), via different routes - * \param type one of: PAYLOAD_TYPE_ANON_REQ + * \param secret ECDH shared secret * \param sender public key provided by sender */ - virtual void onAnonDataRecv(Packet* packet, uint8_t type, const Identity& sender, uint8_t* data, size_t len) { } + virtual void onAnonDataRecv(Packet* packet, const uint8_t* secret, const Identity& sender, uint8_t* data, size_t len) { } /** * \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded) From de3e4bc27c3e0c3ec9bc088f5b43a439ceb1773b Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 7 Jul 2025 22:59:01 +1000 Subject: [PATCH 33/60] * added REQ_TYPE_GET_AVG_MIN_MAX * TimeSeriesData * very basic SensorMesh::sendAlert() --- examples/simple_sensor/SensorMesh.cpp | 91 ++++++++++++++++++++++++++- examples/simple_sensor/SensorMesh.h | 30 ++++++++- examples/simple_sensor/main.cpp | 24 ++++--- 3 files changed, 135 insertions(+), 10 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 92a57f83..df568ddf 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -50,6 +50,7 @@ #define REQ_TYPE_GET_STATUS 0x01 #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +#define REQ_TYPE_GET_AVG_MIN_MAX 0x04 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -154,6 +155,22 @@ uint8_t SensorMesh::handleRequest(bool is_admin, uint32_t sender_timestamp, uint memcpy(&reply_data[4], telemetry.getBuffer(), tlen); return 4 + tlen; // reply_len } + case REQ_TYPE_GET_AVG_MIN_MAX: { + uint32_t start_secs_ago, end_secs_ago; + memcpy(&start_secs_ago, &payload[0], 4); + memcpy(&end_secs_ago, &payload[4], 4); + uint8_t res1 = payload[8]; // reserved for future (extra query params) + uint8_t res2 = payload[8]; + + MinMaxAvg data[8]; + int n; + if (res1 == 0 && res2 == 0) { + n = querySeriesData(start_secs_ago, end_secs_ago, data, 8); + } else { + n = 0; + } + return 0; // TODO: encode data[0..n) + } } return 0; // unknown command } @@ -192,6 +209,34 @@ ContactInfo* SensorMesh::putContact(const mesh::Identity& id) { return c; } +void SensorMesh::sendAlert(const char* text) { + int text_len = strlen(text); + + // send text message to all admins + for (int i = 0; i < num_contacts; i++) { + auto c = &contacts[i]; + if (!c->isAdmin()) continue; + + uint8_t data[MAX_PACKET_PAYLOAD]; + uint32_t now = getRTCClock()->getCurrentTimeUnique(); // need different timestamp per packet + memcpy(data, &now, 4); + data[4] = (TXT_TYPE_PLAIN << 2); // attempt and flags + memcpy(&data[5], text, text_len); + // calc expected ACK reply + // uint32_t expected_ack; + // mesh::Utils::sha256((uint8_t *)&expected_ack, 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE); + + auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len); + if (pkt) { + if (c->out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(pkt, c->out_path, c->out_path_len); + } else { + sendFlood(pkt); + } + } + } +} + void SensorMesh::alertIfLow(Trigger& t, float value, float threshold, const char* text) { if (value < threshold) { if (!t.triggered) { @@ -222,6 +267,50 @@ void SensorMesh::alertIfHigh(Trigger& t, float value, float threshold, const cha } } +void SensorMesh::recordData(TimeSeriesData& data, float value) { + uint32_t now = getRTCClock()->getCurrentTime(); + if (now >= data.last_timestamp + data.interval_secs) { + data.last_timestamp = now; + + data.data[data.next] = value; // append to cycle table + data.next = (data.next + 1) % data.num_slots; + } +} + +void SensorMesh::calcDataMinMaxAvg(const TimeSeriesData& data, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) { + int i = data.next, n = data.num_slots; + uint32_t ago = data.interval_secs * data.num_slots; + int num_values = 0; + float total = 0.0f; + + dest->_channel = channel; + dest->_lpp_type = lpp_type; + + // start at earliest recording, through to most recent + while (n > 0) { + n--; + i = (i + 1) % data.num_slots; + if (ago >= end_secs_ago && ago < start_secs_ago) { + float v = data.data[i]; + num_values++; + total += v; + if (num_values == 1) { + dest->_max = dest->_min = v; + } else { + if (v < dest->_min) dest->_min = v; + if (v > dest->_max) dest->_max = v; + } + } + ago -= data.interval_secs; + } + // calc average + if (num_values > 0) { + dest->_avg = total / num_values; + } else { + dest->_avg = NAN; + } +} + float SensorMesh::getAirtimeBudgetFactor() const { return _prefs.airtime_factor; } @@ -577,7 +666,7 @@ void SensorMesh::loop() { // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions - checkForAlerts(); + onSensorDataRead(); // save telemetry to time-series datastore File file = openAppend(_fs, "/s_data"); diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 6293b21e..0f94b128 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -93,7 +93,33 @@ protected: void alertIfLow(Trigger& t, float value, float threshold, const char* text); void alertIfHigh(Trigger& t, float value, float threshold, const char* text); - virtual void checkForAlerts() = 0; // for app to implement + class TimeSeriesData { + public: + float* data; + int num_slots, next; + uint32_t last_timestamp; + uint32_t interval_secs; + + TimeSeriesData(float* array, int num, uint32_t secs) : num_slots(num), data(array), last_timestamp(0), next(0), interval_secs(secs) { + memset(data, 0, sizeof(float)*num); + } + TimeSeriesData(int num, uint32_t secs) : num_slots(num), last_timestamp(0), next(0), interval_secs(secs) { + data = new float[num]; + memset(data, 0, sizeof(float)*num); + } + }; + + void recordData(TimeSeriesData& data, float value); + + struct MinMaxAvg { + float _min, _max, _avg; + uint8_t _lpp_type, _channel; + }; + + void calcDataMinMaxAvg(const TimeSeriesData& data, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type); + + virtual void onSensorDataRead() = 0; // for app to implement + virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0; // for app to implement // Mesh overrides float getAirtimeBudgetFactor() const override; @@ -130,6 +156,6 @@ private: mesh::Packet* createSelfAdvert(); ContactInfo* putContact(const mesh::Identity& id); - void sendAlert(const char* text) { } // TODO + void sendAlert(const char* text); }; diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 63122e9a..500efef4 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -8,18 +8,28 @@ class MyMesh : public SensorMesh { public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) - : SensorMesh(board, radio, ms, rng, rtc, tables) { } + : SensorMesh(board, radio, ms, rng, rtc, tables), + battery_data(12*24, 5*60) // 24 hours worth of battery data, every 5 minutes + { + } protected: - /* ========================== custom alert logic here ========================== */ + /* ========================== custom logic here ========================== */ Trigger low_batt; + TimeSeriesData battery_data; - void checkForAlerts() override { - alertIfLow(low_batt, getVoltage(TELEM_CHANNEL_SELF), 3.4f, "Battery low!"); - // alertIf ... - // alertIf ... + void onSensorDataRead() override { + float batt_voltage = getVoltage(TELEM_CHANNEL_SELF); + + recordData(battery_data, batt_voltage); // record battery + alertIfLow(low_batt, batt_voltage, 3.4f, "Battery low!"); } - /* ============================================================================= */ + + int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override { + calcDataMinMaxAvg(battery_data, start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE); + return 1; + } + /* ======================================================================= */ }; StdRNG fast_rng; From ac834922dee1782b8aaa6c674a45be635c76b19d Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 8 Jul 2025 16:35:11 +1000 Subject: [PATCH 34/60] * simplified alertIf() * refactored TimeSeriesData to top-level class --- examples/simple_sensor/SensorMesh.cpp | 63 +---------------------- examples/simple_sensor/SensorMesh.h | 30 ++--------- examples/simple_sensor/TimeSeriesData.cpp | 45 ++++++++++++++++ examples/simple_sensor/TimeSeriesData.h | 29 +++++++++++ examples/simple_sensor/main.cpp | 6 +-- 5 files changed, 82 insertions(+), 91 deletions(-) create mode 100644 examples/simple_sensor/TimeSeriesData.cpp create mode 100644 examples/simple_sensor/TimeSeriesData.h diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index df568ddf..fa08b156 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -237,8 +237,8 @@ void SensorMesh::sendAlert(const char* text) { } } -void SensorMesh::alertIfLow(Trigger& t, float value, float threshold, const char* text) { - if (value < threshold) { +void SensorMesh::alertIf(bool condition, Trigger& t, const char* text) { + if (condition) { if (!t.triggered) { t.triggered = true; t.time = getRTCClock()->getCurrentTime(); @@ -252,65 +252,6 @@ void SensorMesh::alertIfLow(Trigger& t, float value, float threshold, const char } } -void SensorMesh::alertIfHigh(Trigger& t, float value, float threshold, const char* text) { - if (value > threshold) { - if (!t.triggered) { - t.triggered = true; - t.time = getRTCClock()->getCurrentTime(); - sendAlert(text); - } - } else { - if (t.triggered) { - t.triggered = false; - // TODO: apply debounce logic - } - } -} - -void SensorMesh::recordData(TimeSeriesData& data, float value) { - uint32_t now = getRTCClock()->getCurrentTime(); - if (now >= data.last_timestamp + data.interval_secs) { - data.last_timestamp = now; - - data.data[data.next] = value; // append to cycle table - data.next = (data.next + 1) % data.num_slots; - } -} - -void SensorMesh::calcDataMinMaxAvg(const TimeSeriesData& data, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) { - int i = data.next, n = data.num_slots; - uint32_t ago = data.interval_secs * data.num_slots; - int num_values = 0; - float total = 0.0f; - - dest->_channel = channel; - dest->_lpp_type = lpp_type; - - // start at earliest recording, through to most recent - while (n > 0) { - n--; - i = (i + 1) % data.num_slots; - if (ago >= end_secs_ago && ago < start_secs_ago) { - float v = data.data[i]; - num_values++; - total += v; - if (num_values == 1) { - dest->_max = dest->_min = v; - } else { - if (v < dest->_min) dest->_min = v; - if (v > dest->_max) dest->_max = v; - } - } - ago -= data.interval_secs; - } - // calc average - if (num_values > 0) { - dest->_avg = total / num_values; - } else { - dest->_avg = NAN; - } -} - float SensorMesh::getAirtimeBudgetFactor() const { return _prefs.airtime_factor; } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 0f94b128..e8b4d88f 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -3,6 +3,8 @@ #include // needed for PlatformIO #include +#include "TimeSeriesData.h" + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include #elif defined(RP2040_PLATFORM) @@ -90,33 +92,7 @@ protected: Trigger() { triggered = false; time = 0; } }; - void alertIfLow(Trigger& t, float value, float threshold, const char* text); - void alertIfHigh(Trigger& t, float value, float threshold, const char* text); - - class TimeSeriesData { - public: - float* data; - int num_slots, next; - uint32_t last_timestamp; - uint32_t interval_secs; - - TimeSeriesData(float* array, int num, uint32_t secs) : num_slots(num), data(array), last_timestamp(0), next(0), interval_secs(secs) { - memset(data, 0, sizeof(float)*num); - } - TimeSeriesData(int num, uint32_t secs) : num_slots(num), last_timestamp(0), next(0), interval_secs(secs) { - data = new float[num]; - memset(data, 0, sizeof(float)*num); - } - }; - - void recordData(TimeSeriesData& data, float value); - - struct MinMaxAvg { - float _min, _max, _avg; - uint8_t _lpp_type, _channel; - }; - - void calcDataMinMaxAvg(const TimeSeriesData& data, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type); + void alertIf(bool condition, Trigger& t, const char* text); virtual void onSensorDataRead() = 0; // for app to implement virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0; // for app to implement diff --git a/examples/simple_sensor/TimeSeriesData.cpp b/examples/simple_sensor/TimeSeriesData.cpp new file mode 100644 index 00000000..ff7daa25 --- /dev/null +++ b/examples/simple_sensor/TimeSeriesData.cpp @@ -0,0 +1,45 @@ +#include "TimeSeriesData.h" + +void TimeSeriesData::recordData(mesh::RTCClock* clock, float value) { + uint32_t now = clock->getCurrentTime(); + if (now >= last_timestamp + interval_secs) { + last_timestamp = now; + + data[next] = value; // append to cycle table + next = (next + 1) % num_slots; + } +} + +void TimeSeriesData::calcDataMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const { + int i = next, n = num_slots; + uint32_t ago = clock->getCurrentTime() - last_timestamp; + int num_values = 0; + float total = 0.0f; + + dest->_channel = channel; + dest->_lpp_type = lpp_type; + + // start at most recet recording, back-track through to oldest + while (n > 0) { + n--; + i = (i + num_slots - 1) % num_slots; // go back by one + if (ago >= end_secs_ago && ago < start_secs_ago) { // filter by the desired time range + float v = data[i]; + num_values++; + total += v; + if (num_values == 1) { + dest->_max = dest->_min = v; + } else { + if (v < dest->_min) dest->_min = v; + if (v > dest->_max) dest->_max = v; + } + } + ago += interval_secs; + } + // calc average + if (num_values > 0) { + dest->_avg = total / num_values; + } else { + dest->_avg = NAN; + } +} diff --git a/examples/simple_sensor/TimeSeriesData.h b/examples/simple_sensor/TimeSeriesData.h new file mode 100644 index 00000000..ea9e823b --- /dev/null +++ b/examples/simple_sensor/TimeSeriesData.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +struct MinMaxAvg { + float _min, _max, _avg; + uint8_t _lpp_type, _channel; +}; + +class TimeSeriesData { + float* data; + int num_slots, next; + uint32_t last_timestamp; + uint32_t interval_secs; + +public: + TimeSeriesData(float* array, int num, uint32_t secs) : num_slots(num), data(array), last_timestamp(0), next(0), interval_secs(secs) { + memset(data, 0, sizeof(float)*num); + } + TimeSeriesData(int num, uint32_t secs) : num_slots(num), last_timestamp(0), next(0), interval_secs(secs) { + data = new float[num]; + memset(data, 0, sizeof(float)*num); + } + + void recordData(mesh::RTCClock* clock, float value); + void calcDataMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const; +}; + diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 500efef4..03d5cb5b 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -21,12 +21,12 @@ protected: void onSensorDataRead() override { float batt_voltage = getVoltage(TELEM_CHANNEL_SELF); - recordData(battery_data, batt_voltage); // record battery - alertIfLow(low_batt, batt_voltage, 3.4f, "Battery low!"); + battery_data.recordData(getRTCClock(), batt_voltage); // record battery + alertIf(batt_voltage < 3.4f, low_batt, "Battery low!"); } int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override { - calcDataMinMaxAvg(battery_data, start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE); + battery_data.calcDataMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE); return 1; } /* ======================================================================= */ From 9cecbad2a7d4c8847b487e9247ee738d134e1d72 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 8 Jul 2025 17:50:06 +1000 Subject: [PATCH 35/60] * refactor: CommonCLI, processing of optional command prefix moved to handleCommand() call sites * Sensor, anon_req now just for admin login (guest password now unused) * special CLI command, "setperm {pubkey-hex} {permissions-int16}" for admin(s) to manage user access (permissions 0 = remove) --- examples/simple_repeater/main.cpp | 20 +++- examples/simple_room_server/main.cpp | 20 +++- examples/simple_sensor/SensorMesh.cpp | 150 +++++++++++++++----------- examples/simple_sensor/SensorMesh.h | 14 ++- examples/simple_sensor/main.cpp | 2 +- src/helpers/CommonCLI.cpp | 8 -- 6 files changed, 125 insertions(+), 89 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index d8f1cde1..cd9ee12a 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -500,12 +500,12 @@ protected: } uint8_t temp[166]; - const char *command = (const char *) &data[5]; + char *command = (char *) &data[5]; char *reply = (char *) &temp[5]; if (is_retry) { *reply = 0; } else { - _cli.handleCommand(sender_timestamp, command, reply); + handleCommand(sender_timestamp, command, reply); } int text_len = strlen(reply); if (text_len > 0) { @@ -581,8 +581,6 @@ public: _prefs.interference_threshold = 0; // disabled } - CommonCLI* getCLI() { return &_cli; } - void begin(FILESYSTEM* fs) { mesh::Mesh::begin(); _fs = fs; @@ -706,6 +704,18 @@ public: ((SimpleMeshTables *)getTables())->resetStats(); } + void handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + while (*command == ' ') command++; // skip leading spaces + + if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) + memcpy(reply, command, 3); // reflect the prefix back + reply += 3; + command += 3; + } + + _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + } + void loop() { mesh::Mesh::loop(); @@ -817,7 +827,7 @@ void loop() { if (len > 0 && command[len - 1] == '\r') { // received complete line command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index b0ee472f..a1400cb3 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -333,7 +333,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { } return 0; // unknown command } - + protected: float getAirtimeBudgetFactor() const override { return _prefs.airtime_factor; @@ -555,7 +555,7 @@ protected: if (is_retry) { temp[5] = 0; // no reply } else { - _cli.handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]); + handleCommand(sender_timestamp, (char *) &data[5], (char *) &temp[5]); temp[4] = (TXT_TYPE_CLI_DATA << 2); // attempt and flags, (NOTE: legacy was: TXT_TYPE_PLAIN) } send_ack = false; @@ -743,8 +743,6 @@ public: _num_posted = _num_post_pushes = 0; } - CommonCLI* getCLI() { return &_cli; } - void begin(FILESYSTEM* fs) { mesh::Mesh::begin(); _fs = fs; @@ -845,6 +843,18 @@ public: ((SimpleMeshTables *)getTables())->resetStats(); } + void handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + while (*command == ' ') command++; // skip leading spaces + + if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) + memcpy(reply, command, 3); // reflect the prefix back + reply += 3; + command += 3; + } + + _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + } + void loop() { mesh::Mesh::loop(); @@ -998,7 +1008,7 @@ void loop() { if (len > 0 && command[len - 1] == '\r') { // received complete line command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index fa08b156..8b1b6f24 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -92,12 +92,11 @@ void SensorMesh::loadContacts() { while (!full) { ContactInfo c; uint8_t pub_key[32]; - uint8_t unused; + uint8_t unused[5]; bool success = (file.read(pub_key, 32) == 32); - success = success && (file.read(&c.type, 1) == 1); - success = success && (file.read(&c.flags, 1) == 1); - success = success && (file.read(&unused, 1) == 1); + success = success && (file.read((uint8_t *) &c.permissions, 2) == 2); + success = success && (file.read(unused, 5) == 5); 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); @@ -121,16 +120,16 @@ void SensorMesh::loadContacts() { void SensorMesh::saveContacts() { File file = openWrite(_fs, "/s_contacts"); if (file) { - uint8_t unused = 0; + uint8_t unused[5]; + memset(unused, 0, sizeof(unused)); for (int i = 0; i < num_contacts; i++) { auto c = &contacts[i]; - if (c->type == 0) continue; // don't persist guest contacts + if (c->permissions == 0) continue; // skip deleted entries bool success = (file.write(c->id.pub_key, 32) == 32); - success = success && (file.write(&c->type, 1) == 1); - success = success && (file.write(&c->flags, 1) == 1); - success = success && (file.write(&unused, 1) == 1); + success = success && (file.write((uint8_t *) &c->permissions, 2) == 2); + success = success && (file.write(unused, 5) == 5); success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1); success = success && (file.write(c->out_path, 64) == 64); success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); @@ -141,36 +140,34 @@ void SensorMesh::saveContacts() { } } -uint8_t SensorMesh::handleRequest(bool is_admin, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { +uint8_t SensorMesh::handleRequest(uint16_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') - switch (req_type) { - case REQ_TYPE_GET_TELEMETRY_DATA: { - telemetry.reset(); - telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); - // query other sensors -- target specific - sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions for admin or guest + if (req_type == REQ_TYPE_GET_TELEMETRY_DATA && (perms & PERM_GET_TELEMETRY) != 0) { + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions for admin or guest - uint8_t tlen = telemetry.getSize(); - memcpy(&reply_data[4], telemetry.getBuffer(), tlen); - return 4 + tlen; // reply_len - } - case REQ_TYPE_GET_AVG_MIN_MAX: { - uint32_t start_secs_ago, end_secs_ago; - memcpy(&start_secs_ago, &payload[0], 4); - memcpy(&end_secs_ago, &payload[4], 4); - uint8_t res1 = payload[8]; // reserved for future (extra query params) - uint8_t res2 = payload[8]; + uint8_t tlen = telemetry.getSize(); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + return 4 + tlen; // reply_len + } + if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_GET_MIN_MAX_AVG) != 0) { + uint32_t start_secs_ago, end_secs_ago; + memcpy(&start_secs_ago, &payload[0], 4); + memcpy(&end_secs_ago, &payload[4], 4); + uint8_t res1 = payload[8]; // reserved for future (extra query params) + uint8_t res2 = payload[8]; - MinMaxAvg data[8]; - int n; - if (res1 == 0 && res2 == 0) { - n = querySeriesData(start_secs_ago, end_secs_ago, data, 8); - } else { - n = 0; - } - return 0; // TODO: encode data[0..n) + MinMaxAvg data[8]; + int n; + if (res1 == 0 && res2 == 0) { + n = querySeriesData(start_secs_ago, end_secs_ago, data, 8); + } else { + n = 0; } + return 0; // TODO: encode data[0..n) } return 0; // unknown command } @@ -209,6 +206,18 @@ ContactInfo* SensorMesh::putContact(const mesh::Identity& id) { return c; } +void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint16_t perms) { + mesh::Identity id(pubkey); + auto c = putContact(id); + + if (perms == 0) { // no permissions, remove from contacts + memset(c, 0, sizeof(*c)); + } else { + c->permissions = perms; // update their permissions + } + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger saveContacts() +} + void SensorMesh::sendAlert(const char* text) { int text_len = strlen(text); @@ -283,12 +292,7 @@ int SensorMesh::getAGCResetInterval() const { } uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) { - bool is_admin; - if (strcmp((char *) data, _prefs.password) == 0) { // check for valid password - is_admin = true; - } else if (strcmp((char *) data, _prefs.guest_password) == 0) { // check guest password - is_admin = false; - } else { + if (strcmp((char *) data, _prefs.password) != 0) { // check for valid password #if MESH_DEBUG MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); #endif @@ -304,46 +308,61 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* MESH_DEBUG_PRINTLN("Login success!"); client->last_timestamp = sender_timestamp; client->last_activity = getRTCClock()->getCurrentTime(); - client->type = is_admin ? 1 : 0; + client->permissions = PERM_IS_ADMIN; memcpy(client->shared_secret, secret, PUB_KEY_SIZE); - if (is_admin) { - // only need to saveContacts() if this is an admin - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - } + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); uint32_t now = getRTCClock()->getCurrentTimeUnique(); memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp reply_data[4] = RESP_SERVER_LOGIN_OK; reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) - reply_data[6] = client->type; + reply_data[6] = 1; // 1 = is admin reply_data[7] = 0; // FUTURE: reserved getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness return 12; // reply length } +void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + while (*command == ' ') command++; // skip leading spaces + + if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) + memcpy(reply, command, 3); // reflect the prefix back + reply += 3; + command += 3; + } + + // handle sensor-specific CLI commands + if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int16} + char* hex = &command[8]; + char* sp = strchr(hex, ' '); // look for separator char + if (sp == NULL || sp - hex != PUB_KEY_SIZE*2) { + strcpy(reply, "Err - bad pubkey len"); + } else { + *sp++ = 0; // replace space with null terminator + + uint8_t pubkey[PUB_KEY_SIZE]; + if (mesh::Utils::fromHex(pubkey, PUB_KEY_SIZE, hex)) { + uint16_t perms = atoi(sp); + applyContactPermissions(pubkey, perms); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - bad pubkey"); + } + } + } else { + _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + } +} + void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) { if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) uint32_t timestamp; memcpy(×tamp, data, 4); data[len] = 0; // ensure null terminator - - uint8_t req_code; - uint8_t i = 4; - if (data[4] < 32) { // non-print char, is a request code - req_code = data[i++]; - } else { - req_code = REQ_TYPE_LOGIN; - } - - uint8_t reply_len; - if (req_code == REQ_TYPE_LOGIN) { - reply_len = handleLoginReq(sender, secret, timestamp, &data[i]); - } else { - reply_len = handleRequest(false, timestamp, req_code, &data[i], len - i); - } + uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]); if (reply_len == 0) return; // invalid request @@ -406,7 +425,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i memcpy(×tamp, data, 4); if (timestamp > from.last_timestamp) { // prevent replay attacks - uint8_t reply_len = handleRequest(from.isAdmin(), timestamp, data[4], &data[5], len - 5); + uint8_t reply_len = handleRequest(from.isAdmin() ? 0xFFFF : from.permissions, timestamp, data[4], &data[5], len - 5); if (reply_len == 0) return; // invalid command from.last_timestamp = timestamp; @@ -445,9 +464,9 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i data[len] = 0; // need to make a C string again, with null terminator uint8_t temp[166]; - const char *command = (const char *) &data[5]; + char *command = (char *) &data[5]; char *reply = (char *) &temp[5]; - _cli.handleCommand(sender_timestamp, command, reply); + handleCommand(sender_timestamp, command, reply); int text_len = strlen(reply); if (text_len > 0) { @@ -489,8 +508,9 @@ bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint memcpy(from.out_path, path, from.out_path_len = path_len); // store a copy of path, for sendDirect() from.last_activity = getRTCClock()->getCurrentTime(); + // REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes?? if (from.isAdmin()) { - // only need to saveContacts() if this is an admin + // only do saveContacts() (of this out_path change) if this is an admin dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index e8b4d88f..196acb8f 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -23,17 +23,20 @@ #include #include +#define PERM_IS_ADMIN 0x8000 +#define PERM_GET_TELEMETRY 0x0001 +#define PERM_GET_MIN_MAX_AVG 0x0002 + struct ContactInfo { mesh::Identity id; - uint8_t type; // 1 = admin, 0 = guest - uint8_t flags; + uint16_t permissions; int8_t out_path_len; uint8_t out_path[MAX_PATH_SIZE]; uint8_t shared_secret[PUB_KEY_SIZE]; uint32_t last_timestamp; // by THEIR clock (transient) uint32_t last_activity; // by OUR clock (transient) - bool isAdmin() const { return type != 0; } + bool isAdmin() const { return (permissions & PERM_IS_ADMIN) != 0; } }; #ifndef FIRMWARE_BUILD_DATE @@ -56,8 +59,8 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { public: SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); void begin(FILESYSTEM* fs); - CommonCLI* getCLI() { return &_cli; } void loop(); + void handleCommand(uint32_t sender_timestamp, char* command, char* reply); // CommonCLI callbacks const char* getFirmwareVer() override { return FIRMWARE_VERSION; } @@ -128,9 +131,10 @@ private: void loadContacts(); void saveContacts(); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data); - uint8_t handleRequest(bool is_admin, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); + uint8_t handleRequest(uint16_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); ContactInfo* putContact(const mesh::Identity& id); + void applyContactPermissions(const uint8_t* pubkey, uint16_t perms); void sendAlert(const char* text); diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 03d5cb5b..25610a69 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -122,7 +122,7 @@ void loop() { if (len > 0 && command[len - 1] == '\r') { // received complete line command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 323f3633..97b4ffe1 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -127,14 +127,6 @@ void CommonCLI::checkAdvertInterval() { } void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) { - while (*command == ' ') command++; // skip leading spaces - - if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) - memcpy(reply, command, 3); // reflect the prefix back - reply += 3; - command += 3; - } - if (memcmp(command, "reboot", 6) == 0) { _board->reboot(); // doesn't return } else if (memcmp(command, "advert", 6) == 0) { From 29435342b0eedede0841d06bce983057ab10933e Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 8 Jul 2025 20:41:26 +1000 Subject: [PATCH 36/60] * implemented getter methods for telemetry value types --- examples/simple_sensor/SensorMesh.cpp | 119 ++++++++++++++++++++++---- examples/simple_sensor/SensorMesh.h | 14 ++- src/helpers/CommonCLI.cpp | 7 +- src/helpers/CommonCLI.h | 5 +- 4 files changed, 115 insertions(+), 30 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 8b1b6f24..293a503c 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -221,10 +221,10 @@ void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint16_t perms) void SensorMesh::sendAlert(const char* text) { int text_len = strlen(text); - // send text message to all admins + // send text message to all contacts with RECV_ALERT permission for (int i = 0; i < num_contacts; i++) { auto c = &contacts[i]; - if (!c->isAdmin()) continue; + if ((c->permissions & PERM_RECV_ALERTS) == 0) continue; // contact does NOT want alerts uint8_t data[MAX_PACKET_PAYLOAD]; uint32_t now = getRTCClock()->getCurrentTimeUnique(); // need different timestamp per packet @@ -308,7 +308,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* MESH_DEBUG_PRINTLN("Login success!"); client->last_timestamp = sender_timestamp; client->last_activity = getRTCClock()->getCurrentTime(); - client->permissions = PERM_IS_ADMIN; + client->permissions = PERM_IS_ADMIN | PERM_RECV_ALERTS; memcpy(client->shared_secret, secret, PUB_KEY_SIZE); dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); @@ -542,7 +542,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise _prefs.cr = LORA_CR; _prefs.tx_power_dbm = LORA_TX_POWER; _prefs.advert_interval = 1; // default to 2 minutes for NEW installs - _prefs.flood_advert_interval = 3; // 3 hours + _prefs.flood_advert_interval = 0; // disabled _prefs.disable_fwd = true; _prefs.flood_max = 64; _prefs.interference_threshold = 0; // disabled @@ -604,6 +604,102 @@ void SensorMesh::setTxPower(uint8_t power_dbm) { radio_set_tx_power(power_dbm); } +static uint8_t getDataSize(uint8_t type) { + switch (type) { + case LPP_GPS: + return 9; + case LPP_POLYLINE: + return 8; // TODO: this is MINIMIUM + case LPP_GYROMETER: + case LPP_ACCELEROMETER: + return 6; + case LPP_GENERIC_SENSOR: + case LPP_FREQUENCY: + case LPP_DISTANCE: + case LPP_ENERGY: + case LPP_UNIXTIME: + return 4; + case LPP_COLOUR: + return 3; + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + case LPP_LUMINOSITY: + case LPP_TEMPERATURE: + case LPP_CONCENTRATION: + case LPP_BAROMETRIC_PRESSURE: + case LPP_ALTITUDE: + case LPP_VOLTAGE: + case LPP_CURRENT: + case LPP_DIRECTION: + case LPP_POWER: + return 2; + } + return 1; +} + +static uint32_t getMultiplier(uint8_t type) { + switch (type) { + case LPP_CURRENT: + case LPP_DISTANCE: + case LPP_ENERGY: + return 1000; + case LPP_VOLTAGE: + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + return 100; + case LPP_TEMPERATURE: + case LPP_BAROMETRIC_PRESSURE: + return 10; + } + return 1; +} + +static bool isSigned(uint8_t type) { + return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER || + type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER; +} + +static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { + uint32_t value = 0; + for (uint8_t i = 0; i < size; i++) { + value = (value << 8) + buffer[i]; + } + + int sign = 1; + if (is_signed) { + uint32_t bit = 1ul << ((size * 8) - 1); + if ((value & bit) == bit) { + value = (bit << 1) - value; + sign = -1; + } + } + return sign * ((float) value / multiplier); +} + +float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) { + auto buf = telemetry.getBuffer(); + uint8_t size = telemetry.getSize(); + uint8_t i = 0; + + while (i + 2 < size) { + // Get channel # + uint8_t ch = buf[i++]; + // Get data type + uint8_t t = buf[i++]; + uint8_t sz = getDataSize(t); + + if (ch == channel && t == type) { + return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); + } + i += sz; // skip + } + return 0.0f; // not found +} + +bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) { + return false; // TODO +} + void SensorMesh::loop() { mesh::Mesh::loop(); @@ -629,21 +725,6 @@ void SensorMesh::loop() { onSensorDataRead(); - // save telemetry to time-series datastore - File file = openAppend(_fs, "/s_data"); - if (file) { - file.write((uint8_t *)&curr, 4); // start record with RTC timestamp - uint8_t tlen = telemetry.getSize(); - file.write(&tlen, 1); - file.write(telemetry.getBuffer(), tlen); - uint8_t zero = 0; - while (tlen < MAX_PACKET_PAYLOAD - 4) { // pad with zeroes, for fixed record length - file.write(&zero, 1); - tlen++; - } - file.close(); - } - last_read_time = curr; } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 196acb8f..05104c99 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -26,6 +26,7 @@ #define PERM_IS_ADMIN 0x8000 #define PERM_GET_TELEMETRY 0x0001 #define PERM_GET_MIN_MAX_AVG 0x0002 +#define PERM_RECV_ALERTS 0x0100 struct ContactInfo { mesh::Identity id; @@ -83,9 +84,18 @@ public: const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; } void clearStats() override { } + float getTelemValue(uint8_t channel, uint8_t type); + protected: - // telemetry data queries - float getVoltage(uint8_t channel) { return 0.0f; } // TODO: extract from curr telemetry buffer + // current telemetry data queries + float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); } + float getCurrent(uint8_t channel) { return getTelemValue(channel, LPP_CURRENT); } + float getPower(uint8_t channel) { return getTelemValue(channel, LPP_POWER); } + float getTemperature(uint8_t channel) { return getTelemValue(channel, LPP_TEMPERATURE); } + float getRelativeHumidity(uint8_t channel) { return getTelemValue(channel, LPP_RELATIVE_HUMIDITY); } + float getBarometricPressure(uint8_t channel) { return getTelemValue(channel, LPP_BAROMETRIC_PRESSURE); } + float getAltitude(uint8_t channel) { return getTelemValue(channel, LPP_ALTITUDE); } + bool getGPS(uint8_t channel, float& lat, float& lon, float& alt); // alerts struct Trigger { diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 97b4ffe1..df510c28 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -120,10 +120,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { #define MIN_LOCAL_ADVERT_INTERVAL 60 -void CommonCLI::checkAdvertInterval() { +void CommonCLI::savePrefs() { if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) { _prefs->advert_interval = 0; // turn it off, now that device has been manually configured } + _callbacks->savePrefs(); } void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) { @@ -166,7 +167,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(command, "password ", 9) == 0) { // change admin password StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password)); - checkAdvertInterval(); savePrefs(); sprintf(reply, "password now: %s", _prefs->password); // echo back just to let admin know for sure!! } else if (memcmp(command, "clear stats", 11) == 0) { @@ -265,7 +265,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch strcpy(reply, "OK"); } else if (memcmp(config, "name ", 5) == 0) { StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); - checkAdvertInterval(); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "repeat ", 7) == 0) { @@ -292,12 +291,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } } else if (memcmp(config, "lat ", 4) == 0) { _prefs->node_lat = atof(&config[4]); - checkAdvertInterval(); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "lon ", 4) == 0) { _prefs->node_lon = atof(&config[4]); - checkAdvertInterval(); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "rxdelay ", 8) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 1778c715..b50bf7f3 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -55,10 +55,7 @@ class CommonCLI { char tmp[80]; mesh::RTCClock* getRTCClock() { return _rtc; } - void savePrefs() { _callbacks->savePrefs(); } - - void checkAdvertInterval(); - + void savePrefs(); void loadPrefsInt(FILESYSTEM* _fs, const char* filename); public: From 112b360ef45d92b8f08ef5774d40e267f836dbfd Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 8 Jul 2025 21:07:27 +1000 Subject: [PATCH 37/60] * implemented encoding responses to REQ_TYPE_GET_AVG_MIN_MAX --- examples/simple_sensor/SensorMesh.cpp | 187 ++++++++++++++++---------- 1 file changed, 114 insertions(+), 73 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 293a503c..d457cd19 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -140,6 +140,101 @@ void SensorMesh::saveContacts() { } } +static uint8_t getDataSize(uint8_t type) { + switch (type) { + case LPP_GPS: + return 9; + case LPP_POLYLINE: + return 8; // TODO: this is MINIMIUM + case LPP_GYROMETER: + case LPP_ACCELEROMETER: + return 6; + case LPP_GENERIC_SENSOR: + case LPP_FREQUENCY: + case LPP_DISTANCE: + case LPP_ENERGY: + case LPP_UNIXTIME: + return 4; + case LPP_COLOUR: + return 3; + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + case LPP_LUMINOSITY: + case LPP_TEMPERATURE: + case LPP_CONCENTRATION: + case LPP_BAROMETRIC_PRESSURE: + case LPP_ALTITUDE: + case LPP_VOLTAGE: + case LPP_CURRENT: + case LPP_DIRECTION: + case LPP_POWER: + return 2; + } + return 1; +} + +static uint32_t getMultiplier(uint8_t type) { + switch (type) { + case LPP_CURRENT: + case LPP_DISTANCE: + case LPP_ENERGY: + return 1000; + case LPP_VOLTAGE: + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + return 100; + case LPP_TEMPERATURE: + case LPP_BAROMETRIC_PRESSURE: + return 10; + } + return 1; +} + +static bool isSigned(uint8_t type) { + return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER || + type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER; +} + +static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { + uint32_t value = 0; + for (uint8_t i = 0; i < size; i++) { + value = (value << 8) + buffer[i]; + } + + int sign = 1; + if (is_signed) { + uint32_t bit = 1ul << ((size * 8) - 1); + if ((value & bit) == bit) { + value = (bit << 1) - value; + sign = -1; + } + } + return sign * ((float) value / multiplier); +} + +static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) { + // check sign + bool sign = value < 0; + if (sign) value = -value; + + // get value to store + uint32_t v = value * multiplier; + + // format an uint32_t as if it was an int32_t + if (is_signed & sign) { + uint32_t mask = (1 << (size * 8)) - 1; + v = v & mask; + if (sign) v = mask - v + 1; + } + + // add bytes (MSB first) + for (uint8_t i=1; i<=size; i++) { + dest[size - i] = (v & 0xFF); + v >>= 8; + } + return size; +} + uint8_t SensorMesh::handleRequest(uint16_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') @@ -167,7 +262,25 @@ uint8_t SensorMesh::handleRequest(uint16_t perms, uint32_t sender_timestamp, uin } else { n = 0; } - return 0; // TODO: encode data[0..n) + + uint8_t ofs = 4; + { + uint32_t now = getRTCClock()->getCurrentTime(); + memcpy(&reply_data[ofs], &now, 4); ofs += 4; + } + + for (int i = 0; i < n; i++) { + auto d = &data[i]; + reply_data[ofs++] = d->_channel; + reply_data[ofs++] = d->_lpp_type; + uint8_t sz = getDataSize(d->_lpp_type); + uint32_t mult = getMultiplier(d->_lpp_type); + bool is_signed = isSigned(d->_lpp_type); + ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed); + ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed); + ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed); + } + return ofs; } return 0; // unknown command } @@ -604,78 +717,6 @@ void SensorMesh::setTxPower(uint8_t power_dbm) { radio_set_tx_power(power_dbm); } -static uint8_t getDataSize(uint8_t type) { - switch (type) { - case LPP_GPS: - return 9; - case LPP_POLYLINE: - return 8; // TODO: this is MINIMIUM - case LPP_GYROMETER: - case LPP_ACCELEROMETER: - return 6; - case LPP_GENERIC_SENSOR: - case LPP_FREQUENCY: - case LPP_DISTANCE: - case LPP_ENERGY: - case LPP_UNIXTIME: - return 4; - case LPP_COLOUR: - return 3; - case LPP_ANALOG_INPUT: - case LPP_ANALOG_OUTPUT: - case LPP_LUMINOSITY: - case LPP_TEMPERATURE: - case LPP_CONCENTRATION: - case LPP_BAROMETRIC_PRESSURE: - case LPP_ALTITUDE: - case LPP_VOLTAGE: - case LPP_CURRENT: - case LPP_DIRECTION: - case LPP_POWER: - return 2; - } - return 1; -} - -static uint32_t getMultiplier(uint8_t type) { - switch (type) { - case LPP_CURRENT: - case LPP_DISTANCE: - case LPP_ENERGY: - return 1000; - case LPP_VOLTAGE: - case LPP_ANALOG_INPUT: - case LPP_ANALOG_OUTPUT: - return 100; - case LPP_TEMPERATURE: - case LPP_BAROMETRIC_PRESSURE: - return 10; - } - return 1; -} - -static bool isSigned(uint8_t type) { - return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER || - type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER; -} - -static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { - uint32_t value = 0; - for (uint8_t i = 0; i < size; i++) { - value = (value << 8) + buffer[i]; - } - - int sign = 1; - if (is_signed) { - uint32_t bit = 1ul << ((size * 8) - 1); - if ((value & bit) == bit) { - value = (bit << 1) - value; - sign = -1; - } - } - return sign * ((float) value / multiplier); -} - float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) { auto buf = telemetry.getBuffer(); uint8_t size = telemetry.getSize(); From 2715058eb2fa116094bec183cf6cb6d83101a3a2 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 8 Jul 2025 21:56:26 +1000 Subject: [PATCH 38/60] * misc fixes --- examples/simple_sensor/SensorMesh.cpp | 10 ++++++++++ examples/simple_sensor/main.cpp | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index d457cd19..1ed90074 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -327,6 +327,7 @@ void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint16_t perms) memset(c, 0, sizeof(*c)); } else { c->permissions = perms; // update their permissions + self_id.calcSharedSecret(c->shared_secret, pubkey); } dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger saveContacts() } @@ -464,6 +465,15 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r strcpy(reply, "Err - bad pubkey"); } } + } else if (sender_timestamp == 0 && strcmp(command, "getperm") == 0) { + Serial.println("Permissions:"); + for (int i = 0; i < num_contacts; i++) { + auto c = &contacts[i]; + + mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE); + Serial.printf(" %04X\n", c->permissions); + } + reply[0] = 0; } else { _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 25610a69..16e14d5b 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -41,7 +41,7 @@ void halt() { while (1) ; } -static char command[80]; +static char command[120]; void setup() { Serial.begin(115200); From 541cd8cfd960618e393f8b6a51c24ba004efe773 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 8 Jul 2025 23:18:41 +1000 Subject: [PATCH 39/60] * misc --- examples/simple_sensor/SensorMesh.cpp | 13 ------------- examples/simple_sensor/SensorMesh.h | 1 - 2 files changed, 14 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 1ed90074..48fd10fa 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -521,19 +521,6 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { } } -void SensorMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) { - mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl -#if 0 - // if this a zero hop advert, add it to neighbours - if (packet->path_len == 0) { - AdvertDataParser parser(app_data, app_data_len); - if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters - putNeighbour(id, timestamp, packet->getSNR()); - } - } -#endif -} - void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) { int i = matching_peer_indexes[sender_idx]; if (i < 0 || i >= num_contacts) { diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 05104c99..00234355 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -121,7 +121,6 @@ protected: void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; - void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override; void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override; bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; From 7d4760898515f9815adbdae5b7e93d66a3432d8e Mon Sep 17 00:00:00 2001 From: jasper Date: Tue, 8 Jul 2025 21:16:03 +0200 Subject: [PATCH 40/60] Changed the Barometric Pressure value since it was a factor 100 to high --- src/helpers/sensors/EnvironmentSensorManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index a424c46b..ab5b459e 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -154,7 +154,7 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen if (BME280_initialized) { telemetry.addTemperature(TELEM_CHANNEL_SELF, BME280.readTemperature()); telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME280.readHumidity()); - telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()); + telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()/100); telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA)); } #endif From 797ab85283218b690d8b6c2b946b983b2f64fa7c Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 9 Jul 2025 15:50:36 +1000 Subject: [PATCH 41/60] * sensor node: now have two alert priorities, LO, HI --- examples/simple_sensor/SensorMesh.cpp | 11 ++++++----- examples/simple_sensor/SensorMesh.h | 9 +++++---- examples/simple_sensor/main.cpp | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 48fd10fa..61444b2f 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -332,13 +332,14 @@ void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint16_t perms) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger saveContacts() } -void SensorMesh::sendAlert(const char* text) { +void SensorMesh::sendAlert(AlertPriority pri, const char* text) { int text_len = strlen(text); + uint16_t pri_mask = (pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO; // send text message to all contacts with RECV_ALERT permission for (int i = 0; i < num_contacts; i++) { auto c = &contacts[i]; - if ((c->permissions & PERM_RECV_ALERTS) == 0) continue; // contact does NOT want alerts + if ((c->permissions & pri_mask) == 0) continue; // contact does NOT want alert uint8_t data[MAX_PACKET_PAYLOAD]; uint32_t now = getRTCClock()->getCurrentTimeUnique(); // need different timestamp per packet @@ -360,12 +361,12 @@ void SensorMesh::sendAlert(const char* text) { } } -void SensorMesh::alertIf(bool condition, Trigger& t, const char* text) { +void SensorMesh::alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text) { if (condition) { if (!t.triggered) { t.triggered = true; t.time = getRTCClock()->getCurrentTime(); - sendAlert(text); + sendAlert(pri, text); } } else { if (t.triggered) { @@ -422,7 +423,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* MESH_DEBUG_PRINTLN("Login success!"); client->last_timestamp = sender_timestamp; client->last_activity = getRTCClock()->getCurrentTime(); - client->permissions = PERM_IS_ADMIN | PERM_RECV_ALERTS; + client->permissions = PERM_IS_ADMIN | PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO; // initially opt-in to receive alerts (can opt out) memcpy(client->shared_secret, secret, PUB_KEY_SIZE); dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 00234355..e92c163f 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -26,7 +26,8 @@ #define PERM_IS_ADMIN 0x8000 #define PERM_GET_TELEMETRY 0x0001 #define PERM_GET_MIN_MAX_AVG 0x0002 -#define PERM_RECV_ALERTS 0x0100 +#define PERM_RECV_ALERTS_LO 0x0100 // low priority alerts +#define PERM_RECV_ALERTS_HI 0x0200 // high priority alerts struct ContactInfo { mesh::Identity id; @@ -104,8 +105,8 @@ protected: Trigger() { triggered = false; time = 0; } }; - - void alertIf(bool condition, Trigger& t, const char* text); + enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT }; + void alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text); virtual void onSensorDataRead() = 0; // for app to implement virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0; // for app to implement @@ -145,6 +146,6 @@ private: ContactInfo* putContact(const mesh::Identity& id); void applyContactPermissions(const uint8_t* pubkey, uint16_t perms); - void sendAlert(const char* text); + void sendAlert(AlertPriority pri, const char* text); }; diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 16e14d5b..792790d8 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -22,7 +22,7 @@ protected: float batt_voltage = getVoltage(TELEM_CHANNEL_SELF); battery_data.recordData(getRTCClock(), batt_voltage); // record battery - alertIf(batt_voltage < 3.4f, low_batt, "Battery low!"); + alertIf(batt_voltage < 3.4f, low_batt, HIGH_PRI_ALERT, "Battery low!"); } int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override { From 781f7e99f6dabb51fce1c5cc9445463cf9c08db2 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 9 Jul 2025 23:10:33 +1000 Subject: [PATCH 42/60] * companion: added CMD_GET_TUNING_PARAMS -> RESP_CODE_TUNING_PARAMS --- examples/companion_radio/MyMesh.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6ddc19d6..c230cb8a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -45,6 +45,7 @@ #define CMD_GET_CUSTOM_VARS 40 #define CMD_SET_CUSTOM_VAR 41 #define CMD_GET_ADVERT_PATH 42 +#define CMD_GET_TUNING_PARAMS 43 #define RESP_CODE_OK 0 #define RESP_CODE_ERR 1 @@ -69,6 +70,7 @@ #define RESP_CODE_SIGNATURE 20 #define RESP_CODE_CUSTOM_VARS 21 #define RESP_CODE_ADVERT_PATH 22 +#define RESP_CODE_TUNING_PARAMS 23 #define SEND_TIMEOUT_BASE_MILLIS 500 #define FLOOD_SEND_TIMEOUT_FACTOR 16.0f @@ -1016,6 +1018,13 @@ void MyMesh::handleCmdFrame(size_t len) { _prefs.airtime_factor = ((float)af) / 1000.0f; savePrefs(); writeOKFrame(); + } else if (cmd_frame[0] == CMD_GET_TUNING_PARAMS) { + uint32_t rx = _prefs.rx_delay_base * 1000, af = _prefs.airtime_factor * 1000; + int i = 0; + out_frame[i++] = RESP_CODE_TUNING_PARAMS; + memcpy(&out_frame[i], &rx, 4); i += 4; + memcpy(&out_frame[i], &af, 4); i += 4; + _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) { _prefs.manual_add_contacts = cmd_frame[1]; if (len >= 3) { From 5f7bd0fe7765cb8385c2839d42947ac9000cf748 Mon Sep 17 00:00:00 2001 From: Florent Date: Wed, 9 Jul 2025 17:22:31 +0200 Subject: [PATCH 43/60] wio-e5-mini: simple_sensor target --- variants/wio-e5-mini/platformio.ini | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/variants/wio-e5-mini/platformio.ini b/variants/wio-e5-mini/platformio.ini index dfe4a090..93508d8e 100644 --- a/variants/wio-e5-mini/platformio.ini +++ b/variants/wio-e5-mini/platformio.ini @@ -25,6 +25,15 @@ build_flags = ${lora_e5_mini.build_flags} build_src_filter = ${lora_e5_mini.build_src_filter} +<../examples/simple_repeater/main.cpp> +[env:wio-e5-mini-sensor] +extends = lora_e5_mini +build_flags = ${lora_e5_mini.build_flags} + -D LORA_TX_POWER=22 + -D ADVERT_NAME='"wio-e5-mini Sensor"' + -D ADMIN_PASSWORD='"password"' +build_src_filter = ${lora_e5_mini.build_src_filter} + +<../examples/simple_sensor> + [env:wio-e5-mini_companion_radio_usb] extends = lora_e5_mini build_flags = ${lora_e5_mini.build_flags} From 9d0dd7947f32703be531311665bf06f323625cc2 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 9 Jul 2025 10:21:24 -0700 Subject: [PATCH 44/60] move rak4631 specific files into variant folder --- {src/helpers/nrf52 => variants/rak4631}/RAK4631Board.cpp | 0 {src/helpers/nrf52 => variants/rak4631}/RAK4631Board.h | 0 variants/rak4631/platformio.ini | 1 - variants/rak4631/target.h | 2 +- 4 files changed, 1 insertion(+), 2 deletions(-) rename {src/helpers/nrf52 => variants/rak4631}/RAK4631Board.cpp (100%) rename {src/helpers/nrf52 => variants/rak4631}/RAK4631Board.h (100%) diff --git a/src/helpers/nrf52/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp similarity index 100% rename from src/helpers/nrf52/RAK4631Board.cpp rename to variants/rak4631/RAK4631Board.cpp diff --git a/src/helpers/nrf52/RAK4631Board.h b/variants/rak4631/RAK4631Board.h similarity index 100% rename from src/helpers/nrf52/RAK4631Board.h rename to variants/rak4631/RAK4631Board.h diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 86879ed7..5fcdb3d0 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -15,7 +15,6 @@ build_flags = ${nrf52840_base.build_flags} -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 build_src_filter = ${nrf52840_base.build_src_filter} - + +<../variants/rak4631> lib_deps = ${nrf52840_base.lib_deps} diff --git a/variants/rak4631/target.h b/variants/rak4631/target.h index 3f26ab33..e1545581 100644 --- a/variants/rak4631/target.h +++ b/variants/rak4631/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include From cdd44212a128bbd6c1b4d18903f225eb3f92eedc Mon Sep 17 00:00:00 2001 From: Normunds Gavars Date: Thu, 10 Jul 2025 00:08:59 +0300 Subject: [PATCH 45/60] 393 clean up Promicro variant --- {src/helpers/nrf52 => variants/promicro}/PromicroBoard.cpp | 0 {src/helpers/nrf52 => variants/promicro}/PromicroBoard.h | 0 variants/promicro/platformio.ini | 2 -- variants/promicro/target.h | 2 +- 4 files changed, 1 insertion(+), 3 deletions(-) rename {src/helpers/nrf52 => variants/promicro}/PromicroBoard.cpp (100%) rename {src/helpers/nrf52 => variants/promicro}/PromicroBoard.h (100%) diff --git a/src/helpers/nrf52/PromicroBoard.cpp b/variants/promicro/PromicroBoard.cpp similarity index 100% rename from src/helpers/nrf52/PromicroBoard.cpp rename to variants/promicro/PromicroBoard.cpp diff --git a/src/helpers/nrf52/PromicroBoard.h b/variants/promicro/PromicroBoard.h similarity index 100% rename from src/helpers/nrf52/PromicroBoard.h rename to variants/promicro/PromicroBoard.h diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 246019c2..4e5edc22 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -23,7 +23,6 @@ build_flags = ${nrf52_base.build_flags} -D ENV_INCLUDE_INA3221=1 -D ENV_INCLUDE_INA219=1 build_src_filter = ${nrf52_base.build_src_filter} - + + +<../variants/promicro> lib_deps= ${nrf52_base.lib_deps} @@ -130,7 +129,6 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_RX_BOOSTED_GAIN=1 build_src_filter = ${nrf52_base.build_src_filter} - + + +<../variants/promicro> lib_deps= ${nrf52_base.lib_deps} diff --git a/variants/promicro/target.h b/variants/promicro/target.h index c634d18a..7646d459 100644 --- a/variants/promicro/target.h +++ b/variants/promicro/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include From ed7ca6fb605c4f453a833e381f6576abdcc3ac41 Mon Sep 17 00:00:00 2001 From: Normunds Gavars Date: Thu, 10 Jul 2025 00:55:38 +0300 Subject: [PATCH 46/60] 393 clean up Minewsemi ME25LS01 variant --- .../minewsemi_me25ls01}/MinewsemiME25LS01Board.cpp | 0 .../minewsemi_me25ls01}/MinewsemiME25LS01Board.h | 0 variants/minewsemi_me25ls01/platformio.ini | 1 - variants/minewsemi_me25ls01/target.h | 2 +- 4 files changed, 1 insertion(+), 2 deletions(-) rename {src/helpers/nrf52 => variants/minewsemi_me25ls01}/MinewsemiME25LS01Board.cpp (100%) rename {src/helpers/nrf52 => variants/minewsemi_me25ls01}/MinewsemiME25LS01Board.h (100%) diff --git a/src/helpers/nrf52/MinewsemiME25LS01Board.cpp b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.cpp similarity index 100% rename from src/helpers/nrf52/MinewsemiME25LS01Board.cpp rename to variants/minewsemi_me25ls01/MinewsemiME25LS01Board.cpp diff --git a/src/helpers/nrf52/MinewsemiME25LS01Board.h b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h similarity index 100% rename from src/helpers/nrf52/MinewsemiME25LS01Board.h rename to variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h diff --git a/variants/minewsemi_me25ls01/platformio.ini b/variants/minewsemi_me25ls01/platformio.ini index 302e695f..f7265af4 100644 --- a/variants/minewsemi_me25ls01/platformio.ini +++ b/variants/minewsemi_me25ls01/platformio.ini @@ -34,7 +34,6 @@ build_flags = ${nrf52840_me25ls01.build_flags} -D ENV_INCLUDE_INA219=1 build_src_filter = ${nrf52840_me25ls01.build_src_filter} + - + +<../variants/minewsemi_me25ls01> + debug_tool = jlink diff --git a/variants/minewsemi_me25ls01/target.h b/variants/minewsemi_me25ls01/target.h index aad55757..db832f91 100644 --- a/variants/minewsemi_me25ls01/target.h +++ b/variants/minewsemi_me25ls01/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include From 78cd655789a507ae3a5ddb1ed78a6fd7e07bd951 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 11 Jul 2025 01:08:38 +1000 Subject: [PATCH 47/60] Seeed Wio Tracker L1: initial support --- boards/seeed-wio-tracker-l1.json | 61 ++++++++ variants/wio-tracker-l1/WioTrackerL1Board.cpp | 96 ++++++++++++ variants/wio-tracker-l1/WioTrackerL1Board.h | 42 +++++ variants/wio-tracker-l1/platformio.ini | 92 +++++++++++ variants/wio-tracker-l1/target.cpp | 146 ++++++++++++++++++ variants/wio-tracker-l1/target.h | 47 ++++++ variants/wio-tracker-l1/variant.cpp | 73 +++++++++ variants/wio-tracker-l1/variant.h | 102 ++++++++++++ 8 files changed, 659 insertions(+) create mode 100644 boards/seeed-wio-tracker-l1.json create mode 100644 variants/wio-tracker-l1/WioTrackerL1Board.cpp create mode 100644 variants/wio-tracker-l1/WioTrackerL1Board.h create mode 100644 variants/wio-tracker-l1/platformio.ini create mode 100644 variants/wio-tracker-l1/target.cpp create mode 100644 variants/wio-tracker-l1/target.h create mode 100644 variants/wio-tracker-l1/variant.cpp create mode 100644 variants/wio-tracker-l1/variant.h diff --git a/boards/seeed-wio-tracker-l1.json b/boards/seeed-wio-tracker-l1.json new file mode 100644 index 00000000..3602baab --- /dev/null +++ b/boards/seeed-wio-tracker-l1.json @@ -0,0 +1,61 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_SEEED_WIO_TRACKER_L1 -DNRF52840_XXAA -DSEEED_WIO_TRACKER_L1 ", + "f_cpu": "64000000L", + "hwids": [ + [ "0x2886", "0x1667" ], + [ "0x2886", "0x1668" ] + ], + "mcu": "nrf52840", + "variant": "Seeed_Wio_Tracker_L1", + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bsp": { + "name": "adafruit" + }, + "bootloader": { + "settings_addr": "0xFF000" + }, + "usb_product": "Seeed Wio Tracker L1" + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "openocd_target": "nrf52.cfg", + "svd_path": "nrf52840.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "Seeed Wio Tracker L1", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "protocol": "nrfutil", + "speed": 115200, + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "cmsis-dap", + "sam-ba", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://wiki.seeedstudio.com/wio_tracker_l1_node/", + "vendor": "Seeed Studio" +} \ No newline at end of file diff --git a/variants/wio-tracker-l1/WioTrackerL1Board.cpp b/variants/wio-tracker-l1/WioTrackerL1Board.cpp new file mode 100644 index 00000000..c5c9db65 --- /dev/null +++ b/variants/wio-tracker-l1/WioTrackerL1Board.cpp @@ -0,0 +1,96 @@ +#include +#include "WioTrackerL1Board.h" + +#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 WioTrackerL1Board::begin() { + // for future use, sub-classes SHOULD call this from their begin() + startup_reason = BD_STARTUP_NORMAL; + 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); + pinMode(PIN_BUTTON5, INPUT_PULLUP); + pinMode(PIN_BUTTON6, INPUT_PULLUP); + + + #if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) + Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL); + #endif + + Wire.begin(); + + #ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); + #endif + + delay(10); // give sx1262 some time to power up +} + +bool WioTrackerL1Board::startOTAUpdate(const char* id, char reply[]) { + // Config the peripheral connection with maximum bandwidth + // more SRAM required by SoftDevice + // Note: All config***() function must be called before begin() + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); + + Bluefruit.begin(1, 0); + // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 + Bluefruit.setTxPower(4); + // Set the BLE device name + Bluefruit.setName("WioTrackerL1 OTA"); + + Bluefruit.Periph.setConnectCallback(connect_callback); + Bluefruit.Periph.setDisconnectCallback(disconnect_callback); + + // To be consistent OTA DFU should be added first if it exists + bledfu.begin(); + + // Set up and start advertising + // Advertising packet + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + Bluefruit.Advertising.addName(); + + /* Start Advertising + - Enable auto advertising if disconnected + - Interval: fast mode = 20 ms, slow mode = 152.5 ms + - Timeout for fast mode is 30 seconds + - Start(timeout) with timeout = 0 will advertise forever (until connected) + + For recommended advertising interval + https://developer.apple.com/library/content/qa/qa1931/_index.html + */ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + + uint8_t mac_addr[6]; + memset(mac_addr, 0, sizeof(mac_addr)); + Bluefruit.getAddr(mac_addr); + sprintf(reply, "OK - mac: %02X:%02X:%02X:%02X:%02X:%02X", + mac_addr[5], mac_addr[4], mac_addr[3], mac_addr[2], mac_addr[1], mac_addr[0]); + + return true; +} diff --git a/variants/wio-tracker-l1/WioTrackerL1Board.h b/variants/wio-tracker-l1/WioTrackerL1Board.h new file mode 100644 index 00000000..03aef79c --- /dev/null +++ b/variants/wio-tracker-l1/WioTrackerL1Board.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +class WioTrackerL1Board : public mesh::MainBoard { +protected: + uint8_t startup_reason; + uint8_t btn_prev_state; + +public: + void begin(); + uint8_t getStartupReason() const override { return startup_reason; } + +#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); + delay(10); + adcvalue = analogRead(PIN_VBAT_READ); + return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return "Seeed Wio Tracker L1"; + } + + void reboot() override { + NVIC_SystemReset(); + } + + bool startOTAUpdate(const char* id, char reply[]) override; +}; diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini new file mode 100644 index 00000000..380ff90f --- /dev/null +++ b/variants/wio-tracker-l1/platformio.ini @@ -0,0 +1,92 @@ +[WioTrackerL1] +extends = nrf52_base +board = seeed-wio-tracker-l1 +board_build.ldscript = boards/nrf52840_s140_v7.ld +build_flags = ${nrf52_base.build_flags} + -I lib/nrf52/s140_nrf52_7.3.0_API/include + -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 + -I variants/wio-tracker-l1 + -D WIO_TRACKER_L1 + -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_OLED_RESET=-1 + ; -D MESH_DEBUG=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + +<../variants/wio-tracker-l1> + + + + +lib_deps= ${nrf52_base.lib_deps} + adafruit/Adafruit SH110X @ ^2.1.13 + adafruit/Adafruit GFX Library @ ^1.12.1 + stevemarple/MicroNMEA @ ^2.0.6 + +[env:WioTrackerL1_Repeater] +extends = WioTrackerL1 +build_src_filter = ${WioTrackerL1.build_src_filter} + +<../examples/simple_repeater> +build_flags = + ${WioTrackerL1.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Repeater"' + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1_room_server] +extends = WioTrackerL1 +build_src_filter = ${WioTrackerL1.build_src_filter} + +<../examples/simple_room_server> +build_flags = ${WioTrackerL1.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Room"' + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1_companion_radio_usb] +extends = WioTrackerL1 +build_flags = ${WioTrackerL1.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D DISPLAY_CLASS=SH1106Display +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${WioTrackerL1.build_src_filter} + +<../examples/companion_radio> + + + + +lib_deps = ${WioTrackerL1.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:WioTrackerL1_companion_radio_ble] +extends = WioTrackerL1 +build_flags = ${WioTrackerL1.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 + -D PIN_BUZZER=12 +build_src_filter = ${WioTrackerL1.build_src_filter} + + + +<../examples/companion_radio> + + +lib_deps = ${WioTrackerL1.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 diff --git a/variants/wio-tracker-l1/target.cpp b/variants/wio-tracker-l1/target.cpp new file mode 100644 index 00000000..0809e19e --- /dev/null +++ b/variants/wio-tracker-l1/target.cpp @@ -0,0 +1,146 @@ +#include +#include "target.h" +#include +#include + +WioTrackerL1Board 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); +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); +WioTrackerL1SensorManager sensors = WioTrackerL1SensorManager(nmea); + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#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); +} + +void WioTrackerL1SensorManager::start_gps() +{ + if (!gps_active) + { + MESH_DEBUG_PRINTLN("starting GPS"); + digitalWrite(PIN_GPS_STANDBY, HIGH); + gps_active = true; + } +} + +void WioTrackerL1SensorManager::stop_gps() +{ + if (gps_active) + { + MESH_DEBUG_PRINTLN("stopping GPS"); + digitalWrite(PIN_GPS_STANDBY, LOW); + gps_active = false; + } +} + +bool WioTrackerL1SensorManager::begin() +{ + Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX); // be sure to tx into rx and rx into tx + Serial1.begin(GPS_BAUDRATE); + + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, HIGH); // Wake GPS from standby + delay(500); + + // We'll consider GPS detected if we see any data on Serial1 + if (Serial1.available() > 0) + { + MESH_DEBUG_PRINTLN("GPS detected"); + } + else + { + MESH_DEBUG_PRINTLN("No GPS detected"); + } + digitalWrite(PIN_GPS_STANDBY, LOW); // Put GPS back into standby mode + return true; +} + +bool WioTrackerL1SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP &telemetry) +{ + if (requester_permissions & TELEM_PERM_LOCATION) + { // does requester have permission? + telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); + } + return true; +} + +void WioTrackerL1SensorManager::loop() +{ + static long next_gps_update = 0; + _location->loop(); + if (millis() > next_gps_update && gps_active) // don't bother if gps position is not enabled + { + if (_location->isValid()) + { + node_lat = ((double)_location->getLatitude()) / 1000000.; + node_lon = ((double)_location->getLongitude()) / 1000000.; + node_altitude = ((double)_location->getAltitude()) / 1000.0; + MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + } + next_gps_update = millis() + (1000 * 60); // after initial update, only check every minute TODO: should be configurable + } +} + +int WioTrackerL1SensorManager::getNumSettings() const { return 1; } // just one supported: "gps" (power switch) + +const char *WioTrackerL1SensorManager::getSettingName(int i) const +{ + return i == 0 ? "gps" : NULL; +} + +const char *WioTrackerL1SensorManager::getSettingValue(int i) const +{ + if (i == 0) + { + return gps_active ? "1" : "0"; + } + return NULL; +} + +bool WioTrackerL1SensorManager::setSettingValue(const char *name, const char *value) +{ + if (strcmp(name, "gps") == 0) + { + if (strcmp(value, "0") == 0) + { + stop_gps(); + } + else + { + start_gps(); + } + return true; + } + return false; // not supported +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/wio-tracker-l1/target.h b/variants/wio-tracker-l1/target.h new file mode 100644 index 00000000..0aac6c59 --- /dev/null +++ b/variants/wio-tracker-l1/target.h @@ -0,0 +1,47 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include +#endif +#include + +class WioTrackerL1SensorManager : public SensorManager +{ + bool gps_active = false; + LocationProvider *_location; + + void start_gps(); + void stop_gps(); + +public: + WioTrackerL1SensorManager(LocationProvider &location) : _location(&location) {} + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP &telemetry) override; + void loop() override; + int getNumSettings() const override; + const char *getSettingName(int i) const override; + const char *getSettingValue(int i) const override; + bool setSettingValue(const char *name, const char *value) override; +}; + + +extern WioTrackerL1Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern WioTrackerL1SensorManager sensors; +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#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/wio-tracker-l1/variant.cpp b/variants/wio-tracker-l1/variant.cpp new file mode 100644 index 00000000..3db5ec9a --- /dev/null +++ b/variants/wio-tracker-l1/variant.cpp @@ -0,0 +1,73 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" +#include "nrf.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 41, // D0 P1.09 GNSS_WAKEUP + 7, // D1 P0.07 LORA_DIO1 + 39, // D2 P1,07 LORA_RESET + 42, // D3 P1.10 LORA_BUSY + 46, // D4 P1.14 (A4/SDA) LORA_CS + 40, // D5 P1.08 (A5/SCL) LORA_SW + 27, // D6 P0.27 (UART_TX) GNSS_TX + 26, // D7 P0.26 (UART_RX) GNSS_RX + 30, // D8 P0.30 (SPI_SCK) LORA_SCK + 3, // D9 P0.3 (SPI_MISO) LORA_MISO + 28, // D10 P0.28 (SPI_MOSI) LORA_MOSI + + // D11-D12 - LED outputs + 33, // D11 P1.1 User LED + // Buzzzer + 32, // D12 P1.0 Buzzer + + // D13 - User input + 8, // D13 P0.08 User Button + + // D14-D15 - OLED + 6, // D14 P0.06 OLED SDA + 5, // D15 P0.05 OLED SCL + + // D16 - Battery voltage ADC input + 31, // D16 P0.31 VBAT_ADC + // GROVE + 43, // D17 P0.00 GROVE SDA + 44, // D18 P0.01 GROVE SCL + + // FLASH + 21, // D19 P0.21 (QSPI_SCK) + 25, // D20 P0.25 (QSPI_CSN) + 20, // D21 P0.20 (QSPI_SIO_0 DI) + 24, // D22 P0.24 (QSPI_SIO_1 DO) + 22, // D23 P0.22 (QSPI_SIO_2 WP) + 23, // D24 P0.23 (QSPI_SIO_3 HOLD) + + // JOYSTICK + 36, // D25 TB_UP + 12, // D26 TB_DOWN + 11, // D27 TB_LEFT + 35, // D28 TB_RIGHT + 37, // D29 TB_PRESS + + // VBAT ENABLE + 4, // D30 BAT_CTL +}; + +void initVariant() { + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + + // VBAT_ENABLE + pinMode(VBAT_ENABLE, OUTPUT); + digitalWrite(VBAT_ENABLE, HIGH); + + // set LED pin as output and set it low + pinMode(PIN_LED, OUTPUT); + digitalWrite(PIN_LED, LOW); + + // set buzzer pin as output and set it low + pinMode(12, OUTPUT); + digitalWrite(12, LOW); + pinMode(12, OUTPUT); +} diff --git a/variants/wio-tracker-l1/variant.h b/variants/wio-tracker-l1/variant.h new file mode 100644 index 00000000..7f51dcb8 --- /dev/null +++ b/variants/wio-tracker-l1/variant.h @@ -0,0 +1,102 @@ +#ifndef _SEEED_WIO_TRACKER_L1_H_ +#define _SEEED_WIO_TRACKER_L1_H_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#define PINS_COUNT (33) +#define NUM_DIGITAL_PINS (33) +#define NUM_ANALOG_INPUTS (8) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED (11) +#define LED_BLUE (-1) // Disable annoying flashing caused by Bluefruit +#define LED_BUILTIN PIN_LED +#define P_LORA_TX_LED PIN_LED +#define LED_STATE_ON 1 + +// Buttons +#define PIN_BUTTON1 (13) // Menu / User Button +#define PIN_BUTTON2 (25) // Joystick Up +#define PIN_BUTTON3 (26) // Joystick Down +#define PIN_BUTTON4 (27) // Joystick Left +#define PIN_BUTTON5 (28) // Joystick Right +#define PIN_BUTTON6 (28) // Joystick Press +#define PIN_USER_BTN PIN_BUTTON1 +#define JOYSTICK_UP PIN_BUTTON2 +#define JOYSTICK_DOWN PIN_BUTTON3 +#define JOYSTICK_LEFT PIN_BUTTON4 +#define JOYSTICK_RIGHT PIN_BUTTON5 +#define JOYSTICK_PRESS PIN_BUTTON6 + +// Buzzer +// #define PIN_BUZZER (12) // Buzzer pin (defined per firmware type) + +#define VBAT_ENABLE (30) + +// Analog pins +#define PIN_VBAT_READ (16) +#define AREF_VOLTAGE (3.6F) +#define ADC_MULTIPLIER (2.0F) +#define ADC_RESOLUTION (12) + +// Serial interfaces +#define PIN_SERIAL1_RX (7) +#define PIN_SERIAL1_TX (6) + +// SPI Interfaces +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (9) +#define PIN_SPI_MOSI (10) +#define PIN_SPI_SCK (8) + +// Lora 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 (1) +#define P_LORA_RESET (2) +#define P_LORA_BUSY (3) +#define P_LORA_NSS (4) +#define SX126X_RXEN (5) +#define SX126X_TXEN RADIOLIB_NC + +// Wire Interfaces +#define WIRE_INTERFACES_COUNT (2) + +#define PIN_WIRE_SDA (14) +#define PIN_WIRE_SCL (15) +#define PIN_WIRE1_SDA (17) +#define PIN_WIRE1_SCL (18) +#define I2C_NO_RESCAN +#define DISPLAY_ADDRESS 0x3D // SH1106 OLED I2C address + +// GPS L76KB +#define GPS_BAUDRATE 9600 +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_STANDBY (0) +#define PIN_GPS_EN (18) + +// QSPI Pins +#define PIN_QSPI_SCK (21) +#define PIN_QSPI_CS (22) +#define PIN_QSPI_IO0 (23) +#define PIN_QSPI_IO1 (24) +#define PIN_QSPI_IO2 (25) +#define PIN_QSPI_IO3 (26) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +#endif \ No newline at end of file From 0d1b5b17d3dcebbe79c731c62bad444a59e2a450 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 12 Jul 2025 12:26:16 +1000 Subject: [PATCH 48/60] * simple_sensor: added alert send queue, with retries, checks for ACKs, etc. Low pri alerts only 1 send attempt, otherwise 4 attempts --- examples/simple_sensor/SensorMesh.cpp | 130 +++++++++++++++++----- examples/simple_sensor/SensorMesh.h | 27 +++-- examples/simple_sensor/TimeSeriesData.cpp | 4 +- examples/simple_sensor/TimeSeriesData.h | 2 +- examples/simple_sensor/main.cpp | 7 +- 5 files changed, 126 insertions(+), 44 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 61444b2f..d542407d 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -58,6 +58,8 @@ #define LAZY_CONTACTS_WRITE_DELAY 5000 +#define ALERT_ACK_EXPIRY_MILLIS 6000 // wait 6 secs for ACKs to alert messages + static File openAppend(FILESYSTEM* _fs, const char* fname) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) return _fs->open(fname, FILE_O_WRITE); @@ -163,6 +165,7 @@ static uint8_t getDataSize(uint8_t type) { case LPP_TEMPERATURE: case LPP_CONCENTRATION: case LPP_BAROMETRIC_PRESSURE: + case LPP_RELATIVE_HUMIDITY: case LPP_ALTITUDE: case LPP_VOLTAGE: case LPP_CURRENT: @@ -185,6 +188,7 @@ static uint32_t getMultiplier(uint8_t type) { return 100; case LPP_TEMPERATURE: case LPP_BAROMETRIC_PRESSURE: + case LPP_RELATIVE_HUMIDITY: return 10; } return 1; @@ -332,46 +336,54 @@ void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint16_t perms) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger saveContacts() } -void SensorMesh::sendAlert(AlertPriority pri, const char* text) { - int text_len = strlen(text); - uint16_t pri_mask = (pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO; +void SensorMesh::sendAlert(ContactInfo* c, Trigger* t) { + int text_len = strlen(t->text); - // send text message to all contacts with RECV_ALERT permission - for (int i = 0; i < num_contacts; i++) { - auto c = &contacts[i]; - if ((c->permissions & pri_mask) == 0) continue; // contact does NOT want alert + uint8_t data[MAX_PACKET_PAYLOAD]; + memcpy(data, &t->timestamp, 4); + data[4] = (TXT_TYPE_PLAIN << 2) | t->attempt; // attempt and flags + memcpy(&data[5], t->text, text_len); - uint8_t data[MAX_PACKET_PAYLOAD]; - uint32_t now = getRTCClock()->getCurrentTimeUnique(); // need different timestamp per packet - memcpy(data, &now, 4); - data[4] = (TXT_TYPE_PLAIN << 2); // attempt and flags - memcpy(&data[5], text, text_len); - // calc expected ACK reply - // uint32_t expected_ack; - // mesh::Utils::sha256((uint8_t *)&expected_ack, 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE); + // calc expected ACK reply + mesh::Utils::sha256((uint8_t *)&t->expected_acks[t->attempt], 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE); + t->attempt++; - auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len); - if (pkt) { - if (c->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(pkt, c->out_path, c->out_path_len); - } else { - sendFlood(pkt); - } + auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len); + if (pkt) { + if (c->out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(pkt, c->out_path, c->out_path_len); + } else { + sendFlood(pkt); } } + t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS); } void SensorMesh::alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text) { if (condition) { - if (!t.triggered) { - t.triggered = true; - t.time = getRTCClock()->getCurrentTime(); - sendAlert(pri, text); + if (!t.isTriggered() && num_alert_tasks < MAX_CONCURRENT_ALERTS) { + StrHelper::strncpy(t.text, text, sizeof(t.text)); + t.pri = pri; + t.send_expiry = 0; // signal that initial send is needed + t.attempt = 4; + t.curr_contact_idx = -1; // start iterating thru contacts[] + + alert_tasks[num_alert_tasks++] = &t; // add to queue } } else { - if (t.triggered) { - t.triggered = false; - // TODO: apply debounce logic + if (t.isTriggered()) { + t.text[0] = 0; + // remove 't' from alert queue + int i = 0; + while (i < num_alert_tasks && alert_tasks[i] != &t) i++; + + if (i < num_alert_tasks) { // found, now delete from array + num_alert_tasks--; + while (i < num_alert_tasks) { + alert_tasks[i] = alert_tasks[i + 1]; + i++; + } + } } } } @@ -629,6 +641,20 @@ bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint return false; } +void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { + if (num_alert_tasks > 0) { + auto t = alert_tasks[0]; // check current alert task + for (int i = 0; i < t->attempt; i++) { + if (ack_crc == t->expected_acks[i]) { // matching ACK! + t->attempt = 4; // signal to move to next contact + t->send_expiry = 0; + packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit + return; + } + } + } +} + 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, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) @@ -637,6 +663,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; last_read_time = 0; + num_alert_tasks = 0; // defaults memset(&_prefs, 0, sizeof(_prefs)); @@ -736,7 +763,14 @@ float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) { } bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) { - return false; // TODO + if (channel == TELEM_CHANNEL_SELF) { + lat = sensors.node_lat; + lon = sensors.node_lon; + alt = sensors.node_altitude; + return true; + } + // REVISIT: custom GPS channels?? + return false; } void SensorMesh::loop() { @@ -767,6 +801,42 @@ void SensorMesh::loop() { last_read_time = curr; } + // check the alert send queue + if (num_alert_tasks > 0) { + auto t = alert_tasks[0]; // process head of queue + + if (millisHasNowPassed(t->send_expiry)) { // next send needed? + if (t->attempt >= 4) { // max attempts reached, try next contact + t->curr_contact_idx++; + if (t->curr_contact_idx >= num_contacts) { // no more contacts to try? + num_alert_tasks--; // remove t from queue + for (int i = 0; i < num_alert_tasks; i++) { + alert_tasks[i] = alert_tasks[i + 1]; + } + } else { + auto c = &contacts[t->curr_contact_idx]; + uint16_t pri_mask = (t->pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO; + + if (c->permissions & pri_mask) { // contact wants alert + // reset attempts + t->attempt = (t->pri == LOW_PRI_ALERT) ? 3 : 0; // Low pri alerts, start at attempt #3 (ie. only make ONE attempt) + t->timestamp = getRTCClock()->getCurrentTimeUnique(); // need unique timestamp per contact + + sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry + } else { + // next contact tested in next ::loop() + } + } + } else if (t->curr_contact_idx < num_contacts) { + auto c = &contacts[t->curr_contact_idx]; // send next attempt + sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry + } else { + // contact list has likely been modified while waiting for alert ACK, cancel this task + t->attempt = 4; // next ::loop() will remove t from queue + } + } + } + // is there are pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { saveContacts(); diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index e92c163f..e0527fa0 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -55,7 +55,8 @@ struct ContactInfo { #define MAX_CONTACTS 32 #endif -#define MAX_SEARCH_RESULTS 8 +#define MAX_SEARCH_RESULTS 8 +#define MAX_CONCURRENT_ALERTS 4 class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { public: @@ -99,13 +100,20 @@ protected: bool getGPS(uint8_t channel, float& lat, float& lon, float& alt); // alerts - struct Trigger { - bool triggered; - uint32_t time; - - Trigger() { triggered = false; time = 0; } - }; enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT }; + + struct Trigger { + uint32_t timestamp; + AlertPriority pri; + uint32_t expected_acks[4]; + int8_t curr_contact_idx; + uint8_t attempt; + unsigned long send_expiry; + char text[MAX_PACKET_PAYLOAD]; + + Trigger() { text[0] = 0; } + bool isTriggered() const { return text[0] != 0; } + }; void alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text); virtual void onSensorDataRead() = 0; // for app to implement @@ -124,6 +132,7 @@ protected: void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override; bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; + void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override; private: FILESYSTEM* _fs; @@ -137,6 +146,8 @@ private: CayenneLPP telemetry; uint32_t last_read_time; int matching_peer_indexes[MAX_SEARCH_RESULTS]; + int num_alert_tasks; + Trigger* alert_tasks[MAX_CONCURRENT_ALERTS]; void loadContacts(); void saveContacts(); @@ -146,6 +157,6 @@ private: ContactInfo* putContact(const mesh::Identity& id); void applyContactPermissions(const uint8_t* pubkey, uint16_t perms); - void sendAlert(AlertPriority pri, const char* text); + void sendAlert(ContactInfo* c, Trigger* t); }; diff --git a/examples/simple_sensor/TimeSeriesData.cpp b/examples/simple_sensor/TimeSeriesData.cpp index ff7daa25..f6157f9a 100644 --- a/examples/simple_sensor/TimeSeriesData.cpp +++ b/examples/simple_sensor/TimeSeriesData.cpp @@ -10,7 +10,7 @@ void TimeSeriesData::recordData(mesh::RTCClock* clock, float value) { } } -void TimeSeriesData::calcDataMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const { +void TimeSeriesData::calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const { int i = next, n = num_slots; uint32_t ago = clock->getCurrentTime() - last_timestamp; int num_values = 0; @@ -40,6 +40,6 @@ void TimeSeriesData::calcDataMinMaxAvg(mesh::RTCClock* clock, uint32_t start_sec if (num_values > 0) { dest->_avg = total / num_values; } else { - dest->_avg = NAN; + dest->_max = dest->_min = dest->_avg = NAN; } } diff --git a/examples/simple_sensor/TimeSeriesData.h b/examples/simple_sensor/TimeSeriesData.h index ea9e823b..6efa7834 100644 --- a/examples/simple_sensor/TimeSeriesData.h +++ b/examples/simple_sensor/TimeSeriesData.h @@ -24,6 +24,6 @@ public: } void recordData(mesh::RTCClock* clock, float value); - void calcDataMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const; + void calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const; }; diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 792790d8..d1dd67c5 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -15,18 +15,19 @@ public: protected: /* ========================== custom logic here ========================== */ - Trigger low_batt; + Trigger low_batt, critical_batt; TimeSeriesData battery_data; void onSensorDataRead() override { float batt_voltage = getVoltage(TELEM_CHANNEL_SELF); battery_data.recordData(getRTCClock(), batt_voltage); // record battery - alertIf(batt_voltage < 3.4f, low_batt, HIGH_PRI_ALERT, "Battery low!"); + alertIf(batt_voltage < 3.4f, critical_batt, HIGH_PRI_ALERT, "Battery is critical!"); + alertIf(batt_voltage < 3.6f, low_batt, LOW_PRI_ALERT, "Battery is low"); } int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override { - battery_data.calcDataMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE); + battery_data.calcMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE); return 1; } /* ======================================================================= */ From 854a8dfe2fb2363fe10d01a399a4d9b1a9aaf506 Mon Sep 17 00:00:00 2001 From: recrof Date: Sat, 12 Jul 2025 20:06:56 +0200 Subject: [PATCH 49/60] move rak to nrf52_core, remove nrf52840_core --- platformio.ini | 8 -------- variants/rak4631/platformio.ini | 10 +++++----- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/platformio.ini b/platformio.ini index d20508a8..00a2d0fb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -77,14 +77,6 @@ build_flags = ${arduino_base.build_flags} -D NRF52_PLATFORM -D LFS_NO_ASSERT=1 -[nrf52840_base] -extends = nrf52_base -build_flags = ${nrf52_base.build_flags} -lib_deps = - ${nrf52_base.lib_deps} - rweather/Crypto @ ^0.4.0 - https://github.com/adafruit/Adafruit_nRF52_Arduino - ; ----------------- RP2040 --------------------- [rp2040_base] diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 5fcdb3d0..d4f8028b 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -1,9 +1,9 @@ [rak4631] -extends = nrf52840_base +extends = nrf52_base platform = https://github.com/maxgerhardt/platform-nordicnrf52.git#rak board = wiscore_rak4631 board_check = true -build_flags = ${nrf52840_base.build_flags} +build_flags = ${nrf52_base.build_flags} -I variants/rak4631 -D RAK_4631 -D PIN_BOARD_SCL=14 @@ -14,10 +14,10 @@ build_flags = ${nrf52840_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -build_src_filter = ${nrf52840_base.build_src_filter} +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631> lib_deps = - ${nrf52840_base.lib_deps} + ${nrf52_base.lib_deps} adafruit/Adafruit SSD1306 @ ^2.5.13 stevemarple/MicroNMEA @ ^2.0.6 @@ -55,7 +55,7 @@ build_flags = build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> -lib_deps = +lib_deps = ${rak4631.lib_deps} sparkfun/SparkFun u-blox GNSS Arduino Library @ ^2.2.27 https://github.com/boschsensortec/Bosch-BSEC2-Library From 339ee035aaab998d39e87632d2bfb80a1e8c97a7 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 13 Jul 2025 15:30:49 +1000 Subject: [PATCH 50/60] * simple_sensor: handleCustomCommand() hook --- examples/simple_sensor/SensorMesh.cpp | 7 ++++++- examples/simple_sensor/SensorMesh.h | 1 + examples/simple_sensor/main.cpp | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index d542407d..2943fd90 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -257,7 +257,7 @@ uint8_t SensorMesh::handleRequest(uint16_t perms, uint32_t sender_timestamp, uin memcpy(&start_secs_ago, &payload[0], 4); memcpy(&end_secs_ago, &payload[4], 4); uint8_t res1 = payload[8]; // reserved for future (extra query params) - uint8_t res2 = payload[8]; + uint8_t res2 = payload[9]; MinMaxAvg data[8]; int n; @@ -460,6 +460,11 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r command += 3; } + // first, see if this is a custom-handled CLI command (ie. in main.cpp) + if (handleCustomCommand(sender_timestamp, command, reply)) { + return; // command has been handled + } + // handle sensor-specific CLI commands if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int16} char* hex = &command[8]; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index e0527fa0..6edbfc26 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -118,6 +118,7 @@ protected: virtual void onSensorDataRead() = 0; // for app to implement virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0; // for app to implement + virtual bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) { return false; } // Mesh overrides float getAirtimeBudgetFactor() const override; diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index d1dd67c5..c9e282a2 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -30,6 +30,14 @@ protected: battery_data.calcMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE); return 1; } + + bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) override { + if (strcmp(command, "magic") == 0) { // example 'custom' command handling + strcpy(reply, "**Magic now done**"); + return true; // handled + } + return false; // not handled + } /* ======================================================================= */ }; From be68aaed20aa35bf97cfdb214925885b190f1963 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 13 Jul 2025 18:50:52 +1000 Subject: [PATCH 51/60] * simple_sensor: new REQ_TYPE_GET_ACCESS_LIST --- examples/simple_sensor/SensorMesh.cpp | 14 ++++++++++++++ examples/simple_sensor/SensorMesh.h | 4 +--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 2943fd90..d169f9b5 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -51,6 +51,7 @@ #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 #define REQ_TYPE_GET_AVG_MIN_MAX 0x04 +#define REQ_TYPE_GET_ACCESS_LIST 0x05 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -286,6 +287,19 @@ uint8_t SensorMesh::handleRequest(uint16_t perms, uint32_t sender_timestamp, uin } return ofs; } + if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_IS_ADMIN) != 0) { + uint8_t res1 = payload[0]; // reserved for future (extra query params) + uint8_t res2 = payload[1]; + if (res1 == 0 && res2 == 0) { + uint8_t ofs = 4; + for (int i = 0; i < num_contacts && ofs + 8 <= sizeof(reply_data) - 4; i++) { + auto c = &contacts[i]; + memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix + memcpy(&reply_data[ofs], &c->permissions, 2); ofs += 2; + } + return ofs; + } + } return 0; // unknown command } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 6edbfc26..77c30a8d 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -51,9 +51,7 @@ struct ContactInfo { #define FIRMWARE_ROLE "sensor" -#ifndef MAX_CONTACTS - #define MAX_CONTACTS 32 -#endif +#define MAX_CONTACTS 20 #define MAX_SEARCH_RESULTS 8 #define MAX_CONCURRENT_ALERTS 4 From f9e595687eab1e74e85a0f7f3252dfe3e12dc0b3 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Sun, 13 Jul 2025 15:21:02 +0200 Subject: [PATCH 52/60] Heltec Wireless Paper fix: radio init failed: -2 --- variants/heltec_wireless_paper/target.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/variants/heltec_wireless_paper/target.cpp b/variants/heltec_wireless_paper/target.cpp index 65eaab04..d434b241 100644 --- a/variants/heltec_wireless_paper/target.cpp +++ b/variants/heltec_wireless_paper/target.cpp @@ -1,11 +1,14 @@ #include "target.h" - #include HeltecV3Board board; -static SPIClass spi; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#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); @@ -21,7 +24,11 @@ DISPLAY_CLASS display; 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() { @@ -42,4 +49,4 @@ void radio_set_tx_power(uint8_t dbm) { mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); // create new random identity -} \ No newline at end of file +} From 3c92c6aa3b3d9900e0df2f1de0a90f354938b239 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Sun, 13 Jul 2025 22:41:27 +0200 Subject: [PATCH 53/60] sensecap_solar: disable GPS until it's supported --- variants/sensecap_solar/variant.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/sensecap_solar/variant.cpp b/variants/sensecap_solar/variant.cpp index 2b3ca305..05774c10 100644 --- a/variants/sensecap_solar/variant.cpp +++ b/variants/sensecap_solar/variant.cpp @@ -64,6 +64,8 @@ void initVariant() { pinMode(LED_BLUE, OUTPUT); digitalWrite(LED_BLUE, LOW); + /* disable gps until we actually support it. pinMode(GPS_EN, OUTPUT); digitalWrite(GPS_EN, HIGH); + */ } From 4a2978736e4cd1ebf1c50ee0368f5af2c3490474 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 14 Jul 2025 10:12:27 +1000 Subject: [PATCH 54/60] * Sensor: "get acl" command --- examples/simple_sensor/SensorMesh.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index d169f9b5..d385a552 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -59,7 +59,7 @@ #define LAZY_CONTACTS_WRITE_DELAY 5000 -#define ALERT_ACK_EXPIRY_MILLIS 6000 // wait 6 secs for ACKs to alert messages +#define ALERT_ACK_EXPIRY_MILLIS 8000 // wait 8 secs for ACKs to alert messages static File openAppend(FILESYSTEM* _fs, const char* fname) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) @@ -497,13 +497,14 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r strcpy(reply, "Err - bad pubkey"); } } - } else if (sender_timestamp == 0 && strcmp(command, "getperm") == 0) { - Serial.println("Permissions:"); + } else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) { + Serial.println("ACL:"); for (int i = 0; i < num_contacts; i++) { auto c = &contacts[i]; + Serial.printf("%04X ", c->permissions); mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE); - Serial.printf(" %04X\n", c->permissions); + Serial.printf("\n"); } reply[0] = 0; } else { From df33321bdc37cd7633e196163eafcda0ae7f2ba5 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 14 Jul 2025 12:25:34 +1000 Subject: [PATCH 55/60] * companion: added CMD_SEND_BINARY_REQ (50) --- examples/companion_radio/MyMesh.cpp | 43 ++++++++++++---- examples/companion_radio/MyMesh.h | 2 +- src/helpers/BaseChatMesh.cpp | 77 ++++++++++++++++++++--------- src/helpers/BaseChatMesh.h | 1 + 4 files changed, 90 insertions(+), 33 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index c230cb8a..47111c32 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -46,6 +46,8 @@ #define CMD_SET_CUSTOM_VAR 41 #define CMD_GET_ADVERT_PATH 42 #define CMD_GET_TUNING_PARAMS 43 +// NOTE: CMD range 44..49 parked, potentially for WiFi operations +#define CMD_SEND_BINARY_REQ 50 #define RESP_CODE_OK 0 #define RESP_CODE_ERR 1 @@ -92,7 +94,7 @@ #define PUSH_CODE_LOG_RX_DATA 0x88 #define PUSH_CODE_TRACE_DATA 0x89 #define PUSH_CODE_NEW_ADVERT 0x8A -#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B +#define PUSH_CODE_BINARY_RESPONSE 0x8B // was 'PUSH_CODE_TELEMETRY_RESPONSE' #define ERR_CODE_UNSUPPORTED_CMD 1 #define ERR_CODE_NOT_FOUND 2 @@ -490,11 +492,11 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); _serial->writeFrame(out_frame, i); - } else if (len > 4 && tag == pending_telemetry) { // check for telemetry response - pending_telemetry = 0; + } else if (len > 4 && tag == pending_req) { // check for matching response tag + pending_req = 0; int i = 0; - out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; + out_frame[i++] = PUSH_CODE_BINARY_RESPONSE; out_frame[i++] = 0; // reserved memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix @@ -566,7 +568,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _cli_rescue = false; offline_queue_len = 0; app_target_ver = 0; - pending_login = pending_status = pending_telemetry = 0; + pending_login = pending_status = pending_req = 0; next_ack_idx = 0; sign_data = NULL; dirty_contacts_expiry = 0; @@ -1103,7 +1105,7 @@ void MyMesh::handleCmdFrame(size_t len) { if (result == MSG_SEND_FAILED) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { - pending_telemetry = pending_status = 0; + pending_req = pending_status = 0; memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse() out_frame[0] = RESP_CODE_SENT; out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; @@ -1123,7 +1125,7 @@ void MyMesh::handleCmdFrame(size_t len) { if (result == MSG_SEND_FAILED) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { - pending_telemetry = pending_login = 0; + pending_req = pending_login = 0; // FUTURE: pending_status = tag; // match this in onContactResponse() memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme out_frame[0] = RESP_CODE_SENT; @@ -1135,7 +1137,7 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found } - } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) { + } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) { // can deprecate, in favour of CMD_SEND_BINARY_REQ uint8_t *pub_key = &cmd_frame[4]; ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); if (recipient) { @@ -1145,7 +1147,7 @@ void MyMesh::handleCmdFrame(size_t len) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { pending_status = pending_login = 0; - pending_telemetry = tag; // match this in onContactResponse() + pending_req = tag; // match this in onContactResponse() out_frame[0] = RESP_CODE_SENT; out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; memcpy(&out_frame[2], &tag, 4); @@ -1162,7 +1164,7 @@ void MyMesh::handleCmdFrame(size_t len) { sensors.querySensors(0xFF, telemetry); int i = 0; - out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; + out_frame[i++] = PUSH_CODE_BINARY_RESPONSE; out_frame[i++] = 0; // reserved memcpy(&out_frame[i], self_id.pub_key, 6); i += 6; // pub_key_prefix @@ -1170,6 +1172,27 @@ void MyMesh::handleCmdFrame(size_t len) { memcpy(&out_frame[i], telemetry.getBuffer(), tlen); i += tlen; _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_SEND_BINARY_REQ && len >= 2 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + uint8_t *req_data = &cmd_frame[1 + PUB_KEY_SIZE]; + uint32_t tag, est_timeout; + int result = sendRequest(*recipient, req_data, len - (1 + PUB_KEY_SIZE), tag, est_timeout); + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + pending_status = pending_login = 0; + pending_req = tag; // match this in onContactResponse() + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found + } } else if (cmd_frame[0] == CMD_HAS_CONNECTION && len >= 1 + PUB_KEY_SIZE) { uint8_t *pub_key = &cmd_frame[1]; if (hasConnectionTo(pub_key)) { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 247a9b3d..73e67684 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -160,7 +160,7 @@ private: NodePrefs _prefs; uint32_t pending_login; uint32_t pending_status; - uint32_t pending_telemetry; + uint32_t pending_req; // pending _BINARY_REQ (or legacy _TELEMETRY_REQ) BaseSerialInterface *_serial; ContactsIterator _iter; diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 271f28da..d6ba17b9 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -403,22 +403,52 @@ bool BaseChatMesh::importContact(const uint8_t src_buf[], uint8_t len) { } int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout) { - int tlen; - uint8_t temp[24]; - uint32_t now = getRTCClock()->getCurrentTimeUnique(); - memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique - if (recipient.type == ADV_TYPE_ROOM) { - memcpy(&temp[4], &recipient.sync_since, 4); - int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently - memcpy(&temp[8], password, len); - tlen = 8 + len; - } else { - int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently - memcpy(&temp[4], password, len); - tlen = 4 + len; - } + mesh::Packet* pkt; + { + int tlen; + uint8_t temp[24]; + uint32_t now = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique + if (recipient.type == ADV_TYPE_ROOM) { + memcpy(&temp[4], &recipient.sync_since, 4); + int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently + memcpy(&temp[8], password, len); + tlen = 8 + len; + } else { + int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently + memcpy(&temp[4], password, len); + tlen = 4 + len; + } - auto pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen); + pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen); + } + if (pkt) { + uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); + if (recipient.out_path_len < 0) { + sendFlood(pkt); + est_timeout = calcFloodTimeoutMillisFor(t); + return MSG_SEND_SENT_FLOOD; + } else { + sendDirect(pkt, recipient.out_path, recipient.out_path_len); + est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len); + return MSG_SEND_SENT_DIRECT; + } + } + return MSG_SEND_FAILED; +} + +int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout) { + if (data_len > MAX_PACKET_PAYLOAD - 16) return MSG_SEND_FAILED; + + mesh::Packet* pkt; + { + uint8_t temp[MAX_PACKET_PAYLOAD]; + tag = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique + memcpy(&temp[4], req_data, data_len); + + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, 4 + data_len); + } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); if (recipient.out_path_len < 0) { @@ -435,14 +465,17 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, } int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout) { - uint8_t temp[13]; - tag = getRTCClock()->getCurrentTimeUnique(); - memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique - temp[4] = req_type; - memset(&temp[5], 0, 4); // reserved (possibly for 'since' param) - getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique + mesh::Packet* pkt; + { + uint8_t temp[13]; + tag = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique + temp[4] = req_type; + memset(&temp[5], 0, 4); // reserved (possibly for 'since' param) + getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique - auto pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp)); + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp)); + } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); if (recipient.out_path_len < 0) { diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 53cd5018..903b4ece 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -134,6 +134,7 @@ public: bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len); int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout); int sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout); + int sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout); bool shareContactZeroHop(const ContactInfo& contact); uint8_t exportContact(const ContactInfo& contact, uint8_t dest_buf[]); bool importContact(const uint8_t src_buf[], uint8_t len); From 1930dc347ef95c2b0f42bd074acdfdd41762fb21 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 14 Jul 2025 12:46:51 +1000 Subject: [PATCH 56/60] * companion: reverted PUSH_CODE_TELEMETRY_RESPONSE, added new PUSH_CODE_BINARY_RESPONSE --- examples/companion_radio/MyMesh.cpp | 32 ++++++++++++++++++++--------- examples/companion_radio/MyMesh.h | 3 ++- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 47111c32..78f09023 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -94,7 +94,8 @@ #define PUSH_CODE_LOG_RX_DATA 0x88 #define PUSH_CODE_TRACE_DATA 0x89 #define PUSH_CODE_NEW_ADVERT 0x8A -#define PUSH_CODE_BINARY_RESPONSE 0x8B // was 'PUSH_CODE_TELEMETRY_RESPONSE' +#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B +#define PUSH_CODE_BINARY_RESPONSE 0x8C #define ERR_CODE_UNSUPPORTED_CMD 1 #define ERR_CODE_NOT_FOUND 2 @@ -492,14 +493,25 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); _serial->writeFrame(out_frame, i); + } else if (len > 4 && tag == pending_telemetry) { // check for matching response tag + pending_telemetry = 0; + + int i = 0; + out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + memcpy(&out_frame[i], &data[4], len - 4); + i += (len - 4); + _serial->writeFrame(out_frame, i); } else if (len > 4 && tag == pending_req) { // check for matching response tag pending_req = 0; int i = 0; out_frame[i++] = PUSH_CODE_BINARY_RESPONSE; out_frame[i++] = 0; // reserved - memcpy(&out_frame[i], contact.id.pub_key, 6); - i += 6; // pub_key_prefix + memcpy(&out_frame[i], &tag, 4); // app needs to match this to RESP_CODE_SENT.tag + i += 4; memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); _serial->writeFrame(out_frame, i); @@ -568,7 +580,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _cli_rescue = false; offline_queue_len = 0; app_target_ver = 0; - pending_login = pending_status = pending_req = 0; + pending_login = pending_status = pending_telemetry = pending_req = 0; next_ack_idx = 0; sign_data = NULL; dirty_contacts_expiry = 0; @@ -1105,7 +1117,7 @@ void MyMesh::handleCmdFrame(size_t len) { if (result == MSG_SEND_FAILED) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { - pending_req = pending_status = 0; + pending_req = pending_telemetry = pending_status = 0; memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse() out_frame[0] = RESP_CODE_SENT; out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; @@ -1125,7 +1137,7 @@ void MyMesh::handleCmdFrame(size_t len) { if (result == MSG_SEND_FAILED) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { - pending_req = pending_login = 0; + pending_req = pending_telemetry = pending_login = 0; // FUTURE: pending_status = tag; // match this in onContactResponse() memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme out_frame[0] = RESP_CODE_SENT; @@ -1146,8 +1158,8 @@ void MyMesh::handleCmdFrame(size_t len) { if (result == MSG_SEND_FAILED) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { - pending_status = pending_login = 0; - pending_req = tag; // match this in onContactResponse() + pending_status = pending_login = pending_req = 0; + pending_telemetry = tag; // match this in onContactResponse() out_frame[0] = RESP_CODE_SENT; out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; memcpy(&out_frame[2], &tag, 4); @@ -1164,7 +1176,7 @@ void MyMesh::handleCmdFrame(size_t len) { sensors.querySensors(0xFF, telemetry); int i = 0; - out_frame[i++] = PUSH_CODE_BINARY_RESPONSE; + out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; out_frame[i++] = 0; // reserved memcpy(&out_frame[i], self_id.pub_key, 6); i += 6; // pub_key_prefix @@ -1182,7 +1194,7 @@ void MyMesh::handleCmdFrame(size_t len) { if (result == MSG_SEND_FAILED) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { - pending_status = pending_login = 0; + pending_status = pending_login = pending_telemetry = 0; pending_req = tag; // match this in onContactResponse() out_frame[0] = RESP_CODE_SENT; out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 73e67684..48a3fd8a 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -160,7 +160,8 @@ private: NodePrefs _prefs; uint32_t pending_login; uint32_t pending_status; - uint32_t pending_req; // pending _BINARY_REQ (or legacy _TELEMETRY_REQ) + uint32_t pending_telemetry; // pending _TELEMETRY_REQ + uint32_t pending_req; // pending _BINARY_REQ BaseSerialInterface *_serial; ContactsIterator _iter; From da8bd717a4a14f2a5e3d9705d4124e5d35cc87c9 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 14 Jul 2025 13:09:22 +1000 Subject: [PATCH 57/60] * companion: serial protocol ver bump (FIRMWARE_VER_CODE) now 7 --- examples/companion_radio/MyMesh.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 48a3fd8a..24fe72c7 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -7,7 +7,7 @@ #endif /*------------ Frame Protocol --------------*/ -#define FIRMWARE_VER_CODE 6 +#define FIRMWARE_VER_CODE 7 #ifndef FIRMWARE_BUILD_DATE #define FIRMWARE_BUILD_DATE "2 Jul 2025" From 7947e8a2d861acd68b3e3fa1563b3393ef5d2e9a Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 15 Jul 2025 15:05:38 +1000 Subject: [PATCH 58/60] * simple_sensor: redesigned permissions * companion: PUSH_CODE_LOGIN_SUCCESS now has extra byte in frame for ACL permissions --- examples/companion_radio/MyMesh.cpp | 1 + examples/simple_sensor/SensorMesh.cpp | 35 ++++++++++++++------------- examples/simple_sensor/SensorMesh.h | 25 ++++++++++++------- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 78f09023..bae89dcb 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -471,6 +471,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, i += 6; // pub_key_prefix memcpy(&out_frame[i], &tag, 4); i += 4; // NEW: include server timestamp + out_frame[i++] = data[7]; // NEW (v7): ACL permissions } else { out_frame[i++] = PUSH_CODE_LOGIN_FAIL; out_frame[i++] = 0; // reserved diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index d385a552..5bbe9f4d 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -95,11 +95,11 @@ void SensorMesh::loadContacts() { while (!full) { ContactInfo c; uint8_t pub_key[32]; - uint8_t unused[5]; + uint8_t unused[6]; bool success = (file.read(pub_key, 32) == 32); - success = success && (file.read((uint8_t *) &c.permissions, 2) == 2); - success = success && (file.read(unused, 5) == 5); + success = success && (file.read((uint8_t *) &c.permissions, 1) == 1); + success = success && (file.read(unused, 6) == 6); 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); @@ -131,8 +131,8 @@ void SensorMesh::saveContacts() { if (c->permissions == 0) continue; // skip deleted entries bool success = (file.write(c->id.pub_key, 32) == 32); - success = success && (file.write((uint8_t *) &c->permissions, 2) == 2); - success = success && (file.write(unused, 5) == 5); + success = success && (file.write((uint8_t *) &c->permissions, 1) == 1); + success = success && (file.write(unused, 6) == 6); success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1); success = success && (file.write(c->out_path, 64) == 64); success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); @@ -240,7 +240,7 @@ static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t mult return size; } -uint8_t SensorMesh::handleRequest(uint16_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { +uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') if (req_type == REQ_TYPE_GET_TELEMETRY_DATA && (perms & PERM_GET_TELEMETRY) != 0) { @@ -248,12 +248,13 @@ uint8_t SensorMesh::handleRequest(uint16_t perms, uint32_t sender_timestamp, uin telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions for admin or guest + // TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms); uint8_t tlen = telemetry.getSize(); memcpy(&reply_data[4], telemetry.getBuffer(), tlen); return 4 + tlen; // reply_len } - if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_GET_MIN_MAX_AVG) != 0) { + if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_GET_OTHER_STATS) != 0) { uint32_t start_secs_ago, end_secs_ago; memcpy(&start_secs_ago, &payload[0], 4); memcpy(&end_secs_ago, &payload[4], 4); @@ -287,15 +288,15 @@ uint8_t SensorMesh::handleRequest(uint16_t perms, uint32_t sender_timestamp, uin } return ofs; } - if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_IS_ADMIN) != 0) { + if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_LEVEL3) { uint8_t res1 = payload[0]; // reserved for future (extra query params) uint8_t res2 = payload[1]; if (res1 == 0 && res2 == 0) { uint8_t ofs = 4; - for (int i = 0; i < num_contacts && ofs + 8 <= sizeof(reply_data) - 4; i++) { + for (int i = 0; i < num_contacts && ofs + 7 <= sizeof(reply_data) - 4; i++) { auto c = &contacts[i]; memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix - memcpy(&reply_data[ofs], &c->permissions, 2); ofs += 2; + reply_data[ofs++] = c->permissions; } return ofs; } @@ -337,11 +338,11 @@ ContactInfo* SensorMesh::putContact(const mesh::Identity& id) { return c; } -void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint16_t perms) { +void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint8_t perms) { mesh::Identity id(pubkey); auto c = putContact(id); - if (perms == 0) { // no permissions, remove from contacts + if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts memset(c, 0, sizeof(*c)); } else { c->permissions = perms; // update their permissions @@ -449,7 +450,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* MESH_DEBUG_PRINTLN("Login success!"); client->last_timestamp = sender_timestamp; client->last_activity = getRTCClock()->getCurrentTime(); - client->permissions = PERM_IS_ADMIN | PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO; // initially opt-in to receive alerts (can opt out) + client->permissions = PERM_ACL_LEVEL3 | PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO; // initially opt-in to receive alerts (can opt out) memcpy(client->shared_secret, secret, PUB_KEY_SIZE); dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); @@ -459,7 +460,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* reply_data[4] = RESP_SERVER_LOGIN_OK; reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) reply_data[6] = 1; // 1 = is admin - reply_data[7] = 0; // FUTURE: reserved + reply_data[7] = client->permissions; getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness return 12; // reply length @@ -480,7 +481,7 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r } // handle sensor-specific CLI commands - if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int16} + if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8} char* hex = &command[8]; char* sp = strchr(hex, ' '); // look for separator char if (sp == NULL || sp - hex != PUB_KEY_SIZE*2) { @@ -490,7 +491,7 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r uint8_t pubkey[PUB_KEY_SIZE]; if (mesh::Utils::fromHex(pubkey, PUB_KEY_SIZE, hex)) { - uint16_t perms = atoi(sp); + uint8_t perms = atoi(sp); applyContactPermissions(pubkey, perms); strcpy(reply, "OK"); } else { @@ -502,7 +503,7 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r for (int i = 0; i < num_contacts; i++) { auto c = &contacts[i]; - Serial.printf("%04X ", c->permissions); + Serial.printf("%02X ", c->permissions); mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE); Serial.printf("\n"); } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 77c30a8d..a357506f 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -23,22 +23,29 @@ #include #include -#define PERM_IS_ADMIN 0x8000 -#define PERM_GET_TELEMETRY 0x0001 -#define PERM_GET_MIN_MAX_AVG 0x0002 -#define PERM_RECV_ALERTS_LO 0x0100 // low priority alerts -#define PERM_RECV_ALERTS_HI 0x0200 // high priority alerts +#define PERM_ACL_ROLE_MASK 3 // lower 2 bits +#define PERM_ACL_GUEST 0 +#define PERM_ACL_LEVEL1 1 +#define PERM_ACL_LEVEL2 2 +#define PERM_ACL_LEVEL3 3 // admin + +#define PERM_GET_TELEMETRY (1 << 2) +#define PERM_GET_OTHER_STATS (1 << 3) +#define PERM_RESERVED1 (1 << 4) +#define PERM_RESERVED2 (1 << 5) +#define PERM_RECV_ALERTS_LO (1 << 6) // low priority alerts +#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts struct ContactInfo { mesh::Identity id; - uint16_t permissions; + uint8_t permissions; int8_t out_path_len; uint8_t out_path[MAX_PATH_SIZE]; uint8_t shared_secret[PUB_KEY_SIZE]; uint32_t last_timestamp; // by THEIR clock (transient) uint32_t last_activity; // by OUR clock (transient) - bool isAdmin() const { return (permissions & PERM_IS_ADMIN) != 0; } + bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_LEVEL3; } }; #ifndef FIRMWARE_BUILD_DATE @@ -151,10 +158,10 @@ private: void loadContacts(); void saveContacts(); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data); - uint8_t handleRequest(uint16_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); + uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); ContactInfo* putContact(const mesh::Identity& id); - void applyContactPermissions(const uint8_t* pubkey, uint16_t perms); + void applyContactPermissions(const uint8_t* pubkey, uint8_t perms); void sendAlert(ContactInfo* c, Trigger* t); From fccb3b6c397ff3afc0b1e2c4400431cd87cffa60 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 15 Jul 2025 15:56:59 +1000 Subject: [PATCH 59/60] * companion: added CMD_FACTORY_RESET (51) --- examples/companion_radio/MyMesh.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index bae89dcb..9331a50c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -48,6 +48,7 @@ #define CMD_GET_TUNING_PARAMS 43 // NOTE: CMD range 44..49 parked, potentially for WiFi operations #define CMD_SEND_BINARY_REQ 50 +#define CMD_FACTORY_RESET 51 #define RESP_CODE_OK 0 #define RESP_CODE_ERR 1 @@ -1361,6 +1362,15 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_NOT_FOUND); } + } else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) { + bool success = _store->formatFileSystem(); + if (success) { + writeOKFrame(); + delay(1000); + board.reboot(); // doesn't return + } else { + writeErrFrame(ERR_CODE_FILE_IO_ERROR); + } } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); From f74819f8db988097c9dcb653b1411d32801f7c47 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 15 Jul 2025 15:59:10 +1000 Subject: [PATCH 60/60] * ver bump --- examples/companion_radio/MyMesh.h | 4 ++-- examples/simple_repeater/main.cpp | 4 ++-- examples/simple_room_server/main.cpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 24fe72c7..81bf261e 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -10,11 +10,11 @@ #define FIRMWARE_VER_CODE 7 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "2 Jul 2025" +#define FIRMWARE_BUILD_DATE "15 Jul 2025" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "v1.7.2" +#define FIRMWARE_VERSION "v1.7.3" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index cd9ee12a..2ec94c4b 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -22,11 +22,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "2 Jul 2025" + #define FIRMWARE_BUILD_DATE "15 Jul 2025" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.7.2" + #define FIRMWARE_VERSION "v1.7.3" #endif #ifndef LORA_FREQ diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index a1400cb3..b8f74a0c 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -22,11 +22,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "2 Jul 2025" + #define FIRMWARE_BUILD_DATE "15 Jul 2025" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.7.2" + #define FIRMWARE_VERSION "v1.7.3" #endif #ifndef LORA_FREQ