From 0d1b5b17d3dcebbe79c731c62bad444a59e2a450 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 12 Jul 2025 12:26:16 +1000 Subject: [PATCH 01/13] * 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 02/13] 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 03/13] * 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 04/13] * 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 05/13] 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 06/13] 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 07/13] * 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 08/13] * 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 09/13] * 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 10/13] * 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 11/13] * 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 12/13] * 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 13/13] * 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