mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-01 01:36:12 +00:00
The "_dirty bool, then if (_dirty) the_mesh.savePrefs() on exit" pattern was duplicated across 9 screens with subtle divergence: some cleared the flag after saving, some left it set and relied on onShow() to reset. Replace all 12 exit sites with a single _task->savePrefsIfDirty(flag) helper that saves once only if dirty and always clears the flag, so the "did we touch flash?" answer lives in one place and the reset is consistent. Edit sites still mark the flag manually (inherent to change tracking); only the persist-on-exit boilerplate is centralised. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
223 lines
7.4 KiB
C++
223 lines
7.4 KiB
C++
#pragma once
|
|
// Custom screen — not part of upstream UITask.cpp
|
|
// Included by UITask.cpp after KeyboardWidget.h is defined.
|
|
|
|
class BotScreen : public UIScreen {
|
|
UITask* _task;
|
|
NodePrefs* _prefs;
|
|
|
|
// Items: 0=Enable(DM), 1=Channel, 2=Trigger DM, 3=Reply DM, 4=Trigger Ch,
|
|
// 5=Reply Ch, 6=Commands, 7=Quiet from, 8=Quiet to
|
|
static const int ITEM_COUNT = 9;
|
|
|
|
int _sel;
|
|
int _scroll; // index of first visible item (managed by drawList in render)
|
|
bool _dirty;
|
|
|
|
// keyboard state (reused for trigger and reply fields)
|
|
int _kb_field; // -1=off, else the item index being edited (2..5)
|
|
KeyboardWidget* _kb;
|
|
|
|
// channel cache (refreshed on enter)
|
|
int _num_channels;
|
|
uint8_t _channel_indices[MAX_GROUP_CHANNELS];
|
|
|
|
void refreshChannels() {
|
|
_num_channels = 0;
|
|
ChannelDetails ch;
|
|
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
|
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0')
|
|
_channel_indices[_num_channels++] = (uint8_t)i;
|
|
}
|
|
}
|
|
|
|
// Returns index into _channel_indices[] for current bot_channel_idx, or -1 if not found/disabled.
|
|
int currentChannelListIdx() const {
|
|
if (!_prefs->bot_channel_enabled) return -1;
|
|
for (int i = 0; i < _num_channels; i++)
|
|
if (_channel_indices[i] == _prefs->bot_channel_idx) return i;
|
|
return -1;
|
|
}
|
|
|
|
public:
|
|
BotScreen(UITask* task, NodePrefs* prefs, KeyboardWidget* kb) : _task(task), _prefs(prefs), _kb(kb) {}
|
|
|
|
void onShow() override {
|
|
_sel = 0;
|
|
_scroll = 0;
|
|
_kb_field = -1;
|
|
_dirty = false;
|
|
refreshChannels();
|
|
}
|
|
|
|
int render(DisplayDriver& display) override {
|
|
display.setTextSize(1);
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
if (_kb_field >= 0) {
|
|
return _kb->render(display);
|
|
}
|
|
|
|
int val_x = display.valCol();
|
|
|
|
display.drawCenteredHeader("AUTO-REPLY BOT");
|
|
// reply counter, right-aligned in the header
|
|
uint16_t sent = the_mesh.botReplyCount();
|
|
if (sent > 0) {
|
|
char cbuf[12];
|
|
snprintf(cbuf, sizeof(cbuf), "%u", (unsigned)sent);
|
|
int cw = display.getTextWidth(cbuf);
|
|
display.setCursor(display.width() - cw - 1, 0);
|
|
display.print(cbuf);
|
|
}
|
|
|
|
static const char* labels[] = { "Enable", "Channel", "Trigger DM", "Reply DM",
|
|
"Trigger Ch", "Reply Ch", "Commands",
|
|
"Quiet from", "Quiet to" };
|
|
drawList(display, ITEM_COUNT, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
|
display.drawSelectionRow(0, y - 1, display.width() - reserve, display.lineStep() - 1, sel);
|
|
display.setCursor(2, y);
|
|
display.print(labels[i]);
|
|
display.setCursor(val_x, y);
|
|
|
|
if (i == 0) {
|
|
display.print(_prefs->bot_enabled ? "ON" : "OFF");
|
|
} else if (i == 1) {
|
|
if (!_prefs->bot_channel_enabled || _num_channels == 0) {
|
|
display.print("OFF");
|
|
} else {
|
|
ChannelDetails ch;
|
|
if (the_mesh.getChannel(_prefs->bot_channel_idx, ch) && ch.name[0])
|
|
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve,ch.name);
|
|
else
|
|
display.print("?");
|
|
}
|
|
} else if (i == 2 || i == 4) {
|
|
const char* tr = (i == 2) ? _prefs->bot_trigger : _prefs->bot_trigger_ch;
|
|
const char* shown = !tr[0] ? "(none)"
|
|
: (tr[0] == '*' && !tr[1]) ? "(any msg)" // wildcard / away mode
|
|
: tr;
|
|
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve,shown);
|
|
} else if (i == 3 || i == 5) {
|
|
const char* rp = (i == 3) ? _prefs->bot_reply_dm : _prefs->bot_reply_ch;
|
|
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve,rp[0] ? rp : "(none)");
|
|
} else if (i == 6) {
|
|
display.print(_prefs->bot_commands_enabled ? "ON" : "OFF");
|
|
} else { // i == 7 (quiet from) or i == 8 (quiet to)
|
|
bool off = (_prefs->bot_quiet_start == _prefs->bot_quiet_end);
|
|
if (off) {
|
|
display.print("OFF");
|
|
} else {
|
|
char hb[8];
|
|
snprintf(hb, sizeof(hb), "%02d:00", i == 7 ? _prefs->bot_quiet_start : _prefs->bot_quiet_end);
|
|
display.print(hb);
|
|
}
|
|
}
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
});
|
|
return 2000;
|
|
}
|
|
|
|
bool handleInput(char c) override {
|
|
bool up = (c == KEY_UP);
|
|
bool down = (c == KEY_DOWN);
|
|
bool left = keyIsPrev(c);
|
|
bool right = keyIsNext(c);
|
|
bool enter = (c == KEY_ENTER);
|
|
bool cancel = (c == KEY_CANCEL || c == KEY_CONTEXT_MENU);
|
|
|
|
if (_kb_field >= 0) {
|
|
auto res = _kb->handleInput(c);
|
|
if (res == KeyboardWidget::DONE) {
|
|
char* dst = fieldBuf(_kb_field);
|
|
int cap = fieldCap(_kb_field);
|
|
if (dst) { strncpy(dst, _kb->buf, cap - 1); dst[cap - 1] = '\0'; }
|
|
_dirty = true;
|
|
_kb_field = -1;
|
|
} else if (res == KeyboardWidget::CANCELLED) {
|
|
_kb_field = -1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (cancel) {
|
|
_task->savePrefsIfDirty(_dirty);
|
|
_task->gotoToolsScreen();
|
|
return true;
|
|
}
|
|
// drawList() reclamps _scroll from _sel every render.
|
|
if (up) { _sel = (_sel > 0) ? _sel - 1 : ITEM_COUNT - 1; return true; }
|
|
if (down) { _sel = (_sel < ITEM_COUNT - 1) ? _sel + 1 : 0; return true; }
|
|
|
|
if (_sel == 0 && (enter || left || right)) {
|
|
_prefs->bot_enabled ^= 1;
|
|
_dirty = true;
|
|
return true;
|
|
}
|
|
if (_sel == 1) {
|
|
if (_num_channels == 0) return false;
|
|
// Cycle: OFF → ch[0] → ch[1] → ... → OFF
|
|
int idx = currentChannelListIdx(); // -1 = OFF
|
|
if (right || enter) {
|
|
idx++;
|
|
if (idx >= _num_channels) idx = -1; // wrap to OFF
|
|
}
|
|
if (left) {
|
|
idx--;
|
|
if (idx < -1) idx = _num_channels - 1;
|
|
}
|
|
if (left || right || enter) {
|
|
if (idx < 0) {
|
|
_prefs->bot_channel_enabled = 0;
|
|
} else {
|
|
_prefs->bot_channel_enabled = 1;
|
|
_prefs->bot_channel_idx = _channel_indices[idx];
|
|
}
|
|
_dirty = true;
|
|
return true;
|
|
}
|
|
}
|
|
if (_sel == 6 && (enter || left || right)) {
|
|
_prefs->bot_commands_enabled ^= 1;
|
|
_dirty = true;
|
|
return true;
|
|
}
|
|
if ((_sel == 7 || _sel == 8) && (left || right)) {
|
|
uint8_t& h = (_sel == 7) ? _prefs->bot_quiet_start : _prefs->bot_quiet_end;
|
|
h = right ? (h + 1) % 24 : (h + 23) % 24;
|
|
_dirty = true;
|
|
return true;
|
|
}
|
|
if ((_sel == 2 || _sel == 3 || _sel == 4 || _sel == 5) && enter) {
|
|
_kb_field = _sel;
|
|
_kb->begin(fieldBuf(_sel), fieldCap(_sel) - 1);
|
|
bool is_trigger = (_sel == 2 || _sel == 4);
|
|
if (is_trigger) _kb->clearPlaceholders(); // trigger is literal — placeholders never match
|
|
else kbAddSensorPlaceholders(*_kb, &sensors);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private:
|
|
// Maps a keyboard-editable item index to its backing string + capacity.
|
|
char* fieldBuf(int item) {
|
|
switch (item) {
|
|
case 2: return _prefs->bot_trigger;
|
|
case 3: return _prefs->bot_reply_dm;
|
|
case 4: return _prefs->bot_trigger_ch;
|
|
case 5: return _prefs->bot_reply_ch;
|
|
default: return nullptr;
|
|
}
|
|
}
|
|
int fieldCap(int item) {
|
|
switch (item) {
|
|
case 2: return sizeof(_prefs->bot_trigger);
|
|
case 3: return sizeof(_prefs->bot_reply_dm);
|
|
case 4: return sizeof(_prefs->bot_trigger_ch);
|
|
case 5: return sizeof(_prefs->bot_reply_ch);
|
|
default: return 0;
|
|
}
|
|
}
|
|
};
|