ESP32 link indicators, trip export with V2X + RSSI, trip/CSV cleanup

UI:
- Top bar shows a USB glyph reflecting the serial link on the ESP32-C5 path,
  instead of a Wi-Fi glyph driven by an MQTT state that is permanently
  disconnected there.
- Recording screen's OBU stream row follows whichever transport the selected
  hardware uses. It previously read as offline throughout a recording that was
  actively beaconing CAMs.
- Live map uses distinct markers: a centred dot for own position (a fact about
  the viewer, not a tracked object) and a teardrop pin for remote stations,
  tinted by alert severity. Both were osmdroid's identical default pin before,
  and severity required tapping a marker to read its label.
- Sensor Monitor moves out of the bottom nav to Settings > Developer. A live
  phone-sensor feed is a bench tool; the Dashboard already reports GNSS/IMU
  health. Screen and route are unchanged, just not in the rider's way.

Detection engine on the ESP32 path:
- Seed our own StationID from the persisted value CamTransmitLoop transmits.
  It was null here (the CiT One learns it from v2x/rx/obu_gnss, which doesn't
  exist on this path), so the self-heard-TX filter never fired: the ESP32 runs
  promiscuous for raw TX to work at all, hears our own CAMs back off the air,
  and they were tracked as a remote station - a ghost vehicle on top of the ego
  position, fed to the engine as a collision partner for itself.

RSSI:
- The firmware has always sent per-frame RSSI in byte 0 of every CAM_RX frame;
  the app discarded it. Now carried on Cam, persisted per V2X message, shown per
  station in the received-CAM list, and exported. Null for own CAMs and the
  whole CiT One path, neither of which has a measurement.

Trips, CSV and export (schema v3 -> v4):
- trips.sessionId links a trip to the CSV session recorded alongside it. The two
  are written by independent subsystems that the Recording button happens to
  start together; without the link, deleting a trip orphaned its CSV forever.
  Timestamp matching was rejected - close recordings would delete the wrong file.
- Deleting a trip now removes the CSV file AND its sessions row, so it stops
  appearing in the Session Log pointing at nothing.
- Per-trip combined CSV export: GPS track, detected events, V2X messages (with
  RSSI) and the raw sensor samples in one file, keyed by a leading type column.
  One file rather than a zip of tables because the point of the export is
  correlating those streams, and splitting them pushes the join downstream.
- Export takes the Activity context. Sharing from the ViewModel's Application
  context threw AndroidRuntimeException on startActivity - this crashed on the
  first tap of the share button.

Trips recorded before v4 have a null sessionId, so their exports omit raw sensor
rows and their CSVs still need clearing by hand once.
This commit is contained in:
Ashin Walpola
2026-08-11 15:47:54 +02:00
parent f507a8a9fd
commit f1770e11dd
22 changed files with 496 additions and 34 deletions
@@ -28,6 +28,9 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState
import com.hawhamburg.micr0bu.data.transport.UsbSerialState
import com.hawhamburg.micr0bu.data.transport.ObuHardware
import com.hawhamburg.micr0bu.ui.components.StatusTopBar
import com.hawhamburg.micr0bu.ui.navigation.BottomNavBar
import com.hawhamburg.micr0bu.ui.navigation.Screen
@@ -122,7 +125,14 @@ class MainActivity : AppCompatActivity() {
}
Scaffold(
topBar = { StatusTopBar(state, mqttConnectionState) },
topBar = {
StatusTopBar(
state = state,
mqttConnectionState = mqttConnectionState,
isEsp32 = obuHardware == ObuHardware.ESP32_C5,
usbSerialState = usbSerialState,
)
},
bottomBar = { BottomNavBar(navController) },
) { innerPadding ->
NavHost(
@@ -165,12 +175,19 @@ class MainActivity : AppCompatActivity() {
RecordingScreen(
state = state,
mqttConnectionState = mqttConnectionState,
obuConnected = if (obuHardware == ObuHardware.ESP32_C5)
usbSerialState == UsbSerialState.CONNECTED
else
mqttConnectionState == MqttConnectionState.CONNECTED,
tripServiceState = tripServiceState,
showBatteryOptPrompt = showBatteryOptPrompt,
onToggleRecording = {
// Start / stop both CSV recording and the event detection service
// Start / stop both CSV recording and the event detection
// service. Order matters on start: the CSV session must exist
// before the trip row is written, so the trip can store its
// id and later delete the CSV along with itself.
viewModel.toggleRecording()
tripViewModel.toggleRecording()
tripViewModel.toggleRecording(viewModel.activeSessionId)
},
onOpenSessionLog = {
navController.navigate(Screen.Log.route)
@@ -294,6 +311,7 @@ class MainActivity : AppCompatActivity() {
DeveloperSettingsScreen(
state = state,
onDeveloperMode = viewModel::setDeveloperMode,
onOpenSensorMonitor = { navController.navigate(Screen.Sensors.route) },
onBack = { navController.popBackStack() },
)
}