Skip to content

Commit b79bf23

Browse files
committed
fix: preserve Notes after incomplete loads
1 parent 981ebc6 commit b79bf23

8 files changed

Lines changed: 652 additions & 11 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ jobs:
3131
"$RUNNER_TEMP/storage_io_test"
3232
- name: Test CSV tail recovery
3333
run: python3 tools/test_storage_csv_tail.py --compiler cc
34+
- name: Test Notes storage
35+
run: python3 tools/test_notes_io.py --compiler cc
3436
- name: Test capture reader
3537
run: |
3638
cc -std=c11 -Wall -Wextra -Werror -pedantic -I main main/capture_data.c tests/capture_data_test.c -o "$RUNNER_TEMP/capture_data_test"
@@ -114,6 +116,21 @@ jobs:
114116
if-no-files-found: warn
115117
retention-days: 14
116118

119+
- name: Retain Notes storage test evidence
120+
uses: actions/upload-artifact@v4
121+
if: always()
122+
with:
123+
name: notes-io-logs
124+
path: |
125+
build/notes-io/compile.log
126+
build/notes-io/result.log
127+
build/notes-io/source.json
128+
build/notes-io/notes_callbacks.inc
129+
build/notes-io/notes_config.inc
130+
build/notes-io/storage_source.inc
131+
if-no-files-found: warn
132+
retention-days: 14
133+
117134
firmware:
118135
runs-on: ubuntu-latest
119136
container: espressif/idf:v5.4.2
@@ -219,6 +236,23 @@ jobs:
219236
- name: Test offline apps, network consoles and OTA manifests/images
220237
shell: pwsh
221238
run: ./tools/test_native_ui_ci.ps1
239+
- name: Test Notes with real LVGL
240+
shell: pwsh
241+
run: python tools/test_notes_io.py --compiler clang --lvgl-library build/offline-ui/out/lvgl_host.lib --output build/notes-ui
242+
- name: Retain Notes UI test evidence
243+
uses: actions/upload-artifact@v4
244+
if: always()
245+
with:
246+
name: notes-ui-logs
247+
path: |
248+
build/notes-ui/compile.log
249+
build/notes-ui/result.log
250+
build/notes-ui/source.json
251+
build/notes-ui/notes_callbacks.inc
252+
build/notes-ui/notes_config.inc
253+
build/notes-ui/storage_source.inc
254+
if-no-files-found: warn
255+
retention-days: 14
222256
- uses: actions/upload-artifact@v4
223257
if: always()
224258
with:

‎ROADMAP.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Phase 1 software progress is also underway:
6262

6363
- [x] System reports the firmware/commit, reset reason, uptime, panel, heap, storage capacity, current OTA state, and persistent last OTA result.
6464
- [x] Notes and ride summaries use recoverable backup replacement; rides publish only after durable sync, and health logs repair incomplete final rows.
65+
- [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.
6566
- [x] Shared CSV tail recovery now scans backwards in 512-byte blocks. Eight previous-source controls exceed stdio seek budgets; a 64 KiB partial tail drops from 65,538 seeks to 130 with the same retained prefix. All 32 native byte/boundary/fault cases, original storage checks and five MQTT/HTTP logging cases pass with closed modeled handles. The unflashed firmware builds with unchanged configuration. Linux CI retains source hashes and compilation/result logs; physical SD timing and power cuts remain open.
6667
- [x] Built-in SD paths, timestamps, units, 8.3-safe collision names, `.TMP`, and `.BAK` behavior are documented.
6768
- [x] Persisted brightness choices, a dimmed two-minute screen, and configurable 5/10/30-minute or Never screen-off behavior are implemented.

‎docs/architecture.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ The serial log viewer loads at most 512 validated rows and closes the file befor
8383

8484
CSV tail repair uses a 512-byte scratch buffer and scans backwards by blocks before truncating at the most recent LF. HTTP, MQTT and Ring health logging share this helper with serialized writers. A seek/read failure prevents truncation; later truncate/sync/close failures preserve the first error. Native checks verify retained bytes and reduced stdio call counts, while physical FAT/SD latency and power-loss durability remain device checks.
8585

86+
Notes uses a temporary PSRAM buffer to load an entire file of up to 65,535 bytes and frees that buffer after LVGL copies the text. It applies the editor's character limit after loading, using LVGL's bulk text path. Recovery/read/close failures, oversized files and NUL data disable editing and Save; the save callback rechecks ownership, SD availability and byte length before changing paths. The launcher leave hook clears its editor pointer and permission before content deletion. The existing replacement helper still syncs/closes `NOTE.TMP`, retains the previous complete generation in `NOTE.BAK`, and reports publication failures. Real LVGL checks use the unchanged 96 KiB pool; physical SD and panel behavior remain open.
87+
8688
See [Storage and compatibility contract](compatibility.md) and [Data formats](data-formats.md).
8789

8890
## Network and radio

‎docs/data-formats.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Tab5 OS keeps user-owned documents and logs on the removable microSD card. Built
2121

2222
Files ending in `.TMP` are unpublished: they may be incomplete after power loss, or fully synced but retained because publication failed. They must not be reported as finished captures without validation. A `.BAK` file is the previous complete generation and can be used for recovery when its corresponding final file is absent.
2323

24+
Notes edits complete text files of at most 65,535 bytes. A missing note starts an empty draft; a missing final note can be restored from `NOTE.BAK`. Recovery, open, read or close failures disable editing and Save until a successful reopen. Oversized files and files containing NUL bytes also disable Save and keep their original bytes. Save checks the draft's byte length before changing any path, including for UTF-8 text whose character count is smaller than its byte count. Notes loads the complete text in bulk before applying the editor's character limit. Leaving Notes clears its editor pointer and Save permission. Native files and LVGL checks verify these boundaries; physical SD removal, full-card and power-loss behavior remain hardware gates.
25+
2426
Files opens published Scope and I2C `.CSV` captures as graphs when their headers match the formats below. The viewer shows up to the first 2,048 well-formed rows, leaves the file unchanged, and shows failed I2C reads as gaps. Other files retain the text preview.
2527

2628
Files also opens published UART and RS-485 `.CSV` logs as a timestamped RX/TX timeline. The viewer loads up to the first 512 well-formed rows, shows eight rows per page, and lets you filter by direction or switch between hex and escaped ASCII without changing the file. Unpublished `.TMP` files retain the text preview.

‎docs/testing.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Python 3 and Node.js are needed for the quick checks. The configuration checks a
1111
| SDK setup, board selection or root CMake | [Firmware configuration](#firmware-configuration). |
1212
| Relay, parsers, calculations, clipboard or file formats | [Core command reference](#core-command-reference); choose the affected module and any independent reference test. |
1313
| Storage writes or incomplete CSV rows | [Core command reference](#core-command-reference), [CSV tail recovery](#csv-tail-recovery) and a firmware build. |
14+
| Notes loading, saving or lifecycle | [Notes storage and UI](#notes-storage-and-ui) and a firmware build. |
1415
| Offline apps, SPI or I2C editor/result flows | [Offline and peripheral UI](#offline-and-peripheral-ui). |
1516
| Serial, saved serial logs or RTU Frames | [Serial and RTU UI](#serial-and-rtu-ui). |
1617
| Modbus TCP, NTP, Wake-on-LAN, UDP or HTTP fixtures | [Network fixtures and UI](#network-fixtures-and-ui). |
@@ -96,6 +97,20 @@ Seek/read failures preserve all bytes, including a short read without a supplied
9697

9798
Generated source, full storage/header/test hashes and compile/result logs stay under `build/storage-csv-tail/`. Linux CI runs the fixture and retains those files as `storage-csv-tail-logs`, including compiler/test failures. Its 32 cases are separate from the seven-suite Windows native total.
9899

100+
## Notes storage and UI
101+
102+
```powershell
103+
python tools/test_notes_io.py --compiler 'C:/path/to/clang.exe'
104+
# With the cached Debug LVGL library from the offline UI runner:
105+
python tools/test_notes_io.py --compiler 'C:/path/to/clang.exe' --lvgl-library build/offline-ui/out/lvgl_host.lib --output build/notes-ui
106+
```
107+
108+
The portable runner compiles four unchanged Notes functions extracted from `main/main.c` and the actual complete `storage_io.c`. Its 36 native-file cases cover missing/empty notes, backup recovery, the former 2 KiB boundary, 5 KiB and maximum-size files, oversized and NUL-containing files, allocation/recovery/open/read/close errors, the first read error, save length/UTF-8 guards, card availability, stream/publication/rollback failures and stale Save/reopen behavior. Every case independently checks final, backup and temporary bytes and closes its modeled file/allocation handles. Four prior-source full-file controls reproduce truncation to 2,047 bytes; recovery and read faults also reproduce unsafe Save actions. New allocation/probe/leave controls test the added boundaries rather than asserting that the previous callbacks used those APIs.
109+
110+
The optional real LVGL mode runs five additional cases against the cached pinned library with its existing 96 KiB pool. It checks complete 65,535-byte ASCII and 65,534-byte UTF-8 load/save, an oversized original file, a multibyte draft whose byte count exceeds the limit, and 25 reopen cycles with identical free bytes and allocation counts. Text is installed before the character limit so LVGL uses its bulk path. Files are native; SD faults and PSRAM allocation APIs are controlled. This does not prove physical rendering, card latency, removal or power-loss durability.
111+
112+
Generated callbacks/config/storage source, full main/function/storage/header/test hashes and compile/result logs stay under the selected output directory. Linux CI retains the 36-case evidence as `notes-io-logs`. Windows CI runs the five real LVGL cases after the existing seven suites and retains separate `notes-ui-logs`; those five add to the prior 815 native checks. Both artifacts retain diagnostics when their test step fails.
113+
99114
## OTA manifests
100115

101116
```powershell

‎main/main.c‎

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
#define BROWSER_MAX_TEXT 12288
131131
#define BROWSER_MAX_LINKS 12
132132
#define EBOOK_PAGE_BYTES 8192
133+
#define NOTE_MAX_BYTES 65535
133134
#define OTA_MANIFEST_URL "https://github.com/DevanMetz/Tab5OS/releases/latest/download/tab5_os.json"
134135
#define BATTERY_EMPTY_MV 6000
135136
#define BATTERY_FULL_MV 8230
@@ -359,6 +360,7 @@ static int battery_millivolts;
359360
static int battery_milliamps;
360361
static int battery_percent;
361362
static lv_obj_t *note_area;
363+
static bool note_can_save;
362364
static lv_obj_t *counter_label;
363365
static bool internal_ready;
364366
static esp_err_t storage_init_error = ESP_OK;
@@ -4563,9 +4565,44 @@ static void ebooks_clicked(lv_event_t *event)
45634565
show_ebooks();
45644566
}
45654567

4568+
static void notes_leave(void)
4569+
{
4570+
note_area = NULL;
4571+
note_can_save = false;
4572+
}
4573+
4574+
static int load_note(char *note)
4575+
{
4576+
note[0] = '\0';
4577+
if (storage_recover_replace(SD_PATH "/DOCS/NOTE.TXT", SD_PATH "/DOCS/NOTE.BAK") != 0)
4578+
return errno ? errno : EIO;
4579+
FILE *file = fopen(SD_PATH "/DOCS/NOTE.TXT", "rb");
4580+
if (!file) return errno == ENOENT ? 0 : errno ? errno : EIO;
4581+
4582+
errno = 0;
4583+
size_t read = fread(note, 1, NOTE_MAX_BYTES, file);
4584+
int error = ferror(file) ? (errno ? errno : EIO) : 0;
4585+
if (!error && fgetc(file) != EOF) error = EFBIG;
4586+
if (!error && ferror(file)) error = errno ? errno : EIO;
4587+
if (!error && memchr(note, '\0', read)) error = EILSEQ;
4588+
if (fclose(file) != 0 && !error) error = errno ? errno : EIO;
4589+
note[read] = '\0';
4590+
return error;
4591+
}
4592+
45664593
static void save_note(lv_event_t *event)
45674594
{
4595+
if (!note_can_save || !note_area) return;
45684596
lv_obj_t *status = lv_event_get_user_data(event);
4597+
if (!sd_ready) {
4598+
lv_label_set_text(status, "Save failed: SD card unavailable");
4599+
return;
4600+
}
4601+
const char *text = lv_textarea_get_text(note_area);
4602+
if (strlen(text) > NOTE_MAX_BYTES) {
4603+
lv_label_set_text_fmt(status, "Note too long (maximum %d bytes)", NOTE_MAX_BYTES);
4604+
return;
4605+
}
45694606
mkdir(SD_PATH "/DOCS", 0775);
45704607
const char *temporary_path = SD_PATH "/DOCS/NOTE.TMP";
45714608
const char *final_path = SD_PATH "/DOCS/NOTE.TXT";
@@ -4578,7 +4615,7 @@ static void save_note(lv_event_t *event)
45784615
lv_label_set_text_fmt(status, "Save failed: %s", strerror(error));
45794616
return;
45804617
}
4581-
if (fputs(lv_textarea_get_text(note_area), file) < 0) {
4618+
if (fputs(text, file) < 0) {
45824619
int write_error = errno ? errno : EIO;
45834620
fclose(file);
45844621
file = NULL;
@@ -4617,21 +4654,36 @@ static void notes_clicked(lv_event_t *event)
46174654
lv_obj_t *save_label = lv_label_create(save);
46184655
lv_label_set_text(save_label, "Save");
46194656
lv_obj_center(save_label);
4620-
if (!sd_ready) lv_obj_add_state(save, LV_STATE_DISABLED);
4621-
4622-
static char note[2048];
4623-
storage_recover_replace(SD_PATH "/DOCS/NOTE.TXT", SD_PATH "/DOCS/NOTE.BAK");
4624-
FILE *file = fopen(SD_PATH "/DOCS/NOTE.TXT", "rb");
4625-
size_t read = file ? fread(note, 1, sizeof(note) - 1, file) : 0;
4626-
if (file) fclose(file);
4627-
note[read] = '\0';
4657+
note_can_save = false;
4658+
char *note = sd_ready ? heap_caps_malloc(NOTE_MAX_BYTES + 1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) : NULL;
4659+
if (sd_ready && !note) {
4660+
lv_label_set_text(status, "Load failed: not enough memory; reopen Notes to retry");
4661+
} else if (note) {
4662+
int error = load_note(note);
4663+
if (!error) note_can_save = true;
4664+
else if (error == EFBIG)
4665+
lv_label_set_text_fmt(status, "Note exceeds %d bytes; Save disabled", NOTE_MAX_BYTES);
4666+
else if (error == EILSEQ)
4667+
lv_label_set_text(status, "Note contains NUL bytes; Save disabled");
4668+
else {
4669+
sd_record_error(error);
4670+
lv_label_set_text_fmt(status, "Load failed: %s; reopen Notes to retry", strerror(error));
4671+
}
4672+
}
46284673

46294674
note_area = lv_textarea_create(content);
46304675
lv_obj_set_size(note_area, 640, 420);
4631-
lv_textarea_set_text(note_area, note);
4676+
lv_textarea_set_text(note_area, note_can_save ? note : "");
4677+
lv_textarea_set_max_length(note_area, NOTE_MAX_BYTES);
4678+
heap_caps_free(note);
46324679
lv_obj_t *keyboard = lv_keyboard_create(content);
46334680
lv_obj_set_size(keyboard, 640, 500);
46344681
lv_keyboard_set_textarea(keyboard, note_area);
4682+
if (!note_can_save) {
4683+
lv_obj_add_state(save, LV_STATE_DISABLED);
4684+
lv_obj_add_state(note_area, LV_STATE_DISABLED);
4685+
lv_obj_add_state(keyboard, LV_STATE_DISABLED);
4686+
}
46354687
}
46364688

46374689
static void update_counter(void)
@@ -7597,7 +7649,7 @@ static void screensaver_tick(lv_timer_t *timer)
75977649

75987650
static const app_definition_t launcher_apps[] = {
75997651
{LV_SYMBOL_DIRECTORY, "Files", 0x2196F3, files_clicked, NULL, 0, 0},
7600-
{LV_SYMBOL_EDIT, "Notes", 0x00A896, notes_clicked, NULL, 1, 0},
7652+
{LV_SYMBOL_EDIT, "Notes", 0x00A896, notes_clicked, notes_leave, 1, 0},
76017653
{LV_SYMBOL_PLUS, "Counter", 0xF59E0B, counter_clicked, NULL, 2, 0},
76027654
{LV_SYMBOL_CHARGE, "GPIO", 0xE65100, gpio_clicked, NULL, 3, 0},
76037655
{LV_SYMBOL_WIFI, "Settings", 0x0288D1, settings_clicked, settings_leave, 0, 1},

0 commit comments

Comments
 (0)