CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
SerialCmd.cpp
Go to the documentation of this file.
1
5
11#include "cdc_core/Cp437.h"
13#include "cdc_core/UsbManager.h"
14#include "cdc_core/PinManager.h"
18#include "cdc_core/CpuStats.h"
22#include "cdc_os_ui/AppUi.h"
25#include "cdc_log.h"
28#include "cdc_ui/ViewStack.h"
30#include "esp_timer.h"
31#include "esp_attr.h"
32#include "nvs_flash.h"
33#include "freertos/FreeRTOS.h"
34#include "freertos/task.h"
35#include "esp_memory_utils.h"
36#include <cstring>
37#include <cctype>
38#include <cstdlib>
39#include <sys/time.h>
40#include <time.h>
41
42static const char* TAG = "SERIAL";
43
44namespace cdc::serial {
45
49
50static constexpr size_t HISTORY_MAX = 10;
51static constexpr size_t HEX_DUMP_WIDTH = 16;
52static constexpr size_t NVS_KEY_MAX_LEN = 15;
53static constexpr size_t NVS_NAMESPACE_MAX_LEN = 15;
54static constexpr int YEAR_MIN = 2020;
55static constexpr int YEAR_MAX = 2100;
56static constexpr uint32_t WIPE_PROGRESS_INTERVAL = 64;
57
61
63static size_t s_cmdBufferPos = 0;
64static bool s_initialized = false;
65
70static size_t s_historyCount = 0;
71static size_t s_historyHead = 0;
72static size_t s_historyPos = 0;
73
77enum class EscState : uint8_t { NONE, ESC, BRACKET };
79
85
89static bool s_authenticated = false;
90static uint64_t s_authTimestamp = 0;
91
95
96#if FEATURE_SECURE_SERIAL
101static void resetAuthTimer() {
102 if (s_authenticated) {
103 s_authTimestamp = esp_timer_get_time();
104 }
105}
106#endif
107
113static void historyAdd(const char* cmd) {
114 if (!cmd || !*cmd) return;
115
118
122 }
123}
124
130static const char* historyGet(size_t idx) {
131 if (idx >= s_historyCount) return nullptr;
132 size_t pos = (s_historyHead + HISTORY_MAX - 1 - idx) % HISTORY_MAX;
133 return s_historyBuffer[pos];
134}
135
142static void redrawLine(const char* newContent, size_t& bufferPos) {
143 while (bufferPos > 0) {
144 Console::print("\b \b");
145 bufferPos--;
146 }
147
148 if (newContent) {
149 strncpy(s_cmdBuffer, newContent, SerialCmd::CMD_BUFFER_SIZE - 1);
151 size_t len = strlen(s_cmdBuffer);
152 bufferPos = len;
154 }
155}
156
160
165 bool valid;
166 long value;
167};
168
176static SlotParseResult parseSlotArg(const char* args, uint16_t maxSlot, const char* slotTypeName) {
177 SlotParseResult result = {false, 0};
178
179 if (!args || !*args) {
180 Console::printf("Usage: Provide a %s number\r\n", slotTypeName);
181 return result;
182 }
183
184 char* endptr = nullptr;
185 long slotVal = strtol(args, &endptr, 10);
186
187 if (endptr == args || *endptr != '\0' || slotVal < 0) {
188 Console::printf("ERROR: Invalid %s number\r\n", slotTypeName);
189 return result;
190 }
191
192 if (slotVal >= maxSlot) {
193 Console::printf("ERROR: Invalid %s (0-%d)\r\n", slotTypeName, maxSlot - 1);
194 return result;
195 }
196
197 result.valid = true;
198 result.value = slotVal;
199 return result;
200}
201
205
212 if (!se) {
213 Console::printf("ERROR: Secure Element not available\r\n");
214 }
215 return se;
216}
217
221
229static void printHexDump(const uint8_t* data, size_t len, size_t maxBytes) {
230 for (size_t i = 0; i < len && i < maxBytes; i += HEX_DUMP_WIDTH) {
231 Console::printf(" %04X: ", (unsigned)i);
232 for (size_t j = 0; j < HEX_DUMP_WIDTH && (i + j) < len; j++) {
233 Console::printf("%02X ", data[i + j]);
234 }
235 Console::printf("\r\n");
236 }
237 if (len > maxBytes) {
238 Console::printf(" ... (%d more bytes)\r\n", (int)(len - maxBytes));
239 }
240}
241
247static const char* getNvsTypeName(nvs_type_t type) {
248 switch (type) {
249 case NVS_TYPE_U8: return "u8";
250 case NVS_TYPE_I8: return "i8";
251 case NVS_TYPE_U16: return "u16";
252 case NVS_TYPE_I16: return "i16";
253 case NVS_TYPE_U32: return "u32";
254 case NVS_TYPE_I32: return "i32";
255 case NVS_TYPE_U64: return "u64";
256 case NVS_TYPE_I64: return "i64";
257 case NVS_TYPE_STR: return "str";
258 case NVS_TYPE_BLOB: return "blob";
259 default: return "?";
260 }
261}
262
269static nvs_type_t findNvsKeyType(const char* ns, const char* key) {
270 nvs_iterator_t it = nullptr;
271 esp_err_t err = nvs_entry_find("nvs", ns, NVS_TYPE_ANY, &it);
272 nvs_type_t keyType = NVS_TYPE_ANY;
273
274 while (it != nullptr) {
275 nvs_entry_info_t info;
276 nvs_entry_info(it, &info);
277 if (strcmp(info.key, key) == 0) {
278 keyType = info.type;
279 break;
280 }
281 err = nvs_entry_next(&it);
282 if (err != ESP_OK) break;
283 }
284 nvs_release_iterator(it);
285 return keyType;
286}
287
295static void printNvsValue(nvs_handle_t nvs, const char* key, nvs_type_t type) {
296 switch (type) {
297 case NVS_TYPE_U8: {
298 uint8_t val;
299 if (nvs_get_u8(nvs, key, &val) == ESP_OK) {
300 Console::printf("%u (0x%02X)\r\n", val, val);
301 }
302 break;
303 }
304 case NVS_TYPE_I8: {
305 int8_t val;
306 if (nvs_get_i8(nvs, key, &val) == ESP_OK) {
307 Console::printf("%d\r\n", val);
308 }
309 break;
310 }
311 case NVS_TYPE_U16: {
312 uint16_t val;
313 if (nvs_get_u16(nvs, key, &val) == ESP_OK) {
314 Console::printf("%u (0x%04X)\r\n", val, val);
315 }
316 break;
317 }
318 case NVS_TYPE_I16: {
319 int16_t val;
320 if (nvs_get_i16(nvs, key, &val) == ESP_OK) {
321 Console::printf("%d\r\n", val);
322 }
323 break;
324 }
325 case NVS_TYPE_U32: {
326 uint32_t val;
327 if (nvs_get_u32(nvs, key, &val) == ESP_OK) {
328 Console::printf("%lu (0x%08lX)\r\n", (unsigned long)val, (unsigned long)val);
329 }
330 break;
331 }
332 case NVS_TYPE_I32: {
333 int32_t val;
334 if (nvs_get_i32(nvs, key, &val) == ESP_OK) {
335 Console::printf("%ld\r\n", (long)val);
336 }
337 break;
338 }
339 case NVS_TYPE_U64: {
340 uint64_t val;
341 if (nvs_get_u64(nvs, key, &val) == ESP_OK) {
342 Console::printf("%llu\r\n", (unsigned long long)val);
343 }
344 break;
345 }
346 case NVS_TYPE_I64: {
347 int64_t val;
348 if (nvs_get_i64(nvs, key, &val) == ESP_OK) {
349 Console::printf("%lld\r\n", (long long)val);
350 }
351 break;
352 }
353 case NVS_TYPE_STR: {
354 size_t len = 0;
355 if (nvs_get_str(nvs, key, nullptr, &len) == ESP_OK && len > 0) {
356 char* buf = static_cast<char*>(malloc(len));
357 if (!buf) {
358 LOG_E(TAG, "Failed to allocate %d bytes for NVS string", (int)len);
359 Console::printf("(allocation failed)\r\n");
360 break;
361 }
362 if (nvs_get_str(nvs, key, buf, &len) == ESP_OK) {
363 Console::printf("\"%s\"\r\n", buf);
364 }
365 free(buf);
366 }
367 break;
368 }
369 case NVS_TYPE_BLOB: {
370 size_t len = 0;
371 if (nvs_get_blob(nvs, key, nullptr, &len) == ESP_OK && len > 0) {
372 Console::printf("(blob, %d bytes)\r\n", (int)len);
373 uint8_t* buf = static_cast<uint8_t*>(malloc(len));
374 if (!buf) {
375 LOG_E(TAG, "Failed to allocate %d bytes for NVS blob", (int)len);
376 Console::printf(" (allocation failed)\r\n");
377 break;
378 }
379 if (nvs_get_blob(nvs, key, buf, &len) == ESP_OK) {
380 printHexDump(buf, len, len);
381 }
382 free(buf);
383 }
384 break;
385 }
386 default:
387 Console::printf("(unknown type)\r\n");
388 break;
389 }
390}
391
395
402static bool getCurrentTime(struct timeval& tv, struct tm& tm) {
403 gettimeofday(&tv, nullptr);
404 return localtime_r(&tv.tv_sec, &tm) != nullptr;
405}
406
412static bool setSystemTime(struct tm* tm) {
413 struct timeval tv;
414 tv.tv_sec = mktime(tm);
415 tv.tv_usec = 0;
416 return settimeofday(&tv, nullptr) == 0;
417}
418
422
427static void cmdHelp(const char* args) {
428 (void)args;
430}
431
436static void cmdPing(const char* args) {
437 (void)args;
438 Console::printf("PONG\r\n");
439}
440
446static void cmdVersion(const char* args) {
447 (void)args;
448 Console::printf("Firmware: %s\r\n", APP_VERSION);
449 Console::printf("API level: %s\r\n", HOST_API_LEVEL_STR);
450 char last[64];
451 if (cdc::ui::firmwareCheckLastResult(last, sizeof(last))) {
452 Console::printf("Latest: %s\r\n", last);
453 } else {
454 Console::printf("Latest: not checked yet\r\n");
455 }
457}
458
463static void cmdStatus(const char* args) {
464 (void)args;
465 Console::printf("=== System Status ===\r\n");
466 Console::printf("Free heap: %lu bytes\r\n", (unsigned long)esp_get_free_heap_size());
467 Console::printf("Min free heap: %lu bytes\r\n", (unsigned long)esp_get_minimum_free_heap_size());
468 Console::printf("Uptime: %llu ms\r\n", esp_timer_get_time() / 1000ULL);
470}
471
472static void cmdCpu(const char* args) {
473 (void)args;
474 Console::printf("Measuring CPU load (~250 ms)...\r\n");
477 Console::printf("CPU load: %u %%\r\n", (unsigned)load);
479}
480
489static void printHeapRegion(const char* label, uint32_t caps) {
490 multi_heap_info_t info;
491 heap_caps_get_info(&info, caps);
492 if (info.total_free_bytes + info.total_allocated_bytes == 0) return;
493 Console::printf("\r\n-- %s --\r\n", label);
494 Console::printf(" total free : %lu\r\n", (unsigned long)info.total_free_bytes);
495 Console::printf(" total allocated : %lu\r\n", (unsigned long)info.total_allocated_bytes);
496 Console::printf(" largest free : %lu\r\n", (unsigned long)info.largest_free_block);
497 Console::printf(" min ever free : %lu\r\n", (unsigned long)info.minimum_free_bytes);
498 Console::printf(" free blocks : %lu\r\n", (unsigned long)info.free_blocks);
499 Console::printf(" alloc blocks : %lu\r\n", (unsigned long)info.allocated_blocks);
500}
501
502static void cmdMemInfo(const char* args) {
503 (void)args;
504 Console::printf("=== Detailed Memory Info ===\r\n");
505
506 printHeapRegion("Internal (any)", MALLOC_CAP_INTERNAL);
507 printHeapRegion("Internal DRAM (8-bit)", MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
508 printHeapRegion("Internal 32-bit only", MALLOC_CAP_INTERNAL | MALLOC_CAP_32BIT);
509 printHeapRegion("IRAM (executable)", MALLOC_CAP_EXEC);
510 printHeapRegion("DMA-capable", MALLOC_CAP_DMA);
511 printHeapRegion("PSRAM", MALLOC_CAP_SPIRAM);
512 printHeapRegion("RTC slow RAM", MALLOC_CAP_RTCRAM);
513
514 Console::printf("\r\n-- Heap totals (heap_caps_get_total_size) --\r\n");
515 Console::printf(" INTERNAL : %u\r\n", (unsigned)heap_caps_get_total_size(MALLOC_CAP_INTERNAL));
516 Console::printf(" EXEC : %u\r\n", (unsigned)heap_caps_get_total_size(MALLOC_CAP_EXEC));
517 Console::printf(" SPIRAM : %u\r\n", (unsigned)heap_caps_get_total_size(MALLOC_CAP_SPIRAM));
518 Console::printf(" DMA : %u\r\n", (unsigned)heap_caps_get_total_size(MALLOC_CAP_DMA));
519 Console::printf(" INTERNAL|EXEC : free=%u largest=%u total=%u\r\n",
520 (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_EXEC),
521 (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_EXEC),
522 (unsigned)heap_caps_get_total_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_EXEC));
523 Console::printf(" 32BIT : free=%u largest=%u\r\n",
524 (unsigned)heap_caps_get_free_size(MALLOC_CAP_32BIT),
525 (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_32BIT));
526
527#if CONFIG_FREERTOS_USE_TRACE_FACILITY
528 UBaseType_t numTasks = uxTaskGetNumberOfTasks();
529 TaskStatus_t* tasks = (TaskStatus_t*)calloc(numTasks, sizeof(TaskStatus_t));
530 if (tasks) {
531 numTasks = uxTaskGetSystemState(tasks, numTasks, nullptr);
532 Console::printf("\r\n-- Tasks (%u) --\r\n", (unsigned)numTasks);
533 Console::printf(" %-16s Prio StkMinFree Stk@ State Core\r\n", "Name");
534 uint32_t totalStackFree = 0;
535 for (UBaseType_t i = 0; i < numTasks; i++) {
536 const char* st = "?";
537 switch (tasks[i].eCurrentState) {
538 case eRunning: st = "RUN"; break;
539 case eReady: st = "RDY"; break;
540 case eBlocked: st = "BLK"; break;
541 case eSuspended: st = "SUS"; break;
542 case eDeleted: st = "DEL"; break;
543 case eInvalid: st = "INV"; break;
544 }
545 BaseType_t coreId = -1;
546#if INCLUDE_xTaskGetCoreID
547 coreId = xTaskGetCoreID(tasks[i].xHandle);
548#endif
549 const char* stackLoc = "DRAM";
550 if (tasks[i].pxStackBase != nullptr &&
551 esp_ptr_external_ram(tasks[i].pxStackBase)) {
552 stackLoc = "PSRAM";
553 }
554 Console::printf(" %-16s %-4u %-10lu %-5s %-5s %d\r\n",
555 tasks[i].pcTaskName,
556 (unsigned)tasks[i].uxCurrentPriority,
557 (unsigned long)tasks[i].usStackHighWaterMark,
558 stackLoc,
559 st,
560 (int)coreId);
561 totalStackFree += tasks[i].usStackHighWaterMark;
562 }
563 Console::printf(" (Sum stack headroom across all tasks: %lu B)\r\n",
564 (unsigned long)totalStackFree);
565 free(tasks);
566 }
567#else
568 Console::printf("\r\nTask list unavailable (FREERTOS_USE_TRACE_FACILITY=n)\r\n");
569#endif
570
572}
573
574static void cmdMem(const char* args) {
575 (void)args;
576 Console::printf("=== Memory Usage ===\r\n");
577 Console::printf("Heap (total): %lu / %lu bytes free\r\n",
578 (unsigned long)esp_get_free_heap_size(),
579 (unsigned long)heap_caps_get_total_size(MALLOC_CAP_DEFAULT));
580
581 size_t intFree = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
582 size_t intTotal = heap_caps_get_total_size(MALLOC_CAP_INTERNAL);
583 size_t intLargest = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
584 Console::printf("Internal DRAM: %lu / %lu free (largest block %lu)\r\n",
585 (unsigned long)intFree,
586 (unsigned long)intTotal,
587 (unsigned long)intLargest);
588
589 size_t dmaFree = heap_caps_get_free_size(MALLOC_CAP_DMA);
590 size_t dmaLargest = heap_caps_get_largest_free_block(MALLOC_CAP_DMA);
591 Console::printf("DMA-capable: %lu free (largest %lu)\r\n",
592 (unsigned long)dmaFree,
593 (unsigned long)dmaLargest);
594
595 size_t psramFree = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
596 size_t psramTotal = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
597 if (psramTotal > 0) {
598 Console::printf("PSRAM: %lu / %lu bytes free\r\n",
599 (unsigned long)psramFree,
600 (unsigned long)psramTotal);
601 }
603}
604
609static void cmdReboot(const char* args) {
610 (void)args;
611 Console::printf("Rebooting...\r\n");
613 vTaskDelay(pdMS_TO_TICKS(100));
614 esp_restart();
615}
616
621static void cmdBootloader(const char* args) {
622 (void)args;
623 Console::printf("Rebooting into download mode...\r\n");
626}
627
632static void cmdShipMode(const char* args) {
633 (void)args;
634 auto* power = hal::getPowerManagerInstance();
635 if (!power) {
636 Console::printf("ERROR: Power manager not available\r\n");
637 return;
638 }
639 Console::printf("Entering ship mode (battery disconnect)...\r\n");
641 power->enterShipMode();
642 Console::printf("OK\r\n");
643}
644
645static void cmdPaste(const char* args) {
646 if (!args || !*args) {
647 Console::printf("Usage: PASTE <text>\r\n");
648 return;
649 }
651 if (!top || strcmp(top->getName(), "T9InputView") != 0) {
652 Console::printf("ERROR: T9 input view not active\r\n");
653 return;
654 }
655 auto* t9 = static_cast<cdc::ui::T9InputView*>(top);
656 uint16_t added = t9->appendRaw(args);
657 Console::printf("OK: %u chars\r\n", static_cast<unsigned>(added));
658}
659
664static void cmdErrorLog(const char* args) {
665 if (args && strcmp(args, "CLEAR") == 0) {
667 Console::printf("Error log cleared.\r\n");
668 } else {
670 }
671}
672
676
681static void cmdNvsClear(const char* args) {
682 if (!args || strcmp(args, "YES") != 0) {
683 Console::printf("WARNING: This will ERASE ALL NVS data!\r\n");
684 Console::printf(" - All module settings\r\n");
685 Console::printf(" - All stored preferences\r\n");
686 Console::printf(" - WiFi credentials\r\n");
687 Console::printf(" - Timezone settings\r\n");
688 Console::printf("\r\nTo proceed, type: NVS CLEAR YES\r\n");
689 return;
690 }
691
692 Console::printf("Clearing NVS...\r\n");
693 esp_err_t err = core::wipeNvs();
694 if (err != ESP_OK) {
695 Console::printf("ERROR: NVS wipe failed (%s)\r\n", esp_err_to_name(err));
696 return;
697 }
698 Console::printf("OK: NVS cleared. Reboot recommended.\r\n");
699}
700
705static void cmdNvsList(const char* args) {
706 const char* nsFilter = (args && *args) ? args : nullptr;
707
708 nvs_iterator_t it = nullptr;
709 esp_err_t err = nvs_entry_find("nvs", nsFilter, NVS_TYPE_ANY, &it);
710
711 if (err == ESP_ERR_NVS_NOT_FOUND) {
712 if (nsFilter) {
713 Console::printf("Namespace '%s' not found or empty\r\n", nsFilter);
714 } else {
715 Console::printf("NVS is empty\r\n");
716 }
717 return;
718 }
719
720 if (err != ESP_OK) {
721 Console::printf("ERROR: nvs_entry_find failed (%s)\r\n", esp_err_to_name(err));
722 return;
723 }
724
725 Console::printf("=== NVS Contents ===\r\n");
726 if (nsFilter) {
727 Console::printf("Namespace: %s\r\n", nsFilter);
728 }
729
730 char lastNs[NVS_NAMESPACE_MAX_LEN + 1] = {0};
731 int count = 0;
732
733 while (it != nullptr) {
734 nvs_entry_info_t info;
735 nvs_entry_info(it, &info);
736
737 if (!nsFilter && strcmp(lastNs, info.namespace_name) != 0) {
738 strncpy(lastNs, info.namespace_name, sizeof(lastNs) - 1);
739 Console::printf("\r\n[%s]\r\n", info.namespace_name);
740 }
741
742 Console::printf(" %s (%s)\r\n", info.key, getNvsTypeName(info.type));
743 count++;
744
745 err = nvs_entry_next(&it);
746 if (err != ESP_OK) break;
747 }
748
749 nvs_release_iterator(it);
750 Console::printf("\r\nTotal: %d entries\r\n", count);
751}
752
757static void cmdNvsRead(const char* args) {
758 if (!args || !*args) {
759 Console::printf("Usage: NVS READ <namespace> <key>\r\n");
760 return;
761 }
762
763 char ns[NVS_NAMESPACE_MAX_LEN + 1] = {0};
764 char key[NVS_KEY_MAX_LEN + 1] = {0};
765 if (sscanf(args, "%15s %15s", ns, key) != 2) {
766 Console::printf("Usage: NVS READ <namespace> <key>\r\n");
767 return;
768 }
769
770 nvs_handle_t nvs;
771 esp_err_t err = nvs_open(ns, NVS_READONLY, &nvs);
772 if (err != ESP_OK) {
773 Console::printf("ERROR: Cannot open namespace '%s' (%s)\r\n", ns, esp_err_to_name(err));
774 return;
775 }
776
777 nvs_type_t keyType = findNvsKeyType(ns, key);
778 if (keyType == NVS_TYPE_ANY) {
779 Console::printf("ERROR: Key '%s' not found in namespace '%s'\r\n", key, ns);
780 nvs_close(nvs);
781 return;
782 }
783
784 Console::printf("%s.%s = ", ns, key);
785 printNvsValue(nvs, key, keyType);
786 nvs_close(nvs);
787}
788
793static void cmdNvsDel(const char* args) {
794 if (!args || !*args) {
795 Console::printf("Usage: NVS DEL <namespace> [key]\r\n");
796 Console::printf(" Without key: erases entire namespace\r\n");
797 return;
798 }
799
800 char ns[NVS_NAMESPACE_MAX_LEN + 1] = {0};
801 char key[NVS_KEY_MAX_LEN + 1] = {0};
802 int parsed = sscanf(args, "%15s %15s", ns, key);
803
804 if (parsed < 1) {
805 Console::printf("Usage: NVS DEL <namespace> [key]\r\n");
806 return;
807 }
808
809 nvs_handle_t nvs;
810 esp_err_t err = nvs_open(ns, NVS_READWRITE, &nvs);
811 if (err != ESP_OK) {
812 Console::printf("ERROR: Cannot open namespace '%s' (%s)\r\n", ns, esp_err_to_name(err));
813 return;
814 }
815
816 if (parsed == 1 || key[0] == '\0') {
817 err = nvs_erase_all(nvs);
818 if (err == ESP_OK) {
819 nvs_commit(nvs);
820 Console::printf("OK: Namespace '%s' erased\r\n", ns);
821 } else {
822 Console::printf("ERROR: Erase failed (%s)\r\n", esp_err_to_name(err));
823 }
824 } else {
825 err = nvs_erase_key(nvs, key);
826 if (err == ESP_OK) {
827 nvs_commit(nvs);
828 Console::printf("OK: Key '%s.%s' deleted\r\n", ns, key);
829 } else if (err == ESP_ERR_NVS_NOT_FOUND) {
830 Console::printf("ERROR: Key '%s' not found\r\n", key);
831 } else {
832 Console::printf("ERROR: Delete failed (%s)\r\n", esp_err_to_name(err));
833 }
834 }
835
836 nvs_close(nvs);
837}
838
842
847static void cmdGetTime(const char* args) {
848 (void)args;
849 struct timeval tv;
850 struct tm tm;
851 if (getCurrentTime(tv, tm)) {
852 Console::printf("%02d:%02d:%02d\r\n", tm.tm_hour, tm.tm_min, tm.tm_sec);
853 } else {
854 Console::printf("--:--:--\r\n");
855 }
856}
857
862static void cmdGetDate(const char* args) {
863 (void)args;
864 struct timeval tv;
865 struct tm tm;
866 if (getCurrentTime(tv, tm)) {
867 Console::printf("%02d.%02d.%04d\r\n", tm.tm_mday, tm.tm_mon + 1, tm.tm_year + 1900);
868 } else {
869 Console::printf("--.---.----\r\n");
870 }
871}
872
877static void cmdSetTime(const char* args) {
878 if (!args || !*args) {
879 Console::printf("Usage: SET_TIME HH:MM:SS\r\n");
880 return;
881 }
882 int h, m, s;
883 if (sscanf(args, "%d:%d:%d", &h, &m, &s) != 3) {
884 Console::printf("ERROR: Invalid format. Use HH:MM:SS\r\n");
885 return;
886 }
887 if (h < 0 || h > 23 || m < 0 || m > 59 || s < 0 || s > 59) {
888 Console::printf("ERROR: Invalid time values\r\n");
889 return;
890 }
891
892 struct timeval tv;
893 struct tm tm;
894 if (getCurrentTime(tv, tm)) {
895 tm.tm_hour = h;
896 tm.tm_min = m;
897 tm.tm_sec = s;
898 if (setSystemTime(&tm)) {
899 Console::printf("OK: Time set to %02d:%02d:%02d\r\n", h, m, s);
900 if (s_timeCallback) {
902 }
903 } else {
904 Console::printf("ERROR: Failed to set time\r\n");
905 }
906 } else {
907 Console::printf("ERROR: Failed to set time\r\n");
908 }
909}
910
915static void cmdSetDate(const char* args) {
916 if (!args || !*args) {
917 Console::printf("Usage: SET_DATE DD.MM.YYYY | <unix_seconds>\r\n");
918 return;
919 }
920
921 int d, m, y;
922 if (sscanf(args, "%d.%d.%d", &d, &m, &y) == 3) {
923 if (d < 1 || d > 31 || m < 1 || m > 12 || y < YEAR_MIN || y > YEAR_MAX) {
924 Console::printf("ERROR: Invalid date values\r\n");
925 return;
926 }
927 struct timeval tv;
928 struct tm tm;
929 if (getCurrentTime(tv, tm)) {
930 tm.tm_mday = d;
931 tm.tm_mon = m - 1;
932 tm.tm_year = y - 1900;
933 if (setSystemTime(&tm)) {
934 Console::printf("OK: Date set to %02d.%02d.%04d\r\n", d, m, y);
935 if (s_timeCallback) {
937 }
938 return;
939 }
940 }
941 Console::printf("ERROR: Failed to set date\r\n");
942 return;
943 }
944
945 // No dotted date: treat the argument as a Unix timestamp (UTC seconds)
946 // and set the full clock (date and time of day) at once.
947 long long ts;
948 if (sscanf(args, "%lld", &ts) != 1 || ts < 0) {
949 Console::printf("ERROR: Invalid format. Use DD.MM.YYYY or a Unix timestamp\r\n");
950 return;
951 }
952 time_t secs = static_cast<time_t>(ts);
953 struct tm tm;
954 if (!gmtime_r(&secs, &tm)) {
955 Console::printf("ERROR: Failed to set date\r\n");
956 return;
957 }
958 int year = tm.tm_year + 1900;
959 if (year < YEAR_MIN || year > YEAR_MAX) {
960 Console::printf("ERROR: Timestamp out of range (%d-%d)\r\n", YEAR_MIN, YEAR_MAX);
961 return;
962 }
963 struct timeval tv;
964 tv.tv_sec = secs;
965 tv.tv_usec = 0;
966 if (settimeofday(&tv, nullptr) == 0) {
967 Console::printf("OK: Time set to %lld (%04d-%02d-%02d %02d:%02d:%02d UTC)\r\n",
968 ts, year, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
969 if (s_timeCallback) {
971 }
972 } else {
973 Console::printf("ERROR: Failed to set date\r\n");
974 }
975}
976
980
985static void cmdSetName(const char* args) {
986 if (!args) args = "";
987 if (s_textCallback) {
988 s_textCallback("name", args);
989 }
990 Console::printf("OK: Name set to \"%s\"\r\n", args);
991}
992
997static void cmdSetInfo(const char* args) {
998 if (!args) args = "";
999 if (s_textCallback) {
1000 s_textCallback("info", args);
1001 }
1002 Console::printf("OK: Info set to \"%s\"\r\n", args);
1003}
1004
1009static void cmdSetInfo2(const char* args) {
1010 if (!args) args = "";
1011 if (s_textCallback) {
1012 s_textCallback("info2", args);
1013 }
1014 Console::printf("OK: Info2 set to \"%s\"\r\n", args);
1015}
1016
1020
1021#if FEATURE_SECURE_SERIAL
1026static void cmdAuth(const char* args) {
1028
1029 if (pm.isBadgeBlocked()) {
1030 if (pm.isLockoutActive()) {
1031 uint32_t remainingSec = pm.getLockoutRemainingMs() / 1000;
1032 Console::printf("ERROR: PIN locked. Wait %lu seconds.\r\n", (unsigned long)remainingSec);
1033 } else {
1034 Console::printf("ERROR: PIN permanently locked.\r\n");
1035 }
1036 return;
1037 }
1038
1039 if (!args || !*args) {
1040 // No PIN argument: treat as logout.
1043 Console::printf("OK: Logged out\r\n");
1044 } else {
1045 Console::printf("Usage: AUTH <pin>\r\n");
1046 Console::printf("Retries: %d\r\n", pm.getBadgeRetries());
1047 }
1048 return;
1049 }
1050
1051 if (SerialCmd::authenticate(args)) {
1052 Console::printf("OK: Authenticated\r\n");
1053 } else {
1054 // Wrong PIN drops any active session so the next command runs
1055 // unprivileged instead of inheriting the previous session.
1058 }
1059 uint8_t retries = pm.getBadgeRetries();
1060 if (retries == 0) {
1061 if (pm.isLockoutActive()) {
1062 uint32_t remainingSec = pm.getLockoutRemainingMs() / 1000;
1063 Console::printf("ERROR: Wrong PIN. Locked for %lu seconds.\r\n", (unsigned long)remainingSec);
1064 } else {
1065 Console::printf("ERROR: Wrong PIN. Permanently locked.\r\n");
1066 }
1067 } else {
1068 Console::printf("ERROR: Wrong PIN. %d retries remaining.\r\n", retries);
1069 }
1070 }
1071}
1072
1077static void cmdLogout(const char* args) {
1078 (void)args;
1080 Console::printf("OK: Logged out\r\n");
1081}
1082#endif
1083
1087
1092static void cmdPinReset(const char* args) {
1093 (void)args;
1095 Console::printf("OK: Badge PIN retries reset to %d\r\n",
1096 core::PinManager::instance().getBadgeRetries());
1097}
1098
1103static void cmdPinStatus(const char* args) {
1104 (void)args;
1106 Console::printf("Badge PIN: retries=%d blocked=%s set=%s\r\n",
1107 pm.getBadgeRetries(),
1108 pm.isBadgeBlocked() ? "yes" : "no",
1109 pm.isPinSet() ? "yes" : "no");
1110}
1111
1116static void cmdPinChange(const char* args) {
1117 if (!args || !args[0]) {
1118 Console::printf("Usage: PIN CHANGE <currentPin> <newPin>\r\n");
1119 return;
1120 }
1121
1122 const char* space = strchr(args, ' ');
1123 if (!space) {
1124 Console::printf("Usage: PIN CHANGE <currentPin> <newPin>\r\n");
1125 return;
1126 }
1127
1128 char currentPin[core::PinManager::BADGE_PIN_MAX + 1] = {};
1129 char newPin[core::PinManager::BADGE_PIN_MAX + 1] = {};
1130
1131 size_t curLen = static_cast<size_t>(space - args);
1132 if (curLen == 0 || curLen > core::PinManager::BADGE_PIN_MAX) {
1133 Console::printf("ERROR: PIN length must be %u-%u digits\r\n",
1134 static_cast<unsigned>(core::PinManager::BADGE_PIN_MIN),
1135 static_cast<unsigned>(core::PinManager::BADGE_PIN_MAX));
1136 return;
1137 }
1138 memcpy(currentPin, args, curLen);
1139 currentPin[curLen] = '\0';
1140
1141 const char* p = space + 1;
1142 while (*p == ' ') p++;
1143 size_t newLen = strlen(p);
1144 if (newLen == 0 || newLen > core::PinManager::BADGE_PIN_MAX) {
1145 Console::printf("ERROR: PIN length must be %u-%u digits\r\n",
1146 static_cast<unsigned>(core::PinManager::BADGE_PIN_MIN),
1147 static_cast<unsigned>(core::PinManager::BADGE_PIN_MAX));
1148 return;
1149 }
1150 memcpy(newPin, p, newLen);
1151 newPin[newLen] = '\0';
1152
1153 auto isDigits = [](const char* s) {
1154 for (; *s; ++s) if (!isdigit(static_cast<unsigned char>(*s))) return false;
1155 return true;
1156 };
1157 if (!isDigits(currentPin) || !isDigits(newPin)) {
1158 Console::printf("ERROR: PIN must be digits only\r\n");
1159 return;
1160 }
1161
1163 if (pm.isBadgeBlocked()) {
1164 Console::printf("ERROR: PIN entry blocked (lockout active or no retries left)\r\n");
1165 return;
1166 }
1167
1168 if (!pm.changeBadgePin(currentPin, newPin)) {
1169 Console::printf("ERROR: PIN change failed (current PIN wrong or new PIN invalid)\r\n");
1170 return;
1171 }
1172
1173 Console::printf("OK: PIN changed\r\n");
1174}
1175
1184static void cmdPinDuress(const char* args) {
1185 if (!args || !args[0]) {
1186 Console::printf("Usage: PIN DURESS <pin>\r\n");
1187 return;
1188 }
1189
1190 char duressPin[core::PinManager::BADGE_PIN_MAX + 1] = {};
1191 size_t len = strlen(args);
1192 if (len == 0 || len > core::PinManager::BADGE_PIN_MAX) {
1193 Console::printf("ERROR: PIN length must be %u-%u digits\r\n",
1194 static_cast<unsigned>(core::PinManager::BADGE_PIN_MIN),
1195 static_cast<unsigned>(core::PinManager::BADGE_PIN_MAX));
1196 return;
1197 }
1198 memcpy(duressPin, args, len);
1199 duressPin[len] = '\0';
1200
1201 if (!core::PinManager::instance().setDuressPin(duressPin)) {
1202 Console::printf("ERROR: Duress PIN rejected (invalid length, non-digit, or equal to badge PIN)\r\n");
1203 return;
1204 }
1205
1206 Console::printf("OK: Duress PIN armed\r\n");
1207}
1208
1213static void cmdPinDuressClear(const char* args) {
1214 (void)args;
1216 Console::printf("OK: Duress PIN cleared\r\n");
1217}
1218
1222
1227static void cmdTr01Status(const char* args) {
1228 (void)args;
1229 auto* se = getSecureElementWithCheck();
1230 if (!se) return;
1231
1232 Console::printf("TR01 Status:\r\n");
1233 Console::printf(" Session: %s\r\n", se->isSessionActive() ? "active" : "inactive");
1234}
1235
1240static void cmdTr01Info(const char* args) {
1241 (void)args;
1242 auto* se = getSecureElementWithCheck();
1243 if (!se) return;
1244
1245 uint8_t chipId[8];
1246 uint8_t riscvVer[4] = {0};
1247 uint8_t spectVer[4] = {0};
1248
1249 Console::printf("TR01 Info:\r\n");
1250
1251 if (se->getChipId(chipId, sizeof(chipId))) {
1252 Console::printf(" Chip ID: ");
1253 for (int i = 0; i < 8; i++) {
1254 Console::printf("%02X", chipId[i]);
1255 }
1256 Console::printf("\r\n");
1257 } else {
1258 Console::printf(" Chip ID: (read failed)\r\n");
1259 }
1260
1261 if (se->getFwVersion(riscvVer, spectVer)) {
1262 Console::printf(" RISC-V FW: v%u.%u.%u (build %u)\r\n",
1263 riscvVer[3], riscvVer[2], riscvVer[1], riscvVer[0]);
1264 Console::printf(" SPECT FW: v%u.%u.%u (build %u)\r\n",
1265 spectVer[3], spectVer[2], spectVer[1], spectVer[0]);
1266 } else {
1267 Console::printf(" FW Version: (read failed)\r\n");
1268 }
1269}
1270
1275static void cmdTr01Session(const char* args) {
1276 (void)args;
1277 auto* se = getSecureElementWithCheck();
1278 if (!se) return;
1279
1280 if (se->isSessionActive()) {
1281 Console::printf("Session already active, reconnecting...\r\n");
1282 se->sessionEnd();
1283 }
1284
1285 if (se->sessionStart()) {
1286 Console::printf("OK: Session started\r\n");
1287 } else {
1288 Console::printf("ERROR: Session start failed\r\n");
1289 }
1290}
1291
1296static void cmdTr01Slots(const char* args) {
1297 (void)args;
1298 auto* se = getSecureElementWithCheck();
1299 if (!se) return;
1300
1301 Console::printf("ECC Key Slots (0-31):\r\n");
1302 int eccCount = 0;
1303 for (uint8_t i = 0; i < hal::ISecureElement::ECC_SLOT_COUNT; i++) {
1304 if (se->eccSlotUsed(i)) {
1305 Console::printf(" [%02d] Used\r\n", i);
1306 eccCount++;
1307 }
1308 }
1309 if (eccCount == 0) {
1310 Console::printf(" (none)\r\n");
1311 }
1312
1313 Console::printf("\r\nR-Memory Slots:\r\n");
1314 Console::printf(" Slot 0: System PIN/lockout\r\n");
1317 [](const cdc::core::TropicSlotMap::SlotRange& r, void*) {
1318 Console::printf(" %4u-%4u: %s\r\n", r.start, r.end,
1319 r.moduleName ? r.moduleName : "?");
1320 },
1321 nullptr);
1322}
1323
1328static void cmdTr01RmemRead(const char* args) {
1329 auto result = parseSlotArg(args, hal::ISecureElement::RMEM_SLOT_COUNT, "R-Memory slot");
1330 if (!result.valid) {
1331 Console::printf("Usage: TR01 RMEM_READ <slot>\r\n");
1332 return;
1333 }
1334
1335 auto* se = getSecureElementWithCheck();
1336 if (!se) return;
1337
1338 uint16_t slot = static_cast<uint16_t>(result.value);
1339 uint8_t data[256];
1340 uint16_t actualLen = 0;
1341
1342 hal::SeResult seResult = se->rmemRead(slot, data, sizeof(data), &actualLen);
1343 if (seResult != hal::SeResult::OK) {
1344 Console::printf("ERROR: Read failed (slot may be empty)\r\n");
1345 return;
1346 }
1347
1348 Console::printf("R-Memory Slot %d (%d bytes):\r\n", slot, actualLen);
1349 printHexDump(data, actualLen, actualLen);
1350}
1351
1356static void cmdTr01EccDel(const char* args) {
1357 auto result = parseSlotArg(args, hal::ISecureElement::ECC_SLOT_COUNT, "ECC slot");
1358 if (!result.valid) {
1359 Console::printf("Usage: TR01 ECC_DEL <slot>\r\n");
1360 return;
1361 }
1362
1363 auto* se = getSecureElementWithCheck();
1364 if (!se) return;
1365
1366 uint8_t slot = static_cast<uint8_t>(result.value);
1367 hal::SeResult seResult = se->eccDelete(slot);
1368 if (seResult == hal::SeResult::OK) {
1369 Console::printf("OK: ECC slot %d deleted\r\n", slot);
1370 } else {
1371 Console::printf("ERROR: Delete failed\r\n");
1372 }
1373}
1374
1379static void cmdTr01RmemDel(const char* args) {
1380 auto result = parseSlotArg(args, hal::ISecureElement::RMEM_SLOT_COUNT, "R-Memory slot");
1381 if (!result.valid) {
1382 Console::printf("Usage: TR01 RMEM_DEL <slot>\r\n");
1383 return;
1384 }
1385
1386 auto* se = getSecureElementWithCheck();
1387 if (!se) return;
1388
1389 uint16_t slot = static_cast<uint16_t>(result.value);
1390 hal::SeResult seResult = se->rmemErase(slot);
1391 if (seResult == hal::SeResult::OK) {
1392 Console::printf("OK: R-Memory slot %d erased\r\n", slot);
1393 } else {
1394 Console::printf("ERROR: Erase failed\r\n");
1395 }
1396}
1397
1402static void cmdTr01Resync(const char* args) {
1403 (void)args;
1404 auto* se = getSecureElementWithCheck();
1405 if (!se) return;
1406
1407 Console::printf("Resyncing TR01 session...\r\n");
1408
1409 if (se->isSessionActive()) {
1410 se->sessionEnd();
1411 }
1412
1413 if (se->sessionStart()) {
1414 Console::printf("OK: Session restarted, cache invalidated\r\n");
1415 } else {
1416 Console::printf("ERROR: Session restart failed\r\n");
1417 }
1418}
1419
1424static void cmdTr01CacheRebuild(const char* args) {
1425 (void)args;
1426 auto& storage = core::TropicStorage::instance();
1427 Console::printf("Rebuilding TR01 cache...\r\n");
1428
1429 auto logFn = [](uint16_t slot, const char* message, void* ctx) {
1430 (void)ctx;
1431 if (!message) return;
1432 if (strcmp(message, "invalid header") == 0 ||
1433 strcmp(message, "mismatched module") == 0 ||
1434 strcmp(message, "nvs write failed") == 0 ||
1435 strcmp(message, "session start failed") == 0 ||
1436 strcmp(message, "read failed") == 0) {
1437 Console::printf(" slot %u: %s\r\n", slot, message);
1438 } else {
1439 Console::printf(" slot %u: found %s\r\n", slot, message);
1440 }
1441 };
1442
1443 if (storage.rebuildVerbose(logFn, nullptr)) {
1444 Console::printf("OK: Cache rebuilt\r\n");
1445 } else {
1446 Console::printf("ERROR: Cache rebuild failed\r\n");
1447 }
1448}
1449
1454static void cmdTr01Cleanup(const char* args) {
1455 (void)args;
1456 auto& storage = core::TropicStorage::instance();
1457 Console::printf("Cleaning TR01 cache + slots...\r\n");
1458 if (storage.cleanup()) {
1459 Console::printf("OK: Cleanup complete\r\n");
1460 } else {
1461 Console::printf("ERROR: Cleanup failed\r\n");
1462 }
1463}
1464
1469static void cmdTr01Wipe(const char* args) {
1470 auto* se = getSecureElementWithCheck();
1471 if (!se) return;
1472
1473 if (!args || strcmp(args, "CONFIRM") != 0) {
1474 Console::printf("WARNING: This will ERASE ALL data on TROPIC01!\r\n");
1475 Console::printf(" - All ECC keys (slots 0-31)\r\n");
1476 Console::printf(" - All R-Memory data (slots 0-511)\r\n");
1477 Console::printf("\r\nTo proceed, type: TR01 WIPE CONFIRM\r\n");
1478 return;
1479 }
1480
1481 Console::printf("=== TROPIC01 Factory Reset ===\r\n");
1483
1484 if (!se->isSessionActive()) {
1485 if (!se->sessionStart()) {
1486 Console::printf("ERROR: Cannot start session\r\n");
1487 return;
1488 }
1489 }
1490
1491 Console::printf("Erasing ECC keys and R-Memory (this may take a while)...\r\n");
1493 auto result = core::wipeTropic(se, WIPE_PROGRESS_INTERVAL,
1494 [](uint16_t current, uint16_t total) {
1495 Console::printf(" Progress: %d/%d\r\n", current, total);
1497 });
1498
1499 if (!result.sessionReady) {
1500 Console::printf("ERROR: SE session unavailable\r\n");
1501 return;
1502 }
1503
1504 Console::printf("\r\n=== Factory Reset Complete ===\r\n");
1505 Console::printf("Deleted: %d ECC keys, %d R-Memory slots\r\n",
1506 result.eccDeleted, result.rmemDeleted);
1507}
1508
1512
1517 if (s_initialized) return;
1518
1519 Console::init();
1520
1521#if FEATURE_SECURE_SERIAL
1523 getCommandRegistry().setOnCommandExecuted(resetAuthTimer);
1524#if !DEBUG_MODE
1525 // Release profile: suppress INFO/DEBUG/VERBOSE log output until a session
1526 // is authenticated. ERROR/WARN keep flowing so boot failures are still
1527 // visible.
1529#endif
1530#else
1531 log_set_level(CDC_LOG_LEVEL_DEBUG);
1532#endif
1533
1535
1536 s_initialized = true;
1537 LOG_I(TAG, "Serial command processor initialized");
1538
1539 Console::printf("\r\n=== CDC Badge OS Serial Console ===\r\n");
1540#if FEATURE_SECURE_SERIAL
1541 Console::printf("Login with: AUTH <pin>\r\n");
1542#endif
1543 Console::printf("Type 'HELP' for available commands.\r\n");
1545}
1546
1556void SerialCmd::handleHistoryNav(HistoryDirection dir) {
1557 if (dir == HistoryDirection::OLDER) {
1558 if (s_historyPos >= s_historyCount) return;
1559 const char* hist = historyGet(s_historyPos);
1560 if (hist) {
1562 s_historyPos++;
1563 }
1564 return;
1565 }
1566
1567 // HistoryDirection::NEWER
1568 if (s_historyPos == 0) return;
1569 s_historyPos--;
1570 if (s_historyPos == 0) {
1572 return;
1573 }
1574 const char* hist = historyGet(s_historyPos - 1);
1575 if (hist) {
1577 }
1578}
1579
1591bool SerialCmd::handleEscape(int c) {
1592 if (s_escState == EscState::ESC) {
1593 if (c == '[') {
1595 } else {
1597 }
1598 return true;
1599 }
1600
1603 switch (c) {
1604 case 'A':
1605 handleHistoryNav(HistoryDirection::OLDER);
1606 break;
1607 case 'B':
1608 handleHistoryNav(HistoryDirection::NEWER);
1609 break;
1610 default:
1611 break;
1612 }
1613 return true;
1614 }
1615
1616 return false;
1617}
1618
1629void SerialCmd::handleSpecialChar(int c, bool& commandReady) {
1630 commandReady = false;
1631
1632 // Binary-streaming bypass: while a byte interceptor is installed, every
1633 // incoming byte is delivered raw with no echo, no buffering and no line
1634 // processing. Used by `PLUGIN UPLOAD` to slurp the raw payload.
1635 if (auto bi = getCommandRegistry().getByteInterceptor()) {
1636 bi(static_cast<uint8_t>(c));
1637 return;
1638 }
1639
1640 switch (c) {
1641 case 0x1B: // ESC
1643 return;
1644
1645 case '\r':
1646 case '\n':
1647 Console::print("\r\n");
1649 if (s_cmdBufferPos > 0) {
1651 executeCommand(s_cmdBuffer);
1652 }
1653 s_cmdBufferPos = 0;
1654 s_historyPos = 0;
1656 commandReady = true;
1657 return;
1658
1659 case 0x7F: // Backspace (DEL)
1660 case 0x08: // Backspace (BS)
1661 if (s_cmdBufferPos > 0) {
1663 Console::print("\b \b");
1664 }
1665 return;
1666
1667 case 0x03: // Ctrl+C
1668 Console::print("^C\r\n");
1669 s_cmdBufferPos = 0;
1670 s_historyPos = 0;
1672 return;
1673
1674 case 0x15: // Ctrl+U
1675 while (s_cmdBufferPos > 0) {
1676 Console::print("\b \b");
1678 }
1679 return;
1680
1681 default: {
1682 static uint8_t utf8Pending = 0;
1683 static uint32_t utf8Cp = 0;
1684
1685 auto appendByte = [](uint8_t b) {
1686 if (s_cmdBufferPos < CMD_BUFFER_SIZE - 1) {
1687 s_cmdBuffer[s_cmdBufferPos++] = static_cast<char>(b);
1688 Console::putchar(static_cast<char>(b));
1689 }
1690 };
1691
1692 if (utf8Pending) {
1693 if ((c & 0xC0) == 0x80) {
1694 utf8Cp = (utf8Cp << 6) | (c & 0x3F);
1695 if (--utf8Pending == 0) {
1696 uint8_t cp437 = cdc::core::cp437::fromUnicode(utf8Cp);
1697 if (cp437) appendByte(cp437);
1698 }
1699 } else {
1700 utf8Pending = 0;
1701 }
1702 return;
1703 }
1704
1705 if ((c & 0xE0) == 0xC0) {
1706 utf8Cp = c & 0x1F;
1707 utf8Pending = 1;
1708 return;
1709 }
1710 if ((c & 0xF0) == 0xE0) {
1711 utf8Cp = c & 0x0F;
1712 utf8Pending = 2;
1713 return;
1714 }
1715 if ((c & 0xF8) == 0xF0) {
1716 utf8Cp = c & 0x07;
1717 utf8Pending = 3;
1718 return;
1719 }
1720
1721 if (c >= 0x20 && c < 0x7F) {
1722 appendByte(static_cast<uint8_t>(c));
1723 } else if (c >= 0x80 && c <= 0xFF) {
1724 // Raw CP437/Latin-1 byte from terminals that don't speak UTF-8.
1725 appendByte(static_cast<uint8_t>(c));
1726 }
1727 return;
1728 }
1729 }
1730}
1731
1737 bool anyCommandReady = false;
1738 // Drain every byte that is currently buffered, not just one per main-loop
1739 // tick. Without this, binary uploads were rate-limited to one byte per
1740 // ~50 ms UI tick (~20 B/s).
1741 for (int i = 0; i < 4096; ++i) {
1742 int c = Console::getchar();
1743 if (c < 0) break;
1744
1745 if (handleEscape(c)) continue;
1746
1747 bool commandReady = false;
1748 handleSpecialChar(c, commandReady);
1749 if (commandReady) anyCommandReady = true;
1750 }
1751 return anyCommandReady;
1752}
1753
1761
1767 s_textCallback = callback;
1768}
1769
1775 s_timeCallback = callback;
1776}
1777
1783#if FEATURE_SECURE_SERIAL
1784 if (!s_authenticated) return false;
1785
1786 uint64_t now = esp_timer_get_time();
1787 if ((now - s_authTimestamp) > (AUTH_TIMEOUT_MS * 1000ULL)) {
1788 s_authenticated = false;
1789#if !DEBUG_MODE
1790 log_set_level(CDC_LOG_LEVEL_WARN);
1791#endif
1792 LOG_I(TAG, "Session timed out");
1793 return false;
1794 }
1795
1796 return true;
1797#else
1798 return true;
1799#endif
1800}
1801
1807bool SerialCmd::authenticate(const char* pin) {
1809
1810 if (pm.isBadgeBlocked()) {
1811 if (pm.isLockoutActive()) {
1812 uint32_t remainingSec = pm.getLockoutRemainingMs() / 1000;
1813 LOG_W(TAG, "PIN locked, %lu seconds remaining", (unsigned long)remainingSec);
1814 } else {
1815 LOG_W(TAG, "PIN permanently blocked (retries exhausted)");
1816 }
1817 return false;
1818 }
1819
1820 if (!pin || !*pin) {
1821 LOG_W(TAG, "Empty PIN provided");
1822 return false;
1823 }
1824
1825 if (!pm.verifyBadgePin(pin)) {
1826 LOG_W(TAG, "Authentication failed, %d retries remaining", pm.getBadgeRetries());
1827 return false;
1828 }
1829
1830 s_authenticated = true;
1831 s_authTimestamp = esp_timer_get_time();
1832#if !DEBUG_MODE
1833 log_set_level(CDC_LOG_LEVEL_DEBUG);
1834#endif
1835 LOG_I(TAG, "Authenticated via serial");
1836 return true;
1837}
1838
1843#if FEATURE_SECURE_SERIAL
1844 if (s_authenticated) {
1845 s_authTimestamp = esp_timer_get_time();
1846 }
1847#endif
1848}
1849
1854 s_authenticated = false;
1855 s_authTimestamp = 0;
1856#if !DEBUG_MODE
1857 log_set_level(CDC_LOG_LEVEL_WARN);
1858#endif
1859 LOG_I(TAG, "Logged out");
1860}
1861
1866void SerialCmd::executeCommand(char* cmd) {
1867 cmd = trim(cmd);
1868 if (!*cmd) return;
1869
1870 char first_token[24];
1871 size_t i = 0;
1872 while (cmd[i] && !isspace(static_cast<unsigned char>(cmd[i])) &&
1873 i < sizeof(first_token) - 1) {
1874 first_token[i] = cmd[i];
1875 i++;
1876 }
1877 first_token[i] = '\0';
1878 LOG_D(TAG, "Executing: %s", first_token);
1880}
1881
1887char* SerialCmd::trim(char* str) {
1888 if (!str) return str;
1889
1890 while (*str && isspace(static_cast<unsigned char>(*str))) str++;
1891 if (*str == '\0') return str;
1892
1893 char* end = str + strlen(str) - 1;
1894 while (end > str && isspace(static_cast<unsigned char>(*end))) end--;
1895 *(end + 1) = '\0';
1896
1897 return str;
1898}
1899
1907
1908static constexpr uint32_t WIFI_SCAN_POLL_MS = 100;
1910
1911static const char* wifiSecurityName(hal::WifiSecurity sec) {
1912 switch (sec) {
1913 case hal::WifiSecurity::OPEN: return "OPEN";
1914 case hal::WifiSecurity::WEP: return "WEP";
1915 case hal::WifiSecurity::WPA_PSK: return "WPA";
1916 case hal::WifiSecurity::WPA2_PSK: return "WPA2";
1917 case hal::WifiSecurity::WPA3_PSK: return "WPA3";
1918 case hal::WifiSecurity::WPA2_ENTERPRISE: return "WPA2-E";
1919 default: return "?";
1920 }
1921}
1922
1923static const char* wifiStateName(hal::WifiState st) {
1924 switch (st) {
1925 case hal::WifiState::DISCONNECTED: return "DISCONNECTED";
1926 case hal::WifiState::CONNECTING: return "CONNECTING";
1927 case hal::WifiState::CONNECTED: return "CONNECTED";
1928 case hal::WifiState::CONNECTION_FAILED: return "FAILED";
1929 case hal::WifiState::GOT_IP: return "GOT_IP";
1930 default: return "?";
1931 }
1932}
1933
1934static const char* wifiModeName(hal::WifiMode m) {
1935 switch (m) {
1936 case hal::WifiMode::OFF: return "OFF";
1937 case hal::WifiMode::STA: return "STA";
1938 case hal::WifiMode::AP: return "AP";
1939 case hal::WifiMode::STA_AP: return "STA_AP";
1940 default: return "?";
1941 }
1942}
1943
1948static uint8_t wifiDedupAndSort(hal::WifiScanResult* results, uint8_t count) {
1949 if (count <= 1) return count;
1950
1951 uint8_t unique = 0;
1952 for (uint8_t i = 0; i < count; i++) {
1953 bool seen = false;
1954 for (uint8_t j = 0; j < unique; j++) {
1955 if (strcmp(results[i].ssid, results[j].ssid) == 0) {
1956 seen = true;
1957 if (results[i].rssi > results[j].rssi) results[j] = results[i];
1958 break;
1959 }
1960 }
1961 if (!seen && results[i].ssid[0] != '\0') {
1962 if (unique != i) results[unique] = results[i];
1963 unique++;
1964 }
1965 }
1966
1967 for (uint8_t i = 0; i < unique; i++) {
1968 for (uint8_t j = i + 1; j < unique; j++) {
1969 if (results[j].rssi > results[i].rssi) {
1970 hal::WifiScanResult tmp = results[i];
1971 results[i] = results[j];
1972 results[j] = tmp;
1973 }
1974 }
1975 }
1976 return unique;
1977}
1978
1982static void cmdWifiScan(const char* args) {
1983 (void)args;
1984
1985 auto* wifi = hal::getWifiControllerInstance();
1986 if (!wifi) {
1987 Console::printf("ERROR: WiFi not available\r\n");
1988 return;
1989 }
1990
1991 if (!wifi->isEnabled() || wifi->getMode() == hal::WifiMode::AP) {
1992 if (!wifi->enable(hal::WifiMode::STA)) {
1993 Console::printf("ERROR: Failed to enable WiFi\r\n");
1994 return;
1995 }
1996 }
1997
1998 Console::printf("Scanning...\r\n");
1999
2000 if (!wifi->startScan()) {
2001 Console::printf("ERROR: Scan start failed\r\n");
2002 return;
2003 }
2004
2005 uint32_t elapsed = 0;
2006 while (!wifi->isScanComplete() && elapsed < ui::WIFI_SCAN_TIMEOUT_MS) {
2007 vTaskDelay(pdMS_TO_TICKS(WIFI_SCAN_POLL_MS));
2008 elapsed += WIFI_SCAN_POLL_MS;
2009 }
2010
2011 if (!wifi->isScanComplete()) {
2012 Console::printf("ERROR: Scan timeout\r\n");
2013 return;
2014 }
2015
2017 uint8_t count = wifi->getScanResults(results, WIFI_MAX_SCAN_RESULTS);
2018 count = wifiDedupAndSort(results, count);
2019
2020 if (count == 0) {
2021 Console::printf("No networks found\r\n");
2022 } else {
2023 Console::printf("# %-32s %5s %3s %s\r\n", "SSID", "RSSI", "Ch", "Security");
2024 for (uint8_t i = 0; i < count; i++) {
2025 Console::printf("%-2u %-32s %4d %3u %s\r\n",
2026 static_cast<unsigned>(i + 1),
2027 results[i].ssid,
2028 results[i].rssi,
2029 static_cast<unsigned>(results[i].channel),
2030 wifiSecurityName(results[i].security));
2031 }
2032 }
2033
2034 Console::printf("OK\r\n");
2035}
2036
2040static void cmdWifiStatus(const char* args) {
2041 (void)args;
2042
2043 auto* wifi = hal::getWifiControllerInstance();
2044 if (!wifi) {
2045 Console::printf("ERROR: WiFi not available\r\n");
2046 return;
2047 }
2048
2049 Console::printf("Mode: %s\r\n", wifiModeName(wifi->getMode()));
2050 Console::printf("Enabled: %s\r\n", wifi->isEnabled() ? "yes" : "no");
2051 Console::printf("State: %s\r\n", wifiStateName(wifi->getWifiState()));
2052
2053 if (wifi->isConnected()) {
2054 Console::printf("SSID: %s\r\n", wifi->getCurrentSsid());
2055
2056 char ip[16] = {};
2057 if (wifi->getIpAddress(ip, sizeof(ip))) {
2058 Console::printf("IP: %s\r\n", ip);
2059 }
2060 uint8_t mac[6] = {};
2061 if (wifi->getMacAddress(mac)) {
2062 Console::printf("MAC: %02X:%02X:%02X:%02X:%02X:%02X\r\n",
2063 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
2064 }
2065 int8_t rssi = wifi->getRssi();
2066 if (rssi != 0) {
2067 Console::printf("RSSI: %d dBm\r\n", rssi);
2068 }
2069 }
2070
2071 auto& wh = ui::WifiHandlers::instance();
2072 wh.loadConfig();
2073 const auto& cfg = wh.config();
2074 if (cfg.valid) {
2075 Console::printf("\r\nSaved config:\r\n");
2076 Console::printf("SSID: %s\r\n", cfg.ssid);
2077 Console::printf("Security: %s\r\n",
2078 wifiSecurityName(static_cast<hal::WifiSecurity>(cfg.security)));
2079 Console::printf("Timeout: %lu ms\r\n",
2080 static_cast<unsigned long>(wh.getConnectTimeoutMs()));
2081 } else {
2082 Console::printf("\r\nSaved config: (none)\r\n");
2083 }
2084
2085 Console::printf("OK\r\n");
2086}
2087
2093static void cmdWifiOn(const char* args) {
2095
2096 if (args && args[0]) {
2097 if (strcasecmp(args, "ap") == 0) {
2098 mode = hal::WifiMode::AP;
2099 } else if (strcasecmp(args, "sta_ap") == 0) {
2100 mode = hal::WifiMode::STA_AP;
2101 } else if (strcasecmp(args, "sta") != 0) {
2102 Console::printf("Usage: WIFI ON [sta|ap|sta_ap]\r\n");
2103 return;
2104 }
2105 }
2106
2107 auto* wifi = hal::getWifiControllerInstance();
2108 if (!wifi) {
2109 Console::printf("ERROR: WiFi not available\r\n");
2110 return;
2111 }
2112
2113 if (!wifi->enable(mode)) {
2114 Console::printf("ERROR: Failed to enable WiFi\r\n");
2115 return;
2116 }
2117
2118 Console::printf("OK: %s mode enabled\r\n", wifiModeName(mode));
2119
2120 if (mode == hal::WifiMode::AP) return;
2121
2122 auto& wh = ui::WifiHandlers::instance();
2123 wh.loadConfig();
2124 if (!wh.config().valid) return;
2125
2126 Console::printf("Reconnecting to %s...\r\n", wh.config().ssid);
2127 if (!wh.setUserEnabled(true)) {
2128 wifi->disable();
2129 const char* err = wh.getLastError();
2130 Console::printf("ERROR: Reconnect failed (%s)\r\n", err ? err : "?");
2131 return;
2132 }
2133
2134 char ip[16] = {};
2135 wifi->getIpAddress(ip, sizeof(ip));
2136 Console::printf("OK: %s\r\n", ip[0] ? ip : "connected");
2137}
2138
2142static void cmdWifiOff(const char* args) {
2143 (void)args;
2144
2145 auto* wifi = hal::getWifiControllerInstance();
2146 if (!wifi) {
2147 Console::printf("ERROR: WiFi not available\r\n");
2148 return;
2149 }
2150 if (!wifi->isEnabled()) {
2151 Console::printf("OK: already off\r\n");
2152 return;
2153 }
2155 Console::printf("OK: WiFi disabled\r\n");
2156}
2157
2161static void cmdWifiConnect(const char* args) {
2162 if (!args || !args[0]) {
2163 Console::printf("Usage: WIFI CONNECT <ssid> <password>\r\n");
2164 return;
2165 }
2166
2167 const char* space = strchr(args, ' ');
2168 if (!space) {
2169 Console::printf("Usage: WIFI CONNECT <ssid> <password>\r\n");
2170 return;
2171 }
2172
2173 char ssid[33] = {};
2174 char password[65] = {};
2175
2176 size_t ssidLen = static_cast<size_t>(space - args);
2177 if (ssidLen >= sizeof(ssid)) ssidLen = sizeof(ssid) - 1;
2178 memcpy(ssid, args, ssidLen);
2179 ssid[ssidLen] = '\0';
2180
2181 const char* pw = space + 1;
2182 while (*pw == ' ') pw++;
2183 size_t pwLen = strlen(pw);
2184 if (pwLen >= sizeof(password)) pwLen = sizeof(password) - 1;
2185 memcpy(password, pw, pwLen);
2186 password[pwLen] = '\0';
2187
2188 if (ssid[0] == '\0') {
2189 Console::printf("ERROR: SSID required\r\n");
2190 return;
2191 }
2192
2193 auto& wh = ui::WifiHandlers::instance();
2194 wh.saveCredentials(ssid, password);
2195
2196 Console::printf("Connecting to %s (timeout: %lu ms)...\r\n",
2197 ssid, static_cast<unsigned long>(wh.getConnectTimeoutMs()));
2198
2199 if (!wh.setUserEnabled(true)) {
2200 const char* err = wh.getLastError();
2201 Console::printf("ERROR: Connection failed (%s)\r\n", err ? err : "?");
2202 return;
2203 }
2204
2205 auto* wifi = hal::getWifiControllerInstance();
2206 char ip[16] = {};
2207 if (wifi) wifi->getIpAddress(ip, sizeof(ip));
2208 Console::printf("OK: %s\r\n", ip[0] ? ip : "connected");
2209}
2210
2214static void cmdWifiTimeout(const char* args) {
2215 auto& wh = ui::WifiHandlers::instance();
2216 if (!args || !args[0]) {
2217 Console::printf("%lu ms\r\n",
2218 static_cast<unsigned long>(wh.getConnectTimeoutMs()));
2219 return;
2220 }
2221
2222 char* end = nullptr;
2223 long val = strtol(args, &end, 10);
2224 if (end == args || *end != '\0'
2225 || val < static_cast<long>(ui::WIFI_CONNECT_TIMEOUT_MIN_MS)
2226 || val > static_cast<long>(ui::WIFI_CONNECT_TIMEOUT_MAX_MS)) {
2227 Console::printf("Usage: WIFI TIMEOUT [%lu-%lu]\r\n",
2228 static_cast<unsigned long>(ui::WIFI_CONNECT_TIMEOUT_MIN_MS),
2229 static_cast<unsigned long>(ui::WIFI_CONNECT_TIMEOUT_MAX_MS));
2230 return;
2231 }
2232
2233 if (!wh.setConnectTimeoutMs(static_cast<uint32_t>(val))) {
2234 Console::printf("ERROR: Failed to persist timeout\r\n");
2235 return;
2236 }
2237 Console::printf("OK: %ld ms\r\n", val);
2238}
2239
2243static void cmdWifiForget(const char* args) {
2244 (void)args;
2245
2246 auto& wh = ui::WifiHandlers::instance();
2247 wh.disconnect();
2248 wh.clearConfig();
2249
2250 Console::printf("OK: WiFi configuration cleared\r\n");
2251}
2252
2256
2262static int findModuleIndex(const char* name) {
2263 auto& reg = core::ModuleRegistry::instance();
2264 uint8_t count = reg.getModuleCount();
2265 for (uint8_t i = 0; i < count; i++) {
2266 core::IModule* module = reg.getModuleAt(i);
2267 if (module && module->getName() &&
2268 strcasecmp(module->getName(), name) == 0) {
2269 return i;
2270 }
2271 }
2272 return -1;
2273}
2274
2279static void cmdModuleList(const char* args) {
2280 (void)args;
2281 auto& reg = core::ModuleRegistry::instance();
2282 uint8_t count = reg.getModuleCount();
2283
2284 Console::printf("=== Modules (%u) ===\r\n", static_cast<unsigned>(count));
2285 for (uint8_t i = 0; i < count; i++) {
2286 core::IModule* module = reg.getModuleAt(i);
2287 if (!module) continue;
2288
2289 const char* error = reg.getModuleSlotError(i);
2290 Console::printf(" [%2u] %-16s %-8s %-6s %s\r\n",
2291 static_cast<unsigned>(i),
2292 module->getName() ? module->getName() : "?",
2293 reg.isModuleEnabled(i) ? "enabled" : "disabled",
2294 reg.getModuleStatusLabel(i),
2295 error ? error : "");
2296 }
2297}
2298
2303static void cmdModuleEnable(const char* args) {
2304 if (!args || !*args) {
2305 Console::printf("Usage: MODULE ENABLE <name>\r\n");
2306 return;
2307 }
2308
2309 int index = findModuleIndex(args);
2310 if (index < 0) {
2311 Console::printf("ERROR: Module '%s' not found\r\n", args);
2312 return;
2313 }
2314
2315 auto& reg = core::ModuleRegistry::instance();
2316 uint8_t idx = static_cast<uint8_t>(index);
2317
2318 if (reg.isModuleEnabled(idx)) {
2319 Console::printf("OK: Module '%s' already enabled\r\n", args);
2320 return;
2321 }
2322
2323 bool needsReplugBefore = core::UsbManager::instance().needsReplug();
2324
2325 reg.setModuleEnabled(idx, true);
2326 if (!reg.startModule(idx)) {
2327 switch (reg.classifyStartFailure(idx)) {
2329 Console::printf("ERROR: %s\r\n", reg.getModuleSlotError(idx));
2330 break;
2332 Console::printf("ERROR: No free USB slot - disable a USB module (e.g. GPG) first\r\n");
2333 break;
2335 Console::printf("ERROR: Failed to start module '%s'\r\n", args);
2336 break;
2337 }
2338 return;
2339 }
2340
2341 Console::printf("OK: Module '%s' enabled\r\n", args);
2342
2343 if (core::UsbManager::instance().newlyRequiresReplug(needsReplugBefore)) {
2344 Console::printf("NOTE: USB replug required\r\n");
2345 }
2346}
2347
2352static void cmdModuleDisable(const char* args) {
2353 if (!args || !*args) {
2354 Console::printf("Usage: MODULE DISABLE <name>\r\n");
2355 return;
2356 }
2357
2358 int index = findModuleIndex(args);
2359 if (index < 0) {
2360 Console::printf("ERROR: Module '%s' not found\r\n", args);
2361 return;
2362 }
2363
2364 auto& reg = core::ModuleRegistry::instance();
2365 uint8_t idx = static_cast<uint8_t>(index);
2366
2367 if (!reg.isModuleEnabled(idx)) {
2368 Console::printf("OK: Module '%s' already disabled\r\n", args);
2369 return;
2370 }
2371
2372 bool needsReplugBefore = core::UsbManager::instance().needsReplug();
2373
2374 reg.setModuleEnabled(idx, false);
2375 core::IModule* module = reg.getModuleAt(idx);
2376 if (module && module->getState() == core::ServiceState::STARTED) {
2377 module->stop();
2378 }
2379
2380 Console::printf("OK: Module '%s' disabled\r\n", args);
2381
2382 if (core::UsbManager::instance().newlyRequiresReplug(needsReplugBefore)) {
2383 Console::printf("NOTE: USB replug required\r\n");
2384 }
2385}
2386
2390
2391static const SubCommand kNvsSubs[] = {
2392 {"LIST", "[namespace]", "List entries (optional namespace filter)", cmdNvsList},
2393 {"READ", "<ns> <key>", "Read key value", cmdNvsRead},
2394 {"DEL", "<ns> [key]", "Delete key, or entire namespace if omitted",cmdNvsDel},
2395 {"CLEAR", "YES", "Erase entire NVS (confirmation required)", cmdNvsClear},
2396 {nullptr, nullptr, nullptr, nullptr},
2397};
2398static void cmdNvs(const char* args) { dispatchSubCommand("NVS", args, kNvsSubs); }
2399
2400static const SubCommand kPinSubs[] = {
2401 {"STATUS", "", "Show PIN retries / lockout state", cmdPinStatus},
2402 {"RESET", "", "Reset PIN retries (debug)", cmdPinReset},
2403 {"CHANGE", "<currentPin> <newPin>", "Change badge PIN (4-8 digits)", cmdPinChange},
2404 {"DURESS", "<pin>", "Arm self-destruct PIN (wipes on entry)", cmdPinDuress},
2405 {"DURESS_CLEAR", "", "Disarm the self-destruct PIN", cmdPinDuressClear},
2406 {nullptr, nullptr, nullptr, nullptr},
2407};
2408static void cmdPin(const char* args) { dispatchSubCommand("PIN", args, kPinSubs); }
2409
2410static const SubCommand kTr01Subs[] = {
2411 {"STATUS", "", "Show TR01 connection status", cmdTr01Status},
2412 {"INFO", "", "Show TR01 chip info (ID, firmware)", cmdTr01Info},
2413 {"SESSION", "", "Start/restart TR01 session", cmdTr01Session},
2414 {"SLOTS", "", "Show TR01 slot usage summary", cmdTr01Slots},
2415 {"RMEM_READ", "<slot>", "Read and dump R-Memory slot", cmdTr01RmemRead},
2416 {"ECC_DEL", "<slot>", "Delete ECC key slot", cmdTr01EccDel},
2417 {"RMEM_DEL", "<slot>", "Delete R-Memory slot", cmdTr01RmemDel},
2418 {"RESYNC", "", "Resync TR01 session and cache", cmdTr01Resync},
2419 {"CACHE_REBUILD", "", "Rebuild TR01 cache from chip", cmdTr01CacheRebuild},
2420 {"CLEANUP", "", "Cleanup mismatched slots and rebuild cache", cmdTr01Cleanup},
2421 {"WIPE", "CONFIRM", "Factory reset all TR01 data", cmdTr01Wipe},
2422 {nullptr, nullptr, nullptr, nullptr},
2423};
2424static void cmdTr01(const char* args) { dispatchSubCommand("TR01", args, kTr01Subs); }
2425
2426static const SubCommand kWifiSubs[] = {
2427 {"SCAN", "", "Scan for available networks", cmdWifiScan},
2428 {"STATUS", "", "Show WiFi state and saved configuration", cmdWifiStatus},
2429 {"ON", "[sta|ap|sta_ap]", "Enable WiFi radio (default STA, auto-reconnect)", cmdWifiOn},
2430 {"OFF", "", "Disable WiFi radio", cmdWifiOff},
2431 {"CONNECT", "<ssid> <password>", "Connect to network and persist credentials", cmdWifiConnect},
2432 {"TIMEOUT", "[ms]", "Get or set connect timeout (3000-60000 ms)", cmdWifiTimeout},
2433 {"FORGET", "", "Clear saved WiFi configuration", cmdWifiForget},
2434 {nullptr, nullptr, nullptr, nullptr},
2435};
2436static void cmdWifi(const char* args) { dispatchSubCommand("WIFI", args, kWifiSubs); }
2437
2438static const SubCommand kModuleSubs[] = {
2439 {"LIST", "", "List modules with state and errors", cmdModuleList},
2440 {"ENABLE", "<name>", "Enable a module (persistent)", cmdModuleEnable},
2441 {"DISABLE", "<name>", "Disable a module (persistent)", cmdModuleDisable},
2442 {nullptr, nullptr, nullptr, nullptr},
2443};
2444static void cmdModule(const char* args) { dispatchSubCommand("MODULE", args, kModuleSubs); }
2445
2450 auto& reg = getCommandRegistry();
2451
2452 reg.registerCommand({"HELP", "Show available commands", cmdHelp, "system", false});
2453 reg.registerCommand({"PING", "Check if device is responsive", cmdPing, "system", false});
2454 reg.registerCommand({"VERSION", "Show firmware version and API level", cmdVersion, "system", false});
2455 reg.registerCommand({"STATUS", "Show system status", cmdStatus, "system", false});
2456 reg.registerCommand({"MEM", "Show memory usage", cmdMem, "system", false});
2457 reg.registerCommand({"MEMINFO", "Show detailed memory + task info", cmdMemInfo, "system", false});
2458 reg.registerCommand({"CPU", "Measure aggregate CPU load (~250 ms)", cmdCpu, "system", false});
2459 reg.registerCommand({"ERROR_LOG", "Show error log (CLEAR to reset)", cmdErrorLog, "system", false});
2460 reg.registerCommand({"REBOOT", "Restart the device", cmdReboot, "system", true});
2461 reg.registerCommand({"BOOTLOADER", "Reboot into USB download mode", cmdBootloader, "system", true});
2462 reg.registerCommand({"SHIPMODE", "Enter ship mode (disconnect battery)", cmdShipMode, "system", true});
2463 reg.registerCommand({"PASTE", "Paste text into the active T9 input", cmdPaste, "system", true});
2464
2465 reg.registerCommand({"NVS", "NVS storage: LIST/READ/DEL/CLEAR", cmdNvs, "nvs", true, kNvsSubs});
2466
2467 reg.registerCommand({"GET_TIME", "Show current time", cmdGetTime, "time", false});
2468 reg.registerCommand({"GET_DATE", "Show current date", cmdGetDate, "time", false});
2469 reg.registerCommand({"SET_TIME", "Set time (HH:MM:SS)", cmdSetTime, "time", false});
2470 reg.registerCommand({"SET_DATE", "Set date (DD.MM.YYYY or Unix timestamp)", cmdSetDate, "time", false});
2471
2472 reg.registerCommand({"SET_NAME", "Set display name", cmdSetName, "display", false});
2473 reg.registerCommand({"SET_INFO", "Set info line 1", cmdSetInfo, "display", false});
2474 reg.registerCommand({"SET_INFO2", "Set info line 2", cmdSetInfo2, "display", false});
2475
2476 reg.registerCommand({"PIN", "PIN management: STATUS/RESET/CHANGE/DURESS", cmdPin, "pin", true, kPinSubs});
2477
2478 reg.registerCommand({"TR01", "TROPIC01 secure element: STATUS/INFO/SESSION/SLOTS/RMEM_*/ECC_DEL/...",
2479 cmdTr01, "tr01", true, kTr01Subs});
2480
2481#if FEATURE_SECURE_SERIAL
2482 reg.registerCommand({"AUTH", "Authenticate with PIN", cmdAuth, "auth", false});
2483 reg.registerCommand({"LOGOUT", "End authenticated session", cmdLogout, "auth", false});
2484#endif
2485
2486 reg.registerCommand({"WIFI", "WiFi control: SCAN/STATUS/ON/OFF/CONNECT/TIMEOUT/FORGET",
2487 cmdWifi, "wifi", true, kWifiSubs});
2488
2489 reg.registerCommand({"MODULE", "Module control: LIST/ENABLE/DISABLE",
2490 cmdModule, "module", true, kModuleSubs});
2491}
2492
2493} // namespace cdc::serial
static const char * TAG
Canonical CP437 <-> Unicode/UTF-8 codec.
Expert-menu firmware/version screen with an optional upstream update check against the GitHub release...
char name[cdc::hal::ISecureElement::RMEM_NAME_LEN]
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_D(tag, fmt,...)
Definition cdc_log.h:148
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
void error_log_dump(void)
Dumps buffered error-log entries to console.
Definition cdc_log.cpp:113
void log_set_level(log_level_t level)
Sets runtime log verbosity threshold.
Definition cdc_log.cpp:154
void log_register_authgate_hook(log_authgate_hook_t hook)
Registers (or clears) the auth-gate hook for INFO/DEBUG/VERBOSE.
Definition cdc_log.cpp:442
void error_log_clear(void)
Clears error-log ring buffer state.
Definition cdc_log.cpp:105
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
static uint8_t loadOverWindow(uint32_t windowMs=250)
Measure aggregate CPU load over a blocking window.
Definition CpuStats.cpp:40
Module interface that extends IService with module-specific features.
Definition IModule.h:55
virtual ServiceState getState() const =0
virtual const char * getName() const =0
static ModuleRegistry & instance()
Returns the singleton module registry instance.
void resetBadgeRetries()
Resets badge retry counter to maximum.
static constexpr uint8_t BADGE_PIN_MAX
Definition PinManager.h:50
static constexpr uint8_t BADGE_PIN_MIN
Definition PinManager.h:49
bool clearDuressPin()
Clears the duress PIN, disarming the self-destruct trigger.
static PinManager & instance()
Returns singleton PIN manager instance.
static TropicSlotMap & instance()
Returns singleton Tropic slot-map instance.
void forEachRange(SlotType type, RangeCallback cb, void *user) const
Iterates configured slot ranges of the given type in declaration order.
static TropicStorage & instance()
Returns singleton instance of TROPIC metadata cache manager.
bool needsReplug() const
Definition UsbManager.h:69
static UsbManager & instance()
Returns singleton USB manager instance.
static constexpr uint16_t RMEM_SLOT_COUNT
static constexpr uint8_t ECC_SLOT_COUNT
static constexpr uint8_t MAX_SCAN_RESULTS
static void print(const char *str)
Prints raw string to console.
Definition Console.cpp:78
static void showPrompt()
Prints standard shell prompt.
Definition Console.cpp:120
static void flush()
Flushes pending console output.
Definition Console.cpp:104
static void printf(const char *format,...) __attribute__((format(printf
Prints formatted text to console.
Definition Console.cpp:32
static void putchar(char c)
Writes a single character to console.
Definition Console.cpp:88
static int getchar()
Reads one character from console input.
Definition Console.cpp:96
static void init()
Initializes console wrapper state.
Definition Console.cpp:21
virtual void setAuthProvider(bool(*authCheck)())=0
virtual bool processCommand(const char *line)=0
virtual void setOnCommandExecuted(void(*callback)())=0
static bool isAuthenticated()
Returns whether the serial session is currently authenticated.
static void registerBuiltinCommands()
Registers all built-in serial commands.
static void init()
Public SerialCmd interface implementation.
static bool process()
Processes one pending input character from the serial console.
static ICommandRegistry & getRegistry()
Returns the shared command registry instance.
static void logout()
Logs out the current serial session.
static void setTextCallback(TextChangeCallback callback)
Sets the callback used by text-setting commands.
static bool authenticate(const char *pin)
Attempts to authenticate the serial session with a PIN.
static void setTimeCallback(TimeChangeCallback callback)
Sets the callback invoked after successful date/time updates.
static constexpr uint32_t AUTH_TIMEOUT_MS
Definition SerialCmd.h:26
static constexpr size_t CMD_BUFFER_SIZE
Definition SerialCmd.h:25
static void touchAuthSession()
Keeps the auth session alive during a long-running serial activity.
virtual const char * getName() const =0
uint16_t appendRaw(const char *text)
IView * current() const
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:53
bool setUserEnabled(bool enabled)
Sets the user/system WiFi intent and applies it immediately.
static WifiHandlers & instance()
Returns singleton Wi-Fi handlers instance.
CDC Badge OS plugin host API - canonical C ABI contract.
#define HOST_API_LEVEL_STR
Definition host_api.h:30
uint8_t fromUnicode(uint32_t cp)
Map a Unicode codepoint to its CP437 byte, or 0 if it has none.
Definition Cp437.cpp:40
esp_err_t wipeNvs()
Erases the NVS partition and re-initializes it blank.
TropicWipeResult wipeTropic(hal::ISecureElement *se, uint16_t progressEvery=0, void(*onRmemProgress)(uint16_t current, uint16_t total)=nullptr)
Iterates every TROPIC01 ECC slot (0..ECC_SLOT_COUNT-1) and R-Memory slot (0..RMEM_SLOT_COUNT-1),...
@ UsbBudgetFull
HID interface budget is exhausted.
@ Generic
Start failed for an unspecified reason.
@ SlotError
Module reported a slot-map error.
IWifiController * getWifiControllerInstance()
Returns the singleton Wi-Fi controller service instance.
IPowerManager * getPowerManagerInstance()
Returns the singleton power manager instance.
ISecureElement * getSecureElementInstance()
Returns singleton secure-element stub instance.
static hal::ISecureElement * getSecureElementWithCheck()
Secure-element access helpers.
static void cmdNvsRead(const char *args)
Reads and prints a single NVS key value.
static void cmdTr01Session(const char *args)
Starts a secure-element session, restarting it if already active.
static void cmdTr01Cleanup(const char *args)
Cleans up slot metadata inconsistencies and rebuilds cache state.
static void printNvsValue(nvs_handle_t nvs, const char *key, nvs_type_t type)
Prints an NVS value according to its stored type.
void(*)() TimeChangeCallback
Definition SerialCmd.h:12
static void cmdTr01Slots(const char *args)
Prints usage information for ECC and R-Memory slots.
static void printHexDump(const uint8_t *data, size_t len, size_t maxBytes)
NVS utility helpers used by command handlers.
static void historyAdd(const char *cmd)
General-purpose helper functions.
static const SubCommand kTr01Subs[]
static constexpr int YEAR_MIN
Definition SerialCmd.cpp:54
static void cmdSetInfo2(const char *args)
Sets the second info line through the text callback.
static EscState s_escState
Definition SerialCmd.cpp:78
static void redrawLine(const char *newContent, size_t &bufferPos)
Clears the current console line and redraws it with new content.
static void cmdWifi(const char *args)
static void cmdSetDate(const char *args)
Updates the system clock date component.
static void cmdMem(const char *args)
static void cmdPing(const char *args)
Replies with a liveness check response.
static void cmdWifiOn(const char *args)
WIFI_ON [sta|ap|sta_ap] - enable WiFi radio.
static SlotParseResult parseSlotArg(const char *args, uint16_t maxSlot, const char *slotTypeName)
Parses a slot number from a string argument.
static const char * wifiSecurityName(hal::WifiSecurity sec)
static void cmdTr01Status(const char *args)
TROPIC01 secure-element maintenance and diagnostic handlers.
static size_t s_historyHead
Definition SerialCmd.cpp:71
static void cmdSetTime(const char *args)
Updates the system clock time component.
static void cmdBootloader(const char *args)
Reboots the device into USB download (bootloader) mode.
static size_t s_historyPos
Definition SerialCmd.cpp:72
static void cmdHelp(const char *args)
System command handlers.
static void cmdWifiOff(const char *args)
WIFI_OFF - disconnect and disable WiFi radio.
static void cmdPin(const char *args)
static uint64_t s_authTimestamp
Definition SerialCmd.cpp:90
static void cmdTr01Resync(const char *args)
Restarts the secure-element session to resynchronize state.
static void cmdTr01Info(const char *args)
Prints secure-element chip and firmware information.
static constexpr uint32_t WIPE_PROGRESS_INTERVAL
Definition SerialCmd.cpp:56
static void cmdTr01RmemRead(const char *args)
Reads and dumps one secure-element R-Memory slot.
static void cmdNvs(const char *args)
static void cmdGetDate(const char *args)
Prints the current local date.
static void cmdCpu(const char *args)
static void cmdSetName(const char *args)
Display text command handlers.
static void cmdWifiStatus(const char *args)
WIFI_STATUS - show runtime state and saved configuration.
static void cmdGetTime(const char *args)
Date/time command handlers.
static void cmdTr01CacheRebuild(const char *args)
Rebuilds the Tropic slot cache and prints per-slot diagnostics.
static bool s_authenticated
Session authentication flags and timeout baseline.
Definition SerialCmd.cpp:89
static constexpr size_t HISTORY_MAX
Internal constants used by serial command processing.
Definition SerialCmd.cpp:50
static constexpr uint8_t WIFI_MAX_SCAN_RESULTS
static void cmdPinStatus(const char *args)
Prints the current badge PIN status snapshot.
static void cmdTr01EccDel(const char *args)
Deletes one ECC key slot.
static void cmdTr01Wipe(const char *args)
Performs a destructive secure-element factory wipe after confirmation.
static void cmdPinDuress(const char *args)
Arms the duress / self-destruct PIN.
static void cmdModuleList(const char *args)
MODULE LIST - list registered modules with state and errors.
static void cmdShipMode(const char *args)
Enters ship mode (disconnects the battery via BATFET).
static const SubCommand kWifiSubs[]
static constexpr size_t HEX_DUMP_WIDTH
Definition SerialCmd.cpp:51
static void cmdPinReset(const char *args)
Authentication command handlers for secure serial mode.
static void cmdStatus(const char *args)
Prints runtime status information for the device.
static constexpr size_t NVS_KEY_MAX_LEN
Definition SerialCmd.cpp:52
static nvs_type_t findNvsKeyType(const char *ns, const char *key)
Finds the stored NVS type of a key by namespace iteration.
ICommandRegistry & getCommandRegistry()
Returns singleton command-registry interface.
EscState
Escape-sequence parser state for ANSI key handling.
Definition SerialCmd.cpp:77
static constexpr int YEAR_MAX
Definition SerialCmd.cpp:55
static void cmdWifiForget(const char *args)
WIFI_FORGET - disable WiFi and erase the saved configuration.
static const char * wifiStateName(hal::WifiState st)
static TimeChangeCallback s_timeCallback
Definition SerialCmd.cpp:84
static void cmdModuleDisable(const char *args)
MODULE DISABLE <name> - disable a module by name (persistent).
static bool setSystemTime(struct tm *tm)
Sets system time from a populated local tm structure.
static const SubCommand kModuleSubs[]
void(*)(const char *field, const char *value) TextChangeCallback
Definition SerialCmd.h:11
static void cmdWifiConnect(const char *args)
WIFI_CONNECT <ssid> <password> - connect and persist credentials.
static void cmdWifiTimeout(const char *args)
WIFI_TIMEOUT [ms] - get or set the connect timeout.
static void cmdNvsClear(const char *args)
NVS command handlers.
static const SubCommand kPinSubs[]
static void cmdTr01RmemDel(const char *args)
Erases one R-Memory slot.
static const char * wifiModeName(hal::WifiMode m)
static const SubCommand kNvsSubs[]
Sub-command tables and dispatchers for grouped commands.
static void cmdWifiScan(const char *args)
WIFI_SCAN - scan and print networks (deduplicated, sorted by RSSI).
static size_t s_cmdBufferPos
Definition SerialCmd.cpp:63
static bool getCurrentTime(struct timeval &tv, struct tm &tm)
Date/time parsing and validation helpers.
static void cmdVersion(const char *args)
Prints the firmware version, the plugin host API level and the last upstream firmware-check result (i...
static void cmdNvsList(const char *args)
Lists NVS entries, optionally filtered by namespace.
static void cmdPaste(const char *args)
static void printHeapRegion(const char *label, uint32_t caps)
Prints heap and PSRAM usage statistics.
static constexpr uint32_t WIFI_SCAN_POLL_MS
WiFi serial command handlers.
static char s_cmdBuffer[SerialCmd::CMD_BUFFER_SIZE]
Global static state for line editing and command dispatch.
Definition SerialCmd.cpp:62
static void cmdReboot(const char *args)
Reboots the device after flushing serial output.
static const char * getNvsTypeName(nvs_type_t type)
Returns a human-readable name for an NVS type value.
static void cmdPinChange(const char *args)
Changes the badge PIN after verifying the current one.
static const char * historyGet(size_t idx)
Returns a history entry by reverse index (0 = newest).
static void cmdModule(const char *args)
static int findModuleIndex(const char *name)
Module management serial command handlers.
static size_t s_historyCount
Definition SerialCmd.cpp:70
static constexpr size_t NVS_NAMESPACE_MAX_LEN
Definition SerialCmd.cpp:53
static void cmdModuleEnable(const char *args)
MODULE ENABLE <name> - enable a module by name (persistent).
static void cmdNvsDel(const char *args)
Deletes an NVS key or an entire namespace.
static void cmdSetInfo(const char *args)
Sets the first info line through the text callback.
static bool s_initialized
Definition Console.cpp:15
void dispatchSubCommand(const char *parent, const char *args, const SubCommand *table)
Routes a sub-command line to its handler.
Definition SubCommand.h:73
static uint8_t wifiDedupAndSort(hal::WifiScanResult *results, uint8_t count)
Deduplicates scan results by SSID (keeping strongest RSSI) and sorts the survivors descending by RSSI...
static void cmdMemInfo(const char *args)
static void cmdTr01(const char *args)
static TextChangeCallback s_textCallback
Optional callbacks injected by higher-level modules.
Definition SerialCmd.cpp:83
static char s_historyBuffer[HISTORY_MAX][SerialCmd::CMD_BUFFER_SIZE]
Command history ring buffer allocated in PSRAM.
Definition SerialCmd.cpp:69
static void cmdPinDuressClear(const char *args)
Disarms the duress / self-destruct PIN.
static void cmdErrorLog(const char *args)
Displays the error log or clears it when CLEAR is passed.
void rebootIntoBootloader()
Reboots the device into USB download (bootloader) mode.
static constexpr uint32_t WIFI_SCAN_TIMEOUT_MS
static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MIN_MS
static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MAX_MS
bool firmwareCheckLastResult(char *out, size_t cap)
Formats the last persisted upstream check into out.
#define APP_VERSION
Slot parsing helpers for secure-element commands.