mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
Compare commits
18 Commits
ota-1
...
pro_max_wi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85fff12052 | ||
|
|
adbceea176 | ||
|
|
92e7bee86d | ||
|
|
f58abfe1c6 | ||
|
|
7ed5b122c4 | ||
|
|
342cf4e745 | ||
|
|
c52a190ace | ||
|
|
a7bc7a4733 | ||
|
|
47a0d2cc95 | ||
|
|
5dda0b686e | ||
|
|
829dd3f3a6 | ||
|
|
60dcd6a89e | ||
|
|
19efb52521 | ||
|
|
81ef3ea3c5 | ||
|
|
6f07b7a372 | ||
|
|
b0f74b101a | ||
|
|
06a064538e | ||
|
|
166a433353 |
13
README.md
13
README.md
@@ -1,6 +1,6 @@
|
||||
## Meshcore + Fork = Meck
|
||||
|
||||
A fork created specifically to focus on enabling BLE & WiFi companion firmware for the LilyGo T-Deck Pro & LilyGo T5 E-Paper S3 Pro. Created with the assistance of Claude AI using Meshcore v1.11 code.
|
||||
A fork created specifically to focus on enabling BLE & WiFi companion firmware for the LilyGo T-Deck Pro & LilyGo T5 E-Paper S3 Pro. Created wholly with Claude AI using Meshcore v1.11 code. 100% vibecoded.
|
||||
|
||||
[Check out the Meck discussion channel on the MeshCore Discord](https://discord.com/channels/1343693475589263471/1460136499390447670)
|
||||
|
||||
@@ -750,13 +750,16 @@ There are a number of fairly major features in the pipeline, with no particular
|
||||
- [X] Last heard passive advert list
|
||||
- [X] Touch-to-select on contacts, discovery, settings, text reader, notes screens
|
||||
- [X] Map screen with GPS tile rendering
|
||||
- [ ] Fix M4B rendering to enable chaptered audiobook playback
|
||||
- [ ] Better JPEG and PNG decoding
|
||||
- [ ] Improve EPUB rendering and EPUB format handling
|
||||
- [X] WiFi companion environment
|
||||
- [X] OTA firmware update via phone
|
||||
- [X] DM inbox with per-contact unread indicators
|
||||
- [X] Roomserver message handling and mark-read on login
|
||||
- [ ] Fix M4B rendering to enable chaptered audiobook playback
|
||||
- [ ] Better JPEG and PNG decoding
|
||||
- [ ] Improve EPUB rendering and EPUB format handling
|
||||
- [ ] Figure out a way to silence the ringtone
|
||||
- [ ] Figure out a way to customise the ringtone
|
||||
- [ ] Customised user option for larger-font mode
|
||||
|
||||
**T5S3 E-Paper Pro:**
|
||||
- [X] Core port: display, touch input, LoRa, battery, RTC
|
||||
@@ -777,6 +780,8 @@ There are a number of fairly major features in the pipeline, with no particular
|
||||
- [X] OTA firmware update via phone (WiFi variant)
|
||||
- [X] DM inbox with per-contact unread indicators
|
||||
- [X] Roomserver message handling and mark-read on login
|
||||
- [ ] Improve EPUB rendering and EPUB format handling
|
||||
- [ ] Customised user option for larger-font mode
|
||||
|
||||
## 📞 Get Support
|
||||
|
||||
|
||||
40
boards/t-deck_pro_max.json
Normal file
40
boards/t-deck_pro_max.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"memory_type": "qio_qspi",
|
||||
"partitions": "default_16MB.csv"
|
||||
},
|
||||
"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",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "LilyGo T-Deck Pro MAX (16MB Flash 8MB QSPI PSRAM)",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://www.lilygo.cc/products/t-deck-pro",
|
||||
"vendor": "LilyGo"
|
||||
}
|
||||
@@ -268,10 +268,18 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
if (file.read((uint8_t *)&_prefs.auto_lock_minutes, sizeof(_prefs.auto_lock_minutes)) != sizeof(_prefs.auto_lock_minutes)) {
|
||||
_prefs.auto_lock_minutes = 0; // default: disabled
|
||||
}
|
||||
if (file.read((uint8_t *)&_prefs.hint_shown, sizeof(_prefs.hint_shown)) != sizeof(_prefs.hint_shown)) {
|
||||
_prefs.hint_shown = 0; // default: show boot hint
|
||||
}
|
||||
if (file.read((uint8_t *)&_prefs.large_font, sizeof(_prefs.large_font)) != sizeof(_prefs.large_font)) {
|
||||
_prefs.large_font = 0; // default: tiny font
|
||||
}
|
||||
|
||||
// Clamp to valid ranges
|
||||
if (_prefs.dark_mode > 1) _prefs.dark_mode = 0;
|
||||
if (_prefs.portrait_mode > 1) _prefs.portrait_mode = 0;
|
||||
if (_prefs.hint_shown > 1) _prefs.hint_shown = 0;
|
||||
if (_prefs.large_font > 1) _prefs.large_font = 0;
|
||||
// auto_lock_minutes: only accept known options (0, 2, 5, 10, 15, 30)
|
||||
{
|
||||
uint8_t alm = _prefs.auto_lock_minutes;
|
||||
@@ -324,6 +332,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.dark_mode, sizeof(_prefs.dark_mode)); // 98
|
||||
file.write((uint8_t *)&_prefs.portrait_mode, sizeof(_prefs.portrait_mode)); // 99
|
||||
file.write((uint8_t *)&_prefs.auto_lock_minutes, sizeof(_prefs.auto_lock_minutes)); // 100
|
||||
file.write((uint8_t *)&_prefs.hint_shown, sizeof(_prefs.hint_shown)); // 101
|
||||
file.write((uint8_t *)&_prefs.large_font, sizeof(_prefs.large_font)); // 102
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
@@ -560,12 +560,12 @@ void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, ui
|
||||
recipient.name, delay_millis, _prefs.path_hash_mode, _prefs.path_hash_mode + 1);
|
||||
// TODO: dynamic send_scope, depending on recipient and current 'home' Region
|
||||
if (send_scope.isNull()) {
|
||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||
sendFlood(pkt, delay_millis, getPathHashSize());
|
||||
} else {
|
||||
uint16_t codes[2];
|
||||
codes[0] = send_scope.calcTransportCode(pkt);
|
||||
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
|
||||
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
|
||||
sendFlood(pkt, codes, delay_millis, getPathHashSize());
|
||||
}
|
||||
}
|
||||
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||
@@ -582,12 +582,12 @@ void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pk
|
||||
|
||||
// TODO: have per-channel send_scope
|
||||
if (send_scope.isNull()) {
|
||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||
sendFlood(pkt, delay_millis, getPathHashSize());
|
||||
} else {
|
||||
uint16_t codes[2];
|
||||
codes[0] = send_scope.calcTransportCode(pkt);
|
||||
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
|
||||
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
|
||||
sendFlood(pkt, codes, delay_millis, getPathHashSize());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1240,6 +1240,7 @@ void MyMesh::begin(bool has_display) {
|
||||
if (_prefs.autoadd_max_hops > 64) _prefs.autoadd_max_hops = 0;
|
||||
if (_prefs.dark_mode > 1) _prefs.dark_mode = 0;
|
||||
if (_prefs.portrait_mode > 1) _prefs.portrait_mode = 0;
|
||||
if (_prefs.hint_shown > 1) _prefs.hint_shown = 0;
|
||||
|
||||
#ifdef BLE_PIN_CODE // 123456 by default
|
||||
if (_prefs.ble_pin == 0) {
|
||||
@@ -1489,7 +1490,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if (pkt) {
|
||||
if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop)
|
||||
unsigned long delay_millis = 0;
|
||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||
sendFlood(pkt, delay_millis, getPathHashSize());
|
||||
} else {
|
||||
sendZeroHop(pkt);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
#define FIRMWARE_VER_CODE 10
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "22 March 2026"
|
||||
#define FIRMWARE_BUILD_DATE "26 March 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v1.3"
|
||||
#define FIRMWARE_VERSION "Meck v1.4"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
@@ -150,6 +150,7 @@ protected:
|
||||
uint8_t getAutoAddMaxHops() const override;
|
||||
bool filterRecvFloodPacket(mesh::Packet* packet) override;
|
||||
|
||||
uint8_t getPathHashSize() const override { return _prefs.path_hash_mode + 1; }
|
||||
void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
|
||||
void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override;
|
||||
|
||||
|
||||
@@ -38,4 +38,40 @@ struct NodePrefs { // persisted to file
|
||||
uint8_t dark_mode; // 0=off (white bg), 1=on (black bg)
|
||||
uint8_t portrait_mode; // 0=landscape, 1=portrait — T5S3 only
|
||||
uint8_t auto_lock_minutes; // 0=disabled, 2/5/10/15/30=auto-lock after idle
|
||||
uint8_t hint_shown; // 0=show nav hint on boot, 1=already shown (dismiss permanently)
|
||||
uint8_t large_font; // 0=tiny (built-in 6x8), 1=larger (FreeSans9pt) — T-Deck Pro only
|
||||
|
||||
// --- Font helpers (inline, no overhead) ---
|
||||
// Returns the DisplayDriver text-size index for "small/body" text.
|
||||
// T-Deck Pro: 0 = built-in 6×8, 1 = FreeSans9pt.
|
||||
// T5S3: both 0 and 1 are 12pt fonts (regular vs bold) with identical line
|
||||
// height, so large_font has no layout effect there.
|
||||
inline uint8_t smallTextSize() const {
|
||||
return large_font ? 1 : 0;
|
||||
}
|
||||
|
||||
// Returns the virtual-coordinate line height matching smallTextSize().
|
||||
// T-Deck Pro size 0 → 9 (6×8 + 1px gap), size 1 → 11 (9pt ascent+descent).
|
||||
// T5S3 size 0/1 → same 12pt height → always 9 in virtual coords.
|
||||
inline int smallLineH() const {
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
return 9;
|
||||
#else
|
||||
return large_font ? 11 : 9;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Returns the Y offset for selection highlight fillRect (T-Deck Pro only).
|
||||
// Size 0 (built-in font): cursor positions at top-left, +5 offset in
|
||||
// setCursor places text below → fillRect at y+5 aligns with text.
|
||||
// Size 1 (FreeSans9pt): cursor positions at baseline, ascenders render
|
||||
// upward → fillRect must start above baseline to cover ascenders.
|
||||
// T5S3: always 0 (both sizes use baseline fonts with highlight at y).
|
||||
inline int smallHighlightOff() const {
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
return 0;
|
||||
#else
|
||||
return large_font ? -2 : 5;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
@@ -716,6 +716,12 @@ static void lastHeardToggleContact() {
|
||||
int vx, vy;
|
||||
touchToVirtual(x, y, vx, vy);
|
||||
|
||||
// Dismiss boot navigation hint on any tap
|
||||
if (ui_task.isHintActive()) {
|
||||
ui_task.dismissBootHint();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Status bar tap (top ~18 virtual units) → go home from any non-home screen ---
|
||||
// Exception: text reader reading mode uses full screen for content (no header)
|
||||
if (vy < 18 && !ui_task.isOnHomeScreen()) {
|
||||
@@ -1731,6 +1737,8 @@ void setup() {
|
||||
if (strcmp(prefs->node_name, defaultName) == 0) {
|
||||
MESH_DEBUG_PRINTLN("setup() - Default node name detected, launching onboarding");
|
||||
ui_task.gotoOnboarding();
|
||||
// Show hint immediately overlaid on the onboarding screen
|
||||
if (!prefs->hint_shown) ui_task.showBootHint(true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1763,6 +1771,19 @@ void setup() {
|
||||
MESH_DEBUG_PRINTLN("setup() - BLE disabled at boot (standalone mode)");
|
||||
#endif
|
||||
|
||||
// Alarm clock: create at boot so config is loaded, background alarm check
|
||||
// works from first loop(), and the bell indicator is visible immediately.
|
||||
// Audio object is NOT created here — lazy-init when alarm fires or user opens player.
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
{
|
||||
AlarmScreen* alarmScr = new AlarmScreen(&ui_task);
|
||||
alarmScr->setSDReady(sdCardReady);
|
||||
// Audio pointer set later when needed (fireAlarm or 'k'/'p' key)
|
||||
ui_task.setAlarmScreen(alarmScr);
|
||||
Serial.printf("ALARM: Boot init, %d alarms enabled\n", alarmScr->enabledCount());
|
||||
}
|
||||
#endif
|
||||
|
||||
Serial.printf("setup() complete — free heap: %d, largest block: %d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
MESH_DEBUG_PRINTLN("=== setup() - COMPLETE ===");
|
||||
@@ -1899,6 +1920,61 @@ void loop() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Alarm clock: background alarm check + audio tick
|
||||
#if defined(LilyGo_TDeck_Pro) && defined(MECK_AUDIO_VARIANT)
|
||||
{
|
||||
AlarmScreen* alarmScr = (AlarmScreen*)ui_task.getAlarmScreen();
|
||||
if (alarmScr) {
|
||||
// Service alarm audio decode (like audiobook audioTick)
|
||||
alarmScr->alarmAudioTick();
|
||||
if (alarmScr->isAlarmAudioActive()) {
|
||||
cpuPower.setBoost();
|
||||
}
|
||||
|
||||
// Periodic alarm check (~every 10 seconds)
|
||||
static unsigned long lastAlarmCheck = 0;
|
||||
if (millis() - lastAlarmCheck > ALARM_CHECK_INTERVAL_MS) {
|
||||
lastAlarmCheck = millis();
|
||||
uint32_t rtcNow = the_mesh.getRTCClock()->getCurrentTime();
|
||||
int fireSlot = alarmScr->checkAlarms(rtcNow, the_mesh.getNodePrefs()->utc_offset_hours);
|
||||
if (fireSlot >= 0 && !alarmScr->isRinging()) {
|
||||
// If audiobook is playing, the alarm will take over the shared Audio*
|
||||
// object. The audiobook auto-saves bookmarks every 30s, so at most
|
||||
// 30s of position is lost. User can resume from audiobook player after.
|
||||
AudiobookPlayerScreen* abPlayer =
|
||||
(AudiobookPlayerScreen*)ui_task.getAudiobookScreen();
|
||||
if (abPlayer && abPlayer->isAudioActive()) {
|
||||
Serial.println("ALARM: Audiobook active — alarm taking over Audio");
|
||||
}
|
||||
|
||||
// Ensure Audio object is shared
|
||||
if (!audio) audio = new Audio();
|
||||
alarmScr->setAudio(audio);
|
||||
|
||||
// Fire the alarm
|
||||
alarmScr->fireAlarm(fireSlot);
|
||||
alarmScr->setLastFiredEpoch(fireSlot, rtcNow);
|
||||
|
||||
// Let audio buffer fill before e-ink refresh blocks SPI
|
||||
for (int i = 0; i < 50; i++) {
|
||||
alarmScr->alarmAudioTick();
|
||||
delay(2);
|
||||
}
|
||||
|
||||
// Switch UI to alarm screen (ringing mode)
|
||||
ui_task.gotoAlarmScreen();
|
||||
|
||||
// Wake display if asleep
|
||||
ui_task.keepAlive();
|
||||
ui_task.forceRefresh();
|
||||
|
||||
Serial.printf("ALARM: Fired slot %d, switched to ringing screen\n", fireSlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// SMS: poll for incoming messages from modem
|
||||
#ifdef HAS_4G_MODEM
|
||||
{
|
||||
@@ -2487,10 +2563,48 @@ void handleKeyboardInput() {
|
||||
// Block all keyboard input while lock screen is active.
|
||||
// Still read the key above to clear the TCA8418 buffer.
|
||||
if (ui_task.isLocked()) return;
|
||||
|
||||
// Alt+B backlight toggle (T-Deck Pro MAX — working front-light on IO41)
|
||||
// Cycles: off → low → medium → full → off
|
||||
// Works from any screen; processed before anything else so it never
|
||||
// leaks into compose buffers or screen handlers.
|
||||
#ifdef LilyGo_TDeck_Pro_Max
|
||||
if (key == KB_KEY_BACKLIGHT) {
|
||||
static uint8_t blLevel = 0; // 0=off, 1=low, 2=med, 3=full
|
||||
blLevel = (blLevel + 1) & 3;
|
||||
const uint8_t levels[] = {0, 64, 160, 255};
|
||||
board.backlightSetBrightness(levels[blLevel]);
|
||||
Serial.printf("Backlight: level %d (%d/255)\n", blLevel, levels[blLevel]);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Dismiss boot navigation hint on any keypress
|
||||
if (ui_task.isHintActive()) {
|
||||
ui_task.dismissBootHint();
|
||||
return; // Consume the keypress (don't act on it)
|
||||
}
|
||||
|
||||
Serial.printf("handleKeyboardInput: key='%c' (0x%02X) composeMode=%d\n",
|
||||
key >= 32 ? key : '?', key, composeMode);
|
||||
|
||||
// Alarm ringing: ANY key dismisses (highest priority after lock screen)
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
{
|
||||
AlarmScreen* alarmScr = (AlarmScreen*)ui_task.getAlarmScreen();
|
||||
if (alarmScr && alarmScr->isRinging()) {
|
||||
if (key == 'z') {
|
||||
alarmScr->handleInput('z'); // Snooze
|
||||
} else {
|
||||
alarmScr->dismiss(); // Any other key = dismiss
|
||||
}
|
||||
ui_task.gotoHomeScreen();
|
||||
ui_task.forceRefresh();
|
||||
return; // Consume the key
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (composeMode) {
|
||||
// Emoji picker sub-mode
|
||||
if (emojiPickerMode) {
|
||||
@@ -2602,9 +2716,28 @@ void handleKeyboardInput() {
|
||||
|
||||
// A/D keys switch channels (only when buffer is empty, not in DM mode)
|
||||
if ((key == 'a') && composePos == 0 && !composeDM) {
|
||||
// Previous channel
|
||||
// Previous channel — skip gaps
|
||||
if (composeChannelIdx > 0) {
|
||||
composeChannelIdx--;
|
||||
bool found = false;
|
||||
for (uint8_t prev = composeChannelIdx - 1; ; prev--) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(prev, ch) && ch.name[0] != '\0') {
|
||||
composeChannelIdx = prev;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (prev == 0) break;
|
||||
}
|
||||
if (!found) {
|
||||
// Wrap to last valid channel
|
||||
for (uint8_t i = MAX_GROUP_CHANNELS - 1; i > 0; i--) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
|
||||
composeChannelIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Wrap to last valid channel
|
||||
for (uint8_t i = MAX_GROUP_CHANNELS - 1; i > 0; i--) {
|
||||
@@ -2621,12 +2754,17 @@ void handleKeyboardInput() {
|
||||
}
|
||||
|
||||
if ((key == 'd') && composePos == 0 && !composeDM) {
|
||||
// Next channel
|
||||
ChannelDetails ch;
|
||||
uint8_t nextIdx = composeChannelIdx + 1;
|
||||
if (the_mesh.getChannel(nextIdx, ch) && ch.name[0] != '\0') {
|
||||
composeChannelIdx = nextIdx;
|
||||
} else {
|
||||
// Next channel — skip gaps
|
||||
bool found = false;
|
||||
for (uint8_t next = composeChannelIdx + 1; next < MAX_GROUP_CHANNELS; next++) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(next, ch) && ch.name[0] != '\0') {
|
||||
composeChannelIdx = next;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
composeChannelIdx = 0; // Wrap to first channel
|
||||
}
|
||||
Serial.printf("Compose: Channel switched to %d\n", composeChannelIdx);
|
||||
@@ -3074,7 +3212,7 @@ void handleKeyboardInput() {
|
||||
Serial.printf("Audiobook: lazy init - free heap: %d, largest block: %d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
audio = new Audio();
|
||||
AudiobookPlayerScreen* abScreen = new AudiobookPlayerScreen(&ui_task, audio);
|
||||
AudiobookPlayerScreen* abScreen = new AudiobookPlayerScreen(&ui_task, audio, the_mesh.getNodePrefs());
|
||||
abScreen->setSDReady(sdCardReady);
|
||||
ui_task.setAudiobookScreen(abScreen);
|
||||
Serial.printf("Audiobook: init complete - free heap: %d\n", ESP.getFreeHeap());
|
||||
@@ -3083,6 +3221,23 @@ void handleKeyboardInput() {
|
||||
break;
|
||||
#endif
|
||||
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
case 'k':
|
||||
// Open alarm clock (screen created at boot; just ensure Audio* is available)
|
||||
Serial.println("Opening alarm clock");
|
||||
if (!audio) {
|
||||
Serial.printf("Alarm: lazy init Audio - free heap: %d, largest block: %d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
audio = new Audio();
|
||||
}
|
||||
{
|
||||
AlarmScreen* alarmScr = (AlarmScreen*)ui_task.getAlarmScreen();
|
||||
if (alarmScr) alarmScr->setAudio(audio);
|
||||
}
|
||||
ui_task.gotoAlarmScreen();
|
||||
break;
|
||||
#endif
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
case 't':
|
||||
// Open SMS (4G variant only)
|
||||
@@ -3187,6 +3342,9 @@ void handleKeyboardInput() {
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
|| ui_task.isOnMapScreen()
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
) {
|
||||
ui_task.injectKey('s'); // Pass directly for scrolling
|
||||
} else {
|
||||
@@ -3203,6 +3361,9 @@ void handleKeyboardInput() {
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
|| ui_task.isOnMapScreen()
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
) {
|
||||
ui_task.injectKey('w'); // Pass directly for scrolling
|
||||
} else {
|
||||
@@ -3213,7 +3374,11 @@ void handleKeyboardInput() {
|
||||
|
||||
case 'a':
|
||||
// Navigate left or switch channel (on channel screen)
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()) {
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
) {
|
||||
ui_task.injectKey('a'); // Pass directly for channel/contacts switching
|
||||
} else {
|
||||
Serial.println("Nav: Previous");
|
||||
@@ -3223,7 +3388,11 @@ void handleKeyboardInput() {
|
||||
|
||||
case 'd':
|
||||
// Navigate right or switch channel (on channel screen)
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()) {
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
) {
|
||||
ui_task.injectKey('d'); // Pass directly for channel/contacts switching
|
||||
} else {
|
||||
Serial.println("Nav: Next");
|
||||
@@ -3437,9 +3606,9 @@ void handleKeyboardInput() {
|
||||
break;
|
||||
|
||||
case 'f':
|
||||
// Start discovery scan from contacts screen, or rescan on discovery screen
|
||||
if (ui_task.isOnContactsScreen()) {
|
||||
Serial.println("Contacts: Starting discovery scan...");
|
||||
// Start discovery scan from home/contacts screen, or rescan on discovery screen
|
||||
if (ui_task.isOnContactsScreen() || ui_task.isOnHomeScreen()) {
|
||||
Serial.println("Starting discovery scan...");
|
||||
the_mesh.startDiscovery();
|
||||
ui_task.gotoDiscoveryScreen();
|
||||
} else if (ui_task.isOnDiscoveryScreen()) {
|
||||
@@ -3501,6 +3670,24 @@ void handleKeyboardInput() {
|
||||
ui_task.gotoContactsScreen();
|
||||
break;
|
||||
}
|
||||
// Alarm screen: Q/backspace routing depends on sub-mode
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
if (ui_task.isOnAlarmScreen()) {
|
||||
AlarmScreen* alarmScr = (AlarmScreen*)ui_task.getAlarmScreen();
|
||||
if (alarmScr && alarmScr->isRinging()) {
|
||||
alarmScr->dismiss();
|
||||
ui_task.gotoHomeScreen();
|
||||
} else if (alarmScr && alarmScr->getMode() != AlarmScreen::ALARM_LIST) {
|
||||
// In edit/picker/digit mode — pass to screen (Q = back to list, backspace = delete)
|
||||
ui_task.injectKey(key);
|
||||
} else {
|
||||
// On alarm list — go home
|
||||
Serial.println("Nav: Alarm -> Home");
|
||||
ui_task.gotoHomeScreen();
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
// Last Heard: Q goes back to home
|
||||
if (ui_task.isOnLastHeardScreen()) {
|
||||
Serial.println("Nav: Last Heard -> Home");
|
||||
@@ -3543,6 +3730,13 @@ void handleKeyboardInput() {
|
||||
ui_task.injectKey(key);
|
||||
break;
|
||||
}
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
// Pass unhandled keys to alarm screen (digits for time entry, o for toggle)
|
||||
if (ui_task.isOnAlarmScreen()) {
|
||||
ui_task.injectKey(key);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
Serial.printf("Unhandled key in normal mode: '%c' (0x%02X)\n", key, key);
|
||||
break;
|
||||
}
|
||||
|
||||
1084
examples/companion_radio/ui-new/Alarmscreen.h
Normal file
1084
examples/companion_radio/ui-new/Alarmscreen.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,8 @@
|
||||
// JPEG decoder for cover art — JPEGDEC by bitbank2
|
||||
#include <JPEGDEC.h>
|
||||
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
// Forward declarations
|
||||
class UITask;
|
||||
|
||||
@@ -151,6 +153,7 @@ public:
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
NodePrefs* _prefs;
|
||||
Audio* _audio;
|
||||
Mode _mode;
|
||||
bool _sdReady;
|
||||
@@ -1193,10 +1196,10 @@ private:
|
||||
}
|
||||
|
||||
// Switch to tiny font for file list (6x8 built-in)
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs ? _prefs->smallTextSize() : 0);
|
||||
|
||||
// Calculate visible items — tiny font uses ~8 virtual units per line
|
||||
int itemHeight = 8;
|
||||
// Calculate visible items
|
||||
int itemHeight = (_prefs ? _prefs->smallLineH() : 9) - 1;
|
||||
int listTop = 13;
|
||||
int listBottom = display.height() - 14; // Reserve footer space
|
||||
int visibleItems = (listBottom - listTop) / itemHeight;
|
||||
@@ -1208,7 +1211,7 @@ private:
|
||||
_scrollOffset = _selectedFile - visibleItems + 1;
|
||||
}
|
||||
|
||||
// Approx chars that fit in tiny font (~36 on 128 virtual width)
|
||||
// Approx chars for suffix/type tag sizing (still needed for type tag assembly)
|
||||
const int charsPerLine = 36;
|
||||
|
||||
// Draw file list
|
||||
@@ -1218,9 +1221,7 @@ private:
|
||||
|
||||
if (fileIdx == _selectedFile) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
// setCursor adds +5 to y internally, but fillRect does not.
|
||||
// Offset fillRect by +5 to align highlight bar with text.
|
||||
display.fillRect(0, y + 5, display.width(), itemHeight - 1);
|
||||
display.fillRect(0, y + (_prefs ? _prefs->smallHighlightOff() : 5), display.width(), itemHeight - 1);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
@@ -1231,29 +1232,15 @@ private:
|
||||
char fullLine[96];
|
||||
|
||||
if (fe.isDir) {
|
||||
// Directory entry: show as "/ FolderName" or just ".."
|
||||
if (fe.name == "..") {
|
||||
snprintf(fullLine, sizeof(fullLine), ".. (up)");
|
||||
} else {
|
||||
snprintf(fullLine, sizeof(fullLine), "/%s", fe.name.c_str());
|
||||
// Truncate if needed
|
||||
if ((int)strlen(fullLine) > charsPerLine - 1) {
|
||||
fullLine[charsPerLine - 4] = '.';
|
||||
fullLine[charsPerLine - 3] = '.';
|
||||
fullLine[charsPerLine - 2] = '.';
|
||||
fullLine[charsPerLine - 1] = '\0';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Audio file: "Title - Author [TYPE]"
|
||||
char lineBuf[80];
|
||||
|
||||
// Reserve space for type tag and bookmark indicator
|
||||
int suffixLen = fe.fileType.length() + 3; // " [M4B]" or " [MP3]"
|
||||
int bmkLen = fe.hasBookmark ? 2 : 0; // " >"
|
||||
int availChars = charsPerLine - suffixLen - bmkLen;
|
||||
if (availChars < 10) availChars = 10;
|
||||
|
||||
if (fe.displayAuthor.length() > 0) {
|
||||
snprintf(lineBuf, sizeof(lineBuf), "%s - %s",
|
||||
fe.displayTitle.c_str(), fe.displayAuthor.c_str());
|
||||
@@ -1261,24 +1248,13 @@ private:
|
||||
snprintf(lineBuf, sizeof(lineBuf), "%s", fe.displayTitle.c_str());
|
||||
}
|
||||
|
||||
// Truncate with ellipsis if needed
|
||||
if ((int)strlen(lineBuf) > availChars) {
|
||||
if (availChars > 3) {
|
||||
lineBuf[availChars - 3] = '.';
|
||||
lineBuf[availChars - 2] = '.';
|
||||
lineBuf[availChars - 1] = '.';
|
||||
lineBuf[availChars] = '\0';
|
||||
} else {
|
||||
lineBuf[availChars] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
// Append file type tag
|
||||
snprintf(fullLine, sizeof(fullLine), "%s [%s]", lineBuf, fe.fileType.c_str());
|
||||
}
|
||||
|
||||
display.setCursor(2, y);
|
||||
display.print(fullLine);
|
||||
// Pixel-aware ellipsis — reserve space for bookmark indicator
|
||||
int reserveRight = (!fe.isDir && fe.hasBookmark) ? 10 : 2;
|
||||
display.drawTextEllipsized(2, y, display.width() - reserveRight, fullLine);
|
||||
|
||||
// Bookmark indicator (right-aligned, files only)
|
||||
if (!fe.isDir && fe.hasBookmark) {
|
||||
@@ -1464,8 +1440,8 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
AudiobookPlayerScreen(UITask* task, Audio* audio)
|
||||
: _task(task), _audio(audio), _mode(FILE_LIST),
|
||||
AudiobookPlayerScreen(UITask* task, Audio* audio, NodePrefs* prefs = nullptr)
|
||||
: _task(task), _prefs(prefs), _audio(audio), _mode(FILE_LIST),
|
||||
_sdReady(false), _i2sInitialized(false), _dacPowered(false),
|
||||
_displayRef(nullptr),
|
||||
_selectedFile(0), _scrollOffset(0),
|
||||
|
||||
@@ -637,8 +637,8 @@ public:
|
||||
}
|
||||
|
||||
// Render inbox list
|
||||
display.setTextSize(0);
|
||||
int lineH = 9;
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize());
|
||||
int lineH = the_mesh.getNodePrefs()->smallLineH();
|
||||
int headerH = 14;
|
||||
int footerH = 14;
|
||||
int maxY = display.height() - footerH;
|
||||
@@ -672,7 +672,7 @@ public:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), lineH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineH);
|
||||
display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
@@ -745,8 +745,8 @@ public:
|
||||
|
||||
// --- Path detail overlay ---
|
||||
if (_showPathOverlay) {
|
||||
display.setTextSize(0);
|
||||
int lineH = 9;
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize());
|
||||
int lineH = the_mesh.getNodePrefs()->smallLineH();
|
||||
int y = 14;
|
||||
|
||||
ChannelMessage* msg = getNewestReceivedMsg();
|
||||
@@ -942,7 +942,7 @@ public:
|
||||
}
|
||||
|
||||
if (channelMsgCount == 0) {
|
||||
display.setTextSize(0); // Tiny font for body text
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); // Tiny font for body text
|
||||
display.setCursor(0, 20);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (_viewChannelIdx == 0xFF) {
|
||||
@@ -975,8 +975,8 @@ public:
|
||||
// =================================================================
|
||||
// DM Inbox: list of contacts/rooms you have DM history with
|
||||
// =================================================================
|
||||
display.setTextSize(0);
|
||||
int lineHeight = 9;
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize());
|
||||
int lineHeight = the_mesh.getNodePrefs()->smallLineH();
|
||||
int headerHeight = 14;
|
||||
int footerHeight = 14;
|
||||
int maxY = display.height() - footerHeight;
|
||||
@@ -1056,7 +1056,7 @@ public:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), lineHeight);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineHeight);
|
||||
display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineHeight);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
@@ -1094,8 +1094,8 @@ public:
|
||||
}
|
||||
display.setTextSize(1);
|
||||
} else {
|
||||
display.setTextSize(0); // Tiny font for message body
|
||||
int lineHeight = 9; // 8px font + 1px spacing
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); // Tiny font for message body
|
||||
int lineHeight = the_mesh.getNodePrefs()->smallLineH(); // 8px font + 1px spacing
|
||||
int headerHeight = 14;
|
||||
int footerHeight = 14;
|
||||
int scrollBarW = 4; // Width of scroll indicator on right edge
|
||||
@@ -1163,7 +1163,7 @@ public:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, contentW, maxFillH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, contentW, maxFillH);
|
||||
display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), contentW, maxFillH);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1324,7 +1324,7 @@ public:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, contentW, maxFillH - usedH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, contentW, maxFillH - usedH);
|
||||
display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), contentW, maxFillH - usedH);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1646,7 +1646,26 @@ public:
|
||||
}
|
||||
}
|
||||
} else if (_viewChannelIdx > 0) {
|
||||
_viewChannelIdx--;
|
||||
// Skip backwards over any empty/gap slots
|
||||
uint8_t prev = _viewChannelIdx - 1;
|
||||
bool found = false;
|
||||
while (true) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(prev, ch) && ch.name[0] != '\0') {
|
||||
_viewChannelIdx = prev;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (prev == 0) break;
|
||||
prev--;
|
||||
}
|
||||
if (!found) {
|
||||
// No valid channel below → wrap to DM tab
|
||||
_viewChannelIdx = 0xFF;
|
||||
_dmInboxMode = true;
|
||||
_dmInboxScroll = 0;
|
||||
_dmFilterName[0] = '\0';
|
||||
}
|
||||
} else {
|
||||
// Channel 0 → wrap to DM tab
|
||||
_viewChannelIdx = 0xFF;
|
||||
@@ -1667,11 +1686,17 @@ public:
|
||||
// DM tab → wrap to channel 0
|
||||
_viewChannelIdx = 0;
|
||||
} else {
|
||||
ChannelDetails ch;
|
||||
uint8_t nextIdx = _viewChannelIdx + 1;
|
||||
if (the_mesh.getChannel(nextIdx, ch) && ch.name[0] != '\0') {
|
||||
_viewChannelIdx = nextIdx;
|
||||
} else {
|
||||
// Skip forward over any empty/gap slots
|
||||
bool found = false;
|
||||
for (uint8_t next = _viewChannelIdx + 1; next < MAX_GROUP_CHANNELS; next++) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(next, ch) && ch.name[0] != '\0') {
|
||||
_viewChannelIdx = next;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// Past last channel → go to DM tab
|
||||
_viewChannelIdx = 0xFF;
|
||||
_dmInboxMode = true;
|
||||
|
||||
@@ -162,11 +162,11 @@ public:
|
||||
// Returns: 0=miss, 1=moved, 2=tapped current row.
|
||||
int selectRowAtVY(int vy) {
|
||||
if (_filteredCount == 0) return 0;
|
||||
const int headerH = 14, footerH = 14, lineH = 9;
|
||||
const int headerH = 14, footerH = 14, lineH = the_mesh.getNodePrefs()->smallLineH();
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
const int bodyTop = headerH;
|
||||
#else
|
||||
const int bodyTop = headerH + 5;
|
||||
const int bodyTop = headerH + the_mesh.getNodePrefs()->smallHighlightOff();
|
||||
#endif
|
||||
if (vy < bodyTop || vy >= 128 - footerH) return 0;
|
||||
|
||||
@@ -235,8 +235,8 @@ public:
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// === Body - contact rows ===
|
||||
display.setTextSize(0); // tiny font for compact rows
|
||||
int lineHeight = 9; // 8px font + 1px gap
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); // tiny font for compact rows
|
||||
int lineHeight = the_mesh.getNodePrefs()->smallLineH(); // 8px font + 1px gap
|
||||
int headerHeight = 14;
|
||||
int footerHeight = 14;
|
||||
int maxY = display.height() - footerHeight;
|
||||
@@ -275,7 +275,7 @@ public:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), lineHeight);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineHeight);
|
||||
display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineHeight);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
|
||||
@@ -49,11 +49,11 @@ public:
|
||||
int selectRowAtVY(int vy) {
|
||||
int count = the_mesh.getDiscoveredCount();
|
||||
if (count == 0) return 0;
|
||||
const int headerH = 14, footerH = 14, lineH = 9;
|
||||
const int headerH = 14, footerH = 14, lineH = the_mesh.getNodePrefs()->smallLineH();
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
const int bodyTop = headerH;
|
||||
#else
|
||||
const int bodyTop = headerH + 5;
|
||||
const int bodyTop = headerH + the_mesh.getNodePrefs()->smallHighlightOff();
|
||||
#endif
|
||||
if (vy < bodyTop || vy >= 128 - footerH) return 0;
|
||||
|
||||
@@ -91,8 +91,8 @@ public:
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// === Body — discovered node rows ===
|
||||
display.setTextSize(0); // tiny font for compact rows
|
||||
int lineHeight = 9;
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); // tiny font for compact rows
|
||||
int lineHeight = the_mesh.getNodePrefs()->smallLineH();
|
||||
int headerHeight = 14;
|
||||
int footerHeight = 14;
|
||||
int maxY = display.height() - footerHeight;
|
||||
@@ -129,7 +129,7 @@ public:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), lineHeight);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineHeight);
|
||||
display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineHeight);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
|
||||
@@ -68,11 +68,11 @@ public:
|
||||
// Returns: 0=miss, 1=moved, 2=tapped current row.
|
||||
int selectRowAtVY(int vy) {
|
||||
if (_count == 0) return 0;
|
||||
const int headerH = 14, footerH = 14, lineH = 9;
|
||||
const int headerH = 14, footerH = 14, lineH = the_mesh.getNodePrefs()->smallLineH();
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
const int bodyTop = headerH;
|
||||
#else
|
||||
const int bodyTop = headerH + 5;
|
||||
const int bodyTop = headerH + the_mesh.getNodePrefs()->smallHighlightOff();
|
||||
#endif
|
||||
if (vy < bodyTop || vy >= 128 - footerH) return 0;
|
||||
|
||||
@@ -117,8 +117,8 @@ public:
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// === Body — node rows ===
|
||||
display.setTextSize(0);
|
||||
int lineHeight = 9;
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize());
|
||||
int lineHeight = the_mesh.getNodePrefs()->smallLineH();
|
||||
int headerHeight = 14;
|
||||
int footerHeight = 14;
|
||||
int maxY = display.height() - footerHeight;
|
||||
@@ -147,7 +147,7 @@ public:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), lineHeight);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineHeight);
|
||||
display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineHeight);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <SD.h>
|
||||
#include <vector>
|
||||
#include "Utf8CP437.h"
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
// Forward declarations
|
||||
class UITask;
|
||||
@@ -52,9 +53,11 @@ public:
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
NodePrefs* _prefs;
|
||||
Mode _mode;
|
||||
bool _sdReady;
|
||||
bool _initialized;
|
||||
uint8_t _lastFontPref;
|
||||
DisplayDriver* _display;
|
||||
|
||||
// Display layout (calculated once from display metrics)
|
||||
@@ -518,8 +521,8 @@ private:
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// File list with "+ New Note" at index 0
|
||||
display.setTextSize(0);
|
||||
int listLineH = 9; // Match contacts/discovery for consistent selection highlight
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
int listLineH = _prefs->smallLineH();
|
||||
int startY = 14;
|
||||
int totalItems = 1 + (int)_fileList.size();
|
||||
int maxVisible = (display.height() - startY - _footerHeight) / listLineH;
|
||||
@@ -539,27 +542,21 @@ private:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), listLineH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), listLineH);
|
||||
display.fillRect(0, y + _prefs->smallHighlightOff(), display.width(), listLineH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
display.setCursor(0, y);
|
||||
|
||||
if (i == 0) {
|
||||
display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN);
|
||||
display.print(selected ? "> + New Note" : " + New Note");
|
||||
display.drawTextEllipsized(0, y, display.width() - 4,
|
||||
selected ? "> + New Note" : " + New Note");
|
||||
} else {
|
||||
String line = selected ? "> " : " ";
|
||||
String name = _fileList[i - 1];
|
||||
int maxLen = _charsPerLine - 4;
|
||||
if ((int)name.length() > maxLen) {
|
||||
name = name.substring(0, maxLen - 3) + "...";
|
||||
}
|
||||
line += name;
|
||||
display.print(line.c_str());
|
||||
line += _fileList[i - 1];
|
||||
display.drawTextEllipsized(0, y, display.width() - 4, line.c_str());
|
||||
}
|
||||
y += listLineH;
|
||||
}
|
||||
@@ -605,7 +602,7 @@ private:
|
||||
}
|
||||
|
||||
// Render current page using tiny font
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
int pageStart = _pageOffsets[_currentPage];
|
||||
@@ -722,7 +719,7 @@ private:
|
||||
int textAreaTop = 14;
|
||||
int textAreaBottom = display.height() - 16;
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
|
||||
// Find cursor line
|
||||
int cursorLine = lineForPos(_cursorPos);
|
||||
@@ -771,7 +768,7 @@ private:
|
||||
|
||||
// If buffer is empty, show cursor at top
|
||||
if (_bufLen == 0) {
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, textAreaTop);
|
||||
display.print("|");
|
||||
@@ -829,7 +826,7 @@ private:
|
||||
display.setCursor(0, 20);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.print("From: ");
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
String origDisplay = _renameOriginal;
|
||||
if (origDisplay.length() > 30) origDisplay = origDisplay.substring(0, 27) + "...";
|
||||
display.print(origDisplay.c_str());
|
||||
@@ -840,7 +837,7 @@ private:
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.print("To: ");
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
char displayName[NOTES_RENAME_MAX + 2];
|
||||
snprintf(displayName, sizeof(displayName), "%s|", _renameBuf);
|
||||
@@ -880,7 +877,7 @@ private:
|
||||
display.setCursor(0, 25);
|
||||
display.print("File:");
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setCursor(0, 38);
|
||||
String nameDisplay = _deleteTarget;
|
||||
if (nameDisplay.length() > 35) nameDisplay = nameDisplay.substring(0, 32) + "...";
|
||||
@@ -1096,9 +1093,9 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
NotesScreen(UITask* task)
|
||||
: _task(task), _mode(FILE_LIST),
|
||||
_sdReady(false), _initialized(false), _display(nullptr),
|
||||
NotesScreen(UITask* task, NodePrefs* prefs = nullptr)
|
||||
: _task(task), _prefs(prefs), _mode(FILE_LIST),
|
||||
_sdReady(false), _initialized(false), _lastFontPref(0), _display(nullptr),
|
||||
_charsPerLine(38), _linesPerPage(22), _lineHeight(5), _footerHeight(14),
|
||||
_editCharsPerLine(20), _editLineHeight(12), _editMaxLines(8),
|
||||
_selectedFile(0), _buf(nullptr), _bufLen(0), _cursorPos(0),
|
||||
@@ -1133,15 +1130,31 @@ public:
|
||||
// ---- Layout Init ----
|
||||
|
||||
void initLayout(DisplayDriver& display) {
|
||||
// Re-init if font preference changed since last layout
|
||||
uint8_t curFont = _prefs ? _prefs->large_font : 0;
|
||||
if (_initialized && curFont != _lastFontPref) {
|
||||
_initialized = false;
|
||||
Serial.println("Notes: font changed, recalculating layout");
|
||||
}
|
||||
if (_initialized) return;
|
||||
_lastFontPref = curFont;
|
||||
_display = &display;
|
||||
|
||||
// Tiny font metrics (for read mode)
|
||||
display.setTextSize(0);
|
||||
// Font metrics (for read mode)
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
uint16_t tenCharsW = display.getTextWidth("MMMMMMMMMM");
|
||||
if (tenCharsW > 0) {
|
||||
_charsPerLine = (display.width() * 10) / tenCharsW;
|
||||
}
|
||||
// Proportional font: use average-width measurement instead of M-width
|
||||
if (_prefs && _prefs->large_font) {
|
||||
const char* sample = "the quick brown fox jumps over lazy dog";
|
||||
uint16_t sampleW = display.getTextWidth(sample);
|
||||
int sampleLen = strlen(sample);
|
||||
if (sampleW > 0 && sampleLen > 0) {
|
||||
_charsPerLine = (display.width() * sampleLen * 70) / ((int)sampleW * 100);
|
||||
}
|
||||
}
|
||||
if (_charsPerLine < 15) _charsPerLine = 15;
|
||||
if (_charsPerLine > 60) _charsPerLine = 60;
|
||||
|
||||
@@ -1151,6 +1164,10 @@ public:
|
||||
} else {
|
||||
_lineHeight = 5;
|
||||
}
|
||||
// Large font: formula above assumes built-in 6x8 ratio — too small for 9pt
|
||||
if (_prefs && _prefs->large_font) {
|
||||
_lineHeight = _prefs->smallLineH();
|
||||
}
|
||||
|
||||
_footerHeight = 14;
|
||||
int textAreaHeight = display.height() - _footerHeight;
|
||||
|
||||
@@ -777,8 +777,8 @@ private:
|
||||
// =====================================================================
|
||||
|
||||
void renderCategoryMenu(DisplayDriver& display, int y, int bodyHeight) {
|
||||
display.setTextSize(0);
|
||||
int lineHeight = 9;
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize());
|
||||
int lineHeight = the_mesh.getNodePrefs()->smallLineH();
|
||||
|
||||
// Clock drift info line
|
||||
if (_serverTime > 0) {
|
||||
@@ -862,8 +862,8 @@ private:
|
||||
// =====================================================================
|
||||
|
||||
void renderCommandMenu(DisplayDriver& display, int y, int bodyHeight) {
|
||||
display.setTextSize(0);
|
||||
int lineHeight = 9;
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize());
|
||||
int lineHeight = the_mesh.getNodePrefs()->smallLineH();
|
||||
const AdminCategoryDef& cat = CATEGORIES[_catSel];
|
||||
|
||||
// Category title
|
||||
@@ -1025,7 +1025,7 @@ private:
|
||||
if (_pendingCmd) display.print(_pendingCmd->label);
|
||||
|
||||
y += 14;
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize());
|
||||
display.setCursor(0, y);
|
||||
|
||||
// Show the param value if one was collected
|
||||
@@ -1033,7 +1033,7 @@ private:
|
||||
char preview[80];
|
||||
snprintf(preview, sizeof(preview), "Value: %s", _paramBuf);
|
||||
display.print(preview);
|
||||
y += 10;
|
||||
y += the_mesh.getNodePrefs()->smallLineH() + 1;
|
||||
display.setCursor(0, y);
|
||||
}
|
||||
|
||||
@@ -1071,8 +1071,8 @@ private:
|
||||
// =====================================================================
|
||||
|
||||
void renderResponse(DisplayDriver& display, int y, int bodyHeight) {
|
||||
display.setTextSize(0);
|
||||
int lineHeight = 9;
|
||||
display.setTextSize(the_mesh.getNodePrefs()->smallTextSize());
|
||||
int lineHeight = the_mesh.getNodePrefs()->smallLineH();
|
||||
|
||||
display.setColor((_state == STATE_ERROR) ? DisplayDriver::YELLOW : DisplayDriver::LIGHT);
|
||||
|
||||
@@ -1166,7 +1166,7 @@ private:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), lineHeight);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineHeight);
|
||||
display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineHeight);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else if (warn) {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include "ModemManager.h"
|
||||
#include "SMSStore.h"
|
||||
#include "SMSContacts.h"
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
// Limits
|
||||
#define SMS_INBOX_PAGE_SIZE 4
|
||||
@@ -51,6 +52,7 @@ public:
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
NodePrefs* _prefs;
|
||||
SubView _view;
|
||||
|
||||
// App menu state
|
||||
@@ -117,8 +119,8 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
SMSScreen(UITask* task)
|
||||
: _task(task), _view(APP_MENU)
|
||||
SMSScreen(UITask* task, NodePrefs* prefs = nullptr)
|
||||
: _task(task), _prefs(prefs), _view(APP_MENU)
|
||||
, _menuCursor(0)
|
||||
, _convCount(0), _inboxCursor(0), _inboxScrollTop(0)
|
||||
, _msgCount(0), _msgScrollPos(0)
|
||||
@@ -276,7 +278,7 @@ public:
|
||||
|
||||
// Show modem state text if not ready
|
||||
if (ms != ModemState::READY && ms != ModemState::SENDING_SMS) {
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
const char* label = ModemManager::stateToString(ms);
|
||||
uint16_t labelW = display.getTextWidth(label);
|
||||
@@ -356,7 +358,7 @@ public:
|
||||
|
||||
// Modem status indicator
|
||||
ModemState ms = modemManager.getState();
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setCursor(4, y + lineHeight + 8);
|
||||
if (ms == ModemState::OFF || ms == ModemState::POWERING_ON ||
|
||||
ms == ModemState::INITIALIZING) {
|
||||
@@ -483,7 +485,7 @@ public:
|
||||
bool isAction = (row == 4); // Bottom row has action buttons
|
||||
|
||||
if (isAction) {
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
if (col == 2 && _phoneInputPos > 0) {
|
||||
display.setColor(DisplayDriver::GREEN); // CALL
|
||||
} else if (col == 1) {
|
||||
@@ -544,7 +546,7 @@ public:
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
if (_convCount == 0) {
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, 20);
|
||||
display.print("No conversations");
|
||||
@@ -560,8 +562,8 @@ public:
|
||||
}
|
||||
display.setTextSize(1);
|
||||
} else {
|
||||
display.setTextSize(0);
|
||||
int lineHeight = 10;
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
int lineHeight = _prefs->smallLineH() + 1;
|
||||
int y = 14;
|
||||
|
||||
int visibleCount = (display.height() - 14 - 14) / (lineHeight * 2 + 2);
|
||||
@@ -643,14 +645,14 @@ public:
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
if (_msgCount == 0) {
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, 25);
|
||||
display.print("No messages");
|
||||
display.setTextSize(1);
|
||||
} else {
|
||||
display.setTextSize(0);
|
||||
int lineHeight = 10;
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
int lineHeight = _prefs->smallLineH() + 1;
|
||||
int headerHeight = 14;
|
||||
int footerHeight = 14;
|
||||
|
||||
@@ -764,12 +766,13 @@ public:
|
||||
// Message body
|
||||
display.setCursor(0, 14);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
|
||||
uint16_t testWidth = display.getTextWidth("MMMMMMMMMM");
|
||||
int charsPerLine = (testWidth > 0) ? (display.width() * 10) / testWidth : 20;
|
||||
if (charsPerLine < 12) charsPerLine = 12;
|
||||
|
||||
int composeLH = _prefs->smallLineH() + 1;
|
||||
int y = 14;
|
||||
int x = 0;
|
||||
char cs[2] = {0, 0};
|
||||
@@ -780,7 +783,7 @@ public:
|
||||
x++;
|
||||
if (x >= charsPerLine) {
|
||||
x = 0;
|
||||
y += 10;
|
||||
y += composeLH;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -827,7 +830,7 @@ public:
|
||||
int cnt = smsContacts.count();
|
||||
|
||||
if (cnt == 0) {
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, 25);
|
||||
display.print("No contacts saved");
|
||||
@@ -837,8 +840,8 @@ public:
|
||||
display.print("and press A to add");
|
||||
display.setTextSize(1);
|
||||
} else {
|
||||
display.setTextSize(0);
|
||||
int lineHeight = 10;
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
int lineHeight = _prefs->smallLineH() + 1;
|
||||
int y = 14;
|
||||
|
||||
int visibleCount = (display.height() - 14 - 14) / (lineHeight * 2 + 2);
|
||||
@@ -900,7 +903,7 @@ public:
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// Phone number (read-only)
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, 16);
|
||||
display.print("Phone: ");
|
||||
@@ -956,7 +959,7 @@ public:
|
||||
display.print(dispName);
|
||||
|
||||
// Phone number below name (smaller, dimmer)
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(4, 36);
|
||||
display.print(_callPhone);
|
||||
@@ -1011,7 +1014,7 @@ public:
|
||||
display.print(dispName);
|
||||
|
||||
// Phone number below name (smaller, dimmer)
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(4, 36);
|
||||
display.print(_callPhone);
|
||||
@@ -1070,7 +1073,7 @@ public:
|
||||
display.print(dispName);
|
||||
|
||||
// Phone number below name (smaller, dimmer)
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(4, 36);
|
||||
display.print(_callPhone);
|
||||
@@ -1090,7 +1093,7 @@ public:
|
||||
display.print(timeBuf);
|
||||
|
||||
// Volume (left-aligned)
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
char volLabel[12];
|
||||
snprintf(volLabel, sizeof(volLabel), "Vol: %d/5", _callVolume);
|
||||
|
||||
@@ -112,6 +112,7 @@ enum SettingsRowType : uint8_t {
|
||||
ROW_UTC_OFFSET, // UTC offset (-12 to +14)
|
||||
ROW_MSG_NOTIFY, // Keyboard flash on new msg toggle
|
||||
ROW_DARK_MODE, // Dark mode toggle (inverted display)
|
||||
ROW_LARGE_FONT, // Font size toggle: 0=tiny (default), 1=larger
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
ROW_PORTRAIT_MODE, // Portrait orientation toggle
|
||||
#endif
|
||||
@@ -242,6 +243,9 @@ private:
|
||||
// Dirty flag for radio params  prompt to apply
|
||||
bool _radioChanged;
|
||||
|
||||
// T5S3: signal UITask to open VKB when entering text edit mode
|
||||
bool _needsTextVKB;
|
||||
|
||||
// 4G modem state (runtime cache of config)
|
||||
#ifdef HAS_4G_MODEM
|
||||
bool _modemEnabled;
|
||||
@@ -349,12 +353,12 @@ private:
|
||||
}
|
||||
} else if (_subScreen == SUB_CHANNELS) {
|
||||
// --- Channels sub-screen: only channel-related rows ---
|
||||
// Scan ALL slots — companion app may write non-contiguously, and
|
||||
// gaps can appear after channel deletion if compaction is incomplete.
|
||||
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
|
||||
addRow(ROW_CHANNEL, i);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
addRow(ROW_ADD_CHANNEL);
|
||||
@@ -372,6 +376,7 @@ private:
|
||||
addRow(ROW_GPS_BAUD);
|
||||
addRow(ROW_PATH_HASH_SIZE);
|
||||
addRow(ROW_DARK_MODE);
|
||||
addRow(ROW_LARGE_FONT);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
addRow(ROW_PORTRAIT_MODE);
|
||||
#endif
|
||||
@@ -501,14 +506,12 @@ private:
|
||||
ChannelDetails empty;
|
||||
memset(&empty, 0, sizeof(empty));
|
||||
|
||||
// Find total channel count
|
||||
// Find highest used channel slot (scan all — gaps may exist)
|
||||
int total = 0;
|
||||
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
|
||||
total = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,7 +548,7 @@ public:
|
||||
_editMode(EDIT_NONE), _editPos(0), _editPickerIdx(0),
|
||||
_editFloat(0), _editInt(0), _confirmAction(0),
|
||||
_onboarding(false), _subScreen(SUB_NONE), _savedTopCursor(0),
|
||||
_radioChanged(false) {
|
||||
_radioChanged(false), _needsTextVKB(false) {
|
||||
memset(_editBuf, 0, sizeof(_editBuf));
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
_otaServer = nullptr;
|
||||
@@ -603,13 +606,13 @@ public:
|
||||
// and move cursor there. Returns: 0=miss, 1=moved to new row, 2=tapped current row.
|
||||
int selectRowAtVY(int vy) {
|
||||
if (_editMode != EDIT_NONE) return 0; // Don't change cursor while editing
|
||||
const int headerH = 14, footerH = 14, lineH = 9;
|
||||
// T-Deck Pro render offsets fillRect by +5 (GxEPD baseline compensation),
|
||||
// so visual rows start 5 units below headerH. T5S3 renders at y directly.
|
||||
const int headerH = 14, footerH = 14, lineH = _prefs->smallLineH();
|
||||
// bodyTop must match where the visual rows start (highlight bar position).
|
||||
// T5S3 renders highlight at y directly. T-Deck Pro offsets by smallHighlightOff().
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
const int bodyTop = headerH;
|
||||
#else
|
||||
const int bodyTop = headerH + 5;
|
||||
const int bodyTop = headerH + _prefs->smallHighlightOff();
|
||||
#endif
|
||||
if (vy < bodyTop || vy >= 128 - footerH) return 0; // Outside body area
|
||||
|
||||
@@ -740,6 +743,19 @@ public:
|
||||
|
||||
#endif
|
||||
|
||||
// T5S3 VKB integration for text editing (channel name, device name, freq, APN)
|
||||
bool needsTextVKB() const { return _needsTextVKB; }
|
||||
void clearTextNeedsVKB() { _needsTextVKB = false; }
|
||||
const char* getEditBuf() const { return _editBuf; }
|
||||
SettingsRowType getCurrentRowType() const { return _rows[_cursor].type; }
|
||||
void submitEditText(const char* text) {
|
||||
strncpy(_editBuf, text, SETTINGS_TEXT_BUF - 1);
|
||||
_editBuf[SETTINGS_TEXT_BUF - 1] = '\0';
|
||||
_editPos = strlen(_editBuf);
|
||||
// Simulate Enter to confirm the edit through the normal path
|
||||
handleInput('\r');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OTA firmware update
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -963,7 +979,7 @@ public:
|
||||
display.fillRect(2, 14, display.width() - 4, display.height() - 28);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(2, 14, display.width() - 4, display.height() - 28);
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.drawTextCentered(display.width() / 2, 22, "Flashing Firmware");
|
||||
snprintf(tmp, sizeof(tmp), "%d / %d KB", (int)(totalWritten / 1024), (int)(fileSize / 1024));
|
||||
display.drawTextCentered(display.width() / 2, 42, tmp);
|
||||
@@ -1037,7 +1053,7 @@ public:
|
||||
display.fillRect(2, 14, display.width() - 4, display.height() - 28);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(2, 14, display.width() - 4, display.height() - 28);
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.drawTextCentered(display.width() / 2, 30, "Update Complete!");
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
@@ -1068,6 +1084,9 @@ public:
|
||||
strncpy(_editBuf, initial, SETTINGS_TEXT_BUF - 1);
|
||||
_editBuf[SETTINGS_TEXT_BUF - 1] = '\0';
|
||||
_editPos = strlen(_editBuf);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
_needsTextVKB = true; // Signal UITask to open virtual keyboard
|
||||
#endif
|
||||
}
|
||||
|
||||
void startEditPicker(int initialIdx) {
|
||||
@@ -1114,8 +1133,8 @@ public:
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// === Body ===
|
||||
display.setTextSize(0); // tiny font
|
||||
int lineHeight = 9;
|
||||
display.setTextSize(_prefs->smallTextSize()); // tiny font
|
||||
int lineHeight = _prefs->smallLineH();
|
||||
int headerH = 14;
|
||||
int footerH = 14;
|
||||
int maxY = display.height() - footerH;
|
||||
@@ -1140,7 +1159,7 @@ public:
|
||||
// Highlight needs to start above the baseline to cover ascenders.
|
||||
display.fillRect(0, y, display.width(), lineHeight);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineHeight);
|
||||
display.fillRect(0, y + _prefs->smallHighlightOff(), display.width(), lineHeight);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
@@ -1233,7 +1252,7 @@ public:
|
||||
break;
|
||||
|
||||
case ROW_MSG_NOTIFY:
|
||||
snprintf(tmp, sizeof(tmp), "Msg Rcvd LED Light Pulse: %s",
|
||||
snprintf(tmp, sizeof(tmp), "Msg LED Flash: %s",
|
||||
_prefs->kb_flash_notify ? "ON" : "OFF");
|
||||
display.print(tmp);
|
||||
break;
|
||||
@@ -1266,6 +1285,12 @@ public:
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case ROW_LARGE_FONT:
|
||||
snprintf(tmp, sizeof(tmp), "Font Size: %s",
|
||||
_prefs->large_font ? "LARGER" : "TINY");
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
case ROW_PORTRAIT_MODE:
|
||||
snprintf(tmp, sizeof(tmp), "Portrait Mode: %s",
|
||||
@@ -1506,7 +1531,7 @@ public:
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(bx, by, bw, bh);
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
if (_confirmAction == 1) {
|
||||
uint8_t chIdx = _rows[_cursor].param;
|
||||
ChannelDetails ch;
|
||||
@@ -1534,7 +1559,7 @@ public:
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(bx, by, bw, bh);
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
int wy = by + 4;
|
||||
|
||||
if (_wifiPhase == WIFI_PHASE_SCANNING) {
|
||||
@@ -1620,7 +1645,7 @@ public:
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(bx, by, bw, bh);
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
int oy = by + 4;
|
||||
|
||||
if (_otaPhase == OTA_PHASE_CONFIRM) {
|
||||
@@ -2311,6 +2336,12 @@ public:
|
||||
Serial.printf("Settings: Dark mode = %s\n",
|
||||
_prefs->dark_mode ? "ON" : "OFF");
|
||||
break;
|
||||
case ROW_LARGE_FONT:
|
||||
_prefs->large_font = _prefs->large_font ? 0 : 1;
|
||||
the_mesh.savePrefs();
|
||||
Serial.printf("Settings: Font size = %s\n",
|
||||
_prefs->large_font ? "LARGER" : "TINY");
|
||||
break;
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
case ROW_PORTRAIT_MODE:
|
||||
_prefs->portrait_mode = _prefs->portrait_mode ? 0 : 1;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <vector>
|
||||
#include "Utf8CP437.h"
|
||||
#include "EpubProcessor.h"
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
// Forward declarations
|
||||
class UITask;
|
||||
@@ -327,12 +328,13 @@ inline int indexPagesWordWrap(File& file, long startPos,
|
||||
inline int indexPagesWordWrapPixel(File& file, long startPos,
|
||||
std::vector<long>& pagePositions,
|
||||
int linesPerPage, int maxChars,
|
||||
DisplayDriver* display, int maxPages) {
|
||||
DisplayDriver* display, int maxPages,
|
||||
NodePrefs* prefs = nullptr) {
|
||||
const int BUF_SIZE = READER_BUF_SIZE; // Match page buffer to avoid chunk boundary wrap mismatches
|
||||
char buffer[BUF_SIZE];
|
||||
|
||||
// Ensure body font is active for pixel measurement
|
||||
display->setTextSize(0);
|
||||
display->setTextSize(prefs ? prefs->smallTextSize() : 0);
|
||||
|
||||
file.seek(startPos);
|
||||
int pagesAdded = 0;
|
||||
@@ -396,9 +398,11 @@ public:
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
NodePrefs* _prefs;
|
||||
Mode _mode;
|
||||
bool _sdReady;
|
||||
bool _initialized; // Layout metrics calculated
|
||||
uint8_t _lastFontPref; // Font preference at last layout init (detect changes)
|
||||
bool _bootIndexed; // Boot-time pre-indexing done
|
||||
DisplayDriver* _display; // Stored reference for splash screens
|
||||
|
||||
@@ -1084,8 +1088,8 @@ private:
|
||||
display.setCursor(0, 42);
|
||||
display.print("/books/ on SD card");
|
||||
} else {
|
||||
display.setTextSize(0); // Tiny font for file list
|
||||
int listLineH = 8; // Approximate tiny font line height in virtual coords
|
||||
display.setTextSize(_prefs->smallTextSize()); // Tiny font for file list
|
||||
int listLineH = _prefs->smallLineH();
|
||||
int startY = 14;
|
||||
int maxVisible = (display.height() - startY - _footerHeight) / listLineH;
|
||||
if (maxVisible < 3) maxVisible = 3;
|
||||
@@ -1106,7 +1110,7 @@ private:
|
||||
#else
|
||||
// setCursor adds +5 to y internally, but fillRect does not.
|
||||
// Offset fillRect by +5 to align highlight bar with text.
|
||||
display.fillRect(0, y + 5, display.width(), listLineH);
|
||||
display.fillRect(0, y + _prefs->smallHighlightOff(), display.width(), listLineH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
@@ -1114,8 +1118,6 @@ private:
|
||||
}
|
||||
|
||||
// Set cursor AFTER fillRect so text draws on top of highlight
|
||||
display.setCursor(0, y);
|
||||
|
||||
int type = itemTypeAt(i);
|
||||
String line = selected ? "> " : " ";
|
||||
|
||||
@@ -1125,10 +1127,6 @@ private:
|
||||
} else if (type == 1) {
|
||||
// Subdirectory
|
||||
line += "/" + dirNameAt(i);
|
||||
// Truncate if needed
|
||||
if ((int)line.length() > _charsPerLine) {
|
||||
line = line.substring(0, _charsPerLine - 3) + "...";
|
||||
}
|
||||
} else {
|
||||
// File
|
||||
int fi = fileIndexAt(i);
|
||||
@@ -1141,16 +1139,11 @@ private:
|
||||
suffix = " *";
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate if needed
|
||||
int maxLen = _charsPerLine - 4 - suffix.length();
|
||||
if ((int)name.length() > maxLen) {
|
||||
name = name.substring(0, maxLen - 3) + "...";
|
||||
}
|
||||
line += name + suffix;
|
||||
}
|
||||
|
||||
display.print(line.c_str());
|
||||
// Pixel-aware ellipsis — small margin prevents GxEPD edge wrapping
|
||||
display.drawTextEllipsized(0, y, display.width() - 4, line.c_str());
|
||||
y += listLineH;
|
||||
}
|
||||
display.setTextSize(1); // Restore
|
||||
@@ -1163,7 +1156,7 @@ private:
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.drawTextCentered(display.width() / 2, footerY, "Swipe: Scroll Tap: Open Boot: home");
|
||||
#else
|
||||
display.setCursor(0, footerY);
|
||||
@@ -1177,7 +1170,7 @@ private:
|
||||
|
||||
void renderPage(DisplayDriver& display) {
|
||||
// Use tiny font for maximum text density
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
int y = 0;
|
||||
@@ -1270,7 +1263,7 @@ private:
|
||||
}
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setCursor(0, footerY);
|
||||
display.print(status);
|
||||
const char* right = "Swipe:Page Tap:GoTo Hold:Close";
|
||||
@@ -1287,8 +1280,8 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
TextReaderScreen(UITask* task)
|
||||
: _task(task), _mode(FILE_LIST), _sdReady(false), _initialized(false),
|
||||
TextReaderScreen(UITask* task, NodePrefs* prefs = nullptr)
|
||||
: _task(task), _prefs(prefs), _mode(FILE_LIST), _sdReady(false), _initialized(false), _lastFontPref(0),
|
||||
_bootIndexed(false), _display(nullptr),
|
||||
_charsPerLine(38), _linesPerPage(22), _lineHeight(5),
|
||||
_textAreaHeight(100), _headerHeight(14), _footerHeight(14),
|
||||
@@ -1313,16 +1306,24 @@ public:
|
||||
|
||||
// Call once after display is available to calculate layout metrics
|
||||
void initLayout(DisplayDriver& display) {
|
||||
// Re-init if font preference changed since last layout
|
||||
uint8_t curFont = _prefs ? _prefs->large_font : 0;
|
||||
if (_initialized && curFont != _lastFontPref) {
|
||||
_initialized = false;
|
||||
Serial.println("TextReader: font changed, recalculating layout");
|
||||
}
|
||||
if (_initialized) return;
|
||||
_lastFontPref = curFont;
|
||||
|
||||
// Store display reference for splash screens during openBook
|
||||
_display = &display;
|
||||
|
||||
// Measure tiny font metrics using the display driver
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
|
||||
// Measure character width: use 10 M's for monospace (T-Deck Pro).
|
||||
// T5S3 overrides this below with average-width measurement.
|
||||
// Measure character width: use 10 M's for monospace (T-Deck Pro tiny font).
|
||||
// Proportional fonts (T5S3 and T-Deck Pro large_font) override below with
|
||||
// average-width measurement since M is the widest glyph (~40% wider than average).
|
||||
uint16_t tenCharsW = display.getTextWidth("MMMMMMMMMM");
|
||||
if (tenCharsW > 0) {
|
||||
_charsPerLine = (display.width() * 10) / tenCharsW;
|
||||
@@ -1343,6 +1344,15 @@ public:
|
||||
if (_charsPerLine < 15) _charsPerLine = 15;
|
||||
if (_charsPerLine > 80) _charsPerLine = 80;
|
||||
#else
|
||||
// T-Deck Pro: large_font uses FreeSans9pt (proportional) — same fix
|
||||
if (_prefs && _prefs->large_font) {
|
||||
const char* sample = "the quick brown fox jumps over lazy dog";
|
||||
uint16_t sampleW = display.getTextWidth(sample);
|
||||
int sampleLen = strlen(sample);
|
||||
if (sampleW > 0 && sampleLen > 0) {
|
||||
_charsPerLine = (display.width() * sampleLen * 70) / ((int)sampleW * 100);
|
||||
}
|
||||
}
|
||||
if (_charsPerLine < 15) _charsPerLine = 15;
|
||||
if (_charsPerLine > 60) _charsPerLine = 60;
|
||||
#endif
|
||||
@@ -1362,13 +1372,17 @@ public:
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
// T5S3 uses FreeSans12pt/FreeSerif12pt for size 0 (yAdvance=29px).
|
||||
// Line height in virtual coords depends on orientation:
|
||||
// Landscape: 29px / scale_y(4.22) ≈ 7 + 1 spacing = 8
|
||||
// Portrait: 29px / scale_y(7.50) ≈ 4 + 1 spacing = 5
|
||||
{
|
||||
extern DISPLAY_CLASS display;
|
||||
_lineHeight = display.isPortraitMode() ? 5 : 8;
|
||||
}
|
||||
#else
|
||||
// T-Deck Pro large_font uses FreeSans9pt (yAdvance=22px at scale 1.5625×).
|
||||
// The 6x8 formula above gives ~5-7 which is way too small — lines overlap.
|
||||
// Use smallLineH() which is already tuned for this font.
|
||||
if (_prefs && _prefs->large_font) {
|
||||
_lineHeight = _prefs->smallLineH();
|
||||
}
|
||||
#endif
|
||||
|
||||
_headerHeight = 0; // No header in reading mode (maximize text area)
|
||||
@@ -1574,11 +1588,12 @@ public:
|
||||
// Returns: 0=miss, 1=moved, 2=tapped current row.
|
||||
int selectRowAtVY(int vy) {
|
||||
if (_mode != FILE_LIST) return 0;
|
||||
const int startY = 14, footerH = 14, listLineH = 8;
|
||||
const int startY = 14, footerH = 14;
|
||||
const int listLineH = _prefs ? _prefs->smallLineH() : 9;
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
const int bodyTop = startY;
|
||||
#else
|
||||
const int bodyTop = startY + 5; // GxEPD baseline offset
|
||||
const int bodyTop = startY + (_prefs ? _prefs->smallHighlightOff() : 5);
|
||||
#endif
|
||||
if (vy < bodyTop || vy >= 128 - footerH) return 0;
|
||||
|
||||
|
||||
@@ -12,12 +12,15 @@
|
||||
#include "MapScreen.h"
|
||||
#endif
|
||||
#include "target.h"
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_AUDIO_VARIANT)
|
||||
#include "HomeIcons.h"
|
||||
#endif
|
||||
#if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION)
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro) && !defined(BLE_PIN_CODE) && !defined(MECK_WIFI_COMPANION)
|
||||
#include "esp_sleep.h"
|
||||
#endif
|
||||
|
||||
#ifndef AUTO_OFF_MILLIS
|
||||
#define AUTO_OFF_MILLIS 15000 // 15 seconds
|
||||
@@ -156,7 +159,7 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts,
|
||||
}
|
||||
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
// T5S3: text-only battery indicator — "Batt 99% 4.1v"
|
||||
@@ -170,7 +173,7 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts,
|
||||
display.print(battStr);
|
||||
display.setTextSize(1); // restore default text size
|
||||
#else
|
||||
// T-Deck Pro: icon + percentage text
|
||||
// T-Deck Pro: icon + percentage text (icon hidden in large font)
|
||||
int iconWidth = 16;
|
||||
int iconHeight = 6;
|
||||
int iconY = 0;
|
||||
@@ -181,26 +184,35 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts,
|
||||
sprintf(pctStr, "%d%%", batteryPercentage);
|
||||
uint16_t textWidth = display.getTextWidth(pctStr);
|
||||
|
||||
// layout: [icon][cap 2px][gap 2px][text][margin 2px]
|
||||
int totalWidth = iconWidth + 2 + 2 + textWidth + 2;
|
||||
int iconX = display.width() - totalWidth;
|
||||
if (_node_prefs->large_font) {
|
||||
// Large font: text only — no room for icon in header
|
||||
int textX = display.width() - textWidth - 2;
|
||||
if (outIconX) *outIconX = textX;
|
||||
display.setCursor(textX, textY);
|
||||
display.print(pctStr);
|
||||
} else {
|
||||
// Tiny font: icon + text
|
||||
// layout: [icon][cap 2px][gap 2px][text][margin 2px]
|
||||
int totalWidth = iconWidth + 2 + 2 + textWidth + 2;
|
||||
int iconX = display.width() - totalWidth;
|
||||
|
||||
if (outIconX) *outIconX = iconX;
|
||||
if (outIconX) *outIconX = iconX;
|
||||
|
||||
// battery outline
|
||||
display.drawRect(iconX, iconY, iconWidth, iconHeight);
|
||||
// battery outline
|
||||
display.drawRect(iconX, iconY, iconWidth, iconHeight);
|
||||
|
||||
// battery "cap"
|
||||
display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 2, iconHeight / 2);
|
||||
// battery "cap"
|
||||
display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 2, iconHeight / 2);
|
||||
|
||||
// fill the battery based on the percentage
|
||||
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
|
||||
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
|
||||
// fill the battery based on the percentage
|
||||
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
|
||||
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
|
||||
|
||||
// draw percentage text after the battery cap
|
||||
int textX = iconX + iconWidth + 2 + 2; // after cap + gap
|
||||
display.setCursor(textX, textY);
|
||||
display.print(pctStr);
|
||||
// draw percentage text after the battery cap
|
||||
int textX = iconX + iconWidth + 2 + 2; // after cap + gap
|
||||
display.setCursor(textX, textY);
|
||||
display.print(pctStr);
|
||||
}
|
||||
display.setTextSize(1); // restore default text size
|
||||
#endif
|
||||
}
|
||||
@@ -215,12 +227,31 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts,
|
||||
if (!_task->isAudioPlayingInBackground()) return;
|
||||
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setTextSize(0); // tiny font (same as clock & battery %)
|
||||
display.setTextSize(_node_prefs->smallTextSize()); // tiny font (same as clock & battery %)
|
||||
int x = batteryLeftX - display.getTextWidth(">>") - 2;
|
||||
display.setCursor(x, -3); // align vertically with battery text
|
||||
display.print(">>");
|
||||
display.setTextSize(1); // restore
|
||||
}
|
||||
|
||||
// ---- Alarm enabled indicator ----
|
||||
// Shows a small bell icon to the left of the audio indicator
|
||||
// (or battery icon if no audio playing) when any alarm is enabled.
|
||||
void renderAlarmIndicator(DisplayDriver& display, int batteryLeftX) {
|
||||
AlarmScreen* alarmScr = (AlarmScreen*)_task->getAlarmScreen();
|
||||
if (!alarmScr || alarmScr->enabledCount() == 0) return;
|
||||
|
||||
// Calculate X: shift left past audio indicator if it's showing
|
||||
int rightEdge = batteryLeftX;
|
||||
if (_task->isAudioPlayingInBackground()) {
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
rightEdge = rightEdge - display.getTextWidth(">>") - 2;
|
||||
}
|
||||
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
int x = rightEdge - BELL_ICON_W - 2;
|
||||
display.drawXbm(x, 1, icon_bell_small, BELL_ICON_W, BELL_ICON_H);
|
||||
}
|
||||
#endif
|
||||
|
||||
CayenneLPP sensors_lpp;
|
||||
@@ -276,7 +307,7 @@ public:
|
||||
_task->setHomeShowingTiles(false); // Reset — only set true on FIRST page
|
||||
#endif
|
||||
// node name (tinyfont to avoid overlapping clock)
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
char filtered_name[sizeof(_node_prefs->node_name)];
|
||||
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
|
||||
@@ -290,18 +321,21 @@ public:
|
||||
display.setCursor(0, HOME_HDR_Y);
|
||||
display.print(filtered_name);
|
||||
|
||||
// battery voltage
|
||||
// battery voltage + status icons
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
int battLeftX = display.width(); // default if battery doesn't render
|
||||
renderBatteryIndicator(display, _task->getBattMilliVolts(), &battLeftX);
|
||||
|
||||
// audio background playback indicator (>> icon next to battery)
|
||||
renderAudioIndicator(display, battLeftX);
|
||||
|
||||
// alarm enabled indicator (AL icon, left of audio or battery)
|
||||
renderAlarmIndicator(display, battLeftX);
|
||||
#else
|
||||
renderBatteryIndicator(display, _task->getBattMilliVolts());
|
||||
#endif
|
||||
|
||||
// centered clock (tinyfont) - only show when time is valid
|
||||
// centered clock — only show when time is valid
|
||||
{
|
||||
uint32_t now = _rtc->getCurrentTime();
|
||||
if (now > 1700000000) { // valid timestamp (after ~Nov 2023)
|
||||
@@ -315,11 +349,14 @@ public:
|
||||
char timeBuf[6];
|
||||
sprintf(timeBuf, "%02d:%02d", hrs, mins);
|
||||
|
||||
display.setTextSize(0); // tinyfont
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
uint16_t tw = display.getTextWidth(timeBuf);
|
||||
int clockX = (display.width() - tw) / 2;
|
||||
display.setCursor(clockX, HOME_HDR_Y); // align with node name Y
|
||||
// Ensure clock doesn't overlap the node name
|
||||
int nameRight = display.getTextWidth(filtered_name) + 4;
|
||||
if (clockX < nameRight) clockX = nameRight;
|
||||
display.setCursor(clockX, HOME_HDR_Y);
|
||||
display.print(timeBuf);
|
||||
display.setTextSize(1); // restore
|
||||
}
|
||||
@@ -362,17 +399,17 @@ public:
|
||||
IPAddress ip = WiFi.localIP();
|
||||
if (ip != IPAddress(0,0,0,0)) {
|
||||
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d:%d", ip[0], ip[1], ip[2], ip[3], TCP_PORT);
|
||||
display.setTextSize(0); // Tiny font for IP
|
||||
display.setTextSize(_node_prefs->smallTextSize()); // Tiny font for IP
|
||||
display.drawTextCentered(display.width() / 2, y, tmp);
|
||||
y += 8;
|
||||
y += _node_prefs->smallLineH() - 1;
|
||||
}
|
||||
#endif
|
||||
#if defined(BLE_PIN_CODE) || defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION)
|
||||
if (_task->hasConnection()) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setTextSize(0); // Tiny font for Connected
|
||||
display.setTextSize(_node_prefs->smallTextSize()); // Tiny font for Connected
|
||||
display.drawTextCentered(display.width() / 2, y, "< Connected >");
|
||||
y += 8; // Reduced from 12
|
||||
y += _node_prefs->smallLineH() - 1;
|
||||
#ifdef BLE_PIN_CODE
|
||||
} else if (_task->isSerialEnabled() && the_mesh.getBLEPin() != 0) {
|
||||
display.setColor(DisplayDriver::RED);
|
||||
@@ -423,7 +460,7 @@ public:
|
||||
display.drawXbm(iconX, iconY, tiles[row][col].icon, HOME_ICON_W, HOME_ICON_H);
|
||||
|
||||
// Label centered below icon
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
display.drawTextCentered(tx + tileW / 2, ty + 18, tiles[row][col].label);
|
||||
}
|
||||
}
|
||||
@@ -431,47 +468,99 @@ public:
|
||||
// Nav hint below grid
|
||||
y = gridY + 2 * tileH + gapY + 2;
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
display.drawTextCentered(display.width() / 2, y, "Tap tile to open");
|
||||
}
|
||||
display.setTextSize(1);
|
||||
|
||||
#else
|
||||
// ----- T-Deck Pro: Keyboard shortcut text menu -----
|
||||
// Menu shortcuts - tinyfont monospaced grid
|
||||
y += 6;
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(0); // tinyfont 6x8 monospaced
|
||||
display.drawTextCentered(display.width() / 2, y, "Press:");
|
||||
y += 12;
|
||||
display.drawTextCentered(display.width() / 2, y, "[M] Messages [C] Contacts ");
|
||||
y += 10;
|
||||
display.drawTextCentered(display.width() / 2, y, "[N] Notes [S] Settings ");
|
||||
y += 10;
|
||||
#if HAS_GPS
|
||||
display.drawTextCentered(display.width() / 2, y, "[E] Reader [G] Maps ");
|
||||
#else
|
||||
display.drawTextCentered(display.width() / 2, y, "[E] Reader ");
|
||||
#endif
|
||||
y += 10;
|
||||
#if defined(HAS_4G_MODEM) && defined(MECK_WEB_READER)
|
||||
display.drawTextCentered(display.width() / 2, y, "[T] Phone [B] Browser ");
|
||||
#elif defined(HAS_4G_MODEM)
|
||||
display.drawTextCentered(display.width() / 2, y, "[T] Phone ");
|
||||
#elif defined(MECK_AUDIO_VARIANT) && defined(MECK_WEB_READER)
|
||||
display.drawTextCentered(display.width() / 2, y, "[P] Audiobooks [B] Browser ");
|
||||
#elif defined(MECK_AUDIO_VARIANT)
|
||||
display.drawTextCentered(display.width() / 2, y, "[P] Audiobooks ");
|
||||
#elif defined(MECK_WEB_READER)
|
||||
display.drawTextCentered(display.width() / 2, y, "[B] Browser ");
|
||||
#else
|
||||
y -= 10; // reclaim the row for standalone
|
||||
#endif
|
||||
y += 14;
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
int menuLH = _node_prefs->smallLineH();
|
||||
|
||||
// Nav hint
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.drawTextCentered(display.width() / 2, y, "Press A/D to cycle home views");
|
||||
if (_node_prefs->large_font) {
|
||||
// Proportional font: two-column layout with fixed X positions
|
||||
y += 2;
|
||||
int col1 = 2;
|
||||
int col2 = display.width() / 2;
|
||||
|
||||
display.setCursor(col1, y); display.print("[M] Messages");
|
||||
display.setCursor(col2, y); display.print("[C] Contacts");
|
||||
y += menuLH;
|
||||
display.setCursor(col1, y); display.print("[N] Notes");
|
||||
display.setCursor(col2, y); display.print("[S] Settings");
|
||||
y += menuLH;
|
||||
#if HAS_GPS
|
||||
display.setCursor(col1, y); display.print("[E] Reader");
|
||||
display.setCursor(col2, y); display.print("[G] Maps");
|
||||
#else
|
||||
display.setCursor(col1, y); display.print("[E] Reader");
|
||||
#endif
|
||||
y += menuLH;
|
||||
#if defined(HAS_4G_MODEM) && defined(MECK_WEB_READER)
|
||||
display.setCursor(col1, y); display.print("[T] Phone");
|
||||
display.setCursor(col2, y); display.print("[B] Browser");
|
||||
#elif defined(HAS_4G_MODEM)
|
||||
display.setCursor(col1, y); display.print("[T] Phone");
|
||||
display.setCursor(col2, y); display.print("[F] Discover");
|
||||
#elif defined(MECK_AUDIO_VARIANT)
|
||||
display.setCursor(col1, y); display.print("[P] Audio");
|
||||
display.setCursor(col2, y); display.print("[K] Alarm");
|
||||
y += menuLH;
|
||||
#ifdef MECK_WEB_READER
|
||||
display.setCursor(col1, y); display.print("[B] Browser");
|
||||
display.setCursor(col2, y); display.print("[F] Discover");
|
||||
#else
|
||||
display.setCursor(col1, y); display.print("[F] Discover");
|
||||
#endif
|
||||
#elif defined(MECK_WEB_READER)
|
||||
display.setCursor(col1, y); display.print("[B] Browser");
|
||||
#else
|
||||
display.setCursor(col1, y); display.print("[F] Discover");
|
||||
#endif
|
||||
y += menuLH + 2;
|
||||
} else {
|
||||
// Monospaced built-in font: centered space-padded strings
|
||||
y += 6;
|
||||
display.drawTextCentered(display.width() / 2, y, "Press:");
|
||||
y += 12;
|
||||
display.drawTextCentered(display.width() / 2, y, "[M] Messages [C] Contacts ");
|
||||
y += 10;
|
||||
display.drawTextCentered(display.width() / 2, y, "[N] Notes [S] Settings ");
|
||||
y += 10;
|
||||
#if HAS_GPS
|
||||
display.drawTextCentered(display.width() / 2, y, "[E] Reader [G] Maps ");
|
||||
#else
|
||||
display.drawTextCentered(display.width() / 2, y, "[E] Reader ");
|
||||
#endif
|
||||
y += 10;
|
||||
#if defined(HAS_4G_MODEM) && defined(MECK_WEB_READER)
|
||||
display.drawTextCentered(display.width() / 2, y, "[T] Phone [B] Browser ");
|
||||
#elif defined(HAS_4G_MODEM)
|
||||
display.drawTextCentered(display.width() / 2, y, "[T] Phone [F] Discover ");
|
||||
#elif defined(MECK_AUDIO_VARIANT)
|
||||
display.drawTextCentered(display.width() / 2, y, "[P] Audiobooks [K] Alarm ");
|
||||
y += 10;
|
||||
#ifdef MECK_WEB_READER
|
||||
display.drawTextCentered(display.width() / 2, y, "[B] Browser [F] Discover ");
|
||||
#else
|
||||
display.drawTextCentered(display.width() / 2, y, "[F] Discover ");
|
||||
#endif
|
||||
#elif defined(MECK_WEB_READER)
|
||||
display.drawTextCentered(display.width() / 2, y, "[B] Browser ");
|
||||
#else
|
||||
display.drawTextCentered(display.width() / 2, y, "[F] Discover ");
|
||||
#endif
|
||||
y += 14;
|
||||
}
|
||||
|
||||
// Nav hint (only if room)
|
||||
if (y < display.height() - 14) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.drawTextCentered(display.width() / 2, y,
|
||||
_node_prefs->large_font ? "A/D: cycle views" : "Press A/D to cycle home views");
|
||||
}
|
||||
display.setTextSize(1); // restore
|
||||
#endif
|
||||
} else if (_page == HomePage::RECENT) {
|
||||
@@ -501,7 +590,7 @@ public:
|
||||
}
|
||||
// Hint for full Last Heard screen
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.drawTextCentered(display.width() / 2, display.height() - 24,
|
||||
"Tap here for full Last Heard list");
|
||||
@@ -571,19 +660,20 @@ public:
|
||||
display.drawTextCentered(display.width() / 2, 18, "WiFi Companion");
|
||||
|
||||
int wy = 36;
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
int wLH = _node_prefs->smallLineH() + 1;
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
snprintf(tmp, sizeof(tmp), "SSID: %s", WiFi.SSID().c_str());
|
||||
display.drawTextCentered(display.width() / 2, wy, tmp);
|
||||
wy += 10;
|
||||
wy += wLH;
|
||||
IPAddress ip = WiFi.localIP();
|
||||
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
||||
display.drawTextCentered(display.width() / 2, wy, tmp);
|
||||
wy += 10;
|
||||
wy += wLH;
|
||||
snprintf(tmp, sizeof(tmp), "Port: %d", TCP_PORT);
|
||||
display.drawTextCentered(display.width() / 2, wy, tmp);
|
||||
wy += 12;
|
||||
wy += wLH + 2;
|
||||
if (_task->hasConnection()) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setTextSize(1);
|
||||
@@ -596,7 +686,7 @@ public:
|
||||
} else {
|
||||
display.setColor(DisplayDriver::RED);
|
||||
display.drawTextCentered(display.width() / 2, wy, "Not connected");
|
||||
wy += 12;
|
||||
wy += wLH + 2;
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(display.width() / 2, wy, "Configure in Settings");
|
||||
}
|
||||
@@ -697,7 +787,7 @@ public:
|
||||
display.drawTextCentered(display.width() / 2, by + 4, buf);
|
||||
|
||||
// Show controls hint
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
display.drawTextCentered(display.width() / 2, by + bh - 10, "W/S:adj Enter:ok Q:cancel");
|
||||
display.setTextSize(1);
|
||||
}
|
||||
@@ -1107,12 +1197,10 @@ public:
|
||||
}
|
||||
|
||||
// ---- Unlock hint ----
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(display.width() / 2, 120, "Hold button to unlock");
|
||||
#else
|
||||
display.drawTextCentered(display.width() / 2, 120, "Dbl-press to unlock");
|
||||
#endif
|
||||
|
||||
return 30000;
|
||||
@@ -1198,8 +1286,8 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
((ChannelScreen*)channel_screen)->setDMUnreadPtr(_dmUnread);
|
||||
contacts_screen = new ContactsScreen(this, &rtc_clock);
|
||||
((ContactsScreen*)contacts_screen)->setDMUnreadPtr(_dmUnread);
|
||||
text_reader = new TextReaderScreen(this);
|
||||
notes_screen = new NotesScreen(this);
|
||||
text_reader = new TextReaderScreen(this, node_prefs);
|
||||
notes_screen = new NotesScreen(this, node_prefs);
|
||||
settings_screen = new SettingsScreen(this, &rtc_clock, node_prefs);
|
||||
repeater_admin = nullptr; // Lazy-initialized on first use to preserve heap for audio
|
||||
discovery_screen = new DiscoveryScreen(this, &rtc_clock);
|
||||
@@ -1208,8 +1296,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
lock_screen = new LockScreen(this, &rtc_clock, node_prefs);
|
||||
#endif
|
||||
audiobook_screen = nullptr; // Created and assigned from main.cpp if audio hardware present
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
alarm_screen = nullptr; // Created and assigned from main.cpp if audio hardware present
|
||||
#endif
|
||||
#ifdef HAS_4G_MODEM
|
||||
sms_screen = new SMSScreen(this);
|
||||
sms_screen = new SMSScreen(this, node_prefs);
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
map_screen = new MapScreen(this);
|
||||
@@ -1238,6 +1329,34 @@ void UITask::showAlert(const char* text, int duration_millis) {
|
||||
_next_refresh = millis() + 100; // trigger re-render to show updated text
|
||||
}
|
||||
|
||||
void UITask::showBootHint(bool immediate) {
|
||||
if (immediate) {
|
||||
// Activate now — used when hint should overlay the current screen (e.g. onboarding)
|
||||
_hintActive = true;
|
||||
_hintExpiry = millis() + 8000; // 8 seconds auto-dismiss
|
||||
_pendingBootHint = false;
|
||||
_next_refresh = millis() + 100;
|
||||
Serial.println("[UI] Boot hint activated (immediate)");
|
||||
} else {
|
||||
// Defer until after splash screen — actual activation happens in gotoHomeScreen()
|
||||
_pendingBootHint = true;
|
||||
Serial.println("[UI] Boot hint pending (will show after splash)");
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::dismissBootHint() {
|
||||
if (!_hintActive) return;
|
||||
_hintActive = false;
|
||||
_hintExpiry = 0;
|
||||
// Persist so hint never shows again
|
||||
if (_node_prefs) {
|
||||
_node_prefs->hint_shown = 1;
|
||||
the_mesh.savePrefs();
|
||||
}
|
||||
_next_refresh = millis() + 100;
|
||||
Serial.println("[UI] Boot hint dismissed");
|
||||
}
|
||||
|
||||
void UITask::notify(UIEventType t) {
|
||||
#if defined(PIN_BUZZER)
|
||||
switch(t){
|
||||
@@ -1426,6 +1545,7 @@ void UITask::setCurrScreen(UIScreen* c) {
|
||||
curr = c;
|
||||
_alert_expiry = 0; // Dismiss any active toast — prevents stale overlay from
|
||||
// triggering extra 644ms e-ink refreshes on the new screen
|
||||
if (_hintActive) dismissBootHint(); // Dismiss hint when navigating away
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
@@ -1600,6 +1720,14 @@ void UITask::loop() {
|
||||
}
|
||||
#endif
|
||||
|
||||
if (c != 0 && curr) {
|
||||
// Dismiss boot hint on any button input (boot button on T5S3)
|
||||
if (_hintActive) {
|
||||
dismissBootHint();
|
||||
c = 0; // Consume the press
|
||||
}
|
||||
}
|
||||
|
||||
if (c != 0 && curr) {
|
||||
curr->handleInput(c);
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
@@ -1721,7 +1849,56 @@ if (curr) curr->poll();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (millis() < _alert_expiry) {
|
||||
// Check if settings screen needs VKB for text editing (channel name, freq, APN)
|
||||
if (isOnSettingsScreen() && !_vkbActive) {
|
||||
SettingsScreen* ss = (SettingsScreen*)settings_screen;
|
||||
if (ss->needsTextVKB()) {
|
||||
ss->clearTextNeedsVKB();
|
||||
// Pick a context-appropriate label
|
||||
const char* label = "Edit";
|
||||
SettingsRowType rt = ss->getCurrentRowType();
|
||||
if (rt == ROW_NAME) label = "Node Name";
|
||||
else if (rt == ROW_ADD_CHANNEL) label = "Channel Name";
|
||||
else if (rt == ROW_FREQ) label = "Frequency";
|
||||
showVirtualKeyboard(VKB_SETTINGS_TEXT, label, ss->getEditBuf(), 31);
|
||||
}
|
||||
}
|
||||
|
||||
if (_hintActive && millis() < _hintExpiry) {
|
||||
// Boot navigation hint overlay — multi-line, larger box
|
||||
_display->setTextSize(1);
|
||||
int w = _display->width();
|
||||
int h = _display->height();
|
||||
int boxX = w / 8;
|
||||
int boxY = h / 5;
|
||||
int boxW = w - boxX * 2;
|
||||
int boxH = h * 3 / 5;
|
||||
_display->setColor(DisplayDriver::DARK);
|
||||
_display->fillRect(boxX, boxY, boxW, boxH);
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->drawRect(boxX, boxY, boxW, boxH);
|
||||
int cx = w / 2;
|
||||
int lineH = 11;
|
||||
int startY = boxY + 6;
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
_display->drawTextCentered(cx, startY, "Swipe: Navigate");
|
||||
_display->drawTextCentered(cx, startY + lineH, "Tap: Select");
|
||||
_display->drawTextCentered(cx, startY + lineH * 2, "Long Press: Action");
|
||||
_display->drawTextCentered(cx, startY + lineH * 3, "Boot Btn: Home");
|
||||
_display->drawTextCentered(cx, startY + lineH * 4 + 4, "[Tap to dismiss hint]");
|
||||
#else
|
||||
_display->drawTextCentered(cx, startY, "M:Msgs C:Contacts");
|
||||
_display->drawTextCentered(cx, startY + lineH, "S:Settings E:Reader");
|
||||
_display->drawTextCentered(cx, startY + lineH * 2, "N:Notes W/S:Scroll");
|
||||
_display->drawTextCentered(cx, startY + lineH * 3, "A/D:Cycle Left/Right");
|
||||
_display->drawTextCentered(cx, startY + lineH * 4 + 4, "[X to dismiss hint]");
|
||||
#endif
|
||||
_next_refresh = _hintExpiry;
|
||||
} else if (_hintActive) {
|
||||
// Hint expired — auto-dismiss
|
||||
dismissBootHint();
|
||||
_next_refresh = millis() + 200;
|
||||
} else if (millis() < _alert_expiry) {
|
||||
_display->setTextSize(1);
|
||||
int y = _display->height() / 3;
|
||||
int p = _display->height() / 32;
|
||||
@@ -1737,7 +1914,33 @@ if (curr) curr->poll();
|
||||
}
|
||||
#else
|
||||
int delay_millis = curr->render(*_display);
|
||||
if (millis() < _alert_expiry) { // render alert popup
|
||||
if (_hintActive && millis() < _hintExpiry) {
|
||||
// Boot navigation hint overlay — multi-line, larger box
|
||||
_display->setTextSize(1);
|
||||
int w = _display->width();
|
||||
int h = _display->height();
|
||||
int boxX = w / 8;
|
||||
int boxY = h / 5;
|
||||
int boxW = w - boxX * 2;
|
||||
int boxH = h * 3 / 5;
|
||||
_display->setColor(DisplayDriver::DARK);
|
||||
_display->fillRect(boxX, boxY, boxW, boxH);
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->drawRect(boxX, boxY, boxW, boxH);
|
||||
int cx = w / 2;
|
||||
int lineH = 11;
|
||||
int startY = boxY + 6;
|
||||
_display->drawTextCentered(cx, startY, "M:Msgs C:Contacts");
|
||||
_display->drawTextCentered(cx, startY + lineH, "S:Settings E:Reader");
|
||||
_display->drawTextCentered(cx, startY + lineH * 2, "N:Notes W/S:Scroll");
|
||||
_display->drawTextCentered(cx, startY + lineH * 3, "A/D:Cycle Left/Right");
|
||||
_display->drawTextCentered(cx, startY + lineH * 4 + 4, "[X to dismiss]");
|
||||
_next_refresh = _hintExpiry;
|
||||
} else if (_hintActive) {
|
||||
// Hint expired — auto-dismiss
|
||||
dismissBootHint();
|
||||
_next_refresh = millis() + 200;
|
||||
} else if (millis() < _alert_expiry) { // render alert popup
|
||||
_display->setTextSize(1);
|
||||
int y = _display->height() / 3;
|
||||
int p = _display->height() / 32;
|
||||
@@ -1796,6 +1999,42 @@ if (curr) curr->poll();
|
||||
}
|
||||
#endif
|
||||
|
||||
// ── T5S3 standalone powersaving ──────────────────────────────────────────
|
||||
// When locked with display off, enter ESP32 light sleep (~8 mA total).
|
||||
// Radio stays in continuous RX — DIO1 going HIGH wakes the CPU instantly.
|
||||
// Boot button (GPIO0 LOW) and a 30-min safety timer also wake.
|
||||
// First sleep starts 60s after lock; subsequent cycles wake for 5s to let
|
||||
// the mesh stack process/relay any received packet, then sleep again.
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro) && !defined(BLE_PIN_CODE) && !defined(MECK_WIFI_COMPANION)
|
||||
if (_locked && _display != NULL && !_display->isOn()) {
|
||||
unsigned long now = millis();
|
||||
if (now - _psLastActive >= _psNextSleepSecs * 1000UL) {
|
||||
Serial.println("[POWERSAVE] Entering light sleep (locked+idle)");
|
||||
board.sleep(1800); // Light sleep up to 30 min
|
||||
// ── CPU resumes here on wake ──
|
||||
unsigned long wakeAt = millis();
|
||||
_psLastActive = wakeAt;
|
||||
_psNextSleepSecs = 5; // Stay awake 5s for mesh processing
|
||||
|
||||
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
|
||||
if (cause == ESP_SLEEP_WAKEUP_GPIO) {
|
||||
// Boot button pressed — unlock and return to normal use
|
||||
Serial.println("[POWERSAVE] Woke by button — unlocking");
|
||||
unlockScreen();
|
||||
_psNextSleepSecs = 60; // Reset to long delay after user interaction
|
||||
} else if (cause == ESP_SLEEP_WAKEUP_EXT1) {
|
||||
Serial.println("[POWERSAVE] Woke by LoRa packet");
|
||||
} else if (cause == ESP_SLEEP_WAKEUP_TIMER) {
|
||||
Serial.println("[POWERSAVE] Woke by timer");
|
||||
}
|
||||
}
|
||||
} else if (!_locked) {
|
||||
// Not locked — keep powersaving timer reset so first sleep is 60s after lock
|
||||
_psLastActive = millis();
|
||||
_psNextSleepSecs = 60;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef PIN_VIBRATION
|
||||
vibration.loop();
|
||||
#endif
|
||||
@@ -1922,6 +2161,10 @@ void UITask::lockScreen() {
|
||||
_next_refresh = 0; // Draw lock screen immediately
|
||||
_auto_off = millis() + 60000; // 60s before display off while locked
|
||||
_lastLockRefresh = millis(); // Start 15-min clock refresh cycle
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro) && !defined(BLE_PIN_CODE) && !defined(MECK_WIFI_COMPANION)
|
||||
_psLastActive = millis(); // Start powersaving countdown (60s to first sleep)
|
||||
_psNextSleepSecs = 60;
|
||||
#endif
|
||||
Serial.println("[UI] Screen locked — entering low-power mode");
|
||||
}
|
||||
|
||||
@@ -2044,6 +2287,19 @@ void UITask::onVKBSubmit() {
|
||||
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
|
||||
break;
|
||||
}
|
||||
case VKB_SETTINGS_TEXT: {
|
||||
// Generic settings text edit — copy text back to settings edit buffer
|
||||
// and confirm via the normal Enter path (handles name/freq/channel/APN)
|
||||
SettingsScreen* ss = (SettingsScreen*)settings_screen;
|
||||
if (strlen(text) > 0) {
|
||||
ss->submitEditText(text);
|
||||
} else {
|
||||
// Empty submission — cancel the edit
|
||||
ss->handleInput('q');
|
||||
}
|
||||
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
|
||||
break;
|
||||
}
|
||||
case VKB_NOTES: {
|
||||
NotesScreen* notes = (NotesScreen*)getNotesScreen();
|
||||
if (notes && strlen(text) > 0) {
|
||||
@@ -2248,6 +2504,15 @@ void UITask::gotoHomeScreen() {
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
|
||||
// Activate deferred boot hint now that home screen is visible
|
||||
if (_pendingBootHint) {
|
||||
_pendingBootHint = false;
|
||||
_hintActive = true;
|
||||
_hintExpiry = millis() + 8000; // 8 seconds auto-dismiss
|
||||
_next_refresh = millis() + 100;
|
||||
Serial.println("[UI] Boot hint activated");
|
||||
}
|
||||
}
|
||||
|
||||
bool UITask::isEditingHomeScreen() const {
|
||||
@@ -2375,6 +2640,22 @@ void UITask::gotoAudiobookPlayer() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
void UITask::gotoAlarmScreen() {
|
||||
if (alarm_screen == nullptr) return;
|
||||
AlarmScreen* alarmScr = (AlarmScreen*)alarm_screen;
|
||||
if (_display != NULL) {
|
||||
alarmScr->enter(*_display);
|
||||
}
|
||||
setCurrScreen(alarm_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
void UITask::gotoSMSScreen() {
|
||||
SMSScreen* smsScr = (SMSScreen*)sms_screen;
|
||||
@@ -2505,7 +2786,7 @@ void UITask::gotoWebReader() {
|
||||
if (web_reader == nullptr) {
|
||||
Serial.printf("WebReader: lazy init - free heap: %d, largest block: %d\n",
|
||||
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
|
||||
web_reader = new WebReaderScreen(this);
|
||||
web_reader = new WebReaderScreen(this, _node_prefs);
|
||||
Serial.printf("WebReader: init complete - free heap: %d\n", ESP.getFreeHeap());
|
||||
}
|
||||
WebReaderScreen* wr = (WebReaderScreen*)web_reader;
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
#include "WebReaderScreen.h"
|
||||
#endif
|
||||
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
#include "AlarmScreen.h"
|
||||
#endif
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
#include "VirtualKeyboard.h"
|
||||
#endif
|
||||
@@ -56,6 +60,9 @@ class UITask : public AbstractUITask {
|
||||
NodePrefs* _node_prefs;
|
||||
char _alert[80];
|
||||
unsigned long _alert_expiry;
|
||||
bool _hintActive = false; // Boot navigation hint overlay
|
||||
unsigned long _hintExpiry = 0; // Auto-dismiss time for hint
|
||||
bool _pendingBootHint = false; // Deferred hint — show after splash screen
|
||||
int _msgcount;
|
||||
unsigned long ui_started_at, next_batt_chck;
|
||||
uint8_t _low_batt_count = 0; // Consecutive low-voltage readings for debounce
|
||||
@@ -79,6 +86,9 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* notes_screen; // Notes editor screen
|
||||
UIScreen* settings_screen; // Settings/onboarding screen
|
||||
UIScreen* audiobook_screen; // Audiobook player screen (null if not available)
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
UIScreen* alarm_screen; // Alarm clock screen (audio variant only)
|
||||
#endif
|
||||
#ifdef HAS_4G_MODEM
|
||||
UIScreen* sms_screen; // SMS messaging screen (4G variant only)
|
||||
#endif
|
||||
@@ -103,6 +113,13 @@ class UITask : public AbstractUITask {
|
||||
bool _vkbActive = false;
|
||||
UIScreen* _screenBeforeVKB = nullptr;
|
||||
unsigned long _vkbOpenedAt = 0;
|
||||
|
||||
// Powersaving: light sleep when locked + idle (standalone only — no BLE/WiFi)
|
||||
// Wakes on LoRa packet (DIO1), boot button (GPIO0), or 30-min timer
|
||||
#if !defined(BLE_PIN_CODE) && !defined(MECK_WIFI_COMPANION)
|
||||
unsigned long _psLastActive = 0; // millis() at last wake or lock entry
|
||||
unsigned long _psNextSleepSecs = 60; // Seconds before first sleep (60s), then 5s cycles
|
||||
#endif
|
||||
#ifdef MECK_CARDKB
|
||||
bool _cardkbDetected = false;
|
||||
#endif
|
||||
@@ -169,6 +186,9 @@ public:
|
||||
void gotoSettingsScreen(); // Navigate to settings
|
||||
void gotoOnboarding(); // Navigate to settings in onboarding mode
|
||||
void gotoAudiobookPlayer(); // Navigate to audiobook player
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
void gotoAlarmScreen(); // Navigate to alarm clock
|
||||
#endif
|
||||
void gotoRepeaterAdmin(int contactIdx); // Navigate to repeater admin
|
||||
void gotoRepeaterAdminDirect(int contactIdx); // Auto-login admin (L key from conversation)
|
||||
void gotoDiscoveryScreen(); // Navigate to node discovery scan
|
||||
@@ -186,6 +206,9 @@ public:
|
||||
#endif
|
||||
void showAlert(const char* text, int duration_millis) override;
|
||||
void forceRefresh() override { _next_refresh = 100; }
|
||||
void showBootHint(bool immediate = false); // Show navigation hint overlay on first boot
|
||||
void dismissBootHint(); // Dismiss hint and save preference
|
||||
bool isHintActive() const { return _hintActive; }
|
||||
// Wake display and extend auto-off timer. Call this when handling keys
|
||||
// outside of injectKey() to prevent display auto-off during direct input.
|
||||
void keepAlive() {
|
||||
@@ -215,6 +238,9 @@ public:
|
||||
bool isOnNotesScreen() const { return curr == notes_screen; }
|
||||
bool isOnSettingsScreen() const { return curr == settings_screen; }
|
||||
bool isOnAudiobookPlayer() const { return curr == audiobook_screen; }
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
bool isOnAlarmScreen() const { return curr == alarm_screen; }
|
||||
#endif
|
||||
bool isOnRepeaterAdmin() const { return curr == repeater_admin; }
|
||||
bool isOnDiscoveryScreen() const { return curr == discovery_screen; }
|
||||
bool isOnLastHeardScreen() const { return curr == last_heard_screen; }
|
||||
@@ -280,8 +306,13 @@ public:
|
||||
UIScreen* getContactsScreen() const { return contacts_screen; }
|
||||
UIScreen* getChannelScreen() const { return channel_screen; }
|
||||
UIScreen* getSettingsScreen() const { return settings_screen; }
|
||||
NodePrefs* getNodePrefs() const { return _node_prefs; }
|
||||
UIScreen* getAudiobookScreen() const { return audiobook_screen; }
|
||||
void setAudiobookScreen(UIScreen* s) { audiobook_screen = s; }
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
UIScreen* getAlarmScreen() const { return alarm_screen; }
|
||||
void setAlarmScreen(UIScreen* s) { alarm_screen = s; }
|
||||
#endif
|
||||
UIScreen* getRepeaterAdminScreen() const { return repeater_admin; }
|
||||
UIScreen* getDiscoveryScreen() const { return discovery_screen; }
|
||||
UIScreen* getLastHeardScreen() const { return last_heard_screen; }
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "ModemManager.h"
|
||||
#endif
|
||||
#include "Utf8CP437.h"
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
// Forward declarations
|
||||
class UITask;
|
||||
@@ -1030,8 +1031,10 @@ public:
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
NodePrefs* _prefs;
|
||||
Mode _mode;
|
||||
bool _initialized;
|
||||
uint8_t _lastFontPref;
|
||||
DisplayDriver* _display;
|
||||
|
||||
// Display layout (calculated once)
|
||||
@@ -1424,7 +1427,7 @@ private:
|
||||
_display->print("WiFi Setup");
|
||||
_display->drawRect(0, 11, _display->width(), 1);
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setTextSize(0);
|
||||
_display->setTextSize(_prefs->smallTextSize());
|
||||
_display->setCursor(0, 18);
|
||||
_display->print("Scanning for networks...");
|
||||
_display->endFrame();
|
||||
@@ -1524,7 +1527,7 @@ private:
|
||||
_display->print("Web Reader");
|
||||
_display->drawRect(0, 11, _display->width(), 1);
|
||||
|
||||
_display->setTextSize(0);
|
||||
_display->setTextSize(_prefs->smallTextSize());
|
||||
_display->setCursor(0, 18);
|
||||
_display->print("Connected!");
|
||||
_display->setCursor(0, 30);
|
||||
@@ -2306,7 +2309,7 @@ private:
|
||||
_display->print("Web Reader");
|
||||
_display->drawRect(0, 11, _display->width(), 1);
|
||||
_display->setColor(DisplayDriver::YELLOW);
|
||||
_display->setTextSize(0);
|
||||
_display->setTextSize(_prefs->smallTextSize());
|
||||
_display->setCursor(0, 18);
|
||||
_display->print("Fetch failed:");
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
@@ -2442,7 +2445,7 @@ private:
|
||||
_display->setTextSize(2);
|
||||
_display->setCursor(10, 20);
|
||||
_display->print("Logging in...");
|
||||
_display->setTextSize(0);
|
||||
_display->setTextSize(_prefs->smallTextSize());
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setCursor(10, 45);
|
||||
_display->print("Refreshing session...");
|
||||
@@ -2656,14 +2659,14 @@ private:
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
|
||||
if (_wifiState == WIFI_SCANNING) {
|
||||
display.setCursor(0, 18);
|
||||
display.print("Scanning for networks...");
|
||||
} else if (_wifiState == WIFI_SCAN_DONE) {
|
||||
int y = 14;
|
||||
int listLineH = 8;
|
||||
int listLineH = _prefs ? _prefs->smallLineH() : 9;
|
||||
for (int i = 0; i < _ssidCount && y < display.height() - 24; i++) {
|
||||
bool selected = (i == _selectedSSID);
|
||||
if (selected) {
|
||||
@@ -2671,7 +2674,7 @@ private:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), listLineH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), listLineH);
|
||||
display.fillRect(0, y + _prefs->smallHighlightOff(), display.width(), listLineH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
@@ -2695,7 +2698,7 @@ private:
|
||||
y += 12;
|
||||
display.setCursor(0, y);
|
||||
display.print("Password:");
|
||||
y += 10;
|
||||
y += _prefs->smallLineH() + 1;
|
||||
display.setCursor(0, y);
|
||||
// Show masked password with brief reveal of last char
|
||||
char passBuf[WEB_WIFI_PASS_LEN + 2];
|
||||
@@ -2771,7 +2774,7 @@ private:
|
||||
|
||||
if (isNetworkAvailable()) {
|
||||
display.print("Web Reader");
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
if (isWiFiConnected()) {
|
||||
IPAddress ip = WiFi.localIP();
|
||||
@@ -2797,7 +2800,7 @@ private:
|
||||
const int footerY = display.height() - 12;
|
||||
const int viewportH = display.height() - headerY - footerH;
|
||||
const int scrollbarW = 4;
|
||||
const int listLineH = 8;
|
||||
const int listLineH = _prefs ? _prefs->smallLineH() : 9;
|
||||
const int sepH = 8; // Separator between IRC and web sections
|
||||
const int sectionH = listLineH; // Section header height
|
||||
int maxChars = _charsPerLine - 2; // Account for "> " prefix
|
||||
@@ -2875,7 +2878,7 @@ private:
|
||||
if (totalContentH <= viewportH) _homeScrollY = 0;
|
||||
|
||||
// ---- Render pass (with scroll offset) ----
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
int y = headerY - _homeScrollY; // Start Y in screen coords
|
||||
itemIdx = 0;
|
||||
bool needsScroll = (totalContentH > viewportH);
|
||||
@@ -2895,7 +2898,7 @@ private:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH);
|
||||
display.fillRect(0, y + _prefs->smallHighlightOff(), display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
@@ -2934,7 +2937,7 @@ private:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH);
|
||||
display.fillRect(0, y + _prefs->smallHighlightOff(), display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
@@ -2971,7 +2974,7 @@ private:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH);
|
||||
display.fillRect(0, y + _prefs->smallHighlightOff(), display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
@@ -3024,7 +3027,7 @@ private:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, contentW, itemH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, contentW, itemH);
|
||||
display.fillRect(0, y + _prefs->smallHighlightOff(), contentW, itemH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
@@ -3076,7 +3079,7 @@ private:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, contentW, itemH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, contentW, itemH);
|
||||
display.fillRect(0, y + _prefs->smallHighlightOff(), contentW, itemH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
@@ -3198,7 +3201,7 @@ private:
|
||||
display.setCursor(10, 20);
|
||||
display.print("Loading...");
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
// Word-wrap the URL across multiple lines
|
||||
@@ -3243,7 +3246,7 @@ private:
|
||||
display.print("Download Complete");
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, 16);
|
||||
display.print("Saved to /books/:");
|
||||
@@ -3277,7 +3280,7 @@ private:
|
||||
display.print("Download Failed");
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, 18);
|
||||
display.print(_fetchError.c_str());
|
||||
@@ -3314,7 +3317,7 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
// Determine page bounds
|
||||
@@ -3476,9 +3479,16 @@ private:
|
||||
// ---- Layout Initialization ----
|
||||
|
||||
void initLayout(DisplayDriver& display) {
|
||||
// Re-init if font preference changed since last layout
|
||||
uint8_t curFont = _prefs ? _prefs->large_font : 0;
|
||||
if (_initialized && curFont != _lastFontPref) {
|
||||
_initialized = false;
|
||||
Serial.println("WebReader: font changed, recalculating layout");
|
||||
}
|
||||
if (_initialized) return;
|
||||
_lastFontPref = curFont;
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
uint16_t mWidth = display.getTextWidth("M");
|
||||
if (mWidth > 0) {
|
||||
_charsPerLine = display.width() / mWidth;
|
||||
@@ -3487,6 +3497,19 @@ private:
|
||||
_charsPerLine = 40;
|
||||
_lineHeight = 5;
|
||||
}
|
||||
// Proportional font: use average-width measurement instead of M-width
|
||||
if (_prefs && _prefs->large_font && mWidth > 0) {
|
||||
const char* sample = "the quick brown fox jumps over lazy dog";
|
||||
uint16_t sampleW = display.getTextWidth(sample);
|
||||
int sampleLen = strlen(sample);
|
||||
if (sampleW > 0 && sampleLen > 0) {
|
||||
_charsPerLine = (display.width() * sampleLen * 70) / ((int)sampleW * 100);
|
||||
}
|
||||
}
|
||||
// Large font: formula above assumes built-in 6x8 ratio — too small for 9pt
|
||||
if (_prefs && _prefs->large_font) {
|
||||
_lineHeight = _prefs->smallLineH();
|
||||
}
|
||||
|
||||
_footerHeight = 14;
|
||||
int textAreaHeight = display.height() - _footerHeight;
|
||||
@@ -3931,7 +3954,7 @@ private:
|
||||
if (_activeForm < 0 || _activeForm >= _formCount) return;
|
||||
WebForm& form = _forms[_activeForm];
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
|
||||
// Header
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
@@ -3954,7 +3977,7 @@ private:
|
||||
display.drawRect(0, 9, display.width(), 1);
|
||||
|
||||
int y = 12;
|
||||
int lineH = 10; // Taller lines for form fields
|
||||
int lineH = _prefs->smallLineH() + 1; // Taller lines for form fields
|
||||
int visCount = getVisibleFieldCount(form);
|
||||
|
||||
// Render each visible field
|
||||
@@ -4662,9 +4685,9 @@ private:
|
||||
display.print("IRC Setup");
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
int y = 16;
|
||||
int lineH = 10;
|
||||
int lineH = _prefs->smallLineH() + 1;
|
||||
|
||||
const char* labels[] = {"Server:", "Port:", "Nick:", "Channel:", "[ Connect ]"};
|
||||
const char* chanDisp = (_ircChannel[0] != '\0') ? _ircChannel : "(none)";
|
||||
@@ -4822,7 +4845,7 @@ private:
|
||||
display.print(header);
|
||||
|
||||
// Connection indicator on right
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
if (!_ircConnected) {
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(display.width() - 42, -3);
|
||||
@@ -4848,7 +4871,7 @@ private:
|
||||
|
||||
if (_ircComposing) {
|
||||
// Compose text just above separator (tiny font to match messages)
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, footerY - 12);
|
||||
char compDisp[IRC_COMPOSE_MAX + 4];
|
||||
@@ -4878,10 +4901,10 @@ private:
|
||||
}
|
||||
|
||||
// Message area
|
||||
display.setTextSize(0);
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
int msgAreaTop = 14;
|
||||
int msgAreaBottom = _ircComposing ? footerY - 16 : footerY - 4;
|
||||
int lineH = 8;
|
||||
int lineH = _prefs->smallLineH() - 1;
|
||||
int scrollBarW = 4;
|
||||
int lineW = _charsPerLine - 1; // Reserve space for scroll bar
|
||||
_ircLinesPerPage = (msgAreaBottom - msgAreaTop) / lineH;
|
||||
@@ -5065,8 +5088,8 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
WebReaderScreen(UITask* task)
|
||||
: _task(task), _mode(HOME), _initialized(false), _display(nullptr),
|
||||
WebReaderScreen(UITask* task, NodePrefs* prefs = nullptr)
|
||||
: _task(task), _prefs(prefs), _mode(HOME), _initialized(false), _lastFontPref(0), _display(nullptr),
|
||||
_charsPerLine(40), _linesPerPage(15), _lineHeight(5), _footerHeight(14),
|
||||
_wifiState(WIFI_IDLE), _ssidCount(0), _selectedSSID(0), _wifiPassLen(0),
|
||||
_urlLen(0), _urlCursor(0),
|
||||
@@ -5150,7 +5173,7 @@ public:
|
||||
_display->print("Web Reader");
|
||||
_display->drawRect(0, 11, _display->width(), 1);
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setTextSize(0);
|
||||
_display->setTextSize(_prefs->smallTextSize());
|
||||
_display->setCursor(0, 18);
|
||||
_display->print("Connecting to WiFi...");
|
||||
_display->endFrame();
|
||||
|
||||
@@ -46,4 +46,18 @@ static const uint8_t icon_notepad[] PROGMEM = {
|
||||
static const uint8_t icon_search[] PROGMEM = {
|
||||
0x3C,0x00, 0x42,0x00, 0x81,0x00, 0x81,0x00, 0x81,0x00, 0x42,0x00,
|
||||
0x3C,0x00, 0x03,0x00, 0x01,0x80, 0x00,0xC0, 0x00,0x40, 0x00,0x00,
|
||||
};
|
||||
|
||||
// ⏰ Alarm Clock (AlarmScreen) — 12x12 home tile icon
|
||||
static const uint8_t icon_alarm[] PROGMEM = {
|
||||
0x40,0x40, 0x9E,0x20, 0x20,0x80, 0x44,0x40, 0x44,0x40, 0x46,0x40,
|
||||
0x40,0x40, 0x20,0x80, 0x1F,0x00, 0x00,0x00, 0x20,0x40, 0x40,0x20,
|
||||
};
|
||||
|
||||
// 🔔 Bell — 7x8 status bar indicator (alarm enabled)
|
||||
// MSB-first, 1 byte per row
|
||||
#define BELL_ICON_W 7
|
||||
#define BELL_ICON_H 8
|
||||
static const uint8_t icon_bell_small[] PROGMEM = {
|
||||
0x10, 0x38, 0x7C, 0x7C, 0x7C, 0xFE, 0x00, 0x10,
|
||||
};
|
||||
@@ -1,7 +1,10 @@
|
||||
"""
|
||||
PlatformIO post-build script: merge bootloader + partitions + firmware
|
||||
PlatformIO post-build script: merge bootloader + partitions + firmware + SPIFFS
|
||||
into a single flashable binary.
|
||||
|
||||
Includes a pre-formatted empty SPIFFS image so first-boot doesn't need to
|
||||
format the partition (which takes 1-2 minutes on 16MB flash).
|
||||
|
||||
Output: .pio/build/<env>/firmware_merged.bin
|
||||
Flash: esptool.py --chip esp32s3 write_flash 0x0 firmware_merged.bin
|
||||
|
||||
@@ -12,6 +15,87 @@ Add to each environment (or the base section):
|
||||
|
||||
Import("env")
|
||||
|
||||
def find_spiffs_partition(partitions_bin):
|
||||
"""Parse compiled partitions.bin to find SPIFFS partition offset and size.
|
||||
|
||||
ESP32 partition entry format (32 bytes each):
|
||||
0xAA50 magic, type, subtype, offset(u32le), size(u32le), label(16), flags(u32le)
|
||||
SPIFFS: type=0x01(data), subtype=0x82(spiffs)
|
||||
"""
|
||||
import struct
|
||||
|
||||
with open(partitions_bin, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
for i in range(0, len(data) - 32, 32):
|
||||
magic = struct.unpack_from("<H", data, i)[0]
|
||||
if magic != 0xAA50:
|
||||
continue
|
||||
ptype = data[i + 2]
|
||||
subtype = data[i + 3]
|
||||
offset = struct.unpack_from("<I", data, i + 4)[0]
|
||||
size = struct.unpack_from("<I", data, i + 8)[0]
|
||||
label = data[i + 12:i + 28].split(b'\x00')[0].decode("ascii", errors="ignore")
|
||||
if ptype == 0x01 and subtype == 0x82: # data/spiffs
|
||||
return offset, size, label
|
||||
return None, None, None
|
||||
|
||||
|
||||
def build_spiffs_image(env, size):
|
||||
"""Generate an empty formatted SPIFFS image using mkspiffs."""
|
||||
import subprocess, os, tempfile, glob
|
||||
|
||||
build_dir = env.subst("$BUILD_DIR")
|
||||
spiffs_bin = os.path.join(build_dir, "spiffs_empty.bin")
|
||||
|
||||
# If already generated for this build, reuse it
|
||||
if os.path.isfile(spiffs_bin) and os.path.getsize(spiffs_bin) == size:
|
||||
return spiffs_bin
|
||||
|
||||
# Find mkspiffs in PlatformIO packages
|
||||
pio_home = os.path.expanduser("~/.platformio")
|
||||
mkspiffs_paths = glob.glob(os.path.join(pio_home, "packages", "tool-mkspiffs*", "mkspiffs*"))
|
||||
if not mkspiffs_paths:
|
||||
# Also check platform-specific tool paths
|
||||
mkspiffs_paths = glob.glob(os.path.join(pio_home, "packages", "tool-mklittlefs*", "mkspiffs*"))
|
||||
|
||||
mkspiffs = None
|
||||
for p in mkspiffs_paths:
|
||||
if os.path.isfile(p) and os.access(p, os.X_OK):
|
||||
mkspiffs = p
|
||||
break
|
||||
|
||||
if not mkspiffs:
|
||||
print("[merge] WARNING: mkspiffs not found, skipping SPIFFS image")
|
||||
return None
|
||||
|
||||
# Create empty data directory for mkspiffs
|
||||
data_dir = os.path.join(build_dir, "_empty_spiffs_data")
|
||||
os.makedirs(data_dir, exist_ok=True)
|
||||
|
||||
# SPIFFS block/page sizes — ESP32 Arduino defaults
|
||||
block_size = 4096
|
||||
page_size = 256
|
||||
|
||||
cmd = [
|
||||
mkspiffs,
|
||||
"-c", data_dir,
|
||||
"-b", str(block_size),
|
||||
"-p", str(page_size),
|
||||
"-s", str(size),
|
||||
spiffs_bin,
|
||||
]
|
||||
|
||||
print(f"[merge] Generating empty SPIFFS image ({size // 1024} KB)...")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode == 0 and os.path.isfile(spiffs_bin):
|
||||
print(f"[merge] SPIFFS image OK: {spiffs_bin}")
|
||||
return spiffs_bin
|
||||
else:
|
||||
print(f"[merge] mkspiffs failed: {result.stderr}")
|
||||
return None
|
||||
|
||||
|
||||
def merge_bin(source, target, env):
|
||||
import subprocess, os
|
||||
|
||||
@@ -52,8 +136,18 @@ def merge_bin(source, target, env):
|
||||
"0x10000", firmware,
|
||||
]
|
||||
|
||||
# Try to include a pre-formatted SPIFFS image (eliminates 1-2 min first-boot format)
|
||||
spiffs_offset, spiffs_size, spiffs_label = find_spiffs_partition(partitions)
|
||||
if spiffs_offset and spiffs_size:
|
||||
spiffs_bin = build_spiffs_image(env, spiffs_size)
|
||||
if spiffs_bin:
|
||||
cmd.extend([f"0x{spiffs_offset:x}", spiffs_bin])
|
||||
print(f"[merge] Including SPIFFS image at 0x{spiffs_offset:x} ({spiffs_size // 1024} KB)")
|
||||
else:
|
||||
print("[merge] No SPIFFS partition found in partition table, skipping SPIFFS image")
|
||||
|
||||
print(f"\n[merge] Creating merged firmware for {env_name}...")
|
||||
print(f"[merge] {' '.join(cmd[-6:])}")
|
||||
print(f"[merge] {' '.join(cmd[-8:])}")
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
|
||||
@@ -36,7 +36,7 @@ uint32_t Dispatcher::getCADFailRetryDelay() const {
|
||||
return 200;
|
||||
}
|
||||
uint32_t Dispatcher::getCADFailMaxDuration() const {
|
||||
return 4000; // 4 seconds
|
||||
return 6000; // 6 seconds
|
||||
}
|
||||
|
||||
void Dispatcher::loop() {
|
||||
@@ -273,12 +273,16 @@ void Dispatcher::checkSend() {
|
||||
outbound_start = _ms->getMillis();
|
||||
bool success = _radio->startSendRaw(raw, len);
|
||||
if (!success) {
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime());
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): ERROR: send start failed!", getLogDateTime());
|
||||
|
||||
logTxFail(outbound, outbound->getRawLength());
|
||||
|
||||
releasePacket(outbound); // return to pool
|
||||
|
||||
// re-queue instead of dropping so the packet gets another chance
|
||||
int retry_delay = getCADFailRetryDelay();
|
||||
unsigned long retry_time = futureMillis(retry_delay);
|
||||
_mgr->queueOutbound(outbound, 0, retry_time);
|
||||
outbound = NULL;
|
||||
next_tx_time = retry_time;
|
||||
return;
|
||||
}
|
||||
outbound_expiry = futureMillis(max_airtime);
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
#endif
|
||||
|
||||
void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||
sendFlood(pkt, delay_millis);
|
||||
sendFlood(pkt, delay_millis, getPathHashSize());
|
||||
}
|
||||
void BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||
sendFlood(pkt, delay_millis);
|
||||
sendFlood(pkt, delay_millis, getPathHashSize());
|
||||
}
|
||||
|
||||
mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name) {
|
||||
|
||||
@@ -130,6 +130,7 @@ protected:
|
||||
virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0;
|
||||
virtual void handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len);
|
||||
|
||||
virtual uint8_t getPathHashSize() const = 0;
|
||||
virtual void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0);
|
||||
virtual void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <Wire.h>
|
||||
#include "esp_wifi.h"
|
||||
#include "driver/rtc_io.h"
|
||||
#include "driver/gpio.h"
|
||||
|
||||
class ESP32Board : public mesh::MainBoard {
|
||||
protected:
|
||||
@@ -60,13 +61,20 @@ public:
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(P_LORA_DIO_1) // Supported ESP32 variants
|
||||
if (rtc_gpio_is_valid_gpio((gpio_num_t)P_LORA_DIO_1)) { // Only enter sleep mode if P_LORA_DIO_1 is RTC pin
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // To wake up when receiving a LoRa packet
|
||||
esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // Wake on LoRa packet
|
||||
|
||||
// T5S3: Also wake on boot button press (GPIO0, active LOW).
|
||||
// gpio_wakeup uses level trigger — works for light sleep only.
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro) && defined(PIN_USER_BTN)
|
||||
gpio_wakeup_enable((gpio_num_t)PIN_USER_BTN, GPIO_INTR_LOW_LEVEL);
|
||||
esp_sleep_enable_gpio_wakeup();
|
||||
#endif
|
||||
|
||||
if (secs > 0) {
|
||||
esp_sleep_enable_timer_wakeup(secs * 1000000); // To wake up every hour to do periodically jobs
|
||||
esp_sleep_enable_timer_wakeup(secs * 1000000ULL); // Timer wake (microseconds)
|
||||
}
|
||||
|
||||
esp_light_sleep_start(); // CPU enters light sleep
|
||||
esp_light_sleep_start(); // CPU halts here, resumes on wake
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -154,4 +162,4 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -24,7 +24,7 @@
|
||||
#endif
|
||||
|
||||
#ifndef CPU_FREQ_LOW_POWER
|
||||
#define CPU_FREQ_LOW_POWER 40 // MHz — lock screen / idle standby
|
||||
#define CPU_FREQ_LOW_POWER 80 // MHz — lock screen / idle standby (40 MHz breaks I2C)
|
||||
#endif
|
||||
|
||||
#ifndef CPU_BOOST_TIMEOUT_MS
|
||||
|
||||
@@ -188,9 +188,15 @@ int16_t T5S3Board::getBattTemperature() {
|
||||
}
|
||||
|
||||
// ---- BQ27220 Design Capacity configuration ----
|
||||
// Identical procedure to TDeckBoard — sets 1500 mAh for T5S3's larger cell.
|
||||
// The BQ27220 ships with 3000 mAh default. This writes once on first boot
|
||||
// and persists in battery-backed RAM.
|
||||
// The BQ27220 ships with a 3000 mAh default. T5S3 uses a 1500 mAh cell.
|
||||
// This function checks on boot and writes the correct value via the
|
||||
// MAC Data Memory interface if needed. The value persists in battery-backed
|
||||
// RAM, so this typically only writes once (or after a full battery disconnect).
|
||||
//
|
||||
// When DC and DE are already correct but FCC is stuck (common after initial
|
||||
// flash), the root cause is Qmax Cell 0 (0x9106) and stored FCC (0x929D)
|
||||
// retaining factory 3000 mAh defaults. This function detects and fixes all
|
||||
// three layers: DC/DE, Qmax, and stored FCC.
|
||||
|
||||
bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) {
|
||||
#if HAS_BQ27220
|
||||
@@ -198,23 +204,169 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) {
|
||||
Serial.printf("BQ27220: Design Capacity = %d mAh (target %d)\n", currentDC, designCapacity_mAh);
|
||||
|
||||
if (currentDC == designCapacity_mAh) {
|
||||
// Design Capacity correct, but check if Full Charge Capacity is sane.
|
||||
uint16_t fcc = bq27220_read16(BQ27220_REG_FULL_CAP);
|
||||
Serial.printf("BQ27220: Design Capacity correct, FCC=%d mAh\n", fcc);
|
||||
if (fcc < designCapacity_mAh * 3 / 2) {
|
||||
return true; // FCC is sane, nothing to do
|
||||
Serial.printf("BQ27220: Design Capacity already correct, FCC=%d mAh\n", fcc);
|
||||
if (fcc >= designCapacity_mAh * 3 / 2) {
|
||||
// FCC is >=150% of design — stale from factory defaults (typically 3000 mAh).
|
||||
uint16_t designEnergy = (uint16_t)((uint32_t)designCapacity_mAh * 37 / 10);
|
||||
Serial.printf("BQ27220: FCC %d >> DC %d, checking Design Energy (target %d mWh)\n",
|
||||
fcc, designCapacity_mAh, designEnergy);
|
||||
|
||||
// Unseal to read data memory and issue RESET
|
||||
bq27220_writeControl(0x0414); delay(2);
|
||||
bq27220_writeControl(0x3672); delay(2);
|
||||
// Full Access
|
||||
bq27220_writeControl(0xFFFF); delay(2);
|
||||
bq27220_writeControl(0xFFFF); delay(2);
|
||||
|
||||
// Enter CFG_UPDATE to access data memory
|
||||
bq27220_writeControl(0x0090);
|
||||
bool ready = false;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
delay(20);
|
||||
uint16_t opSt = bq27220_read16(BQ27220_REG_OP_STATUS);
|
||||
if (opSt & 0x0400) { ready = true; break; }
|
||||
}
|
||||
if (ready) {
|
||||
// Read Design Energy at data memory address 0x92A1
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92);
|
||||
Wire.endTransmission();
|
||||
delay(10);
|
||||
uint8_t oldMSB = bq27220_read8(0x40);
|
||||
uint8_t oldLSB = bq27220_read8(0x41);
|
||||
uint16_t currentDE = (oldMSB << 8) | oldLSB;
|
||||
|
||||
if (currentDE != designEnergy) {
|
||||
// Design Energy actually needs updating — write it
|
||||
uint8_t oldChk = bq27220_read8(0x60);
|
||||
uint8_t dLen = bq27220_read8(0x61);
|
||||
uint8_t newMSB = (designEnergy >> 8) & 0xFF;
|
||||
uint8_t newLSB = designEnergy & 0xFF;
|
||||
uint8_t temp = (255 - oldChk - oldMSB - oldLSB);
|
||||
uint8_t newChk = 255 - ((temp + newMSB + newLSB) & 0xFF);
|
||||
|
||||
Serial.printf("BQ27220: DE old=%d new=%d mWh, writing\n", currentDE, designEnergy);
|
||||
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92);
|
||||
Wire.write(newMSB); Wire.write(newLSB);
|
||||
Wire.endTransmission();
|
||||
delay(5);
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x60); Wire.write(newChk); Wire.write(dLen);
|
||||
Wire.endTransmission();
|
||||
delay(10);
|
||||
|
||||
// Exit with reinit since we actually changed data
|
||||
bq27220_writeControl(0x0091); // EXIT_CFG_UPDATE_REINIT
|
||||
delay(200);
|
||||
Serial.println("BQ27220: Design Energy written, exited CFG_UPDATE");
|
||||
} else {
|
||||
// DC and DE are both correct, but FCC is stuck.
|
||||
// Root cause: Qmax Cell 0 (0x9106) and stored FCC (0x929D) retain
|
||||
// factory 3000 mAh defaults. Overwrite both with designCapacity_mAh.
|
||||
Serial.printf("BQ27220: DE correct (%d mWh) — fixing Qmax + stored FCC\n", currentDE);
|
||||
|
||||
// --- Helper lambda for MAC data memory 2-byte write ---
|
||||
// Reads old value + checksum, computes differential checksum, writes new value.
|
||||
auto writeDM16 = [](uint16_t addr, uint16_t newVal) -> bool {
|
||||
// Select address
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x3E);
|
||||
Wire.write(addr & 0xFF);
|
||||
Wire.write((addr >> 8) & 0xFF);
|
||||
Wire.endTransmission();
|
||||
delay(10);
|
||||
|
||||
uint8_t oldMSB = bq27220_read8(0x40);
|
||||
uint8_t oldLSB = bq27220_read8(0x41);
|
||||
uint8_t oldChk = bq27220_read8(0x60);
|
||||
uint8_t dLen = bq27220_read8(0x61);
|
||||
uint16_t oldVal = (oldMSB << 8) | oldLSB;
|
||||
|
||||
if (oldVal == newVal) {
|
||||
Serial.printf("BQ27220: [0x%04X] already %d, skip\n", addr, newVal);
|
||||
return true; // already correct
|
||||
}
|
||||
|
||||
uint8_t newMSB = (newVal >> 8) & 0xFF;
|
||||
uint8_t newLSB = newVal & 0xFF;
|
||||
uint8_t temp = (255 - oldChk - oldMSB - oldLSB);
|
||||
uint8_t newChk = 255 - ((temp + newMSB + newLSB) & 0xFF);
|
||||
|
||||
Serial.printf("BQ27220: [0x%04X] %d -> %d\n", addr, oldVal, newVal);
|
||||
|
||||
// Write new value
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x3E);
|
||||
Wire.write(addr & 0xFF);
|
||||
Wire.write((addr >> 8) & 0xFF);
|
||||
Wire.write(newMSB);
|
||||
Wire.write(newLSB);
|
||||
Wire.endTransmission();
|
||||
delay(5);
|
||||
|
||||
// Write checksum
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x60);
|
||||
Wire.write(newChk);
|
||||
Wire.write(dLen);
|
||||
Wire.endTransmission();
|
||||
delay(10);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Overwrite Qmax Cell 0 (IT Cfg) — this is what FCC is derived from
|
||||
writeDM16(0x9106, designCapacity_mAh);
|
||||
|
||||
// Overwrite stored FCC reference (Gas Gauging, 2 bytes before DC)
|
||||
writeDM16(0x929D, designCapacity_mAh);
|
||||
|
||||
// Exit with reinit to apply the new values
|
||||
bq27220_writeControl(0x0091); // EXIT_CFG_UPDATE_REINIT
|
||||
delay(200);
|
||||
Serial.println("BQ27220: Qmax + stored FCC updated, exited CFG_UPDATE");
|
||||
}
|
||||
} else {
|
||||
Serial.println("BQ27220: Failed to enter CFG_UPDATE for DE check");
|
||||
}
|
||||
|
||||
// Seal first, then issue RESET.
|
||||
// RESET forces the gauge to fully reinitialize its Impedance Track
|
||||
// algorithm and recalculate FCC from the current DC/DE values.
|
||||
bq27220_writeControl(0x0030); // SEAL
|
||||
delay(5);
|
||||
Serial.println("BQ27220: Issuing RESET to force FCC recalculation...");
|
||||
bq27220_writeControl(0x0041); // RESET
|
||||
delay(2000); // Full reset needs generous settle time
|
||||
|
||||
fcc = bq27220_read16(BQ27220_REG_FULL_CAP);
|
||||
Serial.printf("BQ27220: FCC after RESET: %d mAh (target <= %d)\n", fcc, designCapacity_mAh);
|
||||
|
||||
if (fcc > designCapacity_mAh * 3 / 2) {
|
||||
// RESET didn't fix FCC — the gauge IT algorithm is stubbornly
|
||||
// retaining its learned value. This typically resolves after one
|
||||
// full charge/discharge cycle. Software clamp in
|
||||
// getFullChargeCapacity() ensures correct display regardless.
|
||||
Serial.printf("BQ27220: FCC still stale at %d — software clamp active\n", fcc);
|
||||
}
|
||||
}
|
||||
// FCC is stale from factory — fall through to reconfigure
|
||||
Serial.printf("BQ27220: FCC %d >> DC %d, reconfiguring\n", fcc, designCapacity_mAh);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unseal
|
||||
Serial.printf("BQ27220: Updating Design Capacity from %d to %d mAh\n", currentDC, designCapacity_mAh);
|
||||
|
||||
// Step 1: Unseal (default unseal keys)
|
||||
bq27220_writeControl(0x0414); delay(2);
|
||||
bq27220_writeControl(0x3672); delay(2);
|
||||
// Full Access
|
||||
|
||||
// Step 2: Full Access
|
||||
bq27220_writeControl(0xFFFF); delay(2);
|
||||
bq27220_writeControl(0xFFFF); delay(2);
|
||||
|
||||
// Enter CFG_UPDATE
|
||||
// Step 3: Enter CFG_UPDATE
|
||||
bq27220_writeControl(0x0090);
|
||||
bool cfgReady = false;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
@@ -229,7 +381,7 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write Design Capacity at 0x929F
|
||||
// Step 4: Write Design Capacity at 0x929F
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x3E); Wire.write(0x9F); Wire.write(0x92);
|
||||
Wire.endTransmission();
|
||||
@@ -255,7 +407,7 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) {
|
||||
Wire.endTransmission();
|
||||
delay(10);
|
||||
|
||||
// Write Design Energy at 0x92A1
|
||||
// Step 4a: Write Design Energy at 0x92A1
|
||||
{
|
||||
uint16_t designEnergy = (uint16_t)((uint32_t)designCapacity_mAh * 37 / 10);
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
@@ -271,6 +423,9 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) {
|
||||
uint8_t deTemp = (255 - deOldChk - deOldMSB - deOldLSB);
|
||||
uint8_t deNewChk = 255 - ((deTemp + deNewMSB + deNewLSB) & 0xFF);
|
||||
|
||||
Serial.printf("BQ27220: Design Energy: old=%d new=%d mWh\n",
|
||||
(deOldMSB << 8) | deOldLSB, designEnergy);
|
||||
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92);
|
||||
Wire.write(deNewMSB); Wire.write(deNewLSB);
|
||||
@@ -282,16 +437,17 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) {
|
||||
delay(10);
|
||||
}
|
||||
|
||||
// Exit CFG_UPDATE with reinit
|
||||
// Step 5: Exit CFG_UPDATE with reinit
|
||||
bq27220_writeControl(0x0091);
|
||||
Serial.println("BQ27220: Sent EXIT_CFG_UPDATE_REINIT, waiting...");
|
||||
delay(200);
|
||||
|
||||
// Seal
|
||||
// Step 6: Seal
|
||||
bq27220_writeControl(0x0030);
|
||||
delay(5);
|
||||
|
||||
// Force RESET to reinitialize FCC
|
||||
bq27220_writeControl(0x0041);
|
||||
// Step 7: Force RESET to reinitialize FCC from new DC/DE
|
||||
bq27220_writeControl(0x0041); // RESET
|
||||
delay(1000);
|
||||
|
||||
uint16_t verifyDC = bq27220_read16(BQ27220_REG_DESIGN_CAP);
|
||||
@@ -302,4 +458,4 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) {
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
#endif
|
||||
|
||||
#ifndef CPU_FREQ_LOW_POWER
|
||||
#define CPU_FREQ_LOW_POWER 40 // MHz — lock screen / idle standby
|
||||
#define CPU_FREQ_LOW_POWER 80 // MHz — lock screen / idle standby (40 MHz breaks I2C)
|
||||
#endif
|
||||
|
||||
#ifndef CPU_BOOST_TIMEOUT_MS
|
||||
|
||||
@@ -161,24 +161,47 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure keyboard matrix (8 rows x 10 cols)
|
||||
// --- Warm-reboot safe init sequence ---
|
||||
// The TCA8418 stays powered across ESP32 resets (no dedicated RST pin),
|
||||
// so the scanner may still be active from the previous session.
|
||||
// We must disable it before reconfiguring the matrix.
|
||||
|
||||
// 1. Disable scanner — stop all scanning before touching config
|
||||
writeReg(TCA8418_REG_CFG, 0x00);
|
||||
|
||||
// 2. Drain any stale events from the previous session
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if ((readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break;
|
||||
readReg(TCA8418_REG_KEY_EVENT_A);
|
||||
}
|
||||
writeReg(TCA8418_REG_INT_STAT, 0x1F); // Clear all interrupt flags
|
||||
|
||||
// 3. Explicitly clear GPI event masks (prevent phantom GPI events)
|
||||
writeReg(TCA8418_REG_GPI_EM1, 0x00);
|
||||
writeReg(TCA8418_REG_GPI_EM2, 0x00);
|
||||
writeReg(TCA8418_REG_GPI_EM3, 0x00);
|
||||
|
||||
// 4. Configure keyboard matrix (8 rows x 10 cols)
|
||||
writeReg(TCA8418_REG_KP_GPIO1, 0xFF); // Rows 0-7 as keypad
|
||||
writeReg(TCA8418_REG_KP_GPIO2, 0xFF); // Cols 0-7 as keypad
|
||||
writeReg(TCA8418_REG_KP_GPIO3, 0x03); // Cols 8-9 as keypad
|
||||
|
||||
// Enable keypad with FIFO overflow detection
|
||||
writeReg(TCA8418_REG_CFG, 0x11); // KE_IEN + INT_CFG
|
||||
|
||||
// Set debounce
|
||||
// 5. Set debounce
|
||||
writeReg(TCA8418_REG_DEBOUNCE, 0x03);
|
||||
|
||||
// Clear any pending interrupts
|
||||
// 6. Final pre-enable cleanup
|
||||
writeReg(TCA8418_REG_INT_STAT, 0x1F);
|
||||
|
||||
// Flush the FIFO
|
||||
while (readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F) {
|
||||
// 7. Enable scanner — matrix config is stable, safe to start scanning
|
||||
writeReg(TCA8418_REG_CFG, 0x11); // KE_IEN + INT_CFG
|
||||
|
||||
// 8. Let scanner stabilise, then flush any spurious first-scan events
|
||||
delay(5);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if ((readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break;
|
||||
readReg(TCA8418_REG_KEY_EVENT_A);
|
||||
}
|
||||
writeReg(TCA8418_REG_INT_STAT, 0x1F);
|
||||
|
||||
_initialized = true;
|
||||
Serial.println("TCA8418: Keyboard initialized OK");
|
||||
|
||||
347
variants/lilygo_tdeck_pro_max/TDeckProMaxBoard.cpp
Normal file
347
variants/lilygo_tdeck_pro_max/TDeckProMaxBoard.cpp
Normal file
@@ -0,0 +1,347 @@
|
||||
#include <Arduino.h>
|
||||
#include "variant.h"
|
||||
#include "TDeckProMaxBoard.h"
|
||||
#include <Mesh.h> // For MESH_DEBUG_PRINTLN
|
||||
|
||||
// LEDC channel for e-ink backlight PWM (Arduino ESP32 core 2.x channel-based API)
|
||||
#ifdef PIN_EINK_BL
|
||||
#define EINK_BL_LEDC_CHANNEL 0
|
||||
#endif
|
||||
|
||||
// =============================================================================
|
||||
// TDeckProMaxBoard::begin() — Boot sequence for T-Deck Pro MAX V0.1
|
||||
//
|
||||
// Critical ordering:
|
||||
// 1. I2C bus init (XL9555, BQ27220, and all sensors share this bus)
|
||||
// 2. XL9555 init (must be up before ANY peripheral that depends on it)
|
||||
// 3. Touch reset pulse via XL9555 (needed before touch driver init)
|
||||
// 4. Keyboard reset pulse via XL9555 (clean keyboard state)
|
||||
// 5. LoRa power enable via XL9555 (must be on before SPI radio init)
|
||||
// 6. GPS power + UART init
|
||||
// 7. Parent class init (ESP32Board::begin)
|
||||
// 8. LoRa SPI pin config + deep sleep wake handling
|
||||
// 9. BQ27220 fuel gauge check
|
||||
// 10. Low-voltage protection
|
||||
//
|
||||
// NOTE: We do NOT call TDeckBoard::begin() — we reimplement the boot sequence
|
||||
// to handle XL9555-routed pins. BQ27220 methods are inherited unchanged.
|
||||
// =============================================================================
|
||||
|
||||
void TDeckProMaxBoard::begin() {
|
||||
|
||||
MESH_DEBUG_PRINTLN("TDeckProMaxBoard::begin() - T-Deck Pro MAX V0.1");
|
||||
|
||||
// ------ Step 1: I2C bus ------
|
||||
// All I2C devices (XL9555, BQ27220, TCA8418, CST328, DRV2605, ES8311,
|
||||
// BQ25896, BHI260AP) share SDA=13, SCL=14.
|
||||
Wire.begin(I2C_SDA, I2C_SCL);
|
||||
Wire.setClock(100000); // 100kHz — safe for all devices on the bus
|
||||
MESH_DEBUG_PRINTLN(" I2C initialized (SDA=%d SCL=%d)", I2C_SDA, I2C_SCL);
|
||||
|
||||
// ------ Step 2: XL9555 I/O Expander ------
|
||||
// This must happen before anything that needs peripheral power or resets.
|
||||
if (!xl9555_init()) {
|
||||
Serial.println("CRITICAL: XL9555 init failed — peripherals will not work!");
|
||||
// Continue anyway; some things (display, keyboard INT) might still work
|
||||
// without XL9555, but LoRa/GPS/modem will be dead.
|
||||
}
|
||||
|
||||
// ------ Step 3: Touch reset pulse ------
|
||||
// The touch controller (CST328) needs a clean reset via XL9555 IO07
|
||||
// before the touch driver tries to communicate with it.
|
||||
touchReset();
|
||||
|
||||
// ------ Step 4: Keyboard reset pulse ------
|
||||
keyboardReset();
|
||||
|
||||
// ------ Step 5: Parent class init ------
|
||||
// ESP32Board::begin() handles common ESP32 setup.
|
||||
// We skip TDeckBoard::begin() because it uses PIN_PERF_POWERON and
|
||||
// direct GPIO for LoRa/GPS power that don't exist on MAX.
|
||||
ESP32Board::begin();
|
||||
|
||||
// ------ Step 6: GPS UART init ------
|
||||
// GPS power was already enabled by XL9555 boot defaults (GPS_EN HIGH).
|
||||
// Now init the UART with the MAX-specific pins.
|
||||
#if HAS_GPS
|
||||
Serial2.begin(GPS_BAUDRATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
|
||||
MESH_DEBUG_PRINTLN(" GPS Serial2 initialized (RX=%d TX=%d @ %d baud)",
|
||||
GPS_RX_PIN, GPS_TX_PIN, GPS_BAUDRATE);
|
||||
#endif
|
||||
|
||||
// ------ Step 7: Configure user button ------
|
||||
pinMode(PIN_USER_BTN, INPUT);
|
||||
|
||||
// ------ Step 8: Configure LoRa SPI pins ------
|
||||
// LoRa power is already enabled via XL9555 (LORA_EN HIGH in boot defaults).
|
||||
pinMode(P_LORA_MISO, INPUT_PULLUP);
|
||||
|
||||
// ------ Step 9: Handle wake from deep sleep ------
|
||||
esp_reset_reason_t reason = esp_reset_reason();
|
||||
if (reason == ESP_RST_DEEPSLEEP) {
|
||||
uint64_t wakeup_source = esp_sleep_get_ext1_wakeup_status();
|
||||
if (wakeup_source & (1ULL << P_LORA_DIO_1)) {
|
||||
startup_reason = BD_STARTUP_RX_PACKET;
|
||||
}
|
||||
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
|
||||
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
|
||||
}
|
||||
|
||||
// ------ Step 10: BQ27220 fuel gauge ------
|
||||
#if HAS_BQ27220
|
||||
uint16_t voltage = getBattMilliVolts();
|
||||
MESH_DEBUG_PRINTLN(" Battery voltage: %d mV", voltage);
|
||||
configureFuelGauge(); // Inherited from TDeckBoard — sets 1500 mAh
|
||||
#endif
|
||||
|
||||
// ------ Step 11: Early low-voltage protection ------
|
||||
#if HAS_BQ27220 && defined(AUTO_SHUTDOWN_MILLIVOLTS)
|
||||
{
|
||||
uint16_t bootMv = getBattMilliVolts();
|
||||
if (bootMv > 0 && bootMv < AUTO_SHUTDOWN_MILLIVOLTS) {
|
||||
Serial.printf("CRITICAL: Boot voltage %dmV < %dmV — sleeping immediately\n",
|
||||
bootMv, AUTO_SHUTDOWN_MILLIVOLTS);
|
||||
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
|
||||
esp_sleep_enable_ext1_wakeup(1ULL << PIN_USER_BTN, ESP_EXT1_WAKEUP_ANY_HIGH);
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// ------ Step 12: E-ink backlight (working on MAX!) ------
|
||||
// Configure LEDC PWM for backlight brightness control.
|
||||
// Start with backlight OFF — UI code can enable it when needed.
|
||||
#ifdef PIN_EINK_BL
|
||||
// Arduino ESP32 core 2.x uses channel-based LEDC API
|
||||
ledcSetup(EINK_BL_LEDC_CHANNEL, 1000, 8); // Channel 0, 1kHz, 8-bit resolution
|
||||
ledcAttachPin(PIN_EINK_BL, EINK_BL_LEDC_CHANNEL);
|
||||
ledcWrite(EINK_BL_LEDC_CHANNEL, 0); // Off by default
|
||||
MESH_DEBUG_PRINTLN(" Backlight PWM configured on IO%d", PIN_EINK_BL);
|
||||
#endif
|
||||
|
||||
MESH_DEBUG_PRINTLN("TDeckProMaxBoard::begin() - complete");
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// XL9555 I/O Expander — Lightweight I2C Driver
|
||||
// =============================================================================
|
||||
|
||||
bool TDeckProMaxBoard::xl9555_writeReg(uint8_t reg, uint8_t val) {
|
||||
Wire.beginTransmission(I2C_ADDR_XL9555);
|
||||
Wire.write(reg);
|
||||
Wire.write(val);
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
|
||||
uint8_t TDeckProMaxBoard::xl9555_readReg(uint8_t reg) {
|
||||
Wire.beginTransmission(I2C_ADDR_XL9555);
|
||||
Wire.write(reg);
|
||||
Wire.endTransmission(false);
|
||||
Wire.requestFrom((uint8_t)I2C_ADDR_XL9555, (uint8_t)1);
|
||||
return Wire.available() ? Wire.read() : 0xFF;
|
||||
}
|
||||
|
||||
bool TDeckProMaxBoard::xl9555_init() {
|
||||
MESH_DEBUG_PRINTLN(" XL9555: Initializing I/O expander at 0x%02X", I2C_ADDR_XL9555);
|
||||
|
||||
// Verify XL9555 is present on the bus
|
||||
Wire.beginTransmission(I2C_ADDR_XL9555);
|
||||
if (Wire.endTransmission() != 0) {
|
||||
Serial.println(" XL9555: NOT FOUND on I2C bus!");
|
||||
_xlReady = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set ALL pins as outputs (config register: 0 = output)
|
||||
// Port 0 (pins 0-7): all output
|
||||
if (!xl9555_writeReg(XL9555_REG_CONFIG_0, 0x00)) return false;
|
||||
// Port 1 (pins 8-15): all output
|
||||
if (!xl9555_writeReg(XL9555_REG_CONFIG_1, 0x00)) return false;
|
||||
|
||||
// Apply boot defaults
|
||||
_xlPort0 = XL9555_BOOT_PORT0;
|
||||
_xlPort1 = XL9555_BOOT_PORT1;
|
||||
if (!xl9555_writeReg(XL9555_REG_OUTPUT_0, _xlPort0)) return false;
|
||||
if (!xl9555_writeReg(XL9555_REG_OUTPUT_1, _xlPort1)) return false;
|
||||
|
||||
_xlReady = true;
|
||||
|
||||
MESH_DEBUG_PRINTLN(" XL9555: Ready (Port0=0x%02X Port1=0x%02X)", _xlPort0, _xlPort1);
|
||||
MESH_DEBUG_PRINTLN(" XL9555: LoRa=%s GPS=%s 1V8=%s Modem=%s Antenna=%s",
|
||||
(_xlPort0 & (1 << XL_PIN_LORA_EN)) ? "ON" : "OFF",
|
||||
(_xlPort0 & (1 << XL_PIN_GPS_EN)) ? "ON" : "OFF",
|
||||
(_xlPort0 & (1 << XL_PIN_1V8_EN)) ? "ON" : "OFF",
|
||||
(_xlPort0 & (1 << XL_PIN_6609_EN)) ? "ON" : "OFF",
|
||||
(_xlPort0 & (1 << XL_PIN_LORA_SEL)) ? "internal" : "external");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::xl9555_digitalWrite(uint8_t pin, bool value) {
|
||||
if (!_xlReady) return;
|
||||
|
||||
if (pin < 8) {
|
||||
// Port 0
|
||||
if (value) _xlPort0 |= (1 << pin);
|
||||
else _xlPort0 &= ~(1 << pin);
|
||||
xl9555_writeReg(XL9555_REG_OUTPUT_0, _xlPort0);
|
||||
} else if (pin < 16) {
|
||||
// Port 1 (subtract 8 for bit position)
|
||||
uint8_t bit = pin - 8;
|
||||
if (value) _xlPort1 |= (1 << bit);
|
||||
else _xlPort1 &= ~(1 << bit);
|
||||
xl9555_writeReg(XL9555_REG_OUTPUT_1, _xlPort1);
|
||||
}
|
||||
}
|
||||
|
||||
bool TDeckProMaxBoard::xl9555_digitalRead(uint8_t pin) const {
|
||||
if (pin < 8) return (_xlPort0 >> pin) & 1;
|
||||
if (pin < 16) return (_xlPort1 >> (pin - 8)) & 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::xl9555_writePort0(uint8_t val) {
|
||||
_xlPort0 = val;
|
||||
if (_xlReady) xl9555_writeReg(XL9555_REG_OUTPUT_0, val);
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::xl9555_writePort1(uint8_t val) {
|
||||
_xlPort1 = val;
|
||||
if (_xlReady) xl9555_writeReg(XL9555_REG_OUTPUT_1, val);
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// High-level peripheral control
|
||||
// =============================================================================
|
||||
|
||||
// ---- Modem (A7682E) ----
|
||||
|
||||
void TDeckProMaxBoard::modemPowerOn() {
|
||||
MESH_DEBUG_PRINTLN(" XL9555: Modem power ON (6609_EN HIGH)");
|
||||
xl9555_digitalWrite(XL_PIN_6609_EN, HIGH);
|
||||
delay(100); // Allow SGM6609 boost to stabilise
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::modemPowerOff() {
|
||||
MESH_DEBUG_PRINTLN(" XL9555: Modem power OFF (6609_EN LOW)");
|
||||
xl9555_digitalWrite(XL_PIN_6609_EN, LOW);
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::modemPwrkeyPulse() {
|
||||
// A7682E power-on sequence: pulse PWRKEY LOW for >= 500ms
|
||||
// (Some datasheets say pull HIGH then LOW; LilyGo factory sets HIGH then toggles.)
|
||||
MESH_DEBUG_PRINTLN(" XL9555: Modem PWRKEY pulse");
|
||||
xl9555_digitalWrite(XL_PIN_PWRKEY_EN, HIGH);
|
||||
delay(100);
|
||||
xl9555_digitalWrite(XL_PIN_PWRKEY_EN, LOW);
|
||||
delay(1200);
|
||||
xl9555_digitalWrite(XL_PIN_PWRKEY_EN, HIGH);
|
||||
}
|
||||
|
||||
// ---- Audio output selection ----
|
||||
|
||||
void TDeckProMaxBoard::selectAudioES8311() {
|
||||
MESH_DEBUG_PRINTLN(" XL9555: Audio select → ES8311");
|
||||
xl9555_digitalWrite(XL_PIN_AUDIO_SEL, LOW);
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::selectAudioModem() {
|
||||
MESH_DEBUG_PRINTLN(" XL9555: Audio select → A7682E");
|
||||
xl9555_digitalWrite(XL_PIN_AUDIO_SEL, HIGH);
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::amplifierEnable() {
|
||||
xl9555_digitalWrite(XL_PIN_AMPLIFIER, HIGH);
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::amplifierDisable() {
|
||||
xl9555_digitalWrite(XL_PIN_AMPLIFIER, LOW);
|
||||
}
|
||||
|
||||
// ---- LoRa antenna selection ----
|
||||
|
||||
void TDeckProMaxBoard::loraAntennaInternal() {
|
||||
MESH_DEBUG_PRINTLN(" XL9555: LoRa antenna → internal");
|
||||
xl9555_digitalWrite(XL_PIN_LORA_SEL, HIGH);
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::loraAntennaExternal() {
|
||||
MESH_DEBUG_PRINTLN(" XL9555: LoRa antenna → external");
|
||||
xl9555_digitalWrite(XL_PIN_LORA_SEL, LOW);
|
||||
}
|
||||
|
||||
// ---- Motor (DRV2605) ----
|
||||
|
||||
void TDeckProMaxBoard::motorEnable() {
|
||||
xl9555_digitalWrite(XL_PIN_MOTOR_EN, HIGH);
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::motorDisable() {
|
||||
xl9555_digitalWrite(XL_PIN_MOTOR_EN, LOW);
|
||||
}
|
||||
|
||||
// ---- Touch reset ----
|
||||
|
||||
void TDeckProMaxBoard::touchReset() {
|
||||
if (!_xlReady) return;
|
||||
MESH_DEBUG_PRINTLN(" XL9555: Touch reset pulse");
|
||||
xl9555_digitalWrite(XL_PIN_TOUCH_RST, LOW);
|
||||
delay(20);
|
||||
xl9555_digitalWrite(XL_PIN_TOUCH_RST, HIGH);
|
||||
delay(50); // Allow touch controller to come out of reset
|
||||
}
|
||||
|
||||
// ---- Keyboard reset ----
|
||||
|
||||
void TDeckProMaxBoard::keyboardReset() {
|
||||
if (!_xlReady) return;
|
||||
MESH_DEBUG_PRINTLN(" XL9555: Keyboard reset pulse");
|
||||
xl9555_digitalWrite(XL_PIN_KEY_RST, LOW);
|
||||
delay(20);
|
||||
xl9555_digitalWrite(XL_PIN_KEY_RST, HIGH);
|
||||
delay(50);
|
||||
}
|
||||
|
||||
// ---- GPS power ----
|
||||
|
||||
void TDeckProMaxBoard::gpsPowerOn() {
|
||||
xl9555_digitalWrite(XL_PIN_GPS_EN, HIGH);
|
||||
delay(100);
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::gpsPowerOff() {
|
||||
xl9555_digitalWrite(XL_PIN_GPS_EN, LOW);
|
||||
}
|
||||
|
||||
// ---- LoRa power ----
|
||||
|
||||
void TDeckProMaxBoard::loraPowerOn() {
|
||||
xl9555_digitalWrite(XL_PIN_LORA_EN, HIGH);
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::loraPowerOff() {
|
||||
xl9555_digitalWrite(XL_PIN_LORA_EN, LOW);
|
||||
}
|
||||
|
||||
// ---- E-ink backlight (working on MAX!) ----
|
||||
|
||||
void TDeckProMaxBoard::backlightOn() {
|
||||
#ifdef PIN_EINK_BL
|
||||
ledcWrite(EINK_BL_LEDC_CHANNEL, 255);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::backlightOff() {
|
||||
#ifdef PIN_EINK_BL
|
||||
ledcWrite(EINK_BL_LEDC_CHANNEL, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
void TDeckProMaxBoard::backlightSetBrightness(uint8_t duty) {
|
||||
#ifdef PIN_EINK_BL
|
||||
ledcWrite(EINK_BL_LEDC_CHANNEL, duty);
|
||||
#endif
|
||||
}
|
||||
108
variants/lilygo_tdeck_pro_max/TDeckProMaxBoard.h
Normal file
108
variants/lilygo_tdeck_pro_max/TDeckProMaxBoard.h
Normal file
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// TDeckProMaxBoard — Board support for LilyGo T-Deck Pro MAX V0.1
|
||||
//
|
||||
// Extends TDeckBoard (which provides all BQ27220 fuel gauge methods) with:
|
||||
// - XL9555 I/O expander initialisation and control
|
||||
// - XL9555-routed peripheral power management
|
||||
// - Touch/keyboard reset via XL9555
|
||||
// - Modem power/PWRKEY via XL9555
|
||||
// - LoRa antenna selection via XL9555
|
||||
// - Audio output mux (ES8311 vs A7682E) via XL9555
|
||||
// - Speaker amplifier enable via XL9555
|
||||
//
|
||||
// The XL9555 must be initialised before LoRa, GPS, modem, or touch are used.
|
||||
// All power enables, resets, and switches go through I2C — not direct GPIO.
|
||||
// =============================================================================
|
||||
|
||||
#include "variant.h"
|
||||
#include "TDeckBoard.h" // Inherits BQ27220 fuel gauge, deep sleep, power management
|
||||
|
||||
class TDeckProMaxBoard : public TDeckBoard {
|
||||
public:
|
||||
void begin();
|
||||
|
||||
const char* getManufacturerName() const {
|
||||
return "LilyGo T-Deck Pro MAX";
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// XL9555 I/O Expander — lightweight inline driver
|
||||
//
|
||||
// The XL9555 has 16 I/O pins across two 8-bit ports.
|
||||
// Pin 0-7 = Port 0, Pin 8-15 = Port 1.
|
||||
// We shadow the output state in _xlPort0/_xlPort1 to allow
|
||||
// single-bit set/clear without read-modify-write over I2C.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Initialise XL9555: set all used pins as outputs, apply boot defaults.
|
||||
// Returns true if I2C communication with XL9555 succeeded.
|
||||
bool xl9555_init();
|
||||
|
||||
// Set a single XL9555 pin HIGH or LOW (pin 0-15).
|
||||
void xl9555_digitalWrite(uint8_t pin, bool value);
|
||||
|
||||
// Read the current output state of a pin (from shadow, not I2C read).
|
||||
bool xl9555_digitalRead(uint8_t pin) const;
|
||||
|
||||
// Write raw port values (for batch updates).
|
||||
void xl9555_writePort0(uint8_t val);
|
||||
void xl9555_writePort1(uint8_t val);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// High-level peripheral control (delegates to XL9555)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Modem (A7682E) power control
|
||||
void modemPowerOn(); // Enable SGM6609 boost (6609_EN HIGH)
|
||||
void modemPowerOff(); // Disable SGM6609 boost (6609_EN LOW)
|
||||
void modemPwrkeyPulse(); // Toggle PWRKEY: HIGH 100ms → LOW 1200ms → HIGH
|
||||
|
||||
// Audio output selection
|
||||
void selectAudioES8311(); // AUDIO_SEL LOW → ES8311 output to speaker/headphones
|
||||
void selectAudioModem(); // AUDIO_SEL HIGH → A7682E output to speaker/headphones
|
||||
void amplifierEnable(); // NS4150B amplifier ON (louder speaker)
|
||||
void amplifierDisable(); // NS4150B amplifier OFF (saves power)
|
||||
|
||||
// LoRa antenna selection (SKY13453 RF switch)
|
||||
void loraAntennaInternal(); // LORA_SEL HIGH → internal PCB antenna (default)
|
||||
void loraAntennaExternal(); // LORA_SEL LOW → external IPEX antenna
|
||||
|
||||
// Motor (DRV2605) power
|
||||
void motorEnable(); // MOTOR_EN HIGH
|
||||
void motorDisable(); // MOTOR_EN LOW
|
||||
|
||||
// Touch controller reset via XL9555
|
||||
void touchReset(); // Pulse TOUCH_RST: LOW 20ms → HIGH, then 50ms settle
|
||||
|
||||
// Keyboard reset via XL9555
|
||||
void keyboardReset(); // Pulse KEY_RST: LOW 20ms → HIGH, then 50ms settle
|
||||
|
||||
// GPS power control via XL9555
|
||||
void gpsPowerOn(); // GPS_EN HIGH
|
||||
void gpsPowerOff(); // GPS_EN LOW
|
||||
|
||||
// LoRa power control via XL9555
|
||||
void loraPowerOn(); // LORA_EN HIGH
|
||||
void loraPowerOff(); // LORA_EN LOW
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// E-ink front-light control
|
||||
// On MAX, IO41 has a working backlight circuit (boost converter + LEDs).
|
||||
// PWM control for brightness is possible via ledc.
|
||||
// -------------------------------------------------------------------------
|
||||
void backlightOn();
|
||||
void backlightOff();
|
||||
void backlightSetBrightness(uint8_t duty); // 0-255, via LEDC PWM
|
||||
|
||||
private:
|
||||
// Shadow registers for XL9555 output ports (avoid I2C read-modify-write)
|
||||
uint8_t _xlPort0 = XL9555_BOOT_PORT0;
|
||||
uint8_t _xlPort1 = XL9555_BOOT_PORT1;
|
||||
bool _xlReady = false;
|
||||
|
||||
// Low-level I2C helpers
|
||||
bool xl9555_writeReg(uint8_t reg, uint8_t val);
|
||||
uint8_t xl9555_readReg(uint8_t reg);
|
||||
};
|
||||
360
variants/lilygo_tdeck_pro_max/Tca8418keyboard.h
Normal file
360
variants/lilygo_tdeck_pro_max/Tca8418keyboard.h
Normal file
@@ -0,0 +1,360 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
// TCA8418 Register addresses
|
||||
#define TCA8418_REG_CFG 0x01
|
||||
#define TCA8418_REG_INT_STAT 0x02
|
||||
#define TCA8418_REG_KEY_LCK_EC 0x03
|
||||
#define TCA8418_REG_KEY_EVENT_A 0x04
|
||||
#define TCA8418_REG_KP_GPIO1 0x1D
|
||||
#define TCA8418_REG_KP_GPIO2 0x1E
|
||||
#define TCA8418_REG_KP_GPIO3 0x1F
|
||||
#define TCA8418_REG_DEBOUNCE 0x29
|
||||
#define TCA8418_REG_GPI_EM1 0x20
|
||||
#define TCA8418_REG_GPI_EM2 0x21
|
||||
#define TCA8418_REG_GPI_EM3 0x22
|
||||
|
||||
// Key codes for special keys
|
||||
#define KB_KEY_NONE 0
|
||||
#define KB_KEY_BACKSPACE '\b'
|
||||
#define KB_KEY_ENTER '\r'
|
||||
#define KB_KEY_SPACE ' '
|
||||
#define KB_KEY_EMOJI 0x01 // Non-printable code for $ key (emoji picker)
|
||||
#define KB_KEY_BACKLIGHT 0x02 // Non-printable code for Alt+B (backlight toggle, MAX only)
|
||||
|
||||
class TCA8418Keyboard {
|
||||
private:
|
||||
uint8_t _addr;
|
||||
TwoWire* _wire;
|
||||
bool _initialized;
|
||||
bool _shiftActive; // Sticky shift (one-shot or held)
|
||||
bool _shiftConsumed; // Was shift active for the last returned key
|
||||
bool _shiftHeld; // Shift key physically held down
|
||||
bool _shiftUsedWhileHeld; // Was shift consumed by any key while held
|
||||
bool _altActive; // Sticky alt (one-shot)
|
||||
bool _symActive; // Sticky sym (one-shot)
|
||||
unsigned long _lastShiftTime; // For Shift+key combos
|
||||
|
||||
uint8_t readReg(uint8_t reg) {
|
||||
_wire->beginTransmission(_addr);
|
||||
_wire->write(reg);
|
||||
_wire->endTransmission();
|
||||
_wire->requestFrom(_addr, (uint8_t)1);
|
||||
return _wire->available() ? _wire->read() : 0;
|
||||
}
|
||||
|
||||
void writeReg(uint8_t reg, uint8_t val) {
|
||||
_wire->beginTransmission(_addr);
|
||||
_wire->write(reg);
|
||||
_wire->write(val);
|
||||
_wire->endTransmission();
|
||||
}
|
||||
|
||||
// Map raw key codes to characters (from working reader firmware)
|
||||
char getKeyChar(uint8_t keyCode) {
|
||||
switch (keyCode) {
|
||||
// Row 1 - QWERTYUIOP
|
||||
case 10: return 'q'; // Q (was 97 on different hardware)
|
||||
case 9: return 'w';
|
||||
case 8: return 'e';
|
||||
case 7: return 'r';
|
||||
case 6: return 't';
|
||||
case 5: return 'y';
|
||||
case 4: return 'u';
|
||||
case 3: return 'i';
|
||||
case 2: return 'o';
|
||||
case 1: return 'p';
|
||||
|
||||
// Row 2 - ASDFGHJKL + Backspace
|
||||
case 20: return 'a'; // A (was 98 on different hardware)
|
||||
case 19: return 's';
|
||||
case 18: return 'd';
|
||||
case 17: return 'f';
|
||||
case 16: return 'g';
|
||||
case 15: return 'h';
|
||||
case 14: return 'j';
|
||||
case 13: return 'k';
|
||||
case 12: return 'l';
|
||||
case 11: return '\b'; // Backspace
|
||||
|
||||
// Row 3 - Alt ZXCVBNM Sym Enter
|
||||
case 30: return 0; // Alt - handled separately
|
||||
case 29: return 'z';
|
||||
case 28: return 'x';
|
||||
case 27: return 'c';
|
||||
case 26: return 'v';
|
||||
case 25: return 'b';
|
||||
case 24: return 'n';
|
||||
case 23: return 'm';
|
||||
case 22: return 0; // Symbol key - handled separately
|
||||
case 21: return '\r'; // Enter
|
||||
|
||||
// Row 4 - Shift Mic Space Sym Shift
|
||||
case 35: return 0; // Left shift - handled separately
|
||||
case 34: return 0; // Mic
|
||||
case 33: return ' '; // Space
|
||||
case 32: return 0; // Sym - handled separately
|
||||
case 31: return 0; // Right shift - handled separately
|
||||
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Map key with Alt modifier - same as Sym for this keyboard
|
||||
char getAltChar(uint8_t keyCode) {
|
||||
return getSymChar(keyCode); // Alt does same as Sym
|
||||
}
|
||||
|
||||
// Map key with Sym modifier - based on actual T-Deck Pro keyboard silk-screen
|
||||
char getSymChar(uint8_t keyCode) {
|
||||
switch (keyCode) {
|
||||
// Row 1: Q W E R T Y U I O P
|
||||
case 10: return '#'; // Q -> #
|
||||
case 9: return '1'; // W -> 1
|
||||
case 8: return '2'; // E -> 2
|
||||
case 7: return '3'; // R -> 3
|
||||
case 6: return '('; // T -> (
|
||||
case 5: return ')'; // Y -> )
|
||||
case 4: return '_'; // U -> _
|
||||
case 3: return '-'; // I -> -
|
||||
case 2: return '+'; // O -> +
|
||||
case 1: return '@'; // P -> @
|
||||
|
||||
// Row 2: A S D F G H J K L
|
||||
case 20: return '*'; // A -> *
|
||||
case 19: return '4'; // S -> 4
|
||||
case 18: return '5'; // D -> 5
|
||||
case 17: return '6'; // F -> 6
|
||||
case 16: return '/'; // G -> /
|
||||
case 15: return ':'; // H -> :
|
||||
case 14: return ';'; // J -> ;
|
||||
case 13: return '\''; // K -> '
|
||||
case 12: return '"'; // L -> "
|
||||
|
||||
// Row 3: Z X C V B N M
|
||||
case 29: return '7'; // Z -> 7
|
||||
case 28: return '8'; // X -> 8
|
||||
case 27: return '9'; // C -> 9
|
||||
case 26: return '?'; // V -> ?
|
||||
case 25: return '!'; // B -> !
|
||||
case 24: return ','; // N -> ,
|
||||
case 23: return '.'; // M -> .
|
||||
|
||||
// Row 4: Mic key -> 0
|
||||
case 34: return '0'; // Mic -> 0
|
||||
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
TCA8418Keyboard(uint8_t addr = 0x34, TwoWire* wire = &Wire)
|
||||
: _addr(addr), _wire(wire), _initialized(false),
|
||||
_shiftActive(false), _shiftConsumed(false), _shiftHeld(false), _shiftUsedWhileHeld(false), _altActive(false), _symActive(false), _lastShiftTime(0) {}
|
||||
|
||||
bool begin() {
|
||||
// Check if device responds
|
||||
_wire->beginTransmission(_addr);
|
||||
if (_wire->endTransmission() != 0) {
|
||||
Serial.println("TCA8418: Device not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Warm-reboot safe init sequence ---
|
||||
// The TCA8418 stays powered across ESP32 resets (no dedicated RST pin),
|
||||
// so the scanner may still be active from the previous session.
|
||||
// We must disable it before reconfiguring the matrix.
|
||||
|
||||
// 1. Disable scanner — stop all scanning before touching config
|
||||
writeReg(TCA8418_REG_CFG, 0x00);
|
||||
|
||||
// 2. Drain any stale events from the previous session
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if ((readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break;
|
||||
readReg(TCA8418_REG_KEY_EVENT_A);
|
||||
}
|
||||
writeReg(TCA8418_REG_INT_STAT, 0x1F); // Clear all interrupt flags
|
||||
|
||||
// 3. Explicitly clear GPI event masks (prevent phantom GPI events)
|
||||
writeReg(TCA8418_REG_GPI_EM1, 0x00);
|
||||
writeReg(TCA8418_REG_GPI_EM2, 0x00);
|
||||
writeReg(TCA8418_REG_GPI_EM3, 0x00);
|
||||
|
||||
// 4. Configure keyboard matrix (8 rows x 10 cols)
|
||||
writeReg(TCA8418_REG_KP_GPIO1, 0xFF); // Rows 0-7 as keypad
|
||||
writeReg(TCA8418_REG_KP_GPIO2, 0xFF); // Cols 0-7 as keypad
|
||||
writeReg(TCA8418_REG_KP_GPIO3, 0x03); // Cols 8-9 as keypad
|
||||
|
||||
// 5. Set debounce
|
||||
writeReg(TCA8418_REG_DEBOUNCE, 0x03);
|
||||
|
||||
// 6. Final pre-enable cleanup
|
||||
writeReg(TCA8418_REG_INT_STAT, 0x1F);
|
||||
|
||||
// 7. Enable scanner — matrix config is stable, safe to start scanning
|
||||
writeReg(TCA8418_REG_CFG, 0x11); // KE_IEN + INT_CFG
|
||||
|
||||
// 8. Let scanner stabilise, then flush any spurious first-scan events
|
||||
delay(5);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if ((readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break;
|
||||
readReg(TCA8418_REG_KEY_EVENT_A);
|
||||
}
|
||||
writeReg(TCA8418_REG_INT_STAT, 0x1F);
|
||||
|
||||
_initialized = true;
|
||||
Serial.println("TCA8418: Keyboard initialized OK");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read a key press - returns character or 0 if no key
|
||||
char readKey() {
|
||||
if (!_initialized) return 0;
|
||||
|
||||
// Check for key events in FIFO
|
||||
uint8_t keyCount = readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F;
|
||||
if (keyCount == 0) return 0;
|
||||
|
||||
// Read key event from FIFO
|
||||
uint8_t keyEvent = readReg(TCA8418_REG_KEY_EVENT_A);
|
||||
|
||||
// Bit 7: 1 = press, 0 = release
|
||||
bool pressed = (keyEvent & 0x80) != 0;
|
||||
uint8_t keyCode = keyEvent & 0x7F;
|
||||
|
||||
// Clear interrupt
|
||||
writeReg(TCA8418_REG_INT_STAT, 0x1F);
|
||||
|
||||
Serial.printf("KB raw: event=0x%02X code=%d pressed=%d count=%d\n",
|
||||
keyEvent, keyCode, pressed, keyCount);
|
||||
|
||||
// Track shift release (before the general release-ignore)
|
||||
if (!pressed && (keyCode == 35 || keyCode == 31)) {
|
||||
_shiftHeld = false;
|
||||
// If shift was used while held (e.g. cursor nav), clear it completely
|
||||
// so the next bare keypress isn't treated as shifted.
|
||||
// If shift was NOT used (tap-then-release), keep _shiftActive for one-shot.
|
||||
if (_shiftUsedWhileHeld) {
|
||||
_shiftActive = false;
|
||||
}
|
||||
_shiftUsedWhileHeld = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Only act on key press, not release
|
||||
if (!pressed || keyCode == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle modifier keys - set sticky state and return 0
|
||||
if (keyCode == 35 || keyCode == 31) { // Shift keys
|
||||
_shiftActive = true;
|
||||
_shiftHeld = true;
|
||||
_shiftUsedWhileHeld = false;
|
||||
_lastShiftTime = millis();
|
||||
Serial.println("KB: Shift activated");
|
||||
return 0;
|
||||
}
|
||||
if (keyCode == 30) { // Alt key
|
||||
_altActive = true;
|
||||
Serial.println("KB: Alt activated");
|
||||
return 0;
|
||||
}
|
||||
if (keyCode == 32) { // Sym key (bottom row)
|
||||
_symActive = true;
|
||||
Serial.println("KB: Sym activated");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle dedicated $ key (key code 22, next to M)
|
||||
// Bare press = emoji picker, Sym+$ = literal '$'
|
||||
if (keyCode == 22) {
|
||||
if (_symActive) {
|
||||
_symActive = false;
|
||||
Serial.println("KB: Sym+$ -> '$'");
|
||||
return '$';
|
||||
}
|
||||
Serial.println("KB: $ key -> emoji");
|
||||
return KB_KEY_EMOJI;
|
||||
}
|
||||
|
||||
// Handle Mic key - always produces '0' (silk-screened on key)
|
||||
// Sym+Mic also produces '0' (consumes sym so it doesn't leak)
|
||||
if (keyCode == 34) {
|
||||
_symActive = false;
|
||||
Serial.println("KB: Mic -> '0'");
|
||||
return '0';
|
||||
}
|
||||
|
||||
// Get the character
|
||||
char c = 0;
|
||||
|
||||
// Alt+B -> backlight toggle (T-Deck Pro MAX only — working front-light on IO41)
|
||||
if (_altActive && keyCode == 25) { // keyCode 25 = B
|
||||
_altActive = false;
|
||||
Serial.println("KB: Alt+B -> backlight toggle");
|
||||
return KB_KEY_BACKLIGHT;
|
||||
}
|
||||
|
||||
if (_altActive) {
|
||||
c = getAltChar(keyCode);
|
||||
_altActive = false; // Reset sticky alt
|
||||
if (c != 0) {
|
||||
Serial.printf("KB: Alt+key -> '%c'\n", c);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
if (_symActive) {
|
||||
c = getSymChar(keyCode);
|
||||
_symActive = false; // Reset sticky sym
|
||||
if (c != 0) {
|
||||
Serial.printf("KB: Sym+key -> '%c'\n", c);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
c = getKeyChar(keyCode);
|
||||
|
||||
if (c != 0 && _shiftActive) {
|
||||
// Apply shift - uppercase letters
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c = c - 'a' + 'A';
|
||||
}
|
||||
// Track that shift was used while physically held
|
||||
if (_shiftHeld) {
|
||||
_shiftUsedWhileHeld = true;
|
||||
}
|
||||
// Only clear shift if it's one-shot (tap), not held down
|
||||
if (!_shiftHeld) {
|
||||
_shiftActive = false;
|
||||
}
|
||||
_shiftConsumed = true; // Record that shift was active for this key
|
||||
} else {
|
||||
_shiftConsumed = false;
|
||||
}
|
||||
|
||||
if (c != 0) {
|
||||
Serial.printf("KB: code %d -> '%c' (0x%02X)\n", keyCode, c >= 32 ? c : '?', c);
|
||||
} else {
|
||||
Serial.printf("KB: code %d -> UNMAPPED\n", keyCode);
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
bool isReady() const { return _initialized; }
|
||||
|
||||
// Check if shift was pressed within the last N milliseconds
|
||||
bool wasShiftRecentlyPressed(unsigned long withinMs = 500) const {
|
||||
return (millis() - _lastShiftTime) < withinMs;
|
||||
}
|
||||
|
||||
// Check if shift was active when the most recent key was produced
|
||||
// (immune to e-ink refresh timing unlike wasShiftRecentlyPressed)
|
||||
bool wasShiftConsumed() const {
|
||||
return _shiftConsumed;
|
||||
}
|
||||
};
|
||||
232
variants/lilygo_tdeck_pro_max/platformio.ini
Normal file
232
variants/lilygo_tdeck_pro_max/platformio.ini
Normal file
@@ -0,0 +1,232 @@
|
||||
; =============================================================================
|
||||
; T-Deck Pro MAX V0.1 — Meck Build Environments
|
||||
;
|
||||
; Hardware: ESP32-S3 + XL9555 I/O expander + combined 4G (A7682E) + Audio (ES8311)
|
||||
;
|
||||
; Key differences from LilyGo_TDeck_Pro (V1.1):
|
||||
; - Peripheral power controlled via XL9555 (not direct GPIO)
|
||||
; - 4G modem and ES8311 audio coexist (no longer mutually exclusive)
|
||||
; - ES8311 I2C codec replaces PCM5102A (different I2S pins, needs I2C config)
|
||||
; - Several GPIO reassignments (see variant.h for full map)
|
||||
; - 1500 mAh battery (was 1400)
|
||||
; - Working e-ink front-light on IO41
|
||||
;
|
||||
; WHAT WORKS OUT OF THE BOX:
|
||||
; LoRa mesh, keyboard, e-ink display, GPS, touchscreen, battery management,
|
||||
; SD card, text reader, notes, contacts, channels, settings, discovery,
|
||||
; last heard, repeater admin, web reader (WiFi builds), OTA update.
|
||||
;
|
||||
; NEEDS ADAPTATION (future work):
|
||||
; - HAS_4G_MODEM: ModemManager uses direct GPIO for MODEM_POWER_EN/PWRKEY
|
||||
; which are XL9555-routed on MAX. Needs board.modemPowerOn() etc.
|
||||
; - MECK_AUDIO_VARIANT: ES8311 needs I2C codec init (PCM5102A didn't).
|
||||
; I2S pins are different. AudiobookPlayerScreen needs ES8311 driver.
|
||||
; - Combined 4G+audio: existing #ifdef guards treat them as mutually
|
||||
; exclusive. Needs restructuring for coexistence.
|
||||
; =============================================================================
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; Base environment for T-Deck Pro MAX
|
||||
; ---------------------------------------------------------------------------
|
||||
[LilyGo_TDeck_Pro_Max]
|
||||
extends = esp32_base
|
||||
extra_scripts = post:merge_firmware.py
|
||||
board = t-deck_pro_max
|
||||
board_build.flash_mode = qio
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.arduino.memory_type = qio_qspi
|
||||
board_upload.flash_size = 16MB
|
||||
build_flags =
|
||||
${esp32_base.build_flags}
|
||||
${sensor_base.build_flags}
|
||||
; Include MAX variant first (for variant.h, target.h, TDeckProMaxBoard.h)
|
||||
; then V1.1 variant (for TDeckBoard.h, which TDeckProMaxBoard inherits from)
|
||||
-I variants/LilyGo_TDeck_Pro_Max
|
||||
-I variants/LilyGo_TDeck_Pro
|
||||
; Both defines needed: LilyGo_TDeck_Pro for existing UI code guards,
|
||||
; LilyGo_TDeck_Pro_Max for MAX-specific code paths
|
||||
-D LilyGo_TDeck_Pro
|
||||
-D LilyGo_TDeck_Pro_Max
|
||||
-D HAS_XL9555=1
|
||||
-D HAS_GPS=1
|
||||
-D BOARD_HAS_PSRAM=1
|
||||
-D CORE_DEBUG_LEVEL=1
|
||||
-D FORMAT_SPIFFS_IF_FAILED=1
|
||||
-D FORMAT_LITTLEFS_IF_FAILED=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
-D LORA_TX_POWER=22
|
||||
-D SX126X_DIO2_AS_RF_SWITCH
|
||||
-D SX126X_CURRENT_LIMIT=140
|
||||
-D SX126X_RX_BOOSTED_GAIN=1
|
||||
-D SX126X_DIO3_TCXO_VOLTAGE=2.4f
|
||||
; LoRa SPI pins (direct GPIO — unchanged from V1.1)
|
||||
-D P_LORA_DIO_1=5
|
||||
-D P_LORA_NSS=3
|
||||
-D P_LORA_RESET=4
|
||||
-D P_LORA_BUSY=6
|
||||
-D P_LORA_SCLK=36
|
||||
-D P_LORA_MISO=47
|
||||
-D P_LORA_MOSI=33
|
||||
; P_LORA_EN deliberately NOT defined — LoRa power via XL9555 in board.begin()
|
||||
; GPS pins (direct GPIO — changed from V1.1!)
|
||||
-D ENV_INCLUDE_GPS=1
|
||||
-D ENV_SKIP_GPS_DETECT=1
|
||||
-D PIN_GPS_RX=2
|
||||
-D PIN_GPS_TX=16
|
||||
-D GPS_BAUD_RATE=38400
|
||||
; Sensor exclusions (same as V1.1)
|
||||
-D ENV_INCLUDE_AHTX0=0
|
||||
-D ENV_INCLUDE_BME280=0
|
||||
-D ENV_INCLUDE_BMP280=0
|
||||
-D ENV_INCLUDE_SHTC3=0
|
||||
-D ENV_INCLUDE_SHT4X=0
|
||||
-D ENV_INCLUDE_LPS22HB=0
|
||||
-D ENV_INCLUDE_INA3221=0
|
||||
-D ENV_INCLUDE_INA219=0
|
||||
-D ENV_INCLUDE_INA226=0
|
||||
-D ENV_INCLUDE_INA260=0
|
||||
-D ENV_INCLUDE_MLX90614=0
|
||||
-D ENV_INCLUDE_VL53L0X=0
|
||||
-D ENV_INCLUDE_BME680=0
|
||||
-D ENV_INCLUDE_BMP085=0
|
||||
; E-ink display (pin changes from V1.1: RST=9, BL=41)
|
||||
-D USE_EINK
|
||||
-D DISPLAY_CLASS=GxEPDDisplay
|
||||
-D EINK_DISPLAY_MODEL=GxEPD2_310_GDEQ031T10
|
||||
-D EINK_WIDTH=240
|
||||
-D EINK_HEIGHT=320
|
||||
-D EINK_CS=34
|
||||
-D EINK_DC=35
|
||||
-D EINK_RST=9
|
||||
-D EINK_BUSY=37
|
||||
-D EINK_SCLK=36
|
||||
-D EINK_MOSI=33
|
||||
-D EINK_BL=41
|
||||
-D EINK_NOT_HIBERNATE=1
|
||||
; Battery (1500 mAh on MAX, was 1400 on V1.1)
|
||||
-D HAS_BQ27220=1
|
||||
-D AUTO_SHUTDOWN_MILLIVOLTS=2800
|
||||
; Display rendering parameters
|
||||
-D EINK_LIMIT_FASTREFRESH=10
|
||||
-D EINK_LIMIT_GHOSTING_PX=2000
|
||||
-D DISPLAY_ROTATION=0
|
||||
-D EINK_ROTATION=0
|
||||
-D EINK_SCALE_X=1.875f
|
||||
-D EINK_SCALE_Y=2.5f
|
||||
-D EINK_X_OFFSET=0
|
||||
-D EINK_Y_OFFSET=5
|
||||
; Legacy display pin aliases (for GxEPDDisplay.cpp)
|
||||
-D PIN_DISPLAY_CS=34
|
||||
-D PIN_DISPLAY_DC=35
|
||||
-D PIN_DISPLAY_RST=9
|
||||
-D PIN_DISPLAY_BUSY=37
|
||||
-D PIN_DISPLAY_SCLK=36
|
||||
-D PIN_DISPLAY_MISO=-1
|
||||
-D PIN_DISPLAY_MOSI=33
|
||||
-D PIN_DISPLAY_BL=41
|
||||
-D PIN_USER_BTN=0
|
||||
; Touch (INT is direct GPIO; RST is XL9555, handled by board class)
|
||||
-D HAS_TOUCHSCREEN=1
|
||||
-D CST328_PIN_INT=12
|
||||
-D CST328_PIN_RST=-1
|
||||
-D ARDUINO_LOOP_STACK_SIZE=32768
|
||||
build_src_filter = ${esp32_base.build_src_filter}
|
||||
; Include TDeckBoard.cpp from V1.1 (parent class with BQ27220 code)
|
||||
+<../variants/LilyGo_TDeck_Pro/TDeckBoard.cpp>
|
||||
; Include MAX variant (target.cpp + TDeckProMaxBoard.cpp)
|
||||
+<../variants/LilyGo_TDeck_Pro_Max>
|
||||
+<helpers/sensors/*.cpp>
|
||||
lib_deps =
|
||||
${esp32_base.lib_deps}
|
||||
${sensor_base.lib_deps}
|
||||
zinggjm/GxEPD2@^1.5.9
|
||||
adafruit/Adafruit GFX Library@^1.11.0
|
||||
bitbank2/PNGdec@^1.0.1
|
||||
WebServer
|
||||
Update
|
||||
|
||||
|
||||
; ===========================================================================
|
||||
; Meck MAX builds — LoRa mesh works out of the box on all variants.
|
||||
; 4G modem and ES8311 audio need adaptation before they can be enabled.
|
||||
; ===========================================================================
|
||||
|
||||
; MAX + BLE companion (standard BLE phone bridging)
|
||||
; Both 4G + audio hardware present but not yet enabled in firmware.
|
||||
; BLE_PIN_CODE limit: MAX_CONTACTS=500 (BLE protocol ceiling).
|
||||
[env:meck_max_ble]
|
||||
extends = LilyGo_TDeck_Pro_Max
|
||||
build_flags =
|
||||
${LilyGo_TDeck_Pro_Max.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=500
|
||||
-D MAX_GROUP_CHANNELS=20
|
||||
-D BLE_PIN_CODE=123456
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.3.MAX"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
+<helpers/ui/GxEPDDisplay.cpp>
|
||||
lib_deps =
|
||||
${LilyGo_TDeck_Pro_Max.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6
|
||||
bitbank2/JPEGDEC
|
||||
|
||||
; MAX + WiFi companion (WiFi app bridging — no BLE, higher contact limit)
|
||||
; WiFi credentials loaded from SD card (/web/wifi.cfg).
|
||||
; Connect via MeshCore web app, meshcore.js, or Python CLI.
|
||||
[env:meck_max_wifi]
|
||||
extends = LilyGo_TDeck_Pro_Max
|
||||
build_flags =
|
||||
${LilyGo_TDeck_Pro_Max.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=1500
|
||||
-D MAX_GROUP_CHANNELS=20
|
||||
-D MECK_WIFI_COMPANION=1
|
||||
-D TCP_PORT=5000
|
||||
-D WIFI_DEBUG_LOGGING=1
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.3.MAX.WiFi"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
+<helpers/ui/GxEPDDisplay.cpp>
|
||||
lib_deps =
|
||||
${LilyGo_TDeck_Pro_Max.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6
|
||||
bitbank2/JPEGDEC
|
||||
|
||||
; MAX standalone (no BLE/WiFi — maximum battery life, LoRa mesh only)
|
||||
; Contacts in PSRAM (1500 capacity). OTA enabled (WiFi AP on demand).
|
||||
[env:meck_max_standalone]
|
||||
extends = LilyGo_TDeck_Pro_Max
|
||||
build_flags =
|
||||
${LilyGo_TDeck_Pro_Max.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=1500
|
||||
-D MAX_GROUP_CHANNELS=20
|
||||
-D OFFLINE_QUEUE_SIZE=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.3.MAX.SA"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
+<helpers/ui/GxEPDDisplay.cpp>
|
||||
lib_deps =
|
||||
${LilyGo_TDeck_Pro_Max.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
91
variants/lilygo_tdeck_pro_max/target.cpp
Normal file
91
variants/lilygo_tdeck_pro_max/target.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include <Arduino.h>
|
||||
#include "variant.h"
|
||||
#include "target.h"
|
||||
|
||||
TDeckProMaxBoard board;
|
||||
|
||||
#if defined(P_LORA_SCLK)
|
||||
static SPIClass loraSpi(HSPI);
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, loraSpi);
|
||||
#else
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
#endif
|
||||
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
ESP32RTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#if HAS_GPS
|
||||
// Wrap Serial2 with a sentence counter so the UI can show NMEA throughput.
|
||||
// MicroNMEALocationProvider reads through this wrapper transparently.
|
||||
GPSStreamCounter gpsStream(Serial2);
|
||||
MicroNMEALocationProvider gps(gpsStream, &rtc_clock);
|
||||
EnvironmentSensorManager sensors(gps);
|
||||
#else
|
||||
SensorManager sensors;
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
DISPLAY_CLASS display;
|
||||
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
|
||||
#endif
|
||||
|
||||
bool radio_init() {
|
||||
MESH_DEBUG_PRINTLN("radio_init() - starting");
|
||||
|
||||
// NOTE: board.begin() is called by main.cpp setup() before radio_init()
|
||||
// I2C is already initialized there with correct pins
|
||||
|
||||
fallback_clock.begin();
|
||||
MESH_DEBUG_PRINTLN("radio_init() - fallback_clock started");
|
||||
|
||||
// Wire already initialized in board.begin() - just use it for RTC
|
||||
rtc_clock.begin(Wire);
|
||||
MESH_DEBUG_PRINTLN("radio_init() - rtc_clock started");
|
||||
|
||||
#if defined(P_LORA_SCLK)
|
||||
MESH_DEBUG_PRINTLN("radio_init() - initializing LoRa SPI...");
|
||||
loraSpi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS);
|
||||
MESH_DEBUG_PRINTLN("radio_init() - SPI initialized, calling radio.std_init()...");
|
||||
bool result = radio.std_init(&loraSpi);
|
||||
MESH_DEBUG_PRINTLN("radio_init() - radio.std_init() returned: %s", result ? "SUCCESS" : "FAILED");
|
||||
return result;
|
||||
#else
|
||||
MESH_DEBUG_PRINTLN("radio_init() - calling radio.std_init() without custom SPI...");
|
||||
bool result = radio.std_init();
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Longer preamble for low SF improves reliability — each symbol is shorter
|
||||
// at low SF, so more symbols are needed for reliable detection.
|
||||
// SF <= 8 gets 32 symbols (~65ms at SF7/62.5kHz); SF >= 9 keeps 16 (already ~131ms+).
|
||||
// See: https://github.com/meshcore-dev/MeshCore/pull/1954
|
||||
uint16_t preamble = (sf <= 8) ? 32 : 16;
|
||||
radio.setPreambleLength(preamble);
|
||||
MESH_DEBUG_PRINTLN("radio_set_params() - bw=%.1f sf=%u preamble=%u", bw, sf, preamble);
|
||||
}
|
||||
|
||||
void radio_set_tx_power(uint8_t dbm) {
|
||||
radio.setOutputPower(dbm);
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng);
|
||||
}
|
||||
|
||||
void radio_reset_agc() {
|
||||
radio.setRxBoostedGainMode(true);
|
||||
}
|
||||
47
variants/lilygo_tdeck_pro_max/target.h
Normal file
47
variants/lilygo_tdeck_pro_max/target.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
// Include variant.h first to ensure all board-specific defines are available
|
||||
#include "variant.h"
|
||||
|
||||
#define RADIOLIB_STATIC_ONLY 1
|
||||
#include <RadioLib.h>
|
||||
#include <helpers/radiolib/RadioLibWrappers.h>
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <TDeckProMaxBoard.h>
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include <helpers/ui/GxEPDDisplay.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
#endif
|
||||
|
||||
#if HAS_GPS
|
||||
#include "helpers/sensors/EnvironmentSensorManager.h"
|
||||
#include "helpers/sensors/MicroNMEALocationProvider.h"
|
||||
#include "GPSStreamCounter.h"
|
||||
#else
|
||||
#include <helpers/SensorManager.h>
|
||||
#endif
|
||||
|
||||
extern TDeckProMaxBoard board;
|
||||
extern WRAPPER_CLASS radio_driver;
|
||||
extern AutoDiscoverRTCClock rtc_clock;
|
||||
|
||||
#if HAS_GPS
|
||||
extern GPSStreamCounter gpsStream;
|
||||
extern EnvironmentSensorManager sensors;
|
||||
#else
|
||||
extern SensorManager sensors;
|
||||
#endif
|
||||
|
||||
#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(uint8_t dbm);
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
void radio_reset_agc();
|
||||
301
variants/lilygo_tdeck_pro_max/variant.h
Normal file
301
variants/lilygo_tdeck_pro_max/variant.h
Normal file
@@ -0,0 +1,301 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// LilyGo T-Deck Pro MAX V0.1 - Pin Definitions
|
||||
// Hardware revision: HD-V3-250911
|
||||
//
|
||||
// KEY DIFFERENCES FROM T-Deck Pro V1.1:
|
||||
// - XL9555 I/O expander (0x20) controls peripheral power, resets, and switches
|
||||
// (LoRa EN, GPS EN, modem power, touch RST, keyboard RST, antenna sel, etc.)
|
||||
// - 4G (A7682E) and audio (ES8311) coexist on ONE board — no longer mutually exclusive
|
||||
// - ES8311 I2C codec replaces PCM5102A (needs I2C config, different I2S pins)
|
||||
// - E-ink RST moved: IO9 (was IO16)
|
||||
// - E-ink BL moved: IO41 (was IO45, now has working front-light hardware!)
|
||||
// - GPS UART moved: RX=IO2, TX=IO16 (was RX=IO44, TX=IO43)
|
||||
// - GPS/LoRa power via XL9555 (was direct GPIO 39/46)
|
||||
// - Touch RST via XL9555 IO07 (was GPIO 38)
|
||||
// - Modem power/PWRKEY via XL9555 (was direct GPIO 41/40)
|
||||
// - No PIN_PERF_POWERON (IO10 is now modem UART RX)
|
||||
// - Battery: 1500 mAh (was 1400 mAh)
|
||||
// - LoRa antenna switch (SKY13453) controlled by XL9555 IO04
|
||||
// - Audio output mux (A7682E vs ES8311) controlled by XL9555 IO12
|
||||
// - Speaker amplifier (NS4150B) enable via XL9555 IO06
|
||||
// =============================================================================
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// E-Ink Display (GDEQ031T10 - 240x320)
|
||||
// E-ink SHARES the SPI bus with LoRa and SD card (SCK=36, MOSI=33, MISO=47)
|
||||
// They use different chip selects: E-ink CS=34, LoRa CS=3, SD CS=48
|
||||
// -----------------------------------------------------------------------------
|
||||
#define PIN_EINK_CS 34
|
||||
#define PIN_EINK_DC 35
|
||||
#define PIN_EINK_RES 9 // MAX: IO9 (was IO16 on V1.1)
|
||||
#define PIN_EINK_BUSY 37
|
||||
#define PIN_EINK_SCLK 36 // Shared with LoRa + SD
|
||||
#define PIN_EINK_MOSI 33 // Shared with LoRa + SD
|
||||
#define PIN_EINK_BL 41 // MAX: IO41 — working front-light! (was IO45 non-functional on V1.1)
|
||||
|
||||
// Legacy aliases for MeshCore compatibility
|
||||
#define PIN_DISPLAY_CS PIN_EINK_CS
|
||||
#define PIN_DISPLAY_DC PIN_EINK_DC
|
||||
#define PIN_DISPLAY_RST PIN_EINK_RES
|
||||
#define PIN_DISPLAY_BUSY PIN_EINK_BUSY
|
||||
#define PIN_DISPLAY_SCLK PIN_EINK_SCLK
|
||||
#define PIN_DISPLAY_MOSI PIN_EINK_MOSI
|
||||
|
||||
// Display dimensions - native resolution of GDEQ031T10
|
||||
#define LCD_HOR_SIZE 240
|
||||
#define LCD_VER_SIZE 320
|
||||
|
||||
// E-ink model for GxEPD2
|
||||
#define EINK_DISPLAY_MODEL GxEPD2_310_GDEQ031T10
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// SPI Bus - Shared by LoRa, SD Card, AND E-ink display
|
||||
// -----------------------------------------------------------------------------
|
||||
#define BOARD_SPI_SCLK 36
|
||||
#define BOARD_SPI_MISO 47
|
||||
#define BOARD_SPI_MOSI 33
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// I2C Bus
|
||||
// -----------------------------------------------------------------------------
|
||||
#define I2C_SDA 13
|
||||
#define I2C_SCL 14
|
||||
|
||||
// Aliases for ESP32Board base class compatibility
|
||||
#define PIN_BOARD_SDA I2C_SDA
|
||||
#define PIN_BOARD_SCL I2C_SCL
|
||||
|
||||
// I2C Device Addresses
|
||||
#define I2C_ADDR_ES8311 0x18 // ES8311 audio codec (NEW on MAX)
|
||||
#define I2C_ADDR_TOUCH 0x1A // CST328
|
||||
#define I2C_ADDR_XL9555 0x20 // XL9555 I/O expander (NEW on MAX)
|
||||
#define I2C_ADDR_GYROSCOPE 0x28 // BHI260AP
|
||||
#define I2C_ADDR_KEYBOARD 0x34 // TCA8418
|
||||
#define I2C_ADDR_BQ27220 0x55 // Fuel gauge
|
||||
#define I2C_ADDR_DRV2605 0x5A // Motor driver (haptic)
|
||||
#define I2C_ADDR_BQ25896 0x6B // Charger
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// XL9555 I/O Expander — Pin Assignments
|
||||
//
|
||||
// The XL9555 replaces direct GPIO control of peripheral power enables,
|
||||
// resets, and switches. It must be initialised over I2C before LoRa, GPS,
|
||||
// modem, or touch can be used.
|
||||
//
|
||||
// Port 0: pins 0-7, registers 0x02 (output) / 0x06 (direction)
|
||||
// Port 1: pins 8-15, registers 0x03 (output) / 0x07 (direction)
|
||||
// Direction: 0 = output, 1 = input
|
||||
// -----------------------------------------------------------------------------
|
||||
#define HAS_XL9555 1
|
||||
|
||||
// XL9555 I2C registers
|
||||
#define XL9555_REG_INPUT_0 0x00
|
||||
#define XL9555_REG_INPUT_1 0x01
|
||||
#define XL9555_REG_OUTPUT_0 0x02
|
||||
#define XL9555_REG_OUTPUT_1 0x03
|
||||
#define XL9555_REG_INVERT_0 0x04
|
||||
#define XL9555_REG_INVERT_1 0x05
|
||||
#define XL9555_REG_CONFIG_0 0x06 // 0=output, 1=input
|
||||
#define XL9555_REG_CONFIG_1 0x07
|
||||
|
||||
// XL9555 pin assignments (0-7 = Port 0, 8-15 = Port 1)
|
||||
#define XL_PIN_6609_EN 0 // HIGH: Enable A7682E power supply (SGM6609 boost)
|
||||
#define XL_PIN_LORA_EN 1 // HIGH: Enable SX1262 power supply
|
||||
#define XL_PIN_GPS_EN 2 // HIGH: Enable GPS power supply
|
||||
#define XL_PIN_1V8_EN 3 // HIGH: Enable BHI260AP 1.8V power supply
|
||||
#define XL_PIN_LORA_SEL 4 // HIGH: internal antenna, LOW: external antenna (SKY13453)
|
||||
#define XL_PIN_MOTOR_EN 5 // HIGH: Enable DRV2605 power supply
|
||||
#define XL_PIN_AMPLIFIER 6 // HIGH: Enable NS4150B speaker power amplifier
|
||||
#define XL_PIN_TOUCH_RST 7 // LOW: Reset touch controller (active-low)
|
||||
#define XL_PIN_PWRKEY_EN 8 // HIGH: A7682E POWERKEY toggle
|
||||
#define XL_PIN_KEY_RST 9 // LOW: Reset keyboard (active-low)
|
||||
#define XL_PIN_AUDIO_SEL 10 // HIGH: A7682E audio out, LOW: ES8311 audio out
|
||||
// Pins 11-15 are reserved
|
||||
|
||||
// Default XL9555 output state at boot (all power enables ON, resets de-asserted)
|
||||
// Bit layout: [P07..P00] = TOUCH_RST=1, AMP=0, MOTOR_EN=0, LORA_SEL=1, 1V8=1, GPS=1, LORA=1, 6609=0
|
||||
// [P17..P10] = reserved=0, AUDIO_SEL=0, KEY_RST=1, PWRKEY=0
|
||||
//
|
||||
// Conservative boot defaults for Meck:
|
||||
// - LoRa ON, GPS ON, 1.8V ON, internal antenna
|
||||
// - Modem OFF (6609_EN LOW), PWRKEY LOW (toggled later if needed)
|
||||
// - Motor OFF, Amplifier OFF (saves power, enabled on demand)
|
||||
// - Touch RST HIGH (not resetting), Keyboard RST HIGH (not resetting)
|
||||
// - Audio select LOW (ES8311 by default — Meck controls this when needed)
|
||||
#define XL9555_BOOT_PORT0 0b10011110 // 0x9E: T_RST=1, AMP=0, MOT=0, LSEL=1, 1V8=1, GPS=1, LORA=1, 6609=0
|
||||
#define XL9555_BOOT_PORT1 0b00000010 // 0x02: ..., ASEL=0, KRST=1, PKEY=0
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Touch Controller (CST328)
|
||||
// NOTE: Touch RST is via XL9555 pin 7, NOT a direct GPIO!
|
||||
// CST328_PIN_RST is defined as -1 to signal "not a direct GPIO".
|
||||
// The board class handles touch reset via XL9555 in begin().
|
||||
// -----------------------------------------------------------------------------
|
||||
#define HAS_TOUCHSCREEN 1
|
||||
#define CST328_PIN_INT 12
|
||||
#define CST328_PIN_RST -1 // MAX: Routed through XL9555 IO07 — handled by board class
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// GPS
|
||||
// NOTE: GPS power enable is via XL9555 pin 2, NOT a direct GPIO!
|
||||
// PIN_GPS_EN is intentionally NOT defined — the board class handles it via XL9555.
|
||||
// -----------------------------------------------------------------------------
|
||||
#define HAS_GPS 1
|
||||
#define GPS_BAUDRATE 38400
|
||||
// #define PIN_GPS_EN — NOT a direct GPIO on MAX (XL9555 IO02)
|
||||
#define GPS_RX_PIN 2 // MAX: IO2 (was IO44 on V1.1) — ESP32 receives from GPS
|
||||
#define GPS_TX_PIN 16 // MAX: IO16 (was IO43 on V1.1) — ESP32 sends to GPS
|
||||
#define PIN_GPS_PPS 1
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Buttons & Controls
|
||||
// -----------------------------------------------------------------------------
|
||||
#define BUTTON_PIN 0
|
||||
#define PIN_USER_BTN 0
|
||||
|
||||
// Vibration Motor — DRV2605 driver (same as V1.1)
|
||||
// Motor power enable is via XL9555 pin 5, not a direct GPIO.
|
||||
#define HAS_DRV2605 1
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// SD Card
|
||||
// -----------------------------------------------------------------------------
|
||||
#define HAS_SDCARD
|
||||
#define SDCARD_USE_SPI1
|
||||
#define SPI_MOSI 33
|
||||
#define SPI_SCK 36
|
||||
#define SPI_MISO 47
|
||||
#define SPI_CS 48
|
||||
#define SDCARD_CS SPI_CS
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Keyboard (TCA8418)
|
||||
// NOTE: Keyboard RST is via XL9555 pin 9 (active-low).
|
||||
// The board class handles keyboard reset via XL9555 in begin().
|
||||
// -----------------------------------------------------------------------------
|
||||
#define KB_BL_PIN 42
|
||||
#define BOARD_KEYBOARD_INT 15
|
||||
#define HAS_PHYSICAL_KEYBOARD 1
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Audio — ES8311 I2C Codec (NEW on MAX — replaces PCM5102A)
|
||||
//
|
||||
// ES8311 is an I2C-controlled audio codec (unlike PCM5102A which needed no config).
|
||||
// It requires I2C register setup for input source, gain, volume, etc.
|
||||
// Speaker/headphone output is shared with A7682E modem audio, selected via
|
||||
// XL9555 pin AUDIO_SEL: LOW = ES8311, HIGH = A7682E.
|
||||
// Power amplifier (NS4150B) for speaker enabled via XL9555 pin AMPLIFIER.
|
||||
//
|
||||
// I2S pin mapping for ES8311 (completely different from V1.1 PCM5102A!):
|
||||
// MCLK = IO38 (master clock — ES8311 needs this, PCM5102A didn't)
|
||||
// SCLK = IO39 (bit clock, aka BCLK)
|
||||
// LRCK = IO18 (word select, aka LRC/WS)
|
||||
// DSDIN = IO17 (DAC serial data in — ESP32 sends audio TO codec)
|
||||
// ASDOUT= IO40 (ADC serial data out — codec sends mic audio TO ESP32)
|
||||
// -----------------------------------------------------------------------------
|
||||
#define HAS_ES8311_AUDIO 1
|
||||
|
||||
#define BOARD_ES8311_MCLK 38
|
||||
#define BOARD_ES8311_SCLK 39
|
||||
#define BOARD_ES8311_LRCK 18
|
||||
#define BOARD_ES8311_DSDIN 17 // ESP32 → ES8311 (speaker/headphone output)
|
||||
#define BOARD_ES8311_ASDOUT 40 // ES8311 → ESP32 (microphone input)
|
||||
|
||||
// Compatibility aliases for ESP32-audioI2S library (setPinout expects BCLK, LRC, DOUT)
|
||||
#define BOARD_I2S_BCLK BOARD_ES8311_SCLK // IO39
|
||||
#define BOARD_I2S_LRC BOARD_ES8311_LRCK // IO18
|
||||
#define BOARD_I2S_DOUT BOARD_ES8311_DSDIN // IO17
|
||||
#define BOARD_I2S_MCLK BOARD_ES8311_MCLK // IO38 (ESP32-audioI2S may need setMCLK)
|
||||
|
||||
// Microphone — ES8311 built-in ADC (replaces separate PDM mic on V1.1)
|
||||
// Mic data comes through I2S ASDOUT pin, not a separate PDM interface.
|
||||
#define BOARD_MIC_I2S_DIN BOARD_ES8311_ASDOUT // IO40
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Sensors
|
||||
// -----------------------------------------------------------------------------
|
||||
#define HAS_BHI260AP // Gyroscope/IMU (1.8V power via XL9555 IO03)
|
||||
#define BOARD_GYRO_INT 21
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Power Management
|
||||
// -----------------------------------------------------------------------------
|
||||
#define HAS_BQ27220 1
|
||||
#define BQ27220_I2C_ADDR 0x55
|
||||
#define BQ27220_I2C_SDA I2C_SDA
|
||||
#define BQ27220_I2C_SCL I2C_SCL
|
||||
#define BQ27220_DESIGN_CAPACITY 1500 // MAX: 1500 mAh (was 1400 on V1.1)
|
||||
#define BQ27220_DESIGN_CAPACITY_MAH 1500 // Alias used by TDeckBoard.h
|
||||
|
||||
#define HAS_PPM 1
|
||||
#define XPOWERS_CHIP_BQ25896
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// LoRa Radio (SX1262)
|
||||
// NOTE: LoRa power enable is via XL9555 pin 1, NOT GPIO 46!
|
||||
// The board class enables LoRa power via XL9555 in begin().
|
||||
// P_LORA_EN is intentionally NOT defined here — handled by board class.
|
||||
// Antenna selection: XL9555 pin 4 (HIGH=internal, LOW=external via SKY13453).
|
||||
// -----------------------------------------------------------------------------
|
||||
#define USE_SX1262
|
||||
#define USE_SX1268
|
||||
|
||||
// LORA_EN is NOT a direct GPIO on MAX — omit the define entirely.
|
||||
// If any code references P_LORA_EN, it must be guarded with #ifndef HAS_XL9555.
|
||||
// #define LORA_EN — NOT DEFINED (was GPIO 46 on V1.1)
|
||||
|
||||
#define LORA_SCK 36
|
||||
#define LORA_MISO 47
|
||||
#define LORA_MOSI 33 // Shared with e-ink and SD card
|
||||
#define LORA_CS 3
|
||||
#define LORA_RESET 4
|
||||
#define LORA_DIO0 -1 // Not connected on SX1262
|
||||
#define LORA_DIO1 5 // SX1262 IRQ
|
||||
#define LORA_DIO2 6 // SX1262 BUSY
|
||||
|
||||
// SX126X driver aliases (Meshtastic compatibility)
|
||||
#define SX126X_CS LORA_CS
|
||||
#define SX126X_DIO1 LORA_DIO1
|
||||
#define SX126X_BUSY LORA_DIO2
|
||||
#define SX126X_RESET LORA_RESET
|
||||
|
||||
// RadioLib/MeshCore compatibility aliases
|
||||
#define P_LORA_NSS LORA_CS
|
||||
#define P_LORA_DIO_1 LORA_DIO1
|
||||
#define P_LORA_RESET LORA_RESET
|
||||
#define P_LORA_BUSY LORA_DIO2
|
||||
#define P_LORA_SCLK LORA_SCK
|
||||
#define P_LORA_MISO LORA_MISO
|
||||
#define P_LORA_MOSI LORA_MOSI
|
||||
// P_LORA_EN is NOT defined — LoRa power is via XL9555, handled in board begin()
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 4G Modem — A7682E (ALWAYS PRESENT on MAX — no longer optional!)
|
||||
//
|
||||
// On V1.1, 4G and audio were mutually exclusive hardware configurations.
|
||||
// On MAX, both coexist. The XL9555 controls:
|
||||
// - 6609_EN (XL pin 0): modem power supply (SGM6609 boost converter)
|
||||
// - PWRKEY (XL pin 8): modem power key toggle
|
||||
// Audio output from modem vs ES8311 is selected by AUDIO_SEL (XL pin 10).
|
||||
//
|
||||
// MODEM_POWER_EN and MODEM_PWRKEY are NOT direct GPIOs — ModemManager
|
||||
// needs MAX-aware paths (see integration guide).
|
||||
// MODEM_RST does not exist on MAX (IO9 is now LCD_RST).
|
||||
// -----------------------------------------------------------------------------
|
||||
// Direct GPIO modem pins (still accessible as regular GPIO):
|
||||
#define MODEM_RI 7 // Ring indicator (interrupt input)
|
||||
#define MODEM_DTR 8 // Data terminal ready (output)
|
||||
#define MODEM_RX 10 // UART RX (ESP32 receives from modem)
|
||||
#define MODEM_TX 11 // UART TX (ESP32 sends to modem)
|
||||
|
||||
// XL9555-routed modem pins — these are NOT direct GPIO!
|
||||
// MODEM_POWER_EN and MODEM_PWRKEY are intentionally NOT defined.
|
||||
// Existing code guarded by #ifdef MODEM_POWER_EN / #ifdef HAS_4G_MODEM will
|
||||
// be skipped. Use board.modemPowerOn()/modemPwrkeyPulse() instead.
|
||||
// MODEM_RST does not exist on MAX (IO9 is LCD_RST).
|
||||
|
||||
// Compatibility: PIN_PERF_POWERON does not exist on MAX (IO10 is modem UART RX).
|
||||
// Defined as -1 so TDeckBoard.cpp compiles (parent class), but never used at runtime.
|
||||
#define PIN_PERF_POWERON -1
|
||||
Reference in New Issue
Block a user