From b4e37044edf55ce96a7a4e14261ef3ec711faa68 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 3 Sep 2026 14:09:15 +1000 Subject: [PATCH 01/37] * default and reply scopes now applied --- examples/simple_sensor/SensorMesh.cpp | 76 ++++++++++++++++++++++----- examples/simple_sensor/SensorMesh.h | 6 +++ 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 23d0cdc3..287a3c1b 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1,4 +1,5 @@ #include "SensorMesh.h" +#include /* ------------------------------ Config -------------------------------- */ @@ -262,7 +263,7 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) { sendDirect(pkt, c->out_path, c->out_path_len); } else { unsigned long delay_millis = 0; - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); } } t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS); @@ -400,10 +401,11 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r if (sp == NULL) { strcpy(reply, "Err - bad params"); } else { + int hex_len = min(sp - hex, PUB_KEY_SIZE*2); + uint8_t pubkey[PUB_KEY_SIZE]; + *sp++ = 0; // replace space with null terminator - uint8_t pubkey[PUB_KEY_SIZE]; - int hex_len = min(sp - hex, PUB_KEY_SIZE*2); if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) { uint8_t perms = atoi(sp); if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) { @@ -451,6 +453,21 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r } } +mesh::DispatcherAction SensorMesh::onRecvPacket(mesh::Packet* pkt) { + if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { + recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); + } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { + if (region_map.getWildcard().flags & REGION_DENY_FLOOD) { + recv_pkt_region = NULL; + } else { + recv_pkt_region = ®ion_map.getWildcard(); + } + } else { + recv_pkt_region = NULL; + } + return Mesh::onRecvPacket(pkt); +} + 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; @@ -472,10 +489,10 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response 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, packet->getPathHashSize()); + if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); - if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -503,7 +520,7 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) { if (dest.out_path_len == OUT_PATH_UNKNOWN) { mesh::Packet* ack = createAck(ack_hash); - if (ack) sendFlood(ack, TXT_ACK_DELAY, path_hash_size); + if (ack) sendFloodScoped(default_scope, ack, TXT_ACK_DELAY, path_hash_size); } else { uint32_t d = TXT_ACK_DELAY; if (getExtraAckTransmitCount() > 0) { @@ -541,14 +558,14 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i // 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, packet->getPathHashSize()); + if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len); if (reply) { if (from->out_path_len != OUT_PATH_UNKNOWN) { // 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, packet->getPathHashSize()); + sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -571,7 +588,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); - if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, TXT_ACK_DELAY, packet->getPathHashSize()); } else { sendAckTo(*from, ack_hash, packet->getPathHashSize()); } @@ -601,7 +618,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len); if (reply) { if (from->out_path_len == OUT_PATH_UNKNOWN) { - sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); + sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } else { sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS); } @@ -708,6 +725,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise last_read_time = 0; num_alert_tasks = 0; set_radio_at = revert_radio_at = 0; + recv_pkt_region = NULL; // defaults _prefs.airtime_factor = 1.0; @@ -820,11 +838,43 @@ void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params } +void SensorMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) { + if (scope.isNull()) { + sendFlood(pkt, delay_millis, path_hash_size); + } else { + uint16_t codes[2]; + codes[0] = scope.calcTransportCode(pkt); + codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? + sendFlood(pkt, codes, delay_millis, path_hash_size); + } +} + +void SensorMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) { + TransportKey req_scope; + bool is_wildcard = recv_pkt_region != NULL && recv_pkt_region->isWildcard(); + bool req_scope_known = recv_pkt_region != NULL && !is_wildcard + && region_map.getTransportKeysFor(*recv_pkt_region, &req_scope, 1) > 0; + + switch (mesh::chooseReplyScope(req_scope_known, is_wildcard, !default_scope.isNull())) { + case mesh::REPLY_SCOPE_REQUEST: + sendFloodScoped(req_scope, packet, delay_millis, path_hash_size); // reply with same scope as request + break; + case mesh::REPLY_SCOPE_DEFAULT: + // requester's scope is unknown: DIRECT request (no transport codes), or code matched no Region. + // un-scoped would be dropped at hop 0 by repeaters running flood.max.unscoped=0 + sendFloodScoped(default_scope, packet, delay_millis, path_hash_size); + break; + case mesh::REPLY_SCOPE_NONE: + sendFlood(packet, delay_millis, path_hash_size); // send un-scoped + break; + } +} + void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet* pkt = createSelfAdvert(); if (pkt) { if (flood) { - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); } else { sendZeroHop(pkt, delay_millis); } @@ -902,7 +952,7 @@ void SensorMesh::loop() { if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { mesh::Packet* pkt = createSelfAdvert(); unsigned long delay_millis = 0; - if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + if (pkt) sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); updateFloodAdvertTimer(); // schedule next flood advert updateAdvertTimer(); // also schedule local advert (so they don't overlap) @@ -973,7 +1023,7 @@ void SensorMesh::loop() { } } - // is there are pending dirty contacts write needed? + // pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { acl.save(_fs); dirty_contacts_expiry = 0; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index b5e96d5c..845402ba 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -81,6 +81,9 @@ public: float getTelemValue(uint8_t channel, uint8_t type); + void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size); + void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); + protected: // current telemetry data queries float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); } @@ -122,6 +125,7 @@ protected: int getInterferenceThreshold() const override; bool getCADEnabled() const override; int getAGCResetInterval() const override; + mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) 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; @@ -129,6 +133,7 @@ protected: 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 onControlDataRecv(mesh::Packet* packet) override; void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override; + virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len); void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1); private: @@ -142,6 +147,7 @@ private: CayenneLPP telemetry; TransportKeyStore key_store; RegionMap region_map; + RegionEntry* recv_pkt_region; TransportKey default_scope; uint32_t last_read_time; int matching_peer_indexes[MAX_SEARCH_RESULTS]; From 9bc6060831a0ad6064911836518e143d8a7c5ab9 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 4 Sep 2026 15:13:17 +1000 Subject: [PATCH 02/37] * new: REQ_TYPE_SUBSCRIBE and REQ_TYPE_UNSUBSCRIBE --- examples/simple_sensor/SensorMesh.cpp | 124 ++++++++++++++++++++++---- examples/simple_sensor/SensorMesh.h | 9 +- src/helpers/ClientACL.h | 5 ++ 3 files changed, 116 insertions(+), 22 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 287a3c1b..0a78f5da 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -55,6 +55,8 @@ #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 #define REQ_TYPE_GET_AVG_MIN_MAX 0x04 #define REQ_TYPE_GET_ACCESS_LIST 0x05 +#define REQ_TYPE_SUBSCRIBE 0x10 +#define REQ_TYPE_UNSUBSCRIBE 0x11 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -74,6 +76,8 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) { #endif } +/* --------------------- Cayenne LPP helpers ----------------------------*/ + static uint8_t getDataSize(uint8_t type) { switch (type) { case LPP_GPS: @@ -171,7 +175,53 @@ static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t mult return size; } -uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { +static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type) { + 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 +} + +/* ------------------ end Cayenne LPP helpers ----------------------*/ + +bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { + if (telemetry.getSize() != prev_telem_size) return true; + + 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); + + float v = getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); + float pv = getFloat(&prev_telem[i], sz, getMultiplier(t), isSigned(t)); + float min_delta = findTelemValue(min_deltas, min_deltas_len, ch, t); + if (abs(v - pv) > min_delta) return true; // Yes, has changed + + i += sz; // skip + } + return false; // no changes +} + +uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { + uint8_t perms = from->isAdmin() ? 0xFF : from->permissions; + 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) { // allow all @@ -235,6 +285,29 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint return ofs; } } + if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 2 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + uint8_t reserved = payload[0]; + RegionEntry* r; + if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope + r = recv_pkt_region; + } else { // use default scope + r = region_map.getDefaultRegion(); + } + from->extra.sensor.scope_region_id = r ? r->id : 0; + from->extra.sensor.min_deltas_len = payload[1]; + // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) + memcpy(from->extra.sensor.min_deltas, &payload[2], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[1])); + + getRNG()->random(&reply_data[4], 2); // just some entropy for better packet-hash uniqueness + strcpy((char *)&reply_data[6], r ? r->name : ""); // reply with name of scope that will be used + return 6 + strlen((char *)&reply_data[6]); + } + if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + from->extra.sensor.scope_region_id = 0; + reply_data[4] = 0; // success + getRNG()->random(&reply_data[5], 3); // just some entropy for better packet-hash uniqueness + return 8; + } return 0; // unknown command } @@ -548,7 +621,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() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5); + uint8_t reply_len = handleRequest(from, timestamp, data[4], &data[5], len - 5); if (reply_len == 0) return; // invalid command from->last_timestamp = timestamp; @@ -726,6 +799,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise num_alert_tasks = 0; set_radio_at = revert_radio_at = 0; recv_pkt_region = NULL; + prev_telem_size = 0; // defaults _prefs.airtime_factor = 1.0; @@ -916,23 +990,7 @@ void SensorMesh::formatPacketStatsReply(char *reply) { } 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 + return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type); } bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) { @@ -982,6 +1040,34 @@ void SensorMesh::loop() { // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions + // compare with previous telemetry, check if any deltas are greater than subscriber minimums + for (int i = 0; i < acl.getNumClients(); i++) { + auto c = acl.getClientByIdx(i); + if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0) continue; // skip deleted entries, or Not subscribed to deltas + RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); + if (r == NULL) continue; // unknown region scope + if (telemHasChanged(c->extra.sensor.min_deltas, c->extra.sensor.min_deltas_len)) { + TransportKey scope; + if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { + uint8_t tlen = telemetry.getSize(); + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); // this will be an unknown 'tag' to the client + memcpy(reply_data, ×tamp, 4); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 4 + tlen); + if (reply) { + if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT + sendDirect(reply, c->out_path, c->out_path_len, 0); + } else { + sendFloodScoped(scope, reply, 0, _prefs.path_hash_mode + 1); + } + } + } + } + } + memcpy(prev_telem, telemetry.getBuffer(), telemetry.getSize()); // save snapshot for next compare cycle + prev_telem_size = telemetry.getSize(); + onSensorDataRead(); last_read_time = curr; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 845402ba..7ea87f20 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -96,12 +96,12 @@ protected: bool getGPS(uint8_t channel, float& lat, float& lon, float& alt); // alerts - enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT }; + enum AlertPriority : uint8_t { LOW_PRI_ALERT, HIGH_PRI_ALERT }; struct Trigger { uint32_t timestamp; - AlertPriority pri; uint32_t expected_acks[4]; + AlertPriority pri; int8_t curr_contact_idx; uint8_t attempt; unsigned long send_expiry; @@ -145,6 +145,8 @@ private: uint8_t reply_data[MAX_PACKET_PAYLOAD]; unsigned long dirty_contacts_expiry; CayenneLPP telemetry; + uint8_t prev_telem_size; + uint8_t prev_telem[MAX_PACKET_PAYLOAD - 4]; TransportKeyStore key_store; RegionMap region_map; RegionEntry* recv_pkt_region; @@ -159,8 +161,9 @@ private: uint8_t pending_sf; uint8_t pending_cr; + bool telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); - uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); + uint8_t handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); void sendAlert(const ClientInfo* c, Trigger* t); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index e0654464..9fde35ec 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -28,6 +28,11 @@ struct ClientInfo { unsigned long ack_timeout; uint8_t push_failures; } room; + struct { + uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber + uint8_t min_deltas_len; + uint8_t min_deltas[14]; // LPP encoded + } sensor; } extra; bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; } From 3a5a6b90ab3d9e65950dd4e8e03026ce73da4103 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 4 Sep 2026 22:02:49 +1000 Subject: [PATCH 03/37] * sensor: CLI "sub" and "unsub" commands --- examples/simple_sensor/SensorMesh.cpp | 68 ++++++++++++++++++++++++++- examples/simple_sensor/SensorMesh.h | 2 +- examples/simple_sensor/main.cpp | 2 +- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 0a78f5da..008cbcfc 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -193,6 +193,43 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u return 0.0f; // not found } +static uint8_t compileLPPSpec(char* txt, uint8_t* dest, size_t max_len) { + const char* parts[3]; + int n = mesh::Utils::parseTextParts(txt, parts, 3, ','); + uint8_t len = 0; + for (int i = 0; i < n && len + 6 <= max_len; i++) { + const char* cp = strchr(parts[i], ':'); + if (cp) { + uint8_t t; + float factor = 1.0f; + cp++; // skip the ':' + char* ep = strchr(cp, 0) - 1; // find LAST char + if (*ep == 'V') { // Volts + t = LPP_VOLTAGE; + } else if (*ep == 'W') { // Watts + t = LPP_POWER; + } else if (*ep == 'C') { // Celcius + t = LPP_TEMPERATURE; + } else if (*ep == 'P') { // Pascals + t = LPP_BAROMETRIC_PRESSURE; + } else if (*ep == 'A') { // Amps + t = LPP_CURRENT; + } else if (*ep == 'm') { + t = LPP_DISTANCE; factor = 0.001f; + } else { + t = 0; + } + + if (t) { + dest[len++] = atoi(parts[i]); // channel number + dest[len++] = t; // LPP type + len += putFloat(&dest[len], atof(cp) * factor, getDataSize(t), getMultiplier(t), isSigned(t)); + } + } + } + return len; +} + /* ------------------ end Cayenne LPP helpers ----------------------*/ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { @@ -453,7 +490,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* return 13; // reply length } -void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { +void SensorMesh::handleCommand(ClientInfo* from, 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) @@ -502,6 +539,33 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r Serial.printf("\n"); } reply[0] = 0; + } else if (from != NULL && memcmp(command, "sub", 3) == 0 && (command[3] == ' ' || command[3] == 0)) { // subscribe + uint8_t perms = from->isAdmin() ? 0xFF : from->permissions; + if ((perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + RegionEntry* r; + if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope + r = recv_pkt_region; + } else { // use default scope + r = region_map.getDefaultRegion(); + } + if (command[3] == ' ') { + // compile params as LPP data, eg. "sub 1:0.2V" + from->extra.sensor.min_deltas_len = compileLPPSpec(&command[4], from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); + } else { + from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) + } + from->extra.sensor.scope_region_id = r ? r->id : 0; + if (from->extra.sensor.scope_region_id) { + strcpy(reply, "OK - subscribed"); + } else { + strcpy(reply, "Err - region scope needed"); + } + } else { + strcpy(reply, "Err - no permission"); + } + } else if (from != NULL && strcmp(command, "unsub") == 0) { // unsubscribe + from->extra.sensor.scope_region_id = 0; + strcpy(reply, "OK - unsubscribed"); } else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read if (command[2] == ' ') { // it's a write uint32_t val; @@ -676,7 +740,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i uint8_t temp[166]; char *command = (char *) &data[5]; char *reply = (char *) &temp[5]; - handleCommand(sender_timestamp, command, reply); + handleCommand(from, sender_timestamp, command, reply); int text_len = strlen(reply); if (text_len > 0) { diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 7ea87f20..6d5857d0 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -51,7 +51,7 @@ public: SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); void begin(FILESYSTEM* fs); void loop(); - void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply); // CommonCLI callbacks const char* getFirmwareVer() override { return FIRMWARE_VERSION; } diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 69182f3a..9e3b230b 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -137,7 +137,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.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(NULL, 0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } From 62d0556a7024dd86046cea9d74e176f4737d91a0 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 5 Sep 2026 15:59:29 +1000 Subject: [PATCH 04/37] * added region load/save support * SUBSCRIBE now with timeout param (default 30 mins, max ~18 hours) --- examples/simple_sensor/SensorMesh.cpp | 102 ++++++++++++++++++++++---- examples/simple_sensor/SensorMesh.h | 7 +- src/helpers/ClientACL.h | 1 + 3 files changed, 93 insertions(+), 17 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 008cbcfc..c3f2d9b0 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -322,8 +322,10 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u return ofs; } } - if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 2 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { - uint8_t reserved = payload[0]; + if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 4 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + uint16_t timeout_secs; + memcpy(&timeout_secs, &payload[0], 2); + uint8_t reserved = payload[2]; RegionEntry* r; if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope r = recv_pkt_region; @@ -331,16 +333,19 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u r = region_map.getDefaultRegion(); } from->extra.sensor.scope_region_id = r ? r->id : 0; - from->extra.sensor.min_deltas_len = payload[1]; + from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; + from->extra.sensor.min_deltas_len = payload[3]; // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) - memcpy(from->extra.sensor.min_deltas, &payload[2], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[1])); + memcpy(from->extra.sensor.min_deltas, &payload[4], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[3])); - getRNG()->random(&reply_data[4], 2); // just some entropy for better packet-hash uniqueness - strcpy((char *)&reply_data[6], r ? r->name : ""); // reply with name of scope that will be used - return 6 + strlen((char *)&reply_data[6]); + memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); // reply with actual expiry timestamp (or 0 for error) + strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used + return 6 + strlen((char *)&reply_data[8]); } if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { from->extra.sensor.scope_region_id = 0; + from->extra.sensor.expiry_timestamp = 0; + // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? reply_data[4] = 0; // success getRNG()->random(&reply_data[5], 3); // just some entropy for better packet-hash uniqueness return 8; @@ -441,6 +446,25 @@ int SensorMesh::getAGCResetInterval() const { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } +void SensorMesh::startRegionsLoad() { + temp_map.resetFrom(region_map); // rebuild regions in a temp instance + memset(load_stack, 0, sizeof(load_stack)); + load_stack[0] = &temp_map.getWildcard(); + region_load_active = true; +} + +bool SensorMesh::saveRegions() { + return region_map.save(_fs); +} + +void SensorMesh::onDefaultRegionChanged(const RegionEntry* r) { + if (r) { + region_map.getTransportKeysFor(*r, &default_scope, 1); + } else { + memset(default_scope.key, 0, sizeof(default_scope.key)); + } +} + uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) { ClientInfo* client; if (data[0] == 0) { // blank password, just check if sender is in ACL @@ -454,7 +478,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* } else { if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password #if MESH_DEBUG - MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); + MESH_DEBUG_PRINTLN("Invalid password: %s", &data[0]); #endif return 0; } @@ -491,6 +515,40 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* } void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply) { + if (region_load_active) { + if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation + region_map = temp_map; // copy over the temp instance as new current map + region_load_active = false; + + sprintf(reply, "OK - loaded %d regions", region_map.getCount()); + } else { + char *np = command; + while (*np == ' ') np++; // skip indent + int indent = np - command; + + char *ep = np; + while (RegionMap::is_name_char(*ep)) ep++; + if (*ep) { *ep++ = 0; } // set null terminator for end of name + + while (*ep && *ep != 'F') ep++; // look for (optional) flags + + if (indent > 0 && indent < 8 && strlen(np) > 0) { + auto parent = load_stack[indent - 1]; + if (parent) { + auto old = region_map.findByName(np); + auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0); // carry-over the current ID (if name already exists) + if (nw) { + nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD); // carry-over flags from curr + + load_stack[indent] = nw; // keep pointers to parent regions, to resolve parent_id's + } + } + } + reply[0] = 0; + } + return; + } + while (*command == ' ') command++; // skip leading spaces if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) @@ -548,15 +606,25 @@ void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char } else { // use default scope r = region_map.getDefaultRegion(); } - if (command[3] == ' ') { - // compile params as LPP data, eg. "sub 1:0.2V" - from->extra.sensor.min_deltas_len = compileLPPSpec(&command[4], from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); - } else { - from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) + // defaults: + from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) + uint16_t timeout_secs = 30*60; // expires after 30 mins + if (command[3] == ' ') { // eg. "sub 300 1:0.2V" + char* cp = &command[4]; + while (*cp >= '0' && *cp <= '9') cp++; + if (cp > &command[4]) { + timeout_secs = atoi(&command[4]); + if (*cp == ' ') { + cp++; // skip the space + from->extra.sensor.min_deltas_len = compileLPPSpec(cp, from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); + } + } } from->extra.sensor.scope_region_id = r ? r->id : 0; - if (from->extra.sensor.scope_region_id) { - strcpy(reply, "OK - subscribed"); + from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; + if (from->extra.sensor.expiry_timestamp) { + DateTime dt = DateTime(from->extra.sensor.expiry_timestamp); + sprintf(reply, "OK - sub expires: %02d:%02d (UTC)", dt.hour(), dt.minute()); } else { strcpy(reply, "Err - region scope needed"); } @@ -853,7 +921,7 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), - region_map(key_store), + region_map(key_store), temp_map(key_store), _cli(board, rtc, sensors, region_map, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) { @@ -864,6 +932,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise set_radio_at = revert_radio_at = 0; recv_pkt_region = NULL; prev_telem_size = 0; + region_load_active = false; // defaults _prefs.airtime_factor = 1.0; @@ -1110,6 +1179,7 @@ void SensorMesh::loop() { if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0) continue; // skip deleted entries, or Not subscribed to deltas RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); if (r == NULL) continue; // unknown region scope + if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired if (telemHasChanged(c->extra.sensor.min_deltas, c->extra.sensor.min_deltas_len)) { TransportKey scope; if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 6d5857d0..e0e03c81 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -78,6 +78,9 @@ public: void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override { } void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override; + void startRegionsLoad() override; + bool saveRegions() override; + void onDefaultRegionChanged(const RegionEntry* r) override; float getTelemValue(uint8_t channel, uint8_t type); @@ -148,8 +151,9 @@ private: uint8_t prev_telem_size; uint8_t prev_telem[MAX_PACKET_PAYLOAD - 4]; TransportKeyStore key_store; - RegionMap region_map; + RegionMap region_map, temp_map; RegionEntry* recv_pkt_region; + RegionEntry* load_stack[8]; TransportKey default_scope; uint32_t last_read_time; int matching_peer_indexes[MAX_SEARCH_RESULTS]; @@ -160,6 +164,7 @@ private: float pending_bw; uint8_t pending_sf; uint8_t pending_cr; + bool region_load_active; bool telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 9fde35ec..1e9f472d 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -29,6 +29,7 @@ struct ClientInfo { uint8_t push_failures; } room; struct { + uint32_t expiry_timestamp; // epoch seconds uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber uint8_t min_deltas_len; uint8_t min_deltas[14]; // LPP encoded From d71a54cd3dfe8dc5eca7c0e0834c6e6e3ea18837 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 7 Sep 2026 13:56:47 +1000 Subject: [PATCH 05/37] * CLI: removed "sub" and "unsub" (cannot support via command line) * _SUBSCRIBE: binary req/resp payload changes (now keeps supplied 'push_tag') * Sensor: added MCU temperature support in telemetry --- examples/simple_sensor/SensorMesh.cpp | 112 ++++++-------------------- src/helpers/ClientACL.h | 1 + 2 files changed, 27 insertions(+), 86 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index c3f2d9b0..8c6b06a6 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -55,8 +55,10 @@ #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 #define REQ_TYPE_GET_AVG_MIN_MAX 0x04 #define REQ_TYPE_GET_ACCESS_LIST 0x05 -#define REQ_TYPE_SUBSCRIBE 0x10 -#define REQ_TYPE_UNSUBSCRIBE 0x11 +#define REQ_TYPE_GET_NEIGHBOURS 0x06 // repeater only (at present) + +#define REQ_TYPE_SUBSCRIBE 0x08 +#define REQ_TYPE_UNSUBSCRIBE 0x09 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -193,43 +195,6 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u return 0.0f; // not found } -static uint8_t compileLPPSpec(char* txt, uint8_t* dest, size_t max_len) { - const char* parts[3]; - int n = mesh::Utils::parseTextParts(txt, parts, 3, ','); - uint8_t len = 0; - for (int i = 0; i < n && len + 6 <= max_len; i++) { - const char* cp = strchr(parts[i], ':'); - if (cp) { - uint8_t t; - float factor = 1.0f; - cp++; // skip the ':' - char* ep = strchr(cp, 0) - 1; // find LAST char - if (*ep == 'V') { // Volts - t = LPP_VOLTAGE; - } else if (*ep == 'W') { // Watts - t = LPP_POWER; - } else if (*ep == 'C') { // Celcius - t = LPP_TEMPERATURE; - } else if (*ep == 'P') { // Pascals - t = LPP_BAROMETRIC_PRESSURE; - } else if (*ep == 'A') { // Amps - t = LPP_CURRENT; - } else if (*ep == 'm') { - t = LPP_DISTANCE; factor = 0.001f; - } else { - t = 0; - } - - if (t) { - dest[len++] = atoi(parts[i]); // channel number - dest[len++] = t; // LPP type - len += putFloat(&dest[len], atof(cp) * factor, getDataSize(t), getMultiplier(t), isSigned(t)); - } - } - } - return len; -} - /* ------------------ end Cayenne LPP helpers ----------------------*/ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { @@ -269,6 +234,10 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u // query other sensors -- target specific sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest // TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms); + float temperature = board.getMCUTemperature(); + if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } uint8_t tlen = telemetry.getSize(); memcpy(&reply_data[4], telemetry.getBuffer(), tlen); @@ -322,10 +291,11 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u return ofs; } } - if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 4 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 8 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + memcpy(&from->extra.sensor.push_tag, &payload[0], 4); uint16_t timeout_secs; - memcpy(&timeout_secs, &payload[0], 2); - uint8_t reserved = payload[2]; + memcpy(&timeout_secs, &payload[4], 2); + uint8_t reserved = payload[6]; RegionEntry* r; if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope r = recv_pkt_region; @@ -334,16 +304,17 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u } from->extra.sensor.scope_region_id = r ? r->id : 0; from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; - from->extra.sensor.min_deltas_len = payload[3]; + from->extra.sensor.min_deltas_len = min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[7]); // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) - memcpy(from->extra.sensor.min_deltas, &payload[4], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[3])); + memcpy(from->extra.sensor.min_deltas, &payload[8], from->extra.sensor.min_deltas_len); memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); // reply with actual expiry timestamp (or 0 for error) strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used - return 6 + strlen((char *)&reply_data[8]); + return 8 + strlen((char *)&reply_data[8]); } if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { from->extra.sensor.scope_region_id = 0; + from->extra.sensor.push_tag = 0; from->extra.sensor.expiry_timestamp = 0; // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? reply_data[4] = 0; // success @@ -597,43 +568,6 @@ void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char Serial.printf("\n"); } reply[0] = 0; - } else if (from != NULL && memcmp(command, "sub", 3) == 0 && (command[3] == ' ' || command[3] == 0)) { // subscribe - uint8_t perms = from->isAdmin() ? 0xFF : from->permissions; - if ((perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { - RegionEntry* r; - if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope - r = recv_pkt_region; - } else { // use default scope - r = region_map.getDefaultRegion(); - } - // defaults: - from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) - uint16_t timeout_secs = 30*60; // expires after 30 mins - if (command[3] == ' ') { // eg. "sub 300 1:0.2V" - char* cp = &command[4]; - while (*cp >= '0' && *cp <= '9') cp++; - if (cp > &command[4]) { - timeout_secs = atoi(&command[4]); - if (*cp == ' ') { - cp++; // skip the space - from->extra.sensor.min_deltas_len = compileLPPSpec(cp, from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); - } - } - } - from->extra.sensor.scope_region_id = r ? r->id : 0; - from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; - if (from->extra.sensor.expiry_timestamp) { - DateTime dt = DateTime(from->extra.sensor.expiry_timestamp); - sprintf(reply, "OK - sub expires: %02d:%02d (UTC)", dt.hour(), dt.minute()); - } else { - strcpy(reply, "Err - region scope needed"); - } - } else { - strcpy(reply, "Err - no permission"); - } - } else if (from != NULL && strcmp(command, "unsub") == 0) { // unsubscribe - from->extra.sensor.scope_region_id = 0; - strcpy(reply, "OK - unsubscribed"); } else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read if (command[2] == ' ') { // it's a write uint32_t val; @@ -1172,6 +1106,11 @@ void SensorMesh::loop() { telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions + // This MCU temperature will be overridden by external sensors (if any) + float temperature = board.getMCUTemperature(); + if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } // compare with previous telemetry, check if any deltas are greater than subscriber minimums for (int i = 0; i < acl.getNumClients(); i++) { @@ -1184,11 +1123,12 @@ void SensorMesh::loop() { TransportKey scope; if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { uint8_t tlen = telemetry.getSize(); - uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); // this will be an unknown 'tag' to the client - memcpy(reply_data, ×tamp, 4); - memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + memcpy(reply_data, &c->extra.sensor.push_tag, 4); + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + memcpy(&reply_data[4], ×tamp, 4); + memcpy(&reply_data[8], telemetry.getBuffer(), tlen); - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 4 + tlen); + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 8 + tlen); if (reply) { if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, c->out_path, c->out_path_len, 0); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 1e9f472d..39e71f2c 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -30,6 +30,7 @@ struct ClientInfo { } room; struct { uint32_t expiry_timestamp; // epoch seconds + uint32_t push_tag; uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber uint8_t min_deltas_len; uint8_t min_deltas[14]; // LPP encoded From 8660d1c973d1770275fe36cd990623a5a807b204 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 8 Sep 2026 17:55:02 +1000 Subject: [PATCH 06/37] * refactor: LPP helpers moved to LPPData class --- examples/simple_sensor/SensorMesh.cpp | 126 +++----------------- src/helpers/sensors/LPPDataHelpers.h | 159 ++++++++++++++++++-------- 2 files changed, 128 insertions(+), 157 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 8c6b06a6..de58e2b1 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1,5 +1,6 @@ #include "SensorMesh.h" #include +#include /* ------------------------------ Config -------------------------------- */ @@ -80,103 +81,6 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) { /* --------------------- Cayenne LPP helpers ----------------------------*/ -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_RELATIVE_HUMIDITY: - 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: - case LPP_RELATIVE_HUMIDITY: - 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; -} - static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type) { uint8_t i = 0; @@ -185,10 +89,10 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u uint8_t ch = buf[i++]; // Get data type uint8_t t = buf[i++]; - uint8_t sz = getDataSize(t); + uint8_t sz = LPPData::getDataSize(t); if (ch == channel && t == type) { - return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); + return LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); } i += sz; // skip } @@ -209,10 +113,10 @@ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_l uint8_t ch = buf[i++]; // Get data type uint8_t t = buf[i++]; - uint8_t sz = getDataSize(t); + uint8_t sz = LPPData::getDataSize(t); - float v = getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); - float pv = getFloat(&prev_telem[i], sz, getMultiplier(t), isSigned(t)); + float v = LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + float pv = LPPData::getFloat(&prev_telem[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); float min_delta = findTelemValue(min_deltas, min_deltas_len, ch, t); if (abs(v - pv) > min_delta) return true; // Yes, has changed @@ -268,12 +172,12 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u 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); + uint8_t sz = LPPData::getDataSize(d->_lpp_type); + uint32_t mult = LPPData::getMultiplier(d->_lpp_type); + bool is_signed = LPPData::isSigned(d->_lpp_type); + ofs += LPPData::putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed); + ofs += LPPData::putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed); + ofs += LPPData::putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed); } return ofs; } @@ -317,9 +221,9 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u from->extra.sensor.push_tag = 0; from->extra.sensor.expiry_timestamp = 0; // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? - reply_data[4] = 0; // success - getRNG()->random(&reply_data[5], 3); // just some entropy for better packet-hash uniqueness - return 8; + memset(&reply_data[4], 0, 8); // success + getRNG()->random(&reply_data[12], 2); // just some entropy for better packet-hash uniqueness + return 12 + 2; } return 0; // unknown command } diff --git a/src/helpers/sensors/LPPDataHelpers.h b/src/helpers/sensors/LPPDataHelpers.h index 70a036c4..db49cade 100644 --- a/src/helpers/sensors/LPPDataHelpers.h +++ b/src/helpers/sensors/LPPDataHelpers.h @@ -63,12 +63,66 @@ #define LPP_ERROR_OVERFLOW 1 #define LPP_ERROR_UNKOWN_TYPE 2 -class LPPReader { - const uint8_t* _buf; - uint8_t _len; - uint8_t _pos; +class LPPData { +public: + 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_RELATIVE_HUMIDITY: + case LPP_ALTITUDE: + case LPP_VOLTAGE: + case LPP_CURRENT: + case LPP_DIRECTION: + case LPP_POWER: + return 2; + } + return 1; + } - float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { + 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: + case LPP_RELATIVE_HUMIDITY: + 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]; @@ -85,6 +139,36 @@ class LPPReader { 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; + } + +}; + +class LPPReader { + const uint8_t* _buf; + uint8_t _len; + uint8_t _pos; + public: LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { } @@ -103,72 +187,42 @@ public: } bool readGPS(float& lat, float& lon, float& alt) { - lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; - lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; - alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3; + lat = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; + lon = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; + alt = LPPData::getFloat(&_buf[_pos], 3, 100, true); _pos += 3; return _pos <= _len; } bool readVoltage(float& voltage) { - voltage = getFloat(&_buf[_pos], 2, 100, false); _pos += 2; + voltage = LPPData::getFloat(&_buf[_pos], 2, 100, false); _pos += 2; return _pos <= _len; } bool readCurrent(float& amps) { - amps = getFloat(&_buf[_pos], 2, 1000, true); _pos += 2; + amps = LPPData::getFloat(&_buf[_pos], 2, 1000, true); _pos += 2; return _pos <= _len; } bool readPower(float& watts) { - watts = getFloat(&_buf[_pos], 2, 1, false); _pos += 2; + watts = LPPData::getFloat(&_buf[_pos], 2, 1, false); _pos += 2; return _pos <= _len; } bool readTemperature(float& degrees_c) { - degrees_c = getFloat(&_buf[_pos], 2, 10, true); _pos += 2; + degrees_c = LPPData::getFloat(&_buf[_pos], 2, 10, true); _pos += 2; return _pos <= _len; } bool readPressure(float& pa) { - pa = getFloat(&_buf[_pos], 2, 10, false); _pos += 2; + pa = LPPData::getFloat(&_buf[_pos], 2, 10, false); _pos += 2; return _pos <= _len; } bool readRelativeHumidity(float& pct) { - pct = getFloat(&_buf[_pos], 1, 2, false); _pos += 1; + pct = LPPData::getFloat(&_buf[_pos], 1, 2, false); _pos += 1; return _pos <= _len; } bool readAltitude(float& m) { - m = getFloat(&_buf[_pos], 2, 1, true); _pos += 2; + m = LPPData::getFloat(&_buf[_pos], 2, 1, true); _pos += 2; return _pos <= _len; } void skipData(uint8_t type) { - switch (type) { - case LPP_GPS: - _pos += 9; break; - case LPP_POLYLINE: - _pos += 8; break; // TODO: this is MINIMUM - case LPP_GYROMETER: - case LPP_ACCELEROMETER: - _pos += 6; break; - case LPP_GENERIC_SENSOR: - case LPP_FREQUENCY: - case LPP_DISTANCE: - case LPP_ENERGY: - case LPP_UNIXTIME: - _pos += 4; break; - case LPP_COLOUR: - _pos += 3; break; - 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: - _pos += 2; break; - default: - _pos++; - } + _pos += LPPData::getDataSize(type); } }; @@ -185,6 +239,19 @@ class LPPWriter { public: LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { } + bool writeData(uint8_t channel, uint8_t type, float v) { + uint8_t sz = LPPData::getDataSize(type); + bool s = LPPData::isSigned(type); + uint32_t mul = LPPData::getMultiplier(type); + if (_len + 2 + sz <= _max_len) { + _buf[_len++] = channel; + _buf[_len++] = type; + _len += LPPData::putFloat(&_buf[_len], v, sz, mul, s); + return true; + } + return false; + } + bool writeVoltage(uint8_t channel, float voltage) { if (_len + 4 <= _max_len) { _buf[_len++] = channel; From d69ae98b5a2db3b7484183d5acaac8b3d550f3ff Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 12 Sep 2026 16:53:45 +1000 Subject: [PATCH 07/37] * telemHasChanged() mod, so that only _specified_ min_delta values are considered. --- examples/simple_sensor/SensorMesh.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index de58e2b1..4f2ed8bf 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -81,7 +81,7 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) { /* --------------------- Cayenne LPP helpers ----------------------------*/ -static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type) { +static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type, float def_value) { uint8_t i = 0; while (i + 2 < size) { @@ -96,7 +96,7 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u } i += sz; // skip } - return 0.0f; // not found + return def_value; // not found } /* ------------------ end Cayenne LPP helpers ----------------------*/ @@ -117,12 +117,14 @@ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_l float v = LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); float pv = LPPData::getFloat(&prev_telem[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); - float min_delta = findTelemValue(min_deltas, min_deltas_len, ch, t); + float min_delta = min_deltas_len > 0 + ? findTelemValue(min_deltas, min_deltas_len, ch, t, 1.0e+16f) // default is just something BIG + : 0.0f; // for ANY change if (abs(v - pv) > min_delta) return true; // Yes, has changed i += sz; // skip } - return false; // no changes + return false; // no changes (OR none of the -specified- telemetry values changed by min_delta) } uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { @@ -961,7 +963,7 @@ void SensorMesh::formatPacketStatsReply(char *reply) { } float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) { - return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type); + return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type, 0.0f); } bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) { From 21cdd7b319c0145ebc25dcd05eec34341434a798 Mon Sep 17 00:00:00 2001 From: TJ Downes <273720+tjdownes@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:38:55 -0700 Subject: [PATCH 08/37] Add Muzi Base board support (Duo + Uno + SuperIO) One nRF52840 variant shared across both radios, picked per env: Duo runs the LR1121, Uno the SX1262. Same PCB, only the radio and its DIO1 pin change. SuperIO adds the SH1107 OLED, GPS, buzzer and joystick. GPS is driven by the 3-position mode switch (Mode 2 = on), polled live the way thinknode_m1 does it. Five quick user-button presses power the unit off; a press wakes it back on (arms the button as an nRF52 GPIO SENSE source before SYSTEMOFF). Consolidates #2054 and andyshinn's shared-base/uno work, rebased on dev. Co-authored-by: lbibass Co-authored-by: Andy Shinn --- boards/muzi_base.json | 72 +++++ src/helpers/radiolib/CustomLR1121.h | 41 +++ src/helpers/radiolib/CustomLR1121Wrapper.h | 50 ++++ src/helpers/ui/SH1107Display.cpp | 113 +++++++ src/helpers/ui/SH1107Display.h | 43 +++ variants/muzi_base/muzi_baseBoard.cpp | 52 ++++ variants/muzi_base/muzi_baseBoard.h | 48 +++ variants/muzi_base/platformio.ini | 326 +++++++++++++++++++++ variants/muzi_base/target.cpp | 162 ++++++++++ variants/muzi_base/target.h | 64 ++++ variants/muzi_base/variant.cpp | 90 ++++++ variants/muzi_base/variant.h | 169 +++++++++++ 12 files changed, 1230 insertions(+) create mode 100644 boards/muzi_base.json create mode 100644 src/helpers/radiolib/CustomLR1121.h create mode 100644 src/helpers/radiolib/CustomLR1121Wrapper.h create mode 100644 src/helpers/ui/SH1107Display.cpp create mode 100644 src/helpers/ui/SH1107Display.h create mode 100644 variants/muzi_base/muzi_baseBoard.cpp create mode 100644 variants/muzi_base/muzi_baseBoard.h create mode 100644 variants/muzi_base/platformio.ini create mode 100644 variants/muzi_base/target.cpp create mode 100644 variants/muzi_base/target.h create mode 100644 variants/muzi_base/variant.cpp create mode 100644 variants/muzi_base/variant.h diff --git a/boards/muzi_base.json b/boards/muzi_base.json new file mode 100644 index 00000000..1b2cf7df --- /dev/null +++ b/boards/muzi_base.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_MUZI_BASE -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x4405" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ] + ], + "usb_product": "muzi_base", + "mcu": "nrf52840", + "variant": "MUZI_BASE", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": [ + "jlink" + ], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "Muzi Base", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ] + }, + "url": "https://github.com/muzi-works", + "vendor": "MuziWorks" +} diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h new file mode 100644 index 00000000..873bfabc --- /dev/null +++ b/src/helpers/radiolib/CustomLR1121.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include "MeshCore.h" + +class CustomLR1121 : public LR1121 { + bool _rx_boosted = false; + + public: + CustomLR1121(Module *mod) : LR1121(mod) { } + + size_t getPacketLength(bool update) override { + size_t len = LR1121::getPacketLength(update); + if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { + // we've just received a corrupted packet + // this may have triggered a bug causing subsequent packets to be shifted + // call standby() to return radio to known-good state + // recvRaw will call startReceive() to restart rx + MESH_DEBUG_PRINTLN("LR1121: got header err, calling standby()"); + standby(); + } + return len; + } + + float getFreqMHz() const { return freqMHz; } + + int16_t setRxBoostedGainMode(bool en) { + _rx_boosted = en; + return LR1121::setRxBoostedGainMode(en); + } + + bool getRxBoostedGainMode() const { return _rx_boosted; } + + bool isReceiving() { + uint16_t irq = getIrqStatus(); + bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); + return detected; + } + uint8_t getSpreadingFactor() const { return spreadingFactor; } + +}; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomLR1121Wrapper.h b/src/helpers/radiolib/CustomLR1121Wrapper.h new file mode 100644 index 00000000..5361ee23 --- /dev/null +++ b/src/helpers/radiolib/CustomLR1121Wrapper.h @@ -0,0 +1,50 @@ +#pragma once + +#include "CustomLR1121.h" +#include "RadioLibWrappers.h" +#include "LR11x0Reset.h" + +class CustomLR1121Wrapper : public RadioLibWrapper { +public: + CustomLR1121Wrapper(CustomLR1121& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } + + void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override { + ((CustomLR1121 *)_radio)->setFrequency(freq); + ((CustomLR1121 *)_radio)->setSpreadingFactor(sf); + ((CustomLR1121 *)_radio)->setBandwidth(bw); + ((CustomLR1121 *)_radio)->setCodingRate(cr); + updatePreamble(sf); + } + + void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1121 *)_radio)->getFreqMHz(), getRxBoostedGainMode()); } + bool isReceivingPacket() override { + return ((CustomLR1121 *)_radio)->isReceiving(); + } + float getCurrentRSSI() override { + float rssi = -110; + ((CustomLR1121 *)_radio)->getRssiInst(&rssi); + return rssi; + } + + void onSendFinished() override { + RadioLibWrapper::onSendFinished(); + _radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts + } + + uint32_t getEstAirtimeFor(int len_bytes) override { + auto airtime = RadioLibWrapper::getEstAirtimeFor(len_bytes); + return airtime < 200 ? 200 : airtime; // at least 200 millis + } + + float getLastRSSI() const override { return ((CustomLR1121 *)_radio)->getRSSI(); } + float getLastSNR() const override { return ((CustomLR1121 *)_radio)->getSNR(); } + + uint8_t getSpreadingFactor() const override { return ((CustomLR1121 *)_radio)->getSpreadingFactor(); } + + bool setRxBoostedGainMode(bool en) override { + return ((CustomLR1121 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; + } + bool getRxBoostedGainMode() const override { + return ((CustomLR1121 *)_radio)->getRxBoostedGainMode(); + } +}; diff --git a/src/helpers/ui/SH1107Display.cpp b/src/helpers/ui/SH1107Display.cpp new file mode 100644 index 00000000..09bf424f --- /dev/null +++ b/src/helpers/ui/SH1107Display.cpp @@ -0,0 +1,113 @@ +#include "SH1107Display.h" +#include +#include "Adafruit_SH110X.h" + +#ifndef DISPLAY_ROTATION +#define DISPLAY_ROTATION 0 +#endif + +ColorVal UIColor::window_bkg = SH110X_BLACK; +ColorVal UIColor::title_bkg = SH110X_BLACK; +ColorVal UIColor::title_txt = SH110X_WHITE; +ColorVal UIColor::primary_txt = SH110X_WHITE; +ColorVal UIColor::secondary_txt = SH110X_WHITE; +ColorVal UIColor::warning_txt = SH110X_WHITE; +ColorVal UIColor::popup_bkg = SH110X_BLACK; +ColorVal UIColor::popup_txt = SH110X_WHITE; +ColorVal UIColor::corp_blue = SH110X_WHITE; + +bool SH1107Display::i2c_probe(TwoWire &wire, uint8_t addr) +{ + wire.beginTransmission(addr); + uint8_t error = wire.endTransmission(); + return (error == 0); +} + +bool SH1107Display::begin() +{ + bool result = display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS); + if (result) { + display.setRotation(DISPLAY_ROTATION); + } + return result; +} + +void SH1107Display::turnOn() +{ + display.oled_command(SH110X_DISPLAYON); + uint8_t cmd[] = {0xD5, 0xF0}; + display.oled_commandList(cmd, 2); + _isOn = true; +} + +void SH1107Display::turnOff() +{ + display.oled_command(SH110X_DISPLAYOFF); + _isOn = false; +} + +void SH1107Display::clear() +{ + display.clearDisplay(); + display.display(); +} + +void SH1107Display::startFrame(ColorVal bkg) +{ + display.clearDisplay(); // TODO: apply 'bkg' + display.setContrast(120); // 0-127. default setting was causing some flickering. + // display.SH110X_SETPRECHARGE(255); + _color = SH110X_WHITE; + display.setTextColor(_color); + display.setTextSize(1); + display.cp437(true); // Use full 256 char 'Code Page 437' font +} + +void SH1107Display::setTextSize(int sz) +{ + display.setTextSize(sz); +} + +void SH1107Display::setColor(ColorVal c) +{ + _color = (c != 0) ? SH110X_WHITE : SH110X_BLACK; + display.setTextColor(_color); +} + +void SH1107Display::setCursor(int x, int y) +{ + display.setCursor(x, y); +} + +void SH1107Display::print(const char *str) +{ + display.print(str); +} + +void SH1107Display::fillRect(int x, int y, int w, int h) +{ + display.fillRect(x, y, w, h, _color); +} + +void SH1107Display::drawRect(int x, int y, int w, int h) +{ + display.drawRect(x, y, w, h, _color); +} + +void SH1107Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) +{ + display.drawBitmap(x, y, bits, w, h, SH110X_WHITE); +} + +uint16_t SH1107Display::getTextWidth(const char *str) +{ + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); + return w; +} + +void SH1107Display::endFrame() +{ + display.display(); +} diff --git a/src/helpers/ui/SH1107Display.h b/src/helpers/ui/SH1107Display.h new file mode 100644 index 00000000..417184c2 --- /dev/null +++ b/src/helpers/ui/SH1107Display.h @@ -0,0 +1,43 @@ +#pragma once + +#include "DisplayDriver.h" +#include +#include +#define SH110X_NO_SPLASH +#include + +#ifndef PIN_OLED_RESET +#define PIN_OLED_RESET -1 +#endif + +#ifndef DISPLAY_ADDRESS +#define DISPLAY_ADDRESS 0x3c +#endif + +class SH1107Display : public DisplayDriver +{ + Adafruit_SH1107 display; + bool _isOn; + uint8_t _color; + + bool i2c_probe(TwoWire &wire, uint8_t addr); + +public: + SH1107Display() : DisplayDriver(128, 128), display(128, 128, &Wire, PIN_OLED_RESET) { _isOn = false; } + bool begin(); + + bool isOn() override { return _isOn; } + void turnOn() override; + void turnOff() override; + void clear() override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; + void setTextSize(int sz) override; + void setColor(ColorVal c) override; + void setCursor(int x, int y) override; + void print(const char *str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override; + uint16_t getTextWidth(const char *str) override; + void endFrame() override; +}; diff --git a/variants/muzi_base/muzi_baseBoard.cpp b/variants/muzi_base/muzi_baseBoard.cpp new file mode 100644 index 00000000..ed713fa0 --- /dev/null +++ b/variants/muzi_base/muzi_baseBoard.cpp @@ -0,0 +1,52 @@ +#include +#include + +#include "muzi_baseBoard.h" + +#ifdef NRF52_POWER_MANAGEMENT +const PowerMgtConfig power_config = { + .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, + .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK +}; + +void muzi_baseBoard::initiateShutdown(uint8_t reason) { + // Disable LoRa module power before shutdown + if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || + reason == SHUTDOWN_REASON_BOOT_PROTECT) { + configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + } + + enterSystemOff(reason); +} +#endif // NRF52_POWER_MANAGEMENT + +void muzi_baseBoard::begin() { + NRF52BoardDCDC::begin(); + pinMode(PIN_VBAT_READ, INPUT); +#ifdef muzi_base_superIO + // 12V rail is only needed for the superIO display + pinMode(SCREEN_12V_ENABLE, OUTPUT); + digitalWrite(SCREEN_12V_ENABLE, HIGH); // Enable 12V power for SH1107 display + delay(250); +#endif + Wire.begin(); + // delay(1000); // wait for display to initialize. otherwise it doesn't come up on boot. + +#ifdef PIN_USER_BTN + pinMode(PIN_USER_BTN, INPUT_PULLUP); +#endif + 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_BOARD_SDA) && defined(PIN_BOARD_SCL) +// Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); +// #endif +#ifdef NRF52_POWER_MANAGEMENT + checkBootVoltage(&power_config); +#endif +} diff --git a/variants/muzi_base/muzi_baseBoard.h b/variants/muzi_base/muzi_baseBoard.h new file mode 100644 index 00000000..e5412b6a --- /dev/null +++ b/variants/muzi_base/muzi_baseBoard.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +// The Muzi Base PCB ships in two radio flavors. Keep the user-facing identity +// (OTA/DFU name and reported manufacturer) distinct per radio even though the +// board logic is shared. +#if defined(USE_SX1262) + #define MUZI_BASE_OTA_NAME "MUZI_BASE_UNO_OTA" + #define MUZI_BASE_MFR_NAME "Muzi Base Uno" +#else + #define MUZI_BASE_OTA_NAME "MUZI_BASE_DUO_OTA" + #define MUZI_BASE_MFR_NAME "Muzi Base Duo" +#endif + +class muzi_baseBoard : public NRF52BoardDCDC { +protected: +#ifdef NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif + +public: + muzi_baseBoard() : NRF52Board(MUZI_BASE_OTA_NAME) {} + void begin(); + + #define BATTERY_SAMPLES 8 + + uint16_t getBattMilliVolts() override { + analogReadResolution(12); + analogReference(AR_INTERNAL_3_0); + delay(1); + + uint32_t raw = 0; + for (int i = 0; i < BATTERY_SAMPLES; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / BATTERY_SAMPLES; + + // ADC_MULTIPLIER is the voltage divider ratio + return (raw * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return MUZI_BASE_MFR_NAME; + } +}; diff --git a/variants/muzi_base/platformio.ini b/variants/muzi_base/platformio.ini new file mode 100644 index 00000000..eb35d569 --- /dev/null +++ b/variants/muzi_base/platformio.ini @@ -0,0 +1,326 @@ +; ============================================================================ +; Muzi Base (MuziWorks) +; +; One nRF52840 board, shipped in two radio flavors. The radio is selected per +; environment; all board logic lives in variants/muzi_base and is shared: +; muzi_base_duo_* -> LR1121 (USE_LR1121) +; muzi_base_uno_* -> SX1262 (USE_SX1262) +; ============================================================================ + +[muzi_base_common] +extends = nrf52_base +board = muzi_base +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/muzi_base + -I src/helpers/ui + -D muzi_base + -D NRF52_POWER_MANAGEMENT + -D PIN_USER_BTN=PIN_BUTTON1 + -D USER_BTN_PRESSED=LOW + -D PIN_STATUS_LED=35 + -D LORA_TX_POWER=22 + -D P_LORA_BUSY=LORA_BUSY + -D P_LORA_SCLK=LORA_SCLK + -D P_LORA_NSS=LORA_NSS + -D P_LORA_DIO_1=LORA_DIO_1 + -D P_LORA_MISO=LORA_MISO + -D P_LORA_MOSI=LORA_MOSI + -D P_LORA_RESET=LORA_RESET + -D QSPIFLASH=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + +<../variants/muzi_base> +debug_tool = jlink +upload_protocol = nrfutil +lib_deps = + ${nrf52_base.lib_deps} + ${sensor_base.lib_deps} + +; --------------------------------------------------------------------------- +; Radio selection (Duo = LR1121, Uno = SX1262). The matching radio pins / TCXO +; / RF-switch defines are picked up in variant.h via USE_LR1121 / USE_SX1262. +; --------------------------------------------------------------------------- +[muzi_base_duo] +extends = muzi_base_common +build_flags = ${muzi_base_common.build_flags} + -D USE_LR1121 + -D RADIO_CLASS=CustomLR1121 + -D WRAPPER_CLASS=CustomLR1121Wrapper + -D RF_SWITCH_TABLE + -D RX_BOOSTED_GAIN=true + -D LR11X0_DIO_AS_RF_SWITCH=true + -D LR11X0_DIO3_TCXO_VOLTAGE=3.0 + +[muzi_base_uno] +extends = muzi_base_common +build_flags = ${muzi_base_common.build_flags} + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_DIO2_AS_RF_SWITCH=1 + -D SX126X_DIO3_TCXO_VOLTAGE=3.3 + +; =========================================================================== +; Duo (LR1121) environments +; =========================================================================== +[env:muzi_base_duo_repeater] +extends = muzi_base_duo +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_duo.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${muzi_base_duo.lib_deps} + +[env:muzi_base_duo_room_server] +extends = muzi_base_duo +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base 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 = ${muzi_base_duo.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${muzi_base_duo.lib_deps} + +[env:muzi_base_duo_companion_radio_usb] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${muzi_base_duo.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:muzi_base_duo_companion_radio_ble] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + ; -D ADVERT_NAME='"@@MAC"' +build_src_filter = ${muzi_base_duo.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[muzi_base_duo_superIO] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -D muzi_base_superIO + -D UI_HAS_JOYSTICK=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1107Display + -D DISPLAY_ROTATION=2 + -D ENV_INCLUDE_GPS=1 + -D ENV_SKIP_GPS_DETECT + -D PIN_BUZZER=22 +build_src_filter = ${muzi_base_duo.build_src_filter} + + + + + + + + + + +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit SH110X@^2.1.14 + artronshop/ArtronShop_RX8130CE@1.0.0 + adafruit/Adafruit GFX Library @ ^1.12.1 +debug_tool = jlink +upload_protocol = nrfutil + +[env:muzi_base_duo_companion_radio_ble_superIO] +extends = muzi_base_duo_superIO +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo_superIO.build_flags} + -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -I examples/companion_radio/ui-new + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${muzi_base_duo_superIO.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${muzi_base_duo_superIO.lib_deps} + +; =========================================================================== +; Uno (SX1262) environments +; =========================================================================== +[env:muzi_base_uno_repeater] +extends = muzi_base_uno +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_uno.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${muzi_base_uno.lib_deps} + +[env:muzi_base_uno_room_server] +extends = muzi_base_uno +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base 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 = ${muzi_base_uno.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${muzi_base_uno.lib_deps} + +[env:muzi_base_uno_companion_radio_usb] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${muzi_base_uno.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:muzi_base_uno_companion_radio_ble] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + ; -D ADVERT_NAME='"@@MAC"' +build_src_filter = ${muzi_base_uno.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[muzi_base_uno_superIO] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -D muzi_base_superIO + -D UI_HAS_JOYSTICK=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1107Display + -D DISPLAY_ROTATION=2 + -D ENV_INCLUDE_GPS=1 + -D ENV_SKIP_GPS_DETECT + -D PIN_BUZZER=22 +build_src_filter = ${muzi_base_uno.build_src_filter} + + + + + + + + + + +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit SH110X@^2.1.14 + artronshop/ArtronShop_RX8130CE@1.0.0 + adafruit/Adafruit GFX Library @ ^1.12.1 +debug_tool = jlink +upload_protocol = nrfutil + +[env:muzi_base_uno_companion_radio_ble_superIO] +extends = muzi_base_uno_superIO +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno_superIO.build_flags} + -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -I examples/companion_radio/ui-new + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${muzi_base_uno_superIO.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${muzi_base_uno_superIO.lib_deps} diff --git a/variants/muzi_base/target.cpp b/variants/muzi_base/target.cpp new file mode 100644 index 00000000..2af0c710 --- /dev/null +++ b/variants/muzi_base/target.cpp @@ -0,0 +1,162 @@ +#include +#include +#include "target.h" +#include "variant.h" + +muzi_baseBoard 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); +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); + MuziBaseSensorManager sensors = MuziBaseSensorManager(nmea); +#else + MuziBaseSensorManager sensors; +#endif + +// user button for the N-click power-off. single-click mode (multiclick off) so +// each press is its own CLICK; we count them ourselves. reliable across the +// idle-sleep loop because it's the same MomentaryButton the UI navigates with. +static MomentaryButton pwr_btn(PIN_USER_BTN, 0, true, true, false); + +// power off, arming the user button as a wake source first so a later press +// turns it back on. wait for the button to release (the 5th click already fires +// on release, so this returns quickly), then set nRF52 GPIO SENSE (press = LOW); +// it survives into SYSTEMOFF because shutdownPeripherals() doesn't touch this pin. +static void muziPowerOff() { + uint32_t t0 = millis(); + while (digitalRead(PIN_USER_BTN) == USER_BTN_PRESSED && (millis() - t0) < 3000) { delay(5); } + delay(50); // settle + nrf_gpio_cfg_sense_input(g_ADigitalPinMap[PIN_USER_BTN], NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + board.powerOff(); // shutdownPeripherals() + SYSTEMOFF +} + +bool MuziBaseSensorManager::begin() { + pwr_btn.begin(); + bool ok = EnvironmentSensorManager::begin(); +#if ENV_INCLUDE_GPS + pinMode(PIN_GPS_SWITCH, INPUT); + _last_gps_sw = digitalRead(PIN_GPS_SWITCH); // initial gps state from the switch + if (_last_gps_sw == HIGH) start_gps(); else stop_gps(); +#endif + return ok; +} + +void MuziBaseSensorManager::loop() { + // user button: USER_BTN_POWER_OFF_CLICKS quick clicks -> power off + if (pwr_btn.check() == BUTTON_EVENT_CLICK) { + unsigned long t = millis(); + if (t - _pwr_last_click > 2000) _pwr_clicks = 0; // restart if too slow + _pwr_last_click = t; + if (++_pwr_clicks >= USER_BTN_POWER_OFF_CLICKS) muziPowerOff(); + } +#if ENV_INCLUDE_GPS + unsigned long now = millis(); + if (now > _next_sw_check) { // check the mode switch ~once a sec + _next_sw_check = now + 1000; + int sw = digitalRead(PIN_GPS_SWITCH); + if (sw != _last_gps_sw) { + _last_gps_sw = sw; + if (sw == HIGH) start_gps(); else stop_gps(); + } + } +#endif + EnvironmentSensorManager::loop(); +} + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, false, false); + MomentaryButton joystick_left(JOYSTICK_LEFT, 1000, true, false, false); + MomentaryButton joystick_right(JOYSTICK_RIGHT, 1000, true, false, false); + MomentaryButton back_btn(PIN_BACK_BTN, 1000, true, false, true); +#endif + +#if defined(USE_LR1121) + #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_NC + }; + + static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW, LOW}}, + { LR11x0::MODE_RX, {HIGH, LOW}}, + { LR11x0::MODE_TX, {LOW, HIGH}}, + { LR11x0::MODE_TX_HP, {LOW, HIGH}}, + { LR11x0::MODE_TX_HF, {LOW, LOW}}, + { LR11x0::MODE_GNSS, {LOW, LOW}}, + { LR11x0::MODE_WIFI, {LOW, LOW}}, + END_OF_MODE_TABLE, + }; + #endif +#endif // USE_LR1121 + +bool radio_init() { + //rtc_clock.begin(Wire); + +#if defined(USE_LR1121) + #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(2); + radio.explicitHeader(); + + #ifdef RF_SWITCH_TABLE + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); + #endif + #ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); + #endif + + return true; // success +#else // USE_SX1262 + // CustomSX1262::std_init() configures the SPI pins, runs begin() with the + // TCXO-voltage fallback, sets CRC, current limit, the DIO2 RF switch, and + // RX boosted gain from the SX126X_* build flags. + return radio.std_init(&SPI); +#endif +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(int8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/muzi_base/target.h b/variants/muzi_base/target.h new file mode 100644 index 00000000..cbcfff14 --- /dev/null +++ b/variants/muzi_base/target.h @@ -0,0 +1,64 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include "muzi_baseBoard.h" +#if defined(USE_LR1121) + #include +#elif defined(USE_SX1262) + #include +#else + #error "muzi_base: no radio selected (define USE_LR1121 or USE_SX1262)" +#endif +#include +#include +#include +#include +#include // Added: Include for EnvironmentSensorManager +#include + + +#ifdef muzi_base_superIO + #include + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern MomentaryButton joystick_left; + extern MomentaryButton joystick_right; + extern MomentaryButton back_btn; +#elif defined(DISPLAY_CLASS) + #include "helpers/ui/NullDisplayDriver.h" + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +extern muzi_baseBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; + +// muzi sensor manager, polled from main loop via sensors.loop(): +// - user button: USER_BTN_POWER_OFF_CLICKS quick presses -> power off (all builds) +// - gps mode switch on/off (superIO only, see thinknode_m1) +class MuziBaseSensorManager : public EnvironmentSensorManager { + unsigned long _pwr_last_click = 0; + uint8_t _pwr_clicks = 0; +#if ENV_INCLUDE_GPS + int _last_gps_sw = -1; + unsigned long _next_sw_check = 0; +#endif +public: +#if ENV_INCLUDE_GPS + MuziBaseSensorManager(LocationProvider& location) : EnvironmentSensorManager(location) {} +#else + MuziBaseSensorManager() {} +#endif + bool begin() override; + void loop() override; +}; +extern MuziBaseSensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(int8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/muzi_base/variant.cpp b/variants/muzi_base/variant.cpp new file mode 100644 index 00000000..b179af8a --- /dev/null +++ b/variants/muzi_base/variant.cpp @@ -0,0 +1,90 @@ +/* + * 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, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, +// P1 pins. + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, +}; + +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(PIN_VBAT_READ, INPUT); + pinMode(PIN_BATTERY_CHARGING, INPUT); + pinMode(PIN_CHARGER_FAULT, INPUT); + pinMode(PIN_BUTTON1, INPUT); + pinMode(PIN_BUTTON2, INPUT); + pinMode(PIN_BUTTON3, INPUT); + pinMode(PIN_BUTTON4, INPUT); + pinMode(PIN_BUTTON5, INPUT); + pinMode(PIN_BUTTON6, INPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(LED_BLUE, OUTPUT); + digitalWrite(LED_PIN, LOW); + digitalWrite(LED_BLUE, LOW); + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); // turn off buzzer at start. don't leave it high. + // gps power is driven by the sensor manager (mode switch). off to start. + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); + + pinMode(SCREEN_12V_ENABLE, OUTPUT); + digitalWrite(SCREEN_12V_ENABLE, LOW); // disable 12V power for SH1107 display for now. +} diff --git a/variants/muzi_base/variant.h b/variants/muzi_base/variant.h new file mode 100644 index 00000000..b007c1f8 --- /dev/null +++ b/variants/muzi_base/variant.h @@ -0,0 +1,169 @@ +/* + * 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 PIN_VBAT_READ (31) // P0.31 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.537 +#define ADC_RESOLUTION 14 +#define PIN_BATTERY_CHARGING (32+2) // P1.02 STAT2 +#define PIN_CHARGER_FAULT (27) // P0.27 STAT1 this pin is disabled on meshtastic. +// BQ25185 has 2 status pins: STAT1 and STAT2. Both are high when not charging. STAT1 high, STAT2 low: charging. Recoverable fault: STAT1 low, STAT2 high. Unrecoverable fault: both low. +// We only need to detect charging vs not charging, but someone else can use the fault pin to log when the battery gets too hot or cold. + +// Power management boot protection threshold (millivolts) +#define PWRMGT_VOLTAGE_BOOTLOCK 3100 // Won't boot below this voltage (mV). BB15 battery min voltage is 3v, 3100mV is minimum batt in meshtastic code. + +// LPCOMP wake configuration (voltage recovery from SYSTEMOFF) +#define PWRMGT_LPCOMP_AIN 7 // AIN7 = P0.31 = PIN_VBAT_READ +#define PWRMGT_LPCOMP_REFSEL 4 // 5/8 VDD (~3.13-3.44V) was the default on RAK4631. should still apply here. + +// Other pins +#define PIN_AREF (-1) +#define SCREEN_12V_ENABLE (23) // SH1107 OLED controller has a pin that needs to be enabled to turn on the screen. + +static const uint8_t AREF = (PIN_AREF); // not used + +//////////////////////////////////////////////////////////////////////////////// +// 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 (19) // P0.19 used for GPS RX +#define PIN_SERIAL1_TX (20) // P0.20 used for GPS TX + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (2) + +#define PIN_WIRE1_SDA (4) // P0.4 +#define PIN_WIRE1_SCL (6) // P0.6 +#define PIN_WIRE_SDA (24) // P0.24 OLED I2C +#define PIN_WIRE_SCL (25) // P0.25 OLED I2C +#define I2C_NO_RESCAN +// #define I2C_NO_RESCAN +// #define HAS_QMA6100P +// #define QMA_6100P_INT_PIN (34) // P1.2 + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (32+15) // internally connected to p1.15 +#define PIN_SPI_MOSI (32+14) // internally connected to p1.14 +#define PIN_SPI_SCK (32+13) // internally connected to p1.13 +#define PIN_SPI_NSS (32+12) // internally connected to p1.12 + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define LED_BUILTIN (35) +#define LED_BLUE (-1) // P1.04 turned off, because the blue LED was annoying. +// #define LED_GREEN (35) // P1.03 +#define LED_PIN LED_BUILTIN + +#define LED_STATE_ON LOW + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons +#define PIN_BUTTON1 (10) // P0.10 Menu / User Button | on superIO, this is in the center of the "D-Pad", but it's also the button on the Uno/Duo. +#define PIN_BUTTON2 (21) // Joystick Up +#define PIN_BUTTON3 (17) // Joystick Down +#define PIN_BUTTON4 (37) // Joystick Left +#define PIN_BUTTON5 (16) // Joystick Right +#define PIN_BUTTON6 (15) // Back / Cancel Button. +#define JOYSTICK_PRESS PIN_BUTTON1 +#define JOYSTICK_UP PIN_BUTTON2 +#define JOYSTICK_DOWN PIN_BUTTON3 +#define JOYSTICK_LEFT PIN_BUTTON4 +#define JOYSTICK_RIGHT PIN_BUTTON5 +#define PIN_BACK_BTN PIN_BUTTON6 + +// quick user-button presses that power the device off (per Muzi's docs) +#ifndef USER_BTN_POWER_OFF_CLICKS +#define USER_BTN_POWER_OFF_CLICKS 5 +#endif + +//////////////////////////////////////////////////////////////////////////////// +// LoRa radio +// +// The Muzi Base PCB is populated with one of two radios: +// * Base Uno -> SX1262 (USE_SX1262) +// * Base Duo -> LR1121 (USE_LR1121) +// Both share NSS/SCLK/MISO/MOSI/RESET/BUSY. The only pin difference is the +// radio IRQ (DIO1): the SX1262 routes it to P1.06, the LR1121 to P1.08. +// (see Meshtastic muzi_base variant, which defines both radios on this board) + +#define LORA_NSS (PIN_SPI_NSS) // P1.12 +#define LORA_RESET (32+10) // P1.10 +#define LORA_BUSY (32+11) // P1.11 +#define LORA_SCLK (PIN_SPI_SCK) // P1.13 +#define LORA_MISO (PIN_SPI_MISO) // P1.15 +#define LORA_MOSI (PIN_SPI_MOSI) // P1.14 + +// only the IRQ pin differs per radio. rf-switch/tcxo defines are in platformio.ini +#if defined(USE_LR1121) + #define LORA_DIO_1 (32+8) // P1.08 LR1121 IRQ/DIO1 +#elif defined(USE_SX1262) + #define LORA_DIO_1 (32+6) // P1.06 SX1262 IRQ/DIO1 +#else + #error "muzi_base: no radio selected (define USE_LR1121 or USE_SX1262)" +#endif + +//////////////////////////////////////////////////////////////////////////////// +// QSPI Flash +#define PIN_QSPI_SCK (0 + 3) +#define PIN_QSPI_CS (0 + 26) +#define PIN_QSPI_IO0 (0 + 30) +#define PIN_QSPI_IO1 (0 + 29) +#define PIN_QSPI_IO2 (0 + 28) +#define PIN_QSPI_IO3 (0 + 2) + +#define EXTERNAL_FLASH_DEVICES W25Q128JVPQ +#define EXTERNAL_FLASH_USE_QSPI + +//////////////////////////////////////////////////////////////////////////////// +// GPS +#define HAS_GPS 1 +#define PIN_GPS_RX PIN_SERIAL1_RX +#define PIN_GPS_TX PIN_SERIAL1_TX +#define PIN_GPS_EN (32+1) // P1.01 PWR_IO2 on schematic. gps power, driven by the sensor manager. + +// superIO 3-position mode switch (pins from the meshtastic muzi_base variant). +// "Mode 2" reads HIGH on PIN_GPS_SWITCH and turns the gps on. +#define SWITCH_MODE1 (32+9) // P1.09 +#define SWITCH_MODE2 (12) // P0.12 +#define PIN_GPS_SWITCH SWITCH_MODE2 + +//////////////////////////////////////////////////////////////////////////////// +// Buzzer + +#define BUZZER_PIN (22) // P0.22 same load switch design as GPS_EN. From df82dc23390097e72fdef829d48a34b124cf1036 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 13 Sep 2026 11:05:38 +1000 Subject: [PATCH 09/37] * min_deltas now mandatory --- examples/simple_sensor/SensorMesh.cpp | 49 ++++++++++++++------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 4f2ed8bf..54bce50d 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -102,29 +102,24 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u /* ------------------ end Cayenne LPP helpers ----------------------*/ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { - if (telemetry.getSize() != prev_telem_size) return true; - 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++]; + while (i + 2 < min_deltas_len) { + uint8_t ch = min_deltas[i++]; // Get channel # + uint8_t t = min_deltas[i++]; // Get data type uint8_t sz = LPPData::getDataSize(t); - float v = LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); - float pv = LPPData::getFloat(&prev_telem[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); - float min_delta = min_deltas_len > 0 - ? findTelemValue(min_deltas, min_deltas_len, ch, t, 1.0e+16f) // default is just something BIG - : 0.0f; // for ANY change + float min_delta = LPPData::getFloat(&min_deltas[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + + float v = findTelemValue(buf, size, ch, t, 0.0f); + float pv = findTelemValue(prev_telem, prev_telem_size, ch, t, 0.0f); if (abs(v - pv) > min_delta) return true; // Yes, has changed i += sz; // skip } - return false; // no changes (OR none of the -specified- telemetry values changed by min_delta) + return false; // none of the -specified- telemetry values changed by min_delta } uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { @@ -202,26 +197,34 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u uint16_t timeout_secs; memcpy(&timeout_secs, &payload[4], 2); uint8_t reserved = payload[6]; + uint8_t min_deltas_len = payload[7]; RegionEntry* r; if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope r = recv_pkt_region; } else { // use default scope r = region_map.getDefaultRegion(); } - from->extra.sensor.scope_region_id = r ? r->id : 0; - from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; - from->extra.sensor.min_deltas_len = min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[7]); - // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) - memcpy(from->extra.sensor.min_deltas, &payload[8], from->extra.sensor.min_deltas_len); - - memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); // reply with actual expiry timestamp (or 0 for error) - strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used - return 8 + strlen((char *)&reply_data[8]); + uint8_t reply_len; + if (r && min_deltas_len >= 3 && min_deltas_len <= sizeof(from->extra.sensor.min_deltas)) { + from->extra.sensor.scope_region_id = r->id; + from->extra.sensor.expiry_timestamp = getRTCClock()->getCurrentTime() + timeout_secs; + from->extra.sensor.min_deltas_len = min_deltas_len; + memcpy(from->extra.sensor.min_deltas, &payload[8], min_deltas_len); + // reply with actual expiry timestamp + memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); + strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used + reply_len = 8 + strlen((char *)&reply_data[8]); + } else { + memset(&reply_data[4], 0, 4); // expiry timestamp (0 for error) + reply_len = 8; + } + return reply_len; } if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { from->extra.sensor.scope_region_id = 0; from->extra.sensor.push_tag = 0; from->extra.sensor.expiry_timestamp = 0; + from->extra.sensor.min_deltas_len = 0; // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? memset(&reply_data[4], 0, 8); // success getRNG()->random(&reply_data[12], 2); // just some entropy for better packet-hash uniqueness @@ -1021,7 +1024,7 @@ void SensorMesh::loop() { // compare with previous telemetry, check if any deltas are greater than subscriber minimums for (int i = 0; i < acl.getNumClients(); i++) { auto c = acl.getClientByIdx(i); - if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0) continue; // skip deleted entries, or Not subscribed to deltas + if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0 || c->extra.sensor.min_deltas_len == 0) continue; // skip deleted entries, or Not subscribed to deltas RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); if (r == NULL) continue; // unknown region scope if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired From c6b051d063c611f892fba0341919a969e36991a9 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 13 Sep 2026 11:40:25 +1000 Subject: [PATCH 10/37] * request payload doco --- docs/payloads.md | 52 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/payloads.md b/docs/payloads.md index fb9cbaf9..493f628c 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -93,6 +93,7 @@ Returned path messages provide a description of the route a packet took from the | Field | Size (bytes) | Description | |--------------|-----------------|------------------------------------------| | timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | request sub type | | request data | rest of payload | application-defined request payload body | For the common chat/server helpers in `BaseChatMesh`, the current request type values are: @@ -100,7 +101,13 @@ For the common chat/server helpers in `BaseChatMesh`, the current request type v | Value | Name | Description | |--------|-----------|----------------------------------------------------| | `0x01` | get stats | get stats of repeater or room server | -| `0x02` | keepalive | keep-alive request used for maintained connections | +| `0x02` | keepalive | (deprecated) | +| `0x03` | get telemetry | request node telemetry | +| `0x04` | get min/max/avg | get sensor node stats on time series data | +| `0x05` | get acl | node ACL query | +| `0x06` | get neighbors | node neighbors query | +| `0x08` | subscribe | subscribe to telemetry push | +| `0x09` | ubsubscribe | unsubscribe from telemetry push | #### Get stats @@ -125,21 +132,50 @@ Gets information about the node, possibly including the following: * Number posted (?) * Number of post pushes (?) -#### Get telemetry data - -Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware. - #### Get Telemetry -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x03 (request sub type) | +| permission mask | 1 | bitwise inverse mask to AND to permissions (0 = get ALL telem values) | #### Get Min/Max/Ave (Sensor nodes) -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x04 (request sub type) | +| start | 4 | starting time, seconds ago | +| end | 4 | ending time, seconds ago | +| reserved | 2 | should be zeroes | + +#### Subscribe to Telemetry push - (Sensor nodes) + +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x08 (request sub type) | +| push tag | 4 | 32-bit tag to be used in telemetry push _REPLY payloads | +| timeout secs | 2 | subscription timeout (seconds) | +| reserved | 1 | should be zero | +| min deltas len | 1 | byte length of LPP encoded min_deltas | +| min deltas | (variable) | LPP encoded min_deltas | + +#### Unsubscribe from Telemetry push - (Sensor nodes) + +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x09 (request sub type) | #### Get Access List -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x05 (request sub type) | +| reserved | 2 | should be zeroes | #### Get Neighbors From bc8ccefafe1423ba50a0932da815a40c063e6121 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 14 Sep 2026 20:23:10 +1000 Subject: [PATCH 11/37] * fix for telemHasChanged() --- examples/simple_sensor/SensorMesh.cpp | 41 ++++++++++++++++++--------- examples/simple_sensor/SensorMesh.h | 4 +-- src/helpers/ClientACL.h | 1 + 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 54bce50d..70a619f7 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -101,25 +101,43 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u /* ------------------ end Cayenne LPP helpers ----------------------*/ -bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { +bool SensorMesh::telemHasChanged(ClientInfo* c) { auto buf = telemetry.getBuffer(); uint8_t size = telemetry.getSize(); uint8_t i = 0; + bool changed = false; - while (i + 2 < min_deltas_len) { - uint8_t ch = min_deltas[i++]; // Get channel # - uint8_t t = min_deltas[i++]; // Get data type + while (i + 2 < c->extra.sensor.min_deltas_len) { + uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel # + uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type uint8_t sz = LPPData::getDataSize(t); - float min_delta = LPPData::getFloat(&min_deltas[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + float min_delta = LPPData::getFloat(&c->extra.sensor.min_deltas[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + float pv = LPPData::getFloat(&c->extra.sensor.prev_telem[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); float v = findTelemValue(buf, size, ch, t, 0.0f); - float pv = findTelemValue(prev_telem, prev_telem_size, ch, t, 0.0f); - if (abs(v - pv) > min_delta) return true; // Yes, has changed + if (abs(v - pv) > min_delta) changed = true; // Yes, has changed - i += sz; // skip + i += 2 + sz; // skip } - return false; // none of the -specified- telemetry values changed by min_delta + if (changed) { + // take snapshot of all _monitored_ telem values, for next cycle + i = 0; + while (i + 2 < c->extra.sensor.min_deltas_len) { + uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel # + uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type + uint8_t sz = LPPData::getDataSize(t); + + c->extra.sensor.prev_telem[i] = ch; + c->extra.sensor.prev_telem[i + 1] = t; + + float v = findTelemValue(buf, size, ch, t, 0.0f); + LPPData::putFloat(&c->extra.sensor.prev_telem[i + 2], v, sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + + i += 2 + sz; // skip + } + } + return changed; } uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { @@ -774,7 +792,6 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise num_alert_tasks = 0; set_radio_at = revert_radio_at = 0; recv_pkt_region = NULL; - prev_telem_size = 0; region_load_active = false; // defaults @@ -1028,7 +1045,7 @@ void SensorMesh::loop() { RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); if (r == NULL) continue; // unknown region scope if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired - if (telemHasChanged(c->extra.sensor.min_deltas, c->extra.sensor.min_deltas_len)) { + if (telemHasChanged(c)) { TransportKey scope; if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { uint8_t tlen = telemetry.getSize(); @@ -1048,8 +1065,6 @@ void SensorMesh::loop() { } } } - memcpy(prev_telem, telemetry.getBuffer(), telemetry.getSize()); // save snapshot for next compare cycle - prev_telem_size = telemetry.getSize(); onSensorDataRead(); diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index e0e03c81..6b7a32fe 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -148,8 +148,6 @@ private: uint8_t reply_data[MAX_PACKET_PAYLOAD]; unsigned long dirty_contacts_expiry; CayenneLPP telemetry; - uint8_t prev_telem_size; - uint8_t prev_telem[MAX_PACKET_PAYLOAD - 4]; TransportKeyStore key_store; RegionMap region_map, temp_map; RegionEntry* recv_pkt_region; @@ -166,7 +164,7 @@ private: uint8_t pending_cr; bool region_load_active; - bool telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len); + bool telemHasChanged(ClientInfo* c); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 39e71f2c..9938bc1e 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -34,6 +34,7 @@ struct ClientInfo { uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber uint8_t min_deltas_len; uint8_t min_deltas[14]; // LPP encoded + uint8_t prev_telem[14]; // LPP encoded } sensor; } extra; From 3819a0bd897b63e1ec8e11d0bb34d83a107ffb00 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 15 Sep 2026 17:17:48 +1000 Subject: [PATCH 12/37] * subscribe response now returns expiry_secs (not timestamp) --- examples/simple_sensor/SensorMesh.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 70a619f7..b1860668 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -228,12 +228,13 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u from->extra.sensor.expiry_timestamp = getRTCClock()->getCurrentTime() + timeout_secs; from->extra.sensor.min_deltas_len = min_deltas_len; memcpy(from->extra.sensor.min_deltas, &payload[8], min_deltas_len); - // reply with actual expiry timestamp - memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); + // reply with actual expiry seconds (we could modify/impose restriction) + memcpy(&reply_data[4], &timeout_secs, 2); + memset(&reply_data[6], 0, 2); // reserved strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used reply_len = 8 + strlen((char *)&reply_data[8]); } else { - memset(&reply_data[4], 0, 4); // expiry timestamp (0 for error) + memset(&reply_data[4], 0, 4); // expiry secs (0 for error) reply_len = 8; } return reply_len; From 3caf033de2fad1ce9c0b6f5d957cacfdb4460d26 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 16 Sep 2026 21:03:44 +1000 Subject: [PATCH 13/37] * refactor: removed UITask dependency from MyMesh. New MyMesh::Listener interface * UITask's are now a MyMesh task Listener --- examples/companion_radio/AbstractUITask.h | 11 ++--- examples/companion_radio/MyMesh.cpp | 48 +++++++-------------- examples/companion_radio/MyMesh.h | 14 ++++-- examples/companion_radio/main.cpp | 7 +-- examples/companion_radio/ui-new/UITask.cpp | 48 ++++++++++++++++----- examples/companion_radio/ui-new/UITask.h | 7 ++- examples/companion_radio/ui-orig/UITask.cpp | 47 +++++++++++++++++--- examples/companion_radio/ui-orig/UITask.h | 8 +++- examples/companion_radio/ui-tiny/UITask.cpp | 30 +++++++++++-- examples/companion_radio/ui-tiny/UITask.h | 7 ++- 10 files changed, 152 insertions(+), 75 deletions(-) diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index b25b1442..d927c1a3 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -12,6 +12,7 @@ #endif #include "NodePrefs.h" +#include "MyMesh.h" enum class UIEventType { none, @@ -22,25 +23,21 @@ enum class UIEventType { ack }; -class AbstractUITask { +class AbstractUITask : public MyMesh::Listener { protected: mesh::MainBoard* _board; MultiSerialInterface* _interfaceManager; - bool _connected; AbstractUITask(mesh::MainBoard* board, MultiSerialInterface* interfaceManager) : _board(board), _interfaceManager(interfaceManager) { - _connected = false; } public: - void setHasConnection(bool connected) { _connected = connected; } - bool hasConnection() const { return _connected; } + bool hasConnection() const { return _interfaceManager->isConnected(); } uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); } bool isBluetoothEnabled() const { return _interfaceManager->isBluetoothEnabled(); } void enableBluetooth() { _interfaceManager->enableBluetooth(); } void disableBluetooth() { _interfaceManager->disableBluetooth(); } - virtual void msgRead(int msgcount) = 0; - virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0; + virtual void notify(UIEventType t = UIEventType::none) = 0; virtual void loop() = 0; }; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 01a7473a..7becb0df 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -368,10 +368,6 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); } - } else { -#ifdef DISPLAY_CLASS - if (_ui) _ui->notify(UIEventType::newContactMessage); -#endif } // add inbound-path to mem cache @@ -395,6 +391,8 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path p->path_len = mesh::Packet::copyPath(p->path, path, path_len); } + if (_listener) _listener->onDiscoveredContact(contact, is_new, path_len, path); + if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[] } @@ -520,16 +518,12 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe _serial->writeFrame(frame, 1); } -#ifdef DISPLAY_CLASS // 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) { - _ui->newMsg(path_len, from.name, text, offline_queue_len); - if (!_serial->isConnected()) { - _ui->notify(UIEventType::contactMessage); - } + if (should_display && _listener) { + _listener->onMessageRecv(path_len, from.name, text); + _listener->onQueueSizeChanged(offline_queue_len); } -#endif } bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { @@ -640,20 +634,16 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe uint8_t frame[1]; frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); - } else { -#ifdef DISPLAY_CLASS - if (_ui) _ui->notify(UIEventType::channelMessage); -#endif } -#ifdef DISPLAY_CLASS - // Get the channel name from the channel index - const char *channel_name = "Unknown"; - ChannelDetails channel_details; - if (getChannel(channel_idx, channel_details)) { - channel_name = channel_details.name; + if (_listener) { + // Get the channel name from the channel index + ChannelDetails channel_details; + if (!getChannel(channel_idx, channel_details)) { + strcpy(channel_details.name, "Unknown"); + } + _listener->onChannelMsgRecv(channel_details, path_len, text); + _listener->onQueueSizeChanged(offline_queue_len); } - if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len); -#endif } void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type, @@ -930,9 +920,9 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t void MyMesh::onSendTimeout() {} -MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui) +MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store) : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), - _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) { + _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0) { _iter_started = false; _cli_rescue = false; cli_command[0] = 0; @@ -1463,9 +1453,7 @@ void MyMesh::handleCmdFrame(size_t len) { int out_len; if ((out_len = getFromOfflineQueue(out_frame)) > 0) { _serial->writeFrame(out_frame, out_len); -#ifdef DISPLAY_CLASS - if (_ui) _ui->msgRead(offline_queue_len); -#endif + if (_listener) _listener->onQueueSizeChanged(offline_queue_len); } else { out_frame[0] = RESP_CODE_NO_MORE_MESSAGES; _serial->writeFrame(out_frame, 1); @@ -2466,10 +2454,6 @@ void MyMesh::loop() { saveContacts(); dirty_contacts_expiry = 0; } - -#ifdef DISPLAY_CLASS - if (_ui) _ui->setHasConnection(_serial->isConnected()); -#endif } bool MyMesh::advert() { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 3b98a4f6..be7bc40a 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -2,7 +2,6 @@ #include #include -#include "AbstractUITask.h" /*------------ Frame Protocol --------------*/ #define FIRMWARE_VER_CODE 14 @@ -96,10 +95,19 @@ struct DiscoveredNode { class MyMesh : public BaseChatMesh, public DataStoreHost { public: - MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL); + class Listener { + public: + virtual void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) = 0; + virtual void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; + virtual void onQueueSizeChanged(int msgcount) = 0; + virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; + }; + + MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); void begin(bool has_display); void startInterface(BaseSerialInterface &serial); + void setListener(Listener* listener) { _listener = listener; } const char *getNodeName(); NodePrefs *getNodePrefs(); @@ -237,7 +245,7 @@ private: uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ uint32_t pending_req; // pending _BINARY_REQ BaseSerialInterface *_serial; - AbstractUITask* _ui; + Listener* _listener; ContactsIterator _iter; uint32_t _iter_filter_since; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6dc9acdc..857acf45 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -114,11 +114,7 @@ MultiSerialInterface interface_manager; StdRNG fast_rng; SimpleMeshTables tables; -MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store - #ifdef DISPLAY_CLASS - , &ui_task - #endif -); +MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store); /* END GLOBAL OBJECTS */ @@ -285,6 +281,7 @@ void setup() { #ifdef DISPLAY_CLASS ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved + the_mesh.setListener(&ui_task); #endif board.onBootComplete(); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 53ae480c..fc6707e2 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -751,17 +751,7 @@ switch(t){ #endif } - -void UITask::msgRead(int msgcount) { - _msgcount = msgcount; - if (msgcount == 0) { - gotoHomeScreen(); - } -} - -void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { - _msgcount = msgcount; - +void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); setCurrScreen(msg_preview); @@ -774,6 +764,42 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _next_refresh = 100; // trigger refresh } } + + if (!hasConnection()) { + notify(UIEventType::contactMessage); + } +} + +void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { + ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, channel_details.name, text); + setCurrScreen(msg_preview); + + if (_display != NULL) { + if (!_display->isOn() && !hasConnection()) { + _display->turnOn(); + } + if (_display->isOn()) { + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _next_refresh = 100; // trigger refresh + } + } + + if (!hasConnection()) { + notify(UIEventType::channelMessage); + } +} + +void UITask::onQueueSizeChanged(int msgcount) { + _msgcount = msgcount; + if (msgcount == 0) { + gotoHomeScreen(); + } +} + +void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { + if (!hasConnection()) { + notify(UIEventType::newContactMessage); + } } void UITask::userLedHandler() { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 52d3ffa1..e1200953 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -90,10 +90,13 @@ public: bool getGPSState(); void toggleGPS(); + // MyMesh::Listener + void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onQueueSizeChanged(int offline_queue_size) override; + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; // from AbstractUITask - void msgRead(int msgcount) override; - void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; void loop() override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index b49b05dd..08c84587 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -126,7 +126,7 @@ switch(t){ // Serial.println((int) t); } -void UITask::msgRead(int msgcount) { +void UITask::onQueueSizeChanged(int msgcount) { _msgcount = msgcount; if (msgcount == 0) { clearMsgPreview(); @@ -139,9 +139,7 @@ void UITask::clearMsgPreview() { _need_refresh = true; } -void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { - _msgcount = msgcount; - +void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif @@ -158,10 +156,45 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _display->turnOn(); } if (_display->isOn()) { - _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer - _need_refresh = true; + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _need_refresh = true; } } + if (!hasConnection()) { + notify(UIEventType::contactMessage); + } +} + +void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +#ifdef HAS_DRV2605 + vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) +#endif + + if (path_len == 0xFF) { + sprintf(_origin, "(F) %s", channel_details.name); + } else { + sprintf(_origin, "(%d) %s", (uint32_t) path_len, channel_details.name); + } + StrHelper::strncpy(_msg, text, sizeof(_msg)); + + if (_display != NULL) { + if (!_display->isOn() && !hasConnection()) { + _display->turnOn(); + } + if (_display->isOn()) { + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _need_refresh = true; + } + } + if (!hasConnection()) { + notify(UIEventType::channelMessage); + } +} + +void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { + if (!hasConnection()) { + notify(UIEventType::newContactMessage); + } } void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { @@ -262,7 +295,7 @@ void UITask::renderCurrScreen() { _display->print(tmp); // BT pin - if (!_connected && the_mesh.getBLEPin() != 0) { + if (!hasConnection() && the_mesh.getBLEPin() != 0) { _display->setColor(UIColor::warning_txt); _display->setTextSize(2); _display->setCursor(0, 43); diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 02d126e8..38141479 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -70,9 +70,13 @@ public: bool hasDisplay() const { return _display != NULL; } void clearMsgPreview(); + // MyMesh::Listener + void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onQueueSizeChanged(int offline_queue_size) override; + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + // from AbstractUITask - void msgRead(int msgcount) override; - void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; void loop() override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 9ecd66f1..8a8b77b5 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -503,16 +503,14 @@ switch(t){ } -void UITask::msgRead(int msgcount) { +void UITask::onQueueSizeChanged(int msgcount) { _msgcount = msgcount; if (msgcount == 0) { gotoHomeScreen(); } } -void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { - _msgcount = msgcount; - +void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); @@ -522,6 +520,30 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _next_refresh = 100; // trigger refresh } } + if (!hasConnection()) { + notify(UIEventType::contactMessage); + } +} + +void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { + if (_display != NULL) { + if (!_display->isOn() && !hasConnection()) { + _display->turnOn(); + } + if (_display->isOn()) { + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _next_refresh = 100; // trigger refresh + } + } + if (!hasConnection()) { + notify(UIEventType::channelMessage); + } +} + +void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { + if (!hasConnection()) { + notify(UIEventType::newContactMessage); + } } void UITask::userLedHandler() { diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index dc689478..925e8d71 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -98,10 +98,13 @@ public: bool getGPSState(); void toggleGPS(); + // MyMesh::Listener + void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onQueueSizeChanged(int offline_queue_size) override; + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; // from AbstractUITask - void msgRead(int msgcount) override; - void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; void loop() override; From b71933beb5d618aa74f99c4916bda659bb6e22a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Doma=C5=84ski?= Date: Wed, 16 Sep 2026 22:28:55 +0200 Subject: [PATCH 14/37] Enable DC/DC in shared LR1110 initialization --- src/helpers/radiolib/CustomLR1110.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 75674a1d..f0fde435 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -13,6 +13,16 @@ class CustomLR1110 : public LR1110 { public: CustomLR1110(Module *mod) : LR1110(mod) { } + int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7, + uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10, + uint16_t preambleLength = 8, float tcxoVoltage = 1.6) { + int16_t state = LR1110::begin(freq, bw, sf, cr, syncWord, power, preambleLength, + tcxoVoltage); + // RadioLib begin() defaults to LDO; use the LR1110 DC/DC regulator. + if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC(); + return state; + } + size_t getPacketLength(bool update) override { size_t len = LR1110::getPacketLength(update); if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { From 5c3d9281ffbd5c1cb8ee5570c0d89aebbac91521 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 17 Sep 2026 13:45:33 +1000 Subject: [PATCH 15/37] * refactored the UI_DISCOVER_* feature --- examples/companion_radio/MyMesh.cpp | 64 ++---------------- examples/companion_radio/MyMesh.h | 35 ++-------- examples/companion_radio/ui-new/UITask.cpp | 72 +++++++++++++++++---- examples/companion_radio/ui-new/UITask.h | 23 ++++++- examples/companion_radio/ui-orig/UITask.cpp | 12 +++- examples/companion_radio/ui-orig/UITask.h | 3 +- examples/companion_radio/ui-tiny/UITask.cpp | 8 ++- examples/companion_radio/ui-tiny/UITask.h | 3 +- 8 files changed, 112 insertions(+), 108 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 7becb0df..6e570ea5 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -139,10 +139,6 @@ #define ERR_CODE_FILE_IO_ERROR 5 #define ERR_CODE_ILLEGAL_ARG 6 -// Copied from simple_repeater (could probably be shared) -#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 -#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 - #define MAX_SIGN_DATA_LEN (8 * 1024) // 8K // Auto-add config bitmask @@ -410,53 +406,6 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) -int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) { - if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE; - if (max_num > disc_nodes_count) max_num = disc_nodes_count; - - for (int i = 0; i < max_num; i++) { - nodes[i] = discovered_nodes[i]; - } - return max_num; -} - -bool MyMesh::requestRepeatersDiscovery() { - uint8_t cmd_bytes[6]; - cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only - cmd_bytes[1] = 0xFF; // Repeaters - getRNG()->random(&cmd_bytes[2], 4); // tag - disc_nodes_count = 0; - disc_node_req_tag = *((uint32_t*)&cmd_bytes[2]); - mesh::Packet* req = createControlData(cmd_bytes, sizeof(cmd_bytes)); - if (req) { - sendZeroHop(req); - return true; - } - return false; -} - -void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len) { - if ((p_len < 12) - || (payload[0] & 0xF0 != CTL_TYPE_NODE_DISCOVER_RESP) - || (disc_nodes_count >= DISCOVERED_NODES_TABLE_SIZE) - || (memcmp(&payload[2], &disc_node_req_tag, 4))) { - return; - } - memcpy(&discovered_nodes[disc_nodes_count].pubkey_prefix, &payload[6], 8); - discovered_nodes[disc_nodes_count].type = payload[0] & 0xF; - discovered_nodes[disc_nodes_count].snr_out = ((int8_t)payload[1]) / 4.0; - discovered_nodes[disc_nodes_count].snr_in = _radio->getLastSNR(); - ContactInfo* c = lookupContactByPubKey(&payload[6], 8); - if (c != NULL) { - strncpy(discovered_nodes[disc_nodes_count].name, c->name, 32); - } else { - discovered_nodes[disc_nodes_count].name[0] = 0; - } - disc_nodes_count ++; -} -#endif - void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); @@ -518,10 +467,8 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe _serial->writeFrame(frame, 1); } - // 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 && _listener) { - _listener->onMessageRecv(path_len, from.name, text); + if (_listener) { + _listener->onMessageRecv(from, txt_type, sender_timestamp, path_len, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -840,9 +787,6 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; } -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - checkControlDataForPendingDiscovery(packet->payload, packet->payload_len); -#endif int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); @@ -856,6 +800,8 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { } else { MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline"); } + + if (_listener) _listener->onControlDataRecv(packet); } void MyMesh::onRawDataRecv(mesh::Packet *packet) { @@ -1039,7 +985,7 @@ void MyMesh::begin(bool has_display) { resetContacts(); _store->loadContacts(this); bootstrapRTCfromContacts(); - addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel + addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure public channel _store->loadChannels(this); radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index be7bc40a..f1128624 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -75,6 +75,10 @@ #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +// Copied from simple_repeater +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + struct AdvertPath { uint8_t pubkey_prefix[7]; uint8_t path_len; @@ -83,24 +87,15 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) -struct DiscoveredNode { - uint8_t pubkey_prefix[9]; - float snr_in; - float snr_out; - char name[32]; - uint8_t type; -}; -#endif - class MyMesh : public BaseChatMesh, public DataStoreHost { public: class Listener { public: - virtual void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) = 0; + virtual void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) = 0; virtual void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; virtual void onQueueSizeChanged(int msgcount) = 0; virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; + virtual void onControlDataRecv(const mesh::Packet* packet) = 0; }; MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); @@ -120,11 +115,6 @@ public: int getRecentlyHeard(AdvertPath dest[], int max_num); -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - bool requestRepeatersDiscovery(); - int getDiscoveredNodes(DiscoveredNode nodes[], int max_num); -#endif - protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; @@ -287,19 +277,6 @@ private: #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table - -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - #ifdef UI_RECENT_LIST_SIZE - #define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE - #else - #define DISCOVERED_NODES_TABLE_SIZE 4 - #endif - DiscoveredNode discovered_nodes[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept - uint32_t disc_node_req_tag = 0; - uint32_t disc_nodes_count = 0; - - void checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len); -#endif }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index fc6707e2..33a4e192 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -99,7 +99,7 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN DISCOVERY, #endif #ifndef UI_NO_HIBERNATE @@ -115,8 +115,10 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - DiscoveredNode discovered[UI_RECENT_LIST_SIZE]; +#if UI_DISCOVER_SCREEN + DiscoveredNode discovered[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept + uint32_t disc_node_req_tag = 0; + uint32_t disc_nodes_count = 0; uint32_t discovery_req_time = 0; bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O) #endif @@ -206,6 +208,40 @@ public: : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), _shutdown_init(false), sensors_lpp(200) { } +#if UI_DISCOVER_SCREEN + bool sendDiscoverRequest() { + uint8_t cmd_bytes[6]; + cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only + cmd_bytes[1] = 0xFF; // Repeaters + the_mesh.getRNG()->random((uint8_t *) &disc_node_req_tag, 4); // generate random tag + memcpy(&cmd_bytes[2], &disc_node_req_tag, 4); + disc_nodes_count = 0; + mesh::Packet* req = the_mesh.createControlData(cmd_bytes, sizeof(cmd_bytes)); + if (req) { + the_mesh.sendZeroHop(req); + discovery_req_time = millis(); + return true; + } + return false; + } + + void handleDiscoverResponse(const mesh::Packet* packet) { + if (disc_nodes_count < DISCOVERED_NODES_TABLE_SIZE && memcmp(&packet->payload[2], &disc_node_req_tag, 4) == 0) { + auto d = &discovered[disc_nodes_count++]; + memcpy(d->pubkey_prefix, &packet->payload[6], 8); + d->type = packet->payload[0] & 0xF; + d->snr_out = ((int8_t)packet->payload[1]) / 4.0; + d->snr_in = radio_driver.getLastSNR(); + ContactInfo* c = the_mesh.lookupContactByPubKey(&packet->payload[6], 8); + if (c != NULL) { + strncpy(d->name, c->name, 32); + } else { + d->name[0] = 0; + } + } + } +#endif + void poll() override { if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released _task->shutdown(); @@ -464,9 +500,9 @@ public: if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN } else if (_page == HomePage::DISCOVERY) { - int count = the_mesh.getDiscoveredNodes(discovered, UI_RECENT_LIST_SIZE); + int count = disc_nodes_count; display.setColor(UIColor::primary_txt); int y = 20; for (int i = 0; i < count; i++, y += 11) { @@ -493,8 +529,8 @@ public: } if (millis() < discovery_req_time + 5000) { return 1000; // more frequent updates just after req - } else if (count < UI_RECENT_LIST_SIZE -1) { // show only 5 sec after last disc - y = 10 + 11 * UI_RECENT_LIST_SIZE; + } else if (count < DISCOVERED_NODES_TABLE_SIZE -1) { // show only 5 sec after last disc + y = 10 + 11 * DISCOVERED_NODES_TABLE_SIZE; display.drawTextCentered(display.width() / 2, y, "discover: " PRESS_LABEL); } #endif @@ -525,7 +561,7 @@ public: if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN if (_page == HomePage::DISCOVERY) { _task->showAlert("Repeater disc", 800); } @@ -562,11 +598,10 @@ public: return true; } #endif -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN if (c == KEY_ENTER && _page == HomePage::DISCOVERY) { if (millis() > discovery_req_time + 5000) { // rate limiter - the_mesh.requestRepeatersDiscovery(); - discovery_req_time = millis(); + sendDiscoverRequest(); } return true; } @@ -751,8 +786,11 @@ switch(t){ #endif } -void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { - ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); +void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { + // we only want to show text messages on display, not cli data + if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + + ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from.name, text); setCurrScreen(msg_preview); if (_display != NULL) { @@ -802,6 +840,14 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } +void UITask::onControlDataRecv(const mesh::Packet* packet) { +#if UI_DISCOVER_SCREEN + if (packet->payload_len >= 12 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) { + ((HomeScreen *) home)->handleDiscoverResponse(packet); + } +#endif +} + void UITask::userLedHandler() { #ifdef PIN_STATUS_LED int cur_time = millis(); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index e1200953..842b1ac9 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -22,6 +22,26 @@ #include "../AbstractUITask.h" #include "../NodePrefs.h" +#ifndef UI_DISCOVER_SCREEN + #define UI_DISCOVER_SCREEN 1 +#endif + +#if UI_DISCOVER_SCREEN + #ifdef UI_RECENT_LIST_SIZE + #define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE + #else + #define DISCOVERED_NODES_TABLE_SIZE 4 + #endif + +struct DiscoveredNode { + float snr_in; + float snr_out; + uint8_t pubkey_prefix[9]; + uint8_t type; + char name[32]; +}; +#endif + class UITask : public AbstractUITask { DisplayDriver* _display; SensorManager* _sensors; @@ -91,10 +111,11 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 08c84587..45a81da9 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -139,15 +139,18 @@ void UITask::clearMsgPreview() { _need_refresh = true; } -void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { +void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { + // we only want to show text messages on display, not cli data + if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif if (path_len == 0xFF) { - sprintf(_origin, "(F) %s", from_name); + sprintf(_origin, "(F) %s", from.name); } else { - sprintf(_origin, "(%d) %s", (uint32_t) path_len, from_name); + sprintf(_origin, "(%d) %s", (uint32_t) path_len, from.name); } StrHelper::strncpy(_msg, text, sizeof(_msg)); @@ -197,6 +200,9 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } +void UITask::onControlDataRecv(const mesh::Packet* packet) { +} + void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { // Convert millivolts to percentage #ifndef BATT_MIN_MILLIVOLTS diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 38141479..553dc7d0 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -71,10 +71,11 @@ public: void clearMsgPreview(); // MyMesh::Listener - void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 8a8b77b5..71a8bb21 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -510,7 +510,10 @@ void UITask::onQueueSizeChanged(int msgcount) { } } -void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { +void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { + // we only want to show text messages on display, not cli data + if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); @@ -546,6 +549,9 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } +void UITask::onControlDataRecv(const mesh::Packet* packet) { +} + void UITask::userLedHandler() { #ifdef PIN_STATUS_LED int cur_time = millis(); diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 925e8d71..48c697b9 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -99,10 +99,11 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; From cd54ee9754a0f77d31a69bb99088aec78cd5ee7c Mon Sep 17 00:00:00 2001 From: liamcottle Date: Thu, 17 Sep 2026 15:52:46 +1200 Subject: [PATCH 16/37] add missing null display driver class for thinknode m7 ethernet variant --- variants/thinknode_m7/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 5606d482..69b38e33 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -169,6 +169,7 @@ build_src_filter = ${ThinkNode_M7.build_src_filter} ${ThinkNode_M7_ethernet.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${ThinkNode_M7.lib_deps} From 30dd723c26cee71fc43dc54abb56cbf17d431a4e Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 17 Sep 2026 14:25:41 +1000 Subject: [PATCH 17/37] * MyMesh::begin() refactor, removing last display concepts --- examples/companion_radio/MyMesh.cpp | 24 ++++------------ examples/companion_radio/MyMesh.h | 3 +- examples/companion_radio/main.cpp | 43 +++++++++++++++-------------- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6e570ea5..409fed9f 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -911,7 +911,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe #endif } -void MyMesh::begin(bool has_display) { +void MyMesh::begin() { BaseChatMesh::begin(); if (!_store->loadMainIdentity(self_id)) { @@ -963,24 +963,7 @@ void MyMesh::begin(bool has_display) { _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours -#ifdef BLE_PIN_CODE // 123456 by default - if (_prefs.ble_pin == 0) { -#ifdef DISPLAY_CLASS - if (has_display && BLE_PIN_CODE == 123456) { - StdRNG rng; - _active_ble_pin = rng.nextInt(100000, 999999); // random pin each session - } else { - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin - } -#else - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin -#endif - } else { - _active_ble_pin = _prefs.ble_pin; - } -#else - _active_ble_pin = 0; -#endif + _active_ble_pin = _prefs.ble_pin; resetContacts(); _store->loadContacts(this); @@ -1007,6 +990,9 @@ NodePrefs *MyMesh::getNodePrefs() { uint32_t MyMesh::getBLEPin() { return _active_ble_pin; } +void MyMesh::setBLEPin(uint32_t active_pin) { + _active_ble_pin = active_pin; +} struct FreqRange { uint32_t lower_freq, upper_freq; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f1128624..7bfbb292 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -100,13 +100,14 @@ public: MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); - void begin(bool has_display); + void begin(); void startInterface(BaseSerialInterface &serial); void setListener(Listener* listener) { _listener = listener; } const char *getNodeName(); NodePrefs *getNodePrefs(); uint32_t getBLEPin(); + void setBLEPin(uint32_t active_pin); void loop(); void handleCmdFrame(size_t len); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 857acf45..efc31779 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -172,37 +172,38 @@ void setup() { #endif #endif store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #elif defined(ESP32) SPIFFS.begin(true); store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #else #error "need to define filesystem" #endif +#ifdef BLE_PIN_CODE // 123456 by default + if (the_mesh.getNodePrefs()->ble_pin == 0) { +#ifdef DISPLAY_CLASS + if (disp != NULL && BLE_PIN_CODE == 123456) { + StdRNG rng; + the_mesh.setBLEPin(rng.nextInt(100000, 999999)); // random pin each session + } else { + the_mesh.setBLEPin(BLE_PIN_CODE); // otherwise static pin + } +#else + the_mesh.setBLEPin(BLE_PIN_CODE); // otherwise static pin +#endif + } else { + the_mesh.setBLEPin(the_mesh.getNodePrefs()->ble_pin); + } +#else + the_mesh.setBLEPin(0); +#endif + // add bluetooth interface #if defined(BLE_PIN_CODE) bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); From b3b17025e38c3c486fa414f7bf31d26f8686ec5b Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 17 Sep 2026 17:33:36 +1000 Subject: [PATCH 18/37] * more MyMesh::Listener interface hooks --- examples/companion_radio/MyMesh.cpp | 20 +++++++++++++++++++- examples/companion_radio/MyMesh.h | 15 ++++++++++++--- examples/companion_radio/ui-new/UITask.cpp | 2 +- examples/companion_radio/ui-new/UITask.h | 2 +- examples/companion_radio/ui-orig/UITask.cpp | 5 +---- examples/companion_radio/ui-orig/UITask.h | 3 +-- examples/companion_radio/ui-tiny/UITask.cpp | 5 +---- examples/companion_radio/ui-tiny/UITask.h | 3 +-- 8 files changed, 37 insertions(+), 18 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 409fed9f..61288a6e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -415,6 +415,11 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) { } ContactInfo* MyMesh::processAck(const uint8_t *data) { + if (_listener) { + uint32_t ack_crc; + memcpy(&ack_crc, data, 4); + _listener->onACKRecv(ack_crc); + } // see if matches any in a table for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) { if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient @@ -588,7 +593,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe if (!getChannel(channel_idx, channel_details)) { strcpy(channel_details.name, "Unknown"); } - _listener->onChannelMsgRecv(channel_details, path_len, text); + _listener->onChannelMessageRecv(channel_details, path_len, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -626,6 +631,9 @@ void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet * frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); } + if (_listener) { + _listener->onChannelDataRecv(channel, pkt, data_type, data, data_len); + } } uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, @@ -673,6 +681,8 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim memcpy(&reply[4], telemetry.getBuffer(), tlen); return 4 + tlen; } + } else if (_listener) { + return _listener->onUnhandledRequest(contact, sender_timestamp, data, len, reply); } return 0; // unknown } @@ -748,6 +758,8 @@ 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 (_listener && len > 4) { + _listener->onUnhandledResponse(contact, tag, &data[4], len - 4); } } @@ -822,6 +834,9 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) { } else { MESH_DEBUG_PRINTLN("onRawDataRecv(), data received while app offline"); } + if (_listener) { + _listener->onRawDataRecv(packet); + } } void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, @@ -852,6 +867,9 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, } else { MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline"); } + if (_listener) { + _listener->onTraceRecv(packet, tag, auth_code, flags, path_snrs, path_hashes, path_len); + } } uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 7bfbb292..667b19e1 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -92,10 +92,19 @@ public: class Listener { public: virtual void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) = 0; - virtual void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; + virtual void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; virtual void onQueueSizeChanged(int msgcount) = 0; - virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; - virtual void onControlDataRecv(const mesh::Packet* packet) = 0; + virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { } + virtual void onControlDataRecv(const mesh::Packet* packet) { } + virtual void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type, + const uint8_t *data, size_t data_len) { } + virtual void onACKRecv(uint32_t ack_crc) { } + virtual uint8_t onUnhandledRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, + uint8_t len, uint8_t *reply) { return 0; /* unknown request type */ } + virtual void onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { } + virtual void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { } + virtual void onRawDataRecv(mesh::Packet *packet) { } }; MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 33a4e192..8457367a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -808,7 +808,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, channel_details.name, text); setCurrScreen(msg_preview); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 842b1ac9..47928a17 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -112,7 +112,7 @@ public: // MyMesh::Listener void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; void onControlDataRecv(const mesh::Packet* packet) override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 45a81da9..42e4ab79 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -168,7 +168,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif @@ -200,9 +200,6 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } -void UITask::onControlDataRecv(const mesh::Packet* packet) { -} - void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { // Convert millivolts to percentage #ifndef BATT_MIN_MILLIVOLTS diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 553dc7d0..bb879cb6 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -72,10 +72,9 @@ public: // MyMesh::Listener void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; - void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 71a8bb21..607ac89c 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -528,7 +528,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); @@ -549,9 +549,6 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } -void UITask::onControlDataRecv(const mesh::Packet* packet) { -} - void UITask::userLedHandler() { #ifdef PIN_STATUS_LED int cur_time = millis(); diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 48c697b9..83591d89 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -100,10 +100,9 @@ public: // MyMesh::Listener void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; - void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; From 1077331e3162e8e80c76b900a0c8275d4fed2465 Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Tue, 8 Sep 2026 16:57:12 -0400 Subject: [PATCH 19/37] Extend WiFi disconnect logging to specify reason Today if a user builds firmware with an incorrect WiFi SSID or password, they get a generic `WiFi disconnected.` error message. Extend logging to descirbe the reason that WiFi was disconnected. I gathered the updated logs for the 3 scenarios below. ``` WiFi: Attempting manual WiFi reconnect... WiFi: WiFi disconnected (reason=ASSOC_LEAVE). Flagging for reconnect... WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect... WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect... WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect... ``` ``` WiFi: WiFi disconnected (reason=NO_AP_FOUND). Flagging for reconnect... ``` ``` WiFi: WiFi connected successfully! WiFi: Got connection ``` --- examples/companion_radio/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6dc9acdc..0e989999 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -229,7 +229,8 @@ void setup() { WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){ if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { - WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect..."); + WIFI_DEBUG_PRINTLN("WiFi disconnected (reason=%s). Flagging for reconnect...", + WiFi.disconnectReasonName((wifi_err_reason_t)info.wifi_sta_disconnected.reason)); wifi_needs_reconnect = true; } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); From 8d2a5657accd311e3f75578c46882313bdd4ec65 Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Thu, 17 Sep 2026 22:12:53 -0400 Subject: [PATCH 20/37] Pin Radiolab for stm32 and replace ltoa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The major rev from 19.x to 20.x breaks Radiolab, so pin it for now. This should fix the failing wio-e5-mini_repeater builds. ltoa is removed upstream, so it was replaced with snprintf. The p[] buffer is always 16 bytes. Copy/paste for why claude thinks this is fine below, but also if this is broken it wouldn't be new. ``` exp2 in [23, 30] (int_part = mantissa << (exp2 - 23)): this is where int_part can reach its true max, up to 2,147,483,520 — 10 digits. That's what the snprintf bound of 11 (10 digits + NUL) is sized for. But in this branch frac_part is never assigned anything other than its initial 0, so only *p++ = '0' runs for the fraction — 1 char, not 7. exp2 in [0, 22] (int_part = mantissa >> (23 - exp2)): this is the only branch where frac_part can also be nonzero and enter the 7-digit loop. Here int_part < 2^23 by construction, so it's capped at 7 digits, never 10. ``` --- platformio.ini | 4 +++- src/helpers/TxtDataHelpers.cpp | 9 ++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/platformio.ini b/platformio.ini index de4d6c29..f296f838 100644 --- a/platformio.ini +++ b/platformio.ini @@ -110,7 +110,9 @@ build_flags = ${arduino_base.build_flags} [stm32_base] extends = arduino_base -platform = ststm32 +; Pinned pending upstream fix for upstream conversions. +; https://github.com/jgromes/RadioLib/issues/1873 +platform = ststm32@19.7.1 extra_scripts = post:arch/stm32/build_hex.py build_flags = ${arduino_base.build_flags} -D STM32_PLATFORM diff --git a/src/helpers/TxtDataHelpers.cpp b/src/helpers/TxtDataHelpers.cpp index d327931f..f2621c0d 100644 --- a/src/helpers/TxtDataHelpers.cpp +++ b/src/helpers/TxtDataHelpers.cpp @@ -27,8 +27,9 @@ bool StrHelper::isBlank(const char* str) { } #include +#include -union int32_Float_t +union int32_Float_t { int32_t Long; float Float; @@ -100,11 +101,9 @@ static void _ftoa(float f, char *p, int *status) *p++ = '-'; if (int_part == 0) *p++ = '0'; - else + else { - ltoa(int_part, p, 10); - while (*p) - p++; + p += snprintf(p, 11, "%ld", (long)int_part); } *p++ = '.'; if (frac_part == 0) From 242e401a2c9f5b88980a170fba63b42a571081d3 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 18 Sep 2026 18:17:52 +1200 Subject: [PATCH 21/37] added reboot, shutdown and poweroff to companion cli commands --- 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 01a7473a..c38fa70a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2146,6 +2146,16 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } + if (strcmp(command, "reboot") == 0) { + board.reboot(); // doesn't return + return true; + } + + if (strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) { + board.powerOff(); // doesn't return + return true; + } + if (memcmp(command, "set name ", 9) == 0) { if (AdvertDataParser::isValidName(&command[9])) { StrHelper::strncpy(_prefs.node_name, &command[9], sizeof(_prefs.node_name)); From 843598724c34ee6857dec63d58383f5d49156303 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 18 Sep 2026 19:03:20 +1000 Subject: [PATCH 22/37] * fix for X1 --- examples/companion_radio/ui-orig/UITask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 42e4ab79..a717a540 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -408,7 +408,7 @@ void UITask::userLedHandler() { statusLedWrite(255, 0, 0); // red: battery low } else if (_msgcount > 0) { statusLedWrite(255, 90, 0); // amber: unread messages - } else if (_connected) { + } else if (hasConnection()) { statusLedWrite(0, 0, 255); // blue: app connected } else { statusLedWrite(0, 255, 0); // green: heartbeat From c425648bfc56db6103536426dc155274093c3898 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sat, 19 Sep 2026 11:26:42 +0700 Subject: [PATCH 23/37] Off LED before powering off for repeater --- examples/simple_repeater/UITask.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index e7225557..e0b6a41d 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -140,8 +140,10 @@ void UITask::loop() { } if (_powering_off_at > 0) { // power off timer armed -#ifdef LED_PIN +#if defined(LED_PIN) && defined(LED_STATE_ON) digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff + delay(1000); + digitalWrite(LED_PIN, !LED_STATE_ON); // off LED #endif if (millis() > _powering_off_at) { _board->powerOff(); // should not return From 64434c5326b080283a5576ef1601addc5bae52f4 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 19 Sep 2026 20:13:18 +1000 Subject: [PATCH 24/37] * misc --- examples/companion_radio/MyMesh.cpp | 6 +++--- examples/companion_radio/MyMesh.h | 13 +++++++------ examples/companion_radio/ui-new/UITask.cpp | 8 +++++--- examples/companion_radio/ui-new/UITask.h | 10 +++++++--- examples/companion_radio/ui-orig/UITask.cpp | 6 ++++-- examples/companion_radio/ui-orig/UITask.h | 4 ++-- examples/companion_radio/ui-tiny/UITask.cpp | 4 ++-- examples/companion_radio/ui-tiny/UITask.h | 4 ++-- 8 files changed, 32 insertions(+), 23 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 61288a6e..e1b031a9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -473,7 +473,7 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe } if (_listener) { - _listener->onMessageRecv(from, txt_type, sender_timestamp, path_len, text); + _listener->onMessageRecv(pkt, from, txt_type, sender_timestamp, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -593,7 +593,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe if (!getChannel(channel_idx, channel_details)) { strcpy(channel_details.name, "Unknown"); } - _listener->onChannelMessageRecv(channel_details, path_len, text); + _listener->onChannelMessageRecv(pkt, channel_details, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -632,7 +632,7 @@ void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet * _serial->writeFrame(frame, 1); } if (_listener) { - _listener->onChannelDataRecv(channel, pkt, data_type, data, data_len); + _listener->onChannelDataRecv(pkt, channel, data_type, data, data_len); } } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 667b19e1..9f077c30 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -91,20 +91,21 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { public: class Listener { public: - virtual void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) = 0; - virtual void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; + virtual void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) = 0; + virtual void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) = 0; virtual void onQueueSizeChanged(int msgcount) = 0; virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { } - virtual void onControlDataRecv(const mesh::Packet* packet) { } - virtual void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type, + virtual void onControlDataRecv(const mesh::Packet* pkt) { } + virtual void onChannelDataRecv(mesh::Packet *pkt, const mesh::GroupChannel &channel, uint16_t data_type, const uint8_t *data, size_t data_len) { } virtual void onACKRecv(uint32_t ack_crc) { } virtual uint8_t onUnhandledRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, uint8_t len, uint8_t *reply) { return 0; /* unknown request type */ } virtual void onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { } - virtual void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + virtual void onTraceRecv(mesh::Packet *pkt, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { } - virtual void onRawDataRecv(mesh::Packet *packet) { } + virtual void onRawDataRecv(mesh::Packet *pkt) { } + virtual ~Listener() { } }; MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 8457367a..181bc832 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -786,10 +786,11 @@ switch(t){ #endif } -void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { +void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) { // we only want to show text messages on display, not cli data if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from.name, text); setCurrScreen(msg_preview); @@ -808,7 +809,8 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) { + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, channel_details.name, text); setCurrScreen(msg_preview); @@ -842,7 +844,7 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path void UITask::onControlDataRecv(const mesh::Packet* packet) { #if UI_DISCOVER_SCREEN - if (packet->payload_len >= 12 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) { + if (packet->payload_len >= 14 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) { ((HomeScreen *) home)->handleDiscoverResponse(packet); } #endif diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 47928a17..361b04d0 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -22,6 +22,10 @@ #include "../AbstractUITask.h" #include "../NodePrefs.h" +#ifdef UI_NO_DISCOVER_SCREEN + #error "UI_NO_DISCOVER_SCREEN is obsolete - use -D UI_DISCOVER_SCREEN=0 instead" +#endif + #ifndef UI_DISCOVER_SCREEN #define UI_DISCOVER_SCREEN 1 #endif @@ -111,11 +115,11 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override; + void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; - void onControlDataRecv(const mesh::Packet* packet) override; + void onControlDataRecv(const mesh::Packet* pkt) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index a717a540..db8772f4 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -139,7 +139,7 @@ void UITask::clearMsgPreview() { _need_refresh = true; } -void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { +void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) { // we only want to show text messages on display, not cli data if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; @@ -147,6 +147,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; if (path_len == 0xFF) { sprintf(_origin, "(F) %s", from.name); } else { @@ -168,11 +169,12 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) { #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; if (path_len == 0xFF) { sprintf(_origin, "(F) %s", channel_details.name); } else { diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index bb879cb6..d5f79898 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -71,8 +71,8 @@ public: void clearMsgPreview(); // MyMesh::Listener - void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override; + void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 607ac89c..7b70f836 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -510,7 +510,7 @@ void UITask::onQueueSizeChanged(int msgcount) { } } -void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { +void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) { // we only want to show text messages on display, not cli data if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; @@ -528,7 +528,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) { if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 83591d89..084919aa 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -99,8 +99,8 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override; + void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; From 73afe3a0f548fc1df65c2afff4ae9eea1a1def54 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 20 Sep 2026 14:22:13 +1200 Subject: [PATCH 25/37] allow fetching configured wifi password via companion cli --- examples/companion_radio/MyMesh.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index c38fa70a..79b4a8ce 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2191,6 +2191,10 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* strcpy(reply, "> wifi.pwd updated (reboot to apply)"); return true; } + if (strcmp(command, "get wifi.pwd") == 0) { + sprintf(reply, "> %s", _prefs.wifi_pwd); + return true; + } if (strcmp(command, "set wifi.clear") == 0) { _prefs.wifi_ssid[0] = 0; _prefs.wifi_pwd[0] = 0; @@ -2198,7 +2202,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* strcpy(reply, "> wifi config cleared (reboot to apply)"); return true; } - if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design + if (strcmp(command, "get wifi.ssid") == 0) { sprintf(reply, "> %s", _prefs.getWifiSSID()[0] ? _prefs.getWifiSSID() : "(not set)"); return true; } From 4021a341f8bed9250f3bd145de9412b179f1dcdb Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 20 Sep 2026 15:11:21 +1200 Subject: [PATCH 26/37] add wifi support to thinknode m7 ble variant --- variants/thinknode_m7/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 69b38e33..128b1244 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -98,11 +98,13 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} ${ThinkNode_M7_ethernet.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> From 798ce636df392572fea3186f5382b44fc4d1c486 Mon Sep 17 00:00:00 2001 From: TJ Downes <273720+tjdownes@users.noreply.github.com> Date: Sat, 19 Sep 2026 21:47:24 -0700 Subject: [PATCH 27/37] Address review feedback on Muzi Base support - rename the board class and files to MuziBaseBoard - uppercase the build flags (MUZI_BASE, MUZI_BASE_SUPERIO) - name the superIO envs muzi_base_{duo,uno}_superIO_companion_radio_ble so build.sh picks them up - set MAX_CONTACTS to 350 like the other variants - move the pin setup from initVariant() into MuziBaseBoard::begin() --- .../{muzi_baseBoard.cpp => MuziBaseBoard.cpp} | 20 ++++++++++--- .../{muzi_baseBoard.h => MuziBaseBoard.h} | 4 +-- variants/muzi_base/platformio.ini | 22 +++++++-------- variants/muzi_base/target.cpp | 2 +- variants/muzi_base/target.h | 6 ++-- variants/muzi_base/variant.cpp | 28 ------------------- 6 files changed, 33 insertions(+), 49 deletions(-) rename variants/muzi_base/{muzi_baseBoard.cpp => MuziBaseBoard.cpp} (69%) rename variants/muzi_base/{muzi_baseBoard.h => MuziBaseBoard.h} (91%) diff --git a/variants/muzi_base/muzi_baseBoard.cpp b/variants/muzi_base/MuziBaseBoard.cpp similarity index 69% rename from variants/muzi_base/muzi_baseBoard.cpp rename to variants/muzi_base/MuziBaseBoard.cpp index ed713fa0..a104c550 100644 --- a/variants/muzi_base/muzi_baseBoard.cpp +++ b/variants/muzi_base/MuziBaseBoard.cpp @@ -1,7 +1,7 @@ #include #include -#include "muzi_baseBoard.h" +#include "MuziBaseBoard.h" #ifdef NRF52_POWER_MANAGEMENT const PowerMgtConfig power_config = { @@ -10,7 +10,7 @@ const PowerMgtConfig power_config = { .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK }; -void muzi_baseBoard::initiateShutdown(uint8_t reason) { +void MuziBaseBoard::initiateShutdown(uint8_t reason) { // Disable LoRa module power before shutdown if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { @@ -21,14 +21,26 @@ void muzi_baseBoard::initiateShutdown(uint8_t reason) { } #endif // NRF52_POWER_MANAGEMENT -void muzi_baseBoard::begin() { +void MuziBaseBoard::begin() { NRF52BoardDCDC::begin(); pinMode(PIN_VBAT_READ, INPUT); -#ifdef muzi_base_superIO + pinMode(PIN_BATTERY_CHARGING, INPUT); + pinMode(PIN_CHARGER_FAULT, INPUT); + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + // output latches default to HIGH, so pull these low right after enabling + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); + // gps power is driven by the sensor manager (mode switch). off to start. + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); // 12V rail is only needed for the superIO display pinMode(SCREEN_12V_ENABLE, OUTPUT); +#ifdef MUZI_BASE_SUPERIO digitalWrite(SCREEN_12V_ENABLE, HIGH); // Enable 12V power for SH1107 display delay(250); +#else + digitalWrite(SCREEN_12V_ENABLE, LOW); #endif Wire.begin(); // delay(1000); // wait for display to initialize. otherwise it doesn't come up on boot. diff --git a/variants/muzi_base/muzi_baseBoard.h b/variants/muzi_base/MuziBaseBoard.h similarity index 91% rename from variants/muzi_base/muzi_baseBoard.h rename to variants/muzi_base/MuziBaseBoard.h index e5412b6a..a5a4abbe 100644 --- a/variants/muzi_base/muzi_baseBoard.h +++ b/variants/muzi_base/MuziBaseBoard.h @@ -15,14 +15,14 @@ #define MUZI_BASE_MFR_NAME "Muzi Base Duo" #endif -class muzi_baseBoard : public NRF52BoardDCDC { +class MuziBaseBoard : public NRF52BoardDCDC { protected: #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif public: - muzi_baseBoard() : NRF52Board(MUZI_BASE_OTA_NAME) {} + MuziBaseBoard() : NRF52Board(MUZI_BASE_OTA_NAME) {} void begin(); #define BATTERY_SAMPLES 8 diff --git a/variants/muzi_base/platformio.ini b/variants/muzi_base/platformio.ini index eb35d569..46a0ae4d 100644 --- a/variants/muzi_base/platformio.ini +++ b/variants/muzi_base/platformio.ini @@ -17,7 +17,7 @@ build_flags = ${nrf52_base.build_flags} -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 -I variants/muzi_base -I src/helpers/ui - -D muzi_base + -D MUZI_BASE -D NRF52_POWER_MANAGEMENT -D PIN_USER_BTN=PIN_BUTTON1 -D USER_BTN_PRESSED=LOW @@ -106,7 +106,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -125,7 +125,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D BLE_TX_POWER=0 @@ -150,7 +150,7 @@ extends = muzi_base_duo board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo.build_flags} - -D muzi_base_superIO + -D MUZI_BASE_SUPERIO -D UI_HAS_JOYSTICK=1 -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1107Display @@ -174,12 +174,12 @@ lib_deps = ${muzi_base_duo.lib_deps} debug_tool = jlink upload_protocol = nrfutil -[env:muzi_base_duo_companion_radio_ble_superIO] +[env:muzi_base_duo_superIO_companion_radio_ble] extends = muzi_base_duo_superIO board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo_superIO.build_flags} - -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 @@ -235,7 +235,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -254,7 +254,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D BLE_TX_POWER=0 @@ -279,7 +279,7 @@ extends = muzi_base_uno board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno.build_flags} - -D muzi_base_superIO + -D MUZI_BASE_SUPERIO -D UI_HAS_JOYSTICK=1 -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1107Display @@ -303,12 +303,12 @@ lib_deps = ${muzi_base_uno.lib_deps} debug_tool = jlink upload_protocol = nrfutil -[env:muzi_base_uno_companion_radio_ble_superIO] +[env:muzi_base_uno_superIO_companion_radio_ble] extends = muzi_base_uno_superIO board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno_superIO.build_flags} - -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 diff --git a/variants/muzi_base/target.cpp b/variants/muzi_base/target.cpp index 2af0c710..5bd846c9 100644 --- a/variants/muzi_base/target.cpp +++ b/variants/muzi_base/target.cpp @@ -3,7 +3,7 @@ #include "target.h" #include "variant.h" -muzi_baseBoard board; +MuziBaseBoard board; RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); diff --git a/variants/muzi_base/target.h b/variants/muzi_base/target.h index cbcfff14..45cd595f 100644 --- a/variants/muzi_base/target.h +++ b/variants/muzi_base/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include "muzi_baseBoard.h" +#include "MuziBaseBoard.h" #if defined(USE_LR1121) #include #elif defined(USE_SX1262) @@ -19,7 +19,7 @@ #include -#ifdef muzi_base_superIO +#ifdef MUZI_BASE_SUPERIO #include extern DISPLAY_CLASS display; extern MomentaryButton user_btn; @@ -32,7 +32,7 @@ extern MomentaryButton user_btn; #endif -extern muzi_baseBoard board; +extern MuziBaseBoard board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; diff --git a/variants/muzi_base/variant.cpp b/variants/muzi_base/variant.cpp index b179af8a..cbde3355 100644 --- a/variants/muzi_base/variant.cpp +++ b/variants/muzi_base/variant.cpp @@ -60,31 +60,3 @@ const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = 46, 47, }; - -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(PIN_VBAT_READ, INPUT); - pinMode(PIN_BATTERY_CHARGING, INPUT); - pinMode(PIN_CHARGER_FAULT, INPUT); - pinMode(PIN_BUTTON1, INPUT); - pinMode(PIN_BUTTON2, INPUT); - pinMode(PIN_BUTTON3, INPUT); - pinMode(PIN_BUTTON4, INPUT); - pinMode(PIN_BUTTON5, INPUT); - pinMode(PIN_BUTTON6, INPUT); - pinMode(LED_PIN, OUTPUT); - pinMode(LED_BLUE, OUTPUT); - digitalWrite(LED_PIN, LOW); - digitalWrite(LED_BLUE, LOW); - pinMode(BUZZER_PIN, OUTPUT); - digitalWrite(BUZZER_PIN, LOW); // turn off buzzer at start. don't leave it high. - // gps power is driven by the sensor manager (mode switch). off to start. - pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, LOW); - - pinMode(SCREEN_12V_ENABLE, OUTPUT); - digitalWrite(SCREEN_12V_ENABLE, LOW); // disable 12V power for SH1107 display for now. -} From 114cf9f4fcd0a1486ce01fa4bbf6dd6575a217f3 Mon Sep 17 00:00:00 2001 From: TJ Downes <273720+tjdownes@users.noreply.github.com> Date: Sun, 20 Sep 2026 03:46:25 -0700 Subject: [PATCH 28/37] Use the default BLE TX power on Muzi Base Drop BLE_TX_POWER=0 from the companion envs so they use the default of 4 dBm, like most other variants. The lower power was carried over from the original Base Duo PR and real-world range is better at the default. --- variants/muzi_base/platformio.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/variants/muzi_base/platformio.ini b/variants/muzi_base/platformio.ini index 46a0ae4d..72a3ecac 100644 --- a/variants/muzi_base/platformio.ini +++ b/variants/muzi_base/platformio.ini @@ -128,7 +128,6 @@ build_flags = ${muzi_base_duo.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 @@ -183,7 +182,6 @@ build_flags = ${muzi_base_duo_superIO.build_flags} -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 @@ -257,7 +255,6 @@ build_flags = ${muzi_base_uno.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 @@ -312,7 +309,6 @@ build_flags = ${muzi_base_uno_superIO.build_flags} -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 From 12a8c8d4d42bf42189526a02906d328a893f2177 Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Sun, 20 Sep 2026 15:16:33 -0400 Subject: [PATCH 29/37] Add missing include, and revert removal of lota --- src/helpers/TxtDataHelpers.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/helpers/TxtDataHelpers.cpp b/src/helpers/TxtDataHelpers.cpp index f2621c0d..4358c8b8 100644 --- a/src/helpers/TxtDataHelpers.cpp +++ b/src/helpers/TxtDataHelpers.cpp @@ -28,6 +28,10 @@ bool StrHelper::isBlank(const char* str) { #include #include +#if __has_include() +// https://github.com/stm32duino/Arduino_Core_STM32/issues/3084 +#include +#endif union int32_Float_t { @@ -103,7 +107,9 @@ static void _ftoa(float f, char *p, int *status) *p++ = '0'; else { - p += snprintf(p, 11, "%ld", (long)int_part); + ltoa(int_part, p, 10); + while (*p) + p++; } *p++ = '.'; if (frac_part == 0) From 0a15c487d8b8333dfdc5324deca1a56bb8c8eba0 Mon Sep 17 00:00:00 2001 From: Florent Date: Sat, 29 Aug 2026 17:57:06 -0400 Subject: [PATCH 30/37] m4: inital port --- boards/thinknode_m4.json | 72 ++++++++++++ variants/thinknode_m4/ThinkNodeM4Board.cpp | 46 ++++++++ variants/thinknode_m4/ThinkNodeM4Board.h | 61 ++++++++++ variants/thinknode_m4/platformio.ini | 130 +++++++++++++++++++++ variants/thinknode_m4/target.cpp | 84 +++++++++++++ variants/thinknode_m4/target.h | 26 +++++ variants/thinknode_m4/variant.cpp | 94 +++++++++++++++ variants/thinknode_m4/variant.h | 106 +++++++++++++++++ 8 files changed, 619 insertions(+) create mode 100644 boards/thinknode_m4.json create mode 100644 variants/thinknode_m4/ThinkNodeM4Board.cpp create mode 100644 variants/thinknode_m4/ThinkNodeM4Board.h create mode 100644 variants/thinknode_m4/platformio.ini create mode 100644 variants/thinknode_m4/target.cpp create mode 100644 variants/thinknode_m4/target.h create mode 100644 variants/thinknode_m4/variant.cpp create mode 100644 variants/thinknode_m4/variant.h diff --git a/boards/thinknode_m4.json b/boards/thinknode_m4.json new file mode 100644 index 00000000..babc3e52 --- /dev/null +++ b/boards/thinknode_m4.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x4405" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ] + ], + "usb_product": "elecrow_thinknode_m4", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M4", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": [ + "jlink" + ], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "elecrow nrf", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 815104, + "speed": 115200, + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ] + }, + "url": "https://github.com/Elecrow-RD", + "vendor": "ELECROW" +} \ No newline at end of file diff --git a/variants/thinknode_m4/ThinkNodeM4Board.cpp b/variants/thinknode_m4/ThinkNodeM4Board.cpp new file mode 100644 index 00000000..4a144a3a --- /dev/null +++ b/variants/thinknode_m4/ThinkNodeM4Board.cpp @@ -0,0 +1,46 @@ +#include +#include "ThinkNodeM4Board.h" +#include + +#include + +void ThinkNodeM4Board::begin() { + NRF52Board::begin(); + btn_prev_state = HIGH; + + Wire.begin(); + battery_serial->begin(4800); + + delay(10); // give sx1262 some time to power up +} + +uint16_t ThinkNodeM4Board::getBattMilliVolts() { + int tentatives = 10; + uint8_t data[5]; + uint8_t b; + + if (battery_serial->available() < 10) + return bat_level_mv; + + // discard old data + while (battery_serial->available() > 10) + battery_serial->read(); + + // synchronize + while((b = battery_serial->read()) != 0xFE) { + if (tentatives-- == 0) { + MESH_DEBUG_PRINTLN("Could not find battery frame start %x", b); + return bat_level_mv; + } + } + battery_serial->readBytes(data, 5); + + if (data[4] != 0xFD) { + MESH_DEBUG_PRINTLN("Invalid battery frame end %x", data[4]); + return bat_level_mv; + } + bat_level_percent = data[0]; + //MESH_DEBUG_PRINTLN("Battery level %d\%", bat_level_percent); + bat_level_mv = 2 * (data[1]*1000. + data[2]*10. + data[3]/10.); + return bat_level_mv; +} diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h new file mode 100644 index 00000000..ba7d95be --- /dev/null +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +class ThinkNodeM4Board : public NRF52BoardDCDC { +protected: +#if NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif + uint8_t btn_prev_state; + HardwareSerial * battery_serial; + uint16_t bat_level_mv = 0; + uint8_t bat_level_percent =0; + +public: + ThinkNodeM4Board() : NRF52Board("THINKNODE_M3_OTA"), battery_serial(&Serial2) {} + void begin(); + uint16_t getBattMilliVolts() override; + +#ifdef P_LORA_TX_LED + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off + } +#endif + + const char* getManufacturerName() const override { + return "Elecrow ThinkNode M4"; + } + + int buttonStateChanged() { + #ifdef BUTTON_PIN + uint8_t v = digitalRead(BUTTON_PIN); + if (v != btn_prev_state) { + btn_prev_state = v; + return (v == USER_BTN_PRESSED) ? 1 : -1; + } + #endif + return 0; + } + + void shutdownPeripherals() override { + // shutdown common peripherals + NRF52Board::shutdownPeripherals(); + + digitalWrite(LED_BAT1, LOW); + digitalWrite(LED_BAT2, LOW); + digitalWrite(LED_BAT3, LOW); + digitalWrite(LED_BAT4, LOW); + digitalWrite(LED_STATUS, LOW); + digitalWrite(LED_PIN, LOW); + + digitalWrite(PIN_PWR_EN, LOW); + digitalWrite(I2C_POWER, !I2C_POWER_ACTIVE); + digitalWrite(PIN_GPS_POWER, !GPS_POWER_ACTIVE); + } +}; diff --git a/variants/thinknode_m4/platformio.ini b/variants/thinknode_m4/platformio.ini new file mode 100644 index 00000000..ab8417e9 --- /dev/null +++ b/variants/thinknode_m4/platformio.ini @@ -0,0 +1,130 @@ +[ThinkNode_M4] +extends = nrf52_base +board = thinknode_m4 +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/thinknode_m4 + -I src/helpers/ui + -D THINKNODE_M4 + -D PIN_USER_BTN=12 + -D PIN_STATUS_LED=LED_STATUS + -D RADIO_CLASS=CustomLR1110 + -D WRAPPER_CLASS=CustomLR1110Wrapper + -D LORA_TX_POWER=22 + -D RF_SWITCH_TABLE + -D RX_BOOSTED_GAIN=true + -D P_LORA_BUSY=26 + -D P_LORA_SCLK=6 + -D P_LORA_NSS=27 + -D P_LORA_DIO_1=12 + -D P_LORA_MISO=8 + -D P_LORA_MOSI=7 + -D P_LORA_RESET=40 + -D P_LORA_TX_LED=LED_TX + -D LR11X0_DIO_AS_RF_SWITCH=true + -D LR11X0_DIO3_TCXO_VOLTAGE=1.6 + -D ENV_INCLUDE_GPS=1 +; -D MESH_DEBUG=1 +; -D GPS_NMEA_DEBUG=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + +<../variants/thinknode_m4> + + +debug_tool = stlink +upload_protocol = nrfutil +lib_deps= ${nrf52_base.lib_deps} + +[env:ThinkNode_M4_repeater] +extends = ThinkNode_M4 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D ADVERT_NAME='"ThinkNode_M4 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + +[env:ThinkNode_M4_room_server] +extends = ThinkNode_M4 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D ADVERT_NAME='"ThinkNode_M4 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 + -D RF_SWITCH_TABLE +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + +[env:ThinkNode_M4_companion_radio_usb] +extends = ThinkNode_M4 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + -D PIN_BUZZER=23 + -D PIN_BUZZER_EN=36 + -D ENABLE_USB_INTERFACE +build_src_filter = ${ThinkNode_M4.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${ThinkNode_M4.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M4_companion_radio_ble] +extends = ThinkNode_M4 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + -D PIN_BUZZER=23 + -D PIN_BUZZER_EN=36 +build_src_filter = ${ThinkNode_M4.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${ThinkNode_M4.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M4_kiss_modem] +extends = ThinkNode_M4 +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/kiss_modem/> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 diff --git a/variants/thinknode_m4/target.cpp b/variants/thinknode_m4/target.cpp new file mode 100644 index 00000000..7da3f400 --- /dev/null +++ b/variants/thinknode_m4/target.cpp @@ -0,0 +1,84 @@ +#include +#include "target.h" +#include + +ThinkNodeM4Board 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); +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#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_NC, + RADIOLIB_NC, + RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW , LOW }}, + { LR11x0::MODE_RX, {HIGH, LOW }}, + { LR11x0::MODE_TX, {HIGH, HIGH }}, + { LR11x0::MODE_TX_HP, {LOW , HIGH }}, + { LR11x0::MODE_TX_HF, {LOW , LOW }}, + { LR11x0::MODE_GNSS, {LOW , LOW }}, + { LR11x0::MODE_WIFI, {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(2); + radio.explicitHeader(); + +#ifdef RF_SWITCH_TABLE + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); +#endif +#ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} \ No newline at end of file diff --git a/variants/thinknode_m4/target.h b/variants/thinknode_m4/target.h new file mode 100644 index 00000000..1135f5e8 --- /dev/null +++ b/variants/thinknode_m4/target.h @@ -0,0 +1,26 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include "ThinkNodeM4Board.h" +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include "NullDisplayDriver.h" +#endif + +#ifdef DISPLAY_CLASS + extern NullDisplayDriver display; +#endif + +extern ThinkNodeM4Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/thinknode_m4/variant.cpp b/variants/thinknode_m4/variant.cpp new file mode 100644 index 00000000..7bf17bb8 --- /dev/null +++ b/variants/thinknode_m4/variant.cpp @@ -0,0 +1,94 @@ +/* + * 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[] = +{ + 0, // P0.00 + 1, // P0.01 + 2, // P0.02 + 3, // P0.03 + 4, // P0.04 + 5, // P0.05 + 6, // P0.06 + 7, // P0.07 + 8, // P0.08 + 9, // P0.09 + 10, // P0.10 + 11, // P0.11 + 12, // P0.12 + 13, // P0.13 + 14, // P0.14 + 15, // P0.15 + 16, // P0.16 + 17, // P0.17 + 18, // P0.18 + 19, // P0.19 + 20, // P0.20 + 21, // P0.21 + 22, // P0.22 + 23, // P0.23 + 24, // P0.24 + 25, // P0.25 + 26, // P0.26 + 27, // P0.27 + 28, // P0.28 + 29, // P0.29 + 30, // P0.30 + 31, // P0.31 + 32, // P1.00 + 33, // P1.01 + 34, // P1.02 + 35, // P1.03 + 36, // P1.04 + 37, // P1.05 + 38, // P1.06 + 39, // P1.07 + 40, // P1.08 + 41, // P1.09 + 42, // P1.10 + 43, // P1.11 + 44, // P1.12 + 45, // P1.13 + 46, // P1.14 + 47, // P1.15 +}; + +void initVariant() +{ +/* TODO */ + pinMode(PIN_PWR_EN, OUTPUT); + pinMode(I2C_POWER, OUTPUT); + + digitalWrite(PIN_PWR_EN, HIGH); + digitalWrite(I2C_POWER, I2C_POWER_ACTIVE); + + pinMode(LED_STATUS, OUTPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(LED_BAT1, OUTPUT); + pinMode(LED_BAT2, OUTPUT); + pinMode(LED_BAT3, OUTPUT); + pinMode(LED_BAT4, OUTPUT); + + digitalWrite(LED_BAT1, LOW); + digitalWrite(LED_BAT2, LOW); + digitalWrite(LED_BAT3, LOW); + digitalWrite(LED_BAT4, LOW); + digitalWrite(LED_STATUS, LOW); + digitalWrite(LED_PIN, LOW); + + pinMode(BUTTON_PIN, INPUT_PULLUP); + + pinMode(PIN_GPS_POWER, OUTPUT); + pinMode(PIN_GPS_EN, OUTPUT); + + // Power on gps but in standby + digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); + digitalWrite(PIN_GPS_POWER, GPS_POWER_ACTIVE); +} diff --git a/variants/thinknode_m4/variant.h b/variants/thinknode_m4/variant.h new file mode 100644 index 00000000..cc483184 --- /dev/null +++ b/variants/thinknode_m4/variant.h @@ -0,0 +1,106 @@ +/* + * 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 + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define NRF_APM // detect usb power + +#define EXT_CHRG_DETECT (38) +// Power to radio +#define PIN_PWR_EN (11) + +// I2C bus power +#define I2C_POWER (32) +#define I2C_POWER_ACTIVE LOW + +#define PIN_BAT_RX (5) +#define PIN_BAT_TX (30) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +#define PIN_SERIAL2_RX PIN_BAT_TX +#define PIN_SERIAL2_TX PIN_BAT_RX + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (1) + +#define PIN_WIRE_SDA (23) +#define PIN_WIRE_SCL (25) +#define I2C_NO_RESCAN + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (8) +#define PIN_SPI_MOSI (7) +#define PIN_SPI_SCK (6) +#define PIN_SPI_NSS (27) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define LED_BLUE (-1) // disable blue led +#define LED_STATUS (13) // blue +#define LED_PIN (41) // red +#define LED_TX LED_PIN +#define LED_BUILTIN LED_BLUE +#define LED_STATE_ON HIGH + +#define LED_BAT1 (15) +#define LED_BAT2 (17) +#define LED_BAT3 (34) +#define LED_BAT4 (36) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (4) +#define BUTTON_PIN PIN_BUTTON1 +#define USER_BTN_PRESSED HIGH + +//////////////////////////////////////////////////////////////////////////////// +// GPS + +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 +#define PIN_GPS_RX (44) +#define PIN_GPS_TX (46) + +#define PIN_GPS_POWER (14) +#define GPS_POWER_ACTIVE LOW +#define PIN_GPS_EN (43) +#define GPS_EN_ACTIVE LOW +#define PIN_GPS_RESET (3) +#define GPS_RESET_ACTIVE HIGH From 8bf072a6f011d841cd6c0a4168aba85e5ad6d2ae Mon Sep 17 00:00:00 2001 From: Florent Date: Sun, 20 Sep 2026 16:13:15 -0400 Subject: [PATCH 31/37] M4: Handle poweroff --- variants/thinknode_m4/ThinkNodeM4Board.h | 14 ++++++++++++++ variants/thinknode_m4/platformio.ini | 2 +- variants/thinknode_m4/variant.h | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h index ba7d95be..c05de911 100644 --- a/variants/thinknode_m4/ThinkNodeM4Board.h +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -47,6 +47,16 @@ public: // shutdown common peripherals NRF52Board::shutdownPeripherals(); + #ifdef LED_STATUS + digitalWrite(LED_STATUS, HIGH); + #endif + #ifdef BUTTON_PIN + while(!digitalRead(BUTTON_PIN)); + #endif + #ifdef LED_STATUS + digitalWrite(LED_STATUS, LOW); + #endif + digitalWrite(LED_BAT1, LOW); digitalWrite(LED_BAT2, LOW); digitalWrite(LED_BAT3, LOW); @@ -57,5 +67,9 @@ public: digitalWrite(PIN_PWR_EN, LOW); digitalWrite(I2C_POWER, !I2C_POWER_ACTIVE); digitalWrite(PIN_GPS_POWER, !GPS_POWER_ACTIVE); + + #ifdef BUTTON_PIN + nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW); + #endif } }; diff --git a/variants/thinknode_m4/platformio.ini b/variants/thinknode_m4/platformio.ini index ab8417e9..06762f88 100644 --- a/variants/thinknode_m4/platformio.ini +++ b/variants/thinknode_m4/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/thinknode_m4 -I src/helpers/ui -D THINKNODE_M4 - -D PIN_USER_BTN=12 + -D PIN_USER_BTN=4 -D PIN_STATUS_LED=LED_STATUS -D RADIO_CLASS=CustomLR1110 -D WRAPPER_CLASS=CustomLR1110Wrapper diff --git a/variants/thinknode_m4/variant.h b/variants/thinknode_m4/variant.h index cc483184..6e355d7f 100644 --- a/variants/thinknode_m4/variant.h +++ b/variants/thinknode_m4/variant.h @@ -88,7 +88,7 @@ #define PIN_BUTTON1 (4) #define BUTTON_PIN PIN_BUTTON1 -#define USER_BTN_PRESSED HIGH +#define USER_BTN_PRESSED LOW //////////////////////////////////////////////////////////////////////////////// // GPS From 84f1e11d29823d51a0047beb07d76b9523093ce5 Mon Sep 17 00:00:00 2001 From: Ev Lbibass <27320050+lbibass@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:38:05 -0400 Subject: [PATCH 32/37] update structure of LR1121 to match LR1110 files. Implement DC-DC on LR1121 radio. saves about 30% power consumption at idle. --- src/helpers/radiolib/CustomLR1121.h | 74 ++++++++++++++++++++-- src/helpers/radiolib/CustomLR1121Wrapper.h | 3 + 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h index 873bfabc..67e3b52a 100644 --- a/src/helpers/radiolib/CustomLR1121.h +++ b/src/helpers/radiolib/CustomLR1121.h @@ -4,11 +4,25 @@ #include "MeshCore.h" class CustomLR1121 : public LR1121 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; bool _rx_boosted = false; public: CustomLR1121(Module *mod) : LR1121(mod) { } + int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7, + uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10, + uint16_t preambleLength = 8, float tcxoVoltage = 1.6) { + int16_t state = LR1121::begin(freq, bw, sf, cr, syncWord, power, preambleLength, + tcxoVoltage); + // RadioLib begin() defaults to LDO; use the LR1121 DC/DC regulator. + if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC(); + return state; + } + size_t getPacketLength(bool update) override { size_t len = LR1121::getPacketLength(update); if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { @@ -31,11 +45,61 @@ class CustomLR1121 : public LR1121 { bool getRxBoostedGainMode() const { return _rx_boosted; } - bool isReceiving() { - uint16_t irq = getIrqStatus(); - bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); - return detected; + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags. + return LR1121::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } - uint8_t getSpreadingFactor() const { return spreadingFactor; } + bool isReceiving() { + uint32_t irq = getIrqStatus(); + bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4 + bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5 + bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6 + uint32_t now = millis(); + if (hdrErr) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; + } + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); + } + + uint8_t getSpreadingFactor() const { return spreadingFactor; } }; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomLR1121Wrapper.h b/src/helpers/radiolib/CustomLR1121Wrapper.h index 5361ee23..5a754cee 100644 --- a/src/helpers/radiolib/CustomLR1121Wrapper.h +++ b/src/helpers/radiolib/CustomLR1121Wrapper.h @@ -14,6 +14,9 @@ public: ((CustomLR1121 *)_radio)->setBandwidth(bw); ((CustomLR1121 *)_radio)->setCodingRate(cr); updatePreamble(sf); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomLR1121 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomLR1121 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1121 *)_radio)->getFreqMHz(), getRxBoostedGainMode()); } From b5787757a7666b311f016263fed280bd94c22b9e Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Mon, 21 Sep 2026 15:55:44 +0200 Subject: [PATCH 33/37] Thinknode m4 - don't touch crystal and OTA name --- variants/thinknode_m4/ThinkNodeM4Board.h | 2 +- variants/thinknode_m4/variant.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h index c05de911..c2cae54b 100644 --- a/variants/thinknode_m4/ThinkNodeM4Board.h +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -15,7 +15,7 @@ protected: uint8_t bat_level_percent =0; public: - ThinkNodeM4Board() : NRF52Board("THINKNODE_M3_OTA"), battery_serial(&Serial2) {} + ThinkNodeM4Board() : NRF52Board("THINKNODE_M4_OTA"), battery_serial(&Serial2) {} void begin(); uint16_t getBattMilliVolts() override; diff --git a/variants/thinknode_m4/variant.cpp b/variants/thinknode_m4/variant.cpp index 7bf17bb8..fbdb08e2 100644 --- a/variants/thinknode_m4/variant.cpp +++ b/variants/thinknode_m4/variant.cpp @@ -10,8 +10,8 @@ const uint32_t g_ADigitalPinMap[] = { - 0, // P0.00 - 1, // P0.01 + 0xff, // P0.00 - LFXO (do not use) + 0xff, // P0.01 - LFXO (do not use) 2, // P0.02 3, // P0.03 4, // P0.04 From 0c3fe356d06ea6b9eb6df719c9647ee3664c821d Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Mon, 21 Sep 2026 12:26:36 -0400 Subject: [PATCH 34/37] Constrain `agc.reset.interval` and allow intervals > 255s --- docs/cli_commands.md | 2 +- examples/companion_radio/NodePrefs.h | 4 ++-- src/helpers/CommonCLI.h | 4 ++-- src/helpers/CommonRadioPrefs.cpp | 4 +++- src/helpers/CommonRadioPrefs.h | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f4a43fad..9fde4d43 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -628,7 +628,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set agc.reset.interval ` **Parameters:** -- `value`: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable. +- `value`: Interval in seconds, 0-1020, rounded down to a multiple of 4 (17 becomes 16). 0 to disable. **Default:** `0.0` diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 4b169d73..46a3dbb7 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -106,8 +106,8 @@ private: void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } float getRxDelay() const override { return _parent->rx_delay_base; } void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } - uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } - void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } + uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint16_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } uint8_t getHashMode() const override { return _parent->path_hash_mode; } void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } uint8_t getMultiAcks() const override { return _parent->multi_acks; } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 8591cdc1..2759b0c4 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -119,8 +119,8 @@ private: void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } float getRxDelay() const override { return _parent->rx_delay_base; } void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } - uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } - void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } + uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint16_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } uint8_t getHashMode() const override { return _parent->path_hash_mode; } void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } uint8_t getMultiAcks() const override { return _parent->multi_acks; } diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index 9d92cad9..41b17993 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -165,7 +165,9 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest return true; } if (memcmp(command, "set agc.reset.interval ", 23) == 0) { - setAgcResetInt(atoi(&command[23])); + int secs = atoi(&command[23]); + secs = constrain(secs, 0, 255 * 4); + setAgcResetInt((uint16_t) secs); sprintf(reply, "OK - interval rounded to %d", (uint32_t) getAgcResetInt()); return true; } diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h index 96895bb2..0e83ca9d 100644 --- a/src/helpers/CommonRadioPrefs.h +++ b/src/helpers/CommonRadioPrefs.h @@ -41,8 +41,8 @@ public: virtual float getRxDelay() const = 0; virtual void setRxDelay(float d) = 0; - virtual uint8_t getAgcResetInt() const = 0; - virtual void setAgcResetInt(uint8_t secs) = 0; + virtual uint16_t getAgcResetInt() const = 0; + virtual void setAgcResetInt(uint16_t secs) = 0; virtual uint8_t getHashMode() const = 0; virtual void setHashMode(uint8_t m) = 0; From 61be70849a9a2f123b4b1e0146a1ee05a261ddb5 Mon Sep 17 00:00:00 2001 From: Wolfram Keil Date: Thu, 27 Aug 2026 21:38:46 +0200 Subject: [PATCH 35/37] Add board loop hook for periodic work --- examples/simple_repeater/main.cpp | 2 ++ examples/simple_sensor/main.cpp | 2 ++ src/MeshCore.h | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 1f71da74..d475b13d 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -165,6 +165,8 @@ void loop() { command[0] = 0; // reset command buffer } + board.loop(); // let the board feed its watchdog, run periodic housekeeping + #ifdef ETHERNET_ENABLED ethernet_loop_maintain(); if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) { diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 749ff6ef..db1c5279 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -153,6 +153,8 @@ void loop() { command[0] = 0; // reset command buffer } + board.loop(); // let the board feed its watchdog, run periodic housekeeping + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/src/MeshCore.h b/src/MeshCore.h index 4bc9d3db..2a3582de 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -76,6 +76,10 @@ public: virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; } virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { return false; } + + // Called from the example main loops. Lets a board feed its watchdog and + // run periodic housekeeping. Default no-op. + virtual void loop() { /* no op */ } }; /** From 560fb847d580a2cc97d29ab531c2a0273e20fefc Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 22 Sep 2026 18:01:15 +1000 Subject: [PATCH 36/37] missing board.loop() in companion, room server, kiss modem. --- examples/companion_radio/main.cpp | 1 + examples/kiss_modem/main.cpp | 4 +++- examples/simple_room_server/main.cpp | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 4bc75a81..839d4958 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -297,6 +297,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + board.loop(); #ifdef HAS_EXTERNAL_WATCHDOG external_watchdog.loop(); #endif diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 5836a694..44b2a3df 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -113,7 +113,7 @@ void setup() { uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); -#if defined(ESP32) && ARDUINO_USB_MODE +#if defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS); Serial.setTxBufferSize(USB_TX_BUFFER_SIZE); #endif @@ -149,6 +149,8 @@ void loop() { } } + board.loop(); + if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) { radio_driver.triggerNoiseFloorCalibrate(0); next_noise_floor_calib_ms = millis(); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 227ee2cb..65fc55ef 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -162,6 +162,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + board.loop(); #ifdef HAS_EXTERNAL_WATCHDOG external_watchdog.loop(); #endif From c13b31218869f11c8b6e47f16d96b9bab9329095 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 23 Sep 2026 15:04:51 +1200 Subject: [PATCH 37/37] fix heltec ct62 boot loop --- variants/heltec_ct62/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini index 401a30d2..f98f8d4a 100644 --- a/variants/heltec_ct62/platformio.ini +++ b/variants/heltec_ct62/platformio.ini @@ -1,6 +1,7 @@ [Heltec_ct62] extends = esp32_base board = esp32-c3-devkitm-1 +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = ${esp32_base.build_flags} -I variants/heltec_ct62