2026-05-12 14:45:02 +02:00
|
|
|
#pragma once
|
|
|
|
|
// Custom screen — not part of upstream UITask.cpp
|
2026-05-13 18:22:41 +02:00
|
|
|
// Included by UITask.cpp after KeyboardWidget.h is defined.
|
2026-05-12 14:45:02 +02:00
|
|
|
|
|
|
|
|
class BotScreen : public UIScreen {
|
|
|
|
|
UITask* _task;
|
|
|
|
|
NodePrefs* _prefs;
|
|
|
|
|
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
// 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;
|
2026-05-12 14:45:02 +02:00
|
|
|
|
2026-05-12 20:12:54 +02:00
|
|
|
int _sel;
|
2026-06-16 08:54:11 +02:00
|
|
|
int _scroll; // index of first visible item (managed by drawList in render)
|
2026-05-14 11:36:11 +02:00
|
|
|
bool _dirty;
|
2026-05-12 20:12:54 +02:00
|
|
|
|
2026-05-12 14:45:02 +02:00
|
|
|
// keyboard state (reused for trigger and reply fields)
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
int _kb_field; // -1=off, else the item index being edited (2..5)
|
2026-06-08 20:42:18 +02:00
|
|
|
KeyboardWidget* _kb;
|
2026-05-12 14:45:02 +02:00
|
|
|
|
2026-05-12 20:12:54 +02:00
|
|
|
// channel cache (refreshed on enter)
|
2026-05-12 14:45:02 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 20:12:54 +02:00
|
|
|
// 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;
|
2026-05-12 14:45:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public:
|
2026-06-08 20:42:18 +02:00
|
|
|
BotScreen(UITask* task, NodePrefs* prefs, KeyboardWidget* kb) : _task(task), _prefs(prefs), _kb(kb) {}
|
2026-05-12 14:45:02 +02:00
|
|
|
|
2026-06-29 18:37:04 +02:00
|
|
|
void onShow() override {
|
2026-05-12 14:45:02 +02:00
|
|
|
_sel = 0;
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
_scroll = 0;
|
2026-05-12 14:45:02 +02:00
|
|
|
_kb_field = -1;
|
2026-05-14 11:36:11 +02:00
|
|
|
_dirty = false;
|
2026-05-12 14:45:02 +02:00
|
|
|
refreshChannels();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int render(DisplayDriver& display) override {
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
|
|
|
|
if (_kb_field >= 0) {
|
2026-06-08 20:42:18 +02:00
|
|
|
return _kb->render(display);
|
2026-05-12 14:45:02 +02:00
|
|
|
}
|
|
|
|
|
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
int val_x = display.valCol();
|
|
|
|
|
|
|
|
|
|
display.drawCenteredHeader("AUTO-REPLY BOT");
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
// 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);
|
|
|
|
|
}
|
2026-05-12 14:45:02 +02:00
|
|
|
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
static const char* labels[] = { "Enable", "Channel", "Trigger DM", "Reply DM",
|
|
|
|
|
"Trigger Ch", "Reply Ch", "Commands",
|
|
|
|
|
"Quiet from", "Quiet to" };
|
2026-06-16 08:54:11 +02:00
|
|
|
drawList(display, ITEM_COUNT, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
2026-06-29 18:53:18 +02:00
|
|
|
drawRowSelection(display, y, sel, reserve);
|
2026-05-12 14:45:02 +02:00
|
|
|
display.setCursor(2, y);
|
2026-05-12 20:12:54 +02:00
|
|
|
display.print(labels[i]);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
display.setCursor(val_x, y);
|
2026-05-12 14:45:02 +02:00
|
|
|
|
|
|
|
|
if (i == 0) {
|
|
|
|
|
display.print(_prefs->bot_enabled ? "ON" : "OFF");
|
|
|
|
|
} else if (i == 1) {
|
2026-05-12 20:12:54 +02:00
|
|
|
if (!_prefs->bot_channel_enabled || _num_channels == 0) {
|
|
|
|
|
display.print("OFF");
|
|
|
|
|
} else {
|
2026-05-12 14:45:02 +02:00
|
|
|
ChannelDetails ch;
|
2026-05-12 20:12:54 +02:00
|
|
|
if (the_mesh.getChannel(_prefs->bot_channel_idx, ch) && ch.name[0])
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve,ch.name);
|
2026-05-12 14:45:02 +02:00
|
|
|
else
|
|
|
|
|
display.print("?");
|
|
|
|
|
}
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
} 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;
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve,shown);
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
} else if (i == 3 || i == 5) {
|
|
|
|
|
const char* rp = (i == 3) ? _prefs->bot_reply_dm : _prefs->bot_reply_ch;
|
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +02:00
|
|
|
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve,rp[0] ? rp : "(none)");
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
} 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);
|
|
|
|
|
}
|
2026-05-12 14:45:02 +02:00
|
|
|
}
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
});
|
2026-05-16 23:42:52 +02:00
|
|
|
return 2000;
|
2026-05-12 14:45:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool handleInput(char c) override {
|
|
|
|
|
bool up = (c == KEY_UP);
|
|
|
|
|
bool down = (c == KEY_DOWN);
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
bool left = keyIsPrev(c);
|
|
|
|
|
bool right = keyIsNext(c);
|
2026-05-12 14:45:02 +02:00
|
|
|
bool enter = (c == KEY_ENTER);
|
|
|
|
|
bool cancel = (c == KEY_CANCEL || c == KEY_CONTEXT_MENU);
|
|
|
|
|
|
|
|
|
|
if (_kb_field >= 0) {
|
2026-06-08 20:42:18 +02:00
|
|
|
auto res = _kb->handleInput(c);
|
2026-05-13 18:22:41 +02:00
|
|
|
if (res == KeyboardWidget::DONE) {
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
char* dst = fieldBuf(_kb_field);
|
|
|
|
|
int cap = fieldCap(_kb_field);
|
|
|
|
|
if (dst) { strncpy(dst, _kb->buf, cap - 1); dst[cap - 1] = '\0'; }
|
2026-05-14 11:36:11 +02:00
|
|
|
_dirty = true;
|
2026-05-13 18:22:41 +02:00
|
|
|
_kb_field = -1;
|
|
|
|
|
} else if (res == KeyboardWidget::CANCELLED) {
|
|
|
|
|
_kb_field = -1;
|
2026-05-12 14:45:02 +02:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cancel) {
|
2026-06-29 18:47:51 +02:00
|
|
|
_task->savePrefsIfDirty(_dirty);
|
2026-05-12 14:45:02 +02:00
|
|
|
_task->gotoToolsScreen();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-06-16 08:54:11 +02:00
|
|
|
// drawList() reclamps _scroll from _sel every render.
|
2026-06-18 09:16:57 +02:00
|
|
|
if (up) { _sel = (_sel > 0) ? _sel - 1 : ITEM_COUNT - 1; return true; }
|
|
|
|
|
if (down) { _sel = (_sel < ITEM_COUNT - 1) ? _sel + 1 : 0; return true; }
|
2026-05-12 14:45:02 +02:00
|
|
|
|
|
|
|
|
if (_sel == 0 && (enter || left || right)) {
|
|
|
|
|
_prefs->bot_enabled ^= 1;
|
2026-05-14 11:36:11 +02:00
|
|
|
_dirty = true;
|
2026-05-12 14:45:02 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (_sel == 1) {
|
2026-05-12 20:12:54 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2026-05-12 14:45:02 +02:00
|
|
|
if (left || right || enter) {
|
2026-05-12 20:12:54 +02:00
|
|
|
if (idx < 0) {
|
|
|
|
|
_prefs->bot_channel_enabled = 0;
|
2026-05-12 14:45:02 +02:00
|
|
|
} else {
|
2026-05-12 20:12:54 +02:00
|
|
|
_prefs->bot_channel_enabled = 1;
|
|
|
|
|
_prefs->bot_channel_idx = _channel_indices[idx];
|
2026-05-12 14:45:02 +02:00
|
|
|
}
|
2026-05-14 11:36:11 +02:00
|
|
|
_dirty = true;
|
2026-05-12 14:45:02 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
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) {
|
2026-05-13 18:22:41 +02:00
|
|
|
_kb_field = _sel;
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
_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);
|
2026-05-12 14:45:02 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-12 14:45:02 +02:00
|
|
|
};
|