12#include "esp_crt_bundle.h"
13#include "esp_http_client.h"
16#include "freertos/FreeRTOS.h"
17#include "freertos/task.h"
29static constexpr const char*
TAG =
"FWCHECK";
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";
35static constexpr uint32_t kHttpTimeoutMs = 8000;
39static constexpr const char* kNvsNs =
"fwchk";
40static constexpr const char* kNvsKeyVer =
"ver";
41static constexpr const char* kNvsKeyDate =
"date";
44enum class Status : uint8_t {
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;
66constexpr size_t kWorkerStackBytes = 20u * 1024u;
67EXT_RAM_BSS_ATTR StackType_t s_workerStack[kWorkerStackBytes /
sizeof(StackType_t)];
68StaticTask_t s_workerTcb;
70constexpr size_t kRespCap = 16u * 1024u;
71EXT_RAM_BSS_ATTR
char s_respBuf[kRespCap];
73class FirmwareCheckView :
public InfoView {
75 void onTick(uint32_t nowMs)
override;
78FirmwareCheckView s_view;
81const char* stripV(
const char* v) {
82 return (v && (*v ==
'v' || *v ==
'V')) ? v + 1 : v;
87int compareSemver(
const char* a,
const char* b) {
88 for (
int i = 0; i < 3; ++i) {
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;
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;
110 while (*p && *p !=
'"' && i + 1 < cap) out[i++] = *p++;
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);
121 std::snprintf(out, cap,
"%s%s", vp, ver);
126void storeLastCheck(
const char* tag) {
129 if (rtc && rtc->isTimeSet()) {
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);
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);
147Status queryUpstream(
char* foundTag,
size_t cap) {
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;
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");
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) {
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);
172 s_respBuf[pos] =
'\0';
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
182 LOG_W(
TAG,
"tag_name not found in response");
185 LOG_W(
TAG,
"HTTP status %d", esp_http_client_get_status_code(h));
190 esp_http_client_close(h);
191 esp_http_client_cleanup(h);
196void renderStatus(Status st) {
199 auto append = [&](
const char* fmt, ...) {
200 if (pos >=
sizeof(buf))
return;
203 int w = std::vsnprintf(buf + pos,
sizeof(buf) - pos, fmt, args);
206 size_t ww =
static_cast<size_t>(w);
207 pos += (ww <
sizeof(buf) - pos) ? ww : (sizeof(buf) - pos - 1);
218 case Status::NeedsWifi:
219 append(
"%s",
ui::tr(
"core.fw_needs_wifi"));
221 case Status::Checking:
222 append(
"%s",
ui::tr(
"core.fw_checking"));
224 case Status::UpToDate:
225 append(
"%s",
ui::tr(
"core.fw_up_to_date"));
227 case Status::UpdateAvailable:
228 append(
"%s %s\n%s",
ui::tr(
"core.fw_update_available"), s_foundTag, kReleasesUrl);
230 case Status::CheckFailed:
231 append(
"%s",
ui::tr(
"core.fw_check_failed"));
235 s_view.init(
ui::tr(
"core.firmware_check"), buf);
236 s_view.setHint(
ui::tr(
"core.fw_recheck_hint"));
244void fwCheckWorker(
void*) {
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';
251 s_foundTag[0] =
'\0';
254 s_state.store(kDone, std::memory_order_release);
255 vTaskDelete(
nullptr);
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);
266 s_result = Status::CheckFailed;
267 s_state.store(kDone, std::memory_order_release);
277void runCheck(
bool connectFirst) {
278 if (s_state.load(std::memory_order_acquire) != kIdle)
return;
281 renderStatus(Status::NeedsWifi);
293void onRecheckKey(
void*) {
294 if (s_state.load(std::memory_order_acquire) != kIdle)
return;
295 renderStatus(Status::Checking);
299void FirmwareCheckView::onTick(uint32_t ) {
300 if (s_state.load(std::memory_order_acquire) != kDone)
return;
301 s_state.store(kIdle, std::memory_order_relaxed);
304 if (s_result == Status::UpToDate || s_result == Status::UpdateAvailable) {
305 storeLastCheck(s_foundTag);
308 WifiHandlers::instance().release();
311 renderStatus(s_result);
317 s_state.store(kIdle, std::memory_order_relaxed);
319 s_view.setYesNoCallbacks(onRecheckKey,
nullptr,
nullptr);
322 renderStatus(connected ? Status::Checking : Status::NeedsWifi);
324 if (connected) runCheck(
false);
328 if (!out || cap == 0)
return false;
332 if (nvs.
status() != ESP_OK)
return false;
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);
341 composeLastResult(out, cap, ver, date);
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,...)
RAII wrapper for an NVS handle.
esp_err_t status() const noexcept
esp_err_t result of the original nvs_open call.
void render(bool synchronous=false)
Render current view (and modal if present) and flush to display.
static ViewStack & instance()
Returns singleton view-stack instance.
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
IRtc * getRtcInstance()
Returns the singleton RTC service instance.
Centralized key-code constants for cdc_views.
const char * tr(const char *key)
Look up a translation by string key.
void showFirmwareCheck()
Opens the Firmware Check screen and pushes it onto the view stack.
bool firmwareCheckLastResult(char *out, size_t cap)
Formats the last persisted upstream check into out.