diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e7386fef..4a03b313 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1307,8 +1307,20 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i // auto-discovery overwrite it. Skip the base class call entirely — ACKs // embedded in path responses will still be delivered via separate ACK packets. if (contact.flags & CONTACT_FLAG_CUSTOM_PATH) { +#if defined(MECK_TWATCH) + // Watch: keep the manually set path (don't let discovery overwrite it), but + // STILL deliver an embedded login/response reply riding on this path packet + // -- otherwise logins to custom-path contacts silently time out. Gated to + // the watch so other builds' admin flow is unchanged. + MESH_DEBUG_PRINTLN("onContactPathRecv: custom-path %s, keeping path, delivering response (type=%d len=%d)", contact.name, extra_type, extra_len); + if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 0) { + onContactResponse(contact, extra, extra_len); + } + return false; +#else MESH_DEBUG_PRINTLN("onContactPathRecv: skipping path update for custom-path contact %s", contact.name); return false; +#endif } return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len); } diff --git a/examples/companion_radio/ui-new/PathEditorScreen.h b/examples/companion_radio/ui-new/PathEditorScreen.h index 4824a4ed..23e13f24 100644 --- a/examples/companion_radio/ui-new/PathEditorScreen.h +++ b/examples/companion_radio/ui-new/PathEditorScreen.h @@ -57,6 +57,9 @@ private: bool _dirty; // Path has been modified bool _wantExit; // Set by Save & Exit — caller should navigate back +#if defined(MECK_TWATCH) + bool _wantKeyboard = false; // Set by Add hop -- caller opens the path keyboard +#endif bool _directLocked; // True = path is explicitly set to direct (0 hops, locked) // --- helpers --- @@ -189,6 +192,9 @@ public: _repScroll = 0; _dirty = false; _wantExit = false; +#if defined(MECK_TWATCH) + _wantKeyboard = false; +#endif _directLocked = false; // Load contact info @@ -489,6 +495,61 @@ public: return 5000; } +#if defined(MECK_TWATCH) + // Watch: replace the whole path from a typed string of comma-separated hex + // hops. Each hop is _bytesPerHop bytes (2*_bytesPerHop hex chars), matching + // the current mode toggle -- e.g. 1B "36,21,dd", 2B "3601,2198,dddd", 3B + // "360184,2198a7,dddddd". Returns NULL on success, or an error message (the + // existing path is left unchanged on any error). UITask is only forward + // declared here, so the caller shows the alert rather than this method. + const char* applyComposedPath(const char* str) { + if (str == nullptr) return "Empty path"; + const int expectChars = _bytesPerHop * 2; + uint8_t tmp[MAX_PATH_SIZE]; + int nbytes = 0, hops = 0; + const char* p = str; + while (*p) { + char tok[16]; + int tlen = 0; + while (*p && *p != ',') { + if (*p != ' ') { + if (tlen >= (int)sizeof(tok) - 1) return "Hop too long"; + tok[tlen++] = *p; + } + p++; + } + if (*p == ',') p++; + if (tlen == 0) continue; // tolerate stray/trailing commas + if (tlen != expectChars) return "Wrong hop length"; + if (hops >= 8) return "Max 8 hops"; + for (int b = 0; b < _bytesPerHop; b++) { + int hi = hexNibble(tok[b * 2]); + int lo = hexNibble(tok[b * 2 + 1]); + if (hi < 0 || lo < 0) return "Bad hex"; + if (nbytes >= MAX_PATH_SIZE) return "Path too long"; + tmp[nbytes++] = (uint8_t)((hi << 4) | lo); + } + hops++; + } + if (hops == 0) return "Empty path"; + memset(_pathBuf, 0, sizeof(_pathBuf)); + memcpy(_pathBuf, tmp, nbytes); + _hopCount = hops; + _pathLen = encodePath(); + _directLocked = false; + _dirty = true; + _menuCount = buildMenuCount(); + _menuSel = 0; + return nullptr; + } + static int hexNibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + } +#endif + bool handleInput(char c) override { if (_state == STATE_PICK_HOP) { return handlePickerInput(c); @@ -551,12 +612,19 @@ public: return true; case MENU_ADD_HOP: - // Enter picker mode — adding a hop clears direct lock _directLocked = false; +#if defined(MECK_TWATCH) + // Watch: request the on-screen keyboard to type the whole path as + // comma-separated hex hops. main loop polls wantsKeyboard() and opens + // it (UITask is only forward declared here, so we can't call it). + _wantKeyboard = true; +#else + // Enter picker mode -- adding a hop clears direct lock buildRepeaterList(); _repSel = 0; _repScroll = 0; _state = STATE_PICK_HOP; +#endif return true; case MENU_SET_DIRECT: @@ -706,6 +774,11 @@ public: EditorState getState() const { return _state; } bool isDirty() const { return _dirty; } bool wantsExit() const { return _wantExit; } +#if defined(MECK_TWATCH) + bool wantsKeyboard() const { return _wantKeyboard; } + void clearWantKeyboard() { _wantKeyboard = false; } + int getContactIdx() const { return _contactIdx; } +#endif private: void switchMode(int newBytesPerHop) { diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 0034878d..7db11bdc 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2592,6 +2592,13 @@ if (curr) curr->poll(); gotoHomeScreen(); } showAlert(dmSuccess ? "DM sent!" : "DM failed!", 1500); + } else if (purpose == TWatchKeyboardScreen::TWKB_PATH) { + PathEditorScreen* pe = (PathEditorScreen*)getPathEditorScreen(); + const char* perr = pe ? pe->applyComposedPath(sendText) : "No editor"; + kb->clearOutBuf(); + if (path_editor) setCurrScreen(path_editor); + else gotoHomeScreen(); + if (perr) showAlert(perr, 1500); } else { // TWKB_ADMIN_PASSWORD or TWKB_ADMIN_CLI RepeaterAdminScreen* admin = (RepeaterAdminScreen*)getRepeaterAdminScreen(); if (admin) { @@ -2604,6 +2611,13 @@ if (curr) curr->poll(); } } } + else if (curr == path_editor && path_editor != nullptr) { + PathEditorScreen* pe = (PathEditorScreen*)path_editor; + if (pe->wantsKeyboard()) { + pe->clearWantKeyboard(); + openTWatchKeyboard(TWatchKeyboardScreen::TWKB_PATH, pe->getContactIdx()); + } + } #endif if (_display != NULL && _display->isOn()) { diff --git a/variants/lilygo_twatch_s3/DRV2605Haptic.h b/variants/lilygo_twatch_s3/DRV2605Haptic.h index 0219397d..037edf8e 100644 --- a/variants/lilygo_twatch_s3/DRV2605Haptic.h +++ b/variants/lilygo_twatch_s3/DRV2605Haptic.h @@ -51,6 +51,12 @@ public: writeReg(REG_GO, 1); } + // Return the DRV2605 to standby (~1.8uA). begin() must be run again before the + // next buzz -- a GO in standby is ignored -- so callers reset their ready flag. + void standby() { + writeReg(REG_MODE, 0x40); // STANDBY = 1, internal trigger + } + private: static const uint8_t REG_MODE = 0x01; static const uint8_t REG_RTPIN = 0x02; @@ -83,4 +89,4 @@ private: _wire->requestFrom(_addr, (uint8_t)1); return _wire->available() ? _wire->read() : 0; } -}; +}; \ No newline at end of file diff --git a/variants/lilygo_twatch_s3/TWatchComposeScreens.h b/variants/lilygo_twatch_s3/TWatchComposeScreens.h index 2c8029c4..294a5dec 100644 --- a/variants/lilygo_twatch_s3/TWatchComposeScreens.h +++ b/variants/lilygo_twatch_s3/TWatchComposeScreens.h @@ -91,17 +91,6 @@ class TWatchChannelPicker : public UIScreen { _highlighted = (uint8_t)((_highlighted + 1) % _numChannels); ensureVisible(); } - // Map a draw-space y coordinate to a channel index, or -1 if the tap is - // above the list or on an empty row. getTouch() returns coords already - // divided by UI_ZOOM, so _downY is directly comparable to the row layout. - int rowAtY(int y) const { - if (y < HEADER_H) return -1; - int i = (y - HEADER_H) / ROW_H; - if (i < 0 || i >= visibleRows()) return -1; - int ci = _scrollTop + i; - if (ci >= _numChannels) return -1; - return ci; - } public: TWatchChannelPicker(DisplayDriver* display) @@ -148,11 +137,11 @@ public: _touchDown = false; unsigned long held = millis() - _downAt; if (_downX < 20 && _downY < HEADER_H) { _wantsExit = true; return; } // back arrow - int row = rowAtY(_downY); - if (row < 0) return; // tap outside the channel list - _highlighted = (uint8_t)row; // single tap selects the tapped row - ensureVisible(); - if (held >= TW_LONG_PRESS_MS) _confirmed = true; // long press opens it + if (held >= TW_LONG_PRESS_MS) { + if (_numChannels > 0) _confirmed = true; // select highlighted + } else { + if (_downY < _display->height() / 2) moveUp(); else moveDown(); // tap zones + } } } @@ -417,7 +406,7 @@ class TWatchKeyboardScreen : public UIScreen { uint8_t _channelIdx; public: - enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI }; + enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH }; private: Purpose _purpose; diff --git a/variants/lilygo_twatch_s3/WatchAlarmScreen.h b/variants/lilygo_twatch_s3/WatchAlarmScreen.h index a7f19602..7a4d8d69 100644 --- a/variants/lilygo_twatch_s3/WatchAlarmScreen.h +++ b/variants/lilygo_twatch_s3/WatchAlarmScreen.h @@ -227,6 +227,10 @@ public: } void dismiss() { + if (_hapticReady) { + _haptic.standby(); // drop DRV2605 back to ~1.8uA once the alarm ends + _hapticReady = false; // force begin() (exit standby) on the next alarm + } _mode = LIST; _ringSlot = -1; } @@ -423,4 +427,4 @@ private: save(); _mode = LIST; } -}; +}; \ No newline at end of file diff --git a/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h b/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h index ade7eceb..294a5dec 100644 --- a/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h +++ b/variants/lilygo_twatch_s3_plus/TWatchComposeScreens.h @@ -406,7 +406,7 @@ class TWatchKeyboardScreen : public UIScreen { uint8_t _channelIdx; public: - enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI }; + enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH }; private: Purpose _purpose;