Skip to content

feat - remove wifi sync and update new SD sync method for Omi CV1#5905

Closed
TuEmb wants to merge 2 commits intoBasedHardware:mainfrom
TuEmb:TuEmb/sd_sync_app
Closed

feat - remove wifi sync and update new SD sync method for Omi CV1#5905
TuEmb wants to merge 2 commits intoBasedHardware:mainfrom
TuEmb:TuEmb/sd_sync_app

Conversation

@TuEmb
Copy link
Copy Markdown
Contributor

@TuEmb TuEmb commented Mar 22, 2026

Note: This PR is 100% AI generation

This pull request introduces several improvements and refactorings related to file sync and UI feedback for syncing in the application. The main changes include adding upload progress tracking to backend file sync operations, simplifying the sync page UI by removing WiFi-specific sync logic, and enhancing the user experience with animated progress and speed indicators during sync.

Backend & Progress Tracking:

  • Added support for upload progress callbacks in makeMultipartApiCall and related multipart request functions, allowing real-time tracking of bytes sent, total bytes, and upload speed (KB/s) during file uploads. This includes propagating the onUploadProgress callback through all relevant layers (syncLocalFiles, _buildMultipartRequest, etc.) [1] [2] [3] [4] [5] [6]

Sync Page UI Simplification & Improvements:

  • Removed all WiFi-specific sync options, dialogs, and error handling from sync_page.dart, consolidating the sync flow and error reporting. The settings card and sync initiation logic no longer reference or branch on WiFi/fast transfer methods [1] [2] [3] [4] [5] [6].

  • Updated the sync progress UI to use animated transitions for both speed (KB/s) and progress percentage, providing a smoother and more visually appealing user experience. This includes using TweenAnimationBuilder for speed and progress indicators, and animating the progress bar itself [1] [2] [3].

Preferences Utility:

  • Added getter and setter methods in SharedPreferencesUtil for per-device auto-sync settings, allowing the app to store and retrieve whether auto-sync is enabled for a specific device.

Code Cleanup:

  • Removed unused imports and code related to WiFi sync and fast transfer settings from sync_page.dart for better maintainability [1] [2].

These changes collectively streamline the sync experience, provide better feedback to users, and lay groundwork for more robust file upload handling.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 22, 2026

Greptile Summary

This PR removes WiFi-based SD card sync entirely and replaces it with a new multi-file BLE protocol for Omi CV1, adds real-time upload progress tracking through the HTTP multipart layer, and cleans up the sync UI by removing WiFi-specific dialogs and error handling.

Key changes:

  • New BLE commands CMD_LIST_FILES (0x10), CMD_READ_FILE (0x11), CMD_DELETE_FILE (0x12) and STOP_SYNC (0x03) implemented in OmiDeviceConnection; files are now processed oldest-to-newest with delete-after-download
  • makeMultipartApiCall / _buildMultipartRequest gain an onUploadProgress callback using a StreamTransformer, propagated up through syncLocalFiles into LocalWalSyncImpl for real-time KB/s and ETA display
  • Per-device auto-sync toggle added in device settings, backed by SharedPreferencesUtil and honoured on every fresh BLE connection
  • sync_page.dart and wal_item_detail_page.dart simplified; animated TweenAnimationBuilder progress/speed indicators replace static text

Issues found:

  • _buildWalsFromFileList calls ensureConnection a second time without a null guard; if the BLE link drops between the two calls the result is passed to getDeviceInfo and will crash
  • After deleting a file, the code decrements fileNum for all remaining WALs assuming firmware re-indexes — if the firmware uses stable indices, subsequent reads and deletes will target the wrong files
  • performListFiles and performDeleteFile in OmiDeviceConnection do not cancel their StreamSubscription in exception paths, leaving stale listeners on the storage characteristic
  • performDeleteFile may prematurely complete with true if a stale 1-byte 0x00 notification (e.g. from a preceding stopStorageSync or listFiles empty response) arrives on the shared characteristic before the firmware processes the delete command

Confidence Score: 2/5

  • The PR introduces a significant protocol overhaul with several correctness bugs in the new BLE file-management commands that could cause crashes or silently corrupt sync state in production.
  • Three P1 logic bugs exist: an unguarded null dereference in _buildWalsFromFileList that will crash during WAL discovery if the BLE link drops at the wrong moment, an unverified firmware re-indexing assumption that could cause wrong-file reads/deletes across a multi-file sync session, and StreamSubscription leaks in the new performListFiles/performDeleteFile methods. An additional stale-notification race in performDeleteFile can cause silent false-positive delete confirmations. These issues are in the core data-transfer path and could result in data corruption or app crashes for users with multiple SD card recordings.
  • app/lib/services/wals/sdcard_wal_sync.dart and app/lib/services/devices/omi_connection.dart require the most attention before merge.

Important Files Changed

Filename Overview
app/lib/services/devices/omi_connection.dart Adds performListFiles, performDeleteFile, performSyncDeviceTime, and performGetSyncState — the new multi-file SD card BLE protocol. Both performListFiles and performDeleteFile leak their StreamSubscription on exception paths, and performDeleteFile is susceptible to false-positive completions from stale 1-byte notifications on the shared characteristic.
app/lib/services/wals/sdcard_wal_sync.dart Major protocol overhaul replacing single-file BLE sync with a multi-file CMD_LIST_FILES / CMD_READ_FILE / CMD_DELETE_FILE protocol. Contains a null dereference risk in _buildWalsFromFileList and an unverified assumption about firmware file re-indexing after deletion that could corrupt subsequent sync operations.
app/lib/backend/http/shared.dart Adds upload progress tracking via a StreamTransformer wrapping each file's byte stream, with throttled callbacks and EMA-smoothed speed. Minor inefficiency: totalFileBytes is recalculated via async I/O after the response is already received, duplicating work done inside _buildMultipartRequest.
app/lib/services/devices/device_connection.dart Adds abstract protocol methods (listFiles, deleteFile, stopStorageSync, syncDeviceTime, getSyncState) with safe default no-op implementations, plus automatic time sync on every successful connection. Changes are well-structured and backwards-compatible.
app/lib/services/devices/device_settings.dart Adds per-device auto-sync toggle UI backed by SharedPreferencesUtil. Disabling immediately sends a stop command to firmware; re-enabling only saves the preference and relies on the next reconnection to take effect, which may surprise users who expect the toggle to be instantaneous.
app/lib/pages/conversations/sync_page.dart WiFi-specific sync logic, dialogs, and error-handling removed; sync now always uses BLE. Animated progress/speed indicators added via TweenAnimationBuilder. Simplification is clean and well-executed; no issues found.
app/lib/services/wals/wal_syncs.dart Removes WiFi sync path selection logic, WiFi credential loading, and internet-wait phase. SD card sync now always delegates to sdcardSync.syncAll(). Simpler and straightforward.
app/lib/backend/preferences.dart Adds getDeviceAutoSyncEnabled/setDeviceAutoSyncEnabled helpers keyed by device ID. Implementation is clean with safe defaults and empty-ID guard.
app/lib/providers/sync_provider.dart transferWalToPhone now redirects to syncWals (all files) instead of a per-file sync; WiFi error message mapping removed. The redirect is intentional but callers should be aware the scope of the transfer has widened.
app/lib/services/devices/models.dart Adds StorageFile and SyncStateInfo data classes for the new multi-file BLE protocol. Clean, minimal data models with no issues.
app/lib/providers/device_provider.dart On device connect, reads the per-device auto-sync preference and sends stopStorageSync if disabled. Simple, defensive implementation.
app/lib/pages/conversations/wal_item_detail/wal_item_detail_page.dart WiFi/transfer button logic removed; animated progress indicators added. The "Transfer to Phone" button and its entire code path are removed, simplifying the file.

Sequence Diagram

sequenceDiagram
    participant App as Flutter App
    participant DP as DeviceProvider
    participant SDSync as SDCardWalSync
    participant Conn as OmiDeviceConnection
    participant FW as Omi CV1 Firmware

    App->>DP: device connected
    DP->>DP: check autoSyncEnabled pref
    alt autoSync disabled
        DP->>Conn: stopStorageSync()
        Conn->>FW: STOP_COMMAND [0x03]
    end

    App->>SDSync: getMissingWals()
    SDSync->>Conn: stopStorageSync()
    Conn->>FW: STOP_COMMAND [0x03]
    note over SDSync,FW: 500ms delay
    SDSync->>Conn: listFiles()
    Conn->>FW: CMD_LIST_FILES [0x10]
    FW-->>Conn: [count][ts1:4BE][sz1:4BE]...
    Conn-->>SDSync: List<StorageFile>
    SDSync->>SDSync: _buildWalsFromFileList()

    App->>SDSync: syncAll()
    loop for each WAL (oldest→newest)
        SDSync->>Conn: _readStorageBytesToFile(wal)
        Conn->>FW: CMD_READ_FILE [0x11, fileIndex, offset]
        FW-->>Conn: [ts:4BE][audio_data...]
        note over Conn,SDSync: onPacketReceived → progress updates
        Conn-->>SDSync: all chunks received
        SDSync->>SDSync: upload to cloud via syncLocalFiles()
        SDSync->>SDSync: onUploadProgress → UI speed/progress
        SDSync->>Conn: deleteFile(fileNum)
        Conn->>FW: CMD_DELETE_FILE [0x12, fileIndex]
        FW-->>Conn: [0x00] success
        SDSync->>SDSync: decrement fileNum for higher-index WALs
    end
Loading

Comments Outside Diff (1)

  1. app/lib/services/devices/device_settings.dart, line 818-835 (link)

    P2 Re-enabling auto-sync has no immediate effect on an already-connected device

    _updateAutoSync(true) only persists the preference via SharedPreferencesUtil. The corresponding stop-command path in DeviceProvider is only executed on a fresh connection event, so if the device is already connected when the user flips the toggle back on, auto-sync will not resume until the next reconnect.

    Consider notifying the device to restart auto-sync (or at least informing the user that a reconnect is required) when enabling the toggle while a connection is active:

    if (enabled) {
      // Auto-sync will resume on next connection.
      // TODO: optionally trigger reconnect or show a "Reconnect to apply" hint.
    }

Reviews (1): Last reviewed commit: "remove wifi sync and update new SD sync ..." | Re-trigger Greptile

Comment on lines +268 to +270
var pd = await _device!.getDeviceInfo(connection);
String deviceModel = pd.modelNumber.isNotEmpty ? pd.modelNumber : "Omi";

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 Unguarded null dereference in _buildWalsFromFileList

ServiceManager.instance().device.ensureConnection() returns DeviceConnection?, but the result is passed directly to _device!.getDeviceInfo(connection) without a null check. If the BLE connection drops between the earlier ensureConnection call in _getMissingWals and this secondary call inside _buildWalsFromFileList, connection will be null, and the getDeviceInfo implementation will crash at runtime when it tries to invoke methods on the null connection.

The secondary ensureConnection call is also redundant — the caller already holds a live connection at this point. Consider passing the connection reference in from the call site, or at minimum add a null guard:

Future<List<Wal>> _buildWalsFromFileList(String deviceId, BleAudioCodec codec, List<StorageFile> files) async {
    var connection = await ServiceManager.instance().device.ensureConnection(deviceId);
    if (connection == null) {
      Logger.debug("SDCardWalSync: _buildWalsFromFileList - connection lost, returning empty");
      return [];
    }
    var pd = await _device!.getDeviceInfo(connection);
    String deviceModel = pd.modelNumber.isNotEmpty ? pd.modelNumber : "Omi";

Comment on lines +944 to +953
// After deletion, file indices shift for all files that had higher index.
for (var j = 0; j < wals.length; j++) {
if (j == i) continue;
if (wals[j].fileNum > wal.fileNum) {
wals[j].fileNum = wals[j].fileNum - 1;
}
}
}
} catch (e) {
Logger.debug("SDCardWalSync: Failed to delete file ${wal.fileNum}: $e (data is safe on phone)");
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 File index shift assumption may corrupt subsequent sync operations

After successfully deleting a file at index wal.fileNum, the code decrements the fileNum of every remaining WAL whose index is higher than the deleted one. This is only correct if the firmware re-indexes all remaining files after each deletion (i.e., deleting file 0 causes firmware to rename file 1→0, file 2→1, etc.).

If the firmware instead keeps stable/fixed indices (a common FAT-style approach), this in-memory decrement will cause all subsequent _writeToStorage and deleteFile calls to target the wrong file index. The data would still be safe on the phone, but leftover SD card files could never be deleted, and the next sync would attempt to read from the wrong file.

This assumption about firmware behavior needs to be explicitly verified and documented — it's a protocol contract that should appear in a comment or be validated with a connection-level assertion.

// After deletion, file indices shift for all files that had higher index.
// NOTE: this relies on the firmware shifting its file index table after
// CMD_DELETE_FILE. If firmware uses stable/fixed indices, remove this block.
for (var j = 0; j < wals.length; j++) {

Comment on lines +292 to +344
Future<List<StorageFile>> performListFiles() async {
try {
final completer = Completer<List<StorageFile>>();
StreamSubscription? sub;

final stream =
transport.getCharacteristicStream(storageDataStreamServiceUuid, storageDataStreamCharacteristicUuid);

sub = stream.listen((value) {
if (completer.isCompleted) return;
if (value.isEmpty) return;

int count = value[0];
int expectedLen = 1 + count * 8;

// Empty file list
if (count == 0 && value.length == 1) {
completer.complete([]);
return;
}

// Validate this looks like a file list response (not a data packet or status byte)
if (value.length >= expectedLen && count > 0 && count <= 128) {
List<StorageFile> files = [];
for (int i = 0; i < count; i++) {
int base = 1 + i * 8;
if (base + 8 > value.length) break;
int timestamp = (value[base] << 24) | (value[base + 1] << 16) | (value[base + 2] << 8) | value[base + 3];
int size = (value[base + 4] << 24) | (value[base + 5] << 16) | (value[base + 6] << 8) | value[base + 7];
files.add(StorageFile(index: i, timestamp: timestamp, size: size));
}
Logger.debug('OmiDeviceConnection: Listed ${files.length} files');
completer.complete(files);
}
});

// Send CMD_LIST_FILES
await transport.writeCharacteristic(storageDataStreamServiceUuid, storageDataStreamCharacteristicUuid, [0x10]);

final result = await completer.future.timeout(
const Duration(seconds: 10),
onTimeout: () {
Logger.debug('OmiDeviceConnection: listFiles timeout');
return <StorageFile>[];
},
);

await sub.cancel();
return result;
} catch (e) {
Logger.debug('OmiDeviceConnection: Error listing files: $e');
return [];
}
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 StreamSubscription leaked on exception path

In both performListFiles and performDeleteFile, sub is assigned inside the try block before writeCharacteristic is called. If writeCharacteristic throws (e.g. BLE write error), the catch block returns immediately without calling sub.cancel(). The subscription stays alive on the characteristic until the BLE connection closes, which can cause stale listeners to fire during a later operation on the same characteristic.

A simple fix is to wrap the post-listen work in a try/finally:

sub = stream.listen((value) { ... });
try {
  await transport.writeCharacteristic(..., [0x10]);
  final result = await completer.future.timeout(...);
  return result;
} finally {
  await sub.cancel();
}

The same pattern applies to performDeleteFile (lines 357–378).

Comment on lines +349 to +383
Future<bool> performDeleteFile(int fileIndex) async {
try {
final completer = Completer<bool>();
StreamSubscription? sub;

final stream =
transport.getCharacteristicStream(storageDataStreamServiceUuid, storageDataStreamCharacteristicUuid);

sub = stream.listen((value) {
if (completer.isCompleted) return;
if (value.length == 1) {
// 0 = success, other values = error codes
Logger.debug('OmiDeviceConnection: deleteFile result: ${value[0]}');
completer.complete(value[0] == 0);
}
});

// Send CMD_DELETE_FILE [0x12, fileIndex]
await transport.writeCharacteristic(
storageDataStreamServiceUuid, storageDataStreamCharacteristicUuid, [0x12, fileIndex & 0xFF]);

final result = await completer.future.timeout(
const Duration(seconds: 5),
onTimeout: () {
Logger.debug('OmiDeviceConnection: deleteFile timeout');
return false;
},
);

await sub.cancel();
return result;
} catch (e) {
Logger.debug('OmiDeviceConnection: Error deleting file: $e');
return false;
}
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 performDeleteFile may misinterpret a stale 1-byte notification

The delete response listener fires on any 1-byte packet on storageDataStreamCharacteristicUuid:

if (value.length == 1) {
    completer.complete(value[0] == 0);
}

The BLE GATT notification queue may still hold a single-byte status byte from the preceding stopStorageSync call (byte 0x00 = success) or from the listFiles trailing empty-list response (count = 0). Both are 1-byte payloads with value 0, indistinguishable from a successful delete confirmation. This could cause performDeleteFile to return true before the firmware has actually processed the delete command.

Consider introducing a command-tag prefix in the delete response, or adding a short drain period (await Future.delayed(const Duration(milliseconds: 50))) before setting up the listener to flush pending notifications.

Comment on lines 279 to +290
}
}

if (onUploadProgress != null) {
var totalFileBytes = 0;
for (var file in files) {
totalFileBytes += await file.length();
}
if (totalFileBytes > 0) {
onUploadProgress(totalFileBytes, totalFileBytes, null);
}
}
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 totalFileBytes re-computed via async I/O after response is received

After a successful response, the code calculates totalFileBytes a second time by iterating over files and calling await file.length() for each file. This is the same calculation already performed inside _buildMultipartRequest. For large batches this adds unnecessary async I/O after the upload is already complete.

The 100%-complete callback could instead reuse the total calculated earlier, e.g. by returning it from _buildMultipartRequest or computing it once before the request is built.

// Current (after-response block):
if (onUploadProgress != null) {
  var totalFileBytes = 0;
  for (var file in files) {
    totalFileBytes += await file.length();   // redundant I/O
  }
  if (totalFileBytes > 0) {
    onUploadProgress(totalFileBytes, totalFileBytes, null);
  }
}

@TuEmb TuEmb requested a review from mdmohsin7 March 22, 2026 13:04
@TuEmb TuEmb closed this Mar 28, 2026
@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.

1 participant