mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
tdpro ota update firmware functionality implemented; roomserver ui sender name display fix and speed up delivery time
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user