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"}, // 0 STR_VCARD
36 {"mod_vcard.my_vcard", "My vCard"}, // 1 STR_MY_VCARD
37 {"mod_vcard.nearby", "Nearby"}, // 2 STR_NEARBY
38 {"mod_vcard.scan", "Start Scan"}, // 3 STR_SCAN
39 {"mod_vcard.stop_scan", "Stop Scan"}, // 4 STR_STOP_SCAN
40 {"mod_vcard.advertising", "Start Advertising"},// 5 STR_ADVERTISING
41 {"mod_vcard.stop_adv", "Stop Advertising"},// 6 STR_STOP_ADV
42 {"mod_vcard.exchange", "Exchange"}, // 7 STR_EXCHANGE
43 {"mod_vcard.no_peers", "No peers found"}, // 8 STR_NO_PEERS
44 {"mod_vcard.scanning", "Scanning..."}, // 9 STR_SCANNING
45 {"mod_vcard.exchange_req", "Exchange Request"},// 10 STR_EXCHANGE_REQ
46 {"mod_vcard.accept", "Accept"}, // 11 STR_ACCEPT
47 {"mod_vcard.decline", "Decline"}, // 12 STR_DECLINE
48 {"mod_vcard.exchange_ok", "Exchange successful"},// 13 STR_EXCHANGE_OK
49 {"mod_vcard.exchange_fail", "Exchange failed"}, // 14 STR_EXCHANGE_FAIL
50 {"mod_vcard.connecting", "Connecting..."}, // 15 STR_CONNECTING
51 {"mod_vcard.edit_my_vcard", "Edit my vCard"}, // 16 STR_EDIT_MY_VCARD
52 {"mod_vcard.no_vcard", "No vCard set"}, // 17 STR_NO_VCARD
53 {"mod_vcard.saved", "Saved"}, // 18 STR_SAVED
54 {"mod_vcard.given_name", "First name"}, // 19 STR_GIVEN_NAME
55 {"mod_vcard.family_name", "Last name"}, // 20 STR_FAMILY_NAME
56 {"mod_vcard.formatted_name", "Display name"}, // 21 STR_FORMATTED_NAME
57 {"mod_vcard.organization", "Organization"}, // 22 STR_ORGANIZATION
58 {"mod_vcard.position", "Position"}, // 23 STR_POSITION
59 {"mod_vcard.email", "Email"}, // 24 STR_EMAIL
60 {"mod_vcard.tel_cell", "Phone (Mobile)"}, // 25 STR_TEL_CELL
61 {"mod_vcard.tel_home", "Phone (Home)"}, // 26 STR_TEL_HOME
62 {"mod_vcard.tel_work", "Phone (Work)"}, // 27 STR_TEL_WORK
63 {"mod_vcard.url", "Website"}, // 28 STR_URL
64 {"mod_vcard.telegram", "Telegram"}, // 29 STR_TELEGRAM
65 {"mod_vcard.signal", "Signal"}, // 30 STR_SIGNAL
66 {"mod_vcard.matrix", "Matrix"}, // 31 STR_MATRIX
67 {"mod_vcard.threema", "Threema"}, // 32 STR_THREEMA
68 {"mod_vcard.social_profile", "Social Profile"}, // 33 STR_SOCIAL_PROFILE
69 {"mod_vcard.note", "Note"}, // 34 STR_NOTE
70 {"mod_vcard.send", "Send vCard"}, // 35 STR_SEND
71 {"mod_vcard.received", "Contact (vCard)"}, // 36 STR_RECEIVED
72 {"mod_vcard.received_title", "Received vCards"}, // 37 STR_RECEIVED_TITLE
73 {"mod_vcard.no_received", "No received vCards"},// 38 STR_NO_RECEIVED
74 {"mod_vcard.show_qr", "Show QR"}, // 39 STR_SHOW_QR
75 {"mod_vcard.forward", "Forward"}, // 40 STR_FORWARD
76 {"mod_vcard.confirm_delete", "Delete this contact?"},// 41 STR_CONFIRM_DELETE
77};
78
79// Numeric offsets retained because VcardWizard takes an offset-array + resolver.
80static constexpr uint16_t STR_VCARD = 0;
81static constexpr uint16_t STR_MY_VCARD = 1;
82static constexpr uint16_t STR_NEARBY = 2;
83static constexpr uint16_t STR_SCAN = 3;
84static constexpr uint16_t STR_STOP_SCAN = 4;
85static constexpr uint16_t STR_ADVERTISING = 5;
86static constexpr uint16_t STR_STOP_ADV = 6;
87static constexpr uint16_t STR_EXCHANGE = 7;
88static constexpr uint16_t STR_NO_PEERS = 8;
89static constexpr uint16_t STR_SCANNING = 9;
90static constexpr uint16_t STR_EXCHANGE_REQ = 10;
91static constexpr uint16_t STR_ACCEPT = 11;
92static constexpr uint16_t STR_DECLINE = 12;
93static constexpr uint16_t STR_EXCHANGE_OK = 13;
94static constexpr uint16_t STR_EXCHANGE_FAIL = 14;
95static constexpr uint16_t STR_CONNECTING = 15;
96static constexpr uint16_t STR_EDIT_MY_VCARD = 16;
97static constexpr uint16_t STR_NO_VCARD = 17;
98static constexpr uint16_t STR_SAVED = 18;
99static constexpr uint16_t STR_GIVEN_NAME = 19;
100static constexpr uint16_t STR_FAMILY_NAME = 20;
101static constexpr uint16_t STR_FORMATTED_NAME = 21;
102static constexpr uint16_t STR_ORGANIZATION = 22;
103static constexpr uint16_t STR_POSITION = 23;
104static constexpr uint16_t STR_EMAIL = 24;
105static constexpr uint16_t STR_TEL_CELL = 25;
106static constexpr uint16_t STR_TEL_HOME = 26;
107static constexpr uint16_t STR_TEL_WORK = 27;
108static constexpr uint16_t STR_URL = 28;
109static constexpr uint16_t STR_TELEGRAM = 29;
110static constexpr uint16_t STR_SIGNAL = 30;
111static constexpr uint16_t STR_MATRIX = 31;
112static constexpr uint16_t STR_THREEMA = 32;
113static constexpr uint16_t STR_SOCIAL_PROFILE = 33;
114static constexpr uint16_t STR_NOTE = 34;
115static constexpr uint16_t STR_SEND = 35;
116static constexpr uint16_t STR_RECEIVED = 36;
117static constexpr uint16_t STR_RECEIVED_TITLE = 37;
118static constexpr uint16_t STR_NO_RECEIVED = 38;
119static constexpr uint16_t STR_SHOW_QR = 39;
120static constexpr uint16_t STR_FORWARD = 40;
121static constexpr uint16_t STR_CONFIRM_DELETE = 41;
122
129
130static const char* mstr(uint16_t offset) {
131 if (offset >= std::size(kStrings)) return "?";
132 return ui::tr(kStrings[offset].key);
133}
134
138
143static bool s_viewsInitialized = false;
144
156
157// Received-contacts list view and its backing buffers (PSRAM).
159static bool s_receivedInitialized = false;
160static EXT_RAM_BSS_ATTR ui::ListItem s_recvItems[VCARD_MAX_CARDS];
161static EXT_RAM_BSS_ATTR char s_recvLabels[VCARD_MAX_CARDS][64];
163static uint16_t s_recvCount = 0;
164// Slot the context menu / confirm dialog currently acts on.
165static uint16_t s_activeSlot = 0;
166
167static void rebuildMainMenu();
168static void onMainMenuSelect(uint16_t index, void* userData);
169static void openReceivedList();
170static void rebuildReceivedList();
171static void showVcardDetails(const char* title, const char* raw, bool withActions);
172static void showVcardQr(const char* raw, const char* fallbackTitle);
173static void onReceivedViewMenu(void* userData);
174
183static bool deliverVcard(const uint8_t* data, uint32_t len, const char* /*mime*/,
184 const char* /*peerName*/) {
185 if (!data || len == 0 || len > VCARD_MAX_LEN) return false;
186 static EXT_RAM_BSS_ATTR char buf[VCARD_MAX_LEN + 1];
187 memcpy(buf, data, len);
188 buf[len] = '\0';
189 char err[64] = {0};
190 if (!vcard_store_add(buf, len, err, sizeof(err))) {
191 LOG_W(TAG, "Failed to store received vCard: %s", err);
192 return false;
193 }
194 return true;
195}
196
207static void showVcardDetails(const char* title, const char* raw, bool withActions) {
208 static EXT_RAM_BSS_ATTR vcard_data_t s_parsed;
209 static EXT_RAM_BSS_ATTR char s_text[ui::InfoView::MAX_TEXT_LEN];
210
211 memset(&s_parsed, 0, sizeof(s_parsed));
212 vcard_parse_to_struct(raw, &s_parsed);
213
214 int n = 0;
215 const int cap = static_cast<int>(sizeof(s_text));
216 auto append = [&](const char* fmt, const char* a, const char* b) {
217 if (n >= cap - 1) return;
218 int w = snprintf(s_text + n, static_cast<size_t>(cap - n), fmt, a, b);
219 if (w > 0) n += w;
220 if (n > cap - 1) n = cap - 1;
221 };
222
223 if (s_parsed.formatted_name[0]) {
224 append("%s%s\n\n", s_parsed.formatted_name, "");
225 } else if (s_parsed.given_name[0] || s_parsed.family_name[0]) {
226 append("%s %s\n\n", s_parsed.given_name, s_parsed.family_name);
227 }
228
229 auto field = [&](uint16_t label, const char* value) {
230 if (value[0]) append("%s: %s\n", mstr(label), value);
231 };
232 field(STR_ORGANIZATION, s_parsed.organization);
233 field(STR_POSITION, s_parsed.title);
234 field(STR_EMAIL, s_parsed.email);
235 field(STR_TEL_CELL, s_parsed.tel_cell);
236 field(STR_TEL_HOME, s_parsed.tel_home);
237 field(STR_TEL_WORK, s_parsed.tel_work);
238 field(STR_URL, s_parsed.url);
239 field(STR_TELEGRAM, s_parsed.impp_telegram);
240 field(STR_SIGNAL, s_parsed.impp_signal);
241 field(STR_MATRIX, s_parsed.impp_matrix);
242 field(STR_THREEMA, s_parsed.impp_threema);
243 field(STR_SOCIAL_PROFILE, s_parsed.social_profile);
244 field(STR_NOTE, s_parsed.note);
245
246 if (n == 0) snprintf(s_text, sizeof(s_text), "%s", raw);
247
248 static ui::InfoView s_detailView;
249 s_detailView.init(title, s_text);
250 s_detailView.setOnMenu(withActions ? onReceivedViewMenu : nullptr);
251 ui::ViewStack::instance().push(&s_detailView);
252}
253
259static void showVcardQr(const char* raw, const char* fallbackTitle) {
260 static EXT_RAM_BSS_ATTR vcard_data_t s_parsed;
261 static char s_qrTitle[96];
262 static char s_qrSubtitle[96];
263
264 memset(&s_parsed, 0, sizeof(s_parsed));
265 vcard_parse_to_struct(raw, &s_parsed);
266
267 if (s_parsed.formatted_name[0]) {
268 snprintf(s_qrTitle, sizeof(s_qrTitle), "%s", s_parsed.formatted_name);
269 } else if (s_parsed.given_name[0] || s_parsed.family_name[0]) {
270 snprintf(s_qrTitle, sizeof(s_qrTitle), "%s %s",
271 s_parsed.given_name, s_parsed.family_name);
272 } else {
273 snprintf(s_qrTitle, sizeof(s_qrTitle), "%s", fallbackTitle);
274 }
275
276 const char* sub = s_parsed.organization[0] ? s_parsed.organization
277 : s_parsed.title[0] ? s_parsed.title
278 : s_parsed.email[0] ? s_parsed.email
279 : "";
280 snprintf(s_qrSubtitle, sizeof(s_qrSubtitle), "%s", sub);
281
282 ui::showQRCode(raw, s_qrTitle, s_qrSubtitle[0] ? s_qrSubtitle : nullptr);
283}
284
288static void rebuildMainMenu() {
289 s_mainMenuItems[MENU_MY_VCARD] = {mstr(STR_MY_VCARD), 0, false, nullptr};
291 s_mainMenuItems[MENU_SEND] = {mstr(STR_SEND), 0, false, nullptr};
292 s_mainMenuItems[MENU_RECEIVED] = {mstr(STR_RECEIVED_TITLE), 0, false, nullptr};
294}
295
301static void onMainMenuSelect(uint16_t index, void* userData) {
302 (void)userData;
303
304 switch (index) {
305 case MENU_MY_VCARD: {
306 static EXT_RAM_BSS_ATTR char vcardText[VCARD_MAX_LEN + 1];
307 size_t len = vcard_store_get_own(vcardText, sizeof(vcardText));
308 if (len > 0) {
309 showVcardDetails(mstr(STR_MY_VCARD), vcardText, false);
310 } else {
312 }
313 break;
314 }
315
317 if (vcard_store_has_own()) {
319 } else {
321 }
322 break;
323
324 case MENU_SEND: {
325 // Push our own card to a nearby badge; the framework owns the peer
326 // picker, consent, encryption and progress UI.
327 static EXT_RAM_BSS_ATTR char own[VCARD_MAX_LEN + 1];
328 size_t len = vcard_store_get_own(own, sizeof(own));
329 if (len == 0) {
331 break;
332 }
333 cdc::msg::MessageTransfer::instance().beginInteractiveSend(
334 "text/vcard", reinterpret_cast<const uint8_t*>(own),
335 static_cast<uint32_t>(len));
336 break;
337 }
338
339 case MENU_RECEIVED:
341 break;
342 }
343}
344
345// ============================================================================
346// Received contacts: list, detail, context menu (view / QR / forward / delete).
347// ============================================================================
348
352static void rebuildReceivedList() {
354 for (uint16_t i = 0; i < s_recvCount; i++) {
355 uint16_t slot = s_recvSlots[i];
356 if (!vcard_store_get_display(slot, s_recvLabels[i], sizeof(s_recvLabels[i]))) {
357 s_recvLabels[i][0] = '\0';
358 }
359 s_recvItems[i] = {s_recvLabels[i], 0, false,
360 reinterpret_cast<void*>(static_cast<uintptr_t>(slot))};
361 }
362 s_receivedMenu.setEmptyText(mstr(STR_NO_RECEIVED));
364}
365
371static void onReceivedSelect(uint16_t index, void* userData) {
372 (void)index;
373 s_activeSlot = static_cast<uint16_t>(reinterpret_cast<uintptr_t>(userData));
374 static EXT_RAM_BSS_ATTR char raw[VCARD_MAX_LEN + 1];
375 if (vcard_store_get(s_activeSlot, raw, sizeof(raw)) == 0) return;
377}
378
383
388
390static void ctxReceivedQr() {
391 static EXT_RAM_BSS_ATTR char raw[VCARD_MAX_LEN + 1];
392 if (vcard_store_get(s_activeSlot, raw, sizeof(raw)) == 0) return;
394}
395
397static void ctxReceivedForward() {
398 static EXT_RAM_BSS_ATTR char raw[VCARD_MAX_LEN + 1];
399 size_t len = vcard_store_get(s_activeSlot, raw, sizeof(raw));
400 if (len == 0) return;
401 cdc::msg::MessageTransfer::instance().beginInteractiveSend(
402 "text/vcard", reinterpret_cast<const uint8_t*>(raw),
403 static_cast<uint32_t>(len));
404}
405
410static void onReceivedDeleteConfirm(void* userData) {
411 uint16_t slot = *static_cast<uint16_t*>(userData);
412 if (vcard_store_delete(slot)) {
413 ui::showToastSuccess(ui::tr("core.deleted"));
414 s_receivedMenu.preservePosition();
417 } else {
418 ui::showToastError(ui::tr("core.failed"));
419 }
420}
421
427
432static void onReceivedViewMenu(void* userData) {
433 (void)userData;
434 const ui::ContextMenuItem items[] = {
435 {ui::tr("core.edit"), ctxReceivedEdit},
438 {ui::tr("core.delete"), ctxReceivedDelete},
439 };
440 ui::showContextMenu(ui::tr("core.actions"), items, 4);
441}
442
449static void onReceivedMenu(uint16_t index, void* userData) {
450 (void)index;
451 ui::ContextMenuItem items[4] = {};
452 uint8_t n = 0;
453 items[n++] = {ui::tr("core.add"), ctxReceivedAdd};
454 if (s_recvCount > 0) {
455 s_activeSlot = static_cast<uint16_t>(reinterpret_cast<uintptr_t>(userData));
456 items[n++] = {ui::tr("core.edit"), ctxReceivedEdit};
457 items[n++] = {mstr(STR_FORWARD), ctxReceivedForward};
458 items[n++] = {ui::tr("core.delete"), ctxReceivedDelete};
459 }
460 ui::showContextMenu(ui::tr("core.actions"), items, n);
461}
462
475
476// ============================================================================
477// Lock-screen quick action: show own vCard as a QR code.
478// ============================================================================
479
483static const char* getMyVcardLockscreenLabel() {
484 return mstr(STR_MY_VCARD);
485}
486
492 static EXT_RAM_BSS_ATTR char s_qrBuf[VCARD_MAX_LEN + 1];
493 size_t len = vcard_store_get_own(s_qrBuf, sizeof(s_qrBuf));
494 if (len == 0) {
496 return;
497 }
498 showVcardQr(s_qrBuf, mstr(STR_MY_VCARD));
499}
500
501// ============================================================================
502// Serial Commands (VCARD_SET / VCARD_GET / VCARD_DELETE)
503// ============================================================================
504
505EXT_RAM_BSS_ATTR static char s_vcardBuf[VCARD_MAX_LEN + 64];
506static int s_vcardBufPos = 0;
507static bool s_vcardInputMode = false;
508// Paste target: -1 sets the own card, otherwise the received slot to overwrite.
509static int s_vcardSetSlot = -1;
510static esp_timer_handle_t s_vcardIdleTimer = nullptr;
511// Cancel a stalled paste session after this many seconds of inactivity so a
512// crashed/interrupted client cannot lock the serial console forever.
513static constexpr int64_t VCARD_IDLE_LIMIT_US = 30 * 1000000LL;
514
515static void vcard_session_clear() {
516 s_vcardInputMode = false;
517 s_vcardBufPos = 0;
518 s_vcardSetSlot = -1;
519 memset(s_vcardBuf, 0, sizeof(s_vcardBuf));
521 if (s_vcardIdleTimer) esp_timer_stop(s_vcardIdleTimer);
522}
523
524static void vcard_idle_fired(void*) {
525 if (!s_vcardInputMode) return;
526 serial::Console::printf("\r\nERROR: vCard paste timed out\r\n");
529}
530
531static void vcard_arm_idle_timer() {
532 if (!s_vcardIdleTimer) {
533 esp_timer_create_args_t args = {
534 .callback = vcard_idle_fired,
535 .arg = nullptr,
536 .dispatch_method = ESP_TIMER_TASK,
537 .name = "vcard_idle",
538 .skip_unhandled_events = true,
539 };
540 esp_timer_create(&args, &s_vcardIdleTimer);
541 }
542 esp_timer_stop(s_vcardIdleTimer);
543 esp_timer_start_once(s_vcardIdleTimer, VCARD_IDLE_LIMIT_US);
544}
545
551static bool vcardLineInterceptor(const char* line) {
552 if (!s_vcardInputMode) return false;
553
554 using Console = serial::Console;
556
557 if (strncmp(line, "---", 3) == 0) {
559
560 char err[64] = {};
561 bool ok = (s_vcardSetSlot >= 0)
562 ? vcard_store_update(static_cast<uint16_t>(s_vcardSetSlot), s_vcardBuf,
563 static_cast<size_t>(s_vcardBufPos), err, sizeof(err))
564 : vcard_store_set_own(s_vcardBuf, static_cast<size_t>(s_vcardBufPos), err, sizeof(err));
565 if (ok) {
566 Console::printf("OK: vCard updated\r\n");
567 } else {
568 Console::printf("ERROR: %s\r\n", err[0] ? err : "Invalid vCard");
569 }
570
572 Console::showPrompt();
573 return true;
574 }
575
576 if (strcmp(line, "ABORT") == 0 || strcmp(line, "VCARD_ABORT") == 0) {
577 Console::printf("ABORTED\r\n");
579 Console::showPrompt();
580 return true;
581 }
582
583 size_t lineLen = strlen(line);
584 if (s_vcardBufPos + static_cast<int>(lineLen) + 2 < static_cast<int>(sizeof(s_vcardBuf))) {
585 memcpy(s_vcardBuf + s_vcardBufPos, line, lineLen);
586 s_vcardBufPos += static_cast<int>(lineLen);
587 s_vcardBuf[s_vcardBufPos++] = '\n';
588 } else {
589 Console::printf("ERROR: vCard too large\r\n");
591 Console::showPrompt();
592 }
593 return true;
594}
595
603static void cmdVcardSet(const char* args) {
604 using Console = serial::Console;
605
606 s_vcardSetSlot = -1;
607 if (args && *args) {
608 int slot = atoi(args);
609 char probe[2];
610 if (slot < 0 || slot >= VCARD_MAX_CARDS ||
611 !vcard_store_get_display(static_cast<uint16_t>(slot), probe, sizeof(probe))) {
612 Console::printf("ERROR: no vCard at id %d\r\n", slot);
613 return;
614 }
615 s_vcardSetSlot = slot;
616 }
617
618 Console::printf("Paste vCard 4.0, end with '---' on a new line "
619 "(or 'ABORT' to cancel):\r\n");
620 s_vcardBufPos = 0;
621 s_vcardInputMode = true;
624}
625
630static void vcardPrintLines(char* out) {
631 using Console = serial::Console;
632 char* line = out;
633 char* next;
634 while ((next = strchr(line, '\n')) != nullptr) {
635 *next = '\0';
636 Console::printf("%s\r\n", line);
637 line = next + 1;
638 }
639 if (*line) {
640 Console::printf("%s\r\n", line);
641 }
642}
643
652static void cmdVcardGet(const char* args) {
653 using Console = serial::Console;
654 char out[VCARD_MAX_LEN + 1];
655
656 if (args && *args) {
657 int slot = atoi(args);
658 if (slot < 0 || slot >= VCARD_MAX_CARDS ||
659 vcard_store_get(static_cast<uint16_t>(slot), out, sizeof(out)) == 0) {
660 Console::printf("ERROR: no vCard at id %d\r\n", slot);
661 return;
662 }
663 vcardPrintLines(out);
664 return;
665 }
666
667 size_t len = vcard_store_get_own(out, sizeof(out));
668 if (len == 0) {
669 Console::printf("BEGIN:VCARD\r\n");
670 Console::printf("VERSION:4.0\r\n");
671 Console::printf("N:;;\r\n");
672 Console::printf("FN:\r\n");
673 Console::printf("NOTE:\r\n");
674 Console::printf("TEL;TYPE=HOME:\r\n");
675 Console::printf("TEL;TYPE=CELL:\r\n");
676 Console::printf("TEL;TYPE=WORK:\r\n");
677 Console::printf("EMAIL:\r\n");
678 Console::printf("URL:\r\n");
679 Console::printf("ORG:\r\n");
680 Console::printf("TITLE:\r\n");
681 Console::printf("X-SOCIALPROFILE:\r\n");
682 Console::printf("IMPP:telegram:\r\n");
683 Console::printf("IMPP:signal:\r\n");
684 Console::printf("IMPP:matrix:\r\n");
685 Console::printf("IMPP:threema:\r\n");
686 Console::printf("END:VCARD\r\n");
687 return;
688 }
689
690 vcardPrintLines(out);
691}
692
697static void cmdVcardList(const char* args) {
698 (void)args;
699 using Console = serial::Console;
700
701 uint16_t slots[VCARD_MAX_CARDS];
702 uint16_t count = vcard_store_get_sorted(slots, VCARD_MAX_CARDS);
703 if (count == 0) {
704 Console::printf("No received vCards\r\n");
705 return;
706 }
707
708 char name[64];
709 for (uint16_t i = 0; i < count; i++) {
710 if (!vcard_store_get_display(slots[i], name, sizeof(name))) name[0] = '\0';
711 Console::printf("%2u %s\r\n", slots[i], name);
712 }
713}
714
722static void cmdVcardDelete(const char* args) {
723 using Console = serial::Console;
724
725 if (args && *args) {
726 int slot = atoi(args);
727 if (slot < 0 || slot >= VCARD_MAX_CARDS ||
728 !vcard_store_delete(static_cast<uint16_t>(slot))) {
729 Console::printf("ERROR: no vCard at id %d\r\n", slot);
730 return;
731 }
732 Console::printf("OK: vCard %d deleted\r\n", slot);
733 return;
734 }
735
736 if (vcard_store_clear_own()) {
737 Console::printf("OK: vCard deleted\r\n");
738 } else {
739 Console::printf("ERROR: Failed to delete vCard\r\n");
740 }
741}
742
744 {"SET", "[id]", "Set own vCard, or overwrite received vCard <id> (multiline paste, end with '---' or 'ABORT')", cmdVcardSet},
745 {"GET", "[id]", "Show own vCard, or received vCard <id>", cmdVcardGet},
746 {"LIST", "", "List received vCards", cmdVcardList},
747 {"DELETE", "[id]", "Delete own vCard, or received vCard <id>", cmdVcardDelete},
748 {nullptr, nullptr, nullptr, nullptr},
749};
750
751static void cmdVcard(const char* args) {
753}
754
759 auto& reg = serial::getCommandRegistry();
760 reg.registerCommand({"VCARD",
761 "vCard storage: SET/GET/LIST/DELETE",
762 cmdVcard, "vcard", false, kVcardSubs});
763}
764
765// ============================================================================
766// Module Implementation
767// ============================================================================
768
774 static VcardModule inst;
775 return inst;
776}
777
783 LOG_I(TAG, "Initializing vCard module");
786
788
789 // Register as the handler for incoming "text/vcard" message transfers.
790 cdc::msg::MessageTransfer::instance().registerHandler(
791 "text/vcard", "mod_vcard.received", deliverVcard);
792
795 return true;
796}
797
804 return false;
805 }
807 return true;
808}
809
814 cdc::msg::MessageTransfer::instance().unregisterHandler("text/vcard");
816}
817
824uint8_t VcardModule::getMenuItems(core::ModuleMenuItem* items, uint8_t maxItems) {
825 if (!items || maxItems == 0) return 0;
826
827 items[0] = {
829 110,
830 []() -> ui::IView* {
831 if (!s_viewsInitialized) {
832 s_mainMenu.setOnSelect(onMainMenuSelect);
833 s_viewsInitialized = true;
834 }
836 return &s_mainMenu;
837 },
838 nullptr,
839 getName(),
841 nullptr
842 };
843
844 return 1;
845}
846
854 if (!items || maxItems == 0) return 0;
855 items[0] = {
858 60,
859 nullptr,
860 };
861 return 1;
862}
863
868void VcardModule::onTick(uint32_t nowMs) {
869 (void)nowMs;
870}
871
873static constexpr int kSchemaVer = 1;
874
885 if (!out) return false;
886
887 cJSON_AddNumberToObject(out, "schema_ver", kSchemaVer);
888
889 bool any = false;
890
891 char buf[VCARD_MAX_LEN + 1];
892 if (vcard_store_get_own(buf, sizeof(buf)) > 0) {
893 cJSON_AddStringToObject(out, "own", buf);
894 any = true;
895 }
896
897 cJSON* received = cJSON_AddArrayToObject(out, "received");
898 if (!received) return any;
899
900 uint16_t slots[VCARD_MAX_CARDS];
901 uint16_t count = vcard_store_get_sorted(slots, VCARD_MAX_CARDS);
902 for (uint16_t i = 0; i < count; i++) {
903 if (vcard_store_get(slots[i], buf, sizeof(buf)) == 0) continue;
904 cJSON* item = cJSON_CreateString(buf);
905 if (!item) continue;
906 cJSON_AddItemToArray(received, item);
907 any = true;
908 }
909
910 return any;
911}
912
924static bool importReceivedVcard(const cJSON* je, void* user) {
925 (void)user;
926 if (!cJSON_IsString(je) || !je->valuestring || je->valuestring[0] == '\0') return false;
927
928 const char* text = je->valuestring;
929 size_t len = strlen(text);
930 char err[32] = {};
931 if (vcard_store_add(text, len, err, sizeof(err))) return true;
932
933 // An already-present card means the identity is satisfied (no-op upsert);
934 // only genuine validation/storage failures count as failed.
935 return vcard_store_contains(text, len);
936}
937
948 if (!in) return {};
949
950 const cJSON* schemaVer = cJSON_GetObjectItemCaseSensitive(in, "schema_ver");
951 if (cJSON_IsNumber(schemaVer) && static_cast<int>(schemaVer->valuedouble) != kSchemaVer) {
952 LOG_W(TAG, "vCard backup schema_ver %d != expected %d, skipping",
953 static_cast<int>(schemaVer->valuedouble), kSchemaVer);
954 return {};
955 }
956
957 core::IModule::BackupResult result = {};
958
959 const cJSON* own = cJSON_GetObjectItemCaseSensitive(in, "own");
960 if (cJSON_IsString(own) && own->valuestring && own->valuestring[0] != '\0') {
961 char err[32] = {};
962 if (vcard_store_set_own(own->valuestring, strlen(own->valuestring), err, sizeof(err))) {
963 result.imported++;
964 } else {
965 LOG_W(TAG, "vCard import: own vCard rejected (%s)", err);
966 result.failed++;
967 }
968 }
969
970 const cJSON* received = cJSON_GetObjectItemCaseSensitive(in, "received");
972 result.imported = static_cast<uint16_t>(result.imported + rx.imported);
973 result.failed = static_cast<uint16_t>(result.failed + rx.failed);
974
975 return result;
976}
977
978} // namespace cdc::mod_vcard
979
983extern "C" void mod_vcard_register() {
985 auto& module = cdc::mod_vcard::VcardModule::instance();
986 module.init();
987 });
988}
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(StringResolver resolver, const uint16_t *titleOffsets, uint16_t savedOffset, uint16_t failedOffset)
Configures the wizard with i18n callbacks. Must be called before start() or edit() so step titles can...
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:98
static void printf(const char *format,...) __attribute__((format(printf
Prints formatted text to console.
Definition Console.cpp:30
virtual void setLineInterceptor(LineInterceptor interceptor)
static I18n & instance()
Singleton accessor.
Definition I18n.cpp:287
void registerEnglishTable(const I18nEntry *entries, std::size_t count)
Append English entries to the lookup table.
Definition I18n.cpp:307
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:34
void popToAnchor(IView *anchor)
Pops views until the specified anchor view is the current view.
void push(IView *view, void *context=nullptr)
static constexpr uint16_t STR_SHOW_QR
static constexpr uint16_t STR_MY_VCARD
static constexpr uint16_t STR_NO_RECEIVED
static constexpr uint16_t STR_DECLINE
static constexpr uint16_t STR_SOCIAL_PROFILE
static constexpr uint16_t STR_NO_VCARD
static constexpr uint16_t STR_URL
static constexpr int kSchemaVer
Schema version written to and expected from the vCard backup section.
static const char * mstr(uint16_t offset)
static constexpr uint16_t STR_SCANNING
static constexpr uint16_t STR_SAVED
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 constexpr uint16_t STR_SIGNAL
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 uint16_t STR_NOTE
static constexpr uint16_t STR_STOP_ADV
static constexpr int64_t VCARD_IDLE_LIMIT_US
static constexpr uint16_t STR_VCARD
static void cmdVcardGet(const char *args)
Serial command printing a stored vCard.
static constexpr uint16_t STR_THREEMA
static constexpr uint16_t STR_EXCHANGE_FAIL
static constexpr uint16_t STR_FORWARD
static void ctxReceivedAdd()
Context-menu action: create a new stored contact via the wizard.
static constexpr uint16_t STR_TEL_HOME
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 constexpr uint16_t STR_TEL_WORK
static constexpr uint16_t STR_ACCEPT
static int s_vcardSetSlot
static void ctxReceivedDelete()
Context-menu action: confirm and delete the active contact.
static constexpr uint16_t STR_CONFIRM_DELETE
static constexpr uint16_t STR_EMAIL
static constexpr uint16_t STR_NO_PEERS
MainMenuItem
Main-menu item identifiers.
static constexpr uint16_t STR_POSITION
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 constexpr uint16_t STR_TEL_CELL
static ui::ListView s_mainMenu
View instances used by vCard module UI flow.
static void cmdVcard(const char *args)
static constexpr uint16_t STR_ORGANIZATION
static esp_timer_handle_t s_vcardIdleTimer
static constexpr uint16_t STR_EDIT_MY_VCARD
static constexpr uint16_t STR_RECEIVED
static void registerSerialCommands()
Registers serial commands exposed by vCard module.
static constexpr uint16_t STR_EXCHANGE_REQ
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 constexpr uint16_t STR_ADVERTISING
static constexpr uint16_t STR_CONNECTING
static bool importReceivedVcard(const cJSON *je, void *user)
Imports one received vCard string into storage.
static constexpr uint16_t STR_EXCHANGE
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 constexpr uint16_t STR_SCAN
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 serial::SubCommand kVcardSubs[]
static uint16_t s_recvSlots[VCARD_MAX_CARDS]
static bool s_receivedInitialized
static const uint16_t s_wizardStepOffsets[16]
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 constexpr uint16_t STR_NEARBY
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 constexpr uint16_t STR_TELEGRAM
static constexpr uint16_t STR_GIVEN_NAME
static bool s_vcardInputMode
static constexpr uint16_t STR_MATRIX
static bool s_viewsInitialized
static constexpr uint16_t STR_FAMILY_NAME
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 constexpr uint16_t STR_STOP_SCAN
static constexpr uint16_t STR_EXCHANGE_OK
static uint16_t s_activeSlot
static constexpr uint16_t STR_RECEIVED_TITLE
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 constexpr uint16_t STR_SEND
static int s_vcardBufPos
static void rebuildReceivedList()
Rebuilds the received-contacts list from the store (sorted by name).
static constexpr uint16_t STR_FORMATTED_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:208
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.