Skip to content

Commit dd5760d

Browse files
committed
fix: preserve capture creation failures
1 parent 44b8000 commit dd5760d

7 files changed

Lines changed: 232 additions & 35 deletions

File tree

‎ROADMAP.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ Phase 0 software work is implemented and builds cleanly:
6060

6161
Phase 1 software progress is also underway:
6262

63-
- [x] Scope and I2C capture cleanup no longer retries publication after a rejected write or failed I2C flush/sync. I2C releases recording ownership on known SD failure; both writers preserve the first cause through close and report once. All 35 native writer/storage cases pass; the previous callbacks fail 16 controls. Exact CSV/TMP bytes, Scope filename collision, pre-flush timing and 25 independent failure/success cycles per writer are checked with closed handles. UI, data, time, mount and fault boundaries are controlled; I2C start, ADC/drivers and physical SD/panel remain separate gates. Linux CI retains source hashes and logs.
63+
- [x] Scope and I2C capture cleanup no longer retries publication after a rejected write or failed I2C flush/sync. I2C releases recording ownership on known SD failure; both writers preserve the first cause through close and report once. All 35 original native writer/storage cases pass; the callbacks preceding the writer fix fail 16 controls. Exact CSV/TMP bytes, Scope filename collision, pre-flush timing and 25 independent failure/success cycles per writer are checked with closed handles. UI, data, time, mount and fault boundaries are controlled; ADC/drivers and physical SD/panel remain separate gates. Linux CI retains source hashes and logs.
6464
- [x] Ebook library listings confirm regular-file metadata before accepting `.txt` entries and report directory setup/open/read/stat/close errors, entry capacity and skipped long paths. Failed listings no longer claim an empty library; the first failure survives close. All 28 native existing-directory/file cases pass, including unchanged bytes, callback path ownership, first-error checks and 25 reopen cycles with closed handles. The previous callback fails 18 controls. UI, timer, mount, download and error boundaries are controlled; welcome creation and physical SD/panel checks remain separate. Linux CI retains source hashes and logs.
6565
- [x] UART/RS-485 logging retries only filename collisions, reports the first preparation/write/sync failure, and retains rejected headers as unpublished TMPs. All 82 native start/write/Stop and storage cases pass; the previous functions fail 42 controls. Exact bytes, ten-name limits, tick wrap and 25 failure/restart pairs per link are checked with unchanged retained TMPs and closed handles. UI, clock, mount and faults are controlled; drivers and physical SD/panel remain separate checks. Linux CI retains source hashes and logs.
66+
- [x] Scope and I2C capture constructors stop on preparation/stream-creation failures and preserve fresh first errors through cleanup. The capture suite now passes 78 native cases: 35 writer cases plus 43 constructor cases; the previous constructors fail 14 controls. Both 100-name limits, existing bytes, empty/partial TMP recovery and 25 header-failure/restart pairs per constructor are checked with closed handles and guarded I2C watch ownership. Paths, UI, clocks, data and faults are controlled; physical SD/panel and drivers remain separate checks.
6667
- [x] System reports the firmware/commit, reset reason, uptime, panel, heap, storage capacity, current OTA state, and persistent last OTA result.
6768
- [x] Notes and ride summaries use recoverable backup replacement; rides publish only after durable sync, and health logs repair incomplete final rows.
6869
- [x] Notes loads complete files up to 65,535 bytes, replacing a silent 2,047-byte prefix. Recovery/read/close errors, oversized files and NUL data disable Save; byte-length and SD checks run before storage changes. Its launcher leave hook clears editor ownership. Four preceding-source controls reproduce full-note truncation; 36 native file/fault cases and five real LVGL cases pass, including maximum ASCII/UTF-8 notes and 25 reopens without retained resources in the unchanged 96 KiB pool. Linux and Windows CI retain separate source-bound evidence. Physical SD/panel and power cuts remain open.

‎docs/architecture.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ The ebook library verifies regular-file metadata before offering a `.txt` entry,
103103

104104
Scope captures the first rejected header/row write before cleanup and closes an invalid temporary stream without attempting publication. I2C uses a shared finish path for explicit Stop and error cleanup: known row/flush/sync/SD failures close and release recording ownership, preserve the first cause, and report once; only a healthy Stop attempts publication. This prevents a successful retry during cleanup from turning an earlier failure into a Saved result. Native bytes and closed handles are checked with controlled UI/data/time/fault boundaries; sampling, drivers and physical SD behavior remain separate gates.
105105

106+
Scope and I2C constructors clear errno before each directory/metadata/open/stream boundary and stop on a non-collision failure. Scope closes the descriptor, attempts to remove its new empty TMP and reports a failed stream conversion without another filename attempt. I2C retains that empty file, and clears capture paths/ownership after a rejected header. Its watch flag is enabled only after an accepted header. Native constructor checks verify both 100-name bounds, retained-TMP restart, first errors and closed handles; existing path buffers, sample data, CSV formats and watch timing remain unchanged.
107+
106108
See [Storage and compatibility contract](compatibility.md) and [Data formats](data-formats.md).
107109

108110
## Network and radio

‎docs/data-formats.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ The ebook library accepts case-insensitive `.txt` names only after confirming re
4141

4242
Scope and I2C capture failures never retry publication during cleanup. Rejected header/row writes and failed I2C periodic flush/sync stop recording into the current TMP, close it, preserve the first cause and report failure. Known SD unavailability also closes an active I2C capture before adding another row. Healthy explicit saves still use the existing flush/sync/close/rename helper and CSV formats. A failed TMP can contain partial or complete-looking bytes; check the named file before recovery. Native byte/fault checks do not establish physical durability or power-loss recovery.
4343

44+
Scope and I2C creation advance through the 100 same-second names only on an existing CSV or TMP collision. Directory/stat/open/stream-creation failures stop the attempt, preserve the first cause through cleanup and use `EIO` when errno is absent. Scope attempts to remove its newly created empty TMP after a failed `fdopen`; I2C retains and names that empty file. A rejected I2C header retains its partial TMP and leaves capture ownership clear without enabling watch. Restart skips retained files and preserves their bytes; exhausting all names reports `EEXIST`.
45+
4446
UART and RS-485 log creation retry only existing final or temporary filenames. Other directory/stat/open/stream-creation errors stop the attempt and report their first cause; exhausting the ten same-second names reports `EEXIST`. Rejected header/row writes and failed periodic flush/sync close the log and retain the named TMP without a publication retry. A new log skips that retained name. Missing errno falls back to `EIO`, and tick/close calls cannot replace an already captured write failure. Native file checks cover both links and unchanged existing bytes; physical SD and power-loss behavior remain separate checks.
4547

4648
## CSV conventions

‎docs/testing.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,13 @@ The library retains 64 stored paths of 256 bytes. The entry-limit message means
174174
python tools/test_capture_write.py --compiler 'C:/path/to/clang.exe'
175175
```
176176

177-
The portable runner compiles the actual Scope save callback and I2C log/finish/Stop callbacks together with complete `storage_io.c`. Its 35 cases verify exact native CSV/TMP bytes, the 300-row Scope format, I2C success/transaction-error rows, header/row failures with and without a stream error flag, missing errno, tick-clobbered errno, first-error preservation through close, one-shot flush/sync failures, failed publication, known SD unavailability during logging or Stop, pre-flush timing, a preexisting Scope filename and 25 independent failure/success cycles per writer. The previous callbacks fail 16 controls, including publishing rejected data during cleanup and replacing the first error. All current cases finish with closed native file/descriptor handles and one SD report per I/O failure.
177+
The portable runner compiles the actual Scope save callback and I2C Start/log/finish/Stop callbacks together with complete `storage_io.c`. All 78 native-file cases pass. The original 35 writer cases verify exact CSV/TMP bytes, the 300-row Scope format, I2C success/transaction-error rows, header/row failures with and without a stream error flag, missing errno, tick-clobbered errno, first-error preservation through close, one-shot flush/sync failures, failed publication, known SD unavailability during logging or Stop, pre-flush timing, a preexisting Scope filename and 25 independent failure/success cycles per writer. The callbacks preceding the writer fix fail 16 of those original controls, including publishing rejected data during cleanup and replacing the first error.
178178

179-
Rejected writes and I2C flush/sync failures close the stream without retrying publication. A complete-looking TMP can remain after a failed flush/sync/close; these checks do not establish physical durability or rollback. UI, clocks/ticks, channel/sample/calibration data, directory preparation, logical mount paths, SD error reporting and I/O faults are controlled. UTC supplies deterministic filenames. Windows descriptor creation adds binary mode so CSV bytes match the SDK's LF output. The I2C start constructor, drivers, ADC acquisition, real LVGL, SDK mount/error filtering, physical SD/panel and power cuts are not executed. The cycle fixture removes its owned prior files between independent sessions; it does not prove retained-TMP collision recovery in the I2C constructor.
179+
The added 43 constructor cases use the actual I2C Start and Scope save paths with native directories, exclusive creation, stream conversion and descriptor cleanup. They check directory/stat/open/fdopen errors, fresh errno and first-error preservation, missing errno, both 100-name limits, existing CSV/TMP bytes, I2C header failures, unavailable SD admission, watch ownership and same-second restart after retained partial headers or an empty I2C TMP. Both constructors complete 25 header-failure/restart pairs while preserving the failed TMP and checking the next CSV. Scope removes its newly created empty TMP after a failed `fdopen`; I2C retains and names it. The preceding constructors fail 14 controls, including Scope retrying a stream-creation error and I2C enabling watch after a rejected directory operation with stale errno. All cases end with closed native stream/descriptor ownership and one SD report per I/O failure.
180180

181-
Full main/function/storage/header/test hashes, generated callback/config/storage source and compile/result logs stay under `build/capture-write/`. Linux CI retains `capture-write-logs`, including available failure diagnostics. These 35 cases are separate from the Windows native total.
181+
Rejected writes and I2C flush/sync failures close the stream without retrying publication. A complete-looking TMP can remain after a failed flush/sync/close; these checks do not establish physical durability or rollback. UI, clocks/ticks, channel/sample/calibration data, logical mount paths, SD error reporting and I/O faults are controlled. UTC supplies deterministic filenames; Windows descriptor creation adds binary mode to match SDK LF bytes. Drivers, ADC acquisition, real LVGL, SDK mount/error filtering, physical SD/panel and power cuts are not executed. Recovery checks preserve both generations within each pair; the fixture removes its owned files between pairs and its owned directories after all cases.
182+
183+
Full main/function/storage/header/test hashes, generated callback/config/storage source and compile/result logs stay under `build/capture-write/`. Linux CI retains `capture-write-logs`, including available failure diagnostics. These 78 cases are separate from the Windows native total.
182184

183185
## Serial log writers
184186

‎main/main.c‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2211,6 +2211,7 @@ static void scope_capture_clicked(lv_event_t *event)
22112211
lv_label_set_text_fmt(scope_capture_status, "SD unavailable: %s", strerror(error ? error : ENODEV));
22122212
return;
22132213
}
2214+
errno = 0;
22142215
if (mkdir(SCOPE_PATH, 0775) != 0 && errno != EEXIST) {
22152216
int error = errno ? errno : EIO;
22162217
sd_record_error(error);
@@ -2225,6 +2226,7 @@ static void scope_capture_clicked(lv_event_t *event)
22252226
strftime(date, sizeof(date), "%y%m%d", &local);
22262227
strftime(clock, sizeof(clock), "%H%M%S", &local);
22272228
snprintf(directory, sizeof(directory), SCOPE_PATH "/%s", date);
2229+
errno = 0;
22282230
if (mkdir(directory, 0775) != 0 && errno != EEXIST) {
22292231
int error = errno ? errno : EIO;
22302232
sd_record_error(error);
@@ -2240,22 +2242,26 @@ static void scope_capture_clicked(lv_event_t *event)
22402242
snprintf(temporary_path, sizeof(temporary_path), "%s/%s.TMP", directory, stem);
22412243
snprintf(final_path, sizeof(final_path), "%s/%s.CSV", directory, stem);
22422244
struct stat info;
2245+
errno = 0;
22432246
if (stat(final_path, &info) == 0) continue;
22442247
if (errno != ENOENT) {
22452248
create_error = errno ? errno : EIO;
22462249
break;
22472250
}
2251+
errno = 0;
22482252
int descriptor = open(temporary_path, O_WRONLY | O_CREAT | O_EXCL, 0664);
22492253
if (descriptor < 0) {
22502254
if (errno == EEXIST) continue;
22512255
create_error = errno ? errno : EIO;
22522256
break;
22532257
}
2258+
errno = 0;
22542259
file = fdopen(descriptor, "wb");
22552260
if (!file) {
22562261
create_error = errno ? errno : EIO;
22572262
close(descriptor);
22582263
remove(temporary_path);
2264+
break;
22592265
}
22602266
}
22612267
if (!file) {
@@ -5935,6 +5941,7 @@ static bool i2c_capture_start(void)
59355941
return false;
59365942
}
59375943

5944+
errno = 0;
59385945
if (mkdir(SD_PATH "/I2C", 0775) != 0 && errno != EEXIST) {
59395946
int error = errno ? errno : EIO;
59405947
sd_record_error(error);
@@ -5951,6 +5958,7 @@ static bool i2c_capture_start(void)
59515958
strftime(date, sizeof(date), "%y%m%d", &local);
59525959
strftime(clock, sizeof(clock), "%H%M%S", &local);
59535960
snprintf(directory, sizeof(directory), SD_PATH "/I2C/%s", date);
5961+
errno = 0;
59545962
if (mkdir(directory, 0775) != 0 && errno != EEXIST) {
59555963
int error = errno ? errno : EIO;
59565964
sd_record_error(error);
@@ -5970,18 +5978,21 @@ static bool i2c_capture_start(void)
59705978
snprintf(i2c_capture_final_path, sizeof(i2c_capture_final_path),
59715979
"%s/%s.CSV", directory, stem);
59725980
struct stat info;
5981+
errno = 0;
59735982
if (stat(i2c_capture_final_path, &info) == 0) continue;
59745983
if (errno != ENOENT) {
59755984
create_error = errno ? errno : EIO;
59765985
break;
59775986
}
5987+
errno = 0;
59785988
int descriptor = open(i2c_capture_temporary_path, O_WRONLY | O_CREAT | O_EXCL, 0664);
59795989
if (descriptor < 0) {
59805990
if (errno == EEXIST) continue;
59815991
create_error = errno ? errno : EIO;
59825992
break;
59835993
}
59845994
temporary_created = true;
5995+
errno = 0;
59855996
i2c_capture_file = fdopen(descriptor, "wb");
59865997
if (!i2c_capture_file) {
59875998
create_error = errno ? errno : EIO;
@@ -6005,6 +6016,7 @@ static bool i2c_capture_start(void)
60056016
return false;
60066017
}
60076018

6019+
errno = 0;
60086020
if (fputs("unix_time,address,register,value,status,speed_khz\n", i2c_capture_file) < 0) {
60096021
int error = errno ? errno : EIO;
60106022
fclose(i2c_capture_file);

0 commit comments

Comments
 (0)