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"
24#include "cdc_log.h"
27#include "cdc_ui/ViewStack.h"
28#include "esp_timer.h"
29#include "esp_attr.h"
30#include "nvs_flash.h"
31#include "freertos/FreeRTOS.h"
32#include "freertos/task.h"
33#include "esp_memory_utils.h"
34#include <cstring>
35#include <cctype>
36#include <cstdlib>
37#include <sys/time.h>
38#include <time.h>
39
40static const char* TAG = "SERIAL";
41
42namespace cdc::serial {
43
47
48static constexpr size_t HISTORY_MAX = 10;
49static constexpr size_t HEX_DUMP_WIDTH = 16;
50static constexpr size_t NVS_KEY_MAX_LEN = 15;
51static constexpr size_t NVS_NAMESPACE_MAX_LEN = 15;
52static constexpr int YEAR_MIN = 2020;
53static constexpr int YEAR_MAX = 2100;
54static constexpr uint32_t WIPE_PROGRESS_INTERVAL = 64;
55
59
61static size_t s_cmdBufferPos = 0;
62static bool s_initialized = false;
63
68static size_t s_historyCount = 0;
69static size_t s_historyHead = 0;
70static size_t s_historyPos = 0;
71
75enum class EscState : uint8_t { NONE, ESC, BRACKET };
77
83
87static bool s_authenticated = false;
88static uint64_t s_authTimestamp = 0;
89
93
94#if FEATURE_SECURE_SERIAL
99static void resetAuthTimer() {
100 if (s_authenticated) {
101 s_authTimestamp = esp_timer_get_time();
102 }
103}
104#endif
105
111static void historyAdd(const char* cmd) {
112 if (!cmd || !*cmd) return;
113
116
120 }
121}
122
128static const char* historyGet(size_t idx) {
129 if (idx >= s_historyCount) return nullptr;
130 size_t pos = (s_historyHead + HISTORY_MAX - 1 - idx) % HISTORY_MAX;
131 return s_historyBuffer[pos];
132}
133
140static void redrawLine(const char* newContent, size_t& bufferPos) {
141 while (bufferPos > 0) {
142 Console::print("\b \b");
143 bufferPos--;
144 }
145
146 if (newContent) {
147 strncpy(s_cmdBuffer, newContent, SerialCmd::CMD_BUFFER_SIZE - 1);
149 size_t len = strlen(s_cmdBuffer);
150 bufferPos = len;
152 }
153}
154
158
163 bool valid;
164 long value;
165};
166
174static SlotParseResult parseSlotArg(const char* args, uint16_t maxSlot, const char* slotTypeName) {
175 SlotParseResult result = {false, 0};
176
177 if (!args || !*args) {
178 Console::printf("Usage: Provide a %s number\r\n", slotTypeName);
179 return result;
180 }
181
182 char* endptr = nullptr;
183 long slotVal = strtol(args, &endptr, 10);
184
185 if (endptr == args || *endptr != '\0' || slotVal < 0) {
186 Console::printf("ERROR: Invalid %s number\r\n", slotTypeName);
187 return result;
188 }
189
190 if (slotVal >= maxSlot) {
191 Console::printf("ERROR: Invalid %s (0-%d)\r\n", slotTypeName, maxSlot - 1);
192 return result;
193 }
194
195 result.valid = true;
196 result.value = slotVal;
197 return result;
198}
199
203
210 if (!se) {
211 Console::printf("ERROR: Secure Element not available\r\n");
212 }
213 return se;
214}
215
219
227static void printHexDump(const uint8_t* data, size_t len, size_t maxBytes) {
228 for (size_t i = 0; i < len && i < maxBytes; i += HEX_DUMP_WIDTH) {
229 Console::printf(" %04X: ", (unsigned)i);
230 for (size_t j = 0; j < HEX_DUMP_WIDTH && (i + j) < len; j++) {
231 Console::printf("%02X ", data[i + j]);
232 }
233 Console::printf("\r\n");
234 }
235 if (len > maxBytes) {
236 Console::printf(" ... (%d more bytes)\r\n", (int)(len - maxBytes));
237 }
238}
239
245static const char* getNvsTypeName(nvs_type_t type) {
246 switch (type) {
247 case NVS_TYPE_U8: return "u8";
248 case NVS_TYPE_I8: return "i8";
249 case NVS_TYPE_U16: return "u16";
250 case NVS_TYPE_I16: return "i16";
251 case NVS_TYPE_U32: return "u32";
252 case NVS_TYPE_I32: return "i32";
253 case NVS_TYPE_U64: return "u64";
254 case NVS_TYPE_I64: return "i64";
255 case NVS_TYPE_STR: return "str";
256 case NVS_TYPE_BLOB: return "blob";
257 default: return "?";
258 }
259}
260
267static nvs_type_t findNvsKeyType(const char* ns, const char* key) {
268 nvs_iterator_t it = nullptr;
269 esp_err_t err = nvs_entry_find("nvs", ns, NVS_TYPE_ANY, &it);
270 nvs_type_t keyType = NVS_TYPE_ANY;
271
272 while (it != nullptr) {
273 nvs_entry_info_t info;
274 nvs_entry_info(it, &info);
275 if (strcmp(info.key, key) == 0) {
276 keyType = info.type;
277 break;
278 }
279 err = nvs_entry_next(&it);
280 if (err != ESP_OK) break;
281 }
282 nvs_release_iterator(it);
283 return keyType;
284}
285
293static void printNvsValue(nvs_handle_t nvs, const char* key, nvs_type_t type) {
294 switch (type) {
295 case NVS_TYPE_U8: {
296 uint8_t val;
297 if (nvs_get_u8(nvs, key, &val) == ESP_OK) {
298 Console::printf("%u (0x%02X)\r\n", val, val);
299 }
300 break;
301 }
302 case NVS_TYPE_I8: {
303 int8_t val;
304 if (nvs_get_i8(nvs, key, &val) == ESP_OK) {
305 Console::printf("%d\r\n", val);
306 }
307 break;
308 }
309 case NVS_TYPE_U16: {
310 uint16_t val;
311 if (nvs_get_u16(nvs, key, &val) == ESP_OK) {
312 Console::printf("%u (0x%04X)\r\n", val, val);
313 }
314 break;
315 }
316 case NVS_TYPE_I16: {
317 int16_t val;
318 if (nvs_get_i16(nvs, key, &val) == ESP_OK) {
319 Console::printf("%d\r\n", val);
320 }
321 break;
322 }
323 case NVS_TYPE_U32: {
324 uint32_t val;
325 if (nvs_get_u32(nvs, key, &val) == ESP_OK) {
326 Console::printf("%lu (0x%08lX)\r\n", (unsigned long)val, (unsigned long)val);
327 }
328 break;
329 }
330 case NVS_TYPE_I32: {
331 int32_t val;
332 if (nvs_get_i32(nvs, key, &val) == ESP_OK) {
333 Console::printf("%ld\r\n", (long)val);
334 }
335 break;
336 }
337 case NVS_TYPE_U64: {
338 uint64_t val;
339 if (nvs_get_u64(nvs, key, &val) == ESP_OK) {
340 Console::printf("%llu\r\n", (unsigned long long)val);
341 }
342 break;
343 }
344 case NVS_TYPE_I64: {
345 int64_t val;
346 if (nvs_get_i64(nvs, key, &val) == ESP_OK) {
347 Console::printf("%lld\r\n", (long long)val);
348 }
349 break;
350 }
351 case NVS_TYPE_STR: {
352 size_t len = 0;
353 if (nvs_get_str(nvs, key, nullptr, &len) == ESP_OK && len > 0) {
354 char* buf = static_cast<char*>(malloc(len));
355 if (!buf) {
356 LOG_E(TAG, "Failed to allocate %d bytes for NVS string", (int)len);
357 Console::printf("(allocation failed)\r\n");
358 break;
359 }
360 if (nvs_get_str(nvs, key, buf, &len) == ESP_OK) {
361 Console::printf("\"%s\"\r\n", buf);
362 }
363 free(buf);
364 }
365 break;
366 }
367 case NVS_TYPE_BLOB: {
368 size_t len = 0;
369 if (nvs_get_blob(nvs, key, nullptr, &len) == ESP_OK && len > 0) {
370 Console::printf("(blob, %d bytes)\r\n", (int)len);
371 uint8_t* buf = static_cast<uint8_t*>(malloc(len));
372 if (!buf) {
373 LOG_E(TAG, "Failed to allocate %d bytes for NVS blob", (int)len);
374 Console::printf(" (allocation failed)\r\n");
375 break;
376 }
377 if (nvs_get_blob(nvs, key, buf, &len) == ESP_OK) {
378 printHexDump(buf, len, len);
379 }
380 free(buf);
381 }
382 break;
383 }
384 default:
385 Console::printf("(unknown type)\r\n");
386 break;
387 }
388}
389
393
400static bool getCurrentTime(struct timeval& tv, struct tm& tm) {
401 gettimeofday(&tv, nullptr);
402 return localtime_r(&tv.tv_sec, &tm) != nullptr;
403}
404
410static bool setSystemTime(struct tm* tm) {
411 struct timeval tv;
412 tv.tv_sec = mktime(tm);
413 tv.tv_usec = 0;
414 return settimeofday(&tv, nullptr) == 0;
415}
416
420
425static void cmdHelp(const char* args) {
426 (void)args;
428}
429
434static void cmdPing(const char* args) {
435 (void)args;
436 Console::printf("PONG\r\n");
437}
438
443static void cmdStatus(const char* args) {
444 (void)args;
445 Console::printf("=== System Status ===\r\n");
446 Console::printf("Free heap: %lu bytes\r\n", (unsigned long)esp_get_free_heap_size());
447 Console::printf("Min free heap: %lu bytes\r\n", (unsigned long)esp_get_minimum_free_heap_size());
448 Console::printf("Uptime: %llu ms\r\n", esp_timer_get_time() / 1000ULL);
450}
451
452static void cmdCpu(const char* args) {
453 (void)args;
454 Console::printf("Measuring CPU load (~250 ms)...\r\n");
457 Console::printf("CPU load: %u %%\r\n", (unsigned)load);
459}
460
469static void printHeapRegion(const char* label, uint32_t caps) {
470 multi_heap_info_t info;
471 heap_caps_get_info(&info, caps);
472 if (info.total_free_bytes + info.total_allocated_bytes == 0) return;
473 Console::printf("\r\n-- %s --\r\n", label);
474 Console::printf(" total free : %lu\r\n", (unsigned long)info.total_free_bytes);
475 Console::printf(" total allocated : %lu\r\n", (unsigned long)info.total_allocated_bytes);
476 Console::printf(" largest free : %lu\r\n", (unsigned long)info.largest_free_block);
477 Console::printf(" min ever free : %lu\r\n", (unsigned long)info.minimum_free_bytes);
478 Console::printf(" free blocks : %lu\r\n", (unsigned long)info.free_blocks);
479 Console::printf(" alloc blocks : %lu\r\n", (unsigned long)info.allocated_blocks);
480}
481
482static void cmdMemInfo(const char* args) {
483 (void)args;
484 Console::printf("=== Detailed Memory Info ===\r\n");
485
486 printHeapRegion("Internal (any)", MALLOC_CAP_INTERNAL);
487 printHeapRegion("Internal DRAM (8-bit)", MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
488 printHeapRegion("Internal 32-bit only", MALLOC_CAP_INTERNAL | MALLOC_CAP_32BIT);
489 printHeapRegion("IRAM (executable)", MALLOC_CAP_EXEC);
490 printHeapRegion("DMA-capable", MALLOC_CAP_DMA);
491 printHeapRegion("PSRAM", MALLOC_CAP_SPIRAM);
492 printHeapRegion("RTC slow RAM", MALLOC_CAP_RTCRAM);
493
494 Console::printf("\r\n-- Heap totals (heap_caps_get_total_size) --\r\n");
495 Console::printf(" INTERNAL : %u\r\n", (unsigned)heap_caps_get_total_size(MALLOC_CAP_INTERNAL));
496 Console::printf(" EXEC : %u\r\n", (unsigned)heap_caps_get_total_size(MALLOC_CAP_EXEC));
497 Console::printf(" SPIRAM : %u\r\n", (unsigned)heap_caps_get_total_size(MALLOC_CAP_SPIRAM));
498 Console::printf(" DMA : %u\r\n", (unsigned)heap_caps_get_total_size(MALLOC_CAP_DMA));
499 Console::printf(" INTERNAL|EXEC : free=%u largest=%u total=%u\r\n",
500 (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_EXEC),
501 (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_EXEC),
502 (unsigned)heap_caps_get_total_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_EXEC));
503 Console::printf(" 32BIT : free=%u largest=%u\r\n",
504 (unsigned)heap_caps_get_free_size(MALLOC_CAP_32BIT),
505 (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_32BIT));
506
507#if CONFIG_FREERTOS_USE_TRACE_FACILITY
508 UBaseType_t numTasks = uxTaskGetNumberOfTasks();
509 TaskStatus_t* tasks = (TaskStatus_t*)calloc(numTasks, sizeof(TaskStatus_t));
510 if (tasks) {
511 numTasks = uxTaskGetSystemState(tasks, numTasks, nullptr);
512 Console::printf("\r\n-- Tasks (%u) --\r\n", (unsigned)numTasks);
513 Console::printf(" %-16s Prio StkMinFree Stk@ State Core\r\n", "Name");
514 uint32_t totalStackFree = 0;
515 for (UBaseType_t i = 0; i < numTasks; i++) {
516 const char* st = "?";
517 switch (tasks[i].eCurrentState) {
518 case eRunning: st = "RUN"; break;
519 case eReady: st = "RDY"; break;
520 case eBlocked: st = "BLK"; break;
521 case eSuspended: st = "SUS"; break;
522 case eDeleted: st = "DEL"; break;
523 case eInvalid: st = "INV"; break;
524 }
525 BaseType_t coreId = -1;
526#if INCLUDE_xTaskGetCoreID
527 coreId = xTaskGetCoreID(tasks[i].xHandle);
528#endif
529 const char* stackLoc = "DRAM";
530 if (tasks[i].pxStackBase != nullptr &&
531 esp_ptr_external_ram(tasks[i].pxStackBase)) {
532 stackLoc = "PSRAM";
533 }
534 Console::printf(" %-16s %-4u %-10lu %-5s %-5s %d\r\n",
535 tasks[i].pcTaskName,
536 (unsigned)tasks[i].uxCurrentPriority,
537 (unsigned long)tasks[i].usStackHighWaterMark,
538 stackLoc,
539 st,
540 (int)coreId);
541 totalStackFree += tasks[i].usStackHighWaterMark;
542 }
543 Console::printf(" (Sum stack headroom across all tasks: %lu B)\r\n",
544 (unsigned long)totalStackFree);
545 free(tasks);
546 }
547#else
548 Console::printf("\r\nTask list unavailable (FREERTOS_USE_TRACE_FACILITY=n)\r\n");
549#endif
550
552}
553
554static void cmdMem(const char* args) {
555 (void)args;
556 Console::printf("=== Memory Usage ===\r\n");
557 Console::printf("Heap (total): %lu / %lu bytes free\r\n",
558 (unsigned long)esp_get_free_heap_size(),
559 (unsigned long)heap_caps_get_total_size(MALLOC_CAP_DEFAULT));
560
561 size_t intFree = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
562 size_t intTotal = heap_caps_get_total_size(MALLOC_CAP_INTERNAL);
563 size_t intLargest = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
564 Console::printf("Internal DRAM: %lu / %lu free (largest block %lu)\r\n",
565 (unsigned long)intFree,
566 (unsigned long)intTotal,
567 (unsigned long)intLargest);
568
569 size_t dmaFree = heap_caps_get_free_size(MALLOC_CAP_DMA);
570 size_t dmaLargest = heap_caps_get_largest_free_block(MALLOC_CAP_DMA);
571 Console::printf("DMA-capable: %lu free (largest %lu)\r\n",
572 (unsigned long)dmaFree,
573 (unsigned long)dmaLargest);
574
575 size_t psramFree = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
576 size_t psramTotal = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
577 if (psramTotal > 0) {
578 Console::printf("PSRAM: %lu / %lu bytes free\r\n",
579 (unsigned long)psramFree,
580 (unsigned long)psramTotal);
581 }
583}
584
589static void cmdReboot(const char* args) {
590 (void)args;
591 Console::printf("Rebooting...\r\n");
593 vTaskDelay(pdMS_TO_TICKS(100));
594 esp_restart();
595}
596
601static void cmdBootloader(const char* args) {
602 (void)args;
603 Console::printf("Rebooting into download mode...\r\n");
606}
607
612static void cmdShipMode(const char* args) {
613 (void)args;
614 auto* power = hal::getPowerManagerInstance();
615 if (!power) {
616 Console::printf("ERROR: Power manager not available\r\n");
617 return;
618 }
619 Console::printf("Entering ship mode (battery disconnect)...\r\n");
621 power->enterShipMode();
622 Console::printf("OK\r\n");
623}
624
625static void cmdPaste(const char* args) {
626 if (!args || !*args) {
627 Console::printf("Usage: PASTE <text>\r\n");
628 return;
629 }
631 if (!top || strcmp(top->getName(), "T9InputView") != 0) {
632 Console::printf("ERROR: T9 input view not active\r\n");
633 return;
634 }
635 auto* t9 = static_cast<cdc::ui::T9InputView*>(top);
636 uint16_t added = t9->appendRaw(args);
637 Console::printf("OK: %u chars\r\n", static_cast<unsigned>(added));
638}
639
644static void cmdErrorLog(const char* args) {
645 if (args && strcmp(args, "CLEAR") == 0) {
647 Console::printf("Error log cleared.\r\n");
648 } else {
650 }
651}
652
656
661static void cmdNvsClear(const char* args) {
662 if (!args || strcmp(args, "YES") != 0) {
663 Console::printf("WARNING: This will ERASE ALL NVS data!\r\n");
664 Console::printf(" - All module settings\r\n");
665 Console::printf(" - All stored preferences\r\n");
666 Console::printf(" - WiFi credentials\r\n");
667 Console::printf(" - Timezone settings\r\n");
668 Console::printf("\r\nTo proceed, type: NVS CLEAR YES\r\n");
669 return;
670 }
671
672 Console::printf("Clearing NVS...\r\n");
673 esp_err_t err = core::wipeNvs();
674 if (err != ESP_OK) {
675 Console::printf("ERROR: NVS wipe failed (%s)\r\n", esp_err_to_name(err));
676 return;
677 }
678 Console::printf("OK: NVS cleared. Reboot recommended.\r\n");
679}
680
685static void cmdNvsList(const char* args) {
686 const char* nsFilter = (args && *args) ? args : nullptr;
687
688 nvs_iterator_t it = nullptr;
689 esp_err_t err = nvs_entry_find("nvs", nsFilter, NVS_TYPE_ANY, &it);
690
691 if (err == ESP_ERR_NVS_NOT_FOUND) {
692 if (nsFilter) {
693 Console::printf("Namespace '%s' not found or empty\r\n", nsFilter);
694 } else {
695 Console::printf("NVS is empty\r\n");
696 }
697 return;
698 }
699
700 if (err != ESP_OK) {
701 Console::printf("ERROR: nvs_entry_find failed (%s)\r\n", esp_err_to_name(err));
702 return;
703 }
704
705 Console::printf("=== NVS Contents ===\r\n");
706 if (nsFilter) {
707 Console::printf("Namespace: %s\r\n", nsFilter);
708 }
709
710 char lastNs[NVS_NAMESPACE_MAX_LEN + 1] = {0};
711 int count = 0;
712
713 while (it != nullptr) {
714 nvs_entry_info_t info;
715 nvs_entry_info(it, &info);
716
717 if (!nsFilter && strcmp(lastNs, info.namespace_name) != 0) {
718 strncpy(lastNs, info.namespace_name, sizeof(lastNs) - 1);
719 Console::printf("\r\n[%s]\r\n", info.namespace_name);
720 }
721
722 Console::printf(" %s (%s)\r\n", info.key, getNvsTypeName(info.type));
723 count++;
724
725 err = nvs_entry_next(&it);
726 if (err != ESP_OK) break;
727 }
728
729 nvs_release_iterator(it);
730 Console::printf("\r\nTotal: %d entries\r\n", count);
731}
732
737static void cmdNvsRead(const char* args) {
738 if (!args || !*args) {
739 Console::printf("Usage: NVS READ <namespace> <key>\r\n");
740 return;
741 }
742
743 char ns[NVS_NAMESPACE_MAX_LEN + 1] = {0};
744 char key[NVS_KEY_MAX_LEN + 1] = {0};
745 if (sscanf(args, "%15s %15s", ns, key) != 2) {
746 Console::printf("Usage: NVS READ <namespace> <key>\r\n");
747 return;
748 }
749
750 nvs_handle_t nvs;
751 esp_err_t err = nvs_open(ns, NVS_READONLY, &nvs);
752 if (err != ESP_OK) {
753 Console::printf("ERROR: Cannot open namespace '%s' (%s)\r\n", ns, esp_err_to_name(err));
754 return;
755 }
756
757 nvs_type_t keyType = findNvsKeyType(ns, key);
758 if (keyType == NVS_TYPE_ANY) {
759 Console::printf("ERROR: Key '%s' not found in namespace '%s'\r\n", key, ns);
760 nvs_close(nvs);
761 return;
762 }
763
764 Console::printf("%s.%s = ", ns, key);
765 printNvsValue(nvs, key, keyType);
766 nvs_close(nvs);
767}
768
773static void cmdNvsDel(const char* args) {
774 if (!args || !*args) {
775 Console::printf("Usage: NVS DEL <namespace> [key]\r\n");
776 Console::printf(" Without key: erases entire namespace\r\n");
777 return;
778 }
779
780 char ns[NVS_NAMESPACE_MAX_LEN + 1] = {0};
781 char key[NVS_KEY_MAX_LEN + 1] = {0};
782 int parsed = sscanf(args, "%15s %15s", ns, key);
783
784 if (parsed < 1) {
785 Console::printf("Usage: NVS DEL <namespace> [key]\r\n");
786 return;
787 }
788
789 nvs_handle_t nvs;
790 esp_err_t err = nvs_open(ns, NVS_READWRITE, &nvs);
791 if (err != ESP_OK) {
792 Console::printf("ERROR: Cannot open namespace '%s' (%s)\r\n", ns, esp_err_to_name(err));
793 return;
794 }
795
796 if (parsed == 1 || key[0] == '\0') {
797 err = nvs_erase_all(nvs);
798 if (err == ESP_OK) {
799 nvs_commit(nvs);
800 Console::printf("OK: Namespace '%s' erased\r\n", ns);
801 } else {
802 Console::printf("ERROR: Erase failed (%s)\r\n", esp_err_to_name(err));
803 }
804 } else {
805 err = nvs_erase_key(nvs, key);
806 if (err == ESP_OK) {
807 nvs_commit(nvs);
808 Console::printf("OK: Key '%s.%s' deleted\r\n", ns, key);
809 } else if (err == ESP_ERR_NVS_NOT_FOUND) {
810 Console::printf("ERROR: Key '%s' not found\r\n", key);
811 } else {
812 Console::printf("ERROR: Delete failed (%s)\r\n", esp_err_to_name(err));
813 }
814 }
815
816 nvs_close(nvs);
817}
818
822
827static void cmdGetTime(const char* args) {
828 (void)args;
829 struct timeval tv;
830 struct tm tm;
831 if (getCurrentTime(tv, tm)) {
832 Console::printf("%02d:%02d:%02d\r\n", tm.tm_hour, tm.tm_min, tm.tm_sec);
833 } else {
834 Console::printf("--:--:--\r\n");
835 }
836}
837
842static void cmdGetDate(const char* args) {
843 (void)args;
844 struct timeval tv;
845 struct tm tm;
846 if (getCurrentTime(tv, tm)) {
847 Console::printf("%02d.%02d.%04d\r\n", tm.tm_mday, tm.tm_mon + 1, tm.tm_year + 1900);
848 } else {
849 Console::printf("--.---.----\r\n");
850 }
851}
852
857static void cmdSetTime(const char* args) {
858 if (!args || !*args) {
859 Console::printf("Usage: SET_TIME HH:MM:SS\r\n");
860 return;
861 }
862 int h, m, s;
863 if (sscanf(args, "%d:%d:%d", &h, &m, &s) != 3) {
864 Console::printf("ERROR: Invalid format. Use HH:MM:SS\r\n");
865 return;
866 }
867 if (h < 0 || h > 23 || m < 0 || m > 59 || s < 0 || s > 59) {
868 Console::printf("ERROR: Invalid time values\r\n");
869 return;
870 }
871
872 struct timeval tv;
873 struct tm tm;
874 if (getCurrentTime(tv, tm)) {
875 tm.tm_hour = h;
876 tm.tm_min = m;
877 tm.tm_sec = s;
878 if (setSystemTime(&tm)) {
879 Console::printf("OK: Time set to %02d:%02d:%02d\r\n", h, m, s);
880 if (s_timeCallback) {
882 }
883 } else {
884 Console::printf("ERROR: Failed to set time\r\n");
885 }
886 } else {
887 Console::printf("ERROR: Failed to set time\r\n");
888 }
889}
890
895static void cmdSetDate(const char* args) {
896 if (!args || !*args) {
897 Console::printf("Usage: SET_DATE DD.MM.YYYY | <unix_seconds>\r\n");
898 return;
899 }
900
901 int d, m, y;
902 if (sscanf(args, "%d.%d.%d", &d, &m, &y) == 3) {
903 if (d < 1 || d > 31 || m < 1 || m > 12 || y < YEAR_MIN || y > YEAR_MAX) {
904 Console::printf("ERROR: Invalid date values\r\n");
905 return;
906 }
907 struct timeval tv;
908 struct tm tm;
909 if (getCurrentTime(tv, tm)) {
910 tm.tm_mday = d;
911 tm.tm_mon = m - 1;
912 tm.tm_year = y - 1900;
913 if (setSystemTime(&tm)) {
914 Console::printf("OK: Date set to %02d.%02d.%04d\r\n", d, m, y);
915 if (s_timeCallback) {
917 }
918 return;
919 }
920 }
921 Console::printf("ERROR: Failed to set date\r\n");
922 return;
923 }
924
925 // No dotted date: treat the argument as a Unix timestamp (UTC seconds)
926 // and set the full clock (date and time of day) at once.
927 long long ts;
928 if (sscanf(args, "%lld", &ts) != 1 || ts < 0) {
929 Console::printf("ERROR: Invalid format. Use DD.MM.YYYY or a Unix timestamp\r\n");
930 return;
931 }
932 time_t secs = static_cast<time_t>(ts);
933 struct tm tm;
934 if (!gmtime_r(&secs, &tm)) {
935 Console::printf("ERROR: Failed to set date\r\n");
936 return;
937 }
938 int year = tm.tm_year + 1900;
939 if (year < YEAR_MIN || year > YEAR_MAX) {
940 Console::printf("ERROR: Timestamp out of range (%d-%d)\r\n", YEAR_MIN, YEAR_MAX);
941 return;
942 }
943 struct timeval tv;
944 tv.tv_sec = secs;
945 tv.tv_usec = 0;
946 if (settimeofday(&tv, nullptr) == 0) {
947 Console::printf("OK: Time set to %lld (%04d-%02d-%02d %02d:%02d:%02d UTC)\r\n",
948 ts, year, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
949 if (s_timeCallback) {
951 }
952 } else {
953 Console::printf("ERROR: Failed to set date\r\n");
954 }
955}
956
960
965static void cmdSetName(const char* args) {
966 if (!args) args = "";
967 if (s_textCallback) {
968 s_textCallback("name", args);
969 }
970 Console::printf("OK: Name set to \"%s\"\r\n", args);
971}
972
977static void cmdSetInfo(const char* args) {
978 if (!args) args = "";
979 if (s_textCallback) {
980 s_textCallback("info", args);
981 }
982 Console::printf("OK: Info set to \"%s\"\r\n", args);
983}
984
989static void cmdSetInfo2(const char* args) {
990 if (!args) args = "";
991 if (s_textCallback) {
992 s_textCallback("info2", args);
993 }
994 Console::printf("OK: Info2 set to \"%s\"\r\n", args);
995}
996
1000
1001#if FEATURE_SECURE_SERIAL
1006static void cmdAuth(const char* args) {
1008
1009 if (pm.isBadgeBlocked()) {
1010 if (pm.isLockoutActive()) {
1011 uint32_t remainingSec = pm.getLockoutRemainingMs() / 1000;
1012 Console::printf("ERROR: PIN locked. Wait %lu seconds.\r\n", (unsigned long)remainingSec);
1013 } else {
1014 Console::printf("ERROR: PIN permanently locked.\r\n");
1015 }
1016 return;
1017 }
1018
1019 if (!args || !*args) {
1020 // No PIN argument: treat as logout.
1023 Console::printf("OK: Logged out\r\n");
1024 } else {
1025 Console::printf("Usage: AUTH <pin>\r\n");
1026 Console::printf("Retries: %d\r\n", pm.getBadgeRetries());
1027 }
1028 return;
1029 }
1030
1031 if (SerialCmd::authenticate(args)) {
1032 Console::printf("OK: Authenticated\r\n");
1033 } else {
1034 // Wrong PIN drops any active session so the next command runs
1035 // unprivileged instead of inheriting the previous session.
1038 }
1039 uint8_t retries = pm.getBadgeRetries();
1040 if (retries == 0) {
1041 if (pm.isLockoutActive()) {
1042 uint32_t remainingSec = pm.getLockoutRemainingMs() / 1000;
1043 Console::printf("ERROR: Wrong PIN. Locked for %lu seconds.\r\n", (unsigned long)remainingSec);
1044 } else {
1045 Console::printf("ERROR: Wrong PIN. Permanently locked.\r\n");
1046 }
1047 } else {
1048 Console::printf("ERROR: Wrong PIN. %d retries remaining.\r\n", retries);
1049 }
1050 }
1051}
1052
1057static void cmdLogout(const char* args) {
1058 (void)args;
1060 Console::printf("OK: Logged out\r\n");
1061}
1062#endif
1063
1067
1072static void cmdPinReset(const char* args) {
1073 (void)args;
1075 Console::printf("OK: Badge PIN retries reset to %d\r\n",
1076 core::PinManager::instance().getBadgeRetries());
1077}
1078
1083static void cmdPinStatus(const char* args) {
1084 (void)args;
1086 Console::printf("Badge PIN: retries=%d blocked=%s set=%s\r\n",
1087 pm.getBadgeRetries(),
1088 pm.isBadgeBlocked() ? "yes" : "no",
1089 pm.isPinSet() ? "yes" : "no");
1090}
1091
1096static void cmdPinChange(const char* args) {
1097 if (!args || !args[0]) {
1098 Console::printf("Usage: PIN CHANGE <currentPin> <newPin>\r\n");
1099 return;
1100 }
1101
1102 const char* space = strchr(args, ' ');
1103 if (!space) {
1104 Console::printf("Usage: PIN CHANGE <currentPin> <newPin>\r\n");
1105 return;
1106 }
1107
1108 char currentPin[core::PinManager::BADGE_PIN_MAX + 1] = {};
1109 char newPin[core::PinManager::BADGE_PIN_MAX + 1] = {};
1110
1111 size_t curLen = static_cast<size_t>(space - args);
1112 if (curLen == 0 || curLen > core::PinManager::BADGE_PIN_MAX) {
1113 Console::printf("ERROR: PIN length must be %u-%u digits\r\n",
1114 static_cast<unsigned>(core::PinManager::BADGE_PIN_MIN),
1115 static_cast<unsigned>(core::PinManager::BADGE_PIN_MAX));
1116 return;
1117 }
1118 memcpy(currentPin, args, curLen);
1119 currentPin[curLen] = '\0';
1120
1121 const char* p = space + 1;
1122 while (*p == ' ') p++;
1123 size_t newLen = strlen(p);
1124 if (newLen == 0 || newLen > core::PinManager::BADGE_PIN_MAX) {
1125 Console::printf("ERROR: PIN length must be %u-%u digits\r\n",
1126 static_cast<unsigned>(core::PinManager::BADGE_PIN_MIN),
1127 static_cast<unsigned>(core::PinManager::BADGE_PIN_MAX));
1128 return;
1129 }
1130 memcpy(newPin, p, newLen);
1131 newPin[newLen] = '\0';
1132
1133 auto isDigits = [](const char* s) {
1134 for (; *s; ++s) if (!isdigit(static_cast<unsigned char>(*s))) return false;
1135 return true;
1136 };
1137 if (!isDigits(currentPin) || !isDigits(newPin)) {
1138 Console::printf("ERROR: PIN must be digits only\r\n");
1139 return;
1140 }
1141
1143 if (pm.isBadgeBlocked()) {
1144 Console::printf("ERROR: PIN entry blocked (lockout active or no retries left)\r\n");
1145 return;
1146 }
1147
1148 if (!pm.changeBadgePin(currentPin, newPin)) {
1149 Console::printf("ERROR: PIN change failed (current PIN wrong or new PIN invalid)\r\n");
1150 return;
1151 }
1152
1153 Console::printf("OK: PIN changed\r\n");
1154}
1155
1164static void cmdPinDuress(const char* args) {
1165 if (!args || !args[0]) {
1166 Console::printf("Usage: PIN DURESS <pin>\r\n");
1167 return;
1168 }
1169
1170 char duressPin[core::PinManager::BADGE_PIN_MAX + 1] = {};
1171 size_t len = strlen(args);
1172 if (len == 0 || len > core::PinManager::BADGE_PIN_MAX) {
1173 Console::printf("ERROR: PIN length must be %u-%u digits\r\n",
1174 static_cast<unsigned>(core::PinManager::BADGE_PIN_MIN),
1175 static_cast<unsigned>(core::PinManager::BADGE_PIN_MAX));
1176 return;
1177 }
1178 memcpy(duressPin, args, len);
1179 duressPin[len] = '\0';
1180
1181 if (!core::PinManager::instance().setDuressPin(duressPin)) {
1182 Console::printf("ERROR: Duress PIN rejected (invalid length, non-digit, or equal to badge PIN)\r\n");
1183 return;
1184 }
1185
1186 Console::printf("OK: Duress PIN armed\r\n");
1187}
1188
1193static void cmdPinDuressClear(const char* args) {
1194 (void)args;
1196 Console::printf("OK: Duress PIN cleared\r\n");
1197}
1198
1202
1207static void cmdTr01Status(const char* args) {
1208 (void)args;
1209 auto* se = getSecureElementWithCheck();
1210 if (!se) return;
1211
1212 Console::printf("TR01 Status:\r\n");
1213 Console::printf(" Session: %s\r\n", se->isSessionActive() ? "active" : "inactive");
1214}
1215
1220static void cmdTr01Info(const char* args) {
1221 (void)args;
1222 auto* se = getSecureElementWithCheck();
1223 if (!se) return;
1224
1225 uint8_t chipId[8];
1226 uint8_t riscvVer[4] = {0};
1227 uint8_t spectVer[4] = {0};
1228
1229 Console::printf("TR01 Info:\r\n");
1230
1231 if (se->getChipId(chipId, sizeof(chipId))) {
1232 Console::printf(" Chip ID: ");
1233 for (int i = 0; i < 8; i++) {
1234 Console::printf("%02X", chipId[i]);
1235 }
1236 Console::printf("\r\n");
1237 } else {
1238 Console::printf(" Chip ID: (read failed)\r\n");
1239 }
1240
1241 if (se->getFwVersion(riscvVer, spectVer)) {
1242 Console::printf(" RISC-V FW: v%u.%u.%u (build %u)\r\n",
1243 riscvVer[3], riscvVer[2], riscvVer[1], riscvVer[0]);
1244 Console::printf(" SPECT FW: v%u.%u.%u (build %u)\r\n",
1245 spectVer[3], spectVer[2], spectVer[1], spectVer[0]);
1246 } else {
1247 Console::printf(" FW Version: (read failed)\r\n");
1248 }
1249}
1250
1255static void cmdTr01Session(const char* args) {
1256 (void)args;
1257 auto* se = getSecureElementWithCheck();
1258 if (!se) return;
1259
1260 if (se->isSessionActive()) {
1261 Console::printf("Session already active, reconnecting...\r\n");
1262 se->sessionEnd();
1263 }
1264
1265 if (se->sessionStart()) {
1266 Console::printf("OK: Session started\r\n");
1267 } else {
1268 Console::printf("ERROR: Session start failed\r\n");
1269 }
1270}
1271
1276static void cmdTr01Slots(const char* args) {
1277 (void)args;
1278 auto* se = getSecureElementWithCheck();
1279 if (!se) return;
1280
1281 Console::printf("ECC Key Slots (0-31):\r\n");
1282 int eccCount = 0;
1283 for (uint8_t i = 0; i < hal::ISecureElement::ECC_SLOT_COUNT; i++) {
1284 if (se->eccSlotUsed(i)) {
1285 Console::printf(" [%02d] Used\r\n", i);
1286 eccCount++;
1287 }
1288 }
1289 if (eccCount == 0) {
1290 Console::printf(" (none)\r\n");
1291 }
1292
1293 Console::printf("\r\nR-Memory Slots:\r\n");
1294 Console::printf(" Slot 0: System PIN/lockout\r\n");
1297 [](const cdc::core::TropicSlotMap::SlotRange& r, void*) {
1298 Console::printf(" %4u-%4u: %s\r\n", r.start, r.end,
1299 r.moduleName ? r.moduleName : "?");
1300 },
1301 nullptr);
1302}
1303
1308static void cmdTr01RmemRead(const char* args) {
1309 auto result = parseSlotArg(args, hal::ISecureElement::RMEM_SLOT_COUNT, "R-Memory slot");
1310 if (!result.valid) {
1311 Console::printf("Usage: TR01 RMEM_READ <slot>\r\n");
1312 return;
1313 }
1314
1315 auto* se = getSecureElementWithCheck();
1316 if (!se) return;
1317
1318 uint16_t slot = static_cast<uint16_t>(result.value);
1319 uint8_t data[256];
1320 uint16_t actualLen = 0;
1321
1322 hal::SeResult seResult = se->rmemRead(slot, data, sizeof(data), &actualLen);
1323 if (seResult != hal::SeResult::OK) {
1324 Console::printf("ERROR: Read failed (slot may be empty)\r\n");
1325 return;
1326 }
1327
1328 Console::printf("R-Memory Slot %d (%d bytes):\r\n", slot, actualLen);
1329 printHexDump(data, actualLen, actualLen);
1330}
1331
1336static void cmdTr01EccDel(const char* args) {
1337 auto result = parseSlotArg(args, hal::ISecureElement::ECC_SLOT_COUNT, "ECC slot");
1338 if (!result.valid) {
1339 Console::printf("Usage: TR01 ECC_DEL <slot>\r\n");
1340 return;
1341 }
1342
1343 auto* se = getSecureElementWithCheck();
1344 if (!se) return;
1345
1346 uint8_t slot = static_cast<uint8_t>(result.value);
1347 hal::SeResult seResult = se->eccDelete(slot);
1348 if (seResult == hal::SeResult::OK) {
1349 Console::printf("OK: ECC slot %d deleted\r\n", slot);
1350 } else {
1351 Console::printf("ERROR: Delete failed\r\n");
1352 }
1353}
1354
1359static void cmdTr01RmemDel(const char* args) {
1360 auto result = parseSlotArg(args, hal::ISecureElement::RMEM_SLOT_COUNT, "R-Memory slot");
1361 if (!result.valid) {
1362 Console::printf("Usage: TR01 RMEM_DEL <slot>\r\n");
1363 return;
1364 }
1365
1366 auto* se = getSecureElementWithCheck();
1367 if (!se) return;
1368
1369 uint16_t slot = static_cast<uint16_t>(result.value);
1370 hal::SeResult seResult = se->rmemErase(slot);
1371 if (seResult == hal::SeResult::OK) {
1372 Console::printf("OK: R-Memory slot %d erased\r\n", slot);
1373 } else {
1374 Console::printf("ERROR: Erase failed\r\n");
1375 }
1376}
1377
1382static void cmdTr01Resync(const char* args) {
1383 (void)args;
1384 auto* se = getSecureElementWithCheck();
1385 if (!se) return;
1386
1387 Console::printf("Resyncing TR01 session...\r\n");
1388
1389 if (se->isSessionActive()) {
1390 se->sessionEnd();
1391 }
1392
1393 if (se->sessionStart()) {
1394 Console::printf("OK: Session restarted, cache invalidated\r\n");
1395 } else {
1396 Console::printf("ERROR: Session restart failed\r\n");
1397 }
1398}
1399
1404static void cmdTr01CacheRebuild(const char* args) {
1405 (void)args;
1406 auto& storage = core::TropicStorage::instance();
1407 Console::printf("Rebuilding TR01 cache...\r\n");
1408
1409 auto logFn = [](uint16_t slot, const char* message, void* ctx) {
1410 (void)ctx;
1411 if (!message) return;
1412 if (strcmp(message, "invalid header") == 0 ||
1413 strcmp(message, "mismatched module") == 0 ||
1414 strcmp(message, "nvs write failed") == 0 ||
1415 strcmp(message, "session start failed") == 0 ||
1416 strcmp(message, "read failed") == 0) {
1417 Console::printf(" slot %u: %s\r\n", slot, message);
1418 } else {
1419 Console::printf(" slot %u: found %s\r\n", slot, message);
1420 }
1421 };
1422
1423 if (storage.rebuildVerbose(logFn, nullptr)) {
1424 Console::printf("OK: Cache rebuilt\r\n");
1425 } else {
1426 Console::printf("ERROR: Cache rebuild failed\r\n");
1427 }
1428}
1429
1434static void cmdTr01Cleanup(const char* args) {
1435 (void)args;
1436 auto& storage = core::TropicStorage::instance();
1437 Console::printf("Cleaning TR01 cache + slots...\r\n");
1438 if (storage.cleanup()) {
1439 Console::printf("OK: Cleanup complete\r\n");
1440 } else {
1441 Console::printf("ERROR: Cleanup failed\r\n");
1442 }
1443}
1444
1449static void cmdTr01Wipe(const char* args) {
1450 auto* se = getSecureElementWithCheck();
1451 if (!se) return;
1452
1453 if (!args || strcmp(args, "CONFIRM") != 0) {
1454 Console::printf("WARNING: This will ERASE ALL data on TROPIC01!\r\n");
1455 Console::printf(" - All ECC keys (slots 0-31)\r\n");
1456 Console::printf(" - All R-Memory data (slots 0-511)\r\n");
1457 Console::printf("\r\nTo proceed, type: TR01 WIPE CONFIRM\r\n");
1458 return;
1459 }
1460
1461 Console::printf("=== TROPIC01 Factory Reset ===\r\n");
1463
1464 if (!se->isSessionActive()) {
1465 if (!se->sessionStart()) {
1466 Console::printf("ERROR: Cannot start session\r\n");
1467 return;
1468 }
1469 }
1470
1471 Console::printf("Erasing ECC keys and R-Memory (this may take a while)...\r\n");
1473 auto result = core::wipeTropic(se, WIPE_PROGRESS_INTERVAL,
1474 [](uint16_t current, uint16_t total) {
1475 Console::printf(" Progress: %d/%d\r\n", current, total);
1477 });
1478
1479 if (!result.sessionReady) {
1480 Console::printf("ERROR: SE session unavailable\r\n");
1481 return;
1482 }
1483
1484 Console::printf("\r\n=== Factory Reset Complete ===\r\n");
1485 Console::printf("Deleted: %d ECC keys, %d R-Memory slots\r\n",
1486 result.eccDeleted, result.rmemDeleted);
1487}
1488
1492
1497 if (s_initialized) return;
1498
1499 Console::init();
1500
1501#if FEATURE_SECURE_SERIAL
1503 getCommandRegistry().setOnCommandExecuted(resetAuthTimer);
1504#if !DEBUG_MODE
1505 // Release profile: suppress INFO/DEBUG/VERBOSE log output until a session
1506 // is authenticated. ERROR/WARN keep flowing so boot failures are still
1507 // visible.
1509#endif
1510#else
1511 log_set_level(CDC_LOG_LEVEL_DEBUG);
1512#endif
1513
1515
1516 s_initialized = true;
1517 LOG_I(TAG, "Serial command processor initialized");
1518
1519 Console::printf("\r\n=== CDC Badge OS Serial Console ===\r\n");
1520#if FEATURE_SECURE_SERIAL
1521 Console::printf("Login with: AUTH <pin>\r\n");
1522#endif
1523 Console::printf("Type 'HELP' for available commands.\r\n");
1525}
1526
1536void SerialCmd::handleHistoryNav(HistoryDirection dir) {
1537 if (dir == HistoryDirection::OLDER) {
1538 if (s_historyPos >= s_historyCount) return;
1539 const char* hist = historyGet(s_historyPos);
1540 if (hist) {
1542 s_historyPos++;
1543 }
1544 return;
1545 }
1546
1547 // HistoryDirection::NEWER
1548 if (s_historyPos == 0) return;
1549 s_historyPos--;
1550 if (s_historyPos == 0) {
1552 return;
1553 }
1554 const char* hist = historyGet(s_historyPos - 1);
1555 if (hist) {
1557 }
1558}
1559
1571bool SerialCmd::handleEscape(int c) {
1572 if (s_escState == EscState::ESC) {
1573 if (c == '[') {
1575 } else {
1577 }
1578 return true;
1579 }
1580
1583 switch (c) {
1584 case 'A':
1585 handleHistoryNav(HistoryDirection::OLDER);
1586 break;
1587 case 'B':
1588 handleHistoryNav(HistoryDirection::NEWER);
1589 break;
1590 default:
1591 break;
1592 }
1593 return true;
1594 }
1595
1596 return false;
1597}
1598
1609void SerialCmd::handleSpecialChar(int c, bool& commandReady) {
1610 commandReady = false;
1611
1612 // Binary-streaming bypass: while a byte interceptor is installed, every
1613 // incoming byte is delivered raw with no echo, no buffering and no line
1614 // processing. Used by `PLUGIN UPLOAD` to slurp the raw payload.
1615 if (auto bi = getCommandRegistry().getByteInterceptor()) {
1616 bi(static_cast<uint8_t>(c));
1617 return;
1618 }
1619
1620 switch (c) {
1621 case 0x1B: // ESC
1623 return;
1624
1625 case '\r':
1626 case '\n':
1627 Console::print("\r\n");
1629 if (s_cmdBufferPos > 0) {
1631 executeCommand(s_cmdBuffer);
1632 }
1633 s_cmdBufferPos = 0;
1634 s_historyPos = 0;
1636 commandReady = true;
1637 return;
1638
1639 case 0x7F: // Backspace (DEL)
1640 case 0x08: // Backspace (BS)
1641 if (s_cmdBufferPos > 0) {
1643 Console::print("\b \b");
1644 }
1645 return;
1646
1647 case 0x03: // Ctrl+C
1648 Console::print("^C\r\n");
1649 s_cmdBufferPos = 0;
1650 s_historyPos = 0;
1652 return;
1653
1654 case 0x15: // Ctrl+U
1655 while (s_cmdBufferPos > 0) {
1656 Console::print("\b \b");
1658 }
1659 return;
1660
1661 default: {
1662 static uint8_t utf8Pending = 0;
1663 static uint32_t utf8Cp = 0;
1664
1665 auto appendByte = [](uint8_t b) {
1666 if (s_cmdBufferPos < CMD_BUFFER_SIZE - 1) {
1667 s_cmdBuffer[s_cmdBufferPos++] = static_cast<char>(b);
1668 Console::putchar(static_cast<char>(b));
1669 }
1670 };
1671
1672 if (utf8Pending) {
1673 if ((c & 0xC0) == 0x80) {
1674 utf8Cp = (utf8Cp << 6) | (c & 0x3F);
1675 if (--utf8Pending == 0) {
1676 uint8_t cp437 = cdc::core::cp437::fromUnicode(utf8Cp);
1677 if (cp437) appendByte(cp437);
1678 }
1679 } else {
1680 utf8Pending = 0;
1681 }
1682 return;
1683 }
1684
1685 if ((c & 0xE0) == 0xC0) {
1686 utf8Cp = c & 0x1F;
1687 utf8Pending = 1;
1688 return;
1689 }
1690 if ((c & 0xF0) == 0xE0) {
1691 utf8Cp = c & 0x0F;
1692 utf8Pending = 2;
1693 return;
1694 }
1695 if ((c & 0xF8) == 0xF0) {
1696 utf8Cp = c & 0x07;
1697 utf8Pending = 3;
1698 return;
1699 }
1700
1701 if (c >= 0x20 && c < 0x7F) {
1702 appendByte(static_cast<uint8_t>(c));
1703 } else if (c >= 0x80 && c <= 0xFF) {
1704 // Raw CP437/Latin-1 byte from terminals that don't speak UTF-8.
1705 appendByte(static_cast<uint8_t>(c));
1706 }
1707 return;
1708 }
1709 }
1710}
1711
1717 bool anyCommandReady = false;
1718 // Drain every byte that is currently buffered, not just one per main-loop
1719 // tick. Without this, binary uploads were rate-limited to one byte per
1720 // ~50 ms UI tick (~20 B/s).
1721 for (int i = 0; i < 4096; ++i) {
1722 int c = Console::getchar();
1723 if (c < 0) break;
1724
1725 if (handleEscape(c)) continue;
1726
1727 bool commandReady = false;
1728 handleSpecialChar(c, commandReady);
1729 if (commandReady) anyCommandReady = true;
1730 }
1731 return anyCommandReady;
1732}
1733
1741
1747 s_textCallback = callback;
1748}
1749
1755 s_timeCallback = callback;
1756}
1757
1763#if FEATURE_SECURE_SERIAL
1764 if (!s_authenticated) return false;
1765
1766 uint64_t now = esp_timer_get_time();
1767 if ((now - s_authTimestamp) > (AUTH_TIMEOUT_MS * 1000ULL)) {
1768 s_authenticated = false;
1769#if !DEBUG_MODE
1770 log_set_level(CDC_LOG_LEVEL_WARN);
1771#endif
1772 LOG_I(TAG, "Session timed out");
1773 return false;
1774 }
1775
1776 return true;
1777#else
1778 return true;
1779#endif
1780}
1781
1787bool SerialCmd::authenticate(const char* pin) {
1789
1790 if (pm.isBadgeBlocked()) {
1791 if (pm.isLockoutActive()) {
1792 uint32_t remainingSec = pm.getLockoutRemainingMs() / 1000;
1793 LOG_W(TAG, "PIN locked, %lu seconds remaining", (unsigned long)remainingSec);
1794 } else {
1795 LOG_W(TAG, "PIN permanently blocked (retries exhausted)");
1796 }
1797 return false;
1798 }
1799
1800 if (!pin || !*pin) {
1801 LOG_W(TAG, "Empty PIN provided");
1802 return false;
1803 }
1804
1805 if (!pm.verifyBadgePin(pin)) {
1806 LOG_W(TAG, "Authentication failed, %d retries remaining", pm.getBadgeRetries());
1807 return false;
1808 }
1809
1810 s_authenticated = true;
1811 s_authTimestamp = esp_timer_get_time();
1812#if !DEBUG_MODE
1813 log_set_level(CDC_LOG_LEVEL_DEBUG);
1814#endif
1815 LOG_I(TAG, "Authenticated via serial");
1816 return true;
1817}
1818
1823#if FEATURE_SECURE_SERIAL
1824 if (s_authenticated) {
1825 s_authTimestamp = esp_timer_get_time();
1826 }
1827#endif
1828}
1829
1834 s_authenticated = false;
1835 s_authTimestamp = 0;
1836#if !DEBUG_MODE
1837 log_set_level(CDC_LOG_LEVEL_WARN);
1838#endif
1839 LOG_I(TAG, "Logged out");
1840}
1841
1846void SerialCmd::executeCommand(char* cmd) {
1847 cmd = trim(cmd);
1848 if (!*cmd) return;
1849
1850 char first_token[24];
1851 size_t i = 0;
1852 while (cmd[i] && !isspace(static_cast<unsigned char>(cmd[i])) &&
1853 i < sizeof(first_token) - 1) {
1854 first_token[i] = cmd[i];
1855 i++;
1856 }
1857 first_token[i] = '\0';
1858 LOG_D(TAG, "Executing: %s", first_token);
1860}
1861
1867char* SerialCmd::trim(char* str) {
1868 if (!str) return str;
1869
1870 while (*str && isspace(static_cast<unsigned char>(*str))) str++;
1871 if (*str == '\0') return str;
1872
1873 char* end = str + strlen(str) - 1;
1874 while (end > str && isspace(static_cast<unsigned char>(*end))) end--;
1875 *(end + 1) = '\0';
1876
1877 return str;
1878}
1879
1887
1888static constexpr uint32_t WIFI_SCAN_POLL_MS = 100;
1890
1891static const char* wifiSecurityName(hal::WifiSecurity sec) {
1892 switch (sec) {
1893 case hal::WifiSecurity::OPEN: return "OPEN";
1894 case hal::WifiSecurity::WEP: return "WEP";
1895 case hal::WifiSecurity::WPA_PSK: return "WPA";
1896 case hal::WifiSecurity::WPA2_PSK: return "WPA2";
1897 case hal::WifiSecurity::WPA3_PSK: return "WPA3";
1898 case hal::WifiSecurity::WPA2_ENTERPRISE: return "WPA2-E";
1899 default: return "?";
1900 }
1901}
1902
1903static const char* wifiStateName(hal::WifiState st) {
1904 switch (st) {
1905 case hal::WifiState::DISCONNECTED: return "DISCONNECTED";
1906 case hal::WifiState::CONNECTING: return "CONNECTING";
1907 case hal::WifiState::CONNECTED: return "CONNECTED";
1908 case hal::WifiState::CONNECTION_FAILED: return "FAILED";
1909 case hal::WifiState::GOT_IP: return "GOT_IP";
1910 default: return "?";
1911 }
1912}
1913
1914static const char* wifiModeName(hal::WifiMode m) {
1915 switch (m) {
1916 case hal::WifiMode::OFF: return "OFF";
1917 case hal::WifiMode::STA: return "STA";
1918 case hal::WifiMode::AP: return "AP";
1919 case hal::WifiMode::STA_AP: return "STA_AP";
1920 default: return "?";
1921 }
1922}
1923
1928static uint8_t wifiDedupAndSort(hal::WifiScanResult* results, uint8_t count) {
1929 if (count <= 1) return count;
1930
1931 uint8_t unique = 0;
1932 for (uint8_t i = 0; i < count; i++) {
1933 bool seen = false;
1934 for (uint8_t j = 0; j < unique; j++) {
1935 if (strcmp(results[i].ssid, results[j].ssid) == 0) {
1936 seen = true;
1937 if (results[i].rssi > results[j].rssi) results[j] = results[i];
1938 break;
1939 }
1940 }
1941 if (!seen && results[i].ssid[0] != '\0') {
1942 if (unique != i) results[unique] = results[i];
1943 unique++;
1944 }
1945 }
1946
1947 for (uint8_t i = 0; i < unique; i++) {
1948 for (uint8_t j = i + 1; j < unique; j++) {
1949 if (results[j].rssi > results[i].rssi) {
1950 hal::WifiScanResult tmp = results[i];
1951 results[i] = results[j];
1952 results[j] = tmp;
1953 }
1954 }
1955 }
1956 return unique;
1957}
1958
1962static void cmdWifiScan(const char* args) {
1963 (void)args;
1964
1965 auto* wifi = hal::getWifiControllerInstance();
1966 if (!wifi) {
1967 Console::printf("ERROR: WiFi not available\r\n");
1968 return;
1969 }
1970
1971 if (!wifi->isEnabled() || wifi->getMode() == hal::WifiMode::AP) {
1972 if (!wifi->enable(hal::WifiMode::STA)) {
1973 Console::printf("ERROR: Failed to enable WiFi\r\n");
1974 return;
1975 }
1976 }
1977
1978 Console::printf("Scanning...\r\n");
1979
1980 if (!wifi->startScan()) {
1981 Console::printf("ERROR: Scan start failed\r\n");
1982 return;
1983 }
1984
1985 uint32_t elapsed = 0;
1986 while (!wifi->isScanComplete() && elapsed < ui::WIFI_SCAN_TIMEOUT_MS) {
1987 vTaskDelay(pdMS_TO_TICKS(WIFI_SCAN_POLL_MS));
1988 elapsed += WIFI_SCAN_POLL_MS;
1989 }
1990
1991 if (!wifi->isScanComplete()) {
1992 Console::printf("ERROR: Scan timeout\r\n");
1993 return;
1994 }
1995
1997 uint8_t count = wifi->getScanResults(results, WIFI_MAX_SCAN_RESULTS);
1998 count = wifiDedupAndSort(results, count);
1999
2000 if (count == 0) {
2001 Console::printf("No networks found\r\n");
2002 } else {
2003 Console::printf("# %-32s %5s %3s %s\r\n", "SSID", "RSSI", "Ch", "Security");
2004 for (uint8_t i = 0; i < count; i++) {
2005 Console::printf("%-2u %-32s %4d %3u %s\r\n",
2006 static_cast<unsigned>(i + 1),
2007 results[i].ssid,
2008 results[i].rssi,
2009 static_cast<unsigned>(results[i].channel),
2010 wifiSecurityName(results[i].security));
2011 }
2012 }
2013
2014 Console::printf("OK\r\n");
2015}
2016
2020static void cmdWifiStatus(const char* args) {
2021 (void)args;
2022
2023 auto* wifi = hal::getWifiControllerInstance();
2024 if (!wifi) {
2025 Console::printf("ERROR: WiFi not available\r\n");
2026 return;
2027 }
2028
2029 Console::printf("Mode: %s\r\n", wifiModeName(wifi->getMode()));
2030 Console::printf("Enabled: %s\r\n", wifi->isEnabled() ? "yes" : "no");
2031 Console::printf("State: %s\r\n", wifiStateName(wifi->getWifiState()));
2032
2033 if (wifi->isConnected()) {
2034 Console::printf("SSID: %s\r\n", wifi->getCurrentSsid());
2035
2036 char ip[16] = {};
2037 if (wifi->getIpAddress(ip, sizeof(ip))) {
2038 Console::printf("IP: %s\r\n", ip);
2039 }
2040 uint8_t mac[6] = {};
2041 if (wifi->getMacAddress(mac)) {
2042 Console::printf("MAC: %02X:%02X:%02X:%02X:%02X:%02X\r\n",
2043 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
2044 }
2045 int8_t rssi = wifi->getRssi();
2046 if (rssi != 0) {
2047 Console::printf("RSSI: %d dBm\r\n", rssi);
2048 }
2049 }
2050
2051 auto& wh = ui::WifiHandlers::instance();
2052 wh.loadConfig();
2053 const auto& cfg = wh.config();
2054 if (cfg.valid) {
2055 Console::printf("\r\nSaved config:\r\n");
2056 Console::printf("SSID: %s\r\n", cfg.ssid);
2057 Console::printf("Security: %s\r\n",
2058 wifiSecurityName(static_cast<hal::WifiSecurity>(cfg.security)));
2059 Console::printf("Timeout: %lu ms\r\n",
2060 static_cast<unsigned long>(wh.getConnectTimeoutMs()));
2061 } else {
2062 Console::printf("\r\nSaved config: (none)\r\n");
2063 }
2064
2065 Console::printf("OK\r\n");
2066}
2067
2073static void cmdWifiOn(const char* args) {
2075
2076 if (args && args[0]) {
2077 if (strcasecmp(args, "ap") == 0) {
2078 mode = hal::WifiMode::AP;
2079 } else if (strcasecmp(args, "sta_ap") == 0) {
2080 mode = hal::WifiMode::STA_AP;
2081 } else if (strcasecmp(args, "sta") != 0) {
2082 Console::printf("Usage: WIFI ON [sta|ap|sta_ap]\r\n");
2083 return;
2084 }
2085 }
2086
2087 auto* wifi = hal::getWifiControllerInstance();
2088 if (!wifi) {
2089 Console::printf("ERROR: WiFi not available\r\n");
2090 return;
2091 }
2092
2093 if (!wifi->enable(mode)) {
2094 Console::printf("ERROR: Failed to enable WiFi\r\n");
2095 return;
2096 }
2097
2098 Console::printf("OK: %s mode enabled\r\n", wifiModeName(mode));
2099
2100 if (mode == hal::WifiMode::AP) return;
2101
2102 auto& wh = ui::WifiHandlers::instance();
2103 wh.loadConfig();
2104 if (!wh.config().valid) return;
2105
2106 Console::printf("Reconnecting to %s...\r\n", wh.config().ssid);
2107 if (!wh.setUserEnabled(true)) {
2108 wifi->disable();
2109 const char* err = wh.getLastError();
2110 Console::printf("ERROR: Reconnect failed (%s)\r\n", err ? err : "?");
2111 return;
2112 }
2113
2114 char ip[16] = {};
2115 wifi->getIpAddress(ip, sizeof(ip));
2116 Console::printf("OK: %s\r\n", ip[0] ? ip : "connected");
2117}
2118
2122static void cmdWifiOff(const char* args) {
2123 (void)args;
2124
2125 auto* wifi = hal::getWifiControllerInstance();
2126 if (!wifi) {
2127 Console::printf("ERROR: WiFi not available\r\n");
2128 return;
2129 }
2130 if (!wifi->isEnabled()) {
2131 Console::printf("OK: already off\r\n");
2132 return;
2133 }
2135 Console::printf("OK: WiFi disabled\r\n");
2136}
2137
2141static void cmdWifiConnect(const char* args) {
2142 if (!args || !args[0]) {
2143 Console::printf("Usage: WIFI CONNECT <ssid> <password>\r\n");
2144 return;
2145 }
2146
2147 const char* space = strchr(args, ' ');
2148 if (!space) {
2149 Console::printf("Usage: WIFI CONNECT <ssid> <password>\r\n");
2150 return;
2151 }
2152
2153 char ssid[33] = {};
2154 char password[65] = {};
2155
2156 size_t ssidLen = static_cast<size_t>(space - args);
2157 if (ssidLen >= sizeof(ssid)) ssidLen = sizeof(ssid) - 1;
2158 memcpy(ssid, args, ssidLen);
2159 ssid[ssidLen] = '\0';
2160
2161 const char* pw = space + 1;
2162 while (*pw == ' ') pw++;
2163 size_t pwLen = strlen(pw);
2164 if (pwLen >= sizeof(password)) pwLen = sizeof(password) - 1;
2165 memcpy(password, pw, pwLen);
2166 password[pwLen] = '\0';
2167
2168 if (ssid[0] == '\0') {
2169 Console::printf("ERROR: SSID required\r\n");
2170 return;
2171 }
2172
2173 auto& wh = ui::WifiHandlers::instance();
2174 wh.saveCredentials(ssid, password);
2175
2176 Console::printf("Connecting to %s (timeout: %lu ms)...\r\n",
2177 ssid, static_cast<unsigned long>(wh.getConnectTimeoutMs()));
2178
2179 if (!wh.setUserEnabled(true)) {
2180 const char* err = wh.getLastError();
2181 Console::printf("ERROR: Connection failed (%s)\r\n", err ? err : "?");
2182 return;
2183 }
2184
2185 auto* wifi = hal::getWifiControllerInstance();
2186 char ip[16] = {};
2187 if (wifi) wifi->getIpAddress(ip, sizeof(ip));
2188 Console::printf("OK: %s\r\n", ip[0] ? ip : "connected");
2189}
2190
2194static void cmdWifiTimeout(const char* args) {
2195 auto& wh = ui::WifiHandlers::instance();
2196 if (!args || !args[0]) {
2197 Console::printf("%lu ms\r\n",
2198 static_cast<unsigned long>(wh.getConnectTimeoutMs()));
2199 return;
2200 }
2201
2202 char* end = nullptr;
2203 long val = strtol(args, &end, 10);
2204 if (end == args || *end != '\0'
2205 || val < static_cast<long>(ui::WIFI_CONNECT_TIMEOUT_MIN_MS)
2206 || val > static_cast<long>(ui::WIFI_CONNECT_TIMEOUT_MAX_MS)) {
2207 Console::printf("Usage: WIFI TIMEOUT [%lu-%lu]\r\n",
2208 static_cast<unsigned long>(ui::WIFI_CONNECT_TIMEOUT_MIN_MS),
2209 static_cast<unsigned long>(ui::WIFI_CONNECT_TIMEOUT_MAX_MS));
2210 return;
2211 }
2212
2213 if (!wh.setConnectTimeoutMs(static_cast<uint32_t>(val))) {
2214 Console::printf("ERROR: Failed to persist timeout\r\n");
2215 return;
2216 }
2217 Console::printf("OK: %ld ms\r\n", val);
2218}
2219
2223static void cmdWifiForget(const char* args) {
2224 (void)args;
2225
2226 auto& wh = ui::WifiHandlers::instance();
2227 wh.disconnect();
2228 wh.clearConfig();
2229
2230 Console::printf("OK: WiFi configuration cleared\r\n");
2231}
2232
2236
2242static int findModuleIndex(const char* name) {
2243 auto& reg = core::ModuleRegistry::instance();
2244 uint8_t count = reg.getModuleCount();
2245 for (uint8_t i = 0; i < count; i++) {
2246 core::IModule* module = reg.getModuleAt(i);
2247 if (module && module->getName() &&
2248 strcasecmp(module->getName(), name) == 0) {
2249 return i;
2250 }
2251 }
2252 return -1;
2253}
2254
2259static void cmdModuleList(const char* args) {
2260 (void)args;
2261 auto& reg = core::ModuleRegistry::instance();
2262 uint8_t count = reg.getModuleCount();
2263
2264 Console::printf("=== Modules (%u) ===\r\n", static_cast<unsigned>(count));
2265 for (uint8_t i = 0; i < count; i++) {
2266 core::IModule* module = reg.getModuleAt(i);
2267 if (!module) continue;
2268
2269 const char* error = reg.getModuleSlotError(i);
2270 Console::printf(" [%2u] %-16s %-8s %-6s %s\r\n",
2271 static_cast<unsigned>(i),
2272 module->getName() ? module->getName() : "?",
2273 reg.isModuleEnabled(i) ? "enabled" : "disabled",
2274 reg.getModuleStatusLabel(i),
2275 error ? error : "");
2276 }
2277}
2278
2283static void cmdModuleEnable(const char* args) {
2284 if (!args || !*args) {
2285 Console::printf("Usage: MODULE ENABLE <name>\r\n");
2286 return;
2287 }
2288
2289 int index = findModuleIndex(args);
2290 if (index < 0) {
2291 Console::printf("ERROR: Module '%s' not found\r\n", args);
2292 return;
2293 }
2294
2295 auto& reg = core::ModuleRegistry::instance();
2296 uint8_t idx = static_cast<uint8_t>(index);
2297
2298 if (reg.isModuleEnabled(idx)) {
2299 Console::printf("OK: Module '%s' already enabled\r\n", args);
2300 return;
2301 }
2302
2303 bool needsReplugBefore = core::UsbManager::instance().needsReplug();
2304
2305 reg.setModuleEnabled(idx, true);
2306 if (!reg.startModule(idx)) {
2307 switch (reg.classifyStartFailure(idx)) {
2309 Console::printf("ERROR: %s\r\n", reg.getModuleSlotError(idx));
2310 break;
2312 Console::printf("ERROR: No free USB slot - disable a USB module (e.g. GPG) first\r\n");
2313 break;
2315 Console::printf("ERROR: Failed to start module '%s'\r\n", args);
2316 break;
2317 }
2318 return;
2319 }
2320
2321 Console::printf("OK: Module '%s' enabled\r\n", args);
2322
2323 if (core::UsbManager::instance().newlyRequiresReplug(needsReplugBefore)) {
2324 Console::printf("NOTE: USB replug required\r\n");
2325 }
2326}
2327
2332static void cmdModuleDisable(const char* args) {
2333 if (!args || !*args) {
2334 Console::printf("Usage: MODULE DISABLE <name>\r\n");
2335 return;
2336 }
2337
2338 int index = findModuleIndex(args);
2339 if (index < 0) {
2340 Console::printf("ERROR: Module '%s' not found\r\n", args);
2341 return;
2342 }
2343
2344 auto& reg = core::ModuleRegistry::instance();
2345 uint8_t idx = static_cast<uint8_t>(index);
2346
2347 if (!reg.isModuleEnabled(idx)) {
2348 Console::printf("OK: Module '%s' already disabled\r\n", args);
2349 return;
2350 }
2351
2352 bool needsReplugBefore = core::UsbManager::instance().needsReplug();
2353
2354 reg.setModuleEnabled(idx, false);
2355 core::IModule* module = reg.getModuleAt(idx);
2356 if (module && module->getState() == core::ServiceState::STARTED) {
2357 module->stop();
2358 }
2359
2360 Console::printf("OK: Module '%s' disabled\r\n", args);
2361
2362 if (core::UsbManager::instance().newlyRequiresReplug(needsReplugBefore)) {
2363 Console::printf("NOTE: USB replug required\r\n");
2364 }
2365}
2366
2370
2371static const SubCommand kNvsSubs[] = {
2372 {"LIST", "[namespace]", "List entries (optional namespace filter)", cmdNvsList},
2373 {"READ", "<ns> <key>", "Read key value", cmdNvsRead},
2374 {"DEL", "<ns> [key]", "Delete key, or entire namespace if omitted",cmdNvsDel},
2375 {"CLEAR", "YES", "Erase entire NVS (confirmation required)", cmdNvsClear},
2376 {nullptr, nullptr, nullptr, nullptr},
2377};
2378static void cmdNvs(const char* args) { dispatchSubCommand("NVS", args, kNvsSubs); }
2379
2380static const SubCommand kPinSubs[] = {
2381 {"STATUS", "", "Show PIN retries / lockout state", cmdPinStatus},
2382 {"RESET", "", "Reset PIN retries (debug)", cmdPinReset},
2383 {"CHANGE", "<currentPin> <newPin>", "Change badge PIN (4-8 digits)", cmdPinChange},
2384 {"DURESS", "<pin>", "Arm self-destruct PIN (wipes on entry)", cmdPinDuress},
2385 {"DURESS_CLEAR", "", "Disarm the self-destruct PIN", cmdPinDuressClear},
2386 {nullptr, nullptr, nullptr, nullptr},
2387};
2388static void cmdPin(const char* args) { dispatchSubCommand("PIN", args, kPinSubs); }
2389
2390static const SubCommand kTr01Subs[] = {
2391 {"STATUS", "", "Show TR01 connection status", cmdTr01Status},
2392 {"INFO", "", "Show TR01 chip info (ID, firmware)", cmdTr01Info},
2393 {"SESSION", "", "Start/restart TR01 session", cmdTr01Session},
2394 {"SLOTS", "", "Show TR01 slot usage summary", cmdTr01Slots},
2395 {"RMEM_READ", "<slot>", "Read and dump R-Memory slot", cmdTr01RmemRead},
2396 {"ECC_DEL", "<slot>", "Delete ECC key slot", cmdTr01EccDel},
2397 {"RMEM_DEL", "<slot>", "Delete R-Memory slot", cmdTr01RmemDel},
2398 {"RESYNC", "", "Resync TR01 session and cache", cmdTr01Resync},
2399 {"CACHE_REBUILD", "", "Rebuild TR01 cache from chip", cmdTr01CacheRebuild},
2400 {"CLEANUP", "", "Cleanup mismatched slots and rebuild cache", cmdTr01Cleanup},
2401 {"WIPE", "CONFIRM", "Factory reset all TR01 data", cmdTr01Wipe},
2402 {nullptr, nullptr, nullptr, nullptr},
2403};
2404static void cmdTr01(const char* args) { dispatchSubCommand("TR01", args, kTr01Subs); }
2405
2406static const SubCommand kWifiSubs[] = {
2407 {"SCAN", "", "Scan for available networks", cmdWifiScan},
2408 {"STATUS", "", "Show WiFi state and saved configuration", cmdWifiStatus},
2409 {"ON", "[sta|ap|sta_ap]", "Enable WiFi radio (default STA, auto-reconnect)", cmdWifiOn},
2410 {"OFF", "", "Disable WiFi radio", cmdWifiOff},
2411 {"CONNECT", "<ssid> <password>", "Connect to network and persist credentials", cmdWifiConnect},
2412 {"TIMEOUT", "[ms]", "Get or set connect timeout (3000-60000 ms)", cmdWifiTimeout},
2413 {"FORGET", "", "Clear saved WiFi configuration", cmdWifiForget},
2414 {nullptr, nullptr, nullptr, nullptr},
2415};
2416static void cmdWifi(const char* args) { dispatchSubCommand("WIFI", args, kWifiSubs); }
2417
2418static const SubCommand kModuleSubs[] = {
2419 {"LIST", "", "List modules with state and errors", cmdModuleList},
2420 {"ENABLE", "<name>", "Enable a module (persistent)", cmdModuleEnable},
2421 {"DISABLE", "<name>", "Disable a module (persistent)", cmdModuleDisable},
2422 {nullptr, nullptr, nullptr, nullptr},
2423};
2424static void cmdModule(const char* args) { dispatchSubCommand("MODULE", args, kModuleSubs); }
2425
2430 auto& reg = getCommandRegistry();
2431
2432 reg.registerCommand({"HELP", "Show available commands", cmdHelp, "system", false});
2433 reg.registerCommand({"PING", "Check if device is responsive", cmdPing, "system", false});
2434 reg.registerCommand({"STATUS", "Show system status", cmdStatus, "system", false});
2435 reg.registerCommand({"MEM", "Show memory usage", cmdMem, "system", false});
2436 reg.registerCommand({"MEMINFO", "Show detailed memory + task info", cmdMemInfo, "system", false});
2437 reg.registerCommand({"CPU", "Measure aggregate CPU load (~250 ms)", cmdCpu, "system", false});
2438 reg.registerCommand({"ERROR_LOG", "Show error log (CLEAR to reset)", cmdErrorLog, "system", false});
2439 reg.registerCommand({"REBOOT", "Restart the device", cmdReboot, "system", true});
2440 reg.registerCommand({"BOOTLOADER", "Reboot into USB download mode", cmdBootloader, "system", true});
2441 reg.registerCommand({"SHIPMODE", "Enter ship mode (disconnect battery)", cmdShipMode, "system", true});
2442 reg.registerCommand({"PASTE", "Paste text into the active T9 input", cmdPaste, "system", true});
2443
2444 reg.registerCommand({"NVS", "NVS storage: LIST/READ/DEL/CLEAR", cmdNvs, "nvs", true, kNvsSubs});
2445
2446 reg.registerCommand({"GET_TIME", "Show current time", cmdGetTime, "time", false});
2447 reg.registerCommand({"GET_DATE", "Show current date", cmdGetDate, "time", false});
2448 reg.registerCommand({"SET_TIME", "Set time (HH:MM:SS)", cmdSetTime, "time", false});
2449 reg.registerCommand({"SET_DATE", "Set date (DD.MM.YYYY or Unix timestamp)", cmdSetDate, "time", false});
2450
2451 reg.registerCommand({"SET_NAME", "Set display name", cmdSetName, "display", false});
2452 reg.registerCommand({"SET_INFO", "Set info line 1", cmdSetInfo, "display", false});
2453 reg.registerCommand({"SET_INFO2", "Set info line 2", cmdSetInfo2, "display", false});
2454
2455 reg.registerCommand({"PIN", "PIN management: STATUS/RESET/CHANGE/DURESS", cmdPin, "pin", true, kPinSubs});
2456
2457 reg.registerCommand({"TR01", "TROPIC01 secure element: STATUS/INFO/SESSION/SLOTS/RMEM_*/ECC_DEL/...",
2458 cmdTr01, "tr01", true, kTr01Subs});
2459
2460#if FEATURE_SECURE_SERIAL
2461 reg.registerCommand({"AUTH", "Authenticate with PIN", cmdAuth, "auth", false});
2462 reg.registerCommand({"LOGOUT", "End authenticated session", cmdLogout, "auth", false});
2463#endif
2464
2465 reg.registerCommand({"WIFI", "WiFi control: SCAN/STATUS/ON/OFF/CONNECT/TIMEOUT/FORGET",
2466 cmdWifi, "wifi", true, kWifiSubs});
2467
2468 reg.registerCommand({"MODULE", "Module control: LIST/ENABLE/DISABLE",
2469 cmdModule, "module", true, kModuleSubs});
2470}
2471
2472} // namespace cdc::serial
static const char * TAG
Canonical CP437 <-> Unicode/UTF-8 codec.
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:112
void log_set_level(log_level_t level)
Sets runtime log verbosity threshold.
Definition cdc_log.cpp:153
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:428
void error_log_clear(void)
Clears error-log ring buffer state.
Definition cdc_log.cpp:104
#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:56
static void showPrompt()
Prints standard shell prompt.
Definition Console.cpp:98
static void flush()
Flushes pending console output.
Definition Console.cpp:82
static void printf(const char *format,...) __attribute__((format(printf
Prints formatted text to console.
Definition Console.cpp:30
static void putchar(char c)
Writes a single character to console.
Definition Console.cpp:66
static int getchar()
Reads one character from console input.
Definition Console.cpp:74
static void init()
Initializes console wrapper state.
Definition Console.cpp:19
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:34
bool setUserEnabled(bool enabled)
Sets the user/system WiFi intent and applies it immediately.
static WifiHandlers & instance()
Returns singleton Wi-Fi handlers instance.
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:52
static void cmdSetInfo2(const char *args)
Sets the second info line through the text callback.
static EscState s_escState
Definition SerialCmd.cpp:76
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:69
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:70
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:88
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:54
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:87
static constexpr size_t HISTORY_MAX
Internal constants used by serial command processing.
Definition SerialCmd.cpp:48
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:49
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:50
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:75
static constexpr int YEAR_MAX
Definition SerialCmd.cpp:53
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:82
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:61
static bool getCurrentTime(struct timeval &tv, struct tm &tm)
Date/time parsing and validation helpers.
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:60
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:68
static constexpr size_t NVS_NAMESPACE_MAX_LEN
Definition SerialCmd.cpp:51
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:13
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:81
static char s_historyBuffer[HISTORY_MAX][SerialCmd::CMD_BUFFER_SIZE]
Command history ring buffer allocated in PSRAM.
Definition SerialCmd.cpp:67
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
Slot parsing helpers for secure-element commands.