소스 검색

* repeater: refactored 'region' CLI commands -> CommonCLI
* room server: added RegionMap, and new CommonCLI wiring, default_scope handling
* sensor: only minimal RegionMap wiring. Still needs work to handle default-scope

Scott Powell 3 달 전
부모
커밋
4131a455a2

+ 22 - 120
examples/simple_repeater/MyMesh.cpp

@@ -844,7 +844,9 @@ void MyMesh::sendNodeDiscoverReq() {
 MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
                mesh::RTCClock &rtc, mesh::MeshTables &tables)
     : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
-      _cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_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),
       discover_limiter(4, 120),  // max 4 every 2 minutes
       anon_limiter(4, 180)   // max 4 every 3 minutes
 #if defined(WITH_RS232_BRIDGE)
@@ -1112,6 +1114,25 @@ void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) {
 #endif
 }
 
+void MyMesh::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 MyMesh::saveRegions() {
+  return region_map.save(_fs);
+}
+
+void MyMesh::onDefaultRegionChanged(const RegionEntry* r) {
+  if (r) {
+    region_map.getTransportKeysFor(*r, &default_scope, 1);
+  } else {
+    memset(default_scope.key, 0, sizeof(default_scope.key));
+  }
+}
+
 void MyMesh::formatStatsReply(char *reply) {
   StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
 }
@@ -1221,125 +1242,6 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
       Serial.printf("\n");
     }
     reply[0] = 0;
-  } else if (memcmp(command, "region", 6) == 0) {
-    reply[0] = 0;
-
-    const char* parts[4];
-    int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
-    if (n == 1) {
-      region_map.exportTo(reply, 160);
-    } else if (n >= 2 && strcmp(parts[1], "load") == 0) {
-      temp_map.resetFrom(region_map);   // rebuild regions in a temp instance
-      memset(load_stack, 0, sizeof(load_stack));
-      load_stack[0] = &temp_map.getWildcard();
-      region_load_active = true;
-    } else if (n >= 2 && strcmp(parts[1], "save") == 0) {
-      _prefs.discovery_mod_timestamp = rtc_clock.getCurrentTime();   // this node is now 'modified' (for discovery info)
-      savePrefs();
-      bool success = region_map.save(_fs);
-      strcpy(reply, success ? "OK" : "Err - save failed");
-    } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) {
-      auto region = region_map.findByNamePrefix(parts[2]);
-      if (region) {
-        region->flags &= ~REGION_DENY_FLOOD;
-        strcpy(reply, "OK");
-      } else {
-        strcpy(reply, "Err - unknown region");
-      }
-    } else if (n >= 3 && strcmp(parts[1], "denyf") == 0) {
-      auto region = region_map.findByNamePrefix(parts[2]);
-      if (region) {
-        region->flags |= REGION_DENY_FLOOD;
-        strcpy(reply, "OK");
-      } else {
-        strcpy(reply, "Err - unknown region");
-      }
-    } else if (n >= 3 && strcmp(parts[1], "get") == 0) {
-      auto region = region_map.findByNamePrefix(parts[2]);
-      if (region) {
-        auto parent = region_map.findById(region->parent);
-        if (parent && parent->id != 0) {
-          sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
-        } else {
-          sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
-        }
-      } else {
-        strcpy(reply, "Err - unknown region");
-      }
-    } else if (n >= 3 && strcmp(parts[1], "home") == 0) {
-      auto home = region_map.findByNamePrefix(parts[2]);
-      if (home) {
-        region_map.setHomeRegion(home);
-        sprintf(reply, " home is now %s", home->name);
-      } else {
-        strcpy(reply, "Err - unknown region");
-      }
-    } else if (n == 2 && strcmp(parts[1], "home") == 0) {
-      auto home = region_map.getHomeRegion();
-      sprintf(reply, " home is %s", home ? home->name : "*");
-    } else if (n >= 3 && strcmp(parts[1], "default") == 0) {
-      if (strcmp(parts[2], "<null>") == 0) {
-        region_map.setDefaultRegion(NULL);
-        memset(default_scope.key, 0, sizeof(default_scope.key));
-        sprintf(reply, " default scope is now <null>");
-      } else {
-        auto def = region_map.findByNamePrefix(parts[2]);
-        if (def) {
-          region_map.setDefaultRegion(def);
-          region_map.getTransportKeysFor(*def, &default_scope, 1);
-          sprintf(reply, " default scope is now %s", def->name);
-        } else {
-          strcpy(reply, "Err - unknown region");
-        }
-      }
-    } else if (n == 2 && strcmp(parts[1], "default") == 0) {
-      auto def = region_map.getDefaultRegion();
-      sprintf(reply, " default scope is %s", def ? def->name : "<null>");
-    } else if (n >= 3 && strcmp(parts[1], "put") == 0) {
-      auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : &region_map.getWildcard();
-      if (parent == NULL) {
-        strcpy(reply, "Err - unknown parent");
-      } else {
-        auto region = region_map.putRegion(parts[2], parent->id);
-        if (region == NULL) {
-          strcpy(reply, "Err - unable to put");
-        } else {
-          strcpy(reply, "OK");
-        }
-      }
-    } else if (n >= 3 && strcmp(parts[1], "remove") == 0) {
-      auto region = region_map.findByName(parts[2]);
-      if (region) {
-        if (region_map.removeRegion(*region)) {
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Err - not empty");
-        }
-      } else {
-        strcpy(reply, "Err - not found");
-      }
-    } else if (n >= 3 && strcmp(parts[1], "list") == 0) {
-      uint8_t mask = 0;
-      bool invert = false;
-      
-      if (strcmp(parts[2], "allowed") == 0) {
-        mask = REGION_DENY_FLOOD;
-        invert = false;  // list regions that DON'T have DENY flag
-      } else if (strcmp(parts[2], "denied") == 0) {
-        mask = REGION_DENY_FLOOD;
-        invert = true;   // list regions that DO have DENY flag
-      } else {
-        strcpy(reply, "Err - use 'allowed' or 'denied'");
-        return;
-      }
-      
-      int len = region_map.exportNamesTo(reply, 160, mask, invert);
-      if (len == 0) {
-        strcpy(reply, "-none-");
-      }
-    } else {
-      strcpy(reply, "Err - ??");
-    }
   } else if (memcmp(command, "discover.neighbors", 18) == 0) {
     const char* sub = command + 18;
     while (*sub == ' ') sub++;

+ 6 - 0
examples/simple_repeater/MyMesh.h

@@ -193,6 +193,8 @@ public:
   }
 
   void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
+
+  // CommonCLICallbacks
   void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
   bool formatFileSystem() override;
   void sendSelfAdvertisement(int delay_millis, bool flood) override;
@@ -212,11 +214,15 @@ public:
   void formatStatsReply(char *reply) override;
   void formatRadioStatsReply(char *reply) override;
   void formatPacketStatsReply(char *reply) override;
+  void startRegionsLoad() override;
+  bool saveRegions() override;
+  void onDefaultRegionChanged(const RegionEntry* r) override;
 
   mesh::LocalIdentity& getSelfId() override { return self_id; }
 
   void saveIdentity(const mesh::LocalIdentity& new_id) override;
   void clearStats() override;
+
   void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
   void loop();
 

+ 131 - 10
examples/simple_room_server/MyMesh.cpp

@@ -75,7 +75,7 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) {
   if (reply) {
     if (client->out_path_len == OUT_PATH_UNKNOWN) {
       unsigned long delay_millis = 0;
-      sendFlood(reply, delay_millis, _prefs.path_hash_mode + 1);
+      sendFloodScoped(default_scope, reply, delay_millis, _prefs.path_hash_mode + 1); // REVISIT
       client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD);
     } else {
       sendDirect(reply, client->out_path, client->out_path_len);
@@ -286,6 +286,23 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
   return true;
 }
 
+bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
+  // just try to determine region for packet (apply later in allowPacketForward())
+  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 =  &region_map.getWildcard();
+    }
+  } else {
+    recv_pkt_region = NULL;
+  }
+  // do normal processing
+  return false;
+}
+
 void MyMesh::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
@@ -361,14 +378,14 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
       // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
       mesh::Packet *path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len,
                                             PAYLOAD_TYPE_RESPONSE, reply_data, 13);
-      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, client->shared_secret, reply_data, 13);
       if (reply) {
         if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
           sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
         } else {
-          sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+          sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
         }
       }
     }
@@ -458,7 +475,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
       if (send_ack) {
         if (client->out_path_len == OUT_PATH_UNKNOWN) {
           mesh::Packet *ack = createAck(ack_hash);
-          if (ack) sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize());
+          if (ack) sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize());
           delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
         } else {
           uint32_t d = TXT_ACK_DELAY;
@@ -491,7 +508,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
         auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
         if (reply) {
           if (client->out_path_len == OUT_PATH_UNKNOWN) {
-            sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+            sendFloodReply(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize());
           } else {
             sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY);
           }
@@ -546,14 +563,14 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
             // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
             mesh::Packet *path = createPathReturn(client->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, client->id, secret, reply_data, reply_len);
             if (reply) {
               if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
                 sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
               } else {
-                sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
+                sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
               }
             }
           }
@@ -595,12 +612,16 @@ void MyMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) {
 MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
                mesh::RTCClock &rtc, mesh::MeshTables &tables)
     : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
-      _cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
+      region_map(key_store), temp_map(key_store),
+      _cli(board, rtc, sensors, region_map, acl, &_prefs, this),
+      telemetry(MAX_PACKET_PAYLOAD - 4)
+{
   last_millis = 0;
   uptime_millis = 0;
   next_local_advert = next_flood_advert = 0;
   dirty_contacts_expiry = 0;
   _logging = false;
+  region_load_active = false;
   set_radio_at = revert_radio_at = 0;
 
   // defaults
@@ -637,6 +658,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
   next_push = 0;
   memset(posts, 0, sizeof(posts));
   _num_posted = _num_post_pushes = 0;
+
+  memset(default_scope.key, 0, sizeof(default_scope.key));
 }
 
 void MyMesh::begin(FILESYSTEM *fs) {
@@ -646,6 +669,27 @@ void MyMesh::begin(FILESYSTEM *fs) {
   _cli.loadPrefs(_fs);
 
   acl.load(_fs, self_id);
+  region_map.load(_fs);
+
+  // establish default-scope
+  {
+    RegionEntry* r = region_map.getDefaultRegion();
+    if (r) {
+      region_map.getTransportKeysFor(*r, &default_scope, 1);
+    } else {
+#ifdef DEFAULT_FLOOD_SCOPE_NAME
+      r = region_map.findByName(DEFAULT_FLOOD_SCOPE_NAME);
+      if (r == NULL) {
+        r = region_map.putRegion(DEFAULT_FLOOD_SCOPE_NAME, 0);  // auto-create the default scope region
+        if (r) { r->flags = 0; }   // Allow-flood
+      }
+      if (r) {
+        region_map.setDefaultRegion(r);
+        region_map.getTransportKeysFor(*r, &default_scope, 1);
+      }
+#endif
+    }
+  }
 
   radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
   radio_set_tx_power(_prefs.tx_power_dbm);
@@ -660,6 +704,30 @@ void MyMesh::begin(FILESYSTEM *fs) {
 #endif
 }
 
+void MyMesh::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 MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
+  if (recv_pkt_region) {  // if _request_ packet scope is known, send reply with same scope
+    TransportKey scope;
+    if (region_map.getTransportKeysFor(*recv_pkt_region, &scope, 1) == 0) {
+      sendFloodScoped(default_scope, packet, delay_millis, path_hash_size);
+    } else {
+      sendFloodScoped(scope, packet, delay_millis, path_hash_size);
+    }
+  } else {
+    sendFloodScoped(default_scope, packet, delay_millis, path_hash_size);
+  }
+}
+
 void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
   set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
   pending_freq = freq;
@@ -687,7 +755,7 @@ void MyMesh::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);
     }
@@ -744,6 +812,25 @@ void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
   store.save("_main", new_id);
 }
 
+void MyMesh::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 MyMesh::saveRegions() {
+  return region_map.save(_fs);
+}
+
+void MyMesh::onDefaultRegionChanged(const RegionEntry* r) {
+  if (r) {
+    region_map.getTransportKeysFor(*r, &default_scope, 1);
+  } else {
+    memset(default_scope.key, 0, sizeof(default_scope.key));
+  }
+}
+
 void MyMesh::clearStats() {
   radio_driver.resetStats();
   resetStats();
@@ -764,6 +851,40 @@ void MyMesh::formatPacketStatsReply(char *reply) {
 }
 
 void MyMesh::handleCommand(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
 
@@ -865,7 +986,7 @@ void MyMesh::loop() {
   if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
     mesh::Packet *pkt = createSelfAdvert();
     uint32_t 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)

+ 17 - 0
examples/simple_room_server/MyMesh.h

@@ -20,6 +20,7 @@
 #include <helpers/CommonCLI.h>
 #include <helpers/StatsFormatHelper.h>
 #include <helpers/ClientACL.h>
+#include <helpers/RegionMap.h>
 #include <RTClib.h>
 #include <target.h>
 
@@ -93,7 +94,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   uint64_t uptime_millis;
   unsigned long next_local_advert, next_flood_advert;
   bool _logging;
+  bool region_load_active;
   NodePrefs _prefs;
+  TransportKeyStore key_store;
+  RegionMap region_map, temp_map;
   ClientACL acl;
   CommonCLI _cli;
   unsigned long dirty_contacts_expiry;
@@ -104,6 +108,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   int next_post_idx;
   PostInfo posts[MAX_UNSYNCED_POSTS];   // cyclic queue
   CayenneLPP telemetry;
+  RegionEntry* load_stack[8];
+  RegionEntry* recv_pkt_region;
+  TransportKey default_scope;
   unsigned long set_radio_at, revert_radio_at;
   float pending_freq;
   float pending_bw;
@@ -144,6 +151,8 @@ protected:
     return _prefs.multi_acks;
   }
 
+  bool filterRecvFloodPacket(mesh::Packet* pkt) override;
+
   bool allowPacketForward(const mesh::Packet* packet) 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 ;
@@ -158,6 +167,8 @@ protected:
   }
 #endif
 
+  void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
+
 public:
   MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
 
@@ -175,6 +186,9 @@ public:
     _cli.savePrefs(_fs);
   }
 
+  void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
+
+  // CommonCLICallbacks
   void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
   bool formatFileSystem() override;
   void sendSelfAdvertisement(int delay_millis, bool flood) override;
@@ -196,6 +210,9 @@ public:
   void formatStatsReply(char *reply) override;
   void formatRadioStatsReply(char *reply) override;
   void formatPacketStatsReply(char *reply) override;
+  void startRegionsLoad() override;
+  bool saveRegions() override;
+  void onDefaultRegionChanged(const RegionEntry* r) override;
 
   mesh::LocalIdentity& getSelfId() override { return self_id; }
 

+ 26 - 1
examples/simple_sensor/SensorMesh.cpp

@@ -696,7 +696,9 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
 
 SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
      : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
-      _cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
+      region_map(key_store),
+      _cli(board, rtc, sensors, region_map, acl, &_prefs, this),
+      telemetry(MAX_PACKET_PAYLOAD - 4)
 {
   next_local_advert = next_flood_advert = 0;
   dirty_contacts_expiry = 0;
@@ -729,6 +731,8 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
   _prefs.gps_enabled = 0;
   _prefs.gps_interval = 0;
   _prefs.advert_loc_policy = ADVERT_LOC_PREFS;
+
+  memset(default_scope.key, 0, sizeof(default_scope.key));
 }
 
 void SensorMesh::begin(FILESYSTEM* fs) {
@@ -738,6 +742,27 @@ void SensorMesh::begin(FILESYSTEM* fs) {
   _cli.loadPrefs(_fs);
 
   acl.load(_fs, self_id);
+  region_map.load(_fs);
+
+  // establish default-scope
+  {
+    RegionEntry* r = region_map.getDefaultRegion();
+    if (r) {
+      region_map.getTransportKeysFor(*r, &default_scope, 1);
+    } else {
+#ifdef DEFAULT_FLOOD_SCOPE_NAME
+      r = region_map.findByName(DEFAULT_FLOOD_SCOPE_NAME);
+      if (r == NULL) {
+        r = region_map.putRegion(DEFAULT_FLOOD_SCOPE_NAME, 0);  // auto-create the default scope region
+        if (r) { r->flags = 0; }   // Allow-flood
+      }
+      if (r) {
+        region_map.setDefaultRegion(r);
+        region_map.getTransportKeysFor(*r, &default_scope, 1);
+      }
+#endif
+    }
+  }
 
   radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
   radio_set_tx_power(_prefs.tx_power_dbm);

+ 4 - 0
examples/simple_sensor/SensorMesh.h

@@ -22,6 +22,7 @@
 #include <helpers/CommonCLI.h>
 #include <helpers/StatsFormatHelper.h>
 #include <helpers/ClientACL.h>
+#include <helpers/RegionMap.h>
 #include <RTClib.h>
 #include <target.h>
 
@@ -138,6 +139,9 @@ private:
   uint8_t reply_data[MAX_PACKET_PAYLOAD];
   unsigned long dirty_contacts_expiry;
   CayenneLPP telemetry;
+  TransportKeyStore key_store;
+  RegionMap region_map;
+  TransportKey default_scope;
   uint32_t last_read_time;
   int matching_peer_indexes[MAX_SEARCH_RESULTS];
   int num_alert_tasks;

+ 552 - 430
src/helpers/CommonCLI.cpp

@@ -207,7 +207,7 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
   }
 }
 
-void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) {
+void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
     if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) {
       _board->powerOff();  // doesn't return
     } else if (memcmp(command, "reboot", 6) == 0) {
@@ -289,437 +289,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
     } else if (memcmp(command, "clear stats", 11) == 0) {
       _callbacks->clearStats();
       strcpy(reply, "(OK - stats reset)");
-    /*
-     * GET commands
-     */
     } else if (memcmp(command, "get ", 4) == 0) {
-      const char* config = &command[4];
-      if (memcmp(config, "dutycycle", 9) == 0) {
-        float dc = 100.0f / (_prefs->airtime_factor + 1.0f);
-        int dc_int = (int)dc;
-        int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f);
-        sprintf(reply, "> %d.%d%%", dc_int, dc_frac);
-      } else if (memcmp(config, "af", 2) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
-      } else if (memcmp(config, "int.thresh", 10) == 0) {
-        sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
-      } else if (memcmp(config, "agc.reset.interval", 18) == 0) {
-        sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
-      } else if (memcmp(config, "multi.acks", 10) == 0) {
-        sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
-      } else if (memcmp(config, "allow.read.only", 15) == 0) {
-        sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
-      } else if (memcmp(config, "flood.advert.interval", 21) == 0) {
-        sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
-      } else if (memcmp(config, "advert.interval", 15) == 0) {
-        sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
-      } else if (memcmp(config, "guest.password", 14) == 0) {
-        sprintf(reply, "> %s", _prefs->guest_password);
-      } else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) {  // from serial command line only
-        uint8_t prv_key[PRV_KEY_SIZE];
-        int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
-        mesh::Utils::toHex(tmp, prv_key, len);
-        sprintf(reply, "> %s", tmp);
-      } else if (memcmp(config, "name", 4) == 0) {
-        sprintf(reply, "> %s", _prefs->node_name);
-      } else if (memcmp(config, "repeat", 6) == 0) {
-        sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on");
-      } else if (memcmp(config, "lat", 3) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
-      } else if (memcmp(config, "lon", 3) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
-#if defined(USE_SX1262) || defined(USE_SX1268)
-      } else if (memcmp(config, "radio.rxgain", 12) == 0) {
-        sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
-#endif
-      } else if (memcmp(config, "radio", 5) == 0) {
-        char freq[16], bw[16];
-        strcpy(freq, StrHelper::ftoa(_prefs->freq));
-        strcpy(bw, StrHelper::ftoa3(_prefs->bw));
-        sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
-      } else if (memcmp(config, "rxdelay", 7) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
-      } else if (memcmp(config, "txdelay", 7) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor));
-      } else if (memcmp(config, "flood.max", 9) == 0) {
-        sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
-      } else if (memcmp(config, "direct.txdelay", 14) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
-      } else if (memcmp(config, "owner.info", 10) == 0) {
-        *reply++ = '>';
-        *reply++ = ' ';
-        const char* sp = _prefs->owner_info;
-        while (*sp) {
-          *reply++ = (*sp == '\n') ? '|' : *sp;    // translate newline back to orig '|'
-          sp++;
-        }
-        *reply = 0;  // set null terminator
-      } else if (memcmp(config, "path.hash.mode", 14) == 0) {
-        sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode);
-      } else if (memcmp(config, "loop.detect", 11) == 0) {
-        if (_prefs->loop_detect == LOOP_DETECT_OFF) {
-          strcpy(reply, "> off");
-        } else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) {
-          strcpy(reply, "> minimal");
-        } else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) {
-          strcpy(reply, "> moderate");
-        } else {
-          strcpy(reply, "> strict");
-        }
-      } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
-        sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
-      } else if (memcmp(config, "freq", 4) == 0) {
-        sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
-      } else if (memcmp(config, "public.key", 10) == 0) {
-        strcpy(reply, "> ");
-        mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
-      } else if (memcmp(config, "role", 4) == 0) {
-        sprintf(reply, "> %s", _callbacks->getRole());
-      } else if (memcmp(config, "bridge.type", 11) == 0) {
-        sprintf(reply, "> %s",
-#ifdef WITH_RS232_BRIDGE
-                "rs232"
-#elif WITH_ESPNOW_BRIDGE
-                "espnow"
-#else
-                "none"
-#endif
-        );
-#ifdef WITH_BRIDGE
-      } else if (memcmp(config, "bridge.enabled", 14) == 0) {
-        sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off");
-      } else if (memcmp(config, "bridge.delay", 12) == 0) {
-        sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
-      } else if (memcmp(config, "bridge.source", 13) == 0) {
-        sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx");
-#endif
-#ifdef WITH_RS232_BRIDGE
-      } else if (memcmp(config, "bridge.baud", 11) == 0) {
-        sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
-#endif
-#ifdef WITH_ESPNOW_BRIDGE
-      } else if (memcmp(config, "bridge.channel", 14) == 0) {
-        sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
-      } else if (memcmp(config, "bridge.secret", 13) == 0) {
-        sprintf(reply, "> %s", _prefs->bridge_secret);
-#endif
-      } else if (memcmp(config, "bootloader.ver", 14) == 0) {
-      #ifdef NRF52_PLATFORM
-          char ver[32];
-          if (_board->getBootloaderVersion(ver, sizeof(ver))) {
-              sprintf(reply, "> %s", ver);
-          } else {
-              strcpy(reply, "> unknown");
-          }
-      #else
-          strcpy(reply, "ERROR: unsupported");
-      #endif
-      } else if (memcmp(config, "adc.multiplier", 14) == 0) {
-        float adc_mult = _board->getAdcMultiplier();
-        if (adc_mult == 0.0f) {
-          strcpy(reply, "Error: unsupported by this board");
-        } else {
-          sprintf(reply, "> %.3f", adc_mult);
-        }
-      // Power management commands
-      } else if (memcmp(config, "pwrmgt.support", 14) == 0) {
-#ifdef NRF52_POWER_MANAGEMENT
-        strcpy(reply, "> supported");
-#else
-        strcpy(reply, "> unsupported");
-#endif
-      } else if (memcmp(config, "pwrmgt.source", 13) == 0) {
-#ifdef NRF52_POWER_MANAGEMENT
-        strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
-#else
-        strcpy(reply, "ERROR: Power management not supported");
-#endif
-      } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
-#ifdef NRF52_POWER_MANAGEMENT
-        sprintf(reply, "> Reset: %s; Shutdown: %s",
-          _board->getResetReasonString(_board->getResetReason()),
-          _board->getShutdownReasonString(_board->getShutdownReason()));
-#else
-        strcpy(reply, "ERROR: Power management not supported");
-#endif
-      } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
-#ifdef NRF52_POWER_MANAGEMENT
-        sprintf(reply, "> %u mV", _board->getBootVoltage());
-#else
-        strcpy(reply, "ERROR: Power management not supported");
-#endif
-      } else {
-        sprintf(reply, "??: %s", config);
-      }
-    /*
-     * SET commands
-     */
+      handleGetCmd(sender_timestamp, command, reply);
     } else if (memcmp(command, "set ", 4) == 0) {
-      const char* config = &command[4];
-      if (memcmp(config, "dutycycle ", 10) == 0) {
-        float dc = atof(&config[10]);
-        if (dc < 1 || dc > 100) {
-          strcpy(reply, "ERROR: dutycycle must be 1-100");
-        } else {
-          _prefs->airtime_factor = (100.0f / dc) - 1.0f;
-          savePrefs();
-          float actual = 100.0f / (_prefs->airtime_factor + 1.0f);
-          int a_int = (int)actual;
-          int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
-          sprintf(reply, "OK - %d.%d%%", a_int, a_frac);
-        }
-      } else if (memcmp(config, "af ", 3) == 0) {
-        _prefs->airtime_factor = atof(&config[3]);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "int.thresh ", 11) == 0) {
-        _prefs->interference_threshold = atoi(&config[11]);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
-        _prefs->agc_reset_interval = atoi(&config[19]) / 4;
-        savePrefs();
-        sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
-      } else if (memcmp(config, "multi.acks ", 11) == 0) {
-        _prefs->multi_acks = atoi(&config[11]);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "allow.read.only ", 16) == 0) {
-        _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "flood.advert.interval ", 22) == 0) {
-        int hours = _atoi(&config[22]);
-        if ((hours > 0 && hours < 3) || (hours > 168)) {
-          strcpy(reply, "Error: interval range is 3-168 hours");
-        } else {
-          _prefs->flood_advert_interval = (uint8_t)(hours);
-          _callbacks->updateFloodAdvertTimer();
-          savePrefs();
-          strcpy(reply, "OK");
-        }
-      } else if (memcmp(config, "advert.interval ", 16) == 0) {
-        int mins = _atoi(&config[16]);
-        if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) {
-          sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL);
-        } else {
-          _prefs->advert_interval = (uint8_t)(mins / 2);
-          _callbacks->updateAdvertTimer();
-          savePrefs();
-          strcpy(reply, "OK");
-        }
-      } else if (memcmp(config, "guest.password ", 15) == 0) {
-        StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "prv.key ", 8) == 0) {
-        uint8_t prv_key[PRV_KEY_SIZE];
-        bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
-        // only allow rekey if key is valid
-        if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) {
-          mesh::LocalIdentity new_id;
-          new_id.readFrom(prv_key, PRV_KEY_SIZE);
-          _callbacks->saveIdentity(new_id);
-          strcpy(reply, "OK, reboot to apply! New pubkey: ");
-          mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE);
-        } else {
-          strcpy(reply, "Error, bad key");
-        }
-      } else if (memcmp(config, "name ", 5) == 0) {
-        if (isValidName(&config[5])) {
-          StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, bad chars");
-        }
-      } else if (memcmp(config, "repeat ", 7) == 0) {
-        _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
-        savePrefs();
-        strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
-#if defined(USE_SX1262) || defined(USE_SX1268)
-      } else if (memcmp(config, "radio.rxgain ", 13) == 0) {
-        _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0;
-        strcpy(reply, "OK");
-        savePrefs();
-        _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain);
-#endif
-      } else if (memcmp(config, "radio ", 6) == 0) {
-        strcpy(tmp, &config[6]);
-        const char *parts[4];
-        int num = mesh::Utils::parseTextParts(tmp, parts, 4);
-        float freq  = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
-        float bw    = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
-        uint8_t sf  = num > 2 ? atoi(parts[2]) : 0;
-        uint8_t cr  = num > 3 ? atoi(parts[3]) : 0;
-        if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
-          _prefs->sf = sf;
-          _prefs->cr = cr;
-          _prefs->freq = freq;
-          _prefs->bw = bw;
-          _callbacks->savePrefs();
-          strcpy(reply, "OK - reboot to apply");
-        } else {
-          strcpy(reply, "Error, invalid radio params");
-        }
-      } else if (memcmp(config, "lat ", 4) == 0) {
-        _prefs->node_lat = atof(&config[4]);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "lon ", 4) == 0) {
-        _prefs->node_lon = atof(&config[4]);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "rxdelay ", 8) == 0) {
-        float db = atof(&config[8]);
-        if (db >= 0) {
-          _prefs->rx_delay_base = db;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, cannot be negative");
-        }
-      } else if (memcmp(config, "txdelay ", 8) == 0) {
-        float f = atof(&config[8]);
-        if (f >= 0) {
-          _prefs->tx_delay_factor = f;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, cannot be negative");
-        }
-      } else if (memcmp(config, "flood.max ", 10) == 0) {
-        uint8_t m = atoi(&config[10]);
-        if (m <= 64) {
-          _prefs->flood_max = m;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, max 64");
-        }
-      } else if (memcmp(config, "direct.txdelay ", 15) == 0) {
-        float f = atof(&config[15]);
-        if (f >= 0) {
-          _prefs->direct_tx_delay_factor = f;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, cannot be negative");
-        }
-      } else if (memcmp(config, "owner.info ", 11) == 0) {
-        config += 11;
-        char *dp = _prefs->owner_info;
-        while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) {
-          *dp++ = (*config == '|') ? '\n' : *config;    // translate '|' to newline chars
-          config++;
-        }
-        *dp = 0;
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "path.hash.mode ", 15) == 0) {
-        config += 15;
-        uint8_t mode = atoi(config);
-        if (mode < 3) {
-          _prefs->path_hash_mode = mode;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error, must be 0,1, or 2");
-        }
-      } else if (memcmp(config, "loop.detect ", 12) == 0) {
-        config += 12;
-        uint8_t mode;
-        if (memcmp(config, "off", 3) == 0) {
-          mode = LOOP_DETECT_OFF;
-        } else if (memcmp(config, "minimal", 7) == 0) {
-          mode = LOOP_DETECT_MINIMAL;
-        } else if (memcmp(config, "moderate", 8) == 0) {
-          mode = LOOP_DETECT_MODERATE;
-        } else if (memcmp(config, "strict", 6) == 0) {
-          mode = LOOP_DETECT_STRICT;
-        } else {
-          mode = 0xFF;
-          strcpy(reply, "Error, must be: off, minimal, moderate, or strict");
-        }
-        if (mode != 0xFF) {
-          _prefs->loop_detect = mode;
-          savePrefs();
-          strcpy(reply, "OK");
-        }
-      } else if (memcmp(config, "tx ", 3) == 0) {
-        _prefs->tx_power_dbm = atoi(&config[3]);
-        savePrefs();
-        _callbacks->setTxPower(_prefs->tx_power_dbm);
-        strcpy(reply, "OK");
-      } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
-        _prefs->freq = atof(&config[5]);
-        savePrefs();
-        strcpy(reply, "OK - reboot to apply");
-#ifdef WITH_BRIDGE
-      } else if (memcmp(config, "bridge.enabled ", 15) == 0) {
-        _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0;
-        _callbacks->setBridgeState(_prefs->bridge_enabled);
-        savePrefs();
-        strcpy(reply, "OK");
-      } else if (memcmp(config, "bridge.delay ", 13) == 0) {
-        int delay = _atoi(&config[13]);
-        if (delay >= 0 && delay <= 10000) {
-          _prefs->bridge_delay = (uint16_t)delay;
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error: delay must be between 0-10000 ms");
-        }
-      } else if (memcmp(config, "bridge.source ", 14) == 0) {
-        _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0;
-        savePrefs();
-        strcpy(reply, "OK");
-#endif
-#ifdef WITH_RS232_BRIDGE
-      } else if (memcmp(config, "bridge.baud ", 12) == 0) {
-        uint32_t baud = atoi(&config[12]);
-        if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) {
-          _prefs->bridge_baud = (uint32_t)baud;
-          _callbacks->restartBridge();
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD);
-        }
-#endif
-#ifdef WITH_ESPNOW_BRIDGE
-      } else if (memcmp(config, "bridge.channel ", 15) == 0) {
-        int ch = atoi(&config[15]);
-        if (ch > 0 && ch < 15) {
-          _prefs->bridge_channel = (uint8_t)ch;
-          _callbacks->restartBridge();
-          savePrefs();
-          strcpy(reply, "OK");
-        } else {
-          strcpy(reply, "Error: channel must be between 1-14");
-        }
-      } else if (memcmp(config, "bridge.secret ", 14) == 0) {
-        StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret));
-        _callbacks->restartBridge();
-        savePrefs();
-        strcpy(reply, "OK");
-#endif
-      } else if (memcmp(config, "adc.multiplier ", 15) == 0) {
-        _prefs->adc_multiplier = atof(&config[15]);
-        if (_board->setAdcMultiplier(_prefs->adc_multiplier)) {
-          savePrefs();
-          if (_prefs->adc_multiplier == 0.0f) {
-            strcpy(reply, "OK - using default board multiplier");
-          } else {
-            sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier);
-          }
-        } else {
-          _prefs->adc_multiplier = 0.0f;
-          strcpy(reply, "Error: unsupported by this board");
-        };
-      } else {
-        sprintf(reply, "unknown config: %s", config);
-      }
+      handleSetCmd(sender_timestamp, command, reply);
     } else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) {
       bool s = _callbacks->formatFileSystem();
       sprintf(reply, "File system erase: %s", s ? "OK" : "Err");
@@ -771,6 +344,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
           *(dp-1) = 0; // remove last CR
         }
       }
+    } else if (memcmp(command, "region", 6) == 0) {
+      handleRegionCmd(command, reply);
 #if ENV_INCLUDE_GPS == 1
     } else if (memcmp(command, "gps on", 6) == 0) {
       if (_sensors->setSettingValue("gps", "1")) {
@@ -886,3 +461,550 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
       strcpy(reply, "Unknown command");
     }
 }
+
+void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
+  const char* config = &command[4];
+  if (memcmp(config, "dutycycle ", 10) == 0) {
+    float dc = atof(&config[10]);
+    if (dc < 1 || dc > 100) {
+      strcpy(reply, "ERROR: dutycycle must be 1-100");
+    } else {
+      _prefs->airtime_factor = (100.0f / dc) - 1.0f;
+      savePrefs();
+      float actual = 100.0f / (_prefs->airtime_factor + 1.0f);
+      int a_int = (int)actual;
+      int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
+      sprintf(reply, "OK - %d.%d%%", a_int, a_frac);
+    }
+  } else if (memcmp(config, "af ", 3) == 0) {
+    _prefs->airtime_factor = atof(&config[3]);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "int.thresh ", 11) == 0) {
+    _prefs->interference_threshold = atoi(&config[11]);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
+    _prefs->agc_reset_interval = atoi(&config[19]) / 4;
+    savePrefs();
+    sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
+  } else if (memcmp(config, "multi.acks ", 11) == 0) {
+    _prefs->multi_acks = atoi(&config[11]);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "allow.read.only ", 16) == 0) {
+    _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "flood.advert.interval ", 22) == 0) {
+    int hours = _atoi(&config[22]);
+    if ((hours > 0 && hours < 3) || (hours > 168)) {
+      strcpy(reply, "Error: interval range is 3-168 hours");
+    } else {
+      _prefs->flood_advert_interval = (uint8_t)(hours);
+      _callbacks->updateFloodAdvertTimer();
+      savePrefs();
+      strcpy(reply, "OK");
+    }
+  } else if (memcmp(config, "advert.interval ", 16) == 0) {
+    int mins = _atoi(&config[16]);
+    if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) {
+      sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL);
+    } else {
+      _prefs->advert_interval = (uint8_t)(mins / 2);
+      _callbacks->updateAdvertTimer();
+      savePrefs();
+      strcpy(reply, "OK");
+    }
+  } else if (memcmp(config, "guest.password ", 15) == 0) {
+    StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "prv.key ", 8) == 0) {
+    uint8_t prv_key[PRV_KEY_SIZE];
+    bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
+    // only allow rekey if key is valid
+    if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) {
+      mesh::LocalIdentity new_id;
+      new_id.readFrom(prv_key, PRV_KEY_SIZE);
+      _callbacks->saveIdentity(new_id);
+      strcpy(reply, "OK, reboot to apply! New pubkey: ");
+      mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE);
+    } else {
+      strcpy(reply, "Error, bad key");
+    }
+  } else if (memcmp(config, "name ", 5) == 0) {
+    if (isValidName(&config[5])) {
+      StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, bad chars");
+    }
+  } else if (memcmp(config, "repeat ", 7) == 0) {
+    _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
+    savePrefs();
+    strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
+#if defined(USE_SX1262) || defined(USE_SX1268)
+  } else if (memcmp(config, "radio.rxgain ", 13) == 0) {
+    _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0;
+    strcpy(reply, "OK");
+    savePrefs();
+    _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain);
+#endif
+  } else if (memcmp(config, "radio ", 6) == 0) {
+    strcpy(tmp, &config[6]);
+    const char *parts[4];
+    int num = mesh::Utils::parseTextParts(tmp, parts, 4);
+    float freq  = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
+    float bw    = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
+    uint8_t sf  = num > 2 ? atoi(parts[2]) : 0;
+    uint8_t cr  = num > 3 ? atoi(parts[3]) : 0;
+    if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
+      _prefs->sf = sf;
+      _prefs->cr = cr;
+      _prefs->freq = freq;
+      _prefs->bw = bw;
+      _callbacks->savePrefs();
+      strcpy(reply, "OK - reboot to apply");
+    } else {
+      strcpy(reply, "Error, invalid radio params");
+    }
+  } else if (memcmp(config, "lat ", 4) == 0) {
+    _prefs->node_lat = atof(&config[4]);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "lon ", 4) == 0) {
+    _prefs->node_lon = atof(&config[4]);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "rxdelay ", 8) == 0) {
+    float db = atof(&config[8]);
+    if (db >= 0) {
+      _prefs->rx_delay_base = db;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, cannot be negative");
+    }
+  } else if (memcmp(config, "txdelay ", 8) == 0) {
+    float f = atof(&config[8]);
+    if (f >= 0) {
+      _prefs->tx_delay_factor = f;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, cannot be negative");
+    }
+  } else if (memcmp(config, "flood.max ", 10) == 0) {
+    uint8_t m = atoi(&config[10]);
+    if (m <= 64) {
+      _prefs->flood_max = m;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, max 64");
+    }
+  } else if (memcmp(config, "direct.txdelay ", 15) == 0) {
+    float f = atof(&config[15]);
+    if (f >= 0) {
+      _prefs->direct_tx_delay_factor = f;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, cannot be negative");
+    }
+  } else if (memcmp(config, "owner.info ", 11) == 0) {
+    config += 11;
+    char *dp = _prefs->owner_info;
+    while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) {
+      *dp++ = (*config == '|') ? '\n' : *config;    // translate '|' to newline chars
+      config++;
+    }
+    *dp = 0;
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "path.hash.mode ", 15) == 0) {
+    config += 15;
+    uint8_t mode = atoi(config);
+    if (mode < 3) {
+      _prefs->path_hash_mode = mode;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error, must be 0,1, or 2");
+    }
+  } else if (memcmp(config, "loop.detect ", 12) == 0) {
+    config += 12;
+    uint8_t mode;
+    if (memcmp(config, "off", 3) == 0) {
+      mode = LOOP_DETECT_OFF;
+    } else if (memcmp(config, "minimal", 7) == 0) {
+      mode = LOOP_DETECT_MINIMAL;
+    } else if (memcmp(config, "moderate", 8) == 0) {
+      mode = LOOP_DETECT_MODERATE;
+    } else if (memcmp(config, "strict", 6) == 0) {
+      mode = LOOP_DETECT_STRICT;
+    } else {
+      mode = 0xFF;
+      strcpy(reply, "Error, must be: off, minimal, moderate, or strict");
+    }
+    if (mode != 0xFF) {
+      _prefs->loop_detect = mode;
+      savePrefs();
+      strcpy(reply, "OK");
+    }
+  } else if (memcmp(config, "tx ", 3) == 0) {
+    _prefs->tx_power_dbm = atoi(&config[3]);
+    savePrefs();
+    _callbacks->setTxPower(_prefs->tx_power_dbm);
+    strcpy(reply, "OK");
+  } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
+    _prefs->freq = atof(&config[5]);
+    savePrefs();
+    strcpy(reply, "OK - reboot to apply");
+#ifdef WITH_BRIDGE
+  } else if (memcmp(config, "bridge.enabled ", 15) == 0) {
+    _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0;
+    _callbacks->setBridgeState(_prefs->bridge_enabled);
+    savePrefs();
+    strcpy(reply, "OK");
+  } else if (memcmp(config, "bridge.delay ", 13) == 0) {
+    int delay = _atoi(&config[13]);
+    if (delay >= 0 && delay <= 10000) {
+      _prefs->bridge_delay = (uint16_t)delay;
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error: delay must be between 0-10000 ms");
+    }
+  } else if (memcmp(config, "bridge.source ", 14) == 0) {
+    _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0;
+    savePrefs();
+    strcpy(reply, "OK");
+#endif
+#ifdef WITH_RS232_BRIDGE
+  } else if (memcmp(config, "bridge.baud ", 12) == 0) {
+    uint32_t baud = atoi(&config[12]);
+    if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) {
+      _prefs->bridge_baud = (uint32_t)baud;
+      _callbacks->restartBridge();
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD);
+    }
+#endif
+#ifdef WITH_ESPNOW_BRIDGE
+  } else if (memcmp(config, "bridge.channel ", 15) == 0) {
+    int ch = atoi(&config[15]);
+    if (ch > 0 && ch < 15) {
+      _prefs->bridge_channel = (uint8_t)ch;
+      _callbacks->restartBridge();
+      savePrefs();
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Error: channel must be between 1-14");
+    }
+  } else if (memcmp(config, "bridge.secret ", 14) == 0) {
+    StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret));
+    _callbacks->restartBridge();
+    savePrefs();
+    strcpy(reply, "OK");
+#endif
+  } else if (memcmp(config, "adc.multiplier ", 15) == 0) {
+    _prefs->adc_multiplier = atof(&config[15]);
+    if (_board->setAdcMultiplier(_prefs->adc_multiplier)) {
+      savePrefs();
+      if (_prefs->adc_multiplier == 0.0f) {
+        strcpy(reply, "OK - using default board multiplier");
+      } else {
+        sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier);
+      }
+    } else {
+      _prefs->adc_multiplier = 0.0f;
+      strcpy(reply, "Error: unsupported by this board");
+    };
+  } else {
+    sprintf(reply, "unknown config: %s", config);
+  }
+}
+
+void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
+  const char* config = &command[4];
+  if (memcmp(config, "dutycycle", 9) == 0) {
+    float dc = 100.0f / (_prefs->airtime_factor + 1.0f);
+    int dc_int = (int)dc;
+    int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f);
+    sprintf(reply, "> %d.%d%%", dc_int, dc_frac);
+  } else if (memcmp(config, "af", 2) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
+  } else if (memcmp(config, "int.thresh", 10) == 0) {
+    sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
+  } else if (memcmp(config, "agc.reset.interval", 18) == 0) {
+    sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
+  } else if (memcmp(config, "multi.acks", 10) == 0) {
+    sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
+  } else if (memcmp(config, "allow.read.only", 15) == 0) {
+    sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
+  } else if (memcmp(config, "flood.advert.interval", 21) == 0) {
+    sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
+  } else if (memcmp(config, "advert.interval", 15) == 0) {
+    sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
+  } else if (memcmp(config, "guest.password", 14) == 0) {
+    sprintf(reply, "> %s", _prefs->guest_password);
+  } else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) {  // from serial command line only
+    uint8_t prv_key[PRV_KEY_SIZE];
+    int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
+    mesh::Utils::toHex(tmp, prv_key, len);
+    sprintf(reply, "> %s", tmp);
+  } else if (memcmp(config, "name", 4) == 0) {
+    sprintf(reply, "> %s", _prefs->node_name);
+  } else if (memcmp(config, "repeat", 6) == 0) {
+    sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on");
+  } else if (memcmp(config, "lat", 3) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
+  } else if (memcmp(config, "lon", 3) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
+#if defined(USE_SX1262) || defined(USE_SX1268)
+  } else if (memcmp(config, "radio.rxgain", 12) == 0) {
+    sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
+#endif
+  } else if (memcmp(config, "radio", 5) == 0) {
+    char freq[16], bw[16];
+    strcpy(freq, StrHelper::ftoa(_prefs->freq));
+    strcpy(bw, StrHelper::ftoa3(_prefs->bw));
+    sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
+  } else if (memcmp(config, "rxdelay", 7) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
+  } else if (memcmp(config, "txdelay", 7) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor));
+  } else if (memcmp(config, "flood.max", 9) == 0) {
+    sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
+  } else if (memcmp(config, "direct.txdelay", 14) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
+  } else if (memcmp(config, "owner.info", 10) == 0) {
+    *reply++ = '>';
+    *reply++ = ' ';
+    const char* sp = _prefs->owner_info;
+    while (*sp) {
+      *reply++ = (*sp == '\n') ? '|' : *sp;    // translate newline back to orig '|'
+      sp++;
+    }
+    *reply = 0;  // set null terminator
+  } else if (memcmp(config, "path.hash.mode", 14) == 0) {
+    sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode);
+  } else if (memcmp(config, "loop.detect", 11) == 0) {
+    if (_prefs->loop_detect == LOOP_DETECT_OFF) {
+      strcpy(reply, "> off");
+    } else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) {
+      strcpy(reply, "> minimal");
+    } else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) {
+      strcpy(reply, "> moderate");
+    } else {
+      strcpy(reply, "> strict");
+    }
+  } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
+    sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
+  } else if (memcmp(config, "freq", 4) == 0) {
+    sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
+  } else if (memcmp(config, "public.key", 10) == 0) {
+    strcpy(reply, "> ");
+    mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
+  } else if (memcmp(config, "role", 4) == 0) {
+    sprintf(reply, "> %s", _callbacks->getRole());
+  } else if (memcmp(config, "bridge.type", 11) == 0) {
+    sprintf(reply, "> %s",
+#ifdef WITH_RS232_BRIDGE
+            "rs232"
+#elif WITH_ESPNOW_BRIDGE
+            "espnow"
+#else
+            "none"
+#endif
+    );
+#ifdef WITH_BRIDGE
+  } else if (memcmp(config, "bridge.enabled", 14) == 0) {
+    sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off");
+  } else if (memcmp(config, "bridge.delay", 12) == 0) {
+    sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
+  } else if (memcmp(config, "bridge.source", 13) == 0) {
+    sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx");
+#endif
+#ifdef WITH_RS232_BRIDGE
+  } else if (memcmp(config, "bridge.baud", 11) == 0) {
+    sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
+#endif
+#ifdef WITH_ESPNOW_BRIDGE
+  } else if (memcmp(config, "bridge.channel", 14) == 0) {
+    sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
+  } else if (memcmp(config, "bridge.secret", 13) == 0) {
+    sprintf(reply, "> %s", _prefs->bridge_secret);
+#endif
+  } else if (memcmp(config, "bootloader.ver", 14) == 0) {
+  #ifdef NRF52_PLATFORM
+      char ver[32];
+      if (_board->getBootloaderVersion(ver, sizeof(ver))) {
+          sprintf(reply, "> %s", ver);
+      } else {
+          strcpy(reply, "> unknown");
+      }
+  #else
+      strcpy(reply, "ERROR: unsupported");
+  #endif
+  } else if (memcmp(config, "adc.multiplier", 14) == 0) {
+    float adc_mult = _board->getAdcMultiplier();
+    if (adc_mult == 0.0f) {
+      strcpy(reply, "Error: unsupported by this board");
+    } else {
+      sprintf(reply, "> %.3f", adc_mult);
+    }
+  // Power management commands
+  } else if (memcmp(config, "pwrmgt.support", 14) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+    strcpy(reply, "> supported");
+#else
+    strcpy(reply, "> unsupported");
+#endif
+  } else if (memcmp(config, "pwrmgt.source", 13) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+    strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
+#else
+    strcpy(reply, "ERROR: Power management not supported");
+#endif
+  } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+    sprintf(reply, "> Reset: %s; Shutdown: %s",
+      _board->getResetReasonString(_board->getResetReason()),
+      _board->getShutdownReasonString(_board->getShutdownReason()));
+#else
+    strcpy(reply, "ERROR: Power management not supported");
+#endif
+  } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+    sprintf(reply, "> %u mV", _board->getBootVoltage());
+#else
+    strcpy(reply, "ERROR: Power management not supported");
+#endif
+  } else {
+    sprintf(reply, "??: %s", config);
+  }
+}
+
+void CommonCLI::handleRegionCmd(char* command, char* reply) {
+  reply[0] = 0;
+
+  const char* parts[4];
+  int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
+  if (n == 1) {
+    _region_map->exportTo(reply, 160);
+  } else if (n >= 2 && strcmp(parts[1], "load") == 0) {
+    _callbacks->startRegionsLoad();
+  } else if (n >= 2 && strcmp(parts[1], "save") == 0) {
+    _prefs->discovery_mod_timestamp = getRTCClock()->getCurrentTime();   // this node is now 'modified' (for discovery info)
+    savePrefs();
+    bool success = _callbacks->saveRegions();
+    strcpy(reply, success ? "OK" : "Err - save failed");
+  } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) {
+    auto region = _region_map->findByNamePrefix(parts[2]);
+    if (region) {
+      region->flags &= ~REGION_DENY_FLOOD;
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Err - unknown region");
+    }
+  } else if (n >= 3 && strcmp(parts[1], "denyf") == 0) {
+    auto region = _region_map->findByNamePrefix(parts[2]);
+    if (region) {
+      region->flags |= REGION_DENY_FLOOD;
+      strcpy(reply, "OK");
+    } else {
+      strcpy(reply, "Err - unknown region");
+    }
+  } else if (n >= 3 && strcmp(parts[1], "get") == 0) {
+    auto region = _region_map->findByNamePrefix(parts[2]);
+    if (region) {
+      auto parent = _region_map->findById(region->parent);
+      if (parent && parent->id != 0) {
+        sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
+      } else {
+        sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
+      }
+    } else {
+      strcpy(reply, "Err - unknown region");
+    }
+  } else if (n >= 3 && strcmp(parts[1], "home") == 0) {
+    auto home = _region_map->findByNamePrefix(parts[2]);
+    if (home) {
+      _region_map->setHomeRegion(home);
+      sprintf(reply, " home is now %s", home->name);
+    } else {
+      strcpy(reply, "Err - unknown region");
+    }
+  } else if (n == 2 && strcmp(parts[1], "home") == 0) {
+    auto home = _region_map->getHomeRegion();
+    sprintf(reply, " home is %s", home ? home->name : "*");
+  } else if (n >= 3 && strcmp(parts[1], "default") == 0) {
+    if (strcmp(parts[2], "<null>") == 0) {
+      _region_map->setDefaultRegion(NULL);
+      _callbacks->onDefaultRegionChanged(NULL);
+      sprintf(reply, " default scope is now <null>");
+    } else {
+      auto def = _region_map->findByNamePrefix(parts[2]);
+      if (def) {
+        _region_map->setDefaultRegion(def);
+        _callbacks->onDefaultRegionChanged(def);
+        sprintf(reply, " default scope is now %s", def->name);
+      } else {
+        strcpy(reply, "Err - unknown region");
+      }
+    }
+  } else if (n == 2 && strcmp(parts[1], "default") == 0) {
+    auto def = _region_map->getDefaultRegion();
+    sprintf(reply, " default scope is %s", def ? def->name : "<null>");
+  } else if (n >= 3 && strcmp(parts[1], "put") == 0) {
+    auto parent = n >= 4 ? _region_map->findByNamePrefix(parts[3]) : &(_region_map->getWildcard());
+    if (parent == NULL) {
+      strcpy(reply, "Err - unknown parent");
+    } else {
+      auto region = _region_map->putRegion(parts[2], parent->id);
+      if (region == NULL) {
+        strcpy(reply, "Err - unable to put");
+      } else {
+        strcpy(reply, "OK");
+      }
+    }
+  } else if (n >= 3 && strcmp(parts[1], "remove") == 0) {
+    auto region = _region_map->findByName(parts[2]);
+    if (region) {
+      if (_region_map->removeRegion(*region)) {
+        strcpy(reply, "OK");
+      } else {
+        strcpy(reply, "Err - not empty");
+      }
+    } else {
+      strcpy(reply, "Err - not found");
+    }
+  } else if (n >= 3 && strcmp(parts[1], "list") == 0) {
+    uint8_t mask = 0;
+    bool invert = false;
+    
+    if (strcmp(parts[2], "allowed") == 0) {
+      mask = REGION_DENY_FLOOD;
+      invert = false;  // list regions that DON'T have DENY flag
+    } else if (strcmp(parts[2], "denied") == 0) {
+      mask = REGION_DENY_FLOOD;
+      invert = true;   // list regions that DO have DENY flag
+    } else {
+      strcpy(reply, "Err - use 'allowed' or 'denied'");
+      return;
+    }
+    
+    int len = _region_map->exportNamesTo(reply, 160, mask, invert);
+    if (len == 0) {
+      strcpy(reply, "-none-");
+    }
+  } else {
+    strcpy(reply, "Err - ??");
+  }
+}

+ 19 - 3
src/helpers/CommonCLI.h

@@ -4,6 +4,7 @@
 #include <helpers/IdentityStore.h>
 #include <helpers/SensorManager.h>
 #include <helpers/ClientACL.h>
+#include <helpers/RegionMap.h>
 
 #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
 #define WITH_BRIDGE
@@ -88,6 +89,16 @@ public:
   virtual void clearStats() = 0;
   virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0;
 
+  virtual void startRegionsLoad() {
+    // no op by default
+  }
+  virtual bool saveRegions() {
+    return false;
+  }
+  virtual void onDefaultRegionChanged(const RegionEntry* r) {
+    // no op by default
+  }
+
   virtual void setBridgeState(bool enable) {
     // no op by default
   };
@@ -107,6 +118,7 @@ class CommonCLI {
   CommonCLICallbacks* _callbacks;
   mesh::MainBoard* _board;
   SensorManager* _sensors;
+  RegionMap* _region_map;
   ClientACL* _acl;
   char tmp[PRV_KEY_SIZE*2 + 4];
 
@@ -114,12 +126,16 @@ class CommonCLI {
   void savePrefs();
   void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
 
+  void handleRegionCmd(char* command, char* reply);
+  void handleGetCmd(uint32_t sender_timestamp, char* command, char* reply);
+  void handleSetCmd(uint32_t sender_timestamp, char* command, char* reply);
+
 public:
-  CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks)
-      : _board(&board), _rtc(&rtc), _sensors(&sensors), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { }
+  CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, RegionMap& region_map, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks)
+      : _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(&region_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { }
 
   void loadPrefs(FILESYSTEM* _fs);
   void savePrefs(FILESYSTEM* _fs);
-  void handleCommand(uint32_t sender_timestamp, const char* command, char* reply);
+  void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
   uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data);
 };