Drive the ESP32-C5 station over USB or BLE, send CAM or VAM, signed or not
The app now speaks the station-link protocol of the new obu-firmware. Esp32Link picks the transport from Settings (UsbSerialTransport or the new BleLinkTransport), tells the previous firmware from the new one by its heartbeat, and runs the session: STATION_CONFIGURE with the current pseudonym MAC (which also starts the board's radio), CREDENTIALS_PROVISION of the bundled demo chain when the board has no ticket, then per message a POTI_UPDATE and a BTP_DATA_REQUEST. Received messages still arrive as V2X_RX frames, so the receive side is unchanged. A board on the previous firmware keeps working for CAM over USB. Settings > Connection > ESP32-C5: link USB-C or Bluetooth, transmit CAM or VAM, "Sign outgoing messages" (on by default). The connection card, top bar and dashboard show the link in use, the pairing passkey and signing counters. - VAM: VamUperCodec (TS 103 300-3 V2.3.1, bytes checked against asn1tools) and VamGenerationRules (clause 6.4, Tables 16/17). - BLE: the firmware's GATT layout (service 0000C175-...), MTU 517, pairing and encryption settled before any other operation (short timeouts during pairing made it loop), backoff between attempts, reasons on the card. - Clock: a PoTi goes to the board once per new fix and never moves the board's clock backwards except for a real correction (>= 60 s); stale and wobbling fix times made the board answer time_regression and restart its stack every few seconds. GnssTimeSource keeps the last measured phone-clock error while GNSS time drops out indoors: the bench phone is 14 minutes fast, and falling back to it made every transmitted timestamp jump by that much. - assets/demo-chain.vcr: throwaway, not EU-registered demo chain generated 2026-09-23 (AT B80B49387A4C12EB, psid 36 and 638). Its private key ships with the app on purpose; receivers verifying against the EU trust list drop what it signs. - Bluetooth permissions requested at start-up on Android 12+. StationLinkTest pins the codec to bytes from the colleague's Python implementation (microbu_link/messages.py). 103 unit tests pass.
This commit is contained in:
@@ -13,8 +13,11 @@ import com.hawhamburg.micr0bu.data.transport.EspLinkStatus
|
||||
import com.hawhamburg.micr0bu.data.transport.ObuHardware
|
||||
import com.hawhamburg.micr0bu.data.transport.TransportType
|
||||
import com.hawhamburg.micr0bu.data.transport.UsbNetworkDetector
|
||||
import com.hawhamburg.micr0bu.data.transport.UsbSerialState
|
||||
import com.hawhamburg.micr0bu.data.transport.UsbSerialTransport
|
||||
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState
|
||||
import com.hawhamburg.micr0bu.data.transport.Esp32Link
|
||||
import com.hawhamburg.micr0bu.data.transport.Esp32Transport
|
||||
import com.hawhamburg.micr0bu.data.transport.OutgoingMessage
|
||||
import com.hawhamburg.micr0bu.data.transport.StationStatus
|
||||
import com.hawhamburg.micr0bu.domain.denm.DenmEvent
|
||||
import com.hawhamburg.micr0bu.domain.denm.DenmParser
|
||||
import com.hawhamburg.micr0bu.domain.spat.SpatIntersection
|
||||
@@ -45,7 +48,7 @@ class MqttViewModel @Inject constructor(
|
||||
private val usbDetector: UsbNetworkDetector,
|
||||
private val camUseCaseRepository: CamUseCaseRepository,
|
||||
private val obuHardwarePrefs: ObuHardwarePreferences,
|
||||
private val usbSerialTransport: UsbSerialTransport,
|
||||
private val esp32Link: Esp32Link,
|
||||
private val camPinger: CamPinger,
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -80,14 +83,42 @@ class MqttViewModel @Inject constructor(
|
||||
/** Auto-detected OBU gateway IP on the USB interface. */
|
||||
val detectedObuIp: StateFlow<String?> = usbDetector.detectedGatewayIp
|
||||
|
||||
/** ESP32-C5 USB-serial link state (Phase 03) — see [UsbSerialTransport]. */
|
||||
val usbSerialState: StateFlow<UsbSerialState> = usbSerialTransport.state
|
||||
/** ESP32-C5 link state, over USB or BLE per [esp32Transport] — see [Esp32Link]. */
|
||||
val esp32LinkState: StateFlow<Esp32LinkState> = esp32Link.state
|
||||
|
||||
/** Latest firmware heartbeat + drop counters, null until the first STATUS frame arrives. */
|
||||
val espLinkStatus: StateFlow<EspLinkStatus?> = usbSerialTransport.linkStatus
|
||||
val espLinkStatus: StateFlow<EspLinkStatus?> = esp32Link.linkStatus
|
||||
|
||||
/** Non-zero means CAMs are being built and dropped — see [UsbSerialTransport.sendCamTx]. */
|
||||
val camSendFailures: StateFlow<Int> = usbSerialTransport.consecutiveWriteFailures
|
||||
/** Non-zero means CAMs are being built and dropped — see [Esp32Link.send]. */
|
||||
val camSendFailures: StateFlow<Int> = esp32Link.consecutiveWriteFailures
|
||||
|
||||
/** Signing and radio counters of the current obu-firmware; null with the previous firmware. */
|
||||
val stationStatus: StateFlow<StationStatus?> = esp32Link.stationStatus
|
||||
|
||||
/** One line about the link session (pairing passkey, provisioning, refusals); null when quiet. */
|
||||
val esp32Detail: StateFlow<String?> = esp32Link.detail
|
||||
|
||||
// ── ESP32-C5 settings ─────────────────────────────────────────────────────
|
||||
|
||||
val esp32Transport: StateFlow<Esp32Transport> = esp32Link.transport
|
||||
|
||||
fun setEsp32Transport(transport: Esp32Transport) {
|
||||
viewModelScope.launch { obuHardwarePrefs.setEsp32Transport(transport) }
|
||||
}
|
||||
|
||||
val outgoingMessage: StateFlow<OutgoingMessage> = obuHardwarePrefs.outgoingMessageFlow
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, OutgoingMessage.CAM)
|
||||
|
||||
fun setOutgoingMessage(message: OutgoingMessage) {
|
||||
viewModelScope.launch { obuHardwarePrefs.setOutgoingMessage(message) }
|
||||
}
|
||||
|
||||
val signOutgoing: StateFlow<Boolean> = obuHardwarePrefs.signOutgoingFlow
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, true)
|
||||
|
||||
fun setSignOutgoing(sign: Boolean) {
|
||||
viewModelScope.launch { obuHardwarePrefs.setSignOutgoing(sign) }
|
||||
}
|
||||
|
||||
// ── ESP32-C5 CAM pinger (manual bench test, Phase 03) ─────────────────────
|
||||
// The ESP32-C5-path equivalent of the CiT One's manual DENM trigger below — a fixed-
|
||||
@@ -352,10 +383,10 @@ class MqttViewModel @Inject constructor(
|
||||
fun connect() = repo.connect()
|
||||
fun disconnect() = repo.disconnect()
|
||||
|
||||
/** Connect/disconnect the ESP32-C5 USB-serial link — separate from [connect]/[disconnect],
|
||||
* which drive the CiT One's MQTT-over-USB-C/Wi-Fi path. See [ConnectionSetupScreen]. */
|
||||
fun connectUsbSerial() = usbSerialTransport.connect()
|
||||
fun disconnectUsbSerial() = usbSerialTransport.disconnect()
|
||||
/** Connect/disconnect the ESP32-C5 link (USB or BLE per [esp32Transport]) — separate from
|
||||
* [connect]/[disconnect], which drive the CiT One's MQTT-over-USB-C/Wi-Fi path. */
|
||||
fun connectEsp32() = esp32Link.connect()
|
||||
fun disconnectEsp32() = esp32Link.disconnect()
|
||||
|
||||
fun selectTopic(topic: String?) { _selectedTopic.value = topic }
|
||||
fun setAutoScroll(enabled: Boolean) { _autoScroll.value = enabled }
|
||||
@@ -389,7 +420,7 @@ class MqttViewModel @Inject constructor(
|
||||
super.onCleared()
|
||||
repo.disconnect()
|
||||
camPinger.stop()
|
||||
// Deliberately NOT usbSerialTransport.disconnect(): the transport is an app-scoped
|
||||
// Deliberately NOT esp32Link.disconnect(): the link is an app-scoped
|
||||
// @Singleton also held by the foreground TripRecordingService (via CamTransmitLoop).
|
||||
// Closing it here would tear the port down when the Activity goes away — e.g. swiping
|
||||
// the app from Recents mid-recording — leaving the still-running service beaconing into
|
||||
|
||||
Reference in New Issue
Block a user