Merge branch 'develop' into pioarduino

This commit is contained in:
Austin
2026-04-27 20:15:22 -04:00
committed by GitHub
87 changed files with 3057 additions and 761 deletions
+3 -3
View File
@@ -8,15 +8,15 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.524
- renovate@43.141.0
- checkov@3.2.525
- renovate@43.142.0
- prettier@3.8.3
- trufflehog@3.95.2
- yamllint@1.38.0
- bandit@1.9.4
- trivy@0.70.0
- taplo@0.10.0
- ruff@0.15.11
- ruff@0.15.12
- isort@8.0.1
- markdownlint@0.48.0
- oxipng@10.1.1
+2 -1
View File
@@ -31,5 +31,6 @@ basename=meshtasticd-$1-$VERSION
pio pkg install --environment "$PIO_ENV" || platformioFailed
pio run --environment "$PIO_ENV" || platformioFailed
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)"
os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_${os_name}_$(uname -m)"
cp bin/native-install.* $OUTDIR/
+43
View File
@@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_v4_r8"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "heltec_wifi_lora_32 v4 r8 (16 MB FLASH, 8 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "heltec"
}
+1 -1
View File
@@ -125,7 +125,7 @@ lib_deps =
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/56e1da4e7d30abcd746a2092a30e422f8cf5fc2b.zip
https://github.com/meshtastic/device-ui/archive/728932970996ec91bdb93cb6dae29c2cb70c66e2.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
+10 -21
View File
@@ -824,8 +824,10 @@ void Power::reboot()
rp2040.reboot();
#elif defined(ARCH_PORTDUINO)
deInitApiServer();
#ifdef __linux__
if (aLinuxInputImpl)
aLinuxInputImpl->deInit();
#endif
SPI.end();
Wire.end();
Serial1.end();
@@ -1043,11 +1045,8 @@ int32_t Power::runOnce()
powerFSM.trigger(EVENT_POWER_CONNECTED);
}
#ifdef T_WATCH_S3
/*
In the T-Watch S3 this code fragment reacts to the short press of the button by switching the
display on and off
*/
#ifdef PMU_POWER_BUTTON_IS_CANCEL
// cancel action also turns the screen on and off.
if (PMU->isPekeyShortPressIrq()) {
LOG_INFO("Input: Corona Button Click");
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0};
@@ -1070,13 +1069,6 @@ int32_t Power::runOnce()
LOG_DEBUG("Battery removed");
}
*/
#ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3?
if (PMU->isPekeyLongPressIrq()) {
LOG_DEBUG("PEK long button press");
if (screen)
screen->setOn(false);
}
#endif
PMU->clearIrqStatus();
}
@@ -1145,7 +1137,7 @@ void Power::attachPowerInterrupts()
if (PMU) {
attachInterrupt(
PMU_IRQ,
[] {
[]() {
pmu_irq = true;
power->setIntervalFromNow(0);
runASAP = true;
@@ -1448,19 +1440,16 @@ bool Power::axpChipInit()
uint64_t pmuIrqMask = 0;
if (PMU->getChipModel() == XPOWERS_AXP192) {
pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_BAT_INSERT_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_VBUS_REMOVE_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
}
pinMode(PMU_IRQ, INPUT);
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ
// because it occurs repeatedly while there is no battery also it could cause
// inadvertent waking from light sleep just because the battery filled we
// don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while
// no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we
// don't have anything hooked to vbus
// We wake on IRQ, so only enable the IRQs that we care about.
// we want USB plug and unplug to update the screen and LED status,
// and short press on the power button to trigger the "cancel" action in the UI (which also turns the screen on and off).
PMU->enableIRQ(pmuIrqMask);
PMU->clearIrqStatus();
+1 -1
View File
@@ -1,8 +1,8 @@
#pragma once
#include "../freertosinc.h"
#include "Print.h"
#include "mesh/generated/meshtastic/mesh.pb.h"
#include <Print.h>
#include <stdarg.h>
#include <string>
+3
View File
@@ -162,6 +162,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#elif defined(HELTEC_MESH_NODE_T096)
#define NUM_PA_POINTS 22
#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 11, 10, 9, 8, 7
#elif defined(HELTEC_V4_R8)
#define NUM_PA_POINTS 22
#define TX_GAIN_LORA 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10, 9, 8, 7
#else
// If a board enables USE_KCT8103L_PA but does not match a known variant and has
// not already provided a PA curve, fail at compile time to avoid unsafe defaults.
+11 -6
View File
@@ -70,20 +70,25 @@ bool GPSUpdateScheduling::isUpdateDue()
// Have we been searching for a GPS position for too long?
bool GPSUpdateScheduling::searchedTooLong()
{
constexpr uint32_t oneMinuteMs = 60UL * 1000UL;
constexpr uint32_t maxSearchClampMs = 15UL * oneMinuteMs; // Hard cap: 15 minutes is always too long
uint32_t elapsed = elapsedSearchMs();
// Anything over 15 minutes is too long, regardless of the broadcast interval.
// TODO: Make a smarter algorithm that backs off the search dwell time when not getting a lock.
if (elapsed > maxSearchClampMs)
return true;
uint32_t minimumOrConfiguredSecs =
Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs);
uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs);
// If broadcast interval set to max, no such thing as "too long"
if (maxSearchMs == UINT32_MAX)
return false;
// If we've been searching longer than our position broadcast interval: that's too long
else if (elapsedSearchMs() > maxSearchMs)
if (elapsed > maxSearchMs)
return true;
// Otherwise, not too long yet!
else
return false;
return false;
}
// Updates the predicted time-to-get-lock, by exponentially smoothing the latest observation
+59 -53
View File
@@ -39,6 +39,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "draw/NodeListRenderer.h"
#include "draw/NotificationRenderer.h"
#include "draw/UIRenderer.h"
#include "graphics/TFTColorRegions.h"
#include "modules/CannedMessageModule.h"
#if !MESHTASTIC_EXCLUDE_GPS
@@ -54,6 +55,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "gps/RTC.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TFTPalette.h"
#include "graphics/emotes.h"
#include "graphics/images.h"
#include "input/TouchScreenImpl1.h"
@@ -69,12 +71,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "target_specific.h"
extern MessageStore messageStore;
#if USE_TFTDISPLAY
extern uint16_t TFT_MESH;
#else
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
#endif
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
#include "mesh/wifi/WiFiAPClient.h"
#endif
@@ -109,6 +105,27 @@ namespace graphics
// A text message frame + debug frame + all the node infos
FrameCallback *normalFrames;
static uint32_t targetFramerate = IDLE_FRAMERATE;
#if GRAPHICS_TFT_COLORING_ENABLED
static inline void prepareFrameColorRegions()
{
#if GRAPHICS_TFT_COLORING_ENABLED
clearTFTColorRegions();
// Full-frame FrameMono inversion for themes that need it (e.g. light themes).
if (isThemeFullFrameInvert()) {
setAndRegisterTFTColorRole(TFTColorRole::FrameMono, getThemeBodyFg(), getThemeBodyBg(), 0, 0, screen->getWidth(),
screen->getHeight());
}
#endif
}
#endif
static inline void updateUiFrame(OLEDDisplayUi *ui)
{
#if GRAPHICS_TFT_COLORING_ENABLED
prepareFrameColorRegions();
#endif
ui->update();
}
// Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization
uint32_t logo_timeout = 5000; // 4 seconds for EACH logo
@@ -227,7 +244,7 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
ui->update();
updateUiFrame(ui);
}
// Called to trigger a banner with custom message and duration
@@ -249,7 +266,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
ui->update();
updateUiFrame(ui);
}
// Called to trigger a banner with custom message and duration
@@ -273,7 +290,7 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
ui->update();
updateUiFrame(ui);
}
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
@@ -296,7 +313,7 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
ui->update();
updateUiFrame(ui);
}
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
@@ -388,30 +405,6 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
{
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
int32_t rawRGB = uiconfig.screen_rgb_color;
// Only validate the combined value once
if (rawRGB > 0 && rawRGB <= 255255255) {
LOG_INFO("Setting screen RGB color to user chosen: 0x%06X", rawRGB);
// Extract each component as a normal int first
int r = (rawRGB >> 16) & 0xFF;
int g = (rawRGB >> 8) & 0xFF;
int b = rawRGB & 0xFF;
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
TFT_MESH = COLOR565(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
}
#ifdef TFT_MESH_OVERRIDE
} else if (rawRGB == 0) {
LOG_INFO("Setting screen RGB color to TFT_MESH_OVERRIDE: 0x%04X", TFT_MESH_OVERRIDE);
// Default to TFT_MESH_OVERRIDE if available
TFT_MESH = TFT_MESH_OVERRIDE;
#endif
} else {
// Default best readable yellow color
LOG_INFO("Setting screen RGB color to default: (255,255,128)");
TFT_MESH = COLOR565(255, 255, 128);
}
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
@@ -474,9 +467,13 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
#endif
#if defined(USE_ST7789)
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
// Keep firmware and ST7789 driver region structs layout-compatible:
// we pass `graphics::colorRegions` through a type cast below.
static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion),
"graphics::TFTColorRegion layout must match ST7789 TFTColorRegion");
static_cast<ST7789Spi *>(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions);
#elif defined(USE_ST7796)
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
static_cast<ST7796Spi *>(dispdev)->setRGB(TFTPalette::White);
#endif
ui = new OLEDDisplayUi(dispdev);
@@ -663,16 +660,16 @@ void Screen::setup()
static_cast<SH1106Wire *>(dispdev)->setSubtype(7);
#endif
#if defined(USE_ST7789) && defined(TFT_MESH)
// Apply custom RGB color (e.g. Heltec T114/T190)
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
#if defined(USE_ST7789)
static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion),
"graphics::TFTColorRegion layout must match ST7789 TFTColorRegion");
static_cast<ST7789Spi *>(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions);
#endif
#if defined(MUZI_BASE)
dispdev->delayPoweron = true;
#endif
#if defined(USE_ST7796) && defined(TFT_MESH)
// Custom text color, if defined in variant.h
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
#if defined(USE_ST7796)
static_cast<ST7796Spi *>(dispdev)->setRGB(TFTPalette::White);
#endif
// Initialize display and UI system
@@ -718,7 +715,7 @@ void Screen::setup()
#endif
{
const char *region = myRegion ? myRegion->name : nullptr;
graphics::UIRenderer::drawIconScreen(region, display, state, x, y);
graphics::UIRenderer::drawBootIconScreen(region, display, state, x, y);
}
};
ui->setFrames(alertFrames, 1);
@@ -757,9 +754,9 @@ void Screen::setup()
// Turn on display and trigger first draw
handleSetOn(true);
graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width());
ui->update();
updateUiFrame(ui);
#ifndef USE_EINK
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
updateUiFrame(ui); // Some SSD1306 clones drop the first draw, so run twice
#endif
serialSinceMsec = millis();
@@ -832,7 +829,7 @@ void Screen::forceDisplay(bool forceUiUpdate)
do {
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
delay(10);
ui->update();
updateUiFrame(ui);
} while (ui->getUiState()->lastUpdate < startUpdate);
// Return to normal frame rate
@@ -903,9 +900,9 @@ int32_t Screen::runOnce()
static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen};
static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]);
ui->setFrames(bootOEMFrames, bootOEMFrameCount);
ui->update();
updateUiFrame(ui);
#ifndef USE_EINK
ui->update();
updateUiFrame(ui);
#endif
showingOEMBootScreen = false;
}
@@ -996,7 +993,7 @@ int32_t Screen::runOnce()
// this must be before the frameState == FIXED check, because we always
// want to draw at least one FIXED frame before doing forceDisplay
ui->update();
updateUiFrame(ui);
// Switch to a low framerate (to save CPU) when we are not in transition
// but we should only call setTargetFPS when framestate changes, because
@@ -1058,7 +1055,7 @@ void Screen::setSSLFrames()
// LOG_DEBUG("Show SSL frames");
static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen};
ui->setFrames(sslFrames, 1);
ui->update();
updateUiFrame(ui);
}
}
@@ -1094,7 +1091,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
do {
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
delay(1);
ui->update();
updateUiFrame(ui);
} while (ui->getUiState()->lastUpdate < startUpdate);
#if defined(USE_EINK_PARALLELDISPLAY)
@@ -1469,9 +1466,15 @@ void Screen::blink()
dispdev->setBrightness(254);
while (count > 0) {
dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight());
#if GRAPHICS_TFT_COLORING_ENABLED
prepareFrameColorRegions();
#endif
dispdev->display();
delay(50);
dispdev->clear();
#if GRAPHICS_TFT_COLORING_ENABLED
prepareFrameColorRegions();
#endif
dispdev->display();
delay(50);
count = count - 1;
@@ -1605,6 +1608,9 @@ void Screen::setFastFramerate()
{
#if defined(M5STACK_UNITC6L)
dispdev->clear();
#if GRAPHICS_TFT_COLORING_ENABLED
prepareFrameColorRegions();
#endif
dispdev->display();
#endif
// We are about to start a transition so speed up fps
@@ -1816,7 +1822,7 @@ int Screen::handleInputEvent(const InputEvent *event)
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
setFastFramerate(); // Draw ASAP
ui->update();
updateUiFrame(ui);
return 0;
}
@@ -1831,7 +1837,7 @@ int Screen::handleInputEvent(const InputEvent *event)
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
setFastFramerate(); // Draw ASAP
ui->update();
updateUiFrame(ui);
menuHandler::handleMenuSwitch(dispdev);
return 0;
+260 -60
View File
@@ -1,16 +1,20 @@
#include "configuration.h"
#if HAS_SCREEN
#include "MeshService.h"
#include "NodeDB.h"
#include "RTC.h"
#include "draw/NodeListRenderer.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TFTColorRegions.h"
#include "graphics/TFTPalette.h"
#include "graphics/draw/UIRenderer.h"
#include "main.h"
#include "meshtastic/config.pb.h"
#include "modules/ExternalNotificationModule.h"
#include "power.h"
#include <OLEDDisplay.h>
#include <cctype>
#include <graphics/images.h>
namespace graphics
@@ -65,6 +69,12 @@ uint32_t lastBlinkShared = 0;
bool isMailIconVisible = true;
uint32_t lastMailBlink = 0;
static inline bool useClockHeaderAccentTheme(uint32_t themeId)
{
return themeId == ThemeID::Pink || themeId == ThemeID::Creamsicle || themeId == ThemeID::MeshtasticGreen ||
themeId == ThemeID::ClassicRed || themeId == ThemeID::MonochromeWhite;
}
// *********************************
// * Rounded Header when inverted *
// *********************************
@@ -85,7 +95,8 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
// *************************
// * Common Header Drawing *
// *************************
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date)
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date,
bool transparent_background, bool use_title_color_override, uint16_t title_color_override)
{
constexpr int HEADER_OFFSET_Y = 1;
y += HEADER_OFFSET_Y;
@@ -100,30 +111,93 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
const int screenW = display->getWidth();
const int screenH = display->getHeight();
const int headerHeight = highlightHeight + 2;
const uint16_t headerColorForRoles = getThemeHeaderBg();
// Color TFT headers use a fixed dark background + white glyphs.
// Keep legacy inverted bitmap behavior only for monochrome displays.
const bool useInvertedHeaderStyle = (isInverted && !force_no_invert && !isTFTColoringEnabled() && !transparent_background);
#if GRAPHICS_TFT_COLORING_ENABLED
int statusLeftEndX = 0;
int statusRightStartX = screenW;
const bool isClockHeader = transparent_background && show_date && (!titleStr || titleStr[0] == '\0');
const auto activeThemeId = getActiveTheme().id;
const bool useClockHeaderAccent = isClockHeader && useClockHeaderAccentTheme(activeThemeId);
#endif
{
const uint16_t headerColor = getThemeHeaderBg();
const uint16_t headerTextColor = getThemeHeaderText();
const uint16_t headerTitleColorForRole = use_title_color_override ? title_color_override : headerTextColor;
uint16_t headerStatusColor = getThemeHeaderStatus();
#if GRAPHICS_TFT_COLORING_ENABLED
// Clock frame uses transparent header + date + empty title.
// For accent clock themes (Pink/Creamsicle + classic monochrome), tint
// status items (battery outline, %, date, mail icon) to the header accent.
if (useClockHeaderAccent) {
headerStatusColor = getThemeHeaderBg();
}
if (transparent_background) {
// Transparent clock headers should inherit whatever body off-color is
// already active under the header (important for light/inverted themes).
const uint16_t transparentBgColor = resolveTFTOffColorAt(0, headerHeight + 1, getThemeBodyBg());
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, transparentBgColor, transparentBgColor, 0, 0, screenW,
headerHeight);
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, transparentBgColor);
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, transparentBgColor);
} else if (useInvertedHeaderStyle) {
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, headerColor, TFTPalette::Black, 0, 0, screenW,
headerHeight);
setTFTColorRole(TFTColorRole::HeaderTitle, headerColor, headerTitleColorForRole);
setTFTColorRole(TFTColorRole::HeaderStatus, headerColor, headerStatusColor);
} else {
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, TFTPalette::Black, headerColor, 0, 0, screenW,
headerHeight);
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, headerColor);
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, headerColor);
}
#endif
if (!force_no_invert) {
// === Inverted Header Background ===
if (isInverted) {
if (useInvertedHeaderStyle) {
display->setColor(BLACK);
display->fillRect(0, 0, screenW, highlightHeight + 2);
display->fillRect(0, 0, screenW, headerHeight);
display->setColor(WHITE);
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
display->setColor(BLACK);
} else {
display->setColor(BLACK);
display->fillRect(0, 0, screenW, highlightHeight + 2);
display->setColor(WHITE);
if (currentResolution == ScreenResolution::High) {
display->drawLine(0, 20, screenW, 20);
} else {
display->drawLine(0, 14, screenW, 14);
display->fillRect(0, 0, screenW, headerHeight);
// Keep the legacy white separator for monochrome displays only when header background is visible.
#if !GRAPHICS_TFT_COLORING_ENABLED
if (!transparent_background) {
display->setColor(WHITE);
if (currentResolution == ScreenResolution::High) {
display->drawLine(0, 20, screenW, 20);
} else {
display->drawLine(0, 14, screenW, 14);
}
}
#endif
}
if (transparent_background) {
display->setColor(WHITE);
}
#if GRAPHICS_TFT_COLORING_ENABLED
// TFT role coloring expects foreground glyph bits to be "set".
display->setColor(WHITE);
#endif
// === Screen Title ===
const char *headerTitle = titleStr ? titleStr : "";
const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle);
const int titleX = (SCREEN_WIDTH - titleWidth) / 2;
#if GRAPHICS_TFT_COLORING_ENABLED
const int titleRegionWidth = titleWidth + (config.display.heading_bold ? 3 : 2);
registerTFTColorRegion(TFTColorRole::HeaderTitle, titleX - 1, y, titleRegionWidth, FONT_HEIGHT_SMALL);
#endif
UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold);
}
display->setTextAlignment(TEXT_ALIGN_LEFT);
@@ -152,6 +226,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH);
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
bool hasBatteryFillRegion = false;
int16_t batteryFillRegionX = 0;
int16_t batteryFillRegionY = 0;
int16_t batteryFillRegionW = 0;
int16_t batteryFillRegionH = 0;
#if GRAPHICS_TFT_COLORING_ENABLED
uint16_t batteryFillColor = getThemeBatteryFillColor(chargePercent);
if (useClockHeaderAccent) {
batteryFillColor = getThemeHeaderBg();
}
#endif
int batteryX = 1;
int batteryY = HEADER_OFFSET_Y + 1;
@@ -180,6 +265,15 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
int fillWidth = 14 * chargePercent / 100;
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
#if GRAPHICS_TFT_COLORING_ENABLED
if (fillWidth > 0) {
hasBatteryFillRegion = true;
batteryFillRegionX = batteryX + 1;
batteryFillRegionY = batteryY + 1;
batteryFillRegionW = fillWidth;
batteryFillRegionH = 11;
}
#endif
}
batteryX += 18; // Icon + 2 pixels
} else {
@@ -194,21 +288,41 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int fillHeight = 8 * chargePercent / 100;
int fillY = batteryY - fillHeight;
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
#if GRAPHICS_TFT_COLORING_ENABLED
if (fillHeight > 0) {
hasBatteryFillRegion = true;
batteryFillRegionX = batteryX + 1;
batteryFillRegionY = fillY + 10;
batteryFillRegionW = 5;
batteryFillRegionH = fillHeight;
}
#endif
}
batteryX += 9; // Icon + 2 pixels
}
}
#if GRAPHICS_TFT_COLORING_ENABLED
statusLeftEndX = batteryX + 2;
#endif
if (chargePercent != 101) {
// === Battery % Display ===
char chargeStr[4];
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
int chargeNumWidth = display->getStringWidth(chargeStr);
const int percentWidth = display->getStringWidth("%");
const int percentX = batteryX + chargeNumWidth - 1;
display->drawString(batteryX, textY, chargeStr);
display->drawString(batteryX + chargeNumWidth - 1, textY, "%");
display->drawString(percentX, textY, "%");
#if GRAPHICS_TFT_COLORING_ENABLED
statusLeftEndX = percentX + percentWidth + 2;
#endif
if (isBold) {
display->drawString(batteryX + 1, textY, chargeStr);
display->drawString(batteryX + chargeNumWidth, textY, "%");
display->drawString(percentX + 1, textY, "%");
#if GRAPHICS_TFT_COLORING_ENABLED
statusLeftEndX = percentX + percentWidth + 3;
#endif
}
}
@@ -253,6 +367,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
timeStrWidth = display->getStringWidth(timeStr);
}
timeX = screenW - xOffset - timeStrWidth + 3;
#if GRAPHICS_TFT_COLORING_ENABLED
statusRightStartX = timeX - (useHorizontalBattery ? 22 : 16);
#endif
// === Show Mail or Mute Icon to the Left of Time ===
int iconRightEdge = timeX - 2;
@@ -278,7 +395,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int iconW = 16, iconH = 12;
int iconX = iconRightEdge - iconW;
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
if (isInverted && !force_no_invert) {
if (useInvertedHeaderStyle) {
display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
display->setColor(BLACK);
@@ -293,7 +410,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
} else {
int iconX = iconRightEdge - (mail_width - 2);
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
if (isInverted && !force_no_invert) {
if (useInvertedHeaderStyle) {
display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
display->setColor(BLACK);
@@ -309,7 +426,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
if (isInverted && !force_no_invert) {
if (useInvertedHeaderStyle) {
display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
display->setColor(BLACK);
@@ -323,7 +440,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int iconX = iconRightEdge - mute_symbol_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
if (isInverted && !force_no_invert) {
if (useInvertedHeaderStyle) {
display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
display->setColor(BLACK);
@@ -351,7 +468,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
} else {
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
int iconRightEdge = screenW - xOffset;
#if GRAPHICS_TFT_COLORING_ENABLED
statusRightStartX = screenW - (useHorizontalBattery ? 22 : 12);
#endif
bool showMail = false;
#ifndef USE_EINK
@@ -393,6 +512,16 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
}
}
}
#endif
#if GRAPHICS_TFT_COLORING_ENABLED
registerTFTColorRegion(TFTColorRole::HeaderStatus, 0, 0, statusLeftEndX, headerHeight);
if (statusRightStartX < screenW) {
registerTFTColorRegion(TFTColorRole::HeaderStatus, statusRightStartX, 0, screenW - statusRightStartX, headerHeight);
}
if (hasBatteryFillRegion) {
registerTFTColorRegionDirect(batteryFillRegionX, batteryFillRegionY, batteryFillRegionW, batteryFillRegionH,
batteryFillColor, headerColorForRoles);
}
#endif
display->setColor(WHITE); // Reset for other UI
}
@@ -430,14 +559,23 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
return;
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
const int footerY = SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale);
const int footerH = (connection_icon_height * scale) + (2 * scale);
const int iconX = 0;
const int iconY = SCREEN_HEIGHT - (connection_icon_height * scale);
const int iconW = connection_icon_width * scale;
const int iconH = connection_icon_height * scale;
#if GRAPHICS_TFT_COLORING_ENABLED
// Only tint the link glyph itself on TFT; keep the footer background black.
setAndRegisterTFTColorRole(TFTColorRole::ConnectionIcon, TFTPalette::Blue, TFTPalette::Black, iconX, iconY, iconW, iconH);
#endif
display->setColor(BLACK);
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
(connection_icon_height * scale) + (2 * scale));
display->fillRect(0, footerY, SCREEN_WIDTH, footerH);
display->setColor(WHITE);
if (currentResolution == ScreenResolution::High) {
const int bytesPerRow = (connection_icon_width + 7) / 8;
int iconX = 0;
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
for (int yy = 0; yy < connection_icon_height; ++yy) {
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
@@ -451,65 +589,127 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
}
} else {
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
connection_icon);
display->drawXbm(iconX, iconY, connection_icon_width, connection_icon_height, connection_icon);
}
}
bool isAllowedPunctuation(char c)
{
const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
return allowed.find(c) != std::string::npos;
switch (c) {
case '.':
case ',':
case '!':
case '?':
case ';':
case ':':
case '-':
case '_':
case '(':
case ')':
case '[':
case ']':
case '{':
case '}':
case '\'':
case '"':
case '@':
case '#':
case '$':
case '/':
case '\\':
case '&':
case '+':
case '=':
case '%':
case '~':
case '^':
case ' ':
return true;
default:
return false;
}
}
static void replaceAll(std::string &s, const std::string &from, const std::string &to)
static inline size_t utf8CodePointLength(unsigned char lead)
{
if (from.empty())
return;
size_t pos = 0;
while ((pos = s.find(from, pos)) != std::string::npos) {
s.replace(pos, from.size(), to);
pos += to.size();
if ((lead & 0x80) == 0x00) {
return 1;
}
if ((lead & 0xE0) == 0xC0) {
return 2;
}
if ((lead & 0xF0) == 0xE0) {
return 3;
}
if ((lead & 0xF8) == 0xF0) {
return 4;
}
return 1;
}
std::string sanitizeString(const std::string &input)
{
static constexpr char kReplacementChar = static_cast<char>(0xBF); // Inverted question mark in ISO-8859-1.
std::string output;
output.reserve(input.size());
bool inReplacement = false;
// Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first.
std::string s = input;
// Curly single quotes: ‘ ’
replaceAll(s, "\xE2\x80\x98", "'"); // U+2018
replaceAll(s, "\xE2\x80\x99", "'"); // U+2019
// Curly double quotes: “ ”
replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C
replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D
// En dash / Em dash: – —
replaceAll(s, "\xE2\x80\x93", "-"); // U+2013
replaceAll(s, "\xE2\x80\x94", "-"); // U+2014
// Non-breaking space
replaceAll(s, "\xC2\xA0", " "); // U+00A0
// Now do your original sanitize pass over the normalized string.
for (unsigned char uc : s) {
char c = static_cast<char>(uc);
if (std::isalnum(uc) || isAllowedPunctuation(c)) {
output += c;
inReplacement = false;
} else {
const size_t inputSize = input.size();
size_t i = 0;
while (i < inputSize) {
const unsigned char byte0 = static_cast<unsigned char>(input[i]);
char normalized = '\0';
size_t consumed = 0;
if (byte0 < 0x80) {
normalized = static_cast<char>(byte0);
consumed = 1;
} else if ((i + 2) < inputSize && byte0 == 0xE2 && static_cast<unsigned char>(input[i + 1]) == 0x80) {
// Smart punctuation: ' ' \" \" - -
switch (static_cast<unsigned char>(input[i + 2])) {
case 0x98:
case 0x99:
normalized = '\'';
consumed = 3;
break;
case 0x9C:
case 0x9D:
normalized = '\"';
consumed = 3;
break;
case 0x93:
case 0x94:
normalized = '-';
consumed = 3;
break;
default:
break;
}
} else if ((i + 1) < inputSize && byte0 == 0xC2 && static_cast<unsigned char>(input[i + 1]) == 0xA0) {
// Non-breaking space.
normalized = ' ';
consumed = 2;
}
if (consumed == 0) {
size_t seqLen = utf8CodePointLength(byte0);
if (seqLen > (inputSize - i)) {
seqLen = 1;
}
if (!inReplacement) {
output += static_cast<char>(0xBF); // ISO-8859-1 for inverted question mark
output.push_back(kReplacementChar);
inReplacement = true;
}
i += seqLen;
continue;
}
const unsigned char normalizedUc = static_cast<unsigned char>(normalized);
if (std::isalnum(normalizedUc) || isAllowedPunctuation(normalized)) {
output.push_back(normalized);
inReplacement = false;
} else if (!inReplacement) {
output.push_back(kReplacementChar);
inReplacement = true;
}
i += consumed;
}
return output;
}
+3 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include <OLEDDisplay.h>
#include <stdint.h>
#include <string>
namespace graphics
@@ -52,7 +53,8 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
// Shared battery/time/mail header
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false,
bool show_date = false);
bool show_date = false, bool transparent_background = false, bool use_title_color_override = false,
uint16_t title_color_override = 0);
// Shared battery/time/mail header
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y);
+819
View File
@@ -0,0 +1,819 @@
#include "TFTColorRegions.h"
#include "NodeDB.h"
#include "TFTPalette.h"
#include <string.h>
namespace graphics
{
TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
namespace
{
struct TFTRoleColorsBe {
uint16_t onColorBe;
uint16_t offColorBe;
};
static uint8_t colorRegionCount = 0;
static constexpr uint32_t kFnv1aOffsetBasis = 2166136261u;
static constexpr uint32_t kFnv1aPrime = 16777619u;
static constexpr uint16_t toBe565(uint16_t color)
{
return static_cast<uint16_t>((color >> 8) | (color << 8));
}
static constexpr bool kRoleIsBody[static_cast<size_t>(TFTColorRole::Count)] = {
false, // HeaderBackground
false, // HeaderTitle
false, // HeaderStatus
true, // SignalBars
true, // ConnectionIcon
true, // UtilizationFill
true, // FavoriteNode
true, // ActionMenuBorder
true, // ActionMenuBody
true, // ActionMenuTitle
true, // FrameMono
false, // BootSplash
true, // FavoriteNodeBGHighlight
false, // NavigationBar
false // NavigationArrow
};
static inline bool isBodyColorRole(TFTColorRole role)
{
return kRoleIsBody[static_cast<size_t>(role)];
}
static inline bool isMonochromeTheme(uint32_t themeId)
{
return themeId == ThemeID::MeshtasticGreen || themeId == ThemeID::ClassicRed || themeId == ThemeID::MonochromeWhite;
}
static inline uint16_t getMonochromeAccent(uint32_t themeId)
{
return (themeId == ThemeID::MeshtasticGreen) ? TFTPalette::MeshtasticGreen
: (themeId == ThemeID::ClassicRed) ? TFTPalette::ClassicRed
: TFTPalette::White;
}
static inline void replaceColor(uint16_t &value, uint16_t from, uint16_t to)
{
if (value == from) {
value = to;
}
}
static inline uint32_t fnv1aAppendByte(uint32_t hash, uint8_t value)
{
return (hash ^ value) * kFnv1aPrime;
}
static inline uint32_t fnv1aAppendU16(uint32_t hash, uint16_t value)
{
hash = fnv1aAppendByte(hash, static_cast<uint8_t>(value & 0xFF));
hash = fnv1aAppendByte(hash, static_cast<uint8_t>((value >> 8) & 0xFF));
return hash;
}
// Compile-time header color overrides (backward-compatible)
#ifdef TFT_HEADER_BG_COLOR_OVERRIDE
static constexpr uint16_t kHeaderBackground = TFT_HEADER_BG_COLOR_OVERRIDE;
#else
static constexpr uint16_t kHeaderBackground = TFTPalette::DarkGray;
#endif
#ifdef TFT_HEADER_TITLE_COLOR_OVERRIDE
static constexpr uint16_t kTitleColor = TFT_HEADER_TITLE_COLOR_OVERRIDE;
#else
static constexpr uint16_t kTitleColor = TFTPalette::White;
#endif
#ifdef TFT_HEADER_STATUS_COLOR_OVERRIDE
static constexpr uint16_t kStatusColor = TFT_HEADER_STATUS_COLOR_OVERRIDE;
#else
static constexpr uint16_t kStatusColor = TFTPalette::White;
#endif
// Theme definitions
// Stored in kThemes[] and looked up by matching uiconfig.screen_rgb_color
// against each entry's .uniqueIdentifier field.
static const TFTThemeDef kThemes[] = {
// Default Dark (ThemeID::DefaultDark = 0)
{
ThemeID::DefaultDark, // id
"Default Dark", // name
0, // uniqueIdentifier
// roles[TFTColorRole::Count]
{
{kHeaderBackground, TFTPalette::Black}, // HeaderBackground
{kHeaderBackground, kTitleColor}, // HeaderTitle
{kHeaderBackground, kStatusColor}, // HeaderStatus
{TFTPalette::Good, TFTPalette::Black}, // SignalBars
{TFTPalette::Blue, TFTPalette::Black}, // ConnectionIcon
{TFTPalette::Good, TFTPalette::Black}, // UtilizationFill
{TFTPalette::Yellow, TFTPalette::Black}, // FavoriteNode
{TFTPalette::DarkGray, TFTPalette::Black}, // ActionMenuBorder
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBody
{TFTPalette::DarkGray, TFTPalette::White}, // ActionMenuTitle
{TFTPalette::Black, TFTPalette::White}, // FrameMono
{TFTPalette::White, TFTPalette::Black}, // BootSplash
{TFTPalette::Yellow, TFTPalette::Black}, // FavoriteNodeBGHighlight
{kStatusColor, kHeaderBackground}, // NavigationBar (icon fg, bar bg)
{kTitleColor, TFTPalette::Black}, // NavigationArrow (arrow fg, body bg)
},
TFTPalette::Good, // batteryFillGood
TFTPalette::Medium, // batteryFillMedium
TFTPalette::Bad, // batteryFillBad
false, // fullFrameInvert
true, // visible
},
// Default Light (ThemeID::DefaultLight = 1)
{
ThemeID::DefaultLight, // id
"Default Light", // name
1, // uniqueIdentifier
{
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderBackground
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderTitle
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderStatus
{TFTPalette::Good, TFTPalette::White}, // SignalBars
{TFTPalette::Blue, TFTPalette::White}, // ConnectionIcon
{TFTPalette::Good, TFTPalette::White}, // UtilizationFill
{TFTPalette::Black, TFTPalette::Yellow}, // FavoriteNode
{TFTPalette::DarkGray, TFTPalette::White}, // ActionMenuBorder
{TFTPalette::Black, TFTPalette::White}, // ActionMenuBody
{TFTPalette::DarkGray, TFTPalette::Black}, // ActionMenuTitle
{TFTPalette::Black, TFTPalette::White}, // FrameMono
{TFTPalette::White, TFTPalette::Black}, // BootSplash
{TFTPalette::Black, TFTPalette::Yellow}, // FavoriteNodeBGHighlight
{TFTPalette::Black, TFTPalette::LightGray}, // NavigationBar (icon fg, bar bg)
{TFTPalette::Black, TFTPalette::White}, // NavigationArrow (arrow fg, body bg)
},
TFTPalette::Good, // batteryFillGood
TFTPalette::Medium, // batteryFillMedium
TFTPalette::Bad, // batteryFillBad
true, // fullFrameInvert
true, // visible
},
// Christmas (ThemeID::Christmas = 2)
{
ThemeID::Christmas, // id
"Christmas", // name
2, // uniqueIdentifier
{
{TFTPalette::ChristmasRed, TFTPalette::Black}, // HeaderBackground
{TFTPalette::ChristmasRed, TFTPalette::Gold}, // HeaderTitle
{TFTPalette::ChristmasRed, TFTPalette::Gold}, // HeaderStatus
{TFTPalette::ChristmasGreen, TFTPalette::Pine}, // SignalBars
{TFTPalette::Gold, TFTPalette::Pine}, // ConnectionIcon
{TFTPalette::ChristmasGreen, TFTPalette::Pine}, // UtilizationFill
{TFTPalette::Gold, TFTPalette::Pine}, // FavoriteNode
{TFTPalette::ChristmasRed, TFTPalette::Pine}, // ActionMenuBorder
{TFTPalette::White, TFTPalette::Pine}, // ActionMenuBody
{TFTPalette::ChristmasRed, TFTPalette::White}, // ActionMenuTitle
{TFTPalette::Pine, TFTPalette::White}, // FrameMono
{TFTPalette::White, TFTPalette::ChristmasRed}, // BootSplash
{TFTPalette::Gold, TFTPalette::Pine}, // FavoriteNodeBGHighlight
{TFTPalette::Gold, TFTPalette::ChristmasRed}, // NavigationBar (icon fg, bar bg)
{TFTPalette::Gold, TFTPalette::Pine}, // NavigationArrow (arrow fg, body bg)
},
TFTPalette::ChristmasGreen, // batteryFillGood
TFTPalette::Gold, // batteryFillMedium
TFTPalette::ChristmasRed, // batteryFillBad
true, // fullFrameInvert
false, // visible
},
// Pink (ThemeID::Pink = 3) light variant
{
ThemeID::Pink, // id
"Pink", // name
3, // uniqueIdentifier
{
{TFTPalette::HotPink, TFTPalette::Black}, // HeaderBackground
{TFTPalette::HotPink, TFTPalette::White}, // HeaderTitle
{TFTPalette::HotPink, TFTPalette::White}, // HeaderStatus
{TFTPalette::DeepPink, TFTPalette::PalePink}, // SignalBars
{TFTPalette::HotPink, TFTPalette::PalePink}, // ConnectionIcon
{TFTPalette::DeepPink, TFTPalette::PalePink}, // UtilizationFill
{TFTPalette::Black, TFTPalette::HotPink}, // FavoriteNode
{TFTPalette::HotPink, TFTPalette::PalePink}, // ActionMenuBorder
{TFTPalette::Black, TFTPalette::PalePink}, // ActionMenuBody
{TFTPalette::HotPink, TFTPalette::White}, // ActionMenuTitle
{TFTPalette::Black, TFTPalette::White}, // FrameMono
{TFTPalette::White, TFTPalette::HotPink}, // BootSplash
{TFTPalette::Black, TFTPalette::HotPink}, // FavoriteNodeBGHighlight
{TFTPalette::White, TFTPalette::HotPink}, // NavigationBar (icon fg, bar bg)
{TFTPalette::HotPink, TFTPalette::PalePink}, // NavigationArrow (arrow fg, body bg)
},
TFTPalette::DeepPink, // batteryFillGood
TFTPalette::HotPink, // batteryFillMedium
TFTPalette::Bad, // batteryFillBad
true, // fullFrameInvert
true, // visible
},
// Blue (ThemeID::Blue = 4) dark variant
{
ThemeID::Blue, // id
"Blue", // name
4, // uniqueIdentifier
{
{TFTPalette::DeepBlue, TFTPalette::Black}, // HeaderBackground
{TFTPalette::DeepBlue, TFTPalette::White}, // HeaderTitle
{TFTPalette::DeepBlue, TFTPalette::SkyBlue}, // HeaderStatus
{TFTPalette::SkyBlue, TFTPalette::Navy}, // SignalBars
{TFTPalette::SkyBlue, TFTPalette::Navy}, // ConnectionIcon
{TFTPalette::SkyBlue, TFTPalette::Navy}, // UtilizationFill
{TFTPalette::SkyBlue, TFTPalette::Navy}, // FavoriteNode
{TFTPalette::DeepBlue, TFTPalette::Navy}, // ActionMenuBorder
{TFTPalette::White, TFTPalette::Navy}, // ActionMenuBody
{TFTPalette::DeepBlue, TFTPalette::White}, // ActionMenuTitle
{TFTPalette::Navy, TFTPalette::White}, // FrameMono
{TFTPalette::White, TFTPalette::DeepBlue}, // BootSplash
{TFTPalette::SkyBlue, TFTPalette::Navy}, // FavoriteNodeBGHighlight
{TFTPalette::SkyBlue, TFTPalette::DeepBlue}, // NavigationBar (icon fg, bar bg)
{TFTPalette::SkyBlue, TFTPalette::Black}, // NavigationArrow (arrow fg, body bg)
},
TFTPalette::SkyBlue, // batteryFillGood
TFTPalette::Medium, // batteryFillMedium
TFTPalette::Bad, // batteryFillBad
true, // fullFrameInvert
true, // visible
},
// Creamsicle (ThemeID::Creamsicle = 5)light variant
{
ThemeID::Creamsicle, // id
"Creamsicle", // name
5, // uniqueIdentifier
{
{TFTPalette::CreamOrange, TFTPalette::Black}, // HeaderBackground
{TFTPalette::CreamOrange, TFTPalette::White}, // HeaderTitle
{TFTPalette::CreamOrange, TFTPalette::White}, // HeaderStatus
{TFTPalette::DeepOrange, TFTPalette::Cream}, // SignalBars
{TFTPalette::CreamOrange, TFTPalette::Cream}, // ConnectionIcon
{TFTPalette::DeepOrange, TFTPalette::Cream}, // UtilizationFill
{TFTPalette::Black, TFTPalette::CreamOrange}, // FavoriteNode
{TFTPalette::CreamOrange, TFTPalette::Cream}, // ActionMenuBorder
{TFTPalette::Black, TFTPalette::Cream}, // ActionMenuBody
{TFTPalette::CreamOrange, TFTPalette::White}, // ActionMenuTitle
{TFTPalette::Black, TFTPalette::White}, // FrameMono
{TFTPalette::White, TFTPalette::CreamOrange}, // BootSplash
{TFTPalette::Black, TFTPalette::CreamOrange}, // FavoriteNodeBGHighlight
{TFTPalette::White, TFTPalette::CreamOrange}, // NavigationBar (icon fg, bar bg)
{TFTPalette::CreamOrange, TFTPalette::White}, // NavigationArrow (arrow fg, body bg)
},
TFTPalette::DeepOrange, // batteryFillGood
TFTPalette::Gold, // batteryFillMedium
TFTPalette::Bad, // batteryFillBad
true, // fullFrameInvert
true, // visible
},
// Meshtastic Green (ThemeID::MeshtasticGreen = 6) classic monochrome
// Pure single-color-on-black look. Every role maps foreground pixels to
// the theme color and background pixels to Black.
{
ThemeID::MeshtasticGreen, // id
"Meshtastic Green", // name
6, // uniqueIdentifier
{
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderBackground
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderTitle
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderStatus
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // SignalBars
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ConnectionIcon
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // UtilizationFill
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // FavoriteNode
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuBorder
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuBody
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuTitle
{TFTPalette::Black, TFTPalette::MeshtasticGreen}, // FrameMono (bodyBg, bodyFg)
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // BootSplash
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // FavoriteNodeBGHighlight
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // NavigationBar
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // NavigationArrow
},
TFTPalette::Black, // batteryFillGood
TFTPalette::Black, // batteryFillMedium
TFTPalette::Black, // batteryFillBad
true, // fullFrameInvert
true, // visible
},
// Classic Red (ThemeID::ClassicRed = 7) classic monochrome
{
ThemeID::ClassicRed, // id
"Classic Red", // name
7, // uniqueIdentifier
{
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderBackground
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderTitle
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderStatus
{TFTPalette::ClassicRed, TFTPalette::Black}, // SignalBars
{TFTPalette::ClassicRed, TFTPalette::Black}, // ConnectionIcon
{TFTPalette::ClassicRed, TFTPalette::Black}, // UtilizationFill
{TFTPalette::ClassicRed, TFTPalette::Black}, // FavoriteNode
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuBorder
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuBody
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuTitle
{TFTPalette::Black, TFTPalette::ClassicRed}, // FrameMono (bodyBg, bodyFg)
{TFTPalette::ClassicRed, TFTPalette::Black}, // BootSplash
{TFTPalette::ClassicRed, TFTPalette::Black}, // FavoriteNodeBGHighlight
{TFTPalette::ClassicRed, TFTPalette::Black}, // NavigationBar
{TFTPalette::ClassicRed, TFTPalette::Black}, // NavigationArrow
},
TFTPalette::Black, // batteryFillGood
TFTPalette::Black, // batteryFillMedium
TFTPalette::Black, // batteryFillBad
true, // fullFrameInvert
true, // visible
},
// Monochrome White (ThemeID::MonochromeWhite = 8) classic monochrome
{
ThemeID::MonochromeWhite, // id
"Monochrome White", // name
8, // uniqueIdentifier
{
{TFTPalette::White, TFTPalette::Black}, // HeaderBackground
{TFTPalette::White, TFTPalette::Black}, // HeaderTitle
{TFTPalette::White, TFTPalette::Black}, // HeaderStatus
{TFTPalette::White, TFTPalette::Black}, // SignalBars
{TFTPalette::White, TFTPalette::Black}, // ConnectionIcon
{TFTPalette::White, TFTPalette::Black}, // UtilizationFill
{TFTPalette::White, TFTPalette::Black}, // FavoriteNode
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBorder
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBody
{TFTPalette::White, TFTPalette::Black}, // ActionMenuTitle
{TFTPalette::Black, TFTPalette::White}, // FrameMono (bodyBg, bodyFg)
{TFTPalette::White, TFTPalette::Black}, // BootSplash
{TFTPalette::White, TFTPalette::Black}, // FavoriteNodeBGHighlight
{TFTPalette::White, TFTPalette::Black}, // NavigationBar
{TFTPalette::White, TFTPalette::Black}, // NavigationArrow
},
TFTPalette::Black, // batteryFillGood
TFTPalette::Black, // batteryFillMedium
TFTPalette::Black, // batteryFillBad
true, // fullFrameInvert
true, // visible
},
};
static constexpr size_t kInternalThemeCount = sizeof(kThemes) / sizeof(kThemes[0]);
// Resolve the kThemes[] index for the currently persisted theme. Called at
// boot (indirectly via getActiveTheme()) and whenever the active theme is
// queried, so uiconfig.screen_rgb_color remains the single source of truth.
// Matches against .uniqueIdentifier - that's the field whose value is stored
// in the user's config. Falls back to 0 (DefaultDark) if no match is found,
// which gracefully handles removed or retired themes.
static inline size_t resolveThemeIndex()
{
const uint32_t savedIdentifier = uiconfig.screen_rgb_color & 0x1F;
for (size_t i = 0; i < kInternalThemeCount; i++) {
if (kThemes[i].uniqueIdentifier == savedIdentifier)
return i;
}
return 0; // Default Dark fallback
}
static inline bool normalizeRegion(int16_t &x, int16_t &y, int16_t &width, int16_t &height)
{
if (width <= 0 || height <= 0) {
return false;
}
if (x < 0) {
width += x;
x = 0;
}
if (y < 0) {
height += y;
y = 0;
}
return width > 0 && height > 0;
}
static inline void appendColorRegion(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColorBe, uint16_t offColorBe)
{
// Keep the last slot permanently disabled as a sentinel for ST7789 scans.
// This leaves MAX_TFT_COLOR_REGIONS - 1 usable entries.
if (colorRegionCount >= MAX_TFT_COLOR_REGIONS - 1) {
memmove(&colorRegions[0], &colorRegions[1], sizeof(TFTColorRegion) * (MAX_TFT_COLOR_REGIONS - 2));
colorRegionCount = MAX_TFT_COLOR_REGIONS - 2;
}
TFTColorRegion &region = colorRegions[colorRegionCount++];
region.x = x;
region.y = y;
region.width = width;
region.height = height;
region.onColorBe = onColorBe;
region.offColorBe = offColorBe;
region.enabled = true;
// Keep one disabled sentinel after the active range for ST7789 countColorRegions().
if (colorRegionCount < MAX_TFT_COLOR_REGIONS) {
colorRegions[colorRegionCount].enabled = false;
}
colorRegions[MAX_TFT_COLOR_REGIONS - 1].enabled = false;
}
// Current working role colors (big-endian). Initialised to Dark defaults;
// call loadThemeDefaults() after boot / theme change to refresh.
static TFTRoleColorsBe roleColors[static_cast<size_t>(TFTColorRole::Count)] = {
{toBe565(kHeaderBackground), toBe565(TFTPalette::Black)}, // HeaderBackground
{toBe565(kHeaderBackground), toBe565(kTitleColor)}, // HeaderTitle
{toBe565(kHeaderBackground), toBe565(kStatusColor)}, // HeaderStatus
{toBe565(TFTPalette::Good), toBe565(TFTPalette::Black)}, // SignalBars
{toBe565(TFTPalette::Blue), toBe565(TFTPalette::Black)}, // ConnectionIcon
{toBe565(TFTPalette::Good), toBe565(TFTPalette::Black)}, // UtilizationFill
{toBe565(TFTPalette::Yellow), toBe565(TFTPalette::Black)}, // FavoriteNode
{toBe565(TFTPalette::DarkGray), toBe565(TFTPalette::Black)}, // ActionMenuBorder
{toBe565(TFTPalette::White), toBe565(TFTPalette::Black)}, // ActionMenuBody
{toBe565(TFTPalette::DarkGray), toBe565(TFTPalette::White)}, // ActionMenuTitle
{toBe565(TFTPalette::Black), toBe565(TFTPalette::White)}, // FrameMono
{toBe565(TFTPalette::White), toBe565(TFTPalette::Black)}, // BootSplash
{toBe565(TFTPalette::Yellow), toBe565(TFTPalette::Black)}, // FavoriteNodeBGHighlight
{toBe565(kStatusColor), toBe565(kHeaderBackground)}, // NavigationBar
{toBe565(kTitleColor), toBe565(TFTPalette::Black)} // NavigationArrow
};
} // namespace
// Theme accessors
const TFTThemeDef &getActiveTheme()
{
return kThemes[resolveThemeIndex()];
}
// Visible-theme accessors
// These iterate only themes flagged .visible = true, preserving kThemes[]
// order. Menu code should use these so hidden themes don't appear in the
// picker while still applying correctly if their ID is persisted.
size_t getVisibleThemeCount()
{
size_t count = 0;
for (size_t i = 0; i < kInternalThemeCount; i++) {
if (kThemes[i].visible)
count++;
}
return count;
}
const TFTThemeDef &getVisibleThemeByIndex(size_t visibleIndex)
{
size_t seen = 0;
for (size_t i = 0; i < kInternalThemeCount; i++) {
if (!kThemes[i].visible)
continue;
if (seen == visibleIndex)
return kThemes[i];
seen++;
}
// Fallback: return first theme (never trust a bad index).
return kThemes[0];
}
size_t getActiveVisibleThemeIndex()
{
const size_t active = resolveThemeIndex();
if (!kThemes[active].visible)
return SIZE_MAX;
size_t visibleIdx = 0;
for (size_t i = 0; i < active; i++) {
if (kThemes[i].visible)
visibleIdx++;
}
return visibleIdx;
}
uint16_t getThemeHeaderBg()
{
#if GRAPHICS_TFT_COLORING_ENABLED
#ifdef TFT_HEADER_BG_COLOR_OVERRIDE
return TFT_HEADER_BG_COLOR_OVERRIDE;
#else
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderBackground)].onColor;
#endif
#else
return TFTPalette::DarkGray;
#endif
}
uint16_t getThemeHeaderText()
{
#if GRAPHICS_TFT_COLORING_ENABLED
#ifdef TFT_HEADER_TITLE_COLOR_OVERRIDE
return TFT_HEADER_TITLE_COLOR_OVERRIDE;
#else
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderTitle)].offColor;
#endif
#else
return TFTPalette::White;
#endif
}
uint16_t getThemeHeaderStatus()
{
#if GRAPHICS_TFT_COLORING_ENABLED
#ifdef TFT_HEADER_STATUS_COLOR_OVERRIDE
return TFT_HEADER_STATUS_COLOR_OVERRIDE;
#else
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderStatus)].offColor;
#endif
#else
return TFTPalette::White;
#endif
}
uint16_t getThemeBodyBg()
{
#if GRAPHICS_TFT_COLORING_ENABLED
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::FrameMono)].onColor;
#else
return TFTPalette::Black;
#endif
}
uint16_t getThemeBodyFg()
{
#if GRAPHICS_TFT_COLORING_ENABLED
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::FrameMono)].offColor;
#else
return TFTPalette::White;
#endif
}
bool isThemeFullFrameInvert()
{
#if GRAPHICS_TFT_COLORING_ENABLED
return kThemes[resolveThemeIndex()].fullFrameInvert;
#else
return false;
#endif
}
uint16_t getThemeBatteryFillColor(int batteryPercent)
{
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
if (batteryPercent <= 20) {
return theme.batteryFillBad;
}
if (batteryPercent <= 50) {
return theme.batteryFillMedium;
}
return theme.batteryFillGood;
}
void loadThemeDefaults()
{
#if GRAPHICS_TFT_COLORING_ENABLED
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
for (uint8_t i = 0; i < static_cast<uint8_t>(TFTColorRole::Count); i++) {
roleColors[i].onColorBe = toBe565(theme.roles[i].onColor);
roleColors[i].offColorBe = toBe565(theme.roles[i].offColor);
}
#endif
}
// Role color assignment with theme-aware transforms
void setTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor)
{
#if !GRAPHICS_TFT_COLORING_ENABLED
return;
#endif
const uint8_t index = static_cast<uint8_t>(role);
if (index >= static_cast<uint8_t>(TFTColorRole::Count)) {
return;
}
const uint32_t themeId = uiconfig.screen_rgb_color & 0x1F;
const bool isHighlightRole = (role == TFTColorRole::FavoriteNode || role == TFTColorRole::FavoriteNodeBGHighlight);
const bool isBodyRole = !isHighlightRole && isBodyColorRole(role);
// Classic monochrome themes collapse all non-black accents into one tone.
if (isMonochromeTheme(themeId)) {
if (onColor != TFTPalette::Black) {
onColor = getMonochromeAccent(themeId);
}
} else {
switch (themeId) {
case ThemeID::DefaultLight:
if (isHighlightRole) {
// High-contrast highlight chips on light UI.
onColor = TFTPalette::Black;
offColor = TFTPalette::Yellow;
} else if (isBodyRole) {
// Invert body colors for readability on white frames.
if (offColor == TFTPalette::Black && role != TFTColorRole::ActionMenuTitle) {
offColor = TFTPalette::White;
}
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
}
break;
case ThemeID::Christmas:
if (isHighlightRole || isBodyRole) {
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::Gold);
replaceColor(offColor, TFTPalette::Black, TFTPalette::Pine);
}
break;
case ThemeID::Pink:
if (isHighlightRole) {
onColor = TFTPalette::Black;
offColor = TFTPalette::HotPink;
} else if (isBodyRole) {
replaceColor(offColor, TFTPalette::Black, TFTPalette::PalePink);
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::DeepPink);
}
break;
case ThemeID::Creamsicle:
if (isHighlightRole) {
onColor = TFTPalette::Black;
offColor = TFTPalette::CreamOrange;
} else if (isBodyRole) {
replaceColor(offColor, TFTPalette::Black, TFTPalette::Cream);
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::DeepOrange);
}
break;
case ThemeID::Blue:
if (isHighlightRole || isBodyRole) {
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::SkyBlue);
replaceColor(offColor, TFTPalette::Black, TFTPalette::Navy);
}
break;
default:
break;
}
}
roleColors[index].onColorBe = toBe565(onColor);
roleColors[index].offColorBe = toBe565(offColor);
}
// Region registration
void registerTFTColorRegion(TFTColorRole role, int16_t x, int16_t y, int16_t width, int16_t height)
{
#if !GRAPHICS_TFT_COLORING_ENABLED
return;
#endif
const uint8_t roleIndex = static_cast<uint8_t>(role);
if (roleIndex >= static_cast<uint8_t>(TFTColorRole::Count)) {
return;
}
if (!normalizeRegion(x, y, width, height)) {
return;
}
const TFTRoleColorsBe &colors = roleColors[roleIndex];
appendColorRegion(x, y, width, height, colors.onColorBe, colors.offColorBe);
}
void setAndRegisterTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor, int16_t x, int16_t y, int16_t width,
int16_t height)
{
#if !GRAPHICS_TFT_COLORING_ENABLED
(void)role;
(void)onColor;
(void)offColor;
(void)x;
(void)y;
(void)width;
(void)height;
return;
#else
setTFTColorRole(role, onColor, offColor);
registerTFTColorRegion(role, x, y, width, height);
#endif
}
void registerTFTColorRegionDirect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColor, uint16_t offColor)
{
#if !GRAPHICS_TFT_COLORING_ENABLED
return;
#endif
if (!normalizeRegion(x, y, width, height))
return;
appendColorRegion(x, y, width, height, toBe565(onColor), toBe565(offColor));
}
void registerTFTActionMenuRegions(int16_t boxLeft, int16_t boxTop, int16_t boxWidth, int16_t boxHeight)
{
#if !GRAPHICS_TFT_COLORING_ENABLED
(void)boxLeft;
(void)boxTop;
(void)boxWidth;
(void)boxHeight;
return;
#else
// Use theme-appropriate menu colors.
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
const TFTThemeRoleColor &menuBody = theme.roles[static_cast<size_t>(TFTColorRole::ActionMenuBody)];
const TFTThemeRoleColor &menuBorder = theme.roles[static_cast<size_t>(TFTColorRole::ActionMenuBorder)];
// Fill role includes a 1px shadow guard so stale frame edges are overwritten uniformly.
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuBody, menuBody.onColor, menuBody.offColor, boxLeft - 1, boxTop - 1,
boxWidth + 2, boxHeight + 2);
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft, boxTop - 2, boxWidth, 1);
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft - 2, boxTop, 1, boxHeight);
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuBorder, menuBorder.onColor, menuBorder.offColor, boxLeft, boxTop, boxWidth,
1);
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft, boxTop + boxHeight - 1, boxWidth, 1);
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft, boxTop, 1, boxHeight);
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft + boxWidth - 1, boxTop, 1, boxHeight);
#endif
}
// Frame signature & utilities
uint32_t getTFTColorFrameSignature()
{
#if !GRAPHICS_TFT_COLORING_ENABLED
return 0;
#else
uint32_t hash = kFnv1aOffsetBasis;
hash = fnv1aAppendByte(hash, colorRegionCount);
for (uint8_t i = 0; i < colorRegionCount; i++) {
const TFTColorRegion &r = colorRegions[i];
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.x));
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.y));
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.width));
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.height));
hash = fnv1aAppendU16(hash, r.onColorBe);
hash = fnv1aAppendU16(hash, r.offColorBe);
}
return hash;
#endif
}
uint8_t getTFTColorRegionCount()
{
#if !GRAPHICS_TFT_COLORING_ENABLED
return 0;
#else
return colorRegionCount;
#endif
}
void clearTFTColorRegions()
{
for (uint8_t i = 0; i < colorRegionCount; i++) {
colorRegions[i].enabled = false;
}
if (colorRegionCount < MAX_TFT_COLOR_REGIONS) {
colorRegions[colorRegionCount].enabled = false;
}
colorRegionCount = 0;
}
uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor)
{
for (int i = static_cast<int>(colorRegionCount) - 1; i >= 0; i--) {
const TFTColorRegion &r = colorRegions[i];
if (x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height) {
return isset ? r.onColorBe : r.offColorBe;
}
}
return isset ? defaultOnColor : defaultOffColor;
}
uint16_t resolveTFTOffColorAt(int16_t x, int16_t y, uint16_t defaultOffColor)
{
#if !GRAPHICS_TFT_COLORING_ENABLED
(void)x;
(void)y;
return defaultOffColor;
#else
const uint16_t defaultOffBe = toBe565(defaultOffColor);
const uint16_t sampledBe = resolveTFTColorPixel(x, y, false, defaultOffBe, defaultOffBe);
return static_cast<uint16_t>((sampledBe >> 8) | (sampledBe << 8));
#endif
}
} // namespace graphics
+163
View File
@@ -0,0 +1,163 @@
#pragma once
#include "configuration.h"
#include <stdint.h>
namespace graphics
{
struct TFTColorRegion {
int16_t x;
int16_t y;
int16_t width;
int16_t height;
uint16_t onColorBe;
uint16_t offColorBe;
// Required by ST7789 driver: it scans until the first disabled entry.
bool enabled = false;
};
static constexpr size_t MAX_TFT_COLOR_REGIONS = 48;
extern TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
enum class TFTColorRole : uint8_t {
HeaderBackground = 0,
HeaderTitle,
HeaderStatus,
SignalBars,
ConnectionIcon,
UtilizationFill,
FavoriteNode,
ActionMenuBorder,
ActionMenuBody,
ActionMenuTitle,
FrameMono,
BootSplash,
FavoriteNodeBGHighlight,
NavigationBar,
NavigationArrow,
Count
};
#if HAS_TFT || defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)
#define GRAPHICS_TFT_COLORING_ENABLED 1
#else
#define GRAPHICS_TFT_COLORING_ENABLED 0
#endif
static constexpr bool kTFTColoringEnabled = GRAPHICS_TFT_COLORING_ENABLED != 0;
constexpr bool isTFTColoringEnabled()
{
return kTFTColoringEnabled;
}
void setTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor);
void registerTFTColorRegion(TFTColorRole role, int16_t x, int16_t y, int16_t width, int16_t height);
// Convenience helper for the common "set role then register one region" flow.
void setAndRegisterTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor, int16_t x, int16_t y, int16_t width,
int16_t height);
// Register a region using explicit colors (no role lookup). Use when the
// color comes from a theme field rather than a role (e.g. battery fill).
void registerTFTColorRegionDirect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColor, uint16_t offColor);
void registerTFTActionMenuRegions(int16_t boxLeft, int16_t boxTop, int16_t boxWidth, int16_t boxHeight);
uint32_t getTFTColorFrameSignature();
uint8_t getTFTColorRegionCount();
void clearTFTColorRegions();
uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor);
// Resolve effective region-mapped OFF color at a coordinate in native-endian RGB565.
uint16_t resolveTFTOffColorAt(int16_t x, int16_t y, uint16_t defaultOffColor);
// -- Theme engine ------------------------------------------------------
// Each theme has four fields that work together:
//
// id - ThemeID:: constant, used for in-code references.
// name - human-readable label shown in the theme picker.
// uniqueIdentifier - the stable numeric value persisted to
// uiconfig.screen_rgb_color and restored at boot.
// This is a CONTRACT with saved configs on disk - once
// assigned, never reuse or renumber, even if the theme is
// deleted or the kThemes[] array is reordered.
// visible - controls whether a theme appears in the picker menu.
// Hidden themes can still be restored and applied if their
// uniqueIdentifier is persisted.
//
// Display order in the menu is controlled by kThemes[] array order among
// themes where visible == true, NOT by any numeric value above.
//
// To add a new theme:
// 1. Add a unique constant in ThemeID below (next unused value).
// 2. Add a kThemes[] entry at the desired menu position, with a unique
// uniqueIdentifier that has never been used by any prior theme.
// 3. Set visible=true if it should appear in the picker.
//
// To retire a theme without breaking saved configs:
// - Preferred: keep the entry and set visible=false so existing saved
// uniqueIdentifier values still resolve to the same theme.
// - If you remove the entry, resolveThemeIndex() falls back to DefaultDark
// when the persisted uniqueIdentifier no longer matches any theme.
// - Do NOT reuse a retired uniqueIdentifier for a future theme.
namespace ThemeID
{
constexpr uint32_t DefaultDark = 0;
constexpr uint32_t DefaultLight = 1;
constexpr uint32_t Christmas = 2;
constexpr uint32_t Pink = 3;
constexpr uint32_t Blue = 4;
constexpr uint32_t Creamsicle = 5;
constexpr uint32_t MeshtasticGreen = 6;
constexpr uint32_t ClassicRed = 7;
constexpr uint32_t MonochromeWhite = 8;
} // namespace ThemeID
// Per-role color pair stored in native (little-endian) RGB565 format.
struct TFTThemeRoleColor {
uint16_t onColor;
uint16_t offColor;
};
// Complete theme definition.
struct TFTThemeDef {
uint32_t id; // ThemeID constant - in-code identifier for this theme.
const char *name; // Human-readable label shown in the theme picker.
uint32_t uniqueIdentifier; // Stable persisted value copied into uiconfig.screen_rgb_color.
// Never reuse or renumber - see file-level notes above.
TFTThemeRoleColor roles[static_cast<size_t>(TFTColorRole::Count)];
uint16_t batteryFillGood;
uint16_t batteryFillMedium;
uint16_t batteryFillBad;
bool fullFrameInvert; // Apply full-frame FrameMono inversion (ST7789 light themes)
bool visible; // Show in the theme picker menu. Hidden themes still apply
// correctly if their uniqueIdentifier is persisted (dev/legacy themes).
};
// Count of themes whose .visible flag is true. Use this when building menus.
size_t getVisibleThemeCount();
// Access the Nth visible theme (0 .. getVisibleThemeCount()-1). Hidden themes
// are skipped, preserving kThemes[] order among the visible entries.
const TFTThemeDef &getVisibleThemeByIndex(size_t visibleIndex);
// Return the theme that matches uiconfig.screen_rgb_color (falls back to Dark).
const TFTThemeDef &getActiveTheme();
// Return the visible-theme index for the currently active theme, or SIZE_MAX
// if the active theme is hidden (so menus can show "no selection").
size_t getActiveVisibleThemeIndex();
// Convenience accessors - safe to call even when coloring is compiled out.
uint16_t getThemeHeaderBg();
uint16_t getThemeHeaderText();
uint16_t getThemeHeaderStatus();
uint16_t getThemeBodyBg();
uint16_t getThemeBodyFg();
bool isThemeFullFrameInvert();
uint16_t getThemeBatteryFillColor(int batteryPercent);
// Reinitialise default roleColors from the active theme. Call after a
// theme change so that any role registered without a prior setTFTColorRole()
// picks up theme-appropriate defaults.
void loadThemeDefaults();
} // namespace graphics
+157 -47
View File
@@ -16,12 +16,6 @@
extern SX1509 gpioExtender;
#endif
#ifdef TFT_MESH_OVERRIDE
uint16_t TFT_MESH = TFT_MESH_OVERRIDE;
#else
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
#endif
#if defined(ST7735S)
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
@@ -428,7 +422,7 @@ static LGFX *tft = nullptr;
#elif defined(ST7789_CS)
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
#ifdef HELTEC_V4_TFT
#if defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)
#include "chsc6x.h"
#include "lgfx/v1/Touch.hpp"
namespace lgfx
@@ -450,7 +444,11 @@ class TOUCH_CHSC6X : public ITouch
bool init(void) override
{
if (chsc6xTouch == nullptr) {
#if (TOUCH_I2C_PORT == 1)
chsc6xTouch = new chsc6x(&Wire1, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN);
#else
chsc6xTouch = new chsc6x(&Wire, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN);
#endif
}
chsc6xTouch->chsc6x_init();
return true;
@@ -487,7 +485,7 @@ class LGFX : public lgfx::LGFX_Device
#if HAS_TOUCHSCREEN
#if defined(T_WATCH_S3) || defined(ELECROW)
lgfx::Touch_FT5x06 _touch_instance;
#elif defined(HELTEC_V4_TFT)
#elif defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)
lgfx::TOUCH_CHSC6X _touch_instance;
#else
lgfx::Touch_GT911 _touch_instance;
@@ -506,7 +504,11 @@ class LGFX : public lgfx::LGFX_Device
cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing
// 80MHz by an integer)
cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving
cfg.spi_3wire = false;
#ifdef SPI_3_WIRE
cfg.spi_3wire = SPI_3_WIRE;
#else
cfg.spi_3wire = true; // Set to true if reception is done on the MOSI pin
#endif
cfg.use_lock = true; // Set to true to use transaction locking
cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch /
// SPI_DMA_CH_AUTO=auto setting)
@@ -556,8 +558,11 @@ class LGFX : public lgfx::LGFX_Device
cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped
cfg.dlen_16bit =
false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI
#if defined(HAS_SDCARD)
cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.)
#else
cfg.bus_shared = false;
#endif
// Set the following only when the display is shifted with a driver with a variable number of pixels, such as the
// ST7735 or ILI9163.
// cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
@@ -1140,7 +1145,9 @@ static LGFX *tft = nullptr;
#endif
#include "SPILock.h"
#include "TFTColorRegions.h"
#include "TFTDisplay.h"
#include "TFTPalette.h"
#include <SPI.h>
#ifdef UNPHONE
@@ -1150,6 +1157,25 @@ extern unPhone unphone;
GpioPin *TFTDisplay::backlightEnable = NULL;
namespace
{
static constexpr uint8_t kFullRepaintChunkRows = 8;
static inline uint16_t getThemeDefaultOnColor()
{
return graphics::TFTPalette::White;
}
static inline uint16_t getThemeDefaultOffColor()
{
#if GRAPHICS_TFT_COLORING_ENABLED
return graphics::getThemeBodyBg();
#else
return TFT_BLACK;
#endif
}
} // namespace
TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus)
{
LOG_DEBUG("TFTDisplay!");
@@ -1189,14 +1215,15 @@ TFTDisplay::~TFTDisplay()
free(linePixelBuffer);
linePixelBuffer = nullptr;
}
if (repaintChunkBuffer != nullptr) {
free(repaintChunkBuffer);
repaintChunkBuffer = nullptr;
}
}
// Write the buffer to the display memory
void TFTDisplay::display(bool fromBlank)
{
if (fromBlank)
tft->fillScreen(TFT_BLACK);
concurrency::LockGuard g(spiLock);
uint32_t x, y;
@@ -1205,12 +1232,70 @@ void TFTDisplay::display(bool fromBlank)
uint32_t x_FirstPixelUpdate;
uint32_t x_LastPixelUpdate;
bool isset, dblbuf_isset;
uint16_t colorTftMesh, colorTftBlack;
uint16_t colorTftWhite, colorTftBlack;
bool somethingChanged = false;
// Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step
colorTftMesh = __builtin_bswap16(TFT_MESH);
colorTftBlack = __builtin_bswap16(TFT_BLACK);
// Theme defaults for non-role pixels.
const uint16_t defaultOnColor = getThemeDefaultOnColor();
const uint16_t defaultOffColor = getThemeDefaultOffColor();
static uint16_t lastDefaultOnColor = 0;
static uint16_t lastDefaultOffColor = 0;
static bool haveLastDefaults = false;
const bool themeDefaultsChanged =
!haveLastDefaults || (defaultOnColor != lastDefaultOnColor) || (defaultOffColor != lastDefaultOffColor);
const bool forceFullRepaint = fromBlank || themeDefaultsChanged;
// If theme defaults changed, reset panel background immediately so stale pixels don't linger.
if (forceFullRepaint) {
tft->fillScreen(defaultOffColor);
}
colorTftWhite = (defaultOnColor >> 8) | ((defaultOnColor & 0xFF) << 8);
colorTftBlack = (defaultOffColor >> 8) | ((defaultOffColor & 0xFF) << 8);
#if GRAPHICS_TFT_COLORING_ENABLED
static uint32_t lastColorFrameSignature = 0;
const bool hasColorRegions = graphics::getTFTColorRegionCount() > 0;
const uint32_t colorFrameSignature = graphics::getTFTColorFrameSignature();
const bool forceFullColorRepaint = forceFullRepaint || (colorFrameSignature != lastColorFrameSignature);
// When region roles/layout changed, color can differ even with identical monochrome glyph bits.
// Repaint full frame only for those frames, then return to diff-based updates.
if (forceFullColorRepaint) {
for (uint32_t yStart = 0; yStart < displayHeight; yStart += kFullRepaintChunkRows) {
const uint32_t rowsThisChunk = min<uint32_t>(kFullRepaintChunkRows, displayHeight - yStart);
for (uint32_t row = 0; row < rowsThisChunk; row++) {
y = yStart + row;
y_byteIndex = (y / 8) * displayWidth;
y_byteMask = (1 << (y & 7));
uint16_t *chunkRow = repaintChunkBuffer + (row * displayWidth);
for (x = 0; x < displayWidth; x++) {
isset = (buffer[x + y_byteIndex] & y_byteMask) != 0;
if (hasColorRegions) {
chunkRow[x] = graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y), isset,
colorTftWhite, colorTftBlack);
} else {
chunkRow[x] = isset ? colorTftWhite : colorTftBlack;
}
}
}
#if defined(HACKADAY_COMMUNICATOR)
tft->draw16bitBeRGBBitmap(0, yStart, repaintChunkBuffer, displayWidth, rowsThisChunk);
#else
tft->pushImage(0, yStart, displayWidth, rowsThisChunk, repaintChunkBuffer);
#endif
}
memcpy(buffer_back, buffer, displayBufferSize);
lastColorFrameSignature = colorFrameSignature;
haveLastDefaults = true;
lastDefaultOnColor = defaultOnColor;
lastDefaultOffColor = defaultOffColor;
graphics::clearTFTColorRegions();
return;
}
#endif
y = 0;
while (y < displayHeight) {
@@ -1219,7 +1304,7 @@ void TFTDisplay::display(bool fromBlank)
// Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas.
if (y_byteMask == 1) {
if (!fromBlank) {
if (!forceFullRepaint) {
for (x = 0; x < displayWidth; x++) {
if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex])
break;
@@ -1237,13 +1322,14 @@ void TFTDisplay::display(bool fromBlank)
}
}
// Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating
for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) {
isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
// Step 2: Scan this row for changed span (first and last changed pixel).
uint32_t x_FirstChanged = 0;
for (x_FirstChanged = 0; x_FirstChanged < displayWidth; x_FirstChanged++) {
isset = buffer[x_FirstChanged + y_byteIndex] & y_byteMask;
if (!fromBlank) {
if (!forceFullRepaint) {
// get src pixel in the page based ordering the OLED lib uses
dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
dblbuf_isset = buffer_back[x_FirstChanged + y_byteIndex] & y_byteMask;
if (isset != dblbuf_isset) {
break;
}
@@ -1253,43 +1339,51 @@ void TFTDisplay::display(bool fromBlank)
}
// Did we find a pixel that needs updating on this row?
if (x_FirstPixelUpdate < displayWidth) {
// Align the first pixel for update to an even number so the total alignment of
// the data will be at 32-bit boundary, which is required by GDMA SPI transfers.
x_FirstPixelUpdate &= ~1;
// Step 3a: copy rest of the pixels in this row into the pixel line buffer,
// while also recording the last pixel in the row that needs updating.
// Since the first changed pixel will be looked up, the x_LastPixelUpdate will be set.
for (x = x_FirstPixelUpdate; x < displayWidth; x++) {
isset = buffer[x + y_byteIndex] & y_byteMask;
linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack;
if (!fromBlank) {
dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask;
if (x_FirstChanged < displayWidth) {
uint32_t x_LastChanged = displayWidth - 1;
while (x_LastChanged > x_FirstChanged) {
isset = buffer[x_LastChanged + y_byteIndex] & y_byteMask;
if (!forceFullRepaint) {
dblbuf_isset = buffer_back[x_LastChanged + y_byteIndex] & y_byteMask;
if (isset != dblbuf_isset) {
x_LastPixelUpdate = x;
break;
}
} else if (isset) {
x_LastPixelUpdate = x;
break;
}
x_LastChanged--;
}
// Step 3b: Round up the last pixel to odd number to maintain 32-bit alignment for SPIs.
// Most displays will have even number of pixels in a row -- this will be in bounds
// of the displayWidth. (Hopefully odd displays will just ignore that extra pixel.)
x_LastPixelUpdate |= 1;
// Ensure the last pixel index does not exceed the display width.
// Align the first pixel for update to an even number so the total alignment of
// the data will be at 32-bit boundary, which is required by GDMA SPI transfers.
x_FirstPixelUpdate = x_FirstChanged & ~1U;
x_LastPixelUpdate = x_LastChanged | 1U;
if (x_LastPixelUpdate >= displayWidth) {
x_LastPixelUpdate = displayWidth - 1;
}
// Step 3: Copy only the changed span into the pixel line buffer.
for (x = x_FirstPixelUpdate; x <= x_LastPixelUpdate; x++) {
isset = buffer[x + y_byteIndex] & y_byteMask;
#if GRAPHICS_TFT_COLORING_ENABLED
if (hasColorRegions) {
linePixelBuffer[x] = graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y), isset,
colorTftWhite, colorTftBlack);
} else {
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
}
#else
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
#endif
}
#if defined(HACKADAY_COMMUNICATOR)
tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
(x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1);
#else
// Step 4: Send the changed pixels on this line to the screen as a single block transfer.
// This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
&linePixelBuffer[x_FirstPixelUpdate]);
tft->pushImage(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
&linePixelBuffer[x_FirstPixelUpdate]);
#endif
somethingChanged = true;
}
@@ -1298,6 +1392,14 @@ void TFTDisplay::display(bool fromBlank)
// Copy the Buffer to the Back Buffer
if (somethingChanged)
memcpy(buffer_back, buffer, displayBufferSize);
#if GRAPHICS_TFT_COLORING_ENABLED
lastColorFrameSignature = colorFrameSignature;
#endif
haveLastDefaults = true;
lastDefaultOnColor = defaultOnColor;
lastDefaultOffColor = defaultOffColor;
graphics::clearTFTColorRegions();
}
void TFTDisplay::sdlLoop()
@@ -1511,7 +1613,7 @@ bool TFTDisplay::connect()
#else
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
#endif
tft->fillScreen(TFT_BLACK);
tft->fillScreen(getThemeDefaultOffColor());
if (this->linePixelBuffer == NULL) {
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
@@ -1521,6 +1623,14 @@ bool TFTDisplay::connect()
return false;
}
}
if (this->repaintChunkBuffer == NULL) {
this->repaintChunkBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth * kFullRepaintChunkRows);
if (!this->repaintChunkBuffer) {
LOG_ERROR("Not enough memory to create TFT repaint chunk buffer\n");
return false;
}
}
return true;
}
+2 -1
View File
@@ -63,4 +63,5 @@ class TFTDisplay : public OLEDDisplay
virtual bool connect() override;
uint16_t *linePixelBuffer = nullptr;
};
uint16_t *repaintChunkBuffer = nullptr;
};
+70
View File
@@ -0,0 +1,70 @@
#pragma once
#include <stdint.h>
namespace graphics
{
namespace TFTPalette
{
constexpr uint16_t rgb565(uint8_t red, uint8_t green, uint8_t blue)
{
return static_cast<uint16_t>(((red & 0xF8) << 8) | ((green & 0xFC) << 3) | ((blue & 0xF8) >> 3));
}
constexpr uint16_t Black = 0x0000;
constexpr uint16_t White = 0xFFFF;
constexpr uint16_t DarkGray = 0x4208;
constexpr uint16_t Gray = 0x8410;
constexpr uint16_t LightGray = 0xC618;
constexpr uint16_t Red = rgb565(255, 0, 0);
constexpr uint16_t Green = rgb565(0, 255, 0);
constexpr uint16_t Blue = rgb565(0, 130, 252);
constexpr uint16_t Yellow = rgb565(255, 255, 0);
constexpr uint16_t Orange = rgb565(255, 165, 0);
constexpr uint16_t Cyan = rgb565(0, 255, 255);
constexpr uint16_t Magenta = rgb565(255, 0, 255);
constexpr uint16_t Good = Green;
constexpr uint16_t Medium = Yellow;
constexpr uint16_t Bad = Red;
// Christmas / seasonal accent colors
constexpr uint16_t ChristmasRed = rgb565(178, 34, 34);
constexpr uint16_t ChristmasGreen = rgb565(0, 128, 0);
constexpr uint16_t Gold = rgb565(255, 215, 0);
constexpr uint16_t Pine = rgb565(15, 35, 10);
// Pink theme colors (light variant)
constexpr uint16_t HotPink = rgb565(255, 105, 180);
constexpr uint16_t PalePink = rgb565(255, 228, 235);
constexpr uint16_t DeepPink = rgb565(200, 50, 120);
// Blue theme colors (dark variant)
constexpr uint16_t SkyBlue = rgb565(100, 180, 255);
constexpr uint16_t Navy = rgb565(15, 15, 50);
constexpr uint16_t DeepBlue = rgb565(30, 60, 120);
// Creamsicle theme colors (light variant)
constexpr uint16_t CreamOrange = rgb565(255, 140, 50);
constexpr uint16_t DeepOrange = rgb565(220, 100, 20);
constexpr uint16_t Cream = rgb565(255, 248, 235);
// Classic monochrome theme accent colors (single-color-on-black themes)
constexpr uint16_t MeshtasticGreen = rgb565(0x67, 0xEA, 0x94);
constexpr uint16_t ClassicRed = rgb565(255, 64, 64);
// Monochrome White reuses TFTPalette::White above.
// Fast contrast picker for monochrome glyph overlays on arbitrary RGB565 backgrounds.
// Uses channel-sum brightness approximation to keep code size small.
constexpr uint16_t pickReadableMonoFg(uint16_t backgroundColor)
{
const uint16_t r = (backgroundColor >> 11) & 0x1F;
const uint16_t g = (backgroundColor >> 5) & 0x3F;
const uint16_t b = backgroundColor & 0x1F;
return ((r + g + b) >= 70) ? DarkGray : White;
}
} // namespace TFTPalette
} // namespace graphics
+7 -3
View File
@@ -145,7 +145,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
// === Set Title, Blank for Clock
const char *titleStr = "";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
graphics::drawCommonHeader(display, x, y, titleStr, true, true, true);
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
char timeString[16];
@@ -293,11 +293,15 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
// Draw an analog clock
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
#if GRAPHICS_TFT_COLORING_ENABLED
// Clear previous frame pixels so moving hands don't leave stale artifacts on TFT light theme.
display->clear();
#endif
display->setTextAlignment(TEXT_ALIGN_LEFT);
// === Set Title, Blank for Clock
const char *titleStr = "";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
graphics::drawCommonHeader(display, x, y, titleStr, true, true, true);
// clock face center coordinates
int16_t centerX = display->getWidth() / 2;
@@ -478,4 +482,4 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
} // namespace ClockRenderer
} // namespace graphics
#endif
#endif
+34 -86
View File
@@ -9,113 +9,61 @@ namespace graphics
{
namespace CompassRenderer
{
// Point helper class for compass calculations
struct Point {
float x, y;
Point(float x, float y) : x(x), y(y) {}
void rotate(float angle)
{
float cos_a = cosf(angle);
float sin_a = sinf(angle);
float new_x = x * cos_a - y * sin_a;
float new_y = x * sin_a + y * cos_a;
x = new_x;
y = new_y;
}
void scale(float factor)
{
x *= factor;
y *= factor;
}
void translate(float dx, float dy)
{
x += dx;
y += dy;
}
};
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius)
{
// Show the compass heading (not implemented in original)
// This could draw a "N" indicator or north arrow
// For now, we'll draw a simple north indicator
// const float radius = 17.0f;
if (currentResolution == ScreenResolution::High) {
radius += 4;
}
float northX = 0.0f;
float northY = -radius;
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) {
const float c = cosf(-myHeading);
const float s = sinf(-myHeading);
const float rx = northX * c - northY * s;
const float ry = northX * s + northY * c;
northX = rx;
northY = ry;
}
northX += compassX;
northY += compassY;
const float northAngle = (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) ? -myHeading : 0.0f;
const int16_t nX = compassX + static_cast<int16_t>((radius - 1) * sinf(northAngle));
const int16_t nY = compassY - static_cast<int16_t>((radius - 1) * cosf(northAngle));
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_CENTER);
#if !GRAPHICS_TFT_COLORING_ENABLED
display->setColor(BLACK);
const int16_t nLabelWidth = display->getStringWidth("N");
if (currentResolution == ScreenResolution::High) {
display->fillRect(northX - 8, northY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6);
display->fillRect(nX - 8, nY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6);
} else {
display->fillRect(northX - 4, northY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6);
display->fillRect(nX - 4, nY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6);
}
display->setColor(WHITE);
display->drawString(northX, northY - 3, "N");
}
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
{
Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially
float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f;
Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY);
Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow};
for (int i = 0; i < 4; i++) {
arrowPoints[i]->rotate(headingRadian);
arrowPoints[i]->scale(compassDiam * 0.6);
arrowPoints[i]->translate(compassX, compassY);
}
#ifdef USE_EINK
display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
#else
display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
#endif
display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y);
display->setColor(WHITE);
display->drawString(nX, nY - 3, "N");
}
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing)
{
float radians = bearing * DEG_TO_RAD;
const float radians = bearing * DEG_TO_RAD;
const float sinA = sinf(radians);
const float cosA = cosf(radians);
const float tipHalf = size * 0.5f;
const float lx = -(size / 6.0f);
const float ly = size / 4.0f;
const float rx = (size / 6.0f);
const float ry = size / 4.0f;
const float tx = 0.0f;
const float ty = size / 4.5f;
Point tip(0, -size / 2);
Point left(-size / 6, size / 4);
Point right(size / 6, size / 4);
Point tail(0, size / 4.5);
const int16_t tipX = static_cast<int16_t>(x + (tipHalf * sinA));
const int16_t tipY = static_cast<int16_t>(y - (tipHalf * cosA));
const int16_t leftX = static_cast<int16_t>(x + (lx * cosA) - (ly * sinA));
const int16_t leftY = static_cast<int16_t>(y + (lx * sinA) + (ly * cosA));
const int16_t rightX = static_cast<int16_t>(x + (rx * cosA) - (ry * sinA));
const int16_t rightY = static_cast<int16_t>(y + (rx * sinA) + (ry * cosA));
const int16_t tailX = static_cast<int16_t>(x + (tx * cosA) - (ty * sinA));
const int16_t tailY = static_cast<int16_t>(y + (tx * sinA) + (ty * cosA));
tip.rotate(radians);
left.rotate(radians);
right.rotate(radians);
tail.rotate(radians);
display->fillTriangle(tipX, tipY, leftX, leftY, tailX, tailY);
display->fillTriangle(tipX, tipY, rightX, rightY, tailX, tailY);
}
tip.translate(x, y);
left.translate(x, y);
right.translate(x, y);
tail.translate(x, y);
display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y);
display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
{
const int16_t size = static_cast<int16_t>(compassDiam * 0.6f);
drawArrowToNode(display, compassX, compassY, size, headingRadian * RAD_TO_DEG);
}
bool getHeadingRadians(double lat, double lon, float &headingRadian)
+1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "graphics/Screen.h"
#include "mesh/generated/meshtastic/mesh.pb.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
+30 -5
View File
@@ -11,6 +11,8 @@
#include "gps/RTC.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TFTColorRegions.h"
#include "graphics/TFTPalette.h"
#include "graphics/TimeFormatters.h"
#include "graphics/images.h"
#include "main.h"
@@ -469,9 +471,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int chUtil_y = getTextPositions(display)[line] + 3;
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50;
int chutil_bar_max_fill = chutil_bar_width - 2; // Account for border
int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7;
int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3;
int chutil_percent = airTime->channelUtilizationPercent();
const int raw_chutil_percent = chutil_percent;
int centerofscreen = SCREEN_WIDTH / 2;
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
@@ -479,7 +483,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
display->drawString(starting_position, getTextPositions(display)[line], chUtil);
// Force 56% or higher to show a full 100% bar, text would still show related percent.
// Force 61% or higher to show a full 100% bar, text would still show related percent.
if (chutil_percent >= 61) {
chutil_percent = 100;
}
@@ -492,9 +496,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
float weight3 = 0.20; // Weight for 40–100%
float totalWeight = weight1 + weight2 + weight3;
int seg1 = chutil_bar_width * (weight1 / totalWeight);
int seg2 = chutil_bar_width * (weight2 / totalWeight);
int seg3 = chutil_bar_width * (weight3 / totalWeight);
int seg1 = chutil_bar_max_fill * (weight1 / totalWeight);
int seg2 = chutil_bar_max_fill * (weight2 / totalWeight);
int seg3 = chutil_bar_max_fill - seg1 - seg2; // Remainder absorbs rounding errors
int fillRight = 0;
@@ -511,7 +515,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
// Fill progress
if (fillRight > 0) {
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
#if GRAPHICS_TFT_COLORING_ENABLED
uint16_t UtilizationFillColor = TFTPalette::Good;
if (raw_chutil_percent >= 60) {
UtilizationFillColor = TFTPalette::Bad;
} else if (raw_chutil_percent >= 35) {
UtilizationFillColor = TFTPalette::Medium;
}
setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black,
starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
#endif
display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
}
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++],
@@ -584,6 +598,17 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
display->setColor(WHITE);
display->drawRect(barX, barY, adjustedBarWidth, barHeight);
#if GRAPHICS_TFT_COLORING_ENABLED
uint16_t UtilizationFillColor = TFTPalette::Good;
if (percent >= 80) {
UtilizationFillColor = TFTPalette::Bad;
} else if (percent >= 60) {
UtilizationFillColor = TFTPalette::Medium;
}
setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black, barX + 1, barY + 1,
fillWidth - 1, barHeight - 2);
#endif
display->fillRect(barX, barY, fillWidth, barHeight);
display->setColor(WHITE);
#endif
+63 -122
View File
@@ -11,6 +11,7 @@
#include "buzz.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TFTColorRegions.h"
#include "graphics/draw/MessageRenderer.h"
#include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h"
@@ -30,8 +31,6 @@
#include <functional>
#include <utility>
extern uint16_t TFT_MESH;
namespace graphics
{
@@ -2028,109 +2027,6 @@ void menuHandler::switchToMUIMenu()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
{
static const ScreenColorOption colorOptions[] = {
{"Back", OptionsAction::Back},
{"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)},
{"Meshtastic Green", OptionsAction::Select, ScreenColor(0x67, 0xEA, 0x94)},
{"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)},
{"Red", OptionsAction::Select, ScreenColor(255, 64, 64)},
{"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)},
{"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)},
{"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)},
{"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)},
{"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)},
{"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)},
{"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)},
{"White", OptionsAction::Select, ScreenColor(255, 255, 255)},
{"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)},
};
constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]);
static std::array<const char *, colorCount> colorLabels{};
auto bannerOptions = createStaticBannerOptions(
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuQueue = SystemBaseMenu;
screen->runNow();
return;
}
if (!option.hasValue) {
return;
}
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
HAS_TFT || defined(HACKADAY_COMMUNICATOR)
const ScreenColor &color = option.value;
if (color.useVariant) {
LOG_INFO("Setting color to system default or defined variant");
} else {
LOG_INFO("Setting color to %s", option.label);
}
uint8_t r = color.r;
uint8_t g = color.g;
uint8_t b = color.b;
display->setColor(BLACK);
display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
display->setColor(WHITE);
if (color.useVariant || (r == 0 && g == 0 && b == 0)) {
#ifdef TFT_MESH_OVERRIDE
TFT_MESH = TFT_MESH_OVERRIDE;
#else
TFT_MESH = COLOR565(255, 255, 128);
#endif
} else {
TFT_MESH = COLOR565(r, g, b);
}
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
static_cast<ST7789Spi *>(screen->getDisplayDevice())->setRGB(TFT_MESH);
#endif
screen->setFrames(graphics::Screen::FOCUS_SYSTEM);
if (color.useVariant || (r == 0 && g == 0 && b == 0)) {
uiconfig.screen_rgb_color = 0;
} else {
uiconfig.screen_rgb_color =
(static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
}
LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color);
saveUIConfig();
#endif
});
int initialSelection = 0;
if (uiconfig.screen_rgb_color == 0) {
initialSelection = 1;
} else {
uint32_t currentColor = uiconfig.screen_rgb_color;
for (size_t i = 0; i < colorCount; ++i) {
if (!colorOptions[i].hasValue) {
continue;
}
const ScreenColor &color = colorOptions[i].value;
if (color.useVariant) {
continue;
}
uint32_t encoded =
(static_cast<uint32_t>(color.r) << 16) | (static_cast<uint32_t>(color.g) << 8) | static_cast<uint32_t>(color.b);
if (encoded == currentColor) {
initialSelection = static_cast<int>(i);
break;
}
}
}
bannerOptions.InitialSelected = initialSelection;
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::rebootMenu()
{
static const char *optionsArray[] = {"Back", "Confirm"};
@@ -2318,9 +2214,9 @@ void menuHandler::screenOptionsMenu()
bool hasSupportBrightness = false;
#endif
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
static const char *optionsArray[6] = {"Back"};
static int optionsEnumArray[6] = {Back};
enum optionsNumbers { Back, Brightness, FrameToggles, DisplayUnits, MessageBubbles, Theme };
static const char *optionsArray[7] = {"Back"};
static int optionsEnumArray[7] = {Back};
int options = 1;
// Only show brightness for B&W displays
@@ -2329,13 +2225,6 @@ void menuHandler::screenOptionsMenu()
optionsEnumArray[options++] = Brightness;
}
// Only show screen color for TFT displays
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
HAS_TFT || defined(HACKADAY_COMMUNICATOR)
optionsArray[options] = "Screen Color";
optionsEnumArray[options++] = ScreenColor;
#endif
optionsArray[options] = "Frame Visibility";
optionsEnumArray[options++] = FrameToggles;
@@ -2345,6 +2234,11 @@ void menuHandler::screenOptionsMenu()
optionsArray[options] = "Message Bubbles";
optionsEnumArray[options++] = MessageBubbles;
#if GRAPHICS_TFT_COLORING_ENABLED
optionsArray[options] = "Theme";
optionsEnumArray[options++] = Theme;
#endif
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Display Options";
bannerOptions.optionsArrayPtr = optionsArray;
@@ -2354,9 +2248,6 @@ void menuHandler::screenOptionsMenu()
if (selected == Brightness) {
menuHandler::menuQueue = menuHandler::BrightnessPicker;
screen->runNow();
} else if (selected == ScreenColor) {
menuHandler::menuQueue = menuHandler::TftColorMenuPicker;
screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
@@ -2366,6 +2257,9 @@ void menuHandler::screenOptionsMenu()
} else if (selected == MessageBubbles) {
menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
screen->runNow();
} else if (selected == Theme) {
menuHandler::menuQueue = menuHandler::ThemeMenu;
screen->runNow();
} else {
menuQueue = SystemBaseMenu;
screen->runNow();
@@ -2649,6 +2543,53 @@ void menuHandler::messageBubblesMenu()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::themeMenu()
{
// Build menu dynamically from the theme table.
// Only visible themes appear!
// Slot budget: 1 for "Back" + up to kMaxThemesInMenu visible themes.
// Bump kMaxThemesInMenu if you add more themes than will fit here.
constexpr size_t kMaxThemesInMenu = 15;
const size_t visibleCount = getVisibleThemeCount();
static const char *optionsArray[kMaxThemesInMenu + 1] = {"Back"};
const size_t shownCount = (visibleCount < kMaxThemesInMenu) ? visibleCount : kMaxThemesInMenu;
const int options = static_cast<int>(shownCount) + 1; // +1 for Back
for (size_t i = 0; i < shownCount; i++) {
optionsArray[i + 1] = getVisibleThemeByIndex(i).name;
}
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Theme";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
// Highlight the currently active theme (visible index + 1 for the Back
// offset). If the active theme is hidden, leave selection on "Back".
const size_t activeVisible = getActiveVisibleThemeIndex();
bannerOptions.InitialSelected = (activeVisible == SIZE_MAX) ? 0 : static_cast<int>(activeVisible) + 1;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 0) {
// Back
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
screen->runNow();
} else {
// Selection is an index into the VISIBLE themes (1-based, slot 0 is Back).
const size_t visibleIdx = static_cast<size_t>(selected - 1);
if (visibleIdx < getVisibleThemeCount()) {
// Persist the theme's uniqueIdentifier so boot-time
// resolveThemeIndex() can restore this theme on next startup.
uiconfig.screen_rgb_color = COLOR565(255, 255, (getVisibleThemeByIndex(visibleIdx).uniqueIdentifier & 0x1F) << 3);
loadThemeDefaults();
saveUIConfig();
screen->runNow();
}
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
{
if (menuQueue != MenuNone)
@@ -2724,9 +2665,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case MuiPicker:
switchToMUIMenu();
break;
case TftColorMenuPicker:
TFTColorPickerMenu(display);
break;
case BrightnessPicker:
BrightnessPickerMenu();
break;
@@ -2799,6 +2737,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case MessageBubblesMenu:
messageBubblesMenu();
break;
case ThemeMenu:
themeMenu();
break;
}
menuQueue = MenuNone;
}
@@ -2810,4 +2751,4 @@ void menuHandler::saveUIConfig()
} // namespace graphics
#endif
#endif
+3 -16
View File
@@ -30,7 +30,6 @@ class menuHandler
ResetNodeDbMenu,
BuzzerModeMenuPicker,
MuiPicker,
TftColorMenuPicker,
BrightnessPicker,
RebootMenu,
ShutdownMenu,
@@ -55,7 +54,8 @@ class menuHandler
NodeNameLengthMenu,
FrameToggles,
DisplayUnits,
MessageBubblesMenu
MessageBubblesMenu,
ThemeMenu
};
static screenMenus menuQueue;
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
@@ -89,7 +89,6 @@ class menuHandler
static void GPSPositionBroadcastMenu();
static void BuzzerModeMenu();
static void switchToMUIMenu();
static void TFTColorPickerMenu(OLEDDisplay *display);
static void nodeListMenu();
static void resetNodeDBMenu();
static void BrightnessPickerMenu();
@@ -110,6 +109,7 @@ class menuHandler
static void frameTogglesMenu();
static void displayUnitsMenu();
static void messageBubblesMenu();
static void themeMenu();
static void textMessageMenu();
private:
@@ -136,23 +136,10 @@ template <typename T> struct MenuOption {
MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
};
struct ScreenColor {
uint8_t r;
uint8_t g;
uint8_t b;
bool useVariant;
explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
{
}
};
using RadioPresetOption = MenuOption<meshtastic_Config_LoRaConfig_ModemPreset>;
using LoraRegionOption = MenuOption<meshtastic_Config_LoRaConfig_RegionCode>;
using TimezoneOption = MenuOption<const char *>;
using CompassOption = MenuOption<meshtastic_CompassMode>;
using ScreenColorOption = MenuOption<ScreenColor>;
using GPSToggleOption = MenuOption<meshtastic_Config_PositionConfig_GpsMode>;
using GPSFormatOption = MenuOption<meshtastic_DeviceUIConfig_GpsCoordinateFormat>;
using NodeNameOption = MenuOption<bool>;
+132 -12
View File
@@ -11,6 +11,8 @@
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TFTColorRegions.h"
#include "graphics/TFTPalette.h"
#include "graphics/TimeFormatters.h"
#include "graphics/emotes.h"
#include "main.h"
@@ -254,6 +256,76 @@ struct MessageBlock {
bool mine;
};
#if GRAPHICS_TFT_COLORING_ENABLED
static void setDarkModeBubbleRoleColors(uint32_t themeId, bool mine)
{
uint16_t bubbleOnColor;
uint16_t bubbleOffColor;
if (themeId == ThemeID::Blue) {
bubbleOnColor = mine ? TFTPalette::Navy : TFTPalette::White;
bubbleOffColor = mine ? TFTPalette::SkyBlue : TFTPalette::DeepBlue;
} else {
bubbleOnColor = mine ? TFTPalette::Black : getThemeBodyFg();
bubbleOffColor = mine ? TFTPalette::SkyBlue : TFTPalette::DarkGray;
}
setTFTColorRole(TFTColorRole::ActionMenuBody, bubbleOnColor, bubbleOffColor);
}
static void registerRoundedBubbleFillRegion(int x, int y, int w, int h, int radius)
{
if (w <= 0 || h <= 0) {
return;
}
if (radius <= 0 || w < 3 || h < 3) {
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, y, w, h);
return;
}
// Keep region count low so we don't churn MAX_TFT_COLOR_REGIONS while
// scrolling long message lists (which can flatten older bubble corners).
int capRows = 0;
if (radius >= 4 && h >= 5) {
capRows = 2; // 5 regions total (2 top caps + middle + 2 bottom caps)
} else if (radius >= 2 && h >= 3) {
capRows = 1; // 3 regions total
}
if (capRows <= 0) {
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, y, w, h);
return;
}
for (int row = 0; row < capRows; ++row) {
int inset = 0;
if (radius >= 4) {
inset = (row == 0) ? 2 : 1;
} else if (radius >= 2) {
inset = 1;
}
const int stripW = w - (inset * 2);
if (stripW <= 0) {
continue;
}
const int topY = y + row;
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x + inset, topY, stripW, 1);
const int bottomY = y + h - 1 - row;
if (bottomY != topY) {
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x + inset, bottomY, stripW, 1);
}
}
const int middleY = y + capRows;
const int middleH = h - (capRows * 2);
if (middleH > 0) {
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, middleY, w, middleH);
}
}
#endif
static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine)
{
if (isHeaderLine) {
@@ -648,6 +720,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const int contentBottom = scrollBottom; // already excludes nav line
const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN;
const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2);
#if GRAPHICS_TFT_COLORING_ENABLED
const uint32_t themeId = getActiveTheme().id;
// Blue is a dark variant but uses full frame inversion, Keep it on the same filled bubble style as Default Dark.
const bool useDarkModeBubbleFill = showBubbles && (!isThemeFullFrameInvert() || themeId == ThemeID::Blue);
#endif
std::vector<int> lineTop;
lineTop.resize(cachedLines.size());
@@ -686,6 +763,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
int bottomY = visualBottom + BUBBLE_PAD_Y;
// On high-res screens, keep a 1px gap under the header
if (currentResolution == ScreenResolution::High) {
const int minTopY = contentTop + 1;
if (topY < minTopY) {
// Preserve bubble height when we push it down from the header.
const int shift = minTopY - topY;
topY = minTopY;
bottomY += shift;
}
}
if (bi + 1 < blocks.size()) {
int nextHeaderIndex = (int)blocks[bi + 1].start;
int nextTop = lineTop[nextHeaderIndex];
@@ -735,24 +823,56 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const int by = topY;
const int bw = bubbleW;
const int bh = bubbleH;
#if GRAPHICS_TFT_COLORING_ENABLED
const bool drawBubbleOutline = !useDarkModeBubbleFill;
#else
const bool drawBubbleOutline = true;
#endif
#if GRAPHICS_TFT_COLORING_ENABLED
if (useDarkModeBubbleFill) {
setDarkModeBubbleRoleColors(themeId, b.mine);
registerRoundedBubbleFillRegion(bx, by, bw, bh, r);
}
#endif
// Draw the 4 corner arcs using drawCircleQuads
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
if (drawBubbleOutline) {
// Draw the 4 corner arcs using drawCircleQuads
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
// Draw the 4 edges between corners
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
// Draw the 4 edges between corners
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
}
} else if (bubbleW > 1 && bubbleH > 1) {
// Fallback to simple rectangle for very small bubbles
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
#if GRAPHICS_TFT_COLORING_ENABLED
const bool drawBubbleOutline = !useDarkModeBubbleFill;
#else
const bool drawBubbleOutline = true;
#endif
#if GRAPHICS_TFT_COLORING_ENABLED
if (useDarkModeBubbleFill) {
setDarkModeBubbleRoleColors(themeId, b.mine);
registerTFTColorRegion(TFTColorRole::ActionMenuBody, bubbleX, topY, bubbleW, bubbleH);
}
#endif
if (drawBubbleOutline) {
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
}
}
}
} // end if (showBubbles)
#if GRAPHICS_TFT_COLORING_ENABLED
if (useDarkModeBubbleFill) {
// Restore theme role defaults so other screens keep their intended palette.
loadThemeDefaults();
}
#endif
// Render visible lines
int lineY = yOffset;
@@ -772,7 +892,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
headerX = x + textIndent;
}
graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1,
false);
true);
// Draw underline just under header text
int underlineY = lineY + FONT_HEIGHT_SMALL;
+57
View File
@@ -11,6 +11,8 @@
#include "gps/RTC.h" // for getTime() function
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TFTColorRegions.h"
#include "graphics/TFTPalette.h"
#include "graphics/images.h"
#include "meshUtils.h"
#include <algorithm>
@@ -213,6 +215,33 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
}
}
static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName,
int16_t nameX, int16_t y, int nameMaxWidth)
{
if (!display || !node || !node->is_favorite || !isTFTColoringEnabled() || !nodeName) {
return;
}
const int textWidth = UIRenderer::measureStringWithEmotes(display, nodeName);
const int regionWidth = min(textWidth, max(0, nameMaxWidth));
if (regionWidth <= 0) {
return;
}
// Node list rows can begin a couple of pixels inside header space.
// Clamp favorite-name color region below the header to avoid black overlap there.
const int16_t minContentY = static_cast<int16_t>(FONT_HEIGHT_SMALL + 1);
const int16_t regionY = max(y, minContentY);
const int16_t yClip = regionY - y;
const int16_t regionHeight = static_cast<int16_t>(FONT_HEIGHT_SMALL - yClip);
if (regionHeight <= 0) {
return;
}
setAndRegisterTFTColorRole(TFTColorRole::FavoriteNode, TFTPalette::Yellow, TFTPalette::Black, nameX, regionY, regionWidth,
regionHeight);
}
// =============================
// Entry Renderers
// =============================
@@ -227,6 +256,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
#if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
char timeStr[10];
@@ -286,6 +318,9 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
#if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
display->setTextAlignment(TEXT_ALIGN_LEFT);
@@ -315,6 +350,19 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
int barStartX = x + barsXOffset;
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
if (bars > 0) {
uint16_t signalBarsColor = TFTPalette::Bad;
if (bars >= 3) {
signalBarsColor = TFTPalette::Good;
} else if (bars == 2) {
signalBarsColor = TFTPalette::Medium;
}
// Highest bar reaches 6 px in this renderer.
setAndRegisterTFTColorRole(TFTColorRole::SignalBars, signalBarsColor, TFTPalette::Black, barStartX, barStartY - 6,
(kBarCount * kBarWidth) + ((kBarCount - 1) * kBarGap), 6);
}
for (int b = 0; b < kBarCount; b++) {
if (b < bars) {
int height = (b * 2);
@@ -350,6 +398,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
#if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
char distStr[10] = "";
@@ -455,6 +506,9 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
#if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
display->setTextAlignment(TEXT_ALIGN_LEFT);
@@ -710,6 +764,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
#if GRAPHICS_TFT_COLORING_ENABLED
registerTFTActionMenuRegions(boxLeft, boxTop, boxWidth, boxHeight);
#endif
// Text
display->drawString(boxLeft + padding, boxTop + padding, buf);
+35 -1
View File
@@ -7,6 +7,8 @@
#include "UIRenderer.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TFTColorRegions.h"
#include "graphics/TFTPalette.h"
#include "graphics/images.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
@@ -608,6 +610,9 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
#if GRAPHICS_TFT_COLORING_ENABLED
registerTFTActionMenuRegions(boxLeft, boxTop, boxWidth, boxHeight);
#endif
// Draw Content
int16_t lineY = boxTop + vPadding;
@@ -630,7 +635,21 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) {
background_yOffset = -1;
}
display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset);
const int16_t titleBarY = boxTop + 1;
const int16_t titleBarHeight = effectiveLineHeight - background_yOffset;
display->fillRect(boxLeft, titleBarY, boxWidth, titleBarHeight);
#if GRAPHICS_TFT_COLORING_ENABLED
if (alertBannerOptions > 0) {
const uint16_t titleTextColor =
(getActiveTheme().id == ThemeID::DefaultLight) ? TFTPalette::Black : getThemeHeaderText();
// Keep title role away from border/corner pixels so rounded-corner masks are not remapped to the title text
// color.
if (boxWidth > 2 && titleBarHeight > 0) {
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuTitle, getThemeHeaderBg(), titleTextColor, boxLeft + 1,
titleBarY, boxWidth - 2, titleBarHeight);
}
}
#endif
display->setColor(BLACK);
int yOffset = 3;
if (current_notification_type == notificationTypeEnum::node_picker) {
@@ -650,6 +669,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
const int barSpacing = 2;
const int barHeightStep = 2;
const int gap = 6;
const int maxBarHeight = totalBars * barHeightStep;
int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true);
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
@@ -664,6 +684,20 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
int baseX = groupStartX + textWidth + gap;
int baseY = lineY + effectiveLineHeight - 1;
#if GRAPHICS_TFT_COLORING_ENABLED
if (graphics::bannerSignalBars > 0) {
uint16_t signalBarsColor = TFTPalette::Medium;
if (graphics::bannerSignalBars <= 1) {
signalBarsColor = TFTPalette::Bad;
} else if (graphics::bannerSignalBars >= 4) {
signalBarsColor = TFTPalette::Good;
}
const int activeBars = min(graphics::bannerSignalBars, totalBars);
const int regionWidth = activeBars * barWidth + (activeBars - 1) * barSpacing;
setAndRegisterTFTColorRole(TFTColorRole::SignalBars, signalBarsColor, TFTPalette::Black, baseX,
baseY - maxBarHeight, regionWidth, maxBarHeight);
}
#endif
for (int b = 0; b < totalBars; b++) {
int barHeight = (b + 1) * barHeightStep;
int x = baseX + b * (barWidth + barSpacing);
File diff suppressed because it is too large Load Diff
+2
View File
@@ -54,6 +54,8 @@ class UIRenderer
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawBootIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Icon and screen drawing functions
static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
+3
View File
@@ -390,8 +390,11 @@ void InputBroker::Init()
seesawRotary = nullptr;
}
}
#ifdef __linux__
// Linux evdev keyboard input only — macOS has no <linux/input.h>.
aLinuxInputImpl = new LinuxInputImpl();
aLinuxInputImpl->init();
#endif
}
#endif
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
+4 -1
View File
@@ -1,5 +1,8 @@
#pragma once
#if ARCH_PORTDUINO
// Linux evdev keyboard input. Only compiled on Linux portduino targets;
// macOS / non-Linux builds have no <linux/input.h> or epoll, and the
// headless build doesn't need real keyboards anyway.
#if ARCH_PORTDUINO && defined(__linux__)
#include "InputBroker.h"
#include "concurrency/OSThread.h"
#include <assert.h>
+2 -1
View File
@@ -1,4 +1,5 @@
#ifdef ARCH_PORTDUINO
// Linux evdev impl. Same Linux-only gating as LinuxInput.h.
#if defined(ARCH_PORTDUINO) && defined(__linux__)
#pragma once
#include "LinuxInput.h"
#include "main.h"
+16 -5
View File
@@ -19,8 +19,12 @@ extern Adafruit_nRFCrypto nRFCrypto;
#include <Arduino.h>
#elif defined(ARCH_PORTDUINO)
#include <random>
#include <sys/random.h>
#include <unistd.h>
#ifdef __linux__
#include <sys/random.h> // getrandom()
#else
#include <stdlib.h> // arc4random_buf() on Darwin/BSD
#endif
#endif
namespace HardwareRNG
@@ -48,10 +52,11 @@ bool mixWithLoRaEntropy(uint8_t *buffer, size_t length)
// and return false so callers know no extra mixing occurred.
RadioLibInterface *radio = RadioLibInterface::instance;
if (!radio) {
// This path can run during portduinoSetup() before the console is initialized.
#ifndef PIO_UNIT_TESTING
LOG_ERROR("No radio instance available to provide entropy");
#endif
// This path can run during portduinoSetup() before the console is initialized,
// both for unit-test binaries and the simulator's meshtasticd; LOG_* dereferences `console`.
if (console) {
LOG_ERROR("No radio instance available to provide entropy");
}
return false;
}
@@ -118,10 +123,16 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
filled = true;
#elif defined(ARCH_PORTDUINO)
// Prefer the host OS RNG first when running under Portduino.
#ifdef __linux__
ssize_t generated = ::getrandom(buffer, length, 0);
if (generated == static_cast<ssize_t>(length)) {
filled = true;
}
#else
// arc4random_buf is available on Darwin/BSD and cannot fail.
::arc4random_buf(buffer, length);
filled = true;
#endif
if (!filled) {
fillWithRandomDevice(buffer, length);
+5 -2
View File
@@ -6,8 +6,11 @@
#include "configuration.h"
#include "detect/LoRaRadioType.h"
// Sentinel marking the end of a modem preset array
static constexpr meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
// Sentinel marking the end of a modem preset array. Declared `const` rather
// than `constexpr` because the cast from 0xFF to the enum is out-of-range and
// therefore not a valid constant expression on Clang 16+ (Apple Clang on
// macOS). The value is only ever compared at runtime, so static-init is fine.
static const meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(0xFF);
// Region profile: bundles the preset list with regulatory parameters shared across regions
+1 -1
View File
@@ -688,7 +688,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32);
#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT)) && \
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)) && \
HAS_TFT
// switch BT off by default; use TFT programming mode or hotkey to enable
config.bluetooth.enabled = false;
+1 -1
View File
@@ -119,7 +119,7 @@ bool RadioLibInterface::canSendImmediately()
return true;
}
bool RadioLibInterface::receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag)
bool RadioLibInterface::receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag)
{
bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag));
// Handle false detections
+1 -1
View File
@@ -220,7 +220,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
protected:
uint32_t activeReceiveStart = 0;
bool receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag);
bool receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag);
/** Do any hardware setup needed on entry into send configuration for the radio.
* Subclasses can customize, but must also call this base method */
+7 -4
View File
@@ -736,9 +736,13 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
// Also, we should set the time from the ISR and it should have msec level resolution
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
// Store a copy of encrypted packet for MQTT
// Store a copy of the encrypted packet for MQTT.
// Local, not a class member: handleReceived re-enters itself when a module
// reply broadcast goes through MeshService::sendToMesh -> Router::sendLocal,
// and a member would be silently overwritten without release on the inner
// call. Each invocation now owns its own copy (issue #9632, #10101, #8729).
DEBUG_HEAP_BEFORE;
p_encrypted = packetPool.allocCopy(*p);
meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
// Take those raw bytes and convert them back into a well structured protobuf we can understand
@@ -832,8 +836,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
#endif
}
packetPool.release(p_encrypted); // Release the encrypted packet
p_encrypted = nullptr;
packetPool.release(p_encrypted); // Release the encrypted packet (release() handles nullptr)
}
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
-3
View File
@@ -92,9 +92,6 @@ class Router : protected concurrency::OSThread, protected PacketHistory
before us */
uint32_t rxDupe = 0, txRelayCanceled = 0;
// pointer to the encrypted packet
meshtastic_MeshPacket *p_encrypted = nullptr;
protected:
friend class RoutingModule;
+2 -2
View File
@@ -124,10 +124,10 @@ bool sanitizeUtf8(char *buf, size_t bufSize)
if (!buf || bufSize == 0)
return false;
// Ensure null-terminated within buffer
// Ensure null-terminated within buffer; report if we had to enforce it
bool replaced = (buf[bufSize - 1] != '\0');
buf[bufSize - 1] = '\0';
bool replaced = false;
size_t i = 0;
size_t len = strlen(buf);
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
+1 -1
View File
@@ -31,7 +31,7 @@
#include <assert.h>
#include <utility>
#include <IPAddress.h>
#include "IPAddress.h"
#if defined(ARCH_PORTDUINO)
#include <netinet/in.h>
#elif !defined(ntohl)
+13 -5
View File
@@ -9,13 +9,10 @@
#include "PortduinoGlue.h"
#include "SHA256.h"
#include "api/ServerAPI.h"
#include "linux/gpio/LinuxGPIOPin.h"
#include "meshUtils.h"
#include <ErriezCRC32.h>
#include <Utility.h>
#include <assert.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <filesystem>
#include <fstream>
#include <iostream>
@@ -25,6 +22,12 @@
#include <sys/ioctl.h>
#include <unistd.h>
#ifdef PORTDUINO_LINUX_HARDWARE
#include "linux/gpio/LinuxGPIOPin.h"
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#endif
#ifdef PORTDUINO_LINUX_HARDWARE
#include <cxxabi.h>
#endif
@@ -130,9 +133,9 @@ void getMacAddr(uint8_t *dmac)
}
} else if (portduino_config.mac_address.length() > 11) {
MAC_from_string(portduino_config.mac_address, dmac);
exit;
return;
} else {
#ifdef PORTDUINO_LINUX_HARDWARE
struct hci_dev_info di = {0};
di.dev_id = 0;
bdaddr_t bdaddr;
@@ -152,6 +155,11 @@ void getMacAddr(uint8_t *dmac)
dmac[3] = di.bdaddr.b[2];
dmac[4] = di.bdaddr.b[1];
dmac[5] = di.bdaddr.b[0];
#else
// No BlueZ on non-Linux hosts (e.g. macOS). Leave dmac at its default;
// the caller can override via the --hwid CLI flag or the YAML config.
(void)dmac;
#endif
}
}
+7 -3
View File
@@ -5,6 +5,7 @@
#include "platform/portduino/PortduinoGlue.h"
#include <RadioLib.h>
#include <csignal>
#include <cstring>
#include <iostream>
#include <libpinedio-usb.h>
#include <unistd.h>
@@ -34,7 +35,7 @@ class Ch341Hal : public RadioLibHal
: RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING)
{
if (serial != "") {
strncpy(pinedio.serial_number, serial.c_str(), 8);
std::strncpy(pinedio.serial_number, serial.c_str(), 8);
pinedio_set_option(&pinedio, PINEDIO_OPTION_SEARCH_SERIAL, 1);
}
// LOG_INFO("USB Serial: %s", pinedio.serial_number);
@@ -59,8 +60,11 @@ class Ch341Hal : public RadioLibHal
void getSerialString(char *_serial, size_t len)
{
len = len > 8 ? 8 : len;
strncpy(_serial, pinedio.serial_number, len);
if (len == 0)
return;
size_t bytesCopied = (len - 1) < 8 ? (len - 1) : 8;
std::strncpy(_serial, pinedio.serial_number, bytesCopied);
_serial[bytesCopied] = '\0';
}
void getProductString(char *_product_string, size_t len)
+1 -1
View File
@@ -77,7 +77,7 @@ RTC_DATA_ATTR int bootCount = 0;
*/
void setCPUFast(bool on)
{
#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT
#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT && !defined(T_LORA_PAGER) && !defined(T_DECK)
if (isWifiAvailable()) {
/*
+4
View File
@@ -303,6 +303,10 @@ void setup()
{
initializeTestEnvironment();
// Wait for portduino's millis() clock to start ticking before tests run
testDelay(10);
testDelay(2000);
UNITY_BEGIN();
RUN_TEST(test_setLastSentToMesh_stores_millis);
+4 -2
View File
@@ -163,7 +163,7 @@ void test_above_max_codepoint()
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
}
int main(int argc, char **argv)
void setup()
{
UNITY_BEGIN();
@@ -191,5 +191,7 @@ int main(int argc, char **argv)
RUN_TEST(test_valid_max_codepoint);
RUN_TEST(test_above_max_codepoint);
return UNITY_END();
exit(UNITY_END());
}
void loop() {}
+4 -3
View File
@@ -35,9 +35,10 @@
// code)
#endif
// Leave undefined to disable our PMU IRQ handler. DO NOT ENABLE THIS because the pmuirq can cause sperious interrupts
// and waking from light sleep
// #define PMU_IRQ 35
// Voiding more warranties.
#define PMU_IRQ 35
#define PMU_POWER_BUTTON_IS_CANCEL // maps a short click of the power button to a cancel action (turning off the screen)
#define HAS_AXP192
#define GPS_UBLOX
#define GPS_RX_PIN 34
+5 -3
View File
@@ -89,8 +89,10 @@ build_flags =
-D VIEW_240x320
-D DISPLAY_SET_RESOLUTION
-D DISPLAY_SIZE=240x320 ; portrait mode
-D LGFX_SPI_3WIRE=true
-D LGFX_PIN_SCK=17
-D LGFX_PIN_MOSI=33
-D LGFX_PIN_MISO=-1
-D LGFX_PIN_DC=16
-D LGFX_PIN_CS=15
-D LGFX_PIN_BL=21
@@ -120,10 +122,10 @@ build_flags =
-D TFT_OFFSET_Y=0
-D TFT_OFFSET_ROTATION=0
-D SCREEN_ROTATE
-D SCREEN_TRANSITION_FRAMERATE=5
-D SCREEN_TRANSITION_FRAMERATE=30
-D BRIGHTNESS_DEFAULT=130 ; Medium Low Brightness
-D HAS_TOUCHSCREEN=1
-D TOUCH_I2C_PORT=0
-D TOUCH_I2C_PORT=1
-D TOUCH_SLAVE_ADDRESS=0x2E
-D SCREEN_TOUCH_INT=TOUCH_INT_PIN
-D SCREEN_TOUCH_RST=TOUCH_RST_PIN
@@ -133,4 +135,4 @@ lib_deps = ${heltec_v4_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
# renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master
https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip
https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip
@@ -0,0 +1,56 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
#define USB_VID 0x303a
#define USB_PID 0x1001
static const uint8_t TX = 43;
static const uint8_t RX = 44;
static const uint8_t SDA = 17;
static const uint8_t SCL = 18;
static const uint8_t SS = 8;
static const uint8_t MOSI = 10;
static const uint8_t MISO = 11;
static const uint8_t SCK = 9;
static const uint8_t A0 = 1;
static const uint8_t A1 = 2;
static const uint8_t A2 = 3;
static const uint8_t A3 = 4;
static const uint8_t A4 = 5;
static const uint8_t A5 = 6;
static const uint8_t A6 = 7;
static const uint8_t A7 = 8;
static const uint8_t A8 = 9;
static const uint8_t A9 = 10;
static const uint8_t A10 = 11;
static const uint8_t A11 = 12;
static const uint8_t A12 = 13;
static const uint8_t A13 = 14;
static const uint8_t A14 = 15;
static const uint8_t A15 = 16;
static const uint8_t A16 = 17;
static const uint8_t A17 = 18;
static const uint8_t A18 = 19;
static const uint8_t A19 = 20;
static const uint8_t T1 = 1;
static const uint8_t T2 = 2;
static const uint8_t T3 = 3;
static const uint8_t T4 = 4;
static const uint8_t T5 = 5;
static const uint8_t T6 = 6;
static const uint8_t T7 = 7;
static const uint8_t T8 = 8;
static const uint8_t T9 = 9;
static const uint8_t T10 = 10;
static const uint8_t T11 = 11;
static const uint8_t T12 = 12;
static const uint8_t T13 = 13;
static const uint8_t T14 = 14;
#endif /* Pins_Arduino_h */
@@ -0,0 +1,145 @@
[heltec_v4_r8_base]
extends = esp32s3_base
board = heltec_v4_r8
board_check = true
board_build.partitions = default_16MB.csv
build_flags =
${esp32s3_base.build_flags}
-D HELTEC_V4_R8
-D HAS_LORA_FEM=1
-D BOARD_HAS_PSRAM
-I variants/esp32s3/heltec_v4_r8
-ULED_BUILTIN
[env:heltec-v4-r8-oled]
custom_meshtastic_hw_model = 132
custom_meshtastic_hw_model_slug = HELTEC_V4_R8
custom_meshtastic_architecture = esp32-s3
custom_meshtastic_actively_supported = true
custom_meshtastic_support_level = 1
custom_meshtastic_display_name = Heltec V4 R8
custom_meshtastic_images = heltec_v4_r8.svg
custom_meshtastic_tags = Heltec
custom_meshtastic_requires_dfu = true
custom_meshtastic_partition_scheme = 16MB
extends = heltec_v4_r8_base
build_flags =
${heltec_v4_r8_base.build_flags}
-D HELTEC_V4_R8_OLED
-D USE_SSD1306 ; Heltec_v4_R8 has an SSD1315 display (compatible with SSD1306 driver)
-D LED_POWER=46
-D RESET_OLED=21
-D I2C_SDA=17
-D I2C_SCL=18
[env:heltec-v4-r8-tft]
custom_meshtastic_hw_model = 132
custom_meshtastic_hw_model_slug = HELTEC_V4_R8
custom_meshtastic_architecture = esp32-s3
custom_meshtastic_actively_supported = true
custom_meshtastic_support_level = 1
custom_meshtastic_display_name = Heltec V4 R8 TFT
custom_meshtastic_images = heltec_v4_r8_tft.svg
custom_meshtastic_tags = Heltec
custom_meshtastic_requires_dfu = true
custom_meshtastic_partition_scheme = 16MB
extends = heltec_v4_r8_base
build_flags =
${heltec_v4_r8_base.build_flags} ;-Os
-D HELTEC_V4_R8_TFT
-D I2C_SDA=17
-D I2C_SCL=18
-D PIN_BUTTON2=46
-D ALT_BUTTON_PIN=PIN_BUTTON2
-D ALT_BUTTON_ACTIVE_LOW=false
-D PIN_BUZZER=4
-D USE_PIN_BUZZER=PIN_BUZZER
-D CONFIG_ARDUHAL_LOG_COLORS
-D RADIOLIB_DEBUG_SPI=0
-D RADIOLIB_DEBUG_PROTOCOL=0
-D RADIOLIB_DEBUG_BASIC=0
-D RADIOLIB_VERBOSE_ASSERT=0
-D RADIOLIB_SPI_PARANOID=0
-D CONFIG_DISABLE_HAL_LOCKS=1
-D INPUTDRIVER_BUTTON_TYPE=0
-D HAS_SCREEN=1
-D HAS_TFT=1
-D RAM_SIZE=5120
-D LV_LVGL_H_INCLUDE_SIMPLE
-D LV_CONF_INCLUDE_SIMPLE
-D LV_COMP_CONF_INCLUDE_SIMPLE
-D LV_USE_SYSMON=0
-D LV_USE_PROFILER=0
-D LV_USE_PERF_MONITOR=0
-D LV_USE_MEM_MONITOR=0
-D LV_USE_LOG=0
-D LV_BUILD_TEST=0
-D USE_LOG_DEBUG
-D LOG_DEBUG_INC=\"DebugConfiguration.h\"
-D USE_PACKET_API
-D LGFX_DRIVER=LGFX_HELTEC_V4_TFT
-D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_HELTEC_V4_TFT.h\"
-D VIEW_240x320
-D DISPLAY_SET_RESOLUTION
-D DISPLAY_SIZE=240x320 ; portrait mode
-D LGFX_SPI_3WIRE=false
-D LGFX_PIN_SCK=16
-D LGFX_PIN_MOSI=15
-D LGFX_PIN_MISO=45
-D LGFX_PIN_DC=48
-D LGFX_PIN_CS=47
-D LGFX_PIN_BL=44
-D LGFX_PIN_RST=21
-D CUSTOM_TOUCH_DRIVER
-D TOUCH_SDA_PIN=I2C_SDA
-D TOUCH_SCL_PIN=I2C_SCL
-D TOUCH_INT_PIN=-1
-D TOUCH_RST_PIN=-1
;base UI
-D TFT_CS=LGFX_PIN_CS
-D ST7789_CS=TFT_CS
-D ST7789_RS=LGFX_PIN_DC
-D ST7789_SDA=LGFX_PIN_MOSI
-D ST7789_SCK=LGFX_PIN_SCK
-D ST7789_RESET=LGFX_PIN_RST
-D ST7789_MISO=LGFX_PIN_MISO
-D ST7789_BUSY=-1
-D ST7789_BL=LGFX_PIN_BL
-D ST7789_SPI_HOST=SPI3_HOST
-D TFT_BL=ST7789_BL
-D SPI_FREQUENCY=75000000
-D SPI_READ_FREQUENCY=SPI_FREQUENCY
-D SPI_3_WIRE=false
-D TFT_HEIGHT=320
-D TFT_WIDTH=240
-D TFT_OFFSET_X=0
-D TFT_OFFSET_Y=0
-D TFT_OFFSET_ROTATION=0
-D SCREEN_ROTATE
-D SCREEN_TRANSITION_FRAMERATE=5
-D BRIGHTNESS_DEFAULT=130 ; Medium Low Brightness
-D HAS_TOUCHSCREEN=1
-D TOUCH_I2C_PORT=0
-D TOUCH_SLAVE_ADDRESS=0x2E
-D SCREEN_TOUCH_INT=TOUCH_INT_PIN
-D SCREEN_TOUCH_RST=TOUCH_RST_PIN
; Have SPI interface SD card slot
-D HAS_SDCARD
-D SDCARD_USE_SPI1
-D SDCARD_USER_SPI_BEGIN
-D SPI_MOSI=LGFX_PIN_MOSI
-D SPI_SCK=LGFX_PIN_SCK
-D SPI_MISO=LGFX_PIN_MISO
-D SPI_CS=3
-D SDCARD_CS=SPI_CS
-D SD_SPI_FREQUENCY=SPI_FREQUENCY
lib_deps = ${heltec_v4_r8_base.lib_deps}
${device-ui_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
# renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master
https://github.com/Quency-D/chsc6x/archive/3b2b6cebf3177b3e2c33d06e07909b0b10159516.zip
+72
View File
@@ -0,0 +1,72 @@
#define VEXT_ENABLE 40 // active low, powers the oled display and the lora antenna boost
#define VEXT_ON_VALUE LOW
#define BUTTON_PIN 0
#define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
#define ADC_CHANNEL ADC1_GPIO1_CHANNEL
#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider
#define ADC_MULTIPLIER 4.9 * 1.035
#define USE_SX1262
#define LORA_DIO0 -1 // a No connect on the SX1262 module
#define LORA_RESET 12
#define LORA_DIO1 14 // SX1262 IRQ
#define LORA_DIO2 13 // SX1262 BUSY
#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TCXO is enabled
#define LORA_SCK 9
#define LORA_MISO 11
#define LORA_MOSI 10
#define LORA_CS 8
#define SX126X_CS LORA_CS
#define SX126X_DIO1 LORA_DIO1
#define SX126X_BUSY LORA_DIO2
#define SX126X_RESET LORA_RESET
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
// Enable Traffic Management Module for Heltec V4
#ifndef HAS_TRAFFIC_MANAGEMENT
#define HAS_TRAFFIC_MANAGEMENT 1
#endif
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048
#endif
// ---- KCT8103L RF FRONT END CONFIGURATION ----
// The Heltec V4.3 uses a KCT8103L FEM chip with integrated PA and LNA
// RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna
// Control logic (from KCT8103L datasheet):
// Transmit PA: CSD=1, CTX=1, CPS=1
// Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF)
// Receive bypass: CSD=1, CTX=1, CPS=0
// Shutdown: CSD=0, CTX=X, CPS=X
// Pin mapping:
// CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH)
// CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown)
// CTX (pin 6) -> GPIO5: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA)
// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7
// KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH)
#define USE_KCT8103L_PA
#define LORA_PA_POWER 7 // VFEM_Ctrl - KCT8103L LDO power enable
#define LORA_KCT8103L_PA_CSD 2 // CSD - KCT8103L chip enable (HIGH=on)
#define LORA_KCT8103L_PA_CTX 5 // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA)
#if HAS_TFT
#define USE_TFTDISPLAY 1
#endif
/*
* GPS pins
*/
#define GPS_L76K
#define PIN_GPS_EN (42)
#define GPS_EN_ACTIVE LOW
#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing
#define PIN_GPS_PPS (41)
// Seems to be missing on this new board
#define GPS_TX_PIN (38) // This is for bits going TOWARDS the CPU
#define GPS_RX_PIN (39) // This is for bits going TOWARDS the GPS
#define GPS_THREAD_INTERVAL 50
@@ -20,5 +20,5 @@ build_flags =
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip
https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip
upload_speed = 921600
@@ -13,7 +13,7 @@ build_flags =
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip
https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip
# renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver
https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip
# renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix
+1 -2
View File
@@ -47,10 +47,9 @@
#define TFT_OFFSET_Y 0
#define TFT_OFFSET_ROTATION 0
#define SCREEN_ROTATE
#define SCREEN_TRANSITION_FRAMERATE 5
#define SCREEN_TRANSITION_FRAMERATE 30
// Picomputer gets a white on black display
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
#define INPUTBROKER_MATRIX_TYPE 1
@@ -36,7 +36,7 @@
#define TFT_OFFSET_ROTATION 0
#define TFT_BL 45
#define SCREEN_ROTATE
#define SCREEN_TRANSITION_FRAMERATE 5 // fps
#define SCREEN_TRANSITION_FRAMERATE 30 // fps
#define USE_TFTDISPLAY 1
#define HAS_TOUCHSCREEN 1
View File
View File
View File
+1 -1
View File
@@ -20,7 +20,7 @@
#define TFT_OFFSET_Y 0
#define TFT_OFFSET_ROTATION 0
#define SCREEN_ROTATE
#define SCREEN_TRANSITION_FRAMERATE 5
#define SCREEN_TRANSITION_FRAMERATE 30
#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness
#define USE_TFTDISPLAY 1
#define HAS_PHYSICAL_KEYBOARD 1
+1
View File
@@ -42,6 +42,7 @@
#define DAC_I2S_MCLK -1
#define HAS_AXP2101
#define PMU_POWER_BUTTON_IS_CANCEL // maps a short click of the power button to a cancel action (turning off the screen)
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
+3 -3
View File
@@ -46,9 +46,9 @@
#define LR11X0_DIO_AS_RF_SWITCH
#endif
// Leave undefined to disable our PMU IRQ handler. DO NOT ENABLE THIS because the pmuirq can cause sperious interrupts
// and waking from light sleep
// #define PMU_IRQ 40
// Voiding warrenties, we're gonna try the IRQ
#define PMU_IRQ 40
#define PMU_POWER_BUTTON_IS_CANCEL // maps a short click of the power button to a cancel action (turning off the screen)
#define HAS_AXP2101
// PCF8563 RTC Module
+1 -1
View File
@@ -18,7 +18,7 @@
#define TFT_OFFSET_Y 0
#define TFT_OFFSET_ROTATION 3
#define SCREEN_ROTATE
#define SCREEN_TRANSITION_FRAMERATE 5
#define SCREEN_TRANSITION_FRAMERATE 30
#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness
#define USE_TFTDISPLAY 1
#define HAS_PHYSICAL_KEYBOARD 1
@@ -73,7 +73,6 @@
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
// Picomputer gets a white on black display
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
// keyboard changes
@@ -89,4 +88,4 @@
{ \
26, 37, 17, 16, 15, 7 \
}
// #end keyboard
// #end keyboard
+1 -2
View File
@@ -97,7 +97,6 @@
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
// Picomputer gets a white on black display
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
// keyboard changes
@@ -113,4 +112,4 @@
{ \
26, 37, 17, 16, 15, 7 \
}
// #end keyboard
// #end keyboard
+1 -2
View File
@@ -74,7 +74,6 @@
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
// Picomputer gets a white on black display
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
// keyboard changes
@@ -90,4 +89,4 @@
{ \
26, 37, 17, 16, 15, 7 \
}
// #end keyboard
// #end keyboard
+2 -2
View File
@@ -35,7 +35,7 @@
#define TFT_OFFSET_ROTATION 6 // unPhone's screen wired unusually, 0 typical
#define TFT_INVERT false
#define SCREEN_ROTATE true
#define SCREEN_TRANSITION_FRAMERATE 5
#define SCREEN_TRANSITION_FRAMERATE 30
#define USE_TFTDISPLAY 1
#define HAS_TOUCHSCREEN 1
@@ -74,4 +74,4 @@
// #define BATTERY_PIN 13 // battery V measurement pin; vbat divider is here
// #define ADC_CHANNEL ADC2_GPIO13_CHANNEL
// #define BAT_MEASURE_ADC_UNIT 2
// #define BAT_MEASURE_ADC_UNIT 2
+20 -10
View File
@@ -2,7 +2,7 @@
[portduino_base]
platform =
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
https://github.com/meshtastic/platform-native/archive/71ed55bb95feb3c43ebde1ec1e2e17643a424c04.zip
https://github.com/meshtastic/platform-native/archive/4ea5e09ac7d51a593e12ec7c1ebb6cd06745ce53.zip
framework = arduino
build_src_filter =
@@ -37,25 +37,35 @@ lib_deps =
# renovate: datasource=github-tags depName=Adafruit_BME680 packageName=adafruit/Adafruit_BME680
https://github.com/adafruit/Adafruit_BME680/archive/refs/tags/2.0.6.zip
build_flags =
; Cross-platform build flags shared between native (Linux) and native-macos builds.
; Anything Linux-only (libbluetooth, libgpiod, libi2c, /sys/class/gpio,
; PORTDUINO_LINUX_HARDWARE, glibc-style _FORTIFY_SOURCE) goes in `build_flags`
; instead. UDP multicast also lives there because the firmware's
; UdpMulticastHandler.h unconditionally `#include <WiFi.h>` on non-NRF52
; targets, which requires the framework's bundled WiFi shim that we
; lib_ignore on macOS.
build_flags_common =
${arduino_base.build_flags}
-D ARCH_PORTDUINO
-fPIC
-D_FORTIFY_SOURCE=2
-fstack-protector-all -Wstack-protector --param ssp-buffer-size=4
-Isrc/platform/portduino
-DRADIOLIB_EEPROM_UNSUPPORTED
-DPORTDUINO_LINUX_HARDWARE
-DHAS_UDP_MULTICAST=1
-lpthread
-lstdc++fs
-lbluetooth
-lgpiod
-lyaml-cpp
-li2c
-luv
-std=gnu17
-std=gnu++17
build_flags =
${portduino_base.build_flags_common}
-DHAS_UDP_MULTICAST=1
-D_FORTIFY_SOURCE=2
-fstack-protector-all -Wstack-protector --param ssp-buffer-size=4
-DPORTDUINO_LINUX_HARDWARE
-lstdc++fs
-lbluetooth
-lgpiod
-li2c
lib_ignore =
Adafruit NeoPixel
Adafruit ST7735 and ST7789 Library
+56
View File
@@ -117,3 +117,59 @@ build_flags = -lgcov --coverage -fprofile-abs-path -fsanitize=address ${env:nati
test_testing_command =
${platformio.build_dir}/${this.__env__}/meshtasticd
-s
; ---------------------------------------------------------------------------
; Native build for macOS (Darwin / arm64 + x86_64). Headless meshtasticd that
; runs in SimRadio mode (`-s`) or against real LoRa hardware via a CH341
; USB-SPI bridge. No BlueZ, libgpiod, or Linux I2C — those require Linux.
;
; Prerequisites (Homebrew):
; brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config
;
; The macOS-side patches now live upstream:
; * meshtastic/platform-native — `String.h`-shadow shim, `-Wno-enum-constexpr-conversion`,
; empty-variant-dir guard. Pulled via `portduino_base.platform` zip pin.
; * meshtastic/framework-portduino — LinuxHardwareI2C macOS stubs, AsyncUDP
; SOCK_NONBLOCK fallback, Common.h __APPLE__ guard, WiFiServer.cpp extern-C
; fix, package.json URL refresh. Pulled by platform-native at its pinned commit.
; This env therefore only carries the firmware-side build flags and src filter.
; ---------------------------------------------------------------------------
[env:native-macos]
extends = native_base
; Apple's ld doesn't accept GNU ld's `-Wl,-Map,<file>` syntax (inherited from
; the top-level platformio.ini). Strip it; the linker map isn't useful for
; the macOS dev loop anyway, and Apple ld's equivalent (`-Wl,-map,<file>`)
; uses different argument shape.
build_unflags = -Wl,-Map,"${platformio.build_dir}"/output.map
build_flags = ${portduino_base.build_flags_common}
-I variants/native/portduino
-I/opt/homebrew/include
-I/opt/homebrew/opt/argp-standalone/include
-I/opt/homebrew/opt/yaml-cpp/include
-L/opt/homebrew/lib
-L/opt/homebrew/opt/argp-standalone/lib
-L/opt/homebrew/opt/yaml-cpp/lib
-largp
-DPORTDUINO_DARWIN
; Headless build — variants/native/portduino/variant.h would otherwise
; default HAS_SCREEN to 1 and pull in screen-renderer source that uses
; VLA-with-initializer (a GNU/GCC extension Apple Clang rejects).
; MESHTASTIC_EXCLUDE_SCREEN gates the optional `screen->setHeading(...)`-
; style screen-driver hooks scattered through sensor sources.
-DHAS_SCREEN=0
-DMESHTASTIC_EXCLUDE_SCREEN=1
!pkg-config --libs openssl --silence-errors || :
; src/input/Linux*.{cpp,h} drive evdev (`<linux/input.h>`) which doesn't exist
; on macOS. graphics/Panel_sdl.* and graphics/TFTDisplay.cpp pull LovyanGFX
; (which we lib_ignore on macOS for the <malloc.h> issue). Neither is needed
; for the headless build.
build_src_filter = ${native_base.build_src_filter}
-<input/LinuxInput.cpp>
-<input/LinuxInputImpl.cpp>
-<graphics/Panel_sdl.cpp>
-<graphics/TFTDisplay.cpp>
; LovyanGFX includes <malloc.h> (Linux-only) and is only needed by TFT
; variants — not relevant for the headless macOS build.
lib_ignore =
${portduino_base.lib_ignore}
LovyanGFX
@@ -23,4 +23,4 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_
lib_deps =
${nrf52840_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip
https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip
@@ -57,9 +57,6 @@ extern "C" {
#define TFT_OFFSET_X 0
#define TFT_OFFSET_Y 0
// T114 gets a muted yellow on black display
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 128)
// #define TFT_OFFSET_ROTATION 0
// #define SCREEN_ROTATE
// #define SCREEN_TRANSITION_FRAMERATE 5
@@ -132,4 +132,4 @@ build_flags = ${heltec_mesh_solar_base.build_flags}
lib_deps =
${heltec_mesh_solar_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip
https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip
+1 -1
View File
@@ -7,7 +7,7 @@ extends = arduino_base
platform_packages =
; our custom Git version with C++17 support in platform.txt
# TODO renovate
platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#cpp17-platform
platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#master
; Don't renovate toolchain-gccarmnoneeabi
platformio/toolchain-gccarmnoneeabi@~1.90301.0