Parcourir la source

Merge branch 'dev' into double-acks

Scott Powell il y a 1 an
Parent
commit
c26418016b

+ 52 - 6
examples/companion_radio/MyMesh.cpp

@@ -46,6 +46,9 @@
 #define CMD_SET_CUSTOM_VAR            41
 #define CMD_SET_CUSTOM_VAR            41
 #define CMD_GET_ADVERT_PATH           42
 #define CMD_GET_ADVERT_PATH           42
 #define CMD_GET_TUNING_PARAMS         43
 #define CMD_GET_TUNING_PARAMS         43
+// NOTE: CMD range 44..49 parked, potentially for WiFi operations
+#define CMD_SEND_BINARY_REQ           50
+#define CMD_FACTORY_RESET             51
 
 
 #define RESP_CODE_OK                  0
 #define RESP_CODE_OK                  0
 #define RESP_CODE_ERR                 1
 #define RESP_CODE_ERR                 1
@@ -93,6 +96,7 @@
 #define PUSH_CODE_TRACE_DATA            0x89
 #define PUSH_CODE_TRACE_DATA            0x89
 #define PUSH_CODE_NEW_ADVERT            0x8A
 #define PUSH_CODE_NEW_ADVERT            0x8A
 #define PUSH_CODE_TELEMETRY_RESPONSE    0x8B
 #define PUSH_CODE_TELEMETRY_RESPONSE    0x8B
+#define PUSH_CODE_BINARY_RESPONSE       0x8C
 
 
 #define ERR_CODE_UNSUPPORTED_CMD        1
 #define ERR_CODE_UNSUPPORTED_CMD        1
 #define ERR_CODE_NOT_FOUND              2
 #define ERR_CODE_NOT_FOUND              2
@@ -468,6 +472,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
       i += 6; // pub_key_prefix
       i += 6; // pub_key_prefix
       memcpy(&out_frame[i], &tag, 4);
       memcpy(&out_frame[i], &tag, 4);
       i += 4; // NEW: include server timestamp
       i += 4; // NEW: include server timestamp
+      out_frame[i++] = data[7]; // NEW (v7): ACL permissions
     } else {
     } else {
       out_frame[i++] = PUSH_CODE_LOGIN_FAIL;
       out_frame[i++] = PUSH_CODE_LOGIN_FAIL;
       out_frame[i++] = 0; // reserved
       out_frame[i++] = 0; // reserved
@@ -490,7 +495,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
     memcpy(&out_frame[i], &data[4], len - 4);
     memcpy(&out_frame[i], &data[4], len - 4);
     i += (len - 4);
     i += (len - 4);
     _serial->writeFrame(out_frame, i);
     _serial->writeFrame(out_frame, i);
-  } else if (len > 4 && tag == pending_telemetry) { // check for telemetry response
+  } else if (len > 4 && tag == pending_telemetry) {  // check for matching response tag
     pending_telemetry = 0;
     pending_telemetry = 0;
 
 
     int i = 0;
     int i = 0;
@@ -501,6 +506,17 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
     memcpy(&out_frame[i], &data[4], len - 4);
     memcpy(&out_frame[i], &data[4], len - 4);
     i += (len - 4);
     i += (len - 4);
     _serial->writeFrame(out_frame, i);
     _serial->writeFrame(out_frame, i);
+  } else if (len > 4 && tag == pending_req) {  // check for matching response tag
+    pending_req = 0;
+
+    int i = 0;
+    out_frame[i++] = PUSH_CODE_BINARY_RESPONSE;
+    out_frame[i++] = 0; // reserved
+    memcpy(&out_frame[i], &tag, 4);   // app needs to match this to RESP_CODE_SENT.tag
+    i += 4;
+    memcpy(&out_frame[i], &data[4], len - 4);
+    i += (len - 4);
+    _serial->writeFrame(out_frame, i);
   }
   }
 }
 }
 
 
@@ -566,7 +582,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
   _cli_rescue = false;
   _cli_rescue = false;
   offline_queue_len = 0;
   offline_queue_len = 0;
   app_target_ver = 0;
   app_target_ver = 0;
-  pending_login = pending_status = pending_telemetry = 0;
+  pending_login = pending_status = pending_telemetry = pending_req = 0;
   next_ack_idx = 0;
   next_ack_idx = 0;
   sign_data = NULL;
   sign_data = NULL;
   dirty_contacts_expiry = 0;
   dirty_contacts_expiry = 0;
@@ -1103,7 +1119,7 @@ void MyMesh::handleCmdFrame(size_t len) {
       if (result == MSG_SEND_FAILED) {
       if (result == MSG_SEND_FAILED) {
         writeErrFrame(ERR_CODE_TABLE_FULL);
         writeErrFrame(ERR_CODE_TABLE_FULL);
       } else {
       } else {
-        pending_telemetry = pending_status = 0;
+        pending_req = pending_telemetry = pending_status = 0;
         memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse()
         memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse()
         out_frame[0] = RESP_CODE_SENT;
         out_frame[0] = RESP_CODE_SENT;
         out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
         out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
@@ -1123,7 +1139,7 @@ void MyMesh::handleCmdFrame(size_t len) {
       if (result == MSG_SEND_FAILED) {
       if (result == MSG_SEND_FAILED) {
         writeErrFrame(ERR_CODE_TABLE_FULL);
         writeErrFrame(ERR_CODE_TABLE_FULL);
       } else {
       } else {
-        pending_telemetry = pending_login = 0;
+        pending_req = pending_telemetry = pending_login = 0;
         // FUTURE:  pending_status = tag;  // match this in onContactResponse()
         // FUTURE:  pending_status = tag;  // match this in onContactResponse()
         memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme
         memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme
         out_frame[0] = RESP_CODE_SENT;
         out_frame[0] = RESP_CODE_SENT;
@@ -1135,7 +1151,7 @@ void MyMesh::handleCmdFrame(size_t len) {
     } else {
     } else {
       writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
       writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
     }
     }
-  } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) {
+  } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) {  // can deprecate, in favour of CMD_SEND_BINARY_REQ
     uint8_t *pub_key = &cmd_frame[4];
     uint8_t *pub_key = &cmd_frame[4];
     ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
     ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
     if (recipient) {
     if (recipient) {
@@ -1144,7 +1160,7 @@ void MyMesh::handleCmdFrame(size_t len) {
       if (result == MSG_SEND_FAILED) {
       if (result == MSG_SEND_FAILED) {
         writeErrFrame(ERR_CODE_TABLE_FULL);
         writeErrFrame(ERR_CODE_TABLE_FULL);
       } else {
       } else {
-        pending_status = pending_login = 0;
+        pending_status = pending_login = pending_req = 0;
         pending_telemetry = tag; // match this in onContactResponse()
         pending_telemetry = tag; // match this in onContactResponse()
         out_frame[0] = RESP_CODE_SENT;
         out_frame[0] = RESP_CODE_SENT;
         out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
         out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
@@ -1170,6 +1186,27 @@ void MyMesh::handleCmdFrame(size_t len) {
     memcpy(&out_frame[i], telemetry.getBuffer(), tlen);
     memcpy(&out_frame[i], telemetry.getBuffer(), tlen);
     i += tlen;
     i += tlen;
     _serial->writeFrame(out_frame, i);
     _serial->writeFrame(out_frame, i);
+  } else if (cmd_frame[0] == CMD_SEND_BINARY_REQ && len >= 2 + PUB_KEY_SIZE) {
+    uint8_t *pub_key = &cmd_frame[1];
+    ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
+    if (recipient) {
+      uint8_t *req_data = &cmd_frame[1 + PUB_KEY_SIZE];
+      uint32_t tag, est_timeout;
+      int result = sendRequest(*recipient, req_data, len - (1 + PUB_KEY_SIZE), tag, est_timeout);
+      if (result == MSG_SEND_FAILED) {
+        writeErrFrame(ERR_CODE_TABLE_FULL);
+      } else {
+        pending_status = pending_login = pending_telemetry = 0;
+        pending_req = tag; // match this in onContactResponse()
+        out_frame[0] = RESP_CODE_SENT;
+        out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
+        memcpy(&out_frame[2], &tag, 4);
+        memcpy(&out_frame[6], &est_timeout, 4);
+        _serial->writeFrame(out_frame, 10);
+      }
+    } else {
+      writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
+    }
   } else if (cmd_frame[0] == CMD_HAS_CONNECTION && len >= 1 + PUB_KEY_SIZE) {
   } else if (cmd_frame[0] == CMD_HAS_CONNECTION && len >= 1 + PUB_KEY_SIZE) {
     uint8_t *pub_key = &cmd_frame[1];
     uint8_t *pub_key = &cmd_frame[1];
     if (hasConnectionTo(pub_key)) {
     if (hasConnectionTo(pub_key)) {
@@ -1325,6 +1362,15 @@ void MyMesh::handleCmdFrame(size_t len) {
     } else {
     } else {
       writeErrFrame(ERR_CODE_NOT_FOUND);
       writeErrFrame(ERR_CODE_NOT_FOUND);
     }
     }
+  } else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) {
+    bool success = _store->formatFileSystem();
+    if (success) {
+      writeOKFrame();
+      delay(1000);
+      board.reboot();  // doesn't return
+    } else {
+      writeErrFrame(ERR_CODE_FILE_IO_ERROR);
+    }
   } else {
   } else {
     writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
     writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
     MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);
     MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);

+ 5 - 4
examples/companion_radio/MyMesh.h

@@ -7,14 +7,14 @@
 #endif
 #endif
 
 
 /*------------ Frame Protocol --------------*/
 /*------------ Frame Protocol --------------*/
-#define FIRMWARE_VER_CODE 6
+#define FIRMWARE_VER_CODE 7
 
 
 #ifndef FIRMWARE_BUILD_DATE
 #ifndef FIRMWARE_BUILD_DATE
-#define FIRMWARE_BUILD_DATE "2 Jul 2025"
+#define FIRMWARE_BUILD_DATE "15 Jul 2025"
 #endif
 #endif
 
 
 #ifndef FIRMWARE_VERSION
 #ifndef FIRMWARE_VERSION
-#define FIRMWARE_VERSION "v1.7.2"
+#define FIRMWARE_VERSION "v1.7.3"
 #endif
 #endif
 
 
 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -160,7 +160,8 @@ private:
   NodePrefs _prefs;
   NodePrefs _prefs;
   uint32_t pending_login;
   uint32_t pending_login;
   uint32_t pending_status;
   uint32_t pending_status;
-  uint32_t pending_telemetry;
+  uint32_t pending_telemetry;   // pending _TELEMETRY_REQ
+  uint32_t pending_req;   // pending _BINARY_REQ
   BaseSerialInterface *_serial;
   BaseSerialInterface *_serial;
 
 
   ContactsIterator _iter;
   ContactsIterator _iter;

+ 2 - 2
examples/simple_repeater/main.cpp

@@ -22,11 +22,11 @@
 /* ------------------------------ Config -------------------------------- */
 /* ------------------------------ Config -------------------------------- */
 
 
 #ifndef FIRMWARE_BUILD_DATE
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "2 Jul 2025"
+  #define FIRMWARE_BUILD_DATE   "15 Jul 2025"
 #endif
 #endif
 
 
 #ifndef FIRMWARE_VERSION
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.7.2"
+  #define FIRMWARE_VERSION   "v1.7.3"
 #endif
 #endif
 
 
 #ifndef LORA_FREQ
 #ifndef LORA_FREQ

+ 2 - 2
examples/simple_room_server/main.cpp

@@ -22,11 +22,11 @@
 /* ------------------------------ Config -------------------------------- */
 /* ------------------------------ Config -------------------------------- */
 
 
 #ifndef FIRMWARE_BUILD_DATE
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "2 Jul 2025"
+  #define FIRMWARE_BUILD_DATE   "15 Jul 2025"
 #endif
 #endif
 
 
 #ifndef FIRMWARE_VERSION
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.7.2"
+  #define FIRMWARE_VERSION   "v1.7.3"
 #endif
 #endif
 
 
 #ifndef LORA_FREQ
 #ifndef LORA_FREQ

+ 140 - 49
examples/simple_sensor/SensorMesh.cpp

@@ -51,6 +51,7 @@
 #define REQ_TYPE_KEEP_ALIVE          0x02
 #define REQ_TYPE_KEEP_ALIVE          0x02
 #define REQ_TYPE_GET_TELEMETRY_DATA  0x03
 #define REQ_TYPE_GET_TELEMETRY_DATA  0x03
 #define REQ_TYPE_GET_AVG_MIN_MAX     0x04
 #define REQ_TYPE_GET_AVG_MIN_MAX     0x04
+#define REQ_TYPE_GET_ACCESS_LIST     0x05
 
 
 #define RESP_SERVER_LOGIN_OK      0   // response to ANON_REQ
 #define RESP_SERVER_LOGIN_OK      0   // response to ANON_REQ
 
 
@@ -58,6 +59,8 @@
 
 
 #define LAZY_CONTACTS_WRITE_DELAY       5000
 #define LAZY_CONTACTS_WRITE_DELAY       5000
 
 
+#define ALERT_ACK_EXPIRY_MILLIS         8000   // wait 8 secs for ACKs to alert messages
+
 static File openAppend(FILESYSTEM* _fs, const char* fname) {
 static File openAppend(FILESYSTEM* _fs, const char* fname) {
   #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
   #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
     return _fs->open(fname, FILE_O_WRITE);
     return _fs->open(fname, FILE_O_WRITE);
@@ -92,11 +95,11 @@ void SensorMesh::loadContacts() {
       while (!full) {
       while (!full) {
         ContactInfo c;
         ContactInfo c;
         uint8_t pub_key[32];
         uint8_t pub_key[32];
-        uint8_t unused[5];
+        uint8_t unused[6];
 
 
         bool success = (file.read(pub_key, 32) == 32);
         bool success = (file.read(pub_key, 32) == 32);
-        success = success && (file.read((uint8_t *) &c.permissions, 2) == 2);
-        success = success && (file.read(unused, 5) == 5);
+        success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
+        success = success && (file.read(unused, 6) == 6);
         success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
         success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
         success = success && (file.read(c.out_path, 64) == 64);
         success = success && (file.read(c.out_path, 64) == 64);
         success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
         success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
@@ -128,8 +131,8 @@ void SensorMesh::saveContacts() {
       if (c->permissions == 0) continue;    // skip deleted entries
       if (c->permissions == 0) continue;    // skip deleted entries
 
 
       bool success = (file.write(c->id.pub_key, 32) == 32);
       bool success = (file.write(c->id.pub_key, 32) == 32);
-      success = success && (file.write((uint8_t *) &c->permissions, 2) == 2);
-      success = success && (file.write(unused, 5) == 5);
+      success = success && (file.write((uint8_t *) &c->permissions, 1) == 1);
+      success = success && (file.write(unused, 6) == 6);
       success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
       success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
       success = success && (file.write(c->out_path, 64) == 64);
       success = success && (file.write(c->out_path, 64) == 64);
       success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
       success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
@@ -163,6 +166,7 @@ static uint8_t getDataSize(uint8_t type) {
       case LPP_TEMPERATURE:
       case LPP_TEMPERATURE:
       case LPP_CONCENTRATION:
       case LPP_CONCENTRATION:
       case LPP_BAROMETRIC_PRESSURE:
       case LPP_BAROMETRIC_PRESSURE:
+      case LPP_RELATIVE_HUMIDITY:
       case LPP_ALTITUDE:
       case LPP_ALTITUDE:
       case LPP_VOLTAGE:
       case LPP_VOLTAGE:
       case LPP_CURRENT:
       case LPP_CURRENT:
@@ -185,6 +189,7 @@ static uint32_t getMultiplier(uint8_t type) {
         return 100;
         return 100;
       case LPP_TEMPERATURE:
       case LPP_TEMPERATURE:
       case LPP_BAROMETRIC_PRESSURE:
       case LPP_BAROMETRIC_PRESSURE:
+      case LPP_RELATIVE_HUMIDITY:
         return 10;
         return 10;
     }
     }
     return 1;
     return 1;
@@ -235,7 +240,7 @@ static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t mult
   return size;
   return size;
 }
 }
 
 
-uint8_t SensorMesh::handleRequest(uint16_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
+uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
   memcpy(reply_data, &sender_timestamp, 4);   // reflect sender_timestamp back in response packet (kind of like a 'tag')
   memcpy(reply_data, &sender_timestamp, 4);   // reflect sender_timestamp back in response packet (kind of like a 'tag')
 
 
   if (req_type == REQ_TYPE_GET_TELEMETRY_DATA && (perms & PERM_GET_TELEMETRY) != 0) {
   if (req_type == REQ_TYPE_GET_TELEMETRY_DATA && (perms & PERM_GET_TELEMETRY) != 0) {
@@ -243,17 +248,18 @@ uint8_t SensorMesh::handleRequest(uint16_t perms, uint32_t sender_timestamp, uin
     telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
     telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
     // query other sensors -- target specific
     // query other sensors -- target specific
     sensors.querySensors(0xFF, telemetry);  // allow all telemetry permissions for admin or guest
     sensors.querySensors(0xFF, telemetry);  // allow all telemetry permissions for admin or guest
+    // TODO: let requester know permissions they have:  telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
 
 
     uint8_t tlen = telemetry.getSize();
     uint8_t tlen = telemetry.getSize();
     memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
     memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
     return 4 + tlen;  // reply_len
     return 4 + tlen;  // reply_len
   }
   }
-  if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_GET_MIN_MAX_AVG) != 0) {
+  if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_GET_OTHER_STATS) != 0) {
     uint32_t start_secs_ago, end_secs_ago;
     uint32_t start_secs_ago, end_secs_ago;
     memcpy(&start_secs_ago, &payload[0], 4);
     memcpy(&start_secs_ago, &payload[0], 4);
     memcpy(&end_secs_ago, &payload[4], 4);
     memcpy(&end_secs_ago, &payload[4], 4);
     uint8_t res1 = payload[8];   // reserved for future  (extra query params)
     uint8_t res1 = payload[8];   // reserved for future  (extra query params)
-    uint8_t res2 = payload[8];
+    uint8_t res2 = payload[9];
 
 
     MinMaxAvg data[8];
     MinMaxAvg data[8];
     int n;
     int n;
@@ -282,6 +288,19 @@ uint8_t SensorMesh::handleRequest(uint16_t perms, uint32_t sender_timestamp, uin
     }
     }
     return ofs;
     return ofs;
   }
   }
+  if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_LEVEL3) {
+    uint8_t res1 = payload[0];   // reserved for future  (extra query params)
+    uint8_t res2 = payload[1];
+    if (res1 == 0 && res2 == 0) {
+      uint8_t ofs = 4;
+      for (int i = 0; i < num_contacts && ofs + 7 <= sizeof(reply_data) - 4; i++) {
+        auto c = &contacts[i];
+        memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6;  // just 6-byte pub_key prefix
+        reply_data[ofs++] = c->permissions;
+      }
+      return ofs;
+    }
+  }
   return 0;  // unknown command
   return 0;  // unknown command
 }
 }
 
 
@@ -319,11 +338,11 @@ ContactInfo* SensorMesh::putContact(const mesh::Identity& id) {
   return c;
   return c;
 }
 }
 
 
-void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint16_t perms) {
+void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint8_t perms) {
   mesh::Identity id(pubkey);
   mesh::Identity id(pubkey);
   auto c = putContact(id);
   auto c = putContact(id);
 
 
-  if (perms == 0) {  // no permissions, remove from contacts
+  if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) {  // guest role is not persisted in contacts
     memset(c, 0, sizeof(*c));
     memset(c, 0, sizeof(*c));
   } else {
   } else {
     c->permissions = perms;  // update their permissions
     c->permissions = perms;  // update their permissions
@@ -332,46 +351,54 @@ void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint16_t perms)
   dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);   // trigger saveContacts()
   dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);   // trigger saveContacts()
 }
 }
 
 
-void SensorMesh::sendAlert(AlertPriority pri, const char* text) {
-  int text_len = strlen(text);
-  uint16_t pri_mask = (pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO;
+void SensorMesh::sendAlert(ContactInfo* c, Trigger* t) {
+  int text_len = strlen(t->text);
 
 
-  // send text message to all contacts with RECV_ALERT permission
-  for (int i = 0; i < num_contacts; i++) {
-    auto c = &contacts[i];
-    if ((c->permissions & pri_mask) == 0) continue;   // contact does NOT want alert
-
-    uint8_t data[MAX_PACKET_PAYLOAD];
-    uint32_t now = getRTCClock()->getCurrentTimeUnique();  // need different timestamp per packet
-    memcpy(data, &now, 4);
-    data[4] = (TXT_TYPE_PLAIN << 2);  // attempt and flags
-    memcpy(&data[5], text, text_len);
-    // calc expected ACK reply
-    // uint32_t expected_ack;
-    // mesh::Utils::sha256((uint8_t *)&expected_ack, 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
-
-    auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len);
-    if (pkt) {
-      if (c->out_path_len >= 0) {  // we have an out_path, so send DIRECT
-        sendDirect(pkt, c->out_path, c->out_path_len);
-      } else {
-        sendFlood(pkt);
-      }
+  uint8_t data[MAX_PACKET_PAYLOAD];
+  memcpy(data, &t->timestamp, 4);
+  data[4] = (TXT_TYPE_PLAIN << 2) | t->attempt;  // attempt and flags
+  memcpy(&data[5], t->text, text_len);
+
+  // calc expected ACK reply
+  mesh::Utils::sha256((uint8_t *)&t->expected_acks[t->attempt], 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
+  t->attempt++;
+
+  auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len);
+  if (pkt) {
+    if (c->out_path_len >= 0) {  // we have an out_path, so send DIRECT
+      sendDirect(pkt, c->out_path, c->out_path_len);
+    } else {
+      sendFlood(pkt);
     }
     }
   }
   }
+  t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
 }
 }
 
 
 void SensorMesh::alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text) {
 void SensorMesh::alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text) {
   if (condition) {
   if (condition) {
-    if (!t.triggered) {
-      t.triggered = true;
-      t.time = getRTCClock()->getCurrentTime();
-      sendAlert(pri, text);
+    if (!t.isTriggered() && num_alert_tasks < MAX_CONCURRENT_ALERTS) {
+      StrHelper::strncpy(t.text, text, sizeof(t.text));
+      t.pri = pri;
+      t.send_expiry = 0;  // signal that initial send is needed
+      t.attempt = 4;
+      t.curr_contact_idx = -1;  // start iterating thru contacts[]
+
+      alert_tasks[num_alert_tasks++] = &t;  // add to queue
     }
     }
   } else {
   } else {
-    if (t.triggered) {
-      t.triggered = false;
-      // TODO: apply debounce logic
+    if (t.isTriggered()) {
+      t.text[0] = 0;
+      // remove 't' from alert queue
+      int i = 0;
+      while (i < num_alert_tasks && alert_tasks[i] != &t) i++;
+
+      if (i < num_alert_tasks) {  // found,  now delete from array
+        num_alert_tasks--;
+        while (i < num_alert_tasks) {
+          alert_tasks[i] = alert_tasks[i + 1];
+          i++;
+        }
+      }
     }
     }
   }
   }
 }
 }
@@ -423,7 +450,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
   MESH_DEBUG_PRINTLN("Login success!");
   MESH_DEBUG_PRINTLN("Login success!");
   client->last_timestamp = sender_timestamp;
   client->last_timestamp = sender_timestamp;
   client->last_activity = getRTCClock()->getCurrentTime();
   client->last_activity = getRTCClock()->getCurrentTime();
-  client->permissions = PERM_IS_ADMIN | PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO;  // initially opt-in to receive alerts (can opt out)
+  client->permissions = PERM_ACL_LEVEL3 | PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO;  // initially opt-in to receive alerts (can opt out)
   memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
   memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
 
 
   dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
   dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
@@ -433,7 +460,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
   reply_data[4] = RESP_SERVER_LOGIN_OK;
   reply_data[4] = RESP_SERVER_LOGIN_OK;
   reply_data[5] = 0;  // NEW: recommended keep-alive interval (secs / 16)
   reply_data[5] = 0;  // NEW: recommended keep-alive interval (secs / 16)
   reply_data[6] = 1;  // 1 = is admin
   reply_data[6] = 1;  // 1 = is admin
-  reply_data[7] = 0;  // FUTURE: reserved
+  reply_data[7] = client->permissions;
   getRNG()->random(&reply_data[8], 4);   // random blob to help packet-hash uniqueness
   getRNG()->random(&reply_data[8], 4);   // random blob to help packet-hash uniqueness
 
 
   return 12;  // reply length
   return 12;  // reply length
@@ -448,8 +475,13 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
     command += 3;
     command += 3;
   }
   }
 
 
+  // first, see if this is a custom-handled CLI command (ie. in main.cpp)
+  if (handleCustomCommand(sender_timestamp, command, reply)) {
+    return;   // command has been handled
+  }
+
   // handle sensor-specific CLI commands
   // handle sensor-specific CLI commands
-  if (memcmp(command, "setperm ", 8) == 0) {   // format:  setperm {pubkey-hex} {permissions-int16}
+  if (memcmp(command, "setperm ", 8) == 0) {   // format:  setperm {pubkey-hex} {permissions-int8}
     char* hex = &command[8];
     char* hex = &command[8];
     char* sp = strchr(hex, ' ');   // look for separator char
     char* sp = strchr(hex, ' ');   // look for separator char
     if (sp == NULL || sp - hex != PUB_KEY_SIZE*2) {
     if (sp == NULL || sp - hex != PUB_KEY_SIZE*2) {
@@ -459,20 +491,21 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
 
 
       uint8_t pubkey[PUB_KEY_SIZE];
       uint8_t pubkey[PUB_KEY_SIZE];
       if (mesh::Utils::fromHex(pubkey, PUB_KEY_SIZE, hex)) {
       if (mesh::Utils::fromHex(pubkey, PUB_KEY_SIZE, hex)) {
-        uint16_t perms = atoi(sp);
+        uint8_t perms = atoi(sp);
         applyContactPermissions(pubkey, perms);
         applyContactPermissions(pubkey, perms);
         strcpy(reply, "OK");
         strcpy(reply, "OK");
       } else {
       } else {
         strcpy(reply, "Err - bad pubkey");
         strcpy(reply, "Err - bad pubkey");
       }
       }
     }
     }
-  } else if (sender_timestamp == 0 && strcmp(command, "getperm") == 0) {
-    Serial.println("Permissions:");
+  } else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
+    Serial.println("ACL:");
     for (int i = 0; i < num_contacts; i++) {
     for (int i = 0; i < num_contacts; i++) {
       auto c = &contacts[i];
       auto c = &contacts[i];
 
 
+      Serial.printf("%02X ", c->permissions);
       mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
       mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
-      Serial.printf(" %04X\n", c->permissions);
+      Serial.printf("\n");
     }
     }
     reply[0] = 0;
     reply[0] = 0;
   } else {
   } else {
@@ -629,6 +662,20 @@ bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint
   return false;
   return false;
 }
 }
 
 
+void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
+  if (num_alert_tasks > 0) {
+    auto t = alert_tasks[0];   // check current alert task
+    for (int i = 0; i < t->attempt; i++) {
+      if (ack_crc == t->expected_acks[i]) {   // matching ACK!
+        t->attempt = 4;  // signal to move to next contact
+        t->send_expiry = 0;
+        packet->markDoNotRetransmit();   // ACK was for this node, so don't retransmit
+        return;
+      }
+    }
+  }
+}
+
 SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
 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),
      : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
       _cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
       _cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
@@ -637,6 +684,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
   next_local_advert = next_flood_advert = 0;
   next_local_advert = next_flood_advert = 0;
   dirty_contacts_expiry = 0;
   dirty_contacts_expiry = 0;
   last_read_time = 0;
   last_read_time = 0;
+  num_alert_tasks = 0;
 
 
   // defaults
   // defaults
   memset(&_prefs, 0, sizeof(_prefs));
   memset(&_prefs, 0, sizeof(_prefs));
@@ -736,7 +784,14 @@ float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
 }
 }
 
 
 bool  SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
 bool  SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
-  return false;   // TODO
+  if (channel == TELEM_CHANNEL_SELF) {
+    lat = sensors.node_lat;
+    lon = sensors.node_lon;
+    alt = sensors.node_altitude;
+    return true;
+  }
+  // REVISIT: custom GPS channels??
+  return false;
 }
 }
 
 
 void SensorMesh::loop() {
 void SensorMesh::loop() {
@@ -767,6 +822,42 @@ void SensorMesh::loop() {
     last_read_time = curr;
     last_read_time = curr;
   }
   }
 
 
+  // check the alert send queue
+  if (num_alert_tasks > 0) {
+    auto t = alert_tasks[0];   // process head of queue
+
+    if (millisHasNowPassed(t->send_expiry)) {  // next send needed?
+      if (t->attempt >= 4) {  // max attempts reached, try next contact
+        t->curr_contact_idx++;
+        if (t->curr_contact_idx >= num_contacts) {  // no more contacts to try?
+          num_alert_tasks--;   // remove t from queue
+          for (int i = 0; i < num_alert_tasks; i++) {
+            alert_tasks[i] = alert_tasks[i + 1];
+          }
+        } else {
+          auto c = &contacts[t->curr_contact_idx];
+          uint16_t pri_mask = (t->pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO;
+
+          if (c->permissions & pri_mask) {   // contact wants alert
+            // reset attempts
+            t->attempt = (t->pri == LOW_PRI_ALERT) ? 3 : 0;   // Low pri alerts, start at attempt #3 (ie. only make ONE attempt)
+            t->timestamp = getRTCClock()->getCurrentTimeUnique();   // need unique timestamp per contact
+
+            sendAlert(c, t);  // NOTE: modifies attempt, expected_acks[] and send_expiry
+          } else {
+            // next contact tested in next ::loop()
+          }
+        }
+      } else if (t->curr_contact_idx < num_contacts) {
+        auto c = &contacts[t->curr_contact_idx];   // send next attempt
+        sendAlert(c, t);  // NOTE: modifies attempt, expected_acks[] and send_expiry
+      } else {
+        // contact list has likely been modified while waiting for alert ACK, cancel this task
+        t->attempt = 4;   // next ::loop() will remove t from queue
+      }
+    }
+  }
+
   // is there are pending dirty contacts write needed?
   // is there are pending dirty contacts write needed?
   if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
   if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
     saveContacts();
     saveContacts();

+ 36 - 19
examples/simple_sensor/SensorMesh.h

@@ -23,22 +23,29 @@
 #include <RTClib.h>
 #include <RTClib.h>
 #include <target.h>
 #include <target.h>
 
 
-#define PERM_IS_ADMIN          0x8000
-#define PERM_GET_TELEMETRY     0x0001
-#define PERM_GET_MIN_MAX_AVG   0x0002
-#define PERM_RECV_ALERTS_LO    0x0100   // low priority alerts
-#define PERM_RECV_ALERTS_HI    0x0200   // high priority alerts
+#define PERM_ACL_ROLE_MASK     3   // lower 2 bits
+#define PERM_ACL_GUEST         0
+#define PERM_ACL_LEVEL1        1
+#define PERM_ACL_LEVEL2        2
+#define PERM_ACL_LEVEL3        3   // admin
+
+#define PERM_GET_TELEMETRY     (1 << 2)
+#define PERM_GET_OTHER_STATS   (1 << 3)
+#define PERM_RESERVED1         (1 << 4)
+#define PERM_RESERVED2         (1 << 5)
+#define PERM_RECV_ALERTS_LO    (1 << 6)   // low priority alerts
+#define PERM_RECV_ALERTS_HI    (1 << 7)   // high priority alerts
 
 
 struct ContactInfo {
 struct ContactInfo {
   mesh::Identity id;
   mesh::Identity id;
-  uint16_t permissions;
+  uint8_t permissions;
   int8_t out_path_len;
   int8_t out_path_len;
   uint8_t out_path[MAX_PATH_SIZE];
   uint8_t out_path[MAX_PATH_SIZE];
   uint8_t shared_secret[PUB_KEY_SIZE];
   uint8_t shared_secret[PUB_KEY_SIZE];
   uint32_t last_timestamp;   // by THEIR clock  (transient)
   uint32_t last_timestamp;   // by THEIR clock  (transient)
   uint32_t last_activity;    // by OUR clock    (transient)
   uint32_t last_activity;    // by OUR clock    (transient)
 
 
-  bool isAdmin() const { return (permissions & PERM_IS_ADMIN) != 0; }
+  bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_LEVEL3; }
 };
 };
 
 
 #ifndef FIRMWARE_BUILD_DATE
 #ifndef FIRMWARE_BUILD_DATE
@@ -51,11 +58,10 @@ struct ContactInfo {
 
 
 #define FIRMWARE_ROLE "sensor"
 #define FIRMWARE_ROLE "sensor"
 
 
-#ifndef MAX_CONTACTS
-  #define MAX_CONTACTS           32
-#endif
+#define MAX_CONTACTS           20
 
 
-#define MAX_SEARCH_RESULTS   8
+#define MAX_SEARCH_RESULTS      8
+#define MAX_CONCURRENT_ALERTS   4
 
 
 class SensorMesh : public mesh::Mesh, public CommonCLICallbacks {
 class SensorMesh : public mesh::Mesh, public CommonCLICallbacks {
 public:
 public:
@@ -99,17 +105,25 @@ protected:
   bool  getGPS(uint8_t channel, float& lat, float& lon, float& alt);
   bool  getGPS(uint8_t channel, float& lat, float& lon, float& alt);
 
 
   // alerts
   // alerts
-  struct Trigger {
-    bool triggered;
-    uint32_t time;
+  enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT };
 
 
-    Trigger() { triggered = false; time = 0; }
+  struct Trigger {
+    uint32_t timestamp;
+    AlertPriority pri;
+    uint32_t expected_acks[4];
+    int8_t   curr_contact_idx;
+    uint8_t  attempt;
+    unsigned long send_expiry;
+    char text[MAX_PACKET_PAYLOAD];
+
+    Trigger() { text[0] = 0; }
+    bool isTriggered() const { return text[0] != 0; }
   };
   };
-  enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT };
   void alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text);
   void alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text);
 
 
   virtual void onSensorDataRead() = 0;   // for app to implement
   virtual void onSensorDataRead() = 0;   // for app to implement
   virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0;  // for app to implement
   virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0;  // for app to implement
+  virtual bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) { return false; }
 
 
   // Mesh overrides
   // Mesh overrides
   float getAirtimeBudgetFactor() const override;
   float getAirtimeBudgetFactor() const override;
@@ -124,6 +138,7 @@ protected:
   void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
   void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
   void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
   void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
   bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
   bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
+  void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
 
 
 private:
 private:
   FILESYSTEM* _fs;
   FILESYSTEM* _fs;
@@ -137,15 +152,17 @@ private:
   CayenneLPP telemetry;
   CayenneLPP telemetry;
   uint32_t last_read_time;
   uint32_t last_read_time;
   int matching_peer_indexes[MAX_SEARCH_RESULTS];
   int matching_peer_indexes[MAX_SEARCH_RESULTS];
+  int num_alert_tasks;
+  Trigger* alert_tasks[MAX_CONCURRENT_ALERTS];
 
 
   void loadContacts();
   void loadContacts();
   void saveContacts();
   void saveContacts();
   uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
   uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
-  uint8_t handleRequest(uint16_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
+  uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
   mesh::Packet* createSelfAdvert();
   mesh::Packet* createSelfAdvert();
   ContactInfo* putContact(const mesh::Identity& id);
   ContactInfo* putContact(const mesh::Identity& id);
-  void applyContactPermissions(const uint8_t* pubkey, uint16_t perms);
+  void applyContactPermissions(const uint8_t* pubkey, uint8_t perms);
 
 
-  void sendAlert(AlertPriority pri, const char* text);
+  void sendAlert(ContactInfo* c, Trigger* t);
 
 
 };
 };

+ 2 - 2
examples/simple_sensor/TimeSeriesData.cpp

@@ -10,7 +10,7 @@ void TimeSeriesData::recordData(mesh::RTCClock* clock, float value) {
   }
   }
 }
 }
 
 
-void TimeSeriesData::calcDataMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const {
+void TimeSeriesData::calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const {
   int i = next, n = num_slots;
   int i = next, n = num_slots;
   uint32_t ago = clock->getCurrentTime() - last_timestamp;
   uint32_t ago = clock->getCurrentTime() - last_timestamp;
   int num_values = 0;
   int num_values = 0;
@@ -40,6 +40,6 @@ void TimeSeriesData::calcDataMinMaxAvg(mesh::RTCClock* clock, uint32_t start_sec
   if (num_values > 0) {
   if (num_values > 0) {
     dest->_avg = total / num_values;
     dest->_avg = total / num_values;
   } else {
   } else {
-    dest->_avg = NAN;
+    dest->_max = dest->_min = dest->_avg = NAN;
   }
   }
 }
 }

+ 1 - 1
examples/simple_sensor/TimeSeriesData.h

@@ -24,6 +24,6 @@ public:
   }
   }
 
 
   void recordData(mesh::RTCClock* clock, float value);
   void recordData(mesh::RTCClock* clock, float value);
-  void calcDataMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const;
+  void calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const;
 };
 };
 
 

+ 12 - 3
examples/simple_sensor/main.cpp

@@ -15,20 +15,29 @@ public:
 
 
 protected:
 protected:
   /* ========================== custom logic here ========================== */
   /* ========================== custom logic here ========================== */
-  Trigger low_batt;
+  Trigger low_batt, critical_batt;
   TimeSeriesData  battery_data;
   TimeSeriesData  battery_data;
 
 
   void onSensorDataRead() override {
   void onSensorDataRead() override {
     float batt_voltage = getVoltage(TELEM_CHANNEL_SELF);
     float batt_voltage = getVoltage(TELEM_CHANNEL_SELF);
 
 
     battery_data.recordData(getRTCClock(), batt_voltage);   // record battery
     battery_data.recordData(getRTCClock(), batt_voltage);   // record battery
-    alertIf(batt_voltage < 3.4f, low_batt, HIGH_PRI_ALERT, "Battery low!");
+    alertIf(batt_voltage < 3.4f, critical_batt, HIGH_PRI_ALERT, "Battery is critical!");
+    alertIf(batt_voltage < 3.6f, low_batt, LOW_PRI_ALERT, "Battery is low");
   }
   }
 
 
   int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override {
   int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override {
-    battery_data.calcDataMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE);
+    battery_data.calcMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE);
     return 1;
     return 1;
   }
   }
+
+  bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) override {
+    if (strcmp(command, "magic") == 0) {    // example 'custom' command handling
+      strcpy(reply, "**Magic now done**");
+      return true;   // handled
+    }
+    return false;  // not handled
+  }
   /* ======================================================================= */
   /* ======================================================================= */
 };
 };
 
 

+ 0 - 8
platformio.ini

@@ -77,14 +77,6 @@ build_flags = ${arduino_base.build_flags}
   -D NRF52_PLATFORM
   -D NRF52_PLATFORM
   -D LFS_NO_ASSERT=1
   -D LFS_NO_ASSERT=1
 
 
-[nrf52840_base]
-extends = nrf52_base
-build_flags = ${nrf52_base.build_flags}
-lib_deps =
-  ${nrf52_base.lib_deps}
-  rweather/Crypto @ ^0.4.0
-  https://github.com/adafruit/Adafruit_nRF52_Arduino
-
 ; ----------------- RP2040 ---------------------
 ; ----------------- RP2040 ---------------------
 
 
 [rp2040_base]
 [rp2040_base]

+ 55 - 22
src/helpers/BaseChatMesh.cpp

@@ -402,22 +402,52 @@ bool BaseChatMesh::importContact(const uint8_t src_buf[], uint8_t len) {
 }
 }
 
 
 int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout) {
 int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout) {
-  int tlen;
-  uint8_t temp[24];
-  uint32_t now = getRTCClock()->getCurrentTimeUnique();
-  memcpy(temp, &now, 4);   // mostly an extra blob to help make packet_hash unique
-  if (recipient.type == ADV_TYPE_ROOM) {
-    memcpy(&temp[4], &recipient.sync_since, 4);
-    int len = strlen(password); if (len > 15) len = 15;  // max 15 chars currently
-    memcpy(&temp[8], password, len);
-    tlen = 8 + len;
-  } else {
-    int len = strlen(password); if (len > 15) len = 15;  // max 15 chars currently
-    memcpy(&temp[4], password, len);
-    tlen = 4 + len;
+  mesh::Packet* pkt;
+  {
+    int tlen;
+    uint8_t temp[24];
+    uint32_t now = getRTCClock()->getCurrentTimeUnique();
+    memcpy(temp, &now, 4);   // mostly an extra blob to help make packet_hash unique
+    if (recipient.type == ADV_TYPE_ROOM) {
+      memcpy(&temp[4], &recipient.sync_since, 4);
+      int len = strlen(password); if (len > 15) len = 15;  // max 15 chars currently
+      memcpy(&temp[8], password, len);
+      tlen = 8 + len;
+    } else {
+      int len = strlen(password); if (len > 15) len = 15;  // max 15 chars currently
+      memcpy(&temp[4], password, len);
+      tlen = 4 + len;
+    }
+
+    pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen);
+  }
+  if (pkt) {
+    uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
+    if (recipient.out_path_len < 0) {
+      sendFlood(pkt);
+      est_timeout = calcFloodTimeoutMillisFor(t);
+      return MSG_SEND_SENT_FLOOD;
+    } else {
+      sendDirect(pkt, recipient.out_path, recipient.out_path_len);
+      est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
+      return MSG_SEND_SENT_DIRECT;
+    }
   }
   }
+  return MSG_SEND_FAILED;
+}
+
+int  BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout) {
+  if (data_len > MAX_PACKET_PAYLOAD - 16) return MSG_SEND_FAILED;
 
 
-  auto pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen);
+  mesh::Packet* pkt;
+  {
+    uint8_t temp[MAX_PACKET_PAYLOAD];
+    tag = getRTCClock()->getCurrentTimeUnique();
+    memcpy(temp, &tag, 4);   // mostly an extra blob to help make packet_hash unique
+    memcpy(&temp[4], req_data, data_len);
+
+    pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, 4 + data_len);
+  }
   if (pkt) {
   if (pkt) {
     uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
     uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
     if (recipient.out_path_len < 0) {
     if (recipient.out_path_len < 0) {
@@ -434,14 +464,17 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
 }
 }
 
 
 int  BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout) {
 int  BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout) {
-  uint8_t temp[13];
-  tag = getRTCClock()->getCurrentTimeUnique();
-  memcpy(temp, &tag, 4);   // mostly an extra blob to help make packet_hash unique
-  temp[4] = req_type;
-  memset(&temp[5], 0, 4);  // reserved (possibly for 'since' param)
-  getRNG()->random(&temp[9], 4);   // random blob to help make packet-hash unique
-
-  auto pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp));
+  mesh::Packet* pkt;
+  {
+    uint8_t temp[13];
+    tag = getRTCClock()->getCurrentTimeUnique();
+    memcpy(temp, &tag, 4);   // mostly an extra blob to help make packet_hash unique
+    temp[4] = req_type;
+    memset(&temp[5], 0, 4);  // reserved (possibly for 'since' param)
+    getRNG()->random(&temp[9], 4);   // random blob to help make packet-hash unique
+
+    pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp));
+  }
   if (pkt) {
   if (pkt) {
     uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
     uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
     if (recipient.out_path_len < 0) {
     if (recipient.out_path_len < 0) {

+ 1 - 0
src/helpers/BaseChatMesh.h

@@ -135,6 +135,7 @@ public:
   bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len);
   bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len);
   int  sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
   int  sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
   int  sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout);
   int  sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout);
+  int  sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout);
   bool shareContactZeroHop(const ContactInfo& contact);
   bool shareContactZeroHop(const ContactInfo& contact);
   uint8_t exportContact(const ContactInfo& contact, uint8_t dest_buf[]);
   uint8_t exportContact(const ContactInfo& contact, uint8_t dest_buf[]);
   bool importContact(const uint8_t src_buf[], uint8_t len);
   bool importContact(const uint8_t src_buf[], uint8_t len);

+ 11 - 4
variants/heltec_wireless_paper/target.cpp

@@ -1,11 +1,14 @@
 #include "target.h"
 #include "target.h"
-
 #include <Arduino.h>
 #include <Arduino.h>
 
 
 HeltecV3Board board;
 HeltecV3Board board;
 
 
-static SPIClass spi;
-RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
+#if defined(P_LORA_SCLK)
+  static SPIClass spi;
+  RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
+#else
+  RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
+#endif
 
 
 WRAPPER_CLASS radio_driver(radio, board);
 WRAPPER_CLASS radio_driver(radio, board);
 
 
@@ -21,7 +24,11 @@ DISPLAY_CLASS display;
 bool radio_init() {
 bool radio_init() {
   fallback_clock.begin();
   fallback_clock.begin();
   rtc_clock.begin(Wire);
   rtc_clock.begin(Wire);
+#if defined(P_LORA_SCLK)
   return radio.std_init(&spi);
   return radio.std_init(&spi);
+#else
+  return radio.std_init();
+#endif
 }
 }
 
 
 uint32_t radio_get_rng_seed() {
 uint32_t radio_get_rng_seed() {
@@ -42,4 +49,4 @@ void radio_set_tx_power(uint8_t dbm) {
 mesh::LocalIdentity radio_new_identity() {
 mesh::LocalIdentity radio_new_identity() {
   RadioNoiseListener rng(radio);
   RadioNoiseListener rng(radio);
   return mesh::LocalIdentity(&rng); // create new random identity
   return mesh::LocalIdentity(&rng); // create new random identity
-}
+}

+ 5 - 5
variants/rak4631/platformio.ini

@@ -1,9 +1,9 @@
 [rak4631]
 [rak4631]
-extends = nrf52840_base
+extends = nrf52_base
 platform = https://github.com/maxgerhardt/platform-nordicnrf52.git#rak
 platform = https://github.com/maxgerhardt/platform-nordicnrf52.git#rak
 board = wiscore_rak4631
 board = wiscore_rak4631
 board_check = true
 board_check = true
-build_flags = ${nrf52840_base.build_flags}
+build_flags = ${nrf52_base.build_flags}
   -I variants/rak4631
   -I variants/rak4631
   -D RAK_4631
   -D RAK_4631
   -D PIN_BOARD_SCL=14
   -D PIN_BOARD_SCL=14
@@ -14,10 +14,10 @@ build_flags = ${nrf52840_base.build_flags}
   -D LORA_TX_POWER=22
   -D LORA_TX_POWER=22
   -D SX126X_CURRENT_LIMIT=140
   -D SX126X_CURRENT_LIMIT=140
   -D SX126X_RX_BOOSTED_GAIN=1
   -D SX126X_RX_BOOSTED_GAIN=1
-build_src_filter = ${nrf52840_base.build_src_filter}
+build_src_filter = ${nrf52_base.build_src_filter}
   +<../variants/rak4631>
   +<../variants/rak4631>
 lib_deps =
 lib_deps =
-  ${nrf52840_base.lib_deps}
+  ${nrf52_base.lib_deps}
   adafruit/Adafruit SSD1306 @ ^2.5.13
   adafruit/Adafruit SSD1306 @ ^2.5.13
   stevemarple/MicroNMEA @ ^2.0.6
   stevemarple/MicroNMEA @ ^2.0.6
 
 
@@ -55,7 +55,7 @@ build_flags =
 build_src_filter = ${rak4631.build_src_filter}
 build_src_filter = ${rak4631.build_src_filter}
   +<helpers/ui/SSD1306Display.cpp>
   +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
   +<../examples/simple_repeater>
-lib_deps = 
+lib_deps =
   ${rak4631.lib_deps}
   ${rak4631.lib_deps}
   sparkfun/SparkFun u-blox GNSS Arduino Library @ ^2.2.27
   sparkfun/SparkFun u-blox GNSS Arduino Library @ ^2.2.27
   https://github.com/boschsensortec/Bosch-BSEC2-Library
   https://github.com/boschsensortec/Bosch-BSEC2-Library

+ 2 - 0
variants/sensecap_solar/variant.cpp

@@ -64,6 +64,8 @@ void initVariant() {
     pinMode(LED_BLUE, OUTPUT);
     pinMode(LED_BLUE, OUTPUT);
     digitalWrite(LED_BLUE, LOW);
     digitalWrite(LED_BLUE, LOW);
 
 
+    /* disable gps until we actually support it.
     pinMode(GPS_EN, OUTPUT);
     pinMode(GPS_EN, OUTPUT);
     digitalWrite(GPS_EN, HIGH);
     digitalWrite(GPS_EN, HIGH);
+    */
 }
 }