Files
pyxis/lib/tdeck_ui/Display.cpp
torlando-tech ac6ceca9f8 Initial commit: standalone Pyxis T-Deck firmware
Split T-Deck firmware from microReticulum examples/lxmf_tdeck/ into its
own repo. microReticulum is consumed as a git submodule dependency pinned
to feat/t-deck. All include paths updated from relative symlinks to bare
includes resolved via library build flags.

Both tdeck (NimBLE) and tdeck-bluedroid environments compile successfully.
Licensed under AGPLv3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 19:48:33 -05:00

412 lines
11 KiB
C++

/*
* Display.cpp - OLED display implementation for microReticulum
*/
#include "Display.h"
#ifdef HAS_DISPLAY
#include "DisplayGraphics.h"
#include "Identity.h"
#include "Interface.h"
#include "Reticulum.h"
#include "Transport.h"
#include "Log.h"
#include "Utilities/OS.h"
// Display library includes (board-specific)
#ifdef ARDUINO
#include <Wire.h>
#ifdef DISPLAY_TYPE_SH1106
#include <Adafruit_SH110X.h>
#elif defined(DISPLAY_TYPE_SSD1306)
#include <Adafruit_SSD1306.h>
#endif
#include <Adafruit_GFX.h>
#endif
namespace RNS {
// Display dimensions
static const int16_t DISPLAY_WIDTH = 128;
static const int16_t DISPLAY_HEIGHT = 64;
// Layout constants
static const int16_t HEADER_HEIGHT = 17;
static const int16_t CONTENT_Y = 21;
static const int16_t LINE_HEIGHT = 10;
static const int16_t LEFT_MARGIN = 2;
// Static member initialization
bool Display::_ready = false;
bool Display::_blanked = false;
uint8_t Display::_current_page = 0;
uint32_t Display::_last_page_flip = 0;
uint32_t Display::_last_update = 0;
uint32_t Display::_start_time = 0;
Bytes Display::_identity_hash;
Interface* Display::_interface = nullptr;
Reticulum* Display::_reticulum = nullptr;
float Display::_rssi = -120.0f;
// Display object (board-specific)
#ifdef ARDUINO
#ifdef DISPLAY_TYPE_SH1106
static Adafruit_SH1106G* display = nullptr;
#elif defined(DISPLAY_TYPE_SSD1306)
static Adafruit_SSD1306* display = nullptr;
#endif
#endif
bool Display::init() {
#ifdef ARDUINO
TRACE("Display::init: Initializing display...");
// Initialize I2C
#if defined(DISPLAY_SDA) && defined(DISPLAY_SCL)
Wire.begin(DISPLAY_SDA, DISPLAY_SCL);
#else
Wire.begin();
#endif
// Create display object
#ifdef DISPLAY_TYPE_SH1106
display = new Adafruit_SH1106G(DISPLAY_WIDTH, DISPLAY_HEIGHT, &Wire, -1);
#ifndef DISPLAY_ADDR
#define DISPLAY_ADDR 0x3C
#endif
if (!display->begin(DISPLAY_ADDR, true)) {
ERROR("Display::init: SH1106 display not found");
delete display;
display = nullptr;
return false;
}
#elif defined(DISPLAY_TYPE_SSD1306)
display = new Adafruit_SSD1306(DISPLAY_WIDTH, DISPLAY_HEIGHT, &Wire, -1);
#ifndef DISPLAY_ADDR
#define DISPLAY_ADDR 0x3C
#endif
if (!display->begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDR)) {
ERROR("Display::init: SSD1306 display not found");
delete display;
display = nullptr;
return false;
}
#else
ERROR("Display::init: No display type defined");
return false;
#endif
// Configure display
display->setRotation(0); // Portrait mode
display->clearDisplay();
display->setTextSize(1);
display->setTextColor(1); // White
display->cp437(true); // Enable extended characters
display->display();
_ready = true;
_start_time = (uint32_t)Utilities::OS::ltime();
_last_page_flip = _start_time;
_last_update = _start_time;
INFO("Display::init: Display initialized successfully");
return true;
#else
// Native build - no display support
return false;
#endif
}
void Display::update() {
if (!_ready || _blanked) return;
#ifdef ARDUINO
uint32_t now = (uint32_t)Utilities::OS::ltime();
// Check for page rotation
if (now - _last_page_flip >= PAGE_INTERVAL) {
_current_page = (_current_page + 1) % NUM_PAGES;
_last_page_flip = now;
}
// Throttle display updates
if (now - _last_update < UPDATE_INTERVAL) return;
_last_update = now;
// Clear and redraw
display->clearDisplay();
// Draw header (common to all pages)
draw_header();
// Draw page-specific content
switch (_current_page) {
case 0:
draw_page_main();
break;
case 1:
draw_page_interface();
break;
case 2:
draw_page_network();
break;
}
// Send to display
display->display();
#endif
}
void Display::set_identity(const Identity& identity) {
if (identity) {
_identity_hash = identity.hash();
}
}
void Display::set_interface(Interface* iface) {
_interface = iface;
}
void Display::set_reticulum(Reticulum* rns) {
_reticulum = rns;
}
void Display::blank(bool blank) {
_blanked = blank;
#ifdef ARDUINO
if (_ready && display) {
if (blank) {
display->clearDisplay();
display->display();
}
}
#endif
}
void Display::set_page(uint8_t page) {
if (page < NUM_PAGES) {
_current_page = page;
_last_page_flip = (uint32_t)Utilities::OS::ltime();
}
}
void Display::next_page() {
_current_page = (_current_page + 1) % NUM_PAGES;
_last_page_flip = (uint32_t)Utilities::OS::ltime();
}
uint8_t Display::current_page() {
return _current_page;
}
bool Display::ready() {
return _ready;
}
void Display::set_rssi(float rssi) {
_rssi = rssi;
}
// Private implementation
void Display::draw_header() {
#ifdef ARDUINO
// Draw "μRNS" text logo
display->setTextSize(2); // 2x size for visibility
display->setCursor(3, 0);
// Use CP437 code 230 (0xE6) for μ character
display->write(0xE6); // μ
display->print("RNS");
display->setTextSize(1); // Reset to normal size
// Draw signal bars on the right
draw_signal_bars(DISPLAY_WIDTH - Graphics::SIGNAL_WIDTH - 2, 4);
// Draw separator line
display->drawLine(0, HEADER_HEIGHT, DISPLAY_WIDTH - 1, HEADER_HEIGHT, 1);
#endif
}
void Display::draw_signal_bars(int16_t x, int16_t y) {
#ifdef ARDUINO
// Temporarily disabled to debug stray pixel
// uint8_t level = 0;
// if (_interface && _interface->online()) {
// level = Graphics::rssi_to_level(_rssi);
// }
// const uint8_t* bitmap = Graphics::get_signal_bitmap(level);
// display->drawBitmap(x, y, bitmap, Graphics::SIGNAL_WIDTH, Graphics::SIGNAL_HEIGHT, 1);
#endif
}
void Display::draw_page_main() {
#ifdef ARDUINO
int16_t y = CONTENT_Y;
// Identity hash
display->setCursor(LEFT_MARGIN, y);
display->print("ID: ");
if (_identity_hash.size() > 0) {
// Show first 12 hex chars
std::string hex = _identity_hash.toHex();
if (hex.length() > 12) hex = hex.substr(0, 12);
display->print(hex.c_str());
} else {
display->print("(none)");
}
y += LINE_HEIGHT + 4;
// Interface status
display->setCursor(LEFT_MARGIN, y);
if (_interface) {
display->print(_interface->name().c_str());
display->print(": ");
display->print(_interface->online() ? "ONLINE" : "OFFLINE");
} else {
display->print("No interface");
}
y += LINE_HEIGHT;
// Link count
display->setCursor(LEFT_MARGIN, y);
display->print("Links: ");
if (_reticulum) {
display->print((int)_reticulum->get_link_count());
} else {
display->print("0");
}
#endif
}
void Display::draw_page_interface() {
#ifdef ARDUINO
int16_t y = CONTENT_Y;
if (!_interface) {
display->setCursor(LEFT_MARGIN, y);
display->print("No interface");
return;
}
// Interface name
display->setCursor(LEFT_MARGIN, y);
display->print("Interface: ");
display->print(_interface->name().c_str());
y += LINE_HEIGHT;
// Mode
display->setCursor(LEFT_MARGIN, y);
display->print("Mode: ");
switch (_interface->mode()) {
case Type::Interface::MODE_GATEWAY:
display->print("Gateway");
break;
case Type::Interface::MODE_ACCESS_POINT:
display->print("Access Point");
break;
case Type::Interface::MODE_POINT_TO_POINT:
display->print("Point-to-Point");
break;
case Type::Interface::MODE_ROAMING:
display->print("Roaming");
break;
case Type::Interface::MODE_BOUNDARY:
display->print("Boundary");
break;
default:
display->print("Unknown");
break;
}
y += LINE_HEIGHT;
// Bitrate
display->setCursor(LEFT_MARGIN, y);
display->print("Bitrate: ");
display->print(format_bitrate(_interface->bitrate()).c_str());
y += LINE_HEIGHT;
// Status
display->setCursor(LEFT_MARGIN, y);
display->print("Status: ");
display->print(_interface->online() ? "Online" : "Offline");
#endif
}
void Display::draw_page_network() {
#ifdef ARDUINO
int16_t y = CONTENT_Y;
// Links and paths
display->setCursor(LEFT_MARGIN, y);
display->print("Links: ");
size_t link_count = _reticulum ? _reticulum->get_link_count() : 0;
display->print((int)link_count);
display->print(" Paths: ");
size_t path_count = _reticulum ? _reticulum->get_path_table().size() : 0;
display->print((int)path_count);
y += LINE_HEIGHT;
// RTT placeholder (would need link reference to get actual RTT)
display->setCursor(LEFT_MARGIN, y);
display->print("RTT: --");
y += LINE_HEIGHT;
// TX/RX bytes (if interface available)
display->setCursor(LEFT_MARGIN, y);
display->print("TX/RX: --");
y += LINE_HEIGHT;
// Uptime
display->setCursor(LEFT_MARGIN, y);
display->print("Uptime: ");
uint32_t uptime_sec = ((uint32_t)Utilities::OS::ltime() - _start_time) / 1000;
display->print(format_time(uptime_sec).c_str());
#endif
}
std::string Display::format_bytes(size_t bytes) {
char buf[16];
if (bytes >= 1024 * 1024) {
snprintf(buf, sizeof(buf), "%.1fM", bytes / (1024.0 * 1024.0));
} else if (bytes >= 1024) {
snprintf(buf, sizeof(buf), "%.1fK", bytes / 1024.0);
} else {
snprintf(buf, sizeof(buf), "%zuB", bytes);
}
return std::string(buf);
}
std::string Display::format_time(uint32_t seconds) {
char buf[16];
if (seconds >= 3600) {
uint32_t hours = seconds / 3600;
uint32_t mins = (seconds % 3600) / 60;
snprintf(buf, sizeof(buf), "%luh %lum", (unsigned long)hours, (unsigned long)mins);
} else if (seconds >= 60) {
uint32_t mins = seconds / 60;
uint32_t secs = seconds % 60;
snprintf(buf, sizeof(buf), "%lum %lus", (unsigned long)mins, (unsigned long)secs);
} else {
snprintf(buf, sizeof(buf), "%lus", (unsigned long)seconds);
}
return std::string(buf);
}
std::string Display::format_bitrate(uint32_t bps) {
char buf[16];
if (bps >= 1000000) {
snprintf(buf, sizeof(buf), "%.1f Mbps", bps / 1000000.0);
} else if (bps >= 1000) {
snprintf(buf, sizeof(buf), "%.1f kbps", bps / 1000.0);
} else {
snprintf(buf, sizeof(buf), "%lu bps", (unsigned long)bps);
}
return std::string(buf);
}
} // namespace RNS
#endif // HAS_DISPLAY