mirror of
https://github.com/pelgraine/Meck.git
synced 2026-06-11 00:34:50 +02:00
Add on-device Rx Log packet sniffer and RX packet counter
RX packet counter: shows total received packets (flood + direct) on the radio details page; RAM-only, resets on boot and on any SF/freq/BW change. Rx Log: app-style packet sniffer opened from Settings > "Rx Log >>". Captures the last 100 received packets pre-filter via Dispatcher::logRx into a PSRAM ring, with decoded "sender: message" lines matched by packet hash for decryptable channels. Each entry shows route/type, time, size, hash, path, channel or From/To, SNR. W/S scroll, Q back to Settings; T5S3 swipe-scroll rides the same path. Files: MyMesh.h/.cpp, RxLogScreen.h (new), UITask.h/.cpp, SettingsScreen.h, main.cpp.
This commit is contained in:
@@ -325,6 +325,33 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::logRx(mesh::Packet* pkt, int len, float score) {
|
||||
if (_rxlog == nullptr) return;
|
||||
RxLogEntry& e = _rxlog[_rxlog_head];
|
||||
memset(&e, 0, sizeof(e));
|
||||
e.timestamp = getRTCClock()->getCurrentTime();
|
||||
e.header = pkt->header;
|
||||
e.path_len = pkt->path_len;
|
||||
e.size = (uint16_t)len;
|
||||
e.snr = pkt->_snr;
|
||||
e.payload0 = (pkt->payload_len > 0) ? pkt->payload[0] : 0;
|
||||
e.payload1 = (pkt->payload_len > 1) ? pkt->payload[1] : 0;
|
||||
pkt->calculatePacketHash(e.hash);
|
||||
uint16_t pbl = pkt->getPathByteLen();
|
||||
if (pbl > MAX_PATH_SIZE) pbl = MAX_PATH_SIZE;
|
||||
memcpy(e.path, pkt->path, pbl);
|
||||
// channel_name / text stay empty here; attached by onChannelMessageRecv() for
|
||||
// decryptable channel messages, matched on packet hash.
|
||||
_rxlog_head = (_rxlog_head + 1) % RXLOG_SIZE;
|
||||
if (_rxlog_count < RXLOG_SIZE) _rxlog_count++;
|
||||
}
|
||||
|
||||
const RxLogEntry* MyMesh::getRxLogEntry(int idx) const {
|
||||
if (_rxlog == nullptr || idx < 0 || idx >= _rxlog_count) return nullptr;
|
||||
int oldest = (_rxlog_head - _rxlog_count + RXLOG_SIZE) % RXLOG_SIZE;
|
||||
return &_rxlog[(oldest + idx) % RXLOG_SIZE];
|
||||
}
|
||||
|
||||
bool MyMesh::isAutoAddEnabled() const {
|
||||
return (_prefs.manual_add_contacts & 1) == 0;
|
||||
}
|
||||
@@ -844,6 +871,24 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
|
||||
_serial->writeFrame(frame, 1);
|
||||
}
|
||||
|
||||
// Rx Log: attach the decoded text + channel name to the matching captured
|
||||
// entry, found by packet hash. logRx() ran first and stored the header fields.
|
||||
if (_rxlog != nullptr) {
|
||||
uint8_t h[MAX_HASH_SIZE];
|
||||
pkt->calculatePacketHash(h);
|
||||
ChannelDetails cd;
|
||||
const char* cname = getChannel(channel_idx, cd) ? cd.name : "";
|
||||
int oldest = (_rxlog_head - _rxlog_count + RXLOG_SIZE) % RXLOG_SIZE;
|
||||
for (int n = _rxlog_count - 1; n >= 0; n--) {
|
||||
RxLogEntry& e = _rxlog[(oldest + n) % RXLOG_SIZE];
|
||||
if (e.text[0] == 0 && memcmp(e.hash, h, MAX_HASH_SIZE) == 0) {
|
||||
StrHelper::strncpy(e.text, text, sizeof(e.text));
|
||||
StrHelper::strncpy(e.channel_name, cname, sizeof(e.channel_name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
// Get the channel name from the channel index
|
||||
const char *channel_name = "Unknown";
|
||||
@@ -1451,6 +1496,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
sign_data = NULL;
|
||||
dirty_contacts_expiry = 0;
|
||||
advert_paths = nullptr; // PSRAM-allocated in begin()
|
||||
_rxlog = nullptr; // PSRAM-allocated in begin()
|
||||
_rxlog_head = 0;
|
||||
_rxlog_count = 0;
|
||||
memset(send_scope.key, 0, sizeof(send_scope.key));
|
||||
memset(_sent_track, 0, sizeof(_sent_track));
|
||||
_sent_track_idx = 0;
|
||||
@@ -1478,6 +1526,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
|
||||
void MyMesh::begin(bool has_display) {
|
||||
advert_paths = (AdvertPath*)ps_calloc(ADVERT_PATH_TABLE_SIZE, sizeof(AdvertPath));
|
||||
_rxlog = (RxLogEntry*)ps_calloc(RXLOG_SIZE, sizeof(RxLogEntry));
|
||||
BaseChatMesh::begin();
|
||||
|
||||
if (!_store->loadMainIdentity(self_id)) {
|
||||
|
||||
@@ -104,6 +104,29 @@ struct DiscoveredNode {
|
||||
#define MECK_CH_PREFIX "[MECK:CH]"
|
||||
#define MECK_CH_PREFIX_LEN 9
|
||||
|
||||
// Rx Log -- on-device packet sniffer ring buffer that mirrors the MeshCore app's
|
||||
// Rx Log. logRx() captures every received packet (including foreign relays, since
|
||||
// it fires pre-filter) into header fields; for decryptable channel messages the
|
||||
// decoded "name: msg" text and channel name are attached later by
|
||||
// onChannelMessageRecv(), matched on packet hash. RAM only, lost on reboot.
|
||||
#define RXLOG_SIZE 100
|
||||
#define RXLOG_TEXT_LEN 64
|
||||
#define RXLOG_CHNAME_LEN 16
|
||||
|
||||
struct RxLogEntry {
|
||||
uint32_t timestamp; // local RTC at receive
|
||||
uint8_t header; // route type + payload type + ver (Packet::header)
|
||||
uint8_t path_len; // hop count (low 6 bits) + bytes-per-hop mode (high 2)
|
||||
uint16_t size; // wire length (getRawLength)
|
||||
int8_t snr; // SNR x4 (snr / 4.0 = dB)
|
||||
uint8_t payload0; // first payload byte: channel hash, or dest hash (addressed)
|
||||
uint8_t payload1; // second payload byte: src hash (addressed types only)
|
||||
uint8_t hash[MAX_HASH_SIZE]; // packet hash
|
||||
uint8_t path[MAX_PATH_SIZE]; // hop hashes (layout per path_len)
|
||||
char channel_name[RXLOG_CHNAME_LEN]; // decrypted channel name (no '#'); empty if undecoded
|
||||
char text[RXLOG_TEXT_LEN]; // decoded "name: msg"; empty if undecoded
|
||||
};
|
||||
|
||||
class MyMesh : public BaseChatMesh, public DataStoreHost {
|
||||
public:
|
||||
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
|
||||
@@ -115,6 +138,18 @@ public:
|
||||
NodePrefs *getNodePrefs();
|
||||
uint32_t getBLEPin();
|
||||
|
||||
// RX packet counter for the radio details page. Returns the number of packets
|
||||
// (flood + direct) received since boot, less a baseline. resetRxPacketCount()
|
||||
// snaps the baseline to the current total so the displayed count can be zeroed
|
||||
// when radio params change. RAM only; reset on boot via Dispatcher::begin().
|
||||
uint32_t getRxPacketCount() const { return (getNumRecvFlood() + getNumRecvDirect()) - _rx_count_baseline; }
|
||||
void resetRxPacketCount() { _rx_count_baseline = getNumRecvFlood() + getNumRecvDirect(); }
|
||||
|
||||
// Rx Log accessors for the Rx Log screen. Entries are ordered oldest..newest.
|
||||
int getRxLogCount() const { return _rxlog_count; }
|
||||
const RxLogEntry* getRxLogEntry(int idx) const; // idx 0 = oldest; nullptr if out of range
|
||||
void clearRxLog() { _rxlog_head = 0; _rxlog_count = 0; }
|
||||
|
||||
void loop();
|
||||
void handleCmdFrame(size_t len);
|
||||
bool advert();
|
||||
@@ -210,6 +245,7 @@ protected:
|
||||
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis=0);
|
||||
|
||||
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
|
||||
void logRx(mesh::Packet* pkt, int len, float score) override;
|
||||
bool isAutoAddEnabled() const override;
|
||||
bool shouldAutoAddContactType(uint8_t type) const override;
|
||||
bool shouldOverwriteWhenFull() const override;
|
||||
@@ -291,6 +327,7 @@ private:
|
||||
mutable bool _forceNextImport = false;
|
||||
bool _deferSaves = false;
|
||||
unsigned long _lastUserInput = 0; // millis() of last keypress -- defer saves until idle
|
||||
uint32_t _rx_count_baseline = 0; // baseline for RX packet counter (radio page); RAM only
|
||||
uint32_t pending_login;
|
||||
uint32_t pending_status;
|
||||
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
|
||||
@@ -351,6 +388,11 @@ private:
|
||||
#endif
|
||||
AdvertPath* advert_paths; // PSRAM-allocated in begin(), size = ADVERT_PATH_TABLE_SIZE
|
||||
|
||||
// Rx Log ring buffer (PSRAM-allocated in begin(), size RXLOG_SIZE). RAM only.
|
||||
RxLogEntry* _rxlog;
|
||||
int _rxlog_head; // index where the next entry will be written
|
||||
int _rxlog_count; // number of valid entries (<= RXLOG_SIZE)
|
||||
|
||||
// Sent message repeat tracking
|
||||
#define SENT_TRACK_SIZE 4
|
||||
#define SENT_FINGERPRINT_SIZE 12
|
||||
|
||||
@@ -4797,6 +4797,11 @@ void handleKeyboardInput() {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Check for Rx Log open request from the settings screen
|
||||
if (settings->isRxLogRequested()) {
|
||||
settings->clearRxLogRequest();
|
||||
ui_task.gotoRxLogScreen();
|
||||
}
|
||||
// Check for channel share request from the settings screen
|
||||
if (settings->isShareRequested()) {
|
||||
int contactIdx = settings->getShareContactIdx();
|
||||
@@ -5337,6 +5342,7 @@ void handleKeyboardInput() {
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
|| ui_task.isOnRxLogScreen()
|
||||
) {
|
||||
ui_task.injectKey('S');
|
||||
}
|
||||
@@ -5357,6 +5363,7 @@ void handleKeyboardInput() {
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
|| ui_task.isHomeOnShutdownPage()
|
||||
|| ui_task.isOnRxLogScreen()
|
||||
) {
|
||||
ui_task.injectKey('s'); // Pass directly for scrolling
|
||||
} else {
|
||||
@@ -5379,6 +5386,7 @@ void handleKeyboardInput() {
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
|| ui_task.isOnRxLogScreen()
|
||||
) {
|
||||
ui_task.injectKey('W');
|
||||
}
|
||||
@@ -5398,6 +5406,7 @@ void handleKeyboardInput() {
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
|| ui_task.isOnRxLogScreen()
|
||||
) {
|
||||
ui_task.injectKey('w'); // Pass directly for scrolling
|
||||
} else {
|
||||
@@ -5758,6 +5767,11 @@ void handleKeyboardInput() {
|
||||
ui_task.gotoContactsScreen();
|
||||
break;
|
||||
}
|
||||
// Rx Log screen: Q goes back to settings (screen handles it)
|
||||
if (ui_task.isOnRxLogScreen()) {
|
||||
ui_task.injectKey('q');
|
||||
break;
|
||||
}
|
||||
// Path editor: Q goes back to contacts (discards unsaved changes)
|
||||
if (ui_task.isOnPathEditor()) {
|
||||
Serial.println("Nav: PathEditor -> Contacts");
|
||||
|
||||
@@ -84,6 +84,8 @@ static const AdminCmdDef CMD_SET_CONFIG[] = {
|
||||
{ "Set AF", "set af ", "Airtime factor:", CMDF_PARAM },
|
||||
{ "Set Repeat", "set repeat ", "on/off:", CMDF_PARAM },
|
||||
{ "Set Flood Max", "set flood.max ", "Max hops (0-64):", CMDF_PARAM },
|
||||
{ "Set Flood Max Unscoped", "set flood.max.unscoped ", "Max hops (64=off):", CMDF_PARAM },
|
||||
{ "Set Flood Adv Max", "set flood.max.advert ", "Max hops (def 8):", CMDF_PARAM },
|
||||
{ "Set RX Delay", "set rxdelay ", "Base (0=off):", CMDF_PARAM },
|
||||
{ "Set TX Delay", "set txdelay ", "Factor:", CMDF_PARAM },
|
||||
{ "Set Direct TX Delay", "set direct.txdelay ", "Factor:", CMDF_PARAM },
|
||||
@@ -99,7 +101,7 @@ static const AdminCmdDef CMD_SET_CONFIG[] = {
|
||||
{ "Temp Radio", "tempradio ", "freq,bw,sf,cr,mins:", CMDF_PARAM },
|
||||
{ "Change Admin Pwd", "password ", "New password:", CMDF_PARAM | CMDF_CONFIRM },
|
||||
};
|
||||
#define CMD_SET_CONFIG_COUNT 19
|
||||
#define CMD_SET_CONFIG_COUNT 21
|
||||
|
||||
// --- Power ---
|
||||
static const AdminCmdDef CMD_POWER[] = {
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <MeshCore.h>
|
||||
|
||||
class UITask; // forward decl -- used only to navigate back to Settings
|
||||
extern MyMesh the_mesh;
|
||||
|
||||
// ==========================================================================
|
||||
// Rx Log Screen -- on-device packet sniffer view, mirrors the MeshCore app's
|
||||
// Rx Log. Reads the capture ring in MyMesh (filled by logRx()) and renders
|
||||
// each received packet as an app-style block: route + payload type, time,
|
||||
// size, hash, path, channel hash/name or From/To, the decoded line (for
|
||||
// decryptable channels), and SNR. Entries are shown newest-first; W/S scroll
|
||||
// by entry, Q returns to Settings (where the screen is opened from).
|
||||
// ==========================================================================
|
||||
|
||||
class RxLogScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
mesh::RTCClock* _rtc;
|
||||
int _scrollPos; // display index of the top visible entry (0 = newest)
|
||||
|
||||
static const char* typeName(uint8_t t) {
|
||||
switch (t) {
|
||||
case PAYLOAD_TYPE_REQ: return "REQUEST";
|
||||
case PAYLOAD_TYPE_RESPONSE: return "RESPONSE";
|
||||
case PAYLOAD_TYPE_TXT_MSG: return "TEXT";
|
||||
case PAYLOAD_TYPE_ACK: return "ACK";
|
||||
case PAYLOAD_TYPE_ADVERT: return "ADVERT";
|
||||
case PAYLOAD_TYPE_GRP_TXT: return "GROUP_TEXT";
|
||||
case PAYLOAD_TYPE_GRP_DATA: return "GRP_DATA";
|
||||
case PAYLOAD_TYPE_ANON_REQ: return "ANON_REQ";
|
||||
case PAYLOAD_TYPE_PATH: return "PATH";
|
||||
case PAYLOAD_TYPE_TRACE: return "TRACE";
|
||||
case PAYLOAD_TYPE_MULTIPART: return "MULTIPART";
|
||||
case PAYLOAD_TYPE_CONTROL: return "CONTROL";
|
||||
case PAYLOAD_TYPE_RAW_CUSTOM: return "RAW";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
// Render one entry block starting at y; returns the y after the block.
|
||||
int renderEntry(DisplayDriver& display, const RxLogEntry& e, int y, int lineH, int maxY) {
|
||||
uint8_t route = e.header & 0x03; // PH_ROUTE_MASK
|
||||
bool flood = (route == 0x00 || route == 0x01); // TRANSPORT_FLOOD or FLOOD
|
||||
uint8_t ptype = (e.header >> 2) & 0x0F; // PH_TYPE_SHIFT / PH_TYPE_MASK
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
// Line 1: route + payload type (left), SNR (right)
|
||||
char l1[40];
|
||||
snprintf(l1, sizeof(l1), "%s %s", flood ? "FLOOD" : "DIRECT", typeName(ptype));
|
||||
display.setCursor(0, y);
|
||||
display.print(l1);
|
||||
char snrbuf[16];
|
||||
snprintf(snrbuf, sizeof(snrbuf), "%.2fdB", e.snr / 4.0f);
|
||||
display.setCursor(display.width() - display.getTextWidth(snrbuf) - 2, y);
|
||||
display.print(snrbuf);
|
||||
y += lineH;
|
||||
if (y + lineH > maxY) return y;
|
||||
|
||||
// Line 2: time (HH:MM:SS, device UTC offset) + size
|
||||
int32_t local = (int32_t)e.timestamp + ((int32_t)the_mesh.getNodePrefs()->utc_offset_hours * 3600);
|
||||
int hrs = (local / 3600) % 24;
|
||||
int mins = (local / 60) % 60;
|
||||
int secs = local % 60;
|
||||
if (hrs < 0) hrs += 24;
|
||||
char l2[40];
|
||||
snprintf(l2, sizeof(l2), "%02d:%02d:%02d %u bytes", hrs, mins, secs, (unsigned)e.size);
|
||||
display.setCursor(0, y);
|
||||
display.print(l2);
|
||||
y += lineH;
|
||||
if (y + lineH > maxY) return y;
|
||||
|
||||
// Line 3: packet hash
|
||||
char hashbuf[2 * MAX_HASH_SIZE + 8];
|
||||
int p = snprintf(hashbuf, sizeof(hashbuf), "Hash: ");
|
||||
for (int i = 0; i < MAX_HASH_SIZE && p < (int)sizeof(hashbuf) - 3; i++) {
|
||||
p += snprintf(hashbuf + p, sizeof(hashbuf) - p, "%02X", e.hash[i]);
|
||||
}
|
||||
display.setCursor(0, y);
|
||||
display.print(hashbuf);
|
||||
y += lineH;
|
||||
if (y + lineH > maxY) return y;
|
||||
|
||||
// Line 4: path (hop count + hop hashes, per the bytes-per-hop mode)
|
||||
uint8_t hops = e.path_len & 63;
|
||||
uint8_t bph = (e.path_len >> 6) + 1;
|
||||
char pathbuf[96];
|
||||
int q = snprintf(pathbuf, sizeof(pathbuf), "Path: %d hops", hops);
|
||||
if (hops > 0) {
|
||||
q += snprintf(pathbuf + q, sizeof(pathbuf) - q, " [");
|
||||
for (int h = 0; h < hops && q < (int)sizeof(pathbuf) - 8; h++) {
|
||||
for (int b = 0; b < bph; b++) {
|
||||
q += snprintf(pathbuf + q, sizeof(pathbuf) - q, "%02x", e.path[h * bph + b]);
|
||||
}
|
||||
if (h < hops - 1) q += snprintf(pathbuf + q, sizeof(pathbuf) - q, ",");
|
||||
}
|
||||
q += snprintf(pathbuf + q, sizeof(pathbuf) - q, "]");
|
||||
}
|
||||
display.drawTextEllipsized(0, y, display.width(), pathbuf);
|
||||
y += lineH;
|
||||
if (y + lineH > maxY) return y;
|
||||
|
||||
// Line 5: channel hash/name (group) OR From/To (addressed)
|
||||
char l5[48];
|
||||
bool haveL5 = false;
|
||||
if (ptype == PAYLOAD_TYPE_GRP_TXT || ptype == PAYLOAD_TYPE_GRP_DATA) {
|
||||
if (e.channel_name[0]) snprintf(l5, sizeof(l5), "Ch %02x #%s", e.payload0, e.channel_name);
|
||||
else snprintf(l5, sizeof(l5), "Ch %02x", e.payload0);
|
||||
haveL5 = true;
|
||||
} else if (ptype == PAYLOAD_TYPE_REQ || ptype == PAYLOAD_TYPE_RESPONSE
|
||||
|| ptype == PAYLOAD_TYPE_TXT_MSG || ptype == PAYLOAD_TYPE_PATH) {
|
||||
snprintf(l5, sizeof(l5), "From %02x To %02x", e.payload1, e.payload0);
|
||||
haveL5 = true;
|
||||
}
|
||||
if (haveL5) {
|
||||
display.drawTextEllipsized(0, y, display.width(), l5);
|
||||
y += lineH;
|
||||
if (y + lineH > maxY) return y;
|
||||
}
|
||||
|
||||
// Line 6: decoded "sender: message" (only present for decryptable channels)
|
||||
if (e.text[0]) {
|
||||
char tb[RXLOG_TEXT_LEN + 4];
|
||||
display.translateUTF8ToBlocks(tb, e.text, sizeof(tb));
|
||||
display.drawTextEllipsized(0, y, display.width(), tb);
|
||||
y += lineH;
|
||||
}
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
public:
|
||||
RxLogScreen(UITask* task, mesh::RTCClock* rtc)
|
||||
: _task(task), _rtc(rtc), _scrollPos(0) {}
|
||||
|
||||
void resetScroll() { _scrollPos = 0; }
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
int count = the_mesh.getRxLogCount();
|
||||
if (_scrollPos < 0) _scrollPos = 0;
|
||||
if (_scrollPos > count - 1) _scrollPos = (count > 0) ? count - 1 : 0;
|
||||
|
||||
// === Header ===
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
char hdr[32];
|
||||
snprintf(hdr, sizeof(hdr), "Rx Log: %d pkts", count);
|
||||
display.print(hdr);
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
int headerHeight = 14;
|
||||
int footerHeight = 14;
|
||||
int maxY = display.height() - footerHeight;
|
||||
int y = headerHeight;
|
||||
|
||||
if (count == 0) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(4, 28);
|
||||
display.print("No packets received yet");
|
||||
display.setCursor(4, 38);
|
||||
display.print("Packets appear as they arrive");
|
||||
} else {
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize());
|
||||
int lineH = the_mesh.getNodePrefs()->smallLineH();
|
||||
|
||||
// Render blocks newest-first, starting at display index _scrollPos,
|
||||
// until the screen is full.
|
||||
for (int d = _scrollPos; d < count && y + lineH <= maxY; d++) {
|
||||
const RxLogEntry* e = the_mesh.getRxLogEntry(count - 1 - d);
|
||||
if (!e) break;
|
||||
y = renderEntry(display, *e, y, lineH, maxY);
|
||||
y += 3; // gap between entry blocks
|
||||
}
|
||||
}
|
||||
|
||||
display.setTextSize(1);
|
||||
|
||||
// === Footer ===
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, footerY);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Swipe:Scroll");
|
||||
#else
|
||||
display.print("Q:Bk W/S:Scroll");
|
||||
#endif
|
||||
|
||||
return 5000; // refresh every 5s to pick up newly received packets
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
int count = the_mesh.getRxLogCount();
|
||||
|
||||
// Scroll up (toward newest)
|
||||
if (c == 'w' || c == 'W' || c == 0xF2) {
|
||||
if (_scrollPos > 0) { _scrollPos--; return true; }
|
||||
return false;
|
||||
}
|
||||
// Scroll down (toward oldest)
|
||||
if (c == 's' || c == 'S' || c == 0xF1) {
|
||||
if (_scrollPos < count - 1) { _scrollPos++; return true; }
|
||||
return false;
|
||||
}
|
||||
// Back to Settings
|
||||
if (c == 'q' || c == 'Q' || c == 0x1B) {
|
||||
if (_task) _task->gotoSettingsScreen();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -185,6 +185,7 @@ enum SettingsRowType : uint8_t {
|
||||
ROW_EXPORT_AUTOADD, // Checkbox: include auto-add preferences (sub-item of contacts)
|
||||
ROW_EXPORT_NOW, // ">> Export Now" action trigger
|
||||
#endif
|
||||
ROW_RXLOG, // Rx Log packet sniffer (opens RxLogScreen)
|
||||
ROW_INFO_HEADER, // "--- Info ---" separator
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
ROW_OTA_TOOLS_SUBMENU, // Folder row → enters OTA Tools sub-screen
|
||||
@@ -322,6 +323,8 @@ private:
|
||||
bool _importRequested; // set by key handler, cleared by main.cpp after calling import
|
||||
#endif
|
||||
|
||||
bool _rxlogRequested = false; // set by key handler, cleared by main.cpp after opening Rx Log
|
||||
|
||||
// Channel share picker state
|
||||
#define SHARE_MAX_CONTACTS 32
|
||||
uint8_t _shareChannelIdx; // channel being shared
|
||||
@@ -529,6 +532,9 @@ private:
|
||||
addRow(ROW_EXPORT_IMPORT_SUBMENU);
|
||||
#endif
|
||||
|
||||
// Rx Log packet sniffer (opens RxLogScreen)
|
||||
addRow(ROW_RXLOG);
|
||||
|
||||
// Info section (stays at top level)
|
||||
addRow(ROW_INFO_HEADER);
|
||||
addRow(ROW_PUB_KEY);
|
||||
@@ -673,6 +679,7 @@ private:
|
||||
radio_set_params(_prefs->freq, _prefs->bw, _prefs->sf, _prefs->cr);
|
||||
radio_set_tx_power(_prefs->tx_power_dbm);
|
||||
the_mesh.savePrefs();
|
||||
the_mesh.resetRxPacketCount(); // zero the radio-page RX counter on radio param change
|
||||
_radioChanged = false;
|
||||
Serial.printf("Settings: Radio params applied - %.3f/%g/%d/%d TX:%d\n",
|
||||
_prefs->freq, _prefs->bw, _prefs->sf, _prefs->cr, _prefs->tx_power_dbm);
|
||||
@@ -931,6 +938,10 @@ public:
|
||||
void clearImportRequest() { _importRequested = false; }
|
||||
#endif
|
||||
|
||||
// Rx Log open request -- checked and cleared by main.cpp
|
||||
bool isRxLogRequested() const { return _rxlogRequested; }
|
||||
void clearRxLogRequest() { _rxlogRequested = false; }
|
||||
|
||||
// Channel share request -- checked and cleared by main.cpp
|
||||
bool isShareRequested() const { return _shareRequested; }
|
||||
int getShareContactIdx() const { return _shareContactIdx; }
|
||||
@@ -2025,6 +2036,11 @@ public:
|
||||
display.print("Channels >>");
|
||||
break;
|
||||
|
||||
case ROW_RXLOG:
|
||||
display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN);
|
||||
display.print("Rx Log >>");
|
||||
break;
|
||||
|
||||
#ifdef HAS_SDCARD
|
||||
case ROW_EXPORT_IMPORT_SUBMENU:
|
||||
display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN);
|
||||
@@ -3749,6 +3765,10 @@ public:
|
||||
Serial.println("Settings: entered Channels sub-screen");
|
||||
break;
|
||||
|
||||
case ROW_RXLOG:
|
||||
_rxlogRequested = true;
|
||||
break;
|
||||
|
||||
case ROW_ADD_CHANNEL:
|
||||
startEditText("");
|
||||
break;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "PathEditorScreen.h"
|
||||
#include "DiscoveryScreen.h"
|
||||
#include "LastHeardScreen.h"
|
||||
#include "RxLogScreen.h"
|
||||
#include "Tracescreen.h"
|
||||
#include "GamesMenuScreen.h"
|
||||
#include "SnakeScreen.h"
|
||||
@@ -808,6 +809,9 @@ public:
|
||||
display.setCursor(0, 53);
|
||||
sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor());
|
||||
display.print(tmp);
|
||||
display.setCursor(0, 64);
|
||||
sprintf(tmp, "RX packets: %u", (unsigned)the_mesh.getRxPacketCount());
|
||||
display.print(tmp);
|
||||
#ifdef BLE_PIN_CODE
|
||||
} else if (_page == HomePage::BLUETOOTH) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
@@ -1458,6 +1462,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
path_editor = nullptr; // Lazy-initialized on first use from contacts screen
|
||||
discovery_screen = new DiscoveryScreen(this, &rtc_clock);
|
||||
last_heard_screen = new LastHeardScreen(&rtc_clock);
|
||||
rxlog_screen = new RxLogScreen(this, &rtc_clock);
|
||||
trace_screen = new TraceScreen(this, &rtc_clock);
|
||||
games_menu_screen = new GamesMenuScreen(this);
|
||||
snake_screen = new SnakeScreen(this, &rtc_clock);
|
||||
@@ -3231,6 +3236,16 @@ void UITask::gotoLastHeardScreen() {
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoRxLogScreen() {
|
||||
((RxLogScreen*)rxlog_screen)->resetScroll();
|
||||
setCurrScreen(rxlog_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoTraceScreen() {
|
||||
TraceScreen* ts = (TraceScreen*)trace_screen;
|
||||
ts->enter(the_mesh.getNodePrefs()->path_hash_mode);
|
||||
|
||||
@@ -100,6 +100,7 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* path_editor; // Custom path editor screen (lazy-init)
|
||||
UIScreen* discovery_screen; // Node discovery scan screen
|
||||
UIScreen* last_heard_screen; // Last heard passive advert list
|
||||
UIScreen* rxlog_screen; // Rx Log packet sniffer
|
||||
UIScreen* trace_screen; // Trace path screen (standalone trace tool)
|
||||
UIScreen* games_menu_screen; // Games launcher menu
|
||||
UIScreen* snake_screen; // Snake game screen
|
||||
@@ -205,6 +206,7 @@ public:
|
||||
void gotoPathEditor(int contactIdx); // Navigate to custom path editor
|
||||
void gotoDiscoveryScreen(); // Navigate to node discovery scan
|
||||
void gotoLastHeardScreen(); // Navigate to last heard passive list
|
||||
void gotoRxLogScreen(); // Navigate to Rx Log packet sniffer
|
||||
void gotoTraceScreen(); // Navigate to trace path screen
|
||||
void gotoGamesMenu(); // Navigate to games launcher menu
|
||||
void gotoSnakeScreen(); // Navigate to snake game
|
||||
@@ -266,6 +268,7 @@ public:
|
||||
bool isOnPathEditor() const { return curr == path_editor; }
|
||||
bool isOnDiscoveryScreen() const { return curr == discovery_screen; }
|
||||
bool isOnLastHeardScreen() const { return curr == last_heard_screen; }
|
||||
bool isOnRxLogScreen() const { return curr == rxlog_screen; }
|
||||
bool isOnTraceScreen() const { return curr == trace_screen; }
|
||||
bool isOnGamesMenu() const { return curr == games_menu_screen; }
|
||||
bool isOnSnakeScreen() const { return curr == snake_screen; }
|
||||
@@ -354,6 +357,7 @@ public:
|
||||
UIScreen* getPathEditorScreen() const { return path_editor; }
|
||||
UIScreen* getDiscoveryScreen() const { return discovery_screen; }
|
||||
UIScreen* getLastHeardScreen() const { return last_heard_screen; }
|
||||
UIScreen* getRxLogScreen() const { return rxlog_screen; }
|
||||
UIScreen* getTraceScreen() const { return trace_screen; }
|
||||
UIScreen* getGamesMenuScreen() const { return games_menu_screen; }
|
||||
UIScreen* getSnakeScreen() const { return snake_screen; }
|
||||
|
||||
Reference in New Issue
Block a user