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;
340 send("OK");
341}
342
343void cmdDisable(const char* args)
344{
345 if (!args || !*args) { send("ERR missing_id"); return; }
346 if (!PluginManager::instance().setPluginDisabled(args, true)) {
347 send("ERR not_found");
348 return;
349 }
350 sendf("OK disabled %s", args);
351}
352
353void cmdEnable(const char* args)
354{
355 if (!args || !*args) { send("ERR missing_id"); return; }
356 if (!PluginManager::instance().setPluginDisabled(args, false)) {
357 send("ERR not_found");
358 return;
359 }
360 sendf("OK enabled %s", args);
361}
362
363void cmdStart(const char* args)
364{
365 if (!args || !*args) { send("ERR missing_id"); return; }
366 auto res = PluginManager::instance().startPlugin(args);
367 if (res == StartResult::Ok) {
368 sendf("OK started %s", args);
369 } else if (res == StartResult::PluginDisabled) {
370 sendf("ERR disabled %s", args);
371 } else {
372 sendf("ERR start %d %s", static_cast<int>(res), args);
373 }
374}
375
376void cmdStop(const char*)
377{
378 if (PluginManager::instance().stopActivePlugin()) send("OK");
379 else send("ERR no_active_plugin");
380}
381
382void cmdCmd(const char* args)
383{
384 if (!args || !*args) { send("ERR missing_id"); return; }
385
386 char id_buf[64] = {0};
387 int consumed = 0;
388 std::sscanf(args, "%63s%n", id_buf, &consumed);
389 if (id_buf[0] == '\0') { send("ERR missing_id"); return; }
390
391 const char* cmd = args + consumed;
392 while (*cmd == ' ') ++cmd;
393 std::string id = id_buf;
394
395 if (PluginManager::instance().isPluginDisabled(id)) {
396 sendf("ERR disabled %s", id.c_str());
397 return;
398 }
399
400 bool started_here = false;
401 if (!PluginManager::instance().isLoaded(id)) {
402 auto res = PluginManager::instance().startPlugin(id);
403 if (res == StartResult::PluginDisabled) {
404 sendf("ERR disabled %s", id.c_str());
405 return;
406 }
408 sendf("ERR start %d %s", static_cast<int>(res), id.c_str());
409 return;
410 }
411 started_here = true;
412 }
413
414 if (PluginManager::instance().dispatchCmd(id, cmd, std::strlen(cmd))) send("OK");
415 else send("ERR no_handler");
416
417 // Only unload what this command loaded; a plugin already running stays running.
418 if (started_here) PluginManager::instance().unloadFromRam(id);
419}
420
421enum class PluginUploadKind { Wasm, Aot, Meta, Lang };
422
423// Shared by every upload path (plugin wasm/meta/lang, core lang overlay,
424// generic file): the caller fills s_upload.target_path + the post-finalize
425// flags, then this opens the .partial temp, resets the byte counters, installs
426// the streaming interceptor and answers READY. It is all one vFAT file write.
427void arm_upload(uint32_t crc)
428{
429 // The host is the sole writer while it holds the volume over USB MSC.
430 if (PluginStorage::hostActive()) { send("ERR msc_active"); return; }
431
432 s_upload.received = 0;
433 s_upload.running_crc = 0xffffffffu;
434 s_upload.expected_crc = crc;
435 s_upload.tmp_path = s_upload.target_path + ".partial";
436 s_upload.fp = ::cdc::core::openFile(s_upload.tmp_path.c_str(), "wb");
437 if (!s_upload.fp) { send("ERR cannot_open"); return; }
438 s_byte_buffer_pos = 0;
439 s_upload.active = true;
440 touch_upload_activity();
441 rearm_upload_timeout();
443 send("READY");
444}
445
446// Parses "<id> <total_size> <crc32_hex>". Derives the target path from the
447// kind, then arms the shared upload.
448void start_upload(const char* args, PluginUploadKind kind)
449{
450 if (s_upload.active) { send("ERR upload_in_progress"); return; }
451
452 char id_buf[64] = {0};
453 unsigned long total_size = 0;
454 unsigned long crc_arg = 0;
455 int parsed = std::sscanf(args, "%63s %lu %lx", id_buf, &total_size, &crc_arg);
456 if (parsed < 2 || total_size == 0) {
457 send("ERR usage:_PLUGIN_UPLOAD_<id>_<size>_<crc32_hex>");
458 return;
459 }
460
461 if (kind == PluginUploadKind::Wasm || kind == PluginUploadKind::Aot) {
462 const std::string target_id = id_buf;
463 if (PluginManager::instance().hasActivePlugin() &&
464 PluginManager::instance().activePluginId() == target_id) {
466 }
467 }
468
469 s_upload.id = id_buf;
470 s_upload.total_size = static_cast<size_t>(total_size);
471 s_upload.was_lang = (kind == PluginUploadKind::Lang);
472 s_upload.was_wasm = (kind == PluginUploadKind::Wasm || kind == PluginUploadKind::Aot);
473 switch (kind) {
474 case PluginUploadKind::Wasm:
475 s_upload.target_path = PluginStorage::wasmPath(s_upload.id);
476 std::remove(PluginStorage::aotPath(s_upload.id).c_str());
477 break;
478 case PluginUploadKind::Aot:
479 s_upload.target_path = PluginStorage::aotPath(s_upload.id);
480 std::remove(PluginStorage::wasmPath(s_upload.id).c_str());
481 break;
482 case PluginUploadKind::Meta:
483 s_upload.target_path = PluginStorage::metaPath(s_upload.id);
484 break;
485 case PluginUploadKind::Lang:
486 s_upload.target_path = PluginStorage::langPath(s_upload.id);
487 break;
488 }
489
490 arm_upload(static_cast<uint32_t>(crc_arg));
491}
492
493void cmdUpload (const char* args) { start_upload(args, PluginUploadKind::Wasm); }
494void cmdUploadAot (const char* args) { start_upload(args, PluginUploadKind::Aot); }
495void cmdUploadMeta(const char* args) { start_upload(args, PluginUploadKind::Meta); }
496void cmdUploadLang(const char* args) { start_upload(args, PluginUploadKind::Lang); }
497
498void cmdAbort(const char*)
499{
500 if (!s_upload.active) { send("OK no_upload"); return; }
501 abort_upload();
502 send("ABORTED");
503}
504
505void cmdDebug(const char*)
506{
507 static bool s_debug_enabled = false;
508 s_debug_enabled = !s_debug_enabled;
509 auto level = s_debug_enabled ? ESP_LOG_DEBUG : ESP_LOG_INFO;
510 static const char* const PLUGIN_TAGS[] = {
511 "PLG_CMD", "PLG_MGR", "PLG_STO", "PLG_UI", "PLG_PRE", "PLG_MAN",
512 "PLUGIN", "GPIO_CMD", "WamrImports",
513 };
514 for (auto* t : PLUGIN_TAGS) esp_log_level_set(t, level);
515 esp_log_level_set("host_*", level);
516
517 if (!s_debug_enabled) {
518 send("OK plugin debug DISABLED");
519 return;
520 }
521
522 sendf("OK plugin debug ENABLED");
523 auto& pm = PluginManager::instance();
524 sendf("--- plugin status ---");
525 sendf("active_plugin: %s",
526 pm.hasActivePlugin() ? pm.activePluginId().c_str() : "(none)");
527 sendf("installed_count: %u",
528 static_cast<unsigned>(pm.listInstalledIds().size()));
529 sendf("psram_free: %u KB",
530 static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024));
531 sendf("internal_free: %u KB",
532 static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024));
533 sendf("upload_active: %s", s_upload.active ? "yes" : "no");
534}
535
536void cmdLangInfo(const char*)
537{
538 auto& i18n = cdc::ui::I18n::instance();
539 sendf("lang: %s", i18n.getLanguageCode().c_str());
540 sendf("dir: %s", cdc::ui::I18n::OVERLAY_DIR);
541 sendf("avail: %u", static_cast<unsigned>(i18n.availableOverlayLanguages().size()));
542 for (const auto& c : i18n.availableOverlayLanguages()) {
543 sendf(" - %s", c.code.c_str());
544 }
545}
546
547void cmdLangReload(const char*)
548{
549 if (cdc::ui::I18n::instance().loadOverlay()) send("OK");
550 else send("ERR reload_failed");
551}
552
553void cmdFeatures(const char*)
554{
555 auto& mgr = PluginManager::instance();
556 size_t count = 0;
557 for (const auto& id : mgr.listInstalledIds()) {
558 auto mf = mgr.getManifest(id);
559 if (!mf) continue;
560 for (const auto& feat : mf->capabilities.provides) {
561 sendf("%s %s%s%s", feat.c_str(), id.c_str(),
562 mgr.isPluginDisabled(id) ? " [disabled]" : "",
563 mgr.isLoaded(id) ? " [loaded]" : "");
564 ++count;
565 }
566 }
567 if (count == 0) send("(no features provided by installed plugins)");
568}
569
570const cdc::serial::SubCommand kPluginSubs[] = {
571 {"LIST", "", "List installed plugins (JSON)", cmdList},
572 {"INFO", "<id>", "Show manifest details for one plugin", cmdInfo},
573 {"START", "<id>", "Start a plugin", cmdStart},
574 {"STOP", "", "Stop the currently active plugin", cmdStop},
575 {"CMD", "<id> <args>", "Forward a command string to a plugin", cmdCmd},
576 {"DISABLE", "<id>", "Disable a plugin and unload it from RAM", cmdDisable},
577 {"ENABLE", "<id>", "Enable a disabled plugin", cmdEnable},
578 {"DELETE", "<id>", "Delete wasm + meta + lang files for plugin", cmdDelete},
579 {"UPLOAD", "<id> <size> <crc32_hex>", "Upload .wasm payload (binary stream)", cmdUpload},
580 {"UPLOAD_AOT", "<id> <size> <crc32_hex>", "Upload .aot payload (binary stream)", cmdUploadAot},
581 {"UPLOAD_META", "<id> <size> <crc32_hex>", "Upload .meta payload (binary stream)", cmdUploadMeta},
582 {"UPLOAD_LANG", "<id> <size> <crc32_hex>", "Upload .lang payload (binary stream)", cmdUploadLang},
583 {"ABORT", "", "Abort an active upload session", cmdAbort},
584 {"DEBUG", "", "Toggle verbose plugin/host_* logging", cmdDebug},
585 {"FEATURES", "", "List external features provided by installed plugins", cmdFeatures},
586 {nullptr, nullptr, nullptr, nullptr},
587};
588void cmdPluginDispatch(const char* args) {
589 cdc::serial::dispatchSubCommand("PLUGIN", args, kPluginSubs);
590}
591
592const cdc::serial::SubCommand kLangSubs[] = {
593 {"INFO", "", "Show active language and available overlays", cmdLangInfo},
594 {"RELOAD", "", "Rescan + reload overlays from /vfat/system/i18n/", cmdLangReload},
595 {nullptr, nullptr, nullptr, nullptr},
596};
597void cmdLangDispatch(const char* args) {
598 cdc::serial::dispatchSubCommand("LANG", args, kLangSubs);
599}
600
601} // namespace
602
603bool beginFileReceive(const char* abs_path, size_t size, uint32_t crc)
604{
605 if (!abs_path || size == 0) { send("ERR bad_args"); return false; }
606 if (s_upload.active) { send("ERR upload_in_progress"); return false; }
607 s_upload.id = "";
608 s_upload.total_size = size;
609 s_upload.was_lang = false;
610 s_upload.was_wasm = false;
611 s_upload.target_path = abs_path;
612 arm_upload(crc);
613 return s_upload.active;
614}
615
617{
619 reg.registerCommand({"PLUGIN",
620 "Plugin manager: LIST/INFO/START/STOP/ENABLE/DISABLE/DELETE/UPLOAD/UPLOAD_META/UPLOAD_LANG/ABORT/DEBUG/FEATURES",
621 cmdPluginDispatch, CMD_MODULE, true, kPluginSubs});
622 reg.registerCommand({"FEATURE_LIST",
623 "List external features provided by installed plugins",
624 cmdFeatures, CMD_MODULE, true, nullptr});
625 reg.registerCommand({"LANG",
626 "i18n overlay: INFO/RELOAD",
627 cmdLangDispatch, CMD_MODULE, true, kLangSubs});
628 LOG_I(TAG, "PLUGIN and LANG serial commands registered");
629}
630
631} // 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 uninstallPlugin(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 bool hostActive()
Reports whether a USB host currently holds the volume over MSC.
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:78
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:306
bool loadOverlay()
Rescan available languages and (re)load the active overlay.
Definition I18n.cpp:555
static constexpr const char * OVERLAY_DIR
Definition I18n.h:70
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