fix(companion): clear stale channel-index refs when a channel is removed

CMD_SET_CHANNEL clearing a slot (empty secret) left bot_channel_idx,
loc_share_channel_idx, and the per-channel melody bitmasks pointing at that
index. A new channel added later at the same slot would then silently
inherit the old one's bot target, Live Share target, or notification
melody. New onChannelRemoved() hook, mirroring onContactRemoved(), turns
the bot/Live Share channel target off (fail closed) and clears the melody
bits for that index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-27 00:39:10 +02:00
parent 43c3f43e10
commit 6a394e2d7a
4 changed files with 45 additions and 0 deletions

View File

@@ -79,5 +79,10 @@ public:
// Locator/Live Share target) drop a reference that would otherwise dangle. // Locator/Live Share target) drop a reference that would otherwise dangle.
// Default no-op. // Default no-op.
virtual void onContactRemoved(const uint8_t* pub_key) {} virtual void onContactRemoved(const uint8_t* pub_key) {}
// A channel slot was cleared (companion app set it to an empty secret).
// Drop any setting that referenced it by index — otherwise a new channel
// added later at the same slot would silently inherit the old one's bot/
// share target or notification melody. Default no-op.
virtual void onChannelRemoved(uint8_t channel_idx) {}
virtual void loop() = 0; virtual void loop() = 0;
}; };

View File

@@ -1665,6 +1665,11 @@ void MyMesh::startInterface(BaseSerialInterface &serial) {
serial.enable(); serial.enable();
} }
static bool isAllZero(const uint8_t* buf, size_t n) {
for (size_t i = 0; i < n; i++) if (buf[i]) return false;
return true;
}
void MyMesh::handleCmdFrame(size_t len) { void MyMesh::handleCmdFrame(size_t len) {
if (cmd_frame[0] == CMD_DEVICE_QUERY && len >= 2) { // sent when app establishes connection if (cmd_frame[0] == CMD_DEVICE_QUERY && len >= 2) { // sent when app establishes connection
app_target_ver = cmd_frame[1]; // which version of protocol does app understand app_target_ver = cmd_frame[1]; // which version of protocol does app understand
@@ -2378,6 +2383,12 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(channel.channel.secret, &cmd_frame[2 + 32], 32); // 256-bit key memcpy(channel.channel.secret, &cmd_frame[2 + 32], 32); // 256-bit key
if (setChannel(channel_idx, channel)) { if (setChannel(channel_idx, channel)) {
saveChannels(); saveChannels();
// An all-zero secret is this codebase's "empty slot" sentinel (same
// check loadChannels()/saveChannels() use) -- the app just cleared this
// channel, so drop anything that referenced it by index, the same way
// onContactRemoved() does for contacts.
if (_ui && isAllZero(channel.channel.secret, sizeof(channel.channel.secret)))
_ui->onChannelRemoved(channel_idx);
writeOKFrame(); writeOKFrame();
} else { } else {
writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
@@ -2390,6 +2401,8 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(channel.channel.secret, &cmd_frame[2 + 32], 16); // 128-bit key memcpy(channel.channel.secret, &cmd_frame[2 + 32], 16); // 128-bit key
if (setChannel(channel_idx, channel)) { if (setChannel(channel_idx, channel)) {
saveChannels(); saveChannels();
if (_ui && isAllZero(channel.channel.secret, sizeof(channel.channel.secret)))
_ui->onChannelRemoved(channel_idx);
writeOKFrame(); writeOKFrame();
} else { } else {
writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx

View File

@@ -2445,6 +2445,29 @@ void UITask::onContactRemoved(const uint8_t* pub_key) {
if (changed) the_mesh.savePrefs(); if (changed) the_mesh.savePrefs();
} }
void UITask::onChannelRemoved(uint8_t channel_idx) {
if (!_node_prefs) return;
bool changed = false;
if (_node_prefs->bot_channel_enabled && _node_prefs->bot_channel_idx == channel_idx) {
_node_prefs->bot_channel_enabled = 0;
changed = true;
}
// Fail closed, same policy as onContactRemoved()'s Live Share case.
if (_node_prefs->loc_share_target_type == 0 && _node_prefs->loc_share_channel_idx == channel_idx) {
_node_prefs->loc_share_enabled = 0;
changed = true;
}
uint64_t mask = 1ULL << channel_idx;
if (_node_prefs->ch_notif_melody_set & mask) {
_node_prefs->ch_notif_melody_set &= ~mask;
_node_prefs->ch_notif_melody_2 &= ~mask;
changed = true;
}
if (changed) the_mesh.savePrefs();
}
// Homing beeper: while armed with a target and inside the radius, emit a short // Homing beeper: while armed with a target and inside the radius, emit a short
// tick whose interval shrinks linearly with distance — slow at the edge, rapid // tick whose interval shrinks linearly with distance — slow at the edge, rapid
// near the centre. Polls distance a few times a second; silent outside the // near the centre. Polls distance a few times a second; silent outside the

View File

@@ -219,6 +219,10 @@ public:
// any per-contact mute/melody override (those tables have only 16 shared // any per-contact mute/melody override (those tables have only 16 shared
// slots, so an orphan isn't just stale — it can starve other contacts). // slots, so an orphan isn't just stale — it can starve other contacts).
void onContactRemoved(const uint8_t* pub_key) override; void onContactRemoved(const uint8_t* pub_key) override;
// A channel slot was cleared: turn off anything armed against it by index
// (the bot's channel, Live Share's channel target) and drop its per-channel
// melody bit, so a future channel re-added at the same slot starts clean.
void onChannelRemoved(uint8_t channel_idx) override;
// Resolve a person target (6-byte pubkey prefix) to a current position: // Resolve a person target (6-byte pubkey prefix) to a current position:
// prefers an active [LOC] live share, falls back to their last-advertised // prefers an active [LOC] live share, falls back to their last-advertised
// GPS fix. Returns false when neither is known. Optional live/ts report // GPS fix. Returns false when neither is known. Optional live/ts report