30#include "esp_heap_caps.h"
41static const char*
TAG =
"PLG_CMD";
49 std::string target_path;
51 size_t total_size = 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;
61static UploadSession s_upload{};
65static inline uint32_t crc32_update(uint32_t crc, uint8_t b) {
67 for (
int i = 0; i < 8; ++i) {
68 crc = (crc >> 1) ^ (0xedb88320u & -(crc & 1));
76static constexpr int64_t UPLOAD_INACTIVITY_LIMIT_US = 15 * 1000000LL;
78static void touch_upload_activity() {
79 s_upload.last_activity_us = esp_timer_get_time();
82void send(
const char* line) {
86void sendf(
const char* fmt, ...)
91 vsnprintf(buf,
sizeof(buf), fmt, ap);
97static esp_timer_handle_t s_upload_timeout_timer =
nullptr;
98static void on_upload_timeout(
void*);
102static uint8_t s_byte_buffer[256];
103static size_t s_byte_buffer_pos = 0;
105static void ensure_timeout_timer() {
106 if (s_upload_timeout_timer)
return;
107 esp_timer_create_args_t args = {
108 .callback = on_upload_timeout,
110 .dispatch_method = ESP_TIMER_TASK,
111 .name =
"plugin_upload_to",
112 .skip_unhandled_events =
true,
114 esp_timer_create(&args, &s_upload_timeout_timer);
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);
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);
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");
141 esp_timer_start_once(s_upload_timeout_timer,
142 UPLOAD_INACTIVITY_LIMIT_US - since);
149static void finalize_upload()
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));
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));
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");
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));
184 s_upload = UploadSession{};
185 if (s_upload_timeout_timer) esp_timer_stop(s_upload_timeout_timer);
190 if (was_wasm && !uploaded_id.empty()) {
192 if (mf && mf->capabilities.background) {
202static inline void flush_byte_buffer()
204 if (s_byte_buffer_pos == 0)
return;
206 std::fwrite(s_byte_buffer, 1, s_byte_buffer_pos, s_upload.fp.get());
208 s_byte_buffer_pos = 0;
211extern "C" void upload_byte(uint8_t b)
213 if (!s_upload.active)
return;
218 touch_upload_activity();
220 s_byte_buffer[s_byte_buffer_pos++] = b;
221 s_upload.running_crc = crc32_update(s_upload.running_crc, b);
224 if (s_byte_buffer_pos >=
sizeof(s_byte_buffer)) {
226 rearm_upload_timeout();
229 if (s_upload.received >= s_upload.total_size) {
236void cmdList(
const char*)
240 for (
size_t i = 0; i < ids.size(); ++i) {
241 std::string
name = ids[i];
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;
253 sendf(
" {\"id\":\"%s\",\"name\":\"%s\",\"version\":\"%s\",\"disabled\":%s}%s",
255 disabled ?
"true" :
"false",
256 (i + 1 < ids.size()) ?
"," :
"");
261void cmdInfo(
const char* args)
263 if (!args || !*args) { send(
"ERR missing_id");
return; }
264 std::string
id = args;
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",
275 const auto& c = mf->capabilities;
279 auto addCap = [&caps](
bool on,
const char*
name) {
280 if (on) {
if (!caps.empty()) caps +=
' '; caps +=
name; }
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());
298 auto joinPins = [](
const std::vector<uint8_t>& v) {
301 for (uint8_t p : v) {
302 if (!s.empty()) s +=
',';
303 snprintf(num,
sizeof(num),
"%u",
static_cast<unsigned>(p));
308 auto joinStr = [](
const std::vector<std::string>& v) {
310 for (
const auto& e : v) {
if (!s.empty()) s +=
','; s += e; }
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());
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());
330 sendf(
" - %s (on_fail=%s)", p.name.c_str(), p.on_fail.c_str());
335void cmdDelete(
const char* args)
337 if (!args || !*args) { send(
"ERR missing_id");
return; }
338 std::string
id = args;
348void cmdDisable(
const char* args)
350 if (!args || !*args) { send(
"ERR missing_id");
return; }
352 send(
"ERR not_found");
355 sendf(
"OK disabled %s", args);
358void cmdEnable(
const char* args)
360 if (!args || !*args) { send(
"ERR missing_id");
return; }
362 send(
"ERR not_found");
365 sendf(
"OK enabled %s", args);
368void cmdStart(
const char* args)
370 if (!args || !*args) { send(
"ERR missing_id");
return; }
373 sendf(
"OK started %s", args);
375 sendf(
"ERR disabled %s", args);
377 sendf(
"ERR start %d %s",
static_cast<int>(res), args);
381void cmdStop(
const char*)
384 else send(
"ERR no_active_plugin");
387void cmdCmd(
const char* args)
389 if (!args || !*args) { send(
"ERR missing_id");
return; }
391 char id_buf[64] = {0};
393 std::sscanf(args,
"%63s%n", id_buf, &consumed);
394 if (id_buf[0] ==
'\0') { send(
"ERR missing_id");
return; }
396 const char* cmd = args + consumed;
397 while (*cmd ==
' ') ++cmd;
398 std::string
id = id_buf;
401 sendf(
"ERR disabled %s",
id.c_str());
405 bool started_here =
false;
409 sendf(
"ERR disabled %s",
id.c_str());
413 sendf(
"ERR start %d %s",
static_cast<int>(res),
id.c_str());
420 else send(
"ERR no_handler");
426enum class PluginUploadKind { Wasm, Aot, Meta, Lang };
432void arm_upload(uint32_t crc)
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";
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();
450void start_upload(
const char* args, PluginUploadKind kind)
452 if (s_upload.active) { send(
"ERR upload_in_progress");
return; }
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>");
463 if (kind == PluginUploadKind::Wasm || kind == PluginUploadKind::Aot) {
464 const std::string target_id = id_buf;
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);
476 case PluginUploadKind::Wasm:
480 case PluginUploadKind::Aot:
484 case PluginUploadKind::Meta:
487 case PluginUploadKind::Lang:
492 arm_upload(
static_cast<uint32_t
>(crc_arg));
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); }
500void cmdAbort(
const char*)
502 if (!s_upload.active) { send(
"OK no_upload");
return; }
507void cmdDebug(
const char*)
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",
516 for (
auto* t : PLUGIN_TAGS) esp_log_level_set(t, level);
517 esp_log_level_set(
"host_*", level);
519 if (!s_debug_enabled) {
520 send(
"OK plugin debug DISABLED");
524 sendf(
"OK plugin debug ENABLED");
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");
538void cmdLangInfo(
const char*)
541 sendf(
"lang: %s", i18n.getLanguageCode().c_str());
543 sendf(
"avail: %u",
static_cast<unsigned>(i18n.availableOverlayLanguages().size()));
544 for (
const auto& c : i18n.availableOverlayLanguages()) {
545 sendf(
" - %s", c.code.c_str());
549void cmdLangReload(
const char*)
552 else send(
"ERR reload_failed");
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},
572void cmdPluginDispatch(
const char* args) {
577 {
"INFO",
"",
"Show active language and available overlays", cmdLangInfo},
578 {
"RELOAD",
"",
"Rescan + reload overlays from /plugins/i18n/", cmdLangReload},
579 {
nullptr,
nullptr,
nullptr,
nullptr},
581void cmdLangDispatch(
const char* args) {
589 if (!abs_path || size == 0) { send(
"ERR bad_args");
return false; }
590 if (s_upload.active) { send(
"ERR upload_in_progress");
return false; }
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;
597 return s_upload.active;
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");
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]
Shared RAII wrappers for firmware resources.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
#define LOG_I(tag, fmt,...)
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.
virtual void setByteInterceptor(ByteInterceptor interceptor)
static void touchAuthSession()
Keeps the auth session alive during a long-running serial activity.
static I18n & instance()
Singleton accessor.
bool loadOverlay()
Rescan available languages and (re)load the active overlay.
static constexpr const char * OVERLAY_DIR
Directory on the plugins FAT holding the per-language files.
FilePtr openFile(const char *path, const char *mode) noexcept
Open a FILE* and wrap it in a FilePtr.
std::unique_ptr< std::FILE, FileCloseDeleter > FilePtr
unique_ptr for FILE* handles. Destructor calls std::fclose.
void registerPluginSerialCommands()
static const char * CMD_MODULE
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.