Add Companion lost-status replies and repair room mailbox test adapters
Save an opt-in off/no/yes answer to private Am I lost questions from known chat contacts. Keep reply traffic and ACK state bounded, preserve pending user delivery, and expose the setting through local CLI and ESP32 WebConfig. Retain compact STM32 application/filesystem boundaries, qualify the Tag BLE queue allowance, and exercise saved-image compatibility and browser controls. Fix stale room mailbox protocol dependencies in the ACL/route test fixtures. Validation: ten Companion builds, sanitized production-path regressions, preference fault/compatibility tests, browser tests, and 86 capacity/build recipe checks passed. No live radio was flashed for this feature.
This commit is contained in:
1 parent
2c1c2c8e99
commit
8031e60b55
46 files changed
+3663
-2020
No files matched your search
@@ -671,6 +671,18 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
|
||||
return rc;
|
||||
}
|
||||
|
||||
int BaseChatMesh::sendMessageDetached(const ContactInfo& recipient,
|
||||
uint32_t timestamp, const char* text,
|
||||
uint32_t& expected_ack,
|
||||
uint32_t& est_timeout,
|
||||
uint8_t* packet_hash) {
|
||||
const unsigned long previous_timeout = txt_send_timeout;
|
||||
const int result = sendMessage(recipient, timestamp, 0, text, expected_ack,
|
||||
est_timeout, packet_hash);
|
||||
txt_send_timeout = previous_timeout;
|
||||
return result;
|
||||
}
|
||||
|
||||
int BaseChatMesh::sendCommandData(const ContactInfo& recipient,
|
||||
uint32_t timestamp, uint8_t attempt,
|
||||
uint8_t txt_type, const char* text,
|
||||
|
||||
@@ -100,6 +100,11 @@ class BaseChatMesh : public mesh::Mesh {
|
||||
|
||||
protected:
|
||||
void sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, uint8_t ack_len=4);
|
||||
// Autonomous replies have independent ACK tracking and must preserve the
|
||||
// application/terminal send's shared timeout.
|
||||
int sendMessageDetached(const ContactInfo& recipient, uint32_t timestamp,
|
||||
const char* text, uint32_t& expected_ack,
|
||||
uint32_t& est_timeout, uint8_t* packet_hash);
|
||||
BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
|
||||
: mesh::Mesh(radio, ms, rng, rtc, mgr, tables)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace companion {
|
||||
|
||||
// This is an owner-selected answer, never an assessment of a GPS position.
|
||||
// Values are also the append-only Companion preference representation.
|
||||
enum LostReply : uint8_t { LostReplyOff = 0, LostReplyNo = 1, LostReplyYes = 2 };
|
||||
|
||||
inline bool lostReplyWhitespace(char c) {
|
||||
return c == ' ' || uint8_t(c - '\t') <= uint8_t('\r' - '\t');
|
||||
}
|
||||
|
||||
inline bool isLostReplyQuestion(const char* text) {
|
||||
if (text == NULL) return false;
|
||||
while (lostReplyWhitespace(*text)) ++text;
|
||||
static const char question[] = "am i lost";
|
||||
for (size_t i = 0; i < sizeof(question) - 1; ++i) {
|
||||
char c = *text++;
|
||||
if (c >= 'A' && c <= 'Z') c = char(c + ('a' - 'A'));
|
||||
if (c != question[i]) return false;
|
||||
}
|
||||
if (*text == '?') ++text;
|
||||
while (lostReplyWhitespace(*text)) ++text;
|
||||
return *text == 0;
|
||||
}
|
||||
|
||||
inline const char* lostReplyName(uint8_t value) {
|
||||
return value == LostReplyNo ? "no" : value == LostReplyYes ? "yes" : "off";
|
||||
}
|
||||
|
||||
// Only directly attached clients call this handler. No radio CLI route may
|
||||
// change the locally saved answer, even with a remote CLI permission flag.
|
||||
template<typename Prefs, typename Save>
|
||||
bool handleLostReplyCommand(Prefs& prefs, const char* command, char* reply,
|
||||
size_t capacity, Save save) {
|
||||
if (strcmp(command, "get lost.reply") == 0) {
|
||||
snprintf(reply, capacity, "> %s", lostReplyName(prefs.lost_reply));
|
||||
return true;
|
||||
}
|
||||
const char prefix[] = "set lost.reply ";
|
||||
if (strncmp(command, prefix, sizeof(prefix) - 1) != 0) return false;
|
||||
const char* value = command + sizeof(prefix) - 1;
|
||||
uint8_t selected;
|
||||
if (strcmp(value, "off") == 0) selected = LostReplyOff;
|
||||
else if (strcmp(value, "no") == 0) selected = LostReplyNo;
|
||||
else if (strcmp(value, "yes") == 0) selected = LostReplyYes;
|
||||
else {
|
||||
snprintf(reply, capacity, "Error: use off, no or yes");
|
||||
return true;
|
||||
}
|
||||
const uint8_t previous = prefs.lost_reply;
|
||||
prefs.lost_reply = selected;
|
||||
if (!save()) {
|
||||
prefs.lost_reply = previous;
|
||||
snprintf(reply, capacity, "Error: could not save lost reply");
|
||||
} else {
|
||||
snprintf(reply, capacity, "> lost.reply %s", value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Four retained full peer identities bound both memory and response traffic.
|
||||
// A peer slot cannot be evicted during its minute-long cooldown. Exact radio
|
||||
// retries remain suppressed while that peer's last answered request is kept;
|
||||
// this runtime cache is intentionally cleared by a reboot.
|
||||
class LostReplyLimiter {
|
||||
public:
|
||||
static constexpr size_t PeerCount = 4;
|
||||
static constexpr size_t KeySize = 32;
|
||||
static constexpr uint32_t PeerIntervalMillis = 60000;
|
||||
static constexpr uint32_t GlobalIntervalMillis = 10000;
|
||||
|
||||
LostReplyLimiter() : last_reply_(0), replied_(false) {
|
||||
memset(peers_, 0, sizeof(peers_));
|
||||
}
|
||||
|
||||
bool canReply(const uint8_t* key, uint32_t timestamp, uint32_t now) const {
|
||||
return replySlot(key, timestamp, now) >= 0;
|
||||
}
|
||||
|
||||
void rememberReply(const uint8_t* key, uint32_t timestamp, uint32_t now) {
|
||||
const int chosen = replySlot(key, timestamp, now);
|
||||
if (chosen < 0) return;
|
||||
Peer& peer = peers_[chosen];
|
||||
memcpy(peer.key, key, KeySize);
|
||||
peer.timestamp = timestamp;
|
||||
peer.replied_at = now;
|
||||
peer.valid = true;
|
||||
last_reply_ = now;
|
||||
replied_ = true;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Peer {
|
||||
uint8_t key[KeySize];
|
||||
uint32_t timestamp;
|
||||
uint32_t replied_at;
|
||||
bool valid;
|
||||
};
|
||||
Peer peers_[PeerCount];
|
||||
uint32_t last_reply_;
|
||||
bool replied_;
|
||||
|
||||
// Share the full identity/cooldown scan between admission and commit.
|
||||
// Inlining both copies costs flash on constrained Companion targets.
|
||||
#if defined(__GNUC__)
|
||||
__attribute__((noinline))
|
||||
#endif
|
||||
int replySlot(const uint8_t* key, uint32_t timestamp, uint32_t now) const {
|
||||
if (key == NULL || (replied_ && uint32_t(now - last_reply_) < GlobalIntervalMillis))
|
||||
return -1;
|
||||
int available = -1;
|
||||
for (size_t i = 0; i < PeerCount; ++i) {
|
||||
const Peer& peer = peers_[i];
|
||||
if (peer.valid && memcmp(peer.key, key, KeySize) == 0) {
|
||||
return peer.timestamp != timestamp
|
||||
&& uint32_t(now - peer.replied_at) >= PeerIntervalMillis ? int(i) : -1;
|
||||
}
|
||||
if (available < 0 && (!peer.valid
|
||||
|| uint32_t(now - peer.replied_at) >= PeerIntervalMillis)) available = int(i);
|
||||
}
|
||||
return available;
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(sizeof(LostReplyLimiter) <= 192, "lost reply state stays bounded");
|
||||
|
||||
} // namespace companion
|
||||
} // namespace mesh
|
||||
@@ -18,7 +18,7 @@
|
||||
// separately, see wcIsAdminPasswordKey below.
|
||||
static const char* const WC_ALLOWED_SET_KEYS[] = {
|
||||
// NodePrefs (radio / node)
|
||||
"name", "bluetooth.name", "bluetooth.mac", "bluetooth.stealth",
|
||||
"name", "bluetooth.name", "bluetooth.mac", "bluetooth.stealth", "lost.reply",
|
||||
"lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
|
||||
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.fem.txgain", "radio.rxps", "powersaving",
|
||||
"repeat",
|
||||
|
||||
+1954
-1943
File diff suppressed because it is too large.
Load diff
@@ -2092,6 +2092,8 @@ void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
|
||||
radio["bluetooth_name"] = (const char*)node.bluetooth_name;
|
||||
radio["bluetooth_mac"] = (const char*)node.bluetooth_mac;
|
||||
radio["bluetooth_stealth"] = node.bluetooth_stealth;
|
||||
static const char* const LOST_REPLY_MODES[] = { "off", "no", "yes" };
|
||||
radio["lost_reply"] = LOST_REPLY_MODES[node.lost_reply <= 2 ? node.lost_reply : 0];
|
||||
radio["lat"] = node.lat;
|
||||
radio["lon"] = node.lon;
|
||||
radio["advert_interval"] = node.advert_interval;
|
||||
@@ -2215,6 +2217,21 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (set.containsKey("lost.reply")) {
|
||||
NodeSnapshot node = {};
|
||||
_cb->getNodeSnapshot(node);
|
||||
if (!(node.capabilities & CAP_LOST_REPLY)) {
|
||||
req->send(400, "application/json", "{\"error\":\"setting unavailable\",\"key\":\"lost.reply\"}");
|
||||
return;
|
||||
}
|
||||
const char* value = set["lost.reply"];
|
||||
if (!value || (strcmp(value, "off") != 0 && strcmp(value, "no") != 0
|
||||
&& strcmp(value, "yes") != 0)) {
|
||||
req->send(400, "application/json", "{\"error\":\"expected off, no or yes\",\"key\":\"lost.reply\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_mode == MODE_SETUP && _initial_setup && !set.containsKey("password")
|
||||
&& (reboot_after || set.containsKey("wifi.ssid"))) {
|
||||
req->send(400, "application/json",
|
||||
|
||||
@@ -92,6 +92,7 @@ public:
|
||||
CAP_ESPNOW_CHANNEL = 1UL << 15,
|
||||
CAP_BLUETOOTH_MAC = 1UL << 16,
|
||||
CAP_BLUETOOTH_STEALTH = 1UL << 17,
|
||||
CAP_LOST_REPLY = 1UL << 18,
|
||||
CAP_DELAYS = CAP_RX_DELAY | CAP_TX_DELAY,
|
||||
};
|
||||
|
||||
@@ -103,6 +104,7 @@ public:
|
||||
char bluetooth_name[32]; // empty = transport's default device name
|
||||
char bluetooth_mac[24]; // empty = chipset factory address
|
||||
bool bluetooth_stealth; // independent of the address policy
|
||||
uint8_t lost_reply; // 0=off, 1=no, 2=yes (Companion only)
|
||||
char admin_password[32]; // empty = trusted LAN/no login prompt
|
||||
double lat;
|
||||
double lon;
|
||||
|
||||
Reference in new issue
Block a user