This commit is contained in:
Dale Ruane 2026-04-20 16:47:31 +01:00 committed by GitHub
commit 69319aaeae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 243 additions and 4 deletions

65
diagram1_home_region.svg Normal file
View file

@ -0,0 +1,65 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 420" font-family="system-ui, -apple-system, sans-serif" font-size="14">
<defs>
<marker id="arrow1" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#059669"/>
</marker>
<marker id="arrow1b" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb"/>
</marker>
</defs>
<!-- Title -->
<text x="360" y="30" text-anchor="middle" font-size="16" font-weight="bold" fill="#1e293b">Scenario 1: Untagged packet tagged with home region (R1)</text>
<!-- R1 zone -->
<rect x="40" y="60" width="520" height="320" rx="16" fill="#eff6ff" stroke="#93c5fd" stroke-width="2" stroke-dasharray="8 4"/>
<text x="60" y="88" font-size="13" font-weight="600" fill="#2563eb">R1 zone</text>
<!-- R2 zone -->
<rect x="580" y="60" width="120" height="160" rx="16" fill="#fef3c7" stroke="#fbbf24" stroke-width="2" stroke-dasharray="8 4"/>
<text x="600" y="88" font-size="13" font-weight="600" fill="#b45309">R2 zone</text>
<!-- R1-A (home=R1) -->
<rect x="120" y="160" width="140" height="70" rx="10" fill="#ffffff" stroke="#2563eb" stroke-width="2"/>
<text x="190" y="188" text-anchor="middle" font-weight="bold" fill="#1e293b">R1-A</text>
<text x="190" y="210" text-anchor="middle" font-size="12" fill="#6b7280">home = R1</text>
<!-- R1-B -->
<rect x="380" y="110" width="120" height="56" rx="10" fill="#ffffff" stroke="#2563eb" stroke-width="2"/>
<text x="440" y="144" text-anchor="middle" font-weight="bold" fill="#1e293b">R1-B</text>
<!-- R1-C -->
<rect x="380" y="230" width="120" height="56" rx="10" fill="#ffffff" stroke="#2563eb" stroke-width="2"/>
<text x="440" y="264" text-anchor="middle" font-weight="bold" fill="#1e293b">R1-C</text>
<!-- R2-A -->
<rect x="600" y="120" width="80" height="56" rx="10" fill="#ffffff" stroke="#b45309" stroke-width="2"/>
<text x="640" y="154" text-anchor="middle" font-weight="bold" fill="#1e293b">R2-A</text>
<!-- Incoming untagged packet -->
<line x1="10" y1="195" x2="115" y2="195" stroke="#059669" stroke-width="2.5" marker-end="url(#arrow1)"/>
<rect x="2" y="168" width="95" height="22" rx="4" fill="#d1fae5"/>
<text x="50" y="183" text-anchor="middle" font-size="11" font-weight="600" fill="#065f46">Untagged flood</text>
<!-- Arrow R1-A to R1-B (tagged R1) -->
<line x1="260" y1="180" x2="375" y2="145" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow1b)"/>
<rect x="278" y="144" width="72" height="20" rx="4" fill="#dbeafe"/>
<text x="314" y="158" text-anchor="middle" font-size="11" font-weight="600" fill="#1e40af">tagged R1</text>
<!-- Arrow R1-A to R1-C (tagged R1) -->
<line x1="260" y1="210" x2="375" y2="250" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow1b)"/>
<rect x="278" y="234" width="72" height="20" rx="4" fill="#dbeafe"/>
<text x="314" y="248" text-anchor="middle" font-size="11" font-weight="600" fill="#1e40af">tagged R1</text>
<!-- Arrow R1-B to R2-A -->
<line x1="500" y1="138" x2="595" y2="145" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow1b)"/>
<!-- Check marks -->
<text x="510" y="138" font-size="18" fill="#059669">&#10004;</text>
<text x="510" y="262" font-size="18" fill="#059669">&#10004;</text>
<text x="682" y="152" font-size="18" fill="#059669">&#10004;</text>
<!-- Explanation -->
<rect x="40" y="350" width="520" height="24" rx="4" fill="#d1fae5"/>
<text x="50" y="367" font-size="12" fill="#065f46">R1-A stamps untagged packet with R1. All neighbours in R1 accept and forward it.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

76
diagram2_wrong_scope.svg Normal file
View file

@ -0,0 +1,76 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 420" font-family="system-ui, -apple-system, sans-serif" font-size="14">
<defs>
<marker id="arrow2" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#059669"/>
</marker>
<marker id="arrow2r" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#dc2626"/>
</marker>
<marker id="arrow2y" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#b45309"/>
</marker>
</defs>
<!-- Title -->
<text x="360" y="30" text-anchor="middle" font-size="16" font-weight="bold" fill="#1e293b">Scenario 2: Arbitrary scope (tagged as R2 instead of home region)</text>
<!-- R1 zone -->
<rect x="40" y="60" width="520" height="320" rx="16" fill="#fef2f2" stroke="#fca5a5" stroke-width="2" stroke-dasharray="8 4"/>
<text x="60" y="88" font-size="13" font-weight="600" fill="#dc2626">R1 zone</text>
<!-- R2 zone -->
<rect x="580" y="60" width="120" height="160" rx="16" fill="#fef3c7" stroke="#fbbf24" stroke-width="2" stroke-dasharray="8 4"/>
<text x="600" y="88" font-size="13" font-weight="600" fill="#b45309">R2 zone</text>
<!-- R1-A (scope=R2) -->
<rect x="120" y="160" width="140" height="70" rx="10" fill="#ffffff" stroke="#dc2626" stroke-width="2"/>
<text x="190" y="188" text-anchor="middle" font-weight="bold" fill="#1e293b">R1-A</text>
<text x="190" y="210" text-anchor="middle" font-size="12" fill="#dc2626">scope = R2</text>
<!-- R1-B -->
<rect x="380" y="110" width="120" height="56" rx="10" fill="#ffffff" stroke="#dc2626" stroke-width="2"/>
<text x="440" y="144" text-anchor="middle" font-weight="bold" fill="#1e293b">R1-B</text>
<!-- R1-C -->
<rect x="380" y="230" width="120" height="56" rx="10" fill="#ffffff" stroke="#dc2626" stroke-width="2"/>
<text x="440" y="264" text-anchor="middle" font-weight="bold" fill="#1e293b">R1-C</text>
<!-- R2-A -->
<rect x="600" y="120" width="80" height="56" rx="10" fill="#ffffff" stroke="#b45309" stroke-width="2"/>
<text x="640" y="154" text-anchor="middle" font-weight="bold" fill="#1e293b">R2-A</text>
<!-- Incoming untagged packet -->
<line x1="10" y1="195" x2="115" y2="195" stroke="#059669" stroke-width="2.5" marker-end="url(#arrow2)"/>
<rect x="2" y="168" width="95" height="22" rx="4" fill="#d1fae5"/>
<text x="50" y="183" text-anchor="middle" font-size="11" font-weight="600" fill="#065f46">Untagged flood</text>
<!-- Arrow R1-A to R1-B (tagged R2 - BLOCKED) -->
<line x1="260" y1="180" x2="375" y2="145" stroke="#dc2626" stroke-width="2" marker-end="url(#arrow2r)"/>
<rect x="278" y="144" width="72" height="20" rx="4" fill="#fee2e2"/>
<text x="314" y="158" text-anchor="middle" font-size="11" font-weight="600" fill="#991b1b">tagged R2</text>
<!-- Arrow R1-A to R1-C (tagged R2 - BLOCKED) -->
<line x1="260" y1="210" x2="375" y2="250" stroke="#dc2626" stroke-width="2" marker-end="url(#arrow2r)"/>
<rect x="278" y="234" width="72" height="20" rx="4" fill="#fee2e2"/>
<text x="314" y="248" text-anchor="middle" font-size="11" font-weight="600" fill="#991b1b">tagged R2</text>
<!-- Arrow R1-A to R2-A (tagged R2 - accepted) -->
<line x1="260" y1="175" x2="595" y2="145" stroke="#b45309" stroke-width="2" marker-end="url(#arrow2y)"/>
<rect x="400" y="152" width="72" height="20" rx="4" fill="#fef3c7"/>
<text x="436" y="166" text-anchor="middle" font-size="11" font-weight="600" fill="#92400e">tagged R2</text>
<!-- X marks for blocked -->
<text x="370" y="128" font-size="22" font-weight="bold" fill="#dc2626">&#10008;</text>
<text x="370" y="270" font-size="22" font-weight="bold" fill="#dc2626">&#10008;</text>
<!-- Check for R2-A -->
<text x="682" y="152" font-size="18" fill="#059669">&#10004;</text>
<!-- Blocked labels -->
<text x="490" y="128" font-size="11" fill="#dc2626" font-weight="600">DROPPED</text>
<text x="490" y="270" font-size="11" fill="#dc2626" font-weight="600">DROPPED</text>
<!-- Explanation -->
<rect x="40" y="350" width="520" height="40" rx="4" fill="#fee2e2"/>
<text x="50" y="367" font-size="12" fill="#991b1b">R1-A tags packet as R2. R1-B and R1-C drop it (wrong region).</text>
<text x="50" y="383" font-size="12" fill="#991b1b">Responses tagged R2 also can't route back through R1 neighbours.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -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 <state>`
**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 <value>`
**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

View file

@ -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 = &region_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 = &region_map.getWildcard();
recv_pkt_region = &region_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

View file

@ -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)

View file

@ -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");

View file

@ -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 {