Skip to content

Commit 62e588a

Browse files
committed
fix: keep failed captures unpublished
1 parent 4f9942d commit 62e588a

8 files changed

Lines changed: 464 additions & 32 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ jobs:
4141
run: python3 tools/test_ebook_pages.py --compiler cc
4242
- name: Test ebook library
4343
run: python3 tools/test_ebook_library.py --compiler cc
44+
- name: Test capture writers
45+
run: python3 tools/test_capture_write.py --compiler cc
4446
- name: Test Files text previews
4547
run: python3 tools/test_file_preview.py --compiler cc
4648
- name: Test Files directories
@@ -200,6 +202,21 @@ jobs:
200202
if-no-files-found: warn
201203
retention-days: 14
202204

205+
- name: Retain capture writer evidence
206+
uses: actions/upload-artifact@v4
207+
if: always()
208+
with:
209+
name: capture-write-logs
210+
path: |
211+
build/capture-write/compile.log
212+
build/capture-write/result.log
213+
build/capture-write/source.json
214+
build/capture-write/capture_write.inc
215+
build/capture-write/capture_write_config.inc
216+
build/capture-write/storage_source.inc
217+
if-no-files-found: warn
218+
retention-days: 14
219+
203220
- name: Retain Files text preview evidence
204221
uses: actions/upload-artifact@v4
205222
if: always()

‎ROADMAP.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ 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.
6364
- [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.
6465
- [x] System reports the firmware/commit, reset reason, uptime, panel, heap, storage capacity, current OTA state, and persistent last OTA result.
6566
- [x] Notes and ride summaries use recoverable backup replacement; rides publish only after durable sync, and health logs repair incomplete final rows.

‎docs/architecture.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ Files directory browsing resets errno before each directory read so per-entry `s
9999

100100
The ebook library verifies regular-file metadata before offering a `.txt` entry, excluding matching directories and unconfirmed paths. Directory setup/open failures show their cause; read/stat/close failures keep confirmed rows visible with an incomplete-list warning. Fresh errno checks preserve the first failure, and failed or skipped listings cannot claim the library is empty. The existing 64-entry and 256-byte path bounds are disclosed. Native checks use an existing BOOKS directory with controlled UI, timer, SD, download and fault boundaries; welcome creation and physical storage remain separate checks.
101101

102+
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.
103+
102104
See [Storage and compatibility contract](compatibility.md) and [Data formats](data-formats.md).
103105

104106
## Network and radio

‎docs/data-formats.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Choose a record from the current page and tap **COPY ROW** to put its original 1
3939

4040
The ebook library accepts case-insensitive `.txt` names only after confirming regular-file metadata, and skips directories or entries with failed metadata. It stores at most 64 paths of 256 bytes, disclosing filled slots and overlong names. Setup/open errors show their cause; read/stat/close errors mark retained confirmed entries incomplete and reach the SD callback with the first failure. The copy-books hint appears only after an error-free listing with no accepted or overlong entries. Native existing-directory checks cover these transitions with controlled UI, timer, mount and error boundaries; welcome creation and physical SD/panel behavior remain separate checks.
4141

42+
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.
43+
4244
## CSV conventions
4345

4446
Before appending, HTTP, MQTT and Ring heart-rate logs remove an unfinished final row by retaining bytes through the last LF. Recovery scans backwards in 512-byte blocks, preserving preceding complete rows. Seek/read failures leave the file unchanged; truncation is synced before closing, and sync/close errors remain errors even if the tail has already been removed. Writers must be stopped during recovery. Native byte checks cover long tails and block boundaries; real SD timing and power-loss behavior remain hardware checks.

‎docs/testing.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Python 3 and Node.js are needed for the quick checks. The configuration checks a
1616
| Ring heart-rate logging, saved timestamps or queue processing | [Ring history storage](#ring-history-storage) and a firmware build. |
1717
| Ebook page loading or navigation | [Ebook pages](#ebook-pages) and a firmware build. |
1818
| Ebook library enumeration | [Ebook library](#ebook-library) and a firmware build. |
19+
| Scope/I2C CSV writes or publication | [Capture writers](#capture-writers) and a firmware build. |
1920
| Scope/I2C capture or saved serial CSV viewers | [CSV viewer storage and UI](#csv-viewer-storage-and-ui), existing serial-copy checks and a firmware build. |
2021
| Files fallback text previews | [Files text previews](#files-text-previews) and a firmware build. |
2122
| Files directory browsing | [Files directories](#files-directories) and a firmware build. |
@@ -166,6 +167,18 @@ Windows Find APIs and POSIX directory APIs supply the entries; native dot entrie
166167

167168
The library retains 64 stored paths of 256 bytes. The entry-limit message means those slots filled; no extra read proves that more entries exist. Generated callback/config source, full main/function/test hashes and compile/result logs stay under `build/ebook-library/`. Linux CI retains `ebook-library-logs`, including available failure diagnostics. These 28 checks are separate from the Windows native total.
168169

170+
## Capture writers
171+
172+
```powershell
173+
python tools/test_capture_write.py --compiler 'C:/path/to/clang.exe'
174+
```
175+
176+
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+
178+
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+
180+
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+
169182
## CSV viewer storage and UI
170183

171184
```powershell

‎main/main.c‎

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2273,26 +2273,33 @@ static void scope_capture_clicked(lv_event_t *event)
22732273
gain_permille = scope_gains_permille[channel_index];
22742274
portEXIT_CRITICAL(&scope_lock);
22752275
uint32_t rate = scope_sample_rates[rate_index];
2276-
bool write_ok = fputs("unix_time,elapsed_us,gpio,millivolts,sample_rate_hz,offset_mv,scale_permille\n", file) >= 0;
2277-
for (size_t i = 0; write_ok && i < SCOPE_CHART_POINTS; i++) {
2276+
errno = 0;
2277+
int error = fputs("unix_time,elapsed_us,gpio,millivolts,sample_rate_hz,offset_mv,scale_permille\n", file) < 0
2278+
? (errno ? errno : EIO) : 0;
2279+
for (size_t i = 0; !error && i < SCOPE_CHART_POINTS; i++) {
22782280
uint32_t elapsed_us = (uint32_t)((uint64_t)i * 1000000U / rate);
2279-
write_ok = fprintf(file, "%lld,%lu,%d,%ld,%lu,%d,%u\n",
2280-
(long long)captured_at, (unsigned long)elapsed_us,
2281-
(int)scope_channels[channel_index].pin, (long)scope_chart_points[i],
2282-
(unsigned long)rate, (int)offset_mv, (unsigned)gain_permille) >= 0;
2281+
errno = 0;
2282+
if (fprintf(file, "%lld,%lu,%d,%ld,%lu,%d,%u\n",
2283+
(long long)captured_at, (unsigned long)elapsed_us,
2284+
(int)scope_channels[channel_index].pin, (long)scope_chart_points[i],
2285+
(unsigned long)rate, (int)offset_mv, (unsigned)gain_permille) < 0)
2286+
error = errno ? errno : EIO;
22832287
}
22842288

2285-
bool saved = write_ok && storage_commit_new_file(&file, temporary_path, final_path) == 0;
2286-
if (!saved && file) {
2287-
errno = errno ? errno : EIO;
2288-
storage_commit_new_file(&file, temporary_path, final_path);
2289+
bool saved = false;
2290+
if (!error) {
2291+
errno = 0;
2292+
saved = storage_commit_new_file(&file, temporary_path, final_path) == 0;
2293+
if (!saved) error = errno ? errno : EIO;
2294+
} else {
2295+
fclose(file);
2296+
file = NULL;
22892297
}
22902298
if (saved) {
22912299
const char *name = strrchr(final_path, '/');
22922300
snprintf(scope_capture_notice, sizeof(scope_capture_notice), "Saved %s",
22932301
name ? name + 1 : "Scope CSV");
22942302
} else {
2295-
int error = errno ? errno : EIO;
22962303
sd_record_error(error);
22972304
struct stat info;
22982305
bool retained = stat(temporary_path, &info) == 0;
@@ -5876,23 +5883,34 @@ static void i2c_result_action(void)
58765883
i2c_update_controls();
58775884
}
58785885

5879-
static bool i2c_capture_stop(void)
5886+
static bool i2c_capture_finish(int error)
58805887
{
5881-
if (!i2c_capture_file) return true;
5888+
if (!i2c_capture_file) return error == 0;
5889+
if (!error && !sd_ready) {
5890+
error = sd_error_snapshot();
5891+
if (!error) error = ENODEV;
5892+
}
58825893

58835894
const char *temporary_name = strrchr(i2c_capture_temporary_path, '/');
58845895
const char *final_name = strrchr(i2c_capture_final_path, '/');
5885-
bool saved = storage_commit_new_file(&i2c_capture_file, i2c_capture_temporary_path,
5886-
i2c_capture_final_path) == 0;
5896+
bool saved = false;
5897+
if (!error) {
5898+
errno = 0;
5899+
saved = storage_commit_new_file(&i2c_capture_file, i2c_capture_temporary_path,
5900+
i2c_capture_final_path) == 0;
5901+
if (!saved) error = errno ? errno : EIO;
5902+
} else {
5903+
fclose(i2c_capture_file);
5904+
i2c_capture_file = NULL;
5905+
}
58875906
if (saved) {
58885907
snprintf(i2c_capture_notice, sizeof(i2c_capture_notice), "Saved %s",
58895908
final_name ? final_name + 1 : "I2C CSV");
58905909
} else {
5891-
int save_error = errno ? errno : EIO;
5892-
sd_record_error(save_error);
5910+
sd_record_error(error);
58935911
snprintf(i2c_capture_notice, sizeof(i2c_capture_notice),
5894-
"Capture not published; %s retained (%s)",
5895-
temporary_name ? temporary_name + 1 : "TMP", strerror(save_error));
5912+
"Capture not published; check %s (%s)",
5913+
temporary_name ? temporary_name + 1 : "TMP", strerror(error));
58965914
}
58975915
i2c_capture_temporary_path[0] = '\0';
58985916
i2c_capture_final_path[0] = '\0';
@@ -5901,6 +5919,11 @@ static bool i2c_capture_stop(void)
59015919
return saved;
59025920
}
59035921

5922+
static bool i2c_capture_stop(void)
5923+
{
5924+
return i2c_capture_finish(0);
5925+
}
5926+
59045927
static bool i2c_capture_start(void)
59055928
{
59065929
i2c_capture_notice[0] = '\0';
@@ -6010,24 +6033,26 @@ static bool i2c_capture_start(void)
60106033
static void i2c_capture_log(esp_err_t transaction_error, uint8_t value)
60116034
{
60126035
if (!i2c_capture_file) return;
6036+
if (!sd_ready) {
6037+
int error = sd_error_snapshot();
6038+
i2c_capture_finish(error ? error : ENODEV);
6039+
return;
6040+
}
60136041

60146042
char value_text[5] = "";
60156043
if (transaction_error == ESP_OK) snprintf(value_text, sizeof(value_text), "0x%02X", value);
6016-
bool failed = fprintf(i2c_capture_file, "%lld,0x%02X,0x%02X,%s,%s,%lu\n",
6017-
(long long)time(NULL), i2c_selected_address, i2c_selected_register,
6018-
value_text, esp_err_to_name(transaction_error),
6019-
(unsigned long)(i2c_bus_speed_hz / 1000)) < 0;
6044+
errno = 0;
6045+
int error = fprintf(i2c_capture_file, "%lld,0x%02X,0x%02X,%s,%s,%lu\n",
6046+
(long long)time(NULL), i2c_selected_address, i2c_selected_register,
6047+
value_text, esp_err_to_name(transaction_error),
6048+
(unsigned long)(i2c_bus_speed_hz / 1000)) < 0 ? (errno ? errno : EIO) : 0;
60206049
TickType_t now = xTaskGetTickCount();
6021-
if (!failed && now - i2c_capture_last_flush_tick >= pdMS_TO_TICKS(I2C_CAPTURE_FLUSH_MS)) {
6022-
failed = storage_sync_file(i2c_capture_file) != 0;
6023-
if (!failed) i2c_capture_last_flush_tick = now;
6024-
}
6025-
if (failed) {
6026-
int error = errno ? errno : EIO;
6027-
sd_record_error(error);
6028-
errno = error;
6029-
i2c_capture_stop();
6050+
if (!error && now - i2c_capture_last_flush_tick >= pdMS_TO_TICKS(I2C_CAPTURE_FLUSH_MS)) {
6051+
errno = 0;
6052+
if (storage_sync_file(i2c_capture_file) != 0) error = errno ? errno : EIO;
6053+
else i2c_capture_last_flush_tick = now;
60306054
}
6055+
if (error) i2c_capture_finish(error);
60316056
}
60326057

60336058
static void i2c_address_step_clicked(lv_event_t *event)

0 commit comments

Comments
 (0)