From 22efe4da6f6e24a36ba9c7571d6a86861276c0e4 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:21:05 +0100 Subject: [PATCH] fs revert cleanup --- zephcore/CMakeLists.txt | 39 ++ .../adapters/datastore/ZephyrDataStore.cpp | 362 ++++-------------- zephcore/adapters/datastore/ZephyrDataStore.h | 39 +- zephcore/app/RepeaterDataStore.cpp | 92 +++-- zephcore/app/RepeaterDataStore.h | 7 +- zephcore/boards/common/filesystem.dtsi | 22 +- zephcore/boards/common/nrf52_common.conf | 5 +- .../boards/common/nrf52_partitions_sdv6.dtsi | 23 +- .../boards/common/nrf52_partitions_sdv7.dtsi | 23 +- zephcore/boards/common/qspi-ext.dtsi | 2 +- zephcore/boards/nrf52840/rak4631/board.conf | 2 +- zephcore/helpers/RegionMap.cpp | 4 +- .../zephyr/0003-lora-sx126x-native.patch | 53 ++- 13 files changed, 274 insertions(+), 399 deletions(-) diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 7f90ece..1fafe28 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -54,6 +54,45 @@ function(zephcore_apply_patches PATCH_DIR TARGET_DIR LABEL) RESULT_VARIABLE PATCH_CHECK ERROR_VARIABLE PATCH_ERR ) + # If forward-apply fails, the tree may have stale patches from a + # previous build (west update doesn't reset dirty files). Extract + # the file list from the patch and git-checkout those paths to + # restore them to the clean upstream state, then re-check. + if(NOT PATCH_CHECK EQUAL 0) + execute_process( + COMMAND git diff --name-only "${PATCH_FILE}" + WORKING_DIRECTORY "${TARGET_DIR}" + OUTPUT_VARIABLE _dummy ERROR_QUIET + ) + # git apply --numstat lists affected files (one per line) + execute_process( + COMMAND git apply --numstat "${PATCH_FILE}" + OUTPUT_VARIABLE PATCH_NUMSTAT + ERROR_QUIET + ) + # Parse file paths from numstat (format: "adds\tdels\tpath") + string(REGEX MATCHALL "[^\t\n]+\t[^\t\n]+\t[^\t\n]+" NUMSTAT_LINES "${PATCH_NUMSTAT}") + set(PATCH_PATHS "") + foreach(_line ${NUMSTAT_LINES}) + string(REGEX REPLACE "^[^\t]+\t[^\t]+\t" "" _path "${_line}") + list(APPEND PATCH_PATHS "${_path}") + endforeach() + if(PATCH_PATHS) + message(STATUS " [${LABEL}] Resetting stale files for: ${PATCH_NAME}") + execute_process( + COMMAND git checkout -- ${PATCH_PATHS} + WORKING_DIRECTORY "${TARGET_DIR}" + ERROR_QUIET + ) + # Re-check after reset + execute_process( + COMMAND git apply --check "${PATCH_FILE}" + WORKING_DIRECTORY "${TARGET_DIR}" + RESULT_VARIABLE PATCH_CHECK + ERROR_VARIABLE PATCH_ERR + ) + endif() + endif() if(NOT PATCH_CHECK EQUAL 0) message(FATAL_ERROR "ZephCore patch FAILED to apply: ${PATCH_NAME}\n" diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp index c338923..300ea71 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.cpp +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -2,18 +2,8 @@ * 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) - * using native Zephyr LittleFS (block_size=4096). - * Others: DTS-automounted /lfs (single partition). - * QSPI: /ext overrides contacts mount when available. - * - * Data format (all platforms): - * contacts3 — 152-byte records - * new_prefs — byte layout - * channels2 — 68-byte records - * _main.id — identity blob - * adv_blobs — fixed record array + * All platforms use DTS-automounted /lfs. + * QSPI /ext overrides contacts mount when available. */ #include "ZephyrDataStore.h" @@ -35,16 +25,9 @@ struct BlobRec { uint8_t data[MAX_ADVERT_PKT_LEN]; }; -/* ── 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) */ +/* Track mount status - filesystems are automounted via DTS fstab */ +static bool lfs_mounted; +static bool ext_lfs_mounted; /* Check if a filesystem is mounted using fs_statvfs */ static bool is_mounted(const char *mount_point) @@ -53,121 +36,28 @@ static bool is_mounted(const char *mount_point) return fs_statvfs(mount_point, &stat) == 0; } -/* ── nRF52 dual-mount: ExtraFS + InternalFS ────────────────────────── */ - -#if FIXED_PARTITION_EXISTS(extrafs_partition) && FIXED_PARTITION_EXISTS(internalfs_partition) -#define HAS_NRF52_DUAL_MOUNT 1 - -/* Native Zephyr LFS — block_size derived from flash erase size (4096). */ -FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(extrafs_data); -FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(internalfs_data); - -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, -}; - -/* Mount a LittleFS partition, format if mount fails. Returns the mount rc. */ -static int mount_or_format(struct fs_mount_t *mnt, int partition_id) -{ - int rc = fs_mount(mnt); - - if (rc < 0) { - LOG_WRN("%s: mount failed (%d) — formatting", mnt->mnt_point, rc); - - /* A failed fs_mount may leave fs->backend set (flash_area - * opened in littlefs_init_backend but lfs_mount failed). - * Clear it so the retry doesn't hit -EBUSY. */ - struct fs_littlefs *fsd = (struct fs_littlefs *)mnt->fs_data; - if (fsd->backend) { - flash_area_close((const struct flash_area *)fsd->backend); - fsd->backend = NULL; - } - - const struct flash_area *fap; - if (flash_area_open(partition_id, &fap) == 0) { - flash_area_flatten(fap, 0, fap->fa_size); - flash_area_close(fap); - } - rc = fs_mount(mnt); - if (rc < 0) { - LOG_ERR("%s: mount failed after format: %d", - mnt->mnt_point, rc); - } - } - return rc; -} - -static bool try_mount_nrf52_dual() -{ - /* Mount ExtraFS — contacts, channels, blobs */ - if (mount_or_format(&extrafs_mnt, - FIXED_PARTITION_ID(extrafs_partition)) < 0) { - return false; - } - efs_mounted = true; - LOG_INF("ExtraFS at /efs: blk_sz=%u blk_cnt=%u", - extrafs_data.cfg.block_size, extrafs_data.cfg.block_count); - - /* Mount InternalFS — prefs, identity, BLE settings */ - if (mount_or_format(&internalfs_mnt, - FIXED_PARTITION_ID(internalfs_partition)) < 0) { - return false; - } - ifs_mounted = true; - LOG_INF("InternalFS at /ifs: blk_sz=%u blk_cnt=%u", - internalfs_data.cfg.block_size, internalfs_data.cfg.block_count); - - 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 (_contacts_mnt != nullptr) { - return true; /* already mounted */ + if (lfs_mounted) { + return true; } - /* 1. Try nRF52 dual-mount (manual, native block_size) */ - 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"; + /* Check if internal LFS was automounted */ + if (is_mounted(mountPoint())) { lfs_mounted = true; - LOG_INF("Single-mount: contacts=/lfs, prefs=/lfs (DTS automount)"); - } - else { - LOG_ERR("No filesystem mounted!"); + LOG_INF("Internal LittleFS at %s (automounted)", mountPoint()); + } else { + LOG_ERR("Internal LittleFS NOT mounted at %s - check DTS fstab!", mountPoint()); return false; } - /* 3. QSPI /ext override (optional, any platform) */ - if (is_mounted(EXT_MNT_POINT)) { + /* Check if external QSPI was automounted */ + if (is_mounted(extMountPoint())) { ext_lfs_mounted = true; - _contacts_mnt = EXT_MNT_POINT; - LOG_INF("QSPI mounted at /ext — contacts redirected to QSPI"); + 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()); } return true; @@ -175,60 +65,11 @@ bool ZephyrDataStore::mount() void ZephyrDataStore::unmount() { -#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 + /* With automount, filesystems are managed by Zephyr - just clear our flags */ 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) { @@ -237,17 +78,36 @@ ZephyrDataStore::ZephyrDataStore(mesh::RTCClock &clock) void ZephyrDataStore::begin() { _has_ext_fs = ext_lfs_mounted; - 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()); + 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()); 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() +{ + 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) { @@ -264,7 +124,8 @@ bool ZephyrDataStore::openRead(const char *path, uint8_t *buf, size_t buf_sz, si { struct fs_file_t file; fs_file_t_init(&file); - if (fs_open(&file, path, FS_O_READ) < 0) { + int rc = fs_open(&file, path, FS_O_READ); + if (rc < 0) { return false; } ssize_t n = fs_read(&file, buf, buf_sz); @@ -278,12 +139,13 @@ 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); - if (exists(path)) { - fs_unlink(path); - } - if (fs_open(&file, path, FS_O_CREATE | FS_O_WRITE) < 0) { + 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); return false; } ssize_t n = fs_write(&file, buf, len); @@ -322,48 +184,32 @@ bool ZephyrDataStore::copyFile(const char *src, const char *dst) return ok && n >= 0; } -/* ── QSPI migration ───────────────────────────────────────────────── */ - void ZephyrDataStore::migrateToExternalFS() { - /* 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 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); } } /* Migrate channels */ - if (!exists(ext_channels) && exists(int_channels)) { + if (!exists(EXT_CHANNELS_FILE) && exists(INT_CHANNELS_FILE)) { LOG_INF("Migrating channels to QSPI"); - if (copyFile(int_channels, ext_channels)) { - removeFile(int_channels); + if (copyFile(INT_CHANNELS_FILE, EXT_CHANNELS_FILE)) { + removeFile(INT_CHANNELS_FILE); } } /* Migrate adv_blobs (extend to 100 records) */ - if (!exists(ext_blobs) && exists(int_blobs)) { + if (!exists(EXT_ADV_BLOBS_FILE) && exists(INT_ADV_BLOBS_FILE)) { LOG_INF("Migrating adv_blobs to QSPI (20 -> 100 slots)"); - if (copyFile(int_blobs, ext_blobs)) { - removeFile(int_blobs); + if (copyFile(INT_ADV_BLOBS_FILE, EXT_ADV_BLOBS_FILE)) { + removeFile(INT_ADV_BLOBS_FILE); struct fs_file_t file; fs_file_t_init(&file); - if (fs_open(&file, ext_blobs, FS_O_RDWR) == 0) { + if (fs_open(&file, EXT_ADV_BLOBS_FILE, FS_O_RDWR) == 0) { fs_seek(&file, 0, FS_SEEK_END); BlobRec zeroes; memset(&zeroes, 0, sizeof(zeroes)); @@ -376,14 +222,14 @@ void ZephyrDataStore::migrateToExternalFS() } /* Clean up old files on internal if they exist on external */ - if (exists(ext_contacts) && exists(int_contacts)) { - removeFile(int_contacts); + if (exists(EXT_CONTACTS_FILE) && exists(INT_CONTACTS_FILE)) { + removeFile(INT_CONTACTS_FILE); } - if (exists(ext_channels) && exists(int_channels)) { - removeFile(int_channels); + if (exists(EXT_CHANNELS_FILE) && exists(INT_CHANNELS_FILE)) { + removeFile(INT_CHANNELS_FILE); } - if (exists(ext_blobs) && exists(int_blobs)) { - removeFile(int_blobs); + if (exists(EXT_ADV_BLOBS_FILE) && exists(INT_ADV_BLOBS_FILE)) { + removeFile(INT_ADV_BLOBS_FILE); } } @@ -419,28 +265,7 @@ bool ZephyrDataStore::formatFileSystem() const struct flash_area *fap; int rc; -#if FIXED_PARTITION_EXISTS(extrafs_partition) - /* nRF52: format ExtraFS */ - rc = flash_area_open(FIXED_PARTITION_ID(extrafs_partition), &fap); - if (rc == 0) { - 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); @@ -450,7 +275,6 @@ bool ZephyrDataStore::formatFileSystem() #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); @@ -490,7 +314,7 @@ bool ZephyrDataStore::loadMainIdentity(mesh::LocalIdentity &identity) { uint8_t buf[PRV_KEY_SIZE + PUB_KEY_SIZE + 32]; size_t len = 0; - if (!openRead(identityFile(), buf, sizeof(buf), len) || len < PRV_KEY_SIZE + PUB_KEY_SIZE) { + if (!openRead(MAIN_ID_FILE, buf, sizeof(buf), len) || len < PRV_KEY_SIZE + PUB_KEY_SIZE) { return false; } return identity.readFrom(buf, len); @@ -503,16 +327,15 @@ bool ZephyrDataStore::saveMainIdentity(const mesh::LocalIdentity &identity) if (n == 0) { return false; } - return openWrite(identityFile(), buf, n); + return openWrite(MAIN_ID_FILE, buf, n); } -/* ── Preferences (Arduino-compatible layout) ───────────────────────── */ +/* ── Preferences ───────────────────────────────────────────────────── */ void ZephyrDataStore::loadPrefs(NodePrefs &prefs) { - const char *path = prefsFile(); - bool prefs_exists = exists(path); - LOG_INF("loadPrefs: exists(%s)=%d", path, prefs_exists ? 1 : 0); + bool prefs_exists = exists(PREFS_FILE); + LOG_INF("loadPrefs: exists(%s)=%d", PREFS_FILE, prefs_exists ? 1 : 0); if (!prefs_exists) { LOG_WRN("loadPrefs: no prefs file found"); return; @@ -520,7 +343,7 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs) uint8_t buf[256]; size_t len = 0; - if (!openRead(path, buf, sizeof(buf), len)) { + if (!openRead(PREFS_FILE, buf, sizeof(buf), len)) { LOG_WRN("loadPrefs: read failed"); return; } @@ -528,7 +351,7 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs) LOG_WRN("loadPrefs: file too small (%d bytes, need 88)", (int)len); return; } - LOG_INF("loadPrefs: loaded %d bytes from %s", (int)len, path); + LOG_INF("loadPrefs: loaded %d bytes from %s", (int)len, PREFS_FILE); size_t off = 0; memcpy(&prefs.airtime_factor, &buf[off], sizeof(float)); @@ -632,9 +455,9 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs) buf[off++] = prefs.rx_boost; /* Total: 93 bytes (Arduino reads 92, ZephCore reads 93) */ - bool ok = openWrite(prefsFile(), buf, off); + bool ok = openWrite(PREFS_FILE, buf, off); LOG_INF("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'", - prefsFile(), ok ? 1 : 0, (int)off, prefs.node_name); + PREFS_FILE, ok ? 1 : 0, (int)off, prefs.node_name); } /* ── Contacts: contacts3 (152B records, Arduino-compatible) ────────── */ @@ -902,40 +725,21 @@ bool ZephyrDataStore::deleteBlobByKey(const uint8_t key[], int key_len) uint32_t ZephyrDataStore::getStorageUsedKb() const { struct fs_statvfs sbuf; - uint32_t used = 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; + if (fs_statvfs(MNT_POINT, &sbuf) != 0) { + return 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; - } - - return used; + uint32_t total = sbuf.f_blocks * sbuf.f_frsize; + uint32_t free = sbuf.f_bfree * sbuf.f_frsize; + return (total - free) / 1024; } uint32_t ZephyrDataStore::getStorageTotalKb() const { struct fs_statvfs sbuf; - uint32_t total = 0; - - if (_contacts_mnt && fs_statvfs(_contacts_mnt, &sbuf) == 0) { - total += (sbuf.f_blocks * sbuf.f_frsize) / 1024; + if (fs_statvfs(MNT_POINT, &sbuf) != 0) { + return 0; } - if (_prefs_mnt && _prefs_mnt != _contacts_mnt && - fs_statvfs(_prefs_mnt, &sbuf) == 0) { - total += (sbuf.f_blocks * sbuf.f_frsize) / 1024; - } - - return total; + return (sbuf.f_blocks * sbuf.f_frsize) / 1024; } uint32_t ZephyrDataStore::getExternalStorageKb() const diff --git a/zephcore/adapters/datastore/ZephyrDataStore.h b/zephcore/adapters/datastore/ZephyrDataStore.h index 592aded..6baa321 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.h +++ b/zephcore/adapters/datastore/ZephyrDataStore.h @@ -2,11 +2,7 @@ * 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 dual-mount layout with native Zephyr LittleFS: - * /efs (ExtraFS @ 0xD4000, 100KB) — contacts3, channels2, blobs - * /ifs (InternalFS @ 0xED000, 28KB) — new_prefs, _main.id - * On other platforms, uses DTS-automounted /lfs for everything. + * All platforms use DTS-automounted /lfs for identity, prefs, contacts. * QSPI /ext overrides contacts path when available (any platform). */ @@ -54,29 +50,36 @@ public: static bool mount(); static void unmount(); + static const char *mountPoint() { return MNT_POINT; } + static const char *extMountPoint() { return EXT_MNT_POINT; } private: - /* 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; + /* 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"; - /* External QSPI flash (optional, any platform) */ + /* External QSPI flash (optional) - contacts, channels, blobs */ 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; - /* 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(); + /* 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; } 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/app/RepeaterDataStore.cpp b/zephcore/app/RepeaterDataStore.cpp index 748b42c..7f09476 100644 --- a/zephcore/app/RepeaterDataStore.cpp +++ b/zephcore/app/RepeaterDataStore.cpp @@ -11,11 +11,6 @@ LOG_MODULE_REGISTER(zephcore_repeater_store, CONFIG_ZEPHCORE_DATASTORE_LOG_LEVEL); -/* File paths for repeater data */ -#define REPEATER_DIR "/lfs/repeater" -#define IDENTITY_FILE "/lfs/repeater/_main.id" -#define PREFS_FILE "/lfs/repeater/prefs" - RepeaterDataStore::RepeaterDataStore() : _initialized(false) { } @@ -24,63 +19,80 @@ bool RepeaterDataStore::begin() { /* Create repeater directory if it doesn't exist */ struct fs_dirent entry; - int ret = fs_stat(REPEATER_DIR, &entry); + int ret = fs_stat(BASE_PATH, &entry); if (ret < 0) { - ret = fs_mkdir(REPEATER_DIR); + ret = fs_mkdir(BASE_PATH); if (ret < 0 && ret != -EEXIST) { - LOG_ERR("Failed to create %s: %d", REPEATER_DIR, ret); + LOG_ERR("Failed to create %s: %d", BASE_PATH, ret); return false; } - LOG_INF("Created %s directory", REPEATER_DIR); + LOG_INF("Created %s directory", BASE_PATH); } _initialized = true; - LOG_INF("RepeaterDataStore initialized"); + LOG_INF("RepeaterDataStore initialized at %s", BASE_PATH); return true; } +const char* RepeaterDataStore::getBasePath() const { return BASE_PATH; } + +const char* RepeaterDataStore::getAclPath() const { + static char buf[48]; + snprintf(buf, sizeof(buf), "%s/acl", BASE_PATH); + return buf; +} + +const char* RepeaterDataStore::getRegionsPath() const { + static char buf[48]; + snprintf(buf, sizeof(buf), "%s/regions2", BASE_PATH); + return buf; +} + bool RepeaterDataStore::loadIdentity(mesh::LocalIdentity& id) { + char path[48]; + snprintf(path, sizeof(path), "%s/_main.id", BASE_PATH); + struct fs_file_t file; fs_file_t_init(&file); - int ret = fs_open(&file, IDENTITY_FILE, FS_O_READ); + int ret = fs_open(&file, path, FS_O_READ); if (ret < 0) { - LOG_WRN("No identity file at %s", IDENTITY_FILE); - return false; // Caller must generate new identity + LOG_WRN("No identity file at %s", path); + return false; } uint8_t buf[PRV_KEY_SIZE + PUB_KEY_SIZE]; ssize_t n = fs_read(&file, buf, sizeof(buf)); fs_close(&file); - LOG_INF("loadIdentity: read %d bytes from %s", (int)n, IDENTITY_FILE); + LOG_INF("loadIdentity: read %d bytes from %s", (int)n, path); if (n >= PRV_KEY_SIZE) { - LOG_INF("loadIdentity: calling readFrom (will derive pubkey if n=%d)", (int)n); if (id.readFrom(buf, n)) { - LOG_INF("Loaded identity from %s", IDENTITY_FILE); + LOG_INF("Loaded identity from %s", path); return true; } LOG_ERR("loadIdentity: readFrom failed"); } LOG_ERR("Identity file corrupt"); - return false; // Caller must generate new identity + return false; } bool RepeaterDataStore::saveIdentity(const mesh::LocalIdentity& id) { - /* Ensure directory exists */ if (!_initialized) begin(); - /* Remove old file first */ - fs_unlink(IDENTITY_FILE); + char path[48]; + snprintf(path, sizeof(path), "%s/_main.id", BASE_PATH); + + fs_unlink(path); struct fs_file_t file; fs_file_t_init(&file); - int ret = fs_open(&file, IDENTITY_FILE, FS_O_CREATE | FS_O_WRITE); + int ret = fs_open(&file, path, FS_O_CREATE | FS_O_WRITE); if (ret < 0) { - LOG_ERR("Failed to open %s for write: %d", IDENTITY_FILE, ret); + LOG_ERR("Failed to open %s for write: %d", path, ret); return false; } @@ -90,7 +102,7 @@ bool RepeaterDataStore::saveIdentity(const mesh::LocalIdentity& id) { fs_close(&file); if (n == len) { - LOG_INF("Saved identity to %s", IDENTITY_FILE); + LOG_INF("Saved identity to %s", path); return true; } @@ -99,20 +111,22 @@ bool RepeaterDataStore::saveIdentity(const mesh::LocalIdentity& id) { } bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) { + char path[48]; + snprintf(path, sizeof(path), "%s/prefs", BASE_PATH); + struct fs_file_t file; fs_file_t_init(&file); - int ret = fs_open(&file, PREFS_FILE, FS_O_READ); + int ret = fs_open(&file, path, FS_O_READ); if (ret < 0) { - LOG_DBG("No prefs file at %s, using defaults", PREFS_FILE); + LOG_DBG("No prefs file at %s, using defaults", path); initNodePrefs(&prefs); strcpy(prefs.node_name, "Repeater"); return true; } - /* Get file size first */ struct fs_dirent entry; - ret = fs_stat(PREFS_FILE, &entry); + ret = fs_stat(path, &entry); LOG_INF("loadPrefs: file size = %d bytes", ret < 0 ? 0 : (int)entry.size); uint8_t pad[8]; @@ -155,7 +169,7 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) { fs_read(&file, prefs.owner_info, sizeof(prefs.owner_info)); fs_close(&file); - LOG_INF("Loaded prefs from %s", PREFS_FILE); + LOG_INF("Loaded prefs from %s", path); LOG_INF(" name='%s' freq=%.3f sf=%u bw=%.1f tx_pwr=%d", prefs.node_name, (double)prefs.freq, prefs.sf, (double)prefs.bw, prefs.tx_power_dbm); @@ -175,18 +189,19 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) { } bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) { - /* Ensure directory exists */ if (!_initialized) begin(); - /* Remove old file first */ - fs_unlink(PREFS_FILE); + char path[48]; + snprintf(path, sizeof(path), "%s/prefs", BASE_PATH); + + fs_unlink(path); struct fs_file_t file; fs_file_t_init(&file); - int ret = fs_open(&file, PREFS_FILE, FS_O_CREATE | FS_O_WRITE); + int ret = fs_open(&file, path, FS_O_CREATE | FS_O_WRITE); if (ret < 0) { - LOG_ERR("Failed to open %s for write: %d", PREFS_FILE, ret); + LOG_ERR("Failed to open %s for write: %d", path, ret); return false; } @@ -231,28 +246,27 @@ bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) { fs_write(&file, prefs.owner_info, sizeof(prefs.owner_info)); fs_close(&file); - LOG_INF("Saved prefs to %s", PREFS_FILE); + LOG_INF("Saved prefs to %s", path); return true; } bool RepeaterDataStore::formatFileSystem() { - LOG_WRN("Factory reset: erasing repeater data..."); + LOG_WRN("Factory reset: erasing repeater data at %s", BASE_PATH); - /* Delete all files in /lfs/repeater/ */ struct fs_dir_t dir; fs_dir_t_init(&dir); - int ret = fs_opendir(&dir, REPEATER_DIR); + int ret = fs_opendir(&dir, BASE_PATH); if (ret < 0) { LOG_WRN("No repeater directory to erase"); return true; } struct fs_dirent entry; - char path[280]; /* REPEATER_DIR (14) + "/" (1) + MAX_FILE_NAME (255) + null */ + char path[280]; while (fs_readdir(&dir, &entry) == 0 && entry.name[0] != '\0') { - snprintf(path, sizeof(path), "%s/%s", REPEATER_DIR, entry.name); + snprintf(path, sizeof(path), "%s/%s", BASE_PATH, entry.name); LOG_INF("Deleting %s", path); fs_unlink(path); } diff --git a/zephcore/app/RepeaterDataStore.h b/zephcore/app/RepeaterDataStore.h index 6747c21..149d4f4 100644 --- a/zephcore/app/RepeaterDataStore.h +++ b/zephcore/app/RepeaterDataStore.h @@ -31,17 +31,18 @@ public: bool savePrefs(const NodePrefs& prefs); /* ACL management - paths passed to ClientACL */ - const char* getAclPath() const { return "/lfs/repeater/acl"; } + const char* getAclPath() const; /* Region management - paths passed to RegionMap */ - const char* getRegionsPath() const { return "/lfs/repeater/regions2"; } + const char* getRegionsPath() const; /* Factory reset - erase all repeater data */ bool formatFileSystem(); /* Get base path for repeater storage */ - static const char* getBasePath() { return "/lfs/repeater"; } + const char* getBasePath() const; private: bool _initialized; + static constexpr const char* BASE_PATH = "/lfs/repeater"; }; diff --git a/zephcore/boards/common/filesystem.dtsi b/zephcore/boards/common/filesystem.dtsi index 3380a52..e2ab59b 100644 --- a/zephcore/boards/common/filesystem.dtsi +++ b/zephcore/boards/common/filesystem.dtsi @@ -1,22 +1,10 @@ /* + * 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 5023680..f15c061 100644 --- a/zephcore/boards/common/nrf52_common.conf +++ b/zephcore/boards/common/nrf52_common.conf @@ -36,12 +36,11 @@ CONFIG_SOC_FLASH_NRF_PARTIAL_ERASE=y CONFIG_NORDIC_QSPI_NOR=n # ========== Storage ========== -# BLE bonds via file-based settings on InternalFS (replaces NVS). -# No NVS partition on nRF52 — ExtraFS occupies that region. +# BLE bonds via file-based settings on LittleFS (replaces NVS). CONFIG_NVS=n CONFIG_SETTINGS_NVS=n CONFIG_SETTINGS_FILE=y -CONFIG_SETTINGS_FILE_PATH="/ifs/settings" +CONFIG_SETTINGS_FILE_PATH="/lfs/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 index 5e3aa14..a726ad2 100644 --- a/zephcore/boards/common/nrf52_partitions_sdv6.dtsi +++ b/zephcore/boards/common/nrf52_partitions_sdv6.dtsi @@ -1,7 +1,6 @@ /* * 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 @@ -19,8 +18,7 @@ * 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) + * 0xD4000 - 0xF4000 (128KB) LittleFS (/lfs automount) * 0xF4000 - 0x100000 (48KB) UF2 bootloader (reserved) */ @@ -43,18 +41,10 @@ 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>; + /* LittleFS — 128KB: identity, prefs, contacts, channels, blobs */ + lfs_partition: partition@d4000 { + label = "lfs"; + reg = <0x000D4000 0x00020000>; }; /* UF2 bootloader — 48KB (reserved) */ @@ -65,3 +55,6 @@ }; }; }; + +/* LittleFS /lfs automount */ +#include "filesystem.dtsi" diff --git a/zephcore/boards/common/nrf52_partitions_sdv7.dtsi b/zephcore/boards/common/nrf52_partitions_sdv7.dtsi index 4d4f663..9bf9c75 100644 --- a/zephcore/boards/common/nrf52_partitions_sdv7.dtsi +++ b/zephcore/boards/common/nrf52_partitions_sdv7.dtsi @@ -1,7 +1,6 @@ /* * 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 │ @@ -48,8 +47,7 @@ * 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) + * 0xD4000 - 0xF4000 (128KB) LittleFS (/lfs automount) * 0xF4000 - 0x100000 (48KB) UF2 bootloader (reserved) */ @@ -72,18 +70,10 @@ 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>; + /* LittleFS — 128KB: identity, prefs, contacts, channels, blobs */ + lfs_partition: partition@d4000 { + label = "lfs"; + reg = <0x000D4000 0x00020000>; }; /* UF2 bootloader — 48KB (reserved) */ @@ -94,3 +84,6 @@ }; }; }; + +/* LittleFS /lfs automount */ +#include "filesystem.dtsi" diff --git a/zephcore/boards/common/qspi-ext.dtsi b/zephcore/boards/common/qspi-ext.dtsi index 0d8b761..514f79a 100644 --- a/zephcore/boards/common/qspi-ext.dtsi +++ b/zephcore/boards/common/qspi-ext.dtsi @@ -13,7 +13,7 @@ * 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. + * Works with all platforms using /lfs mount. */ / { diff --git a/zephcore/boards/nrf52840/rak4631/board.conf b/zephcore/boards/nrf52840/rak4631/board.conf index 651e936..8853a8f 100644 --- a/zephcore/boards/nrf52840/rak4631/board.conf +++ b/zephcore/boards/nrf52840/rak4631/board.conf @@ -2,7 +2,7 @@ # Board-specific configuration - pins and unique features only # # SoftDevice: s140 v6.1.1 (app@0x26000, bootloader@0xF4000) -# Arduino linker: nrf52840_s140_v6_extrafs.ld +# Arduino linker: nrf52840_s140_v6.ld # # Hardware: # - SX1262 LoRa on SPI0 (built into RAK4631 module) diff --git a/zephcore/helpers/RegionMap.cpp b/zephcore/helpers/RegionMap.cpp index 79758a1..b6b130c 100644 --- a/zephcore/helpers/RegionMap.cpp +++ b/zephcore/helpers/RegionMap.cpp @@ -32,7 +32,7 @@ bool RegionMap::is_name_char(uint8_t c) { } bool RegionMap::load(const char* path) { - const char* filepath = path ? path : "/lfs/repeater/regions2"; + const char* filepath = path; struct fs_file_t file; fs_file_t_init(&file); @@ -76,7 +76,7 @@ bool RegionMap::load(const char* path) { } bool RegionMap::save(const char* path) { - const char* filepath = path ? path : "/lfs/repeater/regions2"; + const char* filepath = path; // Remove old file first fs_unlink(filepath); diff --git a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch index a3733d8..bc13095 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 37807cebe38..ecabeb9431f 100644 +index 37807cebe38..7d2f95b277e 100644 --- a/drivers/lora/native/sx126x/sx126x.c +++ b/drivers/lora/native/sx126x/sx126x.c @@ -9,10 +9,19 @@ @@ -95,13 +95,13 @@ index 37807cebe38..ecabeb9431f 100644 { uint8_t val = boosted ? SX126X_RX_GAIN_BOOSTED : SX126X_RX_GAIN_POWER_SAVING; + int ret; -+ + +- return sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &val, 1); + ret = sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &val, 1); + if (ret < 0) { + return ret; + } - -- return sx126x_hal_write_regs(dev, SX126X_REG_RX_GAIN, &val, 1); ++ + /* Add RX gain register to retention list (DS §9.6) so the chip + * preserves the setting across mode transitions. Without this, + * register 0x08AC resets to power-saving on every SetRx. */ @@ -400,7 +400,7 @@ index 37807cebe38..ecabeb9431f 100644 /* Start transmission with 10 second timeout */ ret = sx126x_set_tx(dev, 10000); if (ret < 0) { -@@ -927,6 +1156,75 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, +@@ -927,6 +1156,116 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, return 0; } @@ -472,11 +472,52 @@ index 37807cebe38..ecabeb9431f 100644 + sx126x_set_rx_gain(dev, enable); + k_mutex_unlock(&data->lock); +} ++ ++void sx126x_reset_agc(const struct device *dev) ++{ ++ struct sx126x_data *data = dev->data; ++ const struct sx126x_hal_config *config = dev->config; ++ ++ k_mutex_lock(&data->lock, K_FOREVER); ++ ++ /* Warm sleep — powers down the analog frontend (resets AGC state) ++ * but preserves register configuration. */ ++ uint8_t sleep_cfg = SX126X_SLEEP_WARM_START; ++ ++ sx126x_hal_write_cmd(dev, SX126X_CMD_SET_SLEEP, &sleep_cfg, 1); ++ k_busy_wait(500); ++ ++ /* Wake to STANDBY_RC — required before Calibrate */ ++ sx126x_set_standby(dev, SX126X_STANDBY_RC); ++ ++ /* Full recalibration: ADC, PLL, image, RC oscillators */ ++ sx126x_calibrate(dev, SX126X_CALIBRATE_ALL); ++ k_busy_wait(5000); ++ sx126x_hal_wait_busy(dev, 50); ++ ++ /* Calibrate(0x7F) defaults image cal to 902-928 MHz band. ++ * Re-calibrate for the actual operating frequency. */ ++ if (data->config_valid) { ++ sx126x_calibrate_image(dev, data->config.frequency); ++ } ++ ++ /* Re-apply DIO2 as RF switch if configured */ ++ if (config->dio2_tx_enable) { ++ sx126x_set_dio2_as_rf_switch(dev, true); ++ } ++ ++ /* Re-apply RX boosted gain if it was enabled */ ++ if (data->rx_boost_enabled) { ++ sx126x_set_rx_gain(dev, true); ++ } ++ ++ k_mutex_unlock(&data->lock); ++} + static DEVICE_API(lora, sx126x_lora_api) = { .config = sx126x_lora_config, .send = sx126x_lora_send, -@@ -952,6 +1250,14 @@ static int sx126x_init(const struct device *dev) +@@ -952,6 +1291,14 @@ static int sx126x_init(const struct device *dev) data->dev = dev; atomic_set(&data->state, SX126X_STATE_IDLE); data->config_valid = false;