From f3cba97ab6d0d27cac93565d9dacfef11acb8562 Mon Sep 17 00:00:00 2001 From: Elan456 Date: Tue, 31 Mar 2026 19:31:30 -0400 Subject: [PATCH 01/12] fix: dataSaverSPI erase after buffer flush not before --- src/data_handling/DataSaverSPI.cpp | 53 ++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/src/data_handling/DataSaverSPI.cpp b/src/data_handling/DataSaverSPI.cpp index 9be11d6..867f2c7 100644 --- a/src/data_handling/DataSaverSPI.cpp +++ b/src/data_handling/DataSaverSPI.cpp @@ -67,38 +67,55 @@ int DataSaverSPI::addDataToBuffer(const uint8_t* data, size_t length) { return 0; } -// Write the entire buffer to flash int DataSaverSPI::flushBuffer() { if (bufferIndex == 0) { - return 1; // Nothing to flush + return 1; } - // Check if we need to wrap around - if (nextWriteAddress + bufferIndex > flash->size()) { - // Wrap around - nextWriteAddress = kDataStartAddress; - } + uint32_t writeAddress = nextWriteAddress; + + // Wrap before protection check / write + if (writeAddress + kBufferSize_bytes > flash->size()) { + writeAddress = kDataStartAddress; + } - // Check that we haven't wrapped around to the launch address while in post-launch mode - if (postLaunchMode && nextWriteAddress <= launchWriteAddress && nextWriteAddress + kBufferSize_bytes * 2 > launchWriteAddress) { + // Must check protection BEFORE writing + if (postLaunchMode && + writeAddress <= launchWriteAddress && + writeAddress + kBufferSize_bytes * 2 > launchWriteAddress) { isChipFullDueToPostLaunchProtection = true; - return -1; // Indicate no write due to post-launch protection + return -1; } - if (nextWriteAddress % SFLASH_SECTOR_SIZE == 0) { - if (!flash->eraseSector(nextWriteAddress / SFLASH_SECTOR_SIZE)) { - return -1; - } + // Fill unused buffer bytes with 0xFF (erased state) + if (bufferIndex < kBufferSize_bytes) { + memset(buffer + bufferIndex, kEmptyPageValue, + kBufferSize_bytes - bufferIndex); } - // Write 1 page of data - if (!flash->writeBuffer(nextWriteAddress, buffer, kBufferSize_bytes)) { + // Write current page + if (!flash->writeBuffer(writeAddress, buffer, kBufferSize_bytes)) { return -1; } - nextWriteAddress += kBufferSize_bytes; // keep it aligned to the buffer size or page size - bufferIndex = 0; // Reset the buffer + bufferIndex = 0; bufferFlushes++; + + // Advance + nextWriteAddress = writeAddress + kBufferSize_bytes; + if (nextWriteAddress >= flash->size()) { + nextWriteAddress = kDataStartAddress; + } + + // Pre-erase next sector if next write starts a new sector + if (nextWriteAddress % SFLASH_SECTOR_SIZE == 0) { + if (!flash->eraseSector(nextWriteAddress / SFLASH_SECTOR_SIZE)) { + // Current write already succeeded, so maybe don't treat this + // as a failed flush unless you want "next write prep failed" + return -1; + } + } + return 0; } From da358d4fd8a8f821f689e1f6d69a19743adafe21 Mon Sep 17 00:00:00 2001 From: Elan456 Date: Wed, 1 Apr 2026 00:07:12 -0400 Subject: [PATCH 02/12] test: max gle noise-based test less flaky --- test/test_ground_level_estimator/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_ground_level_estimator/main.cpp b/test/test_ground_level_estimator/main.cpp index 32cda24..e26c8ff 100644 --- a/test/test_ground_level_estimator/main.cpp +++ b/test/test_ground_level_estimator/main.cpp @@ -60,7 +60,7 @@ void test_ground_level_with_noise(void) std::normal_distribution noise(0.0f, 2.0f); // ±2m standard deviation // Feed noisy samples - for (int i = 0; i < 200; ++i) + for (int i = 0; i < 500; ++i) // At 200 samples, the test was a but flaky due to noise. { float noisyASL = trueGroundASL + noise(rng); float agl = estimator.update(noisyASL); @@ -68,7 +68,7 @@ void test_ground_level_with_noise(void) } // With enough samples, running average should converge close to true value - TEST_ASSERT_FLOAT_WITHIN(1.0f, trueGroundASL, estimator.getEGL()); + TEST_ASSERT_FLOAT_WITHIN(1.2f, trueGroundASL, estimator.getEGL()); } // ----------------------------------------------------------------------------- From 0941c0350dad7143c7c274c24fc1514c0ba3d541 Mon Sep 17 00:00:00 2001 From: Elan456 Date: Wed, 1 Apr 2026 00:07:47 -0400 Subject: [PATCH 03/12] fix: dataSaverSPI clears sectors one flush early to reduce flash ready wait times --- .gitignore | 1 + include/data_handling/DataSaverSPI.h | 5 +++ src/data_handling/DataSaverSPI.cpp | 31 +++++++++++++++++-- test/test_datasaver_spi/test_DataSaverSPI.cpp | 16 ++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a0f84c7..8cbb106 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ .DS_Store build/ *.csv +.codex diff --git a/include/data_handling/DataSaverSPI.h b/include/data_handling/DataSaverSPI.h index a40e942..83c11ef 100644 --- a/include/data_handling/DataSaverSPI.h +++ b/include/data_handling/DataSaverSPI.h @@ -6,6 +6,7 @@ #include "data_handling/DataSaver.h" #include #include +#include constexpr uint32_t kMetadataStartAddress = 0x000000; // Start writing metadata at the beginning of flash @@ -287,6 +288,10 @@ class DataSaverSPI : public IDataSaver { // If the flight computer boots and is already in post launch mode, do not write to flash. // Calling clearPostLaunchMode() will allow writing to flash again after a reboot. bool rebootedInPostLaunchMode_ = false; + + // Tracks which sector has already been pre-erased for the next boundary write. + // UINT32_MAX means "no prepared sector". + uint32_t preparedSectorNumber_ = std::numeric_limits::max(); }; #endif // DATA_SAVER_SPI_H diff --git a/src/data_handling/DataSaverSPI.cpp b/src/data_handling/DataSaverSPI.cpp index 3c8f50f..e3bd6e4 100644 --- a/src/data_handling/DataSaverSPI.cpp +++ b/src/data_handling/DataSaverSPI.cpp @@ -86,18 +86,42 @@ int DataSaverSPI::flushBuffer() { return -1; // Indicate no write due to post-launch protection } + // Fallback erase for first entry into a sector. In steady-state this sector + // should already be prepared by the previous flush. if (nextWriteAddress_ % SFLASH_SECTOR_SIZE == 0) { - if (!flash_->eraseSector(nextWriteAddress_ / SFLASH_SECTOR_SIZE)) { - return -1; + uint32_t const currentSectorNumber = nextWriteAddress_ / SFLASH_SECTOR_SIZE; + if (preparedSectorNumber_ != currentSectorNumber) { + if (!flash_->eraseSector(currentSectorNumber)) { + return -1; + } + preparedSectorNumber_ = currentSectorNumber; } } - // Write 1 page of data + // Write 1 page of data. if (!flash_->writeBuffer(nextWriteAddress_, buffer_, kBufferSize_bytes)) { return -1; } nextWriteAddress_ += kBufferSize_bytes; // keep it aligned to the buffer size or page size + + // Keep next write address in the data region immediately so boundary prep + // never targets a sector beyond flash capacity. + if (nextWriteAddress_ >= flash_->size()) { + nextWriteAddress_ = kDataStartAddress; + } + + // Pre-erase one step earlier: after writing the previous page, erase the + // sector that the next page will start in. This lets erase latency overlap + // with buffer fill time. + if (nextWriteAddress_ % SFLASH_SECTOR_SIZE == 0) { + uint32_t const nextSectorNumber = nextWriteAddress_ / SFLASH_SECTOR_SIZE; + if (!flash_->eraseSector(nextSectorNumber)) { + return -1; + } + preparedSectorNumber_ = nextSectorNumber; + } + bufferIndex_ = 0; // Reset the buffer bufferFlushes_++; return 0; @@ -235,6 +259,7 @@ void DataSaverSPI::clearInternalState() { launchWriteAddress_ = 0; bufferFlushes_ = 0; isChipFullDueToPostLaunchProtection_ = false; + preparedSectorNumber_ = std::numeric_limits::max(); } void DataSaverSPI::eraseAllData() { diff --git a/test/test_datasaver_spi/test_DataSaverSPI.cpp b/test/test_datasaver_spi/test_DataSaverSPI.cpp index 9710556..09959d2 100644 --- a/test/test_datasaver_spi/test_DataSaverSPI.cpp +++ b/test/test_datasaver_spi/test_DataSaverSPI.cpp @@ -86,6 +86,21 @@ void test_launch_detected(void) { TEST_ASSERT_NOT_EQUAL(0, dss->getLaunchWriteAddress()); } +void test_next_sector_is_erased_before_crossing_boundary(void) { + for (size_t i = 0; i < FAKE_MEMORY_SIZE_BYTES; i++) { + flash->fakeMemory[i] = 0x00; + } + dss->clearInternalState(); + + // 817 timestamp writes trigger exactly 16 flushes (one 4 KiB sector). + for (uint32_t i = 0; i < 817U; i++) { + TEST_ASSERT_EQUAL(0, dss->saveTimestamp(i)); + } + + TEST_ASSERT_EQUAL_UINT32(16U, dss->getBufferFlushes()); + TEST_ASSERT_EQUAL_HEX8(0xFF, flash->fakeMemory[kDataStartAddress + SFLASH_SECTOR_SIZE]); +} + void test_record_size(void) { Record_t record = {1, 2.0f}; TEST_ASSERT_EQUAL(5, sizeof(record)); // 1 byte for name, 4 bytes for data @@ -106,5 +121,6 @@ int main(void) { RUN_TEST(test_clearplm_next_write); RUN_TEST(test_erase_all_data); RUN_TEST(test_launch_detected); + RUN_TEST(test_next_sector_is_erased_before_crossing_boundary); return UNITY_END(); } From d5068e2a8ad78599ee38566b313270f12b16caff Mon Sep 17 00:00:00 2001 From: Elan456 Date: Wed, 1 Apr 2026 00:24:10 -0400 Subject: [PATCH 04/12] ai: encourage regression testing in the AGENTS.md --- AGENTS.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6525cbc..d1f7f14 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,8 +105,9 @@ python -m http.server --directory build/doxygen 8000 1. Prefer small, focused patches. 2. If behavior changes, update or add tests in `test/` to cover it. -3. Preserve public API names unless the task explicitly asks for a breaking change. -4. Keep docs in sync when changing module behavior: +3. If you are patching a bug, add a regression test that fails without the fix and passes with it. +4. Preserve public API names unless the task explicitly asks for a breaking change. +5. Keep docs in sync when changing module behavior: - top-level `README.md` for high-level behavior, - `docs/*.md` for detailed formats/protocols. From 8e1f3896af8ca5a1225e125bce28daf48ef999ed Mon Sep 17 00:00:00 2001 From: Elan456 Date: Wed, 1 Apr 2026 01:11:24 -0400 Subject: [PATCH 05/12] docs: add docstrings for private methods in DataSaverSPI --- include/data_handling/DataSaverSPI.h | 89 ++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/include/data_handling/DataSaverSPI.h b/include/data_handling/DataSaverSPI.h index 83c11ef..88b0be9 100644 --- a/include/data_handling/DataSaverSPI.h +++ b/include/data_handling/DataSaverSPI.h @@ -70,12 +70,16 @@ class DataSaverSPI : public IDataSaver { * @param timestamp_ms Timestamp in milliseconds to record. * @note When to use: emitted internally when gaps exceed * timestampInterval_ms_, or explicitly in tests. + * @return int 0 on success, 1 when writes are blocked by post-launch state, + * and -1 on write/buffer error. */ int saveTimestamp(uint32_t timestamp_ms); /** * @brief Initialize the flash chip and metadata. * @note When to use: call during setup before any saveDataPoint usage. + * @return true when flash is initialized and writable for logging. + * @return false when initialization fails or chip is already in post-launch mode. */ virtual bool begin() override; @@ -117,6 +121,7 @@ class DataSaverSPI : public IDataSaver { * * The rocket may not be recovered for several hours, this prevents the cool launch data * from being overwitten with boring laying-on-the-ground data. + * @param launchTimestamp_ms Timestamp at which launch was detected. */ void launchDetected(uint32_t launchTimestamp_ms); @@ -126,6 +131,7 @@ class DataSaverSPI : public IDataSaver { * @param ignoreEmptyPages Skip pages that appear unwritten. * @note When to use: post-flight data retrieval before erasing or * redeploying the flash chip. + * @return void */ void dumpData(Stream &serial, bool ignoreEmptyPages); @@ -147,6 +153,7 @@ class DataSaverSPI : public IDataSaver { /** * @brief Returns the last timestamp that was actually written to flash. + * @return Last persisted timestamp in milliseconds. */ uint32_t getLastTimestamp() const { return lastTimestamp_ms_; @@ -155,15 +162,24 @@ class DataSaverSPI : public IDataSaver { /** * @brief Returns the last DataPoint that was written (not necessarily * including timestamp, just the data chunk). + * @return Copy of the last cached data point. */ DataPoint getLastDataPoint() const { return lastDataPoint_; } + /** + * @brief Returns the launch-protected address computed during launch detection. + * @return Address in flash used as post-launch protection boundary. + */ uint32_t getLaunchWriteAddress() const { return launchWriteAddress_; } + /** + * @brief Returns the next flash address where a full page will be written. + * @return Next write address in flash. + */ uint32_t getNextWriteAddress() const { return nextWriteAddress_; } @@ -179,6 +195,7 @@ class DataSaverSPI : public IDataSaver { /** * @brief Returns whether the flash chip is in post-launch mode * without updating the post-launch mode flag or reading from flash. + * @return true when in post-launch mode; otherwise false. */ bool quickGetPostLaunchMode() { return this->postLaunchMode_; @@ -212,11 +229,18 @@ class DataSaverSPI : public IDataSaver { /** * @brief Helper to write a block of bytes to flash at the current * write address and advance that pointer. + * @param data Pointer to bytes to write. + * @param length Number of bytes to write. + * @return true on successful write and pointer advance; otherwise false. */ bool writeToFlash(const uint8_t* data, size_t length); /** * @brief Helper to read a block of bytes from flash (updates read pointer externally). + * @param readAddress Input/output flash address. Advanced by `length` on success. + * @param buffer Output byte buffer. + * @param length Number of bytes to read. + * @return true on successful read and pointer advance; otherwise false. */ bool readFromFlash(uint32_t& readAddress, uint8_t* buffer, size_t length); @@ -229,6 +253,7 @@ class DataSaverSPI : public IDataSaver { /** * @brief Returns the current buffer index * Useful for testing + * @return Number of bytes currently buffered. */ size_t getBufferIndex() const { return bufferIndex_; @@ -237,20 +262,74 @@ class DataSaverSPI : public IDataSaver { /** * @brief Returns the current buffer flush count * Useful for testing + * @return Number of successful page flushes. */ uint32_t getBufferFlushes() const { return bufferFlushes_; } + /** + * @brief Returns whether writes have been stopped by post-launch protection. + * @return true if chip-full post-launch protection latch is set; otherwise false. + */ bool getIsChipFullDueToPostLaunchProtection() const { return isChipFullDueToPostLaunchProtection_; } + /** + * @brief Returns whether startup detected existing post-launch mode metadata. + * @return true if rebooted in post-launch mode; otherwise false. + */ bool getRebootedInPostLaunchMode() const { return rebootedInPostLaunchMode_; } +#ifdef UNIT_TEST + /** + * @brief Test-only helper to override internal post-launch state. + * @param nextWriteAddress_in Next write address to inject. + * @param launchWriteAddress_in Launch-protected address to inject. + * @param postLaunchMode_in Post-launch mode flag to inject. + */ + void setPostLaunchStateForTest(uint32_t nextWriteAddress_in, + uint32_t launchWriteAddress_in, + bool postLaunchMode_in) { + nextWriteAddress_ = nextWriteAddress_in; + launchWriteAddress_ = launchWriteAddress_in; + postLaunchMode_ = postLaunchMode_in; + isChipFullDueToPostLaunchProtection_ = false; + } +#endif + private: + /** + * @brief Normalizes an address into the data region when crossing flash end. + * @param address Candidate flash address. + * @return `address` when in range, otherwise `kDataStartAddress`. + */ + uint32_t normalizeDataAddress(uint32_t address) const; + + /** + * @brief Checks if a sector is protected by post-launch rules. + * @param sectorNumber Sector index to evaluate. + * @return true when sector contains `launchWriteAddress_` and post-launch mode is active. + * @return false otherwise. + */ + bool isProtectedLaunchSector(uint32_t sectorNumber) const; + + /** + * @brief Erases a sector unless blocked by post-launch protection. + * @param sectorNumber Sector index to erase. + * @return int 0 on successful erase, -1 when blocked or flash erase fails. + */ + int eraseSectorIfAllowed(uint32_t sectorNumber); + + /** + * @brief Checks if the upcoming write window would violate post-launch protection. + * @return true when writing must stop and chip-full protection is latched. + * @return false when write may proceed. + */ + bool shouldStopForPostLaunchWindow(); /** * @brief Flushes the buffer to flash. @@ -273,10 +352,20 @@ class DataSaverSPI : public IDataSaver { // Overloaded functions to add data to the buffer from a Record_t or TimestampRecord_t // More efficient than callling addDataToBuffer with each part of the record + /** + * @brief Adds a 5-byte record payload to the page buffer. + * @param record Pointer to packed record bytes. + * @return int 0 on success; -1 on flush/buffer error. + */ int addRecordToBuffer(Record_t * record) { return addDataToBuffer(reinterpret_cast(record), 5); } + /** + * @brief Adds a 5-byte timestamp record payload to the page buffer. + * @param record Pointer to packed timestamp record bytes. + * @return int 0 on success; -1 on flush/buffer error. + */ int addRecordToBuffer(TimestampRecord_t * record) { return addDataToBuffer(reinterpret_cast(record), 5); } From 7b59eff962d3e303ea45d542f57dbcf342c2a9a9 Mon Sep 17 00:00:00 2001 From: Elan456 Date: Wed, 1 Apr 2026 01:17:33 -0400 Subject: [PATCH 06/12] ref: use helper functions to cleanup the flushBuffer logic in DataSaverSPI --- src/data_handling/DataSaverSPI.cpp | 78 ++++++++++++++----- test/test_datasaver_spi/test_DataSaverSPI.cpp | 76 +++++++++++++++++- test/test_ground_level_estimator/main.cpp | 2 +- 3 files changed, 130 insertions(+), 26 deletions(-) diff --git a/src/data_handling/DataSaverSPI.cpp b/src/data_handling/DataSaverSPI.cpp index e3bd6e4..8f75d83 100644 --- a/src/data_handling/DataSaverSPI.cpp +++ b/src/data_handling/DataSaverSPI.cpp @@ -68,33 +68,70 @@ int DataSaverSPI::addDataToBuffer(const uint8_t* data, size_t length) { return 0; } +uint32_t DataSaverSPI::normalizeDataAddress(uint32_t address) const { + if (address >= flash_->size()) { + return kDataStartAddress; + } + return address; +} + +bool DataSaverSPI::isProtectedLaunchSector(uint32_t sectorNumber) const { + if (!postLaunchMode_) { + return false; + } + return sectorNumber == launchWriteAddress_ / SFLASH_SECTOR_SIZE; +} + +int DataSaverSPI::eraseSectorIfAllowed(uint32_t sectorNumber) { + if (isProtectedLaunchSector(sectorNumber)) { + isChipFullDueToPostLaunchProtection_ = true; // We can't erase the next sector to allow for more writes. + return -1; + } + if (!flash_->eraseSector(sectorNumber)) { + return -1; + } + preparedSectorNumber_ = sectorNumber; + return 0; +} + +bool DataSaverSPI::shouldStopForPostLaunchWindow() { + if (!postLaunchMode_) { + return false; + } + if (nextWriteAddress_ > launchWriteAddress_) { + return false; + } + if (nextWriteAddress_ + kBufferSize_bytes * 2 <= launchWriteAddress_) { + return false; + } + isChipFullDueToPostLaunchProtection_ = true; + return true; +} + // Write the entire buffer to flash. int DataSaverSPI::flushBuffer() { if (bufferIndex_ == 0) { return 1; // Nothing to flush } - // Check if we need to wrap around - if (nextWriteAddress_ + bufferIndex_ > flash_->size()) { - // Wrap around + // If the next write would go past the end of the flash, wrap around to the beginning of the data section. + if (nextWriteAddress_ + kBufferSize_bytes > flash_->size()) { nextWriteAddress_ = kDataStartAddress; - } + } - // Check that we haven't wrapped around to the launch address while in post-launch mode - if (postLaunchMode_ && nextWriteAddress_ <= launchWriteAddress_ && nextWriteAddress_ + kBufferSize_bytes * 2 > launchWriteAddress_) { + if (shouldStopForPostLaunchWindow()) { isChipFullDueToPostLaunchProtection_ = true; - return -1; // Indicate no write due to post-launch protection + return -1; } // Fallback erase for first entry into a sector. In steady-state this sector // should already be prepared by the previous flush. - if (nextWriteAddress_ % SFLASH_SECTOR_SIZE == 0) { + if (nextWriteAddress_ % SFLASH_SECTOR_SIZE == 0U) { uint32_t const currentSectorNumber = nextWriteAddress_ / SFLASH_SECTOR_SIZE; if (preparedSectorNumber_ != currentSectorNumber) { - if (!flash_->eraseSector(currentSectorNumber)) { + if (eraseSectorIfAllowed(currentSectorNumber) < 0) { return -1; } - preparedSectorNumber_ = currentSectorNumber; } } @@ -103,23 +140,22 @@ int DataSaverSPI::flushBuffer() { return -1; } - nextWriteAddress_ += kBufferSize_bytes; // keep it aligned to the buffer size or page size - - // Keep next write address in the data region immediately so boundary prep - // never targets a sector beyond flash capacity. - if (nextWriteAddress_ >= flash_->size()) { - nextWriteAddress_ = kDataStartAddress; - } + nextWriteAddress_ = normalizeDataAddress(nextWriteAddress_ + kBufferSize_bytes); // Pre-erase one step earlier: after writing the previous page, erase the // sector that the next page will start in. This lets erase latency overlap // with buffer fill time. - if (nextWriteAddress_ % SFLASH_SECTOR_SIZE == 0) { + if (nextWriteAddress_ % SFLASH_SECTOR_SIZE == 0U) { uint32_t const nextSectorNumber = nextWriteAddress_ / SFLASH_SECTOR_SIZE; - if (!flash_->eraseSector(nextSectorNumber)) { - return -1; + + // If the next sector, is protected, skip the erase and still return 0 + if (!isProtectedLaunchSector(nextSectorNumber)) { + + // Try to erase the next sector and return -1 on failure + if (eraseSectorIfAllowed(nextSectorNumber) < 0) { + return -1; + } } - preparedSectorNumber_ = nextSectorNumber; } bufferIndex_ = 0; // Reset the buffer diff --git a/test/test_datasaver_spi/test_DataSaverSPI.cpp b/test/test_datasaver_spi/test_DataSaverSPI.cpp index 09959d2..3a2d6e6 100644 --- a/test/test_datasaver_spi/test_DataSaverSPI.cpp +++ b/test/test_datasaver_spi/test_DataSaverSPI.cpp @@ -92,13 +92,79 @@ void test_next_sector_is_erased_before_crossing_boundary(void) { } dss->clearInternalState(); - // 817 timestamp writes trigger exactly 16 flushes (one 4 KiB sector). - for (uint32_t i = 0; i < 817U; i++) { + size_t const recordsPerFlush = DataSaverSPI::kBufferSize_bytes / sizeof(TimestampRecord_t); + uint32_t flushesToSectorBoundary = 0; + uint32_t expectedBoundaryAddress = kDataStartAddress; + do { + expectedBoundaryAddress += DataSaverSPI::kBufferSize_bytes; + if (expectedBoundaryAddress >= FAKE_MEMORY_SIZE_BYTES) { + expectedBoundaryAddress = kDataStartAddress; + } + flushesToSectorBoundary++; + } while (expectedBoundaryAddress % SFLASH_SECTOR_SIZE != 0U); + + // +1 triggers the Nth flush by overflowing a full buffer with one more record. + uint32_t const writesNeeded = static_cast(flushesToSectorBoundary * recordsPerFlush) + 1U; + for (uint32_t i = 0; i < writesNeeded; i++) { TEST_ASSERT_EQUAL(0, dss->saveTimestamp(i)); } - TEST_ASSERT_EQUAL_UINT32(16U, dss->getBufferFlushes()); - TEST_ASSERT_EQUAL_HEX8(0xFF, flash->fakeMemory[kDataStartAddress + SFLASH_SECTOR_SIZE]); + TEST_ASSERT_EQUAL_UINT32(flushesToSectorBoundary, dss->getBufferFlushes()); + TEST_ASSERT_EQUAL_HEX8(0xFF, flash->fakeMemory[expectedBoundaryAddress]); +} + +void test_pre_erase_skips_protected_launch_sector(void) { + for (size_t i = 0; i < FAKE_MEMORY_SIZE_BYTES; i++) { + flash->fakeMemory[i] = 0x00; + } + dss->clearInternalState(); + + uint32_t const protectedSectorNumber = kDataStartAddress / SFLASH_SECTOR_SIZE + 1U; + uint32_t const protectedSectorStartAddress = protectedSectorNumber * SFLASH_SECTOR_SIZE; + uint32_t const launchWriteAddress = protectedSectorStartAddress + 3000U; + uint32_t const writeAddressBeforeBoundary = protectedSectorStartAddress - DataSaverSPI::kBufferSize_bytes; + + flash->fakeMemory[launchWriteAddress] = 0x5A; + dss->setPostLaunchStateForTest(writeAddressBeforeBoundary, launchWriteAddress, true); + + uint32_t const recordsPerFlush = DataSaverSPI::kBufferSize_bytes / sizeof(TimestampRecord_t); + + int result = 0; + for (uint32_t i = 0; i < recordsPerFlush + 1U; i++) { + result = dss->saveTimestamp(i); + } + + // This flush writes successfully; only pre-erase is skipped for the + // protected next sector. + TEST_ASSERT_EQUAL(0, result); + TEST_ASSERT_FALSE(dss->getIsChipFullDueToPostLaunchProtection()); + TEST_ASSERT_EQUAL_HEX8(0x5A, flash->fakeMemory[launchWriteAddress]); + + for (uint32_t i = 0; i < recordsPerFlush; i++) { + result = dss->saveTimestamp(1000U + i); + } + + TEST_ASSERT_EQUAL(-1, result); + TEST_ASSERT_TRUE(dss->getIsChipFullDueToPostLaunchProtection()); + TEST_ASSERT_EQUAL_HEX8(0x5A, flash->fakeMemory[launchWriteAddress]); +} + +void test_flush_wraps_using_full_page_write_size(void) { + dss->clearInternalState(); + TEST_ASSERT_EQUAL(0, dss->saveTimestamp(1U)); + + uint32_t const nearEndAddress = FAKE_MEMORY_SIZE_BYTES - DataSaverSPI::kBufferSize_bytes + 1U; + dss->setPostLaunchStateForTest(nearEndAddress, 0U, false); + + int result = 0; + uint32_t const recordsPerFlush = DataSaverSPI::kBufferSize_bytes / sizeof(TimestampRecord_t); + for (uint32_t i = 0; i < recordsPerFlush; i++) { + result = dss->saveTimestamp(2U + i); + } + + TEST_ASSERT_EQUAL(0, result); + TEST_ASSERT_EQUAL_UINT32(1U, dss->getBufferFlushes()); + TEST_ASSERT_EQUAL_UINT32(kDataStartAddress + DataSaverSPI::kBufferSize_bytes, dss->getNextWriteAddress()); } void test_record_size(void) { @@ -122,5 +188,7 @@ int main(void) { RUN_TEST(test_erase_all_data); RUN_TEST(test_launch_detected); RUN_TEST(test_next_sector_is_erased_before_crossing_boundary); + RUN_TEST(test_pre_erase_skips_protected_launch_sector); + RUN_TEST(test_flush_wraps_using_full_page_write_size); return UNITY_END(); } diff --git a/test/test_ground_level_estimator/main.cpp b/test/test_ground_level_estimator/main.cpp index e26c8ff..3dd1daa 100644 --- a/test/test_ground_level_estimator/main.cpp +++ b/test/test_ground_level_estimator/main.cpp @@ -60,7 +60,7 @@ void test_ground_level_with_noise(void) std::normal_distribution noise(0.0f, 2.0f); // ±2m standard deviation // Feed noisy samples - for (int i = 0; i < 500; ++i) // At 200 samples, the test was a but flaky due to noise. + for (int i = 0; i < 500; ++i) // At 200 samples, the test was a bit flaky due to noise. { float noisyASL = trueGroundASL + noise(rng); float agl = estimator.update(noisyASL); From 18ae17032cd9aa4aa843d47ae1a1aa3691c834b1 Mon Sep 17 00:00:00 2001 From: Elan456 Date: Wed, 1 Apr 2026 01:33:02 -0400 Subject: [PATCH 07/12] fix: DataSaverSPI returns 1 on plm block, 0 on success, -1 on error --- AGENTS.md | 2 +- include/data_handling/DataSaverSPI.h | 5 +++-- src/data_handling/DataSaverSPI.cpp | 17 ++++++++++++----- test/test_datasaver_spi/test_DataSaverSPI.cpp | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d1f7f14..111df4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ Avionics is our modular C++ (Arduino-core compatible) library used for all of ou ## Hard Rules 1. Never include Arduino headers directly (``, etc.) from Avionics code. -2. Always include `hal/ArduinoHAL.h` for Arduino-facing APIs. +2. Always include `ArduinoHAL.h` from `hal/` for Arduino-facing APIs. 3. Keep host-native tests buildable; avoid adding dependencies that only exist on embedded targets. 4. Maintain `include/` and `src/` parity for non-header-only modules. 5. Keep changes compatible with the repo's C++ standard (`-std=c++11` in `platformio.ini`). diff --git a/include/data_handling/DataSaverSPI.h b/include/data_handling/DataSaverSPI.h index 88b0be9..9e82d5e 100644 --- a/include/data_handling/DataSaverSPI.h +++ b/include/data_handling/DataSaverSPI.h @@ -61,7 +61,8 @@ class DataSaverSPI : public IDataSaver { * * @param dataPoint The data point to save * @param name An 8-bit “identifier” for the data point (could be a sensor ID) - * @return int 0 on success; non-zero on error + * @return int 0 on success, 1 when writes are blocked by post-launch state, + * and -1 on write/buffer error. */ int saveDataPoint(const DataPoint& dataPoint, uint8_t name) override; @@ -71,7 +72,7 @@ class DataSaverSPI : public IDataSaver { * @note When to use: emitted internally when gaps exceed * timestampInterval_ms_, or explicitly in tests. * @return int 0 on success, 1 when writes are blocked by post-launch state, - * and -1 on write/buffer error. + * and -1 on non-post-launch write/buffer error. */ int saveTimestamp(uint32_t timestamp_ms); diff --git a/src/data_handling/DataSaverSPI.cpp b/src/data_handling/DataSaverSPI.cpp index 8f75d83..e0f0a41 100644 --- a/src/data_handling/DataSaverSPI.cpp +++ b/src/data_handling/DataSaverSPI.cpp @@ -20,19 +20,23 @@ DataSaverSPI::DataSaverSPI(uint16_t timestampInterval_ms, int DataSaverSPI::saveDataPoint(const DataPoint& dataPoint, uint8_t name) { if (rebootedInPostLaunchMode_ || isChipFullDueToPostLaunchProtection_) { - return 1; // Do not save if we rebooted in post-launch mode + return 1; // Do not save if writes are blocked by post-launch state. } // Write a timestamp automatically if enough time has passed since the last one uint32_t const timestamp = dataPoint.timestamp_ms; if (timestamp - lastTimestamp_ms_ > timestampInterval_ms_) { - if (saveTimestamp(timestamp) < 0) { - return -1; + int const timestampResult = saveTimestamp(timestamp); + if (timestampResult != 0) { + return timestampResult; } } Record_t record = {name, dataPoint.data}; - if (addRecordToBuffer(&record) < 0) { + if (addRecordToBuffer(&record) != 0) { + if (isChipFullDueToPostLaunchProtection_) { + return 1; + } return -1; } @@ -42,11 +46,14 @@ int DataSaverSPI::saveDataPoint(const DataPoint& dataPoint, uint8_t name) { int DataSaverSPI::saveTimestamp(uint32_t timestamp_ms){ if (rebootedInPostLaunchMode_ || isChipFullDueToPostLaunchProtection_) { - return 1; // Do not save if we rebooted in post-launch mode + return 1; // Do not save if writes are blocked by post-launch state. } TimestampRecord_t timeStampRecord = {TIMESTAMP, timestamp_ms}; if (addRecordToBuffer(&timeStampRecord) != 0) { + if (isChipFullDueToPostLaunchProtection_) { + return 1; + } return -1; } diff --git a/test/test_datasaver_spi/test_DataSaverSPI.cpp b/test/test_datasaver_spi/test_DataSaverSPI.cpp index 3a2d6e6..96e7836 100644 --- a/test/test_datasaver_spi/test_DataSaverSPI.cpp +++ b/test/test_datasaver_spi/test_DataSaverSPI.cpp @@ -144,7 +144,7 @@ void test_pre_erase_skips_protected_launch_sector(void) { result = dss->saveTimestamp(1000U + i); } - TEST_ASSERT_EQUAL(-1, result); + TEST_ASSERT_EQUAL(1, result); TEST_ASSERT_TRUE(dss->getIsChipFullDueToPostLaunchProtection()); TEST_ASSERT_EQUAL_HEX8(0x5A, flash->fakeMemory[launchWriteAddress]); } From 57fed05e93f1b6563738359719c07f12cfc765e9 Mon Sep 17 00:00:00 2001 From: Elan456 Date: Wed, 1 Apr 2026 01:34:23 -0400 Subject: [PATCH 08/12] docs: remove void return --- AGENTS.md | 1 + include/data_handling/DataSaverSPI.h | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 111df4e..68aff4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,6 +94,7 @@ python -m http.server --directory build/doxygen 8000 - Deploy target: `gh-pages` branch - Published folder: `build/doxygen` - If code changes affect public behavior/API, update doc comments and relevant Markdown docs in the same change. +- Don't use a return type of `void` in doc comments; just omit the `@return` line if there is no return value. ## Data-Driven Tests diff --git a/include/data_handling/DataSaverSPI.h b/include/data_handling/DataSaverSPI.h index 9e82d5e..c778a62 100644 --- a/include/data_handling/DataSaverSPI.h +++ b/include/data_handling/DataSaverSPI.h @@ -132,7 +132,6 @@ class DataSaverSPI : public IDataSaver { * @param ignoreEmptyPages Skip pages that appear unwritten. * @note When to use: post-flight data retrieval before erasing or * redeploying the flash chip. - * @return void */ void dumpData(Stream &serial, bool ignoreEmptyPages); From ff06ce9776d17ed13ee1d4d0e01482774c7b0184 Mon Sep 17 00:00:00 2001 From: Elan456 Date: Wed, 1 Apr 2026 01:37:02 -0400 Subject: [PATCH 09/12] test: setPostLaunchStateForTest now calls clearInternalState to improve consistency --- include/data_handling/DataSaverSPI.h | 3 +++ test/test_datasaver_spi/test_DataSaverSPI.cpp | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/include/data_handling/DataSaverSPI.h b/include/data_handling/DataSaverSPI.h index c778a62..94631ca 100644 --- a/include/data_handling/DataSaverSPI.h +++ b/include/data_handling/DataSaverSPI.h @@ -287,6 +287,8 @@ class DataSaverSPI : public IDataSaver { #ifdef UNIT_TEST /** * @brief Test-only helper to override internal post-launch state. + * Resets in-memory state first so the injected state is + * self-consistent and does not depend on prior test writes. * @param nextWriteAddress_in Next write address to inject. * @param launchWriteAddress_in Launch-protected address to inject. * @param postLaunchMode_in Post-launch mode flag to inject. @@ -294,6 +296,7 @@ class DataSaverSPI : public IDataSaver { void setPostLaunchStateForTest(uint32_t nextWriteAddress_in, uint32_t launchWriteAddress_in, bool postLaunchMode_in) { + clearInternalState(); nextWriteAddress_ = nextWriteAddress_in; launchWriteAddress_ = launchWriteAddress_in; postLaunchMode_ = postLaunchMode_in; diff --git a/test/test_datasaver_spi/test_DataSaverSPI.cpp b/test/test_datasaver_spi/test_DataSaverSPI.cpp index 96e7836..83abaaf 100644 --- a/test/test_datasaver_spi/test_DataSaverSPI.cpp +++ b/test/test_datasaver_spi/test_DataSaverSPI.cpp @@ -151,14 +151,12 @@ void test_pre_erase_skips_protected_launch_sector(void) { void test_flush_wraps_using_full_page_write_size(void) { dss->clearInternalState(); - TEST_ASSERT_EQUAL(0, dss->saveTimestamp(1U)); - uint32_t const nearEndAddress = FAKE_MEMORY_SIZE_BYTES - DataSaverSPI::kBufferSize_bytes + 1U; dss->setPostLaunchStateForTest(nearEndAddress, 0U, false); int result = 0; uint32_t const recordsPerFlush = DataSaverSPI::kBufferSize_bytes / sizeof(TimestampRecord_t); - for (uint32_t i = 0; i < recordsPerFlush; i++) { + for (uint32_t i = 0; i < recordsPerFlush + 1U; i++) { result = dss->saveTimestamp(2U + i); } From fa1a563075bcc7d4c322fc875155b63b75b3c6a8 Mon Sep 17 00:00:00 2001 From: Elan456 Date: Wed, 1 Apr 2026 01:51:10 -0400 Subject: [PATCH 10/12] ref: remove redundant chipfull set --- src/data_handling/DataSaverSPI.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/data_handling/DataSaverSPI.cpp b/src/data_handling/DataSaverSPI.cpp index e0f0a41..4e1500c 100644 --- a/src/data_handling/DataSaverSPI.cpp +++ b/src/data_handling/DataSaverSPI.cpp @@ -127,7 +127,6 @@ int DataSaverSPI::flushBuffer() { } if (shouldStopForPostLaunchWindow()) { - isChipFullDueToPostLaunchProtection_ = true; return -1; } From cbc1840e2c37d4ebecba62199769c596b2dcf212 Mon Sep 17 00:00:00 2001 From: Elan456 Date: Wed, 1 Apr 2026 02:03:04 -0400 Subject: [PATCH 11/12] ref: add eraseSectorForWriteAndLatchOnProtection to be more explicit with 3 return values --- include/data_handling/DataSaverSPI.h | 18 ++++++++++--- src/data_handling/DataSaverSPI.cpp | 27 +++++++++---------- test/test_datasaver_spi/test_DataSaverSPI.cpp | 15 +++++------ 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/include/data_handling/DataSaverSPI.h b/include/data_handling/DataSaverSPI.h index 94631ca..c80025e 100644 --- a/include/data_handling/DataSaverSPI.h +++ b/include/data_handling/DataSaverSPI.h @@ -321,11 +321,23 @@ class DataSaverSPI : public IDataSaver { bool isProtectedLaunchSector(uint32_t sectorNumber) const; /** - * @brief Erases a sector unless blocked by post-launch protection. + * @brief Outcome of attempting to erase a sector before writing into it. + */ + enum class SectorEraseResult { + kErased, + kProtectedSectorLatched, + kFlashEraseFailed + }; + + /** + * @brief Erases a sector for writing and latches post-launch protection if blocked. * @param sectorNumber Sector index to erase. - * @return int 0 on successful erase, -1 when blocked or flash erase fails. + * @return SectorEraseResult::kErased on successful erase. + * @return SectorEraseResult::kProtectedSectorLatched when sector is + * post-launch protected (and chip-full is latched). + * @return SectorEraseResult::kFlashEraseFailed when flash erase fails. */ - int eraseSectorIfAllowed(uint32_t sectorNumber); + SectorEraseResult eraseSectorForWriteAndLatchOnProtection(uint32_t sectorNumber); /** * @brief Checks if the upcoming write window would violate post-launch protection. diff --git a/src/data_handling/DataSaverSPI.cpp b/src/data_handling/DataSaverSPI.cpp index 4e1500c..a1bf307 100644 --- a/src/data_handling/DataSaverSPI.cpp +++ b/src/data_handling/DataSaverSPI.cpp @@ -89,16 +89,17 @@ bool DataSaverSPI::isProtectedLaunchSector(uint32_t sectorNumber) const { return sectorNumber == launchWriteAddress_ / SFLASH_SECTOR_SIZE; } -int DataSaverSPI::eraseSectorIfAllowed(uint32_t sectorNumber) { +DataSaverSPI::SectorEraseResult DataSaverSPI::eraseSectorForWriteAndLatchOnProtection( + uint32_t sectorNumber) { if (isProtectedLaunchSector(sectorNumber)) { - isChipFullDueToPostLaunchProtection_ = true; // We can't erase the next sector to allow for more writes. - return -1; + isChipFullDueToPostLaunchProtection_ = true; + return SectorEraseResult::kProtectedSectorLatched; } if (!flash_->eraseSector(sectorNumber)) { - return -1; + return SectorEraseResult::kFlashEraseFailed; } preparedSectorNumber_ = sectorNumber; - return 0; + return SectorEraseResult::kErased; } bool DataSaverSPI::shouldStopForPostLaunchWindow() { @@ -135,7 +136,9 @@ int DataSaverSPI::flushBuffer() { if (nextWriteAddress_ % SFLASH_SECTOR_SIZE == 0U) { uint32_t const currentSectorNumber = nextWriteAddress_ / SFLASH_SECTOR_SIZE; if (preparedSectorNumber_ != currentSectorNumber) { - if (eraseSectorIfAllowed(currentSectorNumber) < 0) { + SectorEraseResult const eraseResult = + eraseSectorForWriteAndLatchOnProtection(currentSectorNumber); + if (eraseResult != SectorEraseResult::kErased) { return -1; } } @@ -153,14 +156,10 @@ int DataSaverSPI::flushBuffer() { // with buffer fill time. if (nextWriteAddress_ % SFLASH_SECTOR_SIZE == 0U) { uint32_t const nextSectorNumber = nextWriteAddress_ / SFLASH_SECTOR_SIZE; - - // If the next sector, is protected, skip the erase and still return 0 - if (!isProtectedLaunchSector(nextSectorNumber)) { - - // Try to erase the next sector and return -1 on failure - if (eraseSectorIfAllowed(nextSectorNumber) < 0) { - return -1; - } + SectorEraseResult const preEraseResult = + eraseSectorForWriteAndLatchOnProtection(nextSectorNumber); + if (preEraseResult == SectorEraseResult::kFlashEraseFailed) { + return -1; } } diff --git a/test/test_datasaver_spi/test_DataSaverSPI.cpp b/test/test_datasaver_spi/test_DataSaverSPI.cpp index 83abaaf..719e9d9 100644 --- a/test/test_datasaver_spi/test_DataSaverSPI.cpp +++ b/test/test_datasaver_spi/test_DataSaverSPI.cpp @@ -113,7 +113,7 @@ void test_next_sector_is_erased_before_crossing_boundary(void) { TEST_ASSERT_EQUAL_HEX8(0xFF, flash->fakeMemory[expectedBoundaryAddress]); } -void test_pre_erase_skips_protected_launch_sector(void) { +void test_pre_erase_latches_on_protected_launch_sector(void) { for (size_t i = 0; i < FAKE_MEMORY_SIZE_BYTES; i++) { flash->fakeMemory[i] = 0x00; } @@ -134,16 +134,13 @@ void test_pre_erase_skips_protected_launch_sector(void) { result = dss->saveTimestamp(i); } - // This flush writes successfully; only pre-erase is skipped for the - // protected next sector. + // The flush still writes successfully, but pre-erase hitting the protected + // next sector now latches chip-full protection. TEST_ASSERT_EQUAL(0, result); - TEST_ASSERT_FALSE(dss->getIsChipFullDueToPostLaunchProtection()); + TEST_ASSERT_TRUE(dss->getIsChipFullDueToPostLaunchProtection()); TEST_ASSERT_EQUAL_HEX8(0x5A, flash->fakeMemory[launchWriteAddress]); - for (uint32_t i = 0; i < recordsPerFlush; i++) { - result = dss->saveTimestamp(1000U + i); - } - + result = dss->saveTimestamp(1000U); TEST_ASSERT_EQUAL(1, result); TEST_ASSERT_TRUE(dss->getIsChipFullDueToPostLaunchProtection()); TEST_ASSERT_EQUAL_HEX8(0x5A, flash->fakeMemory[launchWriteAddress]); @@ -186,7 +183,7 @@ int main(void) { RUN_TEST(test_erase_all_data); RUN_TEST(test_launch_detected); RUN_TEST(test_next_sector_is_erased_before_crossing_boundary); - RUN_TEST(test_pre_erase_skips_protected_launch_sector); + RUN_TEST(test_pre_erase_latches_on_protected_launch_sector); RUN_TEST(test_flush_wraps_using_full_page_write_size); return UNITY_END(); } From 8586f56b7d9389a9b6a95faa03e6fd8c6a2063ef Mon Sep 17 00:00:00 2001 From: Elan456 Date: Thu, 2 Apr 2026 15:18:17 -0400 Subject: [PATCH 12/12] fix: use >= for saveInterval_ms_ to get 100hz when restricted to 10ms intervals --- src/data_handling/SensorDataHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data_handling/SensorDataHandler.cpp b/src/data_handling/SensorDataHandler.cpp index 001311e..c531878 100644 --- a/src/data_handling/SensorDataHandler.cpp +++ b/src/data_handling/SensorDataHandler.cpp @@ -18,7 +18,7 @@ void SensorDataHandler::restrictSaveSpeed(uint16_t interval_ms){ int SensorDataHandler::addData(DataPoint data){ // Check if the data is old enough to be saved based on the interval - if (data.timestamp_ms - lastSaveTime_ms_ > saveInterval_ms_) { + if (data.timestamp_ms - lastSaveTime_ms_ >= saveInterval_ms_) { dataSaver->saveDataPoint(data, name_); lastSaveTime_ms_ = data.timestamp_ms; lastDataPointSaved_ = data;