CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
ModuleRegistry.cpp
Go to the documentation of this file.
4#include "cdc_core/EventBus.h"
6#include "cdc_core/Raii.h"
7#include "module_defaults.h"
8#include "cdc_log.h"
9#include <nvs_flash.h>
10#include <nvs.h>
11#include <cstring>
12#include <cstdio>
13#include <algorithm>
14#include <memory>
15
16static const char* TAG = "ModuleReg";
17
18namespace {
19struct ModuleDefault {
20 const char* name;
21 bool enabled;
22};
23
24#define MODULE_DEFAULT_ENTRY(n, en) { n, en },
25constexpr ModuleDefault kModuleDefaults[] = {
27};
28#undef MODULE_DEFAULT_ENTRY
29
30const ModuleDefault* findModuleDefault(const char* name) {
31 for (const auto& e : kModuleDefaults) {
32 if (strcmp(e.name, name) == 0) return &e;
33 }
34 return nullptr;
35}
36} // namespace
37
38namespace cdc::core {
39
44ModuleRegistry& ModuleRegistry::instance() {
45 static ModuleRegistry* instance = new ModuleRegistry();
46 return *instance;
47}
48
54 if (!initFunc) return;
55
56 if (initCount_ >= MAX_INITIALIZERS) {
57 LOG_E(TAG, "Module initializer registry full");
58 return;
59 }
60
61 initializers_[initCount_++] = initFunc;
62}
63
68 // Modules access TropicStorage during their init() (slot-map lookups,
69 // initial chunk reads). Hard-fail loudly if main() forgot to bring it
70 // up first instead of letting modules run on undefined state.
71 auto storageState = TropicStorage::instance().getState();
72 if (storageState != ServiceState::STARTED) {
73 LOG_E(TAG, "TropicStorage not STARTED (state=%d) before module init - aborting",
74 static_cast<int>(storageState));
75 return;
76 }
77
78 LOG_I(TAG, "Running %d module initializers", initCount_);
79
80 for (uint8_t i = 0; i < initCount_; i++) {
81 if (initializers_[i]) {
82 initializers_[i]();
83 }
84 }
85
86 // Every registered module must have an explicit entry in module_defaults.h.
87 // A missing entry is a build misconfiguration.
88 for (uint8_t i = 0; i < count_; i++) {
89 if (!findModuleDefault(modules_[i]->getName())) {
90 LOG_E(TAG, "Module '%s' missing from MODULE_DEFAULT_MAP (module_defaults.h)",
91 modules_[i]->getName());
92 }
93 }
94
95 // Load disabled modules list from NVS (name-based, robust against order changes)
96 loadDisabledList();
97
98 // Initializers only init() their modules; exactly the enabled set is
99 // started here. The stop pass enforces that contract for any module
100 // started out-of-band, so a disabled module can never hold resources
101 // (e.g. USB interface slots) across boot.
102 for (uint8_t i = 0; i < count_; i++) {
103 if (!isModuleEnabled(i) && modules_[i]->getState() == ServiceState::STARTED) {
104 LOG_W(TAG, "Stopping disabled module '%s' (started during init)", modules_[i]->getName());
105 modules_[i]->stop();
106 }
107 }
108 for (uint8_t i = 0; i < count_; i++) {
109 if (isModuleEnabled(i) && !startModule(i)) {
110 LOG_W(TAG, "Enabled module '%s' failed to start", modules_[i]->getName());
111 }
112 }
113
114 // After all modules are registered, clean up orphaned NVS data
115 cleanupOrphanedModuleData();
116
117 // Save current module list for next boot
118 saveModuleList();
119}
120
127 if (!module) {
128 LOG_E(TAG, "Cannot register null module");
129 return false;
130 }
131
132 if (count_ >= MAX_MODULES) {
133 LOG_E(TAG, "Module registry full, cannot register '%s'", module->getName());
134 return false;
135 }
136
137 // Check for duplicate
138 for (uint8_t i = 0; i < count_; i++) {
139 if (strcmp(modules_[i]->getName(), module->getName()) == 0) {
140 LOG_W(TAG, "Module '%s' already registered", module->getName());
141 return false;
142 }
143 }
144
145 modules_[count_++] = module;
146 clearModuleError(count_ - 1);
147 (void)applySlotRequest(module, count_ - 1);
148 LOG_I(TAG, "Registered module '%s' v%s", module->getName(), module->getVersion());
149 return true;
150}
151
157 if (!name) return;
158
159 for (uint8_t i = 0; i < count_; i++) {
160 if (strcmp(modules_[i]->getName(), name) == 0) {
161 // Shift remaining modules
162 for (uint8_t j = i; j < count_ - 1; j++) {
163 modules_[j] = modules_[j + 1];
164 }
165 modules_[--count_] = nullptr;
166 LOG_I(TAG, "Unregistered module '%s'", name);
167 return;
168 }
169 }
170}
171
178 if (!name) return nullptr;
179
180 for (uint8_t i = 0; i < count_; i++) {
181 if (strcmp(modules_[i]->getName(), name) == 0) {
182 return modules_[i];
183 }
184 }
185 return nullptr;
186}
187
194 if (index >= count_) return nullptr;
195 return modules_[index];
196}
197
203 bool allOk = true;
204 for (uint8_t i = 0; i < count_; i++) {
205 if (!modules_[i]->init()) {
206 LOG_E(TAG, "Failed to init module '%s'", modules_[i]->getName());
207 allOk = false;
208 }
209 }
210 return allOk;
211}
212
218 bool allOk = true;
219 for (uint8_t i = 0; i < count_; i++) {
220 // Skip disabled modules
221 if (!isModuleEnabled(i)) {
222 LOG_I(TAG, "Module '%s' is disabled, skipping start", modules_[i]->getName());
223 continue;
224 }
225
226 if (!startModule(i)) {
227 allOk = false;
228 }
229 }
230 return allOk;
231}
232
238bool ModuleRegistry::startModule(uint8_t index) {
239 if (index >= count_) return false;
240 IModule* module = modules_[index];
241 if (!module) return false;
242
243 if (hasModuleSlotError(index)) {
244 LOG_E(TAG, "Module '%s' blocked: %s", module->getName(),
245 getModuleSlotError(index) ? getModuleSlotError(index) : "slot map error");
246 return false;
247 }
248
249 if (module->getState() == ServiceState::INITIALIZED ||
250 module->getState() == ServiceState::STOPPED) {
251 if (!module->start()) {
252 LOG_E(TAG, "Failed to start module '%s'", module->getName());
253 return false;
254 }
255 }
256
257 return true;
258}
259
266 if (getModuleSlotError(index)) {
268 }
269 // Best-effort attribution: reflects current global HID occupancy, not a
270 // precise per-call cause for this specific module start.
271 if (UsbManager::instance().hidSlotsFull()) {
273 }
275}
276
281 for (uint8_t i = 0; i < count_; i++) {
282 if (modules_[i]->getState() == ServiceState::STARTED) {
283 modules_[i]->stop();
284 }
285 }
286}
287
295uint8_t ModuleRegistry::getMenuItems(MenuLocation location, ModuleMenuItem* items, uint8_t maxItems) {
296 if (!items || maxItems == 0) return 0;
297
298 uint8_t totalCount = 0;
299
300 // Collect items from all modules
301 for (uint8_t i = 0; i < count_; i++) {
302 if (modules_[i]->getState() != ServiceState::STARTED) continue;
303
304 ModuleMenuItem moduleItems[8] = {};
305 uint8_t count = modules_[i]->getMenuItems(moduleItems, 8);
306
307 for (uint8_t j = 0; j < count && totalCount < maxItems; j++) {
308 if (moduleItems[j].location == location) {
309 // Check visibility
310 if (moduleItems[j].isVisible && !moduleItems[j].isVisible()) {
311 continue;
312 }
313 // Set module name
314 moduleItems[j].moduleName = modules_[i]->getName();
315 items[totalCount++] = moduleItems[j];
316 }
317 }
318 }
319
320 // Sort by priority (bubble sort, small array)
321 for (uint8_t i = 0; i < totalCount; i++) {
322 for (uint8_t j = i + 1; j < totalCount; j++) {
323 if (items[j].priority < items[i].priority) {
324 ModuleMenuItem tmp = items[i];
325 items[i] = items[j];
326 items[j] = tmp;
327 }
328 }
329 }
330
331 return totalCount;
332}
333
338 for (uint8_t i = 0; i < count_; i++) {
339 if (modules_[i]->getState() == ServiceState::STARTED) {
340 modules_[i]->onUnlock();
341 }
342 }
343}
344
349 for (uint8_t i = 0; i < count_; i++) {
350 if (modules_[i]->getState() == ServiceState::STARTED) {
351 modules_[i]->onLock();
352 }
353 }
354}
355
360 for (uint8_t i = 0; i < count_; i++) {
361 if (modules_[i]->getState() == ServiceState::STARTED) {
362 modules_[i]->onUsbConnect();
363 }
364 }
365}
366
371 for (uint8_t i = 0; i < count_; i++) {
372 if (modules_[i]->getState() == ServiceState::STARTED) {
373 modules_[i]->onUsbDisconnect();
374 }
375 }
376}
377
382void ModuleRegistry::dispatchTick(uint32_t nowMs) {
383 for (uint8_t i = 0; i < count_; i++) {
384 if (modules_[i]->getState() == ServiceState::STARTED) {
385 modules_[i]->onTick(nowMs);
386 }
387 }
388}
389
397 if (!items || maxItems == 0) return 0;
398
399 uint8_t totalCount = 0;
400
401 // Collect items from all modules
402 for (uint8_t i = 0; i < count_; i++) {
403 if (modules_[i]->getState() != ServiceState::STARTED) continue;
404
405 LockScreenContextItem moduleItems[4];
406 uint8_t count = modules_[i]->getLockScreenContextItems(moduleItems, 4);
407
408 for (uint8_t j = 0; j < count && totalCount < maxItems; j++) {
409 // Set module name
410 moduleItems[j].moduleName = modules_[i]->getName();
411 items[totalCount++] = moduleItems[j];
412 }
413 }
414
415 // Sort by priority (bubble sort, small array)
416 for (uint8_t i = 0; i < totalCount; i++) {
417 for (uint8_t j = i + 1; j < totalCount; j++) {
418 if (items[j].priority < items[i].priority) {
419 LockScreenContextItem tmp = items[i];
420 items[i] = items[j];
421 items[j] = tmp;
422 }
423 }
424 }
425
426 return totalCount;
427}
428
432
433static constexpr const char* MODULES_NVS_NAMESPACE = "modules";
434static constexpr const char* MODULES_NVS_KEY = "list";
435static constexpr const char* MODULES_NVS_KEY_DISABLED = "disabled";
436static constexpr size_t MAX_MODULE_LIST_SIZE = 256;
437
441void ModuleRegistry::cleanupOrphanedModuleData() {
442 NvsScope handle(MODULES_NVS_NAMESPACE, NVS_READONLY);
443 if (!handle) {
444 LOG_I(TAG, "No previous module list found (first boot)");
445 return;
446 }
447
448 char savedList[MAX_MODULE_LIST_SIZE] = {0};
449 size_t len = sizeof(savedList);
450 esp_err_t err = nvs_get_str(handle, MODULES_NVS_KEY, savedList, &len);
451 handle.close();
452
453 if (err != ESP_OK || len == 0) {
454 LOG_I(TAG, "No saved module list");
455 return;
456 }
457
458 LOG_I(TAG, "Checking for orphaned module data...");
459
460 // Parse comma-separated list and check each module
461 char* saveptr = nullptr;
462 char* token = strtok_r(savedList, ",", &saveptr);
463
464 while (token) {
465 // Skip empty tokens
466 if (strlen(token) == 0) {
467 token = strtok_r(nullptr, ",", &saveptr);
468 continue;
469 }
470
471 // Check if this module is currently registered
472 bool found = false;
473 for (uint8_t i = 0; i < count_; i++) {
474 if (strcmp(modules_[i]->getName(), token) == 0) {
475 found = true;
476 break;
477 }
478 }
479
480 if (!found) {
481 // Module no longer exists - erase its NVS namespace
482 char nsName[20];
483 snprintf(nsName, sizeof(nsName), "%s%s", NVS_PREFIX, token);
484
485 LOG_W(TAG, "Module '%s' removed - erasing NVS namespace '%s'", token, nsName);
486
487 NvsScope modHandle(nsName, NVS_READWRITE);
488 if (modHandle) {
489 nvs_erase_all(modHandle);
490 modHandle.commit();
491 LOG_I(TAG, "Erased NVS data for removed module '%s'", token);
492 }
493 }
494
495 token = strtok_r(nullptr, ",", &saveptr);
496 }
497}
498
502void ModuleRegistry::saveModuleList() {
503 if (count_ == 0) {
504 LOG_I(TAG, "No modules to save");
505 return;
506 }
507
508 // Build comma-separated list of module names
509 char moduleList[MAX_MODULE_LIST_SIZE] = {0};
510 size_t offset = 0;
511
512 for (uint8_t i = 0; i < count_; i++) {
513 const char* name = modules_[i]->getName();
514 size_t nameLen = strlen(name);
515
516 // Check if it fits
517 if (offset + nameLen + 2 > sizeof(moduleList)) {
518 LOG_W(TAG, "Module list too long, truncating");
519 break;
520 }
521
522 if (offset > 0) {
523 moduleList[offset++] = ',';
524 }
525 memcpy(moduleList + offset, name, nameLen);
526 offset += nameLen;
527 }
528 moduleList[offset] = '\0';
529
530 NvsScope handle(MODULES_NVS_NAMESPACE, NVS_READWRITE);
531 if (handle) {
532 nvs_set_str(handle, MODULES_NVS_KEY, moduleList);
533 handle.commit();
534 LOG_I(TAG, "Saved module list: %s", moduleList);
535 } else {
536 LOG_E(TAG, "Failed to save module list");
537 }
538}
539
543
547void ModuleRegistry::loadDisabledList() {
548 bool loaded = false;
549 {
550 NvsScope handle(MODULES_NVS_NAMESPACE, NVS_READONLY);
551 if (handle) {
552 size_t len = sizeof(disabledModules_);
553 if (nvs_get_str(handle, MODULES_NVS_KEY_DISABLED, disabledModules_, &len) == ESP_OK) {
554 LOG_I(TAG, "Loaded disabled modules: %s", disabledModules_);
555 loaded = true;
556 }
557 }
558 }
559 if (loaded) return;
560
561 // No NVS entry: apply factory defaults from module_defaults.h.
562 disabledModules_[0] = '\0';
563 size_t offset = 0;
564 for (uint8_t i = 0; i < count_; i++) {
565 const ModuleDefault* def = findModuleDefault(modules_[i]->getName());
566 bool defaultEnabled = def ? def->enabled : true; // unlisted -> enabled
567 if (defaultEnabled) continue;
568 const char* name = modules_[i]->getName();
569 size_t nameLen = strlen(name);
570 size_t needed = (offset > 0 ? 1 : 0) + nameLen + 1;
571 if (offset + needed > sizeof(disabledModules_)) break;
572 if (offset > 0) disabledModules_[offset++] = ',';
573 memcpy(disabledModules_ + offset, name, nameLen);
574 offset += nameLen;
575 disabledModules_[offset] = '\0';
576 }
577 if (disabledModules_[0] != '\0') {
578 LOG_I(TAG, "Applied factory-default disabled list: %s", disabledModules_);
579 saveDisabledList();
580 }
581}
582
586void ModuleRegistry::saveDisabledList() {
587 NvsScope handle(MODULES_NVS_NAMESPACE, NVS_READWRITE);
588 if (handle) {
589 nvs_set_str(handle, MODULES_NVS_KEY_DISABLED, disabledModules_);
590 handle.commit();
591 LOG_I(TAG, "Saved disabled modules: %s", disabledModules_);
592 } else {
593 LOG_E(TAG, "Failed to save disabled modules list");
594 }
595}
596
603 if (!name || name[0] == '\0') return true;
604 if (disabledModules_[0] == '\0') return true; // No disabled modules
605
606 // Search for name in comma-separated list
607 size_t nameLen = strlen(name);
608 const char* ptr = disabledModules_;
609
610 while (*ptr) {
611 // Skip leading commas
612 while (*ptr == ',') ptr++;
613 if (*ptr == '\0') break;
614
615 // Find end of current token
616 const char* end = ptr;
617 while (*end && *end != ',') end++;
618 size_t tokenLen = end - ptr;
619
620 // Compare
621 if (tokenLen == nameLen && strncmp(ptr, name, nameLen) == 0) {
622 return false; // Found in disabled list
623 }
624
625 ptr = end;
626 }
627
628 return true; // Not in disabled list = enabled
629}
630
636const char* ModuleRegistry::getModuleStatusLabel(uint8_t index) const {
637 if (index >= count_) return "[--]";
638 if (hasModuleSlotError(index)) return "[FAIL]";
639 if (!isModuleEnabled(index)) return "[OFF]";
640 return modules_[index]->getState() == ServiceState::STARTED ? "[ON]" : "[--]";
641}
642
648bool ModuleRegistry::isModuleEnabled(uint8_t index) const {
649 if (index >= count_) return false;
650 return isModuleEnabledByName(modules_[index]->getName());
651}
652
666static void removeNameFromList(const char* list, const char* name,
667 char* dest, size_t capacity) {
668 if (capacity == 0) return;
669
670 auto tmp = std::make_unique<char[]>(capacity);
671 tmp[0] = '\0';
672 size_t newOffset = 0;
673 const size_t nameLen = strlen(name);
674 const size_t maxOffset = capacity;
675
676 const char* ptr = list;
677 while (*ptr) {
678 while (*ptr == ',') ptr++;
679 if (*ptr == '\0') break;
680
681 const char* end = ptr;
682 while (*end && *end != ',') end++;
683 size_t tokenLen = static_cast<size_t>(end - ptr);
684
685 const bool matches = (tokenLen == nameLen &&
686 strncmp(ptr, name, nameLen) == 0);
687 if (!matches) {
688 if (newOffset > 0 && newOffset + 1 < maxOffset) {
689 tmp[newOffset++] = ',';
690 }
691 if (newOffset + tokenLen < maxOffset) {
692 memcpy(tmp.get() + newOffset, ptr, tokenLen);
693 newOffset += tokenLen;
694 }
695 }
696
697 ptr = end;
698 }
699 tmp[newOffset] = '\0';
700 memcpy(dest, tmp.get(), newOffset + 1);
701 dest[capacity - 1] = '\0';
702}
703
715static bool addNameToList(char* list, const char* name, size_t capacity) {
716 const size_t currentLen = strlen(list);
717 const size_t nameLen = strlen(name);
718
719 if (currentLen + nameLen + 2 >= capacity) {
720 return false;
721 }
722
723 size_t writePos = currentLen;
724 if (currentLen > 0) {
725 list[writePos++] = ',';
726 }
727 memcpy(list + writePos, name, nameLen + 1);
728 return true;
729}
730
736void ModuleRegistry::setModuleEnabled(uint8_t index, bool enabled) {
737 if (index >= count_) return;
738
739 const char* name = modules_[index]->getName();
740 const bool currentlyEnabled = isModuleEnabledByName(name);
741
742 if (enabled == currentlyEnabled) return; // No change needed
743
744 if (enabled) {
745 removeNameFromList(disabledModules_, name,
746 disabledModules_, sizeof(disabledModules_));
747 } else {
748 if (!addNameToList(disabledModules_, name, sizeof(disabledModules_))) {
749 LOG_W(TAG, "Disabled list full, cannot add '%s'", name);
750 return;
751 }
752 }
753
754 saveDisabledList();
755}
756
763 if (index >= count_) return false;
764
765 bool wasEnabled = isModuleEnabled(index);
766 setModuleEnabled(index, !wasEnabled);
767 return !wasEnabled; // Return new state
768}
769
775bool ModuleRegistry::hasModuleSlotError(uint8_t index) const {
776 if (index >= count_) return false;
777 return moduleErrors_[index].hasError;
778}
779
785const char* ModuleRegistry::getModuleSlotError(uint8_t index) const {
786 if (index >= count_) return nullptr;
787 return moduleErrors_[index].hasError ? moduleErrors_[index].message : nullptr;
788}
789
795void ModuleRegistry::setModuleError(uint8_t index, const char* message) {
796 if (index >= MAX_MODULES) return;
797 moduleErrors_[index].hasError = true;
798 if (message) {
799 strncpy(moduleErrors_[index].message, message, sizeof(moduleErrors_[index].message) - 1);
800 moduleErrors_[index].message[sizeof(moduleErrors_[index].message) - 1] = '\0';
801 } else {
802 moduleErrors_[index].message[0] = '\0';
803 }
804}
805
810void ModuleRegistry::clearModuleError(uint8_t index) {
811 if (index >= MAX_MODULES) return;
812 moduleErrors_[index].hasError = false;
813 moduleErrors_[index].message[0] = '\0';
814}
815
821void ModuleRegistry::reportModuleError(const char* name, const char* message) {
822 if (!name) return;
823
824 // Find module by name
825 for (uint8_t i = 0; i < count_; i++) {
826 if (strcmp(modules_[i]->getName(), name) == 0) {
827 // Stop the module if it's running
828 if (modules_[i]->getState() == ServiceState::STARTED) {
829 modules_[i]->stop();
830 LOG_W(TAG, "Module '%s' stopped due to error", name);
831 }
832 setModuleError(i, message);
833 LOG_E(TAG, "Module '%s' error: %s", name, message ? message : "(null)");
834
835 // Publish error event for UI notification
836 Event evt;
838 evt.data.value = i;
840 return;
841 }
842 }
843 LOG_W(TAG, "reportModuleError: module '%s' not found", name);
844}
845
851 if (!name) return;
852
853 for (uint8_t i = 0; i < count_; i++) {
854 if (strcmp(modules_[i]->getName(), name) == 0) {
855 clearModuleError(i);
856 return;
857 }
858 }
859}
860
866bool ModuleRegistry::retryModule(uint8_t index) {
867 if (index >= count_) return false;
868
869 IModule* module = modules_[index];
870 if (!module) return false;
871
872 const char* name = module->getName();
873 LOG_I(TAG, "Retrying module '%s'...", name);
874
875 // Clear the error first
876 clearModuleError(index);
877
878 // Try to re-initialize if needed
879 ServiceState state = module->getState();
880 if (state == ServiceState::UNINITIALIZED) {
881 if (!module->init()) {
882 reportModuleError(name, "Init failed on retry");
883 return false;
884 }
885 }
886
887 // Try to start
888 if (!module->start()) {
889 reportModuleError(name, "Start failed on retry");
890 return false;
891 }
892
893 LOG_I(TAG, "Module '%s' retry successful", name);
894 return true;
895}
896
900
908void ModuleRegistry::buildSlotErrorMessage(char* buffer, size_t bufSize,
909 const char* errorType, const char* mapName) {
910 snprintf(buffer, bufSize, "%s for %s", errorType, mapName);
911}
912
918bool ModuleRegistry::validateSlotMap(const char* moduleName) {
919 const auto& slotMap = TropicSlotMap::instance();
920 if (!slotMap.isValid()) {
921 const char* errMsg = slotMap.errorMessage();
922 reportModuleError(moduleName, errMsg ? errMsg : "slot map invalid");
923 return false;
924 }
925 return true;
926}
927
937bool ModuleRegistry::validateEccRange(const char* mapName, const char* moduleName,
938 uint16_t minSlots, IModule::SlotRange& range,
939 uint8_t& moduleId) {
940 const auto& slotMap = TropicSlotMap::instance();
941 TropicSlotMap::SlotRange ecc = {};
942
943 if (!slotMap.getRangeByName(mapName, TropicSlotMap::SlotType::ECC, &ecc)) {
944 char msg[96];
945 buildSlotErrorMessage(msg, sizeof(msg), "missing ECC slot map", mapName);
946 reportModuleError(moduleName, msg);
947 return false;
948 }
949
950 uint16_t count = static_cast<uint16_t>(ecc.end - ecc.start + 1);
951 if (count < minSlots) {
952 char msg[96];
953 buildSlotErrorMessage(msg, sizeof(msg), "not enough ECC slots", mapName);
954 reportModuleError(moduleName, msg);
955 return false;
956 }
957
958 range.hasEcc = true;
959 range.eccStart = static_cast<uint8_t>(ecc.start);
960 range.eccEnd = static_cast<uint8_t>(ecc.end);
961 moduleId = ecc.moduleId;
962 return true;
963}
964
974bool ModuleRegistry::validateRmemRange(const char* mapName, const char* moduleName,
975 uint16_t minSlots, IModule::SlotRange& range,
976 uint8_t& moduleId) {
977 const auto& slotMap = TropicSlotMap::instance();
978 TropicSlotMap::SlotRange rmem = {};
979
980 if (!slotMap.getRangeByName(mapName, TropicSlotMap::SlotType::RMEM, &rmem)) {
981 char msg[96];
982 buildSlotErrorMessage(msg, sizeof(msg), "missing RMEM slot map", mapName);
983 reportModuleError(moduleName, msg);
984 return false;
985 }
986
987 uint16_t count = static_cast<uint16_t>(rmem.end - rmem.start + 1);
988 if (count < minSlots) {
989 char msg[96];
990 buildSlotErrorMessage(msg, sizeof(msg), "not enough RMEM slots", mapName);
991 reportModuleError(moduleName, msg);
992 return false;
993 }
994
995 // Check for module ID mismatch (ECC and RMEM must belong to same module)
996 if (moduleId != 0 && moduleId != rmem.moduleId) {
997 char msg[96];
998 buildSlotErrorMessage(msg, sizeof(msg), "module id mismatch", mapName);
999 reportModuleError(moduleName, msg);
1000 return false;
1001 }
1002
1003 range.hasRmem = true;
1004 range.rmemStart = rmem.start;
1005 range.rmemEnd = rmem.end;
1006 moduleId = rmem.moduleId;
1007 return true;
1008}
1009
1013
1020bool ModuleRegistry::applySlotRequest(IModule* module, uint8_t index) {
1021 if (!module) return false;
1022
1023 const char* moduleName = module->getName();
1024
1025 // Step 1: Validate slot map is initialized and valid
1026 if (!validateSlotMap(moduleName)) {
1027 return false;
1028 }
1029
1030 // Step 2: Get slot request from module
1031 IModule::SlotRequest req = module->getSlotRequest();
1032 const char* mapName = (req.mapName && req.mapName[0] != '\0') ? req.mapName : moduleName;
1033
1034 // No slots required - nothing to validate
1035 if (!mapName || (req.minEccSlots == 0 && req.minRmemSlots == 0)) {
1036 return true;
1037 }
1038
1039 IModule::SlotRange range = {};
1040 uint8_t moduleId = 0;
1041
1042 // Step 3: Validate ECC slot range if required
1043 if (req.minEccSlots > 0) {
1044 if (!validateEccRange(mapName, moduleName, req.minEccSlots, range, moduleId)) {
1045 return false;
1046 }
1047 }
1048
1049 // Step 4: Validate RMEM slot range if required
1050 if (req.minRmemSlots > 0) {
1051 if (!validateRmemRange(mapName, moduleName, req.minRmemSlots, range, moduleId)) {
1052 return false;
1053 }
1054 }
1055
1056 // Step 5: Apply validated slot range to module
1057 range.moduleId = moduleId;
1058 module->setSlotRange(range);
1059 return true;
1060}
1061
1062} // namespace cdc::core
static const char * TAG
#define MODULE_DEFAULT_ENTRY(n, en)
char name[cdc::hal::ISecureElement::RMEM_NAME_LEN]
uint8_t moduleId
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
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
static EventBus & instance()
Returns singleton event-bus instance.
Definition EventBus.cpp:19
bool publish(const Event &event, bool fromISR=false)
Publishes an event to the queue.
Definition EventBus.cpp:88
Module interface that extends IService with module-specific features.
Definition IModule.h:55
virtual const char * getVersion() const =0
Returns the module version string.
virtual ServiceState getState() const =0
virtual const char * getName() const =0
virtual bool start()=0
virtual bool init()=0
void runAllInitializers()
Executes all registered initializers and post-registration housekeeping.
bool retryModule(uint8_t index)
Attempts to recover a failed module by re-initializing and restarting it.
void reportModuleError(const char *name, const char *message)
Records and publishes an operational module error by module name.
bool registerModule(IModule *module)
Registers a module instance in the runtime registry.
void dispatchUnlock()
Dispatches unlock lifecycle event to started modules.
void dispatchUsbConnect()
Dispatches USB-connect lifecycle event to started modules.
const char * getModuleSlotError(uint8_t index) const
Returns stored slot-error message for module index.
void dispatchUsbDisconnect()
Dispatches USB-disconnect lifecycle event to started modules.
const char * getModuleStatusLabel(uint8_t index) const
Returns a short status marker combining enabled flag and run state.
bool startAll()
Starts all enabled modules.
static constexpr const char * NVS_PREFIX
bool hasModuleSlotError(uint8_t index) const
Reports whether a module currently has a slot-validation error.
bool isModuleEnabledByName(const char *name) const
Checks whether a module name is currently enabled.
IModule * getModuleAt(uint8_t index)
Returns module pointer at registry index.
void stopAll()
Stops all currently started modules.
static ModuleRegistry & instance()
Returns the singleton module registry instance.
bool initAll()
Calls init() on all registered modules.
static constexpr uint8_t MAX_MODULES
uint8_t getLockScreenContextItems(LockScreenContextItem *items, uint8_t maxItems)
Collects lock-screen context actions from started modules.
bool isModuleEnabled(uint8_t index) const
Checks whether module at index is enabled.
void registerInitializer(ModuleInitFunc initFunc)
Registers a deferred module initializer callback.
bool toggleModuleEnabled(uint8_t index)
Toggles enabled state for a module.
bool startModule(uint8_t index)
Starts a single module by index.
static constexpr uint8_t MAX_INITIALIZERS
uint8_t getMenuItems(MenuLocation location, ModuleMenuItem *items, uint8_t maxItems)
Collects menu items from started modules for a given location.
void setModuleEnabled(uint8_t index, bool enabled)
Enables or disables a module by updating persisted disabled list.
void unregisterModule(const char *name)
Unregisters a module by name.
void clearModuleErrorByName(const char *name)
Clears stored module error by module name.
ModuleStartFailure classifyStartFailure(uint8_t index) const
Classifies why a preceding startModule() call failed.
void dispatchTick(uint32_t nowMs)
Dispatches periodic tick callback to started modules.
IModule * getModule(const char *name)
Looks up a module by name.
void dispatchLock()
Dispatches lock lifecycle event to started modules.
RAII wrapper for an NVS handle.
Definition Raii.h:106
void close() noexcept
Manually close before destruction. Idempotent.
Definition Raii.h:159
esp_err_t commit() noexcept
Commit pending writes. Caller checks the return value if needed.
Definition Raii.h:153
static TropicSlotMap & instance()
Returns singleton Tropic slot-map instance.
static TropicStorage & instance()
Returns singleton instance of TROPIC metadata cache manager.
ServiceState getState() const override
static UsbManager & instance()
Returns singleton USB manager instance.
#define MODULE_DEFAULT_MAP(X)
static constexpr const char * MODULES_NVS_KEY_DISABLED
MenuLocation
Menu location for module registration.
Definition IModule.h:17
static constexpr size_t MAX_MODULE_LIST_SIZE
static void removeNameFromList(const char *list, const char *name, char *dest, size_t capacity)
Removes a name from a comma-separated list, in place.
void(*)() ModuleInitFunc
ModuleStartFailure
Classified cause of a failed startModule() call.
@ UsbBudgetFull
HID interface budget is exhausted.
@ Generic
Start failed for an unspecified reason.
@ SlotError
Module reported a slot-map error.
static constexpr const char * MODULES_NVS_NAMESPACE
NVS garbage collection for removed modules.
static constexpr const char * MODULES_NVS_KEY
static bool addNameToList(char *list, const char *name, size_t capacity)
Appends a name to a comma-separated list, in place.
uint8_t value
Definition EventBus.h:58
union cdc::core::Event::@234350273243204124075032151001065005273232113040 data
EventType type
Definition EventBus.h:54
Lock screen context menu item registered by a module.
Definition IModule.h:42
Menu item registered by a module.
Definition IModule.h:29
const char * moduleName
Definition IModule.h:34