Skip to content

vCard over cdc_msg

mod_vcard is a regular consumer of the cdc_msg message-transfer framework. It owns no BLE service of its own: it registers a handler for one MIME type and sends its own card through the framework. The old custom ble_vcard GATT service has been deleted.

The vCard payload uses the MIME type text/vcard. The payload is raw vCard 4.0 text.

In VcardModule::init() the module registers itself as the text/vcard handler:

cdc::msg::MessageTransfer::instance().registerHandler(
"text/vcard", // MIME type
"mod_vcard.received", // consent-prompt i18n key -> "Contact (vCard)"
deliverVcard); // delivery callback

VcardModule::stop() unregisters it again (unregisterHandler("text/vcard")).

The descriptor key mod_vcard.received resolves through the module’s own English i18n table to “Contact (vCard)”, which is what the receiver sees in the Incoming transfer consent prompt.

deliverVcard(data, len, mime, peerName) is the DeliverFn. The incoming bytes are untrusted and not NUL-terminated, so the handler:

  1. Rejects empty payloads and anything larger than VCARD_MAX_LEN (768 bytes).
  2. Copies the bytes into a bounded PSRAM buffer and NUL-terminates them.
  3. Calls vcard_store_add() to validate and store the card.

It returns true only when the card was stored. Storage deduplicates on exact text, so receiving an identical card twice is a no-op upsert.

The vCards menu Send vCard action loads the own card and hands it to the framework’s interactive sender:

cdc::msg::MessageTransfer::instance().beginInteractiveSend(
"text/vcard",
reinterpret_cast<const uint8_t*>(own),
static_cast<uint32_t>(len));

The framework then owns the peer picker, the numeric-comparison pairing, the consent on the far side, encryption and the progress UI. If no own card is stored, the action shows a “No vCard set” hint and does not send.

The card is vCard 4.0 text. The own-card editor maps to these vCard fields (via vcard_data_t, generated by vcard_generate_from_struct):

Editor fieldvCard line
First name / Last nameN: (family;given)
Display nameFN: (falls back to “given family” if empty)
OrganizationORG:
PositionTITLE:
EmailEMAIL:
Phone (Home / Mobile / Work)TEL;TYPE=HOME / CELL / WORK
WebsiteURL:
TelegramIMPP:telegram:
SignalIMPP:signal:
MatrixIMPP:matrix:
ThreemaIMPP:threema:
Social ProfileX-SOCIALPROFILE:
NoteNOTE:

Empty fields are omitted from the generated card. The whole card is bounded to VCARD_MAX_LEN (768 bytes), which is also the receive-side cap, well within the framework’s 4096-byte payload limit.

Received cards go into the vCard store (up to VCARD_MAX_CARDS = 100), separate from the own card. The module’s exportBackup/importBackup include the own card (own) and a received array of raw card texts under schema_ver 1; import upserts by exact text.