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:
@@ -93,6 +93,13 @@ class SensorViewModel(application: Application) : AndroidViewModel(application)
|
||||
|
||||
private var recordingTimerJob: Job? = null
|
||||
private var recordingSessionId: String = ""
|
||||
|
||||
/**
|
||||
* ID of the CSV session currently being written, or null when not recording. Read by
|
||||
* MainActivity so the trip started at the same moment can store it - see
|
||||
* [com.hawhamburg.micr0bu.data.db.RecordedTripEntity.sessionId] for why that link matters.
|
||||
*/
|
||||
val activeSessionId: String? get() = recordingSessionId.takeIf { _state.value.isRecording }
|
||||
private var csvWriter: BufferedWriter? = null
|
||||
|
||||
private val isoFmt = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.os.PowerManager
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.hawhamburg.micr0bu.data.TripRepository
|
||||
import com.hawhamburg.micr0bu.data.shareTripCsv
|
||||
import com.hawhamburg.micr0bu.data.db.AppDatabase
|
||||
import com.hawhamburg.micr0bu.data.db.DetectedEventEntity
|
||||
import com.hawhamburg.micr0bu.data.db.RecordedTripEntity
|
||||
@@ -27,7 +28,7 @@ import kotlinx.coroutines.launch
|
||||
*/
|
||||
class TripRecordingViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val repository = TripRepository(AppDatabase.getInstance(application))
|
||||
private val repository = TripRepository(AppDatabase.getInstance(application), application)
|
||||
|
||||
// ── Battery optimisation prompt ───────────────────────────────────────────
|
||||
// Shown once on the Recording screen to ask the user to exempt the app so
|
||||
@@ -85,13 +86,21 @@ class TripRecordingViewModel(application: Application) : AndroidViewModel(applic
|
||||
* Starts the foreground recording service if not already running,
|
||||
* or stops it if a trip is already active.
|
||||
*/
|
||||
fun toggleRecording() {
|
||||
if (serviceState.value.isRecording) stopRecording() else startRecording()
|
||||
fun toggleRecording(sessionId: String? = null) {
|
||||
if (serviceState.value.isRecording) stopRecording() else startRecording(sessionId)
|
||||
}
|
||||
|
||||
fun startRecording() {
|
||||
/**
|
||||
* @param sessionId ID of the CSV session recorded alongside this trip, so the trip row can
|
||||
* reference it and deleting the trip can delete the CSV too. Null if CSV recording isn't
|
||||
* running - the trip is still recorded, it just has no CSV to clean up later.
|
||||
*/
|
||||
fun startRecording(sessionId: String? = null) {
|
||||
val intent = Intent(getApplication(), TripRecordingService::class.java)
|
||||
.apply { action = TripRecordingService.ACTION_START }
|
||||
.apply {
|
||||
action = TripRecordingService.ACTION_START
|
||||
putExtra(TripRecordingService.EXTRA_SESSION_ID, sessionId)
|
||||
}
|
||||
getApplication<Application>().startForegroundService(intent)
|
||||
}
|
||||
|
||||
@@ -106,4 +115,26 @@ class TripRecordingViewModel(application: Application) : AndroidViewModel(applic
|
||||
fun deleteTrip(tripId: Long) {
|
||||
viewModelScope.launch { repository.deleteTrip(tripId) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and shares the combined CSV for [tripId] - GPS track, detected events, V2X messages
|
||||
* and the raw sensor samples recorded alongside it. See [buildTripCsv] for the format and why
|
||||
* it's one file rather than several.
|
||||
*
|
||||
* [context] must be the Activity context (`LocalContext.current` from the composable), not
|
||||
* this ViewModel's Application context: the share sheet is launched with `startActivity`, and
|
||||
* doing that from a non-Activity context throws unless FLAG_ACTIVITY_NEW_TASK is set. Same
|
||||
* pattern as SessionLogScreen's share.
|
||||
*/
|
||||
fun exportTrip(context: Context, tripId: Long) {
|
||||
viewModelScope.launch {
|
||||
val trip = repository.getTrip(tripId) ?: return@launch
|
||||
shareTripCsv(
|
||||
context = context,
|
||||
trip = trip,
|
||||
events = repository.getEventsForTripOnce(tripId),
|
||||
v2xMessages = repository.getV2xMessagesForTripOnce(tripId),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user