mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 19:38:20 +00:00
fix import regression caused by vcontact
This commit is contained in:
@@ -65,6 +65,20 @@ Notes on the mapping:
|
||||
boards without dedicated art fall back to a neutral LoRa icon. Swap in ZephCore-branded
|
||||
art by pointing `img` at your own absolute HTTPS URLs.
|
||||
|
||||
> [!WARNING]
|
||||
> **The configurator's "Erase Flash" is not ZephCore-aware — do not use it on a ZephCore node.**
|
||||
> `erase` is a MeshCore device-level field, not a provider field, so a folded nRF52 device
|
||||
> inherits the *base* device's formatter (e.g. Wio Tracker L1 → `WioTrackerL1_QSPIFlash_Format`).
|
||||
> That formatter targets MeshCore/Ripple's flash layout, which does not match ZephCore's
|
||||
> LittleFS — so it performs a partial, inconsistent wipe (observed: nukes `channels`, leaves
|
||||
> identity/prefs/contacts) and can leave the filesystem half-corrupted. New tiles inherit no
|
||||
> `erase` at all, so Erase Flash is a no-op there.
|
||||
>
|
||||
> To factory-reset a ZephCore node, use ZephCore's own mechanism, which formats **all** of
|
||||
> `/lfs` + `/ext` (and the NVS bond partition) and reboots: the `erase` command over the USB
|
||||
> serial CLI, the companion app's factory reset, or simply reflash (ZephCore auto-formats on
|
||||
> first boot when it detects an incompatible/blank FS).
|
||||
|
||||
## Local test
|
||||
|
||||
```
|
||||
|
||||
+4
-1
@@ -278,11 +278,14 @@ config ZEPHCORE_BLE_PASSKEY
|
||||
|
||||
config ZEPHCORE_BLE_QUEUE_SIZE
|
||||
int "BLE TX/RX message queue depth"
|
||||
default 12
|
||||
default 24
|
||||
range 4 64
|
||||
help
|
||||
Number of frames that can be queued for BLE TX/RX.
|
||||
Larger = better buffering for bursty traffic, more RAM.
|
||||
24 absorbs the app's bursty CMD_GET_CHANNEL pipelining during
|
||||
channel sync (it fires up to MAX_CHANNELS requests back-to-back);
|
||||
at 12 the recv queue overran and dropped requests, stalling sync.
|
||||
|
||||
config ZEPHCORE_BLE_CONN_MIN_INTERVAL
|
||||
int "BLE minimum connection interval (units: 1.25ms)"
|
||||
|
||||
@@ -102,8 +102,28 @@ bool ZephyrDataStore::mount()
|
||||
ext_lfs_mounted = true;
|
||||
LOG_INF("External QSPI LittleFS at %s (automounted, 100 blobs)", extMountPoint());
|
||||
} else {
|
||||
#if DT_NODE_EXISTS(DT_NODELABEL(qspi_lfs))
|
||||
/* Boot-time automount can miss the QSPI on the first boot after a
|
||||
* factory-erase (blank flash) or an early-boot timing race with QSPI
|
||||
* init. Retry the mount explicitly (fs_mount auto-formats blank flash,
|
||||
* and mounts valid data without touching it) so contacts/channels land
|
||||
* on /ext. Without this the store silently falls back to internal /lfs,
|
||||
* and the next boot that does mount /ext runs a needless contact
|
||||
* migration — the "Migrating contacts to external storage" churn. */
|
||||
FS_FSTAB_DECLARE_ENTRY(DT_NODELABEL(qspi_lfs));
|
||||
int rc = fs_mount(&FS_FSTAB_ENTRY(DT_NODELABEL(qspi_lfs)));
|
||||
if (is_mounted(extMountPoint())) {
|
||||
ext_lfs_mounted = true;
|
||||
LOG_INF("External QSPI LittleFS at %s (mounted on retry, rc=%d)",
|
||||
extMountPoint(), rc);
|
||||
} else {
|
||||
ext_lfs_mounted = false;
|
||||
LOG_WRN("External QSPI mount retry failed (rc=%d) - using internal only (20 blobs)", rc);
|
||||
}
|
||||
#else
|
||||
ext_lfs_mounted = false;
|
||||
LOG_INF("External QSPI NOT mounted at %s - using internal only (20 blobs)", extMountPoint());
|
||||
#endif
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -370,6 +390,28 @@ bool ZephyrDataStore::formatFileSystem()
|
||||
if (mounted) {
|
||||
lfs_mounted = true;
|
||||
}
|
||||
|
||||
#if DT_NODE_EXISTS(DT_NODELABEL(qspi_lfs))
|
||||
/* Remount external QSPI too. We unmounted it above and flattened its
|
||||
* partition, so it must be re-mounted here — otherwise a runtime format
|
||||
* (factory reset, or the first-boot "no prefs" auto-format) leaves /ext
|
||||
* unmounted for the rest of the session. begin() then reads
|
||||
* ext_lfs_mounted=false and the store falls back to internal /lfs, so
|
||||
* contacts/channels save to /lfs and get needlessly migrated back to /ext
|
||||
* on the next boot ("Migrating contacts to external storage" churn). */
|
||||
{
|
||||
FS_FSTAB_DECLARE_ENTRY(DT_NODELABEL(qspi_lfs));
|
||||
int ext_rc = fs_mount(&FS_FSTAB_ENTRY(DT_NODELABEL(qspi_lfs)));
|
||||
if (is_mounted(extMountPoint())) {
|
||||
ext_lfs_mounted = true;
|
||||
LOG_INF("formatFileSystem: /ext remounted (rc=%d)", ext_rc);
|
||||
} else {
|
||||
ext_lfs_mounted = false;
|
||||
LOG_ERR("formatFileSystem: /ext remount failed (rc=%d)", ext_rc);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_INF("formatFileSystem: mount() returned %d", mounted ? 1 : 0);
|
||||
return mounted;
|
||||
}
|
||||
@@ -378,6 +420,12 @@ void ZephyrDataStore::factoryReset()
|
||||
{
|
||||
LOG_INF("=== FACTORY RESET STARTING ===");
|
||||
if (formatFileSystem()) {
|
||||
/* Mark the freshly-formatted FS as ZephCore-initialised so the
|
||||
* post-reboot first-boot check (no prefs → format) doesn't format it a
|
||||
* SECOND time. That redundant format re-ran formatFileSystem() without
|
||||
* a following mount(), which is what used to leave /ext unmounted and
|
||||
* push contacts/channels onto internal flash. */
|
||||
writeInitMarker();
|
||||
LOG_INF("=== FACTORY RESET COMPLETE - REBOOT REQUIRED ===");
|
||||
} else {
|
||||
LOG_ERR("=== FACTORY RESET FAILED ===");
|
||||
|
||||
@@ -228,10 +228,7 @@ void CompanionMesh::begin()
|
||||
/* Derive the v-contact pubkey from our identity: stable per node, unique
|
||||
* per device. Deliberately NOT a real keypair — no private key exists
|
||||
* anywhere, so nothing addressed to this key is decryptable by anyone. */
|
||||
static const char vc_salt[] = "zc-vcontact";
|
||||
mesh::Utils::sha256(_vcontact_pubkey, PUB_KEY_SIZE,
|
||||
(const uint8_t *)vc_salt, sizeof(vc_salt) - 1,
|
||||
self_id.pub_key, PUB_KEY_SIZE);
|
||||
deriveVContactKey();
|
||||
/* Stamp lastmod only if a time source already ran (hardware RTC restore
|
||||
* happens before begin()). Otherwise stay deferred (lastmod = 0) until
|
||||
* vcontactClockSynced() — an advert stamped now would show as 1970. */
|
||||
@@ -978,6 +975,17 @@ void CompanionMesh::vcontactNotify(const char *text)
|
||||
vcontactQueueText(text);
|
||||
}
|
||||
|
||||
void CompanionMesh::deriveVContactKey()
|
||||
{
|
||||
/* v-contact pubkey = SHA256("zc-vcontact" || self pubkey). Re-run whenever
|
||||
* the identity changes (boot, CMD_IMPORT_PRIVATE_KEY) so the key always
|
||||
* tracks the current identity. */
|
||||
static const char vc_salt[] = "zc-vcontact";
|
||||
mesh::Utils::sha256(_vcontact_pubkey, PUB_KEY_SIZE,
|
||||
(const uint8_t *)vc_salt, sizeof(vc_salt) - 1,
|
||||
self_id.pub_key, PUB_KEY_SIZE);
|
||||
}
|
||||
|
||||
void CompanionMesh::vcontactPushAdvert()
|
||||
{
|
||||
if (!isVContactEnabled()) return;
|
||||
@@ -1096,6 +1104,27 @@ bool CompanionMesh::vcontactHandleFrame(const uint8_t *data, size_t len)
|
||||
}
|
||||
return false;
|
||||
|
||||
case CMD_GET_ADVERT_PATH:
|
||||
/* The v-contact appears in the app's contact list, so the app queries
|
||||
* its advert path (at connect, and around a config/identity import). The
|
||||
* v-contact has no over-the-air advert, so the real handler's
|
||||
* findAdvertPath() misses and returns ERR_NOT_FOUND — which the app
|
||||
* surfaces as a fatal "not found" that aborts the whole operation. The
|
||||
* v-contact IS this node (loopback), so its path is direct (zero hops):
|
||||
* answer that here, before the real handler runs. Frame layout matches
|
||||
* the real handler: [cmd][reserved][7-byte pubkey prefix]. */
|
||||
if (len >= 2 + 7 && isVContactKey(&data[2], 7)) {
|
||||
uint8_t rsp[6];
|
||||
size_t i = 0;
|
||||
rsp[i++] = PACKET_ADVERT_PATH;
|
||||
put_le32(&rsp[i], _vcontact_lastmod ? _vcontact_lastmod
|
||||
: (uint32_t)getRTCClock()->getCurrentTime()); i += 4;
|
||||
rsp[i++] = 0; // path_len = 0 → direct (zero hop)
|
||||
writeFrame(rsp, i);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case CMD_ADD_UPDATE_CONTACT:
|
||||
case CMD_RESET_PATH:
|
||||
/* Never let the v-contact into the real contacts table (it must stay
|
||||
@@ -2858,7 +2887,18 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
mesh::LocalIdentity new_identity;
|
||||
new_identity.readFrom(&data[1], PRV_KEY_SIZE);
|
||||
if (_store->saveMainIdentity(new_identity)) {
|
||||
/* The v-contact key is derived from our identity, so it
|
||||
* changes with the new key. Tell the connected app to drop
|
||||
* the old v-contact (old key), re-derive, then re-advertise
|
||||
* the new one — otherwise the app's cached loopback contact
|
||||
* points at a key we no longer recognise (messaging + its
|
||||
* advert-path query break until a reboot re-syncs). Order
|
||||
* matters: delete uses the CURRENT (old) key. */
|
||||
bool vc_was_enabled = isVContactEnabled();
|
||||
if (vc_was_enabled) vcontactPushDeleted();
|
||||
self_id = new_identity;
|
||||
deriveVContactKey();
|
||||
if (vc_was_enabled) vcontactPushAdvert();
|
||||
/* Reload contacts to invalidate ECDH shared secrets */
|
||||
resetContacts();
|
||||
_store->loadContacts(this);
|
||||
|
||||
@@ -523,6 +523,9 @@ private:
|
||||
bool vcontactReady() { return isVContactEnabled() && _vcontact_lastmod != 0; }
|
||||
void buildVContact(ContactInfo &c) const;
|
||||
bool isVContactKey(const uint8_t *key, int prefix_len) const;
|
||||
/** (Re)derive _vcontact_pubkey from the current identity. Call on boot and
|
||||
* whenever the identity changes (CMD_IMPORT_PRIVATE_KEY). */
|
||||
void deriveVContactKey();
|
||||
/** Intercept protocol frames addressed to the v-contact. Returns true when
|
||||
* the frame was fully handled (response already written). */
|
||||
bool vcontactHandleFrame(const uint8_t *data, size_t len);
|
||||
|
||||
Reference in New Issue
Block a user