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:
Ashin Walpola
2026-08-10 11:38:01 +02:00
parent 33c4ec5998
commit 64fb78590e
20 changed files with 3076 additions and 596 deletions
@@ -83,6 +83,8 @@ class MainActivity : AppCompatActivity() {
val showBatteryOptPrompt by tripViewModel.showBatteryOptPrompt.collectAsState()
val useCaseEnabledMap by mqttViewModel.useCaseEnabledMap.collectAsState()
val obuHardware by mqttViewModel.obuHardware.collectAsState()
val usbSerialState by mqttViewModel.usbSerialState.collectAsState()
val espRxMode by mqttViewModel.espRxMode.collectAsState()
MicrOBUTheme(darkTheme = state.darkTheme) {
val view = LocalView.current
@@ -135,6 +137,7 @@ class MainActivity : AppCompatActivity() {
mqttConnectionState = mqttConnectionState,
activeTransport = activeTransport,
obuHardware = obuHardware,
usbSerialState = usbSerialState,
usbCableConnected = usbConnected,
obuStationTypeWarning = obuStationTypeWarning,
obuStationType = obuStationType,
@@ -250,6 +253,8 @@ class MainActivity : AppCompatActivity() {
onMqttPrefsChange = mqttViewModel::updatePrefs,
obuHardware = obuHardware,
onObuHardwareChange = mqttViewModel::setObuHardware,
espRxMode = espRxMode,
onEspRxModeChange = mqttViewModel::setEspRxMode,
onBack = { navController.popBackStack() },
)
}
@@ -299,7 +304,13 @@ class MainActivity : AppCompatActivity() {
AboutSettingsScreen(onBack = { navController.popBackStack() })
}
composable(Screen.Connection.route) {
ConnectionSetupScreen()
// Must be the Activity-scoped instance (same one MainActivity holds
// and MqttTopicViewerScreen is given below), not the default
// hiltViewModel() — that would create a separate instance scoped to
// this NavBackStackEntry, whose onCleared() (fired the moment you
// navigate away) would disconnect the shared UsbSerialTransport out
// from under every other screen still using it.
ConnectionSetupScreen(viewModel = mqttViewModel)
}
composable(Screen.Map.route) {
MapScreen(gnss = state.gnss)