diff --git a/app/lib/services/wals/sdcard_wal_sync.dart b/app/lib/services/wals/sdcard_wal_sync.dart index 6a0c2d6a76..18f777b80e 100644 --- a/app/lib/services/wals/sdcard_wal_sync.dart +++ b/app/lib/services/wals/sdcard_wal_sync.dart @@ -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) { @@ -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; @@ -295,7 +304,9 @@ class SDCardWalSyncImpl implements SDCardWalSync { List> bytesData = []; var bytesLeft = 0; - var chunkSize = sdcardChunkSizeSecs * 100; + var chunkSize = sdcardChunkSizeSecs * wal.codec.getFramesPerSecond(); + // Timestamp markers: list of (frameIndex, epoch) for segment splitting + List> timestampMarkers = []; await _storageStream?.cancel(); final completer = Completer(); bool hasError = false; @@ -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) { @@ -358,13 +382,24 @@ 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; @@ -372,6 +407,36 @@ class SDCardWalSyncImpl implements SDCardWalSync { 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; + } + } } }); @@ -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> 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; @@ -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; @@ -465,13 +555,13 @@ class SDCardWalSyncImpl implements SDCardWalSync { return SyncLocalFilesResponse(newConversationIds: [], updatedConversationIds: []); } - Future _registerSingleChunk(Wal wal, File file, int timerStart) async { + Future _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, @@ -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, ); @@ -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> timestampMarkers = []; final initialOffset = wal.storageOffset; var offset = wal.storageOffset; @@ -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) { @@ -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> 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'); + } } } diff --git a/omi/firmware/omi/src/lib/core/button.c b/omi/firmware/omi/src/lib/core/button.c index f7addd4324..13250f1a33 100644 --- a/omi/firmware/omi/src/lib/core/button.c +++ b/omi/firmware/omi/src/lib/core/button.c @@ -386,6 +386,7 @@ void turnoff_all() k_msleep(100); #endif + storage_flush_buffer(); if (is_sd_on()) { app_sd_off(); } @@ -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); diff --git a/omi/firmware/omi/src/lib/core/sd_card.h b/omi/firmware/omi/src/lib/core/sd_card.h index 3e43386188..9fc6bac501 100644 --- a/omi/firmware/omi/src/lib/core/sd_card.h +++ b/omi/firmware/omi/src/lib/core/sd_card.h @@ -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 { @@ -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 */ diff --git a/omi/firmware/omi/src/lib/core/storage.c b/omi/firmware/omi/src/lib/core/storage.c index e643de4c2e..d7f84f452f 100644 --- a/omi/firmware/omi/src/lib/core/storage.c +++ b/omi/firmware/omi/src/lib/core/storage.c @@ -128,11 +128,12 @@ static ssize_t storage_read_characteristic(struct bt_conn *conn, uint16_t offset) { k_msleep(10); - uint32_t amount[2] = {0}; + uint32_t amount[3] = {0}; amount[0] = get_file_size(); amount[1] = get_offset(); - LOG_INF("Storage read requested: file size %u, offset %u", amount[0], amount[1]); - ssize_t result = bt_gatt_attr_read(conn, attr, buf, len, offset, amount, 2 * sizeof(uint32_t)); + amount[2] = get_recording_start_time(); + LOG_INF("Storage read requested: file size %u, offset %u, start_time %u", amount[0], amount[1], amount[2]); + ssize_t result = bt_gatt_attr_read(conn, attr, buf, len, offset, amount, 3 * sizeof(uint32_t)); return result; } @@ -273,64 +274,67 @@ static ssize_t storage_wifi_handler(struct bt_conn *conn, const uint8_t cmd = ((const uint8_t *) buf)[0]; switch (cmd) { - case 0x01: // WIFI_SETUP - LOG_INF("WIFI_SETUP: len=%d", len); - if (len < 2) { - LOG_WRN("WIFI_SETUP: invalid setup length: len=%d", len); - result_buffer[0] = 2; // error: invalid setup length - break; - } - // Parse SSID - // Format: [cmd][ssid_len][ssid][password_len][password] - uint8_t idx = 1; - uint8_t ssid_len = ((const uint8_t *)buf)[idx++]; - LOG_INF("WIFI_SETUP: ssid_len=%d, len=%d", ssid_len, len); - - if (ssid_len == 0 || ssid_len > WIFI_MAX_SSID_LEN || idx + ssid_len > len) { - LOG_WRN("SSID length invalid: ssid_len=%d, len=%d", ssid_len, len); - result_buffer[0] = 3; break; - } - char ssid[WIFI_MAX_SSID_LEN + 1] = {0}; - memcpy(ssid, &((const uint8_t *)buf)[idx], ssid_len); - idx += ssid_len; - LOG_INF("WIFI_SETUP: ssid='%s'", ssid); - - uint8_t pwd_len = ((const uint8_t *)buf)[idx++]; - if (pwd_len < WIFI_MIN_PASSWORD_LEN || pwd_len > WIFI_MAX_PASSWORD_LEN || idx + pwd_len > len) { - LOG_WRN("PWD length invalid: pwd_len=%d, len=%d", pwd_len, len); - result_buffer[0] = 4; break; - } - char pwd[WIFI_MAX_PASSWORD_LEN + 1] = {0}; - if (pwd_len > 0) memcpy(pwd, &((const uint8_t *)buf)[idx], pwd_len); - LOG_INF("WIFI_SETUP: pwd='%s' pwd_len=%d, len=%d", pwd, pwd_len, len); - - setup_wifi_credentials(ssid, pwd); - result_buffer[0] = 0; // success + case 0x01: // WIFI_SETUP + LOG_INF("WIFI_SETUP: len=%d", len); + if (len < 2) { + LOG_WRN("WIFI_SETUP: invalid setup length: len=%d", len); + result_buffer[0] = 2; // error: invalid setup length break; - - case 0x02: // WIFI_START - LOG_INF("WIFI_START command received"); - if (is_wifi_on()) { - LOG_INF("Wi-Fi already on - wait for next session"); - result_buffer[0] = 5; // wait for next session - break; - } - k_work_submit(&wifi_start_work); - result_buffer[0] = 0; + } + // Parse SSID + // Format: [cmd][ssid_len][ssid][password_len][password] + uint8_t idx = 1; + uint8_t ssid_len = ((const uint8_t *) buf)[idx++]; + LOG_INF("WIFI_SETUP: ssid_len=%d, len=%d", ssid_len, len); + + if (ssid_len == 0 || ssid_len > WIFI_MAX_SSID_LEN || idx + ssid_len > len) { + LOG_WRN("SSID length invalid: ssid_len=%d, len=%d", ssid_len, len); + result_buffer[0] = 3; break; - - case 0x03: // WIFI_SHUTDOWN - LOG_INF("WIFI_SHUTDOWN command received"); - storage_stop_transfer(); - wifi_turn_off(); - mic_resume(); - result_buffer[0] = 0; + } + char ssid[WIFI_MAX_SSID_LEN + 1] = {0}; + memcpy(ssid, &((const uint8_t *) buf)[idx], ssid_len); + idx += ssid_len; + LOG_INF("WIFI_SETUP: ssid='%s'", ssid); + + uint8_t pwd_len = ((const uint8_t *) buf)[idx++]; + if (pwd_len < WIFI_MIN_PASSWORD_LEN || pwd_len > WIFI_MAX_PASSWORD_LEN || idx + pwd_len > len) { + LOG_WRN("PWD length invalid: pwd_len=%d, len=%d", pwd_len, len); + result_buffer[0] = 4; break; - - default: - LOG_WRN("Unknown WIFI command: %d", cmd); - result_buffer[0] = 0xFF; // unknown command + } + char pwd[WIFI_MAX_PASSWORD_LEN + 1] = {0}; + if (pwd_len > 0) + memcpy(pwd, &((const uint8_t *) buf)[idx], pwd_len); + LOG_INF("WIFI_SETUP: pwd='%s' pwd_len=%d, len=%d", pwd, pwd_len, len); + + setup_wifi_credentials(ssid, pwd); + result_buffer[0] = 0; // success + break; + + case 0x02: // WIFI_START + LOG_INF("WIFI_START command received"); + if (is_wifi_on()) { + LOG_INF("Wi-Fi already on - wait for next session"); + result_buffer[0] = 5; // wait for next session break; + } + k_work_submit(&wifi_start_work); + result_buffer[0] = 0; + break; + + case 0x03: // WIFI_SHUTDOWN + LOG_INF("WIFI_SHUTDOWN command received"); + storage_stop_transfer(); + wifi_turn_off(); + mic_resume(); + result_buffer[0] = 0; + break; + + default: + LOG_WRN("Unknown WIFI command: %d", cmd); + result_buffer[0] = 0xFF; // unknown command + break; } bt_gatt_notify(conn, &storage_service.attrs[8], &result_buffer, 1); @@ -362,7 +366,7 @@ static void write_to_gatt(struct bt_conn *conn) #ifdef CONFIG_OMI_ENABLE_WIFI static void write_to_tcp() { - + uint32_t to_read = MIN(remaining_length, SD_BLE_SIZE * 10); int ret = read_audio_data(storage_write_buffer, to_read, offset); if (ret > 0) { @@ -386,7 +390,6 @@ static void write_to_tcp() } #endif - void storage_stop_transfer() { remaining_length = 0; diff --git a/omi/firmware/omi/src/lib/core/transport.c b/omi/firmware/omi/src/lib/core/transport.c index a182b19ad2..1cb666bea6 100644 --- a/omi/firmware/omi/src/lib/core/transport.c +++ b/omi/firmware/omi/src/lib/core/transport.c @@ -26,10 +26,10 @@ #ifdef CONFIG_OMI_ENABLE_MONITOR #include "monitor.h" #endif +#include "rtc.h" #include "sd_card.h" #include "settings.h" #include "storage.h" -#include "rtc.h" LOG_MODULE_REGISTER(transport, CONFIG_LOG_DEFAULT_LEVEL); #ifdef CONFIG_OMI_ENABLE_RFSW_CTRL @@ -222,7 +222,7 @@ static ssize_t time_sync_write_handler(struct bt_conn *conn, LOG_INF("Time sync received: %u seconds", epoch_s); - int err = rtc_set_utc_time((uint64_t)epoch_s); + int err = rtc_set_utc_time((uint64_t) epoch_s); if (err) { LOG_ERR("Failed to set RTC time: %d", err); return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); @@ -232,11 +232,8 @@ static ssize_t time_sync_write_handler(struct bt_conn *conn, return len; } -static ssize_t time_sync_read_handler(struct bt_conn *conn, - const struct bt_gatt_attr *attr, - void *buf, - uint16_t len, - uint16_t offset) +static ssize_t +time_sync_read_handler(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) { uint32_t epoch_s = get_utc_time(); LOG_INF("Time sync read: %u seconds", epoch_s); @@ -457,8 +454,8 @@ static void exchange_func(struct bt_conn *conn, uint8_t att_err, struct bt_gatt_ // #ifdef CONFIG_OMI_ENABLE_BATTERY -#define BATTERY_REFRESH_INTERVAL 10000 // 10 seconds -#define CONFIG_OMI_BATTERY_CRITICAL_MV 3500 // mV +#define BATTERY_REFRESH_INTERVAL 10000 // 10 seconds +#define CONFIG_OMI_BATTERY_CRITICAL_MV 3500 // mV uint8_t battery_percentage = 0; void broadcast_battery_level(struct k_work *work_item); @@ -781,11 +778,13 @@ static bool push_to_gatt(struct bt_conn *conn) #define OPUS_PREFIX_LENGTH 1 #define OPUS_PADDED_LENGTH 80 #define MAX_WRITE_SIZE 440 +#define TIMESTAMP_MARKER 0xFF static uint32_t offset = 0; static uint16_t buffer_offset = 0; #ifdef CONFIG_OMI_ENABLE_OFFLINE_STORAGE static uint8_t storage_temp_data[MAX_WRITE_SIZE]; +static bool needs_timestamp_marker = true; bool write_to_storage(void) { uint8_t *buffer = tx_buffer + 2; @@ -821,6 +820,32 @@ bool write_to_storage(void) #endif return true; } + +static void write_timestamp_to_storage(void) +{ + uint32_t now = get_utc_time(); + if (now == 0) { + return; + } + + if (buffer_offset + 5 > MAX_WRITE_SIZE) { + write_to_file(storage_temp_data, MAX_WRITE_SIZE); + buffer_offset = 0; + } + + storage_temp_data[buffer_offset] = TIMESTAMP_MARKER; + memcpy(storage_temp_data + buffer_offset + 1, &now, sizeof(uint32_t)); + buffer_offset += 5; + LOG_INF("Wrote timestamp marker: %u", now); +} + +void storage_flush_buffer(void) +{ + if (buffer_offset > 0) { + write_to_file(storage_temp_data, buffer_offset); + buffer_offset = 0; + } +} #endif static bool use_storage = true; @@ -883,6 +908,7 @@ void pusher(void) if (conn && is_subscribed) { // Push to GATT if connected and subscribed + needs_timestamp_marker = true; push_to_gatt(conn); bt_conn_unref(conn); } else if (!conn) { @@ -890,6 +916,14 @@ void pusher(void) // No BT connection, write to storage if (get_file_size() < MAX_STORAGE_BYTES && is_sd_on()) { storage_full_warned = false; + if (needs_timestamp_marker) { + write_timestamp_to_storage(); + needs_timestamp_marker = false; + } + uint32_t now = get_utc_time(); + if (now > 0) { + set_recording_start_time(now); + } write_to_storage(); } else { if (!storage_full_warned) { @@ -900,7 +934,8 @@ void pusher(void) #endif } else { // Connected but not subscribed, just sleep (buffer will be retried) - if (conn) bt_conn_unref(conn); + if (conn) + bt_conn_unref(conn); k_sleep(K_MSEC(10)); } } diff --git a/omi/firmware/omi/src/lib/core/transport.h b/omi/firmware/omi/src/lib/core/transport.h index 8e2b38410d..d9b4c0ab96 100644 --- a/omi/firmware/omi/src/lib/core/transport.h +++ b/omi/firmware/omi/src/lib/core/transport.h @@ -37,4 +37,9 @@ int broadcast_audio_packets(uint8_t *buffer, size_t size); */ struct bt_conn *get_current_connection(); +/** + * @brief Flush any pending offline storage buffer to SD card + */ +void storage_flush_buffer(void); + #endif // TRANSPORT_H diff --git a/omi/firmware/omi/src/sd_card.c b/omi/firmware/omi/src/sd_card.c index 85b9003317..12aeb1ee7d 100644 --- a/omi/firmware/omi/src/sd_card.c +++ b/omi/firmware/omi/src/sd_card.c @@ -1,6 +1,6 @@ #include "lib/core/sd_card.h" + #include -#include #include #include #include @@ -14,12 +14,12 @@ LOG_MODULE_REGISTER(sd_card, CONFIG_LOG_DEFAULT_LEVEL); -#define DISK_DRIVE_NAME "SD" // Disk drive name -#define DISK_MOUNT_PT "/SD:" // Mount point path -#define SD_REQ_QUEUE_MSGS 25 // Number of messages in the SD request queue -#define SD_FSYNC_THRESHOLD 20000 // Threshold in bytes to trigger fsync -#define WRITE_BATCH_COUNT 10 // Number of writes to batch before writing to SD card -#define ERROR_THRESHOLD 5 // Maximum allowed write errors before taking action +#define DISK_DRIVE_NAME "SD" // Disk drive name +#define DISK_MOUNT_PT "/SD:" // Mount point path +#define SD_REQ_QUEUE_MSGS 25 // Number of messages in the SD request queue +#define SD_FSYNC_THRESHOLD 20000 // Threshold in bytes to trigger fsync +#define WRITE_BATCH_COUNT 10 // Number of writes to batch before writing to SD card +#define ERROR_THRESHOLD 5 // Maximum allowed write errors before taking action // batch write buffer static uint8_t write_batch_buffer[WRITE_BATCH_COUNT * MAX_WRITE_SIZE]; @@ -48,6 +48,7 @@ static bool is_mounted = false; static bool sd_enabled = false; static uint32_t current_file_size = 0; static uint32_t current_file_offset = 0; +static uint32_t recording_start_time = 0; static size_t bytes_since_sync = 0; // Get the device pointer for the SDHC SPI slot from the device tree @@ -186,9 +187,16 @@ static k_tid_t sd_worker_tid = NULL; int app_sd_init(void) { if (!sd_worker_tid) { - sd_worker_tid = k_thread_create(&sd_worker_thread_data, sd_worker_stack, SD_WORKER_STACK_SIZE, - (k_thread_entry_t)sd_worker_thread, NULL, NULL, NULL, - SD_WORKER_PRIORITY, 0, K_NO_WAIT); + sd_worker_tid = k_thread_create(&sd_worker_thread_data, + sd_worker_stack, + SD_WORKER_STACK_SIZE, + (k_thread_entry_t) sd_worker_thread, + NULL, + NULL, + NULL, + SD_WORKER_PRIORITY, + 0, + K_NO_WAIT); k_thread_name_set(sd_worker_tid, "sd_worker"); } return 0; @@ -228,7 +236,6 @@ int read_audio_data(uint8_t *buf, int amount, int offset) return resp.read_bytes; } - uint32_t write_to_file(uint8_t *data, uint32_t length) { sd_req_t req = {0}; @@ -294,6 +301,19 @@ uint32_t get_offset(void) return current_file_offset; } +void set_recording_start_time(uint32_t ts) +{ + if (recording_start_time == 0) { + recording_start_time = ts; + save_offset(current_file_offset); + } +} + +uint32_t get_recording_start_time(void) +{ + return recording_start_time; +} + int app_sd_off(void) { if (is_mounted) { @@ -369,22 +389,29 @@ void sd_worker_thread(void) } if (need_init_offset) { current_file_offset = 0; - uint32_t zero_offset = 0; - ssize_t bw = fs_write(&fil_info, &zero_offset, sizeof(zero_offset)); - if (bw != sizeof(zero_offset)) { - LOG_ERR("[SD_WORK] init info.txt failed to write offset 0: %d\n", (int)bw); + recording_start_time = 0; + uint32_t zero_data[2] = {0, 0}; + ssize_t bw = fs_write(&fil_info, zero_data, sizeof(zero_data)); + if (bw != sizeof(zero_data)) { + LOG_ERR("[SD_WORK] init info.txt failed to write: %d\n", (int) bw); } else { fs_sync(&fil_info); } } else { - /* Read existing offset from info.txt */ + /* Read existing offset (and optional timestamp) from info.txt */ fs_seek(&fil_info, 0, FS_SEEK_SET); - ssize_t rbytes = fs_read(&fil_info, ¤t_file_offset, sizeof(current_file_offset)); - if (rbytes != sizeof(current_file_offset)) { - LOG_ERR("[SD_WORK] Failed to read offset at boot: %d\n", (int)rbytes); - current_file_offset = 0; - } else { + uint32_t info_data[2] = {0, 0}; + ssize_t rbytes = fs_read(&fil_info, info_data, sizeof(info_data)); + if (rbytes >= (ssize_t) sizeof(uint32_t)) { + current_file_offset = info_data[0]; LOG_INF("[SD_WORK] Loaded offset from info.txt: %u\n", current_file_offset); + } else { + LOG_ERR("[SD_WORK] Failed to read offset at boot: %d\n", (int) rbytes); + current_file_offset = 0; + } + if (rbytes >= (ssize_t) sizeof(info_data)) { + recording_start_time = info_data[1]; + LOG_INF("[SD_WORK] Loaded recording start time: %u\n", recording_start_time); } } @@ -396,7 +423,7 @@ void sd_worker_thread(void) if (k_msgq_get(&sd_msgq, &req, K_FOREVER) == 0) { switch (req.type) { case REQ_WRITE_DATA: - LOG_DBG("[SD_WORK] Buffering %u bytes to batch write\n", (unsigned)req.u.write.len); + LOG_DBG("[SD_WORK] Buffering %u bytes to batch write\n", (unsigned) req.u.write.len); memcpy(write_batch_buffer + write_batch_offset, req.u.write.buf, req.u.write.len); write_batch_offset += req.u.write.len; @@ -409,10 +436,13 @@ void sd_worker_thread(void) LOG_ERR("[SD_WORK] seek end before write failed: %d\n", res); } bw = fs_write(&fil_data, write_batch_buffer, write_batch_offset); - if (bw < 0 || (size_t)bw != write_batch_offset) { + if (bw < 0 || (size_t) bw != write_batch_offset) { writing_error_counter++; - LOG_ERR("[SD_WORK] batch write error %d bw=%d wanted=%u\n", (int)bw, (int)bw, (unsigned)write_batch_offset); + LOG_ERR("[SD_WORK] batch write error %d bw=%d wanted=%u\n", + (int) bw, + (int) bw, + (unsigned) write_batch_offset); if (bw > 0) { LOG_INF("Attempting to truncate to correct packet position"); uint32_t truncate_offset = current_file_size + bw - bw % MAX_WRITE_SIZE; @@ -426,7 +456,8 @@ void sd_worker_thread(void) } if (writing_error_counter >= ERROR_THRESHOLD) { - LOG_ERR("[SD_WORK] Too many write errors (%d). Stopping SD worker.\n", writing_error_counter); + LOG_ERR("[SD_WORK] Too many write errors (%d). Stopping SD worker.\n", + writing_error_counter); fs_close(&fil_data); fs_file_t_init(&fil_data); LOG_INF("[SD_WORK] Re-opening data file after too many errors.\n"); @@ -451,7 +482,7 @@ void sd_worker_thread(void) } if (bytes_since_sync >= SD_FSYNC_THRESHOLD) { - LOG_INF("[SD_WORK] fs_sync triggered after %u bytes\n", (unsigned)bytes_since_sync); + LOG_INF("[SD_WORK] fs_sync triggered after %u bytes\n", (unsigned) bytes_since_sync); res = fs_sync(&fil_data); if (res < 0) { LOG_ERR("[SD_WORK] fs_sync data failed: %d\n", res); @@ -463,7 +494,8 @@ void sd_worker_thread(void) case REQ_READ_DATA: LOG_DBG("[SD_WORK] Reading %u bytes from data file at offset %u\n", - (unsigned)req.u.read.length, (unsigned)req.u.read.offset); + (unsigned) req.u.read.length, + (unsigned) req.u.read.offset); if (&fil_data == NULL) { LOG_ERR("[SD_WORK] data file not open (read)\n"); if (req.u.read.resp) { @@ -493,17 +525,22 @@ void sd_worker_thread(void) break; case REQ_SAVE_OFFSET: - LOG_DBG("[SD_WORK] Saving offset %u to info file\n", (unsigned)req.u.info.offset_value); - /* Overwrite info.txt with 4-byte offset value (binary) */ + LOG_DBG("[SD_WORK] Saving offset %u, start_time %u to info file\n", + (unsigned) req.u.info.offset_value, + (unsigned) recording_start_time); + /* Overwrite info.txt with offset + recording start time */ if (&fil_info == NULL) { LOG_ERR("[SD_WORK] info file not open\n"); break; } res = fs_seek(&fil_info, 0, FS_SEEK_SET); if (res == 0) { - bw = fs_write(&fil_info, &req.u.info.offset_value, sizeof(req.u.info.offset_value)); - if (bw < 0 || bw != sizeof(req.u.info.offset_value)) { - LOG_ERR("[SD_WORK] info write err %d\n", (int)bw); + uint32_t info_data[2]; + info_data[0] = req.u.info.offset_value; + info_data[1] = recording_start_time; + bw = fs_write(&fil_info, info_data, sizeof(info_data)); + if (bw < 0 || bw != sizeof(info_data)) { + LOG_ERR("[SD_WORK] info write err %d\n", (int) bw); } else { res = fs_sync(&fil_info); if (res < 0) { @@ -533,6 +570,7 @@ void sd_worker_thread(void) } current_file_size = 0; current_file_offset = 0; + recording_start_time = 0; // Return result to resp if available if (req.u.clear_dir.resp) { req.u.clear_dir.resp->res = 0;