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;
|
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 rows = gridRows();
|
||||||
const int cols = gridCols();
|
const int cols = gridCols();
|
||||||
|
|
||||||
|
|||||||
@@ -1386,6 +1386,13 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
|||||||
uint32_t aoff = autoOffMillis();
|
uint32_t aoff = autoOffMillis();
|
||||||
_auto_off = millis() + (aoff > 0 ? aoff : AUTO_OFF_MILLIS);
|
_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)
|
#if defined(PIN_USER_BTN)
|
||||||
user_btn.begin();
|
user_btn.begin();
|
||||||
#endif
|
#endif
|
||||||
@@ -2063,6 +2070,47 @@ bool UITask::dequeueKey(char& c) {
|
|||||||
return true;
|
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() {
|
void UITask::loop() {
|
||||||
// Background delivery: resend pending on-device DMs whose ACK timed out, and
|
// Background delivery: resend pending on-device DMs whose ACK timed out, and
|
||||||
// finalise the ✗ marker — runs regardless of which screen is active.
|
// finalise the ✗ marker — runs regardless of which screen is active.
|
||||||
@@ -2159,6 +2207,7 @@ void UITask::loop() {
|
|||||||
_analogue_pin_read_millis = millis();
|
_analogue_pin_read_millis = millis();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
pollCardKB();
|
||||||
#if defined(BACKLIGHT_BTN)
|
#if defined(BACKLIGHT_BTN)
|
||||||
if (millis() > next_backlight_btn_check) {
|
if (millis() > next_backlight_btn_check) {
|
||||||
bool touch_state = digitalRead(PIN_BUTTON2);
|
bool touch_state = digitalRead(PIN_BUTTON2);
|
||||||
|
|||||||
@@ -193,6 +193,17 @@ class UITask : public AbstractUITask {
|
|||||||
void enqueueKey(char c);
|
void enqueueKey(char c);
|
||||||
bool dequeueKey(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);
|
void setCurrScreen(UIScreen* c);
|
||||||
|
|
||||||
// Centred alert overlay (the showAlert() box). Wraps long text to up to
|
// 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`).
|
- **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.
|
- **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`.
|
- **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
|
### Fixes
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,13 @@
|
|||||||
#define KEY_NEXT 0xF1
|
#define KEY_NEXT 0xF1
|
||||||
#define KEY_PREV 0xF2
|
#define KEY_PREV 0xF2
|
||||||
#define KEY_CONTEXT_MENU 0xF3
|
#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
|
// "Previous"/"Next" navigation keys: the directional key and its rotary-encoder
|
||||||
// twin. Decrement / increment a value, or step a selection. (Cancel and the
|
// twin. Decrement / increment a value, or step a selection. (Cancel and the
|
||||||
|
|||||||
Reference in New Issue
Block a user