Skip to content

feat - refactor nand flash as ring buffer#7216

Closed
TuEmb wants to merge 11 commits into
BasedHardware:mainfrom
TuEmb:TuEmb/refactor_nand_flash
Closed

feat - refactor nand flash as ring buffer#7216
TuEmb wants to merge 11 commits into
BasedHardware:mainfrom
TuEmb:TuEmb/refactor_nand_flash

Conversation

@TuEmb

@TuEmb TuEmb commented May 10, 2026

Copy link
Copy Markdown
Contributor

Core Idea

  • Offline audio is a sequence-based ring buffer, not a file list.
  • One logical record = 444 bytes:
    • 0..3: UTC timestamp, uint32 BE
    • 4..443: audio payload, 440 bytes
  • Reading does not acknowledge data.
  • Only CMD_RING_ADVANCE persists consumer progress.

GATT

  • Service: 30295780-4301-EABD-2904-2849ADFEAE43
  • Control char: 30295781-4301-EABD-2904-2849ADFEAE43
    • properties: Write + Notify
    • use this for commands and protocol notifications
  • Status char: 30295782-4301-EABD-2904-2849ADFEAE43
    • properties: Read + Notify
    • mainly used for cached status polling

Subscribe to notifications on the Control characteristic.

Status Char Read

Returns 16 bytes = 4 x uint32 LE:

  • 0..3: used_bytes
  • 4..7: unread_packets
  • 8..11: free_bytes
  • 12..15: rtc_valid

Commands

Write commands to the Control characteristic.

0x10 CMD_RING_INFO

  • Request: [0x10]
  • Meaning: request current ring state

0x11 CMD_RING_READ

  • Request: [0x11][start_seq: u64 BE]
  • Or: [0x11][start_seq: u64 BE][packet_count: u32 BE]
  • If packet_count is omitted or 0, firmware streams everything available from start_seq

0x12 CMD_RING_ADVANCE

  • Request: [0x12][new_read_seq: u64 BE]
  • Meaning: durably commit consumer progress

0x13 CMD_RING_CLEAR

  • Request: [0x13]
  • Meaning: clear the entire ring

0x03 CMD_STOP_SYNC

  • Request: [0x03]
  • Meaning: stop current transfer only
  • Does not persist progress

Notifications

All notifications are sent on the Control characteristic.

0x01 NOTIFY_ACK

  • Format: [0x01][status]

0x02 NOTIFY_INFO

  • Format:
    [0x02][read_seq: u64 BE][write_seq: u64 BE][capacity_packets: u32 BE][dropped_packets: u64 BE][packet_size: u16 BE]
  • Current packet_size = 444

0x05 NOTIFY_READ_BEGIN

  • Format:
    [0x05][transfer_start_seq: u64 BE][packet_count: u32 BE]

0x03 NOTIFY_DATA

  • Format:
    [0x03][raw_bytes...]
  • Important: these are BLE chunks, not guaranteed to align to 444-byte records
  • The app must concatenate all payload bytes and rebuild full records itself

0x04 NOTIFY_DONE

  • Format:
    [0x04][status][next_seq: u64 BE]
  • If the app stored everything successfully, use next_seq in CMD_RING_ADVANCE

Status Codes

  • 0: success
  • 6: invalid command
  • 9: storage not ready
  • 10: sequence out of range

Important Semantics

  • read_seq = oldest unread packet
  • write_seq = next sequence after newest committed packet
  • if the ring overwrites old data:
    • firmware auto-advances read_seq
    • firmware increments dropped_packets
  • if the app requests a start_seq older than current read_seq, firmware returns 10

Recommended App Flow

  1. Connect.
  2. Subscribe to Control notifications.
  3. Send CMD_RING_INFO.
  4. Wait for NOTIFY_INFO.
  5. Send CMD_RING_READ(read_seq).
  6. Wait for NOTIFY_READ_BEGIN.
  7. Collect NOTIFY_DATA chunks and rebuild 444-byte records.
  8. Wait for NOTIFY_DONE.
  9. If data is safely saved, send CMD_RING_ADVANCE(next_seq).
  10. Repeat until read_seq == write_seq.

Migration Note

Old flow was file-based:

  • list files
  • read file
  • delete file

New flow is ring-based:

  • get ring info
  • read by sequence
  • advance by sequence
  • clear ring

@greptile-apps

greptile-apps Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the LittleFS file-based offline audio storage with a raw-sector ring buffer written directly to NAND flash sectors. Audio packets are batched into 16 KB aligned sectors, metadata is wear-leveled across 64 dedicated sectors using a generation counter, and the BLE storage service is updated to speak the new sequence-number-based protocol (CMD_RING_INFO, CMD_RING_READ, CMD_RING_ADVANCE, CMD_RING_CLEAR).

  • sd_card.c: Full rewrite. Each batch holds up to 36 audio packets; the ring wraps automatically, advancing read_seq and incrementing dropped_packets on overflow. Legacy compatibility shims delegate to the new ring API.
  • storage.c: BLE service rewritten around the ring API. The GATT read handler now returns lock-free atomic-cached stats; stop_requested is now checked inside the -ENOMEM BLE-TX retry path.
  • sd_card.h: Trimmed from 291 to 57 lines; exposes only the new ring API and sd_ring_info_t.

Confidence Score: 3/5

The core ring-buffer mechanics are well-structured, but silent audio loss and a service starvation path on timeout remain unresolved.

The previously-flagged silent discard of all pre-RTC-sync audio is unchanged. A timeout in any sd_ring_* blocking call leaves the in-flight flag set until the SD worker drains the request, during which all callers receive -EBUSY with no log visibility. The get_oldest_packet_name TOCTOU between sd_ring_get_info and sd_ring_read can produce spurious errors on the BLE file-list path. These issues sit on the core data-path of a newly rewritten storage layer.

omi/firmware/omi/src/sd_card.c needs the most attention: the in-flight timeout handling, the get_oldest_packet_name TOCTOU, and the no-RTC discard path.

Important Files Changed

Filename Overview
omi/firmware/omi/src/sd_card.c Complete rewrite replacing LittleFS with a raw sector ring buffer. Contains TOCTOU in get_oldest_packet_name, a starvation path on sd_ring_get_info timeout, and silent discard of pre-RTC-sync audio.
omi/firmware/omi/src/lib/core/storage.c Rewrites the BLE storage service to use the new ring-buffer API. Replaces the blocking GATT read handler with lock-free atomic reads and adds proper stop_requested check in the -ENOMEM retry path.
omi/firmware/omi/src/lib/core/sd_card.h Header trimmed from 291 to 57 lines; removes all LittleFS request types and exposes only the new ring-buffer API and sd_ring_info_t.
omi/firmware/omi/src/lib/core/transport.c Minor update aligning sd_notify_ble_state calls with the new ring-buffer API. No functional regressions.

Reviews (2): Last reviewed commit: "improv - the audio sync speed increases ..." | Re-trigger Greptile

Comment on lines +380 to +381
done_pending = true;
remaining_packets = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 HEARTBEAT macro and heartbeat_count are vestigial

#define HEARTBEAT 50 is defined but never used. heartbeat_count is incremented on every iteration of write_to_gatt via (heartbeat_count + 1U) % 101U but its value is never read or tested for any condition. Both can be removed.

@TuEmb

TuEmb commented May 10, 2026

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

Comment thread omi/firmware/omi/src/sd_card.c Outdated
Comment on lines 846 to 873
static int get_oldest_packet_name(char *buf, size_t buf_size)
{
if (!filenames || !count || max_files <= 0)
if (!buf || buf_size == 0U) {
return -EINVAL;
if (!is_mounted)
return -ENODEV;
}

int cache_res = ensure_file_cache();
if (cache_res < 0) {
return cache_res;
sd_ring_info_t info;
int ret = sd_ring_get_info(&info);
if (ret < 0) {
return ret;
}

int file_count = cached_file_list_count;
if (file_count > max_files) {
file_count = max_files;
if (info.read_seq == info.write_seq) {
buf[0] = '\0';
return 0;
}

for (int i = 0; i < file_count; i++) {
strncpy(filenames[i], cached_file_names[i], MAX_FILENAME_LEN - 1);
filenames[i][MAX_FILENAME_LEN - 1] = '\0';
if (sizes) {
sizes[i] = cached_file_sizes[i];
}
uint8_t packet[RAW_AUDIO_PACKET_BYTES];
uint32_t bytes_read = 0;
uint32_t packets_read = 0;
ret = sd_ring_read(info.read_seq, packet, sizeof(packet), &bytes_read, &packets_read);
if (ret < 0 || packets_read == 0U) {
return (ret < 0) ? ret : -ENOENT;
}

*count = file_count;
format_timestamp_name(sys_get_be32(packet), buf, buf_size);
return 0;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale info.read_seq used in subsequent sd_ring_read call

get_oldest_packet_name snapshots info.read_seq via sd_ring_get_info, then issues a separate sd_ring_read(info.read_seq, ...). Between these two blocking worker round-trips, an external caller can issue CMD_RING_ADVANCE which updates ring_state.read_seq. If that happens, info.read_seq is now behind the current ring_state.read_seq, and read_packets_internal returns -ERANGE. The caller (get_audio_file_list_with_sizes) propagates this error, causing the file-list operation to fail silently from the BLE client's perspective. The fix is to either use a single atomic worker operation that returns both the oldest seq and its packet, or to handle -ERANGE by retrying with a fresh sd_ring_get_info.

Comment thread omi/firmware/omi/src/sd_card.c Outdated
Comment on lines +1183 to +1188
if (k_sem_take(&resp.sem, K_MSEC(5000)) != 0) {
return -ETIMEDOUT;
}

*info = resp.info;
return resp.res;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 sd_ring_get_info leaves info_in_flight set after timeout, silently starving all callers

When k_sem_take times out, the function returns -ETIMEDOUT without clearing info_in_flight. info_in_flight is only cleared by the SD worker via release_resp_busy when it eventually processes the enqueued request. If the SD worker is stalled, every subsequent call to sd_ring_get_info will immediately return -EBUSY for the full duration — potentially freezing the entire storage service. The same pattern applies to sd_ring_read, sd_ring_advance, and sd_ring_clear. At minimum, the timeout branch should log a warning so operators can detect this condition.

mdmohsin7 added a commit that referenced this pull request May 24, 2026
## Summary

App-side support for the ring-buffer NAND storage protocol introduced in
firmware PR #7216 (TuEmb). Devices on fw >= 3.0.20 will use this path;
fw 3.0.17–.19 keep the existing multi-file LittleFS path; fw < 3.0.17
stay on the legacy SD-card path.

The 444-byte record format (`[ts:4 BE][audio:440]`) is identical to the
multi-file protocol on main — only the control plane changes (read by
u64 sequence, advance by sequence). The audio parser is reused
unchanged.

## Commits (atomic)

1. Add `RingStatus` / `RingInfo` data classes + abstract method surface
on `DeviceConnection`
2. Implement ring-buffer BLE protocol on `OmiDeviceConnection`
(CMD_RING_INFO/READ/ADVANCE/CLEAR + NOTIFY_INFO/ACK/READ_BEGIN/DATA/DONE
handling)
3. Add `RingStorageSyncImpl` mirroring `StorageSyncImpl` but talking
ring (NOTIFY_DATA reassembly across BLE chunks, advance only after
LocalWalSync confirms chunk landed)
4. Gate Phase-0 sync in `WalSyncs.syncAll()` by firmware version; route
`_checkAndStartAutoSync` through `getRingStatus()` for ring-capable
devices
5. Refactor — extract pure-data wire helpers into `RingProtocol` so
they're testable without BLE mocking
6. 30 unit tests covering status / NOTIFY_INFO / NOTIFY_DONE /
NOTIFY_READ_BEGIN parsers, CMD_RING_READ / CMD_RING_ADVANCE u64 BE
encoding, packed audio payload framing, and the BLE chunk reassembler

## Data-safety invariant

`CMD_RING_ADVANCE` is sent only after `NOTIFY_DONE` arrives with
status=0 **and** every chunk has been registered with `LocalWalSync`.
Any failure (cancel, BLE drop, flush error, non-zero DONE status) leaves
the ring untouched — the next sync resumes from the same `read_seq`.
This is the data-loss win versus the file-based protocol's
delete-after-read semantics.

## RTC handling

If `status.rtc_valid == 0` (device RTC not yet synced via
`performSyncTime`), per-record timestamps are treated as unreliable and
timerStart falls back to `now - estimated_duration`, matching the legacy
code path. If `rtc_valid == 1`, the first record's BE u32 timestamp
anchors timerStart.

## Out of scope

- WiFi sync path — unchanged (no longer in user-facing use)
- Backend / cloud upload — `LocalWalSync` is reused as-is; no API
changes

## Test plan

- [x] Local: `cd app && flutter test test/unit/ring_protocol_test.dart`
— 30/30 pass
- [x] Local: full `flutter analyze` — 0 new issues introduced
(pre-existing warnings on main untouched)
- [x] Dev node: connect a fw 3.0.20 device, verify auto-sync banner
triggers on connect when ring has unread packets, verify download
completes and ring advances
- [x] Dev node: cancel sync mid-stream, verify ring is NOT advanced,
verify next sync resumes from same `read_seq`
- [ ] Dev node: connect a fw 3.0.17–.19 device (existing multi-file
path), verify behavior unchanged
- [ ] Dev node: connect a fw < 3.0.17 device (legacy SD card path),
verify behavior unchanged

Closes nothing yet (firmware PR #7216 is the dependency).
pull Bot pushed a commit to codingwatching/omi that referenced this pull request May 24, 2026
…viceConnection

Adds data classes and abstract method surface for the ring-buffer storage
protocol (firmware 3.0.20+, omi PR BasedHardware#7216). Same BLE service UUID as the
existing multi-file protocol; subclasses fill in the new opcodes.

The 444-byte record format ([ts:4 BE][audio:440]) is identical to the
multi-file protocol on main, so the audio parser will be reused. Only
the control-plane (read-by-sequence, advance-by-sequence) is new.
pull Bot pushed a commit to codingwatching/omi that referenced this pull request May 24, 2026
Mirrors StorageSyncImpl but talks the ring protocol from omi PR BasedHardware#7216:

- refreshWalsFromDevice: 16B status read; emits ONE virtual Wal covering
  the entire unread region (the ring is a single logical stream, not a
  list of files). Skipped when <10s of audio, mirroring the file-based
  threshold.
- _syncRing: snapshots ring state via getRingInfo, sends CMD_RING_READ,
  demuxes notifications by opcode (ACK/INFO/DATA/DONE/READ_BEGIN), and
  reassembles the unaligned NOTIFY_DATA byte stream into 444B records.
  Per record, strips the 4B BE timestamp and feeds the 440B audio
  payload through the same packed [size:1][frame:size] parser used by
  the multi-file path.
- Chunks flush to disk + register with LocalWalSync as we go (60s
  chunks per sdcardChunkSizeSecs) so a mid-stream BLE drop still lands
  the early audio in the cloud.
- CMD_RING_ADVANCE is sent ONLY after NOTIFY_DONE arrives with status=0
  AND every chunk has been registered with LocalWalSync. Any failure
  (cancel, BLE drop, flush error, DONE with non-zero status) leaves the
  ring untouched — the next sync resumes from the same read_seq. This
  is the data-safety invariant.
- rtc_valid handling: if the device RTC isn't synced yet, fall back to
  now-duration like the legacy code does. Otherwise the per-record
  timestamp from the first record anchors timerStart.
- dropped_packets > 0 surfaces as a DebugLogManager warning; sync
  proceeds with whatever the firmware still has.
@ThomsenDrake

Copy link
Copy Markdown
Collaborator

Closing as stale after confirmed stale-close triage. This PR was identified as high-confidence stale due to age plus one or more of: conflicts, draft/failing status, superseded work, or obsolete architecture. No merge was performed. If the work is still needed, please reopen with a fresh rebase or start from current main.

@github-actions

Copy link
Copy Markdown
Contributor

Hey @TuEmb 👋

Thank you so much for taking the time to contribute to Omi! We truly appreciate you putting in the effort to submit this pull request.

After careful review, we've decided not to merge this particular PR. Please don't take this personally — we genuinely try to merge as many contributions as possible, but sometimes we have to make tough calls based on:

  • Project standards — Ensuring consistency across the codebase
  • User needs — Making sure changes align with what our users need
  • Code best practices — Maintaining code quality and maintainability
  • Project direction — Keeping aligned with our roadmap and vision

Your contribution is still valuable to us, and we'd love to see you contribute again in the future! If you'd like feedback on how to improve this PR or want to discuss alternative approaches, please don't hesitate to reach out.

Thank you for being part of the Omi community! 💜

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants