mirror of
https://github.com/Colorado-Mesh/meshcore-bot-firmware.git
synced 2026-08-11 08:10:29 +00:00
forge: stabilize bot duplicate coordination
This commit is contained in:
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -56,10 +56,7 @@ jobs:
|
|||||||
- name: Enumerate companion USB+BLE environments and shard
|
- name: Enumerate companion USB+BLE environments and shard
|
||||||
id: shards
|
id: shards
|
||||||
run: |
|
run: |
|
||||||
envs=$(grep -rhE '^\[env:' vendor/MeshCore/variants/ \
|
envs=$(bash scripts/list-release-companion-envs.sh)
|
||||||
| sort -u \
|
|
||||||
| grep -E '_companion_radio_(usb|ble)\]$' \
|
|
||||||
| sed -e 's/\[env://' -e 's/\]//')
|
|
||||||
count=$(printf '%s\n' "$envs" | wc -l | tr -d ' ')
|
count=$(printf '%s\n' "$envs" | wc -l | tr -d ' ')
|
||||||
echo "env_count=$count" >> "$GITHUB_OUTPUT"
|
echo "env_count=$count" >> "$GITHUB_OUTPUT"
|
||||||
echo "Found $count companion USB+BLE environments."
|
echo "Found $count companion USB+BLE environments."
|
||||||
@@ -107,10 +104,7 @@ jobs:
|
|||||||
- name: Select envs for this shard
|
- name: Select envs for this shard
|
||||||
id: select
|
id: select
|
||||||
run: |
|
run: |
|
||||||
envs=$(grep -rhE '^\[env:' vendor/MeshCore/variants/ \
|
envs=$(bash scripts/list-release-companion-envs.sh)
|
||||||
| sort -u \
|
|
||||||
| grep -E '_companion_radio_(usb|ble)\]$' \
|
|
||||||
| sed -e 's/\[env://' -e 's/\]//')
|
|
||||||
shard_envs=$(printf '%s\n' "$envs" \
|
shard_envs=$(printf '%s\n' "$envs" \
|
||||||
| awk -v shard=${{ matrix.shard }} -v total=8 'NR%total==shard')
|
| awk -v shard=${{ matrix.shard }} -v total=8 'NR%total==shard')
|
||||||
printf 'envs<<EOF\n%s\nEOF\n' "$shard_envs" >> "$GITHUB_OUTPUT"
|
printf 'envs<<EOF\n%s\nEOF\n' "$shard_envs" >> "$GITHUB_OUTPUT"
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: cj-vana <cj@depth23.online>
|
||||||
|
Date: Mon, 18 May 2026 08:02:27 -0600
|
||||||
|
Subject: [PATCH] Stabilize bot diagnostics and duplicate suppression
|
||||||
|
|
||||||
|
---
|
||||||
|
examples/companion_radio/BotTypes.h | 9 ++-
|
||||||
|
examples/companion_radio/FirmwareBot.cpp | 49 +++++++------
|
||||||
|
examples/companion_radio/MyMesh.cpp | 48 +++++++++---
|
||||||
|
.../companion_radio/ResponseCoordinator.cpp | 73 +++++++++++++++----
|
||||||
|
.../companion_radio/ResponseCoordinator.h | 4 +
|
||||||
|
5 files changed, 136 insertions(+), 47 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h
|
||||||
|
index f3afa21f..a54949d9 100644
|
||||||
|
--- a/examples/companion_radio/BotTypes.h
|
||||||
|
+++ b/examples/companion_radio/BotTypes.h
|
||||||
|
@@ -32,6 +32,8 @@
|
||||||
|
#define BOT_RESPONSE_DELAY_JITTER_MILLIS 2200UL
|
||||||
|
#define BOT_RESPONSE_PENDING_TTL_MILLIS 95000UL
|
||||||
|
#define BOT_RESPONSE_RECENT_TTL_MILLIS 30000UL
|
||||||
|
+#define BOT_TIE_BREAK_SPREAD_MILLIS 900UL
|
||||||
|
+#define BOT_HOP_TIER_GUARD_MILLIS 600UL
|
||||||
|
#define BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL 0x01
|
||||||
|
#define BOT_SENDER_KEY_PREFIX_LEN 6
|
||||||
|
#define BOT_MIN_AUTH_SENDER_KEY_PREFIX_LEN 4
|
||||||
|
@@ -292,9 +294,12 @@ struct BotCoordinatorPending {
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BotCoordinatorRecent {
|
||||||
|
- bool active;
|
||||||
|
BotFingerprint response_fingerprint;
|
||||||
|
- uint32_t expires_at_millis;
|
||||||
|
+ uint32_t response_expires_at_millis;
|
||||||
|
+ uint32_t request_expires_at_millis;
|
||||||
|
+ uint16_t request_token;
|
||||||
|
+ bool active;
|
||||||
|
+ bool request_active;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BotCoordinatorReady {
|
||||||
|
diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp
|
||||||
|
index afd44d41..d256e61a 100644
|
||||||
|
--- a/examples/companion_radio/FirmwareBot.cpp
|
||||||
|
+++ b/examples/companion_radio/FirmwareBot.cpp
|
||||||
|
@@ -320,11 +320,7 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com
|
||||||
|
|
||||||
|
const char* sender = message.sender_name[0] ? message.sender_name : "unknown";
|
||||||
|
int n;
|
||||||
|
- if (message.channel_kind == BOT_CHANNEL_DM) {
|
||||||
|
- n = snprintf(output, output_len, "@[%s] direct", sender);
|
||||||
|
- } else {
|
||||||
|
- n = snprintf(output, output_len, "@[%s]", sender);
|
||||||
|
- }
|
||||||
|
+ n = snprintf(output, output_len, "@[%s]", sender);
|
||||||
|
if (n < 0) {
|
||||||
|
output[0] = 0;
|
||||||
|
return BOT_WRITE_NO_SPACE;
|
||||||
|
@@ -370,7 +366,7 @@ BotFingerprint fingerprintFor(const BotMessage& message) {
|
||||||
|
hash = fnv1aUpdateChannel(hash, message);
|
||||||
|
hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, sizeof(message.sender_key_prefix));
|
||||||
|
hash = fnv1aUpdateTextLower(hash, message.sender_name, boundedStrLen(message.sender_name, sizeof(message.sender_name)));
|
||||||
|
- hash = fnv1aUpdateU32(hash, message.sender_timestamp);
|
||||||
|
+ if (message.channel_kind == BOT_CHANNEL_DM) hash = fnv1aUpdateU32(hash, message.sender_timestamp);
|
||||||
|
|
||||||
|
char normalized[BOT_MAX_TEXT_LEN + 1];
|
||||||
|
size_t normalized_len = 0;
|
||||||
|
@@ -381,6 +377,24 @@ BotFingerprint fingerprintFor(const BotMessage& message) {
|
||||||
|
return fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
+static int hexNibble(char c);
|
||||||
|
+
|
||||||
|
+bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len) {
|
||||||
|
+ if (token) *token = 0;
|
||||||
|
+ if (prefix_len) *prefix_len = 0;
|
||||||
|
+ if (!text || text_len < 7) return false;
|
||||||
|
+ if (text[0] != '[' || text[5] != ']' || text[6] != ' ') return false;
|
||||||
|
+ uint16_t value = 0;
|
||||||
|
+ for (int i = 0; i < 4; i++) {
|
||||||
|
+ int n = hexNibble(text[1 + i]);
|
||||||
|
+ if (n < 0) return false;
|
||||||
|
+ value = (uint16_t)((value << 4) | (uint16_t)n);
|
||||||
|
+ }
|
||||||
|
+ if (token) *token = value;
|
||||||
|
+ if (prefix_len) *prefix_len = 7;
|
||||||
|
+ return true;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len) {
|
||||||
|
uint64_t hash = 1469598103934665603ULL;
|
||||||
|
hash = fnv1aUpdateChannel(hash, message);
|
||||||
|
@@ -389,6 +403,13 @@ BotFingerprint responseFingerprintFor(const BotMessage& message, const char* res
|
||||||
|
hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, message.sender_key_prefix_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
+ uint16_t token = 0;
|
||||||
|
+ size_t prefix_len = 0;
|
||||||
|
+ if (parseRequestTokenPrefix(response_text, response_text_len, &token, &prefix_len)) {
|
||||||
|
+ response_text += prefix_len;
|
||||||
|
+ response_text_len -= prefix_len;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
char normalized[BOT_MAX_RESPONSE_LEN + 1];
|
||||||
|
size_t normalized_len = 0;
|
||||||
|
normalizeText(response_text, response_text_len, normalized, sizeof(normalized), &normalized_len);
|
||||||
|
@@ -423,22 +444,6 @@ static int hexNibble(char c) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
-bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len) {
|
||||||
|
- if (token) *token = 0;
|
||||||
|
- if (prefix_len) *prefix_len = 0;
|
||||||
|
- if (!text || text_len < 7) return false; // "[XXXX] " = 7 chars minimum
|
||||||
|
- if (text[0] != '[' || text[5] != ']' || text[6] != ' ') return false;
|
||||||
|
- uint16_t value = 0;
|
||||||
|
- for (int i = 0; i < 4; i++) {
|
||||||
|
- int n = hexNibble(text[1 + i]);
|
||||||
|
- if (n < 0) return false;
|
||||||
|
- value = (uint16_t)((value << 4) | (uint16_t)n);
|
||||||
|
- }
|
||||||
|
- if (token) *token = value;
|
||||||
|
- if (prefix_len) *prefix_len = 7;
|
||||||
|
- return true;
|
||||||
|
-}
|
||||||
|
-
|
||||||
|
BotWriteResult prependRequestToken(BotFingerprint request_fingerprint, char* text, size_t text_len, size_t buf_len,
|
||||||
|
size_t* new_len) {
|
||||||
|
if (new_len) *new_len = text_len;
|
||||||
|
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
||||||
|
index 9121d42c..5d333328 100644
|
||||||
|
--- a/examples/companion_radio/MyMesh.cpp
|
||||||
|
+++ b/examples/companion_radio/MyMesh.cpp
|
||||||
|
@@ -120,9 +120,6 @@
|
||||||
|
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
|
||||||
|
#define LAZY_CONTACTS_WRITE_DELAY 5000
|
||||||
|
|
||||||
|
-#if CMESH_BOT_ENABLED
|
||||||
|
-#endif
|
||||||
|
-
|
||||||
|
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
|
||||||
|
|
||||||
|
#if CMESH_BOT_ENABLED
|
||||||
|
@@ -1072,7 +1069,12 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti
|
||||||
|
if (rssi < -32768.0f) rssi = -32768.0f;
|
||||||
|
recordBotNeighbor(from.id.pub_key, (int16_t)rssi, (int8_t)(packet->getSNR() * 4));
|
||||||
|
}
|
||||||
|
- if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) {
|
||||||
|
+ if (packet && packet->isRouteFlood() && packet->path_len <= 0xFF && mesh::Packet::isValidPathLen((uint8_t)packet->path_len)) {
|
||||||
|
+ message.path_len = (uint8_t)packet->path_len;
|
||||||
|
+ message.path_hash_size = packet->getPathHashSize();
|
||||||
|
+ message.path_hash_count = packet->getPathHashCount();
|
||||||
|
+ message.path = packet->path;
|
||||||
|
+ } else if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) {
|
||||||
|
message.path_len = from.out_path_len;
|
||||||
|
message.path_hash_size = (from.out_path_len >> 6) + 1;
|
||||||
|
message.path_hash_count = from.out_path_len & 63;
|
||||||
|
@@ -1430,11 +1432,16 @@ bool MyMesh::dispatchBotTraceDirectLink(const BotMessage &message, const Contact
|
||||||
|
}
|
||||||
|
|
||||||
|
BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
|
||||||
|
+ uint32_t now = _ms->getMillis();
|
||||||
|
+ if (!direct_recipient && ResponseCoordinator::recentlyAnswered(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS,
|
||||||
|
+ FirmwareBot::requestToken(request_fingerprint), now)) {
|
||||||
|
+ bot_stats.suppressed_responses++;
|
||||||
|
+ return true;
|
||||||
|
+ }
|
||||||
|
BotFingerprint response_fingerprint =
|
||||||
|
FirmwareBot::responseFingerprintFor(message, final_response, final_response_len);
|
||||||
|
BotFingerprint fingerprint;
|
||||||
|
uint32_t due_at_millis = 0;
|
||||||
|
- uint32_t now = _ms->getMillis();
|
||||||
|
uint32_t bot_identity_seed;
|
||||||
|
memcpy(&bot_identity_seed, self_id.pub_key, sizeof(bot_identity_seed));
|
||||||
|
uint32_t jitter_seed = (uint32_t)request_fingerprint.value ^ bot_identity_seed;
|
||||||
|
@@ -1504,6 +1511,11 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo
|
||||||
|
if (!have_path || !botTracePathShapeValid(path_len, flags)) return false;
|
||||||
|
|
||||||
|
BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
|
||||||
|
+ if (!direct_recipient && ResponseCoordinator::recentlyAnswered(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS,
|
||||||
|
+ FirmwareBot::requestToken(request_fingerprint), _ms->getMillis())) {
|
||||||
|
+ bot_stats.suppressed_responses++;
|
||||||
|
+ return true;
|
||||||
|
+ }
|
||||||
|
char response[BOT_MAX_RESPONSE_LEN + 1];
|
||||||
|
size_t response_len = botFormatTraceSent(response, sizeof(response), (uint8_t)(path_len / hash_size));
|
||||||
|
char final_response[BOT_MAX_RESPONSE_LEN + 1];
|
||||||
|
@@ -1640,6 +1652,10 @@ bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text,
|
||||||
|
bot_stats.sent_messages++;
|
||||||
|
ResponseCoordinator::recordRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, response_fingerprint,
|
||||||
|
now_millis);
|
||||||
|
+ if (!pending.direct) {
|
||||||
|
+ ResponseCoordinator::recordRequestToken(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS,
|
||||||
|
+ FirmwareBot::requestToken(pending.request_fingerprint), now_millis);
|
||||||
|
+ }
|
||||||
|
} else {
|
||||||
|
bot_stats.send_failures++;
|
||||||
|
}
|
||||||
|
@@ -1747,6 +1763,7 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) {
|
||||||
|
size_t token_prefix_len = 0;
|
||||||
|
bool token_suppressed = false;
|
||||||
|
if (FirmwareBot::parseRequestTokenPrefix(message.text, message.text_len, &token, &token_prefix_len)) {
|
||||||
|
+ ResponseCoordinator::recordRequestToken(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, token, _ms->getMillis());
|
||||||
|
if (ResponseCoordinator::suppressByRequestToken(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, token)) {
|
||||||
|
token_suppressed = true;
|
||||||
|
}
|
||||||
|
@@ -1790,7 +1807,6 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
||||||
|
if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) bot_stats.parse_errors++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
-
|
||||||
|
if ((command.id != BOT_COMMAND_UNKNOWN && command.id != BOT_COMMAND_UNSUPPORTED &&
|
||||||
|
!BotPrefsCodec::commandEnabled(bot_prefs, command.id)) ||
|
||||||
|
FirmwareBot::isCommandOnCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis())) {
|
||||||
|
@@ -1869,6 +1885,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
||||||
|
}
|
||||||
|
|
||||||
|
BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
|
||||||
|
+ if (!direct_recipient && ResponseCoordinator::recentlyAnswered(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS,
|
||||||
|
+ FirmwareBot::requestToken(request_fingerprint), _ms->getMillis())) {
|
||||||
|
+ bot_stats.suppressed_responses++;
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
BotFingerprint response_fingerprint = FirmwareBot::responseFingerprintFor(message, final_response, final_response_len);
|
||||||
|
BotFingerprint fingerprint;
|
||||||
|
uint32_t due_at_millis = 0;
|
||||||
|
@@ -1904,7 +1925,6 @@ void MyMesh::sendQueuedBotResponses() {
|
||||||
|
while (true) {
|
||||||
|
BotCoordinatorReady ready = ResponseCoordinator::poll(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, now);
|
||||||
|
if (ready.result == BOT_COORDINATOR_READY_NONE) return;
|
||||||
|
-
|
||||||
|
if (ready.result == BOT_COORDINATOR_READY_SUPPRESSED) {
|
||||||
|
bot_stats.suppressed_responses++;
|
||||||
|
} else if (ready.result == BOT_COORDINATOR_READY_EXPIRED) {
|
||||||
|
@@ -1941,6 +1961,12 @@ void MyMesh::sendQueuedBotResponses() {
|
||||||
|
pending->active = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
+ if (!pending->direct && ResponseCoordinator::recentlyAnswered(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS,
|
||||||
|
+ FirmwareBot::requestToken(ready.request_fingerprint), now)) {
|
||||||
|
+ bot_stats.suppressed_responses++;
|
||||||
|
+ pending->active = false;
|
||||||
|
+ continue;
|
||||||
|
+ }
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
|
if (pending->direct) {
|
||||||
|
@@ -1960,15 +1986,19 @@ void MyMesh::sendQueuedBotResponses() {
|
||||||
|
}
|
||||||
|
} else if (pending->channel_idx != 0xFF) {
|
||||||
|
ChannelDetails channel;
|
||||||
|
+ bool have_ch = getChannel(pending->channel_idx, channel);
|
||||||
|
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||||
|
- success = getChannel(pending->channel_idx, channel) &&
|
||||||
|
- sendGroupMessage(timestamp, channel.channel, _prefs.node_name, pending->text, pending->text_len);
|
||||||
|
+ success = have_ch && sendGroupMessage(timestamp, channel.channel, _prefs.node_name, pending->text, pending->text_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
bot_stats.sent_messages++;
|
||||||
|
ResponseCoordinator::recordRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS,
|
||||||
|
pending->response_fingerprint, now);
|
||||||
|
+ if (!pending->direct) {
|
||||||
|
+ ResponseCoordinator::recordRequestToken(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS,
|
||||||
|
+ FirmwareBot::requestToken(pending->request_fingerprint), now);
|
||||||
|
+ }
|
||||||
|
} else {
|
||||||
|
bot_stats.send_failures++;
|
||||||
|
}
|
||||||
|
diff --git a/examples/companion_radio/ResponseCoordinator.cpp b/examples/companion_radio/ResponseCoordinator.cpp
|
||||||
|
index 837b3f12..888cdf49 100644
|
||||||
|
--- a/examples/companion_radio/ResponseCoordinator.cpp
|
||||||
|
+++ b/examples/companion_radio/ResponseCoordinator.cpp
|
||||||
|
@@ -14,6 +14,10 @@ bool sameFingerprint(BotFingerprint a, BotFingerprint b) {
|
||||||
|
return a.value == b.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
+uint16_t requestToken(BotFingerprint fingerprint) {
|
||||||
|
+ return (uint16_t)(fingerprint.value & 0xFFFFu);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
uint32_t channelDelayBias(BotChannelKind kind) {
|
||||||
|
if (kind == BOT_CHANNEL_DM) return 0;
|
||||||
|
if (kind == BOT_CHANNEL_BOT) return 200;
|
||||||
|
@@ -21,13 +25,11 @@ uint32_t channelDelayBias(BotChannelKind kind) {
|
||||||
|
return 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
-uint32_t hopDelayBias(BotChannelKind kind, uint8_t path_hash_count, uint16_t hop_step_millis) {
|
||||||
|
+uint32_t hopDelayBias(BotChannelKind kind, uint8_t path_hash_count, uint16_t hop_step_millis, uint16_t jitter_millis) {
|
||||||
|
if (kind == BOT_CHANNEL_DM) return 0;
|
||||||
|
- // Linear + quadratic growth: gap between consecutive hops widens with hop
|
||||||
|
- // count, so a far bot always has more slack than a near bot to receive
|
||||||
|
- // the near bot's reply before its own scheduled fire time.
|
||||||
|
uint32_t hop = (uint32_t)path_hash_count;
|
||||||
|
- uint32_t bias = hop * (uint32_t)hop_step_millis + hop * hop * BOT_HOP_GROW_MILLIS;
|
||||||
|
+ uint32_t tier_step = (uint32_t)hop_step_millis + (uint32_t)jitter_millis + BOT_TIE_BREAK_SPREAD_MILLIS + BOT_HOP_TIER_GUARD_MILLIS;
|
||||||
|
+ uint32_t bias = hop * tier_step + hop * hop * BOT_HOP_GROW_MILLIS;
|
||||||
|
if (bias > BOT_HOP_BIAS_MAX_MILLIS) bias = BOT_HOP_BIAS_MAX_MILLIS;
|
||||||
|
return bias;
|
||||||
|
}
|
||||||
|
@@ -37,7 +39,7 @@ uint32_t tieBreakBias(BotFingerprint request_fingerprint, uint32_t bot_identity_
|
||||||
|
mixed ^= mixed >> 16;
|
||||||
|
mixed *= 0x7feb352dUL;
|
||||||
|
mixed ^= mixed >> 15;
|
||||||
|
- return mixed % 900UL;
|
||||||
|
+ return mixed % BOT_TIE_BREAK_SPREAD_MILLIS;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t queueDelayBias(uint8_t queue_depth) {
|
||||||
|
@@ -82,7 +84,7 @@ uint32_t responseDelayMillis(const BotMessage& message, BotCommandId command_id,
|
||||||
|
(void)command_id;
|
||||||
|
uint32_t jitter = jitter_millis ? jitter_seed % jitter_millis : 0;
|
||||||
|
return (uint32_t)base_delay_millis + channelDelayBias(message.channel_kind) +
|
||||||
|
- hopDelayBias(message.channel_kind, message.path_hash_count, hop_step_millis) +
|
||||||
|
+ hopDelayBias(message.channel_kind, message.path_hash_count, hop_step_millis, jitter_millis) +
|
||||||
|
queueDelayBias(queue_depth) + tieBreakBias(request_fingerprint, bot_identity_seed) + jitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -163,7 +165,7 @@ bool suppressByRequestToken(BotCoordinatorPending pending[], size_t pending_coun
|
||||||
|
for (size_t i = 0; i < pending_count; i++) {
|
||||||
|
if (!pending[i].active) continue;
|
||||||
|
if (pending[i].request_fingerprint.value == 0) continue;
|
||||||
|
- uint16_t token = (uint16_t)(pending[i].request_fingerprint.value & 0xFFFFu);
|
||||||
|
+ uint16_t token = requestToken(pending[i].request_fingerprint);
|
||||||
|
if (token == request_token) {
|
||||||
|
pending[i].suppressed = true;
|
||||||
|
suppressed = true;
|
||||||
|
@@ -228,12 +230,14 @@ void recordRecent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerp
|
||||||
|
slot = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
- if (slot == recent_count && (!recent[i].active || millisDue(now_millis, recent[i].expires_at_millis))) slot = i;
|
||||||
|
+ bool expired_response = !recent[i].active || recent[i].response_fingerprint.value == 0 || millisDue(now_millis, recent[i].response_expires_at_millis);
|
||||||
|
+ bool expired_request = !recent[i].active || !recent[i].request_active || millisDue(now_millis, recent[i].request_expires_at_millis);
|
||||||
|
+ if (slot == recent_count && expired_response && expired_request) slot = i;
|
||||||
|
}
|
||||||
|
if (slot == recent_count) slot = 0;
|
||||||
|
recent[slot].active = true;
|
||||||
|
recent[slot].response_fingerprint = response_fingerprint;
|
||||||
|
- recent[slot].expires_at_millis = now_millis + BOT_RESPONSE_RECENT_TTL_MILLIS;
|
||||||
|
+ recent[slot].response_expires_at_millis = now_millis + BOT_RESPONSE_RECENT_TTL_MILLIS;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool recentlySent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint,
|
||||||
|
@@ -241,11 +245,52 @@ bool recentlySent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerp
|
||||||
|
if (!recent || response_fingerprint.value == 0) return false;
|
||||||
|
for (size_t i = 0; i < recent_count; i++) {
|
||||||
|
if (!recent[i].active) continue;
|
||||||
|
- if (millisDue(now_millis, recent[i].expires_at_millis)) {
|
||||||
|
- recent[i].active = false;
|
||||||
|
- continue;
|
||||||
|
+ if (millisDue(now_millis, recent[i].response_expires_at_millis)) {
|
||||||
|
+ recent[i].response_fingerprint.value = 0;
|
||||||
|
+ }
|
||||||
|
+ if (recent[i].response_fingerprint.value != 0 && sameFingerprint(recent[i].response_fingerprint, response_fingerprint)) return true;
|
||||||
|
+ if (recent[i].response_fingerprint.value == 0 && !recent[i].request_active) recent[i].active = false;
|
||||||
|
+ }
|
||||||
|
+ return false;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+void recordRequestToken(BotCoordinatorRecent recent[], size_t recent_count, uint16_t request_token,
|
||||||
|
+ uint32_t now_millis) {
|
||||||
|
+ if (!recent || recent_count == 0) return;
|
||||||
|
+
|
||||||
|
+ size_t slot = recent_count;
|
||||||
|
+ for (size_t i = 0; i < recent_count; i++) {
|
||||||
|
+ if (recent[i].active && millisDue(now_millis, recent[i].response_expires_at_millis)) {
|
||||||
|
+ recent[i].response_fingerprint.value = 0;
|
||||||
|
+ }
|
||||||
|
+ if (recent[i].active && recent[i].request_active && millisDue(now_millis, recent[i].request_expires_at_millis)) {
|
||||||
|
+ recent[i].request_active = false;
|
||||||
|
+ }
|
||||||
|
+ if (recent[i].active && recent[i].request_active && recent[i].request_token == request_token) {
|
||||||
|
+ slot = i;
|
||||||
|
+ break;
|
||||||
|
+ }
|
||||||
|
+ bool expired_response = !recent[i].active || recent[i].response_fingerprint.value == 0;
|
||||||
|
+ bool expired_request = !recent[i].active || !recent[i].request_active;
|
||||||
|
+ if (slot == recent_count && expired_response && expired_request) slot = i;
|
||||||
|
+ }
|
||||||
|
+ if (slot == recent_count) slot = 0;
|
||||||
|
+ recent[slot].active = true;
|
||||||
|
+ recent[slot].request_active = true;
|
||||||
|
+ recent[slot].request_token = request_token;
|
||||||
|
+ recent[slot].request_expires_at_millis = now_millis + BOT_RESPONSE_RECENT_TTL_MILLIS;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+bool recentlyAnswered(BotCoordinatorRecent recent[], size_t recent_count, uint16_t request_token,
|
||||||
|
+ uint32_t now_millis) {
|
||||||
|
+ if (!recent) return false;
|
||||||
|
+ for (size_t i = 0; i < recent_count; i++) {
|
||||||
|
+ if (!recent[i].active) continue;
|
||||||
|
+ if (recent[i].request_active && millisDue(now_millis, recent[i].request_expires_at_millis)) {
|
||||||
|
+ recent[i].request_active = false;
|
||||||
|
}
|
||||||
|
- if (sameFingerprint(recent[i].response_fingerprint, response_fingerprint)) return true;
|
||||||
|
+ if (recent[i].request_active && recent[i].request_token == request_token) return true;
|
||||||
|
+ if (recent[i].response_fingerprint.value == 0 && !recent[i].request_active) recent[i].active = false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
diff --git a/examples/companion_radio/ResponseCoordinator.h b/examples/companion_radio/ResponseCoordinator.h
|
||||||
|
index 7e053b44..c1b5ca04 100644
|
||||||
|
--- a/examples/companion_radio/ResponseCoordinator.h
|
||||||
|
+++ b/examples/companion_radio/ResponseCoordinator.h
|
||||||
|
@@ -39,5 +39,9 @@ void recordRecent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerp
|
||||||
|
uint32_t now_millis);
|
||||||
|
bool recentlySent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint,
|
||||||
|
uint32_t now_millis);
|
||||||
|
+void recordRequestToken(BotCoordinatorRecent recent[], size_t recent_count, uint16_t request_token,
|
||||||
|
+ uint32_t now_millis);
|
||||||
|
+bool recentlyAnswered(BotCoordinatorRecent recent[], size_t recent_count, uint16_t request_token,
|
||||||
|
+ uint32_t now_millis);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
||||||
From: cj-vana <cj@depth23.online>
|
|
||||||
Date: Sun, 17 May 2026 12:06:00 -0600
|
|
||||||
Subject: [PATCH 14/14] Stabilize firmware bot response tokens
|
|
||||||
|
|
||||||
---
|
|
||||||
examples/companion_radio/FirmwareBot.cpp | 52 ++++++++++++------
|
|
||||||
examples/companion_radio/FirmwareBot.h | 1 +
|
|
||||||
examples/companion_radio/MyMesh.cpp | 67 +++++++++++++++++++++---
|
|
||||||
examples/companion_radio/MyMesh.h | 1 +
|
|
||||||
4 files changed, 96 insertions(+), 25 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp
|
|
||||||
index afd44d41..21312c55 100644
|
|
||||||
--- a/examples/companion_radio/FirmwareBot.cpp
|
|
||||||
+++ b/examples/companion_radio/FirmwareBot.cpp
|
|
||||||
@@ -366,8 +366,17 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com
|
|
||||||
}
|
|
||||||
|
|
||||||
BotFingerprint fingerprintFor(const BotMessage& message) {
|
|
||||||
+ return fingerprintFor(message, NULL, 0);
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+BotFingerprint fingerprintFor(const BotMessage& message, const uint8_t* channel_id, size_t channel_id_len) {
|
|
||||||
uint64_t hash = 1469598103934665603ULL;
|
|
||||||
- hash = fnv1aUpdateChannel(hash, message);
|
|
||||||
+ if (message.channel_kind == BOT_CHANNEL_DM || !channel_id || channel_id_len == 0) {
|
|
||||||
+ hash = fnv1aUpdateChannel(hash, message);
|
|
||||||
+ } else {
|
|
||||||
+ hash = fnv1aUpdate(hash, 0xC7);
|
|
||||||
+ hash = fnv1aUpdateBytes(hash, channel_id, channel_id_len);
|
|
||||||
+ }
|
|
||||||
hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, sizeof(message.sender_key_prefix));
|
|
||||||
hash = fnv1aUpdateTextLower(hash, message.sender_name, boundedStrLen(message.sender_name, sizeof(message.sender_name)));
|
|
||||||
hash = fnv1aUpdateU32(hash, message.sender_timestamp);
|
|
||||||
@@ -381,6 +390,24 @@ BotFingerprint fingerprintFor(const BotMessage& message) {
|
|
||||||
return fingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
+static int hexNibble(char c);
|
|
||||||
+
|
|
||||||
+bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len) {
|
|
||||||
+ if (token) *token = 0;
|
|
||||||
+ if (prefix_len) *prefix_len = 0;
|
|
||||||
+ if (!text || text_len < 7) return false;
|
|
||||||
+ if (text[0] != '[' || text[5] != ']' || text[6] != ' ') return false;
|
|
||||||
+ uint16_t value = 0;
|
|
||||||
+ for (int i = 0; i < 4; i++) {
|
|
||||||
+ int n = hexNibble(text[1 + i]);
|
|
||||||
+ if (n < 0) return false;
|
|
||||||
+ value = (uint16_t)((value << 4) | (uint16_t)n);
|
|
||||||
+ }
|
|
||||||
+ if (token) *token = value;
|
|
||||||
+ if (prefix_len) *prefix_len = 7;
|
|
||||||
+ return true;
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len) {
|
|
||||||
uint64_t hash = 1469598103934665603ULL;
|
|
||||||
hash = fnv1aUpdateChannel(hash, message);
|
|
||||||
@@ -389,6 +416,13 @@ BotFingerprint responseFingerprintFor(const BotMessage& message, const char* res
|
|
||||||
hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, message.sender_key_prefix_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
+ uint16_t token = 0;
|
|
||||||
+ size_t prefix_len = 0;
|
|
||||||
+ if (parseRequestTokenPrefix(response_text, response_text_len, &token, &prefix_len)) {
|
|
||||||
+ response_text += prefix_len;
|
|
||||||
+ response_text_len -= prefix_len;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
char normalized[BOT_MAX_RESPONSE_LEN + 1];
|
|
||||||
size_t normalized_len = 0;
|
|
||||||
normalizeText(response_text, response_text_len, normalized, sizeof(normalized), &normalized_len);
|
|
||||||
@@ -423,22 +457,6 @@ static int hexNibble(char c) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
-bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len) {
|
|
||||||
- if (token) *token = 0;
|
|
||||||
- if (prefix_len) *prefix_len = 0;
|
|
||||||
- if (!text || text_len < 7) return false; // "[XXXX] " = 7 chars minimum
|
|
||||||
- if (text[0] != '[' || text[5] != ']' || text[6] != ' ') return false;
|
|
||||||
- uint16_t value = 0;
|
|
||||||
- for (int i = 0; i < 4; i++) {
|
|
||||||
- int n = hexNibble(text[1 + i]);
|
|
||||||
- if (n < 0) return false;
|
|
||||||
- value = (uint16_t)((value << 4) | (uint16_t)n);
|
|
||||||
- }
|
|
||||||
- if (token) *token = value;
|
|
||||||
- if (prefix_len) *prefix_len = 7;
|
|
||||||
- return true;
|
|
||||||
-}
|
|
||||||
-
|
|
||||||
BotWriteResult prependRequestToken(BotFingerprint request_fingerprint, char* text, size_t text_len, size_t buf_len,
|
|
||||||
size_t* new_len) {
|
|
||||||
if (new_len) *new_len = text_len;
|
|
||||||
diff --git a/examples/companion_radio/FirmwareBot.h b/examples/companion_radio/FirmwareBot.h
|
|
||||||
index 708432a2..f1726712 100644
|
|
||||||
--- a/examples/companion_radio/FirmwareBot.h
|
|
||||||
+++ b/examples/companion_radio/FirmwareBot.h
|
|
||||||
@@ -17,6 +17,7 @@ BotWriteResult writeResponseForChannel(BotChannelKind channel_kind, bool allow_p
|
|
||||||
BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& command, char* output, size_t output_len,
|
|
||||||
size_t* written);
|
|
||||||
BotFingerprint fingerprintFor(const BotMessage& message);
|
|
||||||
+BotFingerprint fingerprintFor(const BotMessage& message, const uint8_t* channel_id, size_t channel_id_len);
|
|
||||||
BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len);
|
|
||||||
uint16_t requestToken(BotFingerprint request_fingerprint);
|
|
||||||
void formatRequestToken(uint16_t token, char out[5]);
|
|
||||||
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
|
||||||
index 9121d42c..1a7ddb7a 100644
|
|
||||||
--- a/examples/companion_radio/MyMesh.cpp
|
|
||||||
+++ b/examples/companion_radio/MyMesh.cpp
|
|
||||||
@@ -121,6 +121,16 @@
|
|
||||||
#define LAZY_CONTACTS_WRITE_DELAY 5000
|
|
||||||
|
|
||||||
#if CMESH_BOT_ENABLED
|
|
||||||
+#ifndef CMESH_BOT_DEBUG
|
|
||||||
+#define CMESH_BOT_DEBUG 0
|
|
||||||
+#endif
|
|
||||||
+#if CMESH_BOT_DEBUG
|
|
||||||
+#define BOT_DEBUG_PRINTF(...) Serial.printf(__VA_ARGS__)
|
|
||||||
+#define BOT_DEBUG_PRINTLN(value) Serial.println(value)
|
|
||||||
+#else
|
|
||||||
+#define BOT_DEBUG_PRINTF(...)
|
|
||||||
+#define BOT_DEBUG_PRINTLN(value)
|
|
||||||
+#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
|
|
||||||
@@ -1055,6 +1065,7 @@ bool MyMesh::handleBotCLI(const char *args) {
|
|
||||||
|
|
||||||
void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_timestamp, const uint8_t *sender_prefix,
|
|
||||||
size_t sender_prefix_len, const char *text, const mesh::Packet *packet) {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: DM rx from=%s text=%.40s enabled=%d\r\n", from.name, text, bot_prefs.enabled);
|
|
||||||
BotMessage message;
|
|
||||||
memset(&message, 0, sizeof(message));
|
|
||||||
message.channel_kind = BotPolicy::classifyChannel(NULL, 0, true, bot_prefs);
|
|
||||||
@@ -1090,6 +1101,7 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti
|
|
||||||
|
|
||||||
void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_name, const char *text,
|
|
||||||
uint32_t sender_timestamp, const mesh::Packet *packet) {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: CH rx ch=%s text=%.40s enabled=%d\r\n", channel_name ? channel_name : "?", text, bot_prefs.enabled);
|
|
||||||
BotMessage message;
|
|
||||||
memset(&message, 0, sizeof(message));
|
|
||||||
size_t channel_len = botBoundedStrLen(channel_name, BOT_MAX_CHANNEL_NAME_LEN);
|
|
||||||
@@ -1429,7 +1441,7 @@ bool MyMesh::dispatchBotTraceDirectLink(const BotMessage &message, const Contact
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
|
|
||||||
+ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx);
|
|
||||||
BotFingerprint response_fingerprint =
|
|
||||||
FirmwareBot::responseFingerprintFor(message, final_response, final_response_len);
|
|
||||||
BotFingerprint fingerprint;
|
|
||||||
@@ -1503,7 +1515,7 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo
|
|
||||||
}
|
|
||||||
if (!have_path || !botTracePathShapeValid(path_len, flags)) return false;
|
|
||||||
|
|
||||||
- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
|
|
||||||
+ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx);
|
|
||||||
char response[BOT_MAX_RESPONSE_LEN + 1];
|
|
||||||
size_t response_len = botFormatTraceSent(response, sizeof(response), (uint8_t)(path_len / hash_size));
|
|
||||||
char final_response[BOT_MAX_RESPONSE_LEN + 1];
|
|
||||||
@@ -1737,6 +1749,16 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
+BotFingerprint MyMesh::botRequestFingerprintFor(const BotMessage &message, uint8_t channel_idx) {
|
|
||||||
+ if (message.channel_kind != BOT_CHANNEL_DM && channel_idx != 0xFF) {
|
|
||||||
+ ChannelDetails channel;
|
|
||||||
+ if (getChannel(channel_idx, channel)) {
|
|
||||||
+ return FirmwareBot::fingerprintFor(message, channel.channel.hash, sizeof(channel.channel.hash));
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ return FirmwareBot::fingerprintFor(message);
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
bool MyMesh::observeBotGroupResponse(const BotMessage &message) {
|
|
||||||
if (!BotPolicy::isPrefixlessCommandAllowed(message.channel_kind)) return false;
|
|
||||||
|
|
||||||
@@ -1749,6 +1771,9 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) {
|
|
||||||
if (FirmwareBot::parseRequestTokenPrefix(message.text, message.text_len, &token, &token_prefix_len)) {
|
|
||||||
if (ResponseCoordinator::suppressByRequestToken(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, token)) {
|
|
||||||
token_suppressed = true;
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: token suppress %04x\r\n", (unsigned)token);
|
|
||||||
+ } else {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: token miss %04x\r\n", (unsigned)token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1763,9 +1788,12 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx) {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: record kind=%d direct=%d enabled=%d\r\n", (int)message.channel_kind, direct_recipient != NULL, bot_prefs.enabled);
|
|
||||||
bot_stats.observed_messages++;
|
|
||||||
BotPolicyDecision decision = BotPolicy::decide(message.channel_kind);
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: policy decision=%d\r\n", (int)decision);
|
|
||||||
if (decision == BOT_POLICY_IGNORE) {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: IGNORE -> drop");
|
|
||||||
bot_stats.ignored_messages++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
@@ -1776,24 +1804,28 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bot_prefs.enabled) {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: bot disabled -> drop");
|
|
||||||
bot_stats.ignored_messages++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
- if (observeKnownBotResponse(message, direct_recipient != NULL)) return;
|
|
||||||
- if (!direct_recipient && observeBotGroupResponse(message)) return;
|
|
||||||
+ if (observeKnownBotResponse(message, direct_recipient != NULL)) { BOT_DEBUG_PRINTLN("CBOT-DBG: known-bot suppress"); return; }
|
|
||||||
+ if (!direct_recipient && observeBotGroupResponse(message)) { BOT_DEBUG_PRINTLN("CBOT-DBG: group-response suppress"); return; }
|
|
||||||
sendQueuedBotResponses();
|
|
||||||
|
|
||||||
BotCommand command;
|
|
||||||
if (!FirmwareBot::parseCommand(message.text, message.text_len, &command,
|
|
||||||
BotPolicy::isPrefixlessCommandAllowed(message.channel_kind))) {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: parseCommand FAILED text=%.40s prefixless=%d\r\n", message.text, BotPolicy::isPrefixlessCommandAllowed(message.channel_kind));
|
|
||||||
if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) bot_stats.parse_errors++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: parsed cmd.id=%d name=%s args=%.30s\r\n", (int)command.id, command.name, command.args);
|
|
||||||
|
|
||||||
if ((command.id != BOT_COMMAND_UNKNOWN && command.id != BOT_COMMAND_UNSUPPORTED &&
|
|
||||||
!BotPrefsCodec::commandEnabled(bot_prefs, command.id)) ||
|
|
||||||
FirmwareBot::isCommandOnCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis())) {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: cmd disabled or on cooldown -> drop");
|
|
||||||
bot_stats.ignored_messages++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
@@ -1868,7 +1900,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
|
|
||||||
+ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx);
|
|
||||||
BotFingerprint response_fingerprint = FirmwareBot::responseFingerprintFor(message, final_response, final_response_len);
|
|
||||||
BotFingerprint fingerprint;
|
|
||||||
uint32_t due_at_millis = 0;
|
|
||||||
@@ -1884,7 +1916,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
|
||||||
queue_depth, bot_prefs.normal_delay_ms,
|
|
||||||
bot_prefs.normal_jitter_ms, bot_prefs.hop_step_ms,
|
|
||||||
&fingerprint, &due_at_millis);
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: schedule result=%d due=%lu now=%lu reqfp=%08lx%08lx\r\n",
|
|
||||||
+ (int)schedule, (unsigned long)due_at_millis, (unsigned long)_ms->getMillis(),
|
|
||||||
+ (unsigned long)(request_fingerprint.value >> 32), (unsigned long)(request_fingerprint.value & 0xFFFFFFFFul));
|
|
||||||
if (schedule == BOT_COORDINATOR_NO_SPACE || schedule == BOT_COORDINATOR_NOT_NORMAL) {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: schedule FAILED -> drop");
|
|
||||||
bot_stats.send_failures++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
@@ -1894,8 +1930,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
|
||||||
FirmwareBot::recordCommandCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis(),
|
|
||||||
BOT_COMMAND_COOLDOWN_MILLIS);
|
|
||||||
if (!enqueueBotResponse(message, direct_recipient, channel_idx, final_response, final_response_len, fingerprint, response_fingerprint)) {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: enqueueBotResponse FAILED -> drop");
|
|
||||||
ResponseCoordinator::cancel(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint);
|
|
||||||
bot_stats.send_failures++;
|
|
||||||
+ } else {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: queued response, text=%.40s\r\n", final_response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1904,6 +1943,8 @@ void MyMesh::sendQueuedBotResponses() {
|
|
||||||
while (true) {
|
|
||||||
BotCoordinatorReady ready = ResponseCoordinator::poll(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, now);
|
|
||||||
if (ready.result == BOT_COORDINATOR_READY_NONE) return;
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: poll result=%d reqfp=%08lx\r\n", (int)ready.result,
|
|
||||||
+ (unsigned long)(ready.request_fingerprint.value & 0xFFFFFFFFul));
|
|
||||||
|
|
||||||
if (ready.result == BOT_COORDINATOR_READY_SUPPRESSED) {
|
|
||||||
bot_stats.suppressed_responses++;
|
|
||||||
@@ -1945,12 +1986,14 @@ void MyMesh::sendQueuedBotResponses() {
|
|
||||||
bool success = false;
|
|
||||||
if (pending->direct) {
|
|
||||||
ContactInfo *recipient = lookupContactByPubKey(pending->recipient_pub_key, PUB_KEY_SIZE);
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: send DM lookup=%s text=%.40s\r\n", recipient ? "FOUND" : "NULL", pending->text);
|
|
||||||
if (recipient) {
|
|
||||||
uint32_t expected_ack = 0;
|
|
||||||
uint32_t est_timeout = 0;
|
|
||||||
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
|
||||||
int result = sendMessage(*recipient, timestamp, 0, pending->text, expected_ack, est_timeout);
|
|
||||||
success = result != MSG_SEND_FAILED;
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: sendMessage result=%d success=%d\r\n", result, success);
|
|
||||||
if (success && expected_ack) {
|
|
||||||
expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis();
|
|
||||||
expected_ack_table[next_ack_idx].ack = expected_ack;
|
|
||||||
@@ -1960,9 +2003,11 @@ void MyMesh::sendQueuedBotResponses() {
|
|
||||||
}
|
|
||||||
} else if (pending->channel_idx != 0xFF) {
|
|
||||||
ChannelDetails channel;
|
|
||||||
+ bool have_ch = getChannel(pending->channel_idx, channel);
|
|
||||||
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
|
||||||
- success = getChannel(pending->channel_idx, channel) &&
|
|
||||||
- sendGroupMessage(timestamp, channel.channel, _prefs.node_name, pending->text, pending->text_len);
|
|
||||||
+ success = have_ch && sendGroupMessage(timestamp, channel.channel, _prefs.node_name, pending->text, pending->text_len);
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: send CH idx=%u have=%d success=%d text=%.40s\r\n",
|
|
||||||
+ (unsigned)pending->channel_idx, have_ch, success, pending->text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
@@ -1970,6 +2015,7 @@ void MyMesh::sendQueuedBotResponses() {
|
|
||||||
ResponseCoordinator::recordRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS,
|
|
||||||
pending->response_fingerprint, now);
|
|
||||||
} else {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: send FAILED -> bot stays silent");
|
|
||||||
bot_stats.send_failures++;
|
|
||||||
}
|
|
||||||
pending->active = false;
|
|
||||||
@@ -2521,9 +2567,14 @@ void MyMesh::begin(bool has_display) {
|
|
||||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
|
||||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
|
||||||
#if CMESH_BOT_ENABLED
|
|
||||||
- if (!_store->loadBotPrefs(bot_prefs)) bot_prefs.prefs_load_failures++;
|
|
||||||
+ bool loaded = _store->loadBotPrefs(bot_prefs);
|
|
||||||
+ if (!loaded) bot_prefs.prefs_load_failures++;
|
|
||||||
BotPrefsCodec::validate(bot_prefs);
|
|
||||||
applyBotPrefs();
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: boot: loaded=%d enabled=%d bot_ch=%s testing_ch=%s mask=0x%08lx delay=%u jitter=%u hop_step=%u\r\n",
|
|
||||||
+ loaded, bot_prefs.enabled, bot_prefs.bot_channel, bot_prefs.testing_channel,
|
|
||||||
+ (unsigned long)bot_prefs.command_mask, bot_prefs.normal_delay_ms, bot_prefs.normal_jitter_ms,
|
|
||||||
+ bot_prefs.hop_step_ms);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h
|
|
||||||
index 46c3ff3c..cfd402df 100644
|
|
||||||
--- a/examples/companion_radio/MyMesh.h
|
|
||||||
+++ b/examples/companion_radio/MyMesh.h
|
|
||||||
@@ -215,6 +215,7 @@ private:
|
|
||||||
void recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx);
|
|
||||||
bool observeKnownBotResponse(const BotMessage &message, bool authoritative_sender);
|
|
||||||
bool observeBotGroupResponse(const BotMessage &message);
|
|
||||||
+ BotFingerprint botRequestFingerprintFor(const BotMessage &message, uint8_t channel_idx);
|
|
||||||
void buildBotCommandContext(BotCommandContext &context, BotCommandId command_id);
|
|
||||||
bool enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx,
|
|
||||||
const char *text, size_t text_len, BotFingerprint request_fingerprint,
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
||||||
From: cj-vana <cj@depth23.online>
|
|
||||||
Date: Sun, 17 May 2026 21:07:25 -0600
|
|
||||||
Subject: [PATCH] Report flood paths in bot DM diagnostics
|
|
||||||
|
|
||||||
---
|
|
||||||
examples/companion_radio/FirmwareBot.cpp | 6 +-----
|
|
||||||
examples/companion_radio/MyMesh.cpp | 7 ++++++-
|
|
||||||
2 files changed, 7 insertions(+), 6 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp
|
|
||||||
index 21312c55..3b4a008e 100644
|
|
||||||
--- a/examples/companion_radio/FirmwareBot.cpp
|
|
||||||
+++ b/examples/companion_radio/FirmwareBot.cpp
|
|
||||||
@@ -320,11 +320,7 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com
|
|
||||||
|
|
||||||
const char* sender = message.sender_name[0] ? message.sender_name : "unknown";
|
|
||||||
int n;
|
|
||||||
- if (message.channel_kind == BOT_CHANNEL_DM) {
|
|
||||||
- n = snprintf(output, output_len, "@[%s] direct", sender);
|
|
||||||
- } else {
|
|
||||||
- n = snprintf(output, output_len, "@[%s]", sender);
|
|
||||||
- }
|
|
||||||
+ n = snprintf(output, output_len, "@[%s]", sender);
|
|
||||||
if (n < 0) {
|
|
||||||
output[0] = 0;
|
|
||||||
return BOT_WRITE_NO_SPACE;
|
|
||||||
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
|
||||||
index 1a7ddb7a..bfed7f98 100644
|
|
||||||
--- a/examples/companion_radio/MyMesh.cpp
|
|
||||||
+++ b/examples/companion_radio/MyMesh.cpp
|
|
||||||
@@ -1083,7 +1083,12 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti
|
|
||||||
if (rssi < -32768.0f) rssi = -32768.0f;
|
|
||||||
recordBotNeighbor(from.id.pub_key, (int16_t)rssi, (int8_t)(packet->getSNR() * 4));
|
|
||||||
}
|
|
||||||
- if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) {
|
|
||||||
+ if (packet && packet->isRouteFlood() && packet->path_len <= 0xFF && mesh::Packet::isValidPathLen((uint8_t)packet->path_len)) {
|
|
||||||
+ message.path_len = (uint8_t)packet->path_len;
|
|
||||||
+ message.path_hash_size = packet->getPathHashSize();
|
|
||||||
+ message.path_hash_count = packet->getPathHashCount();
|
|
||||||
+ message.path = packet->path;
|
|
||||||
+ } else if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) {
|
|
||||||
message.path_len = from.out_path_len;
|
|
||||||
message.path_hash_size = (from.out_path_len >> 6) + 1;
|
|
||||||
message.path_hash_count = from.out_path_len & 63;
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
||||||
From: cj-vana <cj@depth23.online>
|
|
||||||
Date: Sun, 17 May 2026 21:55:45 -0600
|
|
||||||
Subject: [PATCH] Stabilize group bot request tokens
|
|
||||||
|
|
||||||
---
|
|
||||||
examples/companion_radio/FirmwareBot.cpp | 51 ++++++++++--------
|
|
||||||
examples/companion_radio/MyMesh.cpp | 69 ++++++++++++++++++++----
|
|
||||||
2 files changed, 89 insertions(+), 31 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp
|
|
||||||
index e228b7c8..3d3ad96f 100644
|
|
||||||
--- a/examples/companion_radio/FirmwareBot.cpp
|
|
||||||
+++ b/examples/companion_radio/FirmwareBot.cpp
|
|
||||||
@@ -319,10 +319,8 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com
|
|
||||||
if (!output || output_len == 0) return BOT_WRITE_NO_SPACE;
|
|
||||||
|
|
||||||
const char* sender = message.sender_name[0] ? message.sender_name : "unknown";
|
|
||||||
- const char* connection = message.channel_kind == BOT_CHANNEL_DM
|
|
||||||
- ? "direct"
|
|
||||||
- : (message.channel_name[0] ? message.channel_name : "channel");
|
|
||||||
- int n = snprintf(output, output_len, "@[%s] %s", sender, connection);
|
|
||||||
+ int n;
|
|
||||||
+ n = snprintf(output, output_len, "@[%s]", sender);
|
|
||||||
if (n < 0) {
|
|
||||||
output[0] = 0;
|
|
||||||
return BOT_WRITE_NO_SPACE;
|
|
||||||
@@ -337,9 +335,9 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com
|
|
||||||
char path_block[40];
|
|
||||||
int path_n;
|
|
||||||
if (message.path_hash_count == 0 || message.path_hash_size == 0) {
|
|
||||||
- path_n = snprintf(path_block, sizeof(path_block), " | 0h SNR %s%d.%02d", sign, value / 4, (value % 4) * 25);
|
|
||||||
+ path_n = snprintf(path_block, sizeof(path_block), " | 0 hops, SNR %s%d.%02d", sign, value / 4, (value % 4) * 25);
|
|
||||||
} else {
|
|
||||||
- path_n = snprintf(path_block, sizeof(path_block), " | %uh@%uB SNR %s%d.%02d",
|
|
||||||
+ path_n = snprintf(path_block, sizeof(path_block), " | %u hops, %u-byte hashes, SNR %s%d.%02d",
|
|
||||||
(unsigned)message.path_hash_count, (unsigned)message.path_hash_size, sign, value / 4,
|
|
||||||
(value % 4) * 25);
|
|
||||||
}
|
|
||||||
@@ -379,6 +377,24 @@ BotFingerprint fingerprintFor(const BotMessage& message) {
|
|
||||||
return fingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
+static int hexNibble(char c);
|
|
||||||
+
|
|
||||||
+bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len) {
|
|
||||||
+ if (token) *token = 0;
|
|
||||||
+ if (prefix_len) *prefix_len = 0;
|
|
||||||
+ if (!text || text_len < 7) return false;
|
|
||||||
+ if (text[0] != '[' || text[5] != ']' || text[6] != ' ') return false;
|
|
||||||
+ uint16_t value = 0;
|
|
||||||
+ for (int i = 0; i < 4; i++) {
|
|
||||||
+ int n = hexNibble(text[1 + i]);
|
|
||||||
+ if (n < 0) return false;
|
|
||||||
+ value = (uint16_t)((value << 4) | (uint16_t)n);
|
|
||||||
+ }
|
|
||||||
+ if (token) *token = value;
|
|
||||||
+ if (prefix_len) *prefix_len = 7;
|
|
||||||
+ return true;
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len) {
|
|
||||||
uint64_t hash = 1469598103934665603ULL;
|
|
||||||
hash = fnv1aUpdateChannel(hash, message);
|
|
||||||
@@ -387,6 +403,13 @@ BotFingerprint responseFingerprintFor(const BotMessage& message, const char* res
|
|
||||||
hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, message.sender_key_prefix_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
+ uint16_t token = 0;
|
|
||||||
+ size_t prefix_len = 0;
|
|
||||||
+ if (parseRequestTokenPrefix(response_text, response_text_len, &token, &prefix_len)) {
|
|
||||||
+ response_text += prefix_len;
|
|
||||||
+ response_text_len -= prefix_len;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
char normalized[BOT_MAX_RESPONSE_LEN + 1];
|
|
||||||
size_t normalized_len = 0;
|
|
||||||
normalizeText(response_text, response_text_len, normalized, sizeof(normalized), &normalized_len);
|
|
||||||
@@ -421,22 +444,6 @@ static int hexNibble(char c) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
-bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len) {
|
|
||||||
- if (token) *token = 0;
|
|
||||||
- if (prefix_len) *prefix_len = 0;
|
|
||||||
- if (!text || text_len < 7) return false; // "[XXXX] " = 7 chars minimum
|
|
||||||
- if (text[0] != '[' || text[5] != ']' || text[6] != ' ') return false;
|
|
||||||
- uint16_t value = 0;
|
|
||||||
- for (int i = 0; i < 4; i++) {
|
|
||||||
- int n = hexNibble(text[1 + i]);
|
|
||||||
- if (n < 0) return false;
|
|
||||||
- value = (uint16_t)((value << 4) | (uint16_t)n);
|
|
||||||
- }
|
|
||||||
- if (token) *token = value;
|
|
||||||
- if (prefix_len) *prefix_len = 7;
|
|
||||||
- return true;
|
|
||||||
-}
|
|
||||||
-
|
|
||||||
BotWriteResult prependRequestToken(BotFingerprint request_fingerprint, char* text, size_t text_len, size_t buf_len,
|
|
||||||
size_t* new_len) {
|
|
||||||
if (new_len) *new_len = text_len;
|
|
||||||
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
|
||||||
index 9121d42c..9aad730d 100644
|
|
||||||
--- a/examples/companion_radio/MyMesh.cpp
|
|
||||||
+++ b/examples/companion_radio/MyMesh.cpp
|
|
||||||
@@ -121,6 +121,16 @@
|
|
||||||
#define LAZY_CONTACTS_WRITE_DELAY 5000
|
|
||||||
|
|
||||||
#if CMESH_BOT_ENABLED
|
|
||||||
+#ifndef CMESH_BOT_DEBUG
|
|
||||||
+#define CMESH_BOT_DEBUG 0
|
|
||||||
+#endif
|
|
||||||
+#if CMESH_BOT_DEBUG
|
|
||||||
+#define BOT_DEBUG_PRINTF(...) Serial.printf(__VA_ARGS__)
|
|
||||||
+#define BOT_DEBUG_PRINTLN(value) Serial.println(value)
|
|
||||||
+#else
|
|
||||||
+#define BOT_DEBUG_PRINTF(...)
|
|
||||||
+#define BOT_DEBUG_PRINTLN(value)
|
|
||||||
+#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
|
|
||||||
@@ -1055,6 +1065,7 @@ bool MyMesh::handleBotCLI(const char *args) {
|
|
||||||
|
|
||||||
void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_timestamp, const uint8_t *sender_prefix,
|
|
||||||
size_t sender_prefix_len, const char *text, const mesh::Packet *packet) {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: DM rx from=%s text=%.40s enabled=%d\r\n", from.name, text, bot_prefs.enabled);
|
|
||||||
BotMessage message;
|
|
||||||
memset(&message, 0, sizeof(message));
|
|
||||||
message.channel_kind = BotPolicy::classifyChannel(NULL, 0, true, bot_prefs);
|
|
||||||
@@ -1072,7 +1083,12 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti
|
|
||||||
if (rssi < -32768.0f) rssi = -32768.0f;
|
|
||||||
recordBotNeighbor(from.id.pub_key, (int16_t)rssi, (int8_t)(packet->getSNR() * 4));
|
|
||||||
}
|
|
||||||
- if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) {
|
|
||||||
+ if (packet && packet->isRouteFlood() && packet->path_len <= 0xFF && mesh::Packet::isValidPathLen((uint8_t)packet->path_len)) {
|
|
||||||
+ message.path_len = (uint8_t)packet->path_len;
|
|
||||||
+ message.path_hash_size = packet->getPathHashSize();
|
|
||||||
+ message.path_hash_count = packet->getPathHashCount();
|
|
||||||
+ message.path = packet->path;
|
|
||||||
+ } else if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) {
|
|
||||||
message.path_len = from.out_path_len;
|
|
||||||
message.path_hash_size = (from.out_path_len >> 6) + 1;
|
|
||||||
message.path_hash_count = from.out_path_len & 63;
|
|
||||||
@@ -1090,6 +1106,7 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti
|
|
||||||
|
|
||||||
void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_name, const char *text,
|
|
||||||
uint32_t sender_timestamp, const mesh::Packet *packet) {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: CH rx ch=%s text=%.40s enabled=%d\r\n", channel_name ? channel_name : "?", text, bot_prefs.enabled);
|
|
||||||
BotMessage message;
|
|
||||||
memset(&message, 0, sizeof(message));
|
|
||||||
size_t channel_len = botBoundedStrLen(channel_name, BOT_MAX_CHANNEL_NAME_LEN);
|
|
||||||
@@ -1429,7 +1446,7 @@ bool MyMesh::dispatchBotTraceDirectLink(const BotMessage &message, const Contact
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
|
|
||||||
+ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx);
|
|
||||||
BotFingerprint response_fingerprint =
|
|
||||||
FirmwareBot::responseFingerprintFor(message, final_response, final_response_len);
|
|
||||||
BotFingerprint fingerprint;
|
|
||||||
@@ -1503,7 +1520,7 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo
|
|
||||||
}
|
|
||||||
if (!have_path || !botTracePathShapeValid(path_len, flags)) return false;
|
|
||||||
|
|
||||||
- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
|
|
||||||
+ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx);
|
|
||||||
char response[BOT_MAX_RESPONSE_LEN + 1];
|
|
||||||
size_t response_len = botFormatTraceSent(response, sizeof(response), (uint8_t)(path_len / hash_size));
|
|
||||||
char final_response[BOT_MAX_RESPONSE_LEN + 1];
|
|
||||||
@@ -1737,6 +1754,11 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
+BotFingerprint MyMesh::botRequestFingerprintFor(const BotMessage &message, uint8_t channel_idx) {
|
|
||||||
+ (void)channel_idx;
|
|
||||||
+ return FirmwareBot::fingerprintFor(message);
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
bool MyMesh::observeBotGroupResponse(const BotMessage &message) {
|
|
||||||
if (!BotPolicy::isPrefixlessCommandAllowed(message.channel_kind)) return false;
|
|
||||||
|
|
||||||
@@ -1749,6 +1771,9 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) {
|
|
||||||
if (FirmwareBot::parseRequestTokenPrefix(message.text, message.text_len, &token, &token_prefix_len)) {
|
|
||||||
if (ResponseCoordinator::suppressByRequestToken(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, token)) {
|
|
||||||
token_suppressed = true;
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: token suppress %04x\r\n", (unsigned)token);
|
|
||||||
+ } else {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: token miss %04x\r\n", (unsigned)token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1763,9 +1788,12 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx) {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: record kind=%d direct=%d enabled=%d\r\n", (int)message.channel_kind, direct_recipient != NULL, bot_prefs.enabled);
|
|
||||||
bot_stats.observed_messages++;
|
|
||||||
BotPolicyDecision decision = BotPolicy::decide(message.channel_kind);
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: policy decision=%d\r\n", (int)decision);
|
|
||||||
if (decision == BOT_POLICY_IGNORE) {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: IGNORE -> drop");
|
|
||||||
bot_stats.ignored_messages++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
@@ -1776,24 +1804,28 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bot_prefs.enabled) {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: bot disabled -> drop");
|
|
||||||
bot_stats.ignored_messages++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
- if (observeKnownBotResponse(message, direct_recipient != NULL)) return;
|
|
||||||
- if (!direct_recipient && observeBotGroupResponse(message)) return;
|
|
||||||
+ if (observeKnownBotResponse(message, direct_recipient != NULL)) { BOT_DEBUG_PRINTLN("CBOT-DBG: known-bot suppress"); return; }
|
|
||||||
+ if (!direct_recipient && observeBotGroupResponse(message)) { BOT_DEBUG_PRINTLN("CBOT-DBG: group-response suppress"); return; }
|
|
||||||
sendQueuedBotResponses();
|
|
||||||
|
|
||||||
BotCommand command;
|
|
||||||
if (!FirmwareBot::parseCommand(message.text, message.text_len, &command,
|
|
||||||
BotPolicy::isPrefixlessCommandAllowed(message.channel_kind))) {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: parseCommand FAILED text=%.40s prefixless=%d\r\n", message.text, BotPolicy::isPrefixlessCommandAllowed(message.channel_kind));
|
|
||||||
if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) bot_stats.parse_errors++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: parsed cmd.id=%d name=%s args=%.30s\r\n", (int)command.id, command.name, command.args);
|
|
||||||
|
|
||||||
if ((command.id != BOT_COMMAND_UNKNOWN && command.id != BOT_COMMAND_UNSUPPORTED &&
|
|
||||||
!BotPrefsCodec::commandEnabled(bot_prefs, command.id)) ||
|
|
||||||
FirmwareBot::isCommandOnCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis())) {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: cmd disabled or on cooldown -> drop");
|
|
||||||
bot_stats.ignored_messages++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
@@ -1868,7 +1900,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
|
|
||||||
+ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx);
|
|
||||||
BotFingerprint response_fingerprint = FirmwareBot::responseFingerprintFor(message, final_response, final_response_len);
|
|
||||||
BotFingerprint fingerprint;
|
|
||||||
uint32_t due_at_millis = 0;
|
|
||||||
@@ -1884,7 +1916,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
|
||||||
queue_depth, bot_prefs.normal_delay_ms,
|
|
||||||
bot_prefs.normal_jitter_ms, bot_prefs.hop_step_ms,
|
|
||||||
&fingerprint, &due_at_millis);
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: schedule result=%d due=%lu now=%lu reqfp=%08lx%08lx\r\n",
|
|
||||||
+ (int)schedule, (unsigned long)due_at_millis, (unsigned long)_ms->getMillis(),
|
|
||||||
+ (unsigned long)(request_fingerprint.value >> 32), (unsigned long)(request_fingerprint.value & 0xFFFFFFFFul));
|
|
||||||
if (schedule == BOT_COORDINATOR_NO_SPACE || schedule == BOT_COORDINATOR_NOT_NORMAL) {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: schedule FAILED -> drop");
|
|
||||||
bot_stats.send_failures++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
@@ -1894,8 +1930,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
|
||||||
FirmwareBot::recordCommandCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis(),
|
|
||||||
BOT_COMMAND_COOLDOWN_MILLIS);
|
|
||||||
if (!enqueueBotResponse(message, direct_recipient, channel_idx, final_response, final_response_len, fingerprint, response_fingerprint)) {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: enqueueBotResponse FAILED -> drop");
|
|
||||||
ResponseCoordinator::cancel(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint);
|
|
||||||
bot_stats.send_failures++;
|
|
||||||
+ } else {
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: queued response, text=%.40s\r\n", final_response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1904,6 +1943,8 @@ void MyMesh::sendQueuedBotResponses() {
|
|
||||||
while (true) {
|
|
||||||
BotCoordinatorReady ready = ResponseCoordinator::poll(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, now);
|
|
||||||
if (ready.result == BOT_COORDINATOR_READY_NONE) return;
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: poll result=%d reqfp=%08lx\r\n", (int)ready.result,
|
|
||||||
+ (unsigned long)(ready.request_fingerprint.value & 0xFFFFFFFFul));
|
|
||||||
|
|
||||||
if (ready.result == BOT_COORDINATOR_READY_SUPPRESSED) {
|
|
||||||
bot_stats.suppressed_responses++;
|
|
||||||
@@ -1945,12 +1986,14 @@ void MyMesh::sendQueuedBotResponses() {
|
|
||||||
bool success = false;
|
|
||||||
if (pending->direct) {
|
|
||||||
ContactInfo *recipient = lookupContactByPubKey(pending->recipient_pub_key, PUB_KEY_SIZE);
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: send DM lookup=%s text=%.40s\r\n", recipient ? "FOUND" : "NULL", pending->text);
|
|
||||||
if (recipient) {
|
|
||||||
uint32_t expected_ack = 0;
|
|
||||||
uint32_t est_timeout = 0;
|
|
||||||
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
|
||||||
int result = sendMessage(*recipient, timestamp, 0, pending->text, expected_ack, est_timeout);
|
|
||||||
success = result != MSG_SEND_FAILED;
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: sendMessage result=%d success=%d\r\n", result, success);
|
|
||||||
if (success && expected_ack) {
|
|
||||||
expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis();
|
|
||||||
expected_ack_table[next_ack_idx].ack = expected_ack;
|
|
||||||
@@ -1960,9 +2003,11 @@ void MyMesh::sendQueuedBotResponses() {
|
|
||||||
}
|
|
||||||
} else if (pending->channel_idx != 0xFF) {
|
|
||||||
ChannelDetails channel;
|
|
||||||
+ bool have_ch = getChannel(pending->channel_idx, channel);
|
|
||||||
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
|
||||||
- success = getChannel(pending->channel_idx, channel) &&
|
|
||||||
- sendGroupMessage(timestamp, channel.channel, _prefs.node_name, pending->text, pending->text_len);
|
|
||||||
+ success = have_ch && sendGroupMessage(timestamp, channel.channel, _prefs.node_name, pending->text, pending->text_len);
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: send CH idx=%u have=%d success=%d text=%.40s\r\n",
|
|
||||||
+ (unsigned)pending->channel_idx, have_ch, success, pending->text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
@@ -1970,6 +2015,7 @@ void MyMesh::sendQueuedBotResponses() {
|
|
||||||
ResponseCoordinator::recordRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS,
|
|
||||||
pending->response_fingerprint, now);
|
|
||||||
} else {
|
|
||||||
+ BOT_DEBUG_PRINTLN("CBOT-DBG: send FAILED -> bot stays silent");
|
|
||||||
bot_stats.send_failures++;
|
|
||||||
}
|
|
||||||
pending->active = false;
|
|
||||||
@@ -2521,9 +2567,14 @@ void MyMesh::begin(bool has_display) {
|
|
||||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
|
||||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
|
||||||
#if CMESH_BOT_ENABLED
|
|
||||||
- if (!_store->loadBotPrefs(bot_prefs)) bot_prefs.prefs_load_failures++;
|
|
||||||
+ bool loaded = _store->loadBotPrefs(bot_prefs);
|
|
||||||
+ if (!loaded) bot_prefs.prefs_load_failures++;
|
|
||||||
BotPrefsCodec::validate(bot_prefs);
|
|
||||||
applyBotPrefs();
|
|
||||||
+ BOT_DEBUG_PRINTF("CBOT-DBG: boot: loaded=%d enabled=%d bot_ch=%s testing_ch=%s mask=0x%08lx delay=%u jitter=%u hop_step=%u\r\n",
|
|
||||||
+ loaded, bot_prefs.enabled, bot_prefs.bot_channel, bot_prefs.testing_channel,
|
|
||||||
+ (unsigned long)bot_prefs.command_mask, bot_prefs.normal_delay_ms, bot_prefs.normal_jitter_ms,
|
|
||||||
+ bot_prefs.hop_step_ms);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
30
scripts/list-release-companion-envs.sh
Executable file
30
scripts/list-release-companion-envs.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
MESHCORE_DIR="${MESHCORE_DIR:-${ROOT_DIR}/vendor/MeshCore}"
|
||||||
|
|
||||||
|
is_excluded() {
|
||||||
|
case "$1" in
|
||||||
|
Heltec_v2_companion_radio_ble|\
|
||||||
|
LilyGo_TLora_V2_1_1_6_companion_radio_ble|\
|
||||||
|
Station_G2_companion_radio_ble|\
|
||||||
|
Tbeam_SX1262_companion_radio_ble|\
|
||||||
|
Tbeam_SX1276_companion_radio_ble)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -rhE '^\[env:' "${MESHCORE_DIR}/variants/" \
|
||||||
|
| sort -u \
|
||||||
|
| grep -E '_companion_radio_(usb|ble)\]$' \
|
||||||
|
| sed -e 's/\[env://' -e 's/\]//' \
|
||||||
|
| while IFS= read -r env; do
|
||||||
|
if ! is_excluded "$env"; then
|
||||||
|
printf '%s\n' "$env"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -1136,6 +1136,17 @@ static void test_fingerprint() {
|
|||||||
assert(fa.value == fb.value);
|
assert(fa.value == fb.value);
|
||||||
assert(fa.value != fc.value);
|
assert(fa.value != fc.value);
|
||||||
|
|
||||||
|
BotMessage shifted = a;
|
||||||
|
shifted.sender_timestamp++;
|
||||||
|
assert(fa.value == FirmwareBot::fingerprintFor(shifted).value);
|
||||||
|
|
||||||
|
BotMessage dm = a;
|
||||||
|
dm.channel_kind = BOT_CHANNEL_DM;
|
||||||
|
dm.channel_name[0] = 0;
|
||||||
|
BotMessage later_dm = dm;
|
||||||
|
later_dm.sender_timestamp++;
|
||||||
|
assert(FirmwareBot::fingerprintFor(dm).value != FirmwareBot::fingerprintFor(later_dm).value);
|
||||||
|
|
||||||
b.sender_key_prefix[0] = 99;
|
b.sender_key_prefix[0] = 99;
|
||||||
assert(fa.value != FirmwareBot::fingerprintFor(b).value);
|
assert(fa.value != FirmwareBot::fingerprintFor(b).value);
|
||||||
b.sender_key_prefix[0] = a.sender_key_prefix[0];
|
b.sender_key_prefix[0] = a.sender_key_prefix[0];
|
||||||
@@ -1155,6 +1166,7 @@ static void test_group_request_token_uses_stable_channel_name() {
|
|||||||
c.channel_name[3] = 'T';
|
c.channel_name[3] = 'T';
|
||||||
c.channel_name[4] = 0;
|
c.channel_name[4] = 0;
|
||||||
c.path_hash_count = 1;
|
c.path_hash_count = 1;
|
||||||
|
b.sender_timestamp += 99;
|
||||||
|
|
||||||
BotFingerprint fa = FirmwareBot::fingerprintFor(a);
|
BotFingerprint fa = FirmwareBot::fingerprintFor(a);
|
||||||
BotFingerprint fb = FirmwareBot::fingerprintFor(b);
|
BotFingerprint fb = FirmwareBot::fingerprintFor(b);
|
||||||
@@ -1290,7 +1302,8 @@ static void test_response_coordinator_distinct_requests_same_response() {
|
|||||||
ResponseCoordinator::clear(pending, 2);
|
ResponseCoordinator::clear(pending, 2);
|
||||||
BotMessage first = make_message("#bot", "!ping");
|
BotMessage first = make_message("#bot", "!ping");
|
||||||
BotMessage second = first;
|
BotMessage second = first;
|
||||||
second.sender_timestamp++;
|
strncpy(second.text, "!status", sizeof(second.text) - 1);
|
||||||
|
second.text_len = strlen(second.text);
|
||||||
BotFingerprint first_request = FirmwareBot::fingerprintFor(first);
|
BotFingerprint first_request = FirmwareBot::fingerprintFor(first);
|
||||||
BotFingerprint second_request = FirmwareBot::fingerprintFor(second);
|
BotFingerprint second_request = FirmwareBot::fingerprintFor(second);
|
||||||
BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5);
|
BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5);
|
||||||
@@ -1303,7 +1316,7 @@ static void test_response_coordinator_distinct_requests_same_response() {
|
|||||||
assert(response.value == FirmwareBot::responseFingerprintFor(second, "Pong!", 5).value);
|
assert(response.value == FirmwareBot::responseFingerprintFor(second, "Pong!", 5).value);
|
||||||
assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PING, first_request, response, 1000, 0,
|
assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PING, first_request, response, 1000, 0,
|
||||||
identity_seed, 0, &scheduled, &first_due) == BOT_COORDINATOR_SCHEDULED);
|
identity_seed, 0, &scheduled, &first_due) == BOT_COORDINATOR_SCHEDULED);
|
||||||
assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_PING, second_request, response, 1000, 0,
|
assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_STATUS, second_request, response, 1000, 0,
|
||||||
identity_seed, 0, &scheduled, &second_due) == BOT_COORDINATOR_SCHEDULED);
|
identity_seed, 0, &scheduled, &second_due) == BOT_COORDINATOR_SCHEDULED);
|
||||||
assert(scheduled.value == second_request.value);
|
assert(scheduled.value == second_request.value);
|
||||||
|
|
||||||
@@ -1323,7 +1336,8 @@ static void test_response_coordinator_suppression_uses_response_fingerprint() {
|
|||||||
ResponseCoordinator::clear(pending, 2);
|
ResponseCoordinator::clear(pending, 2);
|
||||||
BotMessage first = make_message("#bot", "!ping");
|
BotMessage first = make_message("#bot", "!ping");
|
||||||
BotMessage second = first;
|
BotMessage second = first;
|
||||||
second.sender_timestamp++;
|
strncpy(second.text, "!status", sizeof(second.text) - 1);
|
||||||
|
second.text_len = strlen(second.text);
|
||||||
BotFingerprint first_request = FirmwareBot::fingerprintFor(first);
|
BotFingerprint first_request = FirmwareBot::fingerprintFor(first);
|
||||||
BotFingerprint second_request = FirmwareBot::fingerprintFor(second);
|
BotFingerprint second_request = FirmwareBot::fingerprintFor(second);
|
||||||
BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5);
|
BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5);
|
||||||
@@ -1332,7 +1346,7 @@ static void test_response_coordinator_suppression_uses_response_fingerprint() {
|
|||||||
|
|
||||||
assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PING, first_request, response, 1000, 0,
|
assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PING, first_request, response, 1000, 0,
|
||||||
0x0A0B0C0DUL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED);
|
0x0A0B0C0DUL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED);
|
||||||
assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_PING, second_request, response, 1000, 0,
|
assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_STATUS, second_request, response, 1000, 0,
|
||||||
0x0A0B0C0DUL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED);
|
0x0A0B0C0DUL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED);
|
||||||
assert(ResponseCoordinator::suppress(pending, 2, response));
|
assert(ResponseCoordinator::suppress(pending, 2, response));
|
||||||
BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 2, 1000);
|
BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 2, 1000);
|
||||||
@@ -1363,7 +1377,8 @@ static void test_response_coordinator_distinct_path_outputs() {
|
|||||||
ResponseCoordinator::clear(pending, 2);
|
ResponseCoordinator::clear(pending, 2);
|
||||||
BotMessage first = make_message("#bot", "path");
|
BotMessage first = make_message("#bot", "path");
|
||||||
BotMessage second = first;
|
BotMessage second = first;
|
||||||
second.sender_timestamp++;
|
strncpy(second.text, "status", sizeof(second.text) - 1);
|
||||||
|
second.text_len = strlen(second.text);
|
||||||
uint8_t path_a[] = { 0x12, 0x34, 0x56, 0x78 };
|
uint8_t path_a[] = { 0x12, 0x34, 0x56, 0x78 };
|
||||||
uint8_t path_b[] = { 0xab, 0xcd, 0xef, 0x01 };
|
uint8_t path_b[] = { 0xab, 0xcd, 0xef, 0x01 };
|
||||||
first.path = path_a;
|
first.path = path_a;
|
||||||
@@ -1505,7 +1520,8 @@ static void test_response_coordinator_cancel_by_request() {
|
|||||||
ResponseCoordinator::clear(pending, 2);
|
ResponseCoordinator::clear(pending, 2);
|
||||||
BotMessage first = make_message("#bot", "!ping");
|
BotMessage first = make_message("#bot", "!ping");
|
||||||
BotMessage second = first;
|
BotMessage second = first;
|
||||||
second.sender_timestamp++;
|
strncpy(second.text, "!status", sizeof(second.text) - 1);
|
||||||
|
second.text_len = strlen(second.text);
|
||||||
BotFingerprint first_request = FirmwareBot::fingerprintFor(first);
|
BotFingerprint first_request = FirmwareBot::fingerprintFor(first);
|
||||||
BotFingerprint second_request = FirmwareBot::fingerprintFor(second);
|
BotFingerprint second_request = FirmwareBot::fingerprintFor(second);
|
||||||
BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5);
|
BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5);
|
||||||
@@ -1515,7 +1531,7 @@ static void test_response_coordinator_cancel_by_request() {
|
|||||||
|
|
||||||
assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PING, first_request, response, 1000, 0,
|
assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PING, first_request, response, 1000, 0,
|
||||||
0x01020304UL, 0, &scheduled, &first_due) == BOT_COORDINATOR_SCHEDULED);
|
0x01020304UL, 0, &scheduled, &first_due) == BOT_COORDINATOR_SCHEDULED);
|
||||||
assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_PING, second_request, response, 1000, 0,
|
assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_STATUS, second_request, response, 1000, 0,
|
||||||
0x01020304UL, 0, &scheduled, &second_due) == BOT_COORDINATOR_SCHEDULED);
|
0x01020304UL, 0, &scheduled, &second_due) == BOT_COORDINATOR_SCHEDULED);
|
||||||
assert(ResponseCoordinator::cancel(pending, 2, first_request));
|
assert(ResponseCoordinator::cancel(pending, 2, first_request));
|
||||||
assert(!ResponseCoordinator::cancel(pending, 2, first_request));
|
assert(!ResponseCoordinator::cancel(pending, 2, first_request));
|
||||||
@@ -1580,12 +1596,12 @@ static void test_response_coordinator_hop_count_ranking() {
|
|||||||
base_delay, jitter, hop_step);
|
base_delay, jitter, hop_step);
|
||||||
uint32_t d2 = ResponseCoordinator::responseDelayMillis(two_hop, BOT_COMMAND_PING, request, 0x01020304UL, 0, 0,
|
uint32_t d2 = ResponseCoordinator::responseDelayMillis(two_hop, BOT_COMMAND_PING, request, 0x01020304UL, 0, 0,
|
||||||
base_delay, jitter, hop_step);
|
base_delay, jitter, hop_step);
|
||||||
// Linear + quadratic growth: bias(h) = h*step + h*h*grow
|
uint32_t tier_step = (uint32_t)hop_step + jitter + BOT_TIE_BREAK_SPREAD_MILLIS + BOT_HOP_TIER_GUARD_MILLIS;
|
||||||
assert(d1 == d0 + (uint32_t)hop_step + BOT_HOP_GROW_MILLIS);
|
assert(d1 == d0 + tier_step + BOT_HOP_GROW_MILLIS);
|
||||||
assert(d2 == d0 + (uint32_t)hop_step * 2 + 4 * BOT_HOP_GROW_MILLIS);
|
assert(d2 == d0 + tier_step * 2 + 4 * BOT_HOP_GROW_MILLIS);
|
||||||
// Gap between hops widens as hop count rises:
|
|
||||||
uint32_t gap_0_to_1 = d1 - d0;
|
uint32_t gap_0_to_1 = d1 - d0;
|
||||||
uint32_t gap_1_to_2 = d2 - d1;
|
uint32_t gap_1_to_2 = d2 - d1;
|
||||||
|
assert(gap_0_to_1 > jitter + BOT_TIE_BREAK_SPREAD_MILLIS);
|
||||||
assert(gap_1_to_2 > gap_0_to_1);
|
assert(gap_1_to_2 > gap_0_to_1);
|
||||||
|
|
||||||
uint32_t alt_step = 2000;
|
uint32_t alt_step = 2000;
|
||||||
@@ -1593,8 +1609,9 @@ static void test_response_coordinator_hop_count_ranking() {
|
|||||||
base_delay, jitter, alt_step);
|
base_delay, jitter, alt_step);
|
||||||
uint32_t d1_alt = ResponseCoordinator::responseDelayMillis(one_hop, BOT_COMMAND_PING, request, 0x01020304UL, 0, 0,
|
uint32_t d1_alt = ResponseCoordinator::responseDelayMillis(one_hop, BOT_COMMAND_PING, request, 0x01020304UL, 0, 0,
|
||||||
base_delay, jitter, alt_step);
|
base_delay, jitter, alt_step);
|
||||||
|
uint32_t alt_tier_step = alt_step + jitter + BOT_TIE_BREAK_SPREAD_MILLIS + BOT_HOP_TIER_GUARD_MILLIS;
|
||||||
assert(d0_alt == d0);
|
assert(d0_alt == d0);
|
||||||
assert(d1_alt == d0 + alt_step + BOT_HOP_GROW_MILLIS);
|
assert(d1_alt == d0 + alt_tier_step + BOT_HOP_GROW_MILLIS);
|
||||||
|
|
||||||
BotMessage dm = make_message("dm", "!ping");
|
BotMessage dm = make_message("dm", "!ping");
|
||||||
dm.channel_kind = BOT_CHANNEL_DM;
|
dm.channel_kind = BOT_CHANNEL_DM;
|
||||||
@@ -1623,7 +1640,8 @@ static void test_response_coordinator_hop_bias_cap() {
|
|||||||
base_delay, jitter, hop_step);
|
base_delay, jitter, hop_step);
|
||||||
assert(dmany == d0 + BOT_HOP_BIAS_MAX_MILLIS);
|
assert(dmany == d0 + BOT_HOP_BIAS_MAX_MILLIS);
|
||||||
uint32_t hop_u32 = (uint32_t)many_hops.path_hash_count;
|
uint32_t hop_u32 = (uint32_t)many_hops.path_hash_count;
|
||||||
assert(hop_u32 * (uint32_t)hop_step + hop_u32 * hop_u32 * BOT_HOP_GROW_MILLIS > BOT_HOP_BIAS_MAX_MILLIS);
|
uint32_t tier_step = (uint32_t)hop_step + jitter + BOT_TIE_BREAK_SPREAD_MILLIS + BOT_HOP_TIER_GUARD_MILLIS;
|
||||||
|
assert(hop_u32 * tier_step + hop_u32 * hop_u32 * BOT_HOP_GROW_MILLIS > BOT_HOP_BIAS_MAX_MILLIS);
|
||||||
assert(dmany + BOT_RESPONSE_DELAY_JITTER_MILLIS < BOT_RESPONSE_PENDING_TTL_MILLIS);
|
assert(dmany + BOT_RESPONSE_DELAY_JITTER_MILLIS < BOT_RESPONSE_PENDING_TTL_MILLIS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1640,13 +1658,41 @@ static void test_response_coordinator_same_hop_tiebreak() {
|
|||||||
BOT_RESPONSE_DELAY_JITTER_MILLIS, hop_step);
|
BOT_RESPONSE_DELAY_JITTER_MILLIS, hop_step);
|
||||||
assert(bot_a != bot_b);
|
assert(bot_a != bot_b);
|
||||||
uint32_t spread = bot_a > bot_b ? bot_a - bot_b : bot_b - bot_a;
|
uint32_t spread = bot_a > bot_b ? bot_a - bot_b : bot_b - bot_a;
|
||||||
assert(spread < 900);
|
assert(spread < BOT_TIE_BREAK_SPREAD_MILLIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_response_coordinator_request_token_suppression() {
|
||||||
|
BotCoordinatorPending pending[2];
|
||||||
|
ResponseCoordinator::clear(pending, 2);
|
||||||
|
BotMessage first = make_message("#bot", "path");
|
||||||
|
BotMessage second = first;
|
||||||
|
strncpy(second.text, "test", sizeof(second.text) - 1);
|
||||||
|
second.text_len = strlen(second.text);
|
||||||
|
BotFingerprint first_request = FirmwareBot::fingerprintFor(first);
|
||||||
|
BotFingerprint second_request = FirmwareBot::fingerprintFor(second);
|
||||||
|
BotFingerprint first_response = FirmwareBot::responseFingerprintFor(first, "Path 1 hop", 10);
|
||||||
|
BotFingerprint second_response = FirmwareBot::responseFingerprintFor(second, "@[alice] | 1 hop", 16);
|
||||||
|
BotFingerprint scheduled;
|
||||||
|
uint32_t due = 0;
|
||||||
|
|
||||||
|
assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PATH, first_request, first_response, 1000, 0,
|
||||||
|
0x01020304UL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED);
|
||||||
|
assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_TEST, second_request, second_response, 1000, 0,
|
||||||
|
0x01020304UL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED);
|
||||||
|
assert(ResponseCoordinator::suppressByRequestToken(pending, 2, FirmwareBot::requestToken(first_request)));
|
||||||
|
BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 2, 1000);
|
||||||
|
assert(ready.result == BOT_COORDINATOR_READY_SUPPRESSED);
|
||||||
|
assert(ready.request_fingerprint.value == first_request.value);
|
||||||
|
ready = ResponseCoordinator::poll(pending, 2, due);
|
||||||
|
assert(ready.result == BOT_COORDINATOR_READY_SEND);
|
||||||
|
assert(ready.request_fingerprint.value == second_request.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_response_coordinator_recent_responses() {
|
static void test_response_coordinator_recent_responses() {
|
||||||
BotCoordinatorRecent recent[1];
|
BotCoordinatorRecent recent[1];
|
||||||
ResponseCoordinator::clearRecent(recent, 1);
|
ResponseCoordinator::clearRecent(recent, 1);
|
||||||
BotFingerprint response = { 0xFEDCBA9876543210ULL };
|
BotFingerprint response = { 0xFEDCBA9876543210ULL };
|
||||||
|
BotFingerprint other_response = { 0x0123456789ABCDEFULL };
|
||||||
BotFingerprint zero = { 0 };
|
BotFingerprint zero = { 0 };
|
||||||
|
|
||||||
assert(!ResponseCoordinator::recentlySent(recent, 1, response, 1000));
|
assert(!ResponseCoordinator::recentlySent(recent, 1, response, 1000));
|
||||||
@@ -1655,7 +1701,19 @@ static void test_response_coordinator_recent_responses() {
|
|||||||
ResponseCoordinator::recordRecent(recent, 1, response, 1000);
|
ResponseCoordinator::recordRecent(recent, 1, response, 1000);
|
||||||
assert(ResponseCoordinator::recentlySent(recent, 1, response, 1000));
|
assert(ResponseCoordinator::recentlySent(recent, 1, response, 1000));
|
||||||
assert(ResponseCoordinator::recentlySent(recent, 1, response, 1000 + BOT_RESPONSE_RECENT_TTL_MILLIS - 1));
|
assert(ResponseCoordinator::recentlySent(recent, 1, response, 1000 + BOT_RESPONSE_RECENT_TTL_MILLIS - 1));
|
||||||
|
|
||||||
|
ResponseCoordinator::recordRequestToken(recent, 1, 0x1234, 1000 + 10);
|
||||||
|
assert(ResponseCoordinator::recentlyAnswered(recent, 1, 0x1234, 1000 + 10));
|
||||||
|
assert(ResponseCoordinator::recentlySent(recent, 1, response, 1000 + BOT_RESPONSE_RECENT_TTL_MILLIS - 1));
|
||||||
assert(!ResponseCoordinator::recentlySent(recent, 1, response, 1000 + BOT_RESPONSE_RECENT_TTL_MILLIS));
|
assert(!ResponseCoordinator::recentlySent(recent, 1, response, 1000 + BOT_RESPONSE_RECENT_TTL_MILLIS));
|
||||||
|
assert(ResponseCoordinator::recentlyAnswered(recent, 1, 0x1234, 1000 + BOT_RESPONSE_RECENT_TTL_MILLIS));
|
||||||
|
assert(!ResponseCoordinator::recentlyAnswered(recent, 1, 0x1234, 1000 + 10 + BOT_RESPONSE_RECENT_TTL_MILLIS));
|
||||||
|
|
||||||
|
ResponseCoordinator::recordRequestToken(recent, 1, 0x5678, 2000);
|
||||||
|
assert(ResponseCoordinator::recentlyAnswered(recent, 1, 0x5678, 2000));
|
||||||
|
ResponseCoordinator::recordRecent(recent, 1, other_response, 2000 + 1);
|
||||||
|
assert(ResponseCoordinator::recentlyAnswered(recent, 1, 0x5678, 2000 + 1));
|
||||||
|
assert(ResponseCoordinator::recentlySent(recent, 1, other_response, 2000 + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_response_coordinator_rejects_non_normal() {
|
static void test_response_coordinator_rejects_non_normal() {
|
||||||
@@ -1724,6 +1782,7 @@ int main() {
|
|||||||
test_response_coordinator_schedule_preserves_suppression();
|
test_response_coordinator_schedule_preserves_suppression();
|
||||||
test_response_coordinator_group_observed_response_suppresses();
|
test_response_coordinator_group_observed_response_suppresses();
|
||||||
test_response_coordinator_group_guarded_path_output_suppresses();
|
test_response_coordinator_group_guarded_path_output_suppresses();
|
||||||
|
test_response_coordinator_request_token_suppression();
|
||||||
test_response_coordinator_suppress_expire_full();
|
test_response_coordinator_suppress_expire_full();
|
||||||
test_response_coordinator_cancel_by_request();
|
test_response_coordinator_cancel_by_request();
|
||||||
test_response_coordinator_delay_biases();
|
test_response_coordinator_delay_biases();
|
||||||
|
|||||||
Reference in New Issue
Block a user