mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
Compare commits
7 Commits
pro_max_wi
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce93cfa033 | ||
|
|
2be399f65a | ||
|
|
5679cda38e | ||
|
|
1ea883783c | ||
|
|
bf8cf32bc2 | ||
|
|
465a29bb23 | ||
|
|
81eca29b69 |
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -274,12 +274,20 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
if (file.read((uint8_t *)&_prefs.large_font, sizeof(_prefs.large_font)) != sizeof(_prefs.large_font)) {
|
||||
_prefs.large_font = 0; // default: tiny font
|
||||
}
|
||||
if (file.read((uint8_t *)&_prefs.tx_fail_reset_threshold, sizeof(_prefs.tx_fail_reset_threshold)) != sizeof(_prefs.tx_fail_reset_threshold)) {
|
||||
_prefs.tx_fail_reset_threshold = 3; // default: 3
|
||||
}
|
||||
if (file.read((uint8_t *)&_prefs.rx_fail_reboot_threshold, sizeof(_prefs.rx_fail_reboot_threshold)) != sizeof(_prefs.rx_fail_reboot_threshold)) {
|
||||
_prefs.rx_fail_reboot_threshold = 3; // default: 3
|
||||
}
|
||||
|
||||
// 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;
|
||||
if (_prefs.tx_fail_reset_threshold > 10) _prefs.tx_fail_reset_threshold = 3;
|
||||
if (_prefs.rx_fail_reboot_threshold > 10) _prefs.rx_fail_reboot_threshold = 3;
|
||||
// auto_lock_minutes: only accept known options (0, 2, 5, 10, 15, 30)
|
||||
{
|
||||
uint8_t alm = _prefs.auto_lock_minutes;
|
||||
@@ -334,6 +342,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
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.write((uint8_t *)&_prefs.tx_fail_reset_threshold, sizeof(_prefs.tx_fail_reset_threshold)); // 103
|
||||
file.write((uint8_t *)&_prefs.rx_fail_reboot_threshold, sizeof(_prefs.rx_fail_reboot_threshold)); // 104
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
@@ -264,6 +264,16 @@ int MyMesh::getInterferenceThreshold() const {
|
||||
return _prefs.interference_threshold;
|
||||
}
|
||||
|
||||
uint8_t MyMesh::getTxFailResetThreshold() const {
|
||||
return _prefs.tx_fail_reset_threshold;
|
||||
}
|
||||
uint8_t MyMesh::getRxFailRebootThreshold() const {
|
||||
return _prefs.rx_fail_reboot_threshold;
|
||||
}
|
||||
void MyMesh::onRxUnrecoverable() {
|
||||
board.reboot();
|
||||
}
|
||||
|
||||
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
||||
if (_prefs.rx_delay_base <= 0.0f) return 0;
|
||||
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
|
||||
@@ -2255,6 +2265,10 @@ void MyMesh::checkCLIRescueCmd() {
|
||||
Serial.printf(" > %d\n", _prefs.multi_acks);
|
||||
} else if (strcmp(key, "int.thresh") == 0) {
|
||||
Serial.printf(" > %d\n", _prefs.interference_threshold);
|
||||
} else if (strcmp(key, "tx.fail.threshold") == 0) {
|
||||
Serial.printf(" > %d\n", _prefs.tx_fail_reset_threshold);
|
||||
} else if (strcmp(key, "rx.fail.threshold") == 0) {
|
||||
Serial.printf(" > %d\n", _prefs.rx_fail_reboot_threshold);
|
||||
} else if (strcmp(key, "gps.baud") == 0) {
|
||||
uint32_t effective = _prefs.gps_baudrate ? _prefs.gps_baudrate : GPS_BAUDRATE;
|
||||
Serial.printf(" > %lu (effective: %lu)\n",
|
||||
@@ -2315,6 +2329,8 @@ void MyMesh::checkCLIRescueCmd() {
|
||||
Serial.printf(" af: %.1f\n", _prefs.airtime_factor);
|
||||
Serial.printf(" multi.acks: %d\n", _prefs.multi_acks);
|
||||
Serial.printf(" int.thresh: %d\n", _prefs.interference_threshold);
|
||||
Serial.printf(" tx.fail: %d\n", _prefs.tx_fail_reset_threshold);
|
||||
Serial.printf(" rx.fail: %d\n", _prefs.rx_fail_reboot_threshold);
|
||||
{
|
||||
uint32_t eff_baud = _prefs.gps_baudrate ? _prefs.gps_baudrate : GPS_BAUDRATE;
|
||||
Serial.printf(" gps.baud: %lu\n", (unsigned long)eff_baud);
|
||||
@@ -2710,6 +2726,30 @@ void MyMesh::checkCLIRescueCmd() {
|
||||
Serial.println(" Error: use 0 (disabled) or 14+ (typical: 14)");
|
||||
}
|
||||
|
||||
} else if (memcmp(config, "tx.fail.threshold ", 18) == 0) {
|
||||
int val = atoi(&config[18]);
|
||||
if (val < 0) val = 0;
|
||||
if (val > 10) val = 10;
|
||||
_prefs.tx_fail_reset_threshold = (uint8_t)val;
|
||||
savePrefs();
|
||||
if (val == 0) {
|
||||
Serial.println(" > tx fail reset disabled");
|
||||
} else {
|
||||
Serial.printf(" > tx fail reset after %d failures\n", val);
|
||||
}
|
||||
|
||||
} else if (memcmp(config, "rx.fail.threshold ", 18) == 0) {
|
||||
int val = atoi(&config[18]);
|
||||
if (val < 0) val = 0;
|
||||
if (val > 10) val = 10;
|
||||
_prefs.rx_fail_reboot_threshold = (uint8_t)val;
|
||||
savePrefs();
|
||||
if (val == 0) {
|
||||
Serial.println(" > rx fail reboot disabled");
|
||||
} else {
|
||||
Serial.printf(" > reboot after %d rx recovery failures\n", val);
|
||||
}
|
||||
|
||||
} else if (memcmp(config, "gps.baud ", 9) == 0) {
|
||||
uint32_t val = (uint32_t)atol(&config[9]);
|
||||
if (val == 0 || val == 4800 || val == 9600 || val == 19200 ||
|
||||
@@ -2807,6 +2847,8 @@ void MyMesh::checkCLIRescueCmd() {
|
||||
Serial.println(" af <0-9> Airtime factor");
|
||||
Serial.println(" multi.acks <0|1> Redundant ACKs (default: 1)");
|
||||
Serial.println(" int.thresh <0|14+> Interference threshold dB (0=off, 14=typical)");
|
||||
Serial.println(" tx.fail.threshold <0-10> TX fail radio reset (0=off, default 3)");
|
||||
Serial.println(" rx.fail.threshold <0-10> RX stuck reboot (0=off, default 3)");
|
||||
Serial.println(" gps.baud <rate> GPS baud (0=default, reboot to apply)");
|
||||
Serial.println("");
|
||||
Serial.println(" Clock:");
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
#define FIRMWARE_VER_CODE 10
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "26 March 2026"
|
||||
#define FIRMWARE_BUILD_DATE "27 March 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v1.4"
|
||||
#define FIRMWARE_VERSION "Meck v1.5"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
@@ -143,6 +143,9 @@ public:
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override;
|
||||
int getInterferenceThreshold() const override;
|
||||
uint8_t getTxFailResetThreshold() const override;
|
||||
uint8_t getRxFailRebootThreshold() const override;
|
||||
void onRxUnrecoverable() override;
|
||||
int calcRxDelay(float score, uint32_t air_time) const override;
|
||||
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
|
||||
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
|
||||
|
||||
@@ -40,6 +40,8 @@ struct NodePrefs { // persisted to file
|
||||
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
|
||||
uint8_t tx_fail_reset_threshold; // 0=disabled, 1-10, default 3
|
||||
uint8_t rx_fail_reboot_threshold; // 0=disabled, 1-10, default 3
|
||||
|
||||
// --- Font helpers (inline, no overhead) ---
|
||||
// Returns the DisplayDriver text-size index for "small/body" text.
|
||||
|
||||
@@ -489,7 +489,7 @@
|
||||
static int16_t touchLastX = 0;
|
||||
static int16_t touchLastY = 0;
|
||||
static unsigned long lastTouchSeenMs = 0;
|
||||
#define TOUCH_LONG_PRESS_MS 500
|
||||
#define TOUCH_LONG_PRESS_MS 750
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
#define TOUCH_SWIPE_THRESHOLD 60 // T5S3: 960×540 — 60px ≈ 6% of width
|
||||
#else
|
||||
@@ -931,6 +931,12 @@ static void lastHeardToggleContact() {
|
||||
return KEY_ENTER; // Editing mode or header/footer tap
|
||||
}
|
||||
|
||||
// SMS screen: dedicated dialer/touch handler runs separately (HAS_4G_MODEM block)
|
||||
// Return 0 so the general handler doesn't inject spurious keys
|
||||
#ifdef HAS_4G_MODEM
|
||||
if (ui_task.isOnSMSScreen()) return 0;
|
||||
#endif
|
||||
|
||||
// All other screens: tap = select
|
||||
return KEY_ENTER;
|
||||
}
|
||||
@@ -939,6 +945,11 @@ static void lastHeardToggleContact() {
|
||||
static char mapTouchSwipe(int16_t dx, int16_t dy) {
|
||||
bool horizontal = abs(dx) > abs(dy);
|
||||
|
||||
// SMS screen: dedicated touch handler covers all interaction
|
||||
#ifdef HAS_4G_MODEM
|
||||
if (ui_task.isOnSMSScreen()) return 0;
|
||||
#endif
|
||||
|
||||
// Reader (reading mode): swipe left/right for page turn
|
||||
if (ui_task.isOnTextReader()) {
|
||||
TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen();
|
||||
@@ -1002,6 +1013,11 @@ static void lastHeardToggleContact() {
|
||||
|
||||
// Map a long press to a key
|
||||
static char mapTouchLongPress(int16_t x, int16_t y) {
|
||||
// SMS screen: dedicated touch handler covers all interaction
|
||||
#ifdef HAS_4G_MODEM
|
||||
if (ui_task.isOnSMSScreen()) return 0;
|
||||
#endif
|
||||
|
||||
// Home screen: long press = activate current page action
|
||||
// (BLE toggle, send advert, hibernate, GPS toggle, etc.)
|
||||
if (ui_task.isOnHomeScreen()) {
|
||||
@@ -1819,7 +1835,7 @@ void loop() {
|
||||
the_mesh.loop();
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
} else {
|
||||
// OTA active — poll the web server from the main loop for fast response.
|
||||
// OTA/File Manager active — poll the web server from the main loop for fast response.
|
||||
// The render cycle on T5S3 (960×540 FastEPD) can block for 500ms+ during
|
||||
// e-ink refresh, causing the browser to timeout before handleClient() runs.
|
||||
// Polling here gives us ~1-5ms response time instead.
|
||||
@@ -2178,7 +2194,7 @@ void loop() {
|
||||
// Gestures:
|
||||
// Tap = finger down + up with minimal movement → select/open
|
||||
// Swipe = finger drag > threshold → scroll/page turn
|
||||
// Long press = finger held > 500ms without moving → edit/enter
|
||||
// Long press = finger held > 750ms without moving → edit/enter
|
||||
// After processing an event, cooldown waits for finger lift before next event.
|
||||
// Touch is disabled while lock screen is active.
|
||||
// When virtual keyboard is active (T5S3), taps route to keyboard.
|
||||
@@ -2190,6 +2206,15 @@ void loop() {
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
touchBlocked = touchBlocked || ui_task.isVKBActive();
|
||||
#endif
|
||||
#ifdef HAS_4G_MODEM
|
||||
// SMS dialer has its own dedicated touch handler — don't consume touch data here
|
||||
if (smsMode) {
|
||||
SMSScreen* smsScr = (SMSScreen*)ui_task.getSMSScreen();
|
||||
if (smsScr && smsScr->getSubView() == SMSScreen::PHONE_DIALER) {
|
||||
touchBlocked = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!touchBlocked)
|
||||
{
|
||||
@@ -2564,21 +2589,6 @@ void handleKeyboardInput() {
|
||||
// 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();
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <SD.h>
|
||||
#endif
|
||||
#include <WebServer.h>
|
||||
#include <DNSServer.h>
|
||||
#include <Update.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#endif
|
||||
@@ -143,7 +144,9 @@ enum SettingsRowType : uint8_t {
|
||||
ROW_ADD_CHANNEL, // "+ Add Hashtag Channel"
|
||||
ROW_INFO_HEADER, // "--- Info ---" separator
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
ROW_OTA_TOOLS_SUBMENU, // Folder row → enters OTA Tools sub-screen
|
||||
ROW_FW_UPDATE, // "Firmware Update" — WiFi upload + flash
|
||||
ROW_SD_FILE_MGR, // "SD File Manager" — WiFi file browser
|
||||
#endif
|
||||
ROW_PUB_KEY, // Public key display
|
||||
ROW_FIRMWARE, // Firmware version
|
||||
@@ -168,6 +171,7 @@ enum EditMode : uint8_t {
|
||||
#endif
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
EDIT_OTA, // OTA firmware update flow (multi-phase overlay)
|
||||
EDIT_FILEMGR, // SD file manager flow (WiFi file browser)
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -178,6 +182,9 @@ enum SubScreen : uint8_t {
|
||||
SUB_NONE, // Top-level settings list
|
||||
SUB_CONTACTS, // Contacts settings sub-screen
|
||||
SUB_CHANNELS, // Channels management sub-screen
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
SUB_OTA_TOOLS, // OTA Tools sub-screen (FW update + File Manager)
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
@@ -192,6 +199,13 @@ enum OtaPhase : uint8_t {
|
||||
OTA_PHASE_DONE, // Success, rebooting
|
||||
OTA_PHASE_ERROR, // Error with message
|
||||
};
|
||||
|
||||
// File manager phases
|
||||
enum FmPhase : uint8_t {
|
||||
FM_PHASE_CONFIRM, // "Start SD file manager? Enter:Yes Q:No"
|
||||
FM_PHASE_WAITING, // AP up, file browser active
|
||||
FM_PHASE_ERROR, // Error with message
|
||||
};
|
||||
#endif
|
||||
|
||||
// Max rows in the settings list (increased for contact sub-toggles + WiFi)
|
||||
@@ -281,6 +295,10 @@ private:
|
||||
bool _otaUploadOk;
|
||||
char _otaApName[24];
|
||||
const char* _otaError;
|
||||
// File manager state
|
||||
FmPhase _fmPhase;
|
||||
const char* _fmError;
|
||||
DNSServer* _dnsServer;
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -362,6 +380,12 @@ private:
|
||||
}
|
||||
}
|
||||
addRow(ROW_ADD_CHANNEL);
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
} else if (_subScreen == SUB_OTA_TOOLS) {
|
||||
// --- OTA Tools sub-screen ---
|
||||
addRow(ROW_FW_UPDATE);
|
||||
addRow(ROW_SD_FILE_MGR);
|
||||
#endif
|
||||
} else {
|
||||
// --- Top-level settings list ---
|
||||
addRow(ROW_NAME);
|
||||
@@ -394,12 +418,12 @@ private:
|
||||
// Folder rows for sub-screens
|
||||
addRow(ROW_CONTACTS_SUBMENU);
|
||||
addRow(ROW_CHANNELS_SUBMENU);
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
addRow(ROW_OTA_TOOLS_SUBMENU);
|
||||
#endif
|
||||
|
||||
// Info section (stays at top level)
|
||||
addRow(ROW_INFO_HEADER);
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
addRow(ROW_FW_UPDATE);
|
||||
#endif
|
||||
addRow(ROW_PUB_KEY);
|
||||
addRow(ROW_FIRMWARE);
|
||||
|
||||
@@ -556,6 +580,9 @@ public:
|
||||
_otaBytesReceived = 0;
|
||||
_otaUploadOk = false;
|
||||
_otaError = nullptr;
|
||||
_fmPhase = FM_PHASE_CONFIRM;
|
||||
_fmError = nullptr;
|
||||
_dnsServer = nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1003,10 +1030,18 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// Called from render loop AND main loop to poll the web server
|
||||
// Called from render loop AND main loop to poll the web server.
|
||||
// Handles both OTA firmware upload and SD file manager modes.
|
||||
void pollOTAServer() {
|
||||
if (_otaServer && (_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) {
|
||||
_otaServer->handleClient();
|
||||
if (_otaServer) {
|
||||
if ((_editMode == EDIT_OTA && (_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) ||
|
||||
(_editMode == EDIT_FILEMGR && _fmPhase == FM_PHASE_WAITING)) {
|
||||
_otaServer->handleClient();
|
||||
}
|
||||
}
|
||||
// Process DNS for captive portal redirect (file manager only)
|
||||
if (_dnsServer && _editMode == EDIT_FILEMGR && _fmPhase == FM_PHASE_WAITING) {
|
||||
_dnsServer->processNextRequest();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1073,6 +1108,443 @@ public:
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SD File Manager — WiFi file browser, upload, download, delete
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void startFileMgr() {
|
||||
_editMode = EDIT_FILEMGR;
|
||||
_fmPhase = FM_PHASE_CONFIRM;
|
||||
_fmError = nullptr;
|
||||
}
|
||||
|
||||
void startFileMgrServer() {
|
||||
// Build AP name with last 4 of MAC for uniqueness
|
||||
uint8_t mac[6];
|
||||
WiFi.macAddress(mac);
|
||||
snprintf(_otaApName, sizeof(_otaApName), "Meck-Files-%02X%02X", mac[4], mac[5]);
|
||||
|
||||
// Pause LoRa radio — SD and LoRa share the same SPI bus on both
|
||||
// platforms. Incoming packets during SD writes cause bus contention.
|
||||
extern void otaPauseRadio();
|
||||
otaPauseRadio();
|
||||
|
||||
// Clean WiFi init from any state
|
||||
WiFi.disconnect(true);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
delay(200);
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(_otaApName);
|
||||
delay(500);
|
||||
Serial.printf("FM: AP '%s' started, IP: %s\n",
|
||||
_otaApName, WiFi.softAPIP().toString().c_str());
|
||||
|
||||
// Start DNS server — redirect ALL DNS lookups to our AP IP.
|
||||
// This triggers captive portal detection on phones, which opens the
|
||||
// page in a real browser instead of the restricted captive webview.
|
||||
if (_dnsServer) { delete _dnsServer; }
|
||||
_dnsServer = new DNSServer();
|
||||
_dnsServer->start(53, "*", WiFi.softAPIP());
|
||||
Serial.println("FM: DNS captive portal started");
|
||||
|
||||
// Start web server
|
||||
if (_otaServer) { _otaServer->stop(); delete _otaServer; }
|
||||
_otaServer = new WebServer(80);
|
||||
|
||||
// --- Captive portal detection handlers ---
|
||||
// Phones/OS probe these URLs to detect captive portals. Redirecting
|
||||
// them to our page causes the OS to open a real browser.
|
||||
// iOS / macOS
|
||||
_otaServer->on("/hotspot-detect.html", HTTP_GET, [this]() {
|
||||
Serial.println("FM: captive probe (Apple)");
|
||||
_otaServer->sendHeader("Location", "http://192.168.4.1/");
|
||||
_otaServer->send(302, "text/plain", "");
|
||||
});
|
||||
// Android
|
||||
_otaServer->on("/generate_204", HTTP_GET, [this]() {
|
||||
Serial.println("FM: captive probe (Android)");
|
||||
_otaServer->sendHeader("Location", "http://192.168.4.1/");
|
||||
_otaServer->send(302, "text/plain", "");
|
||||
});
|
||||
_otaServer->on("/gen_204", HTTP_GET, [this]() {
|
||||
_otaServer->sendHeader("Location", "http://192.168.4.1/");
|
||||
_otaServer->send(302, "text/plain", "");
|
||||
});
|
||||
// Windows
|
||||
_otaServer->on("/connecttest.txt", HTTP_GET, [this]() {
|
||||
_otaServer->sendHeader("Location", "http://192.168.4.1/");
|
||||
_otaServer->send(302, "text/plain", "");
|
||||
});
|
||||
_otaServer->on("/redirect", HTTP_GET, [this]() {
|
||||
_otaServer->sendHeader("Location", "http://192.168.4.1/");
|
||||
_otaServer->send(302, "text/plain", "");
|
||||
});
|
||||
// Firefox
|
||||
_otaServer->on("/canonical.html", HTTP_GET, [this]() {
|
||||
_otaServer->sendHeader("Location", "http://192.168.4.1/");
|
||||
_otaServer->send(302, "text/plain", "");
|
||||
});
|
||||
_otaServer->on("/success.txt", HTTP_GET, [this]() {
|
||||
_otaServer->send(200, "text/plain", "success");
|
||||
});
|
||||
|
||||
// --- Main page: server-rendered directory listing (no JS needed) ---
|
||||
_otaServer->on("/", HTTP_GET, [this]() {
|
||||
String path = _otaServer->arg("path");
|
||||
if (path.isEmpty()) path = "/";
|
||||
String msg = _otaServer->arg("msg");
|
||||
Serial.printf("FM: page request path='%s'\n", path.c_str());
|
||||
String html = fmBuildPage(path, msg);
|
||||
_otaServer->send(200, "text/html", html);
|
||||
});
|
||||
|
||||
// --- File download: GET /dl?path=/file.txt ---
|
||||
_otaServer->on("/dl", HTTP_GET, [this]() {
|
||||
String path = _otaServer->arg("path");
|
||||
File f = SD.open(path, FILE_READ);
|
||||
if (!f || f.isDirectory()) {
|
||||
if (f) f.close();
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
_otaServer->send(404, "text/plain", "Not found");
|
||||
return;
|
||||
}
|
||||
String name = path;
|
||||
int lastSlash = name.lastIndexOf('/');
|
||||
if (lastSlash >= 0) name = name.substring(lastSlash + 1);
|
||||
_otaServer->sendHeader("Content-Disposition",
|
||||
"attachment; filename=\"" + name + "\"");
|
||||
size_t fileSize = f.size();
|
||||
_otaServer->setContentLength(fileSize);
|
||||
_otaServer->send(200, "application/octet-stream", "");
|
||||
uint8_t* buf = (uint8_t*)ps_malloc(4096);
|
||||
if (!buf) buf = (uint8_t*)malloc(4096);
|
||||
if (buf) {
|
||||
while (f.available()) {
|
||||
int n = f.read(buf, 4096);
|
||||
if (n > 0) _otaServer->sendContent((const char*)buf, n);
|
||||
}
|
||||
free(buf);
|
||||
}
|
||||
f.close();
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
});
|
||||
|
||||
// --- File upload: POST /upload?dir=/ → redirect back to listing ---
|
||||
_otaServer->on("/upload", HTTP_POST,
|
||||
[this]() {
|
||||
String dir = _otaServer->arg("dir");
|
||||
if (dir.isEmpty()) dir = "/";
|
||||
_otaServer->sendHeader("Location", "/?path=" + dir + "&msg=Upload+complete");
|
||||
_otaServer->send(303, "text/plain", "Redirecting...");
|
||||
},
|
||||
[this]() {
|
||||
HTTPUpload& upload = _otaServer->upload();
|
||||
static File fmUploadFile;
|
||||
|
||||
if (upload.status == UPLOAD_FILE_START) {
|
||||
String dir = _otaServer->arg("dir");
|
||||
if (dir.isEmpty()) dir = "/";
|
||||
if (!dir.endsWith("/")) dir += "/";
|
||||
String fullPath = dir + upload.filename;
|
||||
Serial.printf("FM: Upload start: %s\n", fullPath.c_str());
|
||||
fmUploadFile = SD.open(fullPath, FILE_WRITE);
|
||||
if (!fmUploadFile) Serial.println("FM: Failed to open file for write");
|
||||
|
||||
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
||||
if (fmUploadFile) fmUploadFile.write(upload.buf, upload.currentSize);
|
||||
|
||||
} else if (upload.status == UPLOAD_FILE_END) {
|
||||
if (fmUploadFile) {
|
||||
fmUploadFile.close();
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
Serial.printf("FM: Upload done: %s (%d bytes)\n",
|
||||
upload.filename.c_str(), upload.totalSize);
|
||||
}
|
||||
|
||||
} else if (upload.status == UPLOAD_FILE_ABORTED) {
|
||||
if (fmUploadFile) fmUploadFile.close();
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
Serial.println("FM: Upload aborted");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// --- Create directory: GET /mkdir?name=xxx&dir=/path ---
|
||||
_otaServer->on("/mkdir", HTTP_GET, [this]() {
|
||||
String dir = _otaServer->arg("dir");
|
||||
String name = _otaServer->arg("name");
|
||||
if (dir.isEmpty()) dir = "/";
|
||||
if (name.isEmpty()) {
|
||||
_otaServer->sendHeader("Location", "/?path=" + dir + "&msg=No+name");
|
||||
_otaServer->send(303);
|
||||
return;
|
||||
}
|
||||
String full = dir + (dir.endsWith("/") ? "" : "/") + name;
|
||||
bool ok = SD.mkdir(full);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
Serial.printf("FM: mkdir '%s' %s\n", full.c_str(), ok ? "OK" : "FAIL");
|
||||
_otaServer->sendHeader("Location",
|
||||
"/?path=" + dir + "&msg=" + (ok ? "Folder+created" : "mkdir+failed"));
|
||||
_otaServer->send(303);
|
||||
});
|
||||
|
||||
// --- Delete file/folder: GET /rm?path=/file&ret=/parent ---
|
||||
_otaServer->on("/rm", HTTP_GET, [this]() {
|
||||
String path = _otaServer->arg("path");
|
||||
String ret = _otaServer->arg("ret");
|
||||
if (ret.isEmpty()) ret = "/";
|
||||
if (path.isEmpty() || path == "/") {
|
||||
_otaServer->sendHeader("Location", "/?path=" + ret + "&msg=Bad+path");
|
||||
_otaServer->send(303);
|
||||
return;
|
||||
}
|
||||
File f = SD.open(path);
|
||||
bool ok = false;
|
||||
if (f) {
|
||||
bool isDir = f.isDirectory();
|
||||
f.close();
|
||||
ok = isDir ? SD.rmdir(path) : SD.remove(path);
|
||||
}
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
Serial.printf("FM: rm '%s' %s\n", path.c_str(), ok ? "OK" : "FAIL");
|
||||
_otaServer->sendHeader("Location",
|
||||
"/?path=" + ret + "&msg=" + (ok ? "Deleted" : "Delete+failed"));
|
||||
_otaServer->send(303);
|
||||
});
|
||||
|
||||
// --- Confirm delete page: GET /confirm-rm?path=/file&ret=/parent ---
|
||||
_otaServer->on("/confirm-rm", HTTP_GET, [this]() {
|
||||
String path = _otaServer->arg("path");
|
||||
String ret = _otaServer->arg("ret");
|
||||
if (ret.isEmpty()) ret = "/";
|
||||
String name = path;
|
||||
int sl = name.lastIndexOf('/');
|
||||
if (sl >= 0) name = name.substring(sl + 1);
|
||||
String html = "<!DOCTYPE html><html><head>"
|
||||
"<meta charset='UTF-8'>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||
"<title>Confirm Delete</title>"
|
||||
"<style>"
|
||||
"body{font-family:-apple-system,sans-serif;max-width:480px;margin:40px auto;"
|
||||
"padding:0 20px;background:#1a1a2e;color:#e0e0e0;text-align:center}"
|
||||
".b{display:inline-block;padding:10px 24px;border-radius:6px;text-decoration:none;"
|
||||
"font-weight:bold;margin:8px;font-size:1em}"
|
||||
".br{background:#e74c3c;color:#fff}.bg{background:#4ecca3;color:#1a1a2e}"
|
||||
"</style></head><body>"
|
||||
"<h2 style='color:#e74c3c'>Delete?</h2>"
|
||||
"<p style='font-size:1.1em'>" + fmHtmlEscape(name) + "</p>"
|
||||
"<a class='b br' href='/rm?path=" + fmUrlEncode(path) + "&ret=" + fmUrlEncode(ret) + "'>Delete</a>"
|
||||
"<a class='b bg' href='/?path=" + fmUrlEncode(ret) + "'>Cancel</a>"
|
||||
"</body></html>";
|
||||
_otaServer->send(200, "text/html", html);
|
||||
});
|
||||
|
||||
// Catch-all: redirect unknown URLs to file manager (catches captive portal probes)
|
||||
_otaServer->onNotFound([this]() {
|
||||
Serial.printf("FM: redirect %s -> /\n", _otaServer->uri().c_str());
|
||||
_otaServer->sendHeader("Location", "http://192.168.4.1/");
|
||||
_otaServer->send(302, "text/plain", "");
|
||||
});
|
||||
|
||||
_otaServer->begin();
|
||||
Serial.println("FM: Web server started on port 80");
|
||||
_fmPhase = FM_PHASE_WAITING;
|
||||
}
|
||||
|
||||
void stopFileMgr() {
|
||||
if (_otaServer) { _otaServer->stop(); delete _otaServer; _otaServer = nullptr; }
|
||||
if (_dnsServer) { _dnsServer->stop(); delete _dnsServer; _dnsServer = nullptr; }
|
||||
WiFi.softAPdisconnect(true);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
delay(100);
|
||||
_editMode = EDIT_NONE;
|
||||
extern void otaResumeRadio();
|
||||
otaResumeRadio();
|
||||
#ifdef MECK_WIFI_COMPANION
|
||||
WiFi.mode(WIFI_STA);
|
||||
wifiReconnectSaved();
|
||||
#endif
|
||||
Serial.println("FM: Stopped, AP down, radio resumed");
|
||||
}
|
||||
|
||||
// --- Helpers for server-rendered HTML ---
|
||||
|
||||
static String fmHtmlEscape(const String& s) {
|
||||
String r;
|
||||
r.reserve(s.length());
|
||||
for (unsigned int i = 0; i < s.length(); i++) {
|
||||
char c = s[i];
|
||||
if (c == '&') r += "&";
|
||||
else if (c == '<') r += "<";
|
||||
else if (c == '>') r += ">";
|
||||
else if (c == '"') r += """;
|
||||
else r += c;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static String fmUrlEncode(const String& s) {
|
||||
String r;
|
||||
for (unsigned int i = 0; i < s.length(); i++) {
|
||||
char c = s[i];
|
||||
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '/' || c == '~') {
|
||||
r += c;
|
||||
} else {
|
||||
char hex[4];
|
||||
snprintf(hex, sizeof(hex), "%%%02X", (uint8_t)c);
|
||||
r += hex;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static String fmFormatSize(size_t bytes) {
|
||||
if (bytes < 1024) return String(bytes) + " B";
|
||||
if (bytes < 1048576) return String(bytes / 1024) + " KB";
|
||||
return String(bytes / 1048576) + "." + String((bytes % 1048576) * 10 / 1048576) + " MB";
|
||||
}
|
||||
|
||||
// Build the complete HTML page with inline directory listing
|
||||
String fmBuildPage(const String& path, const String& msg) {
|
||||
String html;
|
||||
html.reserve(4096);
|
||||
|
||||
// --- Head + CSS ---
|
||||
html += "<!DOCTYPE html><html><head>"
|
||||
"<meta charset='UTF-8'>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||
"<title>Meck SD Files</title>"
|
||||
"<style>"
|
||||
"body{font-family:-apple-system,sans-serif;max-width:600px;margin:20px auto;"
|
||||
"padding:0 16px;background:#1a1a2e;color:#e0e0e0}"
|
||||
"h1{color:#4ecca3;font-size:1.3em;margin:8px 0}"
|
||||
".pa{background:#16213e;padding:8px 12px;border-radius:6px;margin:8px 0;"
|
||||
"font-family:monospace;font-size:0.9em;word-break:break-all}"
|
||||
".tb{display:flex;gap:6px;margin:8px 0;flex-wrap:wrap}"
|
||||
".b{background:#4ecca3;color:#1a1a2e;border:none;padding:7px 14px;"
|
||||
"border-radius:5px;font-size:0.85em;font-weight:bold;cursor:pointer;"
|
||||
"text-decoration:none;display:inline-block}"
|
||||
".b:active{background:#3ba88f}"
|
||||
".br{background:#e74c3c;color:#fff;padding:3px 8px;font-size:0.75em}.br:active{background:#c0392b}"
|
||||
".it{display:flex;align-items:center;padding:8px 4px;border-bottom:1px solid #16213e;gap:6px}"
|
||||
".ic{font-size:1.1em;width:22px;text-align:center}"
|
||||
".nm{flex:1;word-break:break-all;color:#e0e0e0;text-decoration:none}"
|
||||
".nm:hover{color:#4ecca3}"
|
||||
".sz{color:#888;font-size:0.8em;min-width:54px;text-align:right;margin-right:4px}"
|
||||
".up{background:#16213e;border:2px dashed #4ecca3;border-radius:8px;"
|
||||
"padding:14px;margin:10px 0;text-align:center}"
|
||||
".em{color:#888;text-align:center;padding:20px}"
|
||||
".ms{background:#16213e;padding:8px 12px;border-radius:6px;margin:8px 0;"
|
||||
"border-left:3px solid #4ecca3;font-size:0.9em}"
|
||||
"</style></head><body>";
|
||||
|
||||
// --- Title + path ---
|
||||
html += "<h1>Meck SD File Manager</h1>";
|
||||
html += "<div class='pa'>" + fmHtmlEscape(path) + "</div>";
|
||||
|
||||
// --- Status message (from redirects) ---
|
||||
if (msg.length() > 0) {
|
||||
html += "<div class='ms'>" + fmHtmlEscape(msg) + "</div>";
|
||||
}
|
||||
|
||||
// --- Navigation buttons ---
|
||||
html += "<div class='tb'>";
|
||||
if (path != "/") {
|
||||
// Compute parent
|
||||
String parent = path;
|
||||
if (parent.endsWith("/")) parent = parent.substring(0, parent.length() - 1);
|
||||
int sl = parent.lastIndexOf('/');
|
||||
parent = (sl <= 0) ? "/" : parent.substring(0, sl);
|
||||
html += "<a class='b' href='/?path=" + fmUrlEncode(parent) + "'>.. Up</a>";
|
||||
}
|
||||
html += "<a class='b' href='/?path=" + fmUrlEncode(path) + "'>Refresh</a>";
|
||||
html += "</div>";
|
||||
|
||||
// --- Directory listing (server-rendered) ---
|
||||
File dir = SD.open(path, FILE_READ);
|
||||
if (!dir || !dir.isDirectory()) {
|
||||
if (dir) dir.close();
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
html += "<div class='em'>Cannot open directory</div>";
|
||||
} else {
|
||||
// Collect entries into arrays for sorting (dirs first, then alpha)
|
||||
struct FmEntry { String name; size_t size; bool isDir; };
|
||||
FmEntry entries[128]; // max entries to display
|
||||
int count = 0;
|
||||
File entry = dir.openNextFile();
|
||||
while (entry && count < 128) {
|
||||
const char* fullName = entry.name();
|
||||
const char* baseName = strrchr(fullName, '/');
|
||||
baseName = baseName ? baseName + 1 : fullName;
|
||||
entries[count].name = baseName;
|
||||
entries[count].size = entry.size();
|
||||
entries[count].isDir = entry.isDirectory();
|
||||
count++;
|
||||
entry.close();
|
||||
entry = dir.openNextFile();
|
||||
}
|
||||
dir.close();
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
|
||||
Serial.printf("FM: listing %d entries for '%s'\n", count, path.c_str());
|
||||
|
||||
// Sort: dirs first, then alphabetical
|
||||
for (int i = 0; i < count - 1; i++) {
|
||||
for (int j = i + 1; j < count; j++) {
|
||||
bool swap = false;
|
||||
if (entries[i].isDir != entries[j].isDir) {
|
||||
swap = !entries[i].isDir && entries[j].isDir;
|
||||
} else {
|
||||
swap = entries[i].name.compareTo(entries[j].name) > 0;
|
||||
}
|
||||
if (swap) {
|
||||
FmEntry tmp = entries[i];
|
||||
entries[i] = entries[j];
|
||||
entries[j] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
html += "<div class='em'>Empty folder</div>";
|
||||
} else {
|
||||
for (int i = 0; i < count; i++) {
|
||||
String fp = path + (path.endsWith("/") ? "" : "/") + entries[i].name;
|
||||
html += "<div class='it'>";
|
||||
html += "<span class='ic'>" + String(entries[i].isDir ? "\xF0\x9F\x93\x81" : "\xF0\x9F\x93\x84") + "</span>";
|
||||
if (entries[i].isDir) {
|
||||
html += "<a class='nm' href='/?path=" + fmUrlEncode(fp) + "'>" + fmHtmlEscape(entries[i].name) + "</a>";
|
||||
} else {
|
||||
html += "<a class='nm' href='/dl?path=" + fmUrlEncode(fp) + "'>" + fmHtmlEscape(entries[i].name) + "</a>";
|
||||
html += "<span class='sz'>" + fmFormatSize(entries[i].size) + "</span>";
|
||||
}
|
||||
html += "<a class='b br' href='/confirm-rm?path=" + fmUrlEncode(fp) + "&ret=" + fmUrlEncode(path) + "'>Del</a>";
|
||||
html += "</div>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Upload form (standard HTML form, no JS needed) ---
|
||||
html += "<div class='up'>"
|
||||
"<form method='POST' action='/upload?dir=" + fmUrlEncode(path) + "' enctype='multipart/form-data'>"
|
||||
"<p>Select files to upload</p>"
|
||||
"<input type='file' name='file' multiple><br><br>"
|
||||
"<button class='b' type='submit'>Upload</button>"
|
||||
"</form></div>";
|
||||
|
||||
// --- New folder (tiny inline form) ---
|
||||
html += "<form action='/mkdir' method='GET' style='margin:8px 0;display:flex;gap:6px'>"
|
||||
"<input type='hidden' name='dir' value='" + fmHtmlEscape(path) + "'>"
|
||||
"<input type='text' name='name' placeholder='New folder name' "
|
||||
"style='flex:1;padding:7px;border-radius:5px;border:1px solid #4ecca3;"
|
||||
"background:#16213e;color:#e0e0e0'>"
|
||||
"<button class='b' type='submit'>Create</button>"
|
||||
"</form>";
|
||||
|
||||
html += "</body></html>";
|
||||
return html;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1121,6 +1593,10 @@ public:
|
||||
display.print("Settings > Contacts");
|
||||
} else if (_subScreen == SUB_CHANNELS) {
|
||||
display.print("Settings > Channels");
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
} else if (_subScreen == SUB_OTA_TOOLS) {
|
||||
display.print("Settings > OTA Tools");
|
||||
#endif
|
||||
} else {
|
||||
display.print("Settings");
|
||||
}
|
||||
@@ -1446,9 +1922,18 @@ public:
|
||||
break;
|
||||
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
case ROW_OTA_TOOLS_SUBMENU:
|
||||
display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN);
|
||||
display.print("OTA Tools >>");
|
||||
break;
|
||||
|
||||
case ROW_FW_UPDATE:
|
||||
display.print("Firmware Update");
|
||||
break;
|
||||
|
||||
case ROW_SD_FILE_MGR:
|
||||
display.print("SD File Manager");
|
||||
break;
|
||||
#endif
|
||||
|
||||
case ROW_INFO_HEADER:
|
||||
@@ -1725,6 +2210,75 @@ public:
|
||||
|
||||
display.setTextSize(1);
|
||||
}
|
||||
|
||||
// === File Manager overlay ===
|
||||
if (_editMode == EDIT_FILEMGR) {
|
||||
int bx = 2, by = 14, bw = display.width() - 4;
|
||||
int bh = display.height() - 28;
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
display.fillRect(bx, by, bw, bh);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(bx, by, bw, bh);
|
||||
|
||||
display.setTextSize(_prefs->smallTextSize());
|
||||
int oy = by + 4;
|
||||
|
||||
if (_fmPhase == FM_PHASE_CONFIRM) {
|
||||
display.drawTextCentered(display.width() / 2, oy, "SD File Manager");
|
||||
oy += 14;
|
||||
display.setCursor(bx + 4, oy);
|
||||
display.print("Start WiFi file server?");
|
||||
oy += 10;
|
||||
display.setCursor(bx + 4, oy);
|
||||
display.print("Upload and download files");
|
||||
oy += 8;
|
||||
display.setCursor(bx + 4, oy);
|
||||
display.print("on SD card via browser.");
|
||||
oy += 10;
|
||||
display.setCursor(bx + 4, oy);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.print("LoRa paused while active.");
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
} else if (_fmPhase == FM_PHASE_WAITING) {
|
||||
display.drawTextCentered(display.width() / 2, oy, "SD File Manager");
|
||||
oy += 14;
|
||||
display.setCursor(bx + 4, oy);
|
||||
display.print("Connect to WiFi network:");
|
||||
oy += 10;
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(bx + 4, oy);
|
||||
display.print(_otaApName);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
oy += 12;
|
||||
display.setCursor(bx + 4, oy);
|
||||
display.print("Then open browser:");
|
||||
oy += 10;
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(bx + 4, oy);
|
||||
char ipBuf[32];
|
||||
snprintf(ipBuf, sizeof(ipBuf), "http://%s", WiFi.softAPIP().toString().c_str());
|
||||
display.print(ipBuf);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
oy += 12;
|
||||
display.setCursor(bx + 4, oy);
|
||||
display.print("File server active...");
|
||||
|
||||
pollOTAServer();
|
||||
|
||||
} else if (_fmPhase == FM_PHASE_ERROR) {
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.drawTextCentered(display.width() / 2, oy, "File Manager Error");
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
oy += 14;
|
||||
if (_fmError) {
|
||||
display.setCursor(bx + 4, oy);
|
||||
display.print(_fmError);
|
||||
}
|
||||
}
|
||||
|
||||
display.setTextSize(1);
|
||||
}
|
||||
#endif
|
||||
|
||||
// === Footer ===
|
||||
@@ -1737,7 +2291,12 @@ public:
|
||||
if (_editMode == EDIT_NONE) {
|
||||
if (_subScreen != SUB_NONE) {
|
||||
display.print("Boot:Back");
|
||||
const char* r = (_subScreen == SUB_CHANNELS) ? "Tap:Select Hold:Del" : "Tap:Toggle Hold:Edit";
|
||||
const char* r;
|
||||
if (_subScreen == SUB_CHANNELS) r = "Tap:Select Hold:Del";
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
else if (_subScreen == SUB_OTA_TOOLS) r = "Tap:Select";
|
||||
#endif
|
||||
else r = "Tap:Toggle Hold:Edit";
|
||||
display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY);
|
||||
display.print(r);
|
||||
} else {
|
||||
@@ -1786,6 +2345,19 @@ public:
|
||||
} else {
|
||||
display.print("Please wait...");
|
||||
}
|
||||
} else if (_editMode == EDIT_FILEMGR) {
|
||||
if (_fmPhase == FM_PHASE_CONFIRM) {
|
||||
display.print("Boot:Cancel");
|
||||
const char* r = "Tap:Start";
|
||||
display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY);
|
||||
display.print(r);
|
||||
} else if (_fmPhase == FM_PHASE_WAITING) {
|
||||
display.print("Boot:Stop");
|
||||
} else if (_fmPhase == FM_PHASE_ERROR) {
|
||||
display.print("Boot:Back");
|
||||
} else {
|
||||
display.print("Please wait...");
|
||||
}
|
||||
#endif
|
||||
} else if (_editMode == EDIT_TEXT) {
|
||||
display.print("Hold:Type");
|
||||
@@ -1823,6 +2395,16 @@ public:
|
||||
} else {
|
||||
display.print("Please wait...");
|
||||
}
|
||||
} else if (_editMode == EDIT_FILEMGR) {
|
||||
if (_fmPhase == FM_PHASE_CONFIRM) {
|
||||
display.print("Enter:Start Q:Cancel");
|
||||
} else if (_fmPhase == FM_PHASE_WAITING) {
|
||||
display.print("Q:Stop");
|
||||
} else if (_fmPhase == FM_PHASE_ERROR) {
|
||||
display.print("Q:Back");
|
||||
} else {
|
||||
display.print("Please wait...");
|
||||
}
|
||||
#endif
|
||||
} else if (_editMode == EDIT_PICKER) {
|
||||
display.print("A/D:Choose Enter:Ok");
|
||||
@@ -1843,9 +2425,10 @@ public:
|
||||
#endif
|
||||
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
// Poll web server frequently during OTA waiting/receiving phases
|
||||
if (_editMode == EDIT_OTA &&
|
||||
(_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) {
|
||||
// Poll web server frequently during OTA waiting/receiving or file manager phases
|
||||
if ((_editMode == EDIT_OTA &&
|
||||
(_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) ||
|
||||
(_editMode == EDIT_FILEMGR && _fmPhase == FM_PHASE_WAITING)) {
|
||||
return 200; // 200ms — fast enough for web server responsiveness
|
||||
}
|
||||
#endif
|
||||
@@ -1912,6 +2495,32 @@ public:
|
||||
// Consume all keys during OTA
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- File Manager flow ---
|
||||
if (_editMode == EDIT_FILEMGR) {
|
||||
if (_fmPhase == FM_PHASE_CONFIRM) {
|
||||
if (c == '\r' || c == 13) {
|
||||
startFileMgrServer();
|
||||
return true;
|
||||
}
|
||||
if (c == 'q' || c == 'Q') {
|
||||
_editMode = EDIT_NONE;
|
||||
return true;
|
||||
}
|
||||
} else if (_fmPhase == FM_PHASE_WAITING) {
|
||||
if (c == 'q' || c == 'Q') {
|
||||
stopFileMgr();
|
||||
return true;
|
||||
}
|
||||
} else if (_fmPhase == FM_PHASE_ERROR) {
|
||||
if (c == 'q' || c == 'Q') {
|
||||
stopFileMgr();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Consume all keys during file manager
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef MECK_WIFI_COMPANION
|
||||
@@ -2484,9 +3093,20 @@ public:
|
||||
startEditText("");
|
||||
break;
|
||||
#ifdef MECK_OTA_UPDATE
|
||||
case ROW_OTA_TOOLS_SUBMENU:
|
||||
_savedTopCursor = _cursor;
|
||||
_subScreen = SUB_OTA_TOOLS;
|
||||
_cursor = 0;
|
||||
_scrollTop = 0;
|
||||
rebuildRows();
|
||||
Serial.println("Settings: entered OTA Tools sub-screen");
|
||||
break;
|
||||
case ROW_FW_UPDATE:
|
||||
startOTA();
|
||||
break;
|
||||
case ROW_SD_FILE_MGR:
|
||||
startFileMgr();
|
||||
break;
|
||||
#endif
|
||||
case ROW_CHANNEL:
|
||||
case ROW_PUB_KEY:
|
||||
|
||||
@@ -1403,6 +1403,107 @@ public:
|
||||
// Called from setup() after SD card init. Scans files, pre-indexes first
|
||||
// 100 pages of each, and shows progress on the e-ink display.
|
||||
|
||||
// Pre-index files inside one level of subdirectories so navigating
|
||||
// into them later is instant (idx files already on SD).
|
||||
void bootIndexSubfolders() {
|
||||
// Work from the root-level _dirList that scanFiles() already populated.
|
||||
// Copy it -- scanFiles() will overwrite _dirList when we scan each subfolder.
|
||||
std::vector<String> subDirs = _dirList;
|
||||
if (subDirs.empty()) return;
|
||||
|
||||
Serial.printf("TextReader: Pre-indexing %d subfolders\n", (int)subDirs.size());
|
||||
|
||||
int totalSubFiles = 0;
|
||||
int cachedSubFiles = 0;
|
||||
int indexedSubFiles = 0;
|
||||
|
||||
for (int d = 0; d < (int)subDirs.size(); d++) {
|
||||
String subPath = String(BOOKS_FOLDER) + "/" + subDirs[d];
|
||||
_currentPath = subPath;
|
||||
scanFiles(); // populates _fileList for this subfolder
|
||||
|
||||
// Also pick up previously converted EPUB cache files for this subfolder
|
||||
String epubCachePath = subPath + "/.epub_cache";
|
||||
if (SD.exists(epubCachePath.c_str())) {
|
||||
File cacheDir = SD.open(epubCachePath.c_str());
|
||||
if (cacheDir && cacheDir.isDirectory()) {
|
||||
File cf = cacheDir.openNextFile();
|
||||
while (cf && _fileList.size() < READER_MAX_FILES) {
|
||||
if (!cf.isDirectory()) {
|
||||
String cname = String(cf.name());
|
||||
int cslash = cname.lastIndexOf('/');
|
||||
if (cslash >= 0) cname = cname.substring(cslash + 1);
|
||||
if (cname.endsWith(".txt") || cname.endsWith(".TXT")) {
|
||||
bool dup = false;
|
||||
for (int k = 0; k < (int)_fileList.size(); k++) {
|
||||
if (_fileList[k] == cname) { dup = true; break; }
|
||||
}
|
||||
if (!dup) _fileList.push_back(cname);
|
||||
}
|
||||
}
|
||||
cf = cacheDir.openNextFile();
|
||||
}
|
||||
cacheDir.close();
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < (int)_fileList.size(); i++) {
|
||||
totalSubFiles++;
|
||||
|
||||
// Try loading existing .idx cache -- if hit, skip
|
||||
FileCache tempCache;
|
||||
if (loadIndex(_fileList[i], tempCache)) {
|
||||
cachedSubFiles++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip .epub files (converted on first open)
|
||||
if (_fileList[i].endsWith(".epub") || _fileList[i].endsWith(".EPUB")) continue;
|
||||
|
||||
// Index this .txt file
|
||||
String fullPath = _currentPath + "/" + _fileList[i];
|
||||
File file = SD.open(fullPath.c_str(), FILE_READ);
|
||||
if (!file) {
|
||||
// Try epub cache fallback
|
||||
String cacheFallback = epubCachePath + "/" + _fileList[i];
|
||||
file = SD.open(cacheFallback.c_str(), FILE_READ);
|
||||
}
|
||||
if (!file) continue;
|
||||
|
||||
indexedSubFiles++;
|
||||
String displayName = subDirs[d] + "/" + _fileList[i];
|
||||
drawBootSplash(indexedSubFiles, 0, displayName);
|
||||
|
||||
FileCache cache;
|
||||
cache.filename = _fileList[i];
|
||||
cache.fileSize = file.size();
|
||||
cache.fullyIndexed = false;
|
||||
cache.lastReadPage = 0;
|
||||
cache.pagePositions.clear();
|
||||
cache.pagePositions.push_back(0);
|
||||
|
||||
indexPagesWordWrap(file, 0, cache.pagePositions,
|
||||
_linesPerPage, _charsPerLine,
|
||||
PREINDEX_PAGES - 1,
|
||||
_textAreaHeight, _lineHeight);
|
||||
cache.fullyIndexed = !file.available();
|
||||
file.close();
|
||||
|
||||
saveIndex(cache.filename, cache.pagePositions, cache.fileSize,
|
||||
cache.fullyIndexed, 0);
|
||||
|
||||
Serial.printf("TextReader: %s/%s - indexed %d pages%s\n",
|
||||
subDirs[d].c_str(), _fileList[i].c_str(),
|
||||
(int)cache.pagePositions.size(),
|
||||
cache.fullyIndexed ? " (complete)" : "");
|
||||
yield(); // Feed WDT between files
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("TextReader: Subfolder pre-index: %d files (%d cached, %d newly indexed)\n",
|
||||
totalSubFiles, cachedSubFiles, indexedSubFiles);
|
||||
}
|
||||
|
||||
void bootIndex(DisplayDriver& display) {
|
||||
if (!_sdReady) return;
|
||||
|
||||
@@ -1444,20 +1545,24 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
if (_fileList.size() == 0) {
|
||||
Serial.println("TextReader: No files to index");
|
||||
if (_fileList.size() == 0 && _dirList.size() == 0) {
|
||||
Serial.println("TextReader: No files or folders to index");
|
||||
_bootIndexed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
int cachedCount = 0;
|
||||
int needsIndexCount = 0;
|
||||
|
||||
// --- Pass 1 & 2: Index root-level files ---
|
||||
if (_fileList.size() > 0) {
|
||||
|
||||
// --- Pass 1: Fast cache load (no per-file splash screens) ---
|
||||
// Try to load existing .idx files from SD for every file.
|
||||
// This is just SD reads — no indexing, no e-ink refreshes.
|
||||
_fileCache.clear();
|
||||
_fileCache.resize(_fileList.size()); // Pre-allocate slots to maintain alignment with _fileList
|
||||
|
||||
int cachedCount = 0;
|
||||
int needsIndexCount = 0;
|
||||
|
||||
for (int i = 0; i < (int)_fileList.size(); i++) {
|
||||
if (loadIndex(_fileList[i], _fileCache[i])) {
|
||||
@@ -1523,6 +1628,26 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
} // end if (_fileList.size() > 0)
|
||||
|
||||
// --- Pass 3: Pre-index files inside subfolders (one level deep) ---
|
||||
// Save root state -- bootIndexSubfolders() will overwrite _fileList/_dirList
|
||||
// via scanFiles() as it iterates each subdirectory.
|
||||
if (_dirList.size() > 0) {
|
||||
std::vector<String> savedFileList = _fileList;
|
||||
std::vector<String> savedDirList = _dirList;
|
||||
std::vector<FileCache> savedFileCache = _fileCache;
|
||||
|
||||
bootIndexSubfolders();
|
||||
|
||||
// Restore root state
|
||||
_currentPath = String(BOOKS_FOLDER);
|
||||
_fileList = savedFileList;
|
||||
_dirList = savedDirList;
|
||||
_fileCache = savedFileCache;
|
||||
}
|
||||
|
||||
|
||||
// Deselect SD to free SPI bus
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
|
||||
|
||||
@@ -52,10 +52,28 @@ void Dispatcher::loop() {
|
||||
prev_isrecv_mode = is_recv;
|
||||
if (!is_recv) {
|
||||
radio_nonrx_start = _ms->getMillis();
|
||||
} else {
|
||||
rx_stuck_count = 0; // radio recovered — reset counter
|
||||
}
|
||||
}
|
||||
if (!is_recv && _ms->getMillis() - radio_nonrx_start > 8000) { // radio has not been in Rx mode for 8 seconds!
|
||||
_err_flags |= ERR_EVENT_STARTRX_TIMEOUT;
|
||||
|
||||
rx_stuck_count++;
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): RX stuck (attempt %d), calling onRxStuck()", getLogDateTime(), rx_stuck_count);
|
||||
onRxStuck();
|
||||
|
||||
uint8_t reboot_threshold = getRxFailRebootThreshold();
|
||||
if (reboot_threshold > 0 && rx_stuck_count >= reboot_threshold) {
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): RX unrecoverable after %d attempts", getLogDateTime(), rx_stuck_count);
|
||||
onRxUnrecoverable();
|
||||
}
|
||||
|
||||
// Reset state to give recovery the full 8s window before re-triggering
|
||||
radio_nonrx_start = _ms->getMillis();
|
||||
prev_isrecv_mode = true;
|
||||
cad_busy_start = 0;
|
||||
next_agc_reset_time = futureMillis(getAGCResetInterval());
|
||||
}
|
||||
|
||||
if (outbound) { // waiting for outbound send to be completed
|
||||
@@ -277,14 +295,27 @@ void Dispatcher::checkSend() {
|
||||
|
||||
logTxFail(outbound, outbound->getRawLength());
|
||||
|
||||
// re-queue instead of dropping so the packet gets another chance
|
||||
// re-queue packet for retry instead of dropping it
|
||||
int retry_delay = getCADFailRetryDelay();
|
||||
unsigned long retry_time = futureMillis(retry_delay);
|
||||
_mgr->queueOutbound(outbound, 0, retry_time);
|
||||
outbound = NULL;
|
||||
next_tx_time = retry_time;
|
||||
|
||||
// count consecutive failures and reset radio if stuck
|
||||
uint8_t threshold = getTxFailResetThreshold();
|
||||
if (threshold > 0) {
|
||||
tx_fail_count++;
|
||||
if (tx_fail_count >= threshold) {
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): TX stuck (%d failures), resetting radio", getLogDateTime(), tx_fail_count);
|
||||
onTxStuck();
|
||||
tx_fail_count = 0;
|
||||
next_tx_time = futureMillis(2000);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
tx_fail_count = 0; // clear counter on successful TX start
|
||||
outbound_expiry = futureMillis(max_airtime);
|
||||
|
||||
#if MESH_PACKET_LOGGING
|
||||
|
||||
@@ -122,6 +122,8 @@ class Dispatcher {
|
||||
bool prev_isrecv_mode;
|
||||
uint32_t n_sent_flood, n_sent_direct;
|
||||
uint32_t n_recv_flood, n_recv_direct;
|
||||
uint8_t tx_fail_count;
|
||||
uint8_t rx_stuck_count;
|
||||
|
||||
void processRecvPacket(Packet* pkt);
|
||||
|
||||
@@ -142,6 +144,8 @@ protected:
|
||||
_err_flags = 0;
|
||||
radio_nonrx_start = 0;
|
||||
prev_isrecv_mode = true;
|
||||
tx_fail_count = 0;
|
||||
rx_stuck_count = 0;
|
||||
}
|
||||
|
||||
virtual DispatcherAction onRecvPacket(Packet* pkt) = 0;
|
||||
@@ -159,6 +163,11 @@ protected:
|
||||
virtual uint32_t getCADFailMaxDuration() const;
|
||||
virtual int getInterferenceThreshold() const { return 0; } // disabled by default
|
||||
virtual int getAGCResetInterval() const { return 0; } // disabled by default
|
||||
virtual uint8_t getTxFailResetThreshold() const { return 3; } // reset radio after N consecutive TX failures; 0=disabled
|
||||
virtual void onTxStuck() { _radio->resetAGC(); } // override to use doFullRadioReset() when available
|
||||
virtual uint8_t getRxFailRebootThreshold() const { return 3; } // reboot after N failed RX recovery attempts; 0=disabled
|
||||
virtual void onRxStuck() { _radio->resetAGC(); } // called each time RX stuck for 8s; override for deeper reset
|
||||
virtual void onRxUnrecoverable() { } // called when reboot threshold exceeded; override to call _board->reboot()
|
||||
|
||||
public:
|
||||
void begin();
|
||||
@@ -188,4 +197,4 @@ private:
|
||||
void checkSend();
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define BLE_WRITE_MIN_INTERVAL 60
|
||||
#define BLE_WRITE_MIN_INTERVAL 30
|
||||
|
||||
bool SerialBLEInterface::isWriteBusy() const {
|
||||
return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write?
|
||||
|
||||
@@ -23,7 +23,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
|
||||
uint8_t buf[MAX_FRAME_SIZE];
|
||||
};
|
||||
|
||||
#define FRAME_QUEUE_SIZE 4
|
||||
#define FRAME_QUEUE_SIZE 8
|
||||
int recv_queue_len;
|
||||
Frame recv_queue[FRAME_QUEUE_SIZE];
|
||||
int send_queue_len;
|
||||
|
||||
@@ -64,6 +64,7 @@ build_src_filter = ${esp32_base.build_src_filter}
|
||||
lib_deps =
|
||||
${esp32_base.lib_deps}
|
||||
WebServer
|
||||
DNSServer
|
||||
Update
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -97,6 +97,7 @@ lib_deps =
|
||||
adafruit/Adafruit GFX Library@^1.11.0
|
||||
bitbank2/PNGdec@^1.0.1
|
||||
WebServer
|
||||
DNSServer
|
||||
Update
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
@@ -150,7 +151,7 @@ build_flags =
|
||||
-D MECK_AUDIO_VARIANT
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.3.WiFi"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.5.WiFi"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -203,7 +204,7 @@ build_flags =
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.3.4G"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.5.4G"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -234,7 +235,7 @@ build_flags =
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.3.4G.WiFi"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.5.4G.WiFi"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -261,7 +262,7 @@ build_flags =
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.3.4G.SA"'
|
||||
-D FIRMWARE_VERSION='"Meck v1.5.4G.SA"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
|
||||
@@ -1,347 +0,0 @@
|
||||
#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
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
#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);
|
||||
};
|
||||
@@ -1,360 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
};
|
||||
@@ -1,232 +0,0 @@
|
||||
; =============================================================================
|
||||
; 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
|
||||
@@ -1,91 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
#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();
|
||||
@@ -1,301 +0,0 @@
|
||||
#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