CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
VcardModule.cpp
Go to the documentation of this file.
4#include "cdc_msg/MessageTransfer.h"
9#include "cdc_core/EventBus.h"
10#include "cdc_core/Raii.h"
11#include "cdc_ui/BackupImport.h"
12#include "cJSON.h"
13#include "cdc_ui/I18n.h"
14#include "cdc_ui/ViewStack.h"
15#include "cdc_views/ListView.h"
16#include "cdc_views/InfoView.h"
20#include "esp_timer.h"
21#include "cdc_views/ToastView.h"
22#include "cdc_log.h"
23#include "esp_attr.h"
24#include "freertos/FreeRTOS.h"
25#include "freertos/semphr.h"
26#include <cstring>
27#include <cstdio>
28#include <cstdlib>
29
30static const char* TAG = "VCARD";
31
32namespace cdc::mod_vcard {
33
34constexpr ui::I18nEntry kStrings[] = {
35 {"mod_vcard.title", "vCards"},
36 {"mod_vcard.my_vcard", "My vCard"},
37 {"mod_vcard.nearby", "Nearby"},
38 {"mod_vcard.scan", "Start Scan"},
39 {"mod_vcard.stop_scan", "Stop Scan"},
40 {"mod_vcard.advertising", "Start Advertising"},
41 {"mod_vcard.stop_adv", "Stop Advertising"},
42 {"mod_vcard.exchange", "Exchange"},
43 {"mod_vcard.no_peers", "No peers found"},
44 {"mod_vcard.scanning", "Scanning..."},
45 {"mod_vcard.exchange_req", "Exchange Request"},
46 {"mod_vcard.accept", "Accept"},
47 {"mod_vcard.decline", "Decline"},
48 {"mod_vcard.exchange_ok", "Exchange successful"},
49 {"mod_vcard.exchange_fail", "Exchange failed"},
50 {"mod_vcard.connecting", "Connecting..."},
51 {"mod_vcard.edit_my_vcard", "Edit my vCard"},
52 {"mod_vcard.no_vcard", "No vCard set"},
53 {"mod_vcard.given_name", "First name"},
54 {"mod_vcard.family_name", "Last name"},
55 {"mod_vcard.formatted_name", "Display name"},
56 {"mod_vcard.organization", "Organization"},
57 {"mod_vcard.position", "Position"},
58 {"mod_vcard.email", "Email"},
59 {"mod_vcard.tel_cell", "Phone (Mobile)"},
60 {"mod_vcard.tel_home", "Phone (Home)"},
61 {"mod_vcard.tel_work", "Phone (Work)"},
62 {"mod_vcard.url", "Website"},
63 {"mod_vcard.telegram", "Telegram"},
64 {"mod_vcard.matrix", "Matrix"},
65 {"mod_vcard.threema", "Threema"},
66 {"mod_vcard.social_profile", "Social Profile"},
67 {"mod_vcard.note", "Note"},
68 {"mod_vcard.send", "Send vCard"},
69 {"mod_vcard.received", "Contact (vCard)"},
70 {"mod_vcard.received_title", "Received vCards"},
71 {"mod_vcard.no_received", "No received vCards"},
72 {"mod_vcard.show_qr", "Show QR"},
73 {"mod_vcard.forward", "Forward"},
74 {"mod_vcard.confirm_delete", "Delete this contact?"},
75};
76
77static const char* const s_wizardStepKeys[16] = {
78 "mod_vcard.given_name", "mod_vcard.family_name", "mod_vcard.formatted_name",
79 "mod_vcard.organization", "mod_vcard.position", "mod_vcard.email",
80 "mod_vcard.tel_cell", "mod_vcard.tel_home", "mod_vcard.tel_work",
81 "mod_vcard.url", "mod_vcard.telegram", "core.signal",
82 "mod_vcard.matrix", "mod_vcard.threema", "mod_vcard.social_profile",
83 "mod_vcard.note",
84};
85
89
94static bool s_viewsInitialized = false;
95
107
108// Received-contacts list view and its backing buffers (PSRAM).
110static bool s_receivedInitialized = false;
111static EXT_RAM_BSS_ATTR ui::ListItem s_recvItems[VCARD_MAX_CARDS];
112static EXT_RAM_BSS_ATTR char s_recvLabels[VCARD_MAX_CARDS][64];
114static uint16_t s_recvCount = 0;
115// Slot the context menu / confirm dialog currently acts on.
116static uint16_t s_activeSlot = 0;
117
118static void rebuildMainMenu();
119static void onMainMenuSelect(uint16_t index, void* userData);
120static void openReceivedList();
121static void rebuildReceivedList();
122static void showVcardDetails(const char* title, const char* raw, bool withActions);
123static void showVcardQr(const char* raw, const char* fallbackTitle);
124static void onReceivedViewMenu(void* userData);
125
134static bool deliverVcard(const uint8_t* data, uint32_t len, const char* /*mime*/,
135 const char* /*peerName*/) {
136 if (!data || len == 0 || len > VCARD_MAX_LEN) return false;
137 static EXT_RAM_BSS_ATTR char buf[VCARD_MAX_LEN + 1];
138 memcpy(buf, data, len);
139 buf[len] = '\0';
140 char err[64] = {0};
141 if (!vcard_store_add(buf, len, err, sizeof(err))) {
142 LOG_W(TAG, "Failed to store received vCard: %s", err);
143 return false;
144 }
145 return true;
146}
147
158static void showVcardDetails(const char* title, const char* raw, bool withActions) {
159 static EXT_RAM_BSS_ATTR vcard_data_t s_parsed;
160 static EXT_RAM_BSS_ATTR char s_text[ui::InfoView::MAX_TEXT_LEN];
161
162 memset(&s_parsed, 0, sizeof(s_parsed));
163 vcard_parse_to_struct(raw, &s_parsed);
164
165 int n = 0;
166 const int cap = static_cast<int>(sizeof(s_text));
167 auto append = [&](const char* fmt, const char* a, const char* b) {
168 if (n >= cap - 1) return;
169 int w = snprintf(s_text + n, static_cast<size_t>(cap - n), fmt, a, b);
170 if (w > 0) n += w;
171 if (n > cap - 1) n = cap - 1;
172 };
173
174 if (s_parsed.formatted_name[0]) {
175 append("%s%s\n\n", s_parsed.formatted_name, "");
176 } else if (s_parsed.given_name[0] || s_parsed.family_name[0]) {
177 append("%s %s\n\n", s_parsed.given_name, s_parsed.family_name);
178 }
179
180 auto field = [&](const char* labelKey, const char* value) {
181 if (value[0]) append("%s: %s\n", ui::tr(labelKey), value);
182 };
183 field("mod_vcard.organization", s_parsed.organization);
184 field("mod_vcard.position", s_parsed.title);
185 field("mod_vcard.email", s_parsed.email);
186 field("mod_vcard.tel_cell", s_parsed.tel_cell);
187 field("mod_vcard.tel_home", s_parsed.tel_home);
188 field("mod_vcard.tel_work", s_parsed.tel_work);
189 field("mod_vcard.url", s_parsed.url);
190 field("mod_vcard.telegram", s_parsed.impp_telegram);
191 field("core.signal", s_parsed.impp_signal);
192 field("mod_vcard.matrix", s_parsed.impp_matrix);
193 field("mod_vcard.threema", s_parsed.impp_threema);
194 field("mod_vcard.social_profile", s_parsed.social_profile);
195 field("mod_vcard.note", s_parsed.note);
196
197 if (n == 0) snprintf(s_text, sizeof(s_text), "%s", raw);
198
199 static ui::InfoView s_detailView;
200 s_detailView.init(title, s_text);
201 s_detailView.setOnMenu(withActions ? onReceivedViewMenu : nullptr);
202 ui::ViewStack::instance().push(&s_detailView);
203}
204
210static void showVcardQr(const char* raw, const char* fallbackTitle) {
211 static EXT_RAM_BSS_ATTR vcard_data_t s_parsed;
212 static char s_qrTitle[96];
213 static char s_qrSubtitle[96];
214
215 memset(&s_parsed, 0, sizeof(s_parsed));
216 vcard_parse_to_struct(raw, &s_parsed);
217
218 if (s_parsed.formatted_name[0]) {
219 snprintf(s_qrTitle, sizeof(s_qrTitle), "%s", s_parsed.formatted_name);
220 } else if (s_parsed.given_name[0] || s_parsed.family_name[0]) {
221 snprintf(s_qrTitle, sizeof(s_qrTitle), "%s %s",
222 s_parsed.given_name, s_parsed.family_name);
223 } else {
224 snprintf(s_qrTitle, sizeof(s_qrTitle), "%s", fallbackTitle);
225 }
226
227 const char* sub = s_parsed.organization[0] ? s_parsed.organization
228 : s_parsed.title[0] ? s_parsed.title
229 : s_parsed.email[0] ? s_parsed.email
230 : "";
231 snprintf(s_qrSubtitle, sizeof(s_qrSubtitle), "%s", sub);
232
233 ui::showQRCode(raw, s_qrTitle, s_qrSubtitle[0] ? s_qrSubtitle : nullptr);
234}
235
239static void rebuildMainMenu() {
240 s_mainMenuItems[MENU_MY_VCARD] = {ui::tr("mod_vcard.my_vcard"), 0, false, nullptr};
241 s_mainMenuItems[MENU_EDIT_MY_VCARD] = {ui::tr("mod_vcard.edit_my_vcard"), 0, false, nullptr};
242 s_mainMenuItems[MENU_SEND] = {ui::tr("mod_vcard.send"), 0, false, nullptr};
243 s_mainMenuItems[MENU_RECEIVED] = {ui::tr("mod_vcard.received_title"), 0, false, nullptr};
244 s_mainMenu.init(ui::tr("mod_vcard.title"), s_mainMenuItems, MENU_COUNT);
245}
246
252static void onMainMenuSelect(uint16_t index, void* userData) {
253 (void)userData;
254
255 switch (index) {
256 case MENU_MY_VCARD: {
257 static EXT_RAM_BSS_ATTR char vcardText[VCARD_MAX_LEN + 1];
258 size_t len = vcard_store_get_own(vcardText, sizeof(vcardText));
259 if (len > 0) {
260 showVcardDetails(ui::tr("mod_vcard.my_vcard"), vcardText, false);
261 } else {
262 ui::showToastInfo(ui::tr("mod_vcard.no_vcard"));
263 }
264 break;
265 }
266
268 if (vcard_store_has_own()) {
270 } else {
272 }
273 break;
274
275 case MENU_SEND: {
276 // Push our own card to a nearby badge; the framework owns the peer
277 // picker, consent, encryption and progress UI.
278 static EXT_RAM_BSS_ATTR char own[VCARD_MAX_LEN + 1];
279 size_t len = vcard_store_get_own(own, sizeof(own));
280 if (len == 0) {
281 ui::showToastInfo(ui::tr("mod_vcard.no_vcard"));
282 break;
283 }
284 cdc::msg::MessageTransfer::instance().beginInteractiveSend(
285 "text/vcard", reinterpret_cast<const uint8_t*>(own),
286 static_cast<uint32_t>(len));
287 break;
288 }
289
290 case MENU_RECEIVED:
292 break;
293 }
294}
295
296// ============================================================================
297// Received contacts: list, detail, context menu (view / QR / forward / delete).
298// ============================================================================
299
303static void rebuildReceivedList() {
305 for (uint16_t i = 0; i < s_recvCount; i++) {
306 uint16_t slot = s_recvSlots[i];
307 if (!vcard_store_get_display(slot, s_recvLabels[i], sizeof(s_recvLabels[i]))) {
308 s_recvLabels[i][0] = '\0';
309 }
310 s_recvItems[i] = {s_recvLabels[i], 0, false,
311 reinterpret_cast<void*>(static_cast<uintptr_t>(slot))};
312 }
313 s_receivedMenu.setEmptyText(ui::tr("mod_vcard.no_received"));
314 s_receivedMenu.init(ui::tr("mod_vcard.received_title"), s_recvItems, s_recvCount);
315}
316
322static void onReceivedSelect(uint16_t index, void* userData) {
323 (void)index;
324 s_activeSlot = static_cast<uint16_t>(reinterpret_cast<uintptr_t>(userData));
325 static EXT_RAM_BSS_ATTR char raw[VCARD_MAX_LEN + 1];
326 if (vcard_store_get(s_activeSlot, raw, sizeof(raw)) == 0) return;
327 showVcardDetails(ui::tr("mod_vcard.received_title"), raw, true);
328}
329
334
339
341static void ctxReceivedQr() {
342 static EXT_RAM_BSS_ATTR char raw[VCARD_MAX_LEN + 1];
343 if (vcard_store_get(s_activeSlot, raw, sizeof(raw)) == 0) return;
344 showVcardQr(raw, ui::tr("mod_vcard.received_title"));
345}
346
348static void ctxReceivedForward() {
349 static EXT_RAM_BSS_ATTR char raw[VCARD_MAX_LEN + 1];
350 size_t len = vcard_store_get(s_activeSlot, raw, sizeof(raw));
351 if (len == 0) return;
352 cdc::msg::MessageTransfer::instance().beginInteractiveSend(
353 "text/vcard", reinterpret_cast<const uint8_t*>(raw),
354 static_cast<uint32_t>(len));
355}
356
361static void onReceivedDeleteConfirm(void* userData) {
362 uint16_t slot = *static_cast<uint16_t*>(userData);
363 if (vcard_store_delete(slot)) {
364 ui::showToastSuccess(ui::tr("core.deleted"));
365 s_receivedMenu.preservePosition();
368 } else {
369 ui::showToastError(ui::tr("core.failed"));
370 }
371}
372
374static void ctxReceivedDelete() {
375 ui::showConfirm(ui::tr("mod_vcard.confirm_delete"), onReceivedDeleteConfirm, nullptr,
377}
378
383static void onReceivedViewMenu(void* userData) {
384 (void)userData;
385 const ui::ContextMenuItem items[] = {
386 {ui::tr("core.edit"), ctxReceivedEdit},
387 {ui::tr("mod_vcard.forward"), ctxReceivedForward},
388 {ui::tr("mod_vcard.show_qr"), ctxReceivedQr},
389 {ui::tr("core.delete"), ctxReceivedDelete},
390 };
391 ui::showContextMenu(ui::tr("core.actions"), items, 4);
392}
393
400static void onReceivedMenu(uint16_t index, void* userData) {
401 (void)index;
402 ui::ContextMenuItem items[4] = {};
403 uint8_t n = 0;
404 items[n++] = {ui::tr("core.add"), ctxReceivedAdd};
405 if (s_recvCount > 0) {
406 s_activeSlot = static_cast<uint16_t>(reinterpret_cast<uintptr_t>(userData));
407 items[n++] = {ui::tr("core.edit"), ctxReceivedEdit};
408 items[n++] = {ui::tr("mod_vcard.forward"), ctxReceivedForward};
409 items[n++] = {ui::tr("core.delete"), ctxReceivedDelete};
410 }
411 ui::showContextMenu(ui::tr("core.actions"), items, n);
412}
413
426
427// ============================================================================
428// Lock-screen quick action: show own vCard as a QR code.
429// ============================================================================
430
434static const char* getMyVcardLockscreenLabel() {
435 return ui::tr("mod_vcard.my_vcard");
436}
437
443 static EXT_RAM_BSS_ATTR char s_qrBuf[VCARD_MAX_LEN + 1];
444 size_t len = vcard_store_get_own(s_qrBuf, sizeof(s_qrBuf));
445 if (len == 0) {
446 ui::showToastError(ui::tr("mod_vcard.no_vcard"));
447 return;
448 }
449 showVcardQr(s_qrBuf, ui::tr("mod_vcard.my_vcard"));
450}
451
452// ============================================================================
453// Serial Commands (VCARD_SET / VCARD_GET / VCARD_DELETE)
454// ============================================================================
455
456EXT_RAM_BSS_ATTR static char s_vcardBuf[VCARD_MAX_LEN + 64];
457static int s_vcardBufPos = 0;
458static bool s_vcardInputMode = false;
459// Paste target: -1 sets the own card, otherwise the received slot to overwrite.
460static int s_vcardSetSlot = -1;
461static esp_timer_handle_t s_vcardIdleTimer = nullptr;
462// Cancel a stalled paste session after this many seconds of inactivity so a
463// crashed/interrupted client cannot lock the serial console forever.
464static constexpr int64_t VCARD_IDLE_LIMIT_US = 30 * 1000000LL;
465
466static void vcard_session_clear() {
467 s_vcardInputMode = false;
468 s_vcardBufPos = 0;
469 s_vcardSetSlot = -1;
470 memset(s_vcardBuf, 0, sizeof(s_vcardBuf));
472 if (s_vcardIdleTimer) esp_timer_stop(s_vcardIdleTimer);
473}
474
475static void vcard_idle_fired(void*) {
476 if (!s_vcardInputMode) return;
477 serial::Console::printf("\r\nERROR: vCard paste timed out\r\n");
480}
481
482static void vcard_arm_idle_timer() {
483 if (!s_vcardIdleTimer) {
484 esp_timer_create_args_t args = {
485 .callback = vcard_idle_fired,
486 .arg = nullptr,
487 .dispatch_method = ESP_TIMER_TASK,
488 .name = "vcard_idle",
489 .skip_unhandled_events = true,
490 };
491 esp_timer_create(&args, &s_vcardIdleTimer);
492 }
493 esp_timer_stop(s_vcardIdleTimer);
494 esp_timer_start_once(s_vcardIdleTimer, VCARD_IDLE_LIMIT_US);
495}
496
502static bool vcardLineInterceptor(const char* line) {
503 if (!s_vcardInputMode) return false;
504
505 using Console = serial::Console;
507
508 if (strncmp(line, "---", 3) == 0) {
510
511 char err[64] = {};
512 bool ok = (s_vcardSetSlot >= 0)
513 ? vcard_store_update(static_cast<uint16_t>(s_vcardSetSlot), s_vcardBuf,
514 static_cast<size_t>(s_vcardBufPos), err, sizeof(err))
515 : vcard_store_set_own(s_vcardBuf, static_cast<size_t>(s_vcardBufPos), err, sizeof(err));
516 if (ok) {
517 Console::printf("OK: vCard updated\r\n");
518 } else {
519 Console::printf("ERROR: %s\r\n", err[0] ? err : "Invalid vCard");
520 }
521
523 Console::showPrompt();
524 return true;
525 }
526
527 if (strcmp(line, "ABORT") == 0 || strcmp(line, "VCARD_ABORT") == 0) {
528 Console::printf("ABORTED\r\n");
530 Console::showPrompt();
531 return true;
532 }
533
534 size_t lineLen = strlen(line);
535 if (s_vcardBufPos + static_cast<int>(lineLen) + 2 < static_cast<int>(sizeof(s_vcardBuf))) {
536 memcpy(s_vcardBuf + s_vcardBufPos, line, lineLen);
537 s_vcardBufPos += static_cast<int>(lineLen);
538 s_vcardBuf[s_vcardBufPos++] = '\n';
539 } else {
540 Console::printf("ERROR: vCard too large\r\n");
542 Console::showPrompt();
543 }
544 return true;
545}
546
554static void cmdVcardSet(const char* args) {
555 using Console = serial::Console;
556
557 s_vcardSetSlot = -1;
558 if (args && *args) {
559 int slot = atoi(args);
560 char probe[2];
561 if (slot < 0 || slot >= VCARD_MAX_CARDS ||
562 !vcard_store_get_display(static_cast<uint16_t>(slot), probe, sizeof(probe))) {
563 Console::printf("ERROR: no vCard at id %d\r\n", slot);
564 return;
565 }
566 s_vcardSetSlot = slot;
567 }
568
569 Console::printf("Paste vCard 4.0, end with '---' on a new line "
570 "(or 'ABORT' to cancel):\r\n");
571 s_vcardBufPos = 0;
572 s_vcardInputMode = true;
575}
576
581static void vcardPrintLines(char* out) {
582 using Console = serial::Console;
583 char* line = out;
584 char* next;
585 while ((next = strchr(line, '\n')) != nullptr) {
586 *next = '\0';
587 Console::printf("%s\r\n", line);
588 line = next + 1;
589 }
590 if (*line) {
591 Console::printf("%s\r\n", line);
592 }
593}
594
603static void cmdVcardGet(const char* args) {
604 using Console = serial::Console;
605 char out[VCARD_MAX_LEN + 1];
606
607 if (args && *args) {
608 int slot = atoi(args);
609 if (slot < 0 || slot >= VCARD_MAX_CARDS ||
610 vcard_store_get(static_cast<uint16_t>(slot), out, sizeof(out)) == 0) {
611 Console::printf("ERROR: no vCard at id %d\r\n", slot);
612 return;
613 }
614 vcardPrintLines(out);
615 return;
616 }
617
618 size_t len = vcard_store_get_own(out, sizeof(out));
619 if (len == 0) {
620 Console::printf("BEGIN:VCARD\r\n");
621 Console::printf("VERSION:4.0\r\n");
622 Console::printf("N:;;\r\n");
623 Console::printf("FN:\r\n");
624 Console::printf("NOTE:\r\n");
625 Console::printf("TEL;TYPE=HOME:\r\n");
626 Console::printf("TEL;TYPE=CELL:\r\n");
627 Console::printf("TEL;TYPE=WORK:\r\n");
628 Console::printf("EMAIL:\r\n");
629 Console::printf("URL:\r\n");
630 Console::printf("ORG:\r\n");
631 Console::printf("TITLE:\r\n");
632 Console::printf("X-SOCIALPROFILE:\r\n");
633 Console::printf("IMPP:telegram:\r\n");
634 Console::printf("IMPP:signal:\r\n");
635 Console::printf("IMPP:matrix:\r\n");
636 Console::printf("IMPP:threema:\r\n");
637 Console::printf("END:VCARD\r\n");
638 return;
639 }
640
641 vcardPrintLines(out);
642}
643
648static void cmdVcardList(const char* args) {
649 (void)args;
650 using Console = serial::Console;
651
652 uint16_t slots[VCARD_MAX_CARDS];
653 uint16_t count = vcard_store_get_sorted(slots, VCARD_MAX_CARDS);
654 if (count == 0) {
655 Console::printf("No received vCards\r\n");
656 return;
657 }
658
659 char name[64];
660 for (uint16_t i = 0; i < count; i++) {
661 if (!vcard_store_get_display(slots[i], name, sizeof(name))) name[0] = '\0';
662 Console::printf("%2u %s\r\n", slots[i], name);
663 }
664}
665
673static void cmdVcardDelete(const char* args) {
674 using Console = serial::Console;
675
676 if (args && *args) {
677 int slot = atoi(args);
678 if (slot < 0 || slot >= VCARD_MAX_CARDS ||
679 !vcard_store_delete(static_cast<uint16_t>(slot))) {
680 Console::printf("ERROR: no vCard at id %d\r\n", slot);
681 return;
682 }
683 Console::printf("OK: vCard %d deleted\r\n", slot);
684 return;
685 }
686
687 if (vcard_store_clear_own()) {
688 Console::printf("OK: vCard deleted\r\n");
689 } else {
690 Console::printf("ERROR: Failed to delete vCard\r\n");
691 }
692}
693
695 {"SET", "[id]", "Set own vCard, or overwrite received vCard <id> (multiline paste, end with '---' or 'ABORT')", cmdVcardSet},
696 {"GET", "[id]", "Show own vCard, or received vCard <id>", cmdVcardGet},
697 {"LIST", "", "List received vCards", cmdVcardList},
698 {"DELETE", "[id]", "Delete own vCard, or received vCard <id>", cmdVcardDelete},
699 {nullptr, nullptr, nullptr, nullptr},
700};
701
702static void cmdVcard(const char* args) {
704}
705
710 auto& reg = serial::getCommandRegistry();
711 reg.registerCommand({"VCARD",
712 "vCard storage: SET/GET/LIST/DELETE",
713 cmdVcard, "vcard", false, kVcardSubs});
714}
715
716// ============================================================================
717// Module Implementation
718// ============================================================================
719
725 static VcardModule inst;
726 return inst;
727}
728
734 LOG_I(TAG, "Initializing vCard module");
737
738 VcardWizard::configure(s_wizardStepKeys, "core.saved", "mod_vcard.exchange_fail");
739
740 // Register as the handler for incoming "text/vcard" message transfers.
741 cdc::msg::MessageTransfer::instance().registerHandler(
742 "text/vcard", "mod_vcard.received", deliverVcard);
743
746 return true;
747}
748
755 return false;
756 }
758 return true;
759}
760
765 cdc::msg::MessageTransfer::instance().unregisterHandler("text/vcard");
767}
768
775uint8_t VcardModule::getMenuItems(core::ModuleMenuItem* items, uint8_t maxItems) {
776 if (!items || maxItems == 0) return 0;
777
778 items[0] = {
779 ui::tr("mod_vcard.title"),
780 110,
781 []() -> ui::IView* {
782 if (!s_viewsInitialized) {
783 s_mainMenu.setOnSelect(onMainMenuSelect);
784 s_viewsInitialized = true;
785 }
787 return &s_mainMenu;
788 },
789 nullptr,
790 getName(),
792 nullptr
793 };
794
795 return 1;
796}
797
805 if (!items || maxItems == 0) return 0;
806 items[0] = {
809 60,
810 nullptr,
811 };
812 return 1;
813}
814
819void VcardModule::onTick(uint32_t nowMs) {
820 (void)nowMs;
821}
822
824static constexpr int kSchemaVer = 1;
825
836 if (!out) return false;
837
838 cJSON_AddNumberToObject(out, "schema_ver", kSchemaVer);
839
840 bool any = false;
841
842 char buf[VCARD_MAX_LEN + 1];
843 if (vcard_store_get_own(buf, sizeof(buf)) > 0) {
844 cJSON_AddStringToObject(out, "own", buf);
845 any = true;
846 }
847
848 cJSON* received = cJSON_AddArrayToObject(out, "received");
849 if (!received) return any;
850
851 uint16_t slots[VCARD_MAX_CARDS];
852 uint16_t count = vcard_store_get_sorted(slots, VCARD_MAX_CARDS);
853 for (uint16_t i = 0; i < count; i++) {
854 if (vcard_store_get(slots[i], buf, sizeof(buf)) == 0) continue;
855 cJSON* item = cJSON_CreateString(buf);
856 if (!item) continue;
857 cJSON_AddItemToArray(received, item);
858 any = true;
859 }
860
861 return any;
862}
863
875static bool importReceivedVcard(const cJSON* je, void* user) {
876 (void)user;
877 if (!cJSON_IsString(je) || !je->valuestring || je->valuestring[0] == '\0') return false;
878
879 const char* text = je->valuestring;
880 size_t len = strlen(text);
881 char err[32] = {};
882 if (vcard_store_add(text, len, err, sizeof(err))) return true;
883
884 // An already-present card means the identity is satisfied (no-op upsert);
885 // only genuine validation/storage failures count as failed.
886 return vcard_store_contains(text, len);
887}
888
899 if (!in) return {};
900
901 const cJSON* schemaVer = cJSON_GetObjectItemCaseSensitive(in, "schema_ver");
902 if (cJSON_IsNumber(schemaVer) && static_cast<int>(schemaVer->valuedouble) != kSchemaVer) {
903 LOG_W(TAG, "vCard backup schema_ver %d != expected %d, skipping",
904 static_cast<int>(schemaVer->valuedouble), kSchemaVer);
905 return {};
906 }
907
908 core::IModule::BackupResult result = {};
909
910 const cJSON* own = cJSON_GetObjectItemCaseSensitive(in, "own");
911 if (cJSON_IsString(own) && own->valuestring && own->valuestring[0] != '\0') {
912 char err[32] = {};
913 if (vcard_store_set_own(own->valuestring, strlen(own->valuestring), err, sizeof(err))) {
914 result.imported++;
915 } else {
916 LOG_W(TAG, "vCard import: own vCard rejected (%s)", err);
917 result.failed++;
918 }
919 }
920
921 const cJSON* received = cJSON_GetObjectItemCaseSensitive(in, "received");
923 result.imported = static_cast<uint16_t>(result.imported + rx.imported);
924 result.failed = static_cast<uint16_t>(result.failed + rx.failed);
925
926 return result;
927}
928
929} // namespace cdc::mod_vcard
930
934extern "C" void mod_vcard_register() {
936 auto& module = cdc::mod_vcard::VcardModule::instance();
937 module.init();
938 });
939}
static const char * TAG
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
char name[cdc::hal::ISecureElement::RMEM_NAME_LEN]
void mod_vcard_register()
Registers vCard module initializer in global module registry.
Shared RAII wrappers for firmware resources.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
bool registerModule(IModule *module)
Registers a module instance in the runtime registry.
static ModuleRegistry & instance()
Returns the singleton module registry instance.
void registerInitializer(ModuleInitFunc initFunc)
Registers a deferred module initializer callback.
void onTick(uint32_t nowMs) override
Periodic vCard module tick forwarding BLE state machine.
void stop() override
Stops vCard BLE service and module runtime.
bool init() override
Initializes module UI strings, serial commands, and BLE service hooks.
static VcardModule & instance()
Returns singleton vCard module instance.
bool exportBackup(cJSON *out) override
Exports the own vCard and all received vCards into the backup section.
uint8_t getMenuItems(core::ModuleMenuItem *items, uint8_t maxItems) override
Provides tools-menu entry for vCard module.
bool start() override
Starts vCard module service.
core::IModule::BackupResult importBackup(const cJSON *in) override
Restores the own vCard and received vCards from the backup section.
const char * getName() const override
Definition VcardModule.h:13
uint8_t getLockScreenContextItems(core::LockScreenContextItem *items, uint8_t maxItems) override
Provides the lock-screen quick action that shows the owner vCard as a QR code.
static void start(ui::IView *returnAnchor)
Starts the wizard with an empty struct.
static void editReceived(ui::IView *returnAnchor, uint16_t slot, DoneCallback onDone)
Starts the wizard prefilled with a stored contact for editing.
static void configure(const char *const *titleKeys, const char *savedKey, const char *failedKey)
Configures the wizard with i18n keys. Must be called before start() or edit() so step titles can be l...
static void startReceived(ui::IView *returnAnchor, DoneCallback onDone)
Starts the wizard to create a new stored contact (received list).
static void edit(ui::IView *returnAnchor)
Starts the wizard prefilled with the currently stored own vCard. Falls back to start() when no vCard ...
static void showPrompt()
Prints standard shell prompt.
Definition Console.cpp:120
static void printf(const char *format,...) __attribute__((format(printf
Prints formatted text to console.
Definition Console.cpp:32
virtual void setLineInterceptor(LineInterceptor interceptor)
static I18n & instance()
Singleton accessor.
Definition I18n.cpp:306
void registerEnglishTable(const I18nEntry *entries, std::size_t count)
Append English entries to the lookup table.
Definition I18n.cpp:326
void setOnMenu(MenuCallback onMenu, void *userData=nullptr)
Definition InfoView.h:54
static constexpr uint16_t MAX_TEXT_LEN
Definition InfoView.h:22
void init(const char *title, const char *text)
Definition InfoView.cpp:66
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:53
void popToAnchor(IView *anchor)
Pops views until the specified anchor view is the current view.
void push(IView *view, void *context=nullptr)
static constexpr int kSchemaVer
Schema version written to and expected from the vCard backup section.
static void vcard_session_clear()
static bool deliverVcard(const uint8_t *data, uint32_t len, const char *, const char *)
Delivers a received vCard into the contact store.
static void onReceivedSelect(uint16_t index, void *userData)
Opens the detail view (with action menu) for the selected contact.
static void ctxReceivedQr()
Context-menu action: show the active contact as a QR code.
static void onReceivedDeleteConfirm(void *userData)
Confirm-dialog handler: deletes the contact and refreshes the list.
static constexpr int64_t VCARD_IDLE_LIMIT_US
static void cmdVcardGet(const char *args)
Serial command printing a stored vCard.
static void ctxReceivedAdd()
Context-menu action: create a new stored contact via the wizard.
static void onMainMenuSelect(uint16_t index, void *userData)
Handles main-menu actions for local vCard operations and sharing.
static void rebuildMainMenu()
Rebuilds the vCard main menu.
static int s_vcardSetSlot
static void ctxReceivedDelete()
Context-menu action: confirm and delete the active contact.
MainMenuItem
Main-menu item identifiers.
static void onMyVcardLockscreenSelect()
Lock-screen quick action: shows the own vCard as a QR code. Falls back to a toast when no vCard has b...
static ui::ListItem s_recvItems[VCARD_MAX_CARDS]
static ui::ListView s_mainMenu
View instances used by vCard module UI flow.
static void cmdVcard(const char *args)
static esp_timer_handle_t s_vcardIdleTimer
static void registerSerialCommands()
Registers serial commands exposed by vCard module.
static char s_vcardBuf[VCARD_MAX_LEN+64]
static void onReceivedMenu(uint16_t index, void *userData)
List context menu (key 3): add a contact; for a selected entry also edit / forward / delete it.
static void vcardPrintLines(char *out)
Prints a NUL-terminated vCard buffer line by line as CRLF output.
static bool importReceivedVcard(const cJSON *je, void *user)
Imports one received vCard string into storage.
static ui::ListView s_receivedMenu
static void showVcardQr(const char *raw, const char *fallbackTitle)
Shows a vCard as a QR code, titled with the contact name.
static void registerStrings()
static void openReceivedList()
Lazily wires callbacks, rebuilds and pushes the received-contacts list.
static bool vcardLineInterceptor(const char *line)
Intercepts multiline vCard paste input, accumulates lines, and stops at "---".
static void ctxReceivedEdit()
Context-menu action: edit the active stored contact via the wizard.
static uint16_t s_recvCount
static void cmdVcardDelete(const char *args)
Serial command deleting a stored vCard.
constexpr ui::I18nEntry kStrings[]
static const char *const s_wizardStepKeys[16]
static const serial::SubCommand kVcardSubs[]
static uint16_t s_recvSlots[VCARD_MAX_CARDS]
static bool s_receivedInitialized
static const char * getMyVcardLockscreenLabel()
Returns the localized label for the lock-screen quick action.
static void cmdVcardSet(const char *args)
Serial command entering multiline vCard paste mode.
static void vcard_arm_idle_timer()
static ui::ListItem s_mainMenuItems[MENU_COUNT]
static void showVcardDetails(const char *title, const char *raw, bool withActions)
Renders a vCard's parsed fields into a scrollable InfoView.
static bool s_vcardInputMode
static bool s_viewsInitialized
static void cmdVcardList(const char *args)
Serial command listing received vCards as "<id> <display name>".
static void onReceivedViewMenu(void *userData)
Detail-view context menu (key 3): edit / forward / QR / delete the currently shown contact.
static uint16_t s_activeSlot
static void ctxReceivedForward()
Context-menu action: forward the active contact to a nearby badge.
static void vcard_idle_fired(void *)
static char s_recvLabels[VCARD_MAX_CARDS][64]
static int s_vcardBufPos
static void rebuildReceivedList()
Rebuilds the received-contacts list from the store (sorted by name).
ICommandRegistry & getCommandRegistry()
Returns singleton command-registry interface.
void dispatchSubCommand(const char *parent, const char *args, const SubCommand *table)
Routes a sub-command line to its handler.
Definition SubCommand.h:73
const char * tr(const char *key)
Look up a translation by string key.
Definition I18n.h:209
cdc::core::IModule::BackupResult importJsonArray(const cJSON *array, BackupEntryHandler handler, void *user)
Iterates a JSON backup array best-effort and tallies the outcome.
void showConfirm(const char *message, ConfirmView::ConfirmCallback onConfirm, ConfirmView::CancelCallback onCancel=nullptr, ConfirmView::Icon icon=ConfirmView::Icon::QUESTION, void *userData=nullptr)
Shows a shared modal confirmation dialog instance.
ContextMenuView * showContextMenu(const char *title, const ContextMenuItem *items, uint8_t count)
Shows the shared context menu instance as modal.
void showToastSuccess(const char *message, uint16_t durationMs=1500)
Shows a success toast message.
QRCodeView * showQRCode(const char *data, const char *title=nullptr, const char *subtitle=nullptr, const char *hint=nullptr)
Shows a shared QR code view instance.
void showToastInfo(const char *message, uint16_t durationMs=1500)
Shows an informational toast message.
void showToastError(const char *message, uint16_t durationMs=1500)
Shows an error toast message.
Per-module restore outcome reported by importBackup().
Definition IModule.h:85
uint16_t failed
Records skipped due to errors.
Definition IModule.h:87
uint16_t imported
Records restored successfully.
Definition IModule.h:86
Lock screen context menu item registered by a module.
Definition IModule.h:42
Menu item registered by a module.
Definition IModule.h:29
Single English translation entry.
Definition I18n.h:44
Structured representation of an own vCard for editor/wizard use.
Definition vcard_store.h:15
size_t vcard_store_get_own(char *out, size_t max_len)
Retrieves local own-vCard text.
bool vcard_store_has_own(void)
Returns whether local own-vCard exists.
#define VCARD_MAX_CARDS
Definition vcard_store.h:7
bool vcard_store_set_own(const char *vcard, size_t len, char *err, size_t err_len)
Stores local own-vCard after validation and field filtering.
bool vcard_store_update(uint16_t slot, const char *vcard, size_t len, char *err, size_t err_len)
Overwrites the vCard stored at slot in place after validation.
size_t vcard_store_get(uint16_t slot, char *out, size_t max_len)
Retrieves raw vCard text from slot.
bool vcard_store_clear_own(void)
Deletes local own-vCard from storage.
bool vcard_parse_to_struct(const char *raw, vcard_data_t *out)
Parses vCard 4.0 raw text into a structured vcard_data_t.
bool vcard_store_add(const char *vcard, size_t len, char *err, size_t err_len)
Adds peer vCard to first free slot after validation and duplicate check.
uint16_t vcard_store_get_sorted(uint16_t *out_slots, uint16_t max_slots)
Returns slot indices of stored cards sorted by last name.
#define VCARD_MAX_LEN
Definition vcard_store.h:6
bool vcard_store_contains(const char *vcard, size_t len)
Reports whether an exact-text vCard is already stored.
bool vcard_store_get_display(uint16_t slot, char *out, size_t max_len)
Retrieves cached display label for slot.
bool vcard_store_delete(uint16_t slot)
Deletes peer vCard at slot index.