initial screen based t-echo lite with card kb support build

This commit is contained in:
pelgraine
2026-04-21 14:07:06 +10:00
parent 291c42a40e
commit db0ecd3c58
18 changed files with 902 additions and 19 deletions

View File

@@ -9,7 +9,8 @@
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
identity_store(fs, "")
identity_store(fs, ""),
_saveFile(fs)
#elif defined(RP2040_PLATFORM)
identity_store(fs, "/identity")
#else
@@ -21,7 +22,8 @@ DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra
#if defined(EXTRAFS) || defined(QSPIFLASH)
DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
identity_store(fs, "")
identity_store(fs, ""),
_saveFile(fs)
#elif defined(RP2040_PLATFORM)
identity_store(fs, "/identity")
#else

View File

@@ -8,11 +8,11 @@
#define FIRMWARE_VER_CODE 11
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "19 April 2026"
#define FIRMWARE_BUILD_DATE "21 April 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v1.7"
#define FIRMWARE_VERSION "Meck v1.7.1"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)

View File

@@ -1,8 +1,8 @@
#include <Arduino.h> // needed for PlatformIO
#ifdef BLE_PIN_CODE
#if defined(ESP32) && defined(BLE_PIN_CODE)
#include <esp_bt.h> // for esp_bt_controller_mem_release (web reader WiFi)
#endif
#ifdef MECK_OTA_UPDATE
#if defined(ESP32) && defined(MECK_OTA_UPDATE)
#include <esp_ota_ops.h>
#endif
#include <Mesh.h>
@@ -890,11 +890,37 @@
}
#endif
// --- T-Echo Lite: CardKB keyboard, GxEPD2 e-ink, no touch ---
#if defined(LILYGO_TECHO_LITE)
#include "ContactsScreen.h"
#include "ChannelScreen.h"
#include "ChannelPickerScreen.h"
#include "SettingsScreen.h"
#include "RepeaterAdminScreen.h"
#include "DiscoveryScreen.h"
#include "LastHeardScreen.h"
#include "PathEditorScreen.h"
#ifdef MECK_CARDKB
#include "CardKBKeyboard.h"
static CardKBKeyboard cardkb;
static unsigned long lastCardKBProbe = 0;
#define CARDKB_PROBE_INTERVAL_MS 5000
#endif
#endif
// Board-agnostic: CPU frequency scaling and AGC reset
CPUPowerManager cpuPower;
#define AGC_RESET_INTERVAL_MS 500
static unsigned long lastAGCReset = 0;
// nRF52 RAM diagnostic
extern "C" char *sbrk(int incr);
static int dbg_free_ram() {
char top;
return &top - reinterpret_cast<char*>(sbrk(0));
}
// Believe it or not, this std C function is busted on some platforms!
static uint32_t _atoi(const char* sp) {
uint32_t n = 0;
@@ -2089,9 +2115,11 @@ void setup() {
#endif
// Initialize CardKB external keyboard (if connected via QWIIC)
#if defined(LilyGo_T5S3_EPaper_Pro) && defined(MECK_CARDKB)
#if defined(MECK_CARDKB)
if (cardkb.begin()) {
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.setCardKBDetected(true);
#endif
Serial.println("setup() - CardKB detected at 0x5F");
} else {
Serial.println("setup() - CardKB not detected (will re-probe)");
@@ -2317,8 +2345,10 @@ void setup() {
the_mesh.setVoiceEnvelopeHandler(voiceEnvelopeCallback);
#endif
Serial.printf("setup() complete — free heap: %d, largest block: %d\n",
#ifdef ESP32
Serial.printf("setup() complete - free heap: %d, largest block: %d\n",
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
#endif
MESH_DEBUG_PRINTLN("=== setup() - COMPLETE ===");
}
@@ -2851,6 +2881,17 @@ void loop() {
#endif
#endif
rtc_clock.tick();
// --- T-Echo Lite runtime diagnostic (remove after debugging) ---
{
static unsigned long lastDbg = 0;
if (millis() - lastDbg > 2000) {
Serial.printf("loop alive - free RAM: %d, screen: %s\n",
dbg_free_ram(),
ui_task.isOnHomeScreen() ? "home" : "other");
lastDbg = millis();
}
}
// Periodic AGC reset - re-assert boosted RX gain to prevent sensitivity drift
#ifdef MECK_OTA_UPDATE
if (!otaRadioPaused)
@@ -3130,12 +3171,12 @@ void loop() {
#endif // MECK_TOUCH_ENABLED
// ---------------------------------------------------------------------------
// CardKB external keyboard polling (T5S3 only, via QWIIC)
// CardKB external keyboard polling (via QWIIC)
// When VKB is active: typed characters feed into the VKB text buffer.
// When VKB is not active: navigation keys route through injectKey().
// ESC key maps to 'q' (back) when no VKB is active.
// ---------------------------------------------------------------------------
#if defined(LilyGo_T5S3_EPaper_Pro) && defined(MECK_CARDKB)
#if defined(MECK_CARDKB)
{
// Hot-plug detection: re-probe periodically
if (millis() - lastCardKBProbe >= CARDKB_PROBE_INTERVAL_MS) {
@@ -3143,7 +3184,9 @@ void loop() {
bool wasDetected = cardkb.isDetected();
bool nowDetected = cardkb.probe();
if (nowDetected != wasDetected) {
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.setCardKBDetected(nowDetected);
#endif
Serial.printf("[CardKB] %s\n", nowDetected ? "Connected" : "Disconnected");
}
}
@@ -3151,15 +3194,22 @@ void loop() {
// Poll for keypress
char ckb = cardkb.readKey();
if (ckb != 0) {
// Block input while locked (same as touch)
// Block input while locked (T5S3 only — T-Echo Lite has no lock screen yet)
#if defined(LilyGo_T5S3_EPaper_Pro)
if (!ui_task.isLocked()) {
#else
{
#endif
cpuPower.setBoost();
ui_task.keepAlive();
#if defined(LilyGo_T5S3_EPaper_Pro)
if (ui_task.isVKBActive()) {
// VKB is open — feed character into VKB text buffer
ui_task.feedCardKBChar(ckb);
} else if (ui_task.isOnHomeScreen()) {
} else
#endif
if (ui_task.isOnHomeScreen()) {
// Home screen: ESC does nothing special, letter shortcuts open tiles
if (ckb == 0x1B) {
// ESC on home — no-op (already home)
@@ -3167,8 +3217,10 @@ void loop() {
switch (ckb) {
case 'm': ui_task.gotoChannelPickerScreen(); break;
case 'c': ui_task.gotoContactsScreen(); break;
#if !defined(LILYGO_TECHO_LITE)
case 'e': ui_task.gotoTextReader(); break;
case 'n': ui_task.gotoNotesScreen(); break;
#endif
case 's': ui_task.gotoSettingsScreen(); break;
case 'f': ui_task.gotoDiscoveryScreen(); break;
case 'h': ui_task.gotoLastHeardScreen(); break;
@@ -3187,6 +3239,7 @@ void loop() {
// Notes editing/renaming: route ALL keys directly (no VKB).
// This gives: Enter=newline, arrows=cursor, printable=insert, ESC=save&exit
#if !defined(LILYGO_TECHO_LITE)
if (ui_task.isOnNotesScreen()) {
NotesScreen* notesScr = (NotesScreen*)ui_task.getNotesScreen();
if (notesScr && (notesScr->isEditing() || notesScr->isRenaming())) {
@@ -3214,6 +3267,7 @@ void loop() {
ui_task.forceRefresh();
}
}
#endif
if (!handled) {
// ESC → back (same as 'q' on T-Deck Pro) for all non-notes screens
@@ -3253,7 +3307,11 @@ void loop() {
if (the_mesh.getContactByIdx(j, ci) && strcmp(ci.name, dmName) == 0) {
char label[40];
snprintf(label, sizeof(label), "DM: %s", dmName);
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, j);
#else
ui_task.injectKey('\r'); // T-Echo Lite: compose via native handler
#endif
ui_task.clearDMUnread(j);
break;
}
@@ -3266,7 +3324,11 @@ void loop() {
if (the_mesh.getChannel(chIdx, ch)) {
char label[40];
snprintf(label, sizeof(label), "To: %s", ch.name);
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.showVirtualKeyboard(VKB_CHANNEL_MSG, label, "", 137, chIdx);
#else
ui_task.injectKey('\r'); // T-Echo Lite: compose via native handler
#endif
}
}
} else if (ui_task.isOnContactsScreen()) {
@@ -3290,7 +3352,11 @@ void loop() {
cs->getSelectedContactName(dname, sizeof(dname));
char label[40];
snprintf(label, sizeof(label), "DM: %s", dname);
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, idx);
#else
ui_task.injectKey('\r'); // T-Echo Lite: compose via native handler
#endif
}
} else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) {
ui_task.gotoRepeaterAdmin(idx);
@@ -3310,9 +3376,17 @@ void loop() {
if (admin) {
RepeaterAdminScreen::AdminState astate = admin->getState();
if (astate == RepeaterAdminScreen::STATE_PASSWORD_ENTRY) {
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.showVirtualKeyboard(VKB_ADMIN_PASSWORD, "Admin Password", "", 32);
#else
ui_task.injectKey('\r');
#endif
} else {
#if defined(LilyGo_T5S3_EPaper_Pro)
ui_task.showVirtualKeyboard(VKB_ADMIN_CLI, "Admin Command", "", 137);
#else
ui_task.injectKey('\r');
#endif
}
}
} else if (ui_task.isOnPathEditor()) {

View File

@@ -5,19 +5,22 @@
// Polls 0x5F on the shared I2C bus via QWIIC connector.
// Maps CardKB special key codes to Meck key constants.
//
// Platform support:
// - ESP32/ESP32-S3 (T5S3, T-Deck Pro): Wire.begin(SDA, SCL)
// - nRF52840 (T-Echo Lite): Wire.begin() uses variant.h pins
//
// Usage:
// CardKBKeyboard cardkb;
// if (cardkb.begin()) { /* detected */ }
// char key = cardkb.readKey(); // returns 0 if no key
// =============================================================================
#if defined(LilyGo_T5S3_EPaper_Pro) && defined(MECK_CARDKB)
#if defined(MECK_CARDKB)
#ifndef CARDKB_KEYBOARD_H
#define CARDKB_KEYBOARD_H
#include <Arduino.h>
#include <Wire.h>
#include "variant.h" // For I2C_SDA, I2C_SCL (bus recovery)
// I2C address (defined in variant.h, fallback here)
#ifndef CARDKB_I2C_ADDR
@@ -75,7 +78,13 @@ public:
_errorCount++;
if (_errorCount >= 3) {
// I2C bus may be stuck — re-init to recover
#if defined(ESP32)
// ESP32: Wire.begin() accepts explicit SDA/SCL pins
Wire.begin(I2C_SDA, I2C_SCL);
#else
// nRF52: Wire.begin() uses PIN_WIRE_SDA/SCL from variant.h
Wire.begin();
#endif
Wire.setClock(100000);
_pollInterval = 500; // Back off for 500ms
_errorCount = 0;
@@ -119,4 +128,4 @@ private:
};
#endif // CARDKB_KEYBOARD_H
#endif // LilyGo_T5S3_EPaper_Pro && MECK_CARDKB
#endif // MECK_CARDKB

View File

@@ -1,7 +1,9 @@
#include "UITask.h"
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#if !defined(LILYGO_TECHO_LITE)
#include "NotesScreen.h"
#endif
#include "RepeaterAdminScreen.h"
#include "PathEditorScreen.h"
#include "DiscoveryScreen.h"
@@ -56,7 +58,9 @@
#include "ChannelScreen.h"
#include "ChannelPickerScreen.h"
#include "ContactsScreen.h"
#if !defined(LILYGO_TECHO_LITE)
#include "TextReaderScreen.h"
#endif
#include "SettingsScreen.h"
#ifdef MECK_AUDIO_VARIANT
#include "AudiobookPlayerScreen.h"
@@ -1300,8 +1304,13 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
((ChannelPickerScreen*)channel_picker_screen)->setChannelScreen((ChannelScreen*)channel_screen);
contacts_screen = new ContactsScreen(this, &rtc_clock);
((ContactsScreen*)contacts_screen)->setDMUnreadPtr(_dmUnread);
#if !defined(LILYGO_TECHO_LITE)
text_reader = new TextReaderScreen(this, node_prefs);
notes_screen = new NotesScreen(this, node_prefs);
#else
text_reader = nullptr; // T-Echo Lite: excluded to save RAM (256KB nRF52)
notes_screen = nullptr;
#endif
settings_screen = new SettingsScreen(this, &rtc_clock, node_prefs);
repeater_admin = nullptr; // Lazy-initialized on first use to preserve heap for audio
path_editor = nullptr; // Lazy-initialized on first use from contacts screen
@@ -1678,6 +1687,7 @@ void UITask::loop() {
c = checkDisplayOn(KEY_NEXT);
} else {
// Navigate back: reader reading→file list, file list→home, others→home
#if !defined(LILYGO_TECHO_LITE)
if (isOnTextReader()) {
TextReaderScreen* reader = (TextReaderScreen*)text_reader;
if (reader && reader->isReading()) {
@@ -1695,7 +1705,9 @@ void UITask::loop() {
gotoHomeScreen();
}
c = 0;
} else if (isOnChannelPickerScreen()) {
} else
#endif
if (isOnChannelPickerScreen()) {
gotoHomeScreen(); // picker → home
c = 0;
} else if (isOnChannelScreen()) {
@@ -2336,12 +2348,14 @@ void UITask::onVKBSubmit() {
break;
}
case VKB_NOTES: {
#if !defined(LILYGO_TECHO_LITE)
NotesScreen* notes = (NotesScreen*)getNotesScreen();
if (notes && strlen(text) > 0) {
for (int i = 0; text[i]; i++) {
notes->handleInput(text[i]);
}
}
#endif
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
break;
}
@@ -2403,6 +2417,7 @@ void UITask::onVKBSubmit() {
}
#endif
case VKB_TEXT_PAGE: {
#if !defined(LILYGO_TECHO_LITE)
if (strlen(text) > 0) {
int pageNum = atoi(text);
TextReaderScreen* reader = (TextReaderScreen*)getTextReaderScreen();
@@ -2410,6 +2425,7 @@ void UITask::onVKBSubmit() {
reader->gotoPage(pageNum);
}
}
#endif
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
break;
}
@@ -2622,6 +2638,8 @@ void UITask::gotoContactsScreen() {
}
void UITask::gotoTextReader() {
if (!text_reader) return; // Not available on this platform
#if !defined(LILYGO_TECHO_LITE)
TextReaderScreen* reader = (TextReaderScreen*)text_reader;
if (_display != NULL) {
reader->enter(*_display);
@@ -2632,9 +2650,12 @@ void UITask::gotoTextReader() {
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
#endif
}
void UITask::gotoNotesScreen() {
if (!notes_screen) return; // Not available on this platform
#if !defined(LILYGO_TECHO_LITE)
NotesScreen* notes = (NotesScreen*)notes_screen;
if (_display != NULL) {
notes->enter(*_display);
@@ -2649,6 +2670,7 @@ void UITask::gotoNotesScreen() {
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
#endif
}
void UITask::gotoSettingsScreen() {

66
patch_nrf52_bsp.py Normal file
View File

@@ -0,0 +1,66 @@
"""
patch_nrf52_bsp.py — Pre-build BSP patches for nRF52 Meck builds
Patches the Adafruit nRF52 BSP's LittleFS File class to add a default
constructor. BSP 1.7.0 removed the default File() constructor, but the
Meck screen headers (NotesScreen, TextReaderScreen, EpubZipReader) have
File member variables that need default construction.
Runs automatically before each build via extra_scripts in platformio.ini.
Idempotent — safe to run repeatedly.
"""
Import("env")
import os
framework_dir = env.PioPlatform().get_package_dir("framework-arduinoadafruitnrf52")
lfs_src = os.path.join(framework_dir, "libraries", "Adafruit_LittleFS", "src")
# -------------------------------------------------------------------------
# 1. Patch header: add File() default constructor declaration
# -------------------------------------------------------------------------
header_path = os.path.join(lfs_src, "Adafruit_LittleFS_File.h")
with open(header_path, "r") as f:
h = f.read()
if "File ();" not in h and "File();" not in h:
h = h.replace(
"File (Adafruit_LittleFS &fs);",
"File (Adafruit_LittleFS &fs);\n File (); // Meck nRF52 compat"
)
with open(header_path, "w") as f:
f.write(h)
print("LittleFS File patch: Added default constructor declaration")
else:
print("LittleFS File patch: OK - header already patched")
# -------------------------------------------------------------------------
# 2. Patch source: add File() default constructor implementation
# Uses C++11 delegating constructor → File(InternalFS)
# -------------------------------------------------------------------------
source_path = os.path.join(lfs_src, "Adafruit_LittleFS_File.cpp")
with open(source_path, "r") as f:
s = f.read()
if "File::File()" not in s:
# Locate InternalFileSystem header (Adafruit BSP has a known typo: "Sytem")
ifs_include = None
for dirname in ["InternalFileSytem", "InternalFileSystem"]:
candidate = os.path.join(framework_dir, "libraries", dirname, "src")
if os.path.isdir(candidate):
rel = os.path.relpath(candidate, lfs_src).replace("\\", "/")
ifs_include = rel + "/InternalFileSystem.h"
break
if ifs_include:
s += "\n// Meck nRF52 compat: default File() constructor\n"
s += '#include "' + ifs_include + '"\n'
s += "File::File() : File(InternalFS) {}\n"
with open(source_path, "w") as f:
f.write(s)
print("LittleFS File patch: Added default constructor implementation")
else:
print("LittleFS File patch: WARNING - could not find InternalFileSystem")
else:
print("LittleFS File patch: OK - source already patched")

View File

@@ -130,7 +130,7 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code);
// If we want to control BLE LED ourselves, uncomment this:
// Bluefruit.autoConnLed(false);
Bluefruit.autoConnLed(false);
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.begin();

View File

@@ -25,6 +25,10 @@ bool GxEPDDisplay::begin() {
// Tell GxEPD2 to use our SPI instance
// Using slower speed (4MHz) for reliable e-ink communication
display.epd2.selectSPI(displaySpi, SPISettings(4000000, MSBFIRST, SPI_MODE0));
#elif defined(LILYGO_TECHO)
// T-Echo Lite: display on SPI1 (pins 19/20), LoRa on SPI (pins 13/15/17)
SPI1.begin();
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
#endif
// Initialize with:
@@ -35,10 +39,15 @@ bool GxEPDDisplay::begin() {
display.init(115200, true, 2, false);
display.setRotation(DISPLAY_ROTATION);
setTextSize(1); // Default to size 1
#ifdef EINK_FULL_REFRESH_ONLY
display.setFullWindow();
display.fillScreen(GxEPD_WHITE);
display.display(false); // Full refresh (SSD1681 doesn't support partial)
#else
display.setPartialWindow(0, 0, display.width(), display.height());
display.fillScreen(GxEPD_WHITE);
display.display(true);
#endif
#if DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, LOW);
@@ -238,7 +247,11 @@ uint16_t GxEPDDisplay::getTextWidth(const char* str) {
void GxEPDDisplay::endFrame() {
uint32_t crc = display_crc.finalize();
if (crc != last_display_crc_value) {
display.display(true); // Partial refresh
#ifdef EINK_FULL_REFRESH_ONLY
display.display(false); // Full refresh (SSD1681 doesn't support partial)
#else
display.display(true); // Partial refresh
#endif
last_display_crc_value = crc;
}
}

View File

@@ -0,0 +1,15 @@
#pragma once
// CPUPowerManager.h — nRF52 no-op stub
// nRF52840 runs at fixed 64 MHz; no frequency scaling available.
// All methods are empty so main.cpp compiles without #ifdef guards.
class CPUPowerManager {
public:
void begin() {}
void loop() {}
void setBoost() {}
void setIdle() {}
void setLowPower() {}
void clearLowPower() {}
int getFrequencyMHz() { return 64; }
};

View File

@@ -0,0 +1,19 @@
#pragma once
// FS.h — nRF52 compatibility stub
// ESP32 Arduino core provides this as the base filesystem abstraction.
// On nRF52, File and filesystem types come from Adafruit_LittleFS.
// This stub exists solely to satisfy #include <FS.h> in shared headers.
#include <Arduino.h>
#include <time.h> // struct tm, gmtime — implicit on ESP32, needs explicit on nRF52
// ESP32 FS.h defines these mode strings; some shared code references them
#ifndef FILE_READ
#define FILE_READ "r"
#endif
#ifndef FILE_WRITE
#define FILE_WRITE "w"
#endif
#ifndef FILE_APPEND
#define FILE_APPEND "a"
#endif

View File

@@ -0,0 +1,43 @@
#pragma once
// SD.h — nRF52 compatibility stub for Meck
// Maps Arduino SD API to Adafruit InternalFS (LittleFS on QSPI flash).
// T-Echo Lite has no SD card slot; file operations use internal flash.
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
#include <time.h> // struct tm, gmtime — implicit on ESP32, explicit on nRF52
// ESP32 SD uses string file modes; define them here for compile compatibility
#ifndef FILE_READ
#define FILE_READ "r"
#endif
#ifndef FILE_WRITE
#define FILE_WRITE "w"
#endif
class SDClass {
public:
// InternalFS is already initialised by main — begin() is a no-op
bool begin(uint8_t cs = 0) { return true; }
// Accept any extra args (cs, SPI, freq) without complaint
template<typename... Args>
bool begin(Args...) { return true; }
bool exists(const char* path) { return InternalFS.exists(path); }
bool remove(const char* path) { return InternalFS.remove(path); }
bool mkdir(const char* path) { return InternalFS.mkdir(path); }
// String mode overload — matches ESP32 SD API (FILE_READ="r", FILE_WRITE="w", "r+")
Adafruit_LittleFS_Namespace::File open(const char* path, const char* mode = "r") {
uint8_t m = FILE_O_READ;
if (mode) {
if (mode[0] == 'w') m = FILE_O_WRITE;
else if (mode[0] == 'r' && mode[1] == '+') m = FILE_O_WRITE;
}
return InternalFS.open(path, m);
}
};
// Static instance per translation unit — no state (just forwards to InternalFS singleton)
static SDClass SD;

View File

@@ -0,0 +1,54 @@
#include <Arduino.h>
#include <Wire.h>
#include "TechoBoard.h"
#ifdef LILYGO_TECHO
void TechoBoard::begin() {
NRF52Board::begin();
// Configure battery measurement control BEFORE Wire.begin()
// to ensure P0.02 is not claimed by another peripheral
pinMode(PIN_VBAT_MEAS_EN, OUTPUT);
digitalWrite(PIN_VBAT_MEAS_EN, LOW);
pinMode(PIN_VBAT_READ, INPUT);
Wire.begin();
pinMode(SX126X_POWER_EN, OUTPUT);
digitalWrite(SX126X_POWER_EN, HIGH);
delay(10);
}
uint16_t TechoBoard::getBattMilliVolts() {
// Use LilyGo's exact ADC configuration
analogReference(AR_INTERNAL_3_0);
analogReadResolution(12);
// Enable battery voltage divider (MOSFET gate on P0.31)
pinMode(PIN_VBAT_MEAS_EN, OUTPUT);
digitalWrite(PIN_VBAT_MEAS_EN, HIGH);
// Reclaim P0.02 for analog input (in case another peripheral touched it)
pinMode(PIN_VBAT_READ, INPUT);
delay(10); // let divider + ADC settle
// Read and average (matching LilyGo's approach)
uint32_t sum = 0;
for (int i = 0; i < 8; i++) {
sum += analogRead(PIN_VBAT_READ);
delayMicroseconds(100);
}
uint16_t adc = sum / 8;
// Disable divider to save power
digitalWrite(PIN_VBAT_MEAS_EN, LOW);
// LilyGo's exact formula: adc * (3000.0 / 4096.0) * 2.0
// = adc * 0.73242188 * 2.0 = adc * 1.46484375
uint16_t millivolts = (uint16_t)((float)adc * (3000.0f / 4096.0f) * 2.0f);
return millivolts;
}
#endif

View File

@@ -0,0 +1,43 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
#include <helpers/NRF52Board.h>
// ============================================================
// T-Echo Lite battery pins — hardcoded from LilyGo t_echo_lite_config.h
// NOT using any defines from variant.h for battery measurement
// ============================================================
#define PIN_VBAT_READ _PINNUM(0, 2) // BATTERY_ADC_DATA
#define PIN_VBAT_MEAS_EN _PINNUM(0, 31) // BATTERY_MEASUREMENT_CONTROL
class TechoBoard : public NRF52BoardDCDC {
public:
TechoBoard() {}
void begin();
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override {
return "LilyGo T-Echo Lite";
}
void powerOff() override {
digitalWrite(PIN_VBAT_MEAS_EN, LOW);
#ifdef LED_RED
digitalWrite(LED_RED, LOW);
#endif
#ifdef LED_GREEN
digitalWrite(LED_GREEN, LOW);
#endif
#ifdef LED_BLUE
digitalWrite(LED_BLUE, LOW);
#endif
#ifdef DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, LOW);
#endif
#ifdef PIN_PWR_EN
digitalWrite(PIN_PWR_EN, LOW);
#endif
sd_power_system_off();
}
};

View File

@@ -0,0 +1,238 @@
; =============================================================================
; LilyGo T-Echo Lite — Meck variant configuration
;
; nRF52840 + SX1262 + GxEPD2 1.22" e-ink (176×192, GDEM0122T61/SSD1681)
; + CardKB via QWIIC (0x5F) + optional L76K GPS
;
; Display: GxEPD2_122_T61 — full refresh only (~2s), no fast/partial refresh.
; UI must minimise unnecessary redraws.
; Scale factors: 1.5×/2.0× give ~117×96 virtual coordinate space.
;
; Platform: nRF52 (Adafruit nRF52 Arduino)
; Board JSON: boards/t-echo.json (nRF52840 PCA10056 compatible)
; =============================================================================
; --- Base configuration for all T-Echo Lite Meck builds (with display) ---
[lilygo_techo_lite_meck]
extends = nrf52_base
board = t-echo
board_build.ldscript = boards/nrf52840_s140_v6.ld
extra_scripts = pre:patch_nrf52_bsp.py
build_flags = ${nrf52_base.build_flags}
-I variants/lilygo_techo_lite
-I src/helpers/nrf52
-I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-D LILYGO_TECHO
-D LILYGO_TECHO_LITE
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_POWER_EN=30
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
; nRF52 compatibility — no PSRAM, no SD card, fallback GPS baud for CLI code paths
-D ps_calloc=calloc
-D ps_malloc=malloc
-D GPS_BAUDRATE=9600
-D SDCARD_CS=-1
-D ROW_AUTO_LOCK=255
; Display — GxEPD2 1.22" e-ink (176×192, SSD1681)
-D DISPLAY_CLASS=GxEPDDisplay
-D EINK_DISPLAY_MODEL=GxEPD2_122_T61
-D EINK_SCALE_X=1.5f
-D EINK_SCALE_Y=2.0f
-D EINK_X_OFFSET=0
-D EINK_Y_OFFSET=10
-D DISPLAY_ROTATION=4
-D EINK_FULL_REFRESH_ONLY=1
-D AUTO_OFF_MILLIS=0
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp>
+<TechoBoard.cpp>
+<helpers/sensors/EnvironmentSensorManager.cpp>
+<helpers/ui/GxEPDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../variants/lilygo_techo_lite>
lib_deps =
${nrf52_base.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit BME280 Library @ ^2.3.0
https://github.com/SoulOfNoob/GxEPD2.git
bakercp/CRC32 @ ^2.0.0
debug_tool = jlink
upload_protocol = nrfutil
; =============================================================================
; Build Environments
; =============================================================================
; --- BLE Companion Radio (no GPS) ---
; Pairs with MeshCore companion app over Bluetooth.
; CardKB provides on-device text input for standalone messaging.
; No GPS — time synced via BLE companion or serial CLI.
[env:meck_techo_lite_ble]
extends = lilygo_techo_lite_meck
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${lilygo_techo_lite_meck.build_flags}
-I src/helpers/ui
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=500
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=64
-D MECK_CARDKB
-D UI_RECENT_LIST_SIZE=9
-D UI_SENSORS_PAGE=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D AUTO_SHUTDOWN_MILLIVOLTS=3300
build_src_filter = ${lilygo_techo_lite_meck.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${lilygo_techo_lite_meck.lib_deps}
densaugeo/base64 @ ~1.4.0
; --- BLE Companion Radio (with GPS) ---
; Same as above + L76K GPS for location and time sync.
; Requires external GPS module connected to UART1 (TX=P0.29, RX=P1.10).
[env:meck_techo_lite_gps_ble]
extends = lilygo_techo_lite_meck
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${lilygo_techo_lite_meck.build_flags}
-I src/helpers/ui
-I examples/companion_radio/ui-new
-D ENV_INCLUDE_GPS=1
-D GPS_BAUD_RATE=9600
-D PIN_GPS_EN=GPS_EN
-D MAX_CONTACTS=500
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=64
-D MECK_CARDKB
-D UI_RECENT_LIST_SIZE=9
-D UI_SENSORS_PAGE=1
-D AUTO_SHUTDOWN_MILLIVOLTS=3300
build_src_filter = ${lilygo_techo_lite_meck.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${lilygo_techo_lite_meck.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
densaugeo/base64 @ ~1.4.0
; --- Repeater ---
; Standalone LoRa repeater node. E-ink shows status.
; CardKB not useful here but included by base for consistency.
[env:meck_techo_lite_repeater]
extends = lilygo_techo_lite_meck
build_src_filter = ${lilygo_techo_lite_meck.build_src_filter}
+<../examples/simple_repeater>
build_flags =
${lilygo_techo_lite_meck.build_flags}
-D ADVERT_NAME='"Meck T-Echo Lite Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
; --- Room Server ---
; BBS-style message board node.
[env:meck_techo_lite_room_server]
extends = lilygo_techo_lite_meck
build_src_filter = ${lilygo_techo_lite_meck.build_src_filter}
+<../examples/simple_room_server>
build_flags =
${lilygo_techo_lite_meck.build_flags}
-D ADVERT_NAME='"Meck T-Echo Lite Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
; =============================================================================
; Headless (no display) variants
; =============================================================================
; --- Headless base (no display, no GxEPD2) ---
[lilygo_techo_lite_meck_core]
extends = nrf52_base
board = t-echo
board_build.ldscript = boards/nrf52840_s140_v6.ld
build_flags = ${nrf52_base.build_flags}
-I variants/lilygo_techo_lite
-I src/helpers/nrf52
-I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-D LILYGO_TECHO
-D LILYGO_TECHO_LITE
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_POWER_EN=30
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
; nRF52 compatibility
-D ps_calloc=calloc
-D ps_malloc=malloc
-D GPS_BAUDRATE=9600
-D SDCARD_CS=-1
-D ROW_AUTO_LOCK=255
-D DISABLE_DIAGNOSTIC_OUTPUT
-D AUTO_OFF_MILLIS=0
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp>
+<TechoBoard.cpp>
+<helpers/sensors/EnvironmentSensorManager.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../variants/lilygo_techo_lite>
lib_deps =
${nrf52_base.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit BME280 Library @ ^2.3.0
bakercp/CRC32 @ ^2.0.0
debug_tool = jlink
upload_protocol = nrfutil
; --- Headless Repeater (no display — lowest power, outdoor deployment) ---
[env:meck_techo_lite_core_repeater]
extends = lilygo_techo_lite_meck_core
build_src_filter = ${lilygo_techo_lite_meck_core.build_src_filter}
+<../examples/simple_repeater>
build_flags =
${lilygo_techo_lite_meck_core.build_flags}
-D ADVERT_NAME='"Meck T-Echo Lite Core Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; --- Headless BLE Companion (no display — phone-only UI) ---
[env:meck_techo_lite_core_ble]
extends = lilygo_techo_lite_meck_core
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${lilygo_techo_lite_meck_core.build_flags}
-D MAX_CONTACTS=500
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=234567
-D OFFLINE_QUEUE_SIZE=64
-D AUTO_SHUTDOWN_MILLIVOLTS=3300
build_src_filter = ${lilygo_techo_lite_meck_core.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
lib_deps =
${lilygo_techo_lite_meck_core.lib_deps}
densaugeo/base64 @ ~1.4.0

View File

@@ -0,0 +1,55 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
TechoBoard board;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
#ifdef ENV_INCLUDE_GPS
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors = EnvironmentSensorManager();
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {
rtc_clock.begin(Wire);
return radio.std_init(&SPI);
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
void radio_reset_agc() {
radio.setRxBoostedGainMode(true);
}

View File

@@ -0,0 +1,32 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <TechoBoard.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#include <helpers/sensors/LocationProvider.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/GxEPDDisplay.h>
#include <helpers/ui/MomentaryButton.h>
#endif
extern TechoBoard board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();
void radio_reset_agc();

View File

@@ -0,0 +1,39 @@
#include "variant.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
const int MISO = PIN_SPI1_MISO;
const int MOSI = PIN_SPI1_MOSI;
const int SCK = PIN_SPI1_SCK;
const uint32_t g_ADigitalPinMap[] = {
0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
40, 41, 42, 43, 44, 45, 46, 47
};
void initVariant() {
pinMode(PIN_PWR_EN, OUTPUT);
digitalWrite(PIN_PWR_EN, HIGH);
pinMode(PIN_BUTTON1, INPUT_PULLUP);
pinMode(PIN_BUTTON2, INPUT_PULLUP);
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
pinMode(LED_BLUE, OUTPUT);
digitalWrite(LED_BLUE, HIGH);
digitalWrite(LED_GREEN, HIGH);
digitalWrite(LED_RED, HIGH);
// pinMode(PIN_TXCO, OUTPUT);
// digitalWrite(PIN_TXCO, HIGH);
pinMode(DISP_POWER, OUTPUT);
digitalWrite(DISP_POWER, LOW);
// shutdown gps
pinMode(GPS_EN, OUTPUT);
digitalWrite(GPS_EN, LOW);
}

View File

@@ -0,0 +1,159 @@
/*
* variant.h
* Copyright (C) 2023 Seeed K.K.
* MIT License
*/
#pragma once
#define _PINNUM(port, pin) ((port) * 32 + (pin))
#include "WVariant.h"
////////////////////////////////////////////////////////////////////////////////
// Low frequency clock source
#define USE_LFXO // 32.768 kHz crystal oscillator
#define VARIANT_MCK (64000000ul)
#define WIRE_INTERFACES_COUNT (1)
////////////////////////////////////////////////////////////////////////////////
// Power
#define PIN_PWR_EN _PINNUM(0, 30) // RT9080_EN
#define BATTERY_PIN _PINNUM(0, 2)
#define ADC_MULTIPLIER (2.0F)
#define ADC_RESOLUTION (14)
#define BATTERY_SENSE_RES (12)
#define AREF_VOLTAGE (3.0)
////////////////////////////////////////////////////////////////////////////////
// Number of pins
#define PINS_COUNT (48)
#define NUM_DIGITAL_PINS (48)
#define NUM_ANALOG_INPUTS (1)
#define NUM_ANALOG_OUTPUTS (0)
////////////////////////////////////////////////////////////////////////////////
// UART pin definition
#define PIN_SERIAL1_RX PIN_GPS_TX
#define PIN_SERIAL1_TX PIN_GPS_RX
////////////////////////////////////////////////////////////////////////////////
// I2C pin definition
#define PIN_WIRE_SDA _PINNUM(1, 4) // (SDA) - per LilyGo IIC_1_SDA
#define PIN_WIRE_SCL _PINNUM(1, 2) // (SCL) - per LilyGo IIC_1_SCL
////////////////////////////////////////////////////////////////////////////////
// SPI pin definition
#define SPI_INTERFACES_COUNT (2)
#define PIN_SPI_MISO _PINNUM(0, 17) // (MISO)
#define PIN_SPI_MOSI _PINNUM(0, 15) // (MOSI)
#define PIN_SPI_SCK _PINNUM(0, 13) // (SCK)
#define PIN_SPI_NSS (-1)
////////////////////////////////////////////////////////////////////////////////
// QSPI FLASH
#define PIN_QSPI_SCK _PINNUM(0, 4)
#define PIN_QSPI_CS _PINNUM(0, 12)
#define PIN_QSPI_IO0 _PINNUM(0, 6)
#define PIN_QSPI_IO1 _PINNUM(0, 8)
#define PIN_QSPI_IO2 _PINNUM(1, 9)
#define PIN_QSPI_IO3 _PINNUM(0, 26)
#define EXTERNAL_FLASH_DEVICES ZD25WQ32CEIGR
#define EXTERNAL_FLASH_USE_QSPI
////////////////////////////////////////////////////////////////////////////////
// Builtin LEDs
#define LED_RED _PINNUM(1, 14) // LED_3
#define LED_BLUE _PINNUM(1, 5) // LED_2
#define LED_GREEN _PINNUM(1, 7) // LED_1
//#define PIN_STATUS_LED LED_BLUE
#define LED_BUILTIN (-1)
#define LED_PIN LED_BUILTIN
#define LED_STATE_ON LOW
////////////////////////////////////////////////////////////////////////////////
// Builtin buttons
#define PIN_BUTTON1 _PINNUM(0, 24) // BOOT
#define BUTTON_PIN PIN_BUTTON1
#define PIN_USER_BTN BUTTON_PIN
#define PIN_BUTTON2 _PINNUM(0, 18)
#define BUTTON_PIN2 PIN_BUTTON2
#define EXTERNAL_FLASH_DEVICES MX25R1635F
#define EXTERNAL_FLASH_USE_QSPI
////////////////////////////////////////////////////////////////////////////////
// Lora
#define USE_SX1262
#define LORA_CS _PINNUM(0, 11)
#define SX126X_POWER_EN _PINNUM(0, 30)
#define SX126X_DIO1 _PINNUM(1, 8)
#define SX126X_BUSY _PINNUM(0, 14)
#define SX126X_RESET _PINNUM(0, 7)
#define SX126X_RF_VC1 _PINNUM(0, 27)
#define SX126X_RF_VC2 _PINNUM(0, 33)
#define P_LORA_DIO_1 SX126X_DIO1
#define P_LORA_NSS LORA_CS
#define P_LORA_RESET SX126X_RESET
#define P_LORA_BUSY SX126X_BUSY
#define P_LORA_SCLK PIN_SPI_SCK
#define P_LORA_MISO PIN_SPI_MISO
#define P_LORA_MOSI PIN_SPI_MOSI
////////////////////////////////////////////////////////////////////////////////
// SPI1
#define PIN_SPI1_MISO (-1) // Not used for Display
#define PIN_SPI1_MOSI _PINNUM(0, 20)
#define PIN_SPI1_SCK _PINNUM(0, 19)
// GxEPD2 needs that for a panel that is not even used !
extern const int MISO;
extern const int MOSI;
extern const int SCK;
////////////////////////////////////////////////////////////////////////////////
// Display
// #define DISP_MISO (-1) // Not used for Display
#define DISP_MOSI _PINNUM(0, 20)
#define DISP_SCLK _PINNUM(0, 19)
#define DISP_CS _PINNUM(0, 22)
#define DISP_DC _PINNUM(0, 21)
#define DISP_RST _PINNUM(0, 28)
#define DISP_BUSY _PINNUM(0, 3)
#define DISP_POWER _PINNUM(1, 12)
// #define DISP_BACKLIGHT (-1) // Display has no backlight
#define PIN_DISPLAY_CS DISP_CS
#define PIN_DISPLAY_DC DISP_DC
#define PIN_DISPLAY_RST DISP_RST
#define PIN_DISPLAY_BUSY DISP_BUSY
////////////////////////////////////////////////////////////////////////////////
// GPS — per LilyGo t_echo_lite_config.h
// PIN_GPS_TX/RX named from GPS module's perspective
#define PIN_GPS_TX _PINNUM(0, 29) // GPS UART TX → MCU RX
#define PIN_GPS_RX _PINNUM(1, 10) // GPS UART RX ← MCU TX
#define GPS_EN _PINNUM(1, 11) // GPS RT9080 power enable
#define PIN_GPS_STANDBY _PINNUM(1, 13) // GPS wake-up
#define PIN_GPS_PPS _PINNUM(1, 15) // GPS 1PPS