diff --git a/README.md b/README.md index 743cdc7..70bd59e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Both paths converge at `CamUseCaseRepository`, which normalises whatever arrived **DENM transmission**; CiT One path only. Triggers the stationary vehicle profile (`hln-sv`, causeCode 94) via the consider it Use Case API. This is a manual antenna and range test tool. It is never triggered by a detected event or a use case alert, and the control is hidden entirely on the ESP32-C5 path. -**Trip recording**; foreground service records all sensor streams and detects cycling manoeuvres (braking, turning, stopping) using orientation-independent signal processing. Works fully offline with no OBU connected. The detected manoeuvres are not shown in the app - they raise the CAM transmit rate through the manoeuvre on the ESP32-C5 path, and are kept in the trip's CSV export for offline analysis. +**Trip recording**; foreground service records all sensor streams and detects cycling manoeuvres (braking, turning, stopping) using orientation-independent signal processing. Works fully offline with no OBU connected. The detected manoeuvres are neither shown nor stored - their only effect is to raise the CAM transmit rate through the manoeuvre on the ESP32-C5 path. **Trip review**; past trips displayed as a route on an OpenStreetMap layer, with duration and distance. diff --git a/app/src/main/java/com/hawhamburg/micr0bu/data/TripExporter.kt b/app/src/main/java/com/hawhamburg/micr0bu/data/TripExporter.kt index dd055ca..4b03167 100644 --- a/app/src/main/java/com/hawhamburg/micr0bu/data/TripExporter.kt +++ b/app/src/main/java/com/hawhamburg/micr0bu/data/TripExporter.kt @@ -3,7 +3,6 @@ package com.hawhamburg.micr0bu.data import android.content.Context import android.content.Intent import androidx.core.content.FileProvider -import com.hawhamburg.micr0bu.data.db.DetectedEventEntity import com.hawhamburg.micr0bu.data.db.RecordedTripEntity import com.hawhamburg.micr0bu.data.db.V2xMessageEntity import kotlinx.coroutines.Dispatchers @@ -23,12 +22,15 @@ fun tripFileName(trip: RecordedTripEntity): String = /** * Builds a single combined CSV for one trip: the raw sensor samples recorded alongside it, the - * events the detector fired, the GPS track, and every V2X message seen during the ride — all in - * one file, ordered by time. + * GPS track, and every V2X message seen during the ride — all in one file, ordered by time. + * + * Detected manoeuvres are deliberately absent. The detector exists to raise the CAM transmit + * rate (see EventDetector's KDoc); its output is not retained, so there is nothing to export + * beyond the per-trip count in the header. * * **Why one file rather than a zip of tables.** The point of the export is correlation: what was - * the bike doing when that CAM arrived, what did the detector make of it. Splitting those into - * separate files pushes the join onto whoever opens it. A leading `type` column keeps the rows + * the bike doing when that CAM arrived. Splitting those into separate files pushes the join + * onto whoever opens it. A leading `type` column keeps the rows * distinguishable, which is the same shape the existing session CSV already uses, so the two * remain readable by the same tooling. * @@ -44,7 +46,6 @@ fun tripFileName(trip: RecordedTripEntity): String = suspend fun buildTripCsv( context: Context, trip: RecordedTripEntity, - events: List, v2xMessages: List, ): String = withContext(Dispatchers.IO) { buildString { @@ -59,7 +60,6 @@ suspend fun buildTripCsv( appendLine() appendLine( "type,timestamp_ms,timestamp_iso,lat,lon,speed_ms,heading_deg," + - "event_type,confidence,peak_accel,peak_gyro,duration_ms," + "station_id,station_type,is_own,yaw_rate_dps,rssi_dbm" ) @@ -68,16 +68,6 @@ suspend fun buildTripCsv( appendLine( "gps,${point.timestamp},${isoUtc.format(Date(point.timestamp))}," + "${point.lat},${point.lon},,," + - ",,,,," + - ",,," - ) - } - - for (e in events) { - appendLine( - "event,${e.timestamp},${isoUtc.format(Date(e.timestamp))}," + - "${e.latitude},${e.longitude},${e.speedMps},," + - "${e.type},${e.confidence},${e.peakAccelMagnitude},${e.peakGyroMagnitude},${e.durationMs}," + ",,,," ) } @@ -86,14 +76,13 @@ suspend fun buildTripCsv( appendLine( "v2x,${m.timestamp},${isoUtc.format(Date(m.timestamp))}," + "${m.latitude},${m.longitude},${m.speedMps},${m.headingDeg}," + - ",,,,," + "${m.stationId},${m.stationType},${m.isOwn},${m.yawRateDps ?: ""},${m.rssiDbm ?: ""}" ) } // Raw sensor samples, copied verbatim from the session CSV. Appended last rather than // merge-sorted in: a long ride is hundreds of thousands of rows, and sorting them against - // the (comparatively tiny) event/V2X sets in memory would defeat the streaming that + // the (comparatively tiny) V2X set in memory would defeat the streaming that // CsvExporter deliberately does. Each row carries its own timestamp, so sort on load. val sessionCsv = trip.sessionId?.let { File(File(context.filesDir, "sessions"), "$it.csv") } if (sessionCsv != null && sessionCsv.exists()) { @@ -110,12 +99,11 @@ suspend fun buildTripCsv( suspend fun shareTripCsv( context: Context, trip: RecordedTripEntity, - events: List, v2xMessages: List, ) { val fileName = tripFileName(trip) val cacheFile = File(context.cacheDir, fileName) - val csv = buildTripCsv(context, trip, events, v2xMessages) + val csv = buildTripCsv(context, trip, v2xMessages) withContext(Dispatchers.IO) { cacheFile.writeText(csv) } 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 05fcf25..6ab7226 100644 --- a/app/src/main/java/com/hawhamburg/micr0bu/data/TripRepository.kt +++ b/app/src/main/java/com/hawhamburg/micr0bu/data/TripRepository.kt @@ -3,11 +3,9 @@ package com.hawhamburg.micr0bu.data import android.content.Context import android.util.Log 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 import kotlinx.coroutines.flow.first import java.io.File @@ -15,7 +13,7 @@ import java.io.File private const val TAG = "TripRepository" /** - * Repository that abstracts Room access for trips and detected events. + * Repository that abstracts Room access for trips and V2X messages. * * All suspend functions are safe to call from a coroutine running on any * dispatcher; Room executes the actual SQL on its own I/O thread pool. @@ -81,14 +79,11 @@ class TripRepository(db: AppDatabase, private val context: Context) { * One-shot snapshots for export. The Flow-returning variants above stay observable for the UI; * an export wants a value it can write out, not a stream it has to unsubscribe from. */ - suspend fun getEventsForTripOnce(tripId: Long): List = - dao.getEventsForTrip(tripId).first() - suspend fun getV2xMessagesForTripOnce(tripId: Long): List = dao.getV2xMessagesForTrip(tripId).first() /** - * Deletes a trip and everything belonging to it: detected events and V2X messages go via the + * Deletes a trip and everything belonging to it: V2X messages go via the * schema's CASCADE foreign keys, and the CSV recorded alongside it is removed here. * * The CSV is a plain file outside the database, so nothing deletes it implicitly - before @@ -109,32 +104,6 @@ class TripRepository(db: AppDatabase, private val context: Context) { } } - // ── Events ──────────────────────────────────────────────────────────────── - - /** - * Persists a domain [DetectedEvent] for the given [tripId]. - * Converts the domain model to the Room entity. - */ - suspend fun insertEvent(tripId: Long, event: DetectedEvent) = - dao.insertEvent( - DetectedEventEntity( - tripId = tripId, - timestamp = event.timestamp, - type = event.type.name, - confidence = event.confidence.name, - latitude = event.latitude, - longitude = event.longitude, - speedMps = event.speedMps.toFloat(), - peakAccelMagnitude = event.peakAccelMagnitude.toFloat(), - peakGyroMagnitude = event.peakGyroMagnitude.toFloat(), - durationMs = event.durationMs, - ) - ) - - /** 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 diff --git a/app/src/main/java/com/hawhamburg/micr0bu/data/db/AppDatabase.kt b/app/src/main/java/com/hawhamburg/micr0bu/data/db/AppDatabase.kt index af5f86c..7591ded 100644 --- a/app/src/main/java/com/hawhamburg/micr0bu/data/db/AppDatabase.kt +++ b/app/src/main/java/com/hawhamburg/micr0bu/data/db/AppDatabase.kt @@ -11,10 +11,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase entities = [ SessionEntity::class, RecordedTripEntity::class, - DetectedEventEntity::class, V2xMessageEntity::class, ], - version = 4, + version = 5, exportSchema = false, ) abstract class AppDatabase : RoomDatabase() { @@ -34,13 +33,29 @@ abstract class AppDatabase : RoomDatabase() { AppDatabase::class.java, "micr0bu.db", ) - .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) .build() .also { INSTANCE = it } } // ── Migrations ──────────────────────────────────────────────────────── + /** + * Drops `detected_events`. The cyclist event detector still runs, but its output is now + * consumed only by the CAM transmit-rate policy (see EventDetector's KDoc) and is no + * longer persisted, displayed, or exported, so the table had no reader left. + * + * `trips.eventCount` is deliberately kept. Dropping a column means recreating `trips` + * and copying every recorded ride across, which is real risk for one unused integer; + * the service still writes an accurate count into it and the CSV header still reports it. + */ + private val MIGRATION_4_5 = object : Migration(4, 5) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP INDEX IF EXISTS `index_detected_events_tripId`") + database.execSQL("DROP TABLE IF EXISTS `detected_events`") + } + } + /** * Two additions: * - `trips.sessionId` links a trip to the CSV recording session captured alongside it, so diff --git a/app/src/main/java/com/hawhamburg/micr0bu/data/db/DetectedEventEntity.kt b/app/src/main/java/com/hawhamburg/micr0bu/data/db/DetectedEventEntity.kt deleted file mode 100644 index 49bbfcc..0000000 --- a/app/src/main/java/com/hawhamburg/micr0bu/data/db/DetectedEventEntity.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.hawhamburg.micr0bu.data.db - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.PrimaryKey - -/** - * One detected cyclist event (braking / turning / stopping) linked to a - * [RecordedTripEntity] via the [tripId] foreign key. - * - * [type] and [confidence] are stored as the enum name strings so that the - * database remains human-readable. - */ -@Entity( - tableName = "detected_events", - foreignKeys = [ - ForeignKey( - entity = RecordedTripEntity::class, - parentColumns = ["id"], - childColumns = ["tripId"], - onDelete = ForeignKey.CASCADE, - ) - ], -) -data class DetectedEventEntity( - @PrimaryKey(autoGenerate = true) - val id: Long = 0, - - @ColumnInfo(index = true) - val tripId: Long, - - /** Wall-clock epoch ms of the first qualifying sensor frame. */ - val timestamp: Long, - - /** EventType.name — one of BRAKING, TURNING, STOPPING. */ - val type: String, - - /** Confidence.name — one of HIGH, MEDIUM, LOW. */ - val confidence: String, - - val latitude: Double, - val longitude: Double, - val speedMps: Float, - val peakAccelMagnitude: Float, - val peakGyroMagnitude: Float, - - /** Duration from first qualifying frame to emission (ms). */ - val durationMs: Long, -) diff --git a/app/src/main/java/com/hawhamburg/micr0bu/data/db/TripDao.kt b/app/src/main/java/com/hawhamburg/micr0bu/data/db/TripDao.kt index 6021a78..ba6ca84 100644 --- a/app/src/main/java/com/hawhamburg/micr0bu/data/db/TripDao.kt +++ b/app/src/main/java/com/hawhamburg/micr0bu/data/db/TripDao.kt @@ -27,17 +27,6 @@ interface TripDao { @Query("DELETE FROM trips WHERE id = :id") suspend fun deleteTripById(id: Long) - // ── Events ──────────────────────────────────────────────────────────────── - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertEvent(event: DetectedEventEntity) - - @Query("SELECT * FROM detected_events WHERE tripId = :tripId ORDER BY timestamp ASC") - fun getEventsForTrip(tripId: Long): Flow> - - @Query("SELECT COUNT(*) FROM detected_events WHERE tripId = :tripId") - suspend fun getEventCountForTrip(tripId: Long): Int - // ── V2X messages (Phase 03) ────────────────────────────────────────────────── @Insert(onConflict = OnConflictStrategy.REPLACE) diff --git a/app/src/main/java/com/hawhamburg/micr0bu/service/TripRecordingService.kt b/app/src/main/java/com/hawhamburg/micr0bu/service/TripRecordingService.kt index 349e52d..6925406 100644 --- a/app/src/main/java/com/hawhamburg/micr0bu/service/TripRecordingService.kt +++ b/app/src/main/java/com/hawhamburg/micr0bu/service/TripRecordingService.kt @@ -142,9 +142,10 @@ class TripRecordingService : Service() { private val gpsTrackBuilder = StringBuilder("[") private var gpsPointCount = 0 - // Number of detected events in this trip. Kept only to populate the trips.eventCount - // column; the per-type counts are no longer surfaced anywhere (see EventDetector's KDoc - // for why the detector still runs). + // Number of manoeuvres the detector fired during this trip. The only thing kept about + // them: it fills the trips.eventCount column, which predates this change and cannot be + // dropped without rebuilding the trips table. See EventDetector's KDoc for why the + // detector still runs at all. private var detectedEventCount = 0 // ── SensorEventListener ─────────────────────────────────────────────────── @@ -265,13 +266,11 @@ class TripRecordingService : Service() { } } - // Collect detector events. The detector's primary consumer is the CAM transmit rate - // policy below; persistence is retained so a recorded ride can still be analysed - // offline, but nothing in the UI reads it. + // Collect detector events. The CAM transmit-rate policy is their only consumer: + // detected manoeuvres are not persisted, exported, or displayed. serviceScope.launch { - detector.events.collect { event -> + detector.events.collect { _ -> if (currentTripId < 0) return@collect - repository.insertEvent(currentTripId, event) // Bump the CAM transmit rate through the maneuver, not just at detection instant. // No-op on the CiT One path (see CamTransmitLoop's KDoc). camTransmitLoop.onDetectedEvent() diff --git a/app/src/main/java/com/hawhamburg/micr0bu/viewmodel/TripRecordingViewModel.kt b/app/src/main/java/com/hawhamburg/micr0bu/viewmodel/TripRecordingViewModel.kt index c68ad19..1553e4c 100644 --- a/app/src/main/java/com/hawhamburg/micr0bu/viewmodel/TripRecordingViewModel.kt +++ b/app/src/main/java/com/hawhamburg/micr0bu/viewmodel/TripRecordingViewModel.kt @@ -117,7 +117,6 @@ class TripRecordingViewModel(application: Application) : AndroidViewModel(applic shareTripCsv( context = context, trip = trip, - events = repository.getEventsForTripOnce(tripId), v2xMessages = repository.getV2xMessagesForTripOnce(tripId), ) } diff --git a/docs/01-requirements-traceability.md b/docs/01-requirements-traceability.md index 156d23f..576906d 100644 --- a/docs/01-requirements-traceability.md +++ b/docs/01-requirements-traceability.md @@ -139,7 +139,7 @@ share no code. The requirement is satisfied twice, by different means. | 11.1 | Orientation-Independent Sensor Strategy | **Done** | `SensorRepository.kt` (magnitude-based) | — | | 11.2 | Running Standard Deviation Event Detector | **Done** | `EventDetector.kt`, `RunningStats.kt` | **18 unit tests, 0 failures** | | 11.3 | Trip Recording Architecture | **Done** | `TripRepository.kt`, `TripRecordingService.kt` *(cited)* | — | -| 11.4 | Data Model | **Done** | `data/db/` Room entities *(cited)* | — | +| 11.4 | Data Model | **Partial — scope reduced** | `data/db/` Room entities *(cited)* | `detected_events` dropped in schema v5, see scope note | | 11.5 | New UI Elements for Phase A | **Partial — scope reduced** | `TripHistoryScreen.kt`, `TripReviewScreen.kt` | event pins/counters removed by decision, see note | | 11.6 | Phase A Success Criteria | **Partial** | — | needs a real ride; see Open Items | @@ -154,7 +154,10 @@ the coloured event pins and detail sheet on the trip review map, and the event c history card — was removed deliberately. A count of the rider's own braking events is not a goal of this project. The detector itself still runs: it is the input to the CAM transmit-rate policy (§ 13), which raises the beacon rate from 1 Hz to the elevated rate for five seconds after a -detected manoeuvre. Events remain persisted and exported to CSV for offline analysis. +detected manoeuvre. That is now its only effect: the `detected_events` table was dropped in schema +v5 and the per-event rows removed from the trip CSV, so a detected manoeuvre is consumed and +discarded. `trips.eventCount` is kept as a single integer per ride, since dropping a SQLite column +means recreating the table. **Defect note (11.2).** Two defects found while documenting the detector were fixed on 2026-09-07. The nine threshold overrides in `TripRecordingService`'s constructor were promoted to diff --git a/docs/MicrOBU-Technical-Documentation.docx b/docs/MicrOBU-Technical-Documentation.docx index d4e8b14..db8c77f 100644 Binary files a/docs/MicrOBU-Technical-Documentation.docx and b/docs/MicrOBU-Technical-Documentation.docx differ diff --git a/docs/MicrOBU-User-Guide.docx b/docs/MicrOBU-User-Guide.docx index 0a715ba..938b515 100644 Binary files a/docs/MicrOBU-User-Guide.docx and b/docs/MicrOBU-User-Guide.docx differ