Remove legacy dead code: pre-LVGL screens and unused stubs

Delete 24 files (2487 lines):
- 18 legacy screen files (BootScreen, HomeScreen, NodesScreen,
  MessagesScreen, MessageView, SettingsScreen, HelpOverlay,
  NameInputScreen, MapScreen) replaced by Lv-prefixed LVGL versions
- PowerManager (superseded by hal/Power)
- hal/Audio (ES7210 stub, working audio is audio/AudioNotify)
- hal/GPS (stub, GPS deprioritized)

Move shared types (SettingType, SettingItem, SettingsCategory) from
deleted SettingsScreen.h into LvSettingsScreen.h.

Clean main.cpp: remove legacy includes, instances, and tab mapping.

Note: StatusBar and TabBar retained — still used by UIManager/main.cpp.
This commit is contained in:
DeFiDude
2026-03-08 14:22:16 -06:00
parent b3fa3927ce
commit 4687bc9d38
26 changed files with 37 additions and 2481 deletions
-13
View File
@@ -1,13 +0,0 @@
#include "Audio.h"
#include "config/BoardConfig.h"
bool Audio::begin() {
// TODO: Initialize ES7210 codec via I2S
// Pins: I2S_WS=5, I2S_DOUT=6, I2S_BCK=7, I2S_DIN=14, I2S_SCK=47, I2S_MCLK=48
Serial.println("[AUDIO] ES7210 init (stub)");
return true;
}
void Audio::setVolume(uint8_t vol) {
_volume = vol;
}
-15
View File
@@ -1,15 +0,0 @@
#pragma once
#include <Arduino.h>
// ES7210 I2S audio codec driver for T-Deck Plus
// Stub — full I2S implementation in Phase 6
class Audio {
public:
bool begin();
void setVolume(uint8_t vol);
uint8_t volume() const { return _volume; }
private:
uint8_t _volume = 80;
};
-13
View File
@@ -1,13 +0,0 @@
#include "GPS.h"
#include "config/BoardConfig.h"
bool GPS::begin() {
// GPS is deprioritized — stub only
// UBlox MIA-M10Q on UART: TX=43, RX=44, 115200 baud
Serial.println("[GPS] Disabled (deprioritized)");
return false;
}
void GPS::loop() {
// No-op
}
-19
View File
@@ -1,19 +0,0 @@
#pragma once
#include <Arduino.h>
// UBlox MIA-M10Q GPS driver for T-Deck Plus
// Stub — GPS is deprioritized per plan
class GPS {
public:
bool begin();
void loop();
bool hasFix() const { return false; }
double latitude() const { return 0; }
double longitude() const { return 0; }
int satellites() const { return 0; }
private:
bool _enabled = false;
};
-23
View File
@@ -21,15 +21,6 @@
#include "ui/UIManager.h"
#include "ui/LvTabBar.h"
#include "ui/LvInput.h"
#include "ui/screens/BootScreen.h"
#include "ui/screens/HomeScreen.h"
#include "ui/screens/NodesScreen.h"
#include "ui/screens/MessagesScreen.h"
#include "ui/screens/MessageView.h"
#include "ui/screens/SettingsScreen.h"
#include "ui/screens/HelpOverlay.h"
// MapScreen removed
#include "ui/screens/NameInputScreen.h"
#include "ui/screens/LvBootScreen.h"
#include "ui/screens/LvHomeScreen.h"
#include "ui/screens/LvNodesScreen.h"
@@ -98,17 +89,6 @@ Power powerMgr;
AudioNotify audio;
IdentityManager identityMgr;
// --- Legacy Screens (kept for fallback during migration) ---
BootScreen bootScreen;
HomeScreen homeScreen;
NodesScreen nodesScreen;
MessagesScreen messagesScreen;
MessageView messageView;
SettingsScreen settingsScreen;
HelpOverlay helpOverlay;
// MapScreen removed
NameInputScreen nameInputScreen;
// --- LVGL Screens ---
LvBootScreen lvBootScreen;
LvHomeScreen lvHomeScreen;
@@ -124,9 +104,6 @@ LvNameInputScreen lvNameInputScreen;
// Tab-screen mapping (4 tabs) — LVGL versions
LvScreen* lvTabScreens[LvTabBar::TAB_COUNT] = {};
// Legacy tab mapping (kept for reference)
Screen* tabScreens[LvTabBar::TAB_COUNT] = {};
// --- State ---
bool radioOnline = false;
bool bootComplete = false;
-59
View File
@@ -1,59 +0,0 @@
#include "PowerManager.h"
#include "hal/Display.h"
void PowerManager::begin(Display* display) {
_display = display;
_lastActivity = millis();
_state = ACTIVE;
if (_display) _display->setBrightness(_fullBrightness);
}
void PowerManager::activity() {
_lastActivity = millis();
if (_state != ACTIVE) {
setState(ACTIVE);
}
}
void PowerManager::loop() {
unsigned long elapsed = millis() - _lastActivity;
switch (_state) {
case ACTIVE:
if (_offTimeout > 0 && elapsed >= _offTimeout) {
setState(SCREEN_OFF);
} else if (_dimTimeout > 0 && elapsed >= _dimTimeout) {
setState(DIMMED);
}
break;
case DIMMED:
if (_offTimeout > 0 && elapsed >= _offTimeout) {
setState(SCREEN_OFF);
}
break;
case SCREEN_OFF:
// Stay off until activity()
break;
}
}
void PowerManager::setState(State newState) {
if (newState == _state) return;
_state = newState;
if (!_display) return;
switch (_state) {
case ACTIVE:
_display->setBrightness(_fullBrightness);
break;
case DIMMED:
_display->setBrightness(DIM_BRIGHTNESS);
break;
case SCREEN_OFF:
_display->setBrightness(0);
break;
}
}
-35
View File
@@ -1,35 +0,0 @@
#pragma once
#include <Arduino.h>
class Display;
class PowerManager {
public:
enum State { ACTIVE, DIMMED, SCREEN_OFF };
void begin(Display* display);
void loop();
// Call on any user activity (keypress, touch, trackball)
void activity();
// Configuration (seconds)
void setDimTimeout(uint16_t seconds) { _dimTimeout = seconds * 1000UL; }
void setOffTimeout(uint16_t seconds) { _offTimeout = seconds * 1000UL; }
void setBrightness(uint8_t brightness) { _fullBrightness = brightness; }
State state() const { return _state; }
bool isScreenOn() const { return _state != SCREEN_OFF; }
private:
void setState(State newState);
Display* _display = nullptr;
State _state = ACTIVE;
unsigned long _lastActivity = 0;
unsigned long _dimTimeout = 30000; // 30s
unsigned long _offTimeout = 60000; // 60s
uint8_t _fullBrightness = 255;
static constexpr uint8_t DIM_BRIGHTNESS = 64;
};
-55
View File
@@ -1,55 +0,0 @@
#include "BootScreen.h"
#include "ui/Theme.h"
#include "config/Config.h"
#include "hal/Display.h"
void BootScreen::draw(LGFX_TDeck& gfx) {
int cx = Theme::SCREEN_W / 2;
// Title
gfx.setTextSize(2);
gfx.setTextColor(Theme::PRIMARY, Theme::BG);
const char* title = "RATDECK";
int tw = strlen(title) * 12; // 12px per char at size 2
gfx.setCursor(cx - tw / 2, 70);
gfx.print(title);
// Version
gfx.setTextSize(1);
gfx.setTextColor(Theme::SECONDARY, Theme::BG);
char ver[32];
snprintf(ver, sizeof(ver), "v%s", RATDECK_VERSION_STRING);
int vw = strlen(ver) * 6;
gfx.setCursor(cx - vw / 2, 95);
gfx.print(ver);
// Progress bar background
int barX = cx - 100;
int barY = 120;
int barW = 200;
int barH = 10;
gfx.fillRect(barX, barY, barW, barH, Theme::BORDER);
// Progress bar fill
int fillW = (int)(barW * _progress);
if (fillW > 0) {
gfx.fillRect(barX, barY, fillW, barH, Theme::PRIMARY);
}
// Status text
gfx.setTextSize(1);
gfx.setTextColor(Theme::SECONDARY, Theme::BG);
int sw = strlen(_status) * 6;
gfx.setCursor(cx - sw / 2, 145);
gfx.print(_status);
}
void BootScreen::setProgress(float progress, const char* status) {
_progress = progress;
strncpy(_status, status, sizeof(_status) - 1);
_status[sizeof(_status) - 1] = '\0';
markDirty();
// Force immediate render during boot (no main loop running yet)
// Caller (main.cpp) must call ui.render() after this
}
-15
View File
@@ -1,15 +0,0 @@
#pragma once
#include "ui/UIManager.h"
class BootScreen : public Screen {
public:
void setProgress(float progress, const char* status);
const char* title() const override { return "Boot"; }
void draw(LGFX_TDeck& gfx) override;
private:
float _progress = 0;
char _status[64] = "Starting...";
};
-52
View File
@@ -1,52 +0,0 @@
#include "HelpOverlay.h"
#include "ui/Theme.h"
#include "hal/Display.h"
void HelpOverlay::draw(LGFX_TDeck& gfx) {
if (!_visible) return;
// Semi-transparent overlay box
int bx = 20, by = Theme::CONTENT_Y + 10;
int bw = Theme::CONTENT_W - 40;
int bh = Theme::CONTENT_H - 20;
gfx.fillRect(bx, by, bw, bh, Theme::BG);
gfx.drawRect(bx, by, bw, bh, Theme::ACCENT);
gfx.setTextSize(1);
int x = bx + 8;
int y = by + 8;
int lineH = 12;
// Title
gfx.setTextColor(Theme::ACCENT, Theme::BG);
gfx.setCursor(x, y);
gfx.print("HOTKEYS");
y += lineH + 4;
const char* lines[] = {
"Ctrl+H This help",
"Ctrl+M Messages",
"Ctrl+N New message",
"Ctrl+S Settings",
"Ctrl+A Force announce",
"Ctrl+D Diagnostics (serial)",
"Ctrl+T Radio test TX",
"Ctrl+R RSSI monitor",
", / Cycle tabs",
"; . Scroll up/down",
"Esc Back",
};
gfx.setTextColor(Theme::PRIMARY, Theme::BG);
for (const char* line : lines) {
gfx.setCursor(x, y);
gfx.print(line);
y += lineH;
}
}
bool HelpOverlay::handleKey(const KeyEvent& event) {
_visible = false;
return true;
}
-17
View File
@@ -1,17 +0,0 @@
#pragma once
#include "ui/UIManager.h"
class HelpOverlay : public Screen {
public:
bool handleKey(const KeyEvent& event) override;
void toggle() { _visible = !_visible; }
bool isVisible() const { return _visible; }
const char* title() const override { return "Help"; }
void draw(LGFX_TDeck& gfx) override;
private:
bool _visible = false;
};
-81
View File
@@ -1,81 +0,0 @@
#include "HomeScreen.h"
#include "ui/Theme.h"
#include "hal/Display.h"
#include "reticulum/ReticulumManager.h"
#include "radio/SX1262.h"
#include "config/UserConfig.h"
#include <Arduino.h>
#include <esp_system.h>
void HomeScreen::update() {
// Only redraw when minute changes or heap changes significantly
unsigned long upMins = millis() / 60000;
uint32_t heap = ESP.getFreeHeap() / 1024;
if (upMins != _lastUptime || heap != _lastHeap) {
_lastUptime = upMins;
_lastHeap = heap;
markDirty();
}
}
bool HomeScreen::handleKey(const KeyEvent& event) {
if (event.enter || event.character == '\n' || event.character == '\r') {
if (_announceCb) _announceCb();
return true;
}
return false;
}
void HomeScreen::draw(LGFX_TDeck& gfx) {
int x = 4;
int y = Theme::CONTENT_Y + 4;
int lineH = 12;
gfx.setTextSize(1);
auto drawLine = [&](uint32_t col, const char* fmt, ...) {
char buf[80];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
gfx.setTextColor(col, Theme::BG);
gfx.setCursor(x, y);
gfx.print(buf);
y += lineH;
};
if (_rns) {
drawLine(Theme::PRIMARY, "LXMF: %s", _rns->destinationHashStr().c_str());
drawLine(Theme::PRIMARY, "Transport: %s",
_rns->isTransportActive() ? "ACTIVE" : "OFFLINE");
drawLine(Theme::PRIMARY, "Paths: %d Links: %d",
(int)_rns->pathCount(), (int)_rns->linkCount());
} else {
drawLine(Theme::MUTED, "Identity: ---");
drawLine(Theme::MUTED, "Transport: OFFLINE");
drawLine(Theme::MUTED, "Paths: 0 Links: 0");
}
if (_radio && _radio->isRadioOnline()) {
drawLine(Theme::PRIMARY, "LoRa: SF%d BW%luk %ddBm",
_radio->getSpreadingFactor(),
(unsigned long)(_radio->getSignalBandwidth() / 1000),
_radio->getTxPower());
} else {
drawLine(Theme::ERROR_CLR, "Radio: OFFLINE");
}
drawLine(Theme::PRIMARY, "Heap: %lukB free",
(unsigned long)(ESP.getFreeHeap() / 1024));
drawLine(Theme::PRIMARY, "PSRAM: %lukB free",
(unsigned long)(ESP.getFreePsram() / 1024));
unsigned long mins = millis() / 60000;
if (mins >= 60) {
drawLine(Theme::PRIMARY, "Uptime: %luh %lum", mins / 60, mins % 60);
} else {
drawLine(Theme::PRIMARY, "Uptime: %lum", mins);
}
}
-30
View File
@@ -1,30 +0,0 @@
#pragma once
#include "ui/UIManager.h"
#include <functional>
class ReticulumManager;
class SX1262;
class UserConfig;
class HomeScreen : public Screen {
public:
void update() override;
bool handleKey(const KeyEvent& event) override;
void setReticulumManager(ReticulumManager* rns) { _rns = rns; }
void setRadio(SX1262* radio) { _radio = radio; }
void setUserConfig(UserConfig* cfg) { _cfg = cfg; }
void setAnnounceCallback(std::function<void()> cb) { _announceCb = cb; }
const char* title() const override { return "Home"; }
void draw(LGFX_TDeck& gfx) override;
private:
ReticulumManager* _rns = nullptr;
SX1262* _radio = nullptr;
UserConfig* _cfg = nullptr;
unsigned long _lastUptime = 0;
uint32_t _lastHeap = 0;
std::function<void()> _announceCb;
};
+37 -2
View File
@@ -16,8 +16,43 @@ class TCPClientInterface;
class ReticulumManager;
class IdentityManager;
// Reuse existing SettingType, SettingItem, SettingsCategory from SettingsScreen.h
#include "SettingsScreen.h"
enum class SettingType : uint8_t {
READONLY,
INTEGER,
TOGGLE,
ENUM_CHOICE,
ACTION,
TEXT_INPUT
};
enum class SettingsView : uint8_t {
CATEGORY_LIST,
ITEM_LIST,
WIFI_PICKER
};
struct SettingItem {
const char* label;
SettingType type;
std::function<int()> getter;
std::function<void(int)> setter;
std::function<String(int)> formatter;
int minVal = 0;
int maxVal = 1;
int step = 1;
std::vector<const char*> enumLabels;
std::function<void()> action;
std::function<String()> textGetter;
std::function<void(const String&)> textSetter;
int maxTextLen = 16;
};
struct SettingsCategory {
const char* name;
int startIdx;
int count;
std::function<String()> summary;
};
class LvSettingsScreen : public LvScreen {
public:
-25
View File
@@ -1,25 +0,0 @@
#include "MapScreen.h"
#include "ui/Theme.h"
#include "hal/Display.h"
void MapScreen::draw(LGFX_TDeck& gfx) {
gfx.setTextSize(1);
gfx.setTextColor(Theme::MUTED, Theme::BG);
const char* lines[] = {
"Map",
"",
"Coming soon",
"",
"Node topology view",
"will appear here"
};
int y = Theme::CONTENT_Y + Theme::CONTENT_H / 2 - 36;
for (const char* line : lines) {
int tw = strlen(line) * 6;
gfx.setCursor(Theme::SCREEN_W / 2 - tw / 2, y);
gfx.print(line);
y += 12;
}
}
-9
View File
@@ -1,9 +0,0 @@
#pragma once
#include "ui/UIManager.h"
class MapScreen : public Screen {
public:
const char* title() const override { return "Map"; }
void draw(LGFX_TDeck& gfx) override;
};
-329
View File
@@ -1,329 +0,0 @@
#include "MessageView.h"
#include "ui/Theme.h"
#include "hal/Display.h"
#include "reticulum/LXMFManager.h"
#include "reticulum/AnnounceManager.h"
#include <Arduino.h>
#include <time.h>
void MessageView::onEnter() {
if (_lxmf) _lxmf->markRead(_peerHex);
_lastMsgCount = -1;
_scrollPixels = 0;
_lastRefreshMs = 0;
refreshMessages();
rebuildLayout();
markDirty();
}
void MessageView::onExit() {
_inputText.clear();
_cachedMsgs.clear();
_layout.clear();
}
void MessageView::refreshMessages() {
if (!_lxmf) return;
_cachedMsgs = _lxmf->getMessages(_peerHex);
_lastRefreshMs = millis();
}
void MessageView::update() {
if (!_lxmf) return;
unsigned long now = millis();
if (now - _lastRefreshMs >= REFRESH_INTERVAL_MS) {
int oldCount = (int)_cachedMsgs.size();
refreshMessages();
if ((int)_cachedMsgs.size() != oldCount) {
_lastMsgCount = (int)_cachedMsgs.size();
rebuildLayout();
_scrollPixels = 0; // snap to bottom on new message
markDirty();
}
}
}
// =============================================================================
// Word wrap + layout
// =============================================================================
std::vector<std::string> MessageView::wordWrap(const std::string& text, int maxChars) {
std::vector<std::string> lines;
if (text.empty()) { lines.push_back(""); return lines; }
size_t pos = 0;
while (pos < text.size()) {
size_t remaining = text.size() - pos;
if ((int)remaining <= maxChars) {
lines.push_back(text.substr(pos));
break;
}
// Find last space within maxChars
int breakAt = maxChars;
for (int i = maxChars; i > maxChars / 2; i--) {
if (text[pos + i] == ' ') { breakAt = i; break; }
}
lines.push_back(text.substr(pos, breakAt));
pos += breakAt;
// Skip the space we broke at
if (pos < text.size() && text[pos] == ' ') pos++;
}
return lines;
}
std::string MessageView::formatTime(double timestamp) {
if (timestamp < 1700000000) return "";
time_t t = (time_t)timestamp;
struct tm* tm = localtime(&t);
if (!tm) return "";
char buf[8];
snprintf(buf, sizeof(buf), "%02d:%02d", tm->tm_hour, tm->tm_min);
return buf;
}
void MessageView::rebuildLayout() {
_layout.clear();
_totalContentH = 0;
for (int i = 0; i < (int)_cachedMsgs.size(); i++) {
const auto& msg = _cachedMsgs[i];
MsgLayout ml;
ml.msgIdx = i;
ml.incoming = msg.incoming;
ml.lines = wordWrap(msg.content, MAX_BUBBLE_CHARS);
ml.timeStr = formatTime(msg.timestamp);
int textH = (int)ml.lines.size() * LINE_H;
ml.totalHeight = textH + BUBBLE_PAD * 2 + BUBBLE_GAP;
// Add time line height if we have a timestamp
if (!ml.timeStr.empty()) ml.totalHeight += 8;
_totalContentH += ml.totalHeight;
_layout.push_back(ml);
}
}
// =============================================================================
// Drawing
// =============================================================================
void MessageView::draw(LGFX_TDeck& gfx) {
gfx.setTextSize(1);
// Header — peer name
gfx.setTextColor(Theme::ACCENT, Theme::BG);
gfx.setCursor(4, Theme::CONTENT_Y + 2);
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);
int headerBottom = Theme::CONTENT_Y + 12;
gfx.drawFastHLine(0, headerBottom, Theme::SCREEN_W, Theme::BORDER);
// Input area
int inputY = Theme::SCREEN_H - Theme::TAB_BAR_H - 16;
gfx.drawFastHLine(0, inputY - 2, Theme::SCREEN_W, Theme::BORDER);
gfx.fillRect(0, inputY, Theme::SCREEN_W, 16, Theme::BG);
gfx.setCursor(4, inputY + 4);
if (_inputText.empty()) {
gfx.setTextColor(Theme::MUTED, Theme::BG);
gfx.print("Type message...");
} else {
gfx.setTextColor(Theme::PRIMARY, Theme::BG);
int maxShow = (Theme::SCREEN_W - 44) / CHAR_W;
if ((int)_inputText.length() > maxShow) {
gfx.print(_inputText.substr(_inputText.length() - maxShow).c_str());
} else {
gfx.print(_inputText.c_str());
}
}
// Cursor blink
int maxShow = (Theme::SCREEN_W - 44) / CHAR_W;
int cursorX = 4 + (int)std::min(_inputText.length(), (size_t)maxShow) * CHAR_W;
if ((millis() / 500) % 2 == 0) {
gfx.fillRect(cursorX, inputY + 2, 2, 10, Theme::ACCENT);
}
// Send button
gfx.setTextColor(Theme::PRIMARY, Theme::SELECTION_BG);
gfx.fillRect(Theme::SCREEN_W - 32, inputY, 30, 14, Theme::SELECTION_BG);
gfx.setCursor(Theme::SCREEN_W - 30, inputY + 3);
gfx.print("Send");
// Message area with bubbles
int msgTop = headerBottom + 2;
int msgBottom = inputY - 3;
int msgAreaH = msgBottom - msgTop;
if (_layout.empty()) return;
// Calculate visible region — draw from bottom up
// _scrollPixels == 0 means showing the latest messages at bottom
int contentBottom = _totalContentH;
int viewBottom = contentBottom - _scrollPixels;
int viewTop = viewBottom - msgAreaH;
int y = msgTop; // screen Y cursor
int contentY = 0; // content space Y cursor
for (int li = 0; li < (int)_layout.size(); li++) {
const auto& ml = _layout[li];
int blockTop = contentY;
int blockBottom = contentY + ml.totalHeight;
// Skip blocks entirely above viewport
if (blockBottom <= viewTop) {
contentY = blockBottom;
continue;
}
// Stop if we've passed the viewport
if (blockTop >= viewBottom) break;
// Map content Y to screen Y
int screenY = msgTop + (blockTop - viewTop);
const auto& msg = _cachedMsgs[ml.msgIdx];
int textH = (int)ml.lines.size() * LINE_H;
int bubbleH = textH + BUBBLE_PAD * 2;
// Calculate bubble width from longest line
int maxLineLen = 0;
for (auto& line : ml.lines) {
if ((int)line.length() > maxLineLen) maxLineLen = line.length();
}
int bubbleW = maxLineLen * CHAR_W + BUBBLE_PAD * 2 + 2;
if (bubbleW < 30) bubbleW = 30;
int bubbleX;
uint32_t bubbleBg, textColor;
if (ml.incoming) {
bubbleX = 4;
bubbleBg = Theme::MSG_IN_BG;
textColor = Theme::ACCENT;
} else {
bubbleX = Theme::SCREEN_W - bubbleW - 4;
bubbleBg = Theme::MSG_OUT_BG;
textColor = Theme::PRIMARY;
}
// Clip to message area
if (screenY >= msgTop && screenY + bubbleH <= msgBottom) {
// Draw bubble background
gfx.fillRoundRect(bubbleX, screenY, bubbleW, bubbleH, 3, bubbleBg);
// Draw text lines
gfx.setTextColor(textColor, bubbleBg);
for (int j = 0; j < (int)ml.lines.size(); j++) {
gfx.setCursor(bubbleX + BUBBLE_PAD + 1, screenY + BUBBLE_PAD + j * LINE_H);
gfx.print(ml.lines[j].c_str());
}
// Status indicator for outgoing
if (!ml.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 = Theme::ERROR_CLR;
}
gfx.setTextColor(indColor, bubbleBg);
gfx.setCursor(bubbleX + bubbleW - CHAR_W - BUBBLE_PAD, screenY + bubbleH - LINE_H - BUBBLE_PAD + 1);
gfx.print(ind);
}
// Timestamp below bubble
if (!ml.timeStr.empty()) {
int timeX = ml.incoming ? bubbleX + 2 : bubbleX + bubbleW - (int)ml.timeStr.length() * CHAR_W - 2;
gfx.setTextColor(Theme::MUTED, Theme::BG);
gfx.setCursor(timeX, screenY + bubbleH + 1);
gfx.print(ml.timeStr.c_str());
}
}
contentY = blockBottom;
}
// Scroll indicator
if (_totalContentH > msgAreaH) {
int thumbH = std::max(8, msgAreaH * msgAreaH / _totalContentH);
int maxScroll = _totalContentH - msgAreaH;
int thumbY = msgTop + (msgAreaH - thumbH) * (_totalContentH - _scrollPixels - msgAreaH) / std::max(1, maxScroll);
thumbY = std::max(msgTop, std::min(thumbY, msgBottom - thumbH));
gfx.fillRect(Theme::SCREEN_W - 2, msgTop, 2, msgAreaH, Theme::BORDER);
gfx.fillRect(Theme::SCREEN_W - 2, thumbY, 2, thumbH, Theme::SECONDARY);
}
}
// =============================================================================
// Input handling
// =============================================================================
void MessageView::sendCurrentMessage() {
if (!_lxmf || _peerHex.empty() || _inputText.empty()) return;
RNS::Bytes destHash;
destHash.assignHex(_peerHex.c_str());
_lxmf->sendMessage(destHash, _inputText.c_str());
_inputText.clear();
// Immediately refresh to show sent message
refreshMessages();
rebuildLayout();
_scrollPixels = 0;
markDirty();
}
bool MessageView::handleKey(const KeyEvent& event) {
if (event.character == 0x1B) {
if (_onBack) _onBack();
return true;
}
if (event.del || event.character == 0x08) {
if (!_inputText.empty()) {
_inputText.pop_back();
markDirty();
} else {
if (_onBack) _onBack();
}
return true;
}
if (event.enter || event.character == '\n' || event.character == '\r') {
sendCurrentMessage();
return true;
}
// Scroll
if (event.up) {
int maxScroll = std::max(0, _totalContentH - 100);
if (_scrollPixels < maxScroll) {
_scrollPixels += 20;
markDirty();
}
return true;
}
if (event.down) {
if (_scrollPixels > 0) {
_scrollPixels -= 20;
if (_scrollPixels < 0) _scrollPixels = 0;
markDirty();
}
return true;
}
if (event.character >= 0x20 && event.character < 0x7F) {
_inputText += (char)event.character;
markDirty();
return true;
}
return false;
}
-63
View File
@@ -1,63 +0,0 @@
#pragma once
#include "ui/UIManager.h"
#include "reticulum/LXMFMessage.h"
#include <functional>
#include <string>
#include <vector>
class LXMFManager;
class AnnounceManager;
// Pre-computed layout for a single message (may span multiple lines)
struct MsgLayout {
int msgIdx; // index into _cachedMsgs
std::vector<std::string> lines; // word-wrapped lines
std::string timeStr; // "12:34" or "HH:MM"
bool incoming;
int totalHeight; // pixel height for this message block
};
class MessageView : public Screen {
public:
using BackCallback = std::function<void()>;
void update() override;
void onEnter() override;
void onExit() override;
bool handleKey(const KeyEvent& event) override;
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"; }
void draw(LGFX_TDeck& gfx) override;
private:
void sendCurrentMessage();
void refreshMessages();
void rebuildLayout();
static std::vector<std::string> wordWrap(const std::string& text, int maxChars);
static std::string formatTime(double timestamp);
LXMFManager* _lxmf = nullptr;
AnnounceManager* _am = nullptr;
BackCallback _onBack;
std::string _peerHex;
std::string _inputText;
int _lastMsgCount = -1;
int _scrollPixels = 0; // pixel scroll from bottom
int _totalContentH = 0; // total height of all messages
std::vector<LXMFMessage> _cachedMsgs;
std::vector<MsgLayout> _layout;
unsigned long _lastRefreshMs = 0;
static constexpr unsigned long REFRESH_INTERVAL_MS = 500;
static constexpr int CHAR_W = 6;
static constexpr int LINE_H = 10;
static constexpr int BUBBLE_PAD = 3;
static constexpr int BUBBLE_GAP = 4;
static constexpr int MAX_BUBBLE_CHARS = 38;
};
-103
View File
@@ -1,103 +0,0 @@
#include "MessagesScreen.h"
#include "ui/Theme.h"
#include "hal/Display.h"
#include "reticulum/LXMFManager.h"
#include "reticulum/AnnounceManager.h"
#include <Arduino.h>
void MessagesScreen::onEnter() {
_lastConvCount = -1;
_selectedIdx = 0;
markDirty();
}
void MessagesScreen::update() {
if (!_lxmf) return;
int convCount = (int)_lxmf->conversations().size();
if (convCount != _lastConvCount) {
_lastConvCount = convCount;
markDirty();
}
}
void MessagesScreen::draw(LGFX_TDeck& gfx) {
gfx.setTextSize(1);
if (!_lxmf || _lxmf->conversations().empty()) {
gfx.setTextColor(Theme::MUTED, Theme::BG);
const char* msg = "No conversations";
int tw = strlen(msg) * 6;
gfx.setCursor(Theme::SCREEN_W / 2 - tw / 2, Theme::CONTENT_Y + Theme::CONTENT_H / 2 - 4);
gfx.print(msg);
return;
}
const auto& convs = _lxmf->conversations();
int y = Theme::CONTENT_Y + 2;
int rowH = 20;
for (size_t i = 0; i < convs.size(); i++) {
if (y + rowH > Theme::SCREEN_H - Theme::TAB_BAR_H) break;
const auto& peerHex = convs[i];
int unread = _lxmf->unreadCount(peerHex);
// Selection highlight
if ((int)i == _selectedIdx) {
gfx.fillRect(0, y, Theme::SCREEN_W, rowH, Theme::SELECTION_BG);
}
// 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(displayName.c_str());
// Unread badge
if (unread > 0) {
char badge[8];
snprintf(badge, sizeof(badge), "(%d)", unread);
gfx.setTextColor(Theme::BADGE_BG, (int)i == _selectedIdx ? Theme::SELECTION_BG : Theme::BG);
gfx.setCursor(Theme::SCREEN_W - 30, y + 6);
gfx.print(badge);
}
// Separator
gfx.drawFastHLine(0, y + rowH - 1, Theme::SCREEN_W, Theme::BORDER);
y += rowH;
}
}
bool MessagesScreen::handleKey(const KeyEvent& event) {
if (!_lxmf) return false;
int count = (int)_lxmf->conversations().size();
if (count == 0) return false;
if (event.up) {
if (_selectedIdx > 0) {
_selectedIdx--;
markDirty();
}
return true;
}
if (event.down) {
if (_selectedIdx < count - 1) {
_selectedIdx++;
markDirty();
}
return true;
}
if (event.enter || event.character == '\n' || event.character == '\r') {
if (_selectedIdx < count && _onOpen) {
_onOpen(_lxmf->conversations()[_selectedIdx]);
}
return true;
}
return false;
}
-31
View File
@@ -1,31 +0,0 @@
#pragma once
#include "ui/UIManager.h"
#include <functional>
#include <string>
class LXMFManager;
class AnnounceManager;
class MessagesScreen : public Screen {
public:
using OpenCallback = std::function<void(const std::string& peerHex)>;
void update() override;
void onEnter() override;
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"; }
void draw(LGFX_TDeck& gfx) override;
private:
LXMFManager* _lxmf = nullptr;
AnnounceManager* _am = nullptr;
OpenCallback _onOpen;
int _lastConvCount = -1;
int _selectedIdx = 0;
};
-102
View File
@@ -1,102 +0,0 @@
#include "NameInputScreen.h"
#include "ui/Theme.h"
#include "config/Config.h"
#include "hal/Display.h"
void NameInputScreen::draw(LGFX_TDeck& gfx) {
int cx = Theme::SCREEN_W / 2;
// Ratspeak branding
gfx.setTextSize(2);
gfx.setTextColor(Theme::PRIMARY, Theme::BG);
const char* brand = "RATSPEAK";
int bw = strlen(brand) * 12;
gfx.setCursor(cx - bw / 2, 30);
gfx.print(brand);
// .org subtitle
gfx.setTextSize(1);
gfx.setTextColor(Theme::ACCENT, Theme::BG);
const char* sub = "ratspeak.org";
int sw = strlen(sub) * 6;
gfx.setCursor(cx - sw / 2, 52);
gfx.print(sub);
// Prompt
gfx.setTextColor(Theme::SECONDARY, Theme::BG);
const char* prompt = "Enter your display name";
int pw = strlen(prompt) * 6;
gfx.setCursor(cx - pw / 2, 85);
gfx.print(prompt);
gfx.setTextColor(Theme::MUTED, Theme::BG);
const char* opt = "(Optional - press Enter to skip)";
int ow = strlen(opt) * 6;
gfx.setCursor(cx - ow / 2, 100);
gfx.print(opt);
// Text input field
int fieldW = 200;
int fieldH = 20;
int fieldX = cx - fieldW / 2;
int fieldY = 125;
gfx.drawRect(fieldX, fieldY, fieldW, fieldH, Theme::PRIMARY);
gfx.fillRect(fieldX + 1, fieldY + 1, fieldW - 2, fieldH - 2, Theme::SELECTION_BG);
// Name text
gfx.setTextSize(1);
gfx.setTextColor(Theme::PRIMARY, Theme::SELECTION_BG);
gfx.setCursor(fieldX + 6, fieldY + 6);
gfx.print(_name);
// Cursor blink (simple solid cursor)
int cursorX = fieldX + 6 + _nameLen * 6;
if (cursorX < fieldX + fieldW - 8) {
bool blink = (millis() / 500) % 2 == 0;
if (blink) {
gfx.fillRect(cursorX, fieldY + 4, 6, 12, Theme::PRIMARY);
}
}
// OK hint
gfx.setTextColor(Theme::ACCENT, Theme::BG);
const char* hint = "[Enter] OK";
int hw = strlen(hint) * 6;
gfx.setCursor(cx - hw / 2, 160);
gfx.print(hint);
// Version at bottom
gfx.setTextColor(Theme::MUTED, Theme::BG);
char ver[32];
snprintf(ver, sizeof(ver), "Ratdeck v%s", RATDECK_VERSION_STRING);
int vw = strlen(ver) * 6;
gfx.setCursor(cx - vw / 2, 190);
gfx.print(ver);
}
bool NameInputScreen::handleKey(const KeyEvent& event) {
if (event.enter || event.character == '\n' || event.character == '\r') {
if (_doneCb) _doneCb(String(_name));
return true;
}
if (event.del || event.character == 8) {
if (_nameLen > 0) {
_nameLen--;
_name[_nameLen] = '\0';
markDirty();
}
return true;
}
// Printable characters
if (event.character >= 0x20 && event.character <= 0x7E && _nameLen < MAX_NAME_LEN) {
_name[_nameLen] = event.character;
_nameLen++;
_name[_nameLen] = '\0';
markDirty();
return true;
}
return true; // Consume all keys on this screen
}
-20
View File
@@ -1,20 +0,0 @@
#pragma once
#include "ui/UIManager.h"
#include <functional>
class NameInputScreen : public Screen {
public:
bool handleKey(const KeyEvent& event) override;
const char* title() const override { return "Setup"; }
void draw(LGFX_TDeck& gfx) override;
void setDoneCallback(std::function<void(const String&)> cb) { _doneCb = cb; }
static constexpr int MAX_NAME_LEN = 16;
private:
char _name[MAX_NAME_LEN + 1] = {0};
int _nameLen = 0;
std::function<void(const String&)> _doneCb;
};
-104
View File
@@ -1,104 +0,0 @@
#include "NodesScreen.h"
#include "ui/Theme.h"
#include "hal/Display.h"
#include "reticulum/AnnounceManager.h"
#include <Arduino.h>
void NodesScreen::onEnter() {
_lastNodeCount = -1;
_selectedIdx = 0;
markDirty();
}
void NodesScreen::update() {
if (!_am) return;
if (_am->nodeCount() != _lastNodeCount) {
_lastNodeCount = _am->nodeCount();
markDirty();
}
}
void NodesScreen::draw(LGFX_TDeck& gfx) {
gfx.setTextSize(1);
if (!_am || _am->nodeCount() == 0) {
gfx.setTextColor(Theme::MUTED, Theme::BG);
const char* msg = "No nodes discovered";
int tw = strlen(msg) * 6;
gfx.setCursor(Theme::SCREEN_W / 2 - tw / 2, Theme::CONTENT_Y + Theme::CONTENT_H / 2 - 4);
gfx.print(msg);
return;
}
const auto& nodes = _am->nodes();
int y = Theme::CONTENT_Y + 2;
int rowH = 18;
for (size_t i = 0; i < nodes.size(); i++) {
if (y + rowH > Theme::SCREEN_H - Theme::TAB_BAR_H) break;
const auto& node = nodes[i];
// Selection highlight
if ((int)i == _selectedIdx) {
gfx.fillRect(0, y, Theme::SCREEN_W, rowH, Theme::SELECTION_BG);
}
uint32_t bgCol = (int)i == _selectedIdx ? Theme::SELECTION_BG : Theme::BG;
// 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(), displayHash.c_str());
gfx.setTextColor(node.saved ? Theme::ACCENT : Theme::PRIMARY, bgCol);
gfx.setCursor(4, y + 5);
gfx.print(buf);
// Hops + age (right side)
unsigned long ageSec = (millis() - node.lastSeen) / 1000;
char infoBuf[24];
if (ageSec < 60) snprintf(infoBuf, sizeof(infoBuf), "%dhop %lus", node.hops, ageSec);
else snprintf(infoBuf, sizeof(infoBuf), "%dhop %lum", node.hops, ageSec / 60);
int tw = strlen(infoBuf) * 6;
gfx.setTextColor(Theme::SECONDARY, bgCol);
gfx.setCursor(Theme::SCREEN_W - tw - 4, y + 5);
gfx.print(infoBuf);
y += rowH;
}
}
bool NodesScreen::handleKey(const KeyEvent& event) {
if (!_am) return false;
int count = _am->nodeCount();
if (count == 0) return false;
if (event.up) {
if (_selectedIdx > 0) {
_selectedIdx--;
markDirty();
}
return true;
}
if (event.down) {
if (_selectedIdx < count - 1) {
_selectedIdx++;
markDirty();
}
return true;
}
if (event.enter || event.character == '\n' || event.character == '\r') {
if (_selectedIdx < count && _onSelect) {
_onSelect(_am->nodes()[_selectedIdx].hash.toHex());
}
return true;
}
return false;
}
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#include "ui/UIManager.h"
#include <functional>
#include <string>
class AnnounceManager;
class NodesScreen : public Screen {
public:
using NodeSelectedCallback = std::function<void(const std::string& peerHex)>;
void update() override;
void onEnter() override;
bool handleKey(const KeyEvent& event) override;
void setAnnounceManager(AnnounceManager* am) { _am = am; }
void setNodeSelectedCallback(NodeSelectedCallback cb) { _onSelect = cb; }
const char* title() const override { return "Nodes"; }
void draw(LGFX_TDeck& gfx) override;
private:
AnnounceManager* _am = nullptr;
NodeSelectedCallback _onSelect;
int _lastNodeCount = -1;
int _selectedIdx = 0;
};
File diff suppressed because it is too large Load Diff
-142
View File
@@ -1,142 +0,0 @@
#pragma once
#include "ui/UIManager.h"
#include "transport/WiFiInterface.h"
#include <string>
#include <vector>
#include <functional>
class UserConfig;
class FlashStore;
class SDStore;
class SX1262;
class AudioNotify;
class Power;
class WiFiInterface;
class TCPClientInterface;
class ReticulumManager;
class IdentityManager;
enum class SettingType : uint8_t {
READONLY,
INTEGER,
TOGGLE,
ENUM_CHOICE,
ACTION,
TEXT_INPUT
};
enum class SettingsView : uint8_t {
CATEGORY_LIST,
ITEM_LIST,
WIFI_PICKER
};
struct SettingItem {
const char* label;
SettingType type;
std::function<int()> getter;
std::function<void(int)> setter;
std::function<String(int)> formatter;
int minVal = 0;
int maxVal = 1;
int step = 1;
std::vector<const char*> enumLabels;
std::function<void()> action;
std::function<String()> textGetter;
std::function<void(const String&)> textSetter;
int maxTextLen = 16;
};
struct SettingsCategory {
const char* name;
int startIdx;
int count;
std::function<String()> summary;
};
class SettingsScreen : public Screen {
public:
void onEnter() override;
void update() override;
bool handleKey(const KeyEvent& event) override;
void setUserConfig(UserConfig* cfg) { _cfg = cfg; }
void setFlashStore(FlashStore* fs) { _flash = fs; }
void setSDStore(SDStore* sd) { _sd = sd; }
void setRadio(SX1262* radio) { _radio = radio; }
void setAudio(AudioNotify* audio) { _audio = audio; }
void setPower(Power* power) { _power = power; }
void setWiFi(WiFiInterface* wifi) { _wifi = wifi; }
void setTCPClients(std::vector<TCPClientInterface*>* tcp) { _tcp = tcp; }
void setRNS(ReticulumManager* rns) { _rns = rns; }
void setIdentityManager(IdentityManager* idm) { _idMgr = idm; }
void setUIManager(UIManager* ui) { _ui = ui; }
void setIdentityHash(const String& hash) { _identityHash = hash; }
void setSaveCallback(std::function<bool()> cb) { _saveCallback = cb; }
const char* title() const override { return "Settings"; }
void draw(LGFX_TDeck& gfx) override;
private:
void buildItems();
void applyAndSave();
void applyPreset(int presetIdx);
int detectPreset() const;
void drawCategoryList(LGFX_TDeck& gfx);
void drawItemList(LGFX_TDeck& gfx);
void drawWifiPicker(LGFX_TDeck& gfx);
bool handleCategoryKeys(const KeyEvent& event);
bool handleItemKeys(const KeyEvent& event);
bool handleWifiPickerKeys(const KeyEvent& event);
void enterCategory(int catIdx);
void exitToCategories();
void skipToNextEditable(int dir);
bool isEditable(int idx) const;
UserConfig* _cfg = nullptr;
FlashStore* _flash = nullptr;
SDStore* _sd = nullptr;
SX1262* _radio = nullptr;
AudioNotify* _audio = nullptr;
Power* _power = nullptr;
WiFiInterface* _wifi = nullptr;
std::vector<TCPClientInterface*>* _tcp = nullptr;
ReticulumManager* _rns = nullptr;
IdentityManager* _idMgr = nullptr;
UIManager* _ui = nullptr;
String _identityHash;
std::function<bool()> _saveCallback;
// View state
SettingsView _view = SettingsView::CATEGORY_LIST;
// Categories
std::vector<SettingsCategory> _categories;
int _categoryIdx = 0;
int _categoryScroll = 0;
// Items (within current category)
std::vector<SettingItem> _items;
int _selectedIdx = 0;
int _itemScroll = 0;
int _catRangeStart = 0;
int _catRangeEnd = 0;
// Edit state
bool _editing = false;
int _editValue = 0;
bool _textEditing = false;
String _editText;
bool _confirmingReset = false;
// WiFi picker
std::vector<WiFiInterface::ScanResult> _wifiResults;
int _wifiPickerIdx = 0;
int _wifiPickerScroll = 0;
bool _wifiScanning = false;
};