Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 171 additions & 36 deletions app/lib/services/wals/sdcard_wal_sync.dart
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,15 @@ class SDCardWalSyncImpl implements SDCardWalSync {
BleAudioCodec codec = await _getAudioCodec(deviceId);
if (totalBytes - storageOffset > 10 * codec.getFramesLengthInBytes() * codec.getFramesPerSecond()) {
var seconds = ((totalBytes - storageOffset) / codec.getFramesLengthInBytes()) ~/ codec.getFramesPerSecond();
var timerStart = DateTime.now().millisecondsSinceEpoch ~/ 1000 - seconds;
// Use device-provided recording start timestamp if available, otherwise estimate
int timerStart;
if (storageFiles.length >= 3 && storageFiles[2] > 0) {
timerStart = storageFiles[2];
} else {
timerStart = DateTime.now().millisecondsSinceEpoch ~/ 1000 - seconds;
}
print(
'SDCardWalSync: totalBytes=$totalBytes storageOffset=$storageOffset frameLengthInBytes=${codec.getFramesLengthInBytes()} fps=${codec.getFramesPerSecond()} calculatedSeconds=$seconds timerStart=$timerStart now=${DateTime.now().millisecondsSinceEpoch ~/ 1000}');

var connection = await ServiceManager.instance().device.ensureConnection(deviceId);
if (connection == null) {
Expand Down Expand Up @@ -285,7 +293,8 @@ class SDCardWalSyncImpl implements SDCardWalSync {
return file;
}

Future _readStorageBytesToFile(Wal wal, Function(File f, int offset, int timerStart) callback) async {
Future _readStorageBytesToFile(
Wal wal, Function(File f, int offset, int timerStart, int chunkFrames) callback) async {
var deviceId = wal.device;
int fileNum = wal.fileNum;
int offset = wal.storageOffset;
Expand All @@ -295,7 +304,9 @@ class SDCardWalSyncImpl implements SDCardWalSync {

List<List<int>> bytesData = [];
var bytesLeft = 0;
var chunkSize = sdcardChunkSizeSecs * 100;
var chunkSize = sdcardChunkSizeSecs * wal.codec.getFramesPerSecond();
// Timestamp markers: list of (frameIndex, epoch) for segment splitting
List<MapEntry<int, int>> timestampMarkers = [];
await _storageStream?.cancel();
final completer = Completer<bool>();
bool hasError = false;
Expand Down Expand Up @@ -345,7 +356,20 @@ class SDCardWalSyncImpl implements SDCardWalSync {
while (packageOffset < value.length - 1) {
var packageSize = value[packageOffset];
if (packageSize == 0) {
packageOffset += packageSize + 1;
packageOffset += 1;
continue;
}
// Timestamp marker: 0xFF followed by 4-byte little-endian epoch
if (packageSize == 0xFF && packageOffset + 5 <= value.length) {
var epoch = value[packageOffset + 1] |
(value[packageOffset + 2] << 8) |
(value[packageOffset + 3] << 16) |
(value[packageOffset + 4] << 24);
packageOffset += 5;
if (epoch > 0) {
timestampMarkers.add(MapEntry(bytesData.length, epoch));
Logger.debug('Timestamp marker: epoch=$epoch at frame ${bytesData.length}');
}
continue;
}
if (packageOffset + 1 + packageSize >= value.length) {
Expand All @@ -358,20 +382,61 @@ class SDCardWalSyncImpl implements SDCardWalSync {
offset += value.length;
}

if (bytesData.length - bytesLeft >= chunkSize) {
// Find the next marker boundary (if any) after bytesLeft
int nextMarkerIdx = bytesData.length;
for (var m in timestampMarkers) {
if (m.key > bytesLeft) {
nextMarkerIdx = m.key;
break;
}
}

// Chunk up to the next marker boundary or chunkSize, whichever comes first
while (bytesData.length - bytesLeft >= chunkSize && bytesLeft + chunkSize <= nextMarkerIdx) {
var chunk = bytesData.sublist(bytesLeft, bytesLeft + chunkSize);
var chunkFrames = chunk.length;
var chunkSecs = chunkFrames ~/ wal.codec.getFramesPerSecond();
bytesLeft += chunkSize;
timerStart += sdcardChunkSizeSecs;
try {
var file = await _flushToDisk(wal, chunk, timerStart);
await callback(file, offset, timerStart);
await callback(file, offset, timerStart, chunkFrames);
} catch (e) {
Logger.debug('Error in callback during chunking: $e');
hasError = true;
if (!completer.isCompleted) {
completer.completeError(e);
}
}
timerStart += chunkSecs;
}

// If we've reached a marker boundary, flush remaining frames before it and advance timerStart
if (nextMarkerIdx <= bytesData.length && bytesLeft < nextMarkerIdx) {
// Only flush if there are enough frames to be meaningful (> 0)
var chunk = bytesData.sublist(bytesLeft, nextMarkerIdx);
var chunkFrames = chunk.length;
if (chunkFrames > 0) {
var chunkSecs = chunkFrames ~/ wal.codec.getFramesPerSecond();
bytesLeft = nextMarkerIdx;
try {
var file = await _flushToDisk(wal, chunk, timerStart);
await callback(file, offset, timerStart, chunkFrames);
} catch (e) {
Logger.debug('Error flushing segment at marker: $e');
hasError = true;
if (!completer.isCompleted) completer.completeError(e);
}
timerStart += chunkSecs;
} else {
bytesLeft = nextMarkerIdx;
}
// Apply the marker's epoch
for (var m in timestampMarkers) {
if (m.key == nextMarkerIdx) {
timerStart = m.value;
break;
}
}
}
});

Expand All @@ -395,11 +460,36 @@ class SDCardWalSyncImpl implements SDCardWalSync {
timeoutTimer.cancel();
}

if (!hasError && bytesLeft < bytesData.length - 1) {
var chunk = bytesData.sublist(bytesLeft);
timerStart += sdcardChunkSizeSecs;
var file = await _flushToDisk(wal, chunk, timerStart);
await callback(file, offset, timerStart);
// Flush remaining data, respecting any unprocessed timestamp markers
if (!hasError && bytesLeft < bytesData.length) {
// Build segment boundaries from any remaining markers
List<List<int>> segments = [];
int segStart = bytesLeft;
int segEpoch = timerStart;
for (var marker in timestampMarkers) {
if (marker.key > bytesLeft && marker.key < bytesData.length) {
if (marker.key > segStart) {
segments.add([segStart, marker.key, segEpoch]);
}
segStart = marker.key;
segEpoch = marker.value;
}
}
if (segStart < bytesData.length) {
segments.add([segStart, bytesData.length, segEpoch]);
}

for (var seg in segments) {
int sStart = seg[0];
int sEnd = seg[1];
int sEpoch = seg[2];
var chunk = bytesData.sublist(sStart, sEnd);
var chunkFrames = chunk.length;
if (chunkFrames > 0) {
var file = await _flushToDisk(wal, chunk, sEpoch);
await callback(file, offset, sEpoch, chunkFrames);
}
}
}

return;
Expand All @@ -424,14 +514,14 @@ class SDCardWalSyncImpl implements SDCardWalSync {
_totalBytesDownloaded = 0;

try {
await _readStorageBytesToFile(wal, (File file, int offset, int timerStart) async {
await _readStorageBytesToFile(wal, (File file, int offset, int timerStart, int chunkFrames) async {
if (_isCancelled) {
throw Exception('Sync cancelled by user');
}

int bytesInChunk = offset - lastOffset;
_updateSpeed(bytesInChunk);
await _registerSingleChunk(wal, file, timerStart);
await _registerSingleChunk(wal, file, timerStart, chunkFrames);
chunksDownloaded++;
lastOffset = offset;

Expand Down Expand Up @@ -465,13 +555,13 @@ class SDCardWalSyncImpl implements SDCardWalSync {
return SyncLocalFilesResponse(newConversationIds: [], updatedConversationIds: []);
}

Future<void> _registerSingleChunk(Wal wal, File file, int timerStart) async {
Future<void> _registerSingleChunk(Wal wal, File file, int timerStart, int chunkFrames) async {
if (_localSync == null) {
Logger.debug("SDCard: WARNING - Cannot register chunk, LocalWalSync not available");
return;
}

int chunkSeconds = sdcardChunkSizeSecs;
int chunkSeconds = chunkFrames ~/ wal.codec.getFramesPerSecond();

Wal localWal = Wal(
codec: wal.codec,
Expand All @@ -484,7 +574,7 @@ class SDCardWalSyncImpl implements SDCardWalSync {
device: wal.device,
deviceModel: wal.deviceModel,
seconds: chunkSeconds,
totalFrames: chunkSeconds * wal.codec.getFramesPerSecond(),
totalFrames: chunkFrames,
syncedFrameOffset: 0,
originalStorage: WalStorage.sdcard,
);
Expand Down Expand Up @@ -913,6 +1003,8 @@ class SDCardWalSyncImpl implements SDCardWalSync {
var bytesLeft = 0;
var chunkSize = sdcardChunkSizeSecs * wal.codec.getFramesPerSecond();
var timerStart = wal.timerStart;
// Timestamp markers: list of (frameIndex, epoch) pairs for segment splitting
List<MapEntry<int, int>> timestampMarkers = [];

final initialOffset = wal.storageOffset;
var offset = wal.storageOffset;
Expand Down Expand Up @@ -1000,6 +1092,22 @@ class SDCardWalSyncImpl implements SDCardWalSync {
continue;
}

// Timestamp marker: 0xFF followed by 4-byte little-endian epoch
if (packageSize == 0xFF && packageOffset + 5 <= bufferLength) {
var epoch = tcpBuffer[packageOffset + 1] |
(tcpBuffer[packageOffset + 2] << 8) |
(tcpBuffer[packageOffset + 3] << 16) |
(tcpBuffer[packageOffset + 4] << 24);
packageOffset += 5;
bytesProcessed = packageOffset;
if (epoch > 0) {
// Record frame index and epoch for segment splitting during flush
timestampMarkers.add(MapEntry(bytesData.length, epoch));
Logger.debug('SDCardWalSync WiFi: Timestamp marker: epoch=$epoch at frame ${bytesData.length}');
}
continue;
}

// Check if we're in padding area at end of block
if (posInBlock > 0 && bytesRemainingInBlock < 12) {
if (packageOffset + bytesRemainingInBlock > bufferLength) {
Expand Down Expand Up @@ -1135,28 +1243,55 @@ class SDCardWalSyncImpl implements SDCardWalSync {
final wasCancelled = _isCancelled;
final transferComplete = offset >= wal.storageTotalBytes;

// Flush all collected data in chunks
while (bytesData.length - bytesLeft >= chunkSize) {
var chunk = bytesData.sublist(bytesLeft, bytesLeft + chunkSize);
bytesLeft += chunkSize;
timerStart += sdcardChunkSizeSecs;
try {
var file = await _flushToDisk(wal, chunk, timerStart);
await _registerSingleChunk(wal, file, timerStart);
} catch (e) {
Logger.debug('SDCardWalSync WiFi: Error flushing chunk: $e');
// Build segment boundaries from timestamp markers
// Each segment: [startFrameIndex, endFrameIndex, epoch]
List<List<int>> segments = [];
int segStart = bytesLeft;
int segEpoch = timerStart;
for (var marker in timestampMarkers) {
int frameIdx = marker.key;
int epoch = marker.value;
if (frameIdx > segStart) {
segments.add([segStart, frameIdx, segEpoch]);
}
segStart = frameIdx;
segEpoch = epoch;
}
// Final segment from last marker (or start) to end
if (segStart < bytesData.length) {
segments.add([segStart, bytesData.length, segEpoch]);
}

// Flush any remaining frames
if (bytesLeft < bytesData.length) {
var chunk = bytesData.sublist(bytesLeft);
timerStart += sdcardChunkSizeSecs;
try {
var file = await _flushToDisk(wal, chunk, timerStart);
await _registerSingleChunk(wal, file, timerStart);
} catch (e) {
Logger.debug('SDCardWalSync WiFi: Error flushing final chunk: $e');
// Flush each segment, chunking within it
for (var seg in segments) {
int sStart = seg[0];
int sEnd = seg[1];
int sEpoch = seg[2];
timerStart = sEpoch;
int pos = sStart;
while (sEnd - pos >= chunkSize) {
var chunk = bytesData.sublist(pos, pos + chunkSize);
var chunkFrames = chunk.length;
var chunkSecs = chunkFrames ~/ wal.codec.getFramesPerSecond();
pos += chunkSize;
try {
var file = await _flushToDisk(wal, chunk, timerStart);
await _registerSingleChunk(wal, file, timerStart, chunkFrames);
} catch (e) {
Logger.debug('SDCardWalSync WiFi: Error flushing chunk: $e');
}
timerStart += chunkSecs;
}
// Flush remaining frames in this segment
if (pos < sEnd) {
var chunk = bytesData.sublist(pos, sEnd);
var chunkFrames = chunk.length;
try {
var file = await _flushToDisk(wal, chunk, timerStart);
await _registerSingleChunk(wal, file, timerStart, chunkFrames);
} catch (e) {
Logger.debug('SDCardWalSync WiFi: Error flushing final chunk: $e');
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion omi/firmware/omi/src/lib/core/button.c
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ void turnoff_all()
k_msleep(100);
#endif

storage_flush_buffer();
if (is_sd_on()) {
app_sd_off();
}
Expand Down Expand Up @@ -426,7 +427,6 @@ void turnoff_all()
return;
}


/* Persist an IMU timestamp base so we can estimate time across system_off. */
lsm6dsl_time_prepare_for_system_off();
k_msleep(1000);
Expand Down
21 changes: 15 additions & 6 deletions omi/firmware/omi/src/lib/core/sd_card.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@
#define MAX_WRITE_SIZE 440

/* Request types for the SD worker */
typedef enum {
REQ_CLEAR_AUDIO_DIR,
REQ_WRITE_DATA,
REQ_READ_DATA,
REQ_SAVE_OFFSET
} sd_req_type_t;
typedef enum { REQ_CLEAR_AUDIO_DIR, REQ_WRITE_DATA, REQ_READ_DATA, REQ_SAVE_OFFSET } sd_req_type_t;

/* Read request response object */
struct read_resp {
Expand Down Expand Up @@ -143,6 +138,20 @@ int save_offset(uint32_t offset);
*/
uint32_t get_offset(void);

/**
* @brief Set the recording start time (only sets if not already set)
*
* @param ts UTC epoch seconds when offline recording started
*/
void set_recording_start_time(uint32_t ts);

/**
* @brief Get the recording start time
*
* @return UTC epoch seconds, or 0 if not set
*/
uint32_t get_recording_start_time(void);

/**
* @brief Turn on SD card power
*/
Expand Down
Loading