tdpro ota update firmware functionality implemented; roomserver ui sender name display fix and speed up delivery time

This commit is contained in:
pelgraine
2026-03-22 13:16:25 +11:00
parent 8839012153
commit bad821ac4b
6 changed files with 663 additions and 42 deletions

View File

@@ -498,7 +498,24 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
if (should_display && _ui) {
const uint8_t* msg_path = (pkt->isRouteFlood() && pkt->path_len > 0) ? pkt->path : nullptr;
_ui->newMsg(path_len, from.name, text, offline_queue_len, msg_path, pkt->_snr);
// For signed messages (room server posts): the extra bytes contain the
// original poster's pub_key prefix. Look up their name and format as
// "PosterName: message" so the UI shows who actually wrote it.
if (txt_type == TXT_TYPE_SIGNED_PLAIN && extra && extra_len >= 4) {
ContactInfo* poster = lookupContactByPubKey(extra, extra_len);
if (poster) {
char formatted[MAX_PACKET_PAYLOAD];
snprintf(formatted, sizeof(formatted), "%s: %s", poster->name, text);
_ui->newMsg(path_len, from.name, formatted, offline_queue_len, msg_path, pkt->_snr);
} else {
// Poster not in contacts — show raw text (no name prefix)
_ui->newMsg(path_len, from.name, text, offline_queue_len, msg_path, pkt->_snr);
}
} else {
_ui->newMsg(path_len, from.name, text, offline_queue_len, msg_path, pkt->_snr);
}
if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::contactMessage); //buzz if enabled
}
#endif

View File

@@ -2,6 +2,9 @@
#ifdef BLE_PIN_CODE
#include <esp_bt.h> // for esp_bt_controller_mem_release (web reader WiFi)
#endif
#ifdef MECK_OTA_UPDATE
#include <esp_ota_ops.h>
#endif
#include <Mesh.h>
#include "MyMesh.h"
#include "variant.h" // Board-specific defines (HAS_GPS, etc.)
@@ -1423,6 +1426,28 @@ void setup() {
MESH_DEBUG_PRINTLN("setup() - ui_task.begin() done");
#endif
// ---------------------------------------------------------------------------
// OTA boot validation — confirm new firmware is working after an OTA update.
// If we reach this point, display + radio + SD + mesh all initialised OK.
// Without this call (when CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is set),
// the bootloader will roll back to the previous partition on next reboot.
// ---------------------------------------------------------------------------
#ifdef MECK_OTA_UPDATE
{
const esp_partition_t* running = esp_ota_get_running_partition();
esp_ota_img_states_t ota_state;
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
if (esp_ota_mark_app_valid_cancel_rollback() == ESP_OK) {
Serial.println("OTA: New firmware validated, rollback cancelled");
} else {
Serial.println("OTA: WARNING - failed to cancel rollback");
}
}
}
}
#endif
// Initialize T-Deck Pro keyboard
#if defined(LilyGo_TDeck_Pro)
initKeyboard();

View File

@@ -557,6 +557,7 @@ public:
if (_viewChannelIdx == 0xFF && _dmInboxMode) {
#define DM_INBOX_MAX 20
struct DMInboxEntry {
uint32_t hash;
char name[32];
int msgCount;
int unreadCount;
@@ -565,28 +566,43 @@ public:
DMInboxEntry inbox[DM_INBOX_MAX];
int inboxCount = 0;
// Scan all DMs and build unique sender list
// Scan all DMs and group by peer hash
for (int i = 0; i < _msgCount && i < CHANNEL_MSG_HISTORY_SIZE; i++) {
int idx = _newestIdx - i;
while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
idx = idx % CHANNEL_MSG_HISTORY_SIZE;
if (!_messages[idx].valid || _messages[idx].channel_idx != 0xFF) continue;
if (_messages[idx].dm_peer_hash == 0) continue;
char sender[32];
if (!extractSenderName(_messages[idx].text, sender, sizeof(sender))) continue;
uint32_t h = _messages[idx].dm_peer_hash;
// Find existing entry
// Find existing entry by hash
int found = -1;
for (int j = 0; j < inboxCount; j++) {
if (strcmp(inbox[j].name, sender) == 0) { found = j; break; }
if (inbox[j].hash == h) { found = j; break; }
}
if (found < 0 && inboxCount < DM_INBOX_MAX) {
found = inboxCount++;
strncpy(inbox[found].name, sender, 31);
inbox[found].name[31] = '\0';
inbox[found].hash = h;
inbox[found].name[0] = '\0';
inbox[found].msgCount = 0;
inbox[found].unreadCount = 0;
inbox[found].newestTs = 0;
// Look up name from contacts by matching peer hash
uint32_t numC = the_mesh.getNumContacts();
ContactInfo ci;
for (uint32_t c = 0; c < numC; c++) {
if (the_mesh.getContactByIdx(c, ci) && peerHash(ci.name) == h) {
strncpy(inbox[found].name, ci.name, 31);
inbox[found].name[31] = '\0';
break;
}
}
// Fallback: extract from text if contact not found
if (inbox[found].name[0] == '\0') {
extractSenderName(_messages[idx].text, inbox[found].name, sizeof(inbox[found].name));
}
}
if (found >= 0) {
inbox[found].msgCount++;
@@ -601,7 +617,7 @@ public:
uint32_t numC = the_mesh.getNumContacts();
ContactInfo ci;
for (uint32_t c = 0; c < numC; c++) {
if (the_mesh.getContactByIdx(c, ci) && strcmp(ci.name, inbox[e].name) == 0) {
if (the_mesh.getContactByIdx(c, ci) && peerHash(ci.name) == inbox[e].hash) {
inbox[e].unreadCount = _dmUnreadPtr[c];
break;
}
@@ -1506,45 +1522,47 @@ public:
_dmInboxScroll++; // Clamped during render
return true;
}
// Enter - open conversation for selected sender
// Enter - open conversation for selected entry
if (c == '\r' || c == 13) {
// Rebuild inbox to find the selected sender name
// Rebuild inbox by hash to find the selected entry
uint32_t seenHash[DM_INBOX_MAX];
int cur = 0;
for (int i = 0; i < _msgCount; i++) {
int idx = _newestIdx - i;
while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
idx = idx % CHANNEL_MSG_HISTORY_SIZE;
if (!_messages[idx].valid || _messages[idx].channel_idx != 0xFF) continue;
char sender[24];
if (!extractSenderName(_messages[idx].text, sender, sizeof(sender))) continue;
// Check if we've seen this sender already
if (_messages[idx].dm_peer_hash == 0) continue;
uint32_t h = _messages[idx].dm_peer_hash;
bool dup = false;
for (int k = 0; k < i; k++) {
int ki = _newestIdx - k;
while (ki < 0) ki += CHANNEL_MSG_HISTORY_SIZE;
ki = ki % CHANNEL_MSG_HISTORY_SIZE;
if (!_messages[ki].valid || _messages[ki].channel_idx != 0xFF) continue;
char prev[24];
if (extractSenderName(_messages[ki].text, prev, sizeof(prev))
&& strcmp(prev, sender) == 0) { dup = true; break; }
for (int k = 0; k < cur; k++) {
if (seenHash[k] == h) { dup = true; break; }
}
if (dup) continue;
if (cur < DM_INBOX_MAX) seenHash[cur] = h;
if (cur == _dmInboxScroll) {
strncpy(_dmFilterName, sender, sizeof(_dmFilterName) - 1);
_dmFilterName[sizeof(_dmFilterName) - 1] = '\0';
_dmInboxMode = false;
_scrollPos = 0;
// Look up contact index
// Found the selected entry — look up name from contacts
_dmFilterName[0] = '\0';
_dmContactIdx = -1;
_dmContactPerms = 0;
uint32_t numC = the_mesh.getNumContacts();
ContactInfo ci;
for (uint32_t c = 0; c < numC; c++) {
if (the_mesh.getContactByIdx(c, ci) && strcmp(ci.name, sender) == 0) {
_dmContactIdx = (int)c;
for (uint32_t c2 = 0; c2 < numC; c2++) {
if (the_mesh.getContactByIdx(c2, ci) && peerHash(ci.name) == h) {
strncpy(_dmFilterName, ci.name, sizeof(_dmFilterName) - 1);
_dmFilterName[sizeof(_dmFilterName) - 1] = '\0';
_dmContactIdx = (int)c2;
break;
}
}
// Fallback to text extraction if contact not found
if (_dmFilterName[0] == '\0') {
extractSenderName(_messages[idx].text, _dmFilterName, sizeof(_dmFilterName));
}
_dmInboxMode = false;
_scrollPos = 0;
return true;
}
cur++;

View File

@@ -22,6 +22,16 @@
#include <SD.h>
#endif
#ifdef MECK_OTA_UPDATE
#ifndef MECK_WIFI_COMPANION
#include <WiFi.h>
#include <SD.h>
#endif
#include <WebServer.h>
#include <Update.h>
#include <esp_ota_ops.h>
#endif
// Forward declarations
class UITask;
class MyMesh;
@@ -131,6 +141,9 @@ enum SettingsRowType : uint8_t {
ROW_CHANNEL, // A channel entry (dynamic, index stored separately)
ROW_ADD_CHANNEL, // "+ Add Hashtag Channel"
ROW_INFO_HEADER, // "--- Info ---" separator
#ifdef MECK_OTA_UPDATE
ROW_FW_UPDATE, // "Firmware Update" — WiFi upload + flash
#endif
ROW_PUB_KEY, // Public key display
ROW_FIRMWARE, // Firmware version
#ifdef HAS_4G_MODEM
@@ -152,6 +165,9 @@ enum EditMode : uint8_t {
#ifdef MECK_WIFI_COMPANION
EDIT_WIFI, // WiFi scan/select/password flow
#endif
#ifdef MECK_OTA_UPDATE
EDIT_OTA, // OTA firmware update flow (multi-phase overlay)
#endif
};
// ---------------------------------------------------------------------------
@@ -163,6 +179,20 @@ enum SubScreen : uint8_t {
SUB_CHANNELS, // Channels management sub-screen
};
#ifdef MECK_OTA_UPDATE
// OTA update phases
enum OtaPhase : uint8_t {
OTA_PHASE_CONFIRM, // "Start firmware update? Enter:Yes Q:No"
OTA_PHASE_AP_START, // Starting WiFi AP + web server
OTA_PHASE_WAITING, // AP up, waiting for device to upload
OTA_PHASE_RECEIVING, // File upload in progress
OTA_PHASE_VERIFY, // Checking downloaded file
OTA_PHASE_FLASH, // Writing to flash — DO NOT POWER OFF
OTA_PHASE_DONE, // Success, rebooting
OTA_PHASE_ERROR, // Error with message
};
#endif
// Max rows in the settings list (increased for contact sub-toggles + WiFi)
#if defined(HAS_4G_MODEM) && defined(MECK_WIFI_COMPANION)
#define SETTINGS_MAX_ROWS 56 // Extra rows for IMEI, Carrier, APN, contacts, WiFi
@@ -238,6 +268,17 @@ private:
#endif
#endif
#ifdef MECK_OTA_UPDATE
// OTA update state
OtaPhase _otaPhase;
WebServer* _otaServer;
File _otaFile;
size_t _otaBytesReceived;
bool _otaUploadOk;
char _otaApName[24];
const char* _otaError;
#endif
// ---------------------------------------------------------------------------
// Contact mode helpers
// ---------------------------------------------------------------------------
@@ -351,6 +392,9 @@ private:
// 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);
@@ -503,6 +547,13 @@ public:
_onboarding(false), _subScreen(SUB_NONE), _savedTopCursor(0),
_radioChanged(false) {
memset(_editBuf, 0, sizeof(_editBuf));
#ifdef MECK_OTA_UPDATE
_otaServer = nullptr;
_otaPhase = OTA_PHASE_CONFIRM;
_otaBytesReceived = 0;
_otaUploadOk = false;
_otaError = nullptr;
#endif
}
void enter() {
@@ -689,6 +740,301 @@ public:
#endif
// ---------------------------------------------------------------------------
// OTA firmware update
// ---------------------------------------------------------------------------
#ifdef MECK_OTA_UPDATE
// HTML upload page served to the browser
static const char* otaUploadPageHTML() {
return
"<!DOCTYPE html><html><head>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>Meck Firmware Update</title>"
"<style>"
"body{font-family:-apple-system,sans-serif;max-width:480px;margin:40px auto;"
"padding:0 20px;background:#1a1a2e;color:#e0e0e0}"
"h1{color:#4ecca3;font-size:1.4em}"
".info{background:#16213e;padding:12px;border-radius:8px;margin:16px 0;font-size:0.9em}"
"input[type=file]{margin:16px 0;color:#e0e0e0}"
"button{background:#4ecca3;color:#1a1a2e;border:none;padding:12px 32px;"
"border-radius:6px;font-size:1.1em;font-weight:bold;cursor:pointer}"
"button:active{background:#3ba88f}"
"#prog{display:none;margin-top:16px}"
".bar{background:#16213e;border-radius:4px;height:24px;overflow:hidden}"
".fill{background:#4ecca3;height:100%;width:0%;transition:width 0.3s}"
"</style></head><body>"
"<h1>Meck Firmware Update</h1>"
"<div class='info'>Select the firmware .bin file and tap Upload. "
"The device will verify and flash it automatically.</div>"
"<form method='POST' action='/upload' enctype='multipart/form-data'>"
"<input type='file' name='firmware' accept='.bin'><br>"
"<button type='submit' onclick=\"document.getElementById('prog').style.display='block'\">"
"Upload Firmware</button></form>"
"<div id='prog'><div>Uploading... do not close this page</div>"
"<div class='bar'><div class='fill' id='fill'></div></div></div>"
"<script>document.querySelector('form').onsubmit=function(){"
"var f=document.getElementById('fill'),w=0;"
"setInterval(function(){w+=2;if(w>90)w=90;f.style.width=w+'%'},500)};</script>"
"</body></html>";
}
void startOTA() {
_editMode = EDIT_OTA;
_otaPhase = OTA_PHASE_CONFIRM;
_otaBytesReceived = 0;
_otaUploadOk = false;
_otaError = nullptr;
}
void startOTAServer() {
// Build AP name with last 4 of MAC for uniqueness
uint8_t mac[6];
WiFi.macAddress(mac);
snprintf(_otaApName, sizeof(_otaApName), "Meck-Update-%02X%02X", mac[4], mac[5]);
// Tear down existing WiFi and start AP
WiFi.disconnect(true);
delay(100);
WiFi.mode(WIFI_AP);
WiFi.softAP(_otaApName);
delay(500); // Let AP stabilise
Serial.printf("OTA: AP '%s' started, IP: %s\n",
_otaApName, WiFi.softAPIP().toString().c_str());
// Start web server
if (_otaServer) { _otaServer->stop(); delete _otaServer; }
_otaServer = new WebServer(80);
_otaServer->on("/", HTTP_GET, [this]() {
_otaServer->send(200, "text/html", otaUploadPageHTML());
});
_otaServer->on("/upload", HTTP_POST,
// Response after upload completes
[this]() {
_otaServer->send(200, "text/html",
_otaUploadOk
? "<html><body style='background:#1a1a2e;color:#4ecca3;font-family:sans-serif;"
"text-align:center;padding:60px'><h1>Upload OK!</h1>"
"<p>The device is now verifying and flashing.<br>It will reboot automatically.</p></body></html>"
: "<html><body style='background:#1a1a2e;color:#e74c3c;font-family:sans-serif;"
"text-align:center;padding:60px'><h1>Upload Failed</h1>"
"<p>Please try again.</p></body></html>"
);
},
// Upload handler — called per chunk
[this]() {
HTTPUpload& upload = _otaServer->upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("OTA: Receiving: %s\n", upload.filename.c_str());
_otaUploadOk = false;
_otaBytesReceived = 0;
if (!SD.exists("/firmware")) SD.mkdir("/firmware");
if (SD.exists("/firmware/update.bin")) {
SD.remove("/firmware/previous.bin");
SD.rename("/firmware/update.bin", "/firmware/previous.bin");
}
_otaFile = SD.open("/firmware/update.bin", FILE_WRITE);
if (!_otaFile) {
Serial.println("OTA: Failed to open SD file");
return;
}
_otaPhase = OTA_PHASE_RECEIVING;
} else if (upload.status == UPLOAD_FILE_WRITE) {
if (_otaFile) {
_otaFile.write(upload.buf, upload.currentSize);
_otaBytesReceived += upload.currentSize;
}
} else if (upload.status == UPLOAD_FILE_END) {
if (_otaFile) {
_otaFile.close();
digitalWrite(SDCARD_CS, HIGH);
Serial.printf("OTA: Received %d bytes\n", _otaBytesReceived);
_otaUploadOk = (_otaBytesReceived > 0);
}
} else if (upload.status == UPLOAD_FILE_ABORTED) {
if (_otaFile) { _otaFile.close(); SD.remove("/firmware/update.bin"); }
digitalWrite(SDCARD_CS, HIGH);
Serial.println("OTA: Upload aborted");
}
}
);
_otaServer->begin();
Serial.println("OTA: Web server started on port 80");
_otaPhase = OTA_PHASE_WAITING;
}
void stopOTA() {
if (_otaServer) { _otaServer->stop(); delete _otaServer; _otaServer = nullptr; }
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_OFF);
delay(100);
_editMode = EDIT_NONE;
// Try to restore STA WiFi from saved credentials
#ifdef MECK_WIFI_COMPANION
WiFi.mode(WIFI_STA);
wifiReconnectSaved();
#endif
Serial.println("OTA: Stopped, AP down");
}
bool verifyFirmwareFile() {
File f = SD.open("/firmware/update.bin", FILE_READ);
if (!f) { _otaError = "File not found on SD"; return false; }
size_t fileSize = f.size();
if (fileSize < 500000 || fileSize > 6500000) {
f.close(); digitalWrite(SDCARD_CS, HIGH);
_otaError = "Bad file size (need 0.5-6MB)";
Serial.printf("OTA: Bad file size: %d\n", fileSize);
return false;
}
// Check ESP32 image magic byte
uint8_t magic;
f.read(&magic, 1);
f.close();
digitalWrite(SDCARD_CS, HIGH);
if (magic != 0xE9) {
_otaError = "Not a firmware file (bad magic)";
Serial.printf("OTA: Bad magic: 0x%02X\n", magic);
return false;
}
return true;
}
bool flashFirmwareFromSD(DisplayDriver& display) {
File firmware = SD.open("/firmware/update.bin", FILE_READ);
if (!firmware) { _otaError = "Cannot open firmware file"; return false; }
size_t fileSize = firmware.size();
if (!Update.begin(fileSize, U_FLASH)) {
_otaError = Update.errorString();
Serial.printf("OTA: Update.begin failed: %s\n", _otaError);
firmware.close();
return false;
}
const int BUF_SIZE = 4096;
uint8_t* buf = (uint8_t*)ps_malloc(BUF_SIZE);
if (!buf) buf = (uint8_t*)malloc(BUF_SIZE);
if (!buf) { firmware.close(); Update.abort(); _otaError = "Out of memory"; return false; }
size_t totalWritten = 0;
char tmp[48];
while (firmware.available()) {
int bytesRead = firmware.read(buf, BUF_SIZE);
if (bytesRead <= 0) break;
size_t written = Update.write(buf, bytesRead);
if (written != (size_t)bytesRead) {
_otaError = "Flash write error";
Serial.printf("OTA: Write error at %d bytes\n", totalWritten);
break;
}
totalWritten += written;
// Update e-ink progress every ~128KB
if (totalWritten % 131072 < (size_t)BUF_SIZE) {
display.startFrame();
display.setColor(DisplayDriver::DARK);
display.fillRect(2, 14, display.width() - 4, display.height() - 28);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(2, 14, display.width() - 4, display.height() - 28);
display.setTextSize(0);
display.drawTextCentered(display.width() / 2, 22, "Flashing Firmware");
snprintf(tmp, sizeof(tmp), "%d / %d KB", (int)(totalWritten / 1024), (int)(fileSize / 1024));
display.drawTextCentered(display.width() / 2, 42, tmp);
display.setColor(DisplayDriver::YELLOW);
display.drawTextCentered(display.width() / 2, 62, "DO NOT POWER OFF");
display.endFrame();
}
}
free(buf);
firmware.close();
digitalWrite(SDCARD_CS, HIGH);
if (!Update.end(true)) {
_otaError = Update.errorString();
Serial.printf("OTA: Update.end failed: %s\n", _otaError);
return false;
}
Serial.printf("OTA: Flash success! %d bytes written\n", totalWritten);
return true;
}
// Called from render loop to poll the web server
void pollOTAServer() {
if (_otaServer && (_otaPhase == OTA_PHASE_WAITING || _otaPhase == OTA_PHASE_RECEIVING)) {
_otaServer->handleClient();
}
}
// Run the verify → flash → reboot sequence after upload completes
void processOTAUpload(DisplayDriver& display) {
// Stop web server and AP first
if (_otaServer) { _otaServer->stop(); delete _otaServer; _otaServer = nullptr; }
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_OFF);
_otaPhase = OTA_PHASE_VERIFY;
if (!verifyFirmwareFile()) {
_otaPhase = OTA_PHASE_ERROR;
return;
}
_otaPhase = OTA_PHASE_FLASH;
// Backup settings before flashing (preserves identity/contacts across updates)
extern void backupSettingsToSD();
backupSettingsToSD();
if (!flashFirmwareFromSD(display)) {
_otaPhase = OTA_PHASE_ERROR;
return;
}
_otaPhase = OTA_PHASE_DONE;
// Show success screen then reboot
display.startFrame();
display.setColor(DisplayDriver::DARK);
display.fillRect(2, 14, display.width() - 4, display.height() - 28);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(2, 14, display.width() - 4, display.height() - 28);
display.setTextSize(0);
display.setColor(DisplayDriver::GREEN);
display.drawTextCentered(display.width() / 2, 30, "Update Complete!");
display.setColor(DisplayDriver::LIGHT);
File fw = SD.open("/firmware/update.bin", FILE_READ);
char tmp[48];
if (fw) {
snprintf(tmp, sizeof(tmp), "Firmware: %d KB", (int)(fw.size() / 1024));
fw.close(); digitalWrite(SDCARD_CS, HIGH);
} else {
strcpy(tmp, "Firmware written");
}
display.drawTextCentered(display.width() / 2, 48, tmp);
display.drawTextCentered(display.width() / 2, 66, "Rebooting in 3 seconds...");
display.endFrame();
delay(3000);
ESP.restart();
}
#endif
// ---------------------------------------------------------------------------
// Edit mode starters
// ---------------------------------------------------------------------------
@@ -1050,6 +1396,12 @@ public:
display.print(tmp);
break;
#ifdef MECK_OTA_UPDATE
case ROW_FW_UPDATE:
display.print("Firmware Update");
break;
#endif
case ROW_INFO_HEADER:
display.setColor(DisplayDriver::YELLOW);
display.print("--- Device Info ---");
@@ -1234,6 +1586,105 @@ public:
}
#endif
#ifdef MECK_OTA_UPDATE
// === OTA update overlay ===
if (_editMode == EDIT_OTA) {
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(0);
int oy = by + 4;
// Detect upload completion — trigger verify + flash sequence
if (_otaUploadOk && (_otaPhase == OTA_PHASE_RECEIVING || _otaPhase == OTA_PHASE_WAITING)) {
display.endFrame(); // Flush current frame before blocking flash
processOTAUpload(display);
return 500; // Won't reach here if flash succeeds (reboots)
}
if (_otaPhase == OTA_PHASE_CONFIRM) {
display.drawTextCentered(display.width() / 2, oy, "Firmware Update");
oy += 14;
display.setCursor(bx + 4, oy);
display.print("Start WiFi upload server?");
oy += 10;
display.setCursor(bx + 4, oy);
display.print("You will upload a .bin file");
oy += 8;
display.setCursor(bx + 4, oy);
display.print("from your device's browser.");
} else if (_otaPhase == OTA_PHASE_AP_START) {
display.drawTextCentered(display.width() / 2, oy + 20, "Starting WiFi...");
} else if (_otaPhase == OTA_PHASE_WAITING) {
display.drawTextCentered(display.width() / 2, oy, "Firmware Update");
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("Waiting for upload...");
// Poll the web server during render
pollOTAServer();
} else if (_otaPhase == OTA_PHASE_RECEIVING) {
display.drawTextCentered(display.width() / 2, oy, "Receiving Firmware");
oy += 16;
char progBuf[32];
snprintf(progBuf, sizeof(progBuf), "%d KB received", (int)(_otaBytesReceived / 1024));
display.drawTextCentered(display.width() / 2, oy, progBuf);
oy += 14;
display.setCursor(bx + 4, oy);
display.print("Do not close browser");
// Keep polling during receive
pollOTAServer();
} else if (_otaPhase == OTA_PHASE_VERIFY) {
display.drawTextCentered(display.width() / 2, oy + 20, "Verifying file...");
} else if (_otaPhase == OTA_PHASE_FLASH) {
display.drawTextCentered(display.width() / 2, oy + 10, "Flashing Firmware");
display.setColor(DisplayDriver::YELLOW);
display.drawTextCentered(display.width() / 2, oy + 30, "DO NOT POWER OFF");
display.setColor(DisplayDriver::LIGHT);
} else if (_otaPhase == OTA_PHASE_ERROR) {
display.setColor(DisplayDriver::YELLOW);
display.drawTextCentered(display.width() / 2, oy, "Update Failed");
display.setColor(DisplayDriver::LIGHT);
oy += 14;
if (_otaError) {
display.setCursor(bx + 4, oy);
display.print(_otaError);
}
}
display.setTextSize(1);
}
#endif
// === Footer ===
int footerY = display.height() - 12;
display.drawRect(0, footerY - 2, display.width(), 1);
@@ -1279,6 +1730,21 @@ public:
display.print("Please wait...");
}
#endif
#ifdef MECK_OTA_UPDATE
} else if (_editMode == EDIT_OTA) {
if (_otaPhase == OTA_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 (_otaPhase == OTA_PHASE_WAITING) {
display.print("Boot:Cancel");
} else if (_otaPhase == OTA_PHASE_ERROR) {
display.print("Boot:Back");
} else {
display.print("Please wait...");
}
#endif
} else if (_editMode == EDIT_TEXT) {
display.print("Hold:Type");
const char* r = "Tap:OK Boot:Cancel";
@@ -1304,6 +1770,18 @@ public:
display.print("Please wait...");
}
#endif
#ifdef MECK_OTA_UPDATE
} else if (_editMode == EDIT_OTA) {
if (_otaPhase == OTA_PHASE_CONFIRM) {
display.print("Enter:Start Q:Cancel");
} else if (_otaPhase == OTA_PHASE_WAITING) {
display.print("Q:Cancel");
} else if (_otaPhase == OTA_PHASE_ERROR) {
display.print("Q:Back");
} else {
display.print("Please wait...");
}
#endif
} else if (_editMode == EDIT_PICKER) {
display.print("A/D:Choose Enter:Ok");
} else if (_editMode == EDIT_NUMBER) {
@@ -1322,6 +1800,13 @@ 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)) {
return 200; // 200ms — fast enough for web server responsiveness
}
#endif
return _editMode != EDIT_NONE ? 700 : 1000;
}
@@ -1354,6 +1839,42 @@ public:
return true; // consume all keys in confirm mode
}
#ifdef MECK_OTA_UPDATE
// --- OTA update flow ---
if (_editMode == EDIT_OTA) {
if (_otaPhase == OTA_PHASE_CONFIRM) {
if (c == '\r' || c == 13) {
_otaPhase = OTA_PHASE_AP_START;
startOTAServer();
return true;
}
if (c == 'q' || c == 'Q') {
_editMode = EDIT_NONE;
return true;
}
} else if (_otaPhase == OTA_PHASE_WAITING) {
// Check if upload just completed
if (_otaUploadOk) {
// Upload finished — run verify + flash (blocking)
// The display reference isn't available here, so we set a flag
// and the render loop will call processOTAUpload()
return true;
}
if (c == 'q' || c == 'Q') {
stopOTA();
return true;
}
} else if (_otaPhase == OTA_PHASE_ERROR) {
if (c == 'q' || c == 'Q') {
stopOTA();
return true;
}
}
// Consume all keys during OTA
return true;
}
#endif
#ifdef MECK_WIFI_COMPANION
// --- WiFi setup flow ---
if (_editMode == EDIT_WIFI) {
@@ -1917,6 +2438,11 @@ public:
case ROW_ADD_CHANNEL:
startEditText("");
break;
#ifdef MECK_OTA_UPDATE
case ROW_FW_UPDATE:
startOTA();
break;
#endif
case ROW_CHANNEL:
case ROW_PUB_KEY:
case ROW_FIRMWARE:

View File

@@ -1313,13 +1313,32 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
}
// Add to channel history screen with channel index, path data, and SNR
// For DMs (channel_idx == 0xFF), prefix text with sender name so the
// DM inbox can extract it later. Channel messages already arrive from
// MeshCore in "Sender: message" format.
// For DMs (channel_idx == 0xFF):
// - Regular DMs: prefix text with sender name ("NodeName: hello")
// - Room server messages: text already contains "OriginalSender: message",
// don't double-prefix. Tag with room server name for conversation filtering.
bool isRoomMsg = false;
if (channel_idx == 0xFF) {
char dmFormatted[CHANNEL_MSG_TEXT_LEN];
snprintf(dmFormatted, sizeof(dmFormatted), "%s: %s", from_name, text);
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, dmFormatted, path, snr);
// Check if sender is a room server
uint32_t numContacts = the_mesh.getNumContacts();
ContactInfo senderContact;
for (uint32_t ci = 0; ci < numContacts; ci++) {
if (the_mesh.getContactByIdx(ci, senderContact) && strcmp(senderContact.name, from_name) == 0) {
if (senderContact.type == ADV_TYPE_ROOM) isRoomMsg = true;
break;
}
}
if (isRoomMsg) {
// Room server: text already has "Poster: message" format — store as-is
// Tag with room server name for conversation filtering
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, text, path, snr, from_name);
} else {
// Regular DM: prefix with sender name
char dmFormatted[CHANNEL_MSG_TEXT_LEN];
snprintf(dmFormatted, sizeof(dmFormatted), "%s: %s", from_name, text);
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, dmFormatted, path, snr);
}
} else {
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, text, path, snr);
}
@@ -1346,14 +1365,15 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
#if defined(LilyGo_TDeck_Pro) || defined(LilyGo_T5S3_EPaper_Pro)
// Don't interrupt user with popup - just show brief notification
// Messages are stored in channel history, accessible via tile/key
if (!isOnRepeaterAdmin()) {
// Suppress toasts for room server messages (bulk sync would spam toasts)
if (!isOnRepeaterAdmin() && !isRoomMsg) {
char alertBuf[40];
snprintf(alertBuf, sizeof(alertBuf), "New: %s", from_name);
showAlert(alertBuf, 2000);
}
#else
// Other devices: Show full preview screen (legacy behavior)
setCurrScreen(msg_preview);
// Other devices: Show full preview screen (legacy behavior, skip room sync)
if (!isRoomMsg) setCurrScreen(msg_preview);
#endif
if (_display != NULL) {
@@ -1362,13 +1382,19 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
}
if (_display->isOn()) {
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
_next_refresh = 100; // trigger refresh
// Throttle refresh during room sync — batch messages instead of 648ms render per msg
if (isRoomMsg) {
unsigned long earliest = millis() + 3000; // At most one refresh per 3s during sync
if (_next_refresh < earliest) _next_refresh = earliest;
} else {
_next_refresh = 100; // trigger refresh
}
}
}
// Keyboard flash notification
// Keyboard flash notification (suppress for room sync)
#ifdef KB_BL_PIN
if (_node_prefs->kb_flash_notify) {
if (_node_prefs->kb_flash_notify && !isRoomMsg) {
digitalWrite(KB_BL_PIN, HIGH);
_kb_flash_off_at = millis() + 200; // 200ms flash
}

View File

@@ -96,6 +96,8 @@ lib_deps =
zinggjm/GxEPD2@^1.5.9
adafruit/Adafruit GFX Library@^1.11.0
bitbank2/PNGdec@^1.0.1
WebServer
Update
; ---------------------------------------------------------------------------
; Meck unified builds — one codebase, six variants via build flags
@@ -114,6 +116,7 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
-D MECK_AUDIO_VARIANT
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
@@ -146,6 +149,7 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
-D MECK_AUDIO_VARIANT
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.3.WiFi"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
@@ -161,6 +165,7 @@ lib_deps =
; Audio standalone (audio-player hardware, no BLE/WiFi — maximum battery life)
; No MECK_WEB_READER: WiFi power draw conflicts with zero-radio-power design.
; OTA enabled: WiFi AP activates only during user-initiated firmware updates.
; Contacts and sort arrays allocated in PSRAM — 1500 contacts uses ~290KB of 8MB.
[env:meck_audio_standalone]
extends = LilyGo_TDeck_Pro
@@ -171,6 +176,7 @@ build_flags =
-D MAX_GROUP_CHANNELS=20
-D OFFLINE_QUEUE_SIZE=1
-D MECK_AUDIO_VARIANT
-D MECK_OTA_UPDATE=1
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
@@ -196,6 +202,7 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.3.4G"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
@@ -226,6 +233,7 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.3.4G.WiFi"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
@@ -252,6 +260,7 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=1
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.3.4G.SA"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>