CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
FirmwareCheck.cpp
Go to the documentation of this file.
3
5#include "cdc_ui/ViewStack.h"
6#include "cdc_ui/I18n.h"
7#include "cdc_core/Raii.h"
8#include "cdc_hal/IRtc.h"
10#include "cdc_log.h"
11
12#include "esp_crt_bundle.h"
13#include "esp_http_client.h"
14#include "esp_attr.h"
15#include "nvs.h"
16#include "freertos/FreeRTOS.h"
17#include "freertos/task.h"
18
19#include <atomic>
20#include <cstdarg>
21#include <cstdio>
22#include <cstdlib>
23#include <cstring>
24#include <ctime>
25
26namespace cdc::ui {
27namespace {
28
29static constexpr const char* TAG = "FWCHECK";
30
31static constexpr const char* kReleaseApiUrl =
32 "https://api.github.com/repos/krim404/cdc-badge-os/releases/latest";
33static constexpr const char* kReleasesUrl = "github.com/krim404/cdc-badge-os/releases";
34
35static constexpr uint32_t kHttpTimeoutMs = 8000;
36
37// NVS namespace/keys for the last upstream check (also read by the VERSION
38// serial command via firmwareCheckLastResult()).
39static constexpr const char* kNvsNs = "fwchk";
40static constexpr const char* kNvsKeyVer = "ver";
41static constexpr const char* kNvsKeyDate = "date";
42
44enum class Status : uint8_t {
45 NeedsWifi,
46 Checking,
47 UpToDate,
48 UpdateAvailable,
49 CheckFailed,
50};
51
52// Worker handshake: the UI task starts the worker, the worker fills s_result /
53// s_foundTag and publishes them via the release-store on s_state. The UI task
54// consumes the result in onTick() (acquire-load). Only one check runs at a time.
55enum : uint8_t { kIdle = 0, kRunning = 1, kDone = 2 };
56std::atomic<uint8_t> s_state{kIdle};
57Status s_result = Status::CheckFailed;
58char s_foundTag[24] = {0};
59bool s_heldWifi = false;
60
61// Only the HTTPS GET runs on this worker. It does NOT disable the flash cache,
62// so its stack lives in PSRAM (internal RAM is scarce, especially with WiFi up),
63// exactly like the browser fetch worker. The cache-disabling steps (WiFi
64// connect, NTP sync, NVS store) run on the UI task instead, whose stack already
65// lives in internal RAM. The response scan buffer is also PSRAM-resident.
66constexpr size_t kWorkerStackBytes = 20u * 1024u;
67EXT_RAM_BSS_ATTR StackType_t s_workerStack[kWorkerStackBytes / sizeof(StackType_t)];
68StaticTask_t s_workerTcb;
69
70constexpr size_t kRespCap = 16u * 1024u;
71EXT_RAM_BSS_ATTR char s_respBuf[kRespCap];
72
73class FirmwareCheckView : public InfoView {
74public:
75 void onTick(uint32_t nowMs) override;
76};
77
78FirmwareCheckView s_view;
79
81const char* stripV(const char* v) {
82 return (v && (*v == 'v' || *v == 'V')) ? v + 1 : v;
83}
84
87int compareSemver(const char* a, const char* b) {
88 for (int i = 0; i < 3; ++i) {
89 char* aEnd = nullptr;
90 char* bEnd = nullptr;
91 long na = std::strtol(a, &aEnd, 10);
92 long nb = std::strtol(b, &bEnd, 10);
93 if (na != nb) return (na > nb) ? 1 : -1;
94 a = (*aEnd == '.') ? aEnd + 1 : aEnd;
95 b = (*bEnd == '.') ? bEnd + 1 : bEnd;
96 }
97 return 0;
98}
99
102bool parseTagName(const char* json, char* out, size_t cap) {
103 const char* key = std::strstr(json, "\"tag_name\"");
104 if (!key) return false;
105 const char* p = key + std::strlen("\"tag_name\"");
106 while (*p == ' ' || *p == '\t' || *p == ':' || *p == '\n' || *p == '\r') ++p;
107 if (*p != '"') return false;
108 ++p;
109 size_t i = 0;
110 while (*p && *p != '"' && i + 1 < cap) out[i++] = *p++;
111 out[i] = '\0';
112 return i > 0;
113}
114
116void composeLastResult(char* out, size_t cap, const char* ver, const char* date) {
117 const char* vp = (ver[0] == 'v' || ver[0] == 'V') ? "" : "v";
118 if (date && date[0]) {
119 std::snprintf(out, cap, "%s%s (checked: %s)", vp, ver, date);
120 } else {
121 std::snprintf(out, cap, "%s%s", vp, ver);
122 }
123}
124
126void storeLastCheck(const char* tag) {
127 char date[12] = {0};
128 auto* rtc = hal::getRtcInstance();
129 if (rtc && rtc->isTimeSet()) {
130 struct tm t{};
131 rtc->getTime(&t);
132 unsigned dd = static_cast<unsigned>(t.tm_mday) % 100u;
133 unsigned mm = (static_cast<unsigned>(t.tm_mon) + 1u) % 100u;
134 unsigned yy = static_cast<unsigned>(t.tm_year + 1900) % 100u;
135 std::snprintf(date, sizeof(date), "%02u.%02u.%02u", dd, mm, yy);
136 }
137 core::NvsScope nvs(kNvsNs, NVS_READWRITE);
138 if (nvs.status() != ESP_OK) return;
139 nvs_set_str(nvs, kNvsKeyVer, tag);
140 nvs_set_str(nvs, kNvsKeyDate, date);
141 nvs.commit();
142}
143
147Status queryUpstream(char* foundTag, size_t cap) {
148 foundTag[0] = '\0';
149 esp_http_client_config_t cfg{};
150 cfg.url = kReleaseApiUrl;
151 cfg.method = HTTP_METHOD_GET;
152 cfg.timeout_ms = static_cast<int>(kHttpTimeoutMs);
153 cfg.crt_bundle_attach = esp_crt_bundle_attach;
154 cfg.buffer_size = 4096;
155 cfg.buffer_size_tx = 1024;
156
157 esp_http_client_handle_t h = esp_http_client_init(&cfg);
158 if (!h) return Status::CheckFailed;
159 esp_http_client_set_header(h, "User-Agent", "cdc-badge-os");
160 esp_http_client_set_header(h, "Accept", "application/vnd.github+json");
161
162 Status result = Status::CheckFailed;
163 if (esp_http_client_open(h, 0) == ESP_OK) {
164 esp_http_client_fetch_headers(h);
165 if (esp_http_client_get_status_code(h) == 200) {
166 size_t pos = 0;
167 int n;
168 while (pos < kRespCap - 1 &&
169 (n = esp_http_client_read(h, s_respBuf + pos, kRespCap - 1 - pos)) > 0) {
170 pos += static_cast<size_t>(n);
171 }
172 s_respBuf[pos] = '\0';
173
174 char tag[24];
175 if (parseTagName(s_respBuf, tag, sizeof(tag))) {
176 std::strncpy(foundTag, tag, cap - 1);
177 foundTag[cap - 1] = '\0';
178 result = (compareSemver(stripV(tag), APP_VERSION) > 0)
179 ? Status::UpdateAvailable
180 : Status::UpToDate;
181 } else {
182 LOG_W(TAG, "tag_name not found in response");
183 }
184 } else {
185 LOG_W(TAG, "HTTP status %d", esp_http_client_get_status_code(h));
186 }
187 } else {
188 LOG_W(TAG, "HTTP open failed");
189 }
190 esp_http_client_close(h);
191 esp_http_client_cleanup(h);
192 return result;
193}
194
196void renderStatus(Status st) {
197 char buf[256];
198 size_t pos = 0;
199 auto append = [&](const char* fmt, ...) {
200 if (pos >= sizeof(buf)) return;
201 va_list args;
202 va_start(args, fmt);
203 int w = std::vsnprintf(buf + pos, sizeof(buf) - pos, fmt, args);
204 va_end(args);
205 if (w > 0) {
206 size_t ww = static_cast<size_t>(w);
207 pos += (ww < sizeof(buf) - pos) ? ww : (sizeof(buf) - pos - 1);
208 }
209 };
210
211 append("Firmware: %s\n%s: %s\n", APP_VERSION, ui::tr("core.fw_api_level"),
213 char last[64];
214 if (firmwareCheckLastResult(last, sizeof(last))) append("%s\n", last);
215 append("\n");
216
217 switch (st) {
218 case Status::NeedsWifi:
219 append("%s", ui::tr("core.fw_needs_wifi"));
220 break;
221 case Status::Checking:
222 append("%s", ui::tr("core.fw_checking"));
223 break;
224 case Status::UpToDate:
225 append("%s", ui::tr("core.fw_up_to_date"));
226 break;
227 case Status::UpdateAvailable:
228 append("%s %s\n%s", ui::tr("core.fw_update_available"), s_foundTag, kReleasesUrl);
229 break;
230 case Status::CheckFailed:
231 append("%s", ui::tr("core.fw_check_failed"));
232 break;
233 }
234
235 s_view.init(ui::tr("core.firmware_check"), buf);
236 s_view.setHint(ui::tr("core.fw_recheck_hint"));
237 if (ViewStack::instance().current() == &s_view) {
239 }
240}
241
244void fwCheckWorker(void*) {
245 char tag[24] = {0};
246 Status result = queryUpstream(tag, sizeof(tag));
247 if (result == Status::UpToDate || result == Status::UpdateAvailable) {
248 std::strncpy(s_foundTag, tag, sizeof(s_foundTag) - 1);
249 s_foundTag[sizeof(s_foundTag) - 1] = '\0';
250 } else {
251 s_foundTag[0] = '\0';
252 }
253 s_result = result;
254 s_state.store(kDone, std::memory_order_release);
255 vTaskDelete(nullptr);
256}
257
259bool spawnWorker() {
260 uint8_t expected = kIdle;
261 if (!s_state.compare_exchange_strong(expected, kRunning)) return false;
262 TaskHandle_t h = xTaskCreateStatic(fwCheckWorker, "fw_check",
263 sizeof(s_workerStack) / sizeof(StackType_t), nullptr, 5,
264 s_workerStack, &s_workerTcb);
265 if (!h) {
266 s_result = Status::CheckFailed;
267 s_state.store(kDone, std::memory_order_release);
268 }
269 return true;
270}
271
277void runCheck(bool connectFirst) {
278 if (s_state.load(std::memory_order_acquire) != kIdle) return;
279 if (connectFirst) {
280 if (!WifiHandlers::instance().acquire()) {
281 renderStatus(Status::NeedsWifi);
282 return;
283 }
284 s_heldWifi = true;
285 }
286 // Sync the clock while online so the stored check date is accurate (reuses
287 // the active connection; returns immediately if the time is already set).
289 spawnWorker();
290}
291
293void onRecheckKey(void*) {
294 if (s_state.load(std::memory_order_acquire) != kIdle) return;
295 renderStatus(Status::Checking);
296 runCheck(!WifiHandlers::instance().isConnected());
297}
298
299void FirmwareCheckView::onTick(uint32_t /*nowMs*/) {
300 if (s_state.load(std::memory_order_acquire) != kDone) return;
301 s_state.store(kIdle, std::memory_order_relaxed);
302 // Persist the result and release WiFi on the UI task (cache-disabling NVS /
303 // teardown must run on this internal-RAM stack, not the PSRAM worker).
304 if (s_result == Status::UpToDate || s_result == Status::UpdateAvailable) {
305 storeLastCheck(s_foundTag);
306 }
307 if (s_heldWifi) {
308 WifiHandlers::instance().release();
309 s_heldWifi = false;
310 }
311 renderStatus(s_result);
312}
313
314} // namespace
315
317 s_state.store(kIdle, std::memory_order_relaxed);
318 s_heldWifi = false;
319 s_view.setYesNoCallbacks(onRecheckKey, nullptr, nullptr);
320
321 bool connected = WifiHandlers::instance().isConnected();
322 renderStatus(connected ? Status::Checking : Status::NeedsWifi);
323 ViewStack::instance().push(&s_view);
324 if (connected) runCheck(false);
325}
326
327bool firmwareCheckLastResult(char* out, size_t cap) {
328 if (!out || cap == 0) return false;
329 out[0] = '\0';
330
331 core::NvsScope nvs(kNvsNs, NVS_READONLY);
332 if (nvs.status() != ESP_OK) return false;
333
334 char ver[24] = {0};
335 char date[12] = {0};
336 size_t vlen = sizeof(ver);
337 size_t dlen = sizeof(date);
338 if (nvs_get_str(nvs, kNvsKeyVer, ver, &vlen) != ESP_OK || ver[0] == '\0') return false;
339 nvs_get_str(nvs, kNvsKeyDate, date, &dlen);
340
341 composeLastResult(out, cap, ver, date);
342 return true;
343}
344
345} // namespace cdc::ui
Expert-menu firmware/version screen with an optional upstream update check against the GitHub release...
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
Shared RAII wrappers for firmware resources.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
RAII wrapper for an NVS handle.
Definition Raii.h:106
esp_err_t status() const noexcept
esp_err_t result of the original nvs_open call.
Definition Raii.h:150
void render(bool synchronous=false)
Render current view (and modal if present) and flush to display.
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:53
void push(IView *view, void *context=nullptr)
bool syncNtp(bool disconnectAfter=true)
Synchronizes system time via NTP.
static WifiHandlers & instance()
Returns singleton Wi-Fi handlers instance.
bool isConnected() const
Returns whether Wi-Fi is currently connected.
CDC Badge OS plugin host API - canonical C ABI contract.
#define HOST_API_LEVEL_STR
Definition host_api.h:30
IRtc * getRtcInstance()
Returns the singleton RTC service instance.
Definition Rtc.cpp:304
Centralized key-code constants for cdc_views.
Definition IModule.h:8
const char * tr(const char *key)
Look up a translation by string key.
Definition I18n.h:209
void showFirmwareCheck()
Opens the Firmware Check screen and pushes it onto the view stack.
static const char * TAG
bool firmwareCheckLastResult(char *out, size_t cap)
Formats the last persisted upstream check into out.
#define APP_VERSION