feat: a channel's scope can follow the default -- Default is the new start value

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-06 14:09:22 +02:00
co-authored by Claude Opus 5.5
parent 5a92d36509
commit c4217d8632
8 changed files with 63 additions and 58 deletions
+5
View File
@@ -333,6 +333,11 @@ static void sanitize(NodePrefs& p) {
if (!battery::validCurve(p.batt_curve_mv)) memset(p.batt_curve_mv, 0, sizeof(p.batt_curve_mv)); if (!battery::validCurve(p.batt_curve_mv)) memset(p.batt_curve_mv, 0, sizeof(p.batt_curve_mv));
flag(p.lock_compact, 0); flag(p.lock_compact, 0);
flag(p.ble_off, 0); flag(p.ble_off, 0);
if (!p.ch_scope_follow) {
for (uint8_t i = 0; i < NodePrefs::MAX_SCOPED_CHANNELS; i++)
if (p.ch_scope_idx[i] == 0) p.ch_scope_idx[i] = NodePrefs::CH_SCOPE_DEFAULT;
p.ch_scope_follow = 1;
}
} }
// The settings a firmware before 2.0 kept in /new_prefs that still matter // The settings a firmware before 2.0 kept in /new_prefs that still matter
+9 -26
View File
@@ -804,6 +804,7 @@ void MyMesh::removeScope(uint8_t idx) {
// fix-up ScopeList::remove() applied to default_idx. // fix-up ScopeList::remove() applied to default_idx.
for (uint8_t i = 0; i < NodePrefs::MAX_SCOPED_CHANNELS; i++) { for (uint8_t i = 0; i < NodePrefs::MAX_SCOPED_CHANNELS; i++) {
uint8_t ci = _prefs.ch_scope_idx[i]; uint8_t ci = _prefs.ch_scope_idx[i];
if (ci == NodePrefs::CH_SCOPE_DEFAULT) continue; // follows the default, fixed up above
if (ci == idx) _prefs.ch_scope_idx[i] = 0; if (ci == idx) _prefs.ch_scope_idx[i] = 0;
else if (ci > idx) _prefs.ch_scope_idx[i] = ci - 1; else if (ci > idx) _prefs.ch_scope_idx[i] = ci - 1;
} }
@@ -835,7 +836,7 @@ void MyMesh::setDefaultScope(uint8_t idx) {
void MyMesh::setChannelScope(uint8_t channel_idx, uint8_t idx) { void MyMesh::setChannelScope(uint8_t channel_idx, uint8_t idx) {
if (channel_idx >= NodePrefs::MAX_SCOPED_CHANNELS) return; if (channel_idx >= NodePrefs::MAX_SCOPED_CHANNELS) return;
_prefs.ch_scope_idx[channel_idx] = _scope_list.clamp(idx); _prefs.ch_scope_idx[channel_idx] = idx == NodePrefs::CH_SCOPE_DEFAULT ? idx : _scope_list.clamp(idx);
} }
void MyMesh::rebuildRepeatScopes() { void MyMesh::rebuildRepeatScopes() {
@@ -887,17 +888,12 @@ void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pk
// this channel's own on-device pick, same precedence DMs already have. // this channel's own on-device pick, same precedence DMs already have.
sendFloodScoped(send_scope, pkt, delay_millis); sendFloodScoped(send_scope, pkt, delay_millis);
} else { } else {
// Resolve THIS channel's own scope-list pick (Messages > channel context // THIS channel's own scope (Messages > channel context menu > Scope:):
// menu > Scope:). Index 0 is "*", which means unscoped -- NOT "inherit the // "Default" follows the list's default, "*" sends unscoped. A channel we
// default": the default is what DMs and the relay filter's primary slot // can't identify (findChannelIdx() == -1, e.g. a send whose secret isn't
// use, and what a channel is seeded with on upgrade, but once a channel // in channels[]) takes the default too.
// has a pick that pick is the whole story. The list default is only the
// fallback for a channel we can't identify at all (findChannelIdx() == -1,
// e.g. a send whose secret isn't in channels[]), where there's no pick to
// read in the first place.
int channel_idx = findChannelIdx(channel); int channel_idx = findChannelIdx(channel);
uint8_t list_idx = (channel_idx >= 0 && channel_idx < NodePrefs::MAX_SCOPED_CHANNELS) uint8_t list_idx = channel_idx >= 0 ? channelScope((uint8_t)channel_idx) : _scope_list.default_idx;
? _prefs.ch_scope_idx[channel_idx] : _scope_list.default_idx;
TransportKey scope = _scope_list.key(list_idx); TransportKey scope = _scope_list.key(list_idx);
sendFloodScoped(scope, pkt, delay_millis); sendFloodScoped(scope, pkt, delay_millis);
} }
@@ -1841,6 +1837,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
// defaults // defaults
memset(&_prefs, 0, sizeof(_prefs)); memset(&_prefs, 0, sizeof(_prefs));
memset(_prefs.ch_scope_idx, NodePrefs::CH_SCOPE_DEFAULT, sizeof(_prefs.ch_scope_idx));
_prefs.airtime_factor = 1.0; _prefs.airtime_factor = 1.0;
strcpy(_prefs.node_name, "NONAME"); strcpy(_prefs.node_name, "NONAME");
_prefs.freq = LORA_FREQ; _prefs.freq = LORA_FREQ;
@@ -1938,7 +1935,7 @@ void MyMesh::begin() {
_store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon); _store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon);
// True only on the first boot after upgrading a device that had the old // True only on the first boot after upgrading a device that had the old
// single Scope field set -- acted on once the channels are loaded, below. // single Scope field set -- acted on once the channels are loaded, below.
bool scope_migrated_legacy = _store->loadScopeList(_scope_list, _prefs); _store->loadScopeList(_scope_list, _prefs);
rebuildRepeatScopes(); rebuildRepeatScopes();
// sanitise bad pref values. NaN/inf must be reset BEFORE constrain(): constrain // sanitise bad pref values. NaN/inf must be reset BEFORE constrain(): constrain
@@ -1975,20 +1972,6 @@ void MyMesh::begin() {
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
} }
// First boot after upgrading from the single device-wide Scope field: every
// channel now carries its own pick, and an unset pick means "*" == unscoped,
// not "inherit the default". Left alone, an upgrader's channel traffic would
// quietly go out unscoped while their DMs kept the old scope. Seed only the
// slots that actually hold a channel today -- a blanket fill would also hand
// the scope to whatever channel gets created in an empty slot later on.
if (scope_migrated_legacy && _scope_list.default_idx >= 1) {
for (uint8_t i = 0; i < NodePrefs::MAX_SCOPED_CHANNELS; i++) {
ChannelDetails ch;
if (getChannel(i, ch) && ch.name[0]) _prefs.ch_scope_idx[i] = _scope_list.default_idx;
}
savePrefs();
}
applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set
applyApc(); // sets TX power to the ceiling and arms APC if enabled applyApc(); // sets TX power to the ceiling and arms APC if enabled
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
+10 -3
View File
@@ -495,12 +495,19 @@ public:
// fixes up repeat_extra_scope_mask, and clears any channel's ch_scope_idx // fixes up repeat_extra_scope_mask, and clears any channel's ch_scope_idx
// that pointed at it (shift-and-clamp, matching ScopeList::remove()). // that pointed at it (shift-and-clamp, matching ScopeList::remove()).
void removeScope(uint8_t idx); void removeScope(uint8_t idx);
// Marks list index idx as the default (used for DMs and any channel/ // Marks list index idx as the default (used for DMs, the repeater's primary
// repeater slot without its own pick). Persists the list and rebuilds // slot and every channel left on "Default"). Persists the list and rebuilds
// repeat_scopes[]. // repeat_scopes[].
void setDefaultScope(uint8_t idx); void setDefaultScope(uint8_t idx);
// Sets channel_idx's own scope-list pick (0 = "*"). Persists prefs. // Sets channel_idx's own scope-list pick (0 = "*", NodePrefs::CH_SCOPE_DEFAULT
// = follow the default). The caller saves the prefs.
void setChannelScope(uint8_t channel_idx, uint8_t idx); void setChannelScope(uint8_t channel_idx, uint8_t idx);
// The list index channel_idx sends in, its "follow the default" resolved.
uint8_t channelScope(uint8_t channel_idx) const {
uint8_t ci = channel_idx < NodePrefs::MAX_SCOPED_CHANNELS ? _prefs.ch_scope_idx[channel_idx]
: NodePrefs::CH_SCOPE_DEFAULT;
return ci == NodePrefs::CH_SCOPE_DEFAULT ? _scope_list.default_idx : _scope_list.clamp(ci);
}
bool isAckPending(uint32_t expected_ack) const { bool isAckPending(uint32_t expected_ack) const {
if (expected_ack == 0) return false; // 0 marks an empty/cleared slot, not a real ACK if (expected_ack == 0) return false; // 0 marks an empty/cleared slot, not a real ACK
+9 -2
View File
@@ -314,12 +314,14 @@ struct NodePrefs { // persisted to file
// Per-channel melody override (2 bitmasks, 1 bit per channel) // Per-channel melody override (2 bitmasks, 1 bit per channel)
uint64_t ch_notif_melody_set; // bit i = channel i has explicit melody [del→onChannelRemoved] uint64_t ch_notif_melody_set; // bit i = channel i has explicit melody [del→onChannelRemoved]
uint64_t ch_notif_melody_2; // bit i = use melody 2 (else melody 1, when set bit is set) uint64_t ch_notif_melody_2; // bit i = use melody 2 (else melody 1, when set bit is set)
// Scope-list index per channel (see ScopeList) -- 0 ("*"/unscoped) by // Scope-list index per channel (see ScopeList): 0 = "*" (unscoped), 1.. a
// default. Fixed at 64 slots (not // list entry, CH_SCOPE_DEFAULT (the default for every channel) = whatever
// the list's default is (MyMesh::channelScope()). Fixed at 64 slots (not
// MAX_GROUP_CHANNELS, which varies by board/variant and would make // MAX_GROUP_CHANNELS, which varies by board/variant and would make
// sizeof(NodePrefs) variant-dependent) -- same implicit channel-count cap // sizeof(NodePrefs) variant-dependent) -- same implicit channel-count cap
// every ch_notif_*/ch_fav_bitmask uint64_t bitmask above already has. // every ch_notif_*/ch_fav_bitmask uint64_t bitmask above already has.
static const uint8_t MAX_SCOPED_CHANNELS = 64; static const uint8_t MAX_SCOPED_CHANNELS = 64;
static const uint8_t CH_SCOPE_DEFAULT = 0xFF;
uint8_t ch_scope_idx[MAX_SCOPED_CHANNELS]; // [del→onChannelRemoved] uint8_t ch_scope_idx[MAX_SCOPED_CHANNELS]; // [del→onChannelRemoved]
struct DmNotifEntry { uint8_t prefix[4]; uint8_t state; }; // state: 0=default,1=muted,2=force-on struct DmNotifEntry { uint8_t prefix[4]; uint8_t state; }; // state: 0=default,1=muted,2=force-on
static const int DM_NOTIF_TABLE_MAX = 16; static const int DM_NOTIF_TABLE_MAX = 16;
@@ -525,6 +527,11 @@ struct NodePrefs { // persisted to file
// hibernate (MyMesh::startInterface). USB keeps working either way. // hibernate (MyMesh::startInterface). USB keeps working either way.
uint8_t ble_off; uint8_t ble_off;
// 1 once ch_scope_idx knows CH_SCOPE_DEFAULT. A 2.0-rc1 file has 0 here and
// its channels' 0 ("*", then the only start value) becomes "follow the
// default" on load (DataStore sanitize).
uint8_t ch_scope_follow;
// Single source of truth for the live-share option tables (shared by the Map // Single source of truth for the live-share option tables (shared by the Map
// UI labels and the auto-send engine in UITask). // UI labels and the auto-send engine in UITask).
static const uint8_t LOC_SHARE_MOVE_COUNT = 4; static const uint8_t LOC_SHARE_MOVE_COUNT = 4;
@@ -193,9 +193,15 @@ static void setFavourite(NodePrefs* p, uint8_t idx, bool on) {
if (on) p->ch_fav_bitmask |= 1ULL << idx; else p->ch_fav_bitmask &= ~(1ULL << idx); if (on) p->ch_fav_bitmask |= 1ULL << idx; else p->ch_fav_bitmask &= ~(1ULL << idx);
} }
// Scope: index into the_mesh.scopeList() (0 = "*", unscoped). // Scope as a picker row: 0 = Default (follow the list's default), then the
// list itself from 1 ("*" = unscoped, then the named entries).
static uint8_t scope(const NodePrefs* p, uint8_t idx) { static uint8_t scope(const NodePrefs* p, uint8_t idx) {
return (p && idx < NodePrefs::MAX_SCOPED_CHANNELS) ? p->ch_scope_idx[idx] : 0; const uint8_t ci = (p && idx < NodePrefs::MAX_SCOPED_CHANNELS) ? p->ch_scope_idx[idx] : NodePrefs::CH_SCOPE_DEFAULT;
return ci == NodePrefs::CH_SCOPE_DEFAULT ? 0 : ci + 1;
}
// A picker row back to the ch_scope_idx value (MyMesh::setChannelScope()).
static uint8_t scopeFromRow(int row) {
return row <= 0 ? NodePrefs::CH_SCOPE_DEFAULT : (uint8_t)(row - 1);
} }
// CONTRACT: every NodePrefs field that keys on a channel index is cleared // CONTRACT: every NodePrefs field that keys on a channel index is cleared
@@ -214,8 +220,8 @@ static bool onRemoved(NodePrefs* p, uint8_t idx) {
if (p->ch_notif_melody_set & m) { p->ch_notif_melody_set &= ~m; p->ch_notif_melody_2 &= ~m; changed = true; } if (p->ch_notif_melody_set & m) { p->ch_notif_melody_set &= ~m; p->ch_notif_melody_2 &= ~m; changed = true; }
if (p->ch_notif_override & m) { p->ch_notif_override &= ~m; p->ch_notif_muted &= ~m; changed = true; } if (p->ch_notif_override & m) { p->ch_notif_override &= ~m; p->ch_notif_muted &= ~m; changed = true; }
if (p->ch_fav_bitmask & m) { p->ch_fav_bitmask &= ~m; changed = true; } if (p->ch_fav_bitmask & m) { p->ch_fav_bitmask &= ~m; changed = true; }
if (idx < NodePrefs::MAX_SCOPED_CHANNELS && p->ch_scope_idx[idx]) { if (idx < NodePrefs::MAX_SCOPED_CHANNELS && p->ch_scope_idx[idx] != NodePrefs::CH_SCOPE_DEFAULT) {
p->ch_scope_idx[idx] = 0; // back to "*", same as a never-configured channel p->ch_scope_idx[idx] = NodePrefs::CH_SCOPE_DEFAULT; // back to Default, as a new channel
changed = true; changed = true;
} }
int slot = favslots::findChannel(p, idx); int slot = favslots::findChannel(p, idx);
@@ -82,9 +82,9 @@ void UITask::channelMenu(int idx) {
(void*)(uintptr_t)C_MELODY); (void*)(uintptr_t)C_MELODY);
const ScopeList& sl = the_mesh.scopeList(); const ScopeList& sl = the_mesh.scopeList();
if (sl.count > 0) { // only once regions are set up (Settings in the app) if (sl.count > 0) { // only once regions are set up (Settings in the app)
int o = 0; int o = snprintf(s_scope_opts, sizeof(s_scope_opts), "Default (%s)", sl.name(sl.default_idx));
for (uint8_t i = 0; i <= sl.count && o < (int)sizeof(s_scope_opts) - 24; i++) for (uint8_t i = 0; i <= sl.count && o < (int)sizeof(s_scope_opts) - 26; i++)
o += snprintf(s_scope_opts + o, sizeof(s_scope_opts) - o, "%s%s", i ? "\n" : "", sl.name(i)); o += snprintf(s_scope_opts + o, sizeof(s_scope_opts) - o, "\n%s", sl.name(i));
choiceRow(g, "Scope", "Region it's sent in", s_scope_opts, chanctl::scope(_prefs, idx), onChanDropdown, choiceRow(g, "Scope", "Region it's sent in", s_scope_opts, chanctl::scope(_prefs, idx), onChanDropdown,
(void*)(uintptr_t)C_SCOPE); (void*)(uintptr_t)C_SCOPE);
} }
@@ -110,7 +110,7 @@ void UITask::channelSet(uint8_t which, int v) {
using namespace chanview; using namespace chanview;
if (!chanctl::exists(s_idx) || !_prefs) return; if (!chanctl::exists(s_idx) || !_prefs) return;
if (which == C_NOTIF) chanctl::setNotif(_prefs, s_idx, (uint8_t)v); if (which == C_NOTIF) chanctl::setNotif(_prefs, s_idx, (uint8_t)v);
else if (which == C_SCOPE) the_mesh.setChannelScope(s_idx, (uint8_t)v); else if (which == C_SCOPE) the_mesh.setChannelScope(s_idx, chanctl::scopeFromRow(v));
else if (which == C_MELODY) { chanctl::setMelody(_prefs, s_idx, (uint8_t)v); hearMelody(v); } else if (which == C_MELODY) { chanctl::setMelody(_prefs, s_idx, (uint8_t)v); hearMelody(v); }
prefsSave(); prefsSave();
} }
@@ -63,10 +63,11 @@ class MessagesScreen : public UIScreen {
char _ctx_notif_item[22]; char _ctx_notif_item[22];
char _ctx_melody_item[20]; char _ctx_melody_item[20];
char _ctx_scope_item[30]; // "Scope: <name>" char _ctx_scope_item[30]; // "Scope: <name>"
// Scope sub-picker: single-select from the shared named-scope list, char _ctx_scope_def[34]; // the picker's first row: "Default (<name>)"
// reusing _ctx_menu itself as a popup -- same idiom as _pin_picker_active's // Scope sub-picker: single-select from Default + the shared named-scope
// "Pick slot" submenu below (row index == list index directly, no // list, reusing _ctx_menu itself as a popup -- same idiom as
// per-row value cycling needed since Enter just picks and closes). // _pin_picker_active's "Pick slot" submenu below (rows as chanctl::scope(),
// no per-row value cycling needed since Enter just picks and closes).
bool _scope_pick_active = false; bool _scope_pick_active = false;
char _ctx_pin_item[28]; // "Pin to dial" or "Unpin (slot N)" char _ctx_pin_item[28]; // "Pin to dial" or "Unpin (slot N)"
char _ctx_fav_item[12]; // "Fav: ON" / "Fav: OFF" — shared by the channel, char _ctx_fav_item[12]; // "Fav: ON" / "Fav: OFF" — shared by the channel,
@@ -1670,7 +1671,7 @@ public:
the_mesh.getChannel(_sel_channel_idx, ch); the_mesh.getChannel(_sel_channel_idx, ch);
char title[32]; char title[32];
NodePrefs* p_hdr = _task->getNodePrefs(); NodePrefs* p_hdr = _task->getNodePrefs();
uint8_t hdr_sc_idx = (p_hdr && _sel_channel_idx < NodePrefs::MAX_SCOPED_CHANNELS) ? p_hdr->ch_scope_idx[_sel_channel_idx] : 0; uint8_t hdr_sc_idx = p_hdr ? the_mesh.channelScope(_sel_channel_idx) : 0;
if (hdr_sc_idx != 0) { if (hdr_sc_idx != 0) {
// Non-wildcard scope set on this channel -- surface it in the title, // Non-wildcard scope set on this channel -- surface it in the title,
// same as the app's own per-channel scope tag, so it's obvious at a // same as the app's own per-channel scope tag, so it's obvious at a
@@ -2076,9 +2077,9 @@ public:
} }
auto res = _ctx_menu.handleInput(c); auto res = _ctx_menu.handleInput(c);
if (_scope_pick_active) { if (_scope_pick_active) {
// Scope sub-menu: row index == list index directly ("*" first). // Scope sub-menu: Default first, then the list ("*" first).
if (res == PopupMenu::SELECTED) { if (res == PopupMenu::SELECTED) {
the_mesh.setChannelScope(_ctx_ch_idx, (uint8_t)_ctx_menu.selectedIndex()); the_mesh.setChannelScope(_ctx_ch_idx, chanctl::scopeFromRow(_ctx_menu.selectedIndex()));
the_mesh.savePrefs(); the_mesh.savePrefs();
} }
if (res != PopupMenu::NONE) _scope_pick_active = false; if (res != PopupMenu::NONE) _scope_pick_active = false;
@@ -2121,12 +2122,12 @@ public:
_history.setChUnread(ch_idx, 0); _history.setChUnread(ch_idx, 0);
markReadAlert(cleared); markReadAlert(cleared);
} else if (sel == 4) { // Scope } else if (sel == 4) { // Scope
NodePrefs* p2 = _task->getNodePrefs();
uint8_t cur = (p2 && ch_idx < NodePrefs::MAX_SCOPED_CHANNELS) ? p2->ch_scope_idx[ch_idx] : 0;
const ScopeList& sl = the_mesh.scopeList(); const ScopeList& sl = the_mesh.scopeList();
snprintf(_ctx_scope_def, sizeof(_ctx_scope_def), "Default (%s)", sl.name(sl.default_idx));
_ctx_menu.begin("Scope", 4); _ctx_menu.begin("Scope", 4);
_ctx_menu.addItem(_ctx_scope_def);
for (uint8_t i = 0; i <= sl.count; i++) _ctx_menu.addItem(sl.name(i)); for (uint8_t i = 0; i <= sl.count; i++) _ctx_menu.addItem(sl.name(i));
_ctx_menu.setSelected(cur); _ctx_menu.setSelected(chanctl::scope(_task->getNodePrefs(), ch_idx));
_scope_pick_active = true; _scope_pick_active = true;
return true; // list rebuild below would close the submenu return true; // list rebuild below would close the submenu
} else if (sel == 5) { // Pin / Unpin } else if (sel == 5) { // Pin / Unpin
@@ -2211,9 +2212,9 @@ public:
{ int pinned_slot = _task->findFavouriteChannelSlot(ch_idx); { int pinned_slot = _task->findFavouriteChannelSlot(ch_idx);
if (pinned_slot >= 0) snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Unpin (slot %d)", pinned_slot + 1); if (pinned_slot >= 0) snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Unpin (slot %d)", pinned_slot + 1);
else snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Pin to dial"); } else snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Pin to dial"); }
{ NodePrefs* p2 = _task->getNodePrefs(); { const uint8_t row = chanctl::scope(_task->getNodePrefs(), ch_idx);
uint8_t sc_idx = (p2 && ch_idx < NodePrefs::MAX_SCOPED_CHANNELS) ? p2->ch_scope_idx[ch_idx] : 0; snprintf(_ctx_scope_item, sizeof(_ctx_scope_item), "Scope: %s",
snprintf(_ctx_scope_item, sizeof(_ctx_scope_item), "Scope: %s", the_mesh.scopeList().name(sc_idx)); } row ? the_mesh.scopeList().name(row - 1) : "Default"); }
_ctx_menu.begin("Channel options", 6); _ctx_menu.begin("Channel options", 6);
_ctx_menu.addItem("Mark all read"); _ctx_menu.addItem("Mark all read");
_ctx_menu.addValueItem(_ctx_notif_item); _ctx_menu.addValueItem(_ctx_notif_item);
@@ -761,13 +761,9 @@ public:
if (res == PopupMenu::SELECTED) { if (res == PopupMenu::SELECTED) {
int sel = _scope_action_menu.selectedIndex(); int sel = _scope_action_menu.selectedIndex();
if (sel == 0) { // Set as default if (sel == 0) { // Set as default
// DMs, the relay's primary slot and every channel left on
// "Default" follow it; the list marks it [default].
the_mesh.setDefaultScope((uint8_t)_scope_action_idx); the_mesh.setDefaultScope((uint8_t)_scope_action_idx);
// "Default" is the protocol's own word (CMD_SET_DEFAULT_FLOOD_SCOPE,
// and what the app shows), but on its own it doesn't say default
// for *what* -- channels carry their own pick and don't inherit it.
// Spell out the two things it actually governs, at the moment the
// user sets it, rather than leaving the list marker to imply more.
_task->showAlert("Default: DMs + relay", 1400);
} else if (sel == 1 && _scope_action_idx >= 1) { // Rename } else if (sel == 1 && _scope_action_idx >= 1) { // Rename
_scope_rename_idx = _scope_action_idx; _scope_rename_idx = _scope_action_idx;
_kb->begin(sl.name((uint8_t)_scope_action_idx), 23); _kb->begin(sl.name((uint8_t)_scope_action_idx), 23);