-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFirmwareUpdateModule.h
More file actions
150 lines (136 loc) · 7.71 KB
/
Copy pathFirmwareUpdateModule.h
File metadata and controls
150 lines (136 loc) · 7.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#pragma once
#include "core/MoonModule.h"
#include "core/build_info.h" // kVersion / kRelease / kBuildDate / kFirmwareName
#include "platform/platform.h" // firmwareSize / firmwarePartition
#include <cstdint>
#include <cstdio>
#include <cstring>
namespace mm {
// FirmwareUpdateModule — surfaces OTA flash progress as live read-only controls.
//
// Not user-configurable: ensureInfraModules() recreates it on every boot if
// absent (same safety net as NetworkModule). The actual flash is driven by
// POST /api/firmware/url in HttpServerModule — that handler spawns the OTA
// task via platform::http_fetch_to_ota(), which writes to two file-scope
// globals (g_otaStatus, g_otaBytesRead, g_otaBytesTotal). This module polls
// them in loop1s() and
// copies into its bound control buffers so the WebSocket state push picks up
// the change at 1 Hz. The shared-buffer + 1 Hz poll pattern is the simplest
// way to bridge a FreeRTOS task and a MoonModule on the scheduler thread
// without locks. No synchronisation: torn reads of
// the status string are acceptable for display-only fields.
//
// On desktop (platform::hasOta == false) the controls still exist for UI
// uniformity but the route returns 501; status stays "idle" forever.
// File-scope globals shared with the OTA route + the platform-layer task.
// Declared `inline` (C++17) so multiple translation units that include the
// header still share one storage instance (the header is included from
// HttpServerModule.cpp via the route, and from the module instantiation
// site in main.cpp — both must see the same g_otaStatus). An anonymous
// namespace would do the opposite — per-TU storage — which is why we
// use `inline` here.
//
// g_otaBytesRead / g_otaBytesTotal are the live byte counters the task writes.
// The UI renders them as "X KB / Y KB" via the existing progress control. The
// total starts at 0 (unknown) and flips to the real image size as soon as
// esp_https_ota_get_image_size returns it; the module's loop1s() re-binds
// the progress control when that transition happens so the static total
// captured by addProgress reflects reality (addProgress takes total by value,
// not pointer — re-bind is the cheaper alternative to widening that contract).
inline char g_otaStatus[64] = "idle";
inline uint32_t g_otaBytesRead = 0;
inline uint32_t g_otaBytesTotal = 0;
class FirmwareUpdateModule : public MoonModule {
public:
// Diagnostics keep ticking regardless of the user toggle; matches
// SystemModule + NetworkModule. The user can't easily re-enable a
// disabled diagnostic module without it being visible.
bool respectsEnabled() const override { return false; }
void setup() override {
// Copy the file-scope globals into the bound buffers on boot so the
// first WS state push surfaces a coherent "idle" / 0 pair.
std::strncpy(statusStr_, g_otaStatus, sizeof(statusStr_) - 1);
statusStr_[sizeof(statusStr_) - 1] = '\0';
publishStatus();
bytesRead_ = g_otaBytesRead;
totalSnap_ = g_otaBytesTotal;
// Firmware identity (static for this build). version is PURE SEMVER (kVersion from
// library.json): a clean "2.0.0" on a stable release, or a prerelease like "2.1.0-dev" on a
// moving/dev build (semver.org §9 — the prerelease suffix is how a not-yet-released build is
// expressed). The release channel is derivable from the version itself (a prerelease suffix
// means "not a stable release"), so it is NOT mixed into the string; kRelease stays the
// separate build-channel tag (which git tag this binary shipped under) without polluting the
// machine-comparable version. This keeps `version` a clean semver the UI's update check can
// compare against the newest GitHub release.
std::snprintf(versionStr_, sizeof(versionStr_), "%s", kVersion);
std::snprintf(buildStr_, sizeof(buildStr_), "%s", kBuildDate);
std::snprintf(firmwareStr_, sizeof(firmwareStr_), "%s", kFirmwareName);
}
void onBuildControls() override {
// Firmware identity (static), then OTA progress. firmwarePartition is the running app
// partition's usage; queried here (idempotent, no I/O) so the gate sees a real total.
controls_.addReadOnly("version", versionStr_, sizeof(versionStr_));
controls_.addReadOnly("build", buildStr_, sizeof(buildStr_));
controls_.addReadOnly("firmware", firmwareStr_, sizeof(firmwareStr_));
firmwareSizeVal_ = static_cast<uint32_t>(platform::firmwareSize());
totalFlashVal_ = static_cast<uint32_t>(platform::firmwarePartition());
if (totalFlashVal_ > 0) {
controls_.addProgress("firmwarePartition", firmwareSizeVal_, totalFlashVal_);
}
// OTA status goes through MoonModule::setStatus() (the per-module status
// slot every module shares), not a bespoke read-only control — same
// choice DevicesModule made. The error-prefixed states map to
// Severity::Error; everything else is neutral. See publishStatus().
//
// Total is captured by value into the descriptor's `aux`; we re-bind
// (via markDirty → HttpServerModule rebuildControls) when totalSnap_
// changes. Initially 0; the UI shows "0KB / 0KB" until esp_https_ota
// reports the image size, then "X KB / 1297KB" for the rest.
controls_.addProgress("update_pct", bytesRead_, totalSnap_);
}
void loop1s() override {
// Poll the OTA task's progress + status. No locks: the writer is
// a single task, reads are atomic at this granularity, and a torn
// read shows as a brief mid-update glimpse — visually harmless.
std::strncpy(statusStr_, g_otaStatus, sizeof(statusStr_) - 1);
statusStr_[sizeof(statusStr_) - 1] = '\0';
publishStatus();
bytesRead_ = g_otaBytesRead;
// Re-bind on total transition. Only fires once per OTA (and once on
// any later OTA the user starts — we deliberately don't reset the
// total to 0 between updates; the previous value is a fine starting
// estimate until the new task reports the new size). rebuildControls
// re-runs onBuildControls() so the addProgress' captured `aux` (total)
// is refreshed to the new totalSnap_ value.
if (g_otaBytesTotal != totalSnap_) {
totalSnap_ = g_otaBytesTotal;
rebuildControls();
}
}
// Point the shared status slot at our owned buffer, choosing the severity
// from the status text: the platform OTA task prefixes every failure with
// "error: " (see platform_esp32_ota.cpp), so that prefix is the Error gate.
// "idle" is the quiescent state and reads better as no banner than as an
// info banner, so it clears the slot. setStatus doesn't copy — statusStr_
// outlives every call, so the pointer stays valid.
void publishStatus() {
if (std::strcmp(statusStr_, "idle") == 0) {
clearStatus();
} else {
setStatus(statusStr_,
std::strncmp(statusStr_, "error:", 6) == 0 ? Severity::Error
: Severity::Status);
}
}
private:
char statusStr_[64] = "idle";
uint32_t bytesRead_ = 0;
uint32_t totalSnap_ = 0;
// Firmware identity (static for this build) + the running app-partition usage.
char versionStr_[32] = {}; // pure semver — e.g. "2.0.0" or "2.1.0-dev.7"
char buildStr_[24] = {};
char firmwareStr_[24] = {}; // build variant name, e.g. "esp32s3-n16r8"
uint32_t firmwareSizeVal_ = 0; // bytes used in the app partition
uint32_t totalFlashVal_ = 0; // app partition size
};
} // namespace mm