basic commit

This commit is contained in:
admin 2025-08-25 23:38:53 -07:00
parent 9e9c045a3f
commit 90ef654c80
15 changed files with 1194 additions and 185 deletions

View File

@ -0,0 +1,22 @@
{
"comets":{
"size": 0.2,
"fade-factor1":64,
"max-comets":16
},
"fire":{
"cooling":66,
"sparking":62,
"brightness":255
},
"custom-color-pack1": {
"color1": "#FF0000",
"color2": "#00FF00",
"color3": "#0000FF"
},
"custom-color-pack2": {
"color1": "#FFFF00",
"color2": "#FF00FF",
"color3": "#00FFFF"
}
}

View File

@ -8,6 +8,7 @@
"wifi-ap":{
"ssid": "ATA_AP",
"append-id": true,
"user": "admin",
"pass": "12345678",
"ip": "192.168.10.1",
"gateway": "192.168.10.1",

View File

@ -133,12 +133,26 @@
font-size: 14px;
}
}
/* Tabs */
.tab-bar { display:flex; gap:8px; justify-content:center; margin:12px 0 16px; flex-wrap:wrap; }
.tab-bar button { max-width:none; flex:0 0 auto; background:#6c757d; }
.tab-bar button.active { background:#007bff; }
.tab-panel { display:none; }
.tab-panel.active { display:block; }
</style>
</head>
<body>
<h1>ATA Firmware Update</h1>
<!-- Tab Buttons -->
<div class="tab-bar">
<button class="tab-btn active" data-tab="tab-upgrade">Upgrade</button>
<button class="tab-btn" data-tab="tab-wifi">WiFi Comm</button>
</div>
<div id="tab-upgrade" class="tab-panel active">
<!-- Status Indicators -->
<div class="status-container">
<span class="status-indicator-ble"></span>
@ -183,21 +197,22 @@
<button id="checkVersionBtn" onclick="checkVersion()" disabled>Check Version</button>
<button id="startUpgradeBtn" onclick="startUpgrade()" disabled>Start Update</button>
</div>
</div> <!-- /tab-upgrade -->
<!-- Wi-Fi Input Fields -->
<div class="input-container">
<div id="tab-wifi" class="tab-panel">
<h2 style="margin-top:0; font-size:18px;">WiFi Connection</h2>
<div class="input-container">
<input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required>
<input type="password" id="wifipassword" name="wifipassword" placeholder="Enter WiFi Password" required>
<div style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()" style="width: auto;">
<label for="showPassword">Show Password</label>
</div>
</div>
<!-- Added margin-top above this button -->
</div>
<div class="btn-container wifi">
<button id="wifiConnectBtn" onclick="wifiConnect()" disabled>Connect Wifi</button>
</div>
</div><!-- /tab-wifi -->
<script>
(function(){
@ -457,6 +472,17 @@
el.inDeviceName.value = BLE_SERVER_NAME;
el.chkShowPass.addEventListener('change', togglePasswordVisibility);
// Inline onclicks already wired; ensure functions are in scope
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn=>{
btn.addEventListener('click', ()=>{
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
btn.classList.add('active');
const id = btn.getAttribute('data-tab');
const panel = document.getElementById(id);
if(panel) panel.classList.add('active');
});
});
updateUI();
logMessage('Ready. Enter device name or use default and press Connect.');
}

View File

@ -60,7 +60,8 @@ void Lights_Set_ON(void);
void Lights_Set_OFF(void);
void Lights_Set_Brightness(uint8_t scale);
void Lights_Set_White(uint8_t val);
void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB color3);
//void createFirePalette(CRGBPalette16& palette, COLOR_PACK& colorPack);
void createFirePalette(CRGBPalette16& palette, const COLOR_PACK& colorPack);
void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src);

View File

@ -11,6 +11,15 @@
//#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/"
#define DEFAULT_MANIFEST_URL "https://minio.boothwizard.com/boothifier/latest/"
#define BUFFER_SIZE 4096
// Maximum allowed manifest size (bytes) to protect memory
#define MAX_MANIFEST_SIZE (64 * 1024)
// Number of HTTP retry attempts for transient failures
#define HTTP_RETRY_COUNT 3
#define HTTP_RETRY_DELAY_MS 500
// Allow external cancellation
extern volatile bool g_UpdateCancelFlag;
extern TaskHandle_t Update_Task_Handle;
@ -186,7 +195,6 @@ void sendUpdateMessage(const char* message, bool complete, int progress);
void handleUpdateProgress(AsyncWebServerRequest *request);
bool checkManifest(Version& remoteVersion);
void startVersionCheckTask();

View File

@ -8,6 +8,21 @@ typedef struct {
CRGB col[8];
} COLOR_PACK;
const COLOR_PACK colorPack_FireRed PROGMEM = { 4, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Black } };
const COLOR_PACK colorPack_FireGreen PROGMEM = { 4, { CRGB::DarkGreen, CRGB::Green, CRGB::LightGreen, CRGB::Black } };
const COLOR_PACK colorPack_FireBlue PROGMEM = { 4, { CRGB::DarkBlue, CRGB::Blue, CRGB::LightBlue, CRGB::Black } };
const COLOR_PACK colorPack_FireViolet PROGMEM = { 4, { CRGB::Purple, CRGB::Blue, CRGB::Violet, CRGB::Black } };
// Fire (compacted: single PROGMEM array, removes duplicate size constants)
const COLOR_PACK fireColorPacks[] PROGMEM = {
colorPack_FireRed,
colorPack_FireGreen,
colorPack_FireBlue,
colorPack_FireViolet
};
// Sectors
const COLOR_PACK colorPack_RAINBOW PROGMEM = { 7, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Green, CRGB::Blue, CRGB::BlueViolet, CRGB::MediumVioletRed } };
const COLOR_PACK colorPack_USA PROGMEM = { 3, { CRGB::Red, CRGB::White, CRGB::Blue } };
@ -15,6 +30,16 @@ const COLOR_PACK colorPack_MEXICO PROGMEM = { 3, { CRGB::Green, CRGB::White, CRG
const COLOR_PACK colorPack_CANADA PROGMEM = { 2, { CRGB::Red, CRGB::White } };
const COLOR_PACK colorPack_GERMANY PROGMEM = { 3, { CRGB::Black, CRGB::Red, CRGB::Yellow } };
const COLOR_PACK combo_colorPacks[] PROGMEM = {
colorPack_RAINBOW,
colorPack_USA,
colorPack_MEXICO,
colorPack_CANADA,
colorPack_GERMANY
};
// Single Colors
const COLOR_PACK colorPack_Single_Red PROGMEM = { 1, { CRGB::Red } };
const COLOR_PACK colorPack_Single_Orange PROGMEM = { 1, { CRGB::OrangeRed } };
@ -25,6 +50,19 @@ const COLOR_PACK colorPack_Single_Viloet PROGMEM = { 1, { CRGB::DarkViolet } };
const COLOR_PACK colorPack_Single_Magenta PROGMEM = { 1, { CRGB::Magenta } };
const COLOR_PACK colorPack_Single_White PROGMEM = { 1, { CRGB::White } };
const COLOR_PACK single_colorPacks[] PROGMEM = {
colorPack_Single_Red,
colorPack_Single_Orange,
colorPack_Single_Yellow,
colorPack_Single_Green,
colorPack_Single_Blue,
colorPack_Single_Viloet,
colorPack_Single_Magenta,
colorPack_Single_White
};
// Dashes
const COLOR_PACK colorPack_RedBlack PROGMEM = { 2, { CRGB::Red, CRGB::Black } };
const COLOR_PACK colorPack_OrangeBlack PROGMEM = { 2, { CRGB::DarkOrange, CRGB::Black } };
@ -35,8 +73,20 @@ const COLOR_PACK colorPack_IndigoBlack PROGMEM = { 2, { CRGB::Indigo, CRGB::Blac
const COLOR_PACK colorPack_VioletBlack PROGMEM = { 2, { CRGB::MediumVioletRed, CRGB::Black } };
const COLOR_PACK colorPack_WhiteBlack PROGMEM = { 2, { CRGB::White, CRGB::Black } };
const COLOR_PACK DashesColorPacks[] PROGMEM = {
colorPack_RedBlack,
colorPack_OrangeBlack,
colorPack_YellowBlack,
colorPack_GreenBlack,
colorPack_BlueBlack,
colorPack_IndigoBlack,
colorPack_VioletBlack,
colorPack_WhiteBlack
};
const COLOR_PACK fire PROGMEM = { 4, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Black } };

View File

@ -375,35 +375,19 @@ void Lights_Control_Task(void *parameters){
case 1:
Anim_Rainbow(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 30);
break;
case 2:
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 1000, ledSettings[0].shift);
case 2: case 3: case 4: { // Timed Fill Animations
int timeDuration = (AnimEvent.AnimationIndex-1) * 1000;
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, timeDuration, ledSettings[0].shift);
whiteTimeout = 20;
break;
case 3:
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 2000, ledSettings[0].shift);
whiteTimeout = 20;
break;
case 4:
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 3000, ledSettings[0].shift);
whiteTimeout = 20;
break;
case 5:
createFirePalette(firePalette, CRGB::Red, CRGB::OrangeRed, CRGB::Orange);
}
case 5: case 6: case 7: case 8: {// Fire Animations
COLOR_PACK fp = fireColorPacks[AnimEvent.AnimationIndex - 5]; // copy const pack to mutable
createFirePalette(firePalette, fp);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
break;
case 6:
createFirePalette(firePalette, CRGB::DarkGreen, CRGB::Green, CRGB::LightGreen);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
break;
case 7:
createFirePalette(firePalette, CRGB::DarkBlue, CRGB::Blue, CRGB::LightBlue);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
break;
case 8:
createFirePalette(firePalette, CRGB::Purple, CRGB::Blue, CRGB::Violet);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
break;
case 9:
}
case 9: // Sec
loadColorPack(colorPack, colorPack_USA);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 1, 80);
break;
@ -430,11 +414,14 @@ void Lights_Control_Task(void *parameters){
case 15:
loadColorPack(colorPack, colorPack_VioletBlack);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80);
case 16: case 17: case 18: case 19: case 20: {
//loadColorPack(colorPack, colorPack_RAINBOW);
int idx = AnimEvent.AnimationIndex - 16;
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, combo_colorPacks[idx], 80, RANDOM_DECAY, true, 1);
break;
case 16:
loadColorPack(colorPack, colorPack_RAINBOW);
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, true, 1);
break;
}
/*
/*
case 17:
loadColorPack(colorPack, colorPack_USA);
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, LINEAR_DECAY, true, 1);
@ -451,6 +438,7 @@ void Lights_Control_Task(void *parameters){
loadColorPack(colorPack, colorPack_RAINBOW);
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, true, 1);
break;
*/
case 21:
loadColorPack(colorPack, colorPack_RAINBOW);
Anim_ColorBreath(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 7000, 90);
@ -472,6 +460,16 @@ void Lights_Control_Task(void *parameters){
}
void createFirePalette(CRGBPalette16& palette, const COLOR_PACK& colorPack) {
for (uint8_t i = 0; i < 16; i++) {
if (i < 3) palette[i] = CRGB::Black;
else if (i < 7) palette[i] = colorPack.col[0];
else if (i < 10) palette[i] = colorPack.col[1];
else if (i < 15) palette[i] = colorPack.col[2];
else palette[i] = CRGB::White;
}
}
/*
void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB color3) {
for (uint8_t i = 0; i < 16; i++) {
if (i < 3) palette[i] = CRGB::Black;
@ -481,6 +479,7 @@ void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB co
else palette[i] = CRGB::White;
}
}
*/
void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src) {
memcpy_P(&dest, &src, sizeof(COLOR_PACK));

View File

@ -574,6 +574,63 @@ void Anim_GradientRotate(bool volatile& activeFlag, CRGB* leds, int size, const
}
void Anim_ColorWipe(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed) {
if (!leds || size <= 0 || colors.size <= 0) return;
int currentIndex = 0;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Wipe color across the strip
fill_solid(leds, size, colors.col[currentIndex]);
FastLED.show();
// Move to the next color
currentIndex = (currentIndex + 1) % colors.size;
return 0;
});
}
void Anim_Sparkle(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed, uint8_t sparkleChance) {
if (!leds || size <= 0 || colors.size <= 0) return;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Randomly light up LEDs with colors from the color pack
for (int i = 0; i < size; i++) {
if (getRandomValue(100) < sparkleChance) {
int colorIndex = getRandomValue(colors.size);
leds[i] = colors.col[colorIndex];
} else {
leds[i] = CRGB::Black; // Turn off LED
}
}
FastLED.show();
return 0;
});
}
void Anim_TheaterChase(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed, uint8_t spacing) {
if (!leds || size <= 0 || colors.size <= 0 || spacing == 0) return;
int colorIndex = 0;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Clear all LEDs
fill_solid(leds, size, CRGB::Black);
// Light up every 'spacing' LED with the current color
for (int i = 0; i < size; i += spacing) {
leds[i] = colors.col[colorIndex];
}
FastLED.show();
// Move to the next color
colorIndex = (colorIndex + 1) % colors.size;
return 0;
});
}
uint32_t getRandomValue(uint32_t maxValue) {
return esp_random() % maxValue;
}

View File

@ -12,6 +12,7 @@
static const char* TAG = "AppUpdater";
TaskHandle_t Update_Task_Handle = NULL;
TaskHandle_t versionCheckTask_Handle = NULL;
volatile bool g_UpdateCancelFlag = false; // cancellation flag
// Queue handle for firmware update messages
//QueueHandle_t updateMsgQueue = NULL;
@ -45,19 +46,30 @@ bool AppUpdater::checkManifest() {
String url = buildUrl(manifestName);
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
// Start the HTTP client and Send GET request for manifest
String payload;
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
if(g_UpdateCancelFlag) return false;
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
if (httpCode == HTTP_CODE_OK) {
payload = http.getString();
http.end();
break;
}
ESP_LOGW(TAG, "Manifest GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
http.end();
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
}
if(payload.isEmpty()){
ESP_LOGE(TAG, "Failed to fetch manifest after retries");
return false;
}
// Read the response
String payload = http.getString();
http.end();
if(payload.length() > MAX_MANIFEST_SIZE){
ESP_LOGE(TAG, "Manifest too large (%u bytes)", (unsigned)payload.length());
return false;
}
// Parse JSON
DeserializationError error = deserializeJson(jsonManifest, payload);
@ -116,9 +128,13 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
String url = buildUrl(remotePath);
ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath);
// Quick skip: if exists and size & MD5 match
bool skip = false;
if(fileSystem.exists(localPath)){
String localMd5 = getLocalMD5(localPath);
if (localMd5.equals(expectedMd5)) {
if(localMd5.equals(expectedMd5)) skip = true;
}
if(skip){
ESP_LOGI(TAG, "File already up to date: %s", localPath);
updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath);
return true;
@ -126,12 +142,19 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
// Start the download
HTTPClient http;
int httpCode = -1;
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
if(g_UpdateCancelFlag) return false;
http.begin(url);
int httpCode = http.GET();
httpCode = http.GET();
if(httpCode == HTTP_CODE_OK) break;
ESP_LOGW(TAG, "File GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
http.end();
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
}
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "Download failed: %d", httpCode);
updateProgress(UpdateStatus::ERROR, 0, "Download failed");
http.end();
return false;
}
@ -172,6 +195,7 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
if (contentLength > 0) {
// Single pass with known content length
while (totalRead < contentLength) {
if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
size_t available = stream->available();
if (available) {
size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
@ -187,13 +211,14 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
md5.add(downloadBuffer.get(), readLen);
totalRead += readLen;
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 90) / contentLength , localPath);
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 80) / contentLength , localPath);
}
yield();
}
} else {
// Unknown content length: read until stream ends
for (;;) {
if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
if (readLen == 0) {
break;
@ -207,7 +232,10 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
md5.add(downloadBuffer.get(), readLen);
totalRead += readLen;
// Progress unknown; emit periodic heartbeats at 0%
// For unknown size, send heartbeats every ~16KB
if((totalRead & 0x3FFF) == 0){
updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
}
yield();
}
}
@ -217,12 +245,15 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
String calculatedMd5 = md5.toString();
// Verify MD5 hash
updateProgress(UpdateStatus::VERIFYING, 90, localPath);
if (!calculatedMd5.equals(expectedMd5)) {
//ESP_LOGE(TAG, "MD5 mismatch for %s", localPath);
fileSystem.remove(tempPath.c_str());
return false;
}
updateProgress(UpdateStatus::VERIFYING, 95, localPath);
// Replace original file with verified temp file
if (fileSystem.exists(localPath)) {
fileSystem.remove(localPath);
@ -233,6 +264,7 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
return false;
}
updateProgress(UpdateStatus::VERIFYING, 100, localPath);
return true;
}
@ -302,12 +334,19 @@ bool AppUpdater::updateApp() {
// Download the firmware
HTTPClient http;
int httpCode = -1;
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
if(g_UpdateCancelFlag) return false;
http.begin(firmwareUrl);
int httpCode = http.GET();
httpCode = http.GET();
if(httpCode == HTTP_CODE_OK) break;
ESP_LOGW(TAG, "Firmware GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
http.end();
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
}
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "Firmware download failed: %d", httpCode);
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed");
http.end();
return false;
}
@ -329,6 +368,7 @@ bool AppUpdater::updateApp() {
if (firmwareSize > 0) {
size_t remaining = firmwareSize;
while (remaining > 0) {
if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
size_t chunk = std::min(remaining, size_t(BUFFER_SIZE));
size_t read = stream->readBytes(downloadBuffer.get(), chunk);
@ -355,6 +395,7 @@ bool AppUpdater::updateApp() {
} else {
// Unknown size: stream until end
for (;;) {
if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
if (read == 0) break;
md5.add(downloadBuffer.get(), read);
@ -371,6 +412,7 @@ bool AppUpdater::updateApp() {
// Verify MD5
md5.calculate();
String calculatedMd5 = md5.toString();
updateProgress(UpdateStatus::VERIFYING, 95, "firmware");
if (!calculatedMd5.equals(expectedMd5)) {
ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str());
updateProgress(UpdateStatus::MD5_FAILED, 0, "Firmware: MD5 mismatch");
@ -388,7 +430,7 @@ bool AppUpdater::updateApp() {
}
http.end();
updateProgress(UpdateStatus::COMPLETE, 0, "Firmware: Complete");
updateProgress(UpdateStatus::COMPLETE, 100, "Firmware: Complete");
return true;
}
@ -459,7 +501,6 @@ void firmwareUpdateTask(void* parameter) {
} catch (const std::exception& e) {
ESP_LOGE(TAG, "Update failed: %s", e.what());
}
end:
delete updater;
Update_Task_Handle = NULL;
vTaskDelete(NULL);
@ -474,15 +515,16 @@ void startVersionCheckTask() {
}
void versionCheckTask(void* parameter){
if(updateUrl == ""){
loadUpdateJson();
}
if(checkManifest(otaVersion) == false){
ESP_LOGE(TAG, "Error checking manifest");
AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "update.json", "firmware.bin");
if(!updater.checkManifest()){
ESP_LOGE(TAG, "Version check: manifest fetch failed");
} else {
otaVersion = updater.otaVersion; // capture remote
ESP_LOGI(TAG, "Version check: remote=%s", otaVersion.toString().c_str());
}
versionCheckTask_Handle = NULL;
vTaskDelete(NULL);
}
@ -589,51 +631,7 @@ void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
bleUpgrade_send_message(message);
}
bool checkManifest(Version& remoteVersion) {
const char* TAG = "manifestCheck";
String url = updateUrl + "update.json";
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
// Start the HTTP client and send GET request for manifest
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
http.end();
return false;
}
// Read the response
String payload = http.getString();
http.end();
//ESP_LOGD(TAG, "%s", payload.c_str());
// Parse JSON
JsonDocument jsonManifest;
DeserializationError error = deserializeJson(jsonManifest, payload);
if (error) {
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
return false;
}
// Check for version section
JsonObject jsonVersion = jsonManifest["version"];
if (jsonVersion.isNull()) {
ESP_LOGE(TAG, "No version section in manifest");
return false;
}
// Get the remote version
byte major = jsonVersion["major"] | 0;
byte minor = jsonVersion["minor"] | 0;
byte patch = jsonVersion["patch"] | 0;
remoteVersion = {major, minor, patch};
ESP_LOGI(TAG, "Remote version: %s", remoteVersion.toString().c_str());
return true;
}
// (Removed duplicate global checkManifest; AppUpdater::checkManifest used instead)
/*
void setup() {

View File

@ -60,6 +60,7 @@ uint8_t calculateChecksum(const uint8_t bArr[]) {
}
// Class for handling characteristic events
/*
class SP110ECallbacks : public NimBLECharacteristicCallbacks {
void onWrite(NimBLECharacteristic *pCharacteristic) override {
std::string rawValue = pCharacteristic->getValue();
@ -75,6 +76,54 @@ class SP110ECallbacks : public NimBLECharacteristicCallbacks {
process_BLE_SP110E_Command(value, length, pCharacteristic);
}
};
*/
// Class for handling characteristic events
class SP110ECallbacks : public NimBLECharacteristicCallbacks {
public:
void onWrite(NimBLECharacteristic* pCharacteristic) override {
if (!pCharacteristic) return;
std::string raw = pCharacteristic->getValue(); // NimBLE copies internally
size_t len = raw.size();
if (len == 0) {
ESP_LOGW(tag, "Write received with zero-length payload");
return;
}
const uint8_t* data = reinterpret_cast<const uint8_t*>(raw.data());
// Log up to first 16 bytes to avoid log spam
//logBytes(data, len);
// Clamp length passed to command processor (its param is uint8_t)
uint8_t procLen = static_cast<uint8_t>(len > 255 ? 255 : len);
// Forward to any subscribed mirror clients first (raw payload)
sendToAllClients(data, procLen);
// Process command (may generate response/notify on same characteristic)
process_BLE_SP110E_Command(data, procLen, pCharacteristic);
}
private:
static void logBytes(const uint8_t* data, size_t len) {
if (!data) return;
char buf[3 * 16 + 1];
size_t toPrint = len > 16 ? 16 : len;
char* p = buf;
for (size_t i = 0; i < toPrint; ++i) {
sprintf(p, "%02X ", data[i]);
p += 3;
}
*p = 0;
if (len > toPrint) {
ESP_LOGI(tag, "Data (%zu bytes): %s...", len, buf);
} else {
ESP_LOGI(tag, "Data (%zu bytes): %s", len, buf);
}
}
};
class LightStickCallbacks : public NimBLECharacteristicCallbacks {
void onRead(NimBLECharacteristic *pCharacteristic) override {
@ -83,7 +132,34 @@ class LightStickCallbacks : public NimBLECharacteristicCallbacks {
}
};
// Function to send data to all connected clients in chunks based on MTU
void sendToAllClients(const uint8_t* data, size_t len) {
if (!pStickCharacteristic || !data || len == 0) return;
// Skip if no subscribed clients (if API available)
#if defined(NIMBLE_INCLUDED) || true
#ifdef CONFIG_BT_NIMBLE_ROLE_PERIPHERAL
if (pStickCharacteristic->getSubscribedCount() == 0) return;
#endif
#endif
// Determine a safe chunk size based on (last) negotiated MTU; fallback to 20.
uint16_t mtu = NimBLEDevice::getMTU(); // Typically 23 default -> 20 payload
size_t maxChunk = (mtu > 3) ? (mtu - 3) : 20;
if (maxChunk == 0) maxChunk = 20;
size_t offset = 0;
while (offset < len) {
size_t chunk = len - offset;
if (chunk > maxChunk) chunk = maxChunk;
pStickCharacteristic->setValue(data + offset, chunk);
// notify() returns void in this NimBLE version, so just call it without checking a return value
pStickCharacteristic->notify();
offset += chunk;
}
}
/*
void sendToAllClients(const uint8_t *data, size_t len) {
// Check if the characteristic is valid and has subscribed clients
if (pStickCharacteristic != nullptr) {
@ -92,6 +168,7 @@ void sendToAllClients(const uint8_t *data, size_t len) {
pStickCharacteristic->notify();
}
}
*/
void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacteristic* bleChar) {
@ -115,6 +192,10 @@ void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacter
//ESP_LOGI(tag, "Lights OFF");
break;
case SET_STATIC_COLOR:
if(len < 7) {
ESP_LOGW(tag, "SET_STATIC_COLOR command requires 3 parameters (R,G,B)");
break;
}
led_status.red = val[1];
led_status.green = val[2];
led_status.blue = val[0];
@ -122,6 +203,10 @@ void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacter
//ESP_LOGI(tag, "Color set to R:%d G:%d B:%d", led_status.red, led_status.green, led_status.blue);
break;
case SET_BRIGHT:
if(len < 5) {
ESP_LOGW(tag, "SET_BRIGHT command requires 1 parameter (brightness)");
break;
}
led_status.bright = val[0];
Lights_Set_Brightness(val[0]);
//ESP_LOGI(tag, "Bright set to %d", led_status.bright);

View File

@ -8,6 +8,7 @@ static const char* tag = "BleServer";
// Class for handling server events
/*
class ServerCallbacks : public NimBLEServerCallbacks {
void onConnect(NimBLEServer* pServer) override {
ESP_LOGI(tag, "Client connected");
@ -31,7 +32,40 @@ class ServerCallbacks : public NimBLEServerCallbacks {
}
}
};
*/
class ServerCallbacks : public NimBLEServerCallbacks {
public:
void onConnect(NimBLEServer* /*pServer*/) override {
ESP_LOGI(tag, "Client connected");
ensureAdvertising("onConnect");
}
void onDisconnect(NimBLEServer* /*pServer*/) override {
ESP_LOGI(tag, "Client disconnected");
ensureAdvertising("onDisconnect");
}
private:
void ensureAdvertising(const char* reason) {
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
if (!adv) {
ESP_LOGE(tag, "[%s] Advertising object unavailable", reason);
return;
}
if (adv->isAdvertising()) {
ESP_LOGD(tag, "[%s] Advertising already running", reason);
return;
}
if (adv->start()) {
ESP_LOGI(tag, "[%s] Advertising (re)started", reason);
} else {
ESP_LOGE(tag, "[%s] Failed to start advertising", reason);
}
}
};
/*
void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
ESP_LOGI(tag, "Initializing BLE...");
@ -72,3 +106,56 @@ void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
ESP_LOGE(tag, "Failed to get advertising object");
}
}
*/
void Init_BleServer(bool isSP110EActive, bool isUpgradeActive) {
ESP_LOGI(tag, "Initializing BLE...");
// Initialize BLE device only once
static bool deviceInitialized = false;
if (!deviceInitialized) {
NimBLEDevice::init(BTDeviceName.c_str());
deviceInitialized = true;
}
// Create server only once
static NimBLEServer* pServer = nullptr;
if (!pServer) {
pServer = NimBLEDevice::createServer();
if (!pServer) {
ESP_LOGE(tag, "Failed to create BLE server");
return;
}
static ServerCallbacks serverCallbacks;
pServer->setCallbacks(&serverCallbacks);
}
// Add services only once (no removal logic if later flags become false)
static bool sp110eAdded = false;
if (isSP110EActive && !sp110eAdded) {
Init_BLE_SP110E(pServer);
sp110eAdded = true;
ESP_LOGI(tag, "SP110E service initialized");
}
static bool upgradeAdded = false;
if (isUpgradeActive && !upgradeAdded) {
Init_UpgradeBLEService(pServer);
upgradeAdded = true;
ESP_LOGI(tag, "Upgrade service initialized");
}
// Start / ensure advertising
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
if (!adv) {
ESP_LOGE(tag, "Failed to get advertising object");
return;
}
if (!adv->isAdvertising()) {
if (!adv->start()) {
ESP_LOGE(tag, "Failed to start advertising");
} else {
ESP_LOGI(tag, "Advertising started");
}
}
}

View File

@ -1,5 +1,10 @@
#include "ColorPalettes.h"
extern const COLOR_PACK combo_colorPacks[] PROGMEM;
extern const COLOR_PACK single_colorPacks[] PROGMEM;
extern const COLOR_PACK fireColorPacks[] PROGMEM;
void Create_Red_Yellow_Violet_Palette(CRGBPalette16& customPalette) {
customPalette = CRGBPalette16(
CRGB::Red, CRGB::Yellow, CRGB::Violet,

View File

@ -222,19 +222,6 @@ void loop()
boardButtons[i]->tick();
}
}
/*
{
boardButtons[0]->tick();
}
if (boardButtons[1] != NULL)
{
boardButtons[1]->tick();
}
if (boardButtons[2] != NULL)
{
boardButtons[2]->tick();
}
*/
}
// Temperature Monitor
@ -246,7 +233,7 @@ void loop()
if (sys_settings.tSensorSettings.enabled)
{
boardTemperature = tSensor->readTemperatureF();
// ESP_LOGD(tag, "Board T: %F", boardTemperature);
// ESP_LOGI(tag, "Board T: %F", boardTemperature);
}
// Fan Control
@ -327,10 +314,12 @@ void loop()
// Turn off white light after timeout
ON_EVERY_N_MILLISECONDS(100)
{
if(whiteTimeout > 0){
// Only decrement if timeout is active
if (whiteTimeout > 0) {
whiteTimeout--;
if(whiteTimeout == 0){
if (whiteTimeout == 0) {
Lights_Set_White(0);
ESP_LOGD(tag, "White light timeout triggered");
}
}
}

View File

@ -44,7 +44,7 @@ String dirDropdownOptions((char *)0);
String savePath((char *)0); // needed for storing file when editing a file
String savePathInput((char *)0);
const char *http_username = "admin";
const char *http_password = "admin";
const char *http_password = "12345678";
const char *param_delete_path = "delete-path";
const char *param_edit_path = "edit-path";
const char *param_dir_pad = "dir-path";
@ -109,6 +109,53 @@ void Wifi_Init()
// Wifi_Scan_for_Networks();
}
void Wifi_Load_Settings(String path)
{
// Load WiFi settings
File file = LittleFS.open(path, "r");
if (!file)
{
ESP_LOGE(tag, "Error opening %s", path.c_str());
return;
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error)
{
ESP_LOGE(tag, "Failed to deserialize %s", path.c_str());
return;
}
JsonObject wifiJson = doc.as<JsonObject>();
if (wifiJson.isNull())
{
ESP_LOGE(tag, "%s is empty", path.c_str());
return;
}
// Load AP settings
JsonObject apJson = wifiJson["wifi-ap"];
if (!apJson.isNull())
{
ap_ssid = jsonConstrainString(tag, apJson, "ssid", "ATA-AP");
ap_pass = jsonConstrainString(tag, apJson, "pass", "12345678");
local_IP.fromString(jsonConstrainString(tag, apJson, "ip", "192.168.10.1"));
gateway.fromString(jsonConstrainString(tag, apJson, "gateway", "192.168.10.1"));
subnet.fromString(jsonConstrainString(tag, apJson, "subnet", "255.255.255.0"));
}
// Load Client settings
JsonObject clientJson = wifiJson["wifi-client"];
if (!apJson.isNull())
{
client_ssid = jsonConstrainString(tag, clientJson, "ssid", "none");
client_pass = jsonConstrainString(tag, clientJson, "pass", "12345678");
}
}
bool StartWifiConnectTask(String ssid = "", String pass = "")
{
if (ssid.isEmpty() || pass.length() < 8)
@ -257,53 +304,6 @@ bool Wifi_Save_Credentials(String path)
return true;
}
void Wifi_Load_Settings(String path)
{
// Load WiFi settings
File file = LittleFS.open(path, "r");
if (!file)
{
ESP_LOGE(tag, "Error opening %s", path.c_str());
return;
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error)
{
ESP_LOGE(tag, "Failed to deserialize %s", path.c_str());
return;
}
JsonObject wifiJson = doc.as<JsonObject>();
if (wifiJson.isNull())
{
ESP_LOGE(tag, "%s is empty", path.c_str());
return;
}
// Load AP settings
JsonObject apJson = wifiJson["wifi-ap"];
if (!apJson.isNull())
{
ap_ssid = jsonConstrainString(tag, apJson, "ssid", "ATA-AP");
ap_pass = jsonConstrainString(tag, apJson, "pass", "12345678");
local_IP.fromString(jsonConstrainString(tag, apJson, "ip", "192.168.10.1"));
gateway.fromString(jsonConstrainString(tag, apJson, "gateway", "192.168.10.1"));
subnet.fromString(jsonConstrainString(tag, apJson, "subnet", "255.255.255.0"));
}
// Load Client settings
JsonObject clientJson = wifiJson["wifi-client"];
if (!apJson.isNull())
{
client_ssid = jsonConstrainString(tag, clientJson, "ssid", "none");
client_pass = jsonConstrainString(tag, clientJson, "pass", "12345678");
}
}
void Wifi_Scan_for_Networks()
{
// Start a scan for available networks
@ -577,10 +577,18 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
// Firmware Update Handlers
server.on("/upgrade/check", HTTP_GET, [](AsyncWebServerRequest *request)
{
//String newVersion;
// Ensure updateUrl is loaded (function resides in AppUpgrade.cpp)
loadUpdateJson();
//bool avai = checkManifest(FIRMWARE_VERSION, newVersion);
checkManifest(otaVersion);
// Pass nullptr bucket to use internally loaded default + subsequently set base via setBaseUrl if needed
AppUpdater updater(LittleFS, localVersion, nullptr, "update.json", "firmware.bin");
// If a dynamic URL was loaded, override base
extern String updateUrl; // declared in AppUpgrade.cpp
if(updateUrl.length()) updater.setBaseUrl(updateUrl);
if(!updater.checkManifest()){
ESP_LOGE(tag, "Manifest check failed via /upgrade/check");
} else {
otaVersion = updater.otaVersion;
}
bool avail = otaVersion > localVersion;
JsonDocument doc;

View File

@ -0,0 +1,673 @@
#include "AppUpgrade.h"
#include "esp_log.h"
#include <MD5Builder.h>
#include <LittleFS.h>
#include <memory>
#include "global.h"
#include "JsonConstrain.h"
#include "BLE_UpdateService.h"
#include <HTTPClient.h>
#include <Update.h>
static const char* TAG = "AppUpdater";
TaskHandle_t Update_Task_Handle = NULL;
TaskHandle_t versionCheckTask_Handle = NULL;
// Queue handle for firmware update messages
//QueueHandle_t updateMsgQueue = NULL;
String updateUrl = "";
Version otaVersion;
AppUpdater::AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName, const char* appBin)
: localVersion(localVersion), manifestName(manifestName), appName(appBin), fileSystem(fs), downloadBuffer(new uint8_t[BUFFER_SIZE])
{
baseUrl = bucket ? String(bucket) : String(DEFAULT_MANIFEST_URL);
// Ensure baseUrl ends with a single '/'
if(!baseUrl.endsWith("/")) baseUrl += "/";
ESP_LOGI(TAG, "AppUpdater initialized (local v%s) baseUrl=%s", localVersion.toString().c_str(), baseUrl.c_str());
}
void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) {
progressCb = callback;
}
void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const char* message) {
status = newStatus;
if (progressCb) {
progressCb(status, percentage, message);
}
}
bool AppUpdater::checkManifest() {
String url = buildUrl(manifestName);
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
// Start the HTTP client and Send GET request for manifest
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
http.end();
return false;
}
// Read the response
String payload = http.getString();
http.end();
// Parse JSON
DeserializationError error = deserializeJson(jsonManifest, payload);
ESP_LOGD(TAG, "Manifest deserialized");
if (error) {
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
return false;
}
// Check for files section
jsonFilesArray = jsonManifest["files"];
if (jsonFilesArray.isNull()) {
ESP_LOGE(TAG, "No files section in manifest");
return false;
}else{
ESP_LOGD(TAG, "%d Files found", jsonFilesArray.size());
}
// Check for version section
JsonObject jsonVersion = jsonManifest["version"];
ESP_LOGD(TAG, "Version section found");
if (jsonVersion.isNull()) {
ESP_LOGE(TAG, "No version section in manifest");
return false;
}
// Get the remote version
byte major = jsonVersion["major"] | 0;
byte minor = jsonVersion["minor"] | 0;
byte patch = jsonVersion["patch"] | 0;
otaVersion = {major, minor, patch};
//Version localVersion;
//::sscanf(localVersion, "%d.%d.%d", &localVersion.major, &localVersion.minor, &localVersion.patch);
// Check if an update is available
updateAvailable = false;
// Only mark update available if remote is strictly newer than local
if (otaVersion <= localVersion) {
ESP_LOGI(TAG, "No updates available");
return false;
}else{
updateAvailable = true;
ESP_LOGD(TAG, "Update available");
}
//ESP_LOGD(TAG, "Manifest content: %s", payload.c_str());
return true;
}
bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const char* expectedMd5) {
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
// Construct full URL
String url = buildUrl(remotePath);
ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath);
String localMd5 = getLocalMD5(localPath);
if (localMd5.equals(expectedMd5)) {
ESP_LOGI(TAG, "File already up to date: %s", localPath);
updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath);
return true;
}
// Start the download
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "Download failed: %d", httpCode);
updateProgress(UpdateStatus::ERROR, 0, "Download failed");
http.end();
return false;
}
// Get the stream and content length
WiFiClient* stream = http.getStreamPtr();
size_t contentLength = http.getSize();
// Verify and save the file
bool success = verifyAndSaveFile(stream, contentLength, localPath, expectedMd5);
http.end();
if(!success){
updateProgress( UpdateStatus::ERROR, 0, "MD5 verification failed");
}else{
updateProgress( UpdateStatus::FILE_SAVED, 100, localPath);
}
return success;
}
bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* expectedMd5)
{
MD5Builder md5;
md5.begin();
size_t totalRead = 0;
// Create temporary filename
String tempPath = String(localPath) + ".tmp";
// Open temporary file for writing
File file = fileSystem.open(tempPath.c_str(), FILE_WRITE);
if (!file) {
ESP_LOGE(TAG, "Failed to open temporary file for writing");
return false;
}
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
if (contentLength > 0) {
// Single pass with known content length
while (totalRead < contentLength) {
size_t available = stream->available();
if (available) {
size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
// Write to temp file and update MD5
if (file.write(downloadBuffer.get(), readLen) != readLen) {
ESP_LOGE(TAG, "Failed to write to temporary file");
file.close();
fileSystem.remove(tempPath.c_str());
return false;
}
md5.add(downloadBuffer.get(), readLen);
totalRead += readLen;
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 90) / contentLength , localPath);
}
yield();
}
} else {
// Unknown content length: read until stream ends
for (;;) {
size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
if (readLen == 0) {
break;
}
if (file.write(downloadBuffer.get(), readLen) != readLen) {
ESP_LOGE(TAG, "Failed to write to temporary file");
file.close();
fileSystem.remove(tempPath.c_str());
return false;
}
md5.add(downloadBuffer.get(), readLen);
totalRead += readLen;
// Progress unknown; emit periodic heartbeats at 0%
updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
yield();
}
}
file.close();
md5.calculate();
String calculatedMd5 = md5.toString();
// Verify MD5 hash
if (!calculatedMd5.equals(expectedMd5)) {
//ESP_LOGE(TAG, "MD5 mismatch for %s", localPath);
fileSystem.remove(tempPath.c_str());
return false;
}
// Replace original file with verified temp file
if (fileSystem.exists(localPath)) {
fileSystem.remove(localPath);
}
if (!fileSystem.rename(tempPath.c_str(), localPath)) {
ESP_LOGE(TAG, "Failed to rename temporary file");
fileSystem.remove(tempPath.c_str());
return false;
}
return true;
}
String AppUpdater::getLocalMD5(const char* filePath){
File file = fileSystem.open(filePath, "r");
if(!file){
ESP_LOGE(TAG, "Error opening %s...", filePath);
return String();
}
MD5Builder md5Builder;
md5Builder.begin();
size_t fileSize = file.size();
size_t totalRead = 0;
size_t readLen = 0;
while (totalRead < fileSize) {
readLen = file.readBytes(reinterpret_cast<char*>(downloadBuffer.get()), std::min(fileSize - totalRead, size_t(BUFFER_SIZE)));
md5Builder.add(downloadBuffer.get(), readLen);
totalRead += readLen;
}
md5Builder.calculate();
file.close();
return md5Builder.toString();
}
bool AppUpdater::updateFilesArray() {
int successCount = 0;
int totalFiles = jsonFilesArray.size();
ESP_LOGI(TAG, "Found %d files in manifest", totalFiles);
// Iterate over each file entry in the manifest
for (JsonObject file : jsonFilesArray) {
const char* remotePath = file["remote"];
const char* localPath = file["local"];
const char* expectedMd5 = file["md5"];
// Skip invalid entries
if (!remotePath || !localPath || !expectedMd5) {
ESP_LOGE(TAG, "Invalid file entry in manifest");
continue;
}
// Attempt to update the file
if (updateFile(remotePath, localPath, expectedMd5)) {
successCount++;
}
}
ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated", successCount, totalFiles);
return successCount == totalFiles;
}
bool AppUpdater::updateApp() {
updateProgress(UpdateStatus::MESSAGE, 0, "Starting firmware update");
// Check for firmware section in manifest
if (!jsonManifest["firmware"].is<JsonObject>() || !jsonManifest["firmware"]["md5"].is<const char*>()) {
ESP_LOGE(TAG, "Invalid firmware section in manifest");
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Invalid firmware section in manifest");
return false;
}
// Get the firmware MD5 hash and URL
const char* expectedMd5 = jsonManifest["firmware"]["md5"];
String firmwareUrl = buildUrl(appName);
// Download the firmware
HTTPClient http;
http.begin(firmwareUrl);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "Firmware download failed: %d", httpCode);
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed");
http.end();
return false;
}
// Check available space
size_t firmwareSize = http.getSize();
if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) {
ESP_LOGE(TAG, "Firmware: Not enough space for update");
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update");
http.end();
return false;
}
// Set up MD5 checking
MD5Builder md5;
md5.begin();
// Download and verify firmware
WiFiClient* stream = http.getStreamPtr();
if (firmwareSize > 0) {
size_t remaining = firmwareSize;
while (remaining > 0) {
size_t chunk = std::min(remaining, size_t(BUFFER_SIZE));
size_t read = stream->readBytes(downloadBuffer.get(), chunk);
// Check for timeout
if (read == 0) {
ESP_LOGE(TAG, "Read timeout");
Update.abort();
http.end();
return false;
}
// Update MD5 and write firmware
md5.add(downloadBuffer.get(), read);
if (Update.write(downloadBuffer.get(), read) != read) {
ESP_LOGE(TAG, "Write failed");
Update.abort();
http.end();
return false;
}
remaining -= read;
updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware");
}
} else {
// Unknown size: stream until end
for (;;) {
size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
if (read == 0) break;
md5.add(downloadBuffer.get(), read);
if (Update.write(downloadBuffer.get(), read) != read) {
ESP_LOGE(TAG, "Write failed");
Update.abort();
http.end();
return false;
}
updateProgress(UpdateStatus::DOWNLOADING, 0, "firmware");
}
}
// Verify MD5
md5.calculate();
String calculatedMd5 = md5.toString();
if (!calculatedMd5.equals(expectedMd5)) {
ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str());
updateProgress(UpdateStatus::MD5_FAILED, 0, "Firmware: MD5 mismatch");
Update.abort();
http.end();
return false;
}
// Finish update
if (!Update.end()) {
ESP_LOGE(TAG, "Update end failed");
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Update failed");
http.end();
return false;
}
http.end();
updateProgress(UpdateStatus::COMPLETE, 0, "Firmware: Complete");
return true;
}
bool AppUpdater::IsUpdateAvailable(){
return updateAvailable;
}
String AppUpdater::buildUrl(const char* path) const {
if(!path || !*path) return baseUrl; // just base
String p(path);
// If already absolute URL, pass through
if(p.startsWith("http://") || p.startsWith("https://")) return p;
// Strip leading slashes to avoid double
while(p.startsWith("/")) p.remove(0,1);
// Ensure baseUrl has single trailing slash
String b = baseUrl;
if(!b.endsWith("/")) b += "/";
return b + p;
}
AsyncEventSource* eventProgress = nullptr;
void startFirmwareUpdateTask(AsyncEventSource* evProg) {
eventProgress = evProg;
if(Update_Task_Handle) {
ESP_LOGW(TAG, "Firmware update task already running");
return;
}
xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 1024*8, NULL, 1, &Update_Task_Handle);
}
void firmwareUpdateTask(void* parameter) {
static const char* TAG = "UpdateTask";
AppUpdater* updater = nullptr;
try {
loadUpdateJson();
// Initialize updater
updater = new AppUpdater(LittleFS, localVersion, updateUrl.c_str(), "update.json", "firmware.bin");
updater->setProgressCallback(updateProgress);
ESP_LOGI(TAG, "Starting update check from: %s", updateUrl.c_str());
// Check and perform updates
if (!updater->checkManifest()) { throw std::runtime_error("Failed to check manifest"); }
if (updater->IsUpdateAvailable()) {
ESP_LOGI(TAG, "Update available, updating files...");
if (!updater->updateFilesArray()) {
throw std::runtime_error("Failed to update files");
}
ESP_LOGI(TAG, "Updating firmware...");
if (!updater->updateApp()) {
throw std::runtime_error("Failed to update firmware");
}
ESP_LOGI(TAG, "Update successful, restarting...");
sendUpdateMessage("Restarting ", true, 100);
vTaskDelay(2000);
ESP.restart();
}
} catch (const std::exception& e) {
ESP_LOGE(TAG, "Update failed: %s", e.what());
}
end:
delete updater;
Update_Task_Handle = NULL;
vTaskDelete(NULL);
}
void startVersionCheckTask() {
if(versionCheckTask_Handle != NULL) {
ESP_LOGW(TAG, "Version Check Tak already running");
return;
}
xTaskCreate(versionCheckTask, "VersionCheckTask", 1024*8, NULL, 1, &versionCheckTask_Handle);
}
void versionCheckTask(void* parameter){
if(updateUrl == ""){
loadUpdateJson();
}
if(checkManifest(otaVersion) == false){
ESP_LOGE(TAG, "Error checking manifest");
}
versionCheckTask_Handle = NULL;
vTaskDelete(NULL);
}
void loadUpdateJson(void) {
try {
ESP_LOGD(TAG, "loadUpdateJaon function...");
if(updateUrl == "") {
String updateJsonPath = "/system/update.json";
// Read and parse update.json
File file = LittleFS.open(updateJsonPath);
if (!file) {
throw std::runtime_error("Failed to open update.json");
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error) { throw std::runtime_error("Failed to parse update.json"); }
// Get update configuration
JsonObject jObj = doc.as<JsonObject>();
String folderName = jsonConstrainString(TAG, jObj, "folder", "latest/");
String baseUrl = jsonConstrainString(TAG, jObj, "baseurl", "https://s3-minio.boothwizard.com/boothifier/");
updateUrl = baseUrl + folderName;
ESP_LOGD(TAG, "updateUrl: %s", updateUrl.c_str());
}
} catch (const std::exception& e) {
ESP_LOGE(TAG, "Update failed: %s", e.what());
}
}
void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const char* message = nullptr) {
char buffer[128];
const char* msg;
bool isComplete = false;
const char* safeMsg = message ? message : "";
switch (newStatus) {
case AppUpdater::UpdateStatus::IDLE:
snprintf(buffer, sizeof(buffer), "Update idle");
msg = buffer;
break;
case AppUpdater::UpdateStatus::MESSAGE:
msg = message ? message : "";
break;
case AppUpdater::UpdateStatus::DOWNLOADING:
snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", safeMsg, percentage);
msg = buffer;
break;
case AppUpdater::UpdateStatus::VERIFYING:
snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", safeMsg, percentage);
msg = buffer;
break;
case AppUpdater::UpdateStatus::FILE_SKIPPED:
snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", safeMsg);
msg = buffer;
break;
case AppUpdater::UpdateStatus::FILE_SAVED:
snprintf(buffer, sizeof(buffer), "%s: File Saved", safeMsg);
msg = buffer;
break;
case AppUpdater::UpdateStatus::MD5_FAILED:
snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", safeMsg);
msg = buffer;
break;
case AppUpdater::UpdateStatus::COMPLETE:
snprintf(buffer, sizeof(buffer), "Firmware Update Complete!!!");
msg = buffer;
isComplete = true;
break;
case AppUpdater::UpdateStatus::ERROR:
snprintf(buffer, sizeof(buffer), "Error!: %s", safeMsg);
msg = buffer;
break;
default:
snprintf(buffer, sizeof(buffer), "Unknown update status: %d", (int)newStatus);
msg = buffer;
break;
}
ESP_LOGI(TAG, "%s", msg);
sendUpdateMessage(msg, isComplete, percentage);
}
void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
if(eventProgress && eventProgress->count() > 0) {
JsonDocument jsonDoc;
jsonDoc["message"] = message;
jsonDoc["complete"] = complete;
jsonDoc["progress"] = progress;
String strMessage;
serializeJson(jsonDoc, strMessage);
eventProgress->send(strMessage.c_str(), "update", millis());
}
else{
ESP_LOGW(TAG, "No clients connected to event source");
}
bleUpgrade_send_message(message);
}
bool checkManifest(Version& remoteVersion) {
const char* TAG = "manifestCheck";
String url = updateUrl + "update.json";
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
// Start the HTTP client and send GET request for manifest
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
http.end();
return false;
}
// Read the response
String payload = http.getString();
http.end();
//ESP_LOGD(TAG, "%s", payload.c_str());
// Parse JSON
JsonDocument jsonManifest;
DeserializationError error = deserializeJson(jsonManifest, payload);
if (error) {
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
return false;
}
// Check for version section
JsonObject jsonVersion = jsonManifest["version"];
if (jsonVersion.isNull()) {
ESP_LOGE(TAG, "No version section in manifest");
return false;
}
// Get the remote version
byte major = jsonVersion["major"] | 0;
byte minor = jsonVersion["minor"] | 0;
byte patch = jsonVersion["patch"] | 0;
remoteVersion = {major, minor, patch};
ESP_LOGI(TAG, "Remote version: %s", remoteVersion.toString().c_str());
return true;
}
/*
void setup() {
Serial.begin(115200);
// Initialize WiFi connection first
// ... WiFi connection code ...
// Initialize filesystem
if(!LittleFS.begin()) {
Serial.println("LittleFS Mount Failed");
return;
}
// Create updater instance with:
// - Current version: "1.0.0"
// - Update server URL: "https://my-update-server.com/"
// - Filesystem: LittleFS
AppUpdater updater("1.0.0", "https://storage.googleapis.com/boothifier/latest/", LittleFS);
// Set progress callback
updater.setProgressCallback([](int progress) {
Serial.printf("Update progress: %d%%\n", progress);
});
// Check and update firmware
if (updater.checkAndUpdate()) {
Serial.println("Update successful! Rebooting...");
ESP.restart();
}
// Update specific files from manifest
int updatedFiles = updater.updateFilesFromManifest("test_update.json");
Serial.printf("Updated %d files\n", updatedFiles);
}
*/