Skip to content

Commit 0486f7a

Browse files
committed
feat(update): add phase tracking and improve download reliability
- Add phase tracking (firmware/filesystem/complete) to update process for better observability - Add Content-Length verification to detect incomplete downloads - Add yield() in download loop to allow async web server to handle status requests - Extend restart delay to 5 seconds to allow status poll to see COMPLETE state - Improve error messages with specific failure details (incomplete, write failures) - Simplify release workflow by removing duplicate latest release creation step - Cast size values to unsigned long for proper logging display
1 parent 170417e commit 0486f7a

5 files changed

Lines changed: 40 additions & 64 deletions

File tree

.github/workflows/release.yml

Lines changed: 3 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ jobs:
198198
with:
199199
tag_name: ${{ steps.version.outputs.version }}
200200
name: Release ${{ steps.version.outputs.version }}
201-
make_latest: false
201+
make_latest: true
202202
files: |
203203
artifacts/firmware.bin
204204
artifacts/littlefs.bin
@@ -231,63 +231,9 @@ jobs:
231231
env:
232232
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
233233

234-
- name: Delete Existing Latest Release & Tag
235-
continue-on-error: true
236-
run: |
237-
# Delete the release via GitHub API (using gh CLI)
238-
echo "Attempting to delete existing 'latest' release..."
239-
gh release delete latest --yes --cleanup-tag 2>/dev/null || echo "No existing latest release to delete"
240-
env:
241-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
242-
243-
- name: Set Latest Tag to Current Commit
234+
- name: Update Latest Tag
244235
run: |
245236
git config user.name "github-actions[bot]"
246237
git config user.email "github-actions[bot]@users.noreply.github.com"
247-
git tag -fa latest -m "Update latest release to ${{ steps.version.outputs.version }}"
238+
git tag -fa latest -m "Update latest tag to ${{ steps.version.outputs.version }}"
248239
git push origin latest --force
249-
250-
- name: Create Latest Release with Current Timestamp
251-
uses: softprops/action-gh-release@v2
252-
with:
253-
tag_name: latest
254-
name: Latest Release
255-
prerelease: false
256-
make_latest: true
257-
files: |
258-
artifacts/firmware.bin
259-
artifacts/littlefs.bin
260-
artifacts/firmware_merged.bin
261-
artifacts/version_manifest.json
262-
artifacts/firmware.bin.sha256
263-
artifacts/littlefs.bin.sha256
264-
artifacts/firmware_merged.bin.sha256
265-
body: |
266-
# Latest Firmware Release
267-
268-
**Version:** ${{ steps.version.outputs.version }}
269-
270-
${{ env.CHANGELOG }}
271-
272-
This is the latest stable release. Use these artifacts for the most current firmware installation.
273-
274-
## Download Firmware
275-
- **Merged Binary (recommended for OTA):** firmware_merged.bin
276-
- **Firmware Binary:** firmware.bin
277-
- **Filesystem Binary:** littlefs.bin
278-
- **Version Manifest:** version_manifest.json (for OTA updates)
279-
280-
## Checksums
281-
**Firmware SHA256:** ${{ env.FIRMWARE_SHA256 }}
282-
**Filesystem SHA256:** ${{ env.FILESYSTEM_SHA256 }}
283-
**Merged SHA256:** ${{ env.MERGED_SHA256 }}
284-
285-
## Build Artifacts
286-
- Firmware Size: ${{ env.FIRMWARE_SIZE }} bytes
287-
- Filesystem Size: ${{ env.FILESYSTEM_SIZE }} bytes
288-
- Merged Size: ${{ env.MERGED_SIZE }} bytes
289-
- Git Commit: ${{ github.sha }}
290-
291-
For version history, see [Releases](https://github.com/${{ github.repository }}/releases)
292-
env:
293-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

lib/HAL_ESP32/HAL_ESP32.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,13 +856,22 @@ bool HAL_ESP32::httpGetStream(const String& url, HttpDataCallback on_data,
856856
return false;
857857
}
858858
}
859+
yield(); // Allow async web server to handle status requests
859860
}
860861
} else {
861862
delay(10);
862863
}
863864
}
864865

865866
client.stop();
867+
868+
// Verify complete download if Content-Length was provided
869+
if (contentLength > 0 && bytesDownloaded != contentLength) {
870+
Serial.printf("[HAL_ESP32] httpGetStream: incomplete download %u/%u bytes\n",
871+
bytesDownloaded, contentLength);
872+
return false;
873+
}
874+
866875
return true;
867876
}
868877

lib/UpdateManager/UpdateManager.cpp

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,13 @@ void UpdateManager::installUpdate(bool skip_filesystem, bool force) {
203203

204204
// --- Install firmware ---
205205
if (manifest_.firmware.url.length() > 0 && manifest_.firmware.size_bytes > 0) {
206+
phase_ = "firmware";
206207
setStatus(UpdateStatus::DOWNLOADING, true);
207208
bytes_downloaded_ = 0;
208209
total_bytes_ = manifest_.firmware.size_bytes;
209210
progress_percent_ = 0;
210211

211-
logger.logInfo("Downloading firmware: " + String(manifest_.firmware.size_bytes) + " bytes");
212+
logger.logInfo("Downloading firmware: " + String((unsigned long)manifest_.firmware.size_bytes) + " bytes");
212213

213214
// Begin OTA firmware partition
214215
if (!hal_->otaBegin(manifest_.firmware.size_bytes, 0)) {
@@ -231,10 +232,12 @@ void UpdateManager::installUpdate(bool skip_filesystem, bool force) {
231232

232233
if (!downloadSuccess) {
233234
hal_->otaAbort();
234-
setError(UpdateError::DOWNLOAD_FAILED, "Firmware download failed");
235+
setError(UpdateError::DOWNLOAD_FAILED, "Firmware download failed or incomplete");
235236
return;
236237
}
237238

239+
logger.logInfo("Firmware download complete: " + String((unsigned long)bytes_downloaded_) + " bytes, finalizing...");
240+
238241
// Finalize firmware OTA
239242
setStatus(UpdateStatus::INSTALLING);
240243
if (!hal_->otaEnd(true)) {
@@ -248,12 +251,13 @@ void UpdateManager::installUpdate(bool skip_filesystem, bool force) {
248251

249252
// --- Install filesystem (if not skipping) ---
250253
if (!skip_filesystem && manifest_.filesystem.url.length() > 0 && manifest_.filesystem.size_bytes > 0) {
254+
phase_ = "filesystem";
251255
bytes_downloaded_ = 0;
252256
total_bytes_ = manifest_.filesystem.size_bytes;
253257
progress_percent_ = 0;
254258
setStatus(UpdateStatus::DOWNLOADING, true);
255259

256-
logger.logInfo("Downloading filesystem: " + String(manifest_.filesystem.size_bytes) + " bytes");
260+
logger.logInfo("Downloading filesystem: " + String((unsigned long)manifest_.filesystem.size_bytes) + " bytes");
257261

258262
// End current filesystem before flashing
259263
hal_->fsEnd();
@@ -268,6 +272,7 @@ void UpdateManager::installUpdate(bool skip_filesystem, bool force) {
268272
[this](const uint8_t* data, size_t len, uint32_t downloaded, uint32_t total) -> bool {
269273
size_t written = hal_->otaWrite(data, len);
270274
if (written != len) {
275+
logger.logError("Filesystem OTA write failed: wrote " + String((unsigned long)written) + "/" + String((unsigned long)len) + " bytes");
271276
return false;
272277
}
273278
bytes_downloaded_ = downloaded;
@@ -279,10 +284,12 @@ void UpdateManager::installUpdate(bool skip_filesystem, bool force) {
279284
if (!fsDownloadSuccess) {
280285
hal_->otaAbort();
281286
hal_->fsBegin();
282-
setError(UpdateError::DOWNLOAD_FAILED, "Filesystem download failed");
287+
setError(UpdateError::DOWNLOAD_FAILED, "Filesystem download failed or incomplete");
283288
return;
284289
}
285290

291+
logger.logInfo("Filesystem download complete: " + String((unsigned long)bytes_downloaded_) + " bytes, finalizing...");
292+
286293
setStatus(UpdateStatus::INSTALLING);
287294
if (!hal_->otaEnd(true)) {
288295
hal_->otaAbort();
@@ -294,10 +301,11 @@ void UpdateManager::installUpdate(bool skip_filesystem, bool force) {
294301
logger.logInfo("Filesystem update installed successfully");
295302
}
296303

304+
phase_ = "complete";
297305
setStatus(UpdateStatus::COMPLETE, true);
298306
progress_percent_ = 100;
299-
logger.logWarning("Update complete! Restarting device...");
300-
delay(1000);
307+
logger.logWarning("Update complete! Restarting device in 5 seconds...");
308+
delay(5000); // Allow time for status poll to see COMPLETE
301309
hal_->restart();
302310
}
303311

@@ -326,6 +334,7 @@ UpdateStatusSnapshot UpdateManager::getStatus() const {
326334
snapshot.bytes_downloaded = bytes_downloaded_;
327335
snapshot.total_bytes = total_bytes_;
328336
snapshot.error_message = last_error_message_;
337+
snapshot.phase = phase_;
329338
snapshot.last_check_time = last_check_time_;
330339
snapshot.next_check_time = 0; // TODO: Calculate based on settings
331340

@@ -397,6 +406,7 @@ JsonDocument UpdateManager::getStatusResponseJson() const {
397406

398407
doc["status"] = statusStr;
399408
doc["progress"] = progress_percent_;
409+
doc["phase"] = phase_;
400410
doc["last_check"] = last_check_time_;
401411
doc["error"] = last_error_message_;
402412

@@ -414,6 +424,7 @@ void UpdateManager::reset() {
414424
status_ = UpdateStatus::IDLE;
415425
error_ = UpdateError::NONE;
416426
last_error_message_ = "";
427+
phase_ = "";
417428
progress_percent_ = 0;
418429
bytes_downloaded_ = 0;
419430
total_bytes_ = 0;

lib/UpdateManager/UpdateManager.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ struct UpdateStatusSnapshot {
7575
uint32_t bytes_downloaded;
7676
uint32_t total_bytes;
7777
String error_message;
78+
String phase; ///< Current phase: "firmware", "filesystem", or ""
7879
unsigned long last_check_time;
7980
unsigned long next_check_time;
8081
};
@@ -115,6 +116,9 @@ class UpdateManager {
115116
// Error tracking
116117
String last_error_message_;
117118

119+
// Phase tracking
120+
String phase_;
121+
118122
/**
119123
* @brief Compare two semantic versions
120124
*

web/src/Update.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface UpdateCheckResult {
2525
interface UpdateStatus {
2626
status: 'idle' | 'checking' | 'available' | 'current' | 'downloading' | 'verifying' | 'installing' | 'complete' | 'error';
2727
progress: number;
28+
phase: string;
2829
last_check: number;
2930
error: string;
3031
}
@@ -279,7 +280,12 @@ function Update() {
279280
<div class="card bg-base-200 card-sm shadow-sm mb-4">
280281
<div class="card-body">
281282
<h3 class="font-bold">Update Progress</h3>
282-
<p>Status: {updateStatus()?.status ?? 'starting...'}</p>
283+
<p>
284+
Status: {updateStatus()?.status ?? 'starting...'}
285+
<Show when={updateStatus()?.phase}>
286+
{' '}({updateStatus()!.phase})
287+
</Show>
288+
</p>
283289
<progress class="progress progress-primary w-full" value={updateStatus()?.progress ?? 0} max="100"></progress>
284290
<p>{updateStatus()?.progress ?? 0}%</p>
285291
</div>

0 commit comments

Comments
 (0)