diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 1eb08f0d..b9bae0fc 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1313,13 +1313,22 @@ void MyMesh::begin(bool has_display) { BaseChatMesh::begin(); if (!_store->loadMainIdentity(self_id)) { + Serial.println("[ID] loadMainIdentity FAILED — generating new identity"); self_id = radio_new_identity(); // create new random identity int count = 0; while (count < 10 && (self_id.pub_key[0] == 0x00 || self_id.pub_key[0] == 0xFF)) { // reserved id hashes self_id = radio_new_identity(); count++; } - _store->saveMainIdentity(self_id); + bool ok = _store->saveMainIdentity(self_id); + Serial.printf("[ID] saveMainIdentity returned %d\n", ok ? 1 : 0); + } else { + Serial.println("[ID] loadMainIdentity OK — using persisted identity"); + } + { + char hex[10]; + mesh::Utils::toHex(hex, self_id.pub_key, 4); + Serial.printf("[ID] pub_key[0..3] = %s\n", hex); } // if name is provided as a build flag, use that as default node name instead @@ -1418,6 +1427,7 @@ void MyMesh::startInterface(BaseSerialInterface &serial) { } void MyMesh::handleCmdFrame(size_t len) { + Serial.printf("[CMD] rx opcode=0x%02X len=%d\n", cmd_frame[0], (int)len); if (cmd_frame[0] == CMD_DEVICE_QEURY && len >= 2) { // sent when app establishes connection app_target_ver = cmd_frame[1]; // which version of protocol does app understand @@ -1720,12 +1730,19 @@ void MyMesh::handleCmdFrame(size_t len) { } } } else if (cmd_frame[0] == CMD_IMPORT_CONTACT && len > 2 + 32 + 64) { + Serial.printf("[IMP] CMD_IMPORT_CONTACT received, len=%d\n", len); if (importContact(&cmd_frame[1], len - 1)) { + Serial.println("[IMP] importContact OK, scheduling dirty flush"); dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); writeOKFrame(); } else { + Serial.println("[IMP] importContact REJECTED by BaseChatMesh"); writeErrFrame(ERR_CODE_ILLEGAL_ARG); } + } else if (cmd_frame[0] == CMD_IMPORT_CONTACT) { + Serial.printf("[IMP] CMD_IMPORT_CONTACT dropped — len=%d too short (need >%d)\n", + len, 2 + 32 + 64); + writeErrFrame(ERR_CODE_ILLEGAL_ARG); } else if (cmd_frame[0] == CMD_SYNC_NEXT_MESSAGE) { int out_len; if ((out_len = getFromOfflineQueue(out_frame)) > 0) { @@ -1862,23 +1879,29 @@ void MyMesh::handleCmdFrame(size_t len) { writeDisabledFrame(); #endif } else if (cmd_frame[0] == CMD_IMPORT_PRIVATE_KEY && len >= 65) { + Serial.printf("[PK] CMD_IMPORT_PRIVATE_KEY received, len=%d\n", (int)len); #if ENABLE_PRIVATE_KEY_IMPORT if (!mesh::LocalIdentity::validatePrivateKey(&cmd_frame[1])) { + Serial.println("[PK] validatePrivateKey FAILED — key bytes rejected"); writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid key } else { + Serial.println("[PK] validatePrivateKey OK — attempting save"); mesh::LocalIdentity identity; identity.readFrom(&cmd_frame[1], 64); if (_store->saveMainIdentity(identity)) { + Serial.println("[PK] saveMainIdentity OK"); self_id = identity; writeOKFrame(); // re-load contacts, to invalidate ecdh shared_secrets resetContacts(); _store->loadContacts(this); } else { + Serial.println("[PK] saveMainIdentity FAILED"); writeErrFrame(ERR_CODE_FILE_IO_ERROR); } } #else + Serial.println("[PK] ENABLE_PRIVATE_KEY_IMPORT not defined — responding DISABLED"); writeDisabledFrame(); #endif } else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) { @@ -2073,17 +2096,21 @@ void MyMesh::handleCmdFrame(size_t len) { writeErrFrame(ERR_CODE_NOT_FOUND); } } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 32) { + Serial.printf("[CH] CMD_SET_CHANNEL 256-bit secret not supported (len=%d)\n", len); writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // not supported (yet) } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16) { uint8_t channel_idx = cmd_frame[1]; + Serial.printf("[CH] CMD_SET_CHANNEL idx=%d len=%d\n", channel_idx, len); ChannelDetails channel; StrHelper::strncpy(channel.name, (char *)&cmd_frame[2], 32); memset(channel.channel.secret, 0, sizeof(channel.channel.secret)); memcpy(channel.channel.secret, &cmd_frame[2 + 32], 16); // NOTE: only 128-bit supported if (setChannel(channel_idx, channel)) { + Serial.println("[CH] setChannel OK, calling saveChannels"); saveChannels(); writeOKFrame(); } else { + Serial.printf("[CH] setChannel REJECTED (bad idx=%d)\n", channel_idx); writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx } } else if (cmd_frame[0] == CMD_SIGN_START) { @@ -2313,6 +2340,8 @@ void MyMesh::handleCmdFrame(size_t len) { _serial->writeFrame(out_frame, i); } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + Serial.printf("[CMD] UNKNOWN opcode=0x%02X len=%d — responded UNSUPPORTED\n", + cmd_frame[0], (int)len); MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); } } @@ -3205,18 +3234,32 @@ void MyMesh::loop() { if (_deferSaves) { // Voice session receiving — push save forward to avoid SPI contention dirty_contacts_expiry = futureMillis(2000); - } else if (!_store->isSaveInProgress()) { - _store->beginSaveContacts(this); + } else { +#ifdef HELTEC_MESH_POCKET + // Meshpocket: use upstream-style synchronous save. Max 500 contacts = + // ~74KB, finishes in well under a second on InternalFS. Avoids the + // chunked trio's File* lifetime pitfalls entirely. + Serial.println("[DS] saveContacts (synchronous) triggered by dirty flag"); + _store->saveContacts(this); dirty_contacts_expiry = 0; +#else + if (!_store->isSaveInProgress()) { + _store->beginSaveContacts(this); + dirty_contacts_expiry = 0; + } +#endif } } - // Drive chunked contact save — write a batch each loop iteration +#ifndef HELTEC_MESH_POCKET + // Drive chunked contact save — write a batch each loop iteration. + // Only used for non-Meshpocket builds (ESP32 PSRAM heavyweight). if (_store->isSaveInProgress() && !_deferSaves) { if (!_store->saveContactsChunk(20)) { // 20 contacts per chunk (~3KB, ~30ms) _store->finishSaveContacts(); // Done or error — verify and commit } } +#endif // Discovery scan timeout if (_discoveryActive && millisHasNowPassed(_discoveryTimeout)) { diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 037bb6b0..66bc82f1 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1663,10 +1663,29 @@ void halt() { void setup() { Serial.begin(115200); - delay(100); // Give serial time to initialize +#ifdef NRF52_PLATFORM + // nRF52 USB Serial (TinyUSB) needs to enumerate before the host sees prints. + // Wait up to 3s for a serial connection; without this, early setup() prints + // are silently dropped. Skip the wait entirely if nothing ever connects + // (running off battery with no USB). + uint32_t t0 = millis(); + while (!Serial && (millis() - t0) < 3000) { delay(10); } +#else + delay(100); // Give serial time to initialize (ESP32: CDC is already up) +#endif + + // Unconditional entry marker — tells us setup() is running and Serial works + Serial.println("[DIAG] setup() entered"); +#ifdef DISPLAY_CLASS + Serial.println("[DIAG] DISPLAY_CLASS is defined at compile time"); +#else + Serial.println("[DIAG] DISPLAY_CLASS NOT defined at compile time!"); +#endif + MESH_DEBUG_PRINTLN("=== setup() - STARTING ==="); board.begin(); + Serial.println("[DIAG] board.begin() returned"); MESH_DEBUG_PRINTLN("setup() - board.begin() done"); #ifdef DISPLAY_CLASS diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 3390ab52..924abc57 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -345,7 +345,10 @@ public: renderBatteryIndicator(display, _task->getBattMilliVolts()); #endif - // centered clock — only show when time is valid + // centered clock — only show when time is valid. + // Skipped on Meshpocket: no RTC hardware means time is unreliable between + // BLE sync events and wrong after any power cycle. +#ifndef HELTEC_MESH_POCKET { uint32_t now = _rtc->getCurrentTime(); if (now > 1700000000) { // valid timestamp (after ~Nov 2023) @@ -371,6 +374,7 @@ public: display.setTextSize(1); // restore } } +#endif // curr page indicator #if defined(LilyGo_T5S3_EPaper_Pro) int y = 14; // Closer to header @@ -483,8 +487,20 @@ public: } display.setTextSize(1); +#elif defined(HELTEC_MESH_POCKET) + // ----- Heltec Meshpocket: single USER button, no keyboard shortcuts ----- + // MSG/Pin/Connected already drawn above (lines ~400-432). Add a nav hint + // at y=72 (virtual coords) — matches other pages on this panel. Note: + // with EINK_SCALE_Y=1.28 and EINK_Y_OFFSET=10 the usable virtual Y range + // is ~0-85, so display.height()-12 overshoots the physical screen. + display.setColor(DisplayDriver::GREEN); + display.setTextSize(_node_prefs->smallTextSize()); + display.drawTextCentered(display.width() / 2, 72, + "Click: next Hold: action"); + display.setTextSize(1); + #else - // ----- T-Deck Pro: Keyboard shortcut text menu ----- + // ----- T-Deck Pro / Heltec V4 / other keyboard variants ----- display.setColor(DisplayDriver::LIGHT); display.setTextSize(_node_prefs->smallTextSize()); int menuLH = _node_prefs->smallLineH(); @@ -1324,6 +1340,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #ifdef MORSE_COMPOSE_ENABLED morse_screen = new MorseScreen(&rtc_clock); + Serial.printf("[UI] MorseScreen allocated at %p\n", morse_screen); +#else + Serial.println("[UI] MORSE_COMPOSE_ENABLED not defined — triple-click disabled"); #endif #if defined(LilyGo_T5S3_EPaper_Pro) @@ -2198,6 +2217,7 @@ char UITask::handleDoubleClick(char c) { } char UITask::handleTripleClick(char c) { + Serial.println("[UI] triple click event received"); MESH_DEBUG_PRINTLN("UITask: triple click triggered"); checkDisplayOn(c); #if defined(LilyGo_T5S3_EPaper_Pro) @@ -2213,13 +2233,18 @@ char UITask::handleTripleClick(char c) { // Triple-click from home screen → enter Morse compose mode. // From any other screen, fall through to the existing buzzer toggle (no-op // on Meshpocket but kept for other single-button variants). + Serial.printf("[UI] triple click: morse_screen=%p curr=%p home=%p\n", + morse_screen, curr, home); if (morse_screen != nullptr && curr == home) { + Serial.println("[UI] triple click: activating MorseScreen"); morse_screen->activate(); setCurrScreen(morse_screen); } else { + Serial.println("[UI] triple click: conditions not met, falling back to buzzer toggle"); toggleBuzzer(); } #else + Serial.println("[UI] triple click: MORSE_COMPOSE_ENABLED not defined"); toggleBuzzer(); #endif #endif diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 5648707e..b8136d26 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -246,6 +246,7 @@ void SerialBLEInterface::enable() { clearBuffers(); _last_health_check = millis(); + Bluefruit.Advertising.restartOnDisconnect(true); Bluefruit.Advertising.start(0); } @@ -259,8 +260,9 @@ void SerialBLEInterface::disable() { _isEnabled = false; BLE_DEBUG_PRINTLN("SerialBLEInterface: disable"); - disconnect(); + Bluefruit.Advertising.restartOnDisconnect(false); Bluefruit.Advertising.stop(); + disconnect(); _last_health_check = 0; } @@ -394,4 +396,4 @@ bool SerialBLEInterface::isConnected() const { bool SerialBLEInterface::isWriteBusy() const { return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); -} +} \ No newline at end of file diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index e2fc6cb9..4543ee23 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -77,4 +77,4 @@ public: #else #define BLE_DEBUG_PRINT(...) {} #define BLE_DEBUG_PRINTLN(...) {} -#endif +#endif \ No newline at end of file diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 07ccedf1..196b67cf 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -25,6 +25,14 @@ bool GxEPDDisplay::begin() { // Tell GxEPD2 to use our SPI instance // Using slower speed (4MHz) for reliable e-ink communication display.epd2.selectSPI(displaySpi, SPISettings(4000000, MSBFIRST, SPI_MODE0)); +#elif defined(NRF52_PLATFORM) + // nRF52 (Meshpocket et al): LoRa sits on the default SPI bus (PIN_SPI_MISO/ + // MOSI/SCK), e-ink sits on SPI1 (MISO/MOSI/SCK globals set by the variant's + // variant.cpp). GxEPD2's default _pSPIx=&SPI would send display traffic to + // the LoRa bus — hand the e-ink its own bus explicitly. This matches + // upstream MeshCore's approach (which uses SPI1 universally). + display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); + SPI1.begin(); #endif // Initialize with: diff --git a/variants/mesh_pocket/platformio.ini b/variants/mesh_pocket/platformio.ini index bb8fede7..11f32856 100644 --- a/variants/mesh_pocket/platformio.ini +++ b/variants/mesh_pocket/platformio.ini @@ -1,10 +1,3 @@ -; ============================================================================ -; Meck — Heltec MeshPocket variant configuration -; ============================================================================ -; nRF52840 + SX1262 + 2.13" E-Ink (GxEPD2_213_B74) -; Single USER button (GPIO 42), no buzzer, no user-accessible LED -; ============================================================================ - [Mesh_pocket] extends = nrf52_base board = heltec_mesh_pocket @@ -27,6 +20,8 @@ build_flags = ${nrf52_base.build_flags} -D EINK_X_OFFSET=0 -D EINK_Y_OFFSET=10 -D DISPLAY_CLASS=GxEPDDisplay + -D DISPLAY_ROTATION=3 + -D MORSE_COMPOSE_ENABLED -D DISABLE_DIAGNOSTIC_OUTPUT build_src_filter = ${nrf52_base.build_src_filter} + @@ -43,20 +38,20 @@ lib_deps = debug_tool = jlink upload_protocol = nrfutil -[env:Mesh_pocket_companion_radio_ble] + +[env:meck_mesh_pocket_companion_radio_ble] extends = Mesh_pocket board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = ${Mesh_pocket.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 - -D MAX_GROUP_CHANNELS=8 - -D BLE_PIN_CODE=234567 - -D OFFLINE_QUEUE_SIZE=64 + -D MAX_CONTACTS=380 + -D MAX_GROUP_CHANNELS=12 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 -D AUTO_OFF_MILLIS=0 - -D MORSE_COMPOSE_ENABLED=1 -; -D BLE_DEBUG_LOGGING=1 + -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1