diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..f6cc838 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,5 +2,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2X_MicrOBU_Android_App_Requirements_v10.docx b/V2X_MicrOBU_Android_App_Requirements_v10.docx
deleted file mode 100644
index b2aa5dd..0000000
Binary files a/V2X_MicrOBU_Android_App_Requirements_v10.docx and /dev/null differ
diff --git a/V2X_MicrOBU_Android_App_Requirements_v15.pdf b/V2X_MicrOBU_Android_App_Requirements_v15.pdf
new file mode 100644
index 0000000..1de3384
Binary files /dev/null and b/V2X_MicrOBU_Android_App_Requirements_v15.pdf differ
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9a3ad3f..3bb7d68 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -73,6 +73,8 @@ dependencies {
implementation(libs.appcompat)
implementation(libs.osmdroid)
implementation(libs.androidx.core.splashscreen)
+ // USB-serial link to the ESP32-C5 (Phase 03) - CDC-ACM/UART driver over Android's USB Host API.
+ implementation(libs.usb.serial.android)
debugImplementation(libs.compose.ui.tooling)
diff --git a/app/src/main/java/com/hawhamburg/micr0bu/MainActivity.kt b/app/src/main/java/com/hawhamburg/micr0bu/MainActivity.kt
index 13ff90c..0058ecb 100644
--- a/app/src/main/java/com/hawhamburg/micr0bu/MainActivity.kt
+++ b/app/src/main/java/com/hawhamburg/micr0bu/MainActivity.kt
@@ -82,6 +82,7 @@ class MainActivity : AppCompatActivity() {
val tripServiceState by tripViewModel.serviceState.collectAsState()
val showBatteryOptPrompt by tripViewModel.showBatteryOptPrompt.collectAsState()
val useCaseEnabledMap by mqttViewModel.useCaseEnabledMap.collectAsState()
+ val obuHardware by mqttViewModel.obuHardware.collectAsState()
MicrOBUTheme(darkTheme = state.darkTheme) {
val view = LocalView.current
@@ -133,6 +134,7 @@ class MainActivity : AppCompatActivity() {
state = state,
mqttConnectionState = mqttConnectionState,
activeTransport = activeTransport,
+ obuHardware = obuHardware,
usbCableConnected = usbConnected,
obuStationTypeWarning = obuStationTypeWarning,
obuStationType = obuStationType,
@@ -145,6 +147,13 @@ class MainActivity : AppCompatActivity() {
}
},
onNavigateToMap = { navController.navigate(Screen.Map.route) },
+ onNavigateToRecord = {
+ navController.navigate(Screen.Record.route) {
+ popUpTo(Screen.Dashboard.route) { saveState = true }
+ launchSingleTop = true
+ restoreState = true
+ }
+ },
)
}
composable(Screen.Sensors.route) {
@@ -239,6 +248,8 @@ class MainActivity : AppCompatActivity() {
ConnectionSettingsScreen(
mqttPrefs = mqttPrefs,
onMqttPrefsChange = mqttViewModel::updatePrefs,
+ obuHardware = obuHardware,
+ onObuHardwareChange = mqttViewModel::setObuHardware,
onBack = { navController.popBackStack() },
)
}
diff --git a/app/src/main/java/com/hawhamburg/micr0bu/data/TripRepository.kt b/app/src/main/java/com/hawhamburg/micr0bu/data/TripRepository.kt
index c4106bf..31db081 100644
--- a/app/src/main/java/com/hawhamburg/micr0bu/data/TripRepository.kt
+++ b/app/src/main/java/com/hawhamburg/micr0bu/data/TripRepository.kt
@@ -3,6 +3,8 @@ package com.hawhamburg.micr0bu.data
import com.hawhamburg.micr0bu.data.db.AppDatabase
import com.hawhamburg.micr0bu.data.db.DetectedEventEntity
import com.hawhamburg.micr0bu.data.db.RecordedTripEntity
+import com.hawhamburg.micr0bu.data.db.V2xMessageEntity
+import com.hawhamburg.micr0bu.domain.cam.Cam
import com.hawhamburg.micr0bu.domain.detection.DetectedEvent
import kotlinx.coroutines.flow.Flow
@@ -90,4 +92,30 @@ class TripRepository(db: AppDatabase) {
/** Emits events for [tripId] ordered by timestamp, updating whenever the DB changes. */
fun getEventsForTrip(tripId: Long): Flow> =
dao.getEventsForTrip(tripId)
+
+ // ── V2X messages (Phase 03) ──────────────────────────────────────────────────
+ // Retention policy: only ever called while a trip is actively recording — see
+ // V2xMessageEntity's KDoc and CamUseCaseRepository.processedCam's collector in
+ // TripRecordingService, which is the only caller.
+
+ /** Persists a domain [Cam] (own or remote) for the given [tripId]. */
+ suspend fun insertV2xMessage(tripId: Long, cam: Cam) =
+ dao.insertV2xMessage(
+ V2xMessageEntity(
+ tripId = tripId,
+ timestamp = cam.timestamp,
+ isOwn = cam.isOwn,
+ stationId = cam.stationId,
+ stationType = cam.stationType,
+ latitude = cam.latitude,
+ longitude = cam.longitude,
+ speedMps = cam.speedMps,
+ headingDeg = cam.headingDeg,
+ yawRateDps = cam.yawRateDps,
+ )
+ )
+
+ /** Emits V2X messages for [tripId] ordered by timestamp, updating whenever the DB changes. */
+ fun getV2xMessagesForTrip(tripId: Long): Flow> =
+ dao.getV2xMessagesForTrip(tripId)
}
diff --git a/app/src/main/java/com/hawhamburg/micr0bu/data/cam/CamUseCaseRepository.kt b/app/src/main/java/com/hawhamburg/micr0bu/data/cam/CamUseCaseRepository.kt
index 5a5d8ec..49b00a1 100644
--- a/app/src/main/java/com/hawhamburg/micr0bu/data/cam/CamUseCaseRepository.kt
+++ b/app/src/main/java/com/hawhamburg/micr0bu/data/cam/CamUseCaseRepository.kt
@@ -6,6 +6,9 @@ import com.hawhamburg.micr0bu.data.SensorRepository
import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState
import com.hawhamburg.micr0bu.data.mqtt.MqttRepository
import com.hawhamburg.micr0bu.data.mqtt.UseCaseAlertPreferences
+import com.hawhamburg.micr0bu.data.transport.SerialFrameType
+import com.hawhamburg.micr0bu.data.transport.UsbSerialTransport
+import com.hawhamburg.micr0bu.domain.asn1.RealAsn1UperCodec
import com.hawhamburg.micr0bu.domain.cam.Cam
import com.hawhamburg.micr0bu.domain.cam.CamParser
import com.hawhamburg.micr0bu.domain.cam.ObuGnssParser
@@ -18,9 +21,12 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
@@ -61,6 +67,8 @@ private const val OBU_GNSS_STALE_MS = 2_500L
class CamUseCaseRepository @Inject constructor(
private val mqttRepository: MqttRepository,
private val prefs: UseCaseAlertPreferences,
+ private val usbSerialTransport: UsbSerialTransport,
+ private val camCodec: RealAsn1UperCodec,
@ApplicationContext private val context: Context,
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@@ -87,6 +95,23 @@ class CamUseCaseRepository @Inject constructor(
alerts.filter { enabled[it.useCase] != false }
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
+ /** Ego bike's latest known position/state, for the V2X Monitor live map view (Section 13). */
+ val ownPosition: StateFlow = engine.ownPosition
+
+ /** Each tracked remote road user's latest known CAM, for the live map view (Section 13). */
+ val remotePositions: StateFlow