Phase 03: ESP32-C5 serial link hardening + bench diagnostics
Enumeration: - Merge the library's stock probe table instead of replacing it, so adding Espressif 0x303A/0x1001 doesn't drop every other supported device - Select the ESP32-C5 by VID/PID rather than list position - Log USB interface descriptors to distinguish CDC data from the JTAG interface Lifecycle: - Don't close the shared port in MqttViewModel.onCleared() - the foreground recording service outlives the ViewModel and would beacon into a dead port - Handle ACTION_USB_DEVICE_DETACHED so the UI stops reporting a stale link - Implement the STATUS heartbeat on both sides (1 Hz) plus a phone-side watchdog - Surface write failures and firmware drop counters on the CAM Pinger card Protocol: - Assert DTR/RTS on open (unverified on hardware - see FLASHING.md step 5) - Raise SERIAL_LINK_MAX_PAYLOAD 160 -> 512 on both sides; real third-party CAMs exceed 160 and were being silently dropped at the resync branch - Move the enlarged buffers off task stacks; serialize send_frame with a mutex Firmware and app must be updated together - a 512/160 mismatch fails silently.
This commit is contained in:
@@ -9,12 +9,17 @@ import com.hawhamburg.micr0bu.data.mqtt.MqttPreferences
|
||||
import com.hawhamburg.micr0bu.data.mqtt.MqttPrefs
|
||||
import com.hawhamburg.micr0bu.data.mqtt.MqttRepository
|
||||
import com.hawhamburg.micr0bu.data.mqtt.ObuHardwarePreferences
|
||||
import com.hawhamburg.micr0bu.data.transport.EspLinkStatus
|
||||
import com.hawhamburg.micr0bu.data.transport.EspRxMode
|
||||
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.domain.denm.DenmUseCase
|
||||
import com.hawhamburg.micr0bu.domain.usecase.UseCaseAlert
|
||||
import com.hawhamburg.micr0bu.domain.usecase.UseCaseType
|
||||
import com.hawhamburg.micr0bu.service.CamPinger
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -33,6 +38,8 @@ class MqttViewModel @Inject constructor(
|
||||
private val usbDetector: UsbNetworkDetector,
|
||||
private val camUseCaseRepository: CamUseCaseRepository,
|
||||
private val obuHardwarePrefs: ObuHardwarePreferences,
|
||||
private val usbSerialTransport: UsbSerialTransport,
|
||||
private val camPinger: CamPinger,
|
||||
) : ViewModel() {
|
||||
|
||||
// ── MQTT connection & messages ────────────────────────────────────────────
|
||||
@@ -58,6 +65,19 @@ class MqttViewModel @Inject constructor(
|
||||
viewModelScope.launch { obuHardwarePrefs.setObuHardware(hardware) }
|
||||
}
|
||||
|
||||
/**
|
||||
* ESP32-C5-only: whether received CAM traffic is processed or discarded — see [EspRxMode]'s
|
||||
* KDoc for the important caveat that this doesn't actually disable the ESP32's receiver
|
||||
* (it can't, without also breaking TX).
|
||||
*/
|
||||
val espRxMode: StateFlow<EspRxMode> = obuHardwarePrefs.espRxModeFlow.stateIn(
|
||||
viewModelScope, SharingStarted.Eagerly, EspRxMode.SEND_AND_RECEIVE,
|
||||
)
|
||||
|
||||
fun setEspRxMode(mode: EspRxMode) {
|
||||
viewModelScope.launch { obuHardwarePrefs.setEspRxMode(mode) }
|
||||
}
|
||||
|
||||
/** True when a 192.168.42.x USB-C tethering network is detected. */
|
||||
val usbConnected: StateFlow<Boolean> = usbDetector.usbNetwork
|
||||
.map { it != null }
|
||||
@@ -66,6 +86,27 @@ 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
|
||||
|
||||
/** Latest firmware heartbeat + drop counters, null until the first STATUS frame arrives. */
|
||||
val espLinkStatus: StateFlow<EspLinkStatus?> = usbSerialTransport.linkStatus
|
||||
|
||||
/** Non-zero means CAMs are being built and dropped — see [UsbSerialTransport.sendCamTx]. */
|
||||
val camSendFailures: StateFlow<Int> = usbSerialTransport.consecutiveWriteFailures
|
||||
|
||||
// ── 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-
|
||||
// location 1 Hz CAM ping the user starts/stops from the V2X Monitor screen to verify the
|
||||
// serial link + ESP32 TX/RX radio path independent of GNSS movement or trip recording.
|
||||
// See CamPinger's KDoc.
|
||||
|
||||
val camPingerActive: StateFlow<Boolean> = camPinger.isActive
|
||||
val camPingerSentCount: StateFlow<Int> = camPinger.sentCount
|
||||
|
||||
fun startCamPinger() = camPinger.start()
|
||||
fun stopCamPinger() = camPinger.stop()
|
||||
|
||||
// ── Prefs ─────────────────────────────────────────────────────────────────
|
||||
|
||||
val mqttPrefs: StateFlow<MqttPrefs> = prefs.prefsFlow.stateIn(
|
||||
@@ -158,6 +199,11 @@ 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()
|
||||
|
||||
fun selectTopic(topic: String?) { _selectedTopic.value = topic }
|
||||
fun setAutoScroll(enabled: Boolean) { _autoScroll.value = enabled }
|
||||
|
||||
@@ -189,5 +235,12 @@ class MqttViewModel @Inject constructor(
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repo.disconnect()
|
||||
camPinger.stop()
|
||||
// Deliberately NOT usbSerialTransport.disconnect(): the transport 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
|
||||
// a dead port. The port closes on explicit user Disconnect, on USB detach (handled
|
||||
// inside the transport), or with the process. See UsbSerialTransport's "Ownership" KDoc.
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user