Merge branch 'develop' into pioarduino

This commit is contained in:
Manuel
2026-04-22 14:01:13 +02:00
committed by GitHub
18 changed files with 4405 additions and 157 deletions
+7 -1
View File
@@ -86,7 +86,13 @@ jobs:
run: sed -i 's/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
- name: PlatformIO Tests
run: platformio test -e coverage -v --junit-output-path testreport.xml
run: |
set -o pipefail
# Filter out SKIPPED summary rows for hardware variants that can't run on the
# native host. They flood the log and make it harder to spot real failures.
# The JUnit XML is written directly to testreport.xml before the pipe, so
# the test artifact is unaffected.
platformio test -e coverage -v --junit-output-path testreport.xml 2>&1 | grep -v "[[:space:]]SKIPPED$"
- name: Save test results
if: always() # run this step even if previous step failed
+28 -18
View File
@@ -275,9 +275,12 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
constexpr int kBarCount = 4;
constexpr int kBarWidth = 2;
constexpr int kBarGap = 1;
int barsXOffset = columnWidth - barsOffset;
int barsRightEdge = x + barsXOffset + ((kBarCount - 1) * (kBarWidth + kBarGap)) + kBarWidth;
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
char nodeName[96];
@@ -304,28 +307,35 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
}
}
// Draw signal strength bars
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
int barWidth = 2;
int barStartX = x + barsXOffset;
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
for (int b = 0; b < 4; b++) {
if (b < bars) {
int height = (b * 2);
display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height);
// Show signal only for direct neighbors (0 hops)
if (isZeroHop) {
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
int barStartX = x + barsXOffset;
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
for (int b = 0; b < kBarCount; b++) {
if (b < bars) {
int height = (b * 2);
display->fillRect(barStartX + (b * (kBarWidth + kBarGap)), barStartY - height, kBarWidth, height);
}
}
}
// Draw hop count
char hopStr[6] = "";
if (node->has_hops_away && node->hops_away > 0)
snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away);
// Draw hop count + hop icon
if (node->has_hops_away && node->hops_away > 0) {
char hopCount[6];
snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away);
if (hopStr[0] != '\0') {
int rightEdge = x + columnWidth - hopOffset;
int textWidth = display->getStringWidth(hopStr);
display->drawString(rightEdge - textWidth, y, hopStr);
const int hopCountWidth = display->getStringWidth(hopCount);
const int gap = 1;
const int totalWidth = hopCountWidth + gap + hop_width;
const int hopX = barsRightEdge - totalWidth;
const int iconY = y + (FONT_HEIGHT_SMALL - hop_height) / 2;
display->drawString(hopX, y, hopCount);
display->drawXbm(hopX + hopCountWidth + gap, iconY, hop_width, hop_height, hop);
}
}
+45 -77
View File
@@ -405,10 +405,10 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
}
#endif
// === 2. Signal and Hops (combined on one line, if available) ===
char signalHopsStr[32] = "";
// === 2. Signal/Hops line (if available) ===
bool haveSignal = false;
int bars = 0;
const char *qualityLabel = nullptr;
// Helper to get SNR limit based on modem preset
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
@@ -429,80 +429,51 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
}
};
// Calculate signal grade using modem preset and SNR only
float snrLimit = getSnrLimit(config.lora.modem_preset);
float snr = node->snr;
// Determine signal quality label and bars using SNR-only grading
const char *qualityLabel = nullptr;
if (snr > snrLimit + 10) {
qualityLabel = "Good";
bars = 4;
} else if (snr > snrLimit + 6) {
qualityLabel = "Good";
bars = 3;
} else if (snr > snrLimit + 2) {
qualityLabel = "Good";
bars = 2;
} else if (snr > snrLimit - 4) {
qualityLabel = "Fair";
bars = 1;
} else {
qualityLabel = "Bad";
bars = 1;
}
// Add extra spacing on the left if we have an API connection to account for the common footer icons
const char *leftSideSpacing =
graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " ";
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
// --- Build the Signal/Hops line ---
// Only show signal if we have valid SNR
if (snr > -100 && snr != 0) {
snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel);
haveSignal = true;
}
if (node->hops_away > 0) {
size_t len = strlen(signalHopsStr);
if (haveSignal) {
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]");
} else {
snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]");
}
}
if (signalHopsStr[0]) {
int yPos = getTextPositions(display)[line++];
int curX = x;
// Split combined string into signal text and hop suffix
char sigPart[20] = "";
const char *hopPart = nullptr;
char *bracket = strchr(signalHopsStr, '[');
if (bracket) {
size_t n = (size_t)(bracket - signalHopsStr);
if (n >= sizeof(sigPart))
n = sizeof(sigPart) - 1;
memcpy(sigPart, signalHopsStr, n);
sigPart[n] = '\0';
// Trim trailing spaces
while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') {
sigPart[strlen(sigPart) - 1] = '\0';
// Signal text/bars are only for direct (zero-hop) nodes with valid SNR.
if (isZeroHop) {
float snr = node->snr;
if (snr > -100 && snr != 0) {
float snrLimit = getSnrLimit(config.lora.modem_preset);
// Determine signal quality label and bars using SNR-only grading.
if (snr > snrLimit + 10) {
qualityLabel = "Good";
bars = 4;
} else if (snr > snrLimit + 6) {
qualityLabel = "Good";
bars = 3;
} else if (snr > snrLimit + 2) {
qualityLabel = "Good";
bars = 2;
} else if (snr > snrLimit - 4) {
qualityLabel = "Fair";
bars = 1;
} else {
qualityLabel = "Bad";
bars = 1;
}
hopPart = bracket; // "[n Hop(s)]"
} else {
strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1);
sigPart[sizeof(sigPart) - 1] = '\0';
haveSignal = true;
}
}
// Draw signal quality text
display->drawString(curX, yPos, sigPart);
curX += display->getStringWidth(sigPart) + 4;
const bool showHops = node->has_hops_away && node->hops_away > 0;
if (haveSignal || showHops) {
int yPos = getTextPositions(display)[line++];
int curX = x + display->getStringWidth(leftSideSpacing);
// Draw signal quality text for zero-hop nodes when present.
if (haveSignal && qualityLabel) {
char signalLabel[20];
snprintf(signalLabel, sizeof(signalLabel), "Sig:%s", qualityLabel);
display->drawString(curX, yPos, signalLabel);
curX += display->getStringWidth(signalLabel) + 4;
}
// Draw signal bars (skip on UltraLow, text only)
if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) {
@@ -541,12 +512,12 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2;
}
// Draw hops AFTER the bars as: [ number + hop icon ]
if (hopPart && node->hops_away > 0) {
// open bracket
display->drawString(curX, yPos, "[");
curX += display->getStringWidth("[") + 1;
// Draw hops for non-zero-hop nodes as: number + hop icon.
// This path is mutually exclusive with the zero-hop signal-bars path above.
if (showHops) {
// hop label
display->drawString(curX, yPos, "Hop:");
curX += display->getStringWidth("Hop:") + 2;
// hop count
char hopCount[6];
@@ -558,9 +529,6 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2;
display->drawXbm(curX, iconY, hop_width, hop_height, hop);
curX += hop_width + 1;
// closing bracket
display->drawString(curX, yPos, "]");
}
}
@@ -0,0 +1,122 @@
/*
NicheGraphics parallel E-Ink driver for the LilyGo T5-S3-ePaper-Pro (ED047TC1).
InkHUD buffer format : 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
FastEPD buffer format: 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
Both formats share the same pixel layout and polarity (1 = white, 0 = black).
The InkHUD safe-area buffer (944×523) is copied into the centre of the physical
960×540 FastEPD buffer so content clears the panel's inactive edge border.
See ED047TC1.h for the H_OFFSET_BYTES / V_OFFSET_TOP / V_OFFSET_BOTTOM constants.
*/
// Ruler diagnostic — uncomment to draw calibration lines at each physical edge.
// #define EINK_EDGE_LINES
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#ifdef T5_S3_EPAPER_PRO
#include "./ED047TC1.h"
#include "FastEPD.h"
#include "configuration.h"
using namespace NicheGraphics::Drivers;
void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
{
// Parallel display — SPI parameters are not used
(void)spi;
(void)pin_dc;
(void)pin_cs;
(void)pin_busy;
(void)pin_rst;
epaper = new FASTEPD;
#if defined(T5_S3_EPAPER_PRO_V1)
epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
#elif defined(T5_S3_EPAPER_PRO_V2)
epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
// Initialize all PCA9535 port-0 pins as outputs / HIGH
for (int i = 0; i < 8; i++) {
epaper->ioPinMode(i, OUTPUT);
epaper->ioWrite(i, HIGH);
}
#else
#error "ED047TC1 driver: unsupported variant — define T5_S3_EPAPER_PRO_V1 or T5_S3_EPAPER_PRO_V2"
#endif
epaper->setMode(BB_MODE_1BPP);
epaper->clearWhite();
epaper->fullUpdate(true); // Blocking initial clear
}
void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
{
if (!epaper)
return;
// InkHUD renders into a DISPLAY_WIDTH × DISPLAY_HEIGHT safe-area buffer.
// We need to place that into the centre of the physical 960×540 FastEPD buffer,
// leaving blank margins at every edge to avoid the panel's inactive border.
const uint32_t srcRowBytes = (DISPLAY_WIDTH + 7) / 8; // bytes per row in InkHUD buffer (118)
const uint32_t dstRowBytes = (960 + 7) / 8; // bytes per row in physical buffer (120)
const uint32_t dstTotalRows = 540;
uint8_t *cur = epaper->currentBuffer();
// Fill physical buffer with white (0xFF = white in FastEPD 1bpp)
memset(cur, 0xFF, dstRowBytes * dstTotalRows);
// Copy each InkHUD row into the physical buffer with horizontal + vertical offsets
for (uint32_t row = 0; row < DISPLAY_HEIGHT; row++) {
const uint8_t *srcRow = imageData + row * srcRowBytes;
uint8_t *dstRow = cur + (row + V_OFFSET_TOP) * dstRowBytes + H_OFFSET_BYTES;
memcpy(dstRow, srcRow, srcRowBytes);
}
#ifdef EINK_EDGE_LINES
// Draw a 1px black box at the exact boundary of the safe area within the
// physical buffer. If the margins are correct, all 4 lines should be
// fully visible and right at the edge of the usable display area.
auto setPixelBlack = [&](uint32_t col, uint32_t row) { cur[row * dstRowBytes + col / 8] &= ~(0x80 >> (col % 8)); };
const uint32_t safeX = H_OFFSET_BYTES * 8;
const uint32_t safeY = V_OFFSET_TOP;
const uint32_t safeW = DISPLAY_WIDTH;
const uint32_t safeH = DISPLAY_HEIGHT;
// Top edge: horizontal line at safeY
for (uint32_t col = safeX; col < safeX + safeW; col++)
setPixelBlack(col, safeY);
// Bottom edge: horizontal line at safeY + safeH - 1
for (uint32_t col = safeX; col < safeX + safeW; col++)
setPixelBlack(col, safeY + safeH - 1);
// Left edge: vertical line at safeX
for (uint32_t row = safeY; row < safeY + safeH; row++)
setPixelBlack(safeX, row);
// Right edge: vertical line at safeX + safeW - 1
for (uint32_t row = safeY; row < safeY + safeH; row++)
setPixelBlack(safeX + safeW - 1, row);
#endif
if (type == FULL) {
epaper->fullUpdate(CLEAR_SLOW, false);
epaper->backupPlane(); // Sync pPrevious so next partialUpdate has a correct baseline
} else {
// FAST: true partial update — compares pCurrent vs pPrevious and only applies
// the update waveform to rows that actually changed. Unchanged rows get a neutral
// signal (no visible effect). partialUpdate() updates pPrevious internally.
epaper->partialUpdate(false, 0, dstTotalRows - 1);
}
}
#endif // T5_S3_EPAPER_PRO
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
@@ -0,0 +1,90 @@
/*
E-Ink display driver adapter
- ED047TC1 (via FastEPD library)
- Manufacturer: E Ink / used in LilyGo T5-E-Paper-S3-Pro
- Size: 4.7 inch
- Physical resolution: 960px x 540px
- Interface: 8-bit parallel (NOT SPI)
Unlike the other NicheGraphics EInk drivers, this one drives a parallel e-paper
panel via the FastEPD library. SPI parameters passed to begin() are ignored.
The ED047TC1 panel has an inactive pixel border on all four edges (~4–8 physical
pixels). DISPLAY_WIDTH / DISPLAY_HEIGHT expose a reduced "safe area" to InkHUD so
that content is never drawn into this dead zone. The update() method copies the
InkHUD frame buffer into the centre of the larger physical 960×540 buffer, using
H_OFFSET_BYTES (horizontal, whole bytes = 8 pixels per byte),
V_OFFSET_TOP and V_OFFSET_BOTTOM (vertical, pixel rows) to position it.
Changing these constants shifts content inward from each physical edge:
H_OFFSET_BYTES = 1 → 8px left margin, 8px right margin (960 – 8 – 8 = 944)
V_OFFSET_TOP = 9 → 9px top margin (asymmetric: top ≠ bottom)
V_OFFSET_BOTTOM = 8 → 8px bottom margin (540 – 9 – 8 = 523)
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./EInk.h"
// Forward declare to avoid pulling FastEPD into all translation units
class FASTEPD;
namespace NicheGraphics::Drivers
{
class ED047TC1 : public EInk
{
// Safe-area dimensions exposed to InkHUD (physical panel is 960×540).
//
// The ED047TC1 has an inactive pixel border on all physical edges.
// The physical buffer coordinates do NOT directly match the visual orientation
// due to FastEPD's portrait scan direction and InkHUD's rotation=3 (270° CW):
//
// Physical buffer Visual on device (rotation=3)
// ───────────────── ──────────────────────────────
// Physical LEFT cols → Visual TOP edge
// Physical RIGHT cols → Visual BOTTOM edge
// Physical TOP rows → Visual RIGHT edge
// Physical BOTTOM rows → Visual LEFT edge
//
// Offset constants shift the InkHUD safe-area away from each physical dead zone:
// H_OFFSET_BYTES : whole bytes from physical left (8px per byte, affects visual TOP)
// Physical right margin = 960 − H_OFFSET_BYTES×8 − DISPLAY_WIDTH (affects visual BOTTOM)
// V_OFFSET_TOP : pixel rows from physical top (affects visual RIGHT)
// V_OFFSET_BOTTOM: pixel rows from physical bottom (affects visual LEFT)
//
// Calibrated by flashing a 1px border box and adjusting until all 4 sides are visible.
static constexpr uint16_t DISPLAY_WIDTH = 944; // 960 − H_OFFSET_BYTES×8 − right_margin (8+8 = 16px)
static constexpr uint16_t DISPLAY_HEIGHT = 523; // 540 − V_OFFSET_TOP − V_OFFSET_BOTTOM (9+8 = 17px)
static constexpr uint8_t H_OFFSET_BYTES = 1; // visual TOP : 8px physical left margin
// visual BOTTOM: 960−8−944=8px physical right margin
static constexpr uint8_t V_OFFSET_TOP = 9; // visual RIGHT : CONFIRMED OK
static constexpr uint8_t V_OFFSET_BOTTOM = 8; // visual LEFT : 8px physical bottom margin
static constexpr UpdateTypes supported = static_cast<UpdateTypes>(FULL | FAST);
public:
ED047TC1() : EInk(DISPLAY_WIDTH, DISPLAY_HEIGHT, supported) {}
// EInk interface — SPI params are not used for this parallel display
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = 0xFF) override;
void update(uint8_t *imageData, UpdateTypes type) override;
protected:
bool isUpdateDone() override { return true; } // FastEPD updates are blocking
private:
FASTEPD *epaper = nullptr;
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -88,8 +88,12 @@ class AppletFont
// Greek
#include "graphics/niche/Fonts/FreeSans12pt_Win1253.h"
#include "graphics/niche/Fonts/FreeSans18pt_Win1253.h"
#include "graphics/niche/Fonts/FreeSans24pt_Win1253.h"
#include "graphics/niche/Fonts/FreeSans6pt_Win1253.h"
#include "graphics/niche/Fonts/FreeSans9pt_Win1253.h"
#define FREESANS_24PT_WIN1253 InkHUD::AppletFont(FreeSans24pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -5, 3)
#define FREESANS_18PT_WIN1253 InkHUD::AppletFont(FreeSans18pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -4, 2)
#define FREESANS_12PT_WIN1253 InkHUD::AppletFont(FreeSans12pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -3, 1)
#define FREESANS_9PT_WIN1253 InkHUD::AppletFont(FreeSans9pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -2, -1)
#define FREESANS_6PT_WIN1253 InkHUD::AppletFont(FreeSans6pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -1, -2)
+8 -2
View File
@@ -143,6 +143,10 @@ int32_t ButtonThread::runOnce()
leadUpSequenceActive = false;
resetLeadUpSequence();
}
#ifdef INPUT_DEBUG
if (buttonCurrentlyPressed)
LOG_WARN("Button held for %u ms", millis() - buttonPressStartTime);
#endif
// Progressive lead-up sound system
if (!_suppressLeadUp && buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) {
@@ -311,7 +315,8 @@ int32_t ButtonThread::runOnce()
void ButtonThread::attachButtonInterrupts()
{
// Interrupt for user button, during normal use. Improves responsiveness.
attachInterrupt(_pinNum, _intRoutine, CHANGE);
if (_intRoutine != nullptr)
attachInterrupt(_pinNum, _intRoutine, CHANGE);
}
/*
@@ -320,7 +325,8 @@ void ButtonThread::attachButtonInterrupts()
*/
void ButtonThread::detachButtonInterrupts()
{
detachInterrupt(_pinNum);
if (_intRoutine != nullptr)
detachInterrupt(_pinNum);
}
#ifdef ARCH_ESP32
+2 -1
View File
@@ -29,7 +29,8 @@ void TouchScreenImpl1::init()
return;
#else
TouchScreenBase::init(true);
inputBroker->registerSource(this);
if (inputBroker)
inputBroker->registerSource(this);
#endif
}
+9
View File
@@ -455,6 +455,15 @@ template <typename T> void SX126xInterface<T>::resetAGC()
// RX boosted gain mode
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
// Re-apply the undocumented 0x8B5 RX sensitivity patch that was set in init().
// The CALIBRATE_ALL (0x7F) command above clears bit 0 of register 0x8B5, which
// silently removes the RX sensitivity improvement introduced in #9571 / #9777.
// Without this re-apply, every SX1262 node loses its RX boost ~60s after boot
// and never recovers until reboot. See empirical evidence in the PR description.
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) != RADIOLIB_ERR_NONE) {
LOG_WARN("SX126x resetAGC: failed to re-apply 0x8B5 RX sensitivity patch");
}
// 6. Resume receiving
startReceive();
}
+4
View File
@@ -81,7 +81,9 @@ meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user)
meshtastic_UserLite lite = meshtastic_UserLite_init_default;
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
lite.hw_model = user.hw_model;
lite.role = user.role;
lite.is_licensed = user.is_licensed;
@@ -99,7 +101,9 @@ meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_User
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum);
strncpy(user.long_name, lite.long_name, sizeof(user.long_name));
user.long_name[sizeof(user.long_name) - 1] = '\0';
strncpy(user.short_name, lite.short_name, sizeof(user.short_name));
user.short_name[sizeof(user.short_name) - 1] = '\0';
user.hw_model = lite.hw_model;
user.role = lite.role;
user.is_licensed = lite.is_licensed;
+7 -3
View File
@@ -285,7 +285,11 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
/* Nepal 865MHz */
meshtastic_Config_LoRaConfig_RegionCode_NP_865 = 25,
/* Brazil 902MHz */
meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26
meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26,
/* ITU Region 1 Amateur Radio 2m band (144-146 MHz) */
meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M = 27,
/* ITU Region 2 / 3 Amateur Radio 2m band (144-148 MHz) */
meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M = 28
} meshtastic_Config_LoRaConfig_RegionCode;
/* Standard predefined channel settings
@@ -702,8 +706,8 @@ extern "C" {
#define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1))
#define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_BR_902
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1))
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M+1))
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO
+1 -1
View File
@@ -39,7 +39,7 @@
#define DAC_I2S_BCK 48
#define DAC_I2S_WS 15
#define DAC_I2S_DOUT 46
#define DAC_I2S_MCLK 0
#define DAC_I2S_MCLK -1
#define HAS_AXP2101
+42 -47
View File
@@ -6,26 +6,27 @@ NicheGraphics attempts a different approach:
Per-device config takes place in this setupNicheGraphics() method
(And a small amount in platformio.ini)
This file sets up InkHUD for Heltec VM-E290.
Different NicheGraphics UIs and different hardware variants will each have their own setup procedure.
This file sets up InkHUD for the LilyGo T5-E-Paper-S3-Pro.
The board uses a 4.7" ED047TC1 parallel e-paper display (960×540, 8-bit parallel interface).
This is driven via the FastEPD library through the NicheGraphics ED047TC1 driver adapter.
*/
#pragma once
#include "configuration.h"
#include "mesh/MeshModule.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
// InkHUD-specific components
// ---------------------------
// #include "graphics/niche/InkHUD/InkHUD.h"
#include "graphics/niche/InkHUD/WindowManager.h"
#include "graphics/niche/InkHUD/InkHUD.h"
// Applets
#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h"
#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h"
#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h"
#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h"
#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h"
#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h"
@@ -34,26 +35,20 @@ Different NicheGraphics UIs and different hardware variants will each have their
// Shared NicheGraphics components
// --------------------------------
#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h"
#include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h"
#include "graphics/niche/Drivers/EInk/ED047TC1.h"
#include "graphics/niche/Inputs/TwoButton.h"
void setupNicheGraphics()
{
using namespace NicheGraphics;
// SPI
// -----------------------------
// Display is connected to HSPI
SPIClass *hspi = new SPIClass(HSPI);
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS);
// E-Ink Driver
// -----------------------------
// The ED047TC1 is a parallel display — no SPI bus setup needed.
// begin() args are part of the EInk interface but are ignored for parallel displays.
// Use E-Ink driver
Drivers::EInk *driver = new Drivers::DEPG0290BNS800;
driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY);
Drivers::EInk *driver = new Drivers::ED047TC1;
driver->begin(nullptr, 0, 0, 0);
// InkHUD
// ----------------------------
@@ -67,57 +62,57 @@ void setupNicheGraphics()
// Set how unhealthy additional FAST updates beyond this number are
inkhud->setDisplayResilience(7, 1.5);
// Prepare fonts
InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252;
InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252;
// Prepare fonts — use larger sizes to suit the 4.7" screen at ~234 DPI
InkHUD::Applet::fontLarge = FREESANS_24PT_WIN1253;
InkHUD::Applet::fontMedium = FREESANS_18PT_WIN1253;
InkHUD::Applet::fontSmall = FREESANS_12PT_WIN1253;
// Init settings, and customize defaults
// Customize default settings
inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle?
inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise
inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise
inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users
inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead
inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery
inkhud->persistence->settings.optionalFeatures.batteryIcon = true;
inkhud->persistence->settings.optionalMenuItems.backlight = true;
// Setup backlight
// Note: AUX button behavior configured further down
Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance();
backlight->setPin(PIN_EINK_EN);
// Alignment must cancel rotation for visual-frame touch input: (rotation + alignment) % 4 == 0.
inkhud->persistence->settings.joystick.alignment = (4 - inkhud->persistence->settings.rotation) % 4;
// Pick applets
// Note: order of applets determines priority of "auto-show" feature
// Optional arguments for defaults:
// - is activated?
// - is autoshown?
// - is foreground on a specific tile (index)?
inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown
inkhud->addApplet("DMs", new InkHUD::DMApplet);
inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0));
inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1));
inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated
inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet);
inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, false, false); // Not Active, not autoshown
inkhud->addApplet("DMs", new InkHUD::DMApplet, true, true); // Activated, Autoshown
inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, true); // Activated, Autoshown
inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1), false, false); // Not Active, not autoshown
inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false); // Activated, not autoshown
inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false); // Activated, not autoshown
inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0
// inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet);
// inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // Not Active, not autoshown
// Backlight
// ----------------------------
Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance();
backlight->setPin(BOARD_BL_EN); // GPIO11 on V2
// Start running InkHUD
inkhud->begin();
// Touch navigation requires joystick mode — enforce post-begin so flash cannot override.
inkhud->persistence->settings.joystick.enabled = true;
inkhud->persistence->settings.joystick.aligned = true;
// Buttons
// --------------------------
Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component
// Setup the main user button (0)
// Setup the main user button (boot button, GPIO 0)
buttons->setWiring(0, BUTTON_PIN);
buttons->setHandlerShortPress(0, []() { InkHUD::InkHUD::getInstance()->shortpress(); });
buttons->setHandlerLongPress(0, []() { InkHUD::InkHUD::getInstance()->longpress(); });
buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); });
buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); });
// Setup the aux button (1)
// Bonus feature of VME290
buttons->setWiring(1, BUTTON_PIN_SECONDARY);
buttons->setHandlerShortPress(1, []() { InkHUD::InkHUD::getInstance()->nextTile(); });
// No dedicated aux button on this board
buttons->start();
}
#endif
#endif
+137 -4
View File
@@ -2,21 +2,123 @@
#ifdef T5_S3_EPAPER_PRO
#include "Observer.h"
#include "TouchDrvGT911.hpp"
#include "Wire.h"
#include "input/InputBroker.h"
#include "input/TouchScreenImpl1.h"
#include "sleep.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "graphics/niche/InkHUD/InkHUD.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
// Bridges touch events from TouchScreenImpl1 directly into InkHUD,
// bypassing the InputBroker (which is excluded in InkHUD builds).
// Routing mirrors the mini-epaper-s3 two-way rocker pattern:
// - Nav left/right: prevApplet/nextApplet when idle, navUp/Down when a system applet has focus (e.g. menu)
// - Nav up/down: navUp/navDown always (menu scroll)
// - Tap: shortpress (cycle applets / confirm in menu)
// - Long press: longpress (open menu / back)
class TouchInkHUDBridge : public Observer<const InputEvent *>
{
int onNotify(const InputEvent *e) override
{
auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance();
// Keep alignment in sync with the current rotation so that visual-frame gestures
// always pass through nav functions without remapping: (rotation + alignment) % 4 == 0.
inkhud->persistence->settings.joystick.alignment = (4 - inkhud->persistence->settings.rotation) % 4;
// Check whether a system applet (e.g. menu) is currently handling input
bool systemHandlingInput = false;
for (NicheGraphics::InkHUD::SystemApplet *sa : inkhud->systemApplets) {
if (sa->handleInput) {
systemHandlingInput = true;
break;
}
}
switch (e->inputEvent) {
case INPUT_BROKER_USER_PRESS:
inkhud->shortpress();
break;
case INPUT_BROKER_SELECT:
inkhud->longpress();
break;
case INPUT_BROKER_LEFT:
if (systemHandlingInput)
inkhud->navUp();
else
inkhud->prevApplet();
break;
case INPUT_BROKER_RIGHT:
if (systemHandlingInput)
inkhud->navDown();
else
inkhud->nextApplet();
break;
case INPUT_BROKER_UP:
inkhud->navUp();
break;
case INPUT_BROKER_DOWN:
inkhud->navDown();
break;
default:
break;
}
return 0;
}
};
static TouchInkHUDBridge touchBridge;
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
TouchDrvGT911 touch;
// Commands the GT911 into standby before the Wire bus is torn down.
// notifyDeepSleep fires before Wire.end() in doDeepSleep(), so I2C is still available here.
struct TouchDeepSleepObserver {
int onDeepSleep(void *)
{
touch.sleep();
return 0;
}
CallbackObserver<TouchDeepSleepObserver, void *> observer{this, &TouchDeepSleepObserver::onDeepSleep};
} static touchDeepSleepObserver;
bool readTouch(int16_t *x, int16_t *y)
{
if (!digitalRead(GT911_PIN_INT)) {
int16_t raw_x;
int16_t raw_y;
if (touch.getPoint(&raw_x, &raw_y)) {
// rotate 90° for landscape
*x = raw_y;
*y = EPD_WIDTH - 1 - raw_x;
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
// Transform raw GT911 axes to visual-frame coordinates for the current display rotation.
// rotation=3 is the physical identity (device's default orientation).
switch (NicheGraphics::InkHUD::InkHUD::getInstance()->persistence->settings.rotation) {
default:
case 3:
*x = raw_x;
*y = raw_y;
break; // identity
case 2:
*x = (EPD_WIDTH - 1) - raw_y;
*y = raw_x;
break; // 90° CW tilt
case 1:
*x = (EPD_HEIGHT - 1) - raw_x;
*y = (EPD_WIDTH - 1) - raw_y;
break; // 180° flip
case 0:
*x = raw_y;
*y = (EPD_HEIGHT - 1) - raw_x;
break; // 90° CCW tilt
}
#else
*x = raw_x;
*y = raw_y;
#endif
LOG_DEBUG("touched(%d/%d)", *x, *y);
return true;
}
@@ -31,15 +133,46 @@ void earlyInitVariant()
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH);
pinMode(BOARD_BL_EN, OUTPUT);
// Program GT911 touch controller to I2C address 0x14 (GT911_SLAVE_ADDRESS_H) before
// the I2C bus scan runs. GPIO3 (INT) defaults LOW on ESP32-S3 cold boot, which would
// leave the GT911 at 0x5D (GT911_SLAVE_ADDRESS_L) — the same address as the SFA30
// air quality sensor — causing a false-positive SFA30 detection during the I2C scan.
//
// GT911 datasheet §4.3 "Address Selection":
// Pull INT HIGH before releasing RST → device latches address 0x14 (SLAVE_ADDRESS_H)
// Pull INT LOW before releasing RST → device latches address 0x5D (SLAVE_ADDRESS_L)
// Minimum RST assert time: 100 µs; minimum startup time after RST deassert: 5 ms.
//
// lateInitVariant() calls touch.begin() which repeats this sequence internally while
// also performing full I2C initialisation; the double-reset is harmless.
pinMode(GT911_PIN_RST, OUTPUT);
digitalWrite(GT911_PIN_RST, LOW);
pinMode(GT911_PIN_INT, OUTPUT);
digitalWrite(GT911_PIN_INT, HIGH); // HIGH → latch address 0x14
delay(1); // > 100 µs
digitalWrite(GT911_PIN_RST, HIGH);
delay(10); // > 5 ms startup
pinMode(GT911_PIN_INT, INPUT); // release INT for interrupt use
}
void variant_shutdown()
{
// Ensure frontlight is off during deep sleep
digitalWrite(BOARD_BL_EN, LOW);
}
// T5-S3-ePaper Pro specific (late-) init
void lateInitVariant(void)
{
touch.setPins(GT911_PIN_RST, GT911_PIN_INT);
if (touch.begin(Wire, GT911_SLAVE_ADDRESS_L, GT911_PIN_SDA, GT911_PIN_SCL)) {
if (touch.begin(Wire, GT911_SLAVE_ADDRESS_H, GT911_PIN_SDA, GT911_PIN_SCL)) {
touchDeepSleepObserver.observer.observe(&notifyDeepSleep);
touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch);
touchScreenImpl1->init();
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
touchBridge.observe(touchScreenImpl1);
#endif
} else {
LOG_ERROR("Failed to find touch controller!");
}
+2 -2
View File
@@ -26,9 +26,9 @@
#define GT911_PIN_RST 9
#endif
#define PCF85063_RTC 0x51
#define PCF8563_RTC 0x51
#define HAS_RTC 1
#define PCF85063_INT 2
#define PCF8563_INT 2
#define USE_POWERSAVE
#define SLEEP_TIME 120