From bb5aca423ca3791fc8b038650ab97b7757851ef4 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:41:16 +0100 Subject: [PATCH] refactor storage, got rid of packet loss on lr1110 --- zephcore/CMakeLists.txt | 1 + zephcore/adapters/ble/ZephyrBLE.cpp | 81 +- zephcore/adapters/ble/ZephyrBLE.h | 8 + .../adapters/datastore/ZephyrDataStore.cpp | 725 +++++++++++------- zephcore/adapters/datastore/ZephyrDataStore.h | 40 +- zephcore/adapters/datastore/lfs_128b_erase.c | 64 ++ .../adapters/radio/lr11xx/lr11xx_hal_zephyr.c | 2 +- zephcore/app/CompanionMesh.cpp | 8 +- zephcore/boards/common/filesystem.dtsi | 22 +- zephcore/boards/common/nrf52_common.conf | 16 +- .../boards/common/nrf52_partitions_sdv6.dtsi | 67 ++ .../boards/common/nrf52_partitions_sdv7.dtsi | 96 +++ zephcore/boards/common/prod.conf | 6 +- zephcore/boards/common/qspi-ext.dtsi | 19 +- zephcore/boards/common/zephcore_common.conf | 6 +- .../ikoka_nano_30dbm/ikoka_nano_30dbm.dts | 46 +- .../boards/nrf52840/rak4631/board.overlay | 51 +- .../nrf52840/t1000_e/t1000_e_nrf52840.dts | 58 +- .../boards/nrf52840/wio_tracker_l1/board.conf | 3 + .../nrf52840/wio_tracker_l1/board.overlay | 4 +- .../wio_tracker_l1_nrf52840.dts | 55 +- zephcore/helpers/ui/buzzer.c | 71 +- .../drivers/lora/lr11xx/lr11xx_lora.c | 157 ++-- .../zephyr/0003-lora-sx126x-native.patch | 4 +- .../zephyr/0007-littlefs-custom-erase.patch | 15 + zephcore/src/main_companion.cpp | 5 +- .../formatter/boards/rak4631/board.overlay | 34 +- zephcore/tools/formatter/src/main.c | 133 +++- 28 files changed, 1053 insertions(+), 744 deletions(-) create mode 100644 zephcore/adapters/datastore/lfs_128b_erase.c create mode 100644 zephcore/boards/common/nrf52_partitions_sdv6.dtsi create mode 100644 zephcore/boards/common/nrf52_partitions_sdv7.dtsi create mode 100644 zephcore/patches/zephyr/0007-littlefs-custom-erase.patch diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index d0f305c..afef849 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -396,6 +396,7 @@ else() src/main_companion.cpp adapters/ble/ZephyrBLE.cpp adapters/datastore/ZephyrDataStore.cpp + adapters/datastore/lfs_128b_erase.c helpers/BaseChatMesh.cpp helpers/TransportKeyStore.cpp helpers/ui/ui_mesh_actions.cpp diff --git a/zephcore/adapters/ble/ZephyrBLE.cpp b/zephcore/adapters/ble/ZephyrBLE.cpp index f3b38bc..e3739ff 100644 --- a/zephcore/adapters/ble/ZephyrBLE.cpp +++ b/zephcore/adapters/ble/ZephyrBLE.cpp @@ -62,6 +62,11 @@ struct frame { /* ========== Static state ========== */ +/* Deferred connection parameter update — applied after initial sync + * completes (NO_MORE_MSGS) rather than immediately on security_changed, + * because the negotiation disrupts BLE throughput during channel/contact sync. */ +static bool conn_params_pending; + /* Callbacks to main */ static const struct ble_callbacks *ble_cbs; @@ -113,9 +118,6 @@ static enum zephcore_iface active_iface = ZEPHCORE_IFACE_NONE; /* DLE tracking — set after successful DLE request to avoid double-request */ static bool dle_requested; -/* PHY override — request Coded|1M once if phone chose 2M */ -static bool phy_override_sent; - /* Advertising state */ static bool adv_switching = false; static bool adv_is_slow = false; @@ -276,7 +278,6 @@ static void connected(struct bt_conn *conn, uint8_t err) * DLE is deferred to le_phy_updated() (after PHY negotiation completes) * with a fallback in security_changed() if PHY update never fires. */ dle_requested = false; - phy_override_sent = false; /* Do NOT proactively request security here. * @@ -321,6 +322,7 @@ static void disconnected(struct bt_conn *conn, uint8_t reason) /* Reset BLE TX state */ ble_tx_in_progress = false; ble_tx_ready = false; + conn_params_pending = false; /* Clear interface state if BLE was active */ if (active_iface == ZEPHCORE_IFACE_BLE) { @@ -391,22 +393,13 @@ static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_ * request_dle() returns immediately (dle_requested flag). */ request_dle(conn); #endif - /* Request our preferred connection parameters. */ - struct bt_le_conn_param conn_param = { - .interval_min = BLE_DEFAULT_MIN_INTERVAL, - .interval_max = BLE_DEFAULT_MAX_INTERVAL, - .latency = BLE_DEFAULT_LATENCY, - .timeout = BLE_DEFAULT_TIMEOUT, - }; - int param_err = bt_conn_le_param_update(conn, &conn_param); - if (param_err) { - LOG_WRN("Failed to request conn param update: %d", param_err); - } else { - LOG_INF("Requested conn params: %d-%dms interval, latency=%d", - BLE_DEFAULT_MIN_INTERVAL * 5 / 4, - BLE_DEFAULT_MAX_INTERVAL * 5 / 4, - BLE_DEFAULT_LATENCY); - } + /* Defer connection parameter update until after the initial + * app sync finishes (channels + contacts + offline messages). + * Requesting a param change now would disrupt BLE throughput + * during the sync burst. CompanionMesh calls + * zephcore_ble_conn_params_ready() when sync is done. */ + conn_params_pending = true; + LOG_INF("conn param update deferred until post-sync"); } } @@ -481,27 +474,9 @@ static void le_phy_updated(struct bt_conn *conn, struct bt_conn_le_phy_info *par LOG_INF("BLE PHY updated: TX=%s RX=%s", phy_name(param->tx_phy), phy_name(param->rx_phy)); - /* If the phone chose 2M, override with Coded|1M preference. - * Coded gives ~4× BLE range (S=8); if the phone doesn't support it, - * the intersection is 1M (better range than 2M for a mesh device). - * Only try once to avoid ping-pong if the phone insists on 2M. */ - if (param->tx_phy == BT_GAP_LE_PHY_2M && !phy_override_sent) { - const struct bt_conn_le_phy_param phy_pref = { - .options = BT_CONN_LE_PHY_OPT_NONE, - .pref_tx_phy = BT_GAP_LE_PHY_CODED | BT_GAP_LE_PHY_1M, - .pref_rx_phy = BT_GAP_LE_PHY_CODED | BT_GAP_LE_PHY_1M, - }; - phy_override_sent = true; - int err = bt_conn_le_phy_update(conn, &phy_pref); - if (err) { - LOG_WRN("PHY override failed: %d, requesting DLE", err); - } else { - LOG_INF("Requested Coded|1M PHY (overriding 2M)"); - return; /* DLE when this callback fires again */ - } - } - - /* PHY is settled — request DLE. Deferred here from connected() + /* Accept whatever PHY the phone chose — overriding 2M with Coded|1M + * caused iPhone to freeze the connection (and the whole node). + * PHY is settled — request DLE. Deferred here from connected() * because the phone starts a PHY procedure on connect and BLE * only allows one LL procedure at a time. */ request_dle(conn); @@ -1017,3 +992,27 @@ void zephcore_ble_disconnect(void) bt_conn_disconnect(current_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); } } + +void zephcore_ble_conn_params_ready(void) +{ + if (!conn_params_pending || !current_conn) { + return; + } + conn_params_pending = false; + + struct bt_le_conn_param conn_param = { + .interval_min = BLE_DEFAULT_MIN_INTERVAL, + .interval_max = BLE_DEFAULT_MAX_INTERVAL, + .latency = BLE_DEFAULT_LATENCY, + .timeout = BLE_DEFAULT_TIMEOUT, + }; + int err = bt_conn_le_param_update(current_conn, &conn_param); + if (err) { + LOG_WRN("Post-sync conn param update failed: %d", err); + } else { + LOG_INF("Post-sync conn params: %d-%dms interval, latency=%d", + BLE_DEFAULT_MIN_INTERVAL * 5 / 4, + BLE_DEFAULT_MAX_INTERVAL * 5 / 4, + BLE_DEFAULT_LATENCY); + } +} diff --git a/zephcore/adapters/ble/ZephyrBLE.h b/zephcore/adapters/ble/ZephyrBLE.h index aab05b1..ac3b3a2 100644 --- a/zephcore/adapters/ble/ZephyrBLE.h +++ b/zephcore/adapters/ble/ZephyrBLE.h @@ -111,6 +111,14 @@ void zephcore_ble_kick_tx(void); */ void zephcore_ble_disconnect(void); +/** + * Apply deferred connection parameters. + * Call after the initial app sync is complete (channels + contacts + + * offline messages) so the param negotiation doesn't disrupt throughput + * during the sync burst. + */ +void zephcore_ble_conn_params_ready(void); + #ifdef __cplusplus } #endif diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp index 32900fa..1e306e5 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.cpp +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -1,6 +1,19 @@ /* * SPDX-License-Identifier: Apache-2.0 * Zephyr DataStore - LittleFS-backed persistence with optional QSPI flash + * + * Universal mount strategy: + * nRF52: Manual mount /efs (ExtraFS, 100KB) + /ifs (InternalFS, 28KB) + * with block_size=128 and custom erase for Arduino MeshCore compat. + * Others: DTS-automounted /lfs (single partition). + * QSPI: /ext overrides contacts mount when available. + * + * Data format (all platforms): + * contacts3 — 152-byte records (Arduino-compatible) + * new_prefs — Arduino-compatible byte layout + * channels2 — 68-byte records (identical to Arduino) + * _main.id — identity blob + * adv_blobs — fixed record array */ #include "ZephyrDataStore.h" @@ -10,6 +23,9 @@ #include #include +/* Custom 128B-block erase — defined in lfs_128b_erase.c */ +extern "C" int lfs_128b_erase(const struct lfs_config *c, lfs_block_t block); + #include LOG_MODULE_REGISTER(zephcore_datastore, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); @@ -22,9 +38,16 @@ struct BlobRec { uint8_t data[MAX_ADVERT_PKT_LEN]; }; -/* Track mount status - filesystems are automounted via DTS fstab */ -static bool lfs_mounted; -static bool ext_lfs_mounted; +/* ── Mount state ────────────────────────────────────────────────────── */ + +/* Resolved at mount time */ +const char *ZephyrDataStore::_contacts_mnt = nullptr; +const char *ZephyrDataStore::_prefs_mnt = nullptr; + +static bool efs_mounted; /* /efs (nRF52 ExtraFS) or false */ +static bool ifs_mounted; /* /ifs (nRF52 InternalFS) or false */ +static bool lfs_mounted; /* /lfs (DTS automount) or false */ +static bool ext_lfs_mounted; /* /ext (QSPI) */ /* Check if a filesystem is mounted using fs_statvfs */ static bool is_mounted(const char *mount_point) @@ -33,28 +56,131 @@ static bool is_mounted(const char *mount_point) return fs_statvfs(mount_point, &stat) == 0; } +/* ── nRF52 dual-mount: ExtraFS + InternalFS (block_size=128) ───────── */ + +#if FIXED_PARTITION_EXISTS(extrafs_partition) && FIXED_PARTITION_EXISTS(internalfs_partition) +#define HAS_NRF52_DUAL_MOUNT 1 + +/* Arduino-compatible LFS: block_size=128 */ +FS_LITTLEFS_DECLARE_CUSTOM_CONFIG(extrafs_data, + 4, /* alignment */ + 16, /* read_size */ + 16, /* prog_size */ + 128, /* cache_size = block_size */ + 128 /* lookahead_size */ +); + +FS_LITTLEFS_DECLARE_CUSTOM_CONFIG(internalfs_data, + 4, /* alignment */ + 16, /* read_size */ + 16, /* prog_size */ + 128, /* cache_size = block_size */ + 128 /* lookahead_size */ +); + +static struct fs_mount_t extrafs_mnt = { + .type = FS_LITTLEFS, + .mnt_point = "/efs", + .fs_data = &extrafs_data, + .storage_dev = (void *)FIXED_PARTITION_ID(extrafs_partition), + .flags = 0, +}; + +static struct fs_mount_t internalfs_mnt = { + .type = FS_LITTLEFS, + .mnt_point = "/ifs", + .fs_data = &internalfs_data, + .storage_dev = (void *)FIXED_PARTITION_ID(internalfs_partition), + .flags = 0, +}; + +static bool try_mount_nrf52_dual() +{ + int rc; + + /* Set custom block_size and erase callback before mount. + * The Zephyr LFS patch (0007) preserves pre-set erase callbacks. */ + extrafs_data.cfg.block_size = 128; + extrafs_data.cfg.erase = lfs_128b_erase; + + internalfs_data.cfg.block_size = 128; + internalfs_data.cfg.erase = lfs_128b_erase; + + /* Mount ExtraFS — contacts, channels, blobs */ + rc = fs_mount(&extrafs_mnt); + if (rc < 0) { + LOG_ERR("ExtraFS mount failed: %d — formatting", rc); + const struct flash_area *fap; + if (flash_area_open(FIXED_PARTITION_ID(extrafs_partition), &fap) == 0) { + flash_area_flatten(fap, 0, fap->fa_size); + flash_area_close(fap); + } + rc = fs_mount(&extrafs_mnt); + if (rc < 0) { + LOG_ERR("ExtraFS mount failed after format: %d", rc); + return false; + } + } + efs_mounted = true; + LOG_INF("ExtraFS at /efs (100KB, 128B blocks)"); + + /* Mount InternalFS — prefs, identity, BLE settings */ + rc = fs_mount(&internalfs_mnt); + if (rc < 0) { + LOG_ERR("InternalFS mount failed: %d — formatting", rc); + const struct flash_area *fap; + if (flash_area_open(FIXED_PARTITION_ID(internalfs_partition), &fap) == 0) { + flash_area_flatten(fap, 0, fap->fa_size); + flash_area_close(fap); + } + rc = fs_mount(&internalfs_mnt); + if (rc < 0) { + LOG_ERR("InternalFS mount failed after format: %d", rc); + return false; + } + } + ifs_mounted = true; + LOG_INF("InternalFS at /ifs (28KB, 128B blocks)"); + + return true; +} + +#else +#define HAS_NRF52_DUAL_MOUNT 0 +static bool try_mount_nrf52_dual() { return false; } +#endif /* extrafs_partition && internalfs_partition */ + +/* ── Universal mount ───────────────────────────────────────────────── */ + bool ZephyrDataStore::mount() { - if (lfs_mounted) { - return true; + if (_contacts_mnt != nullptr) { + return true; /* already mounted */ } - /* Check if internal LFS was automounted */ - if (is_mounted(mountPoint())) { + /* 1. Try nRF52 dual-mount (manual, block_size=128) */ + if (try_mount_nrf52_dual()) { + _contacts_mnt = "/efs"; + _prefs_mnt = "/ifs"; + LOG_INF("nRF52 dual-mount: contacts=/efs, prefs=/ifs"); + } + /* 2. Fallback: DTS automount at /lfs (ESP32, MG24, nRF54L) */ + else if (is_mounted("/lfs")) { + _contacts_mnt = "/lfs"; + _prefs_mnt = "/lfs"; lfs_mounted = true; - LOG_INF("Internal LittleFS at %s (automounted)", mountPoint()); - } else { - LOG_ERR("Internal LittleFS NOT mounted at %s - check DTS fstab!", mountPoint()); + LOG_INF("Single-mount: contacts=/lfs, prefs=/lfs (DTS automount)"); + } + else { + LOG_ERR("No filesystem mounted!"); return false; } - /* Check if external QSPI was automounted */ - if (is_mounted(extMountPoint())) { + /* 3. QSPI /ext override (optional, any platform) */ + if (is_mounted(EXT_MNT_POINT)) { ext_lfs_mounted = true; - LOG_INF("External QSPI LittleFS at %s (automounted, 100 blobs)", extMountPoint()); - } else { - ext_lfs_mounted = false; - LOG_WRN("External QSPI NOT mounted at %s - using internal only (20 blobs)", extMountPoint()); + _contacts_mnt = EXT_MNT_POINT; + LOG_INF("QSPI mounted at /ext — contacts redirected to QSPI"); } return true; @@ -62,11 +188,60 @@ bool ZephyrDataStore::mount() void ZephyrDataStore::unmount() { - /* With automount, filesystems are managed by Zephyr - just clear our flags */ +#if HAS_NRF52_DUAL_MOUNT + if (efs_mounted) { + fs_unmount(&extrafs_mnt); + efs_mounted = false; + } + if (ifs_mounted) { + fs_unmount(&internalfs_mnt); + ifs_mounted = false; + } +#endif lfs_mounted = false; ext_lfs_mounted = false; + _contacts_mnt = nullptr; + _prefs_mnt = nullptr; } +/* ── Path helpers ──────────────────────────────────────────────────── */ + +static char path_buf[3][48]; /* reusable path buffers */ + +const char *ZephyrDataStore::contactsFile() const +{ + snprintf(path_buf[0], sizeof(path_buf[0]), "%s/contacts3", _contacts_mnt); + return path_buf[0]; +} + +const char *ZephyrDataStore::channelsFile() const +{ + snprintf(path_buf[1], sizeof(path_buf[1]), "%s/channels2", _contacts_mnt); + return path_buf[1]; +} + +const char *ZephyrDataStore::advBlobsFile() const +{ + snprintf(path_buf[2], sizeof(path_buf[2]), "%s/adv_blobs", _contacts_mnt); + return path_buf[2]; +} + +const char *ZephyrDataStore::prefsFile() +{ + static char buf[48]; + snprintf(buf, sizeof(buf), "%s/new_prefs", _prefs_mnt); + return buf; +} + +const char *ZephyrDataStore::identityFile() +{ + static char buf[48]; + snprintf(buf, sizeof(buf), "%s/_main.id", _prefs_mnt); + return buf; +} + +/* ── Init ──────────────────────────────────────────────────────────── */ + ZephyrDataStore::ZephyrDataStore(mesh::RTCClock &clock) : _clock(&clock), _has_ext_fs(false) { @@ -75,43 +250,17 @@ ZephyrDataStore::ZephyrDataStore(mesh::RTCClock &clock) void ZephyrDataStore::begin() { _has_ext_fs = ext_lfs_mounted; - LOG_INF("_has_ext_fs=%d (ext_lfs_mounted=%d)", _has_ext_fs ? 1 : 0, ext_lfs_mounted ? 1 : 0); - LOG_INF("contacts path=%s, channels path=%s", contactsFile(), channelsFile()); + LOG_INF("_has_ext_fs=%d, contacts_mnt=%s, prefs_mnt=%s", + _has_ext_fs ? 1 : 0, _contacts_mnt, _prefs_mnt); + LOG_INF("contacts=%s, prefs=%s", contactsFile(), prefsFile()); if (_has_ext_fs) { migrateToExternalFS(); } - /* Clean up stale .tmp files from interrupted saves. - * If a .tmp file exists, the save was interrupted before the - * atomic rename — the original file is still intact. */ - cleanStaleTmpFiles(); - checkAdvBlobFile(); } -void ZephyrDataStore::cleanStaleTmpFiles() -{ - /* If a .tmp file exists, a save was interrupted before the atomic - * rename completed. Since we never unlink the original before - * rename, the original file is guaranteed intact. The .tmp may - * be partial (crash during write) or complete (crash between - * close and rename). Either way, delete it — we keep the last - * successfully-committed version. Worst case: we lose the one - * save that was in progress, but never lose ALL data. */ - const char *paths[] = { contactsFile(), channelsFile(), advBlobsFile() }; - char tmp_path[48]; - for (size_t i = 0; i < ARRAY_SIZE(paths); i++) { - snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", paths[i]); - struct fs_dirent ent; - if (fs_stat(tmp_path, &ent) == 0) { - LOG_INF("PREVIOUS REBOOT CORRUPTED FS! " - "Deleting temp file: %s (%zu bytes)", - tmp_path, ent.size); - fs_unlink(tmp_path); - } - } -} bool ZephyrDataStore::exists(const char *path) { @@ -128,8 +277,7 @@ bool ZephyrDataStore::openRead(const char *path, uint8_t *buf, size_t buf_sz, si { struct fs_file_t file; fs_file_t_init(&file); - int rc = fs_open(&file, path, FS_O_READ); - if (rc < 0) { + if (fs_open(&file, path, FS_O_READ) < 0) { return false; } ssize_t n = fs_read(&file, buf, buf_sz); @@ -143,13 +291,12 @@ bool ZephyrDataStore::openRead(const char *path, uint8_t *buf, size_t buf_sz, si bool ZephyrDataStore::openWrite(const char *path, const uint8_t *buf, size_t len) { - fs_unlink(path); - struct fs_file_t file; fs_file_t_init(&file); - int rc = fs_open(&file, path, FS_O_CREATE | FS_O_WRITE); - if (rc < 0) { - LOG_ERR("openWrite: fs_open(%s) failed: %d", path, rc); + if (exists(path)) { + fs_unlink(path); + } + if (fs_open(&file, path, FS_O_CREATE | FS_O_WRITE) < 0) { return false; } ssize_t n = fs_write(&file, buf, len); @@ -188,33 +335,48 @@ bool ZephyrDataStore::copyFile(const char *src, const char *dst) return ok && n >= 0; } +/* ── QSPI migration ───────────────────────────────────────────────── */ + void ZephyrDataStore::migrateToExternalFS() { - /* Migrate contacts from internal to external if not present */ - if (!exists(EXT_CONTACTS_FILE) && exists(INT_CONTACTS_FILE)) { - LOG_INF("Migrating contacts to external storage"); - if (copyFile(INT_CONTACTS_FILE, EXT_CONTACTS_FILE)) { - removeFile(INT_CONTACTS_FILE); + /* Build internal paths (contacts are on the non-QSPI internal mount). + * On nRF52: /efs/contacts3, on others: /lfs/contacts3 */ + const char *int_mnt = efs_mounted ? "/efs" : "/lfs"; + char int_contacts[48], ext_contacts[48]; + char int_channels[48], ext_channels[48]; + char int_blobs[48], ext_blobs[48]; + + snprintf(int_contacts, sizeof(int_contacts), "%s/contacts3", int_mnt); + snprintf(ext_contacts, sizeof(ext_contacts), "%s/contacts3", EXT_MNT_POINT); + snprintf(int_channels, sizeof(int_channels), "%s/channels2", int_mnt); + snprintf(ext_channels, sizeof(ext_channels), "%s/channels2", EXT_MNT_POINT); + snprintf(int_blobs, sizeof(int_blobs), "%s/adv_blobs", int_mnt); + snprintf(ext_blobs, sizeof(ext_blobs), "%s/adv_blobs", EXT_MNT_POINT); + + /* Migrate contacts */ + if (!exists(ext_contacts) && exists(int_contacts)) { + LOG_INF("Migrating contacts to QSPI"); + if (copyFile(int_contacts, ext_contacts)) { + removeFile(int_contacts); } } - /* Migrate channels from internal to external if not present */ - if (!exists(EXT_CHANNELS_FILE) && exists(INT_CHANNELS_FILE)) { - LOG_INF("Migrating channels to external storage"); - if (copyFile(INT_CHANNELS_FILE, EXT_CHANNELS_FILE)) { - removeFile(INT_CHANNELS_FILE); + /* Migrate channels */ + if (!exists(ext_channels) && exists(int_channels)) { + LOG_INF("Migrating channels to QSPI"); + if (copyFile(int_channels, ext_channels)) { + removeFile(int_channels); } } - /* Migrate adv_blobs - need special handling for size change */ - if (!exists(EXT_ADV_BLOBS_FILE) && exists(INT_ADV_BLOBS_FILE)) { - LOG_INF("Migrating adv_blobs to external storage (20 -> 100 slots)"); - if (copyFile(INT_ADV_BLOBS_FILE, EXT_ADV_BLOBS_FILE)) { - removeFile(INT_ADV_BLOBS_FILE); - /* Extend blob file to 100 records */ + /* Migrate adv_blobs (extend to 100 records) */ + if (!exists(ext_blobs) && exists(int_blobs)) { + LOG_INF("Migrating adv_blobs to QSPI (20 -> 100 slots)"); + if (copyFile(int_blobs, ext_blobs)) { + removeFile(int_blobs); struct fs_file_t file; fs_file_t_init(&file); - if (fs_open(&file, EXT_ADV_BLOBS_FILE, FS_O_RDWR) == 0) { + if (fs_open(&file, ext_blobs, FS_O_RDWR) == 0) { fs_seek(&file, 0, FS_SEEK_END); BlobRec zeroes; memset(&zeroes, 0, sizeof(zeroes)); @@ -227,14 +389,14 @@ void ZephyrDataStore::migrateToExternalFS() } /* Clean up old files on internal if they exist on external */ - if (exists(EXT_CONTACTS_FILE) && exists(INT_CONTACTS_FILE)) { - removeFile(INT_CONTACTS_FILE); + if (exists(ext_contacts) && exists(int_contacts)) { + removeFile(int_contacts); } - if (exists(EXT_CHANNELS_FILE) && exists(INT_CHANNELS_FILE)) { - removeFile(INT_CHANNELS_FILE); + if (exists(ext_channels) && exists(int_channels)) { + removeFile(int_channels); } - if (exists(EXT_ADV_BLOBS_FILE) && exists(INT_ADV_BLOBS_FILE)) { - removeFile(INT_ADV_BLOBS_FILE); + if (exists(ext_blobs) && exists(int_blobs)) { + removeFile(int_blobs); } } @@ -253,14 +415,15 @@ void ZephyrDataStore::checkAdvBlobFile() LOG_ERR("Failed to create adv_blobs file: %d", rc); return; } - int count = maxBlobRecs(); - for (int i = 0; i < count; i++) { + int recs = maxBlobRecs(); + for (int i = 0; i < recs; i++) { fs_write(&file, &zeroes, sizeof(zeroes)); } fs_close(&file); - LOG_INF("Created adv_blobs with %d slots", count); } +/* ── Format / Factory Reset ────────────────────────────────────────── */ + bool ZephyrDataStore::formatFileSystem() { LOG_INF("formatFileSystem: starting..."); @@ -269,72 +432,78 @@ bool ZephyrDataStore::formatFileSystem() const struct flash_area *fap; int rc; - /* Format internal LittleFS partition */ - LOG_INF("formatFileSystem: opening lfs_partition..."); - rc = flash_area_open(FIXED_PARTITION_ID(lfs_partition), &fap); - if (rc < 0) { - LOG_ERR("Failed to open lfs_partition: rc=%d", rc); - return false; - } - LOG_INF("formatFileSystem: lfs_partition opened, size=%u, erasing...", (unsigned)fap->fa_size); - rc = flash_area_flatten(fap, 0, fap->fa_size); - flash_area_close(fap); - if (rc < 0) { - LOG_ERR("Failed to format lfs_partition: rc=%d", rc); - return false; - } - LOG_INF("Formatted internal LittleFS (92KB)"); - - /* Format NVS storage partition (BLE bonds) */ - LOG_INF("formatFileSystem: opening storage_partition (NVS/bonds)..."); - rc = flash_area_open(FIXED_PARTITION_ID(storage_partition), &fap); - if (rc < 0) { - LOG_ERR("Failed to open storage_partition: rc=%d", rc); - return false; - } - LOG_INF("formatFileSystem: storage_partition opened, size=%u, erasing...", (unsigned)fap->fa_size); - rc = flash_area_flatten(fap, 0, fap->fa_size); - flash_area_close(fap); - if (rc < 0) { - LOG_ERR("Failed to format storage_partition: rc=%d", rc); - return false; - } - LOG_INF("Formatted NVS storage (8KB) - BLE bonds cleared"); - - /* NOTE: We intentionally do NOT erase the bootloader area (0xED000-end). - * Storage partition (0xD4000-0xD6000) contains BLE bonds and IS erased. */ - -#if FIXED_PARTITION_EXISTS(qspi_storage_partition) - /* Format external QSPI flash if present */ - LOG_INF("formatFileSystem: opening qspi_storage_partition..."); - rc = flash_area_open(FIXED_PARTITION_ID(qspi_storage_partition), &fap); +#if FIXED_PARTITION_EXISTS(extrafs_partition) + /* nRF52: format ExtraFS */ + rc = flash_area_open(FIXED_PARTITION_ID(extrafs_partition), &fap); if (rc == 0) { - LOG_INF("formatFileSystem: QSPI partition opened, size=%u, erasing (this may take a while)...", (unsigned)fap->fa_size); - rc = flash_area_flatten(fap, 0, fap->fa_size); + LOG_INF("Formatting ExtraFS (%u bytes)", (unsigned)fap->fa_size); + flash_area_flatten(fap, 0, fap->fa_size); + flash_area_close(fap); + } +#endif + +#if FIXED_PARTITION_EXISTS(internalfs_partition) + /* nRF52: format InternalFS */ + rc = flash_area_open(FIXED_PARTITION_ID(internalfs_partition), &fap); + if (rc == 0) { + LOG_INF("Formatting InternalFS (%u bytes)", (unsigned)fap->fa_size); + flash_area_flatten(fap, 0, fap->fa_size); + flash_area_close(fap); + } +#endif + +#if FIXED_PARTITION_EXISTS(lfs_partition) + /* Non-nRF52: format single LFS partition */ + rc = flash_area_open(FIXED_PARTITION_ID(lfs_partition), &fap); + if (rc == 0) { + LOG_INF("Formatting LFS partition (%u bytes)", (unsigned)fap->fa_size); + flash_area_flatten(fap, 0, fap->fa_size); + flash_area_close(fap); + } +#endif + +#if FIXED_PARTITION_EXISTS(storage_partition) + /* Non-nRF52: format NVS storage (BLE bonds) */ + rc = flash_area_open(FIXED_PARTITION_ID(storage_partition), &fap); + if (rc == 0) { + LOG_INF("Formatting NVS storage (%u bytes)", (unsigned)fap->fa_size); + flash_area_flatten(fap, 0, fap->fa_size); + flash_area_close(fap); + } +#endif + +#if FIXED_PARTITION_EXISTS(qspi_storage_partition) + /* QSPI if present (any platform) */ + rc = flash_area_open(FIXED_PARTITION_ID(qspi_storage_partition), &fap); + if (rc == 0) { + LOG_INF("Formatting QSPI (%u bytes, may take a while)", (unsigned)fap->fa_size); + flash_area_flatten(fap, 0, fap->fa_size); flash_area_close(fap); - if (rc < 0) { - LOG_ERR("Failed to format external QSPI storage: rc=%d", rc); - } else { - LOG_INF("Formatted external QSPI storage"); - } - } else { - LOG_WRN("formatFileSystem: couldn't open qspi_storage_partition: rc=%d (may not be present)", rc); } -#else - LOG_INF("formatFileSystem: no qspi_storage_partition defined, skipping QSPI"); #endif - LOG_INF("formatFileSystem: calling mount()..."); bool mounted = mount(); LOG_INF("formatFileSystem: mount() returned %d", mounted ? 1 : 0); return mounted; } +void ZephyrDataStore::factoryReset() +{ + LOG_INF("=== FACTORY RESET STARTING ==="); + if (formatFileSystem()) { + LOG_INF("=== FACTORY RESET COMPLETE - REBOOT REQUIRED ==="); + } else { + LOG_ERR("=== FACTORY RESET FAILED ==="); + } +} + +/* ── Identity ──────────────────────────────────────────────────────── */ + bool ZephyrDataStore::loadMainIdentity(mesh::LocalIdentity &identity) { uint8_t buf[PRV_KEY_SIZE + PUB_KEY_SIZE + 32]; size_t len = 0; - if (!openRead(MAIN_ID_FILE, buf, sizeof(buf), len) || len < PRV_KEY_SIZE + PUB_KEY_SIZE) { + if (!openRead(identityFile(), buf, sizeof(buf), len) || len < PRV_KEY_SIZE + PUB_KEY_SIZE) { return false; } return identity.readFrom(buf, len); @@ -347,19 +516,25 @@ bool ZephyrDataStore::saveMainIdentity(const mesh::LocalIdentity &identity) if (n == 0) { return false; } - return openWrite(MAIN_ID_FILE, buf, n); + return openWrite(identityFile(), buf, n); } +/* ── Preferences (Arduino-compatible layout) ───────────────────────── */ + void ZephyrDataStore::loadPrefs(NodePrefs &prefs) { - bool prefs_exists = exists(PREFS_FILE); - LOG_INF("loadPrefs: exists(%s)=%d", PREFS_FILE, prefs_exists ? 1 : 0); - const char *path = prefs_exists ? PREFS_FILE : "/lfs/node_prefs"; - LOG_INF("loadPrefs: trying path=%s", path); + const char *path = prefsFile(); + bool prefs_exists = exists(path); + LOG_INF("loadPrefs: exists(%s)=%d", path, prefs_exists ? 1 : 0); + if (!prefs_exists) { + LOG_WRN("loadPrefs: no prefs file found"); + return; + } + uint8_t buf[256]; size_t len = 0; if (!openRead(path, buf, sizeof(buf), len)) { - LOG_WRN("loadPrefs: file not found or read failed"); + LOG_WRN("loadPrefs: read failed"); return; } if (len < 88) { @@ -367,6 +542,7 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs) return; } LOG_INF("loadPrefs: loaded %d bytes from %s", (int)len, path); + size_t off = 0; memcpy(&prefs.airtime_factor, &buf[off], sizeof(float)); off += 4; @@ -375,7 +551,7 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs) prefs.airtime_factor *= 10.0f; } memcpy(prefs.node_name, &buf[off], 32); - off += 36; + off += 36; /* 32 name + 4 pad */ memcpy(&prefs.node_lat, &buf[off], sizeof(double)); off += 8; memcpy(&prefs.node_lon, &buf[off], sizeof(double)); @@ -384,7 +560,8 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs) off += 4; prefs.sf = buf[off++]; prefs.cr = buf[off++]; - off++; + /* Offset 62: client_repeat (Arduino-compatible placement) */ + prefs.client_repeat = buf[off++]; prefs.manual_add_contacts = buf[off++]; memcpy(&prefs.bw, &buf[off], sizeof(float)); off += 4; @@ -396,8 +573,9 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs) off += 4; prefs.advert_loc_policy = buf[off++]; prefs.multi_acks = buf[off++]; + /* Offset 78: path_hash_mode (Arduino treats as pad — harmless) */ prefs.path_hash_mode = buf[off++]; - off += 1; + off += 1; /* pad */ memcpy(&prefs.ble_pin, &buf[off], sizeof(uint32_t)); off += 4; prefs.buzzer_quiet = buf[off++]; @@ -406,24 +584,12 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs) off += 4; prefs.autoadd_config = buf[off++]; - /* rx_boost - added later, may not exist in old files */ + /* Offset 91: rx_boost (ZephCore extension — Arduino stops at 91 bytes) */ if (off < len) { prefs.rx_boost = buf[off++]; } else { prefs.rx_boost = 1; /* Default to boosted for better sensitivity */ } - - /* client_repeat (offgrid mode) - added v9, may not exist in old files */ - if (off < len) { - prefs.client_repeat = buf[off++]; - } else { - prefs.client_repeat = 0; /* Default: disabled (companion only) */ - } - - if (strcmp(path, "/lfs/node_prefs") == 0) { - savePrefs(prefs); - removeFile("/lfs/node_prefs"); - } } void ZephyrDataStore::savePrefs(const NodePrefs &prefs) @@ -445,7 +611,8 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs) off += 4; buf[off++] = prefs.sf; buf[off++] = prefs.cr; - buf[off++] = 0; + /* Offset 62: client_repeat (Arduino-compatible placement) */ + buf[off++] = prefs.client_repeat; buf[off++] = prefs.manual_add_contacts; memcpy(&buf[off], &prefs.bw, sizeof(float)); off += 4; @@ -457,8 +624,9 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs) off += 4; buf[off++] = prefs.advert_loc_policy; buf[off++] = prefs.multi_acks; + /* Offset 78: path_hash_mode (Arduino treats as pad — harmless) */ buf[off++] = prefs.path_hash_mode; - buf[off++] = 0; // pad + buf[off++] = 0; /* pad */ memcpy(&buf[off], &prefs.ble_pin, sizeof(uint32_t)); off += 4; buf[off++] = prefs.buzzer_quiet; @@ -466,66 +634,91 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs) memcpy(&buf[off], &prefs.gps_interval, sizeof(uint32_t)); off += 4; buf[off++] = prefs.autoadd_config; + /* Offset 91: rx_boost (ZephCore extension — Arduino ignores) */ buf[off++] = prefs.rx_boost; - buf[off++] = prefs.client_repeat; - bool ok = openWrite(PREFS_FILE, buf, off); - LOG_INF("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'", PREFS_FILE, ok ? 1 : 0, (int)off, prefs.node_name); + /* Total: 92 bytes (Arduino reads 91, ZephCore reads 92) */ + + bool ok = openWrite(prefsFile(), buf, off); + LOG_INF("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'", + prefsFile(), ok ? 1 : 0, (int)off, prefs.node_name); +} + +/* ── Contacts: contacts3 (152B records, Arduino-compatible) ────────── */ + +static constexpr size_t CONTACT_DATA_SZ = 152; /* 32+32+1+1+1+4+1+4+64+4+4+4 */ + +/* Pack a ContactInfo into the 152-byte wire format (Arduino contacts3) */ +static void contact_to_record(const ContactInfo &c, uint8_t rec[CONTACT_DATA_SZ]) +{ + uint8_t *p = rec; + uint8_t unused = 0; + memcpy(p, c.id.pub_key, 32); p += 32; + memcpy(p, c.name, 32); p += 32; + *p++ = c.type; + *p++ = c.flags; + *p++ = unused; + memcpy(p, &c.sync_since, 4); p += 4; + *p++ = c.out_path_len; + memcpy(p, &c.last_advert_timestamp, 4); p += 4; + memcpy(p, c.out_path, 64); p += 64; + memcpy(p, &c.lastmod, 4); p += 4; + memcpy(p, &c.gps_lat, 4); p += 4; + memcpy(p, &c.gps_lon, 4); p += 4; +} + +/* Unpack 152-byte wire format into a ContactInfo */ +static void record_to_contact(const uint8_t rec[CONTACT_DATA_SZ], ContactInfo &c) +{ + const uint8_t *p = rec; + uint8_t pub_key[32]; + uint8_t unused; + memcpy(pub_key, p, 32); p += 32; + memcpy(c.name, p, 32); p += 32; + c.type = *p++; + c.flags = *p++; + unused = *p++; (void)unused; + memcpy(&c.sync_since, p, 4); p += 4; + c.out_path_len = *p++; + memcpy(&c.last_advert_timestamp, p, 4); p += 4; + memcpy(c.out_path, p, 64); p += 64; + memcpy(&c.lastmod, p, 4); p += 4; + memcpy(&c.gps_lat, p, 4); p += 4; + memcpy(&c.gps_lon, p, 4); p += 4; + c.id = mesh::Identity(pub_key); + c.shared_secret_valid = false; } void ZephyrDataStore::loadContacts(DataStoreHost *host) { const char *path = contactsFile(); - LOG_INF("loadContacts: path=%s (_has_ext_fs=%d)", path, _has_ext_fs ? 1 : 0); - - /* Check if file exists and its size */ - struct fs_dirent ent; - if (fs_stat(path, &ent) == 0) { - LOG_INF("loadContacts: file exists, size=%zu bytes", ent.size); - } else { - LOG_WRN("loadContacts: file does not exist at %s", path); - } + LOG_INF("loadContacts: path=%s", path); struct fs_file_t file; fs_file_t_init(&file); int rc = fs_open(&file, path, FS_O_READ); if (rc < 0) { - LOG_WRN("loadContacts: fs_open failed (rc=%d)", rc); + LOG_WRN("loadContacts: no contacts file found"); return; } + uint32_t count = 0; + uint8_t rec[CONTACT_DATA_SZ]; + for (;;) { + ssize_t n = fs_read(&file, rec, CONTACT_DATA_SZ); + if (n <= 0) break; + if (n != (ssize_t)CONTACT_DATA_SZ) { + LOG_WRN("loadContacts: truncated record at #%u (%d bytes)", + count, (int)n); + break; + } + ContactInfo c; - uint8_t pub_key[32]; - uint8_t unused; - ssize_t n = fs_read(&file, pub_key, 32); - if (n != 32) break; - n = fs_read(&file, (uint8_t *)&c.name, 32); - if (n != 32) break; - n = fs_read(&file, &c.type, 1); - if (n != 1) break; - n = fs_read(&file, &c.flags, 1); - if (n != 1) break; - n = fs_read(&file, &unused, 1); - if (n != 1) break; - n = fs_read(&file, (uint8_t *)&c.sync_since, 4); - if (n != 4) break; - n = fs_read(&file, (uint8_t *)&c.out_path_len, 1); - if (n != 1) break; - n = fs_read(&file, (uint8_t *)&c.last_advert_timestamp, 4); - if (n != 4) break; - n = fs_read(&file, c.out_path, 64); - if (n != 64) break; - n = fs_read(&file, (uint8_t *)&c.lastmod, 4); - if (n != 4) break; - n = fs_read(&file, (uint8_t *)&c.gps_lat, 4); - if (n != 4) break; - n = fs_read(&file, (uint8_t *)&c.gps_lon, 4); - if (n != 4) break; - c.id = mesh::Identity(pub_key); - c.shared_secret_valid = false; + record_to_contact(rec, c); if (!host->onContactLoaded(c)) break; count++; } + fs_close(&file); LOG_INF("loadContacts: loaded %u contacts from %s", count, path); } @@ -533,70 +726,42 @@ void ZephyrDataStore::loadContacts(DataStoreHost *host) void ZephyrDataStore::saveContacts(DataStoreHost *host) { const char *path = contactsFile(); - LOG_INF("saveContacts: path=%s (_has_ext_fs=%d)", path, _has_ext_fs ? 1 : 0); + LOG_INF("saveContacts: path=%s", path); - /* Crash-safe write: write to .tmp, sync, then atomic rename. - * If power is lost before rename completes, the original file - * is still intact. Stale .tmp files are cleaned at boot. */ - char tmp_path[48]; - snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path); + if (exists(path)) { + fs_unlink(path); + } struct fs_file_t file; fs_file_t_init(&file); - if (exists(tmp_path)) { - fs_unlink(tmp_path); - } - int rc = fs_open(&file, tmp_path, FS_O_CREATE | FS_O_WRITE); + int rc = fs_open(&file, path, FS_O_CREATE | FS_O_WRITE); if (rc < 0) { - LOG_ERR("saveContacts: fs_open(%s) failed: %d", tmp_path, rc); + LOG_ERR("saveContacts: fs_open(%s) failed: %d", path, rc); return; } - /* Batch each contact into a single fs_write() to minimise LittleFS - * overhead. 12 individual writes per contact × hundreds of contacts - * was 2000+ fs_write() calls — each with CRC/metadata overhead. - * One write per contact cuts the time by ~60-70%. */ - static constexpr size_t REC_SZ = 152; /* 32+32+1+1+1+4+1+4+64+4+4+4 */ - uint8_t rec[REC_SZ]; + + uint8_t rec[CONTACT_DATA_SZ]; uint32_t idx = 0; ContactInfo c; - uint8_t unused = 0; + while (host->getContactForSave(idx, c)) { - uint8_t *p = rec; - memcpy(p, c.id.pub_key, 32); p += 32; - memcpy(p, c.name, 32); p += 32; - *p++ = c.type; - *p++ = c.flags; - *p++ = unused; - memcpy(p, &c.sync_since, 4); p += 4; - *p++ = c.out_path_len; - memcpy(p, &c.last_advert_timestamp, 4); p += 4; - memcpy(p, c.out_path, 64); p += 64; - memcpy(p, &c.lastmod, 4); p += 4; - memcpy(p, &c.gps_lat, 4); p += 4; - memcpy(p, &c.gps_lon, 4); p += 4; - if (fs_write(&file, rec, REC_SZ) != (ssize_t)REC_SZ) break; + contact_to_record(c, rec); + + if (fs_write(&file, rec, CONTACT_DATA_SZ) != (ssize_t)CONTACT_DATA_SZ) { + LOG_ERR("saveContacts: write failed at record %u", idx); + break; + } idx++; } - int sync_rc = fs_sync(&file); + + fs_sync(&file); fs_close(&file); - - if (sync_rc < 0) { - LOG_ERR("saveContacts: sync failed (%d), keeping old file", sync_rc); - fs_unlink(tmp_path); - return; - } - - /* Atomic replace — LittleFS 2 rename() replaces the destination - * in a single CRC-protected metadata commit. No unlink needed. - * Power loss at any point: either old file or new file, never empty. */ - rc = fs_rename(tmp_path, path); - if (rc < 0) { - LOG_ERR("saveContacts: rename %s → %s failed: %d", tmp_path, path, rc); - } else { - LOG_INF("saveContacts: saved %u contacts to %s", idx, path); - } + LOG_INF("saveContacts: saved %u contacts to %s (%u bytes)", + idx, path, idx * CONTACT_DATA_SZ); } +/* ── Channels ──────────────────────────────────────────────────────── */ + void ZephyrDataStore::loadChannels(DataStoreHost *host) { const char *path = channelsFile(); @@ -627,18 +792,16 @@ void ZephyrDataStore::loadChannels(DataStoreHost *host) void ZephyrDataStore::saveChannels(DataStoreHost *host) { const char *path = channelsFile(); + LOG_INF("saveChannels: path=%s", path); - /* Crash-safe write: write to .tmp, sync, then atomic rename. */ - char tmp_path[48]; - snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path); + if (exists(path)) { + fs_unlink(path); + } struct fs_file_t file; fs_file_t_init(&file); - if (exists(tmp_path)) { - fs_unlink(tmp_path); - } - if (fs_open(&file, tmp_path, FS_O_CREATE | FS_O_WRITE) < 0) { - LOG_ERR("saveChannels: fs_open(%s) failed", tmp_path); + if (fs_open(&file, path, FS_O_CREATE | FS_O_WRITE) < 0) { + LOG_ERR("saveChannels: fs_open(%s) failed", path); return; } uint8_t channel_idx = 0; @@ -650,25 +813,13 @@ void ZephyrDataStore::saveChannels(DataStoreHost *host) if (fs_write(&file, (uint8_t *)ch.channel.secret, 32) != 32) break; channel_idx++; } - int sync_rc = fs_sync(&file); + fs_sync(&file); fs_close(&file); - - if (sync_rc < 0) { - LOG_ERR("saveChannels: sync failed (%d), keeping old file", sync_rc); - fs_unlink(tmp_path); - return; - } - - /* Atomic replace — LittleFS 2 rename() replaces the destination - * in a single CRC-protected metadata commit. No unlink needed. */ - int rc = fs_rename(tmp_path, path); - if (rc < 0) { - LOG_ERR("saveChannels: rename %s → %s failed: %d", tmp_path, path, rc); - } else { - LOG_DBG("saveChannels: saved %u channels to %s", channel_idx, path); - } + LOG_INF("saveChannels: saved %u channels to %s", channel_idx, path); } +/* ── Blobs ─────────────────────────────────────────────────────────── */ + uint8_t ZephyrDataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { (void)key_len; @@ -735,13 +886,12 @@ bool ZephyrDataStore::deleteBlobByKey(const uint8_t key[], int key_len) struct fs_file_t file; fs_file_t_init(&file); if (fs_open(&file, path, FS_O_RDWR) < 0) { - return true; // no blob file = nothing to delete + return true; /* no blob file = nothing to delete */ } BlobRec tmp; uint32_t pos = 0; while (fs_read(&file, (uint8_t *)&tmp, sizeof(tmp)) == (ssize_t)sizeof(tmp)) { if (memcmp(key, tmp.key, 7) == 0) { - /* Zero out the record to mark as free */ memset(&tmp, 0, sizeof(tmp)); fs_seek(&file, pos, FS_SEEK_SET); fs_write(&file, (uint8_t *)&tmp, sizeof(tmp)); @@ -753,20 +903,23 @@ bool ZephyrDataStore::deleteBlobByKey(const uint8_t key[], int key_len) return true; } +/* ── Storage stats ─────────────────────────────────────────────────── */ + uint32_t ZephyrDataStore::getStorageUsedKb() const { struct fs_statvfs sbuf; uint32_t used = 0; - /* Internal flash */ - if (fs_statvfs(mountPoint(), &sbuf) == 0) { + /* Primary contacts mount (/efs, /lfs, or /ext) */ + if (_contacts_mnt && fs_statvfs(_contacts_mnt, &sbuf) == 0) { uint32_t total = sbuf.f_blocks * sbuf.f_frsize; uint32_t free = sbuf.f_bfree * sbuf.f_frsize; used += (total - free) / 1024; } - /* External flash if available */ - if (_has_ext_fs && fs_statvfs(extMountPoint(), &sbuf) == 0) { + /* Prefs mount (if different from contacts) */ + if (_prefs_mnt && _prefs_mnt != _contacts_mnt && + fs_statvfs(_prefs_mnt, &sbuf) == 0) { uint32_t total = sbuf.f_blocks * sbuf.f_frsize; uint32_t free = sbuf.f_bfree * sbuf.f_frsize; used += (total - free) / 1024; @@ -780,13 +933,11 @@ uint32_t ZephyrDataStore::getStorageTotalKb() const struct fs_statvfs sbuf; uint32_t total = 0; - /* Internal flash */ - if (fs_statvfs(mountPoint(), &sbuf) == 0) { + if (_contacts_mnt && fs_statvfs(_contacts_mnt, &sbuf) == 0) { total += (sbuf.f_blocks * sbuf.f_frsize) / 1024; } - - /* External flash if available */ - if (_has_ext_fs && fs_statvfs(extMountPoint(), &sbuf) == 0) { + if (_prefs_mnt && _prefs_mnt != _contacts_mnt && + fs_statvfs(_prefs_mnt, &sbuf) == 0) { total += (sbuf.f_blocks * sbuf.f_frsize) / 1024; } @@ -799,20 +950,8 @@ uint32_t ZephyrDataStore::getExternalStorageKb() const return 0; } struct fs_statvfs sbuf; - if (fs_statvfs(extMountPoint(), &sbuf) < 0) { + if (fs_statvfs(EXT_MNT_POINT, &sbuf) < 0) { return 0; } return (sbuf.f_blocks * sbuf.f_frsize) / 1024; } - -void ZephyrDataStore::factoryReset() -{ - LOG_INF("=== FACTORY RESET STARTING ==="); - - /* Format both internal and external (QSPI) filesystems */ - if (formatFileSystem()) { - LOG_INF("=== FACTORY RESET COMPLETE - REBOOT REQUIRED ==="); - } else { - LOG_ERR("=== FACTORY RESET FAILED ==="); - } -} diff --git a/zephcore/adapters/datastore/ZephyrDataStore.h b/zephcore/adapters/datastore/ZephyrDataStore.h index 87eff4b..abb83b3 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.h +++ b/zephcore/adapters/datastore/ZephyrDataStore.h @@ -1,6 +1,13 @@ /* * SPDX-License-Identifier: Apache-2.0 * Zephyr DataStore - LittleFS-backed persistence with optional QSPI flash + * + * Universal across all platforms (nRF52, ESP32, MG24, nRF54L). + * On nRF52, uses Arduino MeshCore-compatible dual-mount layout: + * /efs (ExtraFS @ 0xD4000, 100KB, block_size=128) — contacts3, channels2, blobs + * /ifs (InternalFS @ 0xED000, 28KB, block_size=128) — new_prefs, _main.id + * On other platforms, uses DTS-automounted /lfs for everything. + * QSPI /ext overrides contacts path when available (any platform). */ #pragma once @@ -47,36 +54,29 @@ public: static bool mount(); static void unmount(); - static const char *mountPoint() { return MNT_POINT; } - static const char *extMountPoint() { return EXT_MNT_POINT; } private: - /* Internal flash (always available) - identity, prefs */ - static constexpr const char *MNT_POINT = "/lfs"; - static constexpr const char *PREFS_FILE = "/lfs/new_prefs"; - static constexpr const char *MAIN_ID_FILE = "/lfs/_main.id"; + /* Mount points resolved at mount time: + * nRF52: _contacts_mnt="/efs", _prefs_mnt="/ifs" + * Others: _contacts_mnt="/lfs", _prefs_mnt="/lfs" + * QSPI: _contacts_mnt="/ext" (override) */ + static const char *_contacts_mnt; + static const char *_prefs_mnt; - /* External QSPI flash (optional) - contacts, channels, blobs */ + /* External QSPI flash (optional, any platform) */ static constexpr const char *EXT_MNT_POINT = "/ext"; - static constexpr const char *EXT_CONTACTS_FILE = "/ext/contacts3"; - static constexpr const char *EXT_CHANNELS_FILE = "/ext/channels2"; - static constexpr const char *EXT_ADV_BLOBS_FILE = "/ext/adv_blobs"; - - /* Fallback to internal if no external */ - static constexpr const char *INT_CONTACTS_FILE = "/lfs/contacts3"; - static constexpr const char *INT_CHANNELS_FILE = "/lfs/channels2"; - static constexpr const char *INT_ADV_BLOBS_FILE = "/lfs/adv_blobs"; mesh::RTCClock *_clock; bool _has_ext_fs; - /* Get path based on external availability */ - const char *contactsFile() const { return _has_ext_fs ? EXT_CONTACTS_FILE : INT_CONTACTS_FILE; } - const char *channelsFile() const { return _has_ext_fs ? EXT_CHANNELS_FILE : INT_CHANNELS_FILE; } - const char *advBlobsFile() const { return _has_ext_fs ? EXT_ADV_BLOBS_FILE : INT_ADV_BLOBS_FILE; } + /* Build full paths from resolved mount points */ + const char *contactsFile() const; + const char *channelsFile() const; + const char *advBlobsFile() const; + static const char *prefsFile(); + static const char *identityFile(); int maxBlobRecs() const { return _has_ext_fs ? 100 : 20; } - void cleanStaleTmpFiles(); void checkAdvBlobFile(); void migrateToExternalFS(); bool openRead(const char *path, uint8_t *buf, size_t buf_sz, size_t &out_len); diff --git a/zephcore/adapters/datastore/lfs_128b_erase.c b/zephcore/adapters/datastore/lfs_128b_erase.c new file mode 100644 index 0000000..0958622 --- /dev/null +++ b/zephcore/adapters/datastore/lfs_128b_erase.c @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Custom LittleFS erase callback for 128-byte blocks on 4KB-erase flash. + * + * Arduino MeshCore uses LFS with block_size=128 on nRF52840 which has + * 4096-byte erase granularity. The standard Zephyr erase callback calls + * flash_area_flatten(fa, offset, 128) which fails because the flash driver + * requires page-aligned (4096-byte) erases. + * + * This callback implements read-modify-erase-write: + * 1. Read the entire 4KB page containing the 128B block + * 2. Memset the 128B region to 0xFF (erased state) + * 3. Hardware-erase the 4KB page + * 4. Write back the modified page + */ + +#include +#include +#include +#include + +#define HW_ERASE_SIZE 4096 + +/* Static 4KB buffer for read-modify-erase-write. + * Both ExtraFS and InternalFS mounts are serialized by the Zephyr FS mutex, + * so this is never accessed concurrently. */ +static uint8_t page_buf[HW_ERASE_SIZE]; + +int lfs_128b_erase(const struct lfs_config *c, lfs_block_t block) +{ + const struct flash_area *fa = (const struct flash_area *)c->context; + int rc; + + /* Offset of this LFS block within the flash area */ + size_t block_offset = block * c->block_size; + + /* Align down to the 4KB page boundary (within flash area) */ + size_t page_offset = block_offset & ~((size_t)HW_ERASE_SIZE - 1); + size_t offset_in_page = block_offset - page_offset; + + /* 1. Read entire 4KB page */ + rc = flash_area_read(fa, page_offset, page_buf, HW_ERASE_SIZE); + if (rc < 0) { + return LFS_ERR_IO; + } + + /* 2. Set the 128B block region to erased state (0xFF) */ + memset(&page_buf[offset_in_page], 0xFF, c->block_size); + + /* 3. Hardware-erase the 4KB page */ + rc = flash_area_flatten(fa, page_offset, HW_ERASE_SIZE); + if (rc < 0) { + return LFS_ERR_IO; + } + + /* 4. Write back the modified page */ + rc = flash_area_write(fa, page_offset, page_buf, HW_ERASE_SIZE); + if (rc < 0) { + return LFS_ERR_IO; + } + + return LFS_ERR_OK; +} diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c b/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c index 36fbbb7..7440d13 100644 --- a/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c +++ b/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c @@ -57,7 +57,7 @@ static lr11xx_hal_status_t wait_on_busy(struct lr11xx_hal_context *ctx) gpio_pin_get_dt(&ctx->dio1)); return LR11XX_HAL_STATUS_ERROR; } - k_busy_wait(100); /* 100us */ + k_usleep(100); /* 100us — yields CPU so other threads can run */ loops++; } diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index c520f8f..97e0d1b 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -15,6 +15,7 @@ #include #include #include +#include LOG_MODULE_REGISTER(zephcore_companion, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); /* Protocol commands (matches Arduino companion_radio) - sorted by opcode */ @@ -313,7 +314,7 @@ void CompanionMesh::sendPacketError(uint8_t code) void CompanionMesh::sendPush(uint8_t code, const uint8_t *data, size_t len) { - LOG_INF("sendPush: code=0x%02x len=%u _push_cb=%s", code, (unsigned)len, _push_cb ? "set" : "NULL"); + LOG_DBG("sendPush: code=0x%02x len=%u _push_cb=%s", code, (unsigned)len, _push_cb ? "set" : "NULL"); if (_push_cb) { _push_cb(code, data, len); } else { @@ -1686,6 +1687,11 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) LOG_INF("CMD_SYNC_NEXT_MESSAGE: queue empty, sending NO_MORE_MSGS"); uint8_t rsp[] = { PACKET_NO_MORE_MSGS }; writeFrame(rsp, sizeof(rsp)); + + /* Initial sync is done — safe to apply deferred + * connection parameters now without disrupting + * channel/contact/message throughput. */ + zephcore_ble_conn_params_ready(); } return true; } diff --git a/zephcore/boards/common/filesystem.dtsi b/zephcore/boards/common/filesystem.dtsi index e2ab59b..3380a52 100644 --- a/zephcore/boards/common/filesystem.dtsi +++ b/zephcore/boards/common/filesystem.dtsi @@ -1,10 +1,22 @@ /* - * Common LittleFS filesystem configuration for ZephCore boards. - * Include this in board overlays to get standard /lfs mount. - * - * Requires board to define: &lfs_partition - * * SPDX-License-Identifier: Apache-2.0 + * Common LittleFS /lfs automount — for non-nRF52 boards + * + * See nrf52_partitions_sdv7.dtsi for full include guide. + * + * USE THIS FILE FOR: ESP32, MG24, nRF54L — any board with a single + * LFS partition and standard 4KB erase blocks. + * + * DO NOT USE FOR nRF52: Use nrf52_partitions_sdv6.dtsi or + * nrf52_partitions_sdv7.dtsi instead (dual-mount, 128B blocks). + * + * Requires board DTS to define: lfs_partition + * + * Usage in board DTS/overlay: + * #include "../../common/filesystem.dtsi" + * + * Optional add-on (if board has QSPI flash): + * #include "../../common/qspi-ext.dtsi" */ / { diff --git a/zephcore/boards/common/nrf52_common.conf b/zephcore/boards/common/nrf52_common.conf index e33e130..b606a4e 100644 --- a/zephcore/boards/common/nrf52_common.conf +++ b/zephcore/boards/common/nrf52_common.conf @@ -25,12 +25,24 @@ CONFIG_BT_CTLR_DATA_LENGTH_MAX=251 CONFIG_BT_CTLR_PHY_CODED=y # ========== Flash: nRF52-specific ========== +# Partial erase — splits 4KB page erase into small sub-erases that fit +# between BLE connection events. Without this, the flash driver waits for +# an ~85ms gap in BLE activity (ticker sync) which never comes during an +# active connection, freezing the system on the first flash write. +CONFIG_SOC_FLASH_NRF_PARTIAL_ERASE=y + # QSPI flash — disabled by default (safer — avoids hang if chip missing). # Boards with QSPI (Wio Tracker L1) override in board.conf. CONFIG_NORDIC_QSPI_NOR=n -# Recover from corrupted NVS sectors (nRF52 flash can develop bad regions) -CONFIG_NVS_INIT_BAD_MEMORY_REGION=y +# ========== Storage: Arduino MeshCore compatible layout ========== +# BLE bonds via file-based settings on InternalFS (replaces NVS). +# Required for Arduino MeshCore flash layout compatibility — the NVS region +# (0xD4000-0xD6000) is now part of ExtraFS. +CONFIG_NVS=n +CONFIG_SETTINGS_NVS=n +CONFIG_SETTINGS_FILE=y +CONFIG_SETTINGS_FILE_PATH="/ifs/settings" # ========== SEGGER RTT (J-Link debug) ========== # RTT log backend is in boards/common/logging.conf (only included for debug builds). diff --git a/zephcore/boards/common/nrf52_partitions_sdv6.dtsi b/zephcore/boards/common/nrf52_partitions_sdv6.dtsi new file mode 100644 index 0000000..5e3aa14 --- /dev/null +++ b/zephcore/boards/common/nrf52_partitions_sdv6.dtsi @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * nRF52840 partition layout — SoftDevice s140 v6 + UF2 bootloader + * Arduino MeshCore compatible (dual LFS, block_size=128) + * + * See nrf52_partitions_sdv7.dtsi for full include guide. + * Use THIS file instead of sdv7 when the board's UF2 bootloader + * bundles SoftDevice s140 v6.x (app starts at 0x26000). + * + * Boards: RAK4631 + * + * Usage in board overlay: + * /delete-node/ &boot_partition; + * /delete-node/ &slot0_partition; + * /delete-node/ &slot1_partition; + * /delete-node/ &storage_partition; + * #include "../../common/nrf52_partitions_sdv6.dtsi" + * + * Memory map (1MB internal flash): + * 0x00000 - 0x26000 (152KB) SoftDevice s140 v6 (reserved) + * 0x26000 - 0xD4000 (696KB) Application + * 0xD4000 - 0xED000 (100KB) ExtraFS (contacts, channels, blobs) + * 0xED000 - 0xF4000 (28KB) InternalFS (prefs, identity, BLE settings) + * 0xF4000 - 0x100000 (48KB) UF2 bootloader (reserved) + */ + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* SoftDevice s140 v6.1.1 — 152KB (reserved, managed by bootloader) */ + boot_partition: partition@0 { + label = "softdevice"; + read-only; + reg = <0x00000000 0x00026000>; + }; + + /* Application — 696KB */ + code_partition: partition@26000 { + label = "Application"; + reg = <0x00026000 0x000AE000>; + }; + + /* ExtraFS — 100KB: contacts3, channels2, adv_blobs + * Arduino: CustomLFS(0xD4000, 0x19000, 128) */ + extrafs_partition: partition@d4000 { + label = "extrafs"; + reg = <0x000D4000 0x00019000>; + }; + + /* InternalFS — 28KB: new_prefs, _main.id, BLE settings + * Arduino: Adafruit InternalFS(0xED000, 0x7000, 128) */ + internalfs_partition: partition@ed000 { + label = "internalfs"; + reg = <0x000ED000 0x00007000>; + }; + + /* UF2 bootloader — 48KB (reserved) */ + uf2_partition: partition@f4000 { + label = "UF2"; + read-only; + reg = <0x000F4000 0x0000C000>; + }; + }; +}; diff --git a/zephcore/boards/common/nrf52_partitions_sdv7.dtsi b/zephcore/boards/common/nrf52_partitions_sdv7.dtsi new file mode 100644 index 0000000..4d4f663 --- /dev/null +++ b/zephcore/boards/common/nrf52_partitions_sdv7.dtsi @@ -0,0 +1,96 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * nRF52840 partition layout — SoftDevice s140 v7 + UF2 bootloader + * Arduino MeshCore compatible (dual LFS, block_size=128) + * + * ┌──────────────────────────────────────────────────────────────────┐ + * │ ZephCore Board DTS Include Guide — Storage & Partitions │ + * ├──────────────────────────────────────────────────────────────────┤ + * │ │ + * │ PLATFORM INCLUDE THIS FILE │ + * │ ──────── ────────────────── │ + * │ nRF52 + SD v7 nrf52_partitions_sdv7.dtsi ← THIS FILE │ + * │ nRF52 + SD v6 nrf52_partitions_sdv6.dtsi │ + * │ ESP32 / MG24 / filesystem.dtsi (single /lfs automount) │ + * │ nRF54L │ + * │ │ + * │ OPTIONAL ADD-ON WHEN TO USE │ + * │ ──────────────── ────────── │ + * │ qspi-ext.dtsi Board has QSPI flash → adds /ext mount │ + * │ (contacts/channels/blobs migrate to QSPI) │ + * │ Requires board to define qspi_storage_part │ + * │ │ + * │ sensors-i2c.dtsi Board has I2C sensors (auto-detect) │ + * │ │ + * │ HOW TO IDENTIFY YOUR SOFTDEVICE VERSION: │ + * │ - SD v7: Application starts at 0x27000 (156KB reserved) │ + * │ Boards: T1000-E, Ikoka Nano 30dBm, Wio Tracker L1 │ + * │ - SD v6: Application starts at 0x26000 (152KB reserved) │ + * │ Boards: RAK4631 │ + * │ │ + * │ USAGE IN BOARD DTS (if base includes nrf52840_partition.dtsi): │ + * │ /delete-node/ &boot_partition; │ + * │ /delete-node/ &slot0_partition; │ + * │ /delete-node/ &slot1_partition; │ + * │ /delete-node/ &storage_partition; │ + * │ #include "../../common/nrf52_partitions_sdv7.dtsi" │ + * │ │ + * │ CHOSEN NODE: │ + * │ zephyr,code-partition = &code_partition; │ + * │ (Do NOT set zephyr,settings-partition — uses file settings) │ + * │ │ + * │ QSPI ADD-ON (if board has external QSPI flash): │ + * │ In board.overlay: │ + * │ #include "../../common/qspi-ext.dtsi" │ + * │ │ + * └──────────────────────────────────────────────────────────────────┘ + * + * Memory map (1MB internal flash): + * 0x00000 - 0x27000 (156KB) SoftDevice s140 v7 (reserved) + * 0x27000 - 0xD4000 (692KB) Application + * 0xD4000 - 0xED000 (100KB) ExtraFS (contacts, channels, blobs) + * 0xED000 - 0xF4000 (28KB) InternalFS (prefs, identity, BLE settings) + * 0xF4000 - 0x100000 (48KB) UF2 bootloader (reserved) + */ + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* SoftDevice s140 v7 — 156KB (reserved, managed by bootloader) */ + boot_partition: partition@0 { + label = "softdevice"; + read-only; + reg = <0x00000000 0x00027000>; + }; + + /* Application — 692KB */ + code_partition: partition@27000 { + label = "Application"; + reg = <0x00027000 0x000AD000>; + }; + + /* ExtraFS — 100KB: contacts3, channels2, adv_blobs + * Arduino: CustomLFS(0xD4000, 0x19000, 128) */ + extrafs_partition: partition@d4000 { + label = "extrafs"; + reg = <0x000D4000 0x00019000>; + }; + + /* InternalFS — 28KB: new_prefs, _main.id, BLE settings + * Arduino: Adafruit InternalFS(0xED000, 0x7000, 128) */ + internalfs_partition: partition@ed000 { + label = "internalfs"; + reg = <0x000ED000 0x00007000>; + }; + + /* UF2 bootloader — 48KB (reserved) */ + uf2_partition: partition@f4000 { + label = "UF2"; + read-only; + reg = <0x000F4000 0x0000C000>; + }; + }; +}; diff --git a/zephcore/boards/common/prod.conf b/zephcore/boards/common/prod.conf index a235a47..3c0d7b6 100644 --- a/zephcore/boards/common/prod.conf +++ b/zephcore/boards/common/prod.conf @@ -17,6 +17,6 @@ CONFIG_USE_SEGGER_RTT=n # Disable thread name strings (saves flash — only useful for debug) CONFIG_THREAD_NAME=n -# Max contacts for production (App protocol limit: 510, encoded as uint8 * 2, so you can set it higher, but the current app can't display it or work with it) -# Check board configs for overrides — too low RAM can freeze the device. -CONFIG_ZEPHCORE_MAX_CONTACTS=510 +# Max contacts: Kconfig default is 350 (safe for 100KB ExtraFS). +# Do NOT set here — EXTRA_CONF_FILE overrides board.conf, which would +# prevent boards with more storage (Wio QSPI=510) from raising the limit. diff --git a/zephcore/boards/common/qspi-ext.dtsi b/zephcore/boards/common/qspi-ext.dtsi index ad47652..0d8b761 100644 --- a/zephcore/boards/common/qspi-ext.dtsi +++ b/zephcore/boards/common/qspi-ext.dtsi @@ -1,10 +1,19 @@ /* - * Common QSPI external flash LittleFS configuration for ZephCore boards. - * Include this in board overlays that have QSPI flash for /ext mount. - * - * Requires board to define: &qspi_storage_partition - * * SPDX-License-Identifier: Apache-2.0 + * QSPI external flash /ext automount — optional add-on for any platform + * + * See nrf52_partitions_sdv7.dtsi for full include guide. + * + * When /ext is available, ZephyrDataStore redirects contacts, channels, + * and blobs to QSPI flash (more space, faster writes on native 4KB blocks). + * Prefs and identity stay on internal flash. + * + * Requires board DTS to define: qspi_storage_partition + * + * Usage in board overlay (in addition to partition or filesystem include): + * #include "../../common/qspi-ext.dtsi" + * + * Works with BOTH nRF52 dual-mount and single /lfs mount boards. */ / { diff --git a/zephcore/boards/common/zephcore_common.conf b/zephcore/boards/common/zephcore_common.conf index 722949b..0e2a28c 100644 --- a/zephcore/boards/common/zephcore_common.conf +++ b/zephcore/boards/common/zephcore_common.conf @@ -81,8 +81,10 @@ CONFIG_BT_BONDING_REQUIRED=y CONFIG_BT_MAX_PAIRED=5 CONFIG_BT_KEYS_OVERWRITE_OLDEST=y -# Connection parameter update -CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=y +# Connection parameter update — disabled because we apply conn params +# manually after initial sync completes (zephcore_ble_conn_params_ready). +# The auto-update would fire too early and disrupt channel/contact sync. +CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=n # MTU max (controller limit 251, L2CAP MTU 247, ATT payload 244) CONFIG_BT_BUF_ACL_RX_SIZE=251 diff --git a/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts b/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts index 2a26726..140d29a 100644 --- a/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts +++ b/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts @@ -33,7 +33,6 @@ zephyr,code-partition = &code_partition; zephyr,console = &cdc_acm_uart; zephyr,shell-uart = &cdc_acm_uart; - zephyr,settings-partition = &storage_partition; }; aliases { @@ -178,46 +177,5 @@ zephyr_udc0: &usbd { }; }; -/* LittleFS auto-mount — uses common config for /lfs */ -#include "../../common/filesystem.dtsi" -/* QSPI disabled on this board — see note above */ - -&flash0 { - partitions { - compatible = "fixed-partitions"; - #address-cells = <1>; - #size-cells = <1>; - - /* SoftDevice 156KB (reserved) */ - boot_partition: partition@0 { - label = "softdevice"; - read-only; - reg = <0x00000000 0x00027000>; - }; - - /* Application 692KB */ - code_partition: partition@27000 { - label = "Application"; - reg = <0x00027000 0x000AD000>; - }; - - /* NVS 8KB */ - storage_partition: partition@d4000 { - label = "storage"; - reg = <0x000D4000 0x00002000>; - }; - - /* LittleFS 92KB */ - lfs_partition: partition@d6000 { - label = "lfs"; - reg = <0x000D6000 0x00017000>; - }; - - /* UF2 bootloader 76KB (reserved) */ - uf2_partition: partition@ed000 { - label = "UF2"; - read-only; - reg = <0x000ED000 0x00013000>; - }; - }; -}; +/* Arduino MeshCore compatible partition layout (SoftDevice v7) */ +#include "../../common/nrf52_partitions_sdv7.dtsi" diff --git a/zephcore/boards/nrf52840/rak4631/board.overlay b/zephcore/boards/nrf52840/rak4631/board.overlay index 4bd797a..7d57000 100644 --- a/zephcore/boards/nrf52840/rak4631/board.overlay +++ b/zephcore/boards/nrf52840/rak4631/board.overlay @@ -31,7 +31,6 @@ zephyr,code-partition = &code_partition; zephyr,console = &cdc_acm_uart; zephyr,shell-uart = &cdc_acm_uart; - zephyr,settings-partition = &storage_partition; }; }; @@ -106,51 +105,5 @@ status = "disabled"; }; -/* Filesystem mounts - internal /lfs only (no QSPI) */ -#include "../../common/filesystem.dtsi" -&flash0 { - partitions { - /* - * SoftDevice + UF2 bootloader partition layout - * Total: 1MB internal flash - * - * 0x00000 - 0x26000 (152KB) SoftDevice s140 v6.1.1 (reserved) - * 0x26000 - 0xD4000 (696KB) Application - * 0xD4000 - 0xD6000 (8KB) NVS storage (BLE bonds) - * 0xD6000 - 0xF4000 (120KB) LittleFS (identity, prefs) - * 0xF4000 - 0x100000 (48KB) UF2 bootloader (reserved) - */ - - /* SoftDevice 152KB (reserved, managed by bootloader) */ - boot_partition: partition@0 { - label = "softdevice"; - read-only; - reg = <0x00000000 0x00026000>; - }; - - /* Application 696KB */ - code_partition: partition@26000 { - label = "Application"; - reg = <0x00026000 0x000AE000>; - }; - - /* NVS 8KB for BLE bonds */ - storage_partition: partition@d4000 { - label = "storage"; - reg = <0x000D4000 0x00002000>; - }; - - /* LittleFS 120KB for DataStore (identity, contacts, channels, etc) */ - lfs_partition: partition@d6000 { - label = "lfs"; - reg = <0x000D6000 0x0001E000>; - }; - - /* UF2 bootloader 48KB (reserved) */ - uf2_partition: partition@f4000 { - label = "UF2"; - read-only; - reg = <0x000F4000 0x0000C000>; - }; - }; -}; +/* Arduino MeshCore compatible partition layout (SoftDevice v6) */ +#include "../../common/nrf52_partitions_sdv6.dtsi" diff --git a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts index b6e97ca..de70158 100644 --- a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts +++ b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts @@ -34,10 +34,9 @@ compatible = "seeed,t1000-e"; chosen { - zephyr,code-partition = &slot0_partition; + zephyr,code-partition = &code_partition; zephyr,console = &cdc_acm_uart; zephyr,shell-uart = &cdc_acm_uart; - zephyr,settings-partition = &storage_partition; }; leds { @@ -278,56 +277,5 @@ zephyr_udc0: &usbd { }; }; -&flash0 { - partitions { - compatible = "fixed-partitions"; - #address-cells = <1>; - #size-cells = <1>; - - /* - * SoftDevice + UF2 bootloader partition layout - * Total: 1MB internal flash - * - * 0x00000 - 0x27000 (156KB) SoftDevice s140 v7 (reserved) - * 0x27000 - 0xD4000 (692KB) Application - * 0xD4000 - 0xD6000 (8KB) NVS storage (BLE bonds) - * 0xD6000 - 0xED000 (92KB) LittleFS (identity, prefs) - * 0xED000 - 0x100000 (76KB) UF2 bootloader (reserved) - */ - - /* SoftDevice 156KB (reserved, managed by bootloader) */ - boot_partition: partition@0 { - label = "softdevice"; - read-only; - reg = <0x00000000 0x00027000>; - }; - - /* Application 692KB */ - slot0_partition: partition@27000 { - label = "Application"; - reg = <0x00027000 0x000AD000>; - }; - - /* NVS 8KB for BLE bonds */ - storage_partition: partition@d4000 { - label = "storage"; - reg = <0x000D4000 0x00002000>; - }; - - /* LittleFS 92KB for DataStore (identity, contacts, channels, etc) */ - lfs_partition: partition@d6000 { - label = "lfs"; - reg = <0x000D6000 0x00017000>; - }; - - /* UF2 bootloader 76KB (reserved) */ - uf2_partition: partition@ed000 { - label = "UF2"; - read-only; - reg = <0x000ED000 0x00013000>; - }; - }; -}; - -/* LittleFS auto-mount — uses common config for /lfs */ -#include "../../common/filesystem.dtsi" +/* Arduino MeshCore compatible partition layout (SoftDevice v7) */ +#include "../../common/nrf52_partitions_sdv7.dtsi" diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/board.conf b/zephcore/boards/nrf52840/wio_tracker_l1/board.conf index 135392d..291ddf9 100644 --- a/zephcore/boards/nrf52840/wio_tracker_l1/board.conf +++ b/zephcore/boards/nrf52840/wio_tracker_l1/board.conf @@ -19,5 +19,8 @@ CONFIG_BT_DIS_MODEL_NUMBER_STR="Wio Tracker 1110" CONFIG_NORDIC_QSPI_NOR=y CONFIG_NORDIC_QSPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096 +# Contacts on QSPI (2MB) — override nrf52_common.conf limit of 350 +CONFIG_ZEPHCORE_MAX_CONTACTS=510 + # PWM for buzzer CONFIG_PWM=y diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/board.overlay b/zephcore/boards/nrf52840/wio_tracker_l1/board.overlay index 4a67d79..edcb086 100644 --- a/zephcore/boards/nrf52840/wio_tracker_l1/board.overlay +++ b/zephcore/boards/nrf52840/wio_tracker_l1/board.overlay @@ -23,6 +23,6 @@ }; }; -/* Filesystem mounts - internal /lfs + external /ext QSPI */ -#include "../../common/filesystem.dtsi" +/* Internal LFS mounted manually by ZephyrDataStore (block_size=128, custom erase) */ +/* External QSPI /ext auto-mounted via DTS (standard 4KB blocks) */ #include "../../common/qspi-ext.dtsi" diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts index e6c7137..8bc0c13 100644 --- a/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts +++ b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts @@ -34,10 +34,9 @@ compatible = "seeed,wio-tracker-l1"; chosen { - zephyr,code-partition = &slot0_partition; + zephyr,code-partition = &code_partition; zephyr,console = &cdc_acm_uart; zephyr,shell-uart = &cdc_acm_uart; - zephyr,settings-partition = &storage_partition; zephyr,display = &sh1106; }; @@ -324,56 +323,8 @@ zephyr_udc0: &usbd { }; }; -&flash0 { - partitions { - compatible = "fixed-partitions"; - #address-cells = <1>; - #size-cells = <1>; - - /* - * SoftDevice + UF2 bootloader partition layout - * Total: 1MB internal flash - * - * 0x00000 - 0x27000 (156KB) SoftDevice s140 v7 (reserved) - * 0x27000 - 0xD4000 (692KB) Application - * 0xD4000 - 0xD6000 (8KB) NVS storage (BLE bonds) - * 0xD6000 - 0xED000 (92KB) LittleFS (identity, prefs) - * 0xED000 - 0x100000 (76KB) UF2 bootloader (reserved) - */ - - /* SoftDevice 156KB (reserved, managed by bootloader) */ - boot_partition: partition@0 { - label = "softdevice"; - read-only; - reg = <0x00000000 0x00027000>; - }; - - /* Application 692KB */ - slot0_partition: partition@27000 { - label = "Application"; - reg = <0x00027000 0x000AD000>; - }; - - /* NVS 8KB for BLE bonds */ - storage_partition: partition@d4000 { - label = "storage"; - reg = <0x000D4000 0x00002000>; - }; - - /* LittleFS 92KB for DataStore (identity, contacts, channels, etc) */ - lfs_partition: partition@d6000 { - label = "lfs"; - reg = <0x000D6000 0x00017000>; - }; - - /* UF2 bootloader 76KB (reserved) */ - uf2_partition: partition@ed000 { - label = "UF2"; - read-only; - reg = <0x000ED000 0x00013000>; - }; - }; -}; +/* Arduino MeshCore compatible partition layout (SoftDevice v7) */ +#include "../../common/nrf52_partitions_sdv7.dtsi" /* External QSPI flash partitions */ &qspi_flash { diff --git a/zephcore/helpers/ui/buzzer.c b/zephcore/helpers/ui/buzzer.c index cf81d45..7990789 100644 --- a/zephcore/helpers/ui/buzzer.c +++ b/zephcore/helpers/ui/buzzer.c @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Non-blocking RTTTL parser using Zephyr PWM API. - * Each note is scheduled via k_work_delayable - fully event-driven. + * Each note is scheduled via k_work_delayable on a dedicated work queue + * so flash/BLE/filesystem operations can't delay note timing. * * RTTTL Format: "Name:d=D,o=O,b=B:note,note,..." * D = default duration (1,2,4,8,16,32) @@ -28,6 +29,20 @@ #include LOG_MODULE_REGISTER(buzzer, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); +/* ========== Dedicated Buzzer Work Queue ========== */ +/* Runs note scheduling at high priority so flash/BLE/FS operations + * on the system workqueue can't delay tone timing. */ +#define BUZZER_WQ_STACK_SIZE 512 +#define BUZZER_WQ_PRIORITY 2 /* Higher than default workqueue (usually 10+) */ + +K_THREAD_STACK_DEFINE(buzzer_wq_stack, BUZZER_WQ_STACK_SIZE); +static struct k_work_q buzzer_wq; + +/* Maximum duration (ms) a single tone can play before auto-silence. + * Safety net: if the work queue stalls or the handler doesn't fire, + * the hardware timer kills the PWM after this timeout. */ +#define BUZZER_TONE_MAX_MS 2000 + /* ========== Note Frequency Table ========== */ /* Frequencies for octave 4 (middle C = C4 = 262 Hz) */ /* Index: C=0, C#=1, D=2, D#=3, E=4, F=5, F#=6, G=7, G#=8, A=9, A#=10, B=11 */ @@ -40,6 +55,7 @@ struct buzzer_ctx { struct pwm_dt_spec pwm; const struct device *enable_reg; /* Optional regulator for buzzer amp */ struct k_work_delayable note_work; + struct k_work_delayable safety_work; /* Auto-silence watchdog */ /* RTTTL parser state */ const char *melody; /* Current position in RTTTL string */ @@ -295,6 +311,23 @@ static void buzzer_silence(void) buzzer_set_tone(0); } +/* ========== Safety Watchdog ========== */ + +/** + * Auto-silence handler: kills PWM if a note has been playing too long. + * This is a safety net for crashes or workqueue stalls — the PWM hardware + * is autonomous and keeps driving the pin until explicitly stopped. + */ +static void safety_work_handler(struct k_work *work) +{ + if (ctx.playing) { + LOG_WRN("safety timeout — silencing stuck tone"); + ctx.playing = false; + buzzer_silence(); + buzzer_amp_off(); + } +} + /* ========== Note Work Handler ========== */ static void note_work_handler(struct k_work *work) @@ -305,6 +338,7 @@ static void note_work_handler(struct k_work *work) if (!ctx.playing) { buzzer_silence(); buzzer_amp_off(); + k_work_cancel_delayable(&ctx.safety_work); return; } @@ -313,25 +347,21 @@ static void note_work_handler(struct k_work *work) buzzer_silence(); buzzer_amp_off(); ctx.playing = false; + k_work_cancel_delayable(&ctx.safety_work); return; } /* Play this note */ buzzer_set_tone(freq); - /* Add a small gap between notes (90% tone, 10% silence) */ - uint32_t tone_ms = (dur_ms * 9) / 10; - uint32_t gap_ms = dur_ms - tone_ms; + /* Reset safety watchdog — if the next note_work doesn't fire + * within BUZZER_TONE_MAX_MS, the safety handler kills the PWM. */ + k_work_reschedule_for_queue(&buzzer_wq, &ctx.safety_work, + K_MSEC(BUZZER_TONE_MAX_MS)); - if (gap_ms < 1) { - gap_ms = 1; - } - - /* Schedule silence gap after tone, then next note */ - /* For simplicity: play tone for full duration, next work picks up next note. - * The inter-note gap comes from the 90/10 split. */ - (void)gap_ms; - k_work_reschedule(&ctx.note_work, K_MSEC(dur_ms)); + /* Schedule next note after this note's duration */ + k_work_reschedule_for_queue(&buzzer_wq, &ctx.note_work, + K_MSEC(dur_ms)); } /* ========== Public API ========== */ @@ -370,7 +400,15 @@ int buzzer_init(void) } } + /* Start dedicated buzzer work queue */ + k_work_queue_init(&buzzer_wq); + k_work_queue_start(&buzzer_wq, buzzer_wq_stack, + K_THREAD_STACK_SIZEOF(buzzer_wq_stack), + BUZZER_WQ_PRIORITY, NULL); + k_thread_name_set(&buzzer_wq.thread, "buzzer_wq"); + k_work_init_delayable(&ctx.note_work, note_work_handler); + k_work_init_delayable(&ctx.safety_work, safety_work_handler); ctx.quiet = true; /* Start quiet like Arduino */ ctx.playing = false; @@ -380,7 +418,7 @@ int buzzer_init(void) buzzer_silence(); buzzer_amp_off(); - LOG_INF("buzzer initialized (PWM)"); + LOG_INF("buzzer initialized (PWM, dedicated wq)"); return 0; } @@ -414,8 +452,8 @@ void buzzer_play(const char *rtttl) /* Enable buzzer amplifier power */ buzzer_amp_on(); - /* Start playing first note immediately */ - k_work_reschedule(&ctx.note_work, K_NO_WAIT); + /* Start playing first note immediately on dedicated wq */ + k_work_reschedule_for_queue(&buzzer_wq, &ctx.note_work, K_NO_WAIT); } void buzzer_stop(void) @@ -426,6 +464,7 @@ void buzzer_stop(void) ctx.playing = false; k_work_cancel_delayable(&ctx.note_work); + k_work_cancel_delayable(&ctx.safety_work); buzzer_silence(); buzzer_amp_off(); } diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c index 8b21186..01b3f66 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c @@ -29,7 +29,7 @@ LOG_MODULE_REGISTER(lr11xx_lora, CONFIG_LORA_LOG_LEVEL); /* Dedicated DIO1 work queue — keeps LoRa interrupt processing off the * system work queue so USB/BLE/timer work items cannot delay packet RX. */ -#define LR11XX_DIO1_WQ_STACK_SIZE 1536 +#define LR11XX_DIO1_WQ_STACK_SIZE 2560 K_THREAD_STACK_DEFINE(lr11xx_dio1_wq_stack, LR11XX_DIO1_WQ_STACK_SIZE); /* ── Driver data structures ─────────────────────────────────────────── */ @@ -79,13 +79,18 @@ struct lr11xx_data { volatile bool in_rx_mode; /* Extension features (duty cycle, boost) */ - bool rx_duty_cycle_enabled; + bool rx_duty_cycle_enabled; /* unused — LR1110 always continuous RX */ bool rx_boost_enabled; bool rx_boost_applied; /* RX boost register written to hardware */ /* Deferred hardware init — heavy SPI/radio work runs on first config() */ bool hw_initialized; + /* DIO1 stuck-HIGH detection: counts consecutive empty IRQ cycles. + * If DIO1 stays HIGH with no actionable IRQ for too many cycles, + * the LR1110 is hung — trigger a hardware reset. */ + int dio1_stuck_count; + /* RX data buffer — filled in DIO1 handler, passed to callback */ uint8_t rx_buf[256]; }; @@ -259,71 +264,12 @@ static void lr11xx_apply_modem_config(struct lr11xx_data *data, 0); } -/* ── RX duty cycle (RadioLib algorithm, same as SX126x) ─────────────── */ +/* ── RX duty cycle — disabled on LR1110 ─────────────────────────────── */ -/* Same algorithm as SX126x SetRxDutyCycle — standard RX during the wake - * window, full demodulation capability. Preamble detected → hardware - * extends timeout to receive the complete packet. No activity → back - * to sleep. Identical detection reliability to the SX126x driver. */ - -#define LR11XX_DC_MIN_SYMBOLS_SF7_PLUS 8 -#define LR11XX_DC_MIN_SYMBOLS_SF6_LESS 12 -#define LR11XX_DC_TCXO_DELAY_US 1000 - -static void lr11xx_apply_rx_duty_cycle(struct lr11xx_data *data) -{ - void *ctx = &data->hal_ctx; - struct lora_modem_config *mc = &data->modem_cfg; - - uint8_t sf = (uint8_t)mc->datarate; - float bw_khz = bw_enum_to_khz(mc->bandwidth); - uint16_t preamble_len = mc->preamble_len; - - uint16_t min_symbols = (sf >= 7) ? LR11XX_DC_MIN_SYMBOLS_SF7_PLUS - : LR11XX_DC_MIN_SYMBOLS_SF6_LESS; - - int16_t sleep_symbols = (int16_t)preamble_len - (int16_t)min_symbols; - if (sleep_symbols <= 0) { - LOG_WRN("Preamble too short for duty cycle (need >%d, have %d)", - min_symbols, preamble_len); - data->rx_duty_cycle_enabled = false; - lr11xx_radio_set_rx(ctx, 0xFFFFFF); - return; - } - - uint32_t symbol_us = (uint32_t)((float)(1 << sf) * 1000.0f / bw_khz); - - /* Shave 2 symbols off sleep for timing margin */ - int16_t sleep_symbols_safe = sleep_symbols - 2; - if (sleep_symbols_safe < 1) { - sleep_symbols_safe = 1; - } - uint32_t sleep_period_us = (uint16_t)sleep_symbols_safe * symbol_us; - - uint32_t preamble_total_us = (preamble_len + 1) * symbol_us; - int32_t wake_calc1 = ((int32_t)preamble_total_us - - ((int32_t)sleep_period_us - LR11XX_DC_TCXO_DELAY_US)) / 2; - uint32_t wake_calc2 = (min_symbols + 1) * symbol_us; - - uint32_t wake_period_us = (wake_calc1 > 0 && (uint32_t)wake_calc1 > wake_calc2) - ? (uint32_t)wake_calc1 : wake_calc2; - - /* LR1110 API takes milliseconds (converted to RTC steps internally) */ - uint32_t rx_ms = (wake_period_us + 500) / 1000; - uint32_t sleep_ms = (sleep_period_us + 500) / 1000; - if (rx_ms < 1) { - rx_ms = 1; - } - if (sleep_ms < 1) { - sleep_ms = 1; - } - - lr11xx_radio_set_rx_duty_cycle(ctx, rx_ms, sleep_ms, - LR11XX_RADIO_RX_DUTY_CYCLE_MODE_RX); - - LOG_INF("RX duty cycle: rx=%ums sleep=%ums (SF%d BW%.0f)", - rx_ms, sleep_ms, sf, (double)bw_khz); -} +/* LR1110 SetRxDutyCycle is fundamentally broken: both MODE_RX and + * MODE_CAD fail to detect in-progress preambles, dropping 23-40% of + * packets depending on the sleep fraction. The SX1262 handles this + * correctly. LR1110 always uses continuous RX instead. */ /* ── Start RX (internal) ────────────────────────────────────────────── */ @@ -332,8 +278,7 @@ static void lr11xx_start_rx(struct lr11xx_data *data, { void *ctx = &data->hal_ctx; - LOG_INF("start_rx: t=%lld duty=%d", k_uptime_get(), - data->rx_duty_cycle_enabled); + LOG_DBG("start_rx: t=%lld", k_uptime_get()); /* Standby first — wake from any sleep state */ data->hal_ctx.radio_is_sleeping = true; @@ -356,15 +301,12 @@ static void lr11xx_start_rx(struct lr11xx_data *data, data->rx_boost_applied = true; } - /* Start RX — continuous or duty cycle. + /* Start continuous RX. * 0xFFFFFF is the magic RTC-step value for continuous RX. * Must use the raw RTC-step API — set_rx() converts from ms, - * which overflows uint32_t and gives a ~131 s timeout instead. */ - if (data->rx_duty_cycle_enabled) { - lr11xx_apply_rx_duty_cycle(data); - } else { - lr11xx_radio_set_rx_with_timeout_in_rtc_step(ctx, 0xFFFFFF); - } + * which overflows uint32_t and gives a ~131 s timeout instead. + * LR1110 always uses continuous RX (duty cycle is broken). */ + lr11xx_radio_set_rx_with_timeout_in_rtc_step(ctx, 0xFFFFFF); /* LR1110 firmware sets CMD_ERROR IRQ flag on several write commands * (SetModParams, SetSyncWord, SetRxBoosted, SetRx) across all @@ -393,12 +335,7 @@ static void lr11xx_restart_rx(struct lr11xx_data *data) void *ctx = &data->hal_ctx; lr11xx_system_clear_irq_status(ctx, LR11XX_SYSTEM_IRQ_ALL_MASK); - - if (data->rx_duty_cycle_enabled) { - lr11xx_apply_rx_duty_cycle(data); - } else { - lr11xx_radio_set_rx_with_timeout_in_rtc_step(ctx, 0xFFFFFF); - } + lr11xx_radio_set_rx_with_timeout_in_rtc_step(ctx, 0xFFFFFF); /* RX boost persists through SetRx — no re-apply needed. */ data->in_rx_mode = true; @@ -432,7 +369,7 @@ static void lr11xx_dio1_work_handler(struct k_work *work) goto safety_check; } - LOG_INF("DIO1 IRQ: 0x%08x tx=%d t=%lld", irq, data->tx_active, + LOG_DBG("DIO1 IRQ: 0x%08x tx=%d t=%lld", irq, data->tx_active, k_uptime_get()); /* CMD_ERROR (bit 22) is expected — LR1110 firmware sets it on @@ -445,6 +382,11 @@ static void lr11xx_dio1_work_handler(struct k_work *work) LOG_WRN("IRQ hardware ERROR: 0x%08x", irq); } + /* Any valid IRQ clears the stuck counter */ + if (irq != 0) { + data->dio1_stuck_count = 0; + } + /* ── RX done ── */ if (irq & LR11XX_SYSTEM_IRQ_RX_DONE) { lr11xx_radio_rx_buffer_status_t rx_stat; @@ -499,7 +441,7 @@ static void lr11xx_dio1_work_handler(struct k_work *work) /* ── TX done ── */ if (irq & LR11XX_SYSTEM_IRQ_TX_DONE) { - LOG_INF("TX done"); + LOG_DBG("TX done"); data->tx_active = false; /* Full restart — modem was reconfigured for TX */ @@ -514,8 +456,7 @@ static void lr11xx_dio1_work_handler(struct k_work *work) /* ── Timeout ── */ if (irq & LR11XX_SYSTEM_IRQ_TIMEOUT) { - LOG_DBG("Timeout IRQ — restarting RX (duty_cycle=%d)", - data->rx_duty_cycle_enabled); + LOG_DBG("Timeout IRQ — restarting RX"); if (!data->tx_active) { lr11xx_restart_rx(data); rx_restarted = true; @@ -568,9 +509,25 @@ safety_check: /* Edge-triggered DIO1: if the pin is still HIGH after processing, * a new IRQ arrived during handling. No rising edge will fire, - * so re-submit work to process the pending flags. */ + * so re-submit work to process the pending flags. + * + * Guard against DIO1 stuck HIGH: if we loop here with no + * actionable IRQ, the LR1110 is in a bad state. After 5 + * consecutive empty cycles, do a full hardware reset. */ if (gpio_pin_get_dt(&data->hal_ctx.dio1)) { - k_work_submit_to_queue(&data->dio1_wq, &data->dio1_work); + data->dio1_stuck_count++; + if (data->dio1_stuck_count >= 5) { + LOG_ERR("DIO1 stuck HIGH for %d cycles — " + "hardware reset", data->dio1_stuck_count); + data->dio1_stuck_count = 0; + lr11xx_hardware_reset(data, cfg); + lr11xx_start_rx(data, cfg); + } else { + k_work_submit_to_queue(&data->dio1_wq, + &data->dio1_work); + } + } else { + data->dio1_stuck_count = 0; } k_mutex_unlock(&data->spi_mutex); @@ -707,7 +664,7 @@ static int lr11xx_lora_send_async(const struct device *dev, k_mutex_unlock(&data->spi_mutex); - LOG_INF("TX started: len=%u", data_len); + LOG_DBG("TX started: len=%u", data_len); return 0; } @@ -762,9 +719,8 @@ static int lr11xx_lora_recv_async(const struct device *dev, k_mutex_unlock(&data->spi_mutex); - LOG_DBG("recv_async started%s%s", - data->rx_duty_cycle_enabled ? " (duty cycle)" : "", - data->rx_boost_enabled ? " (boosted)" : ""); + LOG_INF("recv_async started (continuous RX%s)", + data->rx_boost_enabled ? ", boosted" : ""); return 0; } @@ -817,22 +773,9 @@ bool lr11xx_is_receiving(const struct device *dev) void lr11xx_set_rx_duty_cycle(const struct device *dev, bool enable) { - struct lr11xx_data *data = dev->data; - const struct lr11xx_config *cfg = dev->config; - - data->rx_duty_cycle_enabled = enable; - LOG_INF("RX duty cycle %s", enable ? "enabled" : "disabled"); - - /* If currently in RX, restart with proper Standby transition. - * LR1110 requires Standby before SetRx or SetRxDutyCycle — - * issuing these while already in RX puts the radio in an - * undefined state where RSSI reads work but packet detection - * is broken (zero DIO1 IRQs). */ - if (data->in_rx_mode) { - k_mutex_lock(&data->spi_mutex, K_FOREVER); - lr11xx_start_rx(data, cfg); - k_mutex_unlock(&data->spi_mutex); - } + /* LR1110 duty cycle is broken — always continuous RX. Ignore. */ + (void)dev; + (void)enable; } void lr11xx_set_rx_boost(const struct device *dev, bool enable) diff --git a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch index 9960d12..d4208c0 100644 --- a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch +++ b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch @@ -1,5 +1,5 @@ diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c -index 8e0ca45c271..6b0ee88ff0c 100644 +index 8e0ca45c271..eb9342a13b9 100644 --- a/drivers/lora/native/sx126x/sx126x.c +++ b/drivers/lora/native/sx126x/sx126x.c @@ -9,10 +9,19 @@ @@ -13,7 +13,7 @@ index 8e0ca45c271..6b0ee88ff0c 100644 +/* Dedicated DIO1 work queue — keeps LoRa interrupt processing off the + * system work queue so USB/BLE/timer work items cannot delay packet RX. */ -+#define SX126X_DIO1_WQ_STACK_SIZE 1536 ++#define SX126X_DIO1_WQ_STACK_SIZE 2560 +K_THREAD_STACK_DEFINE(sx126x_dio1_wq_stack, SX126X_DIO1_WQ_STACK_SIZE); + +/* Register not in sx126x_regs.h — only used for §15.3 workaround */ diff --git a/zephcore/patches/zephyr/0007-littlefs-custom-erase.patch b/zephcore/patches/zephyr/0007-littlefs-custom-erase.patch new file mode 100644 index 0000000..66cb39c --- /dev/null +++ b/zephcore/patches/zephyr/0007-littlefs-custom-erase.patch @@ -0,0 +1,15 @@ +diff --git a/subsys/fs/littlefs_fs.c b/subsys/fs/littlefs_fs.c +index 6176d533e32..1ea405a3d1f 100644 +--- a/subsys/fs/littlefs_fs.c ++++ b/subsys/fs/littlefs_fs.c +@@ -901,7 +901,9 @@ static int littlefs_init_cfg(struct fs_littlefs *fs, int flags) + #ifdef CONFIG_FS_LITTLEFS_FMP_DEV + lcp->read = lfs_api_read; + lcp->prog = lfs_api_prog; +- lcp->erase = lfs_api_erase; ++ if (!lcp->erase) { ++ lcp->erase = lfs_api_erase; ++ } + #endif + + lcp->read_size = read_size; diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 989fd48..31d625f 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -170,6 +170,9 @@ static size_t write_frame(const uint8_t *src, size_t len) */ static void push_callback(uint8_t code, const uint8_t *data, size_t len) { + /* No point serializing if nobody is listening */ + if (!zephcore_ble_is_connected()) return; + /* Push frame: code byte + optional data */ uint8_t push_buf[1 + MAX_FRAME_SIZE - 1]; size_t total_len = 1 + len; @@ -185,7 +188,7 @@ static void push_callback(uint8_t code, const uint8_t *data, size_t len) memcpy(&push_buf[1], data, len); } - LOG_INF("code=0x%02x len=%u (total frame)", code, (unsigned)total_len); + LOG_DBG("code=0x%02x len=%u (total frame)", code, (unsigned)total_len); write_frame(push_buf, total_len); } diff --git a/zephcore/tools/formatter/boards/rak4631/board.overlay b/zephcore/tools/formatter/boards/rak4631/board.overlay index 005c41b..573b76b 100644 --- a/zephcore/tools/formatter/boards/rak4631/board.overlay +++ b/zephcore/tools/formatter/boards/rak4631/board.overlay @@ -14,7 +14,7 @@ zephyr,code-partition = &code_partition; zephyr,console = &cdc_acm_uart; zephyr,shell-uart = &cdc_acm_uart; - zephyr,settings-partition = &storage_partition; + /* No zephyr,settings-partition — nRF52 uses file-based settings */ }; }; @@ -29,33 +29,5 @@ status = "disabled"; }; -&flash0 { - partitions { - boot_partition: partition@0 { - label = "softdevice"; - read-only; - reg = <0x00000000 0x00026000>; - }; - - code_partition: partition@26000 { - label = "Application"; - reg = <0x00026000 0x000AE000>; - }; - - storage_partition: partition@d4000 { - label = "storage"; - reg = <0x000D4000 0x00002000>; - }; - - lfs_partition: partition@d6000 { - label = "lfs"; - reg = <0x000D6000 0x0001E000>; - }; - - uf2_partition: partition@f4000 { - label = "UF2"; - read-only; - reg = <0x000F4000 0x0000C000>; - }; - }; -}; +/* Arduino MeshCore compatible partition layout (SoftDevice v6) */ +#include "../../../../boards/common/nrf52_partitions_sdv6.dtsi" diff --git a/zephcore/tools/formatter/src/main.c b/zephcore/tools/formatter/src/main.c index 041199c..978f65e 100644 --- a/zephcore/tools/formatter/src/main.c +++ b/zephcore/tools/formatter/src/main.c @@ -1,11 +1,20 @@ /* * ZephCore Universal Flash Formatter * - * Erases all filesystem partitions (NVS + LittleFS + QSPI if present) - * and reboots into Adafruit UF2 DFU mode for clean firmware installation. + * Erases all filesystem partitions and reboots into Adafruit UF2 DFU mode + * for clean firmware installation. * - * Partition addresses come from the board's devicetree overlay — no - * hardcoded addresses, works for both s140 v6.1.1 and v7.3.0 boards. + * nRF52 builds use hardcoded flash addresses (identical across all boards) + * so a single UF2 works on every board with the same SoftDevice version. + * QSPI is compile-time conditional — included only when building for a + * QSPI-capable board target (pin config is board-specific). + * + * Non-nRF52 builds use DTS FIXED_PARTITION_EXISTS guards as before. + * + * Build: + * SD v7: west build -b t1000_e/nrf52840 zephcore/tools/formatter --pristine + * SD v6: west build -b rak4631/nrf52840 zephcore/tools/formatter --pristine + * SD v7 + QSPI: west build -b wio_tracker_l1/nrf52840 zephcore/tools/formatter --pristine * * SPDX-License-Identifier: Apache-2.0 */ @@ -19,14 +28,34 @@ #if defined(CONFIG_SOC_SERIES_NRF52X) || defined(CONFIG_SOC_SERIES_NRF52) #include +#define IS_NRF52 1 +#else +#define IS_NRF52 0 #endif /* Adafruit UF2 bootloader magic — enter mass storage DFU mode */ #define BOOTLOADER_DFU_UF2_MAGIC 0x57 -/* ── LED feedback (optional) ─────────────────────────────────── */ +/* + * nRF52840 Arduino MeshCore partition addresses. + * These are IDENTICAL across all nRF52 boards (SD v6 and v7): + * ExtraFS @ 0xD4000 (100KB) — contacts, channels, blobs + * InternalFS @ 0xED000 (28KB) — prefs, identity, BLE settings + * + * Only the SoftDevice/app boundary differs (v6=0x26000, v7=0x27000), + * which doesn't affect the formatter since we don't touch app flash. + */ +#define NRF52_EXTRAFS_OFF 0xD4000 +#define NRF52_EXTRAFS_SIZE 0x19000 /* 100KB */ +#define NRF52_INTERNALFS_OFF 0xED000 +#define NRF52_INTERNALFS_SIZE 0x7000 /* 28KB */ -#if DT_NODE_EXISTS(DT_ALIAS(led0)) +/* ── LED feedback (optional — may not match actual board) ──── */ + +#if DT_NODE_EXISTS(DT_ALIAS(led0)) && !IS_NRF52 +/* Only use DTS LED on non-nRF52 (board-specific builds). + * On nRF52 universal builds, skip LED to avoid toggling + * wrong pins on boards other than the build target. */ #define HAS_LED 1 static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios); static void led_init(void) { gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); } @@ -39,8 +68,35 @@ static void led_on(void) {} static void led_off(void) {} #endif -/* ── Partition erase helper ──────────────────────────────────── */ +/* ── Flash erase helpers ───────────────────────────────────── */ +#if IS_NRF52 +/** + * Erase a region of internal flash by absolute address. + * Works on any nRF52840 board regardless of DTS. + */ +static int erase_region(const struct device *dev, off_t offset, size_t size, + const char *name) +{ + printk(" %s: erasing 0x%lx - 0x%lx (%u KB)...", + name, (unsigned long)offset, + (unsigned long)(offset + size), + (unsigned)(size / 1024)); + + int rc = flash_erase(dev, offset, size); + if (rc) { + printk(" FAILED (rc %d)\n", rc); + } else { + printk(" OK\n"); + } + return rc; +} +#endif /* IS_NRF52 */ + +#if !IS_NRF52 +/** + * Erase a DTS partition by flash_area ID (non-nRF52 boards). + */ static int erase_partition(uint8_t id, const char *name) { const struct flash_area *fa; @@ -68,6 +124,7 @@ static int erase_partition(uint8_t id, const char *name) flash_area_close(fa); return rc; } +#endif /* !IS_NRF52 */ /* ── Main ────────────────────────────────────────────────────── */ @@ -85,18 +142,68 @@ int main(void) led_init(); led_on(); - /* ── Erase internal NVS partition (BLE bonds + settings) ── */ +#if IS_NRF52 + /* + * nRF52 universal path: hardcoded addresses, no DTS dependency. + * This binary works on ANY nRF52840 board with the same SoftDevice + * version (determines UF2 load address, not erase targets). + */ + const struct device *flash_dev = DEVICE_DT_GET(DT_NODELABEL(flash_controller)); + + if (!device_is_ready(flash_dev)) { + printk(" ERROR: flash device not ready!\n"); + errors++; + } else { + if (erase_region(flash_dev, NRF52_EXTRAFS_OFF, + NRF52_EXTRAFS_SIZE, "ExtraFS (contacts/channels)")) { + errors++; + } + if (erase_region(flash_dev, NRF52_INTERNALFS_OFF, + NRF52_INTERNALFS_SIZE, "InternalFS (prefs/identity)")) { + errors++; + } + } + + /* QSPI: try to erase if present via flash_area (compiled in by DTS). + * On boards without QSPI, FIXED_PARTITION_EXISTS is false at compile + * time so this block is excluded — no runtime probe needed. */ +#if FIXED_PARTITION_EXISTS(qspi_storage_partition) + { + const struct flash_area *fa; + int rc = flash_area_open(FIXED_PARTITION_ID(qspi_storage_partition), &fa); + if (rc == 0) { + printk(" QSPI: erasing 0x%lx (%u KB, may take a while)...", + (unsigned long)fa->fa_off, + (unsigned)(fa->fa_size / 1024)); + rc = flash_area_erase(fa, 0, fa->fa_size); + printk(rc ? " FAILED (rc %d)\n" : " OK\n", rc); + if (rc) errors++; + flash_area_close(fa); + } else { + printk(" QSPI: not accessible (rc %d) — skipped\n", rc); + } + } +#else + printk(" QSPI: not present in build target — skipped\n"); +#endif + +#else /* !IS_NRF52 */ + /* + * Non-nRF52 path: use DTS partitions (board-specific builds). + */ +#if FIXED_PARTITION_EXISTS(storage_partition) if (erase_partition(FIXED_PARTITION_ID(storage_partition), "NVS (storage)")) { errors++; } +#endif - /* ── Erase internal LittleFS partition (identity, contacts, channels) ── */ +#if FIXED_PARTITION_EXISTS(lfs_partition) if (erase_partition(FIXED_PARTITION_ID(lfs_partition), "LittleFS (lfs)")) { errors++; } +#endif - /* ── Erase external QSPI flash (if present in devicetree) ── */ -#if DT_NODE_EXISTS(DT_NODELABEL(qspi_storage_partition)) +#if FIXED_PARTITION_EXISTS(qspi_storage_partition) if (erase_partition(FIXED_PARTITION_ID(qspi_storage_partition), "QSPI external")) { errors++; } @@ -104,6 +211,8 @@ int main(void) printk(" QSPI: not present on this board — skipped\n"); #endif +#endif /* IS_NRF52 */ + led_off(); /* ── Summary ── */ @@ -119,7 +228,7 @@ int main(void) k_msleep(500); /* ── Reboot to UF2 bootloader ── */ -#if defined(CONFIG_SOC_SERIES_NRF52X) || defined(CONFIG_SOC_SERIES_NRF52) +#if IS_NRF52 nrf_power_gpregret_set(NRF_POWER, 0, BOOTLOADER_DFU_UF2_MAGIC); #endif sys_reboot(SYS_REBOOT_COLD);