Phase 03: real CAM UPER codec + ESP32-C5 TX/RX serial link
- Firmware: rewrite obu-firmware TX loop to be serial-driven (no on-chip timer), add promiscuous RX + GeoNetworking/BTP unwrap (gn_unwrap.c), add binary UART framing to the phone (serial_link.c/.h). Drop local cam_encode() - CAM is now built on the phone. - Kotlin: byte-exact UPER CAM encoder/decoder ported from cam.c (BitWriter/BitReader/CamUperCodec), matching SerialFrame codec, real UsbSerialTransport (usb-serial-for-android), CamTransmitLoop (1Hz base rate, event/geofence boost, ESP32-C5-only), wired into CamUseCaseRepository for RX and TripRecordingService for TX. - Add V2X message retention: persist all CAM (own+remote) to Room while recording, drop otherwise (DB v2 -> v3 migration). - Add jitpack repo + usb-serial-for-android dependency. Fixes: UsbSerialTransport now uses SerialInputOutputManager.start()/stop() (this lib version manages its own thread internally) instead of manual Runnable/Thread submission, which didn't compile.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package com.hawhamburg.micr0bu.service
|
||||
|
||||
import android.content.Context
|
||||
import com.hawhamburg.micr0bu.data.GnssReading
|
||||
import com.hawhamburg.micr0bu.data.SensorRepository
|
||||
import com.hawhamburg.micr0bu.data.mqtt.ObuHardwarePreferences
|
||||
import com.hawhamburg.micr0bu.data.transport.ObuHardware
|
||||
import com.hawhamburg.micr0bu.data.transport.UsbSerialTransport
|
||||
import com.hawhamburg.micr0bu.domain.asn1.RealAsn1UperCodec
|
||||
import com.hawhamburg.micr0bu.domain.cam.CamTransmitConfig
|
||||
import com.hawhamburg.micr0bu.domain.cam.PhoneCamBuilder
|
||||
import com.hawhamburg.micr0bu.domain.usecase.GeoMath
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Real CAM transmit loop for the ESP32-C5 hardware path (Phase 03, Section 13) — the phone-side
|
||||
* counterpart to the OBU's old autonomous beacon, now driven from here since the ESP32-C5 has
|
||||
* no onboard CAM generation at all (see `obu-firmware/main/main.c`'s rewritten TX path, which
|
||||
* is purely receive-and-transmit-on-serial-arrival with no timer of its own).
|
||||
*
|
||||
* Started/stopped by [TripRecordingService] around an active recording session — per the
|
||||
* Section 13 spec, CAM transmission only runs while recording, matching the CiT One path's
|
||||
* behavior of "no traffic until there's a trip to correlate it with." Internally also gated on
|
||||
* [ObuHardwarePreferences] currently reporting [ObuHardware.ESP32_C5] — on the CiT One path
|
||||
* this loop stays parked (via [kotlinx.coroutines.flow.collectLatest] on the hardware
|
||||
* preference) and never sends anything.
|
||||
*
|
||||
* Rate policy: [CamTransmitConfig.baseRateHz] (1 Hz) everywhere, bumped to
|
||||
* [CamTransmitConfig.elevatedRateHz] inside a [com.hawhamburg.micr0bu.domain.cam.CamGeofence] or
|
||||
* for [ELEVATED_HOLD_MS] after an external event trigger (harsh braking/turning/stopping — see
|
||||
* [onDetectedEvent], called by [TripRecordingService] from the same
|
||||
* [com.hawhamburg.micr0bu.domain.detection.EventDetector] stream that already drives trip event
|
||||
* logging). Both rate figures are placeholders pending real-world tuning, per
|
||||
* [CamTransmitConfig]'s own disclaimer.
|
||||
*/
|
||||
@Singleton
|
||||
class CamTransmitLoop @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val obuHardwarePrefs: ObuHardwarePreferences,
|
||||
private val usbSerialTransport: UsbSerialTransport,
|
||||
private val codec: RealAsn1UperCodec,
|
||||
) {
|
||||
private val config = CamTransmitConfig()
|
||||
private val sensorRepository = SensorRepository(context)
|
||||
|
||||
private var job: Job? = null
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
@Volatile private var latestGnss: GnssReading? = null
|
||||
@Volatile private var latestGyroZ: Float? = null
|
||||
@Volatile private var elevatedUntilMs: Long = 0L
|
||||
|
||||
/** Own station id for the ESP32-C5 path — see [PhoneCamBuilder]'s KDoc on why this is a placeholder. */
|
||||
@Volatile var stationId: Long = 0L
|
||||
|
||||
/**
|
||||
* Call when a braking/turning/stopping event fires during an active trip — bumps the CAM
|
||||
* rate to [CamTransmitConfig.elevatedRateHz] for [ELEVATED_HOLD_MS] so nearby stations get
|
||||
* denser updates through the maneuver, not just at the instant it was detected.
|
||||
*/
|
||||
fun onDetectedEvent() {
|
||||
elevatedUntilMs = System.currentTimeMillis() + ELEVATED_HOLD_MS
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the loop for the duration of a recording session. Internally stays idle (no
|
||||
* transmission) unless/until the ESP32-C5 is the selected OBU hardware, and automatically
|
||||
* pauses/resumes if that selection changes mid-trip.
|
||||
*/
|
||||
fun start() {
|
||||
if (job?.isActive == true) return
|
||||
elevatedUntilMs = 0L
|
||||
job = scope.launch {
|
||||
obuHardwarePrefs.obuHardwareFlow.collectLatest { hardware ->
|
||||
if (hardware != ObuHardware.ESP32_C5) return@collectLatest
|
||||
runTransmitLoop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
job?.cancel()
|
||||
job = null
|
||||
elevatedUntilMs = 0L
|
||||
}
|
||||
|
||||
private suspend fun runTransmitLoop() = coroutineScope {
|
||||
launch { sensorRepository.gnssFlow().collect { latestGnss = it } }
|
||||
launch { sensorRepository.gyroscopeFlow().collect { latestGyroZ = it.z } }
|
||||
|
||||
while (true) {
|
||||
val gnss = latestGnss
|
||||
if (gnss != null) {
|
||||
val cam = PhoneCamBuilder.build(gnss, latestGyroZ, stationId)
|
||||
val bytes = codec.encodeCam(cam)
|
||||
usbSerialTransport.sendCamTx(bytes)
|
||||
}
|
||||
delay((1000.0 / currentRateHz(gnss)).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentRateHz(gnss: GnssReading?): Double {
|
||||
val now = System.currentTimeMillis()
|
||||
val inGeofence = gnss != null && config.geofences.any { fence ->
|
||||
GeoMath.haversineMeters(gnss.latitude, gnss.longitude, fence.latitude, fence.longitude) <= fence.radiusM
|
||||
}
|
||||
val eventBoosted = now < elevatedUntilMs
|
||||
return if (inGeofence || eventBoosted) config.elevatedRateHz else config.baseRateHz
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ELEVATED_HOLD_MS = 5_000L
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user