diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index bfba4d4..97fd520 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -138,6 +138,18 @@ On first boot with a new SD card, the `/ratputer/` directory tree doesn't exist. - **Ratputer TX confirmed**: All SX1262 registers verified correct (SF7, BW 500kHz, CR 4/5, sync 0x1424, CRC on) - **Heltec V3 RNode receive path**: Has never decoded a single LoRa packet from any source. Shows Ratputer RF as interference (-50 to -81 dBm) but can't decode. This is a Heltec issue, not Ratputer. +### SetModulationParams must be called from STDBY mode + +The SX1262 silently rejects `SetModulationParams` (0x8B) when issued from RX or TX mode. Only STDBY_RC and STDBY_XOSC are valid. The command appears to succeed (no error, BUSY goes low), but the hardware ignores the new SF/BW/CR values. + +**Symptom**: Radio logs show correct SF/BW/CR (read from software variables), but actual TX airtime is wrong. For example, software says SF9 (expected ~900ms for 168 bytes) but actual TX completes in ~280ms (SF7). Both devices see each other's RF (RSSI visible) but every packet fails CRC. SNR is consistently -17 to -20 dB despite short range. + +**Diagnosis**: Add timing instrumentation to `endPacket()` — measure `millis()` from `OP_TX_6X` to `TX_DONE` IRQ. Compare against expected airtime for the configured SF. If actual << expected, the SF didn't apply. + +**Root cause**: `setSpreadingFactor()` / `setSignalBandwidth()` / `setCodingRate4()` were called from `main.cpp` after `begin()` had already entered RX mode via `receive()`. The SX1262 silently dropped the `SetModulationParams` command. + +**Fix**: `setModulationParams()` now calls `standby()` before issuing the SPI command, ensuring the radio is in a valid mode to accept configuration changes. + ### SX1262 calibration must run after TCXO is enabled Per SX1262 datasheet Section 13.1.12, if a TCXO is used, it **must** be enabled before calling `calibrate()` or `calibrate_image()`. Calibration locks to whichever oscillator is active. If TCXO isn't enabled yet, calibration uses the internal RC oscillator (~13MHz, ±3% tolerance). Each chip's RC has a different offset, so two devices end up synthesizing slightly different actual frequencies. The combined error can exceed the LoRa demodulation window. diff --git a/src/hal/Display.cpp b/src/hal/Display.cpp index c5c2890..023f248 100644 --- a/src/hal/Display.cpp +++ b/src/hal/Display.cpp @@ -9,17 +9,6 @@ bool Display::begin() { Serial.printf("[DISPLAY] Initialized: %dx%d (rotation=1, LovyanGFX direct)\n", _gfx.width(), _gfx.height()); - // Quick visual test: draw colored rectangles directly - _gfx.fillRect(0, 0, 107, 120, TFT_RED); - _gfx.fillRect(107, 0, 107, 120, TFT_GREEN); - _gfx.fillRect(214, 0, 106, 120, TFT_BLUE); - _gfx.setTextColor(TFT_WHITE, TFT_BLACK); - _gfx.setTextSize(2); - _gfx.setCursor(60, 140); - _gfx.print("RATDECK DISPLAY TEST"); - delay(1500); - _gfx.fillScreen(TFT_BLACK); - return true; } diff --git a/src/hal/Keyboard.cpp b/src/hal/Keyboard.cpp index d2b7457..1708a03 100644 --- a/src/hal/Keyboard.cpp +++ b/src/hal/Keyboard.cpp @@ -20,7 +20,6 @@ bool Keyboard::begin() { } Serial.println("[KEYBOARD] ESP32-C3 keyboard ready"); - Serial.println("[KEYBOARD] Alt+I/M/J/L = Up/Down/Left/Right, Backspace = Back"); return true; } @@ -66,12 +65,6 @@ void Keyboard::update() { if (altFromMod) { _event.alt = true; - // Map Alt+IJKL/M to arrow keys - char lower = tolower(key); - if (lower == 'i') { _event.up = true; _hasEvent = true; return; } - if (lower == 'm') { _event.down = true; _hasEvent = true; return; } - if (lower == 'j') { _event.left = true; _hasEvent = true; return; } - if (lower == 'l') { _event.right = true; _hasEvent = true; return; } } // Standard key decoding diff --git a/src/hal/Keyboard.h b/src/hal/Keyboard.h index b4eba1e..2245c6f 100644 --- a/src/hal/Keyboard.h +++ b/src/hal/Keyboard.h @@ -22,7 +22,7 @@ struct KeyEvent { bool del; bool tab; bool space; - // Directional arrows (from Alt+IJKL/M or trackball) + // Directional arrows (from trackball) bool up; bool down; bool left; diff --git a/src/main.cpp b/src/main.cpp index 75189f3..a9098a8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -407,6 +407,7 @@ void setup() { bootRender(); announceManager = new AnnounceManager(); announceManager->setStorage(&sdStore, &flash); + announceManager->setLocalDestHash(rns.destination().hash()); announceManager->loadContacts(); announceHandler = RNS::HAnnounceHandler(announceManager); RNS::Transport::register_announce_handler(announceHandler); @@ -545,12 +546,14 @@ void setup() { }); messagesScreen.setLXMFManager(&lxmf); + messagesScreen.setAnnounceManager(announceManager); messagesScreen.setOpenCallback([](const std::string& peerHex) { messageView.setPeerHex(peerHex); ui.setScreen(&messageView); }); messageView.setLXMFManager(&lxmf); + messageView.setAnnounceManager(announceManager); messageView.setBackCallback([]() { ui.setScreen(&messagesScreen); }); @@ -632,14 +635,14 @@ void loop() { // Screen gets the key next bool consumed = ui.handleKey(evt); - // Tab cycling: ,=left /=right or Alt+J/L arrows (only if screen didn't consume) + // Tab cycling: ,=left /=right (only if screen didn't consume) if (!consumed && !evt.ctrl) { - if (evt.left || evt.character == ',') { + if (evt.character == ',') { ui.tabBar().cycleTab(-1); int tab = ui.tabBar().getActiveTab(); if (tabScreens[tab]) ui.setScreen(tabScreens[tab]); } - if (evt.right || evt.character == '/') { + if (evt.character == '/') { ui.tabBar().cycleTab(1); int tab = ui.tabBar().getActiveTab(); if (tabScreens[tab]) ui.setScreen(tabScreens[tab]); diff --git a/src/reticulum/AnnounceManager.cpp b/src/reticulum/AnnounceManager.cpp index b33027c..47837e0 100644 --- a/src/reticulum/AnnounceManager.cpp +++ b/src/reticulum/AnnounceManager.cpp @@ -55,11 +55,17 @@ void AnnounceManager::received_announce( if (rawName.empty()) rawName = app_data.toString(); name = sanitizeName(rawName); } + // Filter out own announces + if (_localDestHash.size() > 0 && destination_hash == _localDestHash) return; + Serial.printf("[ANNOUNCE] From: %s name=\"%s\"\n", destination_hash.toHex().c_str(), name.c_str()); + std::string idHex = announced_identity.hexhash(); + for (auto& node : _nodes) { if (node.hash == destination_hash) { if (!name.empty()) node.name = name; + if (!idHex.empty()) node.identityHex = idHex; node.lastSeen = millis(); node.hops = RNS::Transport::hops_to(destination_hash); if (node.saved) saveContact(node); @@ -86,6 +92,7 @@ void AnnounceManager::received_announce( DiscoveredNode node; node.hash = destination_hash; node.name = name.empty() ? destination_hash.toHex().substr(0, 12) : name; + node.identityHex = idHex; node.lastSeen = millis(); node.hops = RNS::Transport::hops_to(destination_hash); _nodes.push_back(node); @@ -96,6 +103,11 @@ const DiscoveredNode* AnnounceManager::findNode(const RNS::Bytes& hash) const { return nullptr; } +const DiscoveredNode* AnnounceManager::findNodeByHex(const std::string& hexHash) const { + for (const auto& n : _nodes) { if (n.hash.toHex() == hexHash) return &n; } + return nullptr; +} + void AnnounceManager::addManualContact(const std::string& hexHash, const std::string& name) { RNS::Bytes hash; hash.assignHex(hexHash.c_str()); diff --git a/src/reticulum/AnnounceManager.h b/src/reticulum/AnnounceManager.h index bb6617b..7cf4cb4 100644 --- a/src/reticulum/AnnounceManager.h +++ b/src/reticulum/AnnounceManager.h @@ -12,6 +12,7 @@ class FlashStore; struct DiscoveredNode { RNS::Bytes hash; std::string name; + std::string identityHex; int rssi = 0; float snr = 0; uint8_t hops = 0; @@ -30,12 +31,14 @@ public: const RNS::Bytes& app_data) override; void setStorage(SDStore* sd, FlashStore* flash); + void setLocalDestHash(const RNS::Bytes& hash) { _localDestHash = hash; } void saveContacts(); void loadContacts(); const std::vector& nodes() const { return _nodes; } int nodeCount() const { return _nodes.size(); } const DiscoveredNode* findNode(const RNS::Bytes& hash) const; + const DiscoveredNode* findNodeByHex(const std::string& hexHash) const; void addManualContact(const std::string& hexHash, const std::string& name); void evictStale(unsigned long maxAgeMs = 3600000); @@ -46,5 +49,6 @@ private: std::vector _nodes; SDStore* _sd = nullptr; FlashStore* _flash = nullptr; + RNS::Bytes _localDestHash; static constexpr int MAX_NODES = 200; // PSRAM allows more }; diff --git a/src/reticulum/LXMFManager.cpp b/src/reticulum/LXMFManager.cpp index c822e59..e2d177d 100644 --- a/src/reticulum/LXMFManager.cpp +++ b/src/reticulum/LXMFManager.cpp @@ -15,12 +15,13 @@ bool LXMFManager::begin(ReticulumManager* rns, MessageStore* store) { } void LXMFManager::loop() { - while (!_outQueue.empty()) { - LXMFMessage& msg = _outQueue.front(); - if (sendDirect(msg)) { - if (_store) { _store->saveMessage(msg); } - _outQueue.pop_front(); - } else { break; } + if (_outQueue.empty()) return; + LXMFMessage& msg = _outQueue.front(); + if (sendDirect(msg)) { + Serial.printf("[LXMF] Queue drain: status=%s dest=%s\n", + msg.statusStr(), msg.destHash.toHex().substr(0, 8).c_str()); + if (_store) { _store->saveMessage(msg); } + _outQueue.pop_front(); } } @@ -42,8 +43,16 @@ bool LXMFManager::sendMessage(const RNS::Bytes& destHash, const std::string& con bool LXMFManager::sendDirect(LXMFMessage& msg) { RNS::Identity recipientId = RNS::Identity::recall(msg.destHash); if (!recipientId) { - msg.status = LXMFStatus::FAILED; - return true; + msg.retries++; + if (msg.retries >= 5) { + Serial.printf("[LXMF] recall failed for %s after %d retries — marking FAILED\n", + msg.destHash.toHex().substr(0, 8).c_str(), msg.retries); + msg.status = LXMFStatus::FAILED; + return true; + } + Serial.printf("[LXMF] recall failed for %s (retry %d/5) — keeping queued\n", + msg.destHash.toHex().substr(0, 8).c_str(), msg.retries); + return false; // keep in queue, retry next loop } RNS::Destination outDest(recipientId, RNS::Type::Destination::OUT, RNS::Type::Destination::SINGLE, "lxmf", "delivery"); @@ -79,9 +88,14 @@ void LXMFManager::onLinkEstablished(RNS::Link& link) { void LXMFManager::processIncoming(const uint8_t* data, size_t len, const RNS::Bytes& destHash) { LXMFMessage msg; - if (!LXMFMessage::unpackFull(data, len, msg)) return; + if (!LXMFMessage::unpackFull(data, len, msg)) { + Serial.printf("[LXMF] Failed to unpack incoming message (%d bytes)\n", (int)len); + return; + } if (_rns && msg.sourceHash == _rns->destination().hash()) return; msg.destHash = destHash; + Serial.printf("[LXMF] Message from %s (%d bytes) content_len=%d\n", + msg.sourceHash.toHex().substr(0, 8).c_str(), (int)len, (int)msg.content.size()); if (_store) { _store->saveMessage(msg); } std::string peerHex = msg.sourceHash.toHex(); _unread[peerHex]++; diff --git a/src/reticulum/LXMFMessage.h b/src/reticulum/LXMFMessage.h index 4b26565..4837b17 100644 --- a/src/reticulum/LXMFMessage.h +++ b/src/reticulum/LXMFMessage.h @@ -21,6 +21,7 @@ struct LXMFMessage { LXMFStatus status = LXMFStatus::DRAFT; bool incoming = false; bool read = false; + int retries = 0; RNS::Bytes messageId; static std::vector packContent(double timestamp, const std::string& content, const std::string& title); diff --git a/src/ui/screens/MessageView.cpp b/src/ui/screens/MessageView.cpp index ae8664e..e1e8e12 100644 --- a/src/ui/screens/MessageView.cpp +++ b/src/ui/screens/MessageView.cpp @@ -2,6 +2,7 @@ #include "ui/Theme.h" #include "hal/Display.h" #include "reticulum/LXMFManager.h" +#include "reticulum/AnnounceManager.h" #include void MessageView::onEnter() { @@ -27,11 +28,17 @@ void MessageView::update() { void MessageView::draw(LGFX_TDeck& gfx) { gfx.setTextSize(1); - // Header + // Header — show node name if known gfx.setTextColor(Theme::ACCENT, Theme::BG); gfx.setCursor(4, Theme::CONTENT_Y + 2); - char header[32]; - snprintf(header, sizeof(header), "< %s", _peerHex.substr(0, 12).c_str()); + std::string headerName; + if (_am) { + const DiscoveredNode* node = _am->findNodeByHex(_peerHex); + if (node && !node->name.empty()) headerName = node->name; + } + if (headerName.empty()) headerName = _peerHex.substr(0, 12); + char header[48]; + snprintf(header, sizeof(header), "< %s", headerName.c_str()); gfx.print(header); // Divider under header @@ -91,9 +98,15 @@ void MessageView::draw(LGFX_TDeck& gfx) { gfx.setTextColor(Theme::ACCENT, Theme::BG); gfx.setCursor(4, y); } else { + // Status indicator: * = sent, ! = failed, ~ = queued/sending + const char* indicator = "~"; + if (msg.status == LXMFStatus::SENT || msg.status == LXMFStatus::DELIVERED) indicator = "*"; + else if (msg.status == LXMFStatus::FAILED) indicator = "!"; + gfx.setTextColor(Theme::PRIMARY, Theme::BG); - // Right-align outgoing - int tw = std::min((int)msg.content.length(), 40) * 6; + // Right-align outgoing + status + int contentLen = std::min((int)msg.content.length(), 38); + int tw = (contentLen + 2) * 6; // +2 for space + indicator gfx.setCursor(Theme::SCREEN_W - tw - 4, y); } @@ -104,6 +117,20 @@ void MessageView::draw(LGFX_TDeck& gfx) { } else { gfx.print(msg.content.c_str()); } + + // Show status indicator for outgoing messages + if (!msg.incoming) { + const char* ind = "~"; + uint32_t indColor = Theme::MUTED; + if (msg.status == LXMFStatus::SENT || msg.status == LXMFStatus::DELIVERED) { + ind = "*"; indColor = Theme::ACCENT; + } else if (msg.status == LXMFStatus::FAILED) { + ind = "!"; indColor = TFT_RED; + } + gfx.setTextColor(indColor, Theme::BG); + gfx.print(" "); + gfx.print(ind); + } y += lineH; } } diff --git a/src/ui/screens/MessageView.h b/src/ui/screens/MessageView.h index 6a4b986..cfea0ef 100644 --- a/src/ui/screens/MessageView.h +++ b/src/ui/screens/MessageView.h @@ -5,6 +5,7 @@ #include class LXMFManager; +class AnnounceManager; class MessageView : public Screen { public: @@ -17,6 +18,7 @@ public: void setPeerHex(const std::string& hex) { _peerHex = hex; } void setLXMFManager(LXMFManager* lxmf) { _lxmf = lxmf; } + void setAnnounceManager(AnnounceManager* am) { _am = am; } void setBackCallback(BackCallback cb) { _onBack = cb; } const char* title() const override { return "Chat"; } @@ -26,6 +28,7 @@ private: void sendCurrentMessage(); LXMFManager* _lxmf = nullptr; + AnnounceManager* _am = nullptr; BackCallback _onBack; std::string _peerHex; std::string _inputText; diff --git a/src/ui/screens/MessagesScreen.cpp b/src/ui/screens/MessagesScreen.cpp index 577c091..4fc3f82 100644 --- a/src/ui/screens/MessagesScreen.cpp +++ b/src/ui/screens/MessagesScreen.cpp @@ -2,6 +2,7 @@ #include "ui/Theme.h" #include "hal/Display.h" #include "reticulum/LXMFManager.h" +#include "reticulum/AnnounceManager.h" #include void MessagesScreen::onEnter() { @@ -46,10 +47,17 @@ void MessagesScreen::draw(LGFX_TDeck& gfx) { gfx.fillRect(0, y, Theme::SCREEN_W, rowH, Theme::SELECTION_BG); } - // Peer hash + // Peer name (lookup from AnnounceManager) or fallback to hex + std::string displayName; + if (_am) { + const DiscoveredNode* node = _am->findNodeByHex(peerHex); + if (node && !node->name.empty()) displayName = node->name; + } + if (displayName.empty()) displayName = peerHex.substr(0, 16); + gfx.setTextColor(Theme::PRIMARY, (int)i == _selectedIdx ? Theme::SELECTION_BG : Theme::BG); gfx.setCursor(4, y + 6); - gfx.print(peerHex.substr(0, 16).c_str()); + gfx.print(displayName.c_str()); // Unread badge if (unread > 0) { diff --git a/src/ui/screens/MessagesScreen.h b/src/ui/screens/MessagesScreen.h index 81e5834..358e919 100644 --- a/src/ui/screens/MessagesScreen.h +++ b/src/ui/screens/MessagesScreen.h @@ -5,6 +5,7 @@ #include class LXMFManager; +class AnnounceManager; class MessagesScreen : public Screen { public: @@ -15,6 +16,7 @@ public: bool handleKey(const KeyEvent& event) override; void setLXMFManager(LXMFManager* lxmf) { _lxmf = lxmf; } + void setAnnounceManager(AnnounceManager* am) { _am = am; } void setOpenCallback(OpenCallback cb) { _onOpen = cb; } const char* title() const override { return "Messages"; } @@ -22,6 +24,7 @@ public: private: LXMFManager* _lxmf = nullptr; + AnnounceManager* _am = nullptr; OpenCallback _onOpen; int _lastConvCount = -1; int _selectedIdx = 0; diff --git a/src/ui/screens/NodesScreen.cpp b/src/ui/screens/NodesScreen.cpp index eae4b77..1fbb7bd 100644 --- a/src/ui/screens/NodesScreen.cpp +++ b/src/ui/screens/NodesScreen.cpp @@ -46,10 +46,17 @@ void NodesScreen::draw(LGFX_TDeck& gfx) { uint32_t bgCol = (int)i == _selectedIdx ? Theme::SELECTION_BG : Theme::BG; - // Name + hash - std::string hashHex = node.hash.toHex(); + // Name + identity hash (formatted with colons like own identity) + std::string displayHash; + if (!node.identityHex.empty() && node.identityHex.size() >= 12) { + displayHash = node.identityHex.substr(0, 4) + ":" + + node.identityHex.substr(4, 4) + ":" + + node.identityHex.substr(8, 4); + } else { + displayHash = node.hash.toHex().substr(0, 8); + } char buf[64]; - snprintf(buf, sizeof(buf), "%s [%s]", node.name.c_str(), hashHex.substr(0, 8).c_str()); + snprintf(buf, sizeof(buf), "%s [%s]", node.name.c_str(), displayHash.c_str()); gfx.setTextColor(node.saved ? Theme::ACCENT : Theme::PRIMARY, bgCol); gfx.setCursor(4, y + 5); gfx.print(buf);