-
Notifications
You must be signed in to change notification settings - Fork 0
SD Card Layout
Chat history (DM and channel) is stored on the microSD card. The slot used is the internal SDMMC slot 0 (slot 1 hosts the WiFi link to the C6).
Mount point: /sd. Driver: esp_vfs_fat_sdmmc_mount with 4-bit SDMMC,
internal pull-ups, default frequency, no auto-format.
/sd
└── meshcore/
├── channel.bin ← public-channel history
└── dm/
├── a3f2c91a8b7e4f5d.bin ← per-peer DM log (16 hex chars
├── 7e29c84d1b0f6a52.bin = first 8 bytes of peer pubkey)
└── …
The filename is <first 8 bytes of peer pub-key in hex>.bin (16 chars).
With MAX_CONTACTS = 16 the 64-bit prefix collision risk is negligible.
Each record on disk:
struct __attribute__((packed)) history_rec_hdr_t {
uint8_t magic[4]; // "MCR1"
uint16_t plain_len; // 1 .. MAX_MSG_TEXT
uint8_t flags; // bit 0 = is_mine
uint8_t reserved;
uint32_t ts_unix; // seconds, little-endian
uint8_t iv[16]; // random IV for this record
}; // 28 bytes
uint8_t ciphertext[ ceil(plain_len, 16) ]; // AES-128-CBC, PKCS#7- Cipher: AES-128-CBC with PKCS#7 padding.
-
Key derivation: at
history_init(prv_key),key = HMAC-SHA256(prv_key, "mc-history-v1")[0:16](mbedTLSmbedtls_md_hmac). -
IV: 16 bytes from
esp_fill_random, persisted in the header per record. No IV reuse across records.
The history key is therefore derived from the Ed25519 identity private key. Wiping NVS regenerates the identity → previous on-disk records become unreadable (see "Self-heal" below).
history_append_dm(peer_pub, text, is_mine) and
history_append_channel(text, is_mine) both:
- Take the
s_mutex(history lock). - Compose the 28-byte header (magic, length, flags, current Unix time, random IV).
- PKCS#7-pad and AES-CBC encrypt.
-
fopen(path, "ab")+fwrite(hdr) + fwrite(ct)+fclose.
The write is fsync'd by fclose semantics on the FAT driver. Power loss
mid-write loses at most the in-flight record.
history_load_dm(peer_pub, add) and history_load_channel(add) read
sequentially from start of file. For each record:
- Verify magic bytes (
"MCR1"). - Verify
plain_lenin range. - Read ciphertext, decrypt, verify PKCS#7 padding matches
plain_len. - Call the
addcallback withtextandis_mine.
If a record fails the magic / length / padding checks the loop stops
(break). Records before the failure are still loaded.
When the load loop bails out at record 0 with a fatal error
(bad magic / bad len / decrypt mismatch), the entire file is removed:
if (fatal && loaded == 0) {
ESP_LOGW(TAG, "(%s): unreadable from start — removing stale file", path);
remove(path);
}This recovers automatically from "identity key changed" (e.g. after a full NVS erase or partition table flash) — the next append starts a fresh log under the new key instead of being shadowed forever by an unreadable old log.
If only the tail of a file is corrupt, the good prefix is loaded and the bad tail is left in place; subsequent appends will simply be added beyond the corruption and only the bad records (and everything after them) are lost on the next load. This is intentional — we avoid destroying readable history on a single bit flip.
A single s_mutex (FreeRTOS binary semaphore) serialises all SD access
within history.c. Holds are kept short (one append or one full load).
If mount_sd fails the module sets s_status to one of:
| Status | Cause |
|---|---|
ok |
mounted successfully |
no-sd |
ESP_ERR_NOT_FOUND / ESP_ERR_TIMEOUT
|
err |
other mount error |
off |
initial state, before history_init was called |
Append/load become no-ops when s_ready == false, so the rest of the app
keeps running without persistence.