Count and surface RX-queue drops on the ESP32-C5's promiscuous path

wifi_promisc_rx_cb() fed s_rx_queue with a 0-timeout xQueueSend() and never
checked whether it succeeded, so a burst of captured frames arriving faster
than rx_forward_task could drain them vanished with no counter anywhere -
none of oversizeDrops/txFailures/rxCrcErrors caught it. Added a rxQueueDrops
counter, threaded it through the STATUS heartbeat as a new trailing uint16
(old firmware/app on either side still parse fine), and surfaced it on the
CAM Pinger card.

Confirmed on the bench: flashed to the production OBU (COM3) and installed
the matching app build on the phone, then watched the counter over logcat
against obu-cam-transmistter's ~3.3 Hz beacon - it is real (0 -> 89 -> 90
across two sessions) but bursty around connect/reconnect rather than a
continuous overflow under steady single-station traffic.
This commit is contained in:
Ashin Walpola
2026-09-22 14:45:17 +02:00
parent 21e01499d8
commit 7285fa19b7
9 changed files with 209 additions and 15 deletions
@@ -57,8 +57,9 @@ const val SERIAL_LINK_MAX_PAYLOAD = 512
/**
* Decoded [SerialFrameType.STATUS] payload: `[status:1][oversizeDrops:2 LE][txFailures:2 LE]
* [rxCrcErrors:2 LE]` (7 bytes). Counters are free-running totals since firmware boot and
* saturate at 0xFFFF rather than wrapping.
* [rxCrcErrors:2 LE][capabilities:1][rxQueueDrops:2 LE]` (10 bytes; the last two fields are an
* optional tail — see [capabilities] and [rxQueueDrops]). Counters are free-running totals since
* firmware boot and saturate at 0xFFFF rather than wrapping.
*
* Exists so the phone can tell "link alive, no traffic" from "link dead", and so firmware-side
* drops — which otherwise only reach `ESP_LOGW` on the flashing port that the phone isn't
@@ -79,6 +80,15 @@ data class EspLinkStatus(
* the phone needs from such firmware: it accepts nothing beyond the original messages.
*/
val capabilities: Int = 0,
/**
* Promiscuously-captured frames the firmware's `wifi_promisc_rx_cb` had to drop because its
* RX queue (8 deep) was still full of frames `rx_forward_task` hadn't finished forwarding —
* bytes 8-9 of the payload. 0 for firmware that predates this field (payload of 7 or 8 bytes),
* which is the honest answer: such firmware drops these frames identically, it just never
* counted them. A nonzero, growing value here — as opposed to [oversizeDrops] — points at
* bursty RX outrunning the forward task rather than any one frame being too large.
*/
val rxQueueDrops: Int = 0,
) {
/** True when the firmware accepts [SerialFrameType.CAM_TX_PV]. */
val supportsCamTxPv: Boolean get() = capabilities and CAP_CAM_TX_PV != 0
@@ -99,6 +109,7 @@ data class EspLinkStatus(
txFailures = u16(3),
rxCrcErrors = u16(5),
capabilities = if (payload.size > PAYLOAD_SIZE) payload[7].toInt() and 0xFF else 0,
rxQueueDrops = if (payload.size >= 10) u16(8) else 0,
)
}
}
@@ -330,13 +330,15 @@ class UsbSerialTransport @Inject constructor(
prev.txFailures != status.txFailures ||
prev.rxCrcErrors != status.rxCrcErrors ||
prev.status != status.status ||
prev.capabilities != status.capabilities
prev.capabilities != status.capabilities ||
prev.rxQueueDrops != status.rxQueueDrops
) {
Log.i(TAG, "ESP32 counters: status=${status.status} " +
"oversizeDrops=${status.oversizeDrops} " +
"txFailures=${status.txFailures} " +
"rxCrcErrors=${status.rxCrcErrors} " +
"capabilities=${status.capabilities}")
"capabilities=${status.capabilities} " +
"rxQueueDrops=${status.rxQueueDrops}")
}
_linkStatus.value = status
}
@@ -1219,11 +1219,12 @@ private fun CamPingerCard(
Text(
stringResource(
R.string.mqtt_cam_pinger_fw_counters,
s.txFailures, s.oversizeDrops, s.rxCrcErrors,
s.txFailures, s.oversizeDrops, s.rxCrcErrors, s.rxQueueDrops,
),
style = MaterialTheme.typography.labelSmall,
color = if (s.txFailures > 0 || s.oversizeDrops > 0 || s.rxCrcErrors > 0)
ErrorRed else MaterialTheme.colorScheme.onSurfaceVariant,
color = if (s.txFailures > 0 || s.oversizeDrops > 0 || s.rxCrcErrors > 0 ||
s.rxQueueDrops > 0
) ErrorRed else MaterialTheme.colorScheme.onSurfaceVariant,
fontFamily = FontFamily.Monospace,
)
}