diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index f1ac9024..3d0fa85b 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -79,5 +79,10 @@ public: // Locator/Live Share target) drop a reference that would otherwise dangle. // Default no-op. 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; }; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 12a08769..8e9224e3 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1665,6 +1665,11 @@ void MyMesh::startInterface(BaseSerialInterface &serial) { 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) { 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 @@ -2378,6 +2383,12 @@ void MyMesh::handleCmdFrame(size_t len) { memcpy(channel.channel.secret, &cmd_frame[2 + 32], 32); // 256-bit key if (setChannel(channel_idx, channel)) { 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(); } else { 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 if (setChannel(channel_idx, channel)) { saveChannels(); + if (_ui && isAllZero(channel.channel.secret, sizeof(channel.channel.secret))) + _ui->onChannelRemoved(channel_idx); writeOKFrame(); } else { writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index c778b209..4a95278e 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2445,6 +2445,29 @@ void UITask::onContactRemoved(const uint8_t* pub_key) { 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 // 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 diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 38a4c2f7..1a80f414 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -219,6 +219,10 @@ public: // 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). 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: // prefers an active [LOC] live share, falls back to their last-advertised // GPS fix. Returns false when neither is known. Optional live/ts report