mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
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:
@@ -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));
|
||||
flag(p.lock_compact, 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
|
||||
|
||||
@@ -804,6 +804,7 @@ void MyMesh::removeScope(uint8_t idx) {
|
||||
// fix-up ScopeList::remove() applied to default_idx.
|
||||
for (uint8_t i = 0; i < NodePrefs::MAX_SCOPED_CHANNELS; 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;
|
||||
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) {
|
||||
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() {
|
||||
@@ -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.
|
||||
sendFloodScoped(send_scope, pkt, delay_millis);
|
||||
} else {
|
||||
// Resolve THIS channel's own scope-list pick (Messages > channel context
|
||||
// menu > Scope:). Index 0 is "*", which means unscoped -- NOT "inherit the
|
||||
// default": the default is what DMs and the relay filter's primary slot
|
||||
// use, and what a channel is seeded with on upgrade, but once a channel
|
||||
// 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.
|
||||
// THIS channel's own scope (Messages > channel context menu > Scope:):
|
||||
// "Default" follows the list's default, "*" sends unscoped. A channel we
|
||||
// can't identify (findChannelIdx() == -1, e.g. a send whose secret isn't
|
||||
// in channels[]) takes the default too.
|
||||
int channel_idx = findChannelIdx(channel);
|
||||
uint8_t list_idx = (channel_idx >= 0 && channel_idx < NodePrefs::MAX_SCOPED_CHANNELS)
|
||||
? _prefs.ch_scope_idx[channel_idx] : _scope_list.default_idx;
|
||||
uint8_t list_idx = channel_idx >= 0 ? channelScope((uint8_t)channel_idx) : _scope_list.default_idx;
|
||||
TransportKey scope = _scope_list.key(list_idx);
|
||||
sendFloodScoped(scope, pkt, delay_millis);
|
||||
}
|
||||
@@ -1841,6 +1837,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
memset(_prefs.ch_scope_idx, NodePrefs::CH_SCOPE_DEFAULT, sizeof(_prefs.ch_scope_idx));
|
||||
_prefs.airtime_factor = 1.0;
|
||||
strcpy(_prefs.node_name, "NONAME");
|
||||
_prefs.freq = LORA_FREQ;
|
||||
@@ -1938,7 +1935,7 @@ void MyMesh::begin() {
|
||||
_store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon);
|
||||
// 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.
|
||||
bool scope_migrated_legacy = _store->loadScopeList(_scope_list, _prefs);
|
||||
_store->loadScopeList(_scope_list, _prefs);
|
||||
rebuildRepeatScopes();
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
applyApc(); // sets TX power to the ceiling and arms APC if enabled
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
|
||||
@@ -495,12 +495,19 @@ public:
|
||||
// fixes up repeat_extra_scope_mask, and clears any channel's ch_scope_idx
|
||||
// that pointed at it (shift-and-clamp, matching ScopeList::remove()).
|
||||
void removeScope(uint8_t idx);
|
||||
// Marks list index idx as the default (used for DMs and any channel/
|
||||
// repeater slot without its own pick). Persists the list and rebuilds
|
||||
// Marks list index idx as the default (used for DMs, the repeater's primary
|
||||
// slot and every channel left on "Default"). Persists the list and rebuilds
|
||||
// repeat_scopes[].
|
||||
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);
|
||||
// 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 {
|
||||
if (expected_ack == 0) return false; // 0 marks an empty/cleared slot, not a real ACK
|
||||
|
||||
@@ -314,12 +314,14 @@ struct NodePrefs { // persisted to file
|
||||
// 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_2; // bit i = use melody 2 (else melody 1, when set bit is set)
|
||||
// Scope-list index per channel (see ScopeList) -- 0 ("*"/unscoped) by
|
||||
// default. Fixed at 64 slots (not
|
||||
// Scope-list index per channel (see ScopeList): 0 = "*" (unscoped), 1.. a
|
||||
// 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
|
||||
// sizeof(NodePrefs) variant-dependent) -- same implicit channel-count cap
|
||||
// every ch_notif_*/ch_fav_bitmask uint64_t bitmask above already has.
|
||||
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]
|
||||
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;
|
||||
@@ -525,6 +527,11 @@ struct NodePrefs { // persisted to file
|
||||
// hibernate (MyMesh::startInterface). USB keeps working either way.
|
||||
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
|
||||
// UI labels and the auto-send engine in UITask).
|
||||
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);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
@@ -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_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 (idx < NodePrefs::MAX_SCOPED_CHANNELS && p->ch_scope_idx[idx]) {
|
||||
p->ch_scope_idx[idx] = 0; // back to "*", same as a never-configured channel
|
||||
if (idx < NodePrefs::MAX_SCOPED_CHANNELS && p->ch_scope_idx[idx] != NodePrefs::CH_SCOPE_DEFAULT) {
|
||||
p->ch_scope_idx[idx] = NodePrefs::CH_SCOPE_DEFAULT; // back to Default, as a new channel
|
||||
changed = true;
|
||||
}
|
||||
int slot = favslots::findChannel(p, idx);
|
||||
|
||||
@@ -82,9 +82,9 @@ void UITask::channelMenu(int idx) {
|
||||
(void*)(uintptr_t)C_MELODY);
|
||||
const ScopeList& sl = the_mesh.scopeList();
|
||||
if (sl.count > 0) { // only once regions are set up (Settings in the app)
|
||||
int o = 0;
|
||||
for (uint8_t i = 0; i <= sl.count && o < (int)sizeof(s_scope_opts) - 24; i++)
|
||||
o += snprintf(s_scope_opts + o, sizeof(s_scope_opts) - o, "%s%s", i ? "\n" : "", sl.name(i));
|
||||
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) - 26; 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,
|
||||
(void*)(uintptr_t)C_SCOPE);
|
||||
}
|
||||
@@ -110,7 +110,7 @@ void UITask::channelSet(uint8_t which, int v) {
|
||||
using namespace chanview;
|
||||
if (!chanctl::exists(s_idx) || !_prefs) return;
|
||||
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); }
|
||||
prefsSave();
|
||||
}
|
||||
|
||||
@@ -63,10 +63,11 @@ class MessagesScreen : public UIScreen {
|
||||
char _ctx_notif_item[22];
|
||||
char _ctx_melody_item[20];
|
||||
char _ctx_scope_item[30]; // "Scope: <name>"
|
||||
// Scope sub-picker: single-select from the shared named-scope list,
|
||||
// reusing _ctx_menu itself as a popup -- same idiom as _pin_picker_active's
|
||||
// "Pick slot" submenu below (row index == list index directly, no
|
||||
// per-row value cycling needed since Enter just picks and closes).
|
||||
char _ctx_scope_def[34]; // the picker's first row: "Default (<name>)"
|
||||
// Scope sub-picker: single-select from Default + the shared named-scope
|
||||
// list, reusing _ctx_menu itself as a popup -- same idiom as
|
||||
// _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;
|
||||
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,
|
||||
@@ -1670,7 +1671,7 @@ public:
|
||||
the_mesh.getChannel(_sel_channel_idx, ch);
|
||||
char title[32];
|
||||
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) {
|
||||
// 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
|
||||
@@ -2076,9 +2077,9 @@ public:
|
||||
}
|
||||
auto res = _ctx_menu.handleInput(c);
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
if (res != PopupMenu::NONE) _scope_pick_active = false;
|
||||
@@ -2121,12 +2122,12 @@ public:
|
||||
_history.setChUnread(ch_idx, 0);
|
||||
markReadAlert(cleared);
|
||||
} 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();
|
||||
snprintf(_ctx_scope_def, sizeof(_ctx_scope_def), "Default (%s)", sl.name(sl.default_idx));
|
||||
_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));
|
||||
_ctx_menu.setSelected(cur);
|
||||
_ctx_menu.setSelected(chanctl::scope(_task->getNodePrefs(), ch_idx));
|
||||
_scope_pick_active = true;
|
||||
return true; // list rebuild below would close the submenu
|
||||
} else if (sel == 5) { // Pin / Unpin
|
||||
@@ -2211,9 +2212,9 @@ public:
|
||||
{ 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);
|
||||
else snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Pin to dial"); }
|
||||
{ NodePrefs* p2 = _task->getNodePrefs();
|
||||
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", the_mesh.scopeList().name(sc_idx)); }
|
||||
{ const uint8_t row = chanctl::scope(_task->getNodePrefs(), ch_idx);
|
||||
snprintf(_ctx_scope_item, sizeof(_ctx_scope_item), "Scope: %s",
|
||||
row ? the_mesh.scopeList().name(row - 1) : "Default"); }
|
||||
_ctx_menu.begin("Channel options", 6);
|
||||
_ctx_menu.addItem("Mark all read");
|
||||
_ctx_menu.addValueItem(_ctx_notif_item);
|
||||
|
||||
@@ -761,13 +761,9 @@ public:
|
||||
if (res == PopupMenu::SELECTED) {
|
||||
int sel = _scope_action_menu.selectedIndex();
|
||||
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);
|
||||
// "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
|
||||
_scope_rename_idx = _scope_action_idx;
|
||||
_kb->begin(sl.name((uint8_t)_scope_action_idx), 23);
|
||||
|
||||
Reference in New Issue
Block a user