CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
PluginManager.cpp
Go to the documentation of this file.
7#include "wamr_runtime/Wamr.h"
8#include "cdc_ui/I18n.h"
9#include "cdc_log.h"
10#include "cdc_views/ToastView.h"
11
12#include "freertos/FreeRTOS.h"
13#include "freertos/task.h"
14#include "freertos/semphr.h"
15#include "esp_timer.h"
16
20#include "cdc_ui/ViewStack.h"
22
23extern "C" {
24#include "bh_log.h"
25}
26
27extern "C" void plg_ble_pump(void);
28extern "C" void plg_ble_on_unload(void* plugin);
29extern "C" void plg_msg_pump(void);
30extern "C" void plg_msg_on_unload(void* plugin);
31extern "C" void plg_msg_init(void);
32extern "C" void plg_gpio_on_unload(void* plugin);
33extern "C" void plg_http_on_unload(void* plugin);
34extern "C" void plg_socket_on_unload(void* plugin);
35
36#include <algorithm>
37#include <cstdio>
38#include <cstring>
39#include <utility>
40
41namespace cdc::plugin_manager {
42
43static const char* TAG = "PLG_MGR";
44
45namespace {
46
47constexpr uint32_t TICK_INTERVAL_MS = 50;
48constexpr uint32_t TICK_STACK_BYTES = 12288;
49constexpr UBaseType_t TICK_PRIORITY = 5;
50
51// Upper bound on background plugins force-unloaded for trapping in a single
52// dispatch pass. Any beyond this are handled on the next pass.
53constexpr size_t kMaxTrapsPerDispatch = 8;
54
55void invokeDeinit(Plugin& plugin)
56{
57 int32_t rc = 0;
58 if (!plugin.callI("plugin_deinit", {}, &rc)) {
59 LOG_W(TAG, "plugin_deinit missing for %s", plugin.id().c_str());
60 } else if (rc != 0) {
61 LOG_W(TAG, "plugin_deinit returned %ld for %s",
62 static_cast<long>(rc), plugin.id().c_str());
63 }
64}
65
66// Reflect a plugin's sleep inhibitor into the SleepManager. The id string
67// (stable for the plugin's lifetime in RAM) is used as the inhibitor reason,
68// so it must be released before the plugin is unloaded.
69void applySleepInhibitor(const Plugin& plugin, bool on)
70{
72 if (on) {
73 // Auto-acquire only for the static prevent_sleep capability. Dynamic
74 // inhibitors are acquired by the plugin via host_set_sleep_inhibit.
75 if (!plugin.manifest().capabilities.prevent_sleep) return;
76 sm.addSleepInhibitor(plugin.id().c_str());
77 } else {
78 // Always release on unload: covers both the prevent_sleep capability
79 // and any inhibitor acquired dynamically. removeSleepInhibitor is a
80 // no-op when no matching inhibitor is held.
81 sm.removeSleepInhibitor(plugin.id().c_str());
82 }
83}
84
85struct ScopedLock {
86 SemaphoreHandle_t m;
87 bool taken = false;
88 explicit ScopedLock(SemaphoreHandle_t s, uint32_t timeout_ms = 1000) : m(s) {
89 if (m) taken = xSemaphoreTakeRecursive(m, pdMS_TO_TICKS(timeout_ms)) == pdTRUE;
90 }
91 ~ScopedLock() { if (taken && m) xSemaphoreGiveRecursive(m); }
92 explicit operator bool() const { return taken; }
93};
94
95} // namespace
96
97PluginManager& PluginManager::instance() noexcept
98{
99 static PluginManager s;
100 return s;
101}
102
103PluginManager::PluginManager() = default;
104PluginManager::~PluginManager() = default;
105
107{
108 if (initialised_) return true;
109
110 bh_log_set_verbose_level(BH_LOG_LEVEL_FATAL);
111
112 if (!cdc::wamr::init()) { LOG_E(TAG, "WAMR init failed"); return false; }
113 if (!PluginStorage::mount()) { LOG_E(TAG, "FAT mount failed"); return false; }
114 if (!register_host_imports()) { LOG_E(TAG, "imports failed"); return false; }
115
116 call_mutex_ = xSemaphoreCreateRecursiveMutex();
117 if (!call_mutex_) { LOG_E(TAG, "plugin mutex create failed"); return false; }
118
119 auto ids = PluginStorage::listPluginIds();
120 LOG_I(TAG, "PluginManager ready: %u plugin(s) installed",
121 static_cast<unsigned>(ids.size()));
122
123 if (!msg_index_mutex_) msg_index_mutex_ = xSemaphoreCreateMutex();
124 rebuildMessageIndex();
125 plg_msg_init(); // wire the deferred message-handler resolver into cdc_msg
126
127 initialised_ = true;
128 startTickTask();
129 loadAutoloadPlugins();
130 return true;
131}
132
134{
135 stopTickTask();
137
138 {
139 ScopedLock l(static_cast<SemaphoreHandle_t>(call_mutex_));
140 for (auto& p : background_) {
141 (void)p->callI("plugin_on_exit");
142 teardownPlugin(*p, /*runWasmDeinit=*/true);
143 }
144 background_.clear();
145 }
146
147 if (call_mutex_) {
148 vSemaphoreDelete(static_cast<SemaphoreHandle_t>(call_mutex_));
149 call_mutex_ = nullptr;
150 }
153 cdc::wamr::deinit();
154 initialised_ = false;
155}
156
157std::vector<std::string> PluginManager::listInstalledIds() const
158{
160}
161
162std::optional<PluginManifest>
163PluginManager::getManifest(const std::string& id) const
164{
165 std::string meta = PluginStorage::metaPath(id);
166 auto fp = ::cdc::core::openFile(meta.c_str(), "rb");
167 if (!fp) return std::nullopt;
168 std::fseek(fp.get(), 0, SEEK_END);
169 long n = std::ftell(fp.get());
170 if (n <= 0) return std::nullopt;
171 std::fseek(fp.get(), 0, SEEK_SET);
172 std::string buf(static_cast<size_t>(n), '\0');
173 if (std::fread(buf.data(), 1, n, fp.get()) != static_cast<size_t>(n)) {
174 return std::nullopt;
175 }
176 PluginManifest out;
177 if (!PluginManifest::parse(buf.data(), buf.size(), out)) return std::nullopt;
178 return out;
179}
180
181bool PluginManager::isPluginDisabled(const std::string& id) const
182{
183 return PluginStorage::isDisabled(id);
184}
185
186bool PluginManager::setPluginDisabled(const std::string& id, bool disabled)
187{
188 if (!getManifest(id)) return false;
189 if (!PluginStorage::setDisabled(id, disabled)) return false;
190 if (disabled) {
191 (void)unloadFromRam(id);
192 }
193 return true;
194}
195
196StartResult PluginManager::startPlugin(const std::string& id_ref)
197{
198 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
199 if (!lock) return StartResult::Busy;
200 const std::string id = id_ref;
201
202 if (isPluginDisabled(id)) {
203 LOG_W(TAG, "plugin %s is disabled", id.c_str());
205 }
206
207 if (active_ && active_->id() == id) {
208 (void)active_->callI("plugin_on_enter");
209 // Discard a stop queued by the modal that was dismissed to launch this.
210 pending_stop_.store(false, std::memory_order_release);
211 return StartResult::Ok;
212 }
213
214 if (active_) {
215 (void)active_->callI("plugin_on_exit");
216 if (active_->manifest().capabilities.background) {
217 background_.push_back(std::move(active_));
218 } else {
219 teardownPlugin(*active_, /*runWasmDeinit=*/true);
220 }
221 active_.reset();
222 }
223
224 // If the plugin is already running in the background, promote it
225 // to foreground without reloading.
226 auto it = std::find_if(background_.begin(), background_.end(),
227 [&](const std::unique_ptr<Plugin>& p) {
228 return p && p->id() == id;
229 });
230 if (it != background_.end()) {
231 auto plugin = std::move(*it);
232 background_.erase(it);
233
234 plugin_base_depth_ = cdc::ui::ViewStack::instance().depth();
235 int32_t enter_rc = 0;
236 if (!plugin->callI("plugin_on_enter", {}, &enter_rc)) {
237 LOG_E(TAG, "plugin_on_enter missing for %s", id.c_str());
238 background_.push_back(std::move(plugin)); // keep it as background
240 }
241 active_ = std::move(plugin);
242 // Discard a stop queued by the modal that was dismissed to launch this.
243 pending_stop_.store(false, std::memory_order_release);
244 LOG_I(TAG, "plugin %s promoted bg->fg", id.c_str());
245 return StartResult::Ok;
246 }
247
248 auto mf_opt = getManifest(id);
249 if (!mf_opt) {
250 LOG_W(TAG, "manifest missing/invalid for %s", id.c_str());
252 }
253 const PluginManifest& mf = *mf_opt;
254
255 auto check = CapabilityChecker::validate(mf);
256 if (!check.ok()) {
257 LOG_W(TAG, "capability check failed for %s: %s",
258 id.c_str(), check.detail.c_str());
260 }
261
262 auto plugin = std::make_unique<Plugin>();
263 if (!plugin->load(id, mf)) {
264 LOG_E(TAG, "Plugin::load failed for %s", id.c_str());
266 }
267 plugin->loadLangOverlay();
268
269 int32_t init_rc = 0;
270 if (!plugin->callI("plugin_init", {}, &init_rc) || init_rc != 0) {
271 LOG_E(TAG, "plugin_init failed for %s (rc=%ld)", id.c_str(),
272 static_cast<long>(init_rc));
273 teardownPlugin(*plugin, /*runWasmDeinit=*/!plugin->lastCallTrapped());
275 }
276
277 std::string failed_name, on_fail;
278 PrereqResult pr = Prerequisites::walk(*plugin, failed_name, on_fail);
279 if (pr == PrereqResult::HardFailed) {
280 LOG_E(TAG, "prerequisite '%s' aborted start of %s",
281 failed_name.c_str(), id.c_str());
282 teardownPlugin(*plugin, /*runWasmDeinit=*/true);
284 }
285 if (pr == PrereqResult::SoftFailed) {
286 LOG_W(TAG, "prerequisite '%s' soft-failed for %s, continuing",
287 failed_name.c_str(), id.c_str());
288 }
289
290 static cdc::ui::ToastView s_loading_toast;
291 char loading_msg[80];
292 {
293 const auto& meta = plugin->manifest().i18n_meta;
294 const char* display = id.c_str();
295 auto it = meta.find("name");
296 if (it != meta.end() && !it->second.by_lang.empty()) {
297 display = it->second.by_lang.begin()->second.c_str();
298 }
299 std::snprintf(loading_msg, sizeof(loading_msg), "%s\n%s",
300 cdc::ui::tr("core.plugin_loading"), display);
301 }
302 s_loading_toast.init(loading_msg, cdc::ui::ToastView::Icon::TASK, 0, true);
303 cdc::ui::ViewStack::instance().showModal(&s_loading_toast);
304 cdc::ui::ViewStack::instance().render(true); // paint loading toast before the blocking plugin_on_enter
305
306 auto hide_loading_if_top = []() {
307 auto& vs = cdc::ui::ViewStack::instance();
308 if (vs.getModal() == &s_loading_toast) {
309 vs.hideModal();
310 }
311 };
312
313 plugin_base_depth_ = cdc::ui::ViewStack::instance().depth();
314 int32_t enter_rc = 0;
315 if (!plugin->callI("plugin_on_enter", {}, &enter_rc)) {
316 hide_loading_if_top();
317 if (plugin->lastCallTrapped()) {
318 LOG_E(TAG, "plugin_on_enter trapped for %s: %s", id.c_str(),
319 plugin->lastTrapMessage());
320 } else {
321 LOG_E(TAG, "plugin_on_enter export missing for %s", id.c_str());
322 }
323 teardownPlugin(*plugin, /*runWasmDeinit=*/!plugin->lastCallTrapped());
325 }
326 hide_loading_if_top();
327 if (enter_rc != 0) {
328 LOG_W(TAG, "plugin_on_enter returned %ld for %s",
329 static_cast<long>(enter_rc), id.c_str());
330 }
331
332 active_ = std::move(plugin);
333 // Discard a stop queued by the modal that was dismissed to launch this.
334 pending_stop_.store(false, std::memory_order_release);
335 applySleepInhibitor(*active_, true);
336 LOG_I(TAG, "plugin %s started", id.c_str());
337 return StartResult::Ok;
338}
339
341{
342 pending_stop_.store(true, std::memory_order_release);
343}
344
346{
347 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
348 if (!lock) return false;
349 if (!active_) return false;
350 LOG_I(TAG, "stopping plugin %s", active_->id().c_str());
351 (void)active_->callI("plugin_on_exit");
352
353 // Pop the plugin's foreground views and reset its UI state before changing
354 // residency or tearing the instance down, so no live view can reference a
355 // freed/demoted plugin (mirrors unloadFromRam). The GUI stop path already
356 // runs with the views popped; the serial STOP path does not.
357 while (cdc::ui::ViewStack::instance().depth() > 1) {
359 }
361
362 // If the plugin is also a background service, demote it back instead of
363 // unloading.
364 if (active_->manifest().capabilities.background) {
365 background_.push_back(std::move(active_));
366 active_.reset();
367 return true;
368 }
369
370 teardownPlugin(*active_, /*runWasmDeinit=*/true);
371 active_.reset();
372 return true;
373}
374
375bool PluginManager::unloadFromRam(const std::string& id)
376{
377 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
378 if (!lock) return false;
379 if (active_ && active_->id() == id) {
380 LOG_I(TAG, "unloading active plugin %s from RAM", id.c_str());
381 (void)active_->callI("plugin_on_exit");
382 while (cdc::ui::ViewStack::instance().depth() > 1) {
384 }
386 teardownPlugin(*active_, /*runWasmDeinit=*/true);
387 active_.reset();
388 return true;
389 }
390 auto it = std::find_if(background_.begin(), background_.end(),
391 [&](const std::unique_ptr<Plugin>& p) {
392 return p && p->id() == id;
393 });
394 if (it == background_.end()) return false;
395 LOG_I(TAG, "unloading background plugin %s from RAM", id.c_str());
396 teardownPlugin(**it, /*runWasmDeinit=*/true);
397 background_.erase(it);
398 return true;
399}
400
402{
403 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
404 if (!lock) return;
405 if (active_) {
406 LOG_I(TAG, "unloading active plugin %s from RAM", active_->id().c_str());
407 (void)active_->callI("plugin_on_exit");
408 while (cdc::ui::ViewStack::instance().depth() > 1) {
410 }
412 teardownPlugin(*active_, /*runWasmDeinit=*/true);
413 active_.reset();
414 }
415 for (auto& p : background_) {
416 if (p) {
417 LOG_I(TAG, "unloading background plugin %s from RAM", p->id().c_str());
418 teardownPlugin(*p, /*runWasmDeinit=*/true);
419 }
420 }
421 background_.clear();
422}
423
424void PluginManager::teardownPlugin(Plugin& p, bool runWasmDeinit)
425{
426 if (runWasmDeinit) invokeDeinit(p);
427 applySleepInhibitor(p, false);
435 p.unload();
436}
437
438void PluginManager::handleTrap(Plugin& p, const char* fn)
439{
440 LOG_E(TAG, "==================== PLUGIN TRAP ====================");
441 LOG_E(TAG, "plugin '%s' trapped in %s", p.id().c_str(), fn);
442 LOG_E(TAG, " reason : %s", p.lastTrapMessage());
443 LOG_E(TAG, " action : force-unload + release all held resources");
444
445 if (&p == active_.get()) {
446 auto& vs = cdc::ui::ViewStack::instance();
447 while (vs.depth() > plugin_base_depth_ && vs.depth() > 1) vs.pop();
449 teardownPlugin(p, /*runWasmDeinit=*/false);
450 active_.reset();
451 LOG_E(TAG, "=====================================================");
452 return;
453 }
454 for (auto it = background_.begin(); it != background_.end(); ++it) {
455 if (it->get() == &p) {
456 teardownPlugin(**it, /*runWasmDeinit=*/false);
457 background_.erase(it);
458 LOG_E(TAG, "=====================================================");
459 return;
460 }
461 }
462 LOG_E(TAG, "=====================================================");
463}
464
465bool PluginManager::reloadBackgroundPlugin(const std::string& id)
466{
467 if (isPluginDisabled(id)) return false;
468
469 auto manifest = getManifest(id);
470 if (!manifest || !manifest->capabilities.background) return false;
471
472 // background:true is not "start at boot": only refresh an instance that is
473 // already running in the background so it picks up a freshly uploaded
474 // binary. An idle plugin stays unloaded until the user starts it manually.
475 if (!isRunningInBackground(id)) return false;
476
477 (void)unloadFromRam(id);
478
479 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
480 if (!lock) return false;
481 if (!loadIntoBackground(id, *manifest)) return false;
482 LOG_I(TAG, "background plugin %s reloaded", id.c_str());
483 return true;
484}
485
486bool PluginManager::loadIntoBackground(const std::string& id, const PluginManifest& mf)
487{
488 if (isPluginDisabled(id)) {
489 LOG_I(TAG, "bg load %s skipped: disabled", id.c_str());
490 return false;
491 }
492
493 auto plugin = std::make_unique<Plugin>();
494 if (!plugin->load(id, mf)) {
495 LOG_E(TAG, "bg load %s: load failed", id.c_str());
496 return false;
497 }
498 plugin->loadLangOverlay();
499 int32_t init_rc = 0;
500 if (!plugin->callI("plugin_init", {}, &init_rc) || init_rc != 0) {
501 LOG_E(TAG, "bg load %s: plugin_init failed (rc=%ld)", id.c_str(),
502 static_cast<long>(init_rc));
503 teardownPlugin(*plugin, /*runWasmDeinit=*/!plugin->lastCallTrapped());
504 return false;
505 }
506 std::string failed_name, on_fail;
507 PrereqResult pr = Prerequisites::walk(*plugin, failed_name, on_fail);
508 if (pr == PrereqResult::HardFailed) {
509 LOG_E(TAG, "bg load %s: prereq '%s' aborted", id.c_str(), failed_name.c_str());
510 teardownPlugin(*plugin, /*runWasmDeinit=*/true);
511 return false;
512 }
513 background_.push_back(std::move(plugin));
514 applySleepInhibitor(*background_.back(), true);
515 return true;
516}
517
518void PluginManager::loadAutoloadPlugins()
519{
520 auto ids = PluginStorage::listPluginIds();
521 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
522 if (!lock) return;
523 for (const auto& id : ids) {
524 auto mf = getManifest(id);
525 if (!mf || !mf->capabilities.autoload) continue;
526 if (isPluginDisabled(id)) {
527 LOG_I(TAG, "autoload %s skipped: disabled", id.c_str());
528 continue;
529 }
530 if (isLoaded(id)) continue;
531
532 auto check = CapabilityChecker::validate(*mf);
533 if (!check.ok()) {
534 LOG_W(TAG, "autoload %s rejected: %s", id.c_str(), check.detail.c_str());
535 continue;
536 }
537 if (loadIntoBackground(id, *mf)) {
538 LOG_I(TAG, "autoloaded plugin %s (resident)", id.c_str());
539 } else {
540 LOG_W(TAG, "autoload of %s failed", id.c_str());
541 }
542 }
543}
544
545void PluginManager::rebuildMessageIndex()
546{
547 std::vector<std::string> mimes, mids;
548 for (const auto& id : PluginStorage::listPluginIds()) {
549 if (isPluginDisabled(id)) continue; // a disabled plugin can't be activated
550 auto mf = getManifest(id);
551 if (!mf) continue;
552 for (const auto& mt : mf->capabilities.message_types) {
553 mimes.push_back(mt);
554 mids.push_back(id);
555 }
556 }
557 auto* m = static_cast<SemaphoreHandle_t>(msg_index_mutex_);
558 if (m) xSemaphoreTake(m, portMAX_DELAY);
559 msg_index_mime_.swap(mimes);
560 msg_index_id_.swap(mids);
561 if (m) xSemaphoreGive(m);
562}
563
564void PluginManager::maybeRefreshMessageIndex()
565{
566 uint32_t now = static_cast<uint32_t>(esp_timer_get_time() / 1000);
567 if (now - last_index_refresh_ms_ < 2000) return;
568 last_index_refresh_ms_ = now;
569 std::string sig;
570 for (const auto& id : PluginStorage::listPluginIds()) { sig += id; sig.push_back(','); }
571 if (sig == installed_sig_) return;
572 installed_sig_ = sig;
573 rebuildMessageIndex();
574}
575
576bool PluginManager::messageTypeInstalled(const char* mime) const
577{
578 if (!mime) return false;
579 auto* m = static_cast<SemaphoreHandle_t>(msg_index_mutex_);
580 // Called from the BLE host task: never wait indefinitely. On contention,
581 // fail closed (treat as not-installed -> the offer is auto-declined).
582 if (m && xSemaphoreTake(m, pdMS_TO_TICKS(20)) != pdTRUE) return false;
583 bool found = false;
584 for (const auto& mt : msg_index_mime_) {
585 if (mt == mime) { found = true; break; }
586 }
587 if (m) xSemaphoreGive(m);
588 return found;
589}
590
592{
593 if (!mime) return false;
594 std::string id;
595 {
596 auto* m = static_cast<SemaphoreHandle_t>(msg_index_mutex_);
597 if (m) xSemaphoreTake(m, portMAX_DELAY);
598 for (size_t i = 0; i < msg_index_mime_.size(); ++i) {
599 if (msg_index_mime_[i] == mime) { id = msg_index_id_[i]; break; }
600 }
601 if (m) xSemaphoreGive(m);
602 }
603 if (id.empty()) return false;
604
605 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
606 if (!lock) return false;
607 if (isLoaded(id)) return true; // already running: its handler is live
608 if (isPluginDisabled(id)) return false;
609 auto mf = getManifest(id);
610 if (!mf) return false;
611 auto check = CapabilityChecker::validate(*mf);
612 if (!check.ok()) {
613 LOG_W(TAG, "msg activate %s rejected: %s", id.c_str(), check.detail.c_str());
614 return false;
615 }
616 return loadIntoBackground(id, *mf);
617}
618
619uint8_t PluginManager::getLockscreenItems(LockscreenItem* out, uint8_t max) const
620{
622 const uint8_t n = collectLockscreenItems(regs, sizeof(regs) / sizeof(regs[0]));
623 const uint8_t copy = n < max ? n : max;
624 const std::string lang = ::cdc::ui::I18n::instance().getLanguageCode();
625
626 for (uint8_t i = 0; i < copy; ++i) {
627 Plugin* p = static_cast<Plugin*>(regs[i].plugin);
628 out[i].plugin = p;
629 out[i].action_id = regs[i].action_id;
630
631 const char* label = nullptr;
632 if (p) {
633 label = p->trKey(regs[i].label_key); // lang overlay
634 if (!label) {
635 // Fallback: manifest i18n.strings.<key>.<lang|en|first>
636 const auto& strings = p->manifest().i18n_strings;
637 auto it = strings.find(regs[i].label_key);
638 if (it != strings.end()) {
639 auto pick = [&](const std::string& l) -> const char* {
640 auto lit = it->second.by_lang.find(l);
641 return lit != it->second.by_lang.end() ? lit->second.c_str() : nullptr;
642 };
643 label = pick(lang);
644 if (!label) label = pick("en");
645 if (!label && !it->second.by_lang.empty())
646 label = it->second.by_lang.begin()->second.c_str();
647 }
648 }
649 }
650 if (!label) label = regs[i].label_key;
651 std::strncpy(out[i].label, label, sizeof(out[i].label) - 1);
652 out[i].label[sizeof(out[i].label) - 1] = '\0';
653 }
654 return copy;
655}
656
661
662bool PluginManager::hasActivePlugin() const noexcept { return active_ != nullptr; }
663std::string PluginManager::activePluginId() const { return active_ ? active_->id() : std::string{}; }
664
665bool PluginManager::isLoaded(const std::string& id) const
666{
667 if (active_ && active_->id() == id) return true;
668 for (const auto& p : background_) if (p->id() == id) return true;
669 return false;
670}
671
672bool PluginManager::isRunningInBackground(const std::string& id) const
673{
674 for (const auto& p : background_) if (p->id() == id) return true;
675 return false;
676}
677
679{
680 return active_ && active_->manifest().capabilities.background;
681}
682
684{
685 return active_ && active_->manifest().capabilities.prevent_sleep;
686}
687
689{
690 return !background_.empty();
691}
692
693void PluginManager::dispatchButton(uint32_t button_code)
694{
695 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
696 if (!lock) return;
697 if (!active_) return;
698 int32_t rc = 0;
699 (void)active_->callI("plugin_on_button",
700 {static_cast<int32_t>(button_code)}, &rc);
701 if (active_->lastCallTrapped()) handleTrap(*active_, "plugin_on_button");
702}
703
704void PluginManager::dispatchAction(uint32_t action_id, uint32_t idx, uint32_t user_data)
705{
706 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
707 if (!lock) return;
708 if (!active_) return;
709 int32_t rc = 0;
710 (void)active_->callI("plugin_on_action",
711 {static_cast<int32_t>(action_id),
712 static_cast<int32_t>(idx),
713 static_cast<int32_t>(user_data)}, &rc);
714 if (active_->lastCallTrapped()) handleTrap(*active_, "plugin_on_action");
715}
716
717void PluginManager::dispatchActionTo(Plugin* plugin, uint32_t action_id,
718 uint32_t idx, uint32_t user_data)
719{
720 if (!plugin) return;
721 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
722 if (!lock) return;
723
724 bool found = (active_.get() == plugin);
725 if (!found) {
726 for (auto& p : background_) if (p.get() == plugin) { found = true; break; }
727 }
728 if (!found) return; // stale subscription, plugin no longer loaded
729
730 int32_t rc = 0;
731 (void)plugin->callI("plugin_on_action",
732 {static_cast<int32_t>(action_id),
733 static_cast<int32_t>(idx),
734 static_cast<int32_t>(user_data)}, &rc);
735 if (plugin->lastCallTrapped()) handleTrap(*plugin, "plugin_on_action");
736}
737
738void PluginManager::dispatchTick(uint64_t uptime_ms)
739{
740 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
741 if (!lock) return;
742 int32_t rc = 0;
743 const int32_t hi = static_cast<int32_t>(uptime_ms >> 32);
744 const int32_t lo = static_cast<int32_t>(uptime_ms & 0xFFFFFFFFu);
745 if (active_) {
746 (void)active_->callI("plugin_on_tick", {lo, hi}, &rc);
747 if (active_->lastCallTrapped()) handleTrap(*active_, "plugin_on_tick");
748 }
749 Plugin* trapped[kMaxTrapsPerDispatch];
750 size_t n_trapped = 0;
751 for (auto& p : background_) {
752 (void)p->callI("plugin_on_tick", {lo, hi}, &rc);
753 if (p->lastCallTrapped() && n_trapped < kMaxTrapsPerDispatch)
754 trapped[n_trapped++] = p.get();
755 }
756 for (size_t i = 0; i < n_trapped; ++i) handleTrap(*trapped[i], "plugin_on_tick");
757 plg_ble_pump();
758 plg_msg_pump();
759}
760
761void PluginManager::dispatchEventAll(uint32_t event_type, uint32_t value)
762{
763 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
764 if (!lock) return;
765 int32_t rc = 0;
766 if (active_) {
767 (void)active_->callI("plugin_on_event",
768 {static_cast<int32_t>(event_type),
769 static_cast<int32_t>(value)}, &rc);
770 if (active_->lastCallTrapped()) handleTrap(*active_, "plugin_on_event");
771 }
772 Plugin* trapped[kMaxTrapsPerDispatch];
773 size_t n_trapped = 0;
774 for (auto& p : background_) {
775 (void)p->callI("plugin_on_event",
776 {static_cast<int32_t>(event_type),
777 static_cast<int32_t>(value)}, &rc);
778 if (p->lastCallTrapped() && n_trapped < kMaxTrapsPerDispatch)
779 trapped[n_trapped++] = p.get();
780 }
781 for (size_t i = 0; i < n_trapped; ++i) handleTrap(*trapped[i], "plugin_on_event");
782}
783
784bool PluginManager::dispatchCmd(const std::string& id, const char* cmd, size_t len)
785{
786 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
787 if (!lock) return false;
788
789 Plugin* target = (active_ && active_->id() == id) ? active_.get() : nullptr;
790 if (!target) {
791 for (auto& p : background_) if (p->id() == id) { target = p.get(); break; }
792 }
793 if (!target || !target->hasExport("plugin_on_cmd")) return false;
794
795 pending_cmd_.assign(cmd ? cmd : "", len);
796 int32_t rc = 0;
797 (void)target->callI("plugin_on_cmd", {static_cast<int32_t>(len)}, &rc);
798 pending_cmd_.clear();
799 if (target->lastCallTrapped()) handleTrap(*target, "plugin_on_cmd");
800 return true;
801}
802
803int PluginManager::consumeCmd(char* out, size_t out_size)
804{
805 if (!out || out_size == 0) return HOST_ERR_INVALID_ARG;
806 size_t n = pending_cmd_.size();
807 if (n >= out_size) n = out_size - 1;
808 std::memcpy(out, pending_cmd_.data(), n);
809 out[n] = '\0';
810 pending_cmd_.clear();
811 return static_cast<int>(n);
812}
813
814void PluginManager::forEachPlugin(const std::function<bool(Plugin&)>& visitor)
815{
816 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
817 if (!lock) return;
818 if (active_) { if (!visitor(*active_)) return; }
819 for (auto& p : background_) {
820 if (!visitor(*p)) return;
821 }
822}
823
825{
826 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
827 if (!lock) return;
828 if (active_) active_->loadLangOverlay();
829 for (auto& p : background_) p->loadLangOverlay();
830}
831
832void PluginManager::tickTaskTrampoline(void* arg)
833{
834 static_cast<PluginManager*>(arg)->tickTaskLoop();
835 vTaskDelete(nullptr);
836}
837
838void PluginManager::tickTaskLoop()
839{
840 TickType_t last = xTaskGetTickCount();
841 while (!tick_stop_) {
842 vTaskDelayUntil(&last, pdMS_TO_TICKS(TICK_INTERVAL_MS));
843 if (tick_stop_) break;
844 maybeRefreshMessageIndex();
845 if (pending_stop_.exchange(false, std::memory_order_acq_rel)) {
847 PluginListView* listView = PluginListView::active();
848 if (top && listView && top == static_cast<cdc::ui::IView*>(static_cast<cdc::ui::ViewBase*>(listView))) {
850 }
851 }
852 dispatchTick(esp_timer_get_time() / 1000);
853 }
854}
855
856void PluginManager::startTickTask()
857{
858 if (tick_task_) return;
859 tick_stop_ = false;
860 TaskHandle_t handle = nullptr;
861 if (xTaskCreate(&PluginManager::tickTaskTrampoline,
862 "plg_tick", TICK_STACK_BYTES, this,
863 TICK_PRIORITY, &handle) != pdPASS) {
864 LOG_E(TAG, "failed to spawn plg_tick task");
865 return;
866 }
867 tick_task_ = handle;
868}
869
870void PluginManager::stopTickTask()
871{
872 if (!tick_task_) return;
873 tick_stop_ = true;
874 // The task self-deletes after its next wake-up.
875 // Wait briefly so it doesn't reference us after we go away.
876 vTaskDelay(pdMS_TO_TICKS(TICK_INTERVAL_MS * 2));
877 tick_task_ = nullptr;
878}
879
880} // namespace cdc::plugin_manager
static const char * TAG
Load-time validation of plugin capabilities + manifest sanity.
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
Internal registry of plugin lockscreen quick-actions.
Main-menu entry "Plugins" - lists all installed WASM plugins.
void plg_msg_pump(void)
void plg_ble_on_unload(void *plugin)
void plg_msg_on_unload(void *plugin)
void plg_ble_pump(void)
void plg_msg_init(void)
void plg_http_on_unload(void *plugin)
void plg_socket_on_unload(void *plugin)
void plg_gpio_on_unload(void *plugin)
Discovers, loads, runs and unloads WASM plugins on the badge.
Singleton that owns plugin-pushed UI views (lists, confirms, inputs).
Owned WAMR module instance + per-plugin state.
Walks the prerequisites list of a plugin manifest before plugin_on_enter.
Registers the host API as WAMR native imports under module "cdc".
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
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
static CapabilityCheckResult validate(const PluginManifest &manifest)
static PluginListView * active() noexcept
Currently-mounted PluginListView instance, or nullptr if none.
bool dispatchCmd(const std::string &id, const char *cmd, size_t len)
bool activateForMessageType(const char *mime)
Load + start (headless) the installed plugin that declares this MIME type, so its message handler bec...
uint8_t getLockscreenItems(LockscreenItem *out, uint8_t max) const
Snapshot of all plugin lockscreen items. Returns the number written.
void dispatchAction(uint32_t action_id, uint32_t idx, uint32_t user_data)
int consumeCmd(char *out, size_t out_size)
bool hasBackgroundPlugin() const noexcept
True if at least one plugin is currently resident in the background slot.
bool isLoaded(const std::string &id) const
True if a plugin with id is loaded in RAM (foreground or background).
void dispatchButton(uint32_t button_code)
bool hasActivePlugin() const noexcept
bool isRunningInBackground(const std::string &id) const
True if a plugin with id is currently resident in the background slot.
void forEachPlugin(const std::function< bool(Plugin &)> &visitor)
Iterate foreground + background plugins. Visitor returns false to stop.
std::vector< std::string > listInstalledIds() const
static PluginManager & instance() noexcept
bool unloadFromRam(const std::string &id)
void dispatchActionTo(Plugin *plugin, uint32_t action_id, uint32_t idx, uint32_t user_data)
bool setPluginDisabled(const std::string &id, bool disabled)
void dispatchTick(uint64_t uptime_ms)
void triggerLockscreenItem(const LockscreenItem &item)
Fire plugin_on_action(item.action_id, 0, 0) on the owning plugin.
bool reloadBackgroundPlugin(const std::string &id)
bool messageTypeInstalled(const char *mime) const
True if any installed plugin's manifest declares this MIME type for message transfer....
bool isPluginDisabled(const std::string &id) const
std::optional< PluginManifest > getManifest(const std::string &id) const
void dispatchEventAll(uint32_t event_type, uint32_t value)
StartResult startPlugin(const std::string &id)
static bool mount()
Mount the plugins partition. Auto-formats if empty.
static void unmount()
Unmount the plugins partition (rarely used; mostly tests).
static bool isDisabled(const std::string &id)
True when the plugin has a persistent disabled marker.
static bool setDisabled(const std::string &id, bool disabled)
Create or remove the persistent disabled marker for a plugin.
static std::vector< std::string > listPluginIds()
Discover all installed plugin ids. A plugin is recognised by the presence of both <id>....
static std::string metaPath(const std::string &id)
Returns the full VFS path of <id>.meta.
static PluginUiState & instance() noexcept
bool hasExport(const char *name) const
Definition Plugin.cpp:128
const char * trKey(const char *key) const noexcept
Look up a plugin-local translation key in the loaded overlay.
Definition Plugin.cpp:260
bool lastCallTrapped() const noexcept
Definition Plugin.h:82
bool callI(const char *name, std::initializer_list< int32_t > args={}, int32_t *out_i32=nullptr)
Call an exported i32(i32...)->i32 function by name.
Definition Plugin.cpp:145
void unload() noexcept
Destroy WAMR instance + free bytecode buffer. Idempotent.
Definition Plugin.cpp:134
const std::string & id() const noexcept
Definition Plugin.h:89
const PluginManifest & manifest() const noexcept
Definition Plugin.h:88
static PrereqResult walk(Plugin &plugin, std::string &out_failed_name, std::string &out_on_fail)
Walk the plugin's prerequisite list in order. Marks acquired resources on the Plugin so release() can...
static void release(Plugin &plugin)
Release every resource the plugin acquired during walk(), in reverse order of acquisition.
const std::string & getLanguageCode() const
Current language code (lower-case ISO-639-1, e.g. "en", "de").
Definition I18n.h:130
static I18n & instance()
Singleton accessor.
Definition I18n.cpp:287
static SleepManager & instance()
Returns singleton sleep manager instance.
void init(const char *message, Icon icon=Icon::NONE, uint16_t durationMs=1500, bool dismissible=true)
Initializes toast message content and timing behavior.
Definition ToastView.cpp:26
IView * current() const
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:34
void showModal(IView *modal)
uint8_t depth() const
Definition ViewStack.h:83
CDC Badge OS plugin host API - canonical C ABI contract.
#define HOST_ERR_INVALID_ARG
Definition host_api.h:39
void plg_ble_on_unload(void *plugin)
void plg_ble_pump(void)
void plg_gpio_on_unload(void *plugin)
void plg_http_on_unload(void *plugin)
void plg_msg_pump(void)
void plg_msg_on_unload(void *plugin)
void plg_msg_init(void)
void plg_socket_on_unload(void *plugin)
FilePtr openFile(const char *path, const char *mode) noexcept
Open a FILE* and wrap it in a FilePtr.
Definition Raii.h:87
void unregister_host_imports()
Unregister the imports (called from PluginManager::deinit()).
uint8_t collectLockscreenItems(LockscreenRegistration *out, uint8_t max)
static const char * TAG
void clearLockscreenRegistrationFor(void *plugin)
bool register_host_imports()
Register the "cdc" import namespace with WAMR.
const char * tr(const char *key)
Look up a translation by string key.
Definition I18n.h:208
bool autoload
Start this plugin as a resident background instance at badge boot. Plugins without this flag stay unl...
std::vector< std::string > message_types
static bool parse(const char *json, size_t len, PluginManifest &out)
Parse meta.json content. Returns false on schema errors.
std::map< std::string, LocalizedString > i18n_strings