Xteink-X4-crosspoint-reader/src/extension/AppLoader.cpp
2026-02-03 00:01:59 -08:00

315 lines
9.0 KiB
C++

#include "AppLoader.h"
#include <memory>
#include <esp_ota_ops.h>
#include <esp_partition.h>
#include <esp_system.h>
#include "Battery.h"
namespace CrossPoint {
std::vector<AppInfo> AppLoader::scanApps() {
std::vector<AppInfo> apps;
if (!isSDReady()) {
Serial.printf("[%lu] [AppLoader] SD card not ready\n", millis());
return apps;
}
FsFile appsDir = SdMan.open(APPS_BASE_PATH, O_RDONLY);
if (!appsDir || !appsDir.isDirectory()) {
Serial.printf("[%lu] [AppLoader] Apps directory not found: %s\n", millis(), APPS_BASE_PATH);
if (appsDir) appsDir.close();
return apps;
}
char name[128];
FsFile entry = appsDir.openNextFile();
while (entry) {
if (entry.isDirectory()) {
entry.getName(name, sizeof(name));
String appPath = APPS_BASE_PATH;
if (!appPath.endsWith("/")) {
appPath += "/";
}
appPath += name;
String manifestPath = buildManifestPath(appPath);
AppManifest manifest = parseManifest(manifestPath);
if (!manifest.name.isEmpty()) {
apps.emplace_back(manifest, appPath);
Serial.printf("[%lu] [AppLoader] Found app: %s\n", millis(), manifest.name.c_str());
} else {
Serial.printf("[%lu] [AppLoader] Skipping directory (no valid manifest): %s\n", millis(), name);
}
}
entry.close();
entry = appsDir.openNextFile();
}
appsDir.close();
Serial.printf("[%lu] [AppLoader] Found %u app(s)\n", millis(), apps.size());
return apps;
}
AppManifest AppLoader::parseManifest(const String& path) {
AppManifest manifest;
if (!isSDReady()) {
Serial.printf("[%lu] [AppLoader] SD card not ready, cannot parse manifest\n", millis());
return manifest;
}
if (!SdMan.exists(path.c_str())) {
Serial.printf("[%lu] [AppLoader] Manifest file not found: %s\n", millis(), path.c_str());
return manifest;
}
FsFile file = SdMan.open(path.c_str(), O_RDONLY);
if (!file) {
Serial.printf("[%lu] [AppLoader] Failed to open manifest file: %s\n", millis(), path.c_str());
return manifest;
}
const size_t fileSize = file.size();
if (fileSize == 0) {
Serial.printf("[%lu] [AppLoader] Manifest file is empty: %s\n", millis(), path.c_str());
file.close();
return manifest;
}
if (fileSize > MAX_MANIFEST_SIZE) {
Serial.printf("[%lu] [AppLoader] Manifest file too large (%u bytes, max %u): %s\n",
millis(), fileSize, MAX_MANIFEST_SIZE, path.c_str());
file.close();
return manifest;
}
std::unique_ptr<char[]> buffer(new char[fileSize + 1]);
const size_t bytesRead = file.read(buffer.get(), fileSize);
buffer[bytesRead] = '\0';
file.close();
if (bytesRead != fileSize) {
Serial.printf("[%lu] [AppLoader] Failed to read complete manifest file (read %u of %u bytes): %s\n",
millis(), bytesRead, fileSize, path.c_str());
return manifest;
}
// Handle UTF-8 BOM if the manifest was created by an editor that writes it.
const char* json = buffer.get();
if (bytesRead >= 3 && static_cast<uint8_t>(json[0]) == 0xEF && static_cast<uint8_t>(json[1]) == 0xBB &&
static_cast<uint8_t>(json[2]) == 0xBF) {
json += 3;
}
JsonDocument doc;
const DeserializationError error = deserializeJson(doc, json);
if (error) {
Serial.printf("[%lu] [AppLoader] JSON parse error in %s: %s\n",
millis(), path.c_str(), error.c_str());
return manifest;
}
if (doc["name"].is<const char*>()) {
manifest.name = doc["name"].as<String>();
} else {
Serial.printf("[%lu] [AppLoader] Missing or invalid 'name' field in: %s\n", millis(), path.c_str());
return manifest;
}
if (doc["version"].is<const char*>()) {
manifest.version = doc["version"].as<String>();
} else {
manifest.version = "1.0.0";
}
if (doc["description"].is<const char*>()) {
manifest.description = doc["description"].as<String>();
} else {
manifest.description = "";
}
if (doc["author"].is<const char*>()) {
manifest.author = doc["author"].as<String>();
} else {
manifest.author = "Unknown";
}
if (doc["minFirmware"].is<const char*>()) {
manifest.minFirmware = doc["minFirmware"].as<String>();
} else {
manifest.minFirmware = "0.0.0";
}
return manifest;
}
bool AppLoader::flashApp(const String& binPath, ProgressCallback callback) {
if (!isSDReady()) {
Serial.printf("[%lu] [AppLoader] SD card not ready, cannot flash app\n", millis());
return false;
}
const uint16_t batteryPercentage = battery.readPercentage();
if (batteryPercentage < 20) {
Serial.printf("[%lu] [AppLoader] Battery: %u%% - TOO LOW\n", millis(), batteryPercentage);
Serial.printf("[%lu] [AppLoader] Flash aborted: battery below 20%%\n", millis());
return false;
}
Serial.printf("[%lu] [AppLoader] Battery: %u%% - OK\n", millis(), batteryPercentage);
if (!SdMan.exists(binPath.c_str())) {
Serial.printf("[%lu] [AppLoader] App binary not found: %s\n", millis(), binPath.c_str());
return false;
}
FsFile file = SdMan.open(binPath.c_str(), O_RDONLY);
if (!file) {
Serial.printf("[%lu] [AppLoader] Failed to open app binary: %s\n", millis(), binPath.c_str());
return false;
}
const size_t fileSize = file.size();
if (fileSize == 0) {
Serial.printf("[%lu] [AppLoader] App binary is empty: %s\n", millis(), binPath.c_str());
file.close();
return false;
}
uint8_t magicByte = 0;
const size_t magicRead = file.read(&magicByte, 1);
if (magicRead != 1 || magicByte != 0xE9) {
Serial.printf("[%lu] [AppLoader] Invalid firmware magic byte: 0x%02X\n", millis(), magicByte);
file.close();
return false;
}
file.close();
file = SdMan.open(binPath.c_str(), O_RDONLY);
if (!file) {
Serial.printf("[%lu] [AppLoader] Failed to reopen app binary: %s\n", millis(), binPath.c_str());
return false;
}
const esp_partition_t* running = esp_ota_get_running_partition();
if (!running) {
Serial.printf("[%lu] [AppLoader] Failed to get running partition\n", millis());
file.close();
return false;
}
const esp_partition_t* target = esp_ota_get_next_update_partition(NULL);
if (!target) {
Serial.printf("[%lu] [AppLoader] No OTA partition available\n", millis());
file.close();
return false;
}
if (target->address == running->address) {
Serial.printf("[%lu] [AppLoader] Target partition matches running partition, aborting\n", millis());
file.close();
return false;
}
if (fileSize >= target->size) {
Serial.printf("[%lu] [AppLoader] Firmware too large (%u bytes, max %u)\n", millis(), fileSize, target->size);
file.close();
return false;
}
Serial.printf("[%lu] [AppLoader] Flashing to partition: %s (offset: 0x%06X)\n", millis(), target->label,
target->address);
esp_ota_handle_t otaHandle = 0;
esp_err_t err = esp_ota_begin(target, fileSize, &otaHandle);
if (err != ESP_OK) {
Serial.printf("[%lu] [AppLoader] OTA begin failed: %d\n", millis(), err);
file.close();
return false;
}
if (callback) {
callback(0, fileSize);
}
size_t totalWritten = 0;
// Larger chunks reduce SD/OTA overhead significantly.
// 32KB is a good balance on ESP32-C3: faster writes without blowing RAM.
static constexpr size_t flashChunkSize = 32 * 1024;
static uint8_t buffer[flashChunkSize];
size_t lastNotifiedPercent = 0;
while (totalWritten < fileSize) {
const size_t remaining = fileSize - totalWritten;
const size_t toRead = remaining < flashChunkSize ? remaining : flashChunkSize;
const size_t bytesRead = file.read(buffer, toRead);
if (bytesRead == 0) {
Serial.printf("[%lu] [AppLoader] Failed to read firmware data\n", millis());
esp_ota_end(otaHandle);
file.close();
return false;
}
err = esp_ota_write(otaHandle, buffer, bytesRead);
if (err != ESP_OK) {
Serial.printf("[%lu] [AppLoader] OTA write failed at %u/%u bytes: %d\n", millis(), totalWritten, fileSize, err);
esp_ota_end(otaHandle);
file.close();
return false;
}
totalWritten += bytesRead;
if (callback) {
const size_t percent = (totalWritten * 100) / fileSize;
// Throttle UI updates; each screen refresh is ~400ms.
if (percent >= lastNotifiedPercent + 10 || percent == 100) {
lastNotifiedPercent = percent;
callback(totalWritten, fileSize);
}
}
}
file.close();
err = esp_ota_end(otaHandle);
if (err != ESP_OK) {
Serial.printf("[%lu] [AppLoader] OTA end failed: %d\n", millis(), err);
return false;
}
err = esp_ota_set_boot_partition(target);
if (err != ESP_OK) {
Serial.printf("[%lu] [AppLoader] Failed to set boot partition: %d\n", millis(), err);
return false;
}
Serial.printf("[%lu] [AppLoader] Flash complete. Rebooting...\n", millis());
esp_restart();
return true;
}
String AppLoader::buildManifestPath(const String& appDir) const {
String path = appDir;
if (!path.endsWith("/")) {
path += "/";
}
path += MANIFEST_FILENAME;
return path;
}
bool AppLoader::isSDReady() const {
return SdMan.ready();
}
}