Files
MicrOBU/obu-cam-transmistter/NOTES.md
T
Ashin Walpola 21e01499d8 Drive the bench CAM beacon round a street loop in St. Georg
The bench transmitter sent a parked car: one fixed position, speed 0,
no heading, a CAM every second. It now simulates a car driving a loop
through six waypoints around Berliner Tor on the real streets, which
makes it a moving target for the app's map and the use case detection
without taking a car out.

The route is generated, not hand-traced. tools/make_route.py asks the
OSRM demo server for a driving route through the waypoints and back to
the first, thins the 371 street points to 103 (none more than 1.5 m off
the line), and writes main/route_points.h. It also saves OSRM's answer
(--offline rebuilds from it) and a map page to check the route before
flashing. Each waypoint is sent with the direction towards the next one:
without it, points on divided roads such as Beim Strohhause snapped to
the opposite carriageway and the loop came out at 8.4 km of U-turns.
With it the loop is 5.2 km, still including two turn-round detours
that OSRM needs to reach the waypoints legally (Borgfelder Strasse /
Anckelmannsplatz, and Nagelsweg / Norderstrasse / Repsoldstrasse).
Route data (c) OpenStreetMap contributors, ODbL.

main/route.c moves the car along the points. It cruises at 50 km/h and
limits each bend to the speed that keeps sideways acceleration at
2 m/s^2, so a junction turn is taken at about 15 km/h and a gentle curve
barely slows it; braking (2 m/s^2) and acceleration (1.5 m/s^2) are
planned across as many points as a bend needs. A simulated lap on the
host is 5.16 km in 7.7 min, averaging 40 km/h.

CAMs now follow the EN 302 637-2 generation rules instead of a fixed
1 Hz: checked every 100 ms, sent on a heading change over 4 degrees, a
move over 4 m, a speed change over 0.5 m/s, or after 1 s - about 3 Hz
at 50 km/h. generationDeltaTime is milliseconds since boot. The
GeoNetworking source position vector now carries the same speed and
heading as the CAM instead of zeros.

NOTES.md gains build and flash steps (including reading a board's app
descriptor first, since both firmwares name their image
obu_firmware.bin) and a section on the simulated drive. The pointer to
docs/04-transmit-setup.md is corrected: that file is not in the repo.

Flashed to the COM8 board and checked on its console: it starts driving
on power-up and sends CAMs with changing position, speed and heading.
Not yet received over the air.
2026-09-16 14:21:44 +02:00

8.0 KiB

OBU transmit firmware - Phase 2 (in progress: HLN-SV DENM beacon)

Build and flash steps are under "Build and flash" below. (docs/04-transmit-setup.md, referenced here and in the sources, is not in the repo.)

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:

$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.

Build and flash

The firmware needs no button, phone or serial connection to start. On every power-up or reset, app_main sets up the radio and starts tx_task, which starts driving the simulated route and sending CAMs on 5900 MHz. A board flashed with this image starts beaconing on its own as soon as it gets power.

In a fresh PowerShell window:

$env:IDF_PYTHON_ENV_PATH = $null; $env:IDF_PATH = $null
. C:\Espressif\frameworks\esp-idf-v5.5.4\export.ps1
cd C:\Users\Ashin\AndroidStudioProjects\MicrOBU\obu-cam-transmistter
  1. Check what the board is running before you flash it. Both this project and obu-firmware name their image obu_firmware.bin, so the file name tells you nothing. Read the app descriptor instead (replace COMx with the board's port):

    python -m esptool --chip esp32c5 -p COMx read_flash 0x10000 0x100 $env:TEMP\desc.bin
    $b = [IO.File]::ReadAllBytes("$env:TEMP\desc.bin")
    function S($o,$n){ [Text.Encoding]::ASCII.GetString($b,$o,$n).Trim([char]0) }
    "time=" + (S 0x70 16) + " date=" + (S 0x80 16) + " idf=" + (S 0x90 32)
    

    idf=v6.1... means the board runs the production OBU (obu-firmware). Don't flash this beacon over it. idf=v5.5.4 means this transmitter, or another bench image.

  2. Build:

    idf.py build
    

    A rebuild after small changes takes about 1-2 minutes. The output is build\obu_firmware.bin.

  3. Flash (the board must be on its UART bridge port or its native USB port):

    idf.py -p COMx flash
    

    The flash is good when esptool prints Hash of data verified., then resets the board.

  4. Check that it's transmitting:

    idf.py -p COMx monitor
    

    (Exit with Ctrl+].) About 1.4 s after reset you should see:

    W obu-tx: OCB @ 5900 MHz - CAM beacon armed, driving a 103-point street loop
    I obu-tx: CAM sent (119 bytes) @ 5900 MHz genDeltaT=1087 pos=53.5531770,10.0220980 50.0 km/h heading 77.0 pt1
    

    After that, a CAM sent line appears about 3 times a second, with the position, speed and heading changing. These lines only mean each frame was handed to the radio. To confirm the frames actually went out, capture them with a second ESP32-C5 running the receiver firmware.

Don't open the console of the production OBU (COM3) while the phone is attached. Opening the port resets that board and drops the phone's USB link. The beacon boards have no phone attached, so this doesn't apply to them.

Simulated drive

The beacon pretends to be a car driving a loop through St. Georg / Berliner Tor in Hamburg, on the real streets. The route comes from six waypoints, which you set in tools/make_route.py.

Changing the route: edit WAYPOINTS in tools/make_route.py, then run

py -3.11 tools/make_route.py

The script asks the OSRM demo server (router.project-osrm.org) for a legal driving route through the waypoints in order and back to the first, then writes three files:

  • main/route_points.h: the street geometry, thinned to points no more than 1.5 m off the line (currently 103 points).
  • tools/route_osrm.json: OSRM's raw answer. --offline rebuilds the header from it without the network.
  • tools/route_map.html: the route on an OpenStreetMap map. Open it in a browser and check it before building.

Then build and flash as above. Route data (c) OpenStreetMap contributors, ODbL; routing by OSRM.

Things to know about the routing:

  • OSRM follows one-way streets and turn bans, so the loop can be longer than the waypoints suggest. The current one is 5.2 km, with two turn-round detours: a loop via Borgfelder Straße and Anckelmannsplatz between wp2 and wp3, and one round Nagelsweg, Norderstraße and Repsoldstraße between wp5 and wp6. To avoid a detour, move the waypoint on either side of it.
  • Each waypoint is sent with the direction towards the next one. Without it, points on divided roads (Beim Strohhause, for example) snap to the carriageway going the other way, and the loop grows to 8.4 km of U-turns.

How the car drives (main/route.c):

  • Speed: it cruises at 50 km/h (CRUISE_MPS in main/main.c). Each bend gets a speed limit from its radius, keeping sideways acceleration at 2 m/s², so a 90° junction turn is taken at about 15 km/h and a gentle curve barely slows the car. It never drops below 10 km/h (MIN_CORNER_MPS). It brakes at 2 m/s² and accelerates at 1.5 m/s², planning braking across as many points as a bend needs. A lap takes about 7.7 min, averaging 40 km/h.
  • Heading: the compass bearing of the current straight piece. It changes gradually through curves but jumps at sharp junction turns.
  • When CAMs are sent: following ETSI EN 302 637-2, the state is checked every 100 ms. A CAM goes out when the heading changed by more than 4°, the position by more than 4 m, or the speed by more than 0.5 m/s since the last one, and at least once a second. That's about 3 CAMs a second at 50 km/h.
  • What's filled in: position, speed and heading go into both the CAM and the GeoNetworking source position vector. genDeltaT is milliseconds since boot.
  • Testing: route.c only uses standard headers, so you can compile it on the PC with MSYS2 gcc and simulate a lap.

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/route.c / .h - simulated drive round the route loop
  • main/route_points.h - the route, generated by tools/make_route.py
  • 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 (the CAM position comes from the simulated drive above), 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).