diff --git a/diagram1_home_region.svg b/diagram1_home_region.svg
new file mode 100644
index 00000000..4022af3f
--- /dev/null
+++ b/diagram1_home_region.svg
@@ -0,0 +1,65 @@
+
diff --git a/diagram2_wrong_scope.svg b/diagram2_wrong_scope.svg
new file mode 100644
index 00000000..f0af9f5b
--- /dev/null
+++ b/diagram2_wrong_scope.svg
@@ -0,0 +1,76 @@
+
diff --git a/docs/cli_commands.md b/docs/cli_commands.md
index fb698228..70581716 100644
--- a/docs/cli_commands.md
+++ b/docs/cli_commands.md
@@ -728,6 +728,40 @@ This document provides an overview of CLI commands that can be sent to MeshCore
**Parameters:**
- `name`: Region name
+**Note:** The home region is used by the auto-tag feature (see `region.autotag` below) to stamp a transport code onto untagged flood packets this repeater receives, converting them from `ROUTE_TYPE_FLOOD` to `ROUTE_TYPE_TRANSPORT_FLOOD`. When selecting a home region, choose the **most specific region in the local region hierarchy that includes all nodes this repeater can possibly hear** (including via hops you intend to cover — see `region.autotag.max.hops`). Choosing a home region that is too narrow will cause unscoped packets originating from neighbouring regions to be tagged incorrectly; choosing one that is too broad defeats the purpose of scoping.
+
+**When no home region is set:** the repeater has no scope to apply, so auto-tagging is effectively disabled regardless of the `region.autotag` setting — untagged flood packets are forwarded based on the wildcard (`*`) region's flood permission only. To participate in auto-tagging, both a home region must be configured here **and** `region.autotag` must be `on`.
+
+---
+
+#### View or change whether this repeater auto-tags untagged flood packets
+**Usage:**
+- `get region.autotag`
+- `set region.autotag `
+
+**Parameters:**
+- `state`: `on` (enable) or `off` (disable)
+
+**Default:** `off`
+
+**Note:** When enabled, the repeater stamps its home region's transport code onto untagged flood packets (`ROUTE_TYPE_FLOOD`) it receives, converting them to `ROUTE_TYPE_TRANSPORT_FLOOD` before re-broadcast. This scopes legacy / un-scoped traffic into the configured home region, but requires a home region to be configured (see `region home`). Because mis-tagging is possible when the repeater can hear traffic originating outside its home region, this feature is opt-in. See also `region.autotag.max.hops` to limit how far a packet may have travelled before becoming eligible for auto-tagging. The reserved transport code `0xFFFF` (TRANSPORT_CODE_ALL) is always forwarded regardless of local region configuration, allowing explicit mesh-wide flooding when a sender requests it.
+
+---
+
+#### View or change the max hop count for auto-tagging
+**Usage:**
+- `get region.autotag.max.hops`
+- `set region.autotag.max.hops `
+
+**Parameters:**
+- `value`: Maximum path hash count. `0` means only auto-tag packets without scope received directly (zero-hop); higher values also auto-tag packets without scope that already traversed that many repeaters.
+
+**Range:** `0` to `8` (inclusive). Values outside this range are rejected by `set` and clamped to this range on load. The upper bound of `8` is intentionally well below the default `flood.max` of `64`, because auto-tagging packets from far across the mesh almost always produces incorrect region assignments — the limit exists to keep admins honest about the geographic scope they can actually account for.
+
+**Default:** `1`
+
+**Note:** Only applies when `region.autotag` is `on`. Keep this small (0-2) unless you are certain no untagged / older-firmware repeaters exist within that many hops, otherwise distant-origin traffic forwarded through them may be tagged with the wrong region.
+
---
#### View or change the default scope region for this node
diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp
index 666f79fc..03d51f8e 100644
--- a/examples/simple_repeater/MyMesh.cpp
+++ b/examples/simple_repeater/MyMesh.cpp
@@ -429,7 +429,9 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
- if (packet->isRouteFlood() && recv_pkt_region == NULL) {
+ if (packet->isRouteFlood() && packet->hasTransportCodes() && packet->transport_codes[0] == TRANSPORT_CODE_ALL) {
+ // ALL region: always forward regardless of region config
+ } else if (packet->isRouteFlood() && recv_pkt_region == NULL) {
MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet");
return false;
}
@@ -548,12 +550,41 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
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);
+ if (pkt->transport_codes[0] == TRANSPORT_CODE_ALL) {
+ recv_pkt_region = ®ion_map.getWildcard(); // ALL: always allow
+ } else {
+ 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) {
+ // untagged packet: tag with home region if auto-tagging is enabled and a home region is configured
+ RegionEntry* home = region_map.getHomeRegion();
+ if (_prefs.region_autotag && home && home->id != 0
+ && pkt->getPathHashCount() <= _prefs.region_autotag_max_hops) {
+ // calculate transport code for home region and stamp onto packet
+ TransportKey key;
+ if (home->name[0] == '$') {
+ // private region: load key from store
+ if (key_store.loadKeysFor(home->id, &key, 1) < 1) {
+ recv_pkt_region = NULL;
+ return false;
+ }
+ } else if (home->name[0] == '#') {
+ key_store.getAutoKeyFor(home->id, home->name, key);
+ } else {
+ char tmp[sizeof(home->name) + 1];
+ tmp[0] = '#';
+ strcpy(&tmp[1], home->name);
+ key_store.getAutoKeyFor(home->id, tmp, key);
+ }
+ pkt->transport_codes[0] = key.calcTransportCode(pkt);
+ pkt->transport_codes[1] = 0;
+ pkt->header = (pkt->header & ~PH_ROUTE_MASK) | ROUTE_TYPE_TRANSPORT_FLOOD;
+
+ recv_pkt_region = home;
+ } else if (region_map.getWildcard().flags & REGION_DENY_FLOOD) {
recv_pkt_region = NULL;
} else {
- recv_pkt_region = ®ion_map.getWildcard();
+ recv_pkt_region = ®ion_map.getWildcard();
}
} else {
recv_pkt_region = NULL;
@@ -886,6 +917,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 12; // 12 hours
_prefs.flood_max = 64;
+ _prefs.region_autotag = 0; // opt-in, default off
+ _prefs.region_autotag_max_hops = 1; // only tag zero-hop / 1-hop packets by default
_prefs.interference_threshold = 0; // disabled
// bridge defaults
diff --git a/src/Packet.h b/src/Packet.h
index 0886a06c..ccd2da89 100644
--- a/src/Packet.h
+++ b/src/Packet.h
@@ -16,6 +16,8 @@ namespace mesh {
#define ROUTE_TYPE_DIRECT 0x02 // direct route, 'path' is supplied
#define ROUTE_TYPE_TRANSPORT_DIRECT 0x03 // direct route + transport codes
+#define TRANSPORT_CODE_ALL 0xFFFF // special transport code: forward to all regions
+
#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp
index d495aada..0051c66c 100644
--- a/src/helpers/CommonCLI.cpp
+++ b/src/helpers/CommonCLI.cpp
@@ -87,6 +87,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
+ file.read((uint8_t *)&_prefs->region_autotag, sizeof(_prefs->region_autotag)); // 290
+ file.read((uint8_t *)&_prefs->region_autotag_max_hops, sizeof(_prefs->region_autotag_max_hops)); // 291
+ // next: 292
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
// next: 291
@@ -118,6 +121,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
// sanitise settings
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
+ _prefs->region_autotag = constrain(_prefs->region_autotag, 0, 1); // boolean
+ _prefs->region_autotag_max_hops = constrain(_prefs->region_autotag_max_hops, 0, REGION_AUTOTAG_MAX_HOPS_LIMIT);
file.close();
}
@@ -178,6 +183,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
+ file.write((uint8_t *)&_prefs->region_autotag, sizeof(_prefs->region_autotag)); // 290
+ file.write((uint8_t *)&_prefs->region_autotag_max_hops, sizeof(_prefs->region_autotag_max_hops)); // 291
+ // next: 292
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
// next: 291
@@ -725,6 +733,19 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->adc_multiplier = 0.0f;
strcpy(reply, "Error: unsupported by this board");
};
+ } else if (memcmp(config, "region.autotag.max.hops ", 24) == 0) {
+ int h = atoi(&config[24]);
+ if (h >= 0 && h <= REGION_AUTOTAG_MAX_HOPS_LIMIT) {
+ _prefs->region_autotag_max_hops = (uint8_t)h;
+ savePrefs();
+ strcpy(reply, "OK");
+ } else {
+ sprintf(reply, "Error, range is 0-%d", REGION_AUTOTAG_MAX_HOPS_LIMIT);
+ }
+ } else if (memcmp(config, "region.autotag ", 15) == 0) {
+ _prefs->region_autotag = memcmp(&config[15], "on", 2) == 0;
+ savePrefs();
+ strcpy(reply, "OK");
} else {
sprintf(reply, "unknown config: %s", config);
}
@@ -794,6 +815,10 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
*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, "region.autotag.max.hops", 23) == 0) {
+ sprintf(reply, "> %d", (uint32_t)_prefs->region_autotag_max_hops);
+ } else if (memcmp(config, "region.autotag", 14) == 0) {
+ sprintf(reply, "> %s", _prefs->region_autotag ? "on" : "off");
} else if (memcmp(config, "loop.detect", 11) == 0) {
if (_prefs->loop_detect == LOOP_DETECT_OFF) {
strcpy(reply, "> off");
diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h
index ffdc7c65..d26ba83a 100644
--- a/src/helpers/CommonCLI.h
+++ b/src/helpers/CommonCLI.h
@@ -14,6 +14,8 @@
#define ADVERT_LOC_SHARE 1
#define ADVERT_LOC_PREFS 2
+#define REGION_AUTOTAG_MAX_HOPS_LIMIT 8 // upper bound for region.autotag.max.hops pref
+
#define LOOP_DETECT_OFF 0
#define LOOP_DETECT_MINIMAL 1
#define LOOP_DETECT_MODERATE 2
@@ -61,6 +63,8 @@ struct NodePrefs { // persisted to file
uint8_t rx_boosted_gain; // power settings
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t loop_detect;
+ uint8_t region_autotag; // boolean: auto-tag untagged flood packets with home region's transport code
+ uint8_t region_autotag_max_hops; // only auto-tag packets received with pathHashCount <= this value (0 = zero-hop only)
};
class CommonCLICallbacks {