CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
PluginSerialCommands.cpp
Go to the documentation of this file.
1
18
24#include "serial_cmd/Console.h"
26#include "cdc_core/Raii.h"
27#include "cdc_ui/I18n.h"
28#include "cdc_log.h"
29#include "esp_log.h"
30#include "esp_heap_caps.h"
31#include "esp_timer.h"
32
33#include <cstdio>
34#include <cstdlib>
35#include <cstring>
36#include <cstdint>
37#include <string>
38
39namespace cdc::plugin_manager {
40
41static const char* TAG = "PLG_CMD";
42static const char* CMD_MODULE = "plugin_manager";
43
44namespace {
45
46struct UploadSession {
47 bool active = false;
48 std::string id;
49 std::string target_path; // .wasm or .meta or .lang
50 std::string tmp_path; // <target_path>.partial
51 size_t total_size = 0;
52 size_t received = 0;
53 uint32_t expected_crc = 0;
54 uint32_t running_crc = 0xffffffffu;
55 bool was_lang = false;
56 bool was_wasm = false;
58 int64_t last_activity_us = 0;
59};
60
61static UploadSession s_upload{};
62
63// CRC-32 (IEEE 802.3) byte-wise update so we can checksum the stream as it
64// arrives in the ISR/CDC callback, without buffering the entire payload.
65static inline uint32_t crc32_update(uint32_t crc, uint8_t b) {
66 crc ^= b;
67 for (int i = 0; i < 8; ++i) {
68 crc = (crc >> 1) ^ (0xedb88320u & -(crc & 1));
69 }
70 return crc;
71}
72
73// Auto-abort a stuck upload after this many seconds of inactivity. Without
74// this, a crashed/interrupted client would keep the line interceptor active
75// and the serial console permanently unresponsive.
76static constexpr int64_t UPLOAD_INACTIVITY_LIMIT_US = 15 * 1000000LL;
77
78static void touch_upload_activity() {
79 s_upload.last_activity_us = esp_timer_get_time();
80}
81
82void send(const char* line) {
85}
86void sendf(const char* fmt, ...)
87{
88 char buf[256];
89 va_list ap;
90 va_start(ap, fmt);
91 vsnprintf(buf, sizeof(buf), fmt, ap);
92 va_end(ap);
95}
96
97static esp_timer_handle_t s_upload_timeout_timer = nullptr;
98static void on_upload_timeout(void*);
99
100// 256-byte page-aligned write buffer for binary upload streaming.
101// `s_byte_buffer_pos` is reset to 0 by abort_upload() and start_upload().
102static uint8_t s_byte_buffer[256];
103static size_t s_byte_buffer_pos = 0;
104
105static void ensure_timeout_timer() {
106 if (s_upload_timeout_timer) return;
107 esp_timer_create_args_t args = {
108 .callback = on_upload_timeout,
109 .arg = nullptr,
110 .dispatch_method = ESP_TIMER_TASK,
111 .name = "plugin_upload_to",
112 .skip_unhandled_events = true,
113 };
114 esp_timer_create(&args, &s_upload_timeout_timer);
115}
116
117static void rearm_upload_timeout() {
118 ensure_timeout_timer();
119 esp_timer_stop(s_upload_timeout_timer);
120 esp_timer_start_once(s_upload_timeout_timer, UPLOAD_INACTIVITY_LIMIT_US);
121}
122
123void abort_upload()
124{
125 s_upload.fp.reset();
126 if (!s_upload.tmp_path.empty()) std::remove(s_upload.tmp_path.c_str());
127 s_upload = UploadSession{};
128 s_byte_buffer_pos = 0;
130 if (s_upload_timeout_timer) esp_timer_stop(s_upload_timeout_timer);
131}
132
133static void on_upload_timeout(void*) {
134 if (!s_upload.active) return;
135 int64_t since = esp_timer_get_time() - s_upload.last_activity_us;
136 if (since >= UPLOAD_INACTIVITY_LIMIT_US) {
137 LOG_W(TAG, "Upload session timed out after %lld us, aborting", since);
138 send("ERR upload_timeout");
139 abort_upload();
140 } else {
141 esp_timer_start_once(s_upload_timeout_timer,
142 UPLOAD_INACTIVITY_LIMIT_US - since);
143 }
144}
145
146// Byte-streaming finaliser - called after `total_size` payload bytes have
147// been received. Verifies the whole-stream CRC, renames the partial file in
148// place and clears all upload state.
149static void finalize_upload()
150{
151 s_upload.fp.reset();
152
153 if (s_upload.received != s_upload.total_size) {
154 sendf("ERR size_mismatch %u/%u",
155 static_cast<unsigned>(s_upload.received),
156 static_cast<unsigned>(s_upload.total_size));
157 abort_upload();
158 return;
159 }
160
161 const uint32_t actual_crc = s_upload.running_crc ^ 0xffffffffu;
162 if (actual_crc != s_upload.expected_crc) {
163 sendf("ERR crc_mismatch got=%08X want=%08X",
164 static_cast<unsigned>(actual_crc),
165 static_cast<unsigned>(s_upload.expected_crc));
166 abort_upload();
167 return;
168 }
169
170 // FAT-FS rejects rename when the target exists, so drop any previous
171 // version first.
172 std::remove(s_upload.target_path.c_str());
173 if (std::rename(s_upload.tmp_path.c_str(), s_upload.target_path.c_str()) != 0) {
174 send("ERR rename_failed");
175 abort_upload();
176 return;
177 }
178
179 const bool was_lang = s_upload.was_lang;
180 const bool was_wasm = s_upload.was_wasm;
181 const std::string uploaded_id = s_upload.id;
182 sendf("OK %u", static_cast<unsigned>(s_upload.total_size));
183
184 s_upload = UploadSession{};
185 if (s_upload_timeout_timer) esp_timer_stop(s_upload_timeout_timer);
186
187 if (was_lang) {
189 }
190 if (was_wasm && !uploaded_id.empty()) {
191 auto mf = PluginManager::instance().getManifest(uploaded_id);
192 if (mf && mf->capabilities.background) {
194 }
195 }
196}
197
198// Byte interceptor: receives every byte from the serial CDC stream while the
199// upload session is active. Buffers in 256-byte page-aligned blocks before
200// calling fwrite() to keep filesystem syscalls cheap on FAT.
201
202static inline void flush_byte_buffer()
203{
204 if (s_byte_buffer_pos == 0) return;
205 if (s_upload.fp) {
206 std::fwrite(s_byte_buffer, 1, s_byte_buffer_pos, s_upload.fp.get());
207 }
208 s_byte_buffer_pos = 0;
209}
210
211extern "C" void upload_byte(uint8_t b)
212{
213 if (!s_upload.active) return;
214
215 // Keep auth session and inactivity watchdog alive without forcing the
216 // host to send periodic commands.
218 touch_upload_activity();
219
220 s_byte_buffer[s_byte_buffer_pos++] = b;
221 s_upload.running_crc = crc32_update(s_upload.running_crc, b);
222 s_upload.received++;
223
224 if (s_byte_buffer_pos >= sizeof(s_byte_buffer)) {
225 flush_byte_buffer();
226 rearm_upload_timeout();
227 }
228
229 if (s_upload.received >= s_upload.total_size) {
230 flush_byte_buffer();
232 finalize_upload();
233 }
234}
235
236void cmdList(const char*)
237{
239 send("[");
240 for (size_t i = 0; i < ids.size(); ++i) {
241 std::string name = ids[i];
242 std::string version;
243 if (auto mf = PluginManager::instance().getManifest(ids[i])) {
244 version = mf->version;
245 auto it = mf->i18n_meta.find("name");
246 if (it != mf->i18n_meta.end() && !it->second.by_lang.empty()) {
247 auto by = it->second.by_lang.find(mf->default_language);
248 if (by != it->second.by_lang.end()) name = by->second;
249 else name = it->second.by_lang.begin()->second;
250 }
251 }
252 const bool disabled = PluginManager::instance().isPluginDisabled(ids[i]);
253 sendf(" {\"id\":\"%s\",\"name\":\"%s\",\"version\":\"%s\",\"disabled\":%s}%s",
254 ids[i].c_str(), name.c_str(), version.c_str(),
255 disabled ? "true" : "false",
256 (i + 1 < ids.size()) ? "," : "");
257 }
258 send("]");
259}
260
261void cmdInfo(const char* args)
262{
263 if (!args || !*args) { send("ERR missing_id"); return; }
264 std::string id = args;
265 auto mf = PluginManager::instance().getManifest(id);
266 if (!mf) { send("ERR not_found"); return; }
267 sendf("id: %s", mf->id.c_str());
268 sendf("version: %s", mf->version.c_str());
269 sendf("author: %s", mf->author.c_str());
270 sendf("api_level: %s", mf->host_api_level_min.c_str());
271 sendf("linear_kb: %u", static_cast<unsigned>(mf->linear_memory_kb));
272 sendf("disabled: %s",
273 PluginManager::instance().isPluginDisabled(id) ? "yes" : "no");
274
275 const auto& c = mf->capabilities;
276
277 // Boolean capabilities: list only the requested (true) ones.
278 std::string caps;
279 auto addCap = [&caps](bool on, const char* name) {
280 if (on) { if (!caps.empty()) caps += ' '; caps += name; }
281 };
282 addCap(c.wifi, "wifi");
283 addCap(c.ble, "ble");
284 addCap(c.http, "http");
285 addCap(c.socket, "socket");
286 addCap(c.ui_exclusive, "ui_exclusive");
287 addCap(c.display_lowlevel, "display_lowlevel");
288 addCap(c.sao, "sao");
289 addCap(c.grove, "grove");
290 addCap(c.pixel_strip, "pixel_strip");
291 addCap(c.background, "background");
292 addCap(c.autoload, "autoload");
293 addCap(c.usb_cdc, "usb_cdc");
294 addCap(c.prevent_sleep, "prevent_sleep");
295 addCap(c.vfat, "vfat");
296 sendf("caps: %s", caps.empty() ? "-" : caps.c_str());
297
298 auto joinPins = [](const std::vector<uint8_t>& v) {
299 std::string s;
300 char num[8];
301 for (uint8_t p : v) {
302 if (!s.empty()) s += ',';
303 snprintf(num, sizeof(num), "%u", static_cast<unsigned>(p));
304 s += num;
305 }
306 return s;
307 };
308 auto joinStr = [](const std::vector<std::string>& v) {
309 std::string s;
310 for (const auto& e : v) { if (!s.empty()) s += ','; s += e; }
311 return s;
312 };
313
314 // Concrete resource requests (printed only when present).
315 if (!c.gpio_pins.empty()) sendf("gpio_pins: %s", joinPins(c.gpio_pins).c_str());
316 if (!c.pwm_pins.empty()) sendf("pwm_pins: %s", joinPins(c.pwm_pins).c_str());
317 if (!c.adc_pins.empty()) sendf("adc_pins: %s", joinPins(c.adc_pins).c_str());
318 if (!c.i2c_bus.empty()) sendf("i2c_bus: %s", joinPins(c.i2c_bus).c_str());
319 if (!c.rmem.empty()) sendf("rmem: %s", joinStr(c.rmem).c_str());
320 if (!c.ecc.empty()) sendf("ecc: %s", joinStr(c.ecc).c_str());
321 if (!c.ble_service_uuids.empty()) sendf("ble_uuids: %s", joinStr(c.ble_service_uuids).c_str());
322 if (!c.nvs_namespace.empty()) sendf("nvs_ns: %s", c.nvs_namespace.c_str());
323
324 // Prerequisites: name + on-fail policy.
325 sendf("prereqs: %u", static_cast<unsigned>(mf->prerequisites.size()));
326 for (const auto& p : mf->prerequisites) {
327 if (p.on_fail.empty()) {
328 sendf(" - %s", p.name.c_str());
329 } else {
330 sendf(" - %s (on_fail=%s)", p.name.c_str(), p.on_fail.c_str());
331 }
332 }
333}
334
335void cmdDelete(const char* args)
336{
337 if (!args || !*args) { send("ERR missing_id"); return; }
338 std::string id = args;
339 (void)PluginManager::instance().unloadFromRam(id);
340 std::remove(PluginStorage::wasmPath(id).c_str());
341 std::remove(PluginStorage::aotPath(id).c_str());
342 std::remove(PluginStorage::metaPath(id).c_str());
343 std::remove(PluginStorage::langPath(id).c_str());
344 std::remove(PluginStorage::disabledPath(id).c_str());
345 send("OK");
346}
347
348void cmdDisable(const char* args)
349{
350 if (!args || !*args) { send("ERR missing_id"); return; }
351 if (!PluginManager::instance().setPluginDisabled(args, true)) {
352 send("ERR not_found");
353 return;
354 }
355 sendf("OK disabled %s", args);
356}
357
358void cmdEnable(const char* args)
359{
360 if (!args || !*args) { send("ERR missing_id"); return; }
361 if (!PluginManager::instance().setPluginDisabled(args, false)) {
362 send("ERR not_found");
363 return;
364 }
365 sendf("OK enabled %s", args);
366}
367
368void cmdStart(const char* args)
369{
370 if (!args || !*args) { send("ERR missing_id"); return; }
371 auto res = PluginManager::instance().startPlugin(args);
372 if (res == StartResult::Ok) {
373 sendf("OK started %s", args);
374 } else if (res == StartResult::PluginDisabled) {
375 sendf("ERR disabled %s", args);
376 } else {
377 sendf("ERR start %d %s", static_cast<int>(res), args);
378 }
379}
380
381void cmdStop(const char*)
382{
383 if (PluginManager::instance().stopActivePlugin()) send("OK");
384 else send("ERR no_active_plugin");
385}
386
387void cmdCmd(const char* args)
388{
389 if (!args || !*args) { send("ERR missing_id"); return; }
390
391 char id_buf[64] = {0};
392 int consumed = 0;
393 std::sscanf(args, "%63s%n", id_buf, &consumed);
394 if (id_buf[0] == '\0') { send("ERR missing_id"); return; }
395
396 const char* cmd = args + consumed;
397 while (*cmd == ' ') ++cmd;
398 std::string id = id_buf;
399
400 if (PluginManager::instance().isPluginDisabled(id)) {
401 sendf("ERR disabled %s", id.c_str());
402 return;
403 }
404
405 bool started_here = false;
406 if (!PluginManager::instance().isLoaded(id)) {
407 auto res = PluginManager::instance().startPlugin(id);
408 if (res == StartResult::PluginDisabled) {
409 sendf("ERR disabled %s", id.c_str());
410 return;
411 }
413 sendf("ERR start %d %s", static_cast<int>(res), id.c_str());
414 return;
415 }
416 started_here = true;
417 }
418
419 if (PluginManager::instance().dispatchCmd(id, cmd, std::strlen(cmd))) send("OK");
420 else send("ERR no_handler");
421
422 // Only unload what this command loaded; a plugin already running stays running.
423 if (started_here) PluginManager::instance().unloadFromRam(id);
424}
425
426enum class PluginUploadKind { Wasm, Aot, Meta, Lang };
427
428// Shared by every upload path (plugin wasm/meta/lang, core lang overlay,
429// generic file): the caller fills s_upload.target_path + the post-finalize
430// flags, then this opens the .partial temp, resets the byte counters, installs
431// the streaming interceptor and answers READY. It is all one vFAT file write.
432void arm_upload(uint32_t crc)
433{
434 s_upload.received = 0;
435 s_upload.running_crc = 0xffffffffu;
436 s_upload.expected_crc = crc;
437 s_upload.tmp_path = s_upload.target_path + ".partial";
438 s_upload.fp = ::cdc::core::openFile(s_upload.tmp_path.c_str(), "wb");
439 if (!s_upload.fp) { send("ERR cannot_open"); return; }
440 s_byte_buffer_pos = 0;
441 s_upload.active = true;
442 touch_upload_activity();
443 rearm_upload_timeout();
445 send("READY");
446}
447
448// Parses "<id> <total_size> <crc32_hex>". Derives the target path from the
449// kind, then arms the shared upload.
450void start_upload(const char* args, PluginUploadKind kind)
451{
452 if (s_upload.active) { send("ERR upload_in_progress"); return; }
453
454 char id_buf[64] = {0};
455 unsigned long total_size = 0;
456 unsigned long crc_arg = 0;
457 int parsed = std::sscanf(args, "%63s %lu %lx", id_buf, &total_size, &crc_arg);
458 if (parsed < 2 || total_size == 0) {
459 send("ERR usage:_PLUGIN_UPLOAD_<id>_<size>_<crc32_hex>");
460 return;
461 }
462
463 if (kind == PluginUploadKind::Wasm || kind == PluginUploadKind::Aot) {
464 const std::string target_id = id_buf;
465 if (PluginManager::instance().hasActivePlugin() &&
466 PluginManager::instance().activePluginId() == target_id) {
468 }
469 }
470
471 s_upload.id = id_buf;
472 s_upload.total_size = static_cast<size_t>(total_size);
473 s_upload.was_lang = (kind == PluginUploadKind::Lang);
474 s_upload.was_wasm = (kind == PluginUploadKind::Wasm || kind == PluginUploadKind::Aot);
475 switch (kind) {
476 case PluginUploadKind::Wasm:
477 s_upload.target_path = PluginStorage::wasmPath(s_upload.id);
478 std::remove(PluginStorage::aotPath(s_upload.id).c_str());
479 break;
480 case PluginUploadKind::Aot:
481 s_upload.target_path = PluginStorage::aotPath(s_upload.id);
482 std::remove(PluginStorage::wasmPath(s_upload.id).c_str());
483 break;
484 case PluginUploadKind::Meta:
485 s_upload.target_path = PluginStorage::metaPath(s_upload.id);
486 break;
487 case PluginUploadKind::Lang:
488 s_upload.target_path = PluginStorage::langPath(s_upload.id);
489 break;
490 }
491
492 arm_upload(static_cast<uint32_t>(crc_arg));
493}
494
495void cmdUpload (const char* args) { start_upload(args, PluginUploadKind::Wasm); }
496void cmdUploadAot (const char* args) { start_upload(args, PluginUploadKind::Aot); }
497void cmdUploadMeta(const char* args) { start_upload(args, PluginUploadKind::Meta); }
498void cmdUploadLang(const char* args) { start_upload(args, PluginUploadKind::Lang); }
499
500void cmdAbort(const char*)
501{
502 if (!s_upload.active) { send("OK no_upload"); return; }
503 abort_upload();
504 send("ABORTED");
505}
506
507void cmdDebug(const char*)
508{
509 static bool s_debug_enabled = false;
510 s_debug_enabled = !s_debug_enabled;
511 auto level = s_debug_enabled ? ESP_LOG_DEBUG : ESP_LOG_INFO;
512 static const char* const PLUGIN_TAGS[] = {
513 "PLG_CMD", "PLG_MGR", "PLG_STO", "PLG_UI", "PLG_PRE", "PLG_MAN",
514 "PLUGIN", "GPIO_CMD", "WamrImports",
515 };
516 for (auto* t : PLUGIN_TAGS) esp_log_level_set(t, level);
517 esp_log_level_set("host_*", level);
518
519 if (!s_debug_enabled) {
520 send("OK plugin debug DISABLED");
521 return;
522 }
523
524 sendf("OK plugin debug ENABLED");
525 auto& pm = PluginManager::instance();
526 sendf("--- plugin status ---");
527 sendf("active_plugin: %s",
528 pm.hasActivePlugin() ? pm.activePluginId().c_str() : "(none)");
529 sendf("installed_count: %u",
530 static_cast<unsigned>(pm.listInstalledIds().size()));
531 sendf("psram_free: %u KB",
532 static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024));
533 sendf("internal_free: %u KB",
534 static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024));
535 sendf("upload_active: %s", s_upload.active ? "yes" : "no");
536}
537
538void cmdLangInfo(const char*)
539{
540 auto& i18n = cdc::ui::I18n::instance();
541 sendf("lang: %s", i18n.getLanguageCode().c_str());
542 sendf("dir: %s", cdc::ui::I18n::OVERLAY_DIR);
543 sendf("avail: %u", static_cast<unsigned>(i18n.availableOverlayLanguages().size()));
544 for (const auto& c : i18n.availableOverlayLanguages()) {
545 sendf(" - %s", c.code.c_str());
546 }
547}
548
549void cmdLangReload(const char*)
550{
551 if (cdc::ui::I18n::instance().loadOverlay()) send("OK");
552 else send("ERR reload_failed");
553}
554
555const cdc::serial::SubCommand kPluginSubs[] = {
556 {"LIST", "", "List installed plugins (JSON)", cmdList},
557 {"INFO", "<id>", "Show manifest details for one plugin", cmdInfo},
558 {"START", "<id>", "Start a plugin", cmdStart},
559 {"STOP", "", "Stop the currently active plugin", cmdStop},
560 {"CMD", "<id> <args>", "Forward a command string to a plugin", cmdCmd},
561 {"DISABLE", "<id>", "Disable a plugin and unload it from RAM", cmdDisable},
562 {"ENABLE", "<id>", "Enable a disabled plugin", cmdEnable},
563 {"DELETE", "<id>", "Delete wasm + meta + lang files for plugin", cmdDelete},
564 {"UPLOAD", "<id> <size> <crc32_hex>", "Upload .wasm payload (binary stream)", cmdUpload},
565 {"UPLOAD_AOT", "<id> <size> <crc32_hex>", "Upload .aot payload (binary stream)", cmdUploadAot},
566 {"UPLOAD_META", "<id> <size> <crc32_hex>", "Upload .meta payload (binary stream)", cmdUploadMeta},
567 {"UPLOAD_LANG", "<id> <size> <crc32_hex>", "Upload .lang payload (binary stream)", cmdUploadLang},
568 {"ABORT", "", "Abort an active upload session", cmdAbort},
569 {"DEBUG", "", "Toggle verbose plugin/host_* logging", cmdDebug},
570 {nullptr, nullptr, nullptr, nullptr},
571};
572void cmdPluginDispatch(const char* args) {
573 cdc::serial::dispatchSubCommand("PLUGIN", args, kPluginSubs);
574}
575
576const cdc::serial::SubCommand kLangSubs[] = {
577 {"INFO", "", "Show active language and available overlays", cmdLangInfo},
578 {"RELOAD", "", "Rescan + reload overlays from /plugins/i18n/", cmdLangReload},
579 {nullptr, nullptr, nullptr, nullptr},
580};
581void cmdLangDispatch(const char* args) {
582 cdc::serial::dispatchSubCommand("LANG", args, kLangSubs);
583}
584
585} // namespace
586
587bool beginFileReceive(const char* abs_path, size_t size, uint32_t crc)
588{
589 if (!abs_path || size == 0) { send("ERR bad_args"); return false; }
590 if (s_upload.active) { send("ERR upload_in_progress"); return false; }
591 s_upload.id = "";
592 s_upload.total_size = size;
593 s_upload.was_lang = false;
594 s_upload.was_wasm = false;
595 s_upload.target_path = abs_path;
596 arm_upload(crc);
597 return s_upload.active;
598}
599
601{
603 reg.registerCommand({"PLUGIN",
604 "Plugin manager: LIST/INFO/START/STOP/ENABLE/DISABLE/DELETE/UPLOAD/UPLOAD_META/UPLOAD_LANG/ABORT/DEBUG",
605 cmdPluginDispatch, CMD_MODULE, true, kPluginSubs});
606 reg.registerCommand({"LANG",
607 "i18n overlay: INFO/RELOAD",
608 cmdLangDispatch, CMD_MODULE, true, kLangSubs});
609 LOG_I(TAG, "PLUGIN and LANG serial commands registered");
610}
611
612} // namespace cdc::plugin_manager
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
Discovers, loads, runs and unloads WASM plugins on the badge.
In-memory representation of a plugin's meta.json.
Mounts the FAT-FS partition that holds plugin .wasm + .meta files.
char name[cdc::hal::ISecureElement::RMEM_NAME_LEN]
uint8_t version
Shared RAII wrappers for firmware resources.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
std::vector< std::string > listInstalledIds() const
static PluginManager & instance() noexcept
bool unloadFromRam(const std::string &id)
bool reloadBackgroundPlugin(const std::string &id)
bool isPluginDisabled(const std::string &id) const
std::optional< PluginManifest > getManifest(const std::string &id) const
StartResult startPlugin(const std::string &id)
static std::string langPath(const std::string &id)
Returns the full VFS path of <id>.lang (translation overlay).
static std::string aotPath(const std::string &id)
Returns the full VFS path of <id>.aot.
static std::string disabledPath(const std::string &id)
Returns the full VFS path of <id>.disabled.
static std::string wasmPath(const std::string &id)
Returns the full VFS path of <id>.wasm.
static std::string metaPath(const std::string &id)
Returns the full VFS path of <id>.meta.
static void print(const char *str)
Prints raw string to console.
Definition Console.cpp:56
virtual void setByteInterceptor(ByteInterceptor interceptor)
static void touchAuthSession()
Keeps the auth session alive during a long-running serial activity.
static I18n & instance()
Singleton accessor.
Definition I18n.cpp:287
bool loadOverlay()
Rescan available languages and (re)load the active overlay.
Definition I18n.cpp:536
static constexpr const char * OVERLAY_DIR
Directory on the plugins FAT holding the per-language files.
Definition I18n.h:69
FilePtr openFile(const char *path, const char *mode) noexcept
Open a FILE* and wrap it in a FilePtr.
Definition Raii.h:87
std::unique_ptr< std::FILE, FileCloseDeleter > FilePtr
unique_ptr for FILE* handles. Destructor calls std::fclose.
Definition Raii.h:79
static const char * CMD_MODULE
static const char * TAG
bool beginFileReceive(const char *abs_path, size_t size, uint32_t crc)
Receive a file over USB-CDC into the plugins partition.
ICommandRegistry & getCommandRegistry()
Returns singleton command-registry interface.
void dispatchSubCommand(const char *parent, const char *args, const SubCommand *table)
Routes a sub-command line to its handler.
Definition SubCommand.h:73