mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-04-25 10:22:08 +00:00
v1.1.0: UI polish, LXMF messaging fixes, keyboard cleanup
- Remove boot display test (color bars + 1.5s delay) - Show identity hashes on NodesScreen (colon-formatted, matching own identity display) - Show node names in MessagesScreen and MessageView instead of raw hex - Add message status indicators in chat (*/!/~ for sent/failed/queued) - Fix LXMF send: retry Identity::recall() up to 5 times before failing - Add LXMF logging throughout send/receive/queue drain - Remove Alt+IJML arrow key mapping (obsolete with trackball) - Filter own node from discovered nodes list - Update troubleshooting docs with SetModulationParams STDBY fix
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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<DiscoveredNode>& 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<DiscoveredNode> _nodes;
|
||||
SDStore* _sd = nullptr;
|
||||
FlashStore* _flash = nullptr;
|
||||
RNS::Bytes _localDestHash;
|
||||
static constexpr int MAX_NODES = 200; // PSRAM allows more
|
||||
};
|
||||
|
||||
@@ -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]++;
|
||||
|
||||
@@ -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<uint8_t> packContent(double timestamp, const std::string& content, const std::string& title);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "ui/Theme.h"
|
||||
#include "hal/Display.h"
|
||||
#include "reticulum/LXMFManager.h"
|
||||
#include "reticulum/AnnounceManager.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <string>
|
||||
|
||||
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;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "ui/Theme.h"
|
||||
#include "hal/Display.h"
|
||||
#include "reticulum/LXMFManager.h"
|
||||
#include "reticulum/AnnounceManager.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
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) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <string>
|
||||
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user