Files

216 lines
7.8 KiB
C++

#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, Shift+Del 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("Sh+Del: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 == KEY_CANCEL) {
if (_task) _task->gotoSettingsScreen();
return true;
}
return false;
}
};