Fix UPER encoding of CurvatureCalculationMode; verified on hardware
CurvatureCalculationMode is the one extensible ENUMERATED in CAM:
ENUMERATED {yawRateUsed(0), yawRateNotUsed(1), unavailable(2), ...}
UPER encodes an extensible ENUMERATED as an extension bit followed by the root
index - 1 + 2 = 3 bits. All three of our encoders wrote only the 2-bit index,
shifting yawRate and the entire low-frequency container one bit early for any
standards-compliant receiver.
It went unnoticed because every end of this project shared the mistake: the
Kotlin codec was ported bit-for-bit from cam.c, so phone and ESP32 agreed
perfectly with each other and with nothing else. Confirmed against the ETSI
ASN.1 in the C-ITS-Parser checkout, where rasn marks this type - and only this
type - #[non_exhaustive].
Fixed in all three copies of the encoder (app CamUperCodec.kt,
obu-firmware/main/cam.c, obu-cam-transmistter/main/cam.c) plus the decoder,
which now rejects rather than misreads a set extension bit. Frame size is
unchanged at 43 bytes. Transmitter reflashed and the phone decodes its CAMs.
Also in this change:
- serial_link: skip send_frame entirely when no USB host is attached, and raise
the tx mutex timeout above the worst-case hold. With the phone unplugged every
write blocked its full timeout while holding the lock, so forwarded CAM_RX
traffic starved the 1 Hz heartbeat - observed as "tx mutex timeout, dropping
frame" on the console, and it would have tripped the phone's link watchdog.
Verified gone on hardware.
- Log decoded and failed CAMs in CamUseCaseRepository. "The app shows nothing"
had two indistinguishable causes; a silent `?: return` made this bug much
harder to find than it needed to be.
- Remove the ESP32 send-only/send-and-receive toggle. Reception can't be
disabled in firmware (raw TX only works while promiscuous), so it was an
app-side filter pretending to be a radio control.
- V2X monitor follows the serial link state on the ESP32 path instead of MQTT,
which is permanently disconnected there; CAM intake is gated on the link being
up, and engine state is cleared when it drops.
- About screen: 0.5.0, Phase 03.
- Track obu-cam-transmistter, the bench CAM transmitter. Its cam.c is compiled
(unlike obu-firmware's reference copy) and must stay bit-identical to the other
two - this commit is what that coupling costs when it's broken.
- Document the two-toolchain split: this project builds on IDF 5.5.4, obu-firmware
on the pinned 6.1. Exporting both in one shell fails confusingly.
This commit is contained in:
@@ -84,7 +84,6 @@ class MainActivity : AppCompatActivity() {
|
||||
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
|
||||
@@ -253,8 +252,6 @@ class MainActivity : AppCompatActivity() {
|
||||
onMqttPrefsChange = mqttViewModel::updatePrefs,
|
||||
obuHardware = obuHardware,
|
||||
onObuHardwareChange = mqttViewModel::setObuHardware,
|
||||
espRxMode = espRxMode,
|
||||
onEspRxModeChange = mqttViewModel::setEspRxMode,
|
||||
onBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package com.hawhamburg.micr0bu.data.cam
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.hawhamburg.micr0bu.data.GnssReading
|
||||
import com.hawhamburg.micr0bu.data.SensorRepository
|
||||
import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState
|
||||
import com.hawhamburg.micr0bu.data.mqtt.MqttRepository
|
||||
import com.hawhamburg.micr0bu.data.mqtt.ObuHardwarePreferences
|
||||
import com.hawhamburg.micr0bu.data.mqtt.UseCaseAlertPreferences
|
||||
import com.hawhamburg.micr0bu.data.transport.EspRxMode
|
||||
import com.hawhamburg.micr0bu.data.transport.ObuHardware
|
||||
import com.hawhamburg.micr0bu.data.transport.SerialFrameType
|
||||
import com.hawhamburg.micr0bu.data.transport.UsbSerialState
|
||||
import com.hawhamburg.micr0bu.data.transport.UsbSerialTransport
|
||||
import com.hawhamburg.micr0bu.domain.asn1.RealAsn1UperCodec
|
||||
import com.hawhamburg.micr0bu.domain.cam.Cam
|
||||
@@ -36,6 +38,7 @@ import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val TAG = "CamUseCaseRepo"
|
||||
private const val CAM_TOPIC = "v2x-uca/output/json/cam"
|
||||
private const val OBU_GNSS_TOPIC = "v2x/rx/obu_gnss"
|
||||
private const val PRUNE_INTERVAL_MS = 1_000L
|
||||
@@ -78,7 +81,8 @@ class CamUseCaseRepository @Inject constructor(
|
||||
private val engine = UseCaseDetectionEngine()
|
||||
private val sensorRepository = SensorRepository(context)
|
||||
|
||||
@Volatile private var espRxMode: EspRxMode = EspRxMode.SEND_AND_RECEIVE
|
||||
/** Latest selected OBU hardware, so serial-link events only act on the ESP32-C5 path. */
|
||||
@Volatile private var currentHardware: ObuHardware = ObuHardware.CIT_ONE
|
||||
|
||||
private val _ownStationId = MutableStateFlow<Long?>(null)
|
||||
/** The ego OBU's own station ID, learned from `v2x/rx/obu_gnss`. Null until known. */
|
||||
@@ -168,13 +172,26 @@ class CamUseCaseRepository @Inject constructor(
|
||||
scope.launch {
|
||||
usbSerialTransport.incomingFrames.collect { frame ->
|
||||
if (frame.type != SerialFrameType.CAM_RX) return@collect
|
||||
if (espRxMode == EspRxMode.SEND_ONLY) return@collect
|
||||
if (usbSerialTransport.state.value != UsbSerialState.CONNECTED) return@collect
|
||||
handleCamFromSerial(frame.payload)
|
||||
}
|
||||
}
|
||||
|
||||
// Drop everything the serial link taught us the moment it goes down. Without this, the
|
||||
// last-seen positions and their alerts linger on the map and in the use-case panel after
|
||||
// an unplug, which reads as live traffic - the worst kind of stale on a safety display.
|
||||
scope.launch {
|
||||
obuHardwarePrefs.espRxModeFlow.collect { espRxMode = it }
|
||||
usbSerialTransport.state.collect { state ->
|
||||
// ESP32-only: on the CiT One path this transport is permanently DISCONNECTED and
|
||||
// resetting here would wipe perfectly good MQTT-derived state.
|
||||
if (currentHardware == ObuHardware.ESP32_C5 && state != UsbSerialState.CONNECTED) {
|
||||
engine.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
obuHardwarePrefs.obuHardwareFlow.collect { currentHardware = it }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,9 +270,25 @@ class CamUseCaseRepository @Inject constructor(
|
||||
private fun handleCamFromSerial(payload: ByteArray) {
|
||||
if (payload.isEmpty()) return
|
||||
val camBytes = payload.copyOfRange(1, payload.size) // payload[0] is RSSI, not part of the CAM
|
||||
val cam = camCodec.decodeCam(camBytes, System.currentTimeMillis()) ?: return
|
||||
val cam = camCodec.decodeCam(camBytes, System.currentTimeMillis())
|
||||
if (cam == null) {
|
||||
// Logged, not silently dropped: "the app shows nothing" has two completely different
|
||||
// causes - frames not arriving at all, versus arriving and failing to decode - and
|
||||
// without this line they're indistinguishable from the outside. rssi is signed.
|
||||
Log.w(
|
||||
TAG,
|
||||
"handleCamFromSerial: decode FAILED for ${camBytes.size}-byte CAM " +
|
||||
"(rssi=${payload[0].toInt()}) - first bytes: ${camBytes.toHexPreview()}",
|
||||
)
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "handleCamFromSerial: decoded station=${cam.stationId} " +
|
||||
"lat=${cam.latitude} lon=${cam.longitude} speed=${cam.speedMps} rssi=${payload[0].toInt()}")
|
||||
if (_ownStationId.value != null && cam.stationId == _ownStationId.value) return // self-heard TX
|
||||
engine.onRemoteCam(cam)
|
||||
_processedCam.tryEmit(cam)
|
||||
}
|
||||
|
||||
private fun ByteArray.toHexPreview(limit: Int = 16): String =
|
||||
take(limit).joinToString(" ") { "%02x".format(it) } + if (size > limit) " ..." else ""
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.hawhamburg.micr0bu.data.transport.EspRxMode
|
||||
import com.hawhamburg.micr0bu.data.transport.ObuHardware
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -27,7 +26,6 @@ class ObuHardwarePreferences @Inject constructor(
|
||||
) {
|
||||
private object Keys {
|
||||
val OBU_HARDWARE = stringPreferencesKey("obu_hardware")
|
||||
val ESP_RX_MODE = stringPreferencesKey("esp_rx_mode")
|
||||
val OWN_STATION_ID = longPreferencesKey("own_station_id")
|
||||
}
|
||||
|
||||
@@ -39,19 +37,6 @@ class ObuHardwarePreferences @Inject constructor(
|
||||
context.obuHardwareDataStore.edit { prefs -> prefs[Keys.OBU_HARDWARE] = hardware.id }
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the ESP32-C5 path processes received CAM traffic or only transmits — see
|
||||
* [EspRxMode]'s KDoc for what this does and doesn't actually control. Defaults to
|
||||
* [EspRxMode.SEND_AND_RECEIVE] (full duplex, today's existing behavior).
|
||||
*/
|
||||
val espRxModeFlow: Flow<EspRxMode> = context.obuHardwareDataStore.data.map { prefs ->
|
||||
EspRxMode.entries.firstOrNull { it.id == prefs[Keys.ESP_RX_MODE] } ?: EspRxMode.SEND_AND_RECEIVE
|
||||
}
|
||||
|
||||
suspend fun setEspRxMode(mode: EspRxMode) {
|
||||
context.obuHardwareDataStore.edit { prefs -> prefs[Keys.ESP_RX_MODE] = mode.id }
|
||||
}
|
||||
|
||||
/** This device's own CAM StationID, or null if one hasn't been assigned yet. */
|
||||
val ownStationIdFlow: Flow<Long?> = context.obuHardwareDataStore.data.map { prefs ->
|
||||
prefs[Keys.OWN_STATION_ID]
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.hawhamburg.micr0bu.data.transport
|
||||
|
||||
/**
|
||||
* Whether the ESP32-C5 path (Phase 03) processes remote CAM traffic it receives, or only ever
|
||||
* transmits the phone's own CAM.
|
||||
*
|
||||
* **Important nuance:** this does NOT physically disable the ESP32's radio receiver. The
|
||||
* firmware's own promiscuous-mode setup (`main.c`, see comments there) is required for its raw
|
||||
* 802.11p TX path to work at all — ESP-IDF only allows `esp_wifi_80211_tx()` to emit frames when
|
||||
* the MAC is promiscuous or associated to an AP. So the ESP32 always physically receives and
|
||||
* forwards CAM_RX frames over the serial link regardless of this setting; what this setting
|
||||
* actually controls is purely on the phone side — whether [SEND_ONLY] mode ignores those
|
||||
* incoming frames (see [com.hawhamburg.micr0bu.data.cam.CamUseCaseRepository]) instead of
|
||||
* feeding them into the detection engine / UI. Useful for isolating the TX path during bench
|
||||
* testing (e.g. with the [com.hawhamburg.micr0bu.service.CamPinger]) without nearby test traffic
|
||||
* cluttering the use-case alerts or live map.
|
||||
*/
|
||||
enum class EspRxMode(val id: String) {
|
||||
/** Transmit the phone's own CAM only; incoming CAM_RX frames from the ESP32 are discarded. */
|
||||
SEND_ONLY("send_only"),
|
||||
|
||||
/** Normal full-duplex operation: transmit own CAM and process received CAM traffic. */
|
||||
SEND_AND_RECEIVE("send_and_receive"),
|
||||
}
|
||||
@@ -25,8 +25,9 @@ enum class ObuHardware(val id: String) {
|
||||
*
|
||||
* Real serial link + CAM UPER codec are implemented on both sides — see
|
||||
* [com.hawhamburg.micr0bu.data.transport.UsbSerialTransport] (phone) and
|
||||
* `obu-firmware/main/serial_link.c` (firmware). See also [EspRxMode] for the send-only vs
|
||||
* send-and-receive toggle (Settings > Connection).
|
||||
* `obu-firmware/main/serial_link.c` (firmware). Reception is always on: the ESP32 must keep
|
||||
* its receiver enabled for raw TX to work at all (ESP-IDF only emits raw 802.11 frames while
|
||||
* promiscuous or associated), so there is nothing meaningful for the app to toggle.
|
||||
*/
|
||||
ESP32_C5("esp32_c5"),
|
||||
}
|
||||
|
||||
@@ -128,7 +128,17 @@ object CamUperCodec {
|
||||
|
||||
bw.putBits(1023 - (-1023), 11) // curvatureValue: unavailable (not derived - see class KDoc)
|
||||
bw.putBits(7, 3) // curvatureConfidence: unavailable
|
||||
bw.putBits(2, 2) // curvatureCalculationMode: unavailable
|
||||
|
||||
// CurvatureCalculationMode is the ONE extensible ENUMERATED in this message:
|
||||
// ENUMERATED {yawRateUsed(0), yawRateNotUsed(1), unavailable(2), ...}
|
||||
// UPER encodes an extensible ENUMERATED as an extension bit followed by the index into
|
||||
// the root list - 1 + 2 = 3 bits, not 2. Both this encoder and its `cam.c` ancestor wrote
|
||||
// only the 2-bit index, which shifted yawRate and the whole low-frequency container one
|
||||
// bit early for any standards-compliant receiver. It went unnoticed because both ends of
|
||||
// this project shared the same mistake; phone <-> ESP32 agreed perfectly with each other
|
||||
// and with nothing else.
|
||||
bw.putBits(0, 1) // extension bit: value is in the root list
|
||||
bw.putBits(2, 2) // curvatureCalculationMode: unavailable(2)
|
||||
|
||||
val yawRateCentiDegS = cam.yawRateDps
|
||||
?.let { (it * 100.0).roundToInt().coerceIn(-32766, 32766) }
|
||||
@@ -247,7 +257,13 @@ object CamUperCodec {
|
||||
|
||||
br.getBits(11) // curvatureValue
|
||||
br.getBits(3) // curvatureConfidence
|
||||
br.getBits(2) // curvatureCalculationMode
|
||||
|
||||
// CurvatureCalculationMode: extensible ENUMERATED - extension bit, then the root index.
|
||||
// See the matching comment in [encode]. If the extension bit is set the sender used a
|
||||
// value added in a later spec revision, encoded as a length-prefixed extension addition
|
||||
// this decoder can't skip reliably - bail rather than misread everything after it.
|
||||
if (br.getBitsInt(1) != 0) return null
|
||||
br.getBits(2) // curvatureCalculationMode root index
|
||||
|
||||
val yawRateRaw = br.getBitsInt(16) + (-32766)
|
||||
br.getBits(3) // yawRateConfidence
|
||||
|
||||
@@ -138,8 +138,15 @@ fun MqttTopicViewerScreen(
|
||||
viewModel.selectTopic(null)
|
||||
}
|
||||
|
||||
val isConnected = connectionState == MqttConnectionState.CONNECTED
|
||||
val isConnecting = connectionState == MqttConnectionState.CONNECTING
|
||||
val isEsp32 = obuHardware == ObuHardware.ESP32_C5
|
||||
|
||||
// On the ESP32-C5 path there is no MQTT broker, so `connectionState` is permanently
|
||||
// DISCONNECTED and using it here made the screen report "offline" while CAMs streamed in over
|
||||
// serial. Everything on this screen that means "is the OBU link up?" follows the serial link
|
||||
// instead when that hardware is selected.
|
||||
val effectiveState = if (isEsp32) usbSerialState.asConnectionState() else connectionState
|
||||
val isConnected = effectiveState == MqttConnectionState.CONNECTED
|
||||
val isConnecting = effectiveState == MqttConnectionState.CONNECTING
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
@@ -175,10 +182,18 @@ fun MqttTopicViewerScreen(
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
|
||||
ConnectionChip(connectionState)
|
||||
ConnectionChip(effectiveState)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
IconButton(
|
||||
onClick = { if (isConnected || isConnecting) viewModel.disconnect() else viewModel.connect() },
|
||||
onClick = {
|
||||
// Route to whichever transport this hardware actually uses.
|
||||
if (isEsp32) {
|
||||
if (isConnected || isConnecting) viewModel.disconnectUsbSerial()
|
||||
else viewModel.connectUsbSerial()
|
||||
} else {
|
||||
if (isConnected || isConnecting) viewModel.disconnect() else viewModel.connect()
|
||||
}
|
||||
},
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = if (isConnected) ErrorRed else ConnectedGreen,
|
||||
),
|
||||
@@ -206,8 +221,8 @@ fun MqttTopicViewerScreen(
|
||||
activeDenmUseCase = activeDenmUseCase,
|
||||
useCaseAlerts = useCaseAlerts,
|
||||
showDenmTrigger = obuHardware == ObuHardware.CIT_ONE,
|
||||
showCamPinger = obuHardware == ObuHardware.ESP32_C5,
|
||||
isEsp32 = obuHardware == ObuHardware.ESP32_C5,
|
||||
showCamPinger = isEsp32,
|
||||
isEsp32 = isEsp32,
|
||||
denmEvents = denmEvents,
|
||||
usbSerialState = usbSerialState,
|
||||
camPingerActive = camPingerActive,
|
||||
@@ -1209,3 +1224,16 @@ private fun prettyPrintJson(raw: String): String {
|
||||
raw
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the ESP32-C5 serial link's lifecycle onto the MQTT connection vocabulary the shared
|
||||
* connection UI on this screen already speaks, so one indicator can serve both transports rather
|
||||
* than duplicating the chip and its colours per hardware type.
|
||||
*/
|
||||
private fun UsbSerialState.asConnectionState(): MqttConnectionState = when (this) {
|
||||
UsbSerialState.CONNECTED -> MqttConnectionState.CONNECTED
|
||||
UsbSerialState.DEVICE_ATTACHED,
|
||||
UsbSerialState.PERMISSION_REQUESTED -> MqttConnectionState.CONNECTING
|
||||
UsbSerialState.ERROR -> MqttConnectionState.ERROR
|
||||
UsbSerialState.DISCONNECTED -> MqttConnectionState.DISCONNECTED
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.hawhamburg.micr0bu.R
|
||||
import com.hawhamburg.micr0bu.data.mqtt.MqttPrefs
|
||||
import com.hawhamburg.micr0bu.data.transport.EspRxMode
|
||||
import com.hawhamburg.micr0bu.data.transport.ObuHardware
|
||||
import com.hawhamburg.micr0bu.domain.usecase.UseCaseDetectionConfig
|
||||
import com.hawhamburg.micr0bu.domain.usecase.UseCaseType
|
||||
@@ -166,8 +165,6 @@ fun ConnectionSettingsScreen(
|
||||
onMqttPrefsChange: (MqttPrefs) -> Unit,
|
||||
obuHardware: ObuHardware = ObuHardware.CIT_ONE,
|
||||
onObuHardwareChange: (ObuHardware) -> Unit = {},
|
||||
espRxMode: EspRxMode = EspRxMode.SEND_AND_RECEIVE,
|
||||
onEspRxModeChange: (EspRxMode) -> Unit = {},
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
SubScreen(stringResource(R.string.settings_connection), onBack) {
|
||||
@@ -212,47 +209,6 @@ fun ConnectionSettingsScreen(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
|
||||
// CAM reception mode — see EspRxMode's KDoc for the caveat that this is purely
|
||||
// an app-side filter, not a physical radio-receiver toggle (the ESP32 must keep
|
||||
// its receiver on for TX to keep working at all).
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
stringResource(R.string.settings_esp32_rx_mode),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
val isSendOnly = espRxMode == EspRxMode.SEND_ONLY
|
||||
OutlinedButton(
|
||||
onClick = { onEspRxModeChange(EspRxMode.SEND_ONLY) },
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = if (isSendOnly) MaterialTheme.colorScheme.primaryContainer else Color.Transparent,
|
||||
contentColor = if (isSendOnly) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
) { Text(stringResource(R.string.settings_esp32_rx_mode_send_only), fontWeight = if (isSendOnly) FontWeight.Bold else FontWeight.Normal) }
|
||||
|
||||
OutlinedButton(
|
||||
onClick = { onEspRxModeChange(EspRxMode.SEND_AND_RECEIVE) },
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = if (!isSendOnly) MaterialTheme.colorScheme.primaryContainer else Color.Transparent,
|
||||
contentColor = if (!isSendOnly) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
) { Text(stringResource(R.string.settings_esp32_rx_mode_send_and_receive), fontWeight = if (!isSendOnly) FontWeight.Bold else FontWeight.Normal) }
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.settings_esp32_rx_mode_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f),
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ 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
|
||||
@@ -67,19 +66,6 @@ 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 }
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
<string name="settings_wifi_val">Nur Entwicklermodus — noch nicht implementiert</string>
|
||||
<string name="settings_about">Über</string>
|
||||
<string name="settings_app_version">App-Version</string>
|
||||
<string name="settings_app_version_val">0.4.0 (Phase 02 — USB-C + DENM TX)</string>
|
||||
<string name="settings_app_version_val">0.5.0 (Phase 03 — ESP32-C5-Seriellverbindung + CAM vom Smartphone)</string>
|
||||
<string name="settings_connection">Verbindung</string>
|
||||
<string name="settings_usb_auto_detect">OBU per USB-C automatisch erkennen</string>
|
||||
<string name="settings_usb_manual_ip">OBU-IP (manuell)</string>
|
||||
@@ -170,10 +170,6 @@
|
||||
<string name="settings_obu_hardware_cit_one">CiT One</string>
|
||||
<string name="settings_obu_hardware_esp32">ESP32-C5</string>
|
||||
<string name="settings_obu_hardware_esp32_note">Der ESP32-C5 arbeitet als „dummer" Transceiver: CAM wird auf dem Smartphone erstellt und kodiert, über USB-Seriell an den ESP32 gesendet und über ITS-G5 gesendet. Auf diesem Pfad gibt es keinen MQTT-Broker und keine DENM-Use-Case-Engine — siehe den CAM-Pinger im V2X-Monitor für ein manuelles Testwerkzeug.</string>
|
||||
<string name="settings_esp32_rx_mode">CAM-Empfang</string>
|
||||
<string name="settings_esp32_rx_mode_send_only">Nur senden</string>
|
||||
<string name="settings_esp32_rx_mode_send_and_receive">Senden & Empfangen</string>
|
||||
<string name="settings_esp32_rx_mode_desc">„Nur senden" ignoriert von nahen Stationen empfangene CAM (der ESP32 empfängt sie physisch weiterhin — der Empfänger kann nicht abgeschaltet werden, ohne auch das Senden zu unterbrechen — sie werden nur nicht von der App verarbeitet). Nützlich, um den Sendepfad isoliert zu testen.</string>
|
||||
<string name="settings_usb_transport">Aktiver Transport</string>
|
||||
<string name="settings_transport_usbc">USB-C</string>
|
||||
<string name="settings_transport_wifi">WLAN</string>
|
||||
@@ -270,7 +266,7 @@
|
||||
<string name="settings_platform">Plattform</string>
|
||||
<string name="settings_platform_val">Android / Kotlin / Jetpack Compose</string>
|
||||
<string name="settings_project">Projekt</string>
|
||||
<string name="settings_project_val">MicrOBU — HAW Hamburg</string>
|
||||
<string name="settings_project_val">MicrOBU — HAW Hamburg & consider it GmbH</string>
|
||||
|
||||
<!-- Phase A: Trips (bottom nav) -->
|
||||
<string name="nav_trips">Fahrten</string>
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
<string name="settings_wifi_val">Dev mode only — not implemented</string>
|
||||
<string name="settings_about">About</string>
|
||||
<string name="settings_app_version">App version</string>
|
||||
<string name="settings_app_version_val">0.4.0 (Phase 02 — USB-C + DENM TX)</string>
|
||||
<string name="settings_app_version_val">0.5.0 (Phase 03 — ESP32-C5 serial link + phone-built CAM)</string>
|
||||
<string name="settings_connection">Connection</string>
|
||||
<string name="settings_usb_auto_detect">Auto-detect OBU via USB-C</string>
|
||||
<string name="settings_usb_manual_ip">Manual OBU IP</string>
|
||||
@@ -171,10 +171,6 @@
|
||||
<string name="settings_obu_hardware_cit_one">CiT One</string>
|
||||
<string name="settings_obu_hardware_esp32">ESP32-C5</string>
|
||||
<string name="settings_obu_hardware_esp32_note">ESP32-C5 acts as a "dumb" transceiver: CAM is built and encoded on the phone, sent to the ESP32 over USB serial, and broadcast over ITS-G5. No MQTT broker or DENM use-case engine on this path — see the V2X Monitor screen\'s CAM Pinger for a manual test tool.</string>
|
||||
<string name="settings_esp32_rx_mode">CAM Reception</string>
|
||||
<string name="settings_esp32_rx_mode_send_only">Send Only</string>
|
||||
<string name="settings_esp32_rx_mode_send_and_receive">Send & Receive</string>
|
||||
<string name="settings_esp32_rx_mode_desc">"Send Only" ignores CAM received from nearby stations (still physically received by the ESP32 — its receiver can\'t be turned off without also breaking transmit — just not processed by the app). Useful for isolating TX-path testing.</string>
|
||||
<string name="settings_usb_transport">Active transport</string>
|
||||
<string name="settings_transport_usbc">USB-C</string>
|
||||
<string name="settings_transport_wifi">Wi-Fi</string>
|
||||
@@ -271,7 +267,7 @@
|
||||
<string name="settings_platform">Platform</string>
|
||||
<string name="settings_platform_val">Android / Kotlin / Jetpack Compose</string>
|
||||
<string name="settings_project">Project</string>
|
||||
<string name="settings_project_val">MicrOBU — HAW Hamburg</string>
|
||||
<string name="settings_project_val">MicrOBU — HAW Hamburg & consider it GmbH</string>
|
||||
|
||||
<!-- Phase A: Trip Recording (bottom nav) -->
|
||||
<string name="nav_trips">Trips</string>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
# No longer need -Wl,-zmuldefs here - that was only for main/wifi_patches.c's
|
||||
# symbol-override attempt (which didn't work anyway; see docs/04-transmit-setup.md),
|
||||
# and that file is no longer part of the build. Superseded by main/tx_custom.c,
|
||||
# which bypasses the gate at a different layer instead of trying to override it.
|
||||
|
||||
project(obu_firmware)
|
||||
@@ -0,0 +1,53 @@
|
||||
# OBU transmit firmware - Phase 2 (in progress: HLN-SV DENM beacon)
|
||||
|
||||
Started. See `docs/04-transmit-setup.md` in the project root for build/flash
|
||||
steps and how to validate this against your own sniffer.
|
||||
|
||||
## Toolchain: use a dedicated terminal (ESP-IDF 5.5.4)
|
||||
|
||||
This project builds against the **global** ESP-IDF 5.5.4, NOT the 6.1 checkout
|
||||
that `obu-firmware` uses. Keep one terminal per toolchain and never export both
|
||||
in the same window - the second export inherits the first's
|
||||
`IDF_PYTHON_ENV_PATH` and then fails every dependency check (`click`,
|
||||
`esptool`, `cryptography`, ... "not met"). That is env-var bleed, not a broken
|
||||
install: do **not** run `install.bat` to "fix" it, that damages one of the two
|
||||
environments.
|
||||
|
||||
| Terminal | Export | Project |
|
||||
|---|---|---|
|
||||
| Transmitter | `C:\Espressif\frameworks\esp-idf-v5.5.4\export.ps1` | this one |
|
||||
| OBU | `...\micrOBU_workspace\its-g5-receiver-firmware\esp-idf\export.ps1` | `obu-firmware` |
|
||||
|
||||
If a terminal has already been used for the other IDF, clear the state first:
|
||||
|
||||
```powershell
|
||||
$env:IDF_PYTHON_ENV_PATH = $null; $env:IDF_PATH = $null
|
||||
```
|
||||
|
||||
Also note `build/` here was regenerated from scratch (its CMake cache still
|
||||
referenced an older source path under `micrOBU_workspace/v2x-obu-esp32c5/`,
|
||||
which makes `idf.py fullclean` refuse to run). If that error reappears, delete
|
||||
`build/` manually rather than fighting it.
|
||||
|
||||
## CAM encoding
|
||||
|
||||
`main/cam.c` IS compiled here (unlike `obu-firmware`'s copy, which is a
|
||||
reference only). It must stay bit-identical to `obu-firmware/main/cam.c` and
|
||||
the app's `CamUperCodec.kt` - all three encode the same wire format, and a
|
||||
one-bit divergence in any of them is invisible on the bench but wrong against
|
||||
real equipment. See the `CurvatureCalculationMode` comment in that file.
|
||||
|
||||
Implements one profile so far: **HLN-SV** (aftermarket stationary recovery
|
||||
vehicle), causeCode 94 (stationaryVehicle), subCauseCode 0, active while the
|
||||
hazard-light GPIO is grounded. No location/alacarte containers.
|
||||
|
||||
- `main/main.c` - entry point, the `phy_11p_set`/`phy_change_channel(5900,...)`
|
||||
register hack, GPIO polling, TX loop
|
||||
- `main/denm.c` / `.h` - ASN.1 UPER encoding of a minimal DENM
|
||||
- `main/geonet.c` / `.h` - GeoNetworking Basic/Common/SHB headers + BTP-B
|
||||
- `main/dot11p.c` / `.h` - 802.11 OCB (QoS Data, broadcast) frame + LLC/SNAP
|
||||
|
||||
Known gaps, tracked as TODOs in the source: no real GNSS (lat/long hardcoded
|
||||
0), no real time source (detectionTime/referenceTime hardcoded 0, decodes as
|
||||
2004-01-01), fixed (non-rotating) pseudonym MAC, SHB instead of GeoBroadcast
|
||||
(no multi-hop forwarding), unsecured (no IEEE 1609.2 signing).
|
||||
@@ -0,0 +1,8 @@
|
||||
# wifi_patches.c is intentionally NOT in this list anymore - superseded by
|
||||
# tx_custom.c (see that file for why). Left on disk, unused, for history.
|
||||
idf_component_register(
|
||||
SRCS "main.c" "denm.c" "cam.c" "geonet.c" "dot11p.c" "tx_custom.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES esp_event esp_netif nvs_flash driver esp_phy
|
||||
PRIV_REQUIRES esp_wifi
|
||||
)
|
||||
@@ -0,0 +1,137 @@
|
||||
#include "cam.h"
|
||||
#include <string.h>
|
||||
|
||||
// MSB-first bit packer - identical approach to denm.c (ASN.1 UPER is a
|
||||
// bitstream, not a byte stream).
|
||||
typedef struct {
|
||||
uint8_t *buf;
|
||||
size_t buf_len;
|
||||
size_t bit_pos;
|
||||
} bitwriter_t;
|
||||
|
||||
static void bw_init(bitwriter_t *bw, uint8_t *buf, size_t len)
|
||||
{
|
||||
bw->buf = buf;
|
||||
bw->buf_len = len;
|
||||
bw->bit_pos = 0;
|
||||
memset(buf, 0, len);
|
||||
}
|
||||
|
||||
static void bw_put_bits(bitwriter_t *bw, uint64_t value, int nbits)
|
||||
{
|
||||
for (int i = nbits - 1; i >= 0; i--) {
|
||||
size_t byte_idx = bw->bit_pos / 8;
|
||||
int bit_idx = 7 - (int)(bw->bit_pos % 8);
|
||||
if (byte_idx >= bw->buf_len) {
|
||||
return; // overflow guard - check return value of cam_encode
|
||||
}
|
||||
uint8_t bit = (value >> i) & 1;
|
||||
bw->buf[byte_idx] = (uint8_t)(bw->buf[byte_idx] | (bit << bit_idx));
|
||||
bw->bit_pos++;
|
||||
}
|
||||
}
|
||||
|
||||
static size_t bw_byte_len(const bitwriter_t *bw)
|
||||
{
|
||||
return (bw->bit_pos + 7) / 8;
|
||||
}
|
||||
|
||||
int cam_encode(const cam_fields_t *f, uint8_t *buf, size_t buf_len)
|
||||
{
|
||||
bitwriter_t bw;
|
||||
bw_init(&bw, buf, buf_len);
|
||||
|
||||
// ---- ItsPduHeader ---- (SEQUENCE, no OPTIONALs, no "..." -> no preamble)
|
||||
bw_put_bits(&bw, 2, 8); // protocolVersion INTEGER(0..255) = 2
|
||||
bw_put_bits(&bw, 2, 8); // messageID INTEGER(0..255) = cam(2)
|
||||
bw_put_bits(&bw, f->station_id, 32); // stationID StationID INTEGER(0..4294967295)
|
||||
|
||||
// ---- CoopAwareness ---- (SEQUENCE, no OPTIONALs, no "...")
|
||||
// generationDeltaTime GenerationDeltaTime INTEGER(0..65535) -> 16 bits
|
||||
bw_put_bits(&bw, f->generation_delta_time, 16);
|
||||
|
||||
// ---- CamParameters ---- (SEQUENCE, EXTENSIBLE "...", 2 OPTIONALs:
|
||||
// lowFrequencyContainer, specialVehicleContainer)
|
||||
bw_put_bits(&bw, 0, 1); // extension bit: no extension additions
|
||||
bw_put_bits(&bw, 1, 1); // lowFrequencyContainer present
|
||||
bw_put_bits(&bw, 0, 1); // specialVehicleContainer absent
|
||||
|
||||
// ---- BasicContainer ---- (SEQUENCE, EXTENSIBLE "...", no OPTIONALs)
|
||||
bw_put_bits(&bw, 0, 1); // extension bit: none
|
||||
bw_put_bits(&bw, f->station_type, 8); // stationType StationType INTEGER(0..255)
|
||||
|
||||
// ReferencePosition (SEQUENCE, no OPTIONALs/"..."), identical widths to
|
||||
// DENM eventPosition (see denm.c for the constraint derivations):
|
||||
// Latitude INTEGER(-900000000..900000001) -> 31 bits, offset from -900000000
|
||||
uint32_t lat_offset = (uint32_t)((int64_t)f->latitude_tenmicrodeg - (-900000000));
|
||||
bw_put_bits(&bw, lat_offset, 31);
|
||||
// Longitude INTEGER(-1800000000..1800000001) -> 32 bits, offset from -1800000000
|
||||
uint32_t lon_offset = (uint32_t)((int64_t)f->longitude_tenmicrodeg - (-1800000000));
|
||||
bw_put_bits(&bw, lon_offset, 32);
|
||||
// PosConfidenceEllipse: SemiAxisLength(0..4095)->12, HeadingValue(0..3601)->12
|
||||
bw_put_bits(&bw, 4095, 12); // semiMajorConfidence: unavailable
|
||||
bw_put_bits(&bw, 4095, 12); // semiMinorConfidence: unavailable
|
||||
bw_put_bits(&bw, 3601, 12); // semiMajorOrientation: unavailable
|
||||
// Altitude: AltitudeValue(-100000..800001)->20 (offset from -100000),
|
||||
// AltitudeConfidence ENUM 16 values -> 4 bits
|
||||
bw_put_bits(&bw, 900001, 20); // 800001 ("unavailable") - (-100000) = 900001
|
||||
bw_put_bits(&bw, 15, 4); // altitudeConfidence: unavailable(15)
|
||||
|
||||
// ---- HighFrequencyContainer ---- CHOICE { basicVehicleContainerHighFrequency,
|
||||
// rsuContainerHighFrequency, ... } - EXTENSIBLE, 2 root alternatives.
|
||||
bw_put_bits(&bw, 0, 1); // CHOICE extension bit: value is in root
|
||||
bw_put_bits(&bw, 0, 1); // index: 0 = basicVehicleContainerHighFrequency (1 bit for 2 alts)
|
||||
|
||||
// BasicVehicleContainerHighFrequency (SEQUENCE, NOT extensible, 7 OPTIONALs
|
||||
// accelerationControl..cenDsrcTollingZone - all absent).
|
||||
bw_put_bits(&bw, 0, 7); // 7 optional-presence bits, all absent
|
||||
|
||||
// Heading: HeadingValue(0..3601)->12, HeadingConfidence(1..127)->7 (offset from 1)
|
||||
bw_put_bits(&bw, f->heading_ddeg, 12);
|
||||
bw_put_bits(&bw, 127 - 1, 7); // headingConfidence: unavailable(127)
|
||||
// Speed: SpeedValue(0..16383)->14, SpeedConfidence(1..127)->7 (offset from 1)
|
||||
bw_put_bits(&bw, f->speed_cm_s, 14);
|
||||
bw_put_bits(&bw, 127 - 1, 7); // speedConfidence: unavailable(127)
|
||||
// DriveDirection ENUM {forward,backward,unavailable} -> 2 bits
|
||||
bw_put_bits(&bw, 2, 2); // unavailable
|
||||
// VehicleLength: VehicleLengthValue(1..1023)->10 (offset from 1),
|
||||
// VehicleLengthConfidenceIndication ENUM 5 values -> 3 bits
|
||||
bw_put_bits(&bw, (uint32_t)f->vehicle_length_dm - 1, 10);
|
||||
bw_put_bits(&bw, 4, 3); // vehicleLengthConfidenceIndication: unavailable(4)
|
||||
// VehicleWidth INTEGER(1..62) -> 6 bits (offset from 1)
|
||||
bw_put_bits(&bw, (uint32_t)f->vehicle_width_dm - 1, 6);
|
||||
// LongitudinalAcceleration: value(-160..161)->9 (offset from -160),
|
||||
// AccelerationConfidence(0..102)->7
|
||||
bw_put_bits(&bw, 161 - (uint32_t)(-160), 9); // longitudinalAccelerationValue: unavailable(161)
|
||||
bw_put_bits(&bw, 102, 7); // confidence: unavailable(102)
|
||||
// Curvature: CurvatureValue(-1023..1023)->11 (offset from -1023),
|
||||
// CurvatureConfidence ENUM 8 values -> 3 bits
|
||||
bw_put_bits(&bw, 1023 - (uint32_t)(-1023), 11); // curvatureValue: unavailable(1023)
|
||||
bw_put_bits(&bw, 7, 3); // curvatureConfidence: unavailable(7)
|
||||
// CurvatureCalculationMode ENUM {yawRateUsed,yawRateNotUsed,unavailable, ...} - note the
|
||||
// extension marker: UPER encodes an extensible ENUMERATED as an extension bit followed by
|
||||
// the root-list index, so this is 1 + 2 = 3 bits, NOT 2. Writing only the 2-bit index shifted
|
||||
// yawRate and the entire low-frequency container one bit early for any standards-compliant
|
||||
// receiver - including, after its matching fix, the phone app's own decoder.
|
||||
// Keep in lockstep with obu-firmware/main/cam.c and the app's CamUperCodec.kt.
|
||||
bw_put_bits(&bw, 0, 1); // extension bit: value is in the root list
|
||||
bw_put_bits(&bw, 2, 2); // unavailable(2)
|
||||
// YawRate: YawRateValue(-32766..32767)->16 (offset from -32766),
|
||||
// YawRateConfidence ENUM 8 values -> 3 bits
|
||||
bw_put_bits(&bw, 32767 - (uint32_t)(-32766), 16); // yawRateValue: unavailable(32767)
|
||||
bw_put_bits(&bw, 7, 3); // yawRateConfidence: unavailable(7)
|
||||
|
||||
// ---- LowFrequencyContainer ---- CHOICE { basicVehicleContainerLowFrequency,
|
||||
// ... } - EXTENSIBLE, 1 root alternative (index needs 0 bits).
|
||||
bw_put_bits(&bw, 0, 1); // CHOICE extension bit: value is in root
|
||||
|
||||
// BasicVehicleContainerLowFrequency (SEQUENCE, no OPTIONALs/"...")
|
||||
// vehicleRole VehicleRole ENUM 16 values -> 4 bits
|
||||
bw_put_bits(&bw, 0, 4); // default(0)
|
||||
// exteriorLights ExteriorLights BIT STRING(SIZE(8)) -> 8 bits, all off
|
||||
bw_put_bits(&bw, 0, 8);
|
||||
// pathHistory PathHistory ::= SEQUENCE(SIZE(0..40)) OF PathPoint -> count 0..40 = 6 bits
|
||||
bw_put_bits(&bw, 0, 6); // empty path history
|
||||
|
||||
return (int)bw_byte_len(&bw);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef CAM_H
|
||||
#define CAM_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Minimal CAM (Cooperative Awareness Message) per ETSI EN 302 637-2 v1.4.1
|
||||
// (CAM-PDU-Descriptions) + TS 102 894-2 v1.3.1 (CDD / ITS-Container), matching
|
||||
// the field set the working Rust reference (esp32-c_its-companion, feat/tx-cam,
|
||||
// src/applogic/cam_tx.rs) transmits:
|
||||
// - ItsPduHeader (protocolVersion 2, messageID 2 = cam)
|
||||
// - CoopAwareness { generationDeltaTime, camParameters }
|
||||
// - CamParameters {
|
||||
// basicContainer { stationType, referencePosition },
|
||||
// highFrequencyContainer = basicVehicleContainerHighFrequency { ... },
|
||||
// lowFrequencyContainer = basicVehicleContainerLowFrequency { ... }
|
||||
// }
|
||||
// All vehicle-dynamics fields we don't measure are encoded as their ASN.1
|
||||
// "unavailable" value. Speed is a real 0 (correct for a stationary station).
|
||||
|
||||
typedef struct {
|
||||
uint32_t station_id;
|
||||
uint8_t station_type; // StationType(0..255): 5 = passengerCar
|
||||
uint16_t generation_delta_time; // TimestampIts mod 65536 (ms); 0 until a real clock is wired
|
||||
int32_t latitude_tenmicrodeg; // Latitude, 1/10 microdegree
|
||||
int32_t longitude_tenmicrodeg; // Longitude, 1/10 microdegree
|
||||
uint16_t speed_cm_s; // SpeedValue, 0.01 m/s units (0 = stationary)
|
||||
uint16_t heading_ddeg; // HeadingValue, 0.1 deg units (0..3600), 3601 = unavailable
|
||||
uint16_t vehicle_length_dm; // VehicleLengthValue(1..1023), 10cm steps
|
||||
uint8_t vehicle_width_dm; // VehicleWidth(1..62), 10cm steps
|
||||
} cam_fields_t;
|
||||
|
||||
// Encodes the CAM as ASN.1 UPER. Returns bytes written, or -1 if buf too small.
|
||||
int cam_encode(const cam_fields_t *f, uint8_t *buf, size_t buf_len);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,153 @@
|
||||
#include "denm.h"
|
||||
#include <string.h>
|
||||
|
||||
// Minimal MSB-first bit packer - ASN.1 UPER is a bitstream, not a byte
|
||||
// stream, so we can't just memcpy structs.
|
||||
typedef struct {
|
||||
uint8_t *buf;
|
||||
size_t buf_len;
|
||||
size_t bit_pos;
|
||||
} bitwriter_t;
|
||||
|
||||
static void bw_init(bitwriter_t *bw, uint8_t *buf, size_t len)
|
||||
{
|
||||
bw->buf = buf;
|
||||
bw->buf_len = len;
|
||||
bw->bit_pos = 0;
|
||||
memset(buf, 0, len);
|
||||
}
|
||||
|
||||
static void bw_put_bits(bitwriter_t *bw, uint64_t value, int nbits)
|
||||
{
|
||||
for (int i = nbits - 1; i >= 0; i--) {
|
||||
size_t byte_idx = bw->bit_pos / 8;
|
||||
int bit_idx = 7 - (int)(bw->bit_pos % 8);
|
||||
if (byte_idx >= bw->buf_len) {
|
||||
return; // overflow guard - silently truncates, check return value of denm_encode
|
||||
}
|
||||
uint8_t bit = (value >> i) & 1;
|
||||
bw->buf[byte_idx] = (uint8_t)(bw->buf[byte_idx] | (bit << bit_idx));
|
||||
bw->bit_pos++;
|
||||
}
|
||||
}
|
||||
|
||||
static size_t bw_byte_len(const bitwriter_t *bw)
|
||||
{
|
||||
return (bw->bit_pos + 7) / 8;
|
||||
}
|
||||
|
||||
int denm_encode(const denm_fields_t *f, uint8_t *buf, size_t buf_len)
|
||||
{
|
||||
bitwriter_t bw;
|
||||
bw_init(&bw, buf, buf_len);
|
||||
|
||||
// ---- ItsPduHeader ---- (SEQUENCE, no OPTIONALs, no "..." -> no preamble at all)
|
||||
bw_put_bits(&bw, 2, 8); // protocolVersion INTEGER(0..255) = 2
|
||||
bw_put_bits(&bw, 1, 8); // messageID INTEGER(0..255) = denm(1)
|
||||
bw_put_bits(&bw, f->station_id, 32); // stationID = StationID INTEGER(0..4294967295) = 32 bits
|
||||
|
||||
// ---- DenmPayload (DecentralizedEnvironmentalNotificationMessage) ----
|
||||
// No "..." on this SEQUENCE -> no extension bit, just the 3-bit
|
||||
// optional-component preamble in declared order: situation, location,
|
||||
// alacarte. "No additional parameters" means location/alacarte stay
|
||||
// absent.
|
||||
bw_put_bits(&bw, 1, 1); // situation present
|
||||
bw_put_bits(&bw, 0, 1); // location absent
|
||||
bw_put_bits(&bw, 0, 1); // alacarte absent
|
||||
|
||||
// ---- ManagementContainer ----
|
||||
// This SEQUENCE ends in "..." in the real ASN.1 module -> extensible,
|
||||
// so it needs a leading 1-bit extension flag (0 = no extension
|
||||
// additions used) BEFORE the 5-bit optional/default preamble
|
||||
// (termination, relevanceDistance, relevanceTrafficDirection,
|
||||
// validityDuration, transmissionInterval, in that declared order). An
|
||||
// earlier version of this code omitted the extension bit entirely,
|
||||
// which would shift every single bit after it and corrupt the whole
|
||||
// rest of the message for any spec-compliant decoder.
|
||||
bw_put_bits(&bw, 0, 1); // ManagementContainer extension bit: none used
|
||||
bw_put_bits(&bw, f->terminate ? 1 : 0, 1); // termination present only when cancelling
|
||||
bw_put_bits(&bw, 0, 1); // relevanceDistance absent
|
||||
bw_put_bits(&bw, 0, 1); // relevanceTrafficDirection absent
|
||||
bw_put_bits(&bw, 0, 1); // validityDuration absent -> default 600s applies
|
||||
bw_put_bits(&bw, 0, 1); // transmissionInterval absent
|
||||
|
||||
// actionID = ActionID{ originatingStationID StationID(32), sequenceNumber
|
||||
// SequenceNumber(0..65535, 16 bits) } - no OPTIONALs/"..." -> no preamble.
|
||||
// Keep sequenceNumber constant across repeats of the SAME event - it's
|
||||
// the caller's job (see main.c) to only bump it on a genuinely new event
|
||||
// and reuse it for that event's eventual termination message.
|
||||
bw_put_bits(&bw, f->station_id, 32);
|
||||
bw_put_bits(&bw, f->sequence_number, 16);
|
||||
|
||||
// detectionTime / referenceTime: TimestampIts INTEGER(0..4398046511103)
|
||||
// = exactly 42 bits (2^42), ms since 2004-01-01T00:00:00Z. NOT WIRED UP
|
||||
// YET - there's no RTC/NTP sync in this skeleton, so this is 0 (decodes
|
||||
// as 2004-01-01). Wire in SNTP or a GNSS UTC fix before this is real.
|
||||
bw_put_bits(&bw, 0, 42);
|
||||
bw_put_bits(&bw, 0, 42);
|
||||
|
||||
// termination VALUE - only emitted when present (per the preamble bit
|
||||
// above - UPER never encodes a value for an absent optional component).
|
||||
// Termination ::= ENUMERATED{isCancellation(0), isNegation(1)}, no
|
||||
// "...", 2 values -> 1 bit.
|
||||
if (f->terminate) {
|
||||
bw_put_bits(&bw, 0, 1); // isCancellation
|
||||
}
|
||||
|
||||
// eventPosition (ReferencePosition ::= SEQUENCE{latitude, longitude,
|
||||
// positionConfidenceEllipse, altitude} - no OPTIONALs/"..." -> no
|
||||
// preamble, straight concatenation). Widths below are each field's
|
||||
// exact constrained-INTEGER range size from ITS-Container.asn, encoded
|
||||
// as an unsigned offset from the type's declared minimum - NOT assumed
|
||||
// to match neighboring fields (latitude and longitude are different
|
||||
// widths, which is easy to miss).
|
||||
// Latitude ::= INTEGER(-900000000..900000001) -> range 1800000002 -> 31 bits
|
||||
uint32_t lat_offset = (uint32_t)(f->latitude_tenmicrodeg - (-900000000));
|
||||
bw_put_bits(&bw, lat_offset, 31);
|
||||
// Longitude ::= INTEGER(-1800000000..1800000001) -> range 3600000002 -> 32 bits
|
||||
uint32_t lon_offset = (uint32_t)(f->longitude_tenmicrodeg - (-1800000000));
|
||||
bw_put_bits(&bw, lon_offset, 32);
|
||||
// PosConfidenceEllipse ::= SEQUENCE{semiMajorConfidence, semiMinorConfidence,
|
||||
// semiMajorOrientation} - no preamble.
|
||||
// SemiAxisLength ::= INTEGER(0..4095) -> 12 bits (not 16 - this was wrong before)
|
||||
bw_put_bits(&bw, 4095, 12); // semiMajorConfidence: unavailable
|
||||
bw_put_bits(&bw, 4095, 12); // semiMinorConfidence: unavailable
|
||||
// HeadingValue ::= INTEGER(0..3601) -> 12 bits (not 16 - this was wrong before)
|
||||
bw_put_bits(&bw, 3601, 12); // semiMajorOrientation: unavailable
|
||||
// Altitude ::= SEQUENCE{altitudeValue, altitudeConfidence} - no preamble.
|
||||
// AltitudeValue ::= INTEGER(-100000..800001) -> range 900002 -> 20 bits
|
||||
// (not 24 - this was wrong before), offset-encoded from -100000.
|
||||
bw_put_bits(&bw, 900001, 20); // 800001 ("unavailable") - (-100000) = 900001
|
||||
// AltitudeConfidence ::= ENUMERATED, 16 named values, no "..." -> 4 bits
|
||||
bw_put_bits(&bw, 15, 4); // unavailable
|
||||
|
||||
// stationType: StationType INTEGER(0..255) -> 8 bits fixed regardless of
|
||||
// how sparse the named values are.
|
||||
bw_put_bits(&bw, f->station_type, 8);
|
||||
|
||||
// ---- SituationContainer ----
|
||||
// This SEQUENCE also ends in "..." -> its own 1-bit extension flag,
|
||||
// THEN the 2-bit preamble (linkedCause, eventHistory), THEN the
|
||||
// mandatory field values. An earlier version of this code put the
|
||||
// linkedCause/eventHistory bits at the END instead of the start, and
|
||||
// had no extension bit at all - both are structural bugs that would
|
||||
// desync any spec-compliant decoder from this point on.
|
||||
bw_put_bits(&bw, 0, 1); // SituationContainer extension bit: none used
|
||||
bw_put_bits(&bw, 0, 1); // linkedCause absent
|
||||
bw_put_bits(&bw, 0, 1); // eventHistory absent
|
||||
|
||||
// informationQuality: InformationQuality INTEGER(0..7) -> 3 bits
|
||||
bw_put_bits(&bw, 1, 3); // low quality - no real sensor input, just the hazard-light GPIO
|
||||
|
||||
// eventType: CauseCode ::= SEQUENCE{causeCode, subCauseCode, ...} - this
|
||||
// inner SEQUENCE is ALSO extensible ("..."), so it gets its own leading
|
||||
// extension bit before its two mandatory fields.
|
||||
bw_put_bits(&bw, 0, 1); // CauseCode extension bit: none used
|
||||
bw_put_bits(&bw, f->cause_code, 8); // CauseCodeType INTEGER(0..255) -> 8 bits
|
||||
bw_put_bits(&bw, f->sub_cause_code, 8); // SubCauseCodeType INTEGER(0..255) -> 8 bits
|
||||
|
||||
// linkedCause / eventHistory: both absent, already signalled in the
|
||||
// preamble above - UPER writes no value bits for them.
|
||||
|
||||
return (int)bw_byte_len(&bw);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#ifndef DENM_H
|
||||
#define DENM_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// Full CauseCodeType enumeration, straight from the authoritative source:
|
||||
// ETSI TS 102 894-2 (CDD) ITS-Container.asn, CauseCodeType definition.
|
||||
// (Values 1/2/3/14/26/27/91/94/95/97 were already cross-checked earlier
|
||||
// against a real captured DENM; the rest are now confirmed the same way,
|
||||
// from the actual ASN.1 module rather than guessed.)
|
||||
#define DENM_CAUSE_RESERVED 0
|
||||
#define DENM_CAUSE_TRAFFIC_CONDITION 1
|
||||
#define DENM_CAUSE_ACCIDENT 2
|
||||
#define DENM_CAUSE_ROADWORKS 3
|
||||
#define DENM_CAUSE_IMPASSABILITY 5
|
||||
#define DENM_CAUSE_ADVERSE_WEATHER_ADHESION 6
|
||||
#define DENM_CAUSE_AQUAPLANNING 7
|
||||
#define DENM_CAUSE_HAZARDOUS_LOCATION_SURFACE_CONDITION 9
|
||||
#define DENM_CAUSE_HAZARDOUS_LOCATION_OBSTACLE_ON_ROAD 10
|
||||
#define DENM_CAUSE_HAZARDOUS_LOCATION_ANIMAL_ON_ROAD 11
|
||||
#define DENM_CAUSE_HUMAN_PRESENCE_ON_ROAD 12
|
||||
#define DENM_CAUSE_WRONG_WAY_DRIVING 14
|
||||
#define DENM_CAUSE_RESCUE_AND_RECOVERY_WORK_IN_PROGRESS 15
|
||||
#define DENM_CAUSE_ADVERSE_WEATHER_EXTREME 17
|
||||
#define DENM_CAUSE_ADVERSE_WEATHER_VISIBILITY 18
|
||||
#define DENM_CAUSE_ADVERSE_WEATHER_PRECIPITATION 19
|
||||
#define DENM_CAUSE_SLOW_VEHICLE 26
|
||||
#define DENM_CAUSE_DANGEROUS_END_OF_QUEUE 27
|
||||
#define DENM_CAUSE_VEHICLE_BREAKDOWN 91
|
||||
#define DENM_CAUSE_POST_CRASH 92
|
||||
#define DENM_CAUSE_HUMAN_PROBLEM 93
|
||||
#define DENM_CAUSE_STATIONARY_VEHICLE 94
|
||||
#define DENM_CAUSE_EMERGENCY_VEHICLE_APPROACHING 95
|
||||
#define DENM_CAUSE_HAZARDOUS_LOCATION_DANGEROUS_CURVE 96
|
||||
#define DENM_CAUSE_COLLISION_RISK 97
|
||||
#define DENM_CAUSE_SIGNAL_VIOLATION 98
|
||||
#define DENM_CAUSE_DANGEROUS_SITUATION 99
|
||||
|
||||
typedef struct {
|
||||
uint32_t station_id;
|
||||
uint16_t sequence_number; // keep constant across repeats of the SAME event; only bump on a genuinely new event
|
||||
uint8_t cause_code; // e.g. 94 = stationaryVehicle
|
||||
uint8_t sub_cause_code; // 0 = unspecified
|
||||
uint8_t station_type; // StationType, e.g. 5 = passengerCar - match geonet_wrap_shb's station_type param
|
||||
int32_t latitude_tenmicrodeg; // 1/10 microdegree; 0 = placeholder/unavailable
|
||||
int32_t longitude_tenmicrodeg; // 1/10 microdegree; 0 = placeholder/unavailable
|
||||
bool terminate; // true = encode this as a Termination(isCancellation) message instead of a normal update
|
||||
} denm_fields_t;
|
||||
|
||||
// Encodes a minimal DENM (ItsPduHeader + ManagementContainer +
|
||||
// SituationContainer only - no location/alacarte containers) as ASN.1 UPER,
|
||||
// per the actual ETSI EN 302 637-3 / TS 102 894-2 ASN.1 modules (fetched
|
||||
// from forge.etsi.org, not reconstructed from memory). Returns bytes
|
||||
// written, or -1 if buf too small.
|
||||
//
|
||||
// Two things worth knowing if you're reading this against the modules
|
||||
// yourself: ManagementContainer, SituationContainer, and CauseCode are all
|
||||
// declared with a trailing "..." (extensible), which means each needs its
|
||||
// own leading extension bit in the UPER encoding - easy to miss, and this
|
||||
// code got it wrong in an earlier version. Field bit-widths below (e.g.
|
||||
// latitude=31 bits, longitude=32 bits, position-confidence fields=12 bits,
|
||||
// altitudeValue=20 bits) are derived directly from each type's declared
|
||||
// INTEGER constraint range, not assumed to match neighboring fields.
|
||||
int denm_encode(const denm_fields_t *f, uint8_t *buf, size_t buf_len);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "dot11p.h"
|
||||
#include <string.h>
|
||||
|
||||
int dot11p_build_frame(const uint8_t *gn_payload, int gn_len,
|
||||
const uint8_t src_mac[6],
|
||||
uint8_t *out, size_t out_len, bool qos)
|
||||
{
|
||||
static const uint8_t broadcast[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static const uint8_t llc_snap[8] = {0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x89, 0x47};
|
||||
|
||||
int hdr_len = qos ? 26 : 24; // QoS Data adds a 2-byte QoS Control field
|
||||
int total = hdr_len + 8 /* LLC/SNAP */ + gn_len;
|
||||
if ((size_t)total > out_len) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t *p = out;
|
||||
|
||||
// Frame Control: version=0, type=Data(2), subtype=QoS Data(8) -> bytes
|
||||
// 0x88 0x00. This is what real ITS-G5 hardware actually transmits.
|
||||
//
|
||||
// Back on QoS Data again (previously downgraded to non-QoS, subtype 0,
|
||||
// as a working-but-nonstandard fallback - see git history / old comments
|
||||
// here for that whole detour). What changed: main.c no longer calls
|
||||
// esp_wifi_80211_tx() at all - it now goes through
|
||||
// esp_wifi_80211_tx_custom() (tx_custom.c, pulled from
|
||||
// opentrafficmap/its-g5-receiver-firmware_txenabled), which bypasses the
|
||||
// frame-type sanity check entirely by never calling the code path that
|
||||
// contains it. Frame subtype is no longer gated, so there's no reason
|
||||
// left to avoid matching real hardware here.
|
||||
// Frame Control byte 0: version=0, type=Data(2). Subtype: QoS Data(8)=0x88
|
||||
// for the tx_custom path, or plain Data(0)=0x08 for the standard
|
||||
// esp_wifi_80211_tx() path (which rejects QoS Data outright).
|
||||
*p++ = qos ? 0x88 : 0x08; *p++ = 0x00;
|
||||
// Duration
|
||||
*p++ = 0x00; *p++ = 0x00;
|
||||
// Addr1 = destination = broadcast
|
||||
memcpy(p, broadcast, 6); p += 6;
|
||||
// Addr2 = source (our pseudonym)
|
||||
memcpy(p, src_mac, 6); p += 6;
|
||||
// Addr3 = BSSID = broadcast (no BSS exists in OCB mode)
|
||||
memcpy(p, broadcast, 6); p += 6;
|
||||
// Sequence control - left at 0; en_sys_seq=true fills this in for us
|
||||
*p++ = 0x00; *p++ = 0x00;
|
||||
// QoS Control field - only present in QoS Data frames
|
||||
if (qos) {
|
||||
*p++ = 0x00; *p++ = 0x00; // best-effort access category
|
||||
}
|
||||
|
||||
// LLC/SNAP (Ethertype 0x8947 = GeoNetworking)
|
||||
memcpy(p, llc_snap, 8); p += 8;
|
||||
|
||||
// GeoNetworking + BTP + DENM payload
|
||||
memcpy(p, gn_payload, gn_len); p += gn_len;
|
||||
|
||||
return (int)(p - out);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef DOT11P_H
|
||||
#define DOT11P_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// Wraps a GeoNetworking-layer payload in an 802.11 OCB frame: QoS Data
|
||||
// (subtype 8, 26-byte header), matching real ITS-G5 hardware, broadcast, no
|
||||
// BSS (Addr1=Addr3=broadcast), LLC/SNAP with Ethertype 0x8947
|
||||
// (GeoNetworking's registered Ethertype). Output is ready to hand straight
|
||||
// to esp_wifi_80211_tx_custom() (tx_custom.c) - NOT esp_wifi_80211_tx(),
|
||||
// which rejects this frame type outright. `src_mac` is used as Addr2 - pass
|
||||
// the same 6 bytes you gave geonet_wrap_shb, since GN_ADDR's MID field is
|
||||
// defined to be this same link-layer address. Returns bytes written, or -1
|
||||
// if out buffer too small.
|
||||
//
|
||||
// History: this used to be downgraded to non-QoS Data (subtype 0) because
|
||||
// esp_wifi_80211_tx() rejects QoS Data ("unsupport QoS frame type" / esp_err
|
||||
// 258) and an attempted linker-override bypass (old main/wifi_patches.c)
|
||||
// didn't work. Restored to QoS Data now that main.c transmits via
|
||||
// esp_wifi_80211_tx_custom() instead, which bypasses that gate entirely
|
||||
// (see tx_custom.c) - so there's no longer a reason to deviate from the
|
||||
// real frame format.
|
||||
// qos=true -> QoS Data (subtype 8, 26-byte header) for esp_wifi_80211_tx_custom()
|
||||
// qos=false -> plain Data (subtype 0, 24-byte header) which the STANDARD
|
||||
// esp_wifi_80211_tx() accepts (used for the standard-TX isolation test)
|
||||
int dot11p_build_frame(const uint8_t *gn_payload, int gn_len,
|
||||
const uint8_t src_mac[6],
|
||||
uint8_t *out, size_t out_len, bool qos);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,85 @@
|
||||
#include "geonet.h"
|
||||
#include <string.h>
|
||||
|
||||
int geonet_wrap_shb(const uint8_t *its_payload, int its_len,
|
||||
const uint8_t mac[6], uint8_t station_type,
|
||||
int32_t latitude_tenmicrodeg, int32_t longitude_tenmicrodeg,
|
||||
uint16_t btp_dest_port,
|
||||
uint8_t *out, size_t out_len)
|
||||
{
|
||||
// GN Basic Header (4) + GN Common Header (8) + SHB source LPV (24)
|
||||
// + BTP-B header (4) + ITS payload
|
||||
int total = 4 + 8 + 24 + 4 + its_len;
|
||||
if ((size_t)total > out_len) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t *p = out;
|
||||
|
||||
// ---- GN Basic Header (4 bytes) ---- (EN 302 636-4-1 clause 9.6)
|
||||
*p++ = (uint8_t)((1 << 4) | 1); // version=1, NextHeader=1 (Common Header, unsecured)
|
||||
*p++ = 0x00; // reserved
|
||||
*p++ = 0x83; // lifetime (~60s in the base/multiplier encoding) - tune if needed
|
||||
*p++ = 1; // remaining hop limit = 1 (SHB single-hop; matches CAM in the Rust reference)
|
||||
|
||||
// ---- GN Common Header (8 bytes) ---- (clause 9.7)
|
||||
*p++ = (uint8_t)((2 << 4) | 0); // NextHeader=2 (BTP-B), reserved nibble
|
||||
// HeaderType=5 (TSB), HeaderSubtype=0 (SINGLE_HOP) per table 9 - this is
|
||||
// the actual encoding for single-hop broadcast. An earlier version of
|
||||
// this code used (2,0), which is GEOUNICAST - wrong header type entirely
|
||||
// for a broadcast frame; real receivers would try to match the
|
||||
// destination-address extended header GeoUnicast expects and mishandle
|
||||
// or reject the packet.
|
||||
*p++ = (uint8_t)((5 << 4) | 0);
|
||||
*p++ = 0x02; // traffic class: SCF=0, ChannelOffload=0, TC-ID=2 (clause 9.7.5)
|
||||
*p++ = 0x80; // flags: bit0 = "is mobile" station (clause 9.7.2)
|
||||
// Payload length = what follows the WHOLE GeoNetworking header
|
||||
// (Basic+Common+Extended), i.e. BTP-B header + ITS payload only - does
|
||||
// NOT include the 24-byte extended header itself. An earlier version of
|
||||
// this code wrongly added the 24 bytes in here too.
|
||||
uint16_t payload_len = (uint16_t)(4 + its_len);
|
||||
*p++ = (uint8_t)(payload_len >> 8);
|
||||
*p++ = (uint8_t)(payload_len & 0xFF);
|
||||
*p++ = 1; // max hop limit = 1, matches basic header RHL (SHB single-hop)
|
||||
*p++ = 0x00; // reserved
|
||||
|
||||
// ---- SHB extended header: Source Long Position Vector (24 bytes) ----
|
||||
// (clause 9.5.2). GN_ADDR (8 bytes) is itself structured, not a raw
|
||||
// pseudonym (clause 9.5.1): bit0 M-flag(0=auto-derived), bits1-5 ITS-S
|
||||
// type (5-bit), bits6-15 reserved(=0), then octets2-7 = MID, which is
|
||||
// defined to BE the link-layer (802.11) address - so this must match
|
||||
// the source address dot11p_build_frame uses, not just "look similar."
|
||||
uint8_t gn_addr[8];
|
||||
gn_addr[0] = (uint8_t)((0 << 7) | ((station_type & 0x1F) << 2)); // M=0, ST=station_type, top 2 reserved bits=0
|
||||
gn_addr[1] = 0x00; // remaining 8 reserved bits
|
||||
memcpy(&gn_addr[2], mac, 6); // MID = link-layer address
|
||||
memcpy(p, gn_addr, 8); p += 8;
|
||||
// Timestamp (4 bytes, ms since 2004-01-01 mod 2^32) - placeholder 0,
|
||||
// same caveat as detectionTime in denm.c.
|
||||
memset(p, 0, 4); p += 4;
|
||||
// Latitude/Longitude (4+4 bytes, signed, big-endian, 1/10 microdegree) -
|
||||
// fixed-width binary fields, not UPER bit-packed.
|
||||
uint32_t lat_u = (uint32_t)latitude_tenmicrodeg;
|
||||
*p++ = (uint8_t)(lat_u >> 24); *p++ = (uint8_t)(lat_u >> 16);
|
||||
*p++ = (uint8_t)(lat_u >> 8); *p++ = (uint8_t)(lat_u);
|
||||
uint32_t lon_u = (uint32_t)longitude_tenmicrodeg;
|
||||
*p++ = (uint8_t)(lon_u >> 24); *p++ = (uint8_t)(lon_u >> 16);
|
||||
*p++ = (uint8_t)(lon_u >> 8); *p++ = (uint8_t)(lon_u);
|
||||
// PAI(1 bit) + Speed(15 bits), packed into 2 bytes: 0 = PAI false,
|
||||
// speed 0 - which is actually correct semantics for a STATIONARY
|
||||
// vehicle beacon, not just a placeholder.
|
||||
*p++ = 0x00; *p++ = 0x00;
|
||||
// Heading (16 bits, 0.1 degree units): 0 = due north / unavailable
|
||||
*p++ = 0x00; *p++ = 0x00;
|
||||
|
||||
// ---- BTP-B header (4 bytes) ----
|
||||
*p++ = (uint8_t)(btp_dest_port >> 8);
|
||||
*p++ = (uint8_t)(btp_dest_port & 0xFF);
|
||||
*p++ = 0x00; *p++ = 0x00; // destination port info, unused for BTP-B
|
||||
|
||||
// ---- ITS payload (DENM UPER bytes) ----
|
||||
memcpy(p, its_payload, its_len);
|
||||
p += its_len;
|
||||
|
||||
return (int)(p - out);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef GEONET_H
|
||||
#define GEONET_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Wraps an ITS application payload (e.g. from denm_encode) with a minimal
|
||||
// GeoNetworking Basic Header + Common Header + Single-Hop-Broadcast
|
||||
// extended header (HeaderType=TSB(5), HeaderSubtype=SINGLE_HOP(0), per
|
||||
// ETSI EN 302 636-4-1 table 9), then prepends a BTP-B header addressed to
|
||||
// the DENM service port (2002).
|
||||
//
|
||||
// `mac` is the 6-byte pseudonym/link-layer address - pass the SAME address
|
||||
// you hand to dot11p_build_frame's src address, since GN_ADDR's MID field
|
||||
// (the last 6 bytes of the 8-byte GN_ADDR) is defined to BE that
|
||||
// link-layer address (EN 302 636-4-1 clause 9.5.1). `station_type` is the
|
||||
// 5-bit ITS-S type from the same clause (5 = passengerCar) and gets packed
|
||||
// into GN_ADDR alongside the address.
|
||||
//
|
||||
// `latitude_tenmicrodeg`/`longitude_tenmicrodeg` go into the Source Long
|
||||
// Position Vector (clause 9.5.2) as plain 32-bit signed big-endian fields -
|
||||
// NOT UPER bit-packed like the DENM payload's position fields, this is a
|
||||
// fixed-width binary protocol. Pass the SAME values you gave denm_encode's
|
||||
// eventPosition, so the GN-layer position and the DENM's own claimed
|
||||
// position agree.
|
||||
//
|
||||
// Deliberate simplification: real DENM dissemination normally uses
|
||||
// GeoBroadcast (GBC, HeaderType=4) so RSUs/OBUs can forward it across an
|
||||
// area - that needs a sequence number + geo-area fields this skeleton
|
||||
// doesn't build yet. Single-hop broadcast is simpler and is the
|
||||
// best-tested decode path in the receiver firmware you already have
|
||||
// working (same extended header shape as CAM). Fine for a single-vehicle
|
||||
// beacon; revisit if you need real multi-hop forwarding later.
|
||||
//
|
||||
// `btp_dest_port` is the BTP-B destination port for the service being carried
|
||||
// (ETSI TS 103 248): 2001 = CAM, 2002 = DENM, 2003 = MAPEM, 2004 = SPATEM, ...
|
||||
//
|
||||
// Returns bytes written, or -1 if out buffer too small.
|
||||
int geonet_wrap_shb(const uint8_t *its_payload, int its_len,
|
||||
const uint8_t mac[6], uint8_t station_type,
|
||||
int32_t latitude_tenmicrodeg, int32_t longitude_tenmicrodeg,
|
||||
uint16_t btp_dest_port,
|
||||
uint8_t *out, size_t out_len);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,262 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_log.h"
|
||||
#include "hal/modem_syscon_ll.h" // modem_syscon_ll_enable_fe_40m_clock() - see initialize_wifi
|
||||
#include "denm.h"
|
||||
#include "cam.h"
|
||||
#include "geonet.h"
|
||||
#include "dot11p.h"
|
||||
#include "tx_custom.h"
|
||||
|
||||
static const char *TAG = "obu-tx";
|
||||
|
||||
// CAM beacon: transmit a Cooperative Awareness Message every TX_INTERVAL_MS,
|
||||
// unconditionally (no hazard-light gating - CAM is a continuous beacon, unlike
|
||||
// the event-triggered DENM). Matches the working Rust reference
|
||||
// (esp32-c_its-companion, feat/tx-cam), which beacons CAM on 5900 MHz.
|
||||
|
||||
// ISOLATION TEST for whether tx_custom.c is the blocker.
|
||||
// 1 = transmit via the STANDARD, well-tested esp_wifi_80211_tx() using a
|
||||
// plain (non-QoS) Data frame, which that API accepts. This path is known
|
||||
// to actually key the PA. If the sniffer sees frames with this = 1 but
|
||||
// not with = 0, then tx_custom.c (its reverse-engineered driver-struct
|
||||
// offsets) is the problem, not the RF/channel/regulatory setup.
|
||||
// 0 = original path: QoS Data frame via esp_wifi_80211_tx_custom().
|
||||
// Non-QoS Data is non-standard for ITS-G5, but this is purely a "does any RF
|
||||
// leave the chip" test - your capture-all sniffer logs it regardless.
|
||||
//
|
||||
// A/B TEST for the bursty-SDR symptom. Console is stable and tx_custom returns
|
||||
// OK every second, but the SDR only sees sporadic bursts - the fingerprint of
|
||||
// tx_custom.c's reverse-engineered driver-struct offsets not matching THIS IDF
|
||||
// (v5.5.4) as opposed to the reference's bundled IDF. Setting this to 1 routes
|
||||
// TX through the official, well-tested esp_wifi_80211_tx() (non-QoS Data), which
|
||||
// uses NO reverse-engineered structs. If the SDR becomes a steady 1 Hz with
|
||||
// this = 1, tx_custom's struct layout is confirmed as the culprit.
|
||||
#define USE_STANDARD_TX 1
|
||||
|
||||
// Target frequency: 5900 MHz (ITS-G5 G5-CCH, channel 180). This is what the
|
||||
// working Rust reference transmits on, proving the C5 PA reaches it despite the
|
||||
// 5885 datasheet max. The reference sets band-mode 5G, then phy_11p_set +
|
||||
// phy_change_channel(5900) directly - it does NOT call esp_wifi_set_channel at
|
||||
// all, so we don't either (channel 180 isn't a normal Wi-Fi channel anyway).
|
||||
#define TX_FREQ_MHZ 5900
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// ---- CAM beacon profile ----
|
||||
#define STATION_ID 0x0BADC0DE // placeholder 32-bit station id - pick your own
|
||||
#define STATION_TYPE 5 // passengerCar (TS 102 894-2 StationType)
|
||||
#define VEHICLE_LENGTH_DM 40 // VehicleLengthValue, 10cm steps (4.0 m)
|
||||
#define VEHICLE_WIDTH_DM 18 // VehicleWidth, 10cm steps (1.8 m)
|
||||
#define BTP_PORT_CAM 2001 // BTP-B destination port for CAM (ETSI TS 103 248)
|
||||
#define TX_INTERVAL_MS 1000 // CAM beacon period (1 Hz; ITS allows 1-10 Hz)
|
||||
|
||||
// Bench location, hardcoded since there's no GNSS module wired in yet and
|
||||
// the unit is genuinely stationary here: 53°33'16.8"N 10°01'20.6"E, in
|
||||
// 1/10-microdegree units (decimal_degrees * 10,000,000). Replace with real
|
||||
// GNSS output once you have a fix source; until then this beats 0/0
|
||||
// ("Null Island"), which is an obvious placeholder-tell on any map.
|
||||
#define BENCH_LATITUDE_TENMICRODEG 535546667
|
||||
#define BENCH_LONGITUDE_TENMICRODEG 100223889
|
||||
|
||||
// Single source of truth for the pseudonym/link-layer address: used both as
|
||||
// the 802.11 source MAC (Addr2) and as GN_ADDR's MID field, since the GN
|
||||
// spec defines those as being the same address. Locally-administered bit
|
||||
// set (0x02) per normal MAC convention. Fixed/non-rotating for now - real
|
||||
// stacks rotate this every 5-15 min for privacy.
|
||||
static const uint8_t pseudonym_mac[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x01};
|
||||
|
||||
// Undocumented libphy.a calls that push the radio into 802.11p OCB mode on
|
||||
// the 5.9 GHz ITS-G5 band. See docs/04-transmit-setup.md for source + what
|
||||
// to do if the linker can't find these symbols in your ESP-IDF version.
|
||||
extern void phy_11p_set(int enable, int unused);
|
||||
extern void phy_change_channel(int freq_mhz, int bw_mode, int sec_chan_offset, int unused);
|
||||
|
||||
static void send_cam(void)
|
||||
{
|
||||
// GenerationDeltaTime is TimestampIts mod 65536 (ms). No RTC/GNSS time here,
|
||||
// so use a free-running ms counter that advances one beacon-interval per
|
||||
// send. It wraps at 65536, which is exactly the field's defined behaviour.
|
||||
static uint16_t gen_delta = 0;
|
||||
|
||||
uint8_t frame[300];
|
||||
cam_fields_t fields = {
|
||||
.station_id = STATION_ID,
|
||||
.station_type = STATION_TYPE,
|
||||
.generation_delta_time = gen_delta,
|
||||
.latitude_tenmicrodeg = BENCH_LATITUDE_TENMICRODEG,
|
||||
.longitude_tenmicrodeg = BENCH_LONGITUDE_TENMICRODEG,
|
||||
.speed_cm_s = 0, // stationary
|
||||
.heading_ddeg = 3601, // HeadingValue unavailable (no heading source)
|
||||
.vehicle_length_dm = VEHICLE_LENGTH_DM,
|
||||
.vehicle_width_dm = VEHICLE_WIDTH_DM,
|
||||
};
|
||||
gen_delta += TX_INTERVAL_MS;
|
||||
|
||||
uint8_t cam_payload[96];
|
||||
int cam_len = cam_encode(&fields, cam_payload, sizeof(cam_payload));
|
||||
|
||||
uint8_t gn_payload[160];
|
||||
int gn_len = geonet_wrap_shb(cam_payload, cam_len, pseudonym_mac, STATION_TYPE,
|
||||
BENCH_LATITUDE_TENMICRODEG, BENCH_LONGITUDE_TENMICRODEG,
|
||||
BTP_PORT_CAM, gn_payload, sizeof(gn_payload));
|
||||
|
||||
// qos=false for the standard-TX path (esp_wifi_80211_tx accepts only non-QoS
|
||||
// Data - which is exactly what the Rust reference transmits); qos=true would
|
||||
// be a real ITS-G5 QoS Data frame for the tx_custom path.
|
||||
int frame_len = dot11p_build_frame(gn_payload, gn_len, pseudonym_mac, frame, sizeof(frame),
|
||||
USE_STANDARD_TX ? false : true);
|
||||
|
||||
// PHY/OCB/channel is configured ONCE at boot in app_main and left alone,
|
||||
// matching the working Rust reference (band-mode 5G + phy_11p_set +
|
||||
// phy_change_channel(5900), set once).
|
||||
|
||||
if (frame_len > 0) {
|
||||
#if USE_STANDARD_TX
|
||||
// Standard, well-tested raw-TX API with a non-QoS Data frame - the same
|
||||
// transmit path the Rust reference uses (esp-radio send_raw_frame wraps
|
||||
// esp_wifi_80211_tx). err 258 ("unsupport QoS frame type") would mean the
|
||||
// frame wasn't built as non-QoS.
|
||||
esp_err_t err = esp_wifi_80211_tx(WIFI_IF_STA, frame, frame_len, true);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_80211_tx (standard) failed: %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "CAM sent via STANDARD tx (%d bytes) @ %d MHz genDeltaT=%u", frame_len, TX_FREQ_MHZ, gen_delta);
|
||||
}
|
||||
#else
|
||||
// tx_custom path: submits to the driver's internal HMAC TX path,
|
||||
// bypassing the QoS-frame gate. 11A legacy OFDM, 12M rate.
|
||||
wifi_tx_rate_config_t tx_rate_cfg = {
|
||||
.phymode = WIFI_PHY_MODE_11A,
|
||||
.rate = WIFI_PHY_RATE_12M,
|
||||
.ersu = false,
|
||||
.dcm = false,
|
||||
};
|
||||
esp_err_t err = esp_wifi_80211_tx_custom(WIFI_IF_STA, frame, frame_len, true,
|
||||
&tx_rate_cfg, WIFI_BAND_5G, WIFI_BW20);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_80211_tx_custom failed: %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "CAM sent (%d bytes) @ %d MHz genDeltaT=%u", frame_len, TX_FREQ_MHZ, gen_delta);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
ESP_LOGE(TAG, "CAM frame build failed (cam_len=%d gn_len=%d)", cam_len, gn_len);
|
||||
}
|
||||
}
|
||||
|
||||
static void tx_task(void *arg)
|
||||
{
|
||||
while (1) {
|
||||
// CAM is a continuous beacon - send every interval, unconditionally.
|
||||
send_cam();
|
||||
vTaskDelay(pdMS_TO_TICKS(TX_INTERVAL_MS));
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(nvs_flash_init());
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
|
||||
// Enable the modem FRONT-END 40 MHz clock BEFORE esp_wifi_init(). This is
|
||||
// the one step the proven-working receiver firmware
|
||||
// (its-g5-receiver-firmware_txenabled, main/main.c -> initialize_wifi())
|
||||
// performs that this OBU was missing. Without the FE clock enabled the
|
||||
// 5 GHz front-end / transmit chain is not fully clocked - which matches the
|
||||
// exact symptom here: the radio calibrates (boot RF ping) and receives
|
||||
// fine, but data frames are accepted by the API and never actually key the
|
||||
// PA. This is a low-level modem_syscon register write via the HAL LL layer,
|
||||
// copied verbatim from the reference firmware.
|
||||
modem_syscon_ll_enable_fe_40m_clock(&MODEM_SYSCON, 1);
|
||||
|
||||
wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); // match reference initialize_wifi()
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
// ---- Regulatory / TX-authorization override -----------------------------
|
||||
// THE fix for "RX works but TX is silent". By default the driver uses
|
||||
// WIFI_COUNTRY_POLICY_AUTO, whose 5 GHz regulatory table does NOT authorize
|
||||
// transmit on the 5.9 GHz ITS band (and treats DFS channels as no-IR /
|
||||
// radar-gated). Receiving is never gated - which is exactly why the sniffer
|
||||
// hears traffic but our own frames never key the PA, and why the only RF
|
||||
// seen from this board is the uninhibited PHY-calibration burst at boot.
|
||||
//
|
||||
// Switching to WIFI_COUNTRY_POLICY_MANUAL with an explicit 5 GHz channel
|
||||
// mask (wifi_5g_channel_mask, which only takes effect under manual policy)
|
||||
// tells the driver these channels are permitted and lifts the transmit
|
||||
// gate. WIFI_CHANNEL_177 (BIT(28)) = 5885 MHz; we enable the full 5 GHz set
|
||||
// (bits 1..28) so both the primer channel and the target are authorized.
|
||||
// Manual policy = the operator asserts regulatory responsibility, which is
|
||||
// appropriate for licensed/university research on the ITS band.
|
||||
wifi_country_t ctry = {
|
||||
.cc = "US", // nominal under manual policy
|
||||
.schan = 1,
|
||||
.nchan = 11,
|
||||
.policy = WIFI_COUNTRY_POLICY_MANUAL,
|
||||
.wifi_5g_channel_mask = 0x1FFFFFFE, // all 5 GHz channels, bits 1..28 (incl. 140 and 177)
|
||||
};
|
||||
esp_err_t ctry_err = esp_wifi_set_country(&ctry);
|
||||
if (ctry_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_set_country(MANUAL) failed: %d (continuing)", ctry_err);
|
||||
}
|
||||
// Ensure the PA runs at full configured power (not a reduced regulatory
|
||||
// default). Units are 0.25 dBm; 80 = 20 dBm.
|
||||
esp_wifi_set_max_tx_power(80);
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Force the dual-band C5 onto its 5 GHz PHY. This MUST be called after
|
||||
// esp_wifi_start() - calling it before returns ESP_ERR_WIFI_NOT_STARTED
|
||||
// (0x3002 / 12290). Locking the band to 5G explicitly keeps the driver
|
||||
// from ever falling back to 2.4 GHz ch1 (the old "stuck at primary=1"
|
||||
// symptom), which would key the wrong PHY and make us inaudible to a
|
||||
// 5.9 GHz sniffer. Valid 5 GHz channels on the C5 are 36..177. Not
|
||||
// ESP_ERROR_CHECK'd: log and continue if a given IDF build differs.
|
||||
esp_err_t band_err = esp_wifi_set_band_mode(WIFI_BAND_MODE_5G_ONLY);
|
||||
if (band_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_set_band_mode(5G_ONLY) failed: %d (continuing)", band_err);
|
||||
}
|
||||
|
||||
// Disable Wi-Fi power save. An unassociated STA with the default
|
||||
// WIFI_PS_MIN_MODEM power save sleeps its radio between beacons it will
|
||||
// never receive (we're not joined to any AP), and drops outbound raw
|
||||
// frames while asleep - the classic "esp_wifi_80211_tx returns OK but
|
||||
// nothing goes on air". Must be called after esp_wifi_start().
|
||||
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
|
||||
|
||||
// Enable promiscuous mode. This is the single most important change: our
|
||||
// *receiver* firmware (V2X2MAP) - which demonstrably works at 5.9 GHz,
|
||||
// 13k+ frames captured - runs promiscuous, and ESP-IDF documents that the
|
||||
// raw-frame TX path only actually emits when the MAC is in promiscuous
|
||||
// mode or associated to an AP. Plain STA (what this firmware used before)
|
||||
// is neither, so frames were being accepted by the API and then dropped
|
||||
// by the driver. Putting the OBU in the same radio state as the working
|
||||
// sniffer, then injecting, is the whole fix. Must be after start.
|
||||
ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));
|
||||
|
||||
// Force 802.11p OCB mode on the ITS-G5 channel, exactly like the working
|
||||
// Rust reference (esp32-c_its-companion, src/radio.rs setup_wifi_sniffer):
|
||||
// enable 802.11p, then jump straight to the target frequency. With band-mode
|
||||
// already locked to 5 GHz above, NO esp_wifi_set_channel priming is needed -
|
||||
// the reference doesn't call it, and channel 180 (5900 MHz) isn't a normal
|
||||
// Wi-Fi channel anyway. phy_change_channel takes the frequency in MHz.
|
||||
ESP_LOGI(TAG, "about to call phy_11p_set...");
|
||||
phy_11p_set(1, 0);
|
||||
ESP_LOGI(TAG, "phy_11p_set returned, about to call phy_change_channel(%d)...", TX_FREQ_MHZ);
|
||||
phy_change_channel(TX_FREQ_MHZ, 1, 0, 0);
|
||||
ESP_LOGI(TAG, "phy_change_channel returned");
|
||||
|
||||
ESP_LOGW(TAG, "OCB @ %d MHz - CAM beacon armed, transmitting every %d ms",
|
||||
TX_FREQ_MHZ, TX_INTERVAL_MS);
|
||||
|
||||
xTaskCreate(tx_task, "tx_task", 4096, NULL, 5, NULL);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copied verbatim (no logic changes) from opentrafficmap/its-g5-receiver-firmware_txenabled,
|
||||
// main/tx_custom.c (https://codeberg.org/opentrafficmap/its-g5-receiver-firmware_txenabled),
|
||||
// same authors as the receiver firmware (V2X2MAP) already used on the RX side of this
|
||||
// project. Same chip (ESP32-C5), same class of problem (getting a raw 802.11 frame past
|
||||
// esp_wifi_80211_tx()'s built-in frame-type gate), and a proven-different approach from our
|
||||
// own abandoned main/wifi_patches.c attempt - see docs/04-transmit-setup.md for why that one
|
||||
// didn't work and why this one is expected to.
|
||||
//
|
||||
// WHAT THIS DOES DIFFERENTLY FROM esp_wifi_80211_tx(): it doesn't call the public API at all.
|
||||
// It reaches one layer deeper into the closed WiFi driver - ic_ebuf_alloc() (allocates an
|
||||
// internal driver buffer), ieee80211_post_hmac_tx() (submits that buffer straight to the MAC
|
||||
// for transmission) - and never goes through the code path that contains the QoS-frame-type
|
||||
// sanity check that was rejecting us. Notice line "esp_err_t result = 0;//ieee80211_raw_frame_
|
||||
// sanity_check(...)" below: the upstream authors don't override that check (like our old
|
||||
// wifi_patches.c tried to), they just never call the function that calls it.
|
||||
//
|
||||
// REAL RISK, carried over from upstream, not introduced by us: this skips ALL frame-type and
|
||||
// sanity validation, same caveat as our old override attempt. A malformed frame from a bug
|
||||
// elsewhere in our own code could behave worse (silent corruption, crash) than a clean
|
||||
// rejection.
|
||||
//
|
||||
// UNVERIFIED FOR OUR EXACT TOOLCHAIN - things worth checking before trusting this blindly:
|
||||
// 1. The symbols this depends on (ieee80211_post_hmac_tx, ic_ebuf_alloc, ic_get_default_sched,
|
||||
// g_osi_funcs_p, g_wifi_global_lock) are undocumented/internal. We confirmed via `nm`
|
||||
// earlier that ieee80211_raw_frame_sanity_check exists in OUR esp32c5/IDF libnet80211.a -
|
||||
// we have NOT yet independently confirmed these other four/five symbols exist in our
|
||||
// exact ESP-IDF version (as opposed to whatever version the upstream repo's pinned
|
||||
// esp-idf submodule uses). If the linker can't find one of these, that's the first thing
|
||||
// to check - see docs/04-transmit-setup.md for the nm command.
|
||||
// 2. x_eb_txdesc_t / x_middle_data_t / x_ebuf_t below are REVERSE-ENGINEERED struct layouts
|
||||
// of closed-source internal WiFi driver types, pinned only by a sizeof() static_assert -
|
||||
// that assert catches a total-size mismatch but NOT a field-order/semantic mismatch if a
|
||||
// different IDF version shuffled internal fields while keeping the same total size. If our
|
||||
// ESP-IDF version differs meaningfully from upstream's, this could compile and link fine
|
||||
// but write to the wrong offsets internally. Worth checking `idf.py --version` against
|
||||
// whatever esp-idf commit opentrafficmap's repo has pinned as a submodule, as a rough
|
||||
// compatibility signal (not a guarantee either way).
|
||||
#include "esp_private/wifi_os_adapter.h"
|
||||
#include "esp_wifi.h"
|
||||
|
||||
#include "tx_custom.h"
|
||||
|
||||
esp_err_t ieee80211_raw_frame_sanity_check(wifi_interface_t ifx, const void *buffer, int32_t len, bool en_sys_seq);
|
||||
esp_err_t ieee80211_post_hmac_tx(void *ebuf);
|
||||
void *ic_ebuf_alloc(const void *packet, uint32_t unknown, uint32_t len);
|
||||
void *ic_get_default_sched(void);
|
||||
|
||||
extern wifi_osi_funcs_t *g_osi_funcs_p;
|
||||
extern void *g_wifi_global_lock;
|
||||
|
||||
typedef struct x_eb_txdesc
|
||||
{
|
||||
uint32_t flags;
|
||||
uint32_t field_4;
|
||||
uint32_t field_8;
|
||||
uint8_t rate;
|
||||
uint8_t field_d;
|
||||
uint8_t field_e;
|
||||
uint8_t field_f;
|
||||
uint32_t field_10;
|
||||
uint32_t field_14;
|
||||
uint32_t timestamp;
|
||||
void* sched;
|
||||
uint32_t field_20;
|
||||
uint32_t field_24;
|
||||
uint32_t field_28;
|
||||
union {
|
||||
uint32_t field_2c_32;
|
||||
struct {
|
||||
uint8_t field_2c;
|
||||
uint8_t field_2d;
|
||||
uint8_t field_2e;
|
||||
uint8_t field_2f;
|
||||
};
|
||||
};
|
||||
union {
|
||||
uint32_t field_30_32;
|
||||
struct {
|
||||
uint8_t field_30;
|
||||
uint8_t field_31;
|
||||
uint8_t field_32;
|
||||
uint8_t field_33;
|
||||
};
|
||||
};
|
||||
uint32_t field_34;
|
||||
uint32_t field_38;
|
||||
uint32_t field_3c;
|
||||
uint32_t field_40;
|
||||
uint32_t field_44;
|
||||
} x_eb_txdesc_t;
|
||||
static_assert(sizeof(x_eb_txdesc_t) == 0x48);
|
||||
|
||||
typedef struct x_middle_data
|
||||
{
|
||||
uint32_t field_40;
|
||||
uint8_t* buf;
|
||||
uint32_t field_48;
|
||||
uint32_t field_4c;
|
||||
} x_middle_data_t;
|
||||
static_assert(sizeof(x_middle_data_t) == 0x10);
|
||||
|
||||
typedef struct x_ebuf
|
||||
{
|
||||
uint32_t field_0;
|
||||
x_middle_data_t* ds_head;
|
||||
x_middle_data_t* ds_tail;
|
||||
uint16_t field_c;
|
||||
uint16_t field_e;
|
||||
uint32_t extra_data_start;
|
||||
uint16_t header_length;
|
||||
uint32_t data_length;
|
||||
uint16_t field_1c;
|
||||
uint8_t alloc_type;
|
||||
uint8_t field_1f;
|
||||
uint32_t field_20;
|
||||
uint8_t field_24;
|
||||
uint8_t field_25;
|
||||
uint8_t field_26;
|
||||
uint8_t field_27;
|
||||
uint32_t field_28;
|
||||
uint8_t field_2c;
|
||||
uint32_t field_30;
|
||||
uint32_t next_free;
|
||||
x_eb_txdesc_t* txdesc;
|
||||
uint16_t field_3c;
|
||||
uint8_t field_3e;
|
||||
uint8_t field_3f;
|
||||
} x_ebuf_t;
|
||||
static_assert(sizeof(x_ebuf_t) == 0x40);
|
||||
|
||||
esp_err_t esp_wifi_80211_tx_custom(wifi_interface_t ifx, const void *buffer, int32_t len, bool en_sys_seq, wifi_tx_rate_config_t *tx_rate_config, wifi_band_t band, wifi_bandwidth_t bw)
|
||||
{
|
||||
esp_err_t result = 0;//ieee80211_raw_frame_sanity_check(ifx, buffer, len, en_sys_seq);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
g_osi_funcs_p->_mutex_lock(g_wifi_global_lock);
|
||||
x_ebuf_t* eb = ic_ebuf_alloc(buffer, 1, len);
|
||||
|
||||
if (eb)
|
||||
{
|
||||
//eb->data_length = len - 0x1a;
|
||||
eb->data_length = 0;
|
||||
x_eb_txdesc_t *txdesc_1 = eb->txdesc;
|
||||
//eb->header_length = 0x1a;
|
||||
eb->header_length = len;
|
||||
txdesc_1->flags |= 0x4000;
|
||||
txdesc_1->sched = ic_get_default_sched();
|
||||
wifi_phy_rate_t rate = tx_rate_config->rate;
|
||||
x_eb_txdesc_t *txdesc = eb->txdesc;
|
||||
|
||||
if (rate)
|
||||
txdesc->rate = (char)rate;
|
||||
else if (band != WIFI_BAND_5G)
|
||||
txdesc->rate = 0;
|
||||
else
|
||||
txdesc->rate = (char)WIFI_PHY_RATE_6M;
|
||||
|
||||
wifi_phy_mode_t phymode = tx_rate_config->phymode;
|
||||
|
||||
if (phymode == WIFI_PHY_MODE_HE20)
|
||||
{
|
||||
txdesc->flags |= 0x80000000;
|
||||
txdesc->field_2f =
|
||||
(char)((((uint32_t)tx_rate_config->ersu + 6) & 0xf) << 3)
|
||||
| (txdesc->field_2f & 0x87);
|
||||
|
||||
if ((uint32_t)tx_rate_config->dcm)
|
||||
txdesc->field_31 |= 0x80;
|
||||
}
|
||||
else if (phymode == WIFI_PHY_MODE_VHT20)
|
||||
txdesc->flags |= 0x1000000;
|
||||
|
||||
// No idea if this is correct, but this is what the original code does...
|
||||
uint32_t bw_is_bw40 = bw == WIFI_BW40;
|
||||
txdesc->field_8 = (bw_is_bw40 << 0xf) | (txdesc->field_8 & 0xffff7fff);
|
||||
|
||||
if (en_sys_seq)
|
||||
txdesc->flags |= 1;
|
||||
|
||||
txdesc->field_10 =
|
||||
(txdesc->field_10 & 0xfff3ffff) | ((ifx & WIFI_IF_MAX) << 0x12);
|
||||
txdesc->field_14 = 0x100;
|
||||
|
||||
ieee80211_post_hmac_tx(eb);
|
||||
g_osi_funcs_p->_mutex_unlock(g_wifi_global_lock);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = ESP_ERR_NO_MEM;
|
||||
g_osi_funcs_p->_mutex_unlock(g_wifi_global_lock);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copied from opentrafficmap/its-g5-receiver-firmware_txenabled, main/tx_custom.h.
|
||||
// See tx_custom.c for what this does and why we pulled it in.
|
||||
#pragma once
|
||||
|
||||
#include "esp_wifi.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
esp_err_t esp_wifi_80211_tx_custom(wifi_interface_t ifx, const void *buffer, int32_t len, bool en_sys_seq, wifi_tx_rate_config_t *tx_rate_config, wifi_band_t band, wifi_bandwidth_t bw);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// RETIRED - no longer built (removed from main/CMakeLists.txt SRCS), kept only
|
||||
// for history. Confirmed not to work: linked cleanly with -Wl,-zmuldefs but
|
||||
// the QoS-frame rejection persisted identically. Also turned out to be based
|
||||
// on the wrong function signature - the real ieee80211_raw_frame_sanity_check
|
||||
// takes (wifi_interface_t ifx, const void *buffer, int32_t len, bool
|
||||
// en_sys_seq), confirmed from opentrafficmap/its-g5-receiver-firmware_txenabled's
|
||||
// main/tx_custom.c, not the 3x int32_t guessed below. Superseded by
|
||||
// tx_custom.c, which bypasses esp_wifi_80211_tx() (and the function that
|
||||
// calls this check) entirely instead of trying to neutralize the check.
|
||||
// See docs/04-transmit-setup.md.
|
||||
|
||||
// Overrides a function inside the closed-source WiFi library that gates
|
||||
// which raw 802.11 frame types esp_wifi_80211_tx() will accept. By default
|
||||
// it only allows beacon/probe-request/probe-response/action and non-QoS
|
||||
// data frames - it explicitly rejects QoS Data (subtype 8), which is what
|
||||
// real ITS-G5/802.11p hardware actually transmits and expects.
|
||||
//
|
||||
// This is the same technique used by ESP32 WiFi-security tools (deauther/
|
||||
// injection projects) to unlock raw frame injection: define a function with
|
||||
// the exact same name as the library's gate, and link with -Wl,-zmuldefs
|
||||
// (see CMakeLists.txt) so the linker accepts having two definitions of the
|
||||
// same symbol instead of erroring with "multiple definition of
|
||||
// `ieee80211_raw_frame_sanity_check'" - and takes this one instead of the
|
||||
// library's.
|
||||
//
|
||||
// Confirmed present for THIS target/IDF version: `nm` on
|
||||
// components/esp_wifi/lib/esp32c5/libnet80211.a (IDF v5.5.4) shows
|
||||
// `ieee80211_raw_frame_sanity_check` as a normal (non-weak) global text
|
||||
// symbol in ieee80211_node.o. The exact argument count/meaning is
|
||||
// reverse-engineered from community ESP32 (Xtensa) deauther tools, not
|
||||
// confirmed byte-for-byte against esp32c5's actual implementation - if
|
||||
// frames still get rejected, or this crashes, the real signature may take
|
||||
// different arguments than assumed here.
|
||||
//
|
||||
// Real risk, not just an inconvenience: this disables ALL sanity checking
|
||||
// on raw frames going through esp_wifi_80211_tx(), not just the QoS-type
|
||||
// gate. Whatever else that check validates (frame length bounds, etc.) is
|
||||
// now unchecked. Malformed frames from a bug elsewhere in this codebase
|
||||
// could behave worse (silent corruption, crash) than they would have with
|
||||
// the check in place, where they'd have just been rejected cleanly.
|
||||
int ieee80211_raw_frame_sanity_check(int32_t arg1, int32_t arg2, int32_t arg3)
|
||||
{
|
||||
(void)arg1;
|
||||
(void)arg2;
|
||||
(void)arg3;
|
||||
return 0; // 0 = "frame is sane" - always pass
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
CONFIG_IDF_TARGET="esp32c5"
|
||||
@@ -1,5 +1,14 @@
|
||||
# obu-firmware — setup & flashing notes
|
||||
|
||||
## Two toolchains - use a dedicated terminal for each
|
||||
|
||||
This project builds against the receiver-firmware's pinned ESP-IDF **6.1**.
|
||||
The separate `obu-cam-transmistter` project builds against the global ESP-IDF
|
||||
**5.5.4**. Exporting both in one PowerShell window fails: the second export
|
||||
inherits the first's `IDF_PYTHON_ENV_PATH` and reports every Python dependency
|
||||
as unmet. Don't run `install.bat` to "fix" that - open a fresh terminal, or
|
||||
clear the state with `$env:IDF_PYTHON_ENV_PATH = $null; $env:IDF_PATH = $null`.
|
||||
|
||||
## Every new PowerShell session
|
||||
|
||||
Activate the toolchain (obu-firmware has no esp-idf of its own — reuse the
|
||||
|
||||
+13
-2
@@ -1,3 +1,8 @@
|
||||
// NOT COMPILED - deliberately absent from main/CMakeLists.txt's SRCS. This firmware no longer
|
||||
// encodes CAM at all: the phone builds and UPER-encodes it and sends the bytes down serial_link,
|
||||
// and this side only GeoNetworking-wraps opaque payloads. The file is kept as the byte-exact
|
||||
// reference the Kotlin encoder (app CamUperCodec.kt) was ported from, so fixes must be applied
|
||||
// here too or the next person porting from it reintroduces the bug.
|
||||
#include "cam.h"
|
||||
#include <string.h>
|
||||
|
||||
@@ -108,8 +113,14 @@ int cam_encode(const cam_fields_t *f, uint8_t *buf, size_t buf_len)
|
||||
// CurvatureConfidence ENUM 8 values -> 3 bits
|
||||
bw_put_bits(&bw, 1023 - (uint32_t)(-1023), 11); // curvatureValue: unavailable(1023)
|
||||
bw_put_bits(&bw, 7, 3); // curvatureConfidence: unavailable(7)
|
||||
// CurvatureCalculationMode ENUM {yawRateUsed,yawRateNotUsed,unavailable} -> 2 bits
|
||||
bw_put_bits(&bw, 2, 2); // unavailable
|
||||
// CurvatureCalculationMode ENUM {yawRateUsed,yawRateNotUsed,unavailable, ...} - note the
|
||||
// extension marker: UPER encodes an extensible ENUMERATED as an extension bit followed by
|
||||
// the root-list index, so this is 1 + 2 = 3 bits, NOT 2. This file previously wrote only the
|
||||
// 2-bit index, which shifted yawRate and the entire low-frequency container one bit early for
|
||||
// any standards-compliant receiver. Harmless between this project's own encoder and decoder
|
||||
// (both had the same error); wrong against every third-party station.
|
||||
bw_put_bits(&bw, 0, 1); // extension bit: value is in the root list
|
||||
bw_put_bits(&bw, 2, 2); // unavailable(2)
|
||||
// YawRate: YawRateValue(-32766..32767)->16 (offset from -32766),
|
||||
// YawRateConfidence ENUM 8 values -> 3 bits
|
||||
bw_put_bits(&bw, 32767 - (uint32_t)(-32766), 16); // yawRateValue: unavailable(32767)
|
||||
|
||||
@@ -72,7 +72,18 @@ static bool send_frame(uint8_t type, const uint8_t *payload, int len)
|
||||
// so the shared buffer (and the four-part write) can't interleave between callers.
|
||||
static uint8_t s_crc_buf[3 + SERIAL_LINK_MAX_PAYLOAD];
|
||||
|
||||
if (s_tx_mutex && xSemaphoreTake(s_tx_mutex, pdMS_TO_TICKS(200)) != pdTRUE) {
|
||||
// No host on the other end: the TX buffer never drains, so every write below would block its
|
||||
// full timeout and this frame is going nowhere regardless. Bail before taking the mutex -
|
||||
// otherwise a burst of promiscuously-captured CAMs holds the lock for hundreds of ms each and
|
||||
// starves the heartbeat, which is exactly what "tx mutex timeout, dropping frame" was.
|
||||
if (!usb_serial_jtag_is_connected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Timeout must exceed the worst-case hold below (4 writes x SERIAL_LINK_WRITE_TIMEOUT_MS),
|
||||
// or a legitimately slow-but-working host makes contending senders drop frames instead of
|
||||
// waiting their turn.
|
||||
if (s_tx_mutex && xSemaphoreTake(s_tx_mutex, pdMS_TO_TICKS(SERIAL_LINK_TX_LOCK_TIMEOUT_MS)) != pdTRUE) {
|
||||
ESP_LOGW(TAG, "send_frame: tx mutex timeout, dropping frame");
|
||||
return false;
|
||||
}
|
||||
@@ -87,9 +98,9 @@ static bool send_frame(uint8_t type, const uint8_t *payload, int len)
|
||||
// Four separate writes rather than one assembled buffer - simplest given payload is
|
||||
// already wherever the caller has it (avoids a second copy of up to 160 bytes).
|
||||
// usb_serial_jtag_write_bytes() blocks up to the given tick timeout if the host isn't
|
||||
// reading fast enough; 100ms is generous for a ~160-byte frame at USB full-speed and keeps
|
||||
// a wedged/disconnected host from hanging the radio TX/RX tasks indefinitely.
|
||||
const TickType_t write_timeout = pdMS_TO_TICKS(100);
|
||||
// reading fast enough; generous for a single frame at USB full-speed, and keeps a wedged
|
||||
// host from hanging the radio TX/RX tasks indefinitely.
|
||||
const TickType_t write_timeout = pdMS_TO_TICKS(SERIAL_LINK_WRITE_TIMEOUT_MS);
|
||||
int wrote = 0;
|
||||
wrote += usb_serial_jtag_write_bytes(sync, sizeof(sync), write_timeout);
|
||||
wrote += usb_serial_jtag_write_bytes(head, sizeof(head), write_timeout);
|
||||
|
||||
@@ -51,6 +51,13 @@
|
||||
// to SERIAL_LINK_MAX_PAYLOAD below.
|
||||
#define SERIAL_LINK_USB_BUF_SIZE 1024
|
||||
|
||||
// Per-write block ceiling, and the mutex acquire timeout that must comfortably exceed the
|
||||
// worst case of one frame (4 writes: sync, head, payload, crc). Keep that relationship if you
|
||||
// change either number - a lock timeout below the max hold turns normal contention into
|
||||
// dropped frames, which is how the heartbeat was being starved by forwarded CAM_RX traffic.
|
||||
#define SERIAL_LINK_WRITE_TIMEOUT_MS 100
|
||||
#define SERIAL_LINK_TX_LOCK_TIMEOUT_MS 600
|
||||
|
||||
// Max CAM payload this link will carry. MUST match SERIAL_LINK_MAX_PAYLOAD in the app's
|
||||
// SerialFrame.kt - a mismatch means every frame above the smaller of the two is rejected by that
|
||||
// side's "length exceeds max, resync" branch, silently.
|
||||
|
||||
Reference in New Issue
Block a user