mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(ui): CardKB (I2C keyboard) support with full keyboard-only navigation
Auto-detected at boot on Wire1/Grove (addr 0x5F) -- no setting to flip, and a no-op on boards without that bus or with nothing attached. This UI's key codes (KEY_LEFT/UP/DOWN/RIGHT/ENTER/CANCEL) already match CardKB's own byte protocol, so most input needs zero translation and flows through the same key queue as physical buttons. Two bytes get remapped in UITask::pollCardKB(): - Enter, only when the on-screen keyboard's plain grid state is active (no placeholder/accent popup, not in cursor-mode), becomes a new KEY_KB_ENTER sentinel meaning "submit the field" -- reusing plain KEY_ENTER there would insert a stray character, since a CardKB typist's row/col never reflect an intentional grid selection. Everywhere else Enter is untouched, so selecting a placeholder or committing an accent still works normally. - Tab (otherwise unused) becomes KEY_CONTEXT_MENU, standing in for the "Hold-Enter" long-press gesture CardKB has no way to produce -- without it, ~30 context menus across the UI (message reply/navigate, Bot/Admin/ Repeater, ...) would be unreachable from the keyboard alone. KeyboardWidget gains a direct-typing path: printable ASCII inserts straight at the cursor bypassing the grid, Backspace deletes, KEY_KB_ENTER submits. Build-verified: WioTrackerL1_companion_solo_dual and WioTrackerL1Eink_companion_solo_dual both compile and link clean; also smoke-tested Heltec_mesh_solar_companion_radio_ble (no ENV_PIN_SDA/SCL) to confirm zero regression on boards without the feature. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -721,6 +721,30 @@ struct KeyboardWidget {
|
||||
|
||||
if (c == KEY_CANCEL) return CANCELLED;
|
||||
|
||||
// Direct-typing passthrough (CardKB or similar literal-ASCII input
|
||||
// source, see UITask::pollCardKB()). Printable characters insert
|
||||
// straight at the cursor, bypassing the on-screen grid entirely -- no
|
||||
// caps re-application, the source already sends the correct case.
|
||||
// Backspace deletes the previous character. KEY_KB_ENTER (only ever
|
||||
// emitted when the grid is in this exact plain state -- no popup, no
|
||||
// cursor-mode, see pollCardKB()) submits the field, instead of
|
||||
// KEY_ENTER's usual "commit grid cell (row,col)".
|
||||
if (c == KEY_KB_ENTER) return DONE;
|
||||
if (c == 0x08) {
|
||||
if (cursor_pos > 0) {
|
||||
int n = kbUtf8LastCharBytes(buf, cursor_pos);
|
||||
memmove(buf + cursor_pos - n, buf + cursor_pos, len - cursor_pos);
|
||||
len -= n; cursor_pos -= n;
|
||||
buf[len] = '\0';
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
if (c >= 0x20 && c <= 0x7E) {
|
||||
char one[2] = { c, '\0' };
|
||||
insertGlyph(one, false);
|
||||
return NONE;
|
||||
}
|
||||
|
||||
const int rows = gridRows();
|
||||
const int cols = gridCols();
|
||||
|
||||
|
||||
@@ -1386,6 +1386,13 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
uint32_t aoff = autoOffMillis();
|
||||
_auto_off = millis() + (aoff > 0 ? aoff : AUTO_OFF_MILLIS);
|
||||
|
||||
#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL)
|
||||
// Wire1 is already brought up by sensors.begin() (EnvironmentSensorManager),
|
||||
// which runs before this -- just probe for a CardKB sitting on it.
|
||||
Wire1.beginTransmission(0x5F);
|
||||
_has_cardkb = (Wire1.endTransmission() == 0);
|
||||
#endif
|
||||
|
||||
#if defined(PIN_USER_BTN)
|
||||
user_btn.begin();
|
||||
#endif
|
||||
@@ -2063,6 +2070,47 @@ bool UITask::dequeueKey(char& c) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Poll an optional CardKB (I2C keyboard, addr 0x5F) on Wire1/Grove, feeding
|
||||
// the same key queue as every physical button. Most of its output needs no
|
||||
// translation at all: CardKB's own arrow/Enter/Esc byte codes are already
|
||||
// identical to this UI's KEY_LEFT/UP/DOWN/RIGHT/ENTER/CANCEL (0xB4-0xB7, 13,
|
||||
// 27), and Backspace (0x08) / printable ASCII (0x20-0x7E) collide with
|
||||
// nothing that existed before. Two bytes get remapped:
|
||||
// - Enter, while the on-screen keyboard's plain grid state is active (no
|
||||
// placeholder/accent popup, not in cursor-mode) -- there it means "submit
|
||||
// the field", not KEY_ENTER's usual "commit grid cell (row,col)", which
|
||||
// would otherwise insert a stray character since direct typing never
|
||||
// touches the grid. See KeyboardWidget::handleInput()'s KEY_KB_ENTER
|
||||
// branch.
|
||||
// - Tab (0x09, otherwise unused by any screen) -> KEY_CONTEXT_MENU, the
|
||||
// "Hold-Enter" context-menu gesture used in ~30 places across the UI
|
||||
// (message reply/navigate, Bot/Admin/Repeater menus, ...). CardKB has no
|
||||
// press-duration concept to detect a long-press with, so Tab is the
|
||||
// closest free key standing in for it -- full keyboard-only navigation
|
||||
// would otherwise be unable to reach any of those menus at all.
|
||||
void UITask::pollCardKB() {
|
||||
#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL)
|
||||
if (!_has_cardkb) return;
|
||||
if (millis() - _cardkb_poll_ms < 30) return; // ~33 Hz poll, plenty for typing
|
||||
_cardkb_poll_ms = millis();
|
||||
Wire1.requestFrom(0x5F, 1);
|
||||
if (!Wire1.available()) return;
|
||||
uint8_t raw = Wire1.read();
|
||||
if (raw == 0) return; // no key down
|
||||
|
||||
char key;
|
||||
if (raw == 0x0D &&
|
||||
_kb.isVisible() && !_kb._ph_menu.active && !_kb.cursor_mode && !_kb.accent_active) {
|
||||
key = KEY_KB_ENTER;
|
||||
} else if (raw == 0x09) {
|
||||
key = KEY_CONTEXT_MENU;
|
||||
} else {
|
||||
key = (char)raw;
|
||||
}
|
||||
enqueueKey(checkDisplayOn(key));
|
||||
#endif
|
||||
}
|
||||
|
||||
void UITask::loop() {
|
||||
// Background delivery: resend pending on-device DMs whose ACK timed out, and
|
||||
// finalise the ✗ marker — runs regardless of which screen is active.
|
||||
@@ -2159,6 +2207,7 @@ void UITask::loop() {
|
||||
_analogue_pin_read_millis = millis();
|
||||
}
|
||||
#endif
|
||||
pollCardKB();
|
||||
#if defined(BACKLIGHT_BTN)
|
||||
if (millis() > next_backlight_btn_check) {
|
||||
bool touch_state = digitalRead(PIN_BUTTON2);
|
||||
|
||||
@@ -193,6 +193,17 @@ class UITask : public AbstractUITask {
|
||||
void enqueueKey(char c);
|
||||
bool dequeueKey(char& c);
|
||||
|
||||
// Optional M5Stack CardKB (I2C keyboard, addr 0x5F) on the Grove/Wire1 bus
|
||||
// -- reuses ENV_PIN_SDA/ENV_PIN_SCL (already brought up for
|
||||
// EnvironmentSensorManager) as the "this board has a second I2C bus" gate,
|
||||
// rather than a new board-specific pin define. No-op entirely on boards
|
||||
// without that bus, or when nothing ACKs 0x5F at boot.
|
||||
#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL)
|
||||
bool _has_cardkb = false;
|
||||
uint32_t _cardkb_poll_ms = 0;
|
||||
#endif
|
||||
void pollCardKB();
|
||||
|
||||
void setCurrScreen(UIScreen* c);
|
||||
|
||||
// Centred alert overlay (the showAlert() box). Wraps long text to up to
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- **Remote Bot gains Actions: `!buzz`, `!gps`, `!advert`.** A new **Actions** toggle (per target — Direct/Channel/Room, nested under each target's existing **Commands** toggle) enables three commands that change the device's own behaviour instead of just reporting on it: `!buzz [seconds]` sounds the buzzer as a find-me signal (default 5s, capped at 30s, sounds even if the buzzer is muted), `!gps on`/`!gps off` toggles GPS, and `!advert` sends an immediate advert. Off by default; combines with the existing query commands in one message the same way (`!batt !gps on` → `4.10V | GPS: on`).
|
||||
- **Bot Trigger fields accept multiple phrases.** Pack several trigger words into one Trigger field, comma-separated (`hi,hello there,yo`) — matching any one of them fires the reply, same as before for a single phrase.
|
||||
- **Remote Bot Actions gain `!gpio1`..`!gpio4`** (Wio Tracker L1 only — 4 otherwise-unused pins). Each pin is independently set to Off/Input/Output (GPIO1/GPIO2 also offer Analog) from a new **Tools › GPIO** screen; `!gpio1 on`/`!gpio1 off` drives an Output pin remotely, a bare `!gpio1` reports the current mode and reading (including a millivolt value in Analog mode). Gated by the same per-target Actions toggle as `!buzz`/`!gps`/`!advert`.
|
||||
- **Optional CardKB support (Wio Tracker L1, Grove I2C) — full keyboard-only navigation.** Plug an M5Stack CardKB into the Grove connector and it's auto-detected at boot — no setting to flip. Typing goes straight into the message/name field instead of navigating the on-screen keyboard grid; arrows, Enter and Esc work as expected everywhere else too (including inside the placeholder and accent popups); Tab opens the Hold-Enter context menu (message reply/navigate, Bot/Admin/Repeater menus, …) — the whole interface is reachable without touching a physical button.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
#define KEY_NEXT 0xF1
|
||||
#define KEY_PREV 0xF2
|
||||
#define KEY_CONTEXT_MENU 0xF3
|
||||
// A literal-ASCII input source's (e.g. an I2C CardKB) Enter, translated only
|
||||
// when the on-screen keyboard's plain grid state is active (see
|
||||
// UITask::pollCardKB()) -- means "submit the field", not KEY_ENTER's usual
|
||||
// "commit whatever grid cell (row,col) is currently selected", which would
|
||||
// otherwise insert a stray character since direct typing never touches the
|
||||
// grid.
|
||||
#define KEY_KB_ENTER 0x05
|
||||
|
||||
// "Previous"/"Next" navigation keys: the directional key and its rotary-encoder
|
||||
// twin. Decrement / increment a value, or step a selection. (Cancel and the
|
||||
|
||||
Reference in New Issue
Block a user