Compare commits

..
Author SHA1 Message Date
Ashin Walpola 73d4477f1e Move the capture tooling into the repo
live_capture.py and dump_pcap.py were untracked files inside the third-party
its-g5-receiver-firmware checkout, so the tools every on-air measurement depends
on were versioned nowhere and would vanish with a fresh clone of that project.
They are ours rather than that project's, so they now live in capture/, with the
setup notes rewritten as its README: which port the sniffer speaks on and why,
how to capture, how to check a capture, and how to flash the sniffer board.

live_capture.py carries the fix made on 2026-09-14. The sniffer's console
converts LF to CRLF on its way out, and that applies to every 0x0a byte of the
binary pcap stream, not only to log text, so every inserted CR shifts the rest
of the stream. A 787 KB capture parsed cleanly for only 82 of about 2000
records, and DENMs turned up on nonsense BTP ports because their payloads carry
0x0a often. undo_crlf() reverses it on the raw stream before any framing, which
is exact; afterwards a capture parsed to EOF and DENMs read as port 2002.
Captures taken before that date are truncated at their first corrupted record,
so anything measured from them is worth re-checking.

capture/recordings/ is gitignored, since captures are data rather than source.
The host tests now read both that directory and the older one in the receiver
checkout, so no capture has to be moved while it is being written.

dump_pcap.py reads the same console and still needs the same treatment; that is
recorded in TODO.md.
2026-09-14 13:38:38 +02:00
Ashin Walpola 277c6b20f4 Record the on-air verification and how to run the bench beacon
Both firmware fixes are now confirmed over the air, with the sniffer board
capturing and asn1tools judging the result.

GN lifetime: our station transmits 0x05 (1 s), the value both bench stations
use, where the August captures show 0x83 (3200 s) for the same frames. 397 CAMs
from station 999999 decode and re-encode byte-identically, so the whole
transmit chain is right on the wire, not only in the host tests.

yawRateConfidence: the bench beacon, flashed to a spare board, sends CAMs that
decode and re-encode byte-identically as well (72 of 72 from station
0x0BADC0DE). NOTES.md now says how to flash that beacon and how to check what
it sends, including that it shares the phone pinger's MAC and the two are told
apart by station ID.

The new firmware also runs on the OBU with the phone attached: CAM, DENM and
SPATEM from the bench stations all keep decoding in the app now that messages
are cut to the length their header declares.

Signed reception stays open. The CiT One transmits unsigned and nothing else
here signs, so it needs real roadside traffic, the CiT One switched to signed
mode, or a replay firmware on a spare board.

Two bench facts worth not rediscovering are recorded too: opening COM3's
console resets the OBU and drops the phone's USB link, and every capture taken
before today is truncated at its first corrupted record, because the receiver's
console inserts a CR before every 0x0a byte of the binary pcap stream.
live_capture.py now undoes that, a change that lives in the receiver repo and
is not part of this commit.
2026-09-14 13:33:47 +02:00
9964 changed files with 1898 additions and 1597507 deletions
+2 -7
View File
@@ -47,10 +47,5 @@ sdkconfig.old
# and remove them on close, so they are transient and machine-local. # and remove them on close, so they are transient and machine-local.
~$* ~$*
# Full-flash images read back off the bench boards before reflashing them (16 MB each). # Captures are large data files, not source (see capture/README.md).
# Restore with: esptool --chip esp32c5 -p COM<N> write-flash 0 <image> /capture/recordings/
/firmware-backups/
# ESP-IDF component manager downloads (espressif/esp-boost for obu-firmware's vanetza-idf), ~125 MB.
# dependencies.lock beside the project pins them and is committed; this is its cache.
managed_components/
+41 -281
View File
@@ -5,249 +5,44 @@ Engineering to-do list. The reviewer-facing open items live in
## Waiting on hardware ## Waiting on hardware
### Signed-TX firmware (vanetza-idf port), VAM and BLE: first on-air checks (added 2026-09-23)
obu-firmware is now a port of the colleague's `microbu-esp32c5` station (vanetza-idf, TS 103 097
signing, station-link protocol, BLE GATT), built with **ESP-IDF 6.0.2**. See `obu-firmware/NOTES.md`.
The previous firmware is backed up in `firmware-backups/` (restore command in its README.txt).
The app speaks the new protocol over USB or BLE and still falls back to the old frames against the
old firmware.
Done without hardware: IDF 6.0.2 build clean (39 % app partition free); host suite (`make` in
`obu-firmware/test/host`) passes unchanged; app unit tests 103/103, including the VAM encoder
against asn1tools, the station-link codec against the colleague's Python `messages.py`, and the VAM
generation rules. Flashed to **COM3** 2026-09-23 (hash verified); boot log: IDF v6.0.2,
`BLE advertising started as 'micrOBU-4AFA'`, station task ready. The radio stays off until the app
configures the station. New app build installed on the Pixel 9 Pro (adb, `install -r`).
First phone session (user, 2026-09-23): BLE works and CAMs go out. Three faults, fixed and
reflashed/reinstalled the same day:
1. No RX until the CAM pinger ran, with ~177 RX-queue drops: the colleague's `Station::tick()`
returned before draining the radio until the first PoTi had set the clock. Now drained always.
2. "refused a request: time_regression": the loops re-send the latest fix every tick; a stale fix
timestamp read as the clock going back > 1 s, and each time the board rebuilt its stack.
`Esp32Link` now sends a PoTi only for a newer fix (or a >= 60 s real clock correction).
3. BLE reconnect loop: GATT operations with a 5 s timeout ran during Android's pairing, cut it off
and restarted it on every attempt. Encryption/pairing is now settled first (60 s), retries back
off to 30 s, and every failure reason is logged and shown; the board logs encryption changes.
Second session (user, 2026-09-23): USB, RX and signing work; BLE still prompted every time and
never connected; time_regression every ~8 s. Found and fixed, reflashed (full flash, NVS erased):
4. The board never stored a bond: NVS (24 KB, the colleague's 4 MB-board layout) was full, mostly
Wi-Fi settings the previous firmware left behind, and NimBLE's bond write failed. NVS is now
80 KB (app moved to 0x20000) and was erased; boot logs `N bonded phone(s) in NVS`.
5. The station loop waited `pdMS_TO_TICKS(5)` = 0 ticks at 100 Hz, so it spun on the single core
(task watchdog: IDLE starved). Now waits at least one tick.
6. The phone clock is ~14 min fast; GnssTimeSource fell back to it whenever GNSS time blinked out
indoors, so every transmitted timestamp (CAM generationDeltaTime too) jumped 14 min back and
forth. It now keeps the last measured error.
Watch COM3 (`idf.py -p COM3 monitor`, or `readlog.py`-style with DTR/RTS low) during these; it only
resets the board, the phone is on the other port.
- [x] **USB session.** Settings > Connection > ESP32-C5: link USB-C, transmit CAM, signing on.
Phone on the native port, Connect. Expected: the card shows "Provisioning the demo credentials"
once, then Connected and `Signing on · tickets 1 · signed N` with N rising while recording.
COM3: `radio on channel 180, transmit and receive`, `tx power: … dBm`,
`credentials provisioned: 1 roots, 1 authorities, 1 tickets`, and no `radio refused a frame`.
Confirmed by the user 2026-09-23: connects, signing works.
- [x] **Reception intact.** Same session, sim car (COM8) beaconing: its CAMs (station 195936478) on
the V2X map at ~3 Hz as before. Then put a DENM and a SPATEM on air: both show up (they come
through the raw V2X_RX path; the vanetza stack drops them because they are not demo-signed).
Confirmed 2026-09-23: sim car and the RSU's CAM/SPATEM/MAPEM arrive; DENM not yet re-tested.
- [x] **Signed CAM on air** (2026-09-23, CAM pinger over BLE, signing on). Recorded 25 s through
the V2X2MAP bridge's `/api/record` (`micrOBU_workspace/v2x-obu-esp32c5/signed-cam-check.pcap`)
and checked with the new `obu-firmware/test/verify_signed_pcap.py` (asn1tools + OpenSSL, no
vanetza code): 12/12 secured CAMs, station 999999, psid 36, signer = full certificate of the
demo AT `B80B49387A4C12EB`, **all signatures valid**, COER canonical, and the bundle's chain
(AT <- AA <- root) verifies. The CiT One (192.168.40.201) also receives them (~1 Hz on
`v2x/rx/cam`), i.e. a third-party stack unwraps our 1609.2 envelope; its MQTT API exposes no
security fields, and it forwards unsigned and unknown-root messages alike, so it cannot say
whether it verified them. The same capture showed generationTime wobbling by seconds, with
`time_regression` still firing: fixed in Esp32Link (the PoTi never moves the micrOBU's clock
back except for a >= 60 s correction). Re-check: no "restarted its stack" lines in logcat.
- [x] **Unsigned toggle.** Signing off: the same capture shows next header 1 (common header), as
the previous firmware sent. The card's `signed` count stops rising.
Confirmed 2026-09-23: unsigned pinger CAMs show on V2X2MAP as unsigned.
- [x] **Signed VAM on V2X2MAP.** Since 2026-09-23 17:16 the COM10 bridge is the colleague's
v2x2map-0.3.0 from source with a new `verify.py` and `--trust demo-chain.vcr` (launcher:
`micrOBU_workspace/v2x-obu-esp32c5/start-v2x2map-signed.bat`, replacing its-g5-bridge.exe).
Signed CAMs from the pinger already show "signature verified" live. Switch to VAM with signing
on: the VAM must be decoded (cyclist, position) and show "signature verified" too.
Confirmed by the user 2026-09-23: signed VAMs decode and verify.
- [x] **VAM.** Transmit VAM: BTP port 2018, psid 638; with `microbu-esp32c5/tools/wireshark/psid-vru.lua`
Wireshark decodes the VAM (stationType cyclist, bicyclist profile in every ~2 s VAM). Rate:
≥1 per 5 s standing still, about one per GNSS fix while riding.
Covered by the V2X2MAP check above (decoded VAM, psid 638); Wireshark not needed.
- [x] **RX without recording.** Connect only (no recording, no pinger): sim-car CAMs appear and
the RX-queue drop counter stays at 0 or near it.
Confirmed 2026-09-23: messages come in on connect alone.
- [ ] **No time_regression.** Record for a few minutes standing still indoors: no "refused" line on
the card, and COM3 never logs `ITS time moved back`.
- [x] **BLE after the NVS fix.** First forget micrOBU-4AFA in Android's Bluetooth settings (the
phone still holds the bond the board lost). Then Connect, passkey 123456 once; a second
Connect after an app restart must not prompt again, and COM3's next boot must say
`1 bonded phone(s) in NVS`.
Confirmed 2026-09-23: pairs once, reconnects after an app restart.
- [x] **BLE.** Link Bluetooth, unplug USB, Connect. Android asks to pair with micrOBU-4AFA:
passkey 123456. Expected: Connected, CAMs keep going, sim-car CAMs keep arriving.
While USB is plugged in and in use, the phone's BLE scan must not see micrOBU-4AFA
(COM3: `USB link in use: BLE advertising paused`). If it loops again, the card now says why;
"refused this phone's stored pairing" means forget micrOBU-4AFA in Android and pair again.
COM3 shows `encryption change status=...` for the board's side.
Confirmed 2026-09-23: CAMs and VAMs out, reception in, over BLE.
- [ ] **BLE/ITS-G5 coexistence (the unmeasured risk from the hardware review).** With BLE
connected, count the sim car's CAMs received per minute and ours at the sniffer; compare
with the same over USB. A clear drop, or reception stopping altogether, means the coex
arbiter takes the radio off 5900 MHz (our channel is set behind the driver's back with
`phy_change_channel`). Then BLE cannot be used while receiving, or needs a longer connection
interval.
- [ ] **Board reset recovery over BLE.** Press RST mid-session: the app reconnects by itself and
reconfigures on the first STATUS saying `not configured`, with no manual Connect. (Over USB a
reset re-enumerates the port and needs a manual Connect, as before.)
### Confirm the RX queue drop counter explains the bench-session frame drops / map flicker (added 2026-09-22)
Investigated the user's report of "OBU mode keeps dropping a few frames" and "v2x screen comes
and goes" while bench-testing against `obu-cam-transmistter`. Found a real, previously invisible
drop path: `obu-firmware/main/main.c`'s `wifi_promisc_rx_cb()` calls `xQueueSend(s_rx_queue, ...,
0)` (queue depth 8) without checking the return value, so a burst of promiscuously-captured
frames arriving faster than `rx_forward_task` can drain them (each drain can legitimately block up
to ~400ms under USB/UART contention) silently vanishes. None of the existing `EspLinkStatus`
counters (`oversizeDrops`/`txFailures`/`rxCrcErrors`) caught this class of drop.
This plausibly also explains the map symptom: `UseCaseDetectionEngine.pruneStale()` drops a remote
station's marker after `staleRemoteMs` (3 s) with no CAM update. Measured 2026-09-22 via
`tools/cit_one_rx_watch.py --host 192.168.40.201` against `obu-cam-transmistter`'s bench beacon
(stationID 195936478 / 0x0BADC0DE): **75 CAMs in 25 s, ~3 Hz**, not the 1 Hz this note assumed
earlier — faster than assumed means more promiscuous captures per second and a shorter fuse on
`staleRemoteMs`, both of which make the queue-overflow theory more likely, not less.
Fixed to be **visible**, not yet fixed to **not drop**: added a `rxQueueDrops` counter, checked
`xQueueSend`'s return value (`main.c`), wired it through the STATUS heartbeat as a new trailing
`uint16` field (`serial_link.c/.h`, `SerialFrame.kt`'s `EspLinkStatus`), and surfaced it on the
CAM Pinger card (`MqttTopicViewerScreen.kt`, string `mqtt_cam_pinger_fw_counters`). Host build
untouched (serial_link.c/main.c aren't in the host test's standard-headers-only set); IDF build
verification is the remaining pre-flash check. Deliberately did NOT bump `s_rx_queue`'s depth from
8 — no real burst-size data yet, and guessing a bigger number against an unmeasured memory budget
is exactly the kind of assumption [[microbu-hw-review]] flags as needing verification first, not
capacity that's cheap to reason your way into.
Needs: a phone attached to the production OBU's native USB port, watching the CAM Pinger card,
while `obu-cam-transmistter` (or real traffic) beacons.
- [x] `idf.py build` succeeds (obu-firmware, IDF 6.1) — clean, both changed files compiled with no
warnings, 17% flash free.
- [x] Reflashed the production OBU on **COM3** 2026-09-22 (hash verified). Boot log confirms the
new build (`21e0149-dirty`, compiled Sep 22 2026 14:14:09), clean boot, OCB @ 5900 MHz
TX/RX armed, `serial_link up ... 1 Hz heartbeat`, no panic. Incidentally answers part of the
"measure the OBU's actual transmit power" item below: this boot logged
`tx power: 72 quarter-dBm = 18.00 dBm (20.00 requested)` — the driver **is** clamping below
the requested 20 dBm at 5900 MHz, as that item suspected but had not measured.
- [x] 25 s of steady-state console (no phone attached, `obu-cam-transmistter` beaconing nearby):
silent — no crash, no `oversize`/`rx queue full`/`crc` warnings. Inconclusive on its own
(successful forwards aren't logged, and nothing was attached to trigger the ~400 ms UART
stalls the theory needs), but at least rules out a crash-on-boot regression.
- [x] Confirmed the wider bench RF path independently via the CiT One OBU broker
(`py -3.11 tools/cit_one_rx_watch.py --host 192.168.40.201`): heard `obu-cam-transmistter`'s
beacon cleanly, 75/25 s, GN source `14:00:02:00:00:00:00:01`, position in the expected
St. Georg route area. This is a *different* receiver from the production OBU though — it
shows the beacon is genuinely on air, not that COM3 forwards every one of it without drops.
- [x] **Confirmed on real hardware, 2026-09-22.** Installed the updated debug APK (previous build
on the phone was from 2026-09-15, predating this fix entirely) on the Pixel 9 Pro (adb over
Wi-Fi), relaunched against the freshly-reflashed COM3, and read `rx queue drop` via `adb
logcat -s UsbSerialTransport`. The counter mechanism works end-to-end and **the bug is
real**: `rxQueueDrops` was 0 at the last flash (14:22), read as 89 at first reconnect
(14:48, ~26 min later), and 90 at a second reconnect (14:52). No `oversizeDrops`,
`txFailures`, or `rxCrcErrors` moved at all, and zero `decode FAILED` lines — this queue is
the only place frames are going missing.
Nuance: over a clean ~4.5 min window in between (14:48→14:52) with `obu-cam-transmistter`
actively beaconing at a measured **~3.33 Hz** (matches the CiT One's 75/25 s independently)
and 490+ CAMs decoding cleanly with steady cadence and no gaps, the counter did **not**
move — it only ticked at connect/reconnect moments. So this is a low-rate, bursty drop (matches
the user's own "a few frames" framing), not a continuous overflow under steady single-station
traffic; it may be specific to WiFi/PHY activity around association or reconnect rather than
raw beacon rate. Worth a longer, quieter-boot capture before sizing a `s_rx_queue` bump.
Did **not** independently confirm the map-flicker connection this session — that needs eyes
on the app's V2X screen while watching this same counter live, not just logcat.
### On-device check of the full-screen V2X live map (added 2026-09-15)
The live map moved out of the V2X Monitor's view-mode row into its own full-screen destination
(`V2xMapScreen`, route `v2x_map`), reached from the map button in that screen's header. Markers are
now cached and reused across updates instead of being rebuilt on every incoming message, and
SPATEM intersections are drawn as traffic lights at the position of the RSU's own CAM. All of that
compiles and the unit tests pass, but none of it has been seen with live traffic.
Needs: the phone with the app, plus a CAM/DENM/SPATEM source - either the CiT One, or the OBU
ESP32-C5 with a second board or a real RSU transmitting.
- [ ] Both hardware modes: tap the map button, confirm the map fills the screen (no status bar, no
bottom nav) and the back button returns to the V2X Monitor.
- [ ] Panning stays smooth while CAMs are arriving - this is what the marker reuse is for. Compare
against the old behaviour if it still judders.
- [ ] Touching the map stops it recentring; the location FAB resumes follow and lights up.
- [ ] A DENM shows the warning triangle, and a SPATEM intersection shows a traffic light with the
lamp matching the Dashboard's SignalCard for the same intersection.
- [ ] Near a real RSU: confirm the RSU is drawn once, as a traffic light, not as a CAM pin with a
light on top of it. If the RSU sends SPATEM but no CAM, the "signals not shown" note should
appear instead - worth knowing which of the two the HAW RSUs actually do.
### Over-the-air check of the GN lifetime fix (added 2026-09-11) ### Over-the-air check of the GN lifetime fix (added 2026-09-11)
`geonet.c` now writes GN lifetime `0x05` (1 s) instead of `0x83`, which decoded to 3200 s. Changed `geonet.c` now writes GN lifetime `0x05` (1 s) instead of `0x83`, which decoded to 3200 s. Changed
in both `obu-firmware` and `obu-cam-transmistter`. Both still build (IDF 6.1 / 5.5.4), and the in both `obu-firmware` and `obu-cam-transmistter`. Both still build (IDF 6.1 / 5.5.4), and the
compiled `geonet_wrap_shb` stores the new byte, but it has not been seen on air yet. Nothing else compiled `geonet_wrap_shb` stores the new byte. Confirmed on air 2026-09-14. Nothing else
reads this byte (`gn_unwrap.c` ignores it, the app never sees GN headers), so the app does not reads this byte (`gn_unwrap.c` ignores it, the app never sees GN headers), so the app does not
need updating alongside the firmware. need updating alongside the firmware.
Needs: the phone with the app, the OBU ESP32-C5, and a **second** ESP32-C5 running Needs: the phone with the app, the OBU ESP32-C5, and a **second** ESP32-C5 running
`its-g5-receiver-firmware` to capture with. `its-g5-receiver-firmware` to capture with.
- [ ] Flash `obu-firmware` (see `obu-firmware/FLASHING.md`). - [x] Flash `obu-firmware` (done 2026-09-14 on COM3; flash backed up first to
- [ ] Connect the phone, let it send CAMs, and confirm the CAM Pinger's `tx fail` counter stays 0. `Documents/micrOBU_workspace/firmware-backups/COM3-2026-09-14-before-secured-rx.bin`).
- [ ] Capture with the receiver into `its-g5-receiver-firmware/recordings/`. - [x] Capture with the receiver (COM8) into `its-g5-receiver-firmware/recordings/`.
- [ ] Run `python obu-firmware/test/pcap_gn_tally.py its-g5-receiver-firmware/recordings/<capture>.pcap`. - [x] `pcap_gn_tally.py` on capture_20260914_132126.pcap: our station sends SHB, port 2001,
The rows for the phone's pseudonym MACs must show SHB, port 2001, lifetime `0x05`, exactly lifetime `0x05`, same as both bench stations. It was `0x83` in the August captures.
like every other station's CAMs. - [x] Real-station CAMs/DENMs/SPATEM still reach the app (logcat: `handleCamUper`,
- [ ] While the phone is connected: real-station CAMs/DENMs still reach the app (RX path unchanged). `handleDenmUper`, `handleSpatUper` all decoding, 2026-09-14).
- [x] Our own CAMs decode on air: 397 frames from station 999999 decode with asn1tools and
re-encode byte-identically.
- [ ] Confirm the CAM Pinger card's `tx fail` / oversize / CRC counters are 0 (needs a look at the
phone; not readable from the PC).
Partial check possible with one board and no phone: flash it, `idf.py -p COMx monitor`, and look Partial check possible with one board and no phone: flash it, `idf.py -p COMx monitor`, and look
for `OCB @ 5900 MHz - TX/RX armed`. That proves the new build boots and brings the radio up, not for `OCB @ 5900 MHz - TX/RX armed`. That proves the new build boots and brings the radio up, not
that it transmits correctly. that it transmits correctly.
### Measure the OBU's actual transmit power (added 2026-09-14)
Nothing in this project has ever measured it. `main.c` asks for 20 dBm
(`esp_wifi_set_max_tx_power(80)`, 0.25 dBm units) and the build's ceiling is the same
(`CONFIG_ESP_PHY_MAX_TX_POWER=20`), but a request is a ceiling, not a guarantee: the driver clamps
it to its own calibrated table, and 5900 MHz is above the range this chip is rated for, so the
table actually in use is channel 177's. The firmware now reads the value back and logs it at boot,
which records what the driver admits to, not what leaves the antenna.
- [ ] Flash and `idf.py -p COM3 monitor`, then note the `tx power:` line. A value below 80 means
the driver clamped the request, which the code alone cannot tell you.
- [ ] Relative check with the second ESP32-C5 on `its-g5-receiver-firmware`: capture at a measured
distance in a straight line, read the RSSI the receive path already reports, and record
distance and RSSI together. This gives a comparable number between builds and antennas,
which is what matters for range work, without any lab equipment.
- [ ] Only a spectrum analyser or a calibrated reference receiver gives real radiated power. Worth
it only if the range result looks wrong, or if the thesis needs an absolute figure.
For context: ETSI allows up to 33 dBm EIRP on the ITS band, and production OBUs sit around
20 to 23 dBm, so the requested figure is in the right region if the PA really keys it there.
### obu-cam-transmistter yawRateConfidence fix (added 2026-09-11) ### obu-cam-transmistter yawRateConfidence fix (added 2026-09-11)
Its `cam.c` (compiled into that firmware) wrote `yawRateConfidence` as 3 bits / 7 instead of Its `cam.c` (compiled into that firmware) wrote `yawRateConfidence` as 3 bits / 7 instead of
4 bits / unavailable(8), the bug the app fixed on 2026-08-20. Fixed in it and in obu-firmware's 4 bits / unavailable(8), the bug the app fixed on 2026-08-20. Fixed in it and in obu-firmware's
reference copy; asn1tools now decodes the CAM and re-encodes it byte-identically, and it builds on reference copy; asn1tools now decodes the CAM and re-encodes it byte-identically, and it builds on
IDF 5.5.4. No board runs this firmware right now (the production OBU runs obu-firmware), so this IDF 5.5.4. Since 2026-09-14 the spare board on COM10 runs it as a bench beacon:
only matters if it is flashed again:
- [ ] After flashing it: capture, run `pcap_gn_tally.py`, and decode the CAM payload with - [x] Done 2026-09-14: flashed on COM10 and captured on COM8. All 72 CAMs from station
asn1tools (`py -3.11`, modules in `asn1/`). 195936478 (0x0BADC0DE) decode with asn1tools and re-encode byte-identically, so the
4-bit yawRateConfidence is right on air. COM10 now runs this beacon rather than
obu-firmware - reflash it if the spare is needed as an OBU again.
### Signed-message reception and exact payloads (added 2026-09-11) ### Signed-message reception and exact payloads (added 2026-09-11)
@@ -255,69 +50,21 @@ obu-firmware's `gn_unwrap.c` now unwraps TS 103 097 signed packets (signature no
reported as V2X_RX flags bit1) and cuts every message to the length its header declares, dropping reported as V2X_RX flags bit1) and cuts every message to the length its header declares, dropping
the 8 bytes the chip's RX appends to each frame, which were forwarded to the phone until now. the 8 bytes the chip's RX appends to each frame, which were forwarded to the phone until now.
Verified on the host (`obu-firmware/test/host`: chain, replay of all recordings against asn1tools, Verified on the host (`obu-firmware/test/host`: chain, replay of all recordings against asn1tools,
50M-iteration fuzz) and built on IDF 6.1, but not flashed: the production OBU still runs the 50M-iteration fuzz) and flashed to the production OBU on 2026-09-14. The remaining gap is signed
2026-09-10 build. Needs the OBU with this build, the phone, and signed traffic - real vehicles or traffic to receive: real vehicles or
RSUs, since the bench CiT One sends unsigned. A second ESP32 running the receiver firmware is RSUs, since the bench CiT One sends unsigned. A second ESP32 running the receiver firmware is
optional, but shows what was on air at the time. optional, but shows what was on air at the time.
- [ ] Flash obu-firmware (this also carries the GN lifetime fix above). - [x] Flash obu-firmware (done 2026-09-14, COM3).
- [x] Unsigned bench traffic still decodes in the app, with messages now cut to their declared
length (CAM, DENM and SPATEM all decoding in logcat after the flash).
- [ ] Near signed traffic: signed CAMs/DENMs appear in the app, and a simultaneous capture shows - [ ] Near signed traffic: signed CAMs/DENMs appear in the app, and a simultaneous capture shows
them on air (`pcap_gn_tally.py` lists them as `secured`). them on air (`pcap_gn_tally.py` lists them as `secured`). NOT possible at this bench: the
- [ ] Unsigned bench traffic still decodes in the app as before (messages now arrive 8 bytes CiT One transmits unsigned (`ItsGnSecurity = 0`) and nothing else here signs. Needs a drive
shorter). past real RSUs, the CiT One switched to signed mode if its API allows, or a replay firmware
- [ ] The heartbeat's oversize counter still counts over-long messages (e.g. road SPATEMs). on a spare board that re-transmits the recorded signed frames.
- [ ] The heartbeat's oversize counter still counts over-long messages. Not exercised at the
### CiT One custom CAM injection over `v2x/tx/v2/cam` (added 2026-09-14) bench: the SPATEMs here are ~340 bytes on air, far below the cap.
The haw-002 unit now runs the special firmware: Cohda's own CAM transmission disabled, and a
V2X-Gateway build that accepts a `SendV2XMessage` (schemas.consider-innovation.de/its-s/
v2x_interface.proto) carrying a UPER CAM on `v2x/tx/v2/cam`. `tools/cit_one_cam_tx.py` builds
and publishes those from a PC; its `--self-test` passes offline, proving only that the bytes
match `CamEncodeGoldenTest.kt` and that the protobuf wrapper round-trips. Nothing about what
the OBU does with them is established.
Reach the broker over Wi-Fi or Ethernet for now - the USB-peripheral-mode link needs the phone
to be USB host on a `172.25.1.0/24` interface with no DHCP server, which Android cannot
configure from inside an app.
Needs: the CiT One haw-002 on the same network as a PC, and a second ESP32-C5 running
`its-g5-receiver-firmware` sniffing G5CC (`-c 5900`) to capture with.
Bench run 2026-09-14, PC -> haw-002 (192.168.3.201), captured on the RSU (192.168.3.202,
**not** .2.202 - that address does not route). `tools/cit_one_rx_watch.py` decodes what a unit
hears. Result: the injection path works end to end, with one blocker found.
- [x] Publishes without the broker refusing the topic. 1.00 Hz, confirmed by subscribing to
`v2x/tx/v2/cam` on the OBU itself.
- [x] The RSU hears our CAMs on air, 1.00 Hz, matching what we publish.
- [x] `ItsPduHeader` **is** expected in the payload - we send it included and it decodes.
- [x] BTP destination port 2001. GN source address `08:00:26:93:92:01:91:dc`, the OBU's.
- [x] **Our CAM content goes out intact**: position, speed (417), heading (639), width (7) and
length (18) arrive byte-exact. The gateway does not touch the content.
- [x] **The gateway overwrites `stationID`** with the OBU's own (999999 -> 4033890855, which
matches `own_info.stationID` on `v2x/rx/obu_gnss`). This is what the "OBU owns identity"
decision wants, so `--follow-obu-identity` is not needed on this unit.
- [x] ~~BLOCKER: Cohda's own CAM is still transmitting.~~ Fixed 2026-09-14 by disabling CAM in
a second conf file: the RSU now hears only our stream, 0 CAMs with the stack's
unavailable dimensions over 30 s. Note the stack restart gave the unit a new identity
(stationID 4033890855 -> 2553426533, GN source `08:00:26:...` -> `08:00:a2:...`), which is
expected under `ItsGnLocalAddrConfMethod = 2` (anonymous, random at boot).
- [x] Re-checked: 41 published / 41 heard over 40 s, 1.02 Hz both ends, inter-arrival a steady
1.0 s. 100% delivery, no gateway rate limiting. An earlier 0.40 Hz sample was the stack
still settling after the restart and did not persist.
Two topics the v6 API does not document, found by subscribing to `#` on haw-002:
- `v2x/loopback/cam` - a `RecvV2XMessage` (btpHeader.type=2) carrying each CAM the unit
transmits, 1:1 with what we publish and **after** the gateway's stationID rewrite. This is the
TX confirmation we were going to ask consider it for: it makes "did my CAM go out, and under
which identity" answerable on the transmitting unit alone, without an RSU or a second ESP32.
- `v2x/rx/obuinfo` at 10 Hz - the protobuf `OwnStationInfo` (binary twin of `obu_gnss`;
field 2 decodes to the same stationID, field 10 to the same heading). Output only, so it is
not the content-feed input we speculated about.
- [ ] Wire `v2x/loopback/cam` into `cit_one_rx_watch.py` as a local TX check.
- [ ] Sanity-check the rate: `--rate 4` should produce 4 CAMs/s on air, since `ItsDCCEnabled = 0`
on this unit.
## Set up host testing ## Set up host testing
@@ -354,6 +101,19 @@ Suggested order after the host tests exist:
Dropped: building vanetza as a GN/BTP oracle. Real captures (`pcap_gn_tally.py`), the host Dropped: building vanetza as a GN/BTP oracle. Real captures (`pcap_gn_tally.py`), the host
round-trip test and `asn1tools` for UPER cover what it would have checked. round-trip test and `asn1tools` for UPER cover what it would have checked.
## Follow-ups found 2026-09-14
- [x] **Capture tooling moved into this repo** (`capture/`), with the CR-insertion fix. The
sniffer's console inserts a CR before every LF, which also hits every 0x0a byte of the binary
pcap stream, shifting pcap record headers and frames. A 787 KB capture parsed cleanly for
only 82 of ~2000 records, and DENMs showed up on nonsense BTP ports. `undo_crlf()` reverses
it on the raw stream before framing; afterwards a capture parsed to EOF and DENMs read as
port 2002. **Every capture taken before 2026-09-14 is truncated at its first corrupted
record** - re-measure anything derived from them.
- [ ] `capture/dump_pcap.py` reads the same console and still needs the same treatment.
- [ ] **Do not open COM3's console while the phone is attached.** Opening it toggles DTR/RTS on the
CH343 and resets the OBU, which drops the phone's USB link and needs a manual Connect.
## Follow-ups found 2026-09-11 ## Follow-ups found 2026-09-11
- [ ] **App: show the signed flag.** `V2xRxFrame.parse` in `SerialFrame.kt` only reads bit0 of - [ ] **App: show the signed flag.** `V2xRxFrame.parse` in `SerialFrame.kt` only reads bit0 of
+1 -11
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:tools="http://schemas.android.com/tools">
<!-- Networking (MQTT / tile downloads) --> <!-- Networking (MQTT / tile downloads) -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@@ -11,15 +10,6 @@
<!-- Location --> <!-- Location -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- BLE link to the ESP32-C5 (BleLinkTransport). Android 12+ asks for scan/connect at runtime;
older versions use the legacy pair plus location, which is requested anyway. -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Phase A: foreground service (trip recording) --> <!-- Phase A: foreground service (trip recording) -->
Binary file not shown.
@@ -26,12 +26,10 @@ import androidx.core.view.WindowCompat
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState import com.hawhamburg.micr0bu.data.transport.UsbSerialState
import com.hawhamburg.micr0bu.data.transport.Esp32Transport
import com.hawhamburg.micr0bu.data.transport.ObuHardware import com.hawhamburg.micr0bu.data.transport.ObuHardware
import com.hawhamburg.micr0bu.ui.components.StatusTopBar import com.hawhamburg.micr0bu.ui.components.StatusTopBar
import com.hawhamburg.micr0bu.ui.navigation.BottomNavBar import com.hawhamburg.micr0bu.ui.navigation.BottomNavBar
@@ -42,7 +40,6 @@ import com.hawhamburg.micr0bu.ui.screens.MqttTopicViewerScreen
import com.hawhamburg.micr0bu.ui.screens.RecordingScreen import com.hawhamburg.micr0bu.ui.screens.RecordingScreen
import com.hawhamburg.micr0bu.ui.screens.SensorScreen import com.hawhamburg.micr0bu.ui.screens.SensorScreen
import com.hawhamburg.micr0bu.ui.screens.SessionLogScreen import com.hawhamburg.micr0bu.ui.screens.SessionLogScreen
import com.hawhamburg.micr0bu.ui.screens.V2xMapScreen
import com.hawhamburg.micr0bu.ui.screens.MapScreen import com.hawhamburg.micr0bu.ui.screens.MapScreen
import com.hawhamburg.micr0bu.ui.screens.TripHistoryScreen import com.hawhamburg.micr0bu.ui.screens.TripHistoryScreen
import com.hawhamburg.micr0bu.ui.screens.TripReviewScreen import com.hawhamburg.micr0bu.ui.screens.TripReviewScreen
@@ -89,10 +86,7 @@ class MainActivity : AppCompatActivity() {
val showBatteryOptPrompt by tripViewModel.showBatteryOptPrompt.collectAsState() val showBatteryOptPrompt by tripViewModel.showBatteryOptPrompt.collectAsState()
val useCaseEnabledMap by mqttViewModel.useCaseEnabledMap.collectAsState() val useCaseEnabledMap by mqttViewModel.useCaseEnabledMap.collectAsState()
val obuHardware by mqttViewModel.obuHardware.collectAsState() val obuHardware by mqttViewModel.obuHardware.collectAsState()
val esp32LinkState by mqttViewModel.esp32LinkState.collectAsState() val usbSerialState by mqttViewModel.usbSerialState.collectAsState()
val esp32Transport by mqttViewModel.esp32Transport.collectAsState()
val outgoingMessage by mqttViewModel.outgoingMessage.collectAsState()
val signOutgoing by mqttViewModel.signOutgoing.collectAsState()
// Received hazards and live signal state, for the Dashboard's V2X summary cards. // Received hazards and live signal state, for the Dashboard's V2X summary cards.
// Both flows already expire their own entries on a clock, so nothing here has to // Both flows already expire their own entries on a clock, so nothing here has to
// decide when a hazard or a traffic light has gone stale. // decide when a hazard or a traffic light has gone stale.
@@ -110,13 +104,6 @@ class MainActivity : AppCompatActivity() {
} }
val navController = rememberNavController() val navController = rememberNavController()
// The V2X live map is a full-bleed destination: the app's own chrome would eat a
// third of the display on the one screen whose entire job is showing where things
// are relative to each other. It carries its own floating back button, and system
// back still works, so nothing becomes unreachable.
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val isFullBleed = currentBackStackEntry?.destination?.route == Screen.V2xMap.route
val locationLauncher = rememberLauncherForActivityResult( val locationLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions() ActivityResultContracts.RequestMultiplePermissions()
) { permissions -> ) { permissions ->
@@ -132,17 +119,11 @@ class MainActivity : AppCompatActivity() {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.startImuStreams() viewModel.startImuStreams()
// Bluetooth scan/connect ride along with location (Android 12+): the ESP32-C5
// can be reached over BLE, and a denial only matters if that is selected, where
// BleLinkTransport then says so instead of silently finding nothing.
val bluetooth = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
} else emptyArray()
locationLauncher.launch( locationLauncher.launch(
arrayOf( arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION,
) + bluetooth )
) )
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) notificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
@@ -151,17 +132,14 @@ class MainActivity : AppCompatActivity() {
Scaffold( Scaffold(
topBar = { topBar = {
if (!isFullBleed) {
StatusTopBar( StatusTopBar(
state = state, state = state,
mqttConnectionState = mqttConnectionState, mqttConnectionState = mqttConnectionState,
isEsp32 = obuHardware == ObuHardware.ESP32_C5, isEsp32 = obuHardware == ObuHardware.ESP32_C5,
esp32LinkState = esp32LinkState, usbSerialState = usbSerialState,
esp32Bluetooth = esp32Transport == Esp32Transport.BLE,
) )
}
}, },
bottomBar = { if (!isFullBleed) BottomNavBar(navController) }, bottomBar = { BottomNavBar(navController) },
) { innerPadding -> ) { innerPadding ->
NavHost( NavHost(
navController = navController, navController = navController,
@@ -174,8 +152,7 @@ class MainActivity : AppCompatActivity() {
mqttConnectionState = mqttConnectionState, mqttConnectionState = mqttConnectionState,
activeTransport = activeTransport, activeTransport = activeTransport,
obuHardware = obuHardware, obuHardware = obuHardware,
esp32LinkState = esp32LinkState, usbSerialState = usbSerialState,
esp32Bluetooth = esp32Transport == Esp32Transport.BLE,
usbCableConnected = usbConnected, usbCableConnected = usbConnected,
obuStationTypeWarning = obuStationTypeWarning, obuStationTypeWarning = obuStationTypeWarning,
obuStationType = obuStationType, obuStationType = obuStationType,
@@ -214,7 +191,7 @@ class MainActivity : AppCompatActivity() {
state = state, state = state,
mqttConnectionState = mqttConnectionState, mqttConnectionState = mqttConnectionState,
obuConnected = if (obuHardware == ObuHardware.ESP32_C5) obuConnected = if (obuHardware == ObuHardware.ESP32_C5)
esp32LinkState == Esp32LinkState.CONNECTED usbSerialState == UsbSerialState.CONNECTED
else else
mqttConnectionState == MqttConnectionState.CONNECTED, mqttConnectionState == MqttConnectionState.CONNECTED,
tripServiceState = tripServiceState, tripServiceState = tripServiceState,
@@ -276,19 +253,7 @@ class MainActivity : AppCompatActivity() {
} }
composable(Screen.MqttViewer.route) { composable(Screen.MqttViewer.route) {
MqttTopicViewerScreen( MqttTopicViewerScreen(viewModel = mqttViewModel)
viewModel = mqttViewModel,
onOpenMap = { navController.navigate(Screen.V2xMap.route) },
)
}
composable(Screen.V2xMap.route) {
// Activity-scoped instance, like Connection below: a hiltViewModel()
// here would be scoped to this NavBackStackEntry and torn down on the
// way back out, taking the shared transport with it.
V2xMapScreen(
viewModel = mqttViewModel,
onBack = { navController.popBackStack() },
)
} }
composable(Screen.Settings.route) { composable(Screen.Settings.route) {
SettingsScreen( SettingsScreen(
@@ -316,12 +281,6 @@ class MainActivity : AppCompatActivity() {
onMqttPrefsChange = mqttViewModel::updatePrefs, onMqttPrefsChange = mqttViewModel::updatePrefs,
obuHardware = obuHardware, obuHardware = obuHardware,
onObuHardwareChange = mqttViewModel::setObuHardware, onObuHardwareChange = mqttViewModel::setObuHardware,
esp32Transport = esp32Transport,
onEsp32TransportChange = mqttViewModel::setEsp32Transport,
outgoingMessage = outgoingMessage,
onOutgoingMessageChange = mqttViewModel::setOutgoingMessage,
signOutgoing = signOutgoing,
onSignOutgoingChange = mqttViewModel::setSignOutgoing,
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
) )
} }
@@ -21,13 +21,7 @@ import java.time.DateTimeException
* [SystemClock.currentGnssTimeClock] (API 29, this app's minSdk) is a UTC clock the platform keeps * [SystemClock.currentGnssTimeClock] (API 29, this app's minSdk) is a UTC clock the platform keeps
* synchronised from GNSS fixes. One reading of it taken alongside the wall clock gives the wall * synchronised from GNSS fixes. One reading of it taken alongside the wall clock gives the wall
* clock's error, which is then applied to the fix's own timestamp. When GNSS time is unavailable, * clock's error, which is then applied to the fix's own timestamp. When GNSS time is unavailable,
* the last error measured is kept, because the wall clock's error changes slowly while GNSS time * typically indoors before any satellite fix since boot, the wall clock is used unchanged.
* comes and goes indoors. Only before any GNSS time since the app started is the wall clock used
* unchanged.
*
* Keeping it matters: with the bench phone 14 minutes fast (2026-09-23), dropping back to the raw
* wall clock whenever GNSS time blinked out made every transmitted timestamp jump 14 minutes back
* and forth, and the micrOBU restarted its stack at each jump back (time_regression).
* *
* Which clock is in use is logged whenever it changes, with the measured error, so a capture shows * Which clock is in use is logged whenever it changes, with the measured error, so a capture shows
* where a given run's timestamps came from. * where a given run's timestamps came from.
@@ -42,9 +36,6 @@ object GnssTimeSource {
/** Whether the last correction used GNSS time; null before the first. For change-only logging. */ /** Whether the last correction used GNSS time; null before the first. For change-only logging. */
@Volatile private var lastUsedGnss: Boolean? = null @Volatile private var lastUsedGnss: Boolean? = null
/** GNSS time minus wall clock at the last reading of both; null until GNSS time was first seen. */
@Volatile private var lastErrorMs: Long? = null
/** [systemMs], a wall-clock reading, moved onto GNSS time where GNSS time is available. */ /** [systemMs], a wall-clock reading, moved onto GNSS time where GNSS time is available. */
fun correct(systemMs: Long): Long { fun correct(systemMs: Long): Long {
val systemNow = System.currentTimeMillis() val systemNow = System.currentTimeMillis()
@@ -53,9 +44,8 @@ object GnssTimeSource {
} catch (e: DateTimeException) { } catch (e: DateTimeException) {
null null
} }
if (gnssNow != null) lastErrorMs = gnssNow - systemNow
noteSource(gnssNow, systemNow) noteSource(gnssNow, systemNow)
return ItsTime.onGnssTime(systemMs, lastErrorMs?.let { systemNow + it }, systemNow) return ItsTime.onGnssTime(systemMs, gnssNow, systemNow)
} }
private fun noteSource(gnssNow: Long?, systemNow: Long) { private fun noteSource(gnssNow: Long?, systemNow: Long) {
@@ -65,9 +55,6 @@ object GnssTimeSource {
if (gnssNow != null) { if (gnssNow != null) {
Log.i(TAG, "transmit timestamps now on GNSS time; phone clock is " + Log.i(TAG, "transmit timestamps now on GNSS time; phone clock is " +
"${"%+.1f".format((systemNow - gnssNow) / 1000.0)} s off") "${"%+.1f".format((systemNow - gnssNow) / 1000.0)} s off")
} else if (lastErrorMs != null) {
Log.i(TAG, "GNSS time unavailable, keeping the last measured phone clock error of " +
"${"%+.1f".format(-lastErrorMs!! / 1000.0)} s")
} else { } else {
Log.w(TAG, "GNSS time unavailable, transmit timestamps fall back to the phone clock, " + Log.w(TAG, "GNSS time unavailable, transmit timestamps fall back to the phone clock, " +
"which has no automatic time source without a SIM or internet") "which has no automatic time source without a SIM or internet")
@@ -16,8 +16,8 @@ import com.hawhamburg.micr0bu.data.transport.ObuHardware
import com.hawhamburg.micr0bu.data.transport.BtpPort import com.hawhamburg.micr0bu.data.transport.BtpPort
import com.hawhamburg.micr0bu.data.transport.SerialFrameType import com.hawhamburg.micr0bu.data.transport.SerialFrameType
import com.hawhamburg.micr0bu.data.transport.V2xRxFrame import com.hawhamburg.micr0bu.data.transport.V2xRxFrame
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState import com.hawhamburg.micr0bu.data.transport.UsbSerialState
import com.hawhamburg.micr0bu.data.transport.Esp32Link import com.hawhamburg.micr0bu.data.transport.UsbSerialTransport
import com.hawhamburg.micr0bu.domain.asn1.DenmUperCodec import com.hawhamburg.micr0bu.domain.asn1.DenmUperCodec
import com.hawhamburg.micr0bu.domain.asn1.RealAsn1UperCodec import com.hawhamburg.micr0bu.domain.asn1.RealAsn1UperCodec
import com.hawhamburg.micr0bu.domain.asn1.SpatemUperCodec import com.hawhamburg.micr0bu.domain.asn1.SpatemUperCodec
@@ -102,7 +102,7 @@ private const val RAW_PREFERRED_WINDOW_MS = 5_000L
class CamUseCaseRepository @Inject constructor( class CamUseCaseRepository @Inject constructor(
private val mqttRepository: MqttRepository, private val mqttRepository: MqttRepository,
private val prefs: UseCaseAlertPreferences, private val prefs: UseCaseAlertPreferences,
private val esp32Link: Esp32Link, private val usbSerialTransport: UsbSerialTransport,
private val camCodec: RealAsn1UperCodec, private val camCodec: RealAsn1UperCodec,
private val obuHardwarePrefs: ObuHardwarePreferences, private val obuHardwarePrefs: ObuHardwarePreferences,
private val pseudonymManager: PseudonymManager, private val pseudonymManager: PseudonymManager,
@@ -285,9 +285,9 @@ class CamUseCaseRepository @Inject constructor(
// this only ever sees CAM UPER bytes. No-op stream on the CiT One path (the transport // this only ever sees CAM UPER bytes. No-op stream on the CiT One path (the transport
// just never emits CAM_RX frames if nothing's plugged in over serial). // just never emits CAM_RX frames if nothing's plugged in over serial).
scope.launch { scope.launch {
esp32Link.incomingFrames.collect { frame -> usbSerialTransport.incomingFrames.collect { frame ->
if (frame.type != SerialFrameType.V2X_RX) return@collect if (frame.type != SerialFrameType.V2X_RX) return@collect
if (esp32Link.state.value != Esp32LinkState.CONNECTED) return@collect if (usbSerialTransport.state.value != UsbSerialState.CONNECTED) return@collect
val v2x = V2xRxFrame.parse(frame.payload) ?: return@collect val v2x = V2xRxFrame.parse(frame.payload) ?: return@collect
when (v2x.btpPort) { when (v2x.btpPort) {
BtpPort.CAM -> handleCamFromSerial(v2x) BtpPort.CAM -> handleCamFromSerial(v2x)
@@ -304,10 +304,10 @@ class CamUseCaseRepository @Inject constructor(
// last-seen positions and their alerts linger on the map and in the use-case panel after // 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. // an unplug, which reads as live traffic - the worst kind of stale on a safety display.
scope.launch { scope.launch {
esp32Link.state.collect { state -> usbSerialTransport.state.collect { state ->
// ESP32-only: on the CiT One path this transport is permanently DISCONNECTED and // ESP32-only: on the CiT One path this transport is permanently DISCONNECTED and
// resetting here would wipe perfectly good MQTT-derived state. // resetting here would wipe perfectly good MQTT-derived state.
if (currentHardware == ObuHardware.ESP32_C5 && state != Esp32LinkState.CONNECTED) { if (currentHardware == ObuHardware.ESP32_C5 && state != UsbSerialState.CONNECTED) {
engine.reset() engine.reset()
_rsuStations.value = emptyMap() _rsuStations.value = emptyMap()
} }
@@ -1,14 +1,11 @@
package com.hawhamburg.micr0bu.data.mqtt package com.hawhamburg.micr0bu.data.mqtt
import android.content.Context import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.hawhamburg.micr0bu.data.transport.Esp32Transport
import com.hawhamburg.micr0bu.data.transport.ObuHardware import com.hawhamburg.micr0bu.data.transport.ObuHardware
import com.hawhamburg.micr0bu.data.transport.OutgoingMessage
import com.hawhamburg.micr0bu.domain.cam.Pseudonym import com.hawhamburg.micr0bu.domain.cam.Pseudonym
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -35,10 +32,6 @@ class ObuHardwarePreferences @Inject constructor(
val OWN_STATION_ID = longPreferencesKey("own_station_id") val OWN_STATION_ID = longPreferencesKey("own_station_id")
val OWN_MAC = stringPreferencesKey("own_mac") val OWN_MAC = stringPreferencesKey("own_mac")
val OWN_PSEUDONYM_CREATED_MS = longPreferencesKey("own_pseudonym_created_ms") val OWN_PSEUDONYM_CREATED_MS = longPreferencesKey("own_pseudonym_created_ms")
// ESP32-C5 only.
val ESP32_TRANSPORT = stringPreferencesKey("esp32_transport")
val OUTGOING_MESSAGE = stringPreferencesKey("outgoing_message")
val SIGN_OUTGOING = booleanPreferencesKey("sign_outgoing")
} }
val obuHardwareFlow: Flow<ObuHardware> = context.obuHardwareDataStore.data.map { prefs -> val obuHardwareFlow: Flow<ObuHardware> = context.obuHardwareDataStore.data.map { prefs ->
@@ -49,37 +42,6 @@ class ObuHardwarePreferences @Inject constructor(
context.obuHardwareDataStore.edit { prefs -> prefs[Keys.OBU_HARDWARE] = hardware.id } context.obuHardwareDataStore.edit { prefs -> prefs[Keys.OBU_HARDWARE] = hardware.id }
} }
/** How the phone reaches the ESP32-C5: its native USB-C port (default) or BLE. */
val esp32TransportFlow: Flow<Esp32Transport> = context.obuHardwareDataStore.data.map { prefs ->
Esp32Transport.entries.firstOrNull { it.id == prefs[Keys.ESP32_TRANSPORT] } ?: Esp32Transport.USB
}
suspend fun setEsp32Transport(transport: Esp32Transport) {
context.obuHardwareDataStore.edit { prefs -> prefs[Keys.ESP32_TRANSPORT] = transport.id }
}
/** What the ESP32-C5 path transmits while recording: CAM (default) or VAM. */
val outgoingMessageFlow: Flow<OutgoingMessage> = context.obuHardwareDataStore.data.map { prefs ->
OutgoingMessage.entries.firstOrNull { it.id == prefs[Keys.OUTGOING_MESSAGE] } ?: OutgoingMessage.CAM
}
suspend fun setOutgoingMessage(message: OutgoingMessage) {
context.obuHardwareDataStore.edit { prefs -> prefs[Keys.OUTGOING_MESSAGE] = message.id }
}
/**
* Whether outgoing messages are signed (TS 103 097, demo PKI). Default on. Off sends them
* unsigned exactly as the previous firmware did, which verifying receivers may prefer to a
* signature they cannot chain to the EU trust list.
*/
val signOutgoingFlow: Flow<Boolean> = context.obuHardwareDataStore.data.map { prefs ->
prefs[Keys.SIGN_OUTGOING] ?: true
}
suspend fun setSignOutgoing(sign: Boolean) {
context.obuHardwareDataStore.edit { prefs -> prefs[Keys.SIGN_OUTGOING] = sign }
}
/** /**
* The transmit pseudonym last saved by [savePseudonym], or null if there is none. * The transmit pseudonym last saved by [savePseudonym], or null if there is none.
* *
@@ -1,466 +0,0 @@
package com.hawhamburg.micr0bu.data.transport
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
private const val TAG = "BleLinkTransport"
/**
* BLE GATT central for the micrOBU's station link: the Android counterpart of the firmware's
* `obu-firmware/main/simple_ble.cpp` (from the colleague's microbu-esp32c5) and of their Python
* `microbu_link/ble_transport.py`, which this follows step for step.
*
* ## The GATT layout (what the firmware actually implements)
* The station-link README describes a Nordic-UART-shaped service with fragmentation. The firmware
* does something else, and the firmware is what counts here: a custom service
* `0000C175-BA5E-4C17-8000-00805F9B34FB` with one characteristic per primitive, and each GATT value
* is one complete link message, never fragmented. Requests are written with response to the
* characteristic of their opcode ([writeTarget]); replies, STATUS and V2X_RX arrive as
* notifications. A message may be up to 512 octets, so the ATT MTU must be raised to 517 first:
* the firmware refuses to notify a message that does not fit rather than send it cut short.
*
* ## Pairing
* Every characteristic needs an encrypted, authenticated link. The firmware uses LE Secure
* Connections with a fixed passkey, [PASSKEY] (DisplayOnly). Android shows its own pairing dialog
* the first time; the user types the passkey there, and the bond is kept on both sides. The
* passkey is public, so this gives encryption but no protection against an active attacker
* during that first pairing; acceptable for the demo PKI this carries.
*
* ## RF
* BLE shares the C5's single RF front end with 5.9 GHz ITS-G5. The firmware stops advertising
* while the USB link is in use; whether an active BLE connection disturbs ITS-G5 has not been
* measured yet (TODO.md, "Waiting on hardware").
*
* Like [UsbSerialTransport], an app-scoped singleton: only an explicit disconnect or the process
* dying closes it, never a screen or ViewModel going away.
*/
@Singleton
class BleLinkTransport @Inject constructor(
@ApplicationContext private val context: Context,
) {
companion object {
const val PASSKEY = "123456"
const val NAME_PREFIX = "micrOBU"
private val SERVICE: UUID = UUID.fromString("0000c175-ba5e-4c17-8000-00805f9b34fb")
private val BTP_REQUEST: UUID = UUID.fromString("0000c176-ba5e-4c17-8000-00805f9b34fb")
private val BTP_INDICATION: UUID = UUID.fromString("0000c177-ba5e-4c17-8000-00805f9b34fb")
private val POTI: UUID = UUID.fromString("0000c178-ba5e-4c17-8000-00805f9b34fb")
/** Read-encrypted, returns nothing useful: only used to find out whether the link is secure. */
private val STATUS_CHAR: UUID = UUID.fromString("0000c179-ba5e-4c17-8000-00805f9b34fb")
private val ID_EVENT: UUID = UUID.fromString("0000c17a-ba5e-4c17-8000-00805f9b34fb")
private val CONFIG: UUID = UUID.fromString("0000c17b-ba5e-4c17-8000-00805f9b34fb")
private val RESULT: UUID = UUID.fromString("0000c17c-ba5e-4c17-8000-00805f9b34fb")
private val CCCD: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
private const val REQUESTED_MTU = 517
private const val SCAN_TIMEOUT_MS = 15_000L
/** Long enough for the user to find and type the passkey in the system dialog. */
private const val BOND_TIMEOUT_MS = 60_000L
private const val GATT_OP_TIMEOUT_MS = 5_000L
/** After a working link dropped: try again soon. */
private const val RECONNECT_DELAY_MS = 1_000L
/** After failed attempts: 2, 4, 8, 16, then every 30 s, so a broken pairing does not spin. */
private const val RETRY_BASE_MS = 2_000L
private const val RETRY_MAX_MS = 30_000L
/** ATT status codes Android reports when the link lacks the encryption a characteristic needs. */
private val AUTH_FAILURES = setOf(5, 8, 15, 137)
/** Which characteristic a phone -> micrOBU message is written to, by opcode (as ble_transport.py). */
fun writeTarget(opcode: Int): UUID = when (opcode) {
LinkOpcode.BTP_DATA_REQUEST -> BTP_REQUEST
LinkOpcode.POTI_UPDATE -> POTI
0x08 /* SF_IDCHANGE_EVENT_RESPONSE */ -> ID_EVENT
else -> CONFIG
}
}
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)
private val _state = MutableStateFlow(Esp32LinkState.DISCONNECTED)
val state: StateFlow<Esp32LinkState> = _state.asStateFlow()
/** Why the last attempt failed, or what the user has to do (e.g. type the passkey); null when fine. */
private val _detail = MutableStateFlow<String?>(null)
val detail: StateFlow<String?> = _detail.asStateFlow()
private val _incoming = MutableSharedFlow<ByteArray>(extraBufferCapacity = 256)
/** Every link message the micrOBU notifies, one GATT value each. */
val incoming: SharedFlow<ByteArray> = _incoming.asSharedFlow()
/** Name of the connected micrOBU, e.g. "micrOBU-4AF8". */
@Volatile var deviceName: String? = null
private set
@Volatile private var gatt: BluetoothGatt? = null
/** Whether the current GATT connection is up, as the last connection-state callback said. */
@Volatile private var linkUp = false
@Volatile private var wanted = false
private var sessionJob: Job? = null
/** One GATT operation at a time: Android drops a second one issued before the first completes. */
private val gattMutex = Mutex()
@Volatile private var pendingOp: CompletableDeferred<Int>? = null
@Volatile private var connected: CompletableDeferred<Boolean>? = null
@Volatile private var mtuDone: CompletableDeferred<Int>? = null
@Volatile private var servicesDone: CompletableDeferred<Boolean>? = null
fun hasPermissions(): Boolean {
val needed = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
listOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
} else {
listOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
return needed.all { context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED }
}
/** Scans for (or reuses the bond with) a micrOBU, pairs if needed, and opens the link. No-op if already under way. */
fun connect() {
if (sessionJob?.isActive == true) return
wanted = true
sessionJob = scope.launch {
var failures = 0
while (wanted) {
val ok = try {
session()
} catch (e: CancellationException) {
throw e // disconnect(): not a failure to report
} catch (e: Exception) {
fail("BLE session failed: ${e.message}")
}
closeGatt()
if (!wanted) break
failures = if (ok) 0 else failures + 1
_state.value = if (ok) Esp32LinkState.DEVICE_ATTACHED else Esp32LinkState.ERROR
delay(if (ok) RECONNECT_DELAY_MS
else minOf(RETRY_MAX_MS, RETRY_BASE_MS shl (failures - 1).coerceAtMost(4)))
}
_state.value = Esp32LinkState.DISCONNECTED
}
}
fun disconnect() {
wanted = false
sessionJob?.cancel()
sessionJob = null
closeGatt()
_state.value = Esp32LinkState.DISCONNECTED
_detail.value = null
}
/**
* Writes one complete link message to the characteristic of its opcode, with response.
* Suspends until the micrOBU acknowledged the write; false when not connected or it failed.
*/
@SuppressLint("MissingPermission")
suspend fun send(message: ByteArray): Boolean {
val g = gatt ?: return false
if (_state.value != Esp32LinkState.CONNECTED || message.isEmpty()) return false
val characteristic = g.getService(SERVICE)?.getCharacteristic(writeTarget(message[0].toInt() and 0xFF))
?: return false
return gattOp {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
g.writeCharacteristic(characteristic, message, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) ==
BluetoothGatt.GATT_SUCCESS
} else {
@Suppress("DEPRECATION")
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
@Suppress("DEPRECATION")
characteristic.value = message
@Suppress("DEPRECATION")
g.writeCharacteristic(characteristic)
}
} == BluetoothGatt.GATT_SUCCESS
}
/** One connection lifetime. Returns true if it reached CONNECTED before ending. */
@SuppressLint("MissingPermission")
private suspend fun session(): Boolean {
if (!hasPermissions()) {
wanted = false
return fail("Bluetooth permission not granted (Android Settings > Apps > MicrOBU > Permissions)")
}
val adapter = bluetoothManager?.adapter
if (adapter == null || !adapter.isEnabled) return fail("Bluetooth is off")
_state.value = Esp32LinkState.DEVICE_ATTACHED
// A bonded micrOBU is reused without scanning: its address is stable (public address), and
// this is what makes a reconnect after a dropout fast.
val device = adapter.bondedDevices.firstOrNull { it.name?.startsWith(NAME_PREFIX) == true }
?: scan() ?: return fail("No micrOBU advertising nearby (is the phone on its USB port?)")
deviceName = device.name
_detail.value = null
Log.i(TAG, "connecting to ${device.name} ${device.address} (bond state ${device.bondState})")
connected = CompletableDeferred()
gatt = device.connectGatt(context, false, callback, BluetoothDevice.TRANSPORT_LE)
if (withTimeoutOrNull(GATT_OP_TIMEOUT_MS * 2) { connected!!.await() } != true) {
return fail("Could not connect to ${device.name}")
}
val g = gatt ?: return false
// Services first: discovery needs no encryption, and the encryption probe below needs the
// STATUS characteristic.
servicesDone = CompletableDeferred()
g.discoverServices()
if (withTimeoutOrNull(GATT_OP_TIMEOUT_MS) { servicesDone!!.await() } != true) {
return fail("Service discovery on ${device.name} timed out")
}
if (g.getService(SERVICE) == null) {
return fail("${device.name} does not offer the station-link service (old firmware?)")
}
// Encryption before anything else. Every characteristic needs an encrypted, authenticated
// link; the board asks for security as soon as a phone connects. A phone it has a bond with
// encrypts with the stored key. Otherwise Android pairs, with its passkey dialog, which takes
// as long as the user takes. A GATT operation with a short timeout during that cuts the
// pairing off, the link drops, and the next attempt starts pairing again: a loop.
val wasBonded = device.bondState == BluetoothDevice.BOND_BONDED
if (!awaitEncryption(g, device)) {
if (!linkUp) return fail("${device.name} dropped the link while pairing")
return fail(
if (wasBonded) "${device.name} refused this phone's stored pairing. In Android's Bluetooth " +
"settings, forget ${device.name}, then Connect again (passkey $PASSKEY)."
else "Pairing with ${device.name} failed or timed out (passkey $PASSKEY)"
)
}
_state.value = Esp32LinkState.DEVICE_ATTACHED
_detail.value = null
mtuDone = CompletableDeferred()
g.requestMtu(REQUESTED_MTU)
val mtu = withTimeoutOrNull(GATT_OP_TIMEOUT_MS) { mtuDone!!.await() } ?: 23
Log.i(TAG, "ATT MTU $mtu")
if (mtu < LINK_MAX_MESSAGE + 3) {
// The board drops a notification that does not fit rather than truncate it (simple_ble.cpp).
Log.w(TAG, "MTU $mtu is below ${LINK_MAX_MESSAGE + 3}: large V2X_RX messages will not arrive")
}
for (uuid in listOf(RESULT, BTP_INDICATION, ID_EVENT)) {
if (!enableNotifications(g, uuid)) return fail("Could not subscribe to ${device.name} notifications")
}
_state.value = Esp32LinkState.CONNECTED
Log.i(TAG, "BLE station link ready: ${device.name}")
// Wait until the link drops (callback completes `connected` anew with false).
val dropped = CompletableDeferred<Boolean>()
connected = dropped
dropped.await()
Log.w(TAG, "BLE link to ${device.name} lost")
return true
}
@SuppressLint("MissingPermission")
private suspend fun scan(): BluetoothDevice? {
val scanner = bluetoothManager?.adapter?.bluetoothLeScanner ?: return null
val found = CompletableDeferred<BluetoothDevice>()
val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val name = result.scanRecord?.deviceName ?: result.device.name
val offersService = result.scanRecord?.serviceUuids?.any { it.uuid == SERVICE } == true
if (offersService || name?.startsWith(NAME_PREFIX) == true) found.complete(result.device)
}
override fun onScanFailed(errorCode: Int) {
Log.w(TAG, "BLE scan failed: $errorCode")
}
}
val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
scanner.startScan(null, settings, scanCallback)
return try {
withTimeoutOrNull(SCAN_TIMEOUT_MS) { found.await() }
} finally {
runCatching { scanner.stopScan(scanCallback) }
}
}
/**
* Returns once the link is encrypted, pairing first if needed; false if that fails or the user
* does not finish within [BOND_TIMEOUT_MS].
*
* The probe is a read of the STATUS characteristic, which needs an encrypted and authenticated
* link, as the colleague's Python transport does. Android answers a read the link is not
* secure enough for by encrypting, or by pairing and showing the passkey dialog, and then
* retries the read itself. While it pairs, the card says which passkey to type.
*/
@SuppressLint("MissingPermission")
private suspend fun awaitEncryption(g: BluetoothGatt, device: BluetoothDevice): Boolean {
val status = g.getService(SERVICE)?.getCharacteristic(STATUS_CHAR) ?: return false
val receiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
val changed = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
} else {
@Suppress("DEPRECATION") intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
}
if (changed?.address != device.address) return
val state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)
Log.i(TAG, "bond state of ${device.name}: $state")
if (state == BluetoothDevice.BOND_BONDING) {
_state.value = Esp32LinkState.PERMISSION_REQUESTED
_detail.value = "Pair with ${device.name}: enter passkey $PASSKEY"
}
}
}
register(receiver)
try {
if (device.bondState == BluetoothDevice.BOND_BONDING) {
_state.value = Esp32LinkState.PERMISSION_REQUESTED
_detail.value = "Pair with ${device.name}: enter passkey $PASSKEY"
}
// Up to two reads: the first can come back with an authentication error at the moment
// pairing completes, before Android's own retry.
repeat(2) { attempt ->
val result = gattOp(BOND_TIMEOUT_MS) { g.readCharacteristic(status) }
Log.i(TAG, "encryption probe ${attempt + 1}: status $result, bond state ${device.bondState}")
if (result == BluetoothGatt.GATT_SUCCESS) return true
if (result !in AUTH_FAILURES) return false
}
return false
} finally {
runCatching { context.unregisterReceiver(receiver) }
}
}
private fun register(receiver: BroadcastReceiver) {
val filter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag") context.registerReceiver(receiver, filter)
}
}
/** Logs [reason], shows it on the connection card, and ends the attempt. */
private fun fail(reason: String): Boolean {
Log.w(TAG, reason)
_detail.value = reason
return false
}
@SuppressLint("MissingPermission")
private suspend fun enableNotifications(g: BluetoothGatt, uuid: UUID): Boolean {
val characteristic = g.getService(SERVICE)?.getCharacteristic(uuid) ?: return false
if (!g.setCharacteristicNotification(characteristic, true)) return false
val cccd = characteristic.getDescriptor(CCCD) ?: return false
val value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
return gattOp {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
g.writeDescriptor(cccd, value) == BluetoothGatt.GATT_SUCCESS
} else {
@Suppress("DEPRECATION") cccd.value = value
@Suppress("DEPRECATION") g.writeDescriptor(cccd)
}
} == BluetoothGatt.GATT_SUCCESS
}
/** Starts one GATT operation and waits for its callback's status; -1 if it could not start or timed out. */
private suspend fun gattOp(timeoutMs: Long = GATT_OP_TIMEOUT_MS, start: () -> Boolean): Int = gattMutex.withLock {
val op = CompletableDeferred<Int>()
pendingOp = op
if (!start()) {
pendingOp = null
return@withLock -1
}
withTimeoutOrNull(timeoutMs) { op.await() } ?: -1
}
@SuppressLint("MissingPermission")
private fun closeGatt() {
gatt?.let { runCatching { it.disconnect(); it.close() } }
gatt = null
pendingOp?.complete(-1)
connected?.complete(false)
}
private val callback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
Log.i(TAG, "connection state $newState (status $status)")
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
linkUp = true
connected?.complete(true)
}
BluetoothProfile.STATE_DISCONNECTED -> {
linkUp = false
connected?.complete(false)
pendingOp?.complete(-1)
if (_state.value == Esp32LinkState.CONNECTED) _state.value = Esp32LinkState.ERROR
}
}
}
override fun onMtuChanged(g: BluetoothGatt, mtu: Int, status: Int) {
mtuDone?.complete(mtu)
}
override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
servicesDone?.complete(status == BluetoothGatt.GATT_SUCCESS)
}
override fun onDescriptorWrite(g: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
pendingOp?.complete(status)
}
override fun onCharacteristicWrite(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
pendingOp?.complete(status)
}
// API 33+ calls this overload; older versions the deprecated one below.
override fun onCharacteristicRead(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic,
value: ByteArray, status: Int) {
pendingOp?.complete(status)
}
@Deprecated("Deprecated in API 33")
override fun onCharacteristicRead(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
pendingOp?.complete(status)
}
// API 33+ delivers the value here and no longer calls the deprecated overload below.
override fun onCharacteristicChanged(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) {
_incoming.tryEmit(value.copyOf())
}
@Deprecated("Deprecated in API 33")
override fun onCharacteristicChanged(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
@Suppress("DEPRECATION")
characteristic.value?.let { _incoming.tryEmit(it.copyOf()) }
}
}
}
@@ -1,422 +0,0 @@
package com.hawhamburg.micr0bu.data.transport
import android.content.Context
import android.os.SystemClock
import android.util.Log
import com.hawhamburg.micr0bu.data.cam.PseudonymManager
import com.hawhamburg.micr0bu.data.mqtt.ObuHardwarePreferences
import com.hawhamburg.micr0bu.domain.asn1.ItsTime
import com.hawhamburg.micr0bu.domain.cam.StationType
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.roundToInt
/** Connection lifecycle of the ESP32-C5 link, over either transport. */
enum class Esp32LinkState { DISCONNECTED, DEVICE_ATTACHED, PERMISSION_REQUESTED, CONNECTED, ERROR }
/** Which physical link the phone uses to reach the ESP32-C5 (Settings). */
enum class Esp32Transport(val id: String) { USB("usb"), BLE("ble") }
/** What the phone transmits while a trip records (Settings). */
enum class OutgoingMessage(val id: String) { CAM("cam"), VAM("vam") }
/**
* What the board on the other end speaks. The phone cannot ask, so it listens: the previous
* obu-firmware sends a [SerialFrameType.STATUS] heartbeat, the current one a station-link STATUS.
*/
enum class Esp32Protocol { UNKNOWN, LEGACY_SERIAL, STATION_LINK }
/** One ITS message for the air, with what the micrOBU needs to know about the sender. */
class OutgoingIts(
val kind: OutgoingMessage,
val uper: ByteArray,
/** Pseudonym MAC, station type and position; its [GnPositionVector.tstMs] is the fix time. */
val positionVector: GnPositionVector,
/** Android horizontal accuracy, metres (68 %); null when unknown. */
val accuracyM: Float?,
val signed: Boolean,
)
/**
* The one entry point the app uses to talk to the ESP32-C5: picks USB ([UsbSerialTransport]) or
* BLE ([BleLinkTransport]) from the setting, works out which firmware protocol is on the other end,
* and runs the station-link session the current firmware needs.
*
* ## Station-link session (obu-firmware since 2026-09-23)
* The firmware keeps no state the phone depends on, except what it stores itself (credentials in
* NVS), so the phone sets it up each time it sees it unconfigured:
* 1. STATION_CONFIGURE, with the current pseudonym MAC as the GN address and 802.11 source. This
* also starts the radio, which until then neither transmits nor receives.
* 2. If the answer reports no authorization ticket, CREDENTIALS_PROVISION of the demo bundle in
* `assets/demo-chain.vcr` (a disposable chain, not EU-registered: receivers that verify against
* the EU trust list will drop what it signs). The firmware keeps it in NVS from then on.
* 3. Per message: POTI_UPDATE (the fix, which also sets the micrOBU's ITS clock for the signature
* time), then BTP_DATA_REQUEST, secured or unsecured per the "Sign outgoing messages" setting.
* A pseudonym change reconfigures with the new MAC before the next message goes out. A STATUS
* saying "not configured" (the board reset) starts again at 1.
*
* ## Legacy firmware
* A board still on the previous obu-firmware (0xAA55 frames 0x01-0x05, no signing, USB only)
* keeps working for CAM exactly as before. VAM needs the current firmware.
*
* Everything received is surfaced the old way, as [DecodedFrame]s of [SerialFrameType.V2X_RX], so
* the receive side of the app did not change.
*/
@Singleton
class Esp32Link @Inject constructor(
@ApplicationContext private val context: Context,
private val usb: UsbSerialTransport,
private val ble: BleLinkTransport,
private val prefs: ObuHardwarePreferences,
private val pseudonymManager: PseudonymManager,
) {
companion object {
private const val TAG = "Esp32Link"
private const val REPLY_TIMEOUT_MS = 3_000L
/** Applying a bundle verifies the chain and rebuilds the stack on the C5: seconds, not ms. */
private const val PROVISION_TIMEOUT_MS = 15_000L
private const val SEGMENT_SIZE = 240
private const val DEMO_BUNDLE_ASSET = "demo-chain.vcr"
/**
* A PoTi this far behind the last one sent is a real correction of the phone's clock (e.g.
* GNSS time taking over from a wrong system clock), not a repeated fix; it goes through
* and the micrOBU restarts its stack at the new time once.
*/
private const val CLOCK_STEP_BACK_MS = 60_000L
/** How long a refusal stays on the connection card. */
private const val DETAIL_HOLD_MS = 10_000L
const val BTP_PORT_CAM = 2001
const val BTP_PORT_VAM = 2018
const val ITS_AID_CAM = 36L
const val ITS_AID_VAM = 638L
/** CAM SSP version 1, no special-vehicle permissions: what the demo ticket grants for ITS-AID 36. */
val SSP_CAM = byteArrayOf(0x01, 0x00, 0x00)
/** VRU SSP as the demo ticket grants for ITS-AID 638 (same as the colleague's VBS). */
val SSP_VAM = byteArrayOf(0x01)
}
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
val transport: StateFlow<Esp32Transport> =
prefs.esp32TransportFlow.stateIn(scope, SharingStarted.Eagerly, Esp32Transport.USB)
val state: StateFlow<Esp32LinkState> = combine(transport, usb.state, ble.state) { t, u, b ->
if (t == Esp32Transport.USB) u else b
}.stateIn(scope, SharingStarted.Eagerly, Esp32LinkState.DISCONNECTED)
private val _protocol = MutableStateFlow(Esp32Protocol.UNKNOWN)
val protocol: StateFlow<Esp32Protocol> = _protocol.asStateFlow()
private val _incomingFrames = MutableSharedFlow<DecodedFrame>(extraBufferCapacity = 256)
/** Received ITS messages ([SerialFrameType.V2X_RX]) and, from legacy firmware, its heartbeats. */
val incomingFrames: SharedFlow<DecodedFrame> = _incomingFrames.asSharedFlow()
private val _linkStatus = MutableStateFlow<EspLinkStatus?>(null)
/** Heartbeat counters in the old shape, from either firmware; null before the first one. */
val linkStatus: StateFlow<EspLinkStatus?> = _linkStatus.asStateFlow()
private val _stationStatus = MutableStateFlow<StationStatus?>(null)
/** Full station-link STATUS (signing counters, tickets); null with legacy firmware. */
val stationStatus: StateFlow<StationStatus?> = _stationStatus.asStateFlow()
private val _detail = MutableStateFlow<String?>(null)
/** One line for the UI about the session: pairing, provisioning, or why it is stuck. */
val detail: StateFlow<String?> = _detail.asStateFlow()
private val _consecutiveWriteFailures = MutableStateFlow(0)
val consecutiveWriteFailures: StateFlow<Int> = _consecutiveWriteFailures.asStateFlow()
private val _refusedRequests = MutableStateFlow(0)
/** BTP_DATA_REQUESTs the micrOBU answered with anything but accepted (e.g. no ticket). */
val refusedRequests: StateFlow<Int> = _refusedRequests.asStateFlow()
private val sequence = AtomicInteger(0)
private val pending = ConcurrentHashMap<Int, CompletableDeferred<LinkResult>>()
private val sessionMutex = Mutex()
/** The STATION_CONFIGURE the micrOBU is known to run, or null when it has to be sent again. */
@Volatile private var configured: StationConfigure? = null
@Volatile private var lastRefusalLogMs = 0L
/**
* Timestamp of the last POTI_UPDATE sent in this session, or null when the micrOBU's ITS clock
* has to be set again (new session, board reset).
*/
@Volatile private var lastPotiMs: Long? = null
/** [SystemClock.elapsedRealtime] when [lastPotiMs] was sent: with it, where the micrOBU's clock stands now. */
@Volatile private var lastPotiElapsedMs = 0L
/** The fix time (before any clamping) of the last PoTi sent, to send each fix only once. */
@Volatile private var lastPotiFixMs: Long? = null
init {
scope.launch { usb.incomingFrames.collect { onUsbFrame(it) } }
scope.launch { ble.incoming.collect { onLinkMessage(it) } }
scope.launch { usb.linkStatus.collect { if (it != null) _linkStatus.value = it } }
// A new connection, on either transport, starts a new session.
scope.launch {
state.collect { s ->
if (s != Esp32LinkState.CONNECTED) {
configured = null; lastPotiMs = null; lastPotiFixMs = null
_protocol.value = Esp32Protocol.UNKNOWN
_stationStatus.value = null
_linkStatus.value = null
pending.values.forEach { it.cancel() }
pending.clear()
} else if (transport.value == Esp32Transport.BLE) {
_protocol.value = Esp32Protocol.STATION_LINK // BLE exists only on the current firmware
scope.launch { ensureConfigured(null) }
}
}
}
scope.launch { ble.detail.collect { if (transport.value == Esp32Transport.BLE) _detail.value = it } }
// Switching transport in Settings closes the other one; connecting stays a user action.
scope.launch {
transport.collect { t ->
if (t == Esp32Transport.USB) ble.disconnect() else usb.disconnect()
_detail.value = null
}
}
}
fun connect() {
if (transport.value == Esp32Transport.USB) usb.connect() else ble.connect()
}
fun disconnect() {
usb.disconnect()
ble.disconnect()
}
/**
* Hands one message to the micrOBU for transmission. False when it could not be handed over
* (no link, legacy firmware asked for a VAM, session setup failed); the next message retries.
* Acceptance by the micrOBU is not awaited: a refusal shows up in [refusedRequests].
*/
suspend fun send(its: OutgoingIts): Boolean = withContext(Dispatchers.IO) {
val ok = when (_protocol.value) {
Esp32Protocol.LEGACY_SERIAL -> {
if (its.kind == OutgoingMessage.CAM) {
usb.sendCamTx(its.uper, its.positionVector)
} else {
noteRefusal("VAM needs the current obu-firmware; this board runs the previous one")
false
}
}
Esp32Protocol.STATION_LINK -> sendStationLink(its)
Esp32Protocol.UNKNOWN -> false // no heartbeat yet: nothing to address
}
if (ok) _consecutiveWriteFailures.value = 0 else _consecutiveWriteFailures.value++
ok
}
private suspend fun sendStationLink(its: OutgoingIts): Boolean {
val pv = its.positionVector
if (!ensureConfigured(StationConfigure(stationType = pv.stationType, mid = pv.mac))) return false
// The micrOBU's ITS clock must never be sent backwards: past 1 s it answers
// time_regression and rebuilds its whole stack. Two things tried to. The transmit loops
// re-send the latest GNSS fix every tick while fused location pauses, so an old fix time
// arrived again and again while the micrOBU's clock ran on. And the GNSS-corrected fix
// times themselves wobble by seconds indoors (measured 2026-09-23: -2.1 s, +4.9 s between
// consecutive CAMs). So a fix goes over once, and its timestamp is never below where the
// micrOBU's clock stands now, except for a real correction of the phone clock.
val poti = potiFor(its)
if (poti.timestampMs != lastPotiFixMs) {
val last = lastPotiMs
val microbuNow = last?.let { it + (SystemClock.elapsedRealtime() - lastPotiElapsedMs) }
val timestamp = when {
microbuNow == null -> poti.timestampMs
poti.timestampMs < microbuNow - CLOCK_STEP_BACK_MS -> poti.timestampMs
else -> maxOf(poti.timestampMs, microbuNow)
}
if (!write(LinkOpcode.POTI_UPDATE, poti.copy(timestampMs = timestamp).encode())) return false
lastPotiFixMs = poti.timestampMs
lastPotiMs = timestamp
lastPotiElapsedMs = SystemClock.elapsedRealtime()
}
val request = BtpDataRequest(
destinationPort = if (its.kind == OutgoingMessage.CAM) BTP_PORT_CAM else BTP_PORT_VAM,
itsAid = if (its.kind == OutgoingMessage.CAM) ITS_AID_CAM else ITS_AID_VAM,
securityProfile = if (its.signed) LinkSecurityProfile.SECURED else LinkSecurityProfile.UNSECURED,
permissions = if (its.kind == OutgoingMessage.CAM) SSP_CAM else SSP_VAM,
flSdu = its.uper,
)
return write(LinkOpcode.BTP_DATA_REQUEST, request.encode())
}
/**
* Makes sure the micrOBU runs [wanted] (or, when null, any configuration: used right after a
* BLE connect or a board reset, to start its receiver before the first message goes out).
*/
private suspend fun ensureConfigured(wanted: StationConfigure?): Boolean = sessionMutex.withLock {
val current = configured
if (current != null && (wanted == null || current == wanted)) return@withLock true
val config = wanted ?: StationConfigure(stationType = StationType.CYCLIST, mid = pseudonymManager.current().mac)
_detail.value = "Configuring the micrOBU"
val result = request(LinkOpcode.STATION_CONFIGURE, config.encode(), REPLY_TIMEOUT_MS)
if (result == null || !result.accepted) {
_detail.value = "micrOBU did not accept the configuration (${result?.let { LinkResultCode.name(it.code) } ?: "no reply"})"
return@withLock false
}
val info = StationInfo.decode(result.detail)
Log.i(TAG, "station configured: $info")
if (info == null || !info.credentialsLoaded || info.tickets == 0) {
if (!provisionDemoCredentials()) return@withLock false
}
configured = config
_detail.value = null
true
}
private suspend fun provisionDemoCredentials(): Boolean {
_detail.value = "Provisioning the demo credentials"
val bundle = runCatching { context.assets.open(DEMO_BUNDLE_ASSET).use { it.readBytes() } }.getOrElse {
_detail.value = "Demo credential bundle missing from the app"
return false
}
var offset = 0
while (offset < bundle.size) {
val segment = bundle.copyOfRange(offset, minOf(bundle.size, offset + SEGMENT_SIZE))
val last = offset + segment.size == bundle.size
val result = request(LinkOpcode.CREDENTIALS_PROVISION, credentialsSegment(bundle.size, offset, segment),
if (last) PROVISION_TIMEOUT_MS else REPLY_TIMEOUT_MS)
if (result == null || !result.accepted) {
_detail.value = "micrOBU refused the demo credentials (${result?.let { LinkResultCode.name(it.code) } ?: "no reply"})"
return false
}
if (last) {
val d = result.detail
Log.i(TAG, "demo credentials provisioned: " +
if (d.size == 3) "${d[0]} root(s), ${d[1]} authorit(ies), ${d[2]} ticket(s)" else "no report")
}
offset += segment.size
}
return true
}
private fun potiFor(its: OutgoingIts): PotiUpdate {
val pv = its.positionVector
// Semi-axes of the 95 % ellipse from Android's 68 % radius (circular error), as GnPositionVector's PAI bound.
val semiCm = its.accuracyM?.takeIf { it > 0f && it.isFinite() }
?.let { (it * 1.62f * 100).roundToInt().coerceAtMost(65_535) } ?: 0
return PotiUpdate(
timestampMs = fullTimestampIts(pv.tstMs),
latTenMicroDeg = pv.latTenMicroDeg,
lonTenMicroDeg = pv.lonTenMicroDeg,
semiMajorCm = semiCm,
semiMinorCm = semiCm,
speedCms = pv.speedCms.coerceAtLeast(0),
headingDeciDeg = pv.headingDeciDeg,
pai = pv.pai,
)
}
/** [GnPositionVector.tstMs] is already the full TimestampIts; guard against a reduced one anyway. */
private fun fullTimestampIts(tstMs: Long): Long {
if (tstMs > 0xFFFF_FFFFL) return tstMs
val now = ItsTime.timestampIts(System.currentTimeMillis())
return now - ((now - tstMs) and 0xFFFF_FFFFL)
}
private suspend fun request(opcode: Int, body: ByteArray, timeoutMs: Long): LinkResult? {
val seq = nextSequence()
val reply = CompletableDeferred<LinkResult>()
pending[seq] = reply
return try {
if (!writeMessage(LinkMessage(opcode, seq, body).encode())) null
else withTimeoutOrNull(timeoutMs) { reply.await() }
} finally {
pending.remove(seq)
}
}
private suspend fun write(opcode: Int, body: ByteArray): Boolean =
writeMessage(LinkMessage(opcode, nextSequence(), body).encode())
private suspend fun writeMessage(message: ByteArray): Boolean =
if (transport.value == Esp32Transport.USB) usb.sendFrame(SERIAL_FRAME_LINK, message)
else ble.send(message)
private fun nextSequence(): Int = sequence.incrementAndGet() and 0xFFFF
private fun onUsbFrame(frame: DecodedFrame) {
when (frame.type) {
SerialFrameType.STATUS -> _protocol.value = Esp32Protocol.LEGACY_SERIAL
SerialFrameType.V2X_RX -> _incomingFrames.tryEmit(frame)
SERIAL_FRAME_LINK -> onLinkMessage(frame.payload)
}
}
private fun onLinkMessage(octets: ByteArray) {
val message = LinkMessage.decode(octets) ?: return
when (message.opcode) {
LinkOpcode.V2X_RX -> _incomingFrames.tryEmit(DecodedFrame(SerialFrameType.V2X_RX, message.body))
LinkOpcode.RESULT -> {
val result = LinkResult.decode(message.body) ?: return
val waiting = pending.remove(message.sequence)
if (waiting != null) {
waiting.complete(result)
} else if (result.code == LinkResultCode.TIME_REGRESSION) {
// Only sent for a deliberate clock correction (see CLOCK_STEP_BACK_MS): the
// micrOBU accepted the new time and restarted its stack. Not a refusal.
Log.i(TAG, "micrOBU followed a step back of the phone's clock and restarted its stack")
} else if (!result.accepted) {
// A POTI_UPDATE or BTP_DATA_REQUEST the micrOBU refused (they are not awaited).
_refusedRequests.value++
if (result.code == LinkResultCode.NOT_CONFIGURED) { configured = null; lastPotiMs = null; lastPotiFixMs = null }
noteRefusal("micrOBU refused a request: ${LinkResultCode.name(result.code)}")
}
}
LinkOpcode.STATUS -> {
val status = StationStatus.decode(message.body) ?: return
val first = _protocol.value != Esp32Protocol.STATION_LINK
_protocol.value = Esp32Protocol.STATION_LINK
_stationStatus.value = status
_linkStatus.value = EspLinkStatus(
status = 0,
oversizeDrops = 0,
txFailures = status.radioFailed.coerceAtMost(0xFFFF).toInt(),
rxCrcErrors = status.linkCrcErrors.coerceAtMost(0xFFFF).toInt(),
rxQueueDrops = status.radioDropped.coerceAtMost(0xFFFF).toInt(),
)
// Board reset (or first contact): configure now, so its receiver runs even before
// the first message is sent.
if (!status.configured) { configured = null; lastPotiMs = null; lastPotiFixMs = null }
if (first || !status.configured) scope.launch { ensureConfigured(null) }
}
}
}
private fun noteRefusal(line: String) {
val now = System.currentTimeMillis()
if (now - lastRefusalLogMs < 5_000) return
lastRefusalLogMs = now
Log.w(TAG, line)
_detail.value = line
// A refusal is news, not a state: it leaves the card again unless something replaced it.
scope.launch {
delay(DETAIL_HOLD_MS)
_detail.compareAndSet(line, null)
}
}
}
@@ -57,9 +57,8 @@ const val SERIAL_LINK_MAX_PAYLOAD = 512
/** /**
* Decoded [SerialFrameType.STATUS] payload: `[status:1][oversizeDrops:2 LE][txFailures:2 LE] * Decoded [SerialFrameType.STATUS] payload: `[status:1][oversizeDrops:2 LE][txFailures:2 LE]
* [rxCrcErrors:2 LE][capabilities:1][rxQueueDrops:2 LE]` (10 bytes; the last two fields are an * [rxCrcErrors:2 LE]` (7 bytes). Counters are free-running totals since firmware boot and
* optional tail — see [capabilities] and [rxQueueDrops]). Counters are free-running totals since * saturate at 0xFFFF rather than wrapping.
* firmware boot and saturate at 0xFFFF rather than wrapping.
* *
* Exists so the phone can tell "link alive, no traffic" from "link dead", and so firmware-side * Exists so the phone can tell "link alive, no traffic" from "link dead", and so firmware-side
* drops — which otherwise only reach `ESP_LOGW` on the flashing port that the phone isn't * drops — which otherwise only reach `ESP_LOGW` on the flashing port that the phone isn't
@@ -80,15 +79,6 @@ data class EspLinkStatus(
* the phone needs from such firmware: it accepts nothing beyond the original messages. * the phone needs from such firmware: it accepts nothing beyond the original messages.
*/ */
val capabilities: Int = 0, val capabilities: Int = 0,
/**
* Promiscuously-captured frames the firmware's `wifi_promisc_rx_cb` had to drop because its
* RX queue (8 deep) was still full of frames `rx_forward_task` hadn't finished forwarding —
* bytes 8-9 of the payload. 0 for firmware that predates this field (payload of 7 or 8 bytes),
* which is the honest answer: such firmware drops these frames identically, it just never
* counted them. A nonzero, growing value here — as opposed to [oversizeDrops] — points at
* bursty RX outrunning the forward task rather than any one frame being too large.
*/
val rxQueueDrops: Int = 0,
) { ) {
/** True when the firmware accepts [SerialFrameType.CAM_TX_PV]. */ /** True when the firmware accepts [SerialFrameType.CAM_TX_PV]. */
val supportsCamTxPv: Boolean get() = capabilities and CAP_CAM_TX_PV != 0 val supportsCamTxPv: Boolean get() = capabilities and CAP_CAM_TX_PV != 0
@@ -109,7 +99,6 @@ data class EspLinkStatus(
txFailures = u16(3), txFailures = u16(3),
rxCrcErrors = u16(5), rxCrcErrors = u16(5),
capabilities = if (payload.size > PAYLOAD_SIZE) payload[7].toInt() and 0xFF else 0, capabilities = if (payload.size > PAYLOAD_SIZE) payload[7].toInt() and 0xFF else 0,
rxQueueDrops = if (payload.size >= 10) u16(8) else 0,
) )
} }
} }
@@ -1,274 +0,0 @@
package com.hawhamburg.micr0bu.data.transport
/**
* Phone side of the station-link message layer, version 1: the protocol of the colleague's
* vanetza-idf ESP32-C5 firmware (microbu-esp32c5/station-link/README.md), which obu-firmware runs
* since 2026-09-23. Kotlin counterpart of `obu-firmware/main/link_protocol.hpp` and of the
* colleague's Python `microbu_link/messages.py`; the unit test pins these encoders to bytes that
* Python module produced.
*
* Transport independent: over USB each message is the payload of one serial frame of type
* [SERIAL_FRAME_LINK] (same 0xAA55 framing as before, see [SerialFrameEncoder]); over BLE each
* message is one GATT value (see [BleLinkTransport]).
*
* Message: `[opcode:1][flags:1][sequence:2 LE][body]`, at most [LINK_MAX_MESSAGE] octets, all
* integers little-endian. The phone numbers its requests; the firmware answers with a RESULT
* carrying the same sequence.
*
* Only what this app uses is implemented: configure, PoTi, BTP-DATA.request, credential
* provisioning, RESULT, STATUS, and the MicrOBU extension [LinkOpcode.V2X_RX]. The SF-SAP
* identifier-change primitives are not used: the app owns its pseudonym (see
* [com.hawhamburg.micr0bu.data.cam.PseudonymManager]) and reconfigures the station on a change.
*/
const val SERIAL_FRAME_LINK = 0x10
const val LINK_MAX_MESSAGE = 512
const val LINK_HEADER_SIZE = 4
object LinkOpcode {
const val STATION_CONFIGURE = 0x01
const val POTI_UPDATE = 0x02
const val BTP_DATA_REQUEST = 0x03
const val CREDENTIALS_PROVISION = 0x04
const val CREDENTIALS_ERASE = 0x05
const val STATUS_REQUEST = 0x0C
const val RESULT = 0x80
const val BTP_DATA_INDICATION = 0x81
const val STATUS = 0x84
/** MicrOBU extension: body is exactly the old [SerialFrameType.V2X_RX] payload ([V2xRxFrame]). */
const val V2X_RX = 0x85
}
/** RESULT codes: vanetza_idf::Result first, then the link's own. */
object LinkResultCode {
const val ACCEPTED = 0
const val TIME_REGRESSION = 7
const val NOT_CONFIGURED = 0x12
fun name(code: Int): String = when (code) {
0 -> "accepted"; 1 -> "invalid_argument"; 2 -> "unsupported"; 3 -> "wrong_entry_point"
4 -> "security_unavailable"; 5 -> "resource_limit"; 6 -> "rejected"; 7 -> "time_regression"
8 -> "identity_change_pending"; 0x10 -> "unknown_opcode"; 0x11 -> "malformed"
0x12 -> "not_configured"; 0x13 -> "busy"; 0x14 -> "no_credentials"
else -> "code_$code"
}
}
/** GN security profile of a BTP-DATA.request (TS 103 300-3 Table 4). */
object LinkSecurityProfile {
const val STATION_DEFAULT = 0
const val UNSECURED = 1
const val SECURED = 2
}
class LinkMessage(val opcode: Int, val sequence: Int, val body: ByteArray, val flags: Int = 0) {
fun encode(): ByteArray {
require(LINK_HEADER_SIZE + body.size <= LINK_MAX_MESSAGE) {
"link message 0x%02x too long: %d".format(opcode, LINK_HEADER_SIZE + body.size)
}
return byteArrayOf(opcode.toByte(), flags.toByte(), sequence.toByte(), (sequence shr 8).toByte()) + body
}
companion object {
fun decode(octets: ByteArray): LinkMessage? {
if (octets.size < LINK_HEADER_SIZE || octets.size > LINK_MAX_MESSAGE) return null
val sequence = (octets[2].toInt() and 0xFF) or ((octets[3].toInt() and 0xFF) shl 8)
return LinkMessage(octets[0].toInt() and 0xFF, sequence,
octets.copyOfRange(LINK_HEADER_SIZE, octets.size), octets[1].toInt() and 0xFF)
}
}
}
internal class LinkWriter {
private val out = java.io.ByteArrayOutputStream()
fun u8(v: Int) = apply { out.write(v and 0xFF) }
fun u16(v: Int) = apply { u8(v); u8(v shr 8) }
fun u32(v: Long) = apply { for (i in 0 until 4) u8((v ushr (8 * i)).toInt()) }
fun i32(v: Int) = u32(v.toLong())
fun u64(v: Long) = apply { for (i in 0 until 8) u8((v ushr (8 * i)).toInt()) }
fun bytes(b: ByteArray) = apply { out.write(b) }
fun toByteArray(): ByteArray = out.toByteArray()
}
/**
* STATION_CONFIGURE body. (Re)creates the GeoNetworking stack and security entity on the micrOBU.
* [mid] is the pseudonym MAC: with [addressConfiguration] 0 (AUTO) it becomes both the GN_ADDR MID
* and the 802.11 source address, as with the old CAM_TX_PV prefix.
*/
data class StationConfigure(
val stationType: Int,
val mid: ByteArray,
val security: Int = 1,
val addressConfiguration: Int = 0,
val beaconing: Int = 0,
val channelNumber: Int = 180,
val transmitPowerDbm: Int = 20,
/** 0 off, 1 receive only, 2 transmit and receive. */
val radio: Int = 2,
/** Raw GN traffic class octet: TC-ID 2, as the previous firmware's geonet.c. */
val defaultTrafficClass: Int = 2,
/** Raw GN lifetime octet: 1 s. */
val defaultLifetime: Int = 0x05,
) {
init { require(mid.size == 6) }
fun encode(): ByteArray = LinkWriter()
.u8(stationType).u8(security).u8(addressConfiguration).bytes(mid).u8(beaconing)
.u16(channelNumber).u8(transmitPowerDbm).u8(radio).u8(defaultTrafficClass).u8(defaultLifetime)
.toByteArray()
override fun equals(other: Any?): Boolean = other is StationConfigure && encode().contentEquals(other.encode())
override fun hashCode(): Int = encode().contentHashCode()
}
/** RESULT detail of STATION_CONFIGURE. */
data class StationInfo(val credentialsLoaded: Boolean, val tickets: Int) {
companion object {
fun decode(detail: ByteArray): StationInfo? =
if (detail.size != 18) null
else StationInfo(detail[16].toInt() != 0, detail[17].toInt() and 0xFF)
}
}
/**
* POTI_UPDATE body (EN 302 890-2 minimum data set). [timestampMs] is TimestampIts under
* [com.hawhamburg.micr0bu.domain.asn1.ItsTime]'s convention; it also sets the micrOBU's ITS clock,
* which the security entity stamps into every signed message's generationTime.
*/
data class PotiUpdate(
val timestampMs: Long,
val latTenMicroDeg: Int,
val lonTenMicroDeg: Int,
val semiMajorCm: Int = 0,
val semiMinorCm: Int = 0,
val orientationDeciDeg: Int = 0,
val altitudeCm: Int? = null,
val speedCms: Int? = null,
val headingDeciDeg: Int? = null,
val pai: Boolean = false,
) {
fun encode(): ByteArray {
val flags = (if (altitudeCm != null) 1 else 0) or (if (speedCms != null) 2 else 0) or
(if (headingDeciDeg != null) 4 else 0) or (if (pai) 8 else 0)
return LinkWriter().u64(timestampMs).i32(latTenMicroDeg).i32(lonTenMicroDeg)
.u16(semiMajorCm).u16(semiMinorCm).u16(orientationDeciDeg).u8(flags)
.i32(altitudeCm ?: 0).u16(speedCms ?: 0).u16(headingDeciDeg ?: 0)
.toByteArray()
}
}
/**
* BTP_DATA_REQUEST body for a BTP-B single-hop broadcast, the only shape this app sends (CAM, VAM).
* [permissions] is the SSP the authorization ticket must carry for [itsAid].
*/
data class BtpDataRequest(
val destinationPort: Int,
val itsAid: Long,
val securityProfile: Int,
val permissions: ByteArray,
val flSdu: ByteArray,
/** Raw GN lifetime octet; 0xFF = station default. */
val maximumPacketLifetime: Int = 0xFF,
) {
fun encode(): ByteArray = LinkWriter()
.u8(1) // BTP-B
.u16(destinationPort)
.u16(0) // destination port info
.u8(1) // SHB
.u8(1) // communication profile ITS-G5
.u8(securityProfile)
.u8(0xFF) // traffic class: station default
.u8(maximumPacketLifetime)
.u8(0) // hop limit: station default
.u16(0).u16(0) // no repetition
.u32(itsAid)
.u8(permissions.size).bytes(permissions)
.u8(0) // no SN-ENCAP context
.u16(flSdu.size).bytes(flSdu)
.toByteArray()
override fun equals(other: Any?): Boolean = other is BtpDataRequest && encode().contentEquals(other.encode())
override fun hashCode(): Int = encode().contentHashCode()
}
/** One CREDENTIALS_PROVISION segment of a `VCR1` bundle. */
fun credentialsSegment(totalLength: Int, offset: Int, segment: ByteArray): ByteArray {
require(segment.size <= 255)
return LinkWriter().u16(totalLength).u16(offset).u8(segment.size).bytes(segment).toByteArray()
}
data class LinkResult(val code: Int, val detail: ByteArray) {
val accepted: Boolean get() = code == LinkResultCode.ACCEPTED
override fun toString(): String = "LinkResult(${LinkResultCode.name(code)}, ${detail.size} B detail)"
override fun equals(other: Any?): Boolean = other is LinkResult && code == other.code && detail.contentEquals(other.detail)
override fun hashCode(): Int = 31 * code + detail.contentHashCode()
companion object {
fun decode(body: ByteArray): LinkResult? {
if (body.size < 2) return null
val length = body[1].toInt() and 0xFF
if (body.size != 2 + length) return null
return LinkResult(body[0].toInt() and 0xFF, body.copyOfRange(2, body.size))
}
}
}
/** STATUS body (103 octets), sent by the micrOBU every second. Counters are since the last configure. */
data class StationStatus(
val uptimeMs: Long,
val configured: Boolean,
val identifier: ByteArray,
val tickets: Int,
val signedMessages: Long,
val refusedNoTicket: Long,
val refusedChangePending: Long,
val refusedPermission: Long,
val signFailed: Long,
val verified: Long,
val rejected: Long,
val requestsAccepted: Long,
val requestsRefused: Long,
val radioSubmitted: Long,
val radioFailed: Long,
val radioReceived: Long,
val radioDropped: Long,
val linkCrcErrors: Long,
val linkMalformed: Long,
val potiUpdates: Long,
val itsTimeMs: Long,
) {
/** Signing refusals of every kind: no usable ticket, a pending id change, or a missing permission. */
val signRefused: Long get() = refusedNoTicket + refusedChangePending + refusedPermission + signFailed
override fun equals(other: Any?): Boolean = other is StationStatus && toString() == other.toString() &&
identifier.contentEquals(other.identifier)
override fun hashCode(): Int = toString().hashCode()
companion object {
const val SIZE = 103
fun decode(body: ByteArray): StationStatus? {
if (body.size != SIZE) return null
fun u8(i: Int) = body[i].toInt() and 0xFF
fun u32(i: Int) = (0 until 4).fold(0L) { acc, k -> acc or ((body[i + k].toLong() and 0xFF) shl (8 * k)) }
fun u64(i: Int) = (0 until 8).fold(0L) { acc, k -> acc or ((body[i + k].toLong() and 0xFF) shl (8 * k)) }
// [0] uptime u32, [4] configured, [5] gn_address 8, [13] identifier 8, [21] change_pending,
// [22] tickets, [23] 18 x u32 counters, [95] its_time u64
val c = 23
return StationStatus(
uptimeMs = u32(0),
configured = u8(4) != 0,
identifier = body.copyOfRange(13, 21),
tickets = u8(22),
signedMessages = u32(c), refusedNoTicket = u32(c + 4), refusedChangePending = u32(c + 8),
refusedPermission = u32(c + 12), signFailed = u32(c + 16), verified = u32(c + 20),
rejected = u32(c + 24), requestsAccepted = u32(c + 28), requestsRefused = u32(c + 32),
// c + 36: indications (the stack's own verified deliveries; the app uses V2X_RX)
radioSubmitted = u32(c + 40), radioFailed = u32(c + 44), radioReceived = u32(c + 48),
radioDropped = u32(c + 52),
// c + 56: link_rx_frames
linkCrcErrors = u32(c + 60), linkMalformed = u32(c + 64), potiUpdates = u32(c + 68),
itsTimeMs = u64(95),
)
}
}
}
@@ -35,6 +35,9 @@ import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
/** Connection lifecycle for the ESP32-C5 USB-serial link. */
enum class UsbSerialState { DISCONNECTED, DEVICE_ATTACHED, PERMISSION_REQUESTED, CONNECTED, ERROR }
private const val ACTION_USB_PERMISSION = "com.hawhamburg.micr0bu.USB_SERIAL_PERMISSION" private const val ACTION_USB_PERMISSION = "com.hawhamburg.micr0bu.USB_SERIAL_PERMISSION"
private const val TAG = "UsbSerialTransport" private const val TAG = "UsbSerialTransport"
@@ -60,11 +63,6 @@ private const val TAG = "UsbSerialTransport"
* Baud rate is not applicable here — USB Serial/JTAG has no baud concept; `setParameters` below * Baud rate is not applicable here — USB Serial/JTAG has no baud concept; `setParameters` below
* is a no-op the library requires anyway for API-shape reasons but the value is otherwise unused. * is a no-op the library requires anyway for API-shape reasons but the value is otherwise unused.
* *
* Since 2026-09-23 the rest of the app does not use this class directly but [Esp32Link], which
* picks this or [BleLinkTransport] and speaks either the previous firmware's frames (0x01-0x05,
* [sendCamTx]) or the current firmware's station-link messages (frame type [SERIAL_FRAME_LINK],
* [sendFrame]) over it.
*
* ## Ownership * ## Ownership
* This is a `@Singleton` shared by the UI ([com.hawhamburg.micr0bu.viewmodel.MqttViewModel]), the * This is a `@Singleton` shared by the UI ([com.hawhamburg.micr0bu.viewmodel.MqttViewModel]), the
* foreground [com.hawhamburg.micr0bu.service.TripRecordingService]'s * foreground [com.hawhamburg.micr0bu.service.TripRecordingService]'s
@@ -113,8 +111,8 @@ class UsbSerialTransport @Inject constructor(
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val _state = MutableStateFlow(Esp32LinkState.DISCONNECTED) private val _state = MutableStateFlow(UsbSerialState.DISCONNECTED)
val state: StateFlow<Esp32LinkState> = _state.asStateFlow() val state: StateFlow<UsbSerialState> = _state.asStateFlow()
private val _incomingFrames = MutableSharedFlow<DecodedFrame>(extraBufferCapacity = 256) private val _incomingFrames = MutableSharedFlow<DecodedFrame>(extraBufferCapacity = 256)
/** Every valid frame the ESP32 sends (CAM_RX and STATUS) — callers filter by [DecodedFrame.type]. */ /** Every valid frame the ESP32 sends (CAM_RX and STATUS) — callers filter by [DecodedFrame.type]. */
@@ -152,7 +150,7 @@ class UsbSerialTransport @Inject constructor(
} else { } else {
Log.w(TAG, "usbReceiver: permission denied or device null " + Log.w(TAG, "usbReceiver: permission denied or device null " +
"(granted=$granted, device=$device)") "(granted=$granted, device=$device)")
_state.value = Esp32LinkState.ERROR _state.value = UsbSerialState.ERROR
} }
} }
UsbManager.ACTION_USB_DEVICE_DETACHED -> { UsbManager.ACTION_USB_DEVICE_DETACHED -> {
@@ -176,7 +174,7 @@ class UsbSerialTransport @Inject constructor(
* repeatedly (e.g. from a "retry" UI action) — no-ops if already connected. * repeatedly (e.g. from a "retry" UI action) — no-ops if already connected.
*/ */
fun connect() { fun connect() {
if (_state.value == Esp32LinkState.CONNECTED) { if (_state.value == UsbSerialState.CONNECTED) {
Log.i(TAG, "connect(): already connected, no-op") Log.i(TAG, "connect(): already connected, no-op")
return return
} }
@@ -199,7 +197,7 @@ class UsbSerialTransport @Inject constructor(
"(see device list logged above) - either nothing is attached at the Android " + "(see device list logged above) - either nothing is attached at the Android " +
"USB level, or it's attached but its VID/PID doesn't match any entry in " + "USB level, or it's attached but its VID/PID doesn't match any entry in " +
"customProber's table") "customProber's table")
_state.value = Esp32LinkState.DISCONNECTED _state.value = UsbSerialState.DISCONNECTED
return return
} }
if (espDriver == null) { if (espDriver == null) {
@@ -213,14 +211,14 @@ class UsbSerialTransport @Inject constructor(
Log.i(TAG, "connect(): matched device vid=0x${device.vendorId.toString(16)} " + Log.i(TAG, "connect(): matched device vid=0x${device.vendorId.toString(16)} " +
"pid=0x${device.productId.toString(16)} name=${device.deviceName} " + "pid=0x${device.productId.toString(16)} name=${device.deviceName} " +
"ports=${driver.ports.size}") "ports=${driver.ports.size}")
_state.value = Esp32LinkState.DEVICE_ATTACHED _state.value = UsbSerialState.DEVICE_ATTACHED
if (usbManager.hasPermission(device)) { if (usbManager.hasPermission(device)) {
Log.i(TAG, "connect(): permission already granted, opening directly") Log.i(TAG, "connect(): permission already granted, opening directly")
openDevice(device) openDevice(device)
} else { } else {
Log.i(TAG, "connect(): requesting USB permission from user") Log.i(TAG, "connect(): requesting USB permission from user")
_state.value = Esp32LinkState.PERMISSION_REQUESTED _state.value = UsbSerialState.PERMISSION_REQUESTED
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
val permissionIntent = PendingIntent.getBroadcast( val permissionIntent = PendingIntent.getBroadcast(
context, 0, Intent(ACTION_USB_PERMISSION).setPackage(context.packageName), flags, context, 0, Intent(ACTION_USB_PERMISSION).setPackage(context.packageName), flags,
@@ -271,14 +269,14 @@ class UsbSerialTransport @Inject constructor(
if (driver == null || driver.ports.isEmpty()) { if (driver == null || driver.ports.isEmpty()) {
Log.w(TAG, "openDevice(): probeDevice returned null or no ports for " + Log.w(TAG, "openDevice(): probeDevice returned null or no ports for " +
"vid=0x${device.vendorId.toString(16)} pid=0x${device.productId.toString(16)}") "vid=0x${device.vendorId.toString(16)} pid=0x${device.productId.toString(16)}")
_state.value = Esp32LinkState.ERROR _state.value = UsbSerialState.ERROR
return return
} }
val connection = usbManager.openDevice(device) val connection = usbManager.openDevice(device)
if (connection == null) { if (connection == null) {
Log.w(TAG, "openDevice(): usbManager.openDevice() returned null - permission not " + Log.w(TAG, "openDevice(): usbManager.openDevice() returned null - permission not " +
"actually granted, or Android couldn't claim the device") "actually granted, or Android couldn't claim the device")
_state.value = Esp32LinkState.ERROR _state.value = UsbSerialState.ERROR
return return
} }
@@ -289,7 +287,7 @@ class UsbSerialTransport @Inject constructor(
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "openDevice(): port.open()/setParameters() threw", e) Log.e(TAG, "openDevice(): port.open()/setParameters() threw", e)
runCatching { newPort.close() } runCatching { newPort.close() }
_state.value = Esp32LinkState.ERROR _state.value = UsbSerialState.ERROR
return return
} }
@@ -332,15 +330,13 @@ class UsbSerialTransport @Inject constructor(
prev.txFailures != status.txFailures || prev.txFailures != status.txFailures ||
prev.rxCrcErrors != status.rxCrcErrors || prev.rxCrcErrors != status.rxCrcErrors ||
prev.status != status.status || prev.status != status.status ||
prev.capabilities != status.capabilities || prev.capabilities != status.capabilities
prev.rxQueueDrops != status.rxQueueDrops
) { ) {
Log.i(TAG, "ESP32 counters: status=${status.status} " + Log.i(TAG, "ESP32 counters: status=${status.status} " +
"oversizeDrops=${status.oversizeDrops} " + "oversizeDrops=${status.oversizeDrops} " +
"txFailures=${status.txFailures} " + "txFailures=${status.txFailures} " +
"rxCrcErrors=${status.rxCrcErrors} " + "rxCrcErrors=${status.rxCrcErrors} " +
"capabilities=${status.capabilities} " + "capabilities=${status.capabilities}")
"rxQueueDrops=${status.rxQueueDrops}")
} }
_linkStatus.value = status _linkStatus.value = status
} }
@@ -351,7 +347,7 @@ class UsbSerialTransport @Inject constructor(
override fun onRunError(e: Exception) { override fun onRunError(e: Exception) {
Log.e(TAG, "SerialInputOutputManager.onRunError()", e) Log.e(TAG, "SerialInputOutputManager.onRunError()", e)
_state.value = Esp32LinkState.ERROR _state.value = UsbSerialState.ERROR
} }
}) })
ioManager = manager ioManager = manager
@@ -364,13 +360,13 @@ class UsbSerialTransport @Inject constructor(
_consecutiveWriteFailures.value = 0 _consecutiveWriteFailures.value = 0
_linkStatus.value = null _linkStatus.value = null
lastFrameAtMs = SystemClock.elapsedRealtime() lastFrameAtMs = SystemClock.elapsedRealtime()
_state.value = Esp32LinkState.CONNECTED _state.value = UsbSerialState.CONNECTED
startWatchdog() startWatchdog()
} }
} }
/** /**
* Flips the link to [Esp32LinkState.ERROR] once the firmware's 1 Hz STATUS heartbeat has been * Flips the link to [UsbSerialState.ERROR] once the firmware's 1 Hz STATUS heartbeat has been
* missing for [LINK_TIMEOUT_MS]. Without this, "connected" only ever means "the port opened * missing for [LINK_TIMEOUT_MS]. Without this, "connected" only ever means "the port opened
* at some point in the past" — which on a bench is exactly the wrong thing to believe. * at some point in the past" — which on a bench is exactly the wrong thing to believe.
*/ */
@@ -379,14 +375,14 @@ class UsbSerialTransport @Inject constructor(
watchdogJob = scope.launch { watchdogJob = scope.launch {
while (isActive) { while (isActive) {
delay(WATCHDOG_POLL_MS) delay(WATCHDOG_POLL_MS)
if (_state.value != Esp32LinkState.CONNECTED) continue if (_state.value != UsbSerialState.CONNECTED) continue
val silentFor = SystemClock.elapsedRealtime() - lastFrameAtMs val silentFor = SystemClock.elapsedRealtime() - lastFrameAtMs
if (silentFor > LINK_TIMEOUT_MS) { if (silentFor > LINK_TIMEOUT_MS) {
Log.w(TAG, "watchdog: no frame from ESP32 for ${silentFor}ms (heartbeat " + Log.w(TAG, "watchdog: no frame from ESP32 for ${silentFor}ms (heartbeat " +
"expected at 1 Hz) - marking link ERROR. Either the firmware is wedged/" + "expected at 1 Hz) - marking link ERROR. Either the firmware is wedged/" +
"not running, or the host->device direction opened but device->host " + "not running, or the host->device direction opened but device->host " +
"never did (see the DTR note in openDevice()).") "never did (see the DTR note in openDevice()).")
_state.value = Esp32LinkState.ERROR _state.value = UsbSerialState.ERROR
} }
} }
} }
@@ -447,29 +443,7 @@ class UsbSerialTransport @Inject constructor(
} catch (e: Exception) { } catch (e: Exception) {
val failures = _consecutiveWriteFailures.updateAndGet { it + 1 } val failures = _consecutiveWriteFailures.updateAndGet { it + 1 }
Log.w(TAG, "sendCamTx(): write failed (consecutive failures: $failures)", e) Log.w(TAG, "sendCamTx(): write failed (consecutive failures: $failures)", e)
_state.value = Esp32LinkState.ERROR _state.value = UsbSerialState.ERROR
false
}
}
/**
* Writes one frame of any [type] (the station-link messages go as [SERIAL_FRAME_LINK]). Same
* failure accounting as [sendCamTx]. Blocking, 200 ms timeout: call from a background dispatcher.
*/
fun sendFrame(type: Int, payload: ByteArray): Boolean {
val p = port
if (p == null) {
_consecutiveWriteFailures.update { it + 1 }
return false
}
return try {
p.write(SerialFrameEncoder.encode(type, payload), /* timeout ms */ 200)
_consecutiveWriteFailures.value = 0
true
} catch (e: Exception) {
val failures = _consecutiveWriteFailures.updateAndGet { it + 1 }
Log.w(TAG, "sendFrame(0x${type.toString(16)}): write failed (consecutive failures: $failures)", e)
_state.value = Esp32LinkState.ERROR
false false
} }
} }
@@ -485,7 +459,7 @@ class UsbSerialTransport @Inject constructor(
openDeviceName = null openDeviceName = null
_linkStatus.value = null _linkStatus.value = null
_consecutiveWriteFailures.value = 0 _consecutiveWriteFailures.value = 0
_state.value = Esp32LinkState.DISCONNECTED _state.value = UsbSerialState.DISCONNECTED
} }
} }
@@ -8,7 +8,7 @@ import javax.inject.Singleton
* UPER (Unaligned Packed Encoding Rules) codec for CAM, used on the ESP32-C5 hardware path * UPER (Unaligned Packed Encoding Rules) codec for CAM, used on the ESP32-C5 hardware path
* (Phase 03, Section 13): the phone builds outgoing CAM itself * (Phase 03, Section 13): the phone builds outgoing CAM itself
* ([com.hawhamburg.micr0bu.domain.cam.PhoneCamBuilder]) and UPER-encodes it before handing bytes * ([com.hawhamburg.micr0bu.domain.cam.PhoneCamBuilder]) and UPER-encodes it before handing bytes
* to [com.hawhamburg.micr0bu.data.transport.Esp32Link], and UPER-decodes whatever the * to [com.hawhamburg.micr0bu.data.transport.UsbSerialTransport], and UPER-decodes whatever the
* ESP32-C5 forwards back on receive (already stripped of 802.11/GeoNetworking/BTP framing by * ESP32-C5 forwards back on receive (already stripped of 802.11/GeoNetworking/BTP framing by
* the firmware's `gn_unwrap.c` — this only ever sees CAM UPER bytes, never raw radio frames). * the firmware's `gn_unwrap.c` — this only ever sees CAM UPER bytes, never raw radio frames).
* *
@@ -1,145 +0,0 @@
package com.hawhamburg.micr0bu.domain.asn1
import com.hawhamburg.micr0bu.domain.cam.StationType
import kotlin.math.roundToInt
import kotlin.math.roundToLong
/**
* One VAM's content, in the units the phone has. Everything optional here is encoded as the ASN.1
* "unavailable" value when null, never as a made-up number.
*/
data class VamContent(
val stationId: Long,
/** Wall-clock epoch ms of the fix, GNSS-corrected; the generationDeltaTime source. */
val timestamp: Long,
val latitude: Double,
val longitude: Double,
/** Android horizontal accuracy, metres (68 %). Null or 0: unknown. */
val accuracyM: Float?,
val speedMps: Double,
val headingDeg: Double,
/** Along-track acceleration, m/s². */
val accelerationMps2: Double? = null,
/** Include the low-frequency container (profile and size class). */
val includeLowFrequency: Boolean,
)
/**
* UPER encoder for the VAM of ETSI TS 103 300-3 V2.3.1 (VAM-PDU-Descriptions major version 3,
* over the CDD of TS 102 894-2 V2.4.1), for a bicyclist in VRU profile 2.
*
* Covers the same field set as the colleague's reference VBS (microbu-esp32c5,
* station-link/python/microbu_link/vbs.py, `VbsLite.assemble`), which encodes with asn1tools from
* the ETSI modules: header, basic container, the three mandatory fields of the high-frequency
* container, and optionally the low-frequency container with profile and size class. No cluster
* or motion-prediction containers. The unit test cross-checks the bytes against asn1tools.
*
* Transmit only for now: the app neither decodes received VAMs nor shows them.
*/
object VamUperCodec {
const val PROTOCOL_VERSION = 3
const val MESSAGE_ID_VAM = 16
/** VruSubProfileBicyclist.bicyclist */
private const val SUBPROFILE_BICYCLIST = 1
/** VruSizeClass.low */
private const val SIZE_CLASS_LOW = 1
/** VruProfileAndSubprofile CHOICE index of bicyclistAndLightVruVehicle (root: 4 alternatives). */
private const val PROFILE_BICYCLIST_INDEX = 1
private const val SEMI_AXIS_OUT_OF_RANGE = 4094
private const val SEMI_AXIS_UNAVAILABLE = 4095
private const val WGS84_ANGLE_UNAVAILABLE = 3601
private const val ANGLE_CONFIDENCE_UNAVAILABLE = 127
private const val SPEED_OUT_OF_RANGE = 16382
private const val SPEED_CONFIDENCE_UNAVAILABLE = 127
private const val ACCEL_UNAVAILABLE = 161
private const val ACCEL_CONFIDENCE_UNAVAILABLE = 102
private const val ALTITUDE_UNAVAILABLE = 800001
private const val ALTITUDE_CONFIDENCE_UNAVAILABLE = 15
/**
* Android's accuracy is a 68 % radius; the confidence ellipse is 95 %. For a circular 2-D error
* the ratio is about 1.62, the same factor [com.hawhamburg.micr0bu.data.transport.GnPositionVector]
* uses for its PAI bound.
*/
private const val ACCURACY_68_TO_95 = 1.62
private const val ENCODE_BUFFER_BYTES = 64
fun encode(vam: VamContent): ByteArray {
val w = BitWriter(ENCODE_BUFFER_BYTES)
// VAM ::= SEQUENCE { header, vam } -- not extensible
// ItsPduHeader
w.putBits(PROTOCOL_VERSION, 8)
w.putBits(MESSAGE_ID_VAM, 8)
w.putBits(vam.stationId and 0xFFFFFFFFL, 32)
// VruAwareness ::= SEQUENCE { generationDeltaTime, vamParameters }
w.putBits(CamUperCodec.generationDeltaTime(vam.timestamp), 16)
// VamParameters ::= SEQUENCE { basic, hf, lf OPT, clusterInfo OPT, clusterOp OPT, motion OPT, ... }
w.putBits(0, 1) // extension bit
w.putBits(if (vam.includeLowFrequency) 0b1000 else 0b0000, 4)
// BasicContainer ::= SEQUENCE { stationType, referencePosition, ... }
w.putBits(0, 1)
w.putBits(StationType.CYCLIST, 8)
// ReferencePositionWithConfidence ::= SEQUENCE { latitude, longitude, ellipse, altitude }
w.putBits(latitude(vam.latitude) + 900_000_000L, 31)
w.putBits(longitude(vam.longitude) + 1_800_000_000L, 32)
val semiAxis = semiAxisCm(vam.accuracyM)
w.putBits(semiAxis, 12) // semiMajorAxisLength
w.putBits(semiAxis, 12) // semiMinorAxisLength
// Circular error: the orientation of the major axis says nothing, so it is unavailable.
w.putBits(WGS84_ANGLE_UNAVAILABLE, 12)
// Altitude: GnssReading carries no "has altitude" flag, so an honest unavailable.
w.putBits(ALTITUDE_UNAVAILABLE + 100_000, 20)
w.putBits(ALTITUDE_CONFIDENCE_UNAVAILABLE, 4)
// VruHighFrequencyContainer ::= SEQUENCE { heading, speed, longitudinalAcceleration, 11 OPTIONAL, ... }
w.putBits(0, 1)
w.putBits(0, 11)
w.putBits(headingDeciDeg(vam.headingDeg), 12)
w.putBits(ANGLE_CONFIDENCE_UNAVAILABLE - 1, 7) // Wgs84AngleConfidence (1..127)
w.putBits(speedCms(vam.speedMps), 14)
w.putBits(SPEED_CONFIDENCE_UNAVAILABLE - 1, 7) // SpeedConfidence (1..127)
w.putBits(accelDeciMps2(vam.accelerationMps2) + 160, 9)
w.putBits(ACCEL_CONFIDENCE_UNAVAILABLE, 7)
if (vam.includeLowFrequency) {
// VruLowFrequencyContainer ::= SEQUENCE { profileAndSubprofile, sizeClass OPT, exteriorLights OPT, ... }
w.putBits(0, 1)
w.putBits(0b10, 2)
// VruProfileAndSubprofile ::= CHOICE { pedestrian, bicyclistAndLightVruVehicle, motorcyclist, animal, ... }
w.putBits(0, 1)
w.putBits(PROFILE_BICYCLIST_INDEX, 2)
w.putBits(SUBPROFILE_BICYCLIST, 4)
w.putBits(SIZE_CLASS_LOW, 4)
}
return w.toByteArray()
}
private fun latitude(deg: Double): Long =
if (deg.isFinite()) (deg * 1e7).roundToLong().coerceIn(-900_000_000L, 900_000_000L) else 900_000_001L
private fun longitude(deg: Double): Long =
if (deg.isFinite()) (deg * 1e7).roundToLong().coerceIn(-1_799_999_999L, 1_800_000_000L) else 1_800_000_001L
private fun semiAxisCm(accuracyM: Float?): Int {
if (accuracyM == null || !accuracyM.isFinite() || accuracyM <= 0f) return SEMI_AXIS_UNAVAILABLE
val cm = (accuracyM * ACCURACY_68_TO_95 * 100).roundToInt()
return if (cm >= SEMI_AXIS_OUT_OF_RANGE) SEMI_AXIS_OUT_OF_RANGE else cm.coerceAtLeast(1)
}
private fun headingDeciDeg(deg: Double): Int =
if (deg.isFinite()) Math.floorMod((deg * 10).roundToInt(), 3600) else WGS84_ANGLE_UNAVAILABLE
private fun speedCms(mps: Double): Int =
if (mps.isFinite()) (mps * 100).roundToInt().coerceIn(0, SPEED_OUT_OF_RANGE) else 16383
private fun accelDeciMps2(mps2: Double?): Int =
if (mps2 == null || !mps2.isFinite()) ACCEL_UNAVAILABLE else (mps2 * 10).roundToInt().coerceIn(-160, 160)
}
@@ -10,7 +10,7 @@ import kotlin.math.abs
* This class only does the sensor-fusion-into-CAM-fields part, independent of the wire protocol * This class only does the sensor-fusion-into-CAM-fields part, independent of the wire protocol
* to the ESP32-C5. Live flow, driven by [com.hawhamburg.micr0bu.service.CamTransmitLoop]: * to the ESP32-C5. Live flow, driven by [com.hawhamburg.micr0bu.service.CamTransmitLoop]:
* *
* `PhoneCamBuilder.build(...)` → `RealAsn1UperCodec.encodeCam(...)` → `Esp32Link` (write). * `PhoneCamBuilder.build(...)` → `RealAsn1UperCodec.encodeCam(...)` → `UsbSerialTransport` (write).
* *
* Position/speed/heading come straight from GNSS. Yaw rate is derived from the gyroscope's * Position/speed/heading come straight from GNSS. Yaw rate is derived from the gyroscope's
* z-axis reading (rotation about the vertical axis while the phone is roughly flat/mounted * z-axis reading (rotation about the vertical axis while the phone is roughly flat/mounted
@@ -1,60 +0,0 @@
package com.hawhamburg.micr0bu.domain.vam
import com.hawhamburg.micr0bu.domain.usecase.GeoMath
import kotlin.math.abs
/**
* When to send an individual VAM: ETSI TS 103 300-3 V2.3.1 clause 6.4, items 1 to 4, with the
* recommended values of Tables 16 and 17 — the same rules the colleague's reference VBS applies
* (microbu-esp32c5/station-link/python/microbu_link/vbs.py, `VbsLite.due`). No clustering, no
* redundancy mitigation, T_GenVam fixed at its minimum (no DCC input).
*
* Pure and clock-free (callers pass times in ms), so it can be tested without Android.
*/
class VamGenerationRules {
data class Kinematics(val latitude: Double, val longitude: Double, val speedMps: Double, val headingDeg: Double)
private var last: Kinematics? = null
private var lastAtMs = 0L
private var lastLowFrequencyAtMs: Long? = null
/** True if a VAM is due at [nowMs] for the VRU now at [now]. */
fun due(nowMs: Long, now: Kinematics): Boolean {
val previous = last ?: return true
val elapsed = nowMs - lastAtMs
if (elapsed < T_GEN_VAM_MIN_MS) return false
if (elapsed > T_GEN_VAM_MAX_MS) return true // item 1
if (GeoMath.haversineMeters(now.latitude, now.longitude, previous.latitude, previous.longitude) >
MIN_POSITION_CHANGE_M) return true // item 2
if (abs(now.speedMps - previous.speedMps) > MIN_SPEED_CHANGE_MPS) return true // item 3
val headingDelta = abs(((now.headingDeg - previous.headingDeg + 180.0) % 360.0 + 360.0) % 360.0 - 180.0)
return headingDelta > MIN_ORIENTATION_CHANGE_DEG // item 4
}
/** True if the VAM sent at [nowMs] carries the low-frequency container (first VAM, then every T_GenVamLFMin). */
fun includeLowFrequency(nowMs: Long): Boolean =
lastLowFrequencyAtMs.let { it == null || nowMs - it >= T_GEN_VAM_LF_MIN_MS }
/** Records that a VAM went out at [nowMs] for [sent], with or without the low-frequency container. */
fun onSent(nowMs: Long, sent: Kinematics, withLowFrequency: Boolean) {
last = sent
lastAtMs = nowMs
if (withLowFrequency) lastLowFrequencyAtMs = nowMs
}
fun reset() {
last = null
lastAtMs = 0L
lastLowFrequencyAtMs = null
}
companion object {
const val T_GEN_VAM_MIN_MS = 100L
const val T_GEN_VAM_MAX_MS = 5_000L
const val T_GEN_VAM_LF_MIN_MS = 2_000L
const val MIN_POSITION_CHANGE_M = 4.0
const val MIN_SPEED_CHANGE_MPS = 0.5
const val MIN_ORIENTATION_CHANGE_DEG = 4.0
}
}
@@ -6,10 +6,7 @@ import com.hawhamburg.micr0bu.data.GnssReading
import com.hawhamburg.micr0bu.data.GnssTimeSource import com.hawhamburg.micr0bu.data.GnssTimeSource
import com.hawhamburg.micr0bu.data.SensorRepository import com.hawhamburg.micr0bu.data.SensorRepository
import com.hawhamburg.micr0bu.data.transport.GnPositionVector import com.hawhamburg.micr0bu.data.transport.GnPositionVector
import com.hawhamburg.micr0bu.data.mqtt.ObuHardwarePreferences import com.hawhamburg.micr0bu.data.transport.UsbSerialTransport
import com.hawhamburg.micr0bu.data.transport.Esp32Link
import com.hawhamburg.micr0bu.data.transport.OutgoingIts
import com.hawhamburg.micr0bu.data.transport.OutgoingMessage
import com.hawhamburg.micr0bu.domain.asn1.RealAsn1UperCodec import com.hawhamburg.micr0bu.domain.asn1.RealAsn1UperCodec
import com.hawhamburg.micr0bu.domain.cam.OwnStationIds import com.hawhamburg.micr0bu.domain.cam.OwnStationIds
import com.hawhamburg.micr0bu.domain.cam.PhoneCamBuilder import com.hawhamburg.micr0bu.domain.cam.PhoneCamBuilder
@@ -24,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -53,8 +49,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class CamPinger @Inject constructor( class CamPinger @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val esp32Link: Esp32Link, private val usbSerialTransport: UsbSerialTransport,
private val prefs: ObuHardwarePreferences,
private val codec: RealAsn1UperCodec, private val codec: RealAsn1UperCodec,
) { ) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@@ -119,8 +114,7 @@ class CamPinger @Inject constructor(
// Fixed bench identity on every layer, the link-layer address included, so a ping // Fixed bench identity on every layer, the link-layer address included, so a ping
// stays recognisable in a capture and never rotates. // stays recognisable in a capture and never rotates.
val pv = GnPositionVector.fromCam(cam, gnss.accuracyM, OwnStationIds.BENCH_PING_MAC) val pv = GnPositionVector.fromCam(cam, gnss.accuracyM, OwnStationIds.BENCH_PING_MAC)
val signed = prefs.signOutgoingFlow.first() if (usbSerialTransport.sendCamTx(bytes, pv)) {
if (esp32Link.send(OutgoingIts(OutgoingMessage.CAM, bytes, pv, gnss.accuracyM, signed))) {
_sentCount.update { it + 1 } _sentCount.update { it + 1 }
} }
} }
@@ -6,18 +6,13 @@ import com.hawhamburg.micr0bu.data.GnssTimeSource
import com.hawhamburg.micr0bu.data.SensorRepository import com.hawhamburg.micr0bu.data.SensorRepository
import com.hawhamburg.micr0bu.data.cam.PseudonymManager import com.hawhamburg.micr0bu.data.cam.PseudonymManager
import com.hawhamburg.micr0bu.data.mqtt.ObuHardwarePreferences import com.hawhamburg.micr0bu.data.mqtt.ObuHardwarePreferences
import com.hawhamburg.micr0bu.data.transport.Esp32Link
import com.hawhamburg.micr0bu.data.transport.GnPositionVector import com.hawhamburg.micr0bu.data.transport.GnPositionVector
import com.hawhamburg.micr0bu.data.transport.ObuHardware import com.hawhamburg.micr0bu.data.transport.ObuHardware
import com.hawhamburg.micr0bu.data.transport.OutgoingIts import com.hawhamburg.micr0bu.data.transport.UsbSerialTransport
import com.hawhamburg.micr0bu.data.transport.OutgoingMessage
import com.hawhamburg.micr0bu.domain.asn1.RealAsn1UperCodec import com.hawhamburg.micr0bu.domain.asn1.RealAsn1UperCodec
import com.hawhamburg.micr0bu.domain.asn1.VamContent
import com.hawhamburg.micr0bu.domain.asn1.VamUperCodec
import com.hawhamburg.micr0bu.domain.cam.CamTransmitConfig import com.hawhamburg.micr0bu.domain.cam.CamTransmitConfig
import com.hawhamburg.micr0bu.domain.cam.PhoneCamBuilder import com.hawhamburg.micr0bu.domain.cam.PhoneCamBuilder
import com.hawhamburg.micr0bu.domain.usecase.GeoMath import com.hawhamburg.micr0bu.domain.usecase.GeoMath
import com.hawhamburg.micr0bu.domain.vam.VamGenerationRules
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -50,19 +45,12 @@ import javax.inject.Singleton
* [com.hawhamburg.micr0bu.domain.detection.EventDetector] stream that already drives trip event * [com.hawhamburg.micr0bu.domain.detection.EventDetector] stream that already drives trip event
* logging). Both rate figures are placeholders pending real-world tuning, per * logging). Both rate figures are placeholders pending real-world tuning, per
* [CamTransmitConfig]'s own disclaimer. * [CamTransmitConfig]'s own disclaimer.
*
* ## CAM or VAM
* Settings chooses what goes out ([OutgoingMessage]). CAM follows the rate policy above. VAM is
* checked every [VAM_TICK_MS] against the generation rules of TS 103 300-3 clause 6.4
* ([VamGenerationRules]) and sent when one fires, from the same GNSS fix, pseudonym and position
* vector a CAM would use. Whether either is signed is the "Sign outgoing messages" setting; the
* micrOBU does the signing ([Esp32Link]).
*/ */
@Singleton @Singleton
class CamTransmitLoop @Inject constructor( class CamTransmitLoop @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val obuHardwarePrefs: ObuHardwarePreferences, private val obuHardwarePrefs: ObuHardwarePreferences,
private val esp32Link: Esp32Link, private val usbSerialTransport: UsbSerialTransport,
private val codec: RealAsn1UperCodec, private val codec: RealAsn1UperCodec,
private val pseudonymManager: PseudonymManager, private val pseudonymManager: PseudonymManager,
) { ) {
@@ -79,10 +67,6 @@ class CamTransmitLoop @Inject constructor(
/** Previous GNSS fix, kept only to derive along-track acceleration — see [longitudinalAccel]. */ /** Previous GNSS fix, kept only to derive along-track acceleration — see [longitudinalAccel]. */
@Volatile private var previousGnss: GnssReading? = null @Volatile private var previousGnss: GnssReading? = null
@Volatile private var outgoing: OutgoingMessage = OutgoingMessage.CAM
@Volatile private var signOutgoing: Boolean = true
private val vamRules = VamGenerationRules()
/** /**
* Call when a braking/turning/stopping event fires during an active trip — bumps the CAM * Call when a braking/turning/stopping event fires during an active trip — bumps the CAM
* rate to [CamTransmitConfig.elevatedRateHz] for [ELEVATED_HOLD_MS] so nearby stations get * rate to [CamTransmitConfig.elevatedRateHz] for [ELEVATED_HOLD_MS] so nearby stations get
@@ -101,7 +85,6 @@ class CamTransmitLoop @Inject constructor(
if (job?.isActive == true) return if (job?.isActive == true) return
elevatedUntilMs = 0L elevatedUntilMs = 0L
previousGnss = null previousGnss = null
vamRules.reset()
job = scope.launch { job = scope.launch {
obuHardwarePrefs.obuHardwareFlow.collectLatest { hardware -> obuHardwarePrefs.obuHardwareFlow.collectLatest { hardware ->
if (hardware != ObuHardware.ESP32_C5) return@collectLatest if (hardware != ObuHardware.ESP32_C5) return@collectLatest
@@ -119,27 +102,10 @@ class CamTransmitLoop @Inject constructor(
private suspend fun runTransmitLoop() = coroutineScope { private suspend fun runTransmitLoop() = coroutineScope {
launch { sensorRepository.gnssFlow().collect { latestGnss = it } } launch { sensorRepository.gnssFlow().collect { latestGnss = it } }
launch { sensorRepository.gyroscopeFlow().collect { latestGyroZ = it.z } } launch { sensorRepository.gyroscopeFlow().collect { latestGyroZ = it.z } }
launch { obuHardwarePrefs.signOutgoingFlow.collect { signOutgoing = it } }
launch {
obuHardwarePrefs.outgoingMessageFlow.collect {
if (it != outgoing) vamRules.reset()
outgoing = it
}
}
while (true) { while (true) {
val gnss = latestGnss val gnss = latestGnss
if (gnss != null) { if (gnss != null) {
when (outgoing) {
OutgoingMessage.CAM -> sendCam(gnss)
OutgoingMessage.VAM -> sendVamIfDue(gnss)
}
}
delay(if (outgoing == OutgoingMessage.VAM) VAM_TICK_MS else (1000.0 / currentRateHz(gnss)).toLong())
}
}
private suspend fun sendCam(gnss: GnssReading) {
// Asked for per CAM rather than once per trip: that is what lets a pseudonym // Asked for per CAM rather than once per trip: that is what lets a pseudonym
// rotation fall cleanly between two frames instead of inside one. // rotation fall cleanly between two frames instead of inside one.
val pseudonym = pseudonymManager.current() val pseudonym = pseudonymManager.current()
@@ -148,35 +114,10 @@ class CamTransmitLoop @Inject constructor(
val fix = gnss.copy(timestamp = GnssTimeSource.correct(gnss.timestamp)) val fix = gnss.copy(timestamp = GnssTimeSource.correct(gnss.timestamp))
val cam = PhoneCamBuilder.build(fix, latestGyroZ, pseudonym.stationId, longitudinalAccel(gnss)) val cam = PhoneCamBuilder.build(fix, latestGyroZ, pseudonym.stationId, longitudinalAccel(gnss))
val bytes = codec.encodeCam(cam) val bytes = codec.encodeCam(cam)
esp32Link.send(OutgoingIts(OutgoingMessage.CAM, bytes, usbSerialTransport.sendCamTx(bytes, GnPositionVector.fromCam(cam, gnss.accuracyM, pseudonym.mac))
GnPositionVector.fromCam(cam, gnss.accuracyM, pseudonym.mac), gnss.accuracyM, signOutgoing)) }
delay((1000.0 / currentRateHz(gnss)).toLong())
} }
private suspend fun sendVamIfDue(gnss: GnssReading) {
val now = System.currentTimeMillis()
val kinematics = VamGenerationRules.Kinematics(gnss.latitude, gnss.longitude,
gnss.speedMs.toDouble(), gnss.bearingDeg.toDouble())
if (!vamRules.due(now, kinematics)) return
val pseudonym = pseudonymManager.current()
val fix = gnss.copy(timestamp = GnssTimeSource.correct(gnss.timestamp))
// The CAM view of this fix is built only for its position vector, so the GN header of a VAM
// follows exactly the rules a CAM's does. It is not transmitted.
val cam = PhoneCamBuilder.build(fix, latestGyroZ, pseudonym.stationId, longitudinalAccel(gnss))
val withLowFrequency = vamRules.includeLowFrequency(now)
val bytes = VamUperCodec.encode(VamContent(
stationId = pseudonym.stationId,
timestamp = cam.timestamp,
latitude = cam.latitude,
longitude = cam.longitude,
accuracyM = gnss.accuracyM,
speedMps = cam.speedMps,
headingDeg = cam.headingDeg,
accelerationMps2 = cam.accelerationMps2,
includeLowFrequency = withLowFrequency,
))
val handedOver = esp32Link.send(OutgoingIts(OutgoingMessage.VAM, bytes,
GnPositionVector.fromCam(cam, gnss.accuracyM, pseudonym.mac), gnss.accuracyM, signOutgoing))
if (handedOver) vamRules.onSent(now, kinematics, withLowFrequency)
} }
/** /**
@@ -217,9 +158,6 @@ class CamTransmitLoop @Inject constructor(
companion object { companion object {
private const val ELEVATED_HOLD_MS = 5_000L private const val ELEVATED_HOLD_MS = 5_000L
/** How often VAM generation rules are checked: T_GenVamMin, TS 103 300-3 Table 16. */
private const val VAM_TICK_MS = VamGenerationRules.T_GEN_VAM_MIN_MS
/** Below this gap, GNSS speed noise divided by a tiny dt produces absurd accelerations. */ /** Below this gap, GNSS speed noise divided by a tiny dt produces absurd accelerations. */
private const val MIN_ACCEL_DT_SEC = 0.2 private const val MIN_ACCEL_DT_SEC = 0.2
@@ -20,8 +20,6 @@ import androidx.compose.material.icons.filled.GpsOff
import androidx.compose.material.icons.filled.Sensors import androidx.compose.material.icons.filled.Sensors
import androidx.compose.material.icons.filled.SensorsOff import androidx.compose.material.icons.filled.SensorsOff
import androidx.compose.material.icons.filled.Usb import androidx.compose.material.icons.filled.Usb
import androidx.compose.material.icons.filled.BluetoothDisabled
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.UsbOff import androidx.compose.material.icons.filled.UsbOff
import androidx.compose.material.icons.filled.Wifi import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.WifiOff import androidx.compose.material.icons.filled.WifiOff
@@ -41,7 +39,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState import com.hawhamburg.micr0bu.data.transport.UsbSerialState
import com.hawhamburg.micr0bu.viewmodel.SensorUiState import com.hawhamburg.micr0bu.viewmodel.SensorUiState
private val GreenActive = Color(0xFF4CAF50) private val GreenActive = Color(0xFF4CAF50)
@@ -55,9 +53,7 @@ fun StatusTopBar(
state: SensorUiState, state: SensorUiState,
mqttConnectionState: MqttConnectionState, mqttConnectionState: MqttConnectionState,
isEsp32: Boolean = false, isEsp32: Boolean = false,
esp32LinkState: Esp32LinkState = Esp32LinkState.DISCONNECTED, usbSerialState: UsbSerialState = UsbSerialState.DISCONNECTED,
/** The ESP32-C5 is reached over BLE rather than its USB port (Settings). */
esp32Bluetooth: Boolean = false,
) { ) {
TopAppBar( TopAppBar(
title = { title = {
@@ -97,7 +93,7 @@ fun StatusTopBar(
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
ObuStatusIcon(mqttConnectionState, isEsp32, esp32LinkState, esp32Bluetooth = esp32Bluetooth) ObuStatusIcon(mqttConnectionState, isEsp32, usbSerialState)
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
@@ -110,21 +106,20 @@ fun StatusTopBar(
* OBU link indicator. Which transport it reflects depends on the selected hardware: the CiT One * OBU link indicator. Which transport it reflects depends on the selected hardware: the CiT One
* reaches the phone over MQTT (Wi-Fi / USB-C tethering), the ESP32-C5 over a USB-serial link with * reaches the phone over MQTT (Wi-Fi / USB-C tethering), the ESP32-C5 over a USB-serial link with
* no broker at all - so on that path [mqttConnectionState] is permanently DISCONNECTED and would * no broker at all - so on that path [mqttConnectionState] is permanently DISCONNECTED and would
* report the OBU as offline while CAMs were streaming in. Uses a USB or Bluetooth glyph there * report the OBU as offline while CAMs were streaming in. Uses a USB glyph there rather than the
* rather than the Wi-Fi one, since that is literally what the connection is. * Wi-Fi one, since that is literally what the connection is.
*/ */
@Composable @Composable
private fun ObuStatusIcon( private fun ObuStatusIcon(
mqttState: MqttConnectionState, mqttState: MqttConnectionState,
isEsp32: Boolean, isEsp32: Boolean,
esp32LinkState: Esp32LinkState, usbSerialState: UsbSerialState,
esp32Bluetooth: Boolean,
) { ) {
val state = if (isEsp32) esp32LinkState.asConnectionState() else mqttState val state = if (isEsp32) usbSerialState.asConnectionState() else mqttState
val linkUp = state == MqttConnectionState.CONNECTED || state == MqttConnectionState.CONNECTING
val icon = when { val icon = when {
isEsp32 && esp32Bluetooth -> if (linkUp) Icons.Default.Bluetooth else Icons.Default.BluetoothDisabled isEsp32 && state == MqttConnectionState.CONNECTED -> Icons.Default.Usb
isEsp32 -> if (linkUp) Icons.Default.Usb else Icons.Default.UsbOff isEsp32 && state == MqttConnectionState.CONNECTING -> Icons.Default.Usb
isEsp32 -> Icons.Default.UsbOff
state == MqttConnectionState.CONNECTED || state == MqttConnectionState.CONNECTED ||
state == MqttConnectionState.CONNECTING -> Icons.Default.Wifi state == MqttConnectionState.CONNECTING -> Icons.Default.Wifi
else -> Icons.Default.WifiOff else -> Icons.Default.WifiOff
@@ -199,10 +194,10 @@ private fun RecordingPulse() {
* Maps the ESP32-C5 serial link's lifecycle onto the MQTT connection vocabulary this bar's colour * Maps the ESP32-C5 serial link's lifecycle onto the MQTT connection vocabulary this bar's colour
* and pulse logic already speaks, so one indicator serves both transports. * and pulse logic already speaks, so one indicator serves both transports.
*/ */
private fun Esp32LinkState.asConnectionState(): MqttConnectionState = when (this) { private fun UsbSerialState.asConnectionState(): MqttConnectionState = when (this) {
Esp32LinkState.CONNECTED -> MqttConnectionState.CONNECTED UsbSerialState.CONNECTED -> MqttConnectionState.CONNECTED
Esp32LinkState.DEVICE_ATTACHED, UsbSerialState.DEVICE_ATTACHED,
Esp32LinkState.PERMISSION_REQUESTED -> MqttConnectionState.CONNECTING UsbSerialState.PERMISSION_REQUESTED -> MqttConnectionState.CONNECTING
Esp32LinkState.ERROR -> MqttConnectionState.ERROR UsbSerialState.ERROR -> MqttConnectionState.ERROR
Esp32LinkState.DISCONNECTED -> MqttConnectionState.DISCONNECTED UsbSerialState.DISCONNECTED -> MqttConnectionState.DISCONNECTED
} }
@@ -34,8 +34,6 @@ sealed class Screen(val route: String, val labelRes: Int) {
data object Connection : Screen("connection", R.string.nav_connection) data object Connection : Screen("connection", R.string.nav_connection)
data object Map : Screen("map", R.string.map_title) data object Map : Screen("map", R.string.map_title)
data object MqttViewer : Screen("mqtt_viewer", R.string.nav_v2x) data object MqttViewer : Screen("mqtt_viewer", R.string.nav_v2x)
/** Full-screen V2X live map, opened from the V2X Monitor's map button. */
data object V2xMap : Screen("v2x_map", R.string.v2x_map_title)
// Phase A — Trip Recording // Phase A — Trip Recording
data object TripHistory : Screen("trip_history", R.string.nav_trips) data object TripHistory : Screen("trip_history", R.string.nav_trips)
@@ -85,7 +83,6 @@ private fun Screen.ownsRoute(route: String?): Boolean {
route == Screen.Map.route || route == Screen.Map.route ||
route == Screen.Sensors.route route == Screen.Sensors.route
Screen.Record -> route == Screen.Log.route Screen.Record -> route == Screen.Log.route
Screen.MqttViewer -> route == Screen.V2xMap.route
else -> false else -> false
} }
} }
@@ -44,9 +44,7 @@ import com.hawhamburg.micr0bu.R
import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState
import com.hawhamburg.micr0bu.data.transport.ObuHardware import com.hawhamburg.micr0bu.data.transport.ObuHardware
import com.hawhamburg.micr0bu.data.transport.TransportType import com.hawhamburg.micr0bu.data.transport.TransportType
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState import com.hawhamburg.micr0bu.data.transport.UsbSerialState
import com.hawhamburg.micr0bu.data.transport.Esp32Transport
import androidx.compose.material.icons.filled.Bluetooth
import com.hawhamburg.micr0bu.viewmodel.MqttViewModel import com.hawhamburg.micr0bu.viewmodel.MqttViewModel
private val UsbGreen = Color(0xFF4CAF50) private val UsbGreen = Color(0xFF4CAF50)
@@ -67,11 +65,7 @@ fun ConnectionSetupScreen(
val activeTransport by viewModel.activeTransport.collectAsState() val activeTransport by viewModel.activeTransport.collectAsState()
val mqttPrefs by viewModel.mqttPrefs.collectAsState() val mqttPrefs by viewModel.mqttPrefs.collectAsState()
val obuHardware by viewModel.obuHardware.collectAsState() val obuHardware by viewModel.obuHardware.collectAsState()
val esp32LinkState by viewModel.esp32LinkState.collectAsState() val usbSerialState by viewModel.usbSerialState.collectAsState()
val esp32Transport by viewModel.esp32Transport.collectAsState()
val esp32Detail by viewModel.esp32Detail.collectAsState()
val stationStatus by viewModel.stationStatus.collectAsState()
val signOutgoing by viewModel.signOutgoing.collectAsState()
val isConnected = connectionState == MqttConnectionState.CONNECTED val isConnected = connectionState == MqttConnectionState.CONNECTED
val isConnecting = connectionState == MqttConnectionState.CONNECTING val isConnecting = connectionState == MqttConnectionState.CONNECTING
@@ -235,22 +229,22 @@ fun ConnectionSetupScreen(
} }
} else { } else {
// ── ESP32-C5 real connection card (Phase 03) ──────────────────────── // ── ESP32-C5 real connection card (Phase 03) ────────────────────────
// Backed by Esp32Link: USB (UsbSerialTransport, native USB Serial/JTAG CDC-ACM - see // Backed by UsbSerialTransport (native USB Serial/JTAG CDC-ACM link) - see that
// its KDoc for the VID/PID and native-vs-UART-bridge port note) or BLE (BleLinkTransport). // class's KDoc for the VID/PID (0x303A/0x1001) and native-vs-UART-bridge port note.
val isEspConnected = esp32LinkState == Esp32LinkState.CONNECTED val isEspConnected = usbSerialState == UsbSerialState.CONNECTED
val isEspBusy = esp32LinkState == Esp32LinkState.DEVICE_ATTACHED || val isEspBusy = usbSerialState == UsbSerialState.DEVICE_ATTACHED ||
esp32LinkState == Esp32LinkState.PERMISSION_REQUESTED usbSerialState == UsbSerialState.PERMISSION_REQUESTED
val espContainerColor = when { val espContainerColor = when {
isEspConnected -> UsbGreenBg isEspConnected -> UsbGreenBg
isEspBusy -> UsbAmberBg isEspBusy -> UsbAmberBg
else -> UsbGrayBg else -> UsbGrayBg
} }
val (espColor, espStateLabel) = when (esp32LinkState) { val (espColor, espStateLabel) = when (usbSerialState) {
Esp32LinkState.CONNECTED -> UsbGreen to stringResource(R.string.conn_esp32_state_connected) UsbSerialState.CONNECTED -> UsbGreen to stringResource(R.string.conn_esp32_state_connected)
Esp32LinkState.DEVICE_ATTACHED -> UsbAmber to stringResource(R.string.conn_esp32_state_device_attached) UsbSerialState.DEVICE_ATTACHED -> UsbAmber to stringResource(R.string.conn_esp32_state_device_attached)
Esp32LinkState.PERMISSION_REQUESTED -> UsbAmber to stringResource(R.string.conn_esp32_state_permission_requested) UsbSerialState.PERMISSION_REQUESTED -> UsbAmber to stringResource(R.string.conn_esp32_state_permission_requested)
Esp32LinkState.ERROR -> Color(0xFFFF5252) to stringResource(R.string.conn_esp32_state_error) UsbSerialState.ERROR -> Color(0xFFFF5252) to stringResource(R.string.conn_esp32_state_error)
Esp32LinkState.DISCONNECTED -> UsbGray to stringResource(R.string.conn_esp32_state_disconnected) UsbSerialState.DISCONNECTED -> UsbGray to stringResource(R.string.conn_esp32_state_disconnected)
} }
Card( Card(
@@ -263,7 +257,7 @@ fun ConnectionSetupScreen(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
Icon( Icon(
if (esp32Transport == Esp32Transport.BLE) Icons.Default.Bluetooth else Icons.Default.Usb, Icons.Default.Usb,
contentDescription = null, contentDescription = null,
tint = espColor, tint = espColor,
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
@@ -291,36 +285,11 @@ fun ConnectionSetupScreen(
Text(espStateLabel, style = MaterialTheme.typography.bodySmall, color = espColor) Text(espStateLabel, style = MaterialTheme.typography.bodySmall, color = espColor)
} }
Text(
stringResource(
if (esp32Transport == Esp32Transport.BLE) R.string.conn_esp32_via_ble else R.string.conn_esp32_via_usb
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
// Pairing passkey, provisioning progress, or why the micrOBU refused something.
esp32Detail?.let {
Text(it, style = MaterialTheme.typography.bodySmall, color = UsbAmber)
}
if (isEspConnected) {
stationStatus?.let { s ->
Text(
stringResource(
R.string.conn_esp32_signing,
if (signOutgoing) stringResource(R.string.conn_esp32_signing_on) else stringResource(R.string.conn_esp32_signing_off),
s.tickets, s.signedMessages, s.signRefused, s.radioSubmitted,
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
if (isEspConnected || isEspBusy) { if (isEspConnected) {
OutlinedButton( OutlinedButton(
onClick = { viewModel.disconnectEsp32() }, onClick = { viewModel.disconnectUsbSerial() },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
contentColor = Color(0xFFFF5252), contentColor = Color(0xFFFF5252),
@@ -328,13 +297,13 @@ fun ConnectionSetupScreen(
) { ) {
Icon(Icons.Default.LinkOff, null, modifier = Modifier.size(16.dp)) Icon(Icons.Default.LinkOff, null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
// While connecting (BLE retries until it succeeds) this cancels the attempt. Text(stringResource(R.string.conn_disconnect))
Text(stringResource(if (isEspConnected) R.string.conn_disconnect else R.string.conn_esp32_cancel))
} }
} else { } else {
Button( Button(
onClick = { viewModel.connectEsp32() }, onClick = { viewModel.connectUsbSerial() },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = !isEspBusy,
) { ) {
Icon(Icons.Default.Link, null, modifier = Modifier.size(16.dp)) Icon(Icons.Default.Link, null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
@@ -60,7 +60,7 @@ import com.hawhamburg.micr0bu.R
import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState
import com.hawhamburg.micr0bu.data.transport.ObuHardware import com.hawhamburg.micr0bu.data.transport.ObuHardware
import com.hawhamburg.micr0bu.data.transport.TransportType import com.hawhamburg.micr0bu.data.transport.TransportType
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState import com.hawhamburg.micr0bu.data.transport.UsbSerialState
import com.hawhamburg.micr0bu.domain.cam.Cam import com.hawhamburg.micr0bu.domain.cam.Cam
import com.hawhamburg.micr0bu.domain.denm.DenmEvent import com.hawhamburg.micr0bu.domain.denm.DenmEvent
import com.hawhamburg.micr0bu.domain.denm.DenmParser import com.hawhamburg.micr0bu.domain.denm.DenmParser
@@ -78,9 +78,7 @@ fun DashboardScreen(
mqttConnectionState: MqttConnectionState, mqttConnectionState: MqttConnectionState,
activeTransport: TransportType = TransportType.USB_C, activeTransport: TransportType = TransportType.USB_C,
obuHardware: ObuHardware = ObuHardware.CIT_ONE, obuHardware: ObuHardware = ObuHardware.CIT_ONE,
esp32LinkState: Esp32LinkState = Esp32LinkState.DISCONNECTED, usbSerialState: UsbSerialState = UsbSerialState.DISCONNECTED,
/** The ESP32-C5 is reached over BLE rather than its USB port (Settings). */
esp32Bluetooth: Boolean = false,
usbCableConnected: Boolean = false, usbCableConnected: Boolean = false,
obuStationTypeWarning: Boolean = false, obuStationTypeWarning: Boolean = false,
obuStationType: Int? = null, obuStationType: Int? = null,
@@ -283,14 +281,14 @@ fun DashboardScreen(
// even once the serial link is actually up. // even once the serial link is actually up.
val isEsp32 = obuHardware == ObuHardware.ESP32_C5 val isEsp32 = obuHardware == ObuHardware.ESP32_C5
val mqttConnected = mqttConnectionState == MqttConnectionState.CONNECTED val mqttConnected = mqttConnectionState == MqttConnectionState.CONNECTED
val obuConnected = if (isEsp32) esp32LinkState == Esp32LinkState.CONNECTED else mqttConnected val obuConnected = if (isEsp32) usbSerialState == UsbSerialState.CONNECTED else mqttConnected
val transportIcon = if (isEsp32 && esp32Bluetooth) Icons.Default.Bluetooth else when (activeTransport) { val transportIcon = when (activeTransport) {
TransportType.USB_C -> Icons.Default.Usb TransportType.USB_C -> Icons.Default.Usb
TransportType.USB_SERIAL -> Icons.Default.Usb TransportType.USB_SERIAL -> Icons.Default.Usb
TransportType.WIFI -> Icons.Default.Wifi TransportType.WIFI -> Icons.Default.Wifi
TransportType.BLUETOOTH -> Icons.Default.Bluetooth TransportType.BLUETOOTH -> Icons.Default.Bluetooth
} }
val transportInactiveIcon = if (isEsp32 && esp32Bluetooth) Icons.Default.BluetoothDisabled else when (activeTransport) { val transportInactiveIcon = when (activeTransport) {
TransportType.USB_C -> Icons.Default.Usb TransportType.USB_C -> Icons.Default.Usb
TransportType.USB_SERIAL -> Icons.Default.Usb TransportType.USB_SERIAL -> Icons.Default.Usb
TransportType.WIFI -> Icons.Default.WifiOff TransportType.WIFI -> Icons.Default.WifiOff
@@ -326,12 +324,12 @@ fun DashboardScreen(
) )
Text( Text(
text = if (isEsp32) { text = if (isEsp32) {
when (esp32LinkState) { when (usbSerialState) {
Esp32LinkState.CONNECTED -> stringResource(R.string.conn_esp32_state_connected) UsbSerialState.CONNECTED -> stringResource(R.string.conn_esp32_state_connected)
Esp32LinkState.DEVICE_ATTACHED -> stringResource(R.string.conn_esp32_state_device_attached) UsbSerialState.DEVICE_ATTACHED -> stringResource(R.string.conn_esp32_state_device_attached)
Esp32LinkState.PERMISSION_REQUESTED -> stringResource(R.string.conn_esp32_state_permission_requested) UsbSerialState.PERMISSION_REQUESTED -> stringResource(R.string.conn_esp32_state_permission_requested)
Esp32LinkState.ERROR -> stringResource(R.string.conn_esp32_state_error) UsbSerialState.ERROR -> stringResource(R.string.conn_esp32_state_error)
Esp32LinkState.DISCONNECTED -> stringResource(R.string.dash_tap_to_connect) UsbSerialState.DISCONNECTED -> stringResource(R.string.dash_tap_to_connect)
} }
} else when (mqttConnectionState) { } else when (mqttConnectionState) {
MqttConnectionState.CONNECTED -> stringResource(R.string.dash_mqtt_connected) MqttConnectionState.CONNECTED -> stringResource(R.string.dash_mqtt_connected)
@@ -364,16 +362,15 @@ fun DashboardScreen(
TransportChip( TransportChip(
label = stringResource(R.string.dash_transport_usb_serial), label = stringResource(R.string.dash_transport_usb_serial),
icon = Icons.Default.Usb, icon = Icons.Default.Usb,
active = !esp32Bluetooth, active = activeTransport == TransportType.USB_SERIAL,
hasCable = usbCableConnected, hasCable = usbCableConnected,
) )
} }
TransportChip( TransportChip(
label = stringResource(R.string.dash_transport_bt), label = stringResource(R.string.dash_transport_bt),
icon = Icons.Default.Bluetooth, icon = Icons.Default.Bluetooth,
active = isEsp32 && esp32Bluetooth, active = activeTransport == TransportType.BLUETOOTH,
// Only the ESP32-C5 has a BLE link; the CiT One has none. dimmed = true, // Phase 03 — production BT transport still under discussion
dimmed = !isEsp32,
) )
} }
} }
@@ -29,7 +29,6 @@ import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Circle import androidx.compose.material.icons.filled.Circle
import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.LinkOff import androidx.compose.material.icons.filled.LinkOff
import androidx.compose.material.icons.filled.Map
import androidx.compose.material.icons.filled.NotificationsActive import androidx.compose.material.icons.filled.NotificationsActive
import androidx.compose.material.icons.filled.VerticalAlignBottom import androidx.compose.material.icons.filled.VerticalAlignBottom
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
@@ -70,7 +69,7 @@ import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState
import com.hawhamburg.micr0bu.data.mqtt.MqttMessage import com.hawhamburg.micr0bu.data.mqtt.MqttMessage
import com.hawhamburg.micr0bu.data.transport.EspLinkStatus import com.hawhamburg.micr0bu.data.transport.EspLinkStatus
import com.hawhamburg.micr0bu.data.transport.ObuHardware import com.hawhamburg.micr0bu.data.transport.ObuHardware
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState import com.hawhamburg.micr0bu.data.transport.UsbSerialState
import com.hawhamburg.micr0bu.domain.cam.CamParser import com.hawhamburg.micr0bu.domain.cam.CamParser
import com.hawhamburg.micr0bu.domain.cam.StationType import com.hawhamburg.micr0bu.domain.cam.StationType
import com.hawhamburg.micr0bu.domain.denm.DenmParser import com.hawhamburg.micr0bu.domain.denm.DenmParser
@@ -88,15 +87,11 @@ import java.util.Date
import java.util.Locale import java.util.Locale
/** /**
* View toggle for [TopicListPane]: decoded CAM/DENM traffic (LIST) or the raw MQTT topic list * View toggle for [TopicListPane]: decoded CAM/DENM traffic (LIST), the raw MQTT topic list
* (TOPICS, CiT One only - there is no broker on the ESP32-C5 path). * (TOPICS, CiT One only - there is no broker on the ESP32-C5 path), or the V2X live map
* * (MAP, Section 13).
* The live map used to be a third mode here. It is now its own full-screen destination
* ([V2xMapScreen]), reached from the map button in this screen's header: sharing the screen with
* the alert panel and the TX cards left the map about a third of a phone display tall, which is
* not enough to see where anything is relative to anything else.
*/ */
private enum class TopicViewMode { LIST, TOPICS } private enum class TopicViewMode { LIST, TOPICS, MAP }
private val timeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.US) private val timeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
@@ -121,7 +116,6 @@ private val WarningRedBg = Color(0xFF3A0A0A)
@Composable @Composable
fun MqttTopicViewerScreen( fun MqttTopicViewerScreen(
viewModel: MqttViewModel = hiltViewModel(), viewModel: MqttViewModel = hiltViewModel(),
onOpenMap: () -> Unit = {},
) { ) {
val connectionState by viewModel.connectionState.collectAsState() val connectionState by viewModel.connectionState.collectAsState()
val topicMessages by viewModel.topicMessages.collectAsState() val topicMessages by viewModel.topicMessages.collectAsState()
@@ -136,7 +130,7 @@ fun MqttTopicViewerScreen(
val ownCamPosition by viewModel.ownCamPosition.collectAsState() val ownCamPosition by viewModel.ownCamPosition.collectAsState()
// Engine road users PLUS roadside units - the engine deliberately does not track RSUs. // Engine road users PLUS roadside units - the engine deliberately does not track RSUs.
val remoteCamPositions by viewModel.stationsInRange.collectAsState() val remoteCamPositions by viewModel.stationsInRange.collectAsState()
val esp32LinkState by viewModel.esp32LinkState.collectAsState() val usbSerialState by viewModel.usbSerialState.collectAsState()
val camPingerActive by viewModel.camPingerActive.collectAsState() val camPingerActive by viewModel.camPingerActive.collectAsState()
val camPingerSentCount by viewModel.camPingerSentCount.collectAsState() val camPingerSentCount by viewModel.camPingerSentCount.collectAsState()
val camPingerHasFix by viewModel.camPingerHasFix.collectAsState() val camPingerHasFix by viewModel.camPingerHasFix.collectAsState()
@@ -161,7 +155,7 @@ fun MqttTopicViewerScreen(
// DISCONNECTED and using it here made the screen report "offline" while CAMs streamed in over // 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 // serial. Everything on this screen that means "is the OBU link up?" follows the serial link
// instead when that hardware is selected. // instead when that hardware is selected.
val effectiveState = if (isEsp32) esp32LinkState.asConnectionState() else connectionState val effectiveState = if (isEsp32) usbSerialState.asConnectionState() else connectionState
val isConnected = effectiveState == MqttConnectionState.CONNECTED val isConnected = effectiveState == MqttConnectionState.CONNECTED
val isConnecting = effectiveState == MqttConnectionState.CONNECTING val isConnecting = effectiveState == MqttConnectionState.CONNECTING
@@ -199,25 +193,14 @@ fun MqttTopicViewerScreen(
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
} }
// Full-screen live map. In the header rather than in the view-mode row below, so it
// is reachable from the message detail pane too and does not move around as the
// available view modes change with the selected hardware.
IconButton(onClick = onOpenMap) {
Icon(
Icons.Default.Map,
contentDescription = stringResource(R.string.v2x_map_title),
tint = MaterialTheme.colorScheme.primary,
)
}
ConnectionChip(effectiveState) ConnectionChip(effectiveState)
Spacer(Modifier.width(2.dp)) Spacer(Modifier.width(2.dp))
IconButton( IconButton(
onClick = { onClick = {
// Route to whichever transport this hardware actually uses. // Route to whichever transport this hardware actually uses.
if (isEsp32) { if (isEsp32) {
if (isConnected || isConnecting) viewModel.disconnectEsp32() if (isConnected || isConnecting) viewModel.disconnectUsbSerial()
else viewModel.connectEsp32() else viewModel.connectUsbSerial()
} else { } else {
if (isConnected || isConnecting) viewModel.disconnect() else viewModel.connect() if (isConnected || isConnecting) viewModel.disconnect() else viewModel.connect()
} }
@@ -253,7 +236,7 @@ fun MqttTopicViewerScreen(
isEsp32 = isEsp32, isEsp32 = isEsp32,
denmEvents = denmEvents, denmEvents = denmEvents,
spatIntersections = spatIntersections, spatIntersections = spatIntersections,
esp32LinkState = esp32LinkState, usbSerialState = usbSerialState,
camPingerActive = camPingerActive, camPingerActive = camPingerActive,
camPingerSentCount = camPingerSentCount, camPingerSentCount = camPingerSentCount,
camPingerHasFix = camPingerHasFix, camPingerHasFix = camPingerHasFix,
@@ -294,7 +277,7 @@ private fun TopicListPane(
isEsp32: Boolean = false, isEsp32: Boolean = false,
denmEvents: List<com.hawhamburg.micr0bu.domain.denm.DenmEvent> = emptyList(), denmEvents: List<com.hawhamburg.micr0bu.domain.denm.DenmEvent> = emptyList(),
spatIntersections: List<com.hawhamburg.micr0bu.domain.spat.SpatIntersection> = emptyList(), spatIntersections: List<com.hawhamburg.micr0bu.domain.spat.SpatIntersection> = emptyList(),
esp32LinkState: Esp32LinkState = Esp32LinkState.DISCONNECTED, usbSerialState: UsbSerialState = UsbSerialState.DISCONNECTED,
camPingerActive: Boolean = false, camPingerActive: Boolean = false,
camPingerSentCount: Int = 0, camPingerSentCount: Int = 0,
camPingerHasFix: Boolean = false, camPingerHasFix: Boolean = false,
@@ -338,7 +321,7 @@ private fun TopicListPane(
// ── CAM Pinger card — ESP32-C5-only manual bench test, mirrors the DENM card above ── // ── CAM Pinger card — ESP32-C5-only manual bench test, mirrors the DENM card above ──
if (showCamPinger) { if (showCamPinger) {
CamPingerCard( CamPingerCard(
usbConnected = esp32LinkState == Esp32LinkState.CONNECTED, usbConnected = usbSerialState == UsbSerialState.CONNECTED,
pingerActive = camPingerActive, pingerActive = camPingerActive,
sentCount = camPingerSentCount, sentCount = camPingerSentCount,
hasFix = camPingerHasFix, hasFix = camPingerHasFix,
@@ -352,15 +335,12 @@ private fun TopicListPane(
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)) HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f))
} }
// ── List / Topics toggle ────────────────────────────────────────────── // ── List / Topics / Map toggle ────────────────────────────────────────────────────
// Decoded traffic is the default on BOTH hardware paths: what a tester wants to see is // Decoded traffic is the default on BOTH hardware paths: what a tester wants to see is
// the road users and hazards, not the transport that carried them. The raw MQTT topic // the road users and hazards, not the transport that carried them. The raw MQTT topic
// list stays one tap away on the CiT One path (Section 13 asks for the map "in addition // list stays one tap away on the CiT One path (Section 13 asks for the map "in addition
// to", not instead of, the topic list). // to", not instead of, the topic list). It is hidden on the ESP32-C5 path, where there is
// // no broker and `topics` is permanently empty.
// The whole row is hidden on the ESP32-C5 path: there is no broker there, `topics` is
// permanently empty, and a toggle offering a single choice is just noise.
if (!isEsp32) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -370,20 +350,34 @@ private fun TopicListPane(
selected = viewMode == TopicViewMode.LIST, selected = viewMode == TopicViewMode.LIST,
) { viewMode = TopicViewMode.LIST } ) { viewMode = TopicViewMode.LIST }
if (!isEsp32) {
ViewModeButton( ViewModeButton(
label = stringResource(R.string.mqtt_view_topics), label = stringResource(R.string.mqtt_view_topics),
selected = viewMode == TopicViewMode.TOPICS, selected = viewMode == TopicViewMode.TOPICS,
) { viewMode = TopicViewMode.TOPICS } ) { viewMode = TopicViewMode.TOPICS }
} }
ViewModeButton(
label = stringResource(R.string.mqtt_view_map),
selected = viewMode == TopicViewMode.MAP,
) { viewMode = TopicViewMode.MAP }
} }
// ── Decoded traffic / raw topics ────────────────────────────────────── // ── Decoded traffic / raw topics / live map ───────────────────────────
// TOPICS can still be the saved selection from a CiT One session after switching hardware // TOPICS can still be the saved selection from a CiT One session after switching hardware
// to the ESP32-C5, where that button no longer exists - fall back to the decoded list // to the ESP32-C5, where that button no longer exists - fall back to the decoded list
// rather than stranding the user on a pane they can't navigate away from. // rather than stranding the user on a pane they can't navigate away from.
val shownMode = if (viewMode == TopicViewMode.TOPICS && isEsp32) TopicViewMode.LIST else viewMode val shownMode = if (viewMode == TopicViewMode.TOPICS && isEsp32) TopicViewMode.LIST else viewMode
if (shownMode == TopicViewMode.LIST) { if (shownMode == TopicViewMode.MAP) {
V2xLiveMapView(
own = ownCamPosition,
remotes = remoteCamPositions,
alerts = useCaseAlerts,
denms = denmEvents,
modifier = Modifier.fillMaxSize(),
)
} else if (shownMode == TopicViewMode.LIST) {
ReceivedCamPane( ReceivedCamPane(
own = ownCamPosition, own = ownCamPosition,
remotes = remoteCamPositions, remotes = remoteCamPositions,
@@ -1219,12 +1213,11 @@ private fun CamPingerCard(
Text( Text(
stringResource( stringResource(
R.string.mqtt_cam_pinger_fw_counters, R.string.mqtt_cam_pinger_fw_counters,
s.txFailures, s.oversizeDrops, s.rxCrcErrors, s.rxQueueDrops, s.txFailures, s.oversizeDrops, s.rxCrcErrors,
), ),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = if (s.txFailures > 0 || s.oversizeDrops > 0 || s.rxCrcErrors > 0 || color = if (s.txFailures > 0 || s.oversizeDrops > 0 || s.rxCrcErrors > 0)
s.rxQueueDrops > 0 ErrorRed else MaterialTheme.colorScheme.onSurfaceVariant,
) ErrorRed else MaterialTheme.colorScheme.onSurfaceVariant,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
) )
} }
@@ -1473,10 +1466,10 @@ private fun prettyPrintJson(raw: String): String {
* connection UI on this screen already speaks, so one indicator can serve both transports rather * 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. * than duplicating the chip and its colours per hardware type.
*/ */
private fun Esp32LinkState.asConnectionState(): MqttConnectionState = when (this) { private fun UsbSerialState.asConnectionState(): MqttConnectionState = when (this) {
Esp32LinkState.CONNECTED -> MqttConnectionState.CONNECTED UsbSerialState.CONNECTED -> MqttConnectionState.CONNECTED
Esp32LinkState.DEVICE_ATTACHED, UsbSerialState.DEVICE_ATTACHED,
Esp32LinkState.PERMISSION_REQUESTED -> MqttConnectionState.CONNECTING UsbSerialState.PERMISSION_REQUESTED -> MqttConnectionState.CONNECTING
Esp32LinkState.ERROR -> MqttConnectionState.ERROR UsbSerialState.ERROR -> MqttConnectionState.ERROR
Esp32LinkState.DISCONNECTED -> MqttConnectionState.DISCONNECTED UsbSerialState.DISCONNECTED -> MqttConnectionState.DISCONNECTED
} }
@@ -47,9 +47,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import com.hawhamburg.micr0bu.R import com.hawhamburg.micr0bu.R
import com.hawhamburg.micr0bu.data.mqtt.MqttPrefs import com.hawhamburg.micr0bu.data.mqtt.MqttPrefs
import com.hawhamburg.micr0bu.data.transport.Esp32Transport
import com.hawhamburg.micr0bu.data.transport.ObuHardware import com.hawhamburg.micr0bu.data.transport.ObuHardware
import com.hawhamburg.micr0bu.data.transport.OutgoingMessage
import com.hawhamburg.micr0bu.domain.usecase.UseCaseDetectionConfig import com.hawhamburg.micr0bu.domain.usecase.UseCaseDetectionConfig
import com.hawhamburg.micr0bu.domain.usecase.UseCaseType import com.hawhamburg.micr0bu.domain.usecase.UseCaseType
import com.hawhamburg.micr0bu.viewmodel.SensorUiState import com.hawhamburg.micr0bu.viewmodel.SensorUiState
@@ -167,19 +165,13 @@ fun ConnectionSettingsScreen(
onMqttPrefsChange: (MqttPrefs) -> Unit, onMqttPrefsChange: (MqttPrefs) -> Unit,
obuHardware: ObuHardware = ObuHardware.CIT_ONE, obuHardware: ObuHardware = ObuHardware.CIT_ONE,
onObuHardwareChange: (ObuHardware) -> Unit = {}, onObuHardwareChange: (ObuHardware) -> Unit = {},
esp32Transport: Esp32Transport = Esp32Transport.USB,
onEsp32TransportChange: (Esp32Transport) -> Unit = {},
outgoingMessage: OutgoingMessage = OutgoingMessage.CAM,
onOutgoingMessageChange: (OutgoingMessage) -> Unit = {},
signOutgoing: Boolean = true,
onSignOutgoingChange: (Boolean) -> Unit = {},
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
SubScreen(stringResource(R.string.settings_connection), onBack) { SubScreen(stringResource(R.string.settings_connection), onBack) {
SectionCard { SectionCard {
// OBU Hardware selector — CiT One / ESP32-C5 (Phase 03, Section 13). The transport // OBU Hardware selector — CiT One / ESP32-C5 (Phase 03, Section 13). Everything
// cards below apply to the CiT One; the ESP32-C5 has its own card (USB-C or BLE, // below (transport, USB-C options) only really applies to CiT One; ESP32-C5 uses
// CAM or VAM, signing). // USB Serial exclusively and has no transport choice to make here.
Text( Text(
stringResource(R.string.settings_obu_hardware), stringResource(R.string.settings_obu_hardware),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
@@ -220,48 +212,6 @@ fun ConnectionSettingsScreen(
} }
} }
if (obuHardware == ObuHardware.ESP32_C5) {
SectionCard {
TwoWayChoice(
label = stringResource(R.string.settings_esp32_link),
first = stringResource(R.string.settings_transport_usbc),
second = stringResource(R.string.settings_esp32_link_ble),
firstSelected = esp32Transport == Esp32Transport.USB,
onFirst = { onEsp32TransportChange(Esp32Transport.USB) },
onSecond = { onEsp32TransportChange(Esp32Transport.BLE) },
)
if (esp32Transport == Esp32Transport.BLE) {
Text(
stringResource(R.string.settings_esp32_link_ble_note),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 8.dp),
)
}
Divider()
TwoWayChoice(
label = stringResource(R.string.settings_esp32_message),
first = stringResource(R.string.settings_esp32_message_cam),
second = stringResource(R.string.settings_esp32_message_vam),
firstSelected = outgoingMessage == OutgoingMessage.CAM,
onFirst = { onOutgoingMessageChange(OutgoingMessage.CAM) },
onSecond = { onOutgoingMessageChange(OutgoingMessage.VAM) },
)
Divider()
SettingToggleRow(
label = stringResource(R.string.settings_esp32_sign),
checked = signOutgoing,
onCheckedChange = onSignOutgoingChange,
)
Text(
stringResource(R.string.settings_esp32_sign_note),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 8.dp),
)
}
}
if (obuHardware == ObuHardware.CIT_ONE) { if (obuHardware == ObuHardware.CIT_ONE) {
SectionCard { SectionCard {
// Active transport selector // Active transport selector
@@ -661,40 +611,6 @@ private fun LanguageSection() {
} }
} }
/** A labelled pair of outlined buttons, the selected one filled — the style of the OBU hardware picker. */
@Composable
private fun TwoWayChoice(
label: String,
first: String,
second: String,
firstSelected: Boolean,
onFirst: () -> Unit,
onSecond: () -> Unit,
) {
Text(
label,
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 = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
for ((text, selected, onClick) in listOf(Triple(first, firstSelected, onFirst), Triple(second, !firstSelected, onSecond))) {
OutlinedButton(
onClick = onClick,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = if (selected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent,
contentColor = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface,
),
) { Text(text, fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal) }
}
}
}
@Composable @Composable
private fun RowDivider() = HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f)) private fun RowDivider() = HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f))
@@ -1,13 +1,12 @@
package com.hawhamburg.micr0bu.ui.screens package com.hawhamburg.micr0bu.ui.screens
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.view.MotionEvent
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.GpsOff import androidx.compose.material.icons.filled.GpsOff
@@ -31,37 +30,25 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import com.hawhamburg.micr0bu.R import com.hawhamburg.micr0bu.R
import com.hawhamburg.micr0bu.domain.cam.Cam import com.hawhamburg.micr0bu.domain.cam.Cam
import com.hawhamburg.micr0bu.domain.denm.DenmEvent import com.hawhamburg.micr0bu.domain.denm.DenmEvent
import com.hawhamburg.micr0bu.domain.spat.SpatIntersection
import com.hawhamburg.micr0bu.domain.usecase.AlertLevel import com.hawhamburg.micr0bu.domain.usecase.AlertLevel
import com.hawhamburg.micr0bu.domain.usecase.UseCaseAlert import com.hawhamburg.micr0bu.domain.usecase.UseCaseAlert
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Marker
/** /**
* V2X Monitor live map (Phase 03, Section 13) — the map body behind [V2xMapScreen], plotting the * V2X Monitor live map view (Phase 03, Section 13) — plots the ego bike's own position plus
* ego bike's own position, every currently-tracked remote road user's last-known CAM position, * every currently-tracked remote road user's last-known CAM position, in addition to (not
* every live hazard (DENM) and every signalised intersection heard over SPATEM. * replacing) the raw topic list already on this screen. Reuses the same osmdroid pattern as
* [MapScreen]; unlike that screen, this one has no phone-GNSS-only fallback because [own] here
* always reflects whichever ego source [com.hawhamburg.micr0bu.data.cam.CamUseCaseRepository]
* currently trusts (obu_gnss / phone GNSS / CAM-topic-own — see that class's KDoc).
* *
* Marker vocabulary, one shape per message type so the map reads without a legend: * Remote markers are colored by that station's most severe active alert level, if any, so a
* - CAM — teardrop pin, tinted by that station's most severe active alert level * glance at the map shows not just "who's nearby" but "who's a warning right now" — the same
* - DENM — hazard warning triangle * severity coloring already used by [UseCaseAlertPanel].
* - SPATEM — traffic light, with the lamp for the intersection's leading phase lit
*
* Unlike [MapScreen] this has no phone-GNSS-only fallback: [own] always reflects whichever ego
* source [com.hawhamburg.micr0bu.data.cam.CamUseCaseRepository] currently trusts (obu_gnss /
* phone GNSS / CAM-topic-own — see that class's KDoc).
*
* **Markers are reused across updates, not rebuilt.** CAMs arrive at up to 10 Hz per station, and
* every arrival recomposes this view; the previous version cleared the overlay list and rebuilt
* every Marker — decoding and mutating a fresh Drawable per marker per update — which is what
* made panning stutter under live traffic. Drawables are now loaded once per level/phase and
* shared (osmdroid sets the icon's bounds on each draw, so sharing one instance across markers is
* safe), and Marker objects are cached by key. The overlay list is still reordered each update,
* which costs nothing: it moves existing references, it does not allocate.
*/ */
@Composable @Composable
fun V2xLiveMapView( fun V2xLiveMapView(
@@ -69,9 +56,6 @@ fun V2xLiveMapView(
remotes: Map<Long, Cam>, remotes: Map<Long, Cam>,
alerts: List<UseCaseAlert>, alerts: List<UseCaseAlert>,
denms: List<DenmEvent> = emptyList(), denms: List<DenmEvent> = emptyList(),
spats: List<SpatIntersection> = emptyList(),
followOwn: Boolean = true,
onUserPanned: () -> Unit = {},
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -87,17 +71,8 @@ fun V2xLiveMapView(
.mapValues { (_, a) -> a.maxByOrNull { it.alertLevel.ordinal }?.alertLevel } .mapValues { (_, a) -> a.maxByOrNull { it.alertLevel.ordinal }?.alertLevel }
} }
// Loaded once and shared by every marker that needs them. mutate() on the remote pin is still
// essential: without it all four tinted copies would share one ConstantState and the last
// tint applied would recolour every pin on the map.
val icons = remember(context) { MapIcons(context) }
val markers = remember { mutableMapOf<String, Marker>() }
val mapViewRef = remember { mutableStateOf<MapView?>(null) } val mapViewRef = remember { mutableStateOf<MapView?>(null) }
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
// Tracks whether the last update already recentred for this follow session, so re-enabling
// follow animates once instead of fighting the rider's own panning on every frame.
val wasFollowing = remember { mutableStateOf(false) }
DisposableEffect(lifecycleOwner) { DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
@@ -114,58 +89,42 @@ fun V2xLiveMapView(
} }
} }
Column(modifier = modifier.fillMaxSize()) {
Text(
text = stringResource(R.string.v2x_map_remote_count, remotes.size),
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(4.dp))
AndroidView( AndroidView(
factory = { ctx -> factory = { ctx ->
initOsmForV2xMap(ctx) initOsmForV2xMap(ctx)
MapView(ctx).apply { MapView(ctx).apply {
setTileSource(TileSourceFactory.MAPNIK) setTileSource(TileSourceFactory.MAPNIK)
setMultiTouchControls(true) setMultiTouchControls(true)
// Raster tiles are authored for ~160 dpi; without this they are upscaled by the
// display density and labels come out soft on a modern phone.
isTilesScaledToDpi = true
// The floating +/- buttons sit exactly where the rider's thumb lands and
// duplicate pinch-zoom. Pinch and double-tap still work.
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
setMinZoomLevel(4.0)
setMaxZoomLevel(20.0)
controller.setZoom(17.0) controller.setZoom(17.0)
controller.setCenter(ownGeoPoint) controller.setCenter(ownGeoPoint)
// Any touch means the rider is driving the map; follow-own hands over to them
// until they ask for it back. false: the MapView's own gesture handling still
// runs, this only observes.
setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) onUserPanned()
false
}
mapViewRef.value = this mapViewRef.value = this
} }
}, },
update = { mv -> update = { mv ->
val now = System.currentTimeMillis() mv.overlays.clear()
// Intersections we can actually place: SPATEM carries signal state but no geometry // Own position: a centred "you are here" dot, not a pin. Own position is a fact
// (that is MAPEM's job), so the only position available is the sending RSU's own CAM. // about the viewer rather than one of the tracked objects, and when both used
val locatedSpats = spats.mapNotNull { spat -> // osmdroid's identical default pin the two were indistinguishable at a glance.
remotes[spat.stationId]?.let { rsu -> spat to rsu } mv.overlays.add(
} Marker(mv).apply {
// An RSU drawn as a traffic light must not also be drawn as a CAM pin underneath it:
// two markers on one point, the lower one unreachable.
val spatStationIds = locatedSpats.map { (spat, _) -> spat.stationId }.toSet()
val live = mutableSetOf<String>()
// Own position: a centred "you are here" dot, not a pin. Own position is a fact about
// the viewer rather than one of the tracked objects, and when both used osmdroid's
// identical default pin the two were indistinguishable at a glance.
markers.marker(mv, KEY_OWN, live).apply {
position = ownGeoPoint position = ownGeoPoint
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
icon = icons.own icon = ContextCompat.getDrawable(context, R.drawable.ic_map_own)
title = context.getString(R.string.v2x_map_own_label) title = context.getString(R.string.v2x_map_own_label)
} }
)
remotes.forEach { (stationId, cam) -> remotes.forEach { (stationId, cam) ->
if (stationId in spatStationIds) return@forEach
val level = alertByStation[stationId] val level = alertByStation[stationId]
val label = when (level) { val label = when (level) {
AlertLevel.WARNING -> context.getString(R.string.v2x_map_remote_warning, stationId) AlertLevel.WARNING -> context.getString(R.string.v2x_map_remote_warning, stationId)
@@ -173,23 +132,32 @@ fun V2xLiveMapView(
AlertLevel.INFO -> context.getString(R.string.v2x_map_remote_info, stationId) AlertLevel.INFO -> context.getString(R.string.v2x_map_remote_info, stationId)
null -> context.getString(R.string.v2x_map_remote_plain, stationId) null -> context.getString(R.string.v2x_map_remote_plain, stationId)
} }
// Teardrop pin anchored at its tip, tinted by severity, so severity no longer // Teardrop pin anchored at its tip, tinted by severity. Now that these are
// depends on tapping the marker to read its label. // custom drawables, per-instance tinting is possible - severity no longer
markers.marker(mv, "$KEY_CAM$stationId", live).apply { // depends on tapping the marker to read its label. mutate() is essential:
// without it every marker shares one ConstantState and the last tint applied
// would recolour all of them.
val pin = ContextCompat.getDrawable(context, R.drawable.ic_map_remote_station)
?.mutate()
?.apply { setTint(level.toMarkerColor()) }
mv.overlays.add(
Marker(mv).apply {
position = GeoPoint(cam.latitude, cam.longitude) position = GeoPoint(cam.latitude, cam.longitude)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = icons.remotePin(level) icon = pin
title = label title = label
} }
)
} }
// Hazards and signals are added after the vehicle pins, so they draw on top: a hazard // DENM hazard pins, added last so they draw on top of vehicle markers - a hazard
// hidden behind a CAM pin defeats the point of showing it. // hidden behind a CAM pin defeats the point of showing it.
denms.forEach { denm -> denms.forEach { denm ->
markers.marker(mv, "$KEY_DENM${denm.dedupKey}", live).apply { mv.overlays.add(
Marker(mv).apply {
position = GeoPoint(denm.latitude, denm.longitude) position = GeoPoint(denm.latitude, denm.longitude)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = icons.denm icon = ContextCompat.getDrawable(context, R.drawable.ic_denm_warning)
title = denm.causeCode?.let { title = denm.causeCode?.let {
context.getString( context.getString(
R.string.v2x_map_denm_labeled, R.string.v2x_map_denm_labeled,
@@ -199,45 +167,10 @@ fun V2xLiveMapView(
) )
} ?: context.getString(R.string.v2x_map_denm_plain, denm.stationId) } ?: context.getString(R.string.v2x_map_denm_plain, denm.stationId)
} }
}
locatedSpats.forEach { (spat, rsu) ->
val phase = spat.leadingPhase(now)
markers.marker(mv, "$KEY_SPAT${spat.key}", live).apply {
position = GeoPoint(rsu.latitude, rsu.longitude)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = icons.signal(phase)
title = context.getString(
R.string.v2x_spat_rx_title, spat.state.key, spat.stationId,
) )
snippet = spat.state.movements.joinToString(" · ") { movement ->
val seconds = movement.current?.secondsUntil(now)?.takeIf { it in 0.0..99.0 }
context.getString(R.string.v2x_spat_group, movement.signalGroup) +
(seconds?.let { " " + context.getString(R.string.v2x_spat_countdown, it) } ?: "")
}
}
} }
// Drop markers for stations, hazards and intersections that have expired, then rebuild mv.controller.animateTo(ownGeoPoint)
// the overlay list in draw order from the cached Markers. Reordering moves references;
// nothing here allocates a Marker or decodes a Drawable.
markers.keys.retainAll { key ->
(key in live).also { kept -> if (!kept) markers[key]?.closeInfoWindow() }
}
mv.overlays.clear()
markers.entries
.sortedBy { (key, _) -> key.drawOrder() }
.forEach { (_, marker) -> mv.overlays.add(marker) }
// setCenter, not animateTo: an animation restarted on every CAM never finishes, which
// is exactly the judder this used to show under live traffic. The one animated move is
// the rider re-enabling follow, where the travel is worth seeing.
if (followOwn) {
if (wasFollowing.value) mv.controller.setCenter(ownGeoPoint)
else mv.controller.animateTo(ownGeoPoint)
}
wasFollowing.value = followOwn
mv.invalidate() mv.invalidate()
}, },
// osmdroid's onDetach() permanently tears the MapView down: afterwards its // osmdroid's onDetach() permanently tears the MapView down: afterwards its
@@ -253,98 +186,10 @@ fun V2xLiveMapView(
// //
// onRelease is the callback that actually means "this View is gone": Compose // onRelease is the callback that actually means "this View is gone": Compose
// guarantees no further update after it. // guarantees no further update after it.
onRelease = { onRelease = { it.onDetach() },
markers.clear() modifier = Modifier.fillMaxSize(),
it.onDetach()
},
modifier = modifier.fillMaxSize(),
) )
} }
// ── Marker cache ──────────────────────────────────────────────────────────────
private const val KEY_OWN = "own"
private const val KEY_CAM = "cam:"
private const val KEY_DENM = "denm:"
private const val KEY_SPAT = "spat:"
/** Draw order: own dot at the bottom, then vehicles, with hazards and signals on top. */
private fun String.drawOrder(): Int = when {
this == KEY_OWN -> 0
startsWith(KEY_CAM) -> 1
startsWith(KEY_DENM) -> 2
else -> 3
}
/**
* The cached [Marker] for [key], created against [mv] on first use, recording the key in [live]
* so the caller can drop whatever it did not ask for this update.
*/
private fun MutableMap<String, Marker>.marker(
mv: MapView,
key: String,
live: MutableSet<String>,
): Marker {
live += key
return getOrPut(key) { Marker(mv) }
}
/**
* Marker artwork, loaded once per composition rather than per update.
*
* The remote pin is drawn white and tinted per severity here; [mutate] is what keeps the four
* tinted copies independent, since without it they would share one ConstantState and the last
* tint applied would recolour all of them.
*/
private class MapIcons(context: Context) {
val own: Drawable? = ContextCompat.getDrawable(context, R.drawable.ic_map_own)
val denm: Drawable? = ContextCompat.getDrawable(context, R.drawable.ic_denm_warning)
private val pins: Map<AlertLevel?, Drawable?> =
(listOf(null) + AlertLevel.entries).associateWith { level ->
ContextCompat.getDrawable(context, R.drawable.ic_map_remote_station)
?.mutate()
?.apply { setTint(level.toMarkerColor()) }
}
private val signals: Map<SignalLamp, Drawable?> = SignalLamp.entries.associateWith { lamp ->
ContextCompat.getDrawable(context, lamp.drawableRes)
}
fun remotePin(level: AlertLevel?): Drawable? = pins[level]
fun signal(lamp: SignalLamp): Drawable? = signals[lamp]
}
// ── Signal phase → lamp ───────────────────────────────────────────────────────
/** Which lamp of the traffic-light marker is lit. */
private enum class SignalLamp(val drawableRes: Int) {
RED(R.drawable.ic_map_spat_red),
AMBER(R.drawable.ic_map_spat_amber),
GREEN(R.drawable.ic_map_spat_green),
DARK(R.drawable.ic_map_spat_dark),
}
/**
* The lamp to light for this intersection.
*
* Without MAPEM there is no lane geometry, so there is no way to know which of an intersection's
* signal groups applies to the rider's own approach. This follows the rule the Dashboard's
* SignalCard already uses — the group changing soonest speaks for the intersection — so the same
* intersection reads the same way in both places rather than inventing a second convention.
*/
private fun SpatIntersection.leadingPhase(nowMs: Long): SignalLamp {
val leading = state.movements.minByOrNull { movement ->
movement.current?.secondsUntil(nowMs)?.takeIf { it >= 0.0 } ?: Double.MAX_VALUE
}
val phase = leading?.current?.phase
return when {
phase == null -> SignalLamp.DARK
phase.isGo -> SignalLamp.GREEN
phase.isStop -> SignalLamp.RED
phase.isTransition -> SignalLamp.AMBER
else -> SignalLamp.DARK // UNAVAILABLE / DARK / caution
}
} }
/** /**
@@ -383,10 +228,5 @@ private fun initOsmForV2xMap(context: Context) {
Configuration.getInstance().apply { Configuration.getInstance().apply {
load(context, context.getSharedPreferences("osmdroid", Context.MODE_PRIVATE)) load(context, context.getSharedPreferences("osmdroid", Context.MODE_PRIVATE))
userAgentValue = context.packageName userAgentValue = context.packageName
// Panning off the edge of the cache is what makes a raster map feel slow: the default
// 600 MB cap is plenty, but the default 2 download threads are not when a pan exposes a
// screenful of new tiles at once.
tileDownloadThreads = 6.toShort()
tileFileSystemThreads = 6.toShort()
} }
} }
@@ -1,203 +0,0 @@
package com.hawhamburg.micr0bu.ui.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.MyLocation
import androidx.compose.material.icons.filled.Place
import androidx.compose.material.icons.filled.Traffic
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.hawhamburg.micr0bu.R
import com.hawhamburg.micr0bu.viewmodel.MqttViewModel
/**
* Full-screen V2X live map — the map and nothing else, reached from the map button on the V2X
* Monitor screen.
*
* Identical on both hardware paths. Everything drawn here comes from
* [com.hawhamburg.micr0bu.data.cam.CamUseCaseRepository], which already merges the CiT One's MQTT
* feed and the ESP32-C5's serial feed into one set of flows, so this screen never has to know
* which OBU is connected.
*
* The chrome is deliberately minimal and floats over the map rather than boxing it in: a back
* button, a live count per message type, and a recentre button. The counts double as the map's
* legend — each one carries the same icon family as the marker it counts.
*/
@Composable
fun V2xMapScreen(
viewModel: MqttViewModel,
onBack: () -> Unit,
) {
val ownCamPosition by viewModel.ownCamPosition.collectAsState()
// Road users from the detection engine PLUS roadside units, which it deliberately does not
// track - an RSU is what carries the traffic lights below.
val stations by viewModel.stationsInRange.collectAsState()
val alerts by viewModel.useCaseAlerts.collectAsState()
val denms by viewModel.denmEvents.collectAsState()
val spats by viewModel.spatIntersections.collectAsState()
// Follow is on until the rider touches the map, and comes back when they ask for it. Without
// the hand-over, every incoming CAM would drag the viewport back to the ego position and the
// map could not be panned at all while traffic is flowing.
var followOwn by remember { mutableStateOf(true) }
// SPATEM carries no geometry of its own, so an intersection can only be placed if its RSU has
// also been heard over CAM. Saying so is better than silently dropping it: "the map shows two
// of the three lights I can see in the list" is otherwise an unexplained discrepancy.
val unlocatedSpats = spats.count { it.stationId !in stations.keys }
Box(modifier = Modifier.fillMaxSize()) {
V2xLiveMapView(
own = ownCamPosition,
remotes = stations,
alerts = alerts,
denms = denms,
spats = spats,
followOwn = followOwn,
onUserPanned = { followOwn = false },
modifier = Modifier.fillMaxSize(),
)
// ── Floating header: back + live counts, which double as the legend ──
Row(
modifier = Modifier
.align(Alignment.TopStart)
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MapChrome {
IconButton(onClick = onBack, modifier = Modifier.size(36.dp)) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.v2x_map_back),
)
}
}
Spacer(Modifier.width(8.dp))
MapChrome {
Row(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MapCount(
icon = Icons.Default.Place,
tint = CamPinBlue,
count = stations.size,
label = stringResource(R.string.v2x_map_legend_cam),
)
MapCount(
icon = Icons.Default.Warning,
tint = HazardAmber,
count = denms.size,
label = stringResource(R.string.v2x_map_legend_denm),
)
MapCount(
icon = Icons.Default.Traffic,
tint = SignalGreenDot,
count = spats.size,
label = stringResource(R.string.v2x_map_legend_spat),
)
}
}
}
if (unlocatedSpats > 0) {
MapChrome(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(12.dp),
) {
Text(
text = stringResource(R.string.v2x_map_spat_unlocated, unlocatedSpats),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
)
}
}
// Recentre: lit while following, so the button also reports which mode the map is in.
FloatingActionButton(
onClick = { followOwn = true },
containerColor = if (followOwn) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
) {
Icon(
Icons.Default.MyLocation,
contentDescription = stringResource(R.string.v2x_map_follow),
tint = if (followOwn) MaterialTheme.colorScheme.onPrimary
else MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
/** A translucent pill for anything floating over the map, so chrome stays readable over tiles. */
@Composable
private fun MapChrome(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Surface(
shape = RoundedCornerShape(18.dp),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.88f),
tonalElevation = 3.dp,
shadowElevation = 2.dp,
modifier = modifier,
) { content() }
}
@Composable
private fun MapCount(icon: ImageVector, tint: Color, count: Int, label: String) {
Row(
horizontalArrangement = Arrangement.spacedBy(3.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(icon, contentDescription = label, tint = tint, modifier = Modifier.size(16.dp))
Text(
text = count.toString(),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
// Legend tints, matching the marker artwork rather than the theme: these name the drawables on
// the map, so they must not shift with light/dark mode the way theme colours do.
private val CamPinBlue = Color(0xFF78909C)
private val HazardAmber = Color(0xFFFFC107)
private val SignalGreenDot = Color(0xFF4CAF50)
@@ -13,11 +13,8 @@ import com.hawhamburg.micr0bu.data.transport.EspLinkStatus
import com.hawhamburg.micr0bu.data.transport.ObuHardware import com.hawhamburg.micr0bu.data.transport.ObuHardware
import com.hawhamburg.micr0bu.data.transport.TransportType import com.hawhamburg.micr0bu.data.transport.TransportType
import com.hawhamburg.micr0bu.data.transport.UsbNetworkDetector import com.hawhamburg.micr0bu.data.transport.UsbNetworkDetector
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState import com.hawhamburg.micr0bu.data.transport.UsbSerialState
import com.hawhamburg.micr0bu.data.transport.Esp32Link import com.hawhamburg.micr0bu.data.transport.UsbSerialTransport
import com.hawhamburg.micr0bu.data.transport.Esp32Transport
import com.hawhamburg.micr0bu.data.transport.OutgoingMessage
import com.hawhamburg.micr0bu.data.transport.StationStatus
import com.hawhamburg.micr0bu.domain.denm.DenmEvent import com.hawhamburg.micr0bu.domain.denm.DenmEvent
import com.hawhamburg.micr0bu.domain.denm.DenmParser import com.hawhamburg.micr0bu.domain.denm.DenmParser
import com.hawhamburg.micr0bu.domain.spat.SpatIntersection import com.hawhamburg.micr0bu.domain.spat.SpatIntersection
@@ -48,7 +45,7 @@ class MqttViewModel @Inject constructor(
private val usbDetector: UsbNetworkDetector, private val usbDetector: UsbNetworkDetector,
private val camUseCaseRepository: CamUseCaseRepository, private val camUseCaseRepository: CamUseCaseRepository,
private val obuHardwarePrefs: ObuHardwarePreferences, private val obuHardwarePrefs: ObuHardwarePreferences,
private val esp32Link: Esp32Link, private val usbSerialTransport: UsbSerialTransport,
private val camPinger: CamPinger, private val camPinger: CamPinger,
) : ViewModel() { ) : ViewModel() {
@@ -83,42 +80,14 @@ class MqttViewModel @Inject constructor(
/** Auto-detected OBU gateway IP on the USB interface. */ /** Auto-detected OBU gateway IP on the USB interface. */
val detectedObuIp: StateFlow<String?> = usbDetector.detectedGatewayIp val detectedObuIp: StateFlow<String?> = usbDetector.detectedGatewayIp
/** ESP32-C5 link state, over USB or BLE per [esp32Transport] — see [Esp32Link]. */ /** ESP32-C5 USB-serial link state (Phase 03) — see [UsbSerialTransport]. */
val esp32LinkState: StateFlow<Esp32LinkState> = esp32Link.state val usbSerialState: StateFlow<UsbSerialState> = usbSerialTransport.state
/** Latest firmware heartbeat + drop counters, null until the first STATUS frame arrives. */ /** Latest firmware heartbeat + drop counters, null until the first STATUS frame arrives. */
val espLinkStatus: StateFlow<EspLinkStatus?> = esp32Link.linkStatus val espLinkStatus: StateFlow<EspLinkStatus?> = usbSerialTransport.linkStatus
/** Non-zero means CAMs are being built and dropped — see [Esp32Link.send]. */ /** Non-zero means CAMs are being built and dropped — see [UsbSerialTransport.sendCamTx]. */
val camSendFailures: StateFlow<Int> = esp32Link.consecutiveWriteFailures val camSendFailures: StateFlow<Int> = usbSerialTransport.consecutiveWriteFailures
/** Signing and radio counters of the current obu-firmware; null with the previous firmware. */
val stationStatus: StateFlow<StationStatus?> = esp32Link.stationStatus
/** One line about the link session (pairing passkey, provisioning, refusals); null when quiet. */
val esp32Detail: StateFlow<String?> = esp32Link.detail
// ── ESP32-C5 settings ─────────────────────────────────────────────────────
val esp32Transport: StateFlow<Esp32Transport> = esp32Link.transport
fun setEsp32Transport(transport: Esp32Transport) {
viewModelScope.launch { obuHardwarePrefs.setEsp32Transport(transport) }
}
val outgoingMessage: StateFlow<OutgoingMessage> = obuHardwarePrefs.outgoingMessageFlow
.stateIn(viewModelScope, SharingStarted.Eagerly, OutgoingMessage.CAM)
fun setOutgoingMessage(message: OutgoingMessage) {
viewModelScope.launch { obuHardwarePrefs.setOutgoingMessage(message) }
}
val signOutgoing: StateFlow<Boolean> = obuHardwarePrefs.signOutgoingFlow
.stateIn(viewModelScope, SharingStarted.Eagerly, true)
fun setSignOutgoing(sign: Boolean) {
viewModelScope.launch { obuHardwarePrefs.setSignOutgoing(sign) }
}
// ── ESP32-C5 CAM pinger (manual bench test, Phase 03) ───────────────────── // ── ESP32-C5 CAM pinger (manual bench test, Phase 03) ─────────────────────
// The ESP32-C5-path equivalent of the CiT One's manual DENM trigger below — a fixed- // The ESP32-C5-path equivalent of the CiT One's manual DENM trigger below — a fixed-
@@ -383,10 +352,10 @@ class MqttViewModel @Inject constructor(
fun connect() = repo.connect() fun connect() = repo.connect()
fun disconnect() = repo.disconnect() fun disconnect() = repo.disconnect()
/** Connect/disconnect the ESP32-C5 link (USB or BLE per [esp32Transport]) — separate from /** Connect/disconnect the ESP32-C5 USB-serial link — separate from [connect]/[disconnect],
* [connect]/[disconnect], which drive the CiT One's MQTT-over-USB-C/Wi-Fi path. */ * which drive the CiT One's MQTT-over-USB-C/Wi-Fi path. See [ConnectionSetupScreen]. */
fun connectEsp32() = esp32Link.connect() fun connectUsbSerial() = usbSerialTransport.connect()
fun disconnectEsp32() = esp32Link.disconnect() fun disconnectUsbSerial() = usbSerialTransport.disconnect()
fun selectTopic(topic: String?) { _selectedTopic.value = topic } fun selectTopic(topic: String?) { _selectedTopic.value = topic }
fun setAutoScroll(enabled: Boolean) { _autoScroll.value = enabled } fun setAutoScroll(enabled: Boolean) { _autoScroll.value = enabled }
@@ -420,7 +389,7 @@ class MqttViewModel @Inject constructor(
super.onCleared() super.onCleared()
repo.disconnect() repo.disconnect()
camPinger.stop() camPinger.stop()
// Deliberately NOT esp32Link.disconnect(): the link is an app-scoped // Deliberately NOT usbSerialTransport.disconnect(): the transport is an app-scoped
// @Singleton also held by the foreground TripRecordingService (via CamTransmitLoop). // @Singleton also held by the foreground TripRecordingService (via CamTransmitLoop).
// Closing it here would tear the port down when the Activity goes away — e.g. swiping // Closing it here would tear the port down when the Activity goes away — e.g. swiping
// the app from Recents mid-recording — leaving the still-running service beaconing into // the app from Recents mid-recording — leaving the still-running service beaconing into
@@ -1,38 +0,0 @@
<!--
Live-map marker for a signalised intersection heard over SPATEM: a traffic light housing with
the lamp for the intersection's leading phase lit and the other two dimmed.
One drawable per lit lamp rather than one drawable tinted at runtime: setTint recolours every
path in a vector, so a single shared asset could not keep the unlit lamps dark while colouring
the lit one - the whole light would turn one flat colour and stop reading as a traffic light.
Anchored at the bottom in V2xLiveMapView, so the housing sits above the intersection rather
than covering it.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- White outline first, so the marker stays legible over dark map features. -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M4.4,0.8H19.6V23.2H4.4z" />
<!-- Housing. -->
<path
android:fillColor="#FF263238"
android:pathData="M5.8,2.0H18.2V22.0H5.8z" />
<!-- Lamps, top to bottom: red, amber, green. -->
<path
android:fillColor="#FF5A2220"
android:pathData="M12,6.6m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
<path
android:fillColor="#FFFFC107"
android:pathData="M12,12.0m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
<path
android:fillColor="#FF1E4D2B"
android:pathData="M12,17.4m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
</vector>
@@ -1,38 +0,0 @@
<!--
Live-map marker for a signalised intersection heard over SPATEM: a traffic light housing with
the lamp for the intersection's leading phase lit and the other two dimmed.
One drawable per lit lamp rather than one drawable tinted at runtime: setTint recolours every
path in a vector, so a single shared asset could not keep the unlit lamps dark while colouring
the lit one - the whole light would turn one flat colour and stop reading as a traffic light.
Anchored at the bottom in V2xLiveMapView, so the housing sits above the intersection rather
than covering it.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- White outline first, so the marker stays legible over dark map features. -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M4.4,0.8H19.6V23.2H4.4z" />
<!-- Housing. -->
<path
android:fillColor="#FF263238"
android:pathData="M5.8,2.0H18.2V22.0H5.8z" />
<!-- Lamps, top to bottom: red, amber, green. -->
<path
android:fillColor="#FF5A2220"
android:pathData="M12,6.6m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
<path
android:fillColor="#FF4A2E1C"
android:pathData="M12,12.0m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
<path
android:fillColor="#FF1E4D2B"
android:pathData="M12,17.4m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
</vector>
@@ -1,38 +0,0 @@
<!--
Live-map marker for a signalised intersection heard over SPATEM: a traffic light housing with
the lamp for the intersection's leading phase lit and the other two dimmed.
One drawable per lit lamp rather than one drawable tinted at runtime: setTint recolours every
path in a vector, so a single shared asset could not keep the unlit lamps dark while colouring
the lit one - the whole light would turn one flat colour and stop reading as a traffic light.
Anchored at the bottom in V2xLiveMapView, so the housing sits above the intersection rather
than covering it.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- White outline first, so the marker stays legible over dark map features. -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M4.4,0.8H19.6V23.2H4.4z" />
<!-- Housing. -->
<path
android:fillColor="#FF263238"
android:pathData="M5.8,2.0H18.2V22.0H5.8z" />
<!-- Lamps, top to bottom: red, amber, green. -->
<path
android:fillColor="#FF5A2220"
android:pathData="M12,6.6m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
<path
android:fillColor="#FF4A2E1C"
android:pathData="M12,12.0m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
<path
android:fillColor="#FF4CAF50"
android:pathData="M12,17.4m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
</vector>
@@ -1,38 +0,0 @@
<!--
Live-map marker for a signalised intersection heard over SPATEM: a traffic light housing with
the lamp for the intersection's leading phase lit and the other two dimmed.
One drawable per lit lamp rather than one drawable tinted at runtime: setTint recolours every
path in a vector, so a single shared asset could not keep the unlit lamps dark while colouring
the lit one - the whole light would turn one flat colour and stop reading as a traffic light.
Anchored at the bottom in V2xLiveMapView, so the housing sits above the intersection rather
than covering it.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- White outline first, so the marker stays legible over dark map features. -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M4.4,0.8H19.6V23.2H4.4z" />
<!-- Housing. -->
<path
android:fillColor="#FF263238"
android:pathData="M5.8,2.0H18.2V22.0H5.8z" />
<!-- Lamps, top to bottom: red, amber, green. -->
<path
android:fillColor="#FFFF5252"
android:pathData="M12,6.6m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
<path
android:fillColor="#FF4A2E1C"
android:pathData="M12,12.0m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
<path
android:fillColor="#FF1E4D2B"
android:pathData="M12,17.4m-2.6,0a2.6,2.6 0 1,0 5.2,0a2.6,2.6 0 1,0 -5.2,0" />
</vector>
+3 -22
View File
@@ -142,18 +142,12 @@
<string name="gnss_no_fix">Noch kein GPS-Signal - gehen Sie ins Freie</string> <string name="gnss_no_fix">Noch kein GPS-Signal - gehen Sie ins Freie</string>
<string name="map_title">Standortkarte</string> <string name="map_title">Standortkarte</string>
<string name="map_location_label">Aktueller Standort</string> <string name="map_location_label">Aktueller Standort</string>
<string name="v2x_map_remote_count">%1$d erfasste externe Verkehrsteilnehmer</string>
<string name="v2x_map_own_label">Eigen (Ego)</string> <string name="v2x_map_own_label">Eigen (Ego)</string>
<string name="v2x_map_remote_plain">Extern #%1$d</string> <string name="v2x_map_remote_plain">Extern #%1$d</string>
<string name="v2x_map_remote_info">Extern #%1$d · Info</string> <string name="v2x_map_remote_info">Extern #%1$d · Info</string>
<string name="v2x_map_remote_awareness">Extern #%1$d · Aufmerksamkeit</string> <string name="v2x_map_remote_awareness">Extern #%1$d · Aufmerksamkeit</string>
<string name="v2x_map_remote_warning">Extern #%1$d · Warnung</string> <string name="v2x_map_remote_warning">Extern #%1$d · Warnung</string>
<string name="v2x_map_title">V2X-Live-Karte</string>
<string name="v2x_map_back">Zurück</string>
<string name="v2x_map_follow">Auf eigene Position zentrieren</string>
<string name="v2x_map_legend_cam">Verkehrsteilnehmer (CAM)</string>
<string name="v2x_map_legend_denm">Gefahren (DENM)</string>
<string name="v2x_map_legend_spat">Signale (SPATEM)</string>
<string name="v2x_map_spat_unlocated">%1$d Signal(e) nicht dargestellt - Senderposition unbekannt</string>
<!-- Settings --> <!-- Settings -->
<string name="settings_title">Einstellungen</string> <string name="settings_title">Einstellungen</string>
@@ -189,7 +183,7 @@
<string name="settings_obu_hardware">OBU-Hardware</string> <string name="settings_obu_hardware">OBU-Hardware</string>
<string name="settings_obu_hardware_cit_one">CiT One</string> <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">ESP32-C5</string>
<string name="settings_obu_hardware_esp32_note">Das Smartphone erstellt CAM oder VAM und übergibt sie per USB-C oder Bluetooth an den ESP32-C5; der ESP32 ergänzt GeoNetworking, signiert (optional) und sendet über ITS-G5. 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_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_usb_transport">Aktiver Transport</string> <string name="settings_usb_transport">Aktiver Transport</string>
<string name="settings_transport_usbc">USB-C</string> <string name="settings_transport_usbc">USB-C</string>
<string name="settings_transport_wifi">WLAN</string> <string name="settings_transport_wifi">WLAN</string>
@@ -203,6 +197,7 @@
<string name="mqtt_no_topics">Noch keine Nachrichten</string> <string name="mqtt_no_topics">Noch keine Nachrichten</string>
<string name="mqtt_view_list">Liste</string> <string name="mqtt_view_list">Liste</string>
<string name="mqtt_view_topics">Topics</string> <string name="mqtt_view_topics">Topics</string>
<string name="mqtt_view_map">Karte</string>
<string name="mqtt_no_topics_hint">Mit der OBU verbinden und auf V2X-Verkehr warten</string> <string name="mqtt_no_topics_hint">Mit der OBU verbinden und auf V2X-Verkehr warten</string>
<string name="mqtt_no_messages">Noch keine Nachrichten zu diesem Thema</string> <string name="mqtt_no_messages">Noch keine Nachrichten zu diesem Thema</string>
@@ -318,18 +313,4 @@
<string name="v2x_spat_rx_title">Kreuzung %1$s · Station %2$d</string> <string name="v2x_spat_rx_title">Kreuzung %1$s · Station %2$d</string>
<string name="v2x_spat_group">SG%1$d</string> <string name="v2x_spat_group">SG%1$d</string>
<string name="v2x_spat_countdown">%1$.0f s</string> <string name="v2x_spat_countdown">%1$.0f s</string>
<string name="settings_esp32_link">ESP32-C5-Verbindung</string>
<string name="settings_esp32_link_ble">Bluetooth</string>
<string name="settings_esp32_link_ble_note">Beim ersten Verbinden wird das Koppeln mit micrOBU-XXXX angefragt: Passkey 123456 eingeben. Die Platine wirbt nur, solange ihr USB-C-Port nicht benutzt wird. BLE teilt sich das Funk-Frontend mit ITS-G5; der Einfluss auf den 5,9-GHz-Empfang ist noch nicht gemessen.</string>
<string name="settings_esp32_message">Senden während der Aufzeichnung</string>
<string name="settings_esp32_message_cam">CAM</string>
<string name="settings_esp32_message_vam">VAM</string>
<string name="settings_esp32_sign">Ausgehende Nachrichten signieren</string>
<string name="settings_esp32_sign_note">Signiert mit einer Demo-PKI, nicht der EU-Vertrauensliste: Empfänger, die dagegen prüfen, verwerfen diese Nachrichten. Aus sendet sie wie bisher unsigniert.</string>
<string name="conn_esp32_via_usb">über USB-C (nativer Port)</string>
<string name="conn_esp32_via_ble">über Bluetooth (Passkey 123456 beim ersten Koppeln)</string>
<string name="conn_esp32_cancel">Abbrechen</string>
<string name="conn_esp32_signing">Signieren %1$s · Tickets %2$d · signiert %3$d · abgelehnt %4$d · gesendet %5$d</string>
<string name="conn_esp32_signing_on">an</string>
<string name="conn_esp32_signing_off">aus</string>
</resources> </resources>
+4 -23
View File
@@ -143,18 +143,12 @@
<string name="gnss_no_fix">No GPS fix yet - move to an open area</string> <string name="gnss_no_fix">No GPS fix yet - move to an open area</string>
<string name="map_title">Location Map</string> <string name="map_title">Location Map</string>
<string name="map_location_label">Current Location</string> <string name="map_location_label">Current Location</string>
<string name="v2x_map_remote_count">%1$d tracked remote road user(s)</string>
<string name="v2x_map_own_label">Own (ego)</string> <string name="v2x_map_own_label">Own (ego)</string>
<string name="v2x_map_remote_plain">Remote #%1$d</string> <string name="v2x_map_remote_plain">Remote #%1$d</string>
<string name="v2x_map_remote_info">Remote #%1$d · Info</string> <string name="v2x_map_remote_info">Remote #%1$d · Info</string>
<string name="v2x_map_remote_awareness">Remote #%1$d · Awareness</string> <string name="v2x_map_remote_awareness">Remote #%1$d · Awareness</string>
<string name="v2x_map_remote_warning">Remote #%1$d · Warning</string> <string name="v2x_map_remote_warning">Remote #%1$d · Warning</string>
<string name="v2x_map_title">V2X Live Map</string>
<string name="v2x_map_back">Back</string>
<string name="v2x_map_follow">Centre on own position</string>
<string name="v2x_map_legend_cam">Road users (CAM)</string>
<string name="v2x_map_legend_denm">Hazards (DENM)</string>
<string name="v2x_map_legend_spat">Signals (SPATEM)</string>
<string name="v2x_map_spat_unlocated">%1$d signal(s) not shown - sender position unknown</string>
<!-- Settings --> <!-- Settings -->
<string name="settings_title">Settings</string> <string name="settings_title">Settings</string>
@@ -190,7 +184,7 @@
<string name="settings_obu_hardware">OBU Hardware</string> <string name="settings_obu_hardware">OBU Hardware</string>
<string name="settings_obu_hardware_cit_one">CiT One</string> <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">ESP32-C5</string>
<string name="settings_obu_hardware_esp32_note">The phone builds CAM or VAM and hands it to the ESP32-C5 over USB-C or Bluetooth; the ESP32 adds GeoNetworking, signs it (optional) and broadcasts it 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_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_usb_transport">Active transport</string> <string name="settings_usb_transport">Active transport</string>
<string name="settings_transport_usbc">USB-C</string> <string name="settings_transport_usbc">USB-C</string>
<string name="settings_transport_wifi">Wi-Fi</string> <string name="settings_transport_wifi">Wi-Fi</string>
@@ -204,6 +198,7 @@
<string name="mqtt_no_topics">No messages yet</string> <string name="mqtt_no_topics">No messages yet</string>
<string name="mqtt_view_list">List</string> <string name="mqtt_view_list">List</string>
<string name="mqtt_view_topics">Topics</string> <string name="mqtt_view_topics">Topics</string>
<string name="mqtt_view_map">Map</string>
<string name="mqtt_no_topics_hint">Connect to the OBU and wait for V2X traffic</string> <string name="mqtt_no_topics_hint">Connect to the OBU and wait for V2X traffic</string>
<string name="mqtt_no_messages">No messages on this topic yet</string> <string name="mqtt_no_messages">No messages on this topic yet</string>
@@ -264,7 +259,7 @@
<string name="mqtt_cam_pinger_active">Pinging - 1 CAM/s over the serial link</string> <string name="mqtt_cam_pinger_active">Pinging - 1 CAM/s over the serial link</string>
<string name="mqtt_cam_pinger_sent_count">Sent: %1$d</string> <string name="mqtt_cam_pinger_sent_count">Sent: %1$d</string>
<string name="mqtt_cam_pinger_send_failures">Write failures: %1$d consecutive - CAMs are not reaching the ESP32</string> <string name="mqtt_cam_pinger_send_failures">Write failures: %1$d consecutive - CAMs are not reaching the ESP32</string>
<string name="mqtt_cam_pinger_fw_counters">ESP32: tx fail %1$d · oversize %2$d · crc err %3$d · rx queue drop %4$d</string> <string name="mqtt_cam_pinger_fw_counters">ESP32: tx fail %1$d · oversize %2$d · crc err %3$d</string>
<string name="mqtt_cam_pinger_loopback">Own TX heard back: %1$d frames · %2$d dBm</string> <string name="mqtt_cam_pinger_loopback">Own TX heard back: %1$d frames · %2$d dBm</string>
<string name="mqtt_cam_pinger_loopback_no_rssi">Own TX heard back: %1$d frames</string> <string name="mqtt_cam_pinger_loopback_no_rssi">Own TX heard back: %1$d frames</string>
<string name="mqtt_start_pinger">Start Pinger</string> <string name="mqtt_start_pinger">Start Pinger</string>
@@ -325,18 +320,4 @@
<!-- Phase A: Trip Review screen --> <!-- Phase A: Trip Review screen -->
<string name="trip_review_title">Trip Review</string> <string name="trip_review_title">Trip Review</string>
<string name="settings_esp32_link">ESP32-C5 link</string>
<string name="settings_esp32_link_ble">Bluetooth</string>
<string name="settings_esp32_link_ble_note">The first connection asks to pair with micrOBU-XXXX: enter passkey 123456. The board only advertises while nothing uses its USB-C port. BLE shares the radio front end with ITS-G5; its effect on 5.9 GHz reception has not been measured yet.</string>
<string name="settings_esp32_message">Transmit while recording</string>
<string name="settings_esp32_message_cam">CAM</string>
<string name="settings_esp32_message_vam">VAM</string>
<string name="settings_esp32_sign">Sign outgoing messages</string>
<string name="settings_esp32_sign_note">Signed with a demo PKI, not the EU trust list: receivers that verify against it will drop these messages. Off sends them unsigned, as before.</string>
<string name="conn_esp32_via_usb">via USB-C (native port)</string>
<string name="conn_esp32_via_ble">via Bluetooth (passkey 123456 on first pairing)</string>
<string name="conn_esp32_cancel">Cancel</string>
<string name="conn_esp32_signing">Signing %1$s · tickets %2$d · signed %3$d · refused %4$d · on air %5$d</string>
<string name="conn_esp32_signing_on">on</string>
<string name="conn_esp32_signing_off">off</string>
</resources> </resources>
@@ -166,21 +166,6 @@ class CamTxPvSerialTest {
assertFalse(EspLinkStatus.parse("0000000000000002".hexToBytes())!!.supportsCamTxPv) assertFalse(EspLinkStatus.parse("0000000000000002".hexToBytes())!!.supportsCamTxPv)
} }
@Test
fun `firmware that predates the rx queue drop counter reports zero`() {
// 8-byte heartbeat (status + counters + capabilities, no rx queue drops tail).
val status = EspLinkStatus.parse("0000000000000001".hexToBytes())!!
assertEquals(0, status.rxQueueDrops)
}
@Test
fun `rx queue drops are read little-endian from the 10-byte payload`() {
// status=0, oversize=0, txFail=0, rxCrc=0, capabilities=0x01, rxQueueDrops=0x0102 (LE: 02 01)
val status = EspLinkStatus.parse("00000000000000010201".hexToBytes())!!
assertEquals(0x0102, status.rxQueueDrops)
assertTrue(status.supportsCamTxPv)
}
private val mac = "024d49435230".hexToBytes() private val mac = "024d49435230".hexToBytes()
private fun vectorAt(tstMs: Long) = GnPositionVector( private fun vectorAt(tstMs: Long) = GnPositionVector(
@@ -1,119 +0,0 @@
package com.hawhamburg.micr0bu
import com.hawhamburg.micr0bu.data.transport.BtpDataRequest
import com.hawhamburg.micr0bu.data.transport.LinkMessage
import com.hawhamburg.micr0bu.data.transport.LinkOpcode
import com.hawhamburg.micr0bu.data.transport.LinkResult
import com.hawhamburg.micr0bu.data.transport.LinkSecurityProfile
import com.hawhamburg.micr0bu.data.transport.PotiUpdate
import com.hawhamburg.micr0bu.data.transport.StationConfigure
import com.hawhamburg.micr0bu.data.transport.StationInfo
import com.hawhamburg.micr0bu.data.transport.StationStatus
import com.hawhamburg.micr0bu.data.transport.credentialsSegment
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* Pins the phone side of the station-link message layer.
*
* ## Where the expected bytes come from
* The colleague's Python implementation of the same protocol, microbu-esp32c5
* station-link/python/microbu_link/messages.py (`encode_message` over each body's `encode`),
* run with the same field values. That module is what their phone emulator drives the firmware
* with, so an agreement here is agreement with a second, independent implementation.
*/
class StationLinkTest {
private fun String.hexToBytes(): ByteArray = chunked(2).map { it.toInt(16).toByte() }.toByteArray()
private fun ByteArray.hex(): String = joinToString("") { "%02x".format(it) }
@Test
fun `STATION_CONFIGURE matches the Python encoder`() {
val body = StationConfigure(stationType = 2, mid = "021122334455".hexToBytes()).encode()
assertEquals(
"0100070002010002112233445500b40014020205",
LinkMessage(LinkOpcode.STATION_CONFIGURE, 7, body).encode().hex(),
)
}
@Test
fun `POTI_UPDATE matches the Python encoder`() {
val body = PotiUpdate(
timestampMs = 717_254_800_123L, latTenMicroDeg = 535_546_667, lonTenMicroDeg = 100_223_889,
semiMajorCm = 486, semiMinorCm = 486, speedCms = 543, headingDeciDeg = 1234, pai = true,
).encode()
assertEquals(
"02000800fbb2b7ffa60000002bcbeb1f914bf905e601e60100000e000000001f02d204",
LinkMessage(LinkOpcode.POTI_UPDATE, 8, body).encode().hex(),
)
}
@Test
fun `BTP_DATA_REQUEST for a signed CAM matches the Python encoder`() {
val body = BtpDataRequest(
destinationPort = 2001, itsAid = 36, securityProfile = LinkSecurityProfile.SECURED,
permissions = "010000".hexToBytes(), flSdu = "0102030405".hexToBytes(),
).encode()
assertEquals(
"0300090001d1070000010102ffff000000000024000000030100000005000102030405",
LinkMessage(LinkOpcode.BTP_DATA_REQUEST, 9, body).encode().hex(),
)
}
@Test
fun `CREDENTIALS_PROVISION segment matches the Python encoder`() {
val body = credentialsSegment(totalLength = 695, offset = 240, segment = ByteArray(3) { 0xAB.toByte() })
assertEquals("04000a00b702f00003ababab", LinkMessage(LinkOpcode.CREDENTIALS_PROVISION, 10, body).encode().hex())
}
@Test
fun `STATUS from the Python encoder decodes field by field`() {
val message = LinkMessage.decode(
("8400341240e20100010800021122334455b80b49387a4c12eb00010b0000000c0000000d0000000e000000" +
"0f000000100000001100000012000000130000001400000015000000160000001700000018000000" +
"190000001a0000001b0000001c000000fbb2b7ffa6000000").hexToBytes(),
)
assertNotNull(message)
assertEquals(LinkOpcode.STATUS, message!!.opcode)
assertEquals(0x1234, message.sequence)
val status = StationStatus.decode(message.body)!!
assertEquals(123_456L, status.uptimeMs)
assertTrue(status.configured)
assertArrayEquals("b80b49387a4c12eb".hexToBytes(), status.identifier)
assertEquals(1, status.tickets)
assertEquals(11L, status.signedMessages)
assertEquals(12L, status.refusedNoTicket)
assertEquals(13L, status.refusedChangePending)
assertEquals(14L, status.refusedPermission)
assertEquals(15L, status.signFailed)
assertEquals(16L, status.verified)
assertEquals(17L, status.rejected)
assertEquals(18L, status.requestsAccepted)
assertEquals(19L, status.requestsRefused)
assertEquals(21L, status.radioSubmitted)
assertEquals(22L, status.radioFailed)
assertEquals(23L, status.radioReceived)
assertEquals(24L, status.radioDropped)
assertEquals(26L, status.linkCrcErrors)
assertEquals(27L, status.linkMalformed)
assertEquals(28L, status.potiUpdates)
assertEquals(717_254_800_123L, status.itsTimeMs)
}
@Test
fun `a STATUS of the wrong length is refused, as the Python decoder does`() {
assertNull(StationStatus.decode(ByteArray(StationStatus.SIZE + 1)))
}
@Test
fun `RESULT of STATION_CONFIGURE carries the credential state`() {
val message = LinkMessage.decode("8000070000120800021122334455b80b49387a4c12eb0101".hexToBytes())!!
val result = LinkResult.decode(message.body)!!
assertTrue(result.accepted)
assertEquals(StationInfo(credentialsLoaded = true, tickets = 1), StationInfo.decode(result.detail))
}
}
@@ -1,60 +0,0 @@
package com.hawhamburg.micr0bu
import com.hawhamburg.micr0bu.domain.vam.VamGenerationRules
import com.hawhamburg.micr0bu.domain.vam.VamGenerationRules.Kinematics
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
/** TS 103 300-3 clause 6.4 items 1 to 4, with the Table 16/17 values. */
class VamGenerationRulesTest {
private val here = Kinematics(53.5546667, 10.0223889, speedMps = 3.0, headingDeg = 90.0)
private fun sentAt(ms: Long, k: Kinematics = here) = VamGenerationRules().apply { onSent(ms, k, withLowFrequency = true) }
@Test
fun `the first VAM is always due`() {
assertTrue(VamGenerationRules().due(0, here))
}
@Test
fun `nothing is due within T_GenVamMin even after a big jump`() {
assertFalse(sentAt(1_000).due(1_050, here.copy(latitude = here.latitude + 0.001)))
}
@Test
fun `a stationary VRU gets one VAM every T_GenVamMax`() {
val rules = sentAt(1_000)
assertFalse(rules.due(5_900, here))
assertTrue(rules.due(6_001, here))
}
@Test
fun `position, speed and heading changes trigger past their thresholds only`() {
val rules = sentAt(1_000)
// ~3.3 m north: under 4 m. ~5.6 m: over.
assertFalse(rules.due(2_000, here.copy(latitude = here.latitude + 0.00003)))
assertTrue(rules.due(2_000, here.copy(latitude = here.latitude + 0.00005)))
assertFalse(rules.due(2_000, here.copy(speedMps = 3.4)))
assertTrue(rules.due(2_000, here.copy(speedMps = 3.6)))
assertFalse(rules.due(2_000, here.copy(headingDeg = 93.0)))
assertTrue(rules.due(2_000, here.copy(headingDeg = 95.0)))
}
@Test
fun `heading change is measured the short way round north`() {
val rules = sentAt(1_000, here.copy(headingDeg = 358.0))
assertFalse(rules.due(2_000, here.copy(headingDeg = 1.0))) // 3 degrees across north
assertTrue(rules.due(2_000, here.copy(headingDeg = 3.0))) // 5 degrees
}
@Test
fun `the low-frequency container goes with the first VAM, then every T_GenVamLFMin`() {
val rules = VamGenerationRules()
assertTrue(rules.includeLowFrequency(0))
rules.onSent(0, here, withLowFrequency = true)
assertFalse(rules.includeLowFrequency(1_999))
assertTrue(rules.includeLowFrequency(2_000))
}
}
@@ -1,59 +0,0 @@
package com.hawhamburg.micr0bu
import com.hawhamburg.micr0bu.domain.asn1.VamContent
import com.hawhamburg.micr0bu.domain.asn1.VamUperCodec
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Pins the VAM encoder against bytes this code did not produce.
*
* ## Where the expected bytes come from
* asn1tools 0.167, compiling the ETSI modules vanetza-idf ships (asn1/release2:
* TS102894-2v241-CDD.asn, TS103300-3v231/VAM-PDU-Descriptions.asn and its motorcyclist container),
* encoding the same values as a Python dict: the same toolchain the colleague's reference VBS
* (microbu-esp32c5/station-link/python/microbu_link/vbs.py) builds its VAMs with. Station
* 0x12345678, fix at Unix ms 1790170000123 (generationDeltaTime 45819), 53.5546667 N
* 10.0223889 E, 5.43 m/s, heading 123.4 deg, -1.26 m/s^2; every confidence and the altitude
* unavailable, as the encoder sends them.
*/
class VamUperCodecTest {
private fun ByteArray.hex(): String = joinToString("") { "%02x".format(it) }
private fun content(includeLowFrequency: Boolean, accuracyM: Float? = 3.0f) = VamContent(
stationId = 0x12345678,
timestamp = 1_790_170_000_123L,
latitude = 53.5546667,
longitude = 10.0223889,
accuracyM = accuracyM,
speedMps = 5.43,
headingDeg = 123.4,
accelerationMps2 = -1.26,
includeLowFrequency = includeLowFrequency,
)
@Test
fun `VAM with the low-frequency container matches asn1tools`() {
assertEquals(
"031012345678b2fb400aac85a15b8a18ec88f30f3708eddd0f8002697e087ff24f322220",
VamUperCodec.encode(content(includeLowFrequency = true)).hex(),
)
}
@Test
fun `VAM without the low-frequency container matches asn1tools`() {
assertEquals(
"031012345678b2fb000aac85a15b8a18ec88f30f3708eddd0f8002697e087ff24f30",
VamUperCodec.encode(content(includeLowFrequency = false)).hex(),
)
}
@Test
fun `unknown accuracy encodes the confidence ellipse as unavailable`() {
assertEquals(
"031012345678b2fb000aac85a15b8a18ec8fffffff08eddd0f8002697e087ff24f30",
VamUperCodec.encode(content(includeLowFrequency = false, accuracyM = null)).hex(),
)
}
}
+102
View File
@@ -0,0 +1,102 @@
# Sniffer board and capture tooling
How to put an ESP32-C5 on the ITS-G5 channel as a passive sniffer, pull its captures onto this
PC, and check what is on air. The sniffer firmware itself is the third-party
`its-g5-receiver-firmware` checkout beside this repo; only the tooling and these notes are ours.
| File | What it does |
|---|---|
| `live_capture.py` | Streams the device's captures into a growing `.pcap` while it runs. The usual choice. |
| `dump_pcap.py` | Pulls one capture out of the device's in-memory buffer after the fact. |
| `../obu-firmware/test/pcap_gn_tally.py` | Tallies GeoNetworking headers per station over a `.pcap`. |
One-time: `pip install pyserial` (present in Python 3.11 on the bench PC, so `py -3.11` works).
## Which port
The sniffer firmware's console, and with it the pcap stream, goes out **UART0** - the board's
USB-bridge port (a CH343, its own COM number), not the native USB-C port. A board with only one
USB-C port cannot be used as a sniffer for this reason. On the bench this has been COM5 and, after
a re-enumeration, COM8.
## Live capture (preferred)
```powershell
cd capture
py -3.11 live_capture.py COM8
```
It writes `recordings/capture_<timestamp>.pcap` next to itself, flushing after every packet, so
the file can be read while it grows. Stop it with Ctrl+C. Use `-o <dir>` to write elsewhere;
`recordings/` is gitignored, since captures are large and are data rather than source. Captures
taken before 2026-09-14 are still in `its-g5-receiver-firmware/recordings/`; the host tests read
both directories.
### The CR insertion, and why captures used to be corrupt
ESP-IDF's newlib console converts LF to CRLF on its way out, and that applies to every `0x0a` byte
of the **binary** pcap stream, not only to log text. Each inserted CR shifts everything after it,
so pcap record headers and captured frames alike come out corrupt, and the file stops being
parseable at the first occurrence.
Measured on 2026-09-14: a 787 KB capture parsed cleanly for only 82 of about 2000 records, and
DENMs appeared on nonsense BTP ports because their payloads contain `0x0a` often. `undo_crlf()` in
`live_capture.py` reverses it on the raw stream before any framing, which is exact; afterwards a
capture parsed to EOF and DENMs read as port 2002 again.
**Captures taken before 2026-09-14 are truncated at their first corrupted record.** Anything
measured from them is worth re-checking. `dump_pcap.py` reads the same console and has not been
given the same treatment yet.
## Checking a capture
```powershell
py -3.11 ..\obu-firmware\test\pcap_gn_tally.py recordings\capture_<timestamp>.pcap
```
One row per station, packet type, BTP port and GN lifetime. For the messages themselves, decode
the payloads with `asn1tools` against the modules in `../asn1/` and re-encode them: identical bytes
mean the message was read exactly, wrong bytes mean it was not. `obu-firmware/test/check_replay.py`
does this over a whole capture.
## Flashing the sniffer firmware
From the receiver checkout, with its **pinned** ESP-IDF (not the global 5.5.4 install):
```powershell
cd its-g5-receiver-firmware
git submodule update --init --recursive
.\esp-idf\install.bat
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\esp-idf\export.ps1
idf.py set-target esp32c5
idf.py -p COM8 -b 921600 flash
```
Its `sdkconfig` for a bare board (nothing wired) needs SPI Ethernet off, and the pcap destination
set to Memory, both under `idf.py menuconfig`. Wired variants have ready-made configs in that
checkout: `sdkconfig.proto-w5500`, `sdkconfig.proto-enc28j60`, `sdkconfig.proto-spi-eppp`.
To reflash a board that already has a built image, without a toolchain terminal:
```powershell
cd its-g5-receiver-firmware\build
C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe -m esptool --chip esp32c5 -p COM8 -b 921600 write_flash --flash_mode dio --flash_freq 80m --flash_size 16MB 0x2000 bootloader/bootloader.bin 0x8000 partition_table/partition-table.bin 0x1e000 ota_data_initial.bin 0x20000 its-g5-receiver-firmware.bin
```
## Pulling a capture after the fact
Only for the Memory destination, and the buffer is small (`SNIFFER_PCAP_MEMORY_SIZE`, 4096 bytes
by default) - a smoke test, not a session. In the device console (`idf.py -p COM8 monitor`, exit
with Ctrl+T then Ctrl+X):
```
sniffer -P
sniffer --stop
```
Then, with the port free:
```powershell
py -3.11 dump_pcap.py COM8
```
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""
Pulls a capture off the ITS-G5 receiver's in-memory pcap buffer over the existing USB serial
connection and saves it as a real .pcap file on this machine.
Requires the firmware to be built with:
Example Configuration -> Select destination to store pcap file -> Memory
Usage (typical):
1. Close idf.py monitor (only one program can hold the COM port at a time).
2. Run a capture on the device: `sniffer -P` ... let it run ... `sniffer --stop`
3. python dump_pcap.py COM5
The device has no access to this computer's filesystem, so it can't write here directly. Instead,
`pcap --dump` streams the raw pcap bytes back over the same serial link, wrapped in plain-text
markers ("===PCAP-DUMP-START:<len>===" ... raw bytes ... "===PCAP-DUMP-END==="). This script finds
those markers and writes just the raw bytes out as a .pcap file.
Install dependency once: pip install pyserial
"""
import argparse
import datetime
import re
import sys
try:
import serial
except ImportError:
print("Missing dependency. Install it with: pip install pyserial", file=sys.stderr)
sys.exit(1)
START_RE = re.compile(rb"===PCAP-DUMP-START:(\d+)===\n")
END_MARKER = b"\n===PCAP-DUMP-END===\n"
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("port", help="Serial port the device is on, e.g. COM5")
parser.add_argument("-b", "--baud", type=int, default=115200, help="Baud rate (default: 115200)")
parser.add_argument("-o", "--outdir", default="recordings", help="Output directory (default: ./recordings)")
parser.add_argument("-t", "--timeout", type=float, default=15.0, help="Seconds to wait for the dump to start")
args = parser.parse_args()
import os
os.makedirs(args.outdir, exist_ok=True)
print(f"Opening {args.port} @ {args.baud}...")
with serial.Serial(args.port, args.baud, timeout=1) as ser:
# Nudge the console in case there's stale input, then request the dump.
ser.reset_input_buffer()
ser.write(b"\r\n")
ser.write(b"pcap -f dump --dump\r\n")
print("Waiting for dump to start...")
buf = b""
match = None
deadline = datetime.datetime.now() + datetime.timedelta(seconds=args.timeout)
while datetime.datetime.now() < deadline:
chunk = ser.read(256)
if chunk:
buf += chunk
match = START_RE.search(buf)
if match:
break
if not match:
print("Timed out waiting for '===PCAP-DUMP-START:...===' marker.\n"
"Check that: the firmware is built with the Memory pcap destination, a capture was\n"
"actually taken ('sniffer -P' then 'sniffer --stop'), and no other program (like\n"
"idf.py monitor) is holding the serial port open.", file=sys.stderr)
sys.exit(1)
length = int(match.group(1))
print(f"Dump starting, {length} bytes expected.")
# Anything after the marker in our buffer is already part of the payload.
payload = buf[match.end():]
remaining = length - len(payload)
while remaining > 0:
chunk = ser.read(min(remaining, 4096))
if not chunk:
print(f"Serial read timed out with {remaining} bytes still missing.", file=sys.stderr)
sys.exit(1)
payload += chunk
remaining -= len(chunk)
# Drain (and sanity-check) the trailing end marker, but don't fail hard if it's not exact.
tail = ser.read(len(END_MARKER))
if tail != END_MARKER:
print("Warning: end marker didn't match exactly - payload may still be fine.", file=sys.stderr)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
outpath = os.path.join(args.outdir, f"capture_{timestamp}.pcap")
with open(outpath, "wb") as f:
f.write(payload)
print(f"Saved {len(payload)} bytes to {outpath}")
if __name__ == "__main__":
main()
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""
Continuously listens on the ITS-G5 receiver's serial console and writes every captured packet into a
live-growing .pcap file, with no console commands needed on the device side.
The firmware streams every packet it captures out over the same serial connection the console runs on,
automatically, as soon as the sniffer is running (which happens on boot by default). Each packet is framed
with plain-text markers so this script can pull the binary pcap bytes out of the stream even though
regular log lines are interleaved with it:
===PCAP-LIVE-HEADER:<len>===\\n<24 raw bytes>\\n (sent once, the pcap global header)
===PCAP-LIVE-PKT:<len>===\\n<raw bytes>\\n (sent once per captured packet)
Usage:
python live_capture.py COM5
Runs until you press Ctrl+C. Writes to recordings/capture_<timestamp>.pcap, flushing after every packet
so you can open the file in Wireshark while it's still being written (use "File > Open" again, or
Wireshark's own "Follow" won't auto-refresh but re-opening will show the latest packets).
Install dependency once: pip install pyserial
"""
import argparse
import datetime
import os
import re
import sys
import time
try:
import serial
except ImportError:
print("Missing dependency. Install it with: pip install pyserial", file=sys.stderr)
sys.exit(1)
# \r? because the ESP console emits CRLF: on Windows the markers arrive as
# "===PCAP-LIVE-PKT:310===\r\n", which never matched a bare \n and left the capture silently
# empty while the device was streaming perfectly well.
HEADER_RE = re.compile(rb"===PCAP-LIVE-HEADER:(\d+)===\r?\n")
PKT_RE = re.compile(rb"===PCAP-LIVE-PKT:(\d+)===\r?\n")
LINKTYPE_ETHERNET = 1
LINKTYPE_IEEE802_11_RADIOTAP = 127
def mac_str(b):
return ":".join(f"{x:02x}" for x in b)
def build_default_pcap_header(link_type):
"""Synthesizes the same 24-byte global pcap header the firmware would have sent, for when we
connect after the device's one-time header already went out (see the race note in main())."""
header = bytearray(24)
header[0:4] = bytes([0xD4, 0xC3, 0xB2, 0xA1]) # magic (LE bytes of 0xA1B2C3D4)
header[4:6] = (2).to_bytes(2, "little") # major version
header[6:8] = (4).to_bytes(2, "little") # minor version
header[16:20] = (0x40000).to_bytes(4, "little") # snaplen
header[20:24] = link_type.to_bytes(4, "little")
return bytes(header)
def summarize_packet(link_type, record_bytes, index):
"""Best-effort human-readable one-line summary of a captured packet, for live feedback.
record_bytes is the raw 16-byte pcap record header followed by the captured frame."""
seconds = int.from_bytes(record_bytes[0:4], "little")
microseconds = int.from_bytes(record_bytes[4:8], "little")
cap_len = int.from_bytes(record_bytes[8:12], "little")
frame = record_bytes[16:]
ts = f"{seconds}.{microseconds:06d}"
if link_type == LINKTYPE_IEEE802_11_RADIOTAP and len(frame) >= 24:
radiotap_len = int.from_bytes(frame[2:4], "little")
rssi = frame[8] - 256 if frame[8] >= 128 else frame[8]
station_id = int.from_bytes(frame[16:24], "little")
mac_frame = frame[radiotap_len:]
if len(mac_frame) >= 16:
dst = mac_str(mac_frame[4:10])
src = mac_str(mac_frame[10:16])
else:
dst = src = "?"
station = f"{station_id:012x}" if station_id else "unknown"
return (f"#{index:<5} [{ts}] len={cap_len:<5} rssi={rssi:>4}dBm "
f"station={station} {src} -> {dst}")
if link_type == LINKTYPE_ETHERNET and len(frame) >= 14:
dst = mac_str(frame[0:6])
src = mac_str(frame[6:12])
ethertype = int.from_bytes(frame[12:14], "big")
return f"#{index:<5} [{ts}] len={cap_len:<5} eth {src} -> {dst} type=0x{ethertype:04x}"
return f"#{index:<5} [{ts}] len={cap_len:<5} (unrecognized frame format)"
def undo_crlf(chunk, state):
"""Undo the CR the device console inserts before every LF.
ESP-IDF's newlib console converts LF to CRLF on its way out, and that happens to every 0x0A
byte of the binary pcap stream too, not only to log text. Each inserted CR shifts everything
after it, so pcap record headers and captured frames alike come out corrupt. This is the
"byte inserted mid-frame" seen in older recordings; with DENM traffic on air it wrecks most
of a capture (measured 2026-09-14: a 787 KB file parsed cleanly for only 82 records).
Dropping one CR immediately before each LF undoes it exactly, provided it is done on the raw
stream before any framing and a trailing CR is carried across read boundaries. CR and LF are
written as byte values here so the transformation cannot be confused with an escape.
"""
CR, LF = bytes([13]), bytes([10])
if state["pending_cr"]:
chunk = CR + chunk
state["pending_cr"] = False
if chunk.endswith(CR):
chunk = chunk[:-1]
state["pending_cr"] = True
return chunk.replace(CR + LF, LF)
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("port", help="Serial port the device is on, e.g. COM5")
parser.add_argument("-b", "--baud", type=int, default=115200, help="Baud rate (default: 115200)")
parser.add_argument("-o", "--outdir", default="recordings", help="Output directory (default: ./recordings)")
args = parser.parse_args()
os.makedirs(args.outdir, exist_ok=True)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
outpath = os.path.join(args.outdir, f"capture_{timestamp}.pcap")
print(f"Opening {args.port} @ {args.baud}...")
print(f"Writing live capture to {outpath}")
print("Press Ctrl+C to stop.")
header_written = False
link_type = None
packet_count = 0
buf = b""
total_bytes = 0
last_status = time.monotonic()
printed_raw_preview = False
crlf_state = {"pending_cr": False}
with serial.Serial(args.port, args.baud, timeout=1) as ser, open(outpath, "wb") as outfile:
def read_bytes(n):
return undo_crlf(ser.read(n), crlf_state)
try:
while True:
chunk = read_bytes(256)
if chunk:
buf += chunk
total_bytes += len(chunk)
now = time.monotonic()
if now - last_status >= 2:
last_status = now
print(f"[diagnostic] {total_bytes} raw bytes received so far, "
f"{packet_count} packets recognized, header_written={header_written}")
if total_bytes > 0 and not printed_raw_preview and not header_written and not PKT_RE.search(buf):
# We're getting bytes but none of them look like our markers - show a preview
# so we can tell whether this is plain log text (markers just haven't shown up
# yet), garbage (baud/port mismatch), or something else entirely.
preview = buf[:200]
print(f"[diagnostic] no markers matched yet - raw preview: {preview!r}")
printed_raw_preview = True
elif total_bytes == 0:
print("[diagnostic] zero bytes received from the port at all - this points at "
"the wrong COM port, another program holding the port, or a port that "
"isn't actually wired to the console/sniffer output.")
# The device only sends the global header once, right when the sniffer first starts
# (typically within a second or two of boot). If this script connects even slightly
# late - very likely right after a fresh flash, since esptool itself resets the board -
# that header is already gone before we ever see it. Rather than blocking forever
# waiting for a header that's never coming, look for whichever marker shows up first.
header_match = None if header_written else HEADER_RE.search(buf)
pkt_match = PKT_RE.search(buf)
if header_match and (not pkt_match or header_match.start() < pkt_match.start()):
length = int(header_match.group(1))
buf = buf[header_match.end():]
while len(buf) < length:
buf += read_bytes(length - len(buf))
header_bytes = buf[:length]
outfile.write(header_bytes)
outfile.flush()
buf = buf[length:]
header_written = True
if length >= 24:
link_type = int.from_bytes(header_bytes[20:24], "little")
print(f"Got pcap global header (link type {link_type}) - device is streaming.\n")
continue
if not header_written and pkt_match:
link_type = LINKTYPE_IEEE802_11_RADIOTAP
outfile.write(build_default_pcap_header(link_type))
outfile.flush()
header_written = True
print("Note: missed the device's one-time pcap header (it was likely sent before "
"this script connected, e.g. right after a flash/reset) - assuming WLAN "
"radiotap capture and writing a default header instead.\n")
# fall through and process pkt_match below, don't discard this packet
if not pkt_match:
# Keep the buffer from growing unbounded while waiting for a marker, but don't
# discard anything - a marker could be split across reads.
if len(buf) > 65536:
buf = buf[-4096:]
continue
length = int(pkt_match.group(1))
buf = buf[pkt_match.end():]
while len(buf) < length:
buf += read_bytes(length - len(buf))
record_bytes = buf[:length]
outfile.write(record_bytes)
outfile.flush()
buf = buf[length:]
packet_count += 1
print(summarize_packet(link_type, record_bytes, packet_count))
except KeyboardInterrupt:
print(f"\nStopped. {packet_count} packets saved to {outpath}")
if __name__ == "__main__":
main()
-141
View File
@@ -1,141 +0,0 @@
# 06 – Signed ITS messages, VAM and the BLE link (2026-09-23)
What changed when the ESP32-C5 OBU moved onto the colleague's vanetza-idf station, how the pieces
fit together, how it was verified, and what is still open. Hardware checks still to do are in
`TODO.md` ("Signed-TX firmware ...").
## Summary
- **Signing lives on the ESP32-C5.** The authorization ticket's private key is in the board's NVS;
vanetza-idf's security entity signs every secured message there (IEEE 1609.2 / ETSI TS 103 097,
ECDSA NIST P-256). The phone never holds a key and never signs.
- **The phone decides what to send and when.** It builds CAM or VAM (UPER) from its own GNSS/IMU,
hands each message to the board with the flag "signed" or "unsigned", and keeps the board's clock
and position current.
- **Two links, one protocol.** USB-C (native USB Serial/JTAG) or Bluetooth LE, chosen in Settings.
Both carry the colleague's station-link protocol v1 plus one MicrOBU extension for reception.
- **Reception is unchanged for the app.** Every ITS message heard on air reaches the phone, signed
or not, verifiable or not, exactly as with the previous firmware.
- **Demo PKI, not the EU trust list.** Signed messages carry a throwaway chain. Receivers that
verify against the EU trust list drop them; unsigned sending remains available.
## Who does what
| | Phone (app) | ESP32-C5 (obu-firmware) |
|---|---|---|
| CAM / VAM content and UPER encoding | yes | – |
| Send cadence (CAM 1 Hz baseline; VAM per TS 103 300-3 clause 6.4) | yes | – |
| Pseudonym (station ID + MAC, rotated together) | yes | uses the MAC it is configured with |
| Time and position (PoTi) | yes, per new GNSS fix | keeps an ITS clock from it |
| GeoNetworking + BTP headers | – | yes |
| Signing (TS 103 097), certificate handling | – | yes |
| Credentials | ships the demo bundle, provisions it once | stores it in NVS |
| 802.11p radio at 5 900 MHz | – | yes |
| Reception: unwrap GN/BTP, forward | decodes CAM / DENM / SPATEM | yes (all frames) |
## Firmware (obu-firmware)
obu-firmware is now a port of `microbu-esp32c5/firmware`: the colleague's repository, of which
this repository keeps a copy in `microbu-esp32c5/` (their commit cf4b99f plus our V2X2MAP signature
verification; nothing is pushed to their repository). vanetza-idf is taken from
`microbu-esp32c5/external/vanetza-idf`. It builds
with **ESP-IDF 6.0.2 only**: the raw-TX path uses private Wi-Fi driver structures that vanetza-idf
pins to that version. The previous C firmware (IDF 6.1) is backed up as a full flash image in
`firmware-backups/` (gitignored, restore command in its README.txt); its sources stay on disk,
unbuilt. Setup and flashing: `obu-firmware/FLASHING.md`. Design notes and every deviation from the
colleague's code (`MicrOBU:` in the sources): `obu-firmware/NOTES.md`.
Main changes against the colleague's firmware:
- **Raw receive path kept.** vanetza-idf decapsulates strictly and would drop unsigned frames (the
bench car) and anything not signed under the demo root (every RSU). Every captured frame also
goes through the previous firmware's `gn_unwrap.c` and reaches the phone as link opcode
`V2X_RX` (0x85), whose body is the old `SERIAL_MSG_V2X_RX` payload.
- **Unsigned sending kept.** The colleague's station refuses unsecured requests; here they go out
with the previous firmware's `geonet.c` header.
- **Console on UART0** (CH343, COM3 on the bench); the native USB port carries only link frames.
- **BLE pauses advertising while USB is in use** (BLE and ITS-G5 share one RF front end).
- **NVS 80 KB instead of 24 KB**, app at 0x20000. At 24 KB the BLE bond could not be stored and the
phone had to pair on every connection.
- Fixes found on the bench: radio queue drained before the first PoTi (no RX, ~177 queue drops
before); station loop waited 0 ticks at 100 Hz and starved the idle task; 2.4 KB RX buffer moved
off the Wi-Fi task stack; no silent truncation of BLE notifications; ATT MTU 517; serial writes
skipped when no USB host is present.
## Link protocol
Station-link v1 (`microbu-esp32c5/station-link/README.md`): `[opcode][flags][sequence LE][body]`,
little-endian, at most 512 octets. Over USB each message is one `0xAA55` frame of type `0x10`
(the old framing and CRC). Over BLE each message is one GATT value on service
`0000C175-BA5E-4C17-8000-00805F9B34FB` (the README describes a different, Nordic-UART layout; the
firmware is what counts).
| Direction | Message | Used for |
|---|---|---|
| phone → board | `STATION_CONFIGURE` | pseudonym MAC, station type, channel 180, 20 dBm; starts the radio |
| phone → board | `CREDENTIALS_PROVISION` | the demo bundle, once, when the board reports no ticket |
| phone → board | `POTI_UPDATE` | position and ITS time, once per new GNSS fix |
| phone → board | `BTP_DATA_REQUEST` | one CAM (port 2001, psid 36) or VAM (port 2018, psid 638), signed or not |
| board → phone | `RESULT` | answer to a request |
| board → phone | `STATUS` | every second: counters, tickets, signed/refused counts |
| board → phone | `V2X_RX` (0x85, MicrOBU) | every ITS message heard on air |
The app side is `Esp32Link.kt` (session), `StationLink.kt` (codec, pinned by unit tests to bytes
from the colleague's Python implementation), `UsbSerialTransport.kt` and `BleLinkTransport.kt`.
A board still on the previous firmware is recognised by its old heartbeat and keeps working for
CAM over USB.
## Redundancy and recovery
| Situation | What notices | What happens |
|---|---|---|
| USB link dead (board hung, cable) | app watchdog: no frame for 3.5 s (the board's `STATUS` comes every second) | link marked ERROR on the card |
| USB unplugged | Android detach broadcast | port closed; Connect again after re-plugging |
| BLE link lost | BLE supervision timeout (4 s) | app reconnects by itself: 1 s after a drop, then backing off to 30 s if attempts fail |
| Board reset / power cycle | first `STATUS` says "not configured" | app reconfigures (and re-provisions if needed) without user action |
| App closed and reopened | new session | app configures the board again; BLE reconnects with the stored bond, no passkey (confirmed) |
| Phone clock or GNSS time jumping | app tracks the board's clock | PoTi never moves it backwards (except a real correction of ≥ 60 s), so the board does not restart its stack |
| Board firmware wedged | ESP task watchdog (30 s, logs on COM3) | the phone sees it as a dead link (above) |
## App changes
- Settings > Connection > ESP32-C5: **link** USB-C / Bluetooth, **transmit** CAM / VAM, **sign
outgoing messages** (on by default). The connection card, top bar and dashboard show the link in
use, the pairing passkey when needed, and signing counters.
- VAM encoder (`VamUperCodec.kt`, TS 103 300-3 V2.3.1, checked against asn1tools) and the VAM
generation rules (`VamGenerationRules.kt`).
- `GnssTimeSource` keeps the last measured phone-clock error while GNSS time drops out indoors. The
bench phone's clock was 14 minutes fast; falling back to it made every transmitted timestamp
jump by 14 minutes.
- Bluetooth permissions (Android 12+) requested at start-up.
## Credentials (demo PKI)
`app/src/main/assets/demo-chain.vcr`, generated 2026-09-23 with the colleague's `vidf_issue`: root
`6E7D0374FB021901` → AA `B3312F29844299E0` → AT `B80B49387A4C12EB` (two years; psid 36 SSP `010000`,
psid 638 SSP `01`). It is throwaway and not EU-registered; its private key ships with the app on
purpose. The colleague's own demo chain only grants psid 638 and cannot sign CAMs.
## Verification
- **Unit tests** (103): VAM bytes against asn1tools, station-link messages against the colleague's
Python encoder, VAM generation rules.
- **Signatures on air**: `obu-firmware/test/verify_signed_pcap.py` checks a pcap with asn1tools
and OpenSSL, sharing no code with the firmware. Pinger capture of 2026-09-23: 12/12 signed CAMs,
psid 36, signer the demo AT, all signatures valid, chain valid.
- **Third-party stack**: the CiT One receives the signed CAMs (~1 Hz on `v2x/rx/cam`), so its
stack unwraps our envelope. Its MQTT interface exposes no security information, and it forwards
unsigned and unknown-root messages alike, so it cannot tell whether it verified the signature.
- **V2X2MAP (COM10)**: now the colleague's v2x2map 0.3.0 bridge from source with a new
`verify.py` and `--trust demo-chain.vcr`; it shows "signature verified", "SIGNATURE INVALID" or
"not verified" per packet. Signed CAMs and signed VAMs verified live; a one-bit change in a
signed CAM comes out invalid. Launcher: `micrOBU_workspace/v2x-obu-esp32c5/start-v2x2map-signed.bat`.
## Open
- BLE/ITS-G5 coexistence is not measured: does an active BLE connection cost 5.9 GHz reception?
- `time_regression` standing still indoors for several minutes, and board reset recovery over BLE,
after the last fixes.
- The signature's generationTime follows the app's UTC-based `ItsTime`; the colleague's VBS adds
the 5 leap seconds (TAI). Which is right is the open question in `ItsTime.kt`.
- Real EU PKI enrolment/authorisation (TS 102 941) instead of the demo chain.
-43
View File
@@ -1,43 +0,0 @@
# --- ESP-IDF build output ---
firmware/build/
firmware/build-*/
firmware/managed_components/
firmware/.cache/
firmware/.vscode/
firmware/sdkconfig
firmware/sdkconfig.old
firmware/*.log
firmware/rf_characterization_output/
# --- Secrets: never commit real private key material ---
# (this repo only ever ships the disposable, non-registered demo chain)
*.vkey
*.ekey
private/
*_private_encrypted.pem
*_RCA_NEW_*/
# --- PKI reference-generator: regenerate locally, do not commit ---
# (vendored Rust crates for the offline/deterministic c-its build path; see
# pki/uml-l0-rca/reference-generator/README.md to rebuild with `cargo vendor`)
pki/uml-l0-rca/reference-generator/vendor/
pki/uml-l0-rca/reference-generator/_build_cits/
pki/uml-l0-rca/reference-generator/_cargo_vendor/
# --- Python ---
__pycache__/
*.pyc
*.egg-info/
.venv/
venv/
# --- v2x2map bridge local artifacts ---
tools/v2x2map-0.3.0/bridge/recordings/
tools/v2x2map-0.3.0/bridge/dist/
tools/v2x2map-0.3.0/bridge/build/
*.pcap
# --- OS / editor ---
.DS_Store
Thumbs.db
*.swp
-155
View File
@@ -1,155 +0,0 @@
# micrOBU ESP32-C5
A standalone ITS-G5 (802.11p) VRU (Vulnerable Road User) ITS-S station on the
ESP32-C5: firmware, the embedded [Vanetza](https://github.com/riebl/vanetza)
C-ITS protocol stack (`external/vanetza-idf/`, see
[PROVENANCE.md](external/vanetza-idf/PROVENANCE.md)), a phone-emulator
example for the station-internal link, a localhost PKI reference chain, a
Wireshark VAM dissector and the [V2X2MAP](https://github.com/711it/v2x2map)
receiver bridge.
A fresh clone can send a real, signed VAM (VRU Awareness Message) over the
air in a handful of commands — see [Send a signed VAM](#4-send-a-signed-vam-in-a-few-commands)
below. `station-link/python/demo-chain.vcr` is a disposable, **non-EU-registered**
test credential chain generated specifically for this purpose (see
[Security note on credentials](#security-note-on-credentials)); it carries no
real-world trust and is safe to ship.
## Quickstart
### 1. Build & flash the ESP32-C5 firmware
Requires [ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c5/get-started/) 6.0.2 with the `esp32c5` target.
```powershell
cd firmware
idf.py set-target esp32c5
idf.py build
idf.py -p COM<PORT> flash monitor
```
### 2. Install the Wireshark VAM dissector
Wireshark decodes IEEE 1609.2 / ETSI TS 103 097 secured packets only down to
`unsecuredData` unless the PSID is registered in its dissector table. **PSID
638** (VRU Awareness Service, ETSI TS 102 965) is not registered by default
in Wireshark 4.x, so signed VAM traffic stops decoding at the security
envelope without this plugin.
```powershell
# Windows
Copy-Item tools\wireshark\psid-vru.lua "$env:APPDATA\Wireshark\plugins\"
```
```bash
# Linux
mkdir -p ~/.local/lib/wireshark/plugins && cp tools/wireshark/psid-vru.lua ~/.local/lib/wireshark/plugins/
# macOS
mkdir -p ~/.config/wireshark/plugins && cp tools/wireshark/psid-vru.lua ~/.config/wireshark/plugins/
```
Verify under **Help → About Wireshark → Plugins**, or use it directly with `tshark`:
```bash
tshark -X lua_script:tools/wireshark/psid-vru.lua -r capture.pcap
```
See [tools/wireshark/README.md](tools/wireshark/README.md) for details.
### 3. Install Python dependencies
```bash
pip install -r station-link/python/requirements.txt
pip install -r tools/v2x2map-0.3.0/bridge/requirements.txt
```
### 4. Send a signed VAM in a few commands
With the firmware flashed (step 1) and the ESP32-C5 connected over USB
Serial/JTAG, the phone emulator provisions the disposable demo credential
chain and starts a small VRU basic service that assembles and transmits
VAMs:
```bash
python station-link/python/phone_emulator.py \
--port COM<PORT> --bundle station-link/python/demo-chain.vcr \
--radio txrx --duration 30
```
That's it — the micrOBU signs every VAM with the demo AT ticket (VRU ITS-AID
638, `psid 638 ssp 01`) and transmits it over ITS-G5. Capture it with a
second ITS-G5-capable radio (or the [V2X2MAP bridge](#5-run-the-v2x2map-receiver-bridge-optional)
below) and decode it with the [Wireshark dissector](#2-install-the-wireshark-vam-dissector)
from step 2.
To provision over BLE instead of USB, or to mirror packets to a `.pcap`
without radiating, see the header of
[`phone_emulator.py`](station-link/python/phone_emulator.py) for the
`--ble`, `--radio off --divert --pcap` and full `--pki-*` (real online TS
102 941 enrolment/authorization) variants, and
[station-link/README.md](station-link/README.md) for the link protocol
itself.
### 5. Run the V2X2MAP receiver bridge (optional)
A second ESP32-C5 flashed with the receiver firmware in
[`tools/v2x2map-0.3.0/bridge/firmware/`](tools/v2x2map-0.3.0/bridge/firmware/)
can feed a live web dashboard:
```bash
python tools/v2x2map-0.3.0/bridge/its_g5_bridge.py --port COM<PORT> --dashboard-port 8080 --open-browser
```
Open `http://localhost:8080` if it doesn't open automatically. This tool
decodes VAMs for display but does **not** verify signatures (see
[tools/v2x2map-0.3.0/README.md](tools/v2x2map-0.3.0/README.md)).
## Repository layout
| Path | Contents |
|---|---|
| `firmware/` | ESP-IDF firmware project for the ESP32-C5 VRU ITS-S |
| `external/vanetza-idf/` | Vanetza C-ITS stack + ESP-IDF port (upstream provenance in [PROVENANCE.md](external/vanetza-idf/PROVENANCE.md)) |
| `station-link/` | Station-internal link protocol, Python client library, phone emulator |
| `pki/` | Localhost PKI reference chain (root/AA/AT tooling); see [security note](#security-note-on-credentials) |
| `tools/wireshark/` | PSID 638 (VRU) Wireshark Lua dissector |
| `tools/v2x2map-0.3.0/` | Vendored [V2X2MAP](https://github.com/711it/v2x2map) receiver bridge and live dashboard |
## Security note on credentials
`pki/uml-l0-rca/` documents the tooling for a **real, EU CCMS L0 ECTL-registered**
root CA used elsewhere in the wider micrOBU project. Its private key material
is intentionally **not** in this repository — `.gitignore` also backstops
this (`*.vkey`, `*.ekey`, `private/`).
`station-link/python/demo-chain.vcr` is unrelated: a separate, throwaway,
**non-registered** root/AA/AT chain generated specifically for this repo's
quickstart with `pki/uml-l0-rca/bin/windows/vidf_issue.exe`. Its
`HashedId8` values do not match the registered root, it grants no real-world
trust, and regenerating it is safe:
```powershell
$pool = "<some scratch directory>"
$exe = "pki\uml-l0-rca\bin\windows\vidf_issue.exe"
openssl ecparam -name prime256v1 -genkey -noout -out "$pool\demo_root_key.pem"
& $exe root --key "$pool\demo_root_key.pem" --name "Demo Root (NOT REGISTERED)" --id DEMO_RCA --out $pool --years 10
& $exe authority --issuer "$pool\DEMO_RCA.oer" --issuer-key "$pool\demo_root_key.pem" --name "Demo AA" --id DEMO_AA --out $pool --years 5
& $exe ticket --issuer "$pool\DEMO_AA.oer" --issuer-key "$pool\DEMO_AA.vkey" --id DEMO_AT --out $pool --hours 8760 --root "$pool\DEMO_RCA.oer"
python external\vanetza-idf\ports\esp_idf\tools\credential_bundle.py build `
--pool $pool --root DEMO_RCA --aa DEMO_AA --at DEMO_AT --out station-link\python\demo-chain.vcr
```
`pki/uml-l0-rca/reference-generator/` cross-validates certificate generation
against an independent Rust implementation ([`TheEnbyperor/c-its`](https://github.com/TheEnbyperor/c-its),
pinned commit in [`reference-generator/README.md`](pki/uml-l0-rca/reference-generator/README.md)).
Its vendored crates (`vendor/`) are excluded from this repository by
`.gitignore` for size; regenerate with `cargo vendor` from that directory's
`Cargo.lock`, or use `vidf_issue` directly as shown above — the vendor tree
is only needed for that independent cross-check, not for ordinary use.
## License / provenance
- Vanetza and its ESP-IDF port: BSD-3-Clause, see
[external/vanetza-idf/LICENSE.md](external/vanetza-idf/LICENSE.md) and
[external/vanetza-idf/PROVENANCE.md](external/vanetza-idf/PROVENANCE.md).
- V2X2MAP bridge: MIT, see
[tools/v2x2map-0.3.0/LICENSE](tools/v2x2map-0.3.0/LICENSE).
@@ -1,14 +0,0 @@
build
.cache/
build-*/
build-*.log
sdkconfig
sdkconfig.old
sdkconfig.*.old
managed_components/
dependencies.lock
__pycache__/
conanfile.pyc
doxygen/html/
www/
build.asn1/
-158
View File
@@ -1,158 +0,0 @@
if(ESP_PLATFORM)
include("${CMAKE_CURRENT_LIST_DIR}/ports/esp_idf/CMakeLists.txt")
return()
endif()
cmake_minimum_required(VERSION 3.14)
cmake_policy(VERSION 3.14...3.31)
project(Vanetza VERSION 26.02)
set(VANETZA_SOVERSION 0)
list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
# Look up threading library (usually pthread)
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
find_package(Threads MODULE)
# Build configuration options
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
option(BUILD_TESTS "Build unit tests" OFF)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(gtest)
endif()
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib/static)
include(GNUInstallDirs)
include(UseVanetza)
# Enable usage of targets' folder property (good for Visual Studio)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
# Find project dependencies
find_package(Boost 1.70 REQUIRED COMPONENTS date_time OPTIONAL_COMPONENTS program_options CONFIG)
vanetza_optional_dependency(GeographicLib 1.37 VANETZA_WITH_GEOGRAPHICLIB "Use GeographicLib for precise geodesy")
vanetza_optional_dependency(CapnProto 0.8 VANETZA_WITH_RPC "Enable RPC support")
vanetza_optional_dependency(CryptoPP 5.6.1 VANETZA_WITH_CRYPTOPP "Enable Crypto++ support")
vanetza_optional_dependency(OpenSSL 1.1.1 VANETZA_WITH_OPENSSL "Enable OpenSSL extensions")
option(VANETZA_WITH_PQC "Enable the experimental hybrid-PQC certificate profile" OFF)
if(VANETZA_WITH_PQC)
find_package(liboqs 0.14 CONFIG REQUIRED)
endif()
option(VANETZA_EMBED_COUNTRY_DATA "Embed country boundary data into geodesy library" OFF)
add_subdirectory(vanetza/access)
add_subdirectory(vanetza/asn1)
add_subdirectory(vanetza/btp)
add_subdirectory(vanetza/common)
add_subdirectory(vanetza/dcc)
add_subdirectory(vanetza/facilities)
add_subdirectory(vanetza/geodesy)
add_subdirectory(vanetza/geonet)
add_subdirectory(vanetza/gnss)
add_subdirectory(vanetza/net)
add_subdirectory(vanetza/rpc)
add_subdirectory(vanetza/security)
add_subdirectory(vanetza/units)
option(BUILD_SOCKTAP "Build socktap application" OFF)
if(BUILD_SOCKTAP)
add_subdirectory(tools/socktap)
endif()
option(BUILD_CERTIFY "Build certify application" OFF)
if(BUILD_CERTIFY)
add_subdirectory(tools/certify)
if(VANETZA_WITH_PQC)
add_subdirectory(tools/certify-pqc)
endif()
endif()
option(BUILD_BENCHMARK "Build benchmark application" OFF)
if(BUILD_BENCHMARK)
add_subdirectory(tools/benchmark)
endif()
option(BUILD_FUZZ "Build fuzz harness" OFF)
if(BUILD_FUZZ)
add_subdirectory(tools/fuzz-harness)
endif()
option(BUILD_PKI "Build PKI tools" OFF)
if(BUILD_PKI AND VANETZA_WITH_PQC)
message(FATAL_ERROR
"BUILD_PKI cannot be combined with VANETZA_WITH_PQC: the unchanged PKI "
"client uses Vanetza's standard Security ASN.1 profile. Build the PKI "
"client separately with VANETZA_WITH_PQC=OFF.")
endif()
if(BUILD_PKI)
add_subdirectory(tools/pki)
endif()
# interface library for convenience
get_property(_components GLOBAL PROPERTY VANETZA_COMPONENTS)
add_library(vanetza INTERFACE)
add_library(Vanetza::vanetza ALIAS vanetza)
foreach(_component IN LISTS _components)
target_link_libraries(vanetza INTERFACE ${_component})
endforeach()
# installation rules
include(CMakePackageConfigHelpers)
set(CMAKECONFIG_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/cmake/Vanetza")
set(CMAKECONFIG_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/cmake-config")
write_basic_package_version_file(${CMAKECONFIG_BUILD_DIR}/VanetzaConfigVersion.cmake
COMPATIBILITY ExactVersion)
configure_package_config_file(cmake/VanetzaConfig.cmake.in
${CMAKECONFIG_BUILD_DIR}/VanetzaConfig.cmake
INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR}
PATH_VARS CMAKE_INSTALL_INCLUDEDIR)
install(TARGETS vanetza EXPORT ${PROJECT_NAME})
install(FILES
${CMAKECONFIG_BUILD_DIR}/VanetzaConfig.cmake
${CMAKECONFIG_BUILD_DIR}/VanetzaConfigVersion.cmake
DESTINATION ${CMAKECONFIG_INSTALL_DIR})
install(DIRECTORY ${PROJECT_SOURCE_DIR}/cmake/
DESTINATION ${CMAKECONFIG_INSTALL_DIR}
FILES_MATCHING
PATTERN "Find*.cmake")
install(EXPORT ${PROJECT_NAME} NAMESPACE Vanetza:: FILE VanetzaTargets.cmake DESTINATION ${CMAKECONFIG_INSTALL_DIR})
# install all C++ headers except those found for tests
file(GLOB_RECURSE _vanetza_headers
RELATIVE ${PROJECT_SOURCE_DIR}
CONFIGURE_DEPENDS
vanetza/*.hpp)
list(FILTER _vanetza_headers EXCLUDE REGEX "/tests/")
if(NOT VANETZA_WITH_PQC)
list(FILTER _vanetza_headers EXCLUDE REGEX "vanetza/security/pqc/")
endif()
foreach(_header IN LISTS _vanetza_headers)
get_filename_component(_dir ${_header} DIRECTORY)
install(FILES ${_header}
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${_dir})
endforeach()
# export build tree (allows import by outside projects)
option(VANETZA_EXPORT_PACKAGE "Export Vanetza build directory to CMake package registry" OFF)
if(VANETZA_EXPORT_PACKAGE)
export(EXPORT ${PROJECT_NAME} NAMESPACE Vanetza:: FILE VanetzaExports.cmake)
file(COPY ${CMAKECONFIG_BUILD_DIR}/VanetzaConfigVersion.cmake
DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
configure_file(cmake/VanetzaExportsConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/VanetzaConfig.cmake @ONLY)
export(PACKAGE ${PROJECT_NAME})
endif()
# mark build directory as cache dir, see https://bford.info/cachedir
file(WRITE ${PROJECT_BINARY_DIR}/CACHEDIR.TAG
"Signature: 8a477f597d28d172789f06886806bc55")
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
rsource "ports/esp_idf/Kconfig"
-853
View File
@@ -1,853 +0,0 @@
This file contains copies of the GNU Lesser General Public License as well
as the GNU General Public License, both in their third version.
# GNU Lesser General Public License (LGPL) v3
```
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
```
# GNU General Public License (GPL) v3
```
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
```
-26
View File
@@ -1,26 +0,0 @@
# Vanetza Upstream Provenance
This directory (`external/vanetza-idf`) contains the Vanetza protocol stack and its ESP-IDF integration for the micrOBU ESP32-C5 ITS-S station.
## Upstream Software Basis
- **Project**: Vanetza (ETSI C-ITS protocol stack implementation in C++17)
- **Upstream Repository**: `https://github.com/riebl/vanetza.git`
- **Pinned Upstream Revision**: `a7cacc1879f3e2124dfb8ef2d83886b1e82d3b36`
- **Author / Upstream Maintainer**: Raphael Riebl et al.
- **Citekey**: `rieblRieblVanetza2026`
- **License**: BSD-3-Clause (retained in `LICENSE.md`)
## Integrated Components & Third-Party Dependencies
1. **Boost C++ Libraries (v1.87.0)**:
- Located in `ports/esp_idf/third_party/` (`circular_buffer`, `geometry`, `heap`, `iostreams`, `multiprecision`, `rational`).
- License: Boost Software License 1.0 (`BSL-1.0`).
2. **OpenTrafficMap (OTM) Transmitter**:
- Upstream: `https://codeberg.org/opentrafficmap/its-g5-receiver-firmware_txenabled.git`
- Reviewed Revision: `674e34128279235ba34c9f8d778f43cf1d075397`
- Core file: `ports/esp_idf/third_party/otm/main/tx_custom.c` (guarded by SHA-256 in `firmware/CMakeLists.txt`).
3. **ESP-IDF Port (`ports/esp_idf`)**:
- Implements hardware-accelerated crypto (mbedTLS ECC/ECDSA/SHA), NVS credential persistence, ESP32-C5 802.11p PHY bindings (`radio_c5.cmake`), and localhost PKI verification tooling (`ports/esp_idf/tools/local_pki.py`, `pki_client.py`).
-153
View File
@@ -1,153 +0,0 @@
# vanetza-idf
Modular C++17 Vanetza library for ESP-IDF. Choose an access, network/transport,
or facilities-codec profile; supply your own platform and radio adapters.
The portable core targets the ESP32 family. The integrated C5 radio backend
is experimental and requires separate hardware validation.
**Start with the [ESP-IDF integration guide](docs/idf/README.md).** It covers
installation, Kconfig, ownership/event-loop rules, examples, and standalone
host/device tests. [Standards traceability](docs/idf/standards.md) links the public
interfaces to precise ETSI editions and clauses.
The [interface assessment](docs/idf/interface-assessment.md) identifies remaining
contract and capability gaps. [Test campaigns](docs/idf/test-campaigns.md) explains
how the separately installed ETSI framework and two-C5 radio tests connect.
**This is an experimental port, not a completed ETSI-conformant stack.**
See [implementation and conformance gaps](docs/idf/conformance.md) and
[validation results](docs/idf/validation.md). CAM/DENM/VAM codecs and endpoints
are distinct from complete Basic Services.
## Components and configurable boundaries
The arrangement follows the facilities, networking/transport and access layers
of the ETSI ITS station architecture, with management and security as shared
cross-layer services. It is an implementation map, inspired by
[TS 102 723-11, clauses 4–5](https://www.etsi.org/deliver/etsi_ts/102700_102799/10272311/01.01.01_60/ts_10272311v010101p.pdf)
and [EN 303 797, Annex B](https://www.etsi.org/deliver/etsi_en/303700_303799/303797/02.01.01_60/en_303797v020101p.pdf).
It does not imply that every service in those figures is implemented.
```mermaid
flowchart TB
subgraph station["vanetza-idf"]
direction TB
subgraph entry["Application / test entry points"]
direction LR
application["Application or upper tester"]
f_entry["Facilities PDU entry"]
b_entry["BTP-DATA entry"]
a_entry["AL_DATA entry"]
application --> f_entry
application --> b_entry
application --> a_entry
end
subgraph dataplane["Protocol data plane"]
direction LR
subgraph facilities["Facilities"]
direction TB
codecs["CAM R2 / DENM R2 / VAM R2\nASN.1 codecs"]
basic_services["CA / DEN / VRU Basic Services\npending"]
end
subgraph nt["Networking & Transport"]
direction TB
btp["BTP-A / BTP-B"]
gn["GeoNetworking\nSHB / GBC"]
btp --> gn
end
subgraph access_layer["Access boundary"]
direction TB
access["AL_DATA\nparameter validation"]
end
codecs --> btp
gn --> access
end
subgraph shared["Shared / cross-layer services"]
direction LR
position["Position + clock"]
management["Management\nMIB + configuration"]
security["Security entity\nTS 103 097 signing + ticket pool\nverification pending"]
saps["SAP bindings\nSN / SF / MN / MF / MI"]
end
f_entry --> codecs
b_entry --> btp
a_entry --> access
position -.-> gn
management -.-> gn
security -.-> gn
saps -.-> security
saps -.-> gn
end
subgraph integration["Platform / external integration"]
direction LR
etsi["External ETSI TTCN-3\nframework + SUT adapter"]
hil["Optional HIL framing\nbounded VID1 envelope"]
adapter["External access adapter\nsoftware lower tester / radio"]
radio["ESP32-C5 radio backend\nexperimental; DCC + HW validation pending"]
hil -.-> etsi
adapter -.-> radio
end
etsi -.-> application
etsi -.-> adapter
access <-->|"owned GNPDU + metadata"| adapter
```
The main transmit path is deliberately kept horizontal: Facilities → BTP →
GeoNetworking → Access. Management, position/time and security are shown as
shared services because they support protocol processing rather than form another
encapsulation stage. Reception travels back up the selected layers. The access
profile omits the network and facilities layers; the network profile adds
BTP/GeoNetworking; the facilities profile also enables the individually
selectable codecs. Complete CA, DEN and VRU Basic Services are still pending.
The optional HIL envelope carries adapter payloads; it is not an ETSI protocol.
## External TTCN-3 and HIL
Install the TTCN-3 runtime and the [official ETSI ITS framework](https://forge.etsi.org/rep/ITS/TS.ITS)
separately. The library does not install or bundle them. The test application
connects that framework's suite-specific SUT adapter to these points:
| Point | API/boundary | Intended use |
|---|---|---|
| Upper tester | Named NF-SAP binding, `Stack::request`, position/time/security injection | Stimulate implemented behavior and collect actual results; full service operations remain pending |
| Software lower tester | `Access::request` and `Stack::indicate` | Inject/observe GN traffic while the protocol implementation runs on the ESP32 |
| Physical lower tester | Independent ITS-G5 capture/injection radio | Validate transmitted frames, access behavior and RF properties |
Enable `CONFIG_VANETZA_IDF_HIL` for optional bounded framing. USB/UART/BLE/IP
transports and suite-specific command decoding remain in the test application
and host SUT adapter. Preserve the ETSI codec payload and map it to the actual
service under test; never synthesize a successful acknowledgement.
The [hook-up guide](docs/idf/README.md#connect-a-separately-installed-etsi-ttcn-3-framework)
explains payload boundaries, metadata, real-time versus injected-time tests,
PICS/PIXIT, Release 2 ATS adaptation, and evidence retention. A mirrored SUT
packet is software evidence; radio conformance requires independent observation.
## Foundation and acknowledgements
vanetza-idf is an independent ESP-IDF adaptation built on
[Vanetza](https://github.com/riebl/vanetza). It is not maintained by, affiliated
with, or endorsed by the Vanetza team. Upstream authorship and license notices
remain with the original source files. See the
[upstream documentation](https://www.vanetza.org/) for the original library.
Thank you to [OpenTrafficMap](https://codeberg.org/opentrafficmap) for the
foundational work that established how to transmit ITS-G5 using the ESP32-C5
radio, shared in their
[receiver firmware with TX enabled](https://codeberg.org/opentrafficmap/its-g5-receiver-firmware_txenabled).
The C5 backend builds on that work. Its private SDK dependencies and validation
status are documented separately from the portable protocol core.
Vanetza is licensed under LGPLv3; see [LICENSE.md](LICENSE.md).
Third-party components retain their respective notices and licensing terms.
-47
View File
@@ -1,47 +0,0 @@
These ASN.1 files are taken from their respective standardisation documents and are **not** licensed the same way as Vanetza itself.
Please refer to the referenced standardisation documents for details about the applicable Intellectual Property Rights (IPR).
To the best of my knowledge, ETSI does not object to bundling ASN.1 files of their published documents along with software.
Some editorial changes may have been applied, e.g. removal of trailing whitespace etc.
Code generated from the files located here can be found in the *vanetza/asn1/its* directory.
# Standardisation documents
- *EN302637-2v141-CAM.asn* from ETSI EN 302 637-2 v1.4.1 (2019-04)
- *EN302637-3v131-DENM.asn* from ETSI EN 302 637-3 v1.3.1 (2019-04)
- *TS102894-2v131-CDD.asn* from ETSI TS 102 894-2 v1.3.1 (2018-08)
- *TS102941v131-\*.asn* from ETSI TS 102 941 v1.3.1 (2019-02)
- *TS103097v131.asn* from ETSI TS 103 097 v1.3.1 (2017-10)
- *TS103097v211-\*.asn* from ETSI TS 103 097 v2.1.1 (2021-10)
- *TR103562v211.asn* from ETSI TR 103 562 v2.1.1 (2019-12)
- *IEEE1609dot2.asn* and *IEEE1609dot2BaseTypes.asn* from IEEE 1609.2 as printed in ETSI TS 103 097 v1.3.1
# ETSI WebSVN
You may find these ITS ASN.1 modules also in ETSI's public SVN repository: http://oldforge.etsi.org/websvn/listing.php?repname=ITS.ITS_ASN1
While above source is still online (in March 2020), ETSI seems to have moved to https://forge.etsi.org/rep/ITS/ITS_ASN1 by now.
This newer source also contains a license file explicitly. A copy is included below for reference.
> Copyright 2019 ETSI
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
> 1. Redistributions of source code must retain the above copyright notice,
> this list of conditions and the following disclaimer.
> 2. Redistributions in binary form must reproduce the above copyright notice,
> this list of conditions and the following disclaimer in the documentation
> and/or other materials provided with the distribution.
> 3. Neither the name of the copyright holder nor the names of its contributors
> may be used to endorse or promote products derived from this software without
> specific prior written permission.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
> IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
> INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
> DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
> LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
> OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
> OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -1,132 +0,0 @@
CAM-PDU-Descriptions {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) en (302637) cam (2) version (2)
}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
ItsPduHeader, CauseCode, ReferencePosition, AccelerationControl, Curvature, CurvatureCalculationMode, Heading, LanePosition, EmergencyPriority, EmbarkationStatus, Speed, DriveDirection, LongitudinalAcceleration, LateralAcceleration, VerticalAcceleration, StationType, ExteriorLights, DangerousGoodsBasic, SpecialTransportType, LightBarSirenInUse, VehicleRole, VehicleLength, VehicleWidth, PathHistory, RoadworksSubCauseCode, ClosedLanes, TrafficRule, SpeedLimit, SteeringWheelAngle, PerformanceClass, YawRate, ProtectedCommunicationZone, PtActivation, Latitude, Longitude, ProtectedCommunicationZonesRSU, CenDsrcTollingZone FROM ITS-Container {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) version (2)
};
-- The root data frame for cooperative awareness messages
CAM ::= SEQUENCE {
header ItsPduHeader,
cam CoopAwareness
}
CoopAwareness ::= SEQUENCE {
generationDeltaTime GenerationDeltaTime,
camParameters CamParameters
}
CamParameters ::= SEQUENCE {
basicContainer BasicContainer,
highFrequencyContainer HighFrequencyContainer,
lowFrequencyContainer LowFrequencyContainer OPTIONAL,
specialVehicleContainer SpecialVehicleContainer OPTIONAL,
...
}
HighFrequencyContainer ::= CHOICE {
basicVehicleContainerHighFrequency BasicVehicleContainerHighFrequency,
rsuContainerHighFrequency RSUContainerHighFrequency,
...
}
LowFrequencyContainer ::= CHOICE {
basicVehicleContainerLowFrequency BasicVehicleContainerLowFrequency,
...
}
SpecialVehicleContainer ::= CHOICE {
publicTransportContainer PublicTransportContainer,
specialTransportContainer SpecialTransportContainer,
dangerousGoodsContainer DangerousGoodsContainer,
roadWorksContainerBasic RoadWorksContainerBasic,
rescueContainer RescueContainer,
emergencyContainer EmergencyContainer,
safetyCarContainer SafetyCarContainer,
...
}
BasicContainer ::= SEQUENCE {
stationType StationType,
referencePosition ReferencePosition,
...
}
BasicVehicleContainerHighFrequency ::= SEQUENCE {
heading Heading,
speed Speed,
driveDirection DriveDirection,
vehicleLength VehicleLength,
vehicleWidth VehicleWidth,
longitudinalAcceleration LongitudinalAcceleration,
curvature Curvature,
curvatureCalculationMode CurvatureCalculationMode,
yawRate YawRate,
accelerationControl AccelerationControl OPTIONAL,
lanePosition LanePosition OPTIONAL,
steeringWheelAngle SteeringWheelAngle OPTIONAL,
lateralAcceleration LateralAcceleration OPTIONAL,
verticalAcceleration VerticalAcceleration OPTIONAL,
performanceClass PerformanceClass OPTIONAL,
cenDsrcTollingZone CenDsrcTollingZone OPTIONAL
}
BasicVehicleContainerLowFrequency ::= SEQUENCE {
vehicleRole VehicleRole,
exteriorLights ExteriorLights,
pathHistory PathHistory
}
PublicTransportContainer ::= SEQUENCE {
embarkationStatus EmbarkationStatus,
ptActivation PtActivation OPTIONAL
}
SpecialTransportContainer ::= SEQUENCE {
specialTransportType SpecialTransportType,
lightBarSirenInUse LightBarSirenInUse
}
DangerousGoodsContainer ::= SEQUENCE {
dangerousGoodsBasic DangerousGoodsBasic
}
RoadWorksContainerBasic ::= SEQUENCE {
roadworksSubCauseCode RoadworksSubCauseCode OPTIONAL,
lightBarSirenInUse LightBarSirenInUse,
closedLanes ClosedLanes OPTIONAL
}
RescueContainer ::= SEQUENCE {
lightBarSirenInUse LightBarSirenInUse
}
EmergencyContainer ::= SEQUENCE {
lightBarSirenInUse LightBarSirenInUse,
incidentIndication CauseCode OPTIONAL,
emergencyPriority EmergencyPriority OPTIONAL
}
SafetyCarContainer ::= SEQUENCE {
lightBarSirenInUse LightBarSirenInUse,
incidentIndication CauseCode OPTIONAL,
trafficRule TrafficRule OPTIONAL,
speedLimit SpeedLimit OPTIONAL
}
RSUContainerHighFrequency ::= SEQUENCE {
protectedCommunicationZonesRSU ProtectedCommunicationZonesRSU OPTIONAL,
...
}
GenerationDeltaTime ::= INTEGER { oneMilliSec(1) } (0..65535)
END
@@ -1,109 +0,0 @@
DENM-PDU-Descriptions {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) en (302637) denm (1) version (2)
}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
ItsPduHeader, CauseCode, Speed, InformationQuality, ReferencePosition, ClosedLanes, DangerousGoodsExtended, Heading, LanePosition, LightBarSirenInUse, RoadType, HeightLonCarr, PosLonCarr, PosCentMass, PositioningSolutionType, RequestResponseIndication, StationType, SpeedLimit, StationarySince, TimestampIts, WheelBaseVehicle, TurningRadius, PosFrontAx, PositionOfOccupants, Temperature, VehicleMass, VehicleIdentification, EnergyStorageType, ActionID, ItineraryPath, NumberOfOccupants, PositionOfPillars, RelevanceTrafficDirection, RestrictedTypes, Traces, TransmissionInterval, ValidityDuration, RelevanceDistance, EventHistory, TrafficRule, DeltaReferencePosition FROM ITS-Container {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) version (2)
};
DENM ::= SEQUENCE {
header ItsPduHeader,
denm DecentralizedEnvironmentalNotificationMessage
}
DecentralizedEnvironmentalNotificationMessage ::= SEQUENCE {
management ManagementContainer,
situation SituationContainer OPTIONAL,
location LocationContainer OPTIONAL,
alacarte AlacarteContainer OPTIONAL
}
ManagementContainer ::= SEQUENCE {
actionID ActionID,
detectionTime TimestampIts,
referenceTime TimestampIts,
termination Termination OPTIONAL,
eventPosition ReferencePosition,
relevanceDistance RelevanceDistance OPTIONAL,
relevanceTrafficDirection RelevanceTrafficDirection OPTIONAL,
validityDuration ValidityDuration DEFAULT defaultValidity,
transmissionInterval TransmissionInterval OPTIONAL,
stationType StationType,
...
}
SituationContainer ::= SEQUENCE {
informationQuality InformationQuality,
eventType CauseCode,
linkedCause CauseCode OPTIONAL,
eventHistory EventHistory OPTIONAL,
...
}
LocationContainer ::= SEQUENCE {
eventSpeed Speed OPTIONAL,
eventPositionHeading Heading OPTIONAL,
traces Traces,
roadType RoadType OPTIONAL,
...
}
ImpactReductionContainer ::= SEQUENCE {
heightLonCarrLeft HeightLonCarr,
heightLonCarrRight HeightLonCarr,
posLonCarrLeft PosLonCarr,
posLonCarrRight PosLonCarr,
positionOfPillars PositionOfPillars,
posCentMass PosCentMass,
wheelBaseVehicle WheelBaseVehicle,
turningRadius TurningRadius,
posFrontAx PosFrontAx,
positionOfOccupants PositionOfOccupants,
vehicleMass VehicleMass,
requestResponseIndication RequestResponseIndication
}
RoadWorksContainerExtended ::= SEQUENCE {
lightBarSirenInUse LightBarSirenInUse OPTIONAL,
closedLanes ClosedLanes OPTIONAL,
restriction RestrictedTypes OPTIONAL,
speedLimit SpeedLimit OPTIONAL,
incidentIndication CauseCode OPTIONAL,
recommendedPath ItineraryPath OPTIONAL,
startingPointSpeedLimit DeltaReferencePosition OPTIONAL,
trafficFlowRule TrafficRule OPTIONAL,
referenceDenms ReferenceDenms OPTIONAL
}
StationaryVehicleContainer ::= SEQUENCE {
stationarySince StationarySince OPTIONAL,
stationaryCause CauseCode OPTIONAL,
carryingDangerousGoods DangerousGoodsExtended OPTIONAL,
numberOfOccupants NumberOfOccupants OPTIONAL,
vehicleIdentification VehicleIdentification OPTIONAL,
energyStorageType EnergyStorageType OPTIONAL
}
AlacarteContainer ::= SEQUENCE {
lanePosition LanePosition OPTIONAL,
impactReduction ImpactReductionContainer OPTIONAL,
externalTemperature Temperature OPTIONAL,
roadWorks RoadWorksContainerExtended OPTIONAL,
positioningSolution PositioningSolutionType OPTIONAL,
stationaryVehicle StationaryVehicleContainer OPTIONAL,
...
}
defaultValidity INTEGER ::= 600
Termination ::= ENUMERATED {isCancellation(0), isNegation (1)}
ReferenceDenms ::= SEQUENCE (SIZE(1..8, ...)) OF ActionID
END
@@ -1,306 +0,0 @@
IEEE1609dot2 {
iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) base (1) schema (1) major-version-2(2)
}
--******************************************************************************
--
-- IEEE P1609.2 Data Types
--
--******************************************************************************
DEFINITIONS AUTOMATIC TAGS ::= BEGIN
EXPORTS ALL;
IMPORTS
CrlSeries,
EccP256CurvePoint,
EciesP256EncryptedKey,
EncryptionKey,
GeographicRegion,
GroupLinkageValue,
HashAlgorithm,
HashedId3,
HashedId8,
Hostname,
IValue,
LinkageValue,
Opaque,
Psid,
PsidSsp,
PsidSspRange,
PublicEncryptionKey,
PublicVerificationKey,
SequenceOfHashedId3,
SequenceOfPsidSsp,
SequenceOfPsidSspRange,
ServiceSpecificPermissions,
Signature,
SubjectAssurance,
SymmetricEncryptionKey,
ThreeDLocation,
Time64,
Uint3,
Uint8,
Uint16,
Uint32,
ValidityPeriod
FROM IEEE1609dot2BaseTypes {
iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) base(1) base-types(2) major-version-2 (2)
};
--
--*********************************************************************
--
-- Structures for describing secured data
--
--*********************************************************************
-- Necessary to get certain tools to generate sample PDUs
-- TestIeee1609Dot2Data ::= Ieee1609Dot2Data
-- TestCertificate ::= Certificate
-- this structure belongs later in the file but putting it here avoids
-- compiler errors with certain tools
SignedDataPayload ::= SEQUENCE {
data Ieee1609Dot2Data OPTIONAL,
extDataHash HashedData OPTIONAL,
...
}
(WITH COMPONENTS {..., data PRESENT} |
WITH COMPONENTS {..., extDataHash PRESENT})
Ieee1609Dot2Data ::= SEQUENCE {
protocolVersion Uint8(3),
content Ieee1609Dot2Content
}
Ieee1609Dot2Content ::= CHOICE {
unsecuredData Opaque,
signedData SignedData,
encryptedData EncryptedData,
signedCertificateRequest Opaque,
...
}
SignedData ::= SEQUENCE {
hashId HashAlgorithm,
tbsData ToBeSignedData,
signer SignerIdentifier,
signature Signature
}
SignerIdentifier ::= CHOICE {
digest HashedId8,
certificate SequenceOfCertificate,
self NULL,
...
}
ToBeSignedData ::= SEQUENCE {
payload SignedDataPayload,
headerInfo HeaderInfo
}
HashedData::= CHOICE {
sha256HashedData OCTET STRING (SIZE(32)),
...
}
HeaderInfo ::= SEQUENCE {
psid Psid,
generationTime Time64 OPTIONAL,
expiryTime Time64 OPTIONAL,
generationLocation ThreeDLocation OPTIONAL,
p2pcdLearningRequest HashedId3 OPTIONAL,
missingCrlIdentifier MissingCrlIdentifier OPTIONAL,
encryptionKey EncryptionKey OPTIONAL,
...,
inlineP2pcdRequest SequenceOfHashedId3 OPTIONAL,
requestedCertificate Certificate OPTIONAL
}
MissingCrlIdentifier ::= SEQUENCE {
cracaId HashedId3,
crlSeries CrlSeries,
...
}
Countersignature ::= Ieee1609Dot2Data (WITH COMPONENTS {...,
content (WITH COMPONENTS {...,
signedData (WITH COMPONENTS {...,
tbsData (WITH COMPONENTS {...,
payload (WITH COMPONENTS {...,
data ABSENT,
extDataHash PRESENT
}),
headerInfo(WITH COMPONENTS {...,
generationTime PRESENT,
expiryTime ABSENT,
generationLocation ABSENT,
p2pcdLearningRequest ABSENT,
missingCrlIdentifier ABSENT,
encryptionKey ABSENT
})
})
})
})
})
--**********************************************************************
--
-- Structures for describing encrypted data
--
--**********************************************************************
EncryptedData ::= SEQUENCE {
recipients SequenceOfRecipientInfo,
ciphertext SymmetricCiphertext
}
RecipientInfo ::= CHOICE {
pskRecipInfo PreSharedKeyRecipientInfo,
symmRecipInfo SymmRecipientInfo,
certRecipInfo PKRecipientInfo,
signedDataRecipInfo PKRecipientInfo,
rekRecipInfo PKRecipientInfo
}
SequenceOfRecipientInfo ::= SEQUENCE OF RecipientInfo
PreSharedKeyRecipientInfo ::= HashedId8
SymmRecipientInfo ::= SEQUENCE {
recipientId HashedId8,
encKey SymmetricCiphertext
}
PKRecipientInfo ::= SEQUENCE {
recipientId HashedId8,
encKey EncryptedDataEncryptionKey
}
EncryptedDataEncryptionKey ::= CHOICE {
eciesNistP256 EciesP256EncryptedKey,
eciesBrainpoolP256r1 EciesP256EncryptedKey,
...
}
SymmetricCiphertext ::= CHOICE {
aes128ccm AesCcmCiphertext,
...
}
AesCcmCiphertext ::= SEQUENCE {
nonce OCTET STRING (SIZE (12)),
ccmCiphertext Opaque -- 16 bytes longer than plaintext
}
--**********************************************************************
--
-- Certificates and other security management data structures
--
--**********************************************************************
-- Certificates are implicit (type = implicit, toBeSigned includes
-- reconstruction value, signature absent) or explicit (type = explicit,
-- toBeSigned includes verification key, signature present).
Certificate ::= CertificateBase (ImplicitCertificate | ExplicitCertificate)
SequenceOfCertificate ::= SEQUENCE OF Certificate
CertificateBase ::= SEQUENCE {
version Uint8(3),
type CertificateType,
issuer IssuerIdentifier,
toBeSigned ToBeSignedCertificate,
signature Signature OPTIONAL
}
CertificateType ::= ENUMERATED {
explicit,
implicit,
...
}
ImplicitCertificate ::= CertificateBase (WITH COMPONENTS {...,
type(implicit),
toBeSigned (WITH COMPONENTS {...,
verifyKeyIndicator(WITH COMPONENTS {reconstructionValue})
}),
signature ABSENT
})
ExplicitCertificate ::= CertificateBase (WITH COMPONENTS {...,
type(explicit),
toBeSigned(WITH COMPONENTS {...,
verifyKeyIndicator(WITH COMPONENTS {verificationKey})
}),
signature PRESENT
})
IssuerIdentifier ::= CHOICE {
sha256AndDigest HashedId8,
self HashAlgorithm,
...,
sha384AndDigest HashedId8
}
ToBeSignedCertificate ::= SEQUENCE {
id CertificateId,
cracaId HashedId3,
crlSeries CrlSeries,
validityPeriod ValidityPeriod,
region GeographicRegion OPTIONAL,
assuranceLevel SubjectAssurance OPTIONAL,
appPermissions SequenceOfPsidSsp OPTIONAL,
certIssuePermissions SequenceOfPsidGroupPermissions OPTIONAL,
certRequestPermissions SequenceOfPsidGroupPermissions OPTIONAL,
canRequestRollover NULL OPTIONAL,
encryptionKey PublicEncryptionKey OPTIONAL,
verifyKeyIndicator VerificationKeyIndicator,
...
}
(WITH COMPONENTS { ..., appPermissions PRESENT} |
WITH COMPONENTS { ..., certIssuePermissions PRESENT} |
WITH COMPONENTS { ..., certRequestPermissions PRESENT})
CertificateId ::= CHOICE {
linkageData LinkageData,
name Hostname,
binaryId OCTET STRING(SIZE(1..64)),
none NULL,
...
}
LinkageData ::= SEQUENCE {
iCert IValue,
linkage-value LinkageValue,
group-linkage-value GroupLinkageValue OPTIONAL
}
EndEntityType ::= BIT STRING { app (0), enrol (1) } (SIZE (8)) (ALL EXCEPT {})
PsidGroupPermissions ::= SEQUENCE {
subjectPermissions SubjectPermissions,
minChainLength INTEGER DEFAULT 1,
chainLengthRange INTEGER DEFAULT 0,
eeType EndEntityType DEFAULT {app} -- vanetza-idf: IEEE Std 1609.2-2022 6.4.28 (was '00'H)
}
SequenceOfPsidGroupPermissions ::= SEQUENCE OF PsidGroupPermissions
SubjectPermissions ::= CHOICE {
explicit SequenceOfPsidSspRange,
all NULL,
...
}
VerificationKeyIndicator ::= CHOICE {
verificationKey PublicVerificationKey,
reconstructionValue EccP256CurvePoint,
...
}
END
@@ -1,330 +0,0 @@
IEEE1609dot2BaseTypes {
iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) base(1) base-types(2) major-version-2(2)
}
--
--********************************************************************
-- IEEE P1609.2 Base Data Types
--
--********************************************************************
DEFINITIONS AUTOMATIC TAGS ::= BEGIN
EXPORTS ALL;
-- ------------------------------------------------------------------
--
-- Integers
--
-- ------------------------------------------------------------------
Uint3 ::= INTEGER (0..7) -- (hex) 07
Uint8 ::= INTEGER (0..255) -- (hex) ff
Uint16 ::= INTEGER (0..65535) -- (hex) ff ff
Uint32 ::= INTEGER (0..4294967295) -- (hex) ff ff ff ff
Uint64 ::= INTEGER (0..18446744073709551615) -- (hex) ff ff ff ff ff ff ff ff
SequenceOfUint8 ::= SEQUENCE OF Uint8
SequenceOfUint16 ::= SEQUENCE OF Uint16
-- ------------------------------------------------------------------
--
-- OCTET STRING types
--
-- ------------------------------------------------------------------
Opaque ::= OCTET STRING
HashedId10 ::= OCTET STRING (SIZE(10))
HashedId8 ::= OCTET STRING (SIZE(8))
HashedId3 ::= OCTET STRING (SIZE(3))
SequenceOfHashedId3 ::= SEQUENCE OF HashedId3
-- ------------------------------------------------------------------
--
-- Time
--
-- ------------------------------------------------------------------
Time32 ::= Uint32
Time64 ::= Uint64
ValidityPeriod ::= SEQUENCE {
start Time32,
duration Duration
}
Duration ::= CHOICE {
microseconds Uint16,
milliseconds Uint16,
seconds Uint16,
minutes Uint16,
hours Uint16,
sixtyHours Uint16,
years Uint16
}
-- ------------------------------------------------------------------
--
-- Location
--
-- ------------------------------------------------------------------
GeographicRegion ::= CHOICE {
circularRegion CircularRegion,
rectangularRegion SequenceOfRectangularRegion,
polygonalRegion PolygonalRegion,
identifiedRegion SequenceOfIdentifiedRegion,
...
}
CircularRegion ::= SEQUENCE {
center TwoDLocation,
radius Uint16
}
RectangularRegion ::= SEQUENCE {
northWest TwoDLocation,
southEast TwoDLocation
}
SequenceOfRectangularRegion ::= SEQUENCE OF RectangularRegion
PolygonalRegion ::= SEQUENCE SIZE(3..MAX) OF TwoDLocation
TwoDLocation ::= SEQUENCE {
latitude Latitude,
longitude Longitude
}
IdentifiedRegion ::= CHOICE {
countryOnly CountryOnly,
countryAndRegions CountryAndRegions,
countryAndSubregions CountryAndSubregions,
...
}
SequenceOfIdentifiedRegion ::= SEQUENCE OF IdentifiedRegion
CountryOnly ::= Uint16
CountryAndRegions ::= SEQUENCE {
countryOnly CountryOnly,
regions SequenceOfUint8
}
CountryAndSubregions ::= SEQUENCE {
country CountryOnly,
regionAndSubregions SequenceOfRegionAndSubregions
}
RegionAndSubregions ::= SEQUENCE {
region Uint8,
subregions SequenceOfUint16
}
SequenceOfRegionAndSubregions ::= SEQUENCE OF RegionAndSubregions
ThreeDLocation ::= SEQUENCE {
latitude Latitude,
longitude Longitude,
elevation Elevation
}
Latitude ::= NinetyDegreeInt
Longitude ::= OneEightyDegreeInt
Elevation ::= ElevInt
NinetyDegreeInt ::= INTEGER {
min (-900000000),
max (900000000),
unknown (900000001)
} (-900000000..900000001)
KnownLatitude ::= NinetyDegreeInt (min..max) -- Minus 90deg to +90deg in microdegree intervals
UnknownLatitude ::= NinetyDegreeInt (unknown)
OneEightyDegreeInt ::= INTEGER {
min (-1799999999),
max (1800000000),
unknown (1800000001)
} (-1799999999..1800000001)
KnownLongitude ::= OneEightyDegreeInt (min..max)
UnknownLongitude ::= OneEightyDegreeInt (unknown)
ElevInt ::= Uint16 -- Range is from -4096 to 61439 in units of one-tenth of a meter
-- ------------------------------------------------------------------
--
-- Crypto
--
-- ------------------------------------------------------------------
Signature ::= CHOICE {
ecdsaNistP256Signature EcdsaP256Signature,
ecdsaBrainpoolP256r1Signature EcdsaP256Signature,
...,
ecdsaBrainpoolP384r1Signature EcdsaP384Signature
}
EcdsaP256Signature ::= SEQUENCE {
rSig EccP256CurvePoint,
sSig OCTET STRING (SIZE (32))
}
EcdsaP384Signature ::= SEQUENCE {
rSig EccP384CurvePoint,
sSig OCTET STRING (SIZE (48))
}
EccP256CurvePoint ::= CHOICE {
x-only OCTET STRING (SIZE (32)),
fill NULL, -- consistency with 1363 / X9.62
compressed-y-0 OCTET STRING (SIZE (32)),
compressed-y-1 OCTET STRING (SIZE (32)),
uncompressedP256 SEQUENCE {
x OCTET STRING (SIZE (32)),
y OCTET STRING (SIZE (32))
}
}
EccP384CurvePoint ::= CHOICE {
x-only OCTET STRING (SIZE (48)),
fill NULL, -- consistency w 1363 / X9.62
compressed-y-0 OCTET STRING (SIZE (48)),
compressed-y-1 OCTET STRING (SIZE (48)),
uncompressedP384 SEQUENCE {
x OCTET STRING (SIZE (48)),
y OCTET STRING (SIZE (48))
}
}
SymmAlgorithm ::= ENUMERATED {
aes128Ccm,
...
}
HashAlgorithm ::= ENUMERATED {
sha256,
...,
sha384
}
EciesP256EncryptedKey ::= SEQUENCE {
v EccP256CurvePoint,
c OCTET STRING (SIZE (16)),
t OCTET STRING (SIZE (16))
}
EncryptionKey ::= CHOICE {
public PublicEncryptionKey,
symmetric SymmetricEncryptionKey
}
PublicEncryptionKey ::= SEQUENCE {
supportedSymmAlg SymmAlgorithm,
publicKey BasePublicEncryptionKey
}
BasePublicEncryptionKey ::= CHOICE {
eciesNistP256 EccP256CurvePoint,
eciesBrainpoolP256r1 EccP256CurvePoint,
...
}
PublicVerificationKey ::= CHOICE {
ecdsaNistP256 EccP256CurvePoint,
ecdsaBrainpoolP256r1 EccP256CurvePoint,
...,
ecdsaBrainpoolP384r1 EccP384CurvePoint
}
SymmetricEncryptionKey ::= CHOICE {
aes128Ccm OCTET STRING(SIZE(16)),
...
}
-- ------------------------------------------------------------------
--
-- PSID / ITS-AID
--
-- ------------------------------------------------------------------
PsidSsp ::= SEQUENCE {
psid Psid,
ssp ServiceSpecificPermissions OPTIONAL
}
SequenceOfPsidSsp ::= SEQUENCE OF PsidSsp
Psid ::= INTEGER (0..MAX)
SequenceOfPsid ::= SEQUENCE OF Psid
ServiceSpecificPermissions ::= CHOICE {
opaque OCTET STRING (SIZE(0..MAX)),
...,
bitmapSsp BitmapSsp
}
BitmapSsp ::= OCTET STRING (SIZE(0..31))
PsidSspRange ::= SEQUENCE {
psid Psid,
sspRange SspRange OPTIONAL
}
SequenceOfPsidSspRange ::= SEQUENCE OF PsidSspRange
SspRange ::= CHOICE {
opaque SequenceOfOctetString,
all NULL,
... ,
bitmapSspRange BitmapSspRange
}
BitmapSspRange ::= SEQUENCE {
sspValue OCTET STRING (SIZE(1..32)),
sspBitmask OCTET STRING (SIZE(1..32))
}
SequenceOfOctetString ::= SEQUENCE (SIZE (0..MAX)) OF OCTET STRING (SIZE(0..MAX))
-- ------------------------------------------------------------------
--
-- Goes in certs
--
-- ------------------------------------------------------------------
SubjectAssurance ::= OCTET STRING (SIZE(1))
CrlSeries ::= Uint16
-- ------------------------------------------------------------------
--
-- Pseudonym Linkage
--
-- ------------------------------------------------------------------
IValue ::= Uint16
Hostname ::= UTF8String (SIZE(0..255))
LinkageValue ::= OCTET STRING (SIZE(9))
GroupLinkageValue ::= SEQUENCE {
jValue OCTET STRING (SIZE(4)),
value OCTET STRING (SIZE(9))
}
LaId ::= OCTET STRING (SIZE(2))
LinkageSeed ::= OCTET STRING (SIZE(16))
END
@@ -1,372 +0,0 @@
CPM-PDU-Descriptions {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) tr (103562) cpm (1) version (1)
}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS ItsPduHeader, Heading, ReferencePosition, Speed, DriveDirection, LongitudinalAcceleration, LateralAcceleration, VerticalAcceleration, StationType, VehicleLength, VehicleWidth, YawRate, SpeedConfidence
FROM ITS-Container { itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) version (2) }
IntersectionReferenceID, LaneID, NodeOffsetPointXY, Offset-B10, Offset-B11, Offset-B12, Offset-B13, Offset-B14, Offset-B16, RoadSegmentReferenceID, VehicleHeight
FROM DSRC { iso (1) standard (0) signalizedIntersection (19091) profilec(2) dsrc (2) version2 (2) }
GenerationDeltaTime FROM CAM-PDU-Descriptions { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg1(1) en(302637) cam(2) version(2)} ;
-- The root data frame for collective perception message
CPM ::= SEQUENCE {
header ItsPduHeader,
cpm CollectivePerceptionMessage
}
CollectivePerceptionMessage ::= SEQUENCE {
generationDeltaTime GenerationDeltaTime,
cpmParameters CpmParameters
}
CpmParameters ::= SEQUENCE {
managementContainer CpmManagementContainer,
stationDataContainer StationDataContainer OPTIONAL,
sensorInformationContainer SensorInformationContainer OPTIONAL,
perceivedObjectContainer PerceivedObjectContainer OPTIONAL,
freeSpaceAddendumContainer FreeSpaceAddendumContainer OPTIONAL,
numberOfPerceivedObjects NumberOfPerceivedObjects,
...
}
CpmManagementContainer ::= SEQUENCE {
stationType StationType,
perceivedObjectContainerSegmentInfo PerceivedObjectContainerSegmentInfo OPTIONAL,
referencePosition ReferencePosition,
...
}
StationDataContainer ::= CHOICE {
originatingVehicleContainer OriginatingVehicleContainer,
originatingRSUContainer OriginatingRSUContainer,
...
}
OriginatingVehicleContainer ::= SEQUENCE {
heading Heading,
speed Speed,
vehicleOrientationAngle WGS84Angle OPTIONAL,
driveDirection DriveDirection DEFAULT forward,
longitudinalAcceleration LongitudinalAcceleration OPTIONAL,
lateralAcceleration LateralAcceleration OPTIONAL,
verticalAcceleration VerticalAcceleration OPTIONAL,
yawRate YawRate OPTIONAL,
pitchAngle CartesianAngle OPTIONAL,
rollAngle CartesianAngle OPTIONAL,
vehicleLength VehicleLength OPTIONAL,
vehicleWidth VehicleWidth OPTIONAL,
vehicleHeight VehicleHeight OPTIONAL,
trailerDataContainer TrailerDataContainer OPTIONAL,
...
}
OriginatingRSUContainer ::= CHOICE {
intersectionReferenceId IntersectionReferenceID,
roadSegmentReferenceId RoadSegmentReferenceID,
...
}
SensorInformationContainer ::= SEQUENCE SIZE(1..128, ...) OF SensorInformation
SensorInformation ::= SEQUENCE {
sensorID Identifier,
type SensorType,
detectionArea DetectionArea,
freeSpaceConfidence FreeSpaceConfidence OPTIONAL,
...
}
PerceivedObjectContainer ::= SEQUENCE SIZE(1..128, ...) OF PerceivedObject
PerceivedObject ::= SEQUENCE {
objectID Identifier,
sensorIDList SensorIdList OPTIONAL,
timeOfMeasurement TimeOfMeasurement,
objectAge ObjectAge OPTIONAL,
objectConfidence ObjectConfidence DEFAULT 0,
xDistance ObjectDistanceWithConfidence,
yDistance ObjectDistanceWithConfidence,
zDistance ObjectDistanceWithConfidence OPTIONAL,
xSpeed SpeedExtended,
ySpeed SpeedExtended,
zSpeed SpeedExtended OPTIONAL,
xAcceleration LongitudinalAcceleration OPTIONAL,
yAcceleration LateralAcceleration OPTIONAL,
zAcceleration VerticalAcceleration OPTIONAL,
yawAngle CartesianAngle OPTIONAL,
planarObjectDimension1 ObjectDimension OPTIONAL,
planarObjectDimension2 ObjectDimension OPTIONAL,
verticalObjectDimension ObjectDimension OPTIONAL,
objectRefPoint ObjectRefPoint DEFAULT 0,
dynamicStatus DynamicStatus OPTIONAL,
classification ObjectClassDescription OPTIONAL,
matchedPosition MatchedPosition OPTIONAL,
...
}
DetectionArea ::= CHOICE {
vehicleSensor VehicleSensor,
stationarySensorRadial AreaRadial,
stationarySensorPolygon AreaPolygon,
stationarySensorCircular AreaCircular,
stationarySensorEllipse AreaEllipse,
stationarySensorRectangle AreaRectangle,
...
}
VehicleSensor ::= SEQUENCE {
refPointId RefPointId DEFAULT 0,
xSensorOffset XSensorOffset,
ySensorOffset YSensorOffset,
zSensorOffset ZSensorOffset OPTIONAL,
vehicleSensorPropertyList VehicleSensorPropertyList,
...
}
VehicleSensorPropertyList ::= SEQUENCE SIZE(1..10) OF VehicleSensorProperties
VehicleSensorProperties ::= SEQUENCE {
range Range,
horizontalOpeningAngleStart CartesianAngleValue,
horizontalOpeningAngleEnd CartesianAngleValue,
verticalOpeningAngleStart CartesianAngleValue OPTIONAL,
verticalOpeningAngleEnd CartesianAngleValue OPTIONAL,
...
}
AreaCircular ::= SEQUENCE {
nodeCenterPoint OffsetPoint OPTIONAL,
radius Radius
}
AreaEllipse ::= SEQUENCE {
nodeCenterPoint OffsetPoint OPTIONAL,
semiMinorRangeLength SemiRangeLength,
semiMajorRangeLength SemiRangeLength,
semiMajorRangeOrientation WGS84AngleValue,
semiHeight SemiRangeLength OPTIONAL
}
AreaRectangle ::= SEQUENCE {
nodeCenterPoint OffsetPoint OPTIONAL,
semiMajorRangeLength SemiRangeLength,
semiMinorRangeLength SemiRangeLength,
semiMajorRangeOrientation WGS84AngleValue,
semiHeight SemiRangeLength OPTIONAL
}
AreaPolygon ::= SEQUENCE {
polyPointList PolyPointList
}
PolyPointList ::= SEQUENCE (SIZE(3..16, ...)) OF OffsetPoint
AreaRadial ::= SEQUENCE {
range Range,
stationaryHorizontalOpeningAngleStart WGS84AngleValue,
stationaryHorizontalOpeningAngleEnd WGS84AngleValue,
verticalOpeningAngleStart CartesianAngleValue OPTIONAL,
verticalOpeningAngleEnd CartesianAngleValue OPTIONAL,
sensorPositionOffset OffsetPoint OPTIONAL,
sensorHeight SensorHeight OPTIONAL,
...
}
FreeSpaceAddendumContainer ::= SEQUENCE SIZE(1..128, ...) OF FreeSpaceAddendum
FreeSpaceAddendum ::= SEQUENCE {
freeSpaceConfidence FreeSpaceConfidence,
freeSpaceArea FreeSpaceArea,
sensorIDList SensorIdList OPTIONAL,
shadowingApplies ShadowingApplies DEFAULT TRUE,
...
}
FreeSpaceArea ::= CHOICE {
freeSpacePolygon AreaPolygon,
freeSpaceCircular AreaCircular,
freeSpaceEllipse AreaEllipse,
freeSpaceRectangle AreaRectangle,
...
}
ObjectDistanceWithConfidence ::= SEQUENCE {
value DistanceValue,
confidence DistanceConfidence
}
ObjectDimension ::= SEQUENCE {
value ObjectDimensionValue,
confidence ObjectDimensionConfidence
}
CartesianAngle ::= SEQUENCE {
value CartesianAngleValue,
confidence AngleConfidence
}
WGS84Angle ::= SEQUENCE {
value WGS84AngleValue,
confidence AngleConfidence
}
SpeedExtended ::= SEQUENCE {
value SpeedValueExtended,
confidence SpeedConfidence
}
SensorIdList ::= SEQUENCE SIZE(1..128, ...) OF Identifier
TrailerDataContainer ::= SEQUENCE SIZE(1..2) OF TrailerData
TrailerData ::= SEQUENCE {
refPointId RefPointId,
hitchPointOffset HitchPointOffset,
frontOverhang FrontOverhang,
rearOverhang RearOverhang,
trailerWidth VehicleWidth OPTIONAL,
hitchAngle CartesianAngle OPTIONAL,
...
}
LongitudinalLanePosition ::= SEQUENCE {
longitudinalLanePositionValue LongitudinalLanePositionValue,
longitudinalLanePositionConfidence LongitudinalLanePositionConfidence
}
MatchedPosition ::= SEQUENCE {
laneID LaneID OPTIONAL,
longitudinalLanePosition LongitudinalLanePosition OPTIONAL,
...
}
PerceivedObjectContainerSegmentInfo ::= SEQUENCE {
totalMsgSegments SegmentCount,
thisSegmentNum SegmentCount
}
ObjectClassDescription ::= SEQUENCE (SIZE(1..8)) OF ObjectClass
ObjectClass ::= SEQUENCE {
confidence ClassConfidence,
class CHOICE {
vehicle VehicleSubclass,
person PersonSubclass,
animal AnimalSubclass,
other OtherSubclass
}
}
VehicleSubclass ::= SEQUENCE {
type VehicleSubclassType DEFAULT 0,
confidence ClassConfidence DEFAULT 0
}
PersonSubclass ::= SEQUENCE {
type PersonSubclassType DEFAULT 0,
confidence ClassConfidence DEFAULT 0
}
AnimalSubclass ::= SEQUENCE {
type AnimalSubclassType DEFAULT 0,
confidence ClassConfidence DEFAULT 0
}
OtherSubclass ::= SEQUENCE {
type OtherSublassType DEFAULT 0,
confidence ClassConfidence DEFAULT 0
}
OffsetPoint ::= SEQUENCE {
nodeOffsetPointxy NodeOffsetPointXY (WITH COMPONENTS {..., node-LatLon ABSENT, regional ABSENT}),
nodeOffsetPointZ NodeOffsetPointZ OPTIONAL
}
NodeOffsetPointZ ::= CHOICE {
node-Z1 Offset-B10, -- node is within 5.11m of last node
node-Z2 Offset-B11, -- node is within 10.23m of last node
node-Z3 Offset-B12, -- node is within 20.47m of last node
node-Z4 Offset-B13, -- node is within 40.96m of last node
node-Z5 Offset-B14, -- node is within 81.91m of last node
node-Z6 Offset-B16 -- node is within 327.67m of last node
}
AnimalSubclassType ::= INTEGER {unknown(0)} (0..255)
ClassConfidence ::= INTEGER {unknown(0), onePercent(1), oneHundredPercent(100), unavailable(101)} (0..101)
WGS84AngleValue ::= INTEGER {wgs84North(0), wgs84East(900), wgs84South(1800), wgs84West(2700), unavailable(3601)} (0..3601)
CartesianAngleValue ::= INTEGER {zeroPointOneDegree(1), oneDegree(10), unavailable(3601)} (0..3601)
AngleConfidence ::= INTEGER {zeroPointOneDegree (1), oneDegree (10), outOfRange(126), unavailable(127)} (1..127)
SemiRangeLength ::= INTEGER {zeroPointOneMeter(1), oneMeter(10)} (0..10000)
DistanceValue ::= INTEGER {zeroPointZeroOneMeter(1), oneMeter(100)} (-132768..132767)
DistanceConfidence ::= INTEGER {zeroPointZeroOneMeter(1), oneMeter(100), outOfRange(101), unavailable(102)} (0..102)
DynamicStatus ::= INTEGER {dynamic(0), hasBeenDynamic(1), static(2)} (0..2)
HitchPointOffset ::= INTEGER {zeroPointOneMeter(1), oneMeter(10)} (0..100)
FrontOverhang ::= INTEGER {zeroPointOneMeter(1), oneMeter(10)} (0..50)
FreeSpaceConfidence ::= INTEGER {unknown(0), onePercent(1), oneHundredPercent(100), unavailable(101)} (0..101)
LongitudinalLanePositionValue ::= INTEGER {zeroPointOneMeter(1)} (0..32767)
LongitudinalLanePositionConfidence ::= INTEGER {zeroPointZeroOneMeter(1), oneMeter(100), outOfRange(101), unavailable(102)} (0..102)
ObjectAge ::= INTEGER {oneMiliSec(1)} (0..1500)
ObjectConfidence ::= INTEGER {unknown(0), onePercent(1), oneHundredPercent(100), unavailable(101)} (0..101)
ObjectDimensionValue ::= INTEGER {zeroPointOneMeter(1), oneMeter(10)} (0..1023)
ObjectDimensionConfidence ::= INTEGER {zeroPointZeroOneMeter(1), oneMeter(100), outOfRange(101), unavailable(102)} (0..102)
ObjectRefPoint ::= INTEGER {mid(0), bottomLeft(1), midLeft(2), topLeft(3), bottomMid(4), topMid(5), bottomRight(6), midRight(7), topRight(8)} (0..8)
OtherSublassType ::= INTEGER {unknown(0), roadSideUnit(1)} (0..255)
PersonSubclassType ::= INTEGER {unknown(0), pedestrian(1), personInWheelchair(2), cyclist(3), personWithStroller(4), personOnSkates(5), personGroup(6)} (0..255)
Radius ::= INTEGER {zeroPointOneMeter(1), oneMeter(10)} (0..10000)
Range ::= INTEGER {zeroPointOneMeter(1), oneMeter(10)} (0..10000)
RearOverhang ::= INTEGER {zeroPointOneMeter(1), oneMeter(10)} (0..150)
RefPointId ::= INTEGER (0..255)
SensorHeight ::= INTEGER {zeroPointZeroOneMeter(1)} (-5000..5000)
ShadowingApplies ::= BOOLEAN
Identifier ::= INTEGER (0..255)
NumberOfPerceivedObjects ::= INTEGER (0..255)
SensorType ::= INTEGER {undefined(0), radar(1), lidar(2), monovideo(3), stereovision(4), nightvision(5), ultrasonic(6), pmd(7), fusion(8), inductionloop(9), sphericalCamera(10), itssaggregation(11)} (0..15)
SegmentCount ::= INTEGER(1..127)
SpeedValueExtended ::= INTEGER {standstill(0), oneCentimeterPerSec(1),unavailable(16383)} (-16383..16383)
TimeOfMeasurement ::= INTEGER {oneMilliSecond(1)} (-1500..1500)
VehicleSubclassType ::= INTEGER {unknown(0), moped(1), motorcycle(2), passengerCar(3), bus(4), lightTruck(5), heavyTruck(6), trailer(7), specialVehicles(8), tram(9), emergencyVehicle(10), agricultural(11)} (0..255)
XSensorOffset ::= INTEGER {negativeZeroPointZeroOneMeter(-1), negativeOneMeter(-100)} (-5000..0)
YSensorOffset ::= INTEGER {zeroPointZeroOneMeter(1), oneMeter(100)} (-1000..1000)
ZSensorOffset ::= INTEGER {zeroPointZeroOneMeter(1), oneMeter(100)} (0..1000)
END
@@ -1,505 +0,0 @@
ITS-Container {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) version (2)
}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
ItsPduHeader ::= SEQUENCE {
protocolVersion INTEGER (0..255),
messageID INTEGER{ denm(1), cam(2), poi(3), spatem(4), mapem(5), ivim(6), ev-rsr(7), tistpgtransaction(8), srem(9), ssem(10), evcsn(11), saem(12), rtcmem(13) } (0..255), -- Mantis #7209, #7005
stationID StationID
}
StationID ::= INTEGER(0..4294967295)
ReferencePosition ::= SEQUENCE {
latitude Latitude,
longitude Longitude,
positionConfidenceEllipse PosConfidenceEllipse ,
altitude Altitude
}
DeltaReferencePosition ::= SEQUENCE {
deltaLatitude DeltaLatitude,
deltaLongitude DeltaLongitude,
deltaAltitude DeltaAltitude
}
Longitude ::= INTEGER {oneMicrodegreeEast (10), oneMicrodegreeWest (-10), unavailable(1800000001)} (-1800000000..1800000001)
Latitude ::= INTEGER {oneMicrodegreeNorth (10), oneMicrodegreeSouth (-10), unavailable(900000001)} (-900000000..900000001)
Altitude ::= SEQUENCE {
altitudeValue AltitudeValue,
altitudeConfidence AltitudeConfidence
}
AltitudeValue ::= INTEGER {referenceEllipsoidSurface(0), oneCentimeter(1), unavailable(800001)} (-100000..800001)
AltitudeConfidence ::= ENUMERATED {
alt-000-01 (0),
alt-000-02 (1),
alt-000-05 (2),
alt-000-10 (3),
alt-000-20 (4),
alt-000-50 (5),
alt-001-00 (6),
alt-002-00 (7),
alt-005-00 (8),
alt-010-00 (9),
alt-020-00 (10),
alt-050-00 (11),
alt-100-00 (12),
alt-200-00 (13),
outOfRange (14),
unavailable (15)
}
DeltaLongitude ::= INTEGER {oneMicrodegreeEast (10), oneMicrodegreeWest (-10), unavailable(131072)} (-131071..131072)
DeltaLatitude ::= INTEGER {oneMicrodegreeNorth (10), oneMicrodegreeSouth (-10) , unavailable(131072)} (-131071..131072)
DeltaAltitude ::= INTEGER {oneCentimeterUp (1), oneCentimeterDown (-1), unavailable(12800)} (-12700..12800)
PosConfidenceEllipse ::= SEQUENCE {
semiMajorConfidence SemiAxisLength,
semiMinorConfidence SemiAxisLength,
semiMajorOrientation HeadingValue
}
PathPoint ::= SEQUENCE {
pathPosition DeltaReferencePosition,
pathDeltaTime PathDeltaTime OPTIONAL
}
PathDeltaTime ::= INTEGER {tenMilliSecondsInPast(1)} (1..65535, ...)
PtActivation ::= SEQUENCE {
ptActivationType PtActivationType,
ptActivationData PtActivationData
}
PtActivationType ::= INTEGER {undefinedCodingType(0), r09-16CodingType(1), vdv-50149CodingType(2)} (0..255)
PtActivationData ::= OCTET STRING (SIZE(1..20))
AccelerationControl ::= BIT STRING {
brakePedalEngaged (0),
gasPedalEngaged (1),
emergencyBrakeEngaged (2),
collisionWarningEngaged (3),
accEngaged (4),
cruiseControlEngaged (5),
speedLimiterEngaged (6)
} (SIZE(7))
SemiAxisLength ::= INTEGER{oneCentimeter(1), outOfRange(4094), unavailable(4095)} (0..4095)
CauseCode ::= SEQUENCE {
causeCode CauseCodeType,
subCauseCode SubCauseCodeType,
...
}
CauseCodeType ::= INTEGER {
reserved (0),
trafficCondition (1),
accident (2),
roadworks (3),
impassability (5),
adverseWeatherCondition-Adhesion (6),
aquaplannning (7),
hazardousLocation-SurfaceCondition (9),
hazardousLocation-ObstacleOnTheRoad (10),
hazardousLocation-AnimalOnTheRoad (11),
humanPresenceOnTheRoad (12),
wrongWayDriving (14),
rescueAndRecoveryWorkInProgress (15),
adverseWeatherCondition-ExtremeWeatherCondition (17),
adverseWeatherCondition-Visibility (18),
adverseWeatherCondition-Precipitation (19),
slowVehicle (26),
dangerousEndOfQueue (27),
vehicleBreakdown (91),
postCrash (92),
humanProblem (93),
stationaryVehicle (94),
emergencyVehicleApproaching (95),
hazardousLocation-DangerousCurve (96),
collisionRisk (97),
signalViolation (98),
dangerousSituation (99)
} (0..255)
SubCauseCodeType ::= INTEGER (0..255)
TrafficConditionSubCauseCode ::= INTEGER {unavailable(0), increasedVolumeOfTraffic(1), trafficJamSlowlyIncreasing(2), trafficJamIncreasing(3), trafficJamStronglyIncreasing(4), trafficStationary(5), trafficJamSlightlyDecreasing(6), trafficJamDecreasing(7), trafficJamStronglyDecreasing(8)} (0..255)
AccidentSubCauseCode ::= INTEGER {unavailable(0), multiVehicleAccident(1), heavyAccident(2), accidentInvolvingLorry(3), accidentInvolvingBus(4), accidentInvolvingHazardousMaterials(5), accidentOnOppositeLane(6), unsecuredAccident(7), assistanceRequested(8)} (0..255)
RoadworksSubCauseCode ::= INTEGER {unavailable(0), majorRoadworks(1), roadMarkingWork(2), slowMovingRoadMaintenance(3), shortTermStationaryRoadworks(4), streetCleaning(5), winterService(6)} (0..255)
HumanPresenceOnTheRoadSubCauseCode ::= INTEGER {unavailable(0), childrenOnRoadway(1), cyclistOnRoadway(2), motorcyclistOnRoadway(3)} (0..255)
WrongWayDrivingSubCauseCode ::= INTEGER {unavailable(0), wrongLane(1), wrongDirection(2)} (0..255)
AdverseWeatherCondition-ExtremeWeatherConditionSubCauseCode ::= INTEGER {unavailable(0), strongWinds(1), damagingHail(2), hurricane(3), thunderstorm(4), tornado(5), blizzard(6)} (0..255)
AdverseWeatherCondition-AdhesionSubCauseCode ::= INTEGER {unavailable(0), heavyFrostOnRoad(1), fuelOnRoad(2), mudOnRoad(3), snowOnRoad(4), iceOnRoad(5), blackIceOnRoad(6), oilOnRoad(7), looseChippings(8), instantBlackIce(9), roadsSalted(10)} (0..255)
AdverseWeatherCondition-VisibilitySubCauseCode ::= INTEGER {unavailable(0), fog(1), smoke(2), heavySnowfall(3), heavyRain(4), heavyHail(5), lowSunGlare(6), sandstorms(7), swarmsOfInsects(8)} (0..255)
AdverseWeatherCondition-PrecipitationSubCauseCode ::= INTEGER {unavailable(0), heavyRain(1), heavySnowfall(2), softHail(3)} (0..255)
SlowVehicleSubCauseCode ::= INTEGER {unavailable(0), maintenanceVehicle(1), vehiclesSlowingToLookAtAccident(2), abnormalLoad(3), abnormalWideLoad(4), convoy(5), snowplough(6), deicing(7), saltingVehicles(8)} (0..255)
StationaryVehicleSubCauseCode ::= INTEGER {unavailable(0), humanProblem(1), vehicleBreakdown(2), postCrash(3), publicTransportStop(4), carryingDangerousGoods(5)} (0..255)
HumanProblemSubCauseCode ::= INTEGER {unavailable(0), glycemiaProblem(1), heartProblem(2)} (0..255)
EmergencyVehicleApproachingSubCauseCode ::= INTEGER {unavailable(0), emergencyVehicleApproaching(1), prioritizedVehicleApproaching(2)} (0..255)
HazardousLocation-DangerousCurveSubCauseCode ::= INTEGER {unavailable(0), dangerousLeftTurnCurve(1), dangerousRightTurnCurve(2), multipleCurvesStartingWithUnknownTurningDirection(3), multipleCurvesStartingWithLeftTurn(4), multipleCurvesStartingWithRightTurn(5)} (0..255)
HazardousLocation-SurfaceConditionSubCauseCode ::= INTEGER {unavailable(0), rockfalls(1), earthquakeDamage(2), sewerCollapse(3), subsidence(4), snowDrifts(5), stormDamage(6), burstPipe(7), volcanoEruption(8), fallingIce(9)} (0..255)
HazardousLocation-ObstacleOnTheRoadSubCauseCode ::= INTEGER {unavailable(0), shedLoad(1), partsOfVehicles(2), partsOfTyres(3), bigObjects(4), fallenTrees(5), hubCaps(6), waitingVehicles(7)} (0..255)
HazardousLocation-AnimalOnTheRoadSubCauseCode ::= INTEGER {unavailable(0), wildAnimals(1), herdOfAnimals(2), smallAnimals(3), largeAnimals(4)} (0..255)
CollisionRiskSubCauseCode ::= INTEGER {unavailable(0), longitudinalCollisionRisk(1), crossingCollisionRisk(2), lateralCollisionRisk(3), vulnerableRoadUser(4)} (0..255)
SignalViolationSubCauseCode ::= INTEGER {unavailable(0), stopSignViolation(1), trafficLightViolation(2), turningRegulationViolation(3)} (0..255)
RescueAndRecoveryWorkInProgressSubCauseCode ::= INTEGER {unavailable(0), emergencyVehicles(1), rescueHelicopterLanding(2), policeActivityOngoing(3), medicalEmergencyOngoing(4), childAbductionInProgress(5)} (0..255)
DangerousEndOfQueueSubCauseCode ::= INTEGER {unavailable(0), suddenEndOfQueue(1), queueOverHill(2), queueAroundBend(3), queueInTunnel(4)} (0..255)
DangerousSituationSubCauseCode ::= INTEGER {unavailable(0), emergencyElectronicBrakeEngaged(1), preCrashSystemEngaged(2), espEngaged(3), absEngaged(4), aebEngaged(5), brakeWarningEngaged(6), collisionRiskWarningEngaged(7)} (0..255)
VehicleBreakdownSubCauseCode ::= INTEGER {unavailable(0), lackOfFuel (1), lackOfBatteryPower (2), engineProblem(3), transmissionProblem(4), engineCoolingProblem(5), brakingSystemProblem(6), steeringProblem(7), tyrePuncture(8), tyrePressureProblem(9)} (0..255)
PostCrashSubCauseCode ::= INTEGER {unavailable(0), accidentWithoutECallTriggered (1), accidentWithECallManuallyTriggered (2), accidentWithECallAutomaticallyTriggered (3), accidentWithECallTriggeredWithoutAccessToCellularNetwork(4)} (0..255)
Curvature ::= SEQUENCE {
curvatureValue CurvatureValue,
curvatureConfidence CurvatureConfidence
}
CurvatureValue ::= INTEGER {straight(0), unavailable(1023)} (-1023..1023)
CurvatureConfidence ::= ENUMERATED {
onePerMeter-0-00002 (0),
onePerMeter-0-0001 (1),
onePerMeter-0-0005 (2),
onePerMeter-0-002 (3),
onePerMeter-0-01 (4),
onePerMeter-0-1 (5),
outOfRange (6),
unavailable (7)
}
CurvatureCalculationMode ::= ENUMERATED {yawRateUsed(0), yawRateNotUsed(1), unavailable(2), ...}
Heading ::= SEQUENCE {
headingValue HeadingValue,
headingConfidence HeadingConfidence
}
HeadingValue ::= INTEGER {wgs84North(0), wgs84East(900), wgs84South(1800), wgs84West(2700), unavailable(3601)} (0..3601)
HeadingConfidence ::= INTEGER {equalOrWithinZeroPointOneDegree (1), equalOrWithinOneDegree (10), outOfRange(126), unavailable(127)} (1..127)
LanePosition ::= INTEGER {offTheRoad(-1), innerHardShoulder(0), innermostDrivingLane(1), secondLaneFromInside(2), outerHardShoulder(14)} (-1..14)
ClosedLanes ::= SEQUENCE {
innerhardShoulderStatus HardShoulderStatus OPTIONAL,
outerhardShoulderStatus HardShoulderStatus OPTIONAL,
drivingLaneStatus DrivingLaneStatus OPTIONAL,
...
}
HardShoulderStatus ::= ENUMERATED {availableForStopping(0), closed(1), availableForDriving(2)}
DrivingLaneStatus ::= BIT STRING (SIZE (1..13))
PerformanceClass ::= INTEGER {unavailable(0), performanceClassA(1), performanceClassB(2)} (0..7)
SpeedValue ::= INTEGER {standstill(0), oneCentimeterPerSec(1), unavailable(16383)} (0..16383)
SpeedConfidence ::= INTEGER {equalOrWithinOneCentimeterPerSec(1), equalOrWithinOneMeterPerSec(100), outOfRange(126), unavailable(127)} (1..127)
VehicleMass ::= INTEGER {hundredKg(1), unavailable(1024)} (1..1024)
Speed ::= SEQUENCE {
speedValue SpeedValue,
speedConfidence SpeedConfidence
}
DriveDirection ::= ENUMERATED {forward (0), backward (1), unavailable (2)}
EmbarkationStatus ::= BOOLEAN
LongitudinalAcceleration ::= SEQUENCE {
longitudinalAccelerationValue LongitudinalAccelerationValue,
longitudinalAccelerationConfidence AccelerationConfidence
}
LongitudinalAccelerationValue ::= INTEGER {pointOneMeterPerSecSquaredForward(1), pointOneMeterPerSecSquaredBackward(-1), unavailable(161)} (-160 .. 161)
AccelerationConfidence ::= INTEGER {pointOneMeterPerSecSquared(1), outOfRange(101), unavailable(102)} (0 .. 102)
LateralAcceleration ::= SEQUENCE {
lateralAccelerationValue LateralAccelerationValue,
lateralAccelerationConfidence AccelerationConfidence
}
LateralAccelerationValue ::= INTEGER {pointOneMeterPerSecSquaredToRight(-1), pointOneMeterPerSecSquaredToLeft(1), unavailable(161)} (-160 .. 161)
VerticalAcceleration ::= SEQUENCE {
verticalAccelerationValue VerticalAccelerationValue,
verticalAccelerationConfidence AccelerationConfidence
}
VerticalAccelerationValue ::= INTEGER {pointOneMeterPerSecSquaredUp(1), pointOneMeterPerSecSquaredDown(-1), unavailable(161)} (-160 .. 161)
StationType ::= INTEGER {unknown(0), pedestrian(1), cyclist(2), moped(3), motorcycle(4), passengerCar(5), bus(6), lightTruck(7), heavyTruck(8), trailer(9), specialVehicles(10), tram(11), roadSideUnit(15)} (0..255)
ExteriorLights ::= BIT STRING {
lowBeamHeadlightsOn (0),
highBeamHeadlightsOn (1),
leftTurnSignalOn (2),
rightTurnSignalOn (3),
daytimeRunningLightsOn (4),
reverseLightOn (5),
fogLightOn (6),
parkingLightsOn (7)
} (SIZE(8))
DangerousGoodsBasic::= ENUMERATED {
explosives1(0),
explosives2(1),
explosives3(2),
explosives4(3),
explosives5(4),
explosives6(5),
flammableGases(6),
nonFlammableGases(7),
toxicGases(8),
flammableLiquids(9),
flammableSolids(10),
substancesLiableToSpontaneousCombustion(11),
substancesEmittingFlammableGasesUponContactWithWater(12),
oxidizingSubstances(13),
organicPeroxides(14),
toxicSubstances(15),
infectiousSubstances(16),
radioactiveMaterial(17),
corrosiveSubstances(18),
miscellaneousDangerousSubstances(19)
}
DangerousGoodsExtended ::= SEQUENCE {
dangerousGoodsType DangerousGoodsBasic,
unNumber INTEGER (0..9999),
elevatedTemperature BOOLEAN,
tunnelsRestricted BOOLEAN,
limitedQuantity BOOLEAN,
emergencyActionCode IA5String (SIZE (1..24)) OPTIONAL,
phoneNumber PhoneNumber OPTIONAL,
companyName UTF8String (SIZE (1..24)) OPTIONAL,
...
}
SpecialTransportType ::= BIT STRING {heavyLoad(0), excessWidth(1), excessLength(2), excessHeight(3)} (SIZE(4))
LightBarSirenInUse ::= BIT STRING {
lightBarActivated (0),
sirenActivated (1)
} (SIZE(2))
HeightLonCarr ::= INTEGER {oneCentimeter(1), unavailable(100)} (1..100)
PosLonCarr ::= INTEGER {oneCentimeter(1), unavailable(127)} (1..127)
PosPillar ::= INTEGER {tenCentimeters(1), unavailable(30)} (1..30)
PosCentMass ::= INTEGER {tenCentimeters(1), unavailable(63)} (1..63)
RequestResponseIndication ::= ENUMERATED {request(0), response(1)}
SpeedLimit ::= INTEGER {oneKmPerHour(1)} (1..255)
StationarySince ::= ENUMERATED {lessThan1Minute(0), lessThan2Minutes(1), lessThan15Minutes(2), equalOrGreater15Minutes(3)}
Temperature ::= INTEGER {equalOrSmallerThanMinus60Deg (-60), oneDegreeCelsius(1), equalOrGreaterThan67Deg(67)} (-60..67)
TrafficRule ::= ENUMERATED {noPassing(0), noPassingForTrucks(1), passToRight(2), passToLeft(3), ...}
WheelBaseVehicle ::= INTEGER {tenCentimeters(1), unavailable(127)} (1..127)
TurningRadius ::= INTEGER {point4Meters(1), unavailable(255)} (1..255)
PosFrontAx ::= INTEGER {tenCentimeters(1), unavailable(20)} (1..20)
PositionOfOccupants ::= BIT STRING {
row1LeftOccupied (0),
row1RightOccupied (1),
row1MidOccupied (2),
row1NotDetectable (3),
row1NotPresent (4),
row2LeftOccupied (5),
row2RightOccupied (6),
row2MidOccupied (7),
row2NotDetectable (8),
row2NotPresent (9),
row3LeftOccupied (10),
row3RightOccupied (11),
row3MidOccupied (12),
row3NotDetectable (13),
row3NotPresent (14),
row4LeftOccupied (15),
row4RightOccupied (16),
row4MidOccupied (17),
row4NotDetectable (18),
row4NotPresent (19)} (SIZE(20))
PositioningSolutionType ::= ENUMERATED {noPositioningSolution(0), sGNSS(1), dGNSS(2), sGNSSplusDR(3), dGNSSplusDR(4), dR(5), ...}
VehicleIdentification ::= SEQUENCE {
wMInumber WMInumber OPTIONAL,
vDS VDS OPTIONAL,
...
}
WMInumber ::= IA5String (SIZE(1..3))
VDS ::= IA5String (SIZE(6))
EnergyStorageType ::= BIT STRING {hydrogenStorage(0), electricEnergyStorage(1), liquidPropaneGas(2), compressedNaturalGas(3), diesel(4), gasoline(5), ammonia(6)} (SIZE(7))
VehicleLength ::= SEQUENCE {
vehicleLengthValue VehicleLengthValue,
vehicleLengthConfidenceIndication VehicleLengthConfidenceIndication
}
VehicleLengthValue ::= INTEGER {tenCentimeters(1), outOfRange(1022), unavailable(1023)} (1..1023)
VehicleLengthConfidenceIndication ::= ENUMERATED {noTrailerPresent(0), trailerPresentWithKnownLength(1), trailerPresentWithUnknownLength(2), trailerPresenceIsUnknown(3), unavailable(4)}
VehicleWidth ::= INTEGER {tenCentimeters(1), outOfRange(61), unavailable(62)} (1..62)
PathHistory::= SEQUENCE (SIZE(0..40)) OF PathPoint
EmergencyPriority ::= BIT STRING {requestForRightOfWay(0), requestForFreeCrossingAtATrafficLight(1)} (SIZE(2))
InformationQuality ::= INTEGER {unavailable(0), lowest(1), highest(7)} (0..7)
RoadType ::= ENUMERATED {
urban-NoStructuralSeparationToOppositeLanes(0),
urban-WithStructuralSeparationToOppositeLanes(1),
nonUrban-NoStructuralSeparationToOppositeLanes(2),
nonUrban-WithStructuralSeparationToOppositeLanes(3)}
SteeringWheelAngle ::= SEQUENCE {
steeringWheelAngleValue SteeringWheelAngleValue,
steeringWheelAngleConfidence SteeringWheelAngleConfidence
}
SteeringWheelAngleValue ::= INTEGER {straight(0), onePointFiveDegreesToRight(-1), onePointFiveDegreesToLeft(1), unavailable(512)} (-511..512)
SteeringWheelAngleConfidence ::= INTEGER {equalOrWithinOnePointFiveDegree (1), outOfRange(126), unavailable(127)} (1..127)
TimestampIts ::= INTEGER {utcStartOf2004(0), oneMillisecAfterUTCStartOf2004(1)} (0..4398046511103)
VehicleRole ::= ENUMERATED {default(0), publicTransport(1), specialTransport(2), dangerousGoods(3), roadWork(4), rescue(5), emergency(6), safetyCar(7), agriculture(8),commercial(9),military(10),roadOperator(11),taxi(12), reserved1(13), reserved2(14), reserved3(15)}
YawRate::= SEQUENCE {
yawRateValue YawRateValue,
yawRateConfidence YawRateConfidence
}
YawRateValue ::= INTEGER {straight(0), degSec-000-01ToRight(-1), degSec-000-01ToLeft(1), unavailable(32767)} (-32766..32767)
YawRateConfidence ::= ENUMERATED {
degSec-000-01 (0),
degSec-000-05 (1),
degSec-000-10 (2),
degSec-001-00 (3),
degSec-005-00 (4),
degSec-010-00 (5),
degSec-100-00 (6),
outOfRange (7),
unavailable (8)
}
ProtectedZoneType::= ENUMERATED { permanentCenDsrcTolling (0), ..., temporaryCenDsrcTolling (1) }
RelevanceDistance ::= ENUMERATED {lessThan50m(0), lessThan100m(1), lessThan200m(2), lessThan500m(3), lessThan1000m(4), lessThan5km(5), lessThan10km(6), over10km(7)}
RelevanceTrafficDirection ::= ENUMERATED {allTrafficDirections(0), upstreamTraffic(1), downstreamTraffic(2), oppositeTraffic(3)}
TransmissionInterval ::= INTEGER {oneMilliSecond(1), tenSeconds(10000)} (1..10000)
ValidityDuration ::= INTEGER {timeOfDetection(0), oneSecondAfterDetection(1)} (0..86400)
ActionID ::= SEQUENCE {
originatingStationID StationID,
sequenceNumber SequenceNumber
}
ItineraryPath ::= SEQUENCE SIZE(1..40) OF ReferencePosition
ProtectedCommunicationZone ::= SEQUENCE {
protectedZoneType ProtectedZoneType,
expiryTime TimestampIts OPTIONAL,
protectedZoneLatitude Latitude,
protectedZoneLongitude Longitude,
protectedZoneRadius ProtectedZoneRadius OPTIONAL,
protectedZoneID ProtectedZoneID OPTIONAL,
...
}
Traces ::= SEQUENCE SIZE(1..7) OF PathHistory
NumberOfOccupants ::= INTEGER {oneOccupant (1), unavailable(127)} (0..127)
SequenceNumber ::= INTEGER (0..65535)
PositionOfPillars ::= SEQUENCE (SIZE(1..3, ...)) OF PosPillar
RestrictedTypes ::= SEQUENCE (SIZE(1..3, ...)) OF StationType
EventHistory::= SEQUENCE (SIZE(1..23)) OF EventPoint
EventPoint ::= SEQUENCE {
eventPosition DeltaReferencePosition,
eventDeltaTime PathDeltaTime OPTIONAL,
informationQuality InformationQuality
}
ProtectedCommunicationZonesRSU ::= SEQUENCE (SIZE(1..16)) OF ProtectedCommunicationZone
CenDsrcTollingZone ::= SEQUENCE {
protectedZoneLatitude Latitude,
protectedZoneLongitude Longitude,
cenDsrcTollingZoneID CenDsrcTollingZoneID OPTIONAL,
...
}
ProtectedZoneRadius ::= INTEGER {oneMeter(1)} (1..255, ...)
ProtectedZoneID ::= INTEGER (0..134217727)
CenDsrcTollingZoneID ::= ProtectedZoneID
DigitalMap ::= SEQUENCE (SIZE(1..256)) OF ReferencePosition
OpeningDaysHours ::= UTF8String
PhoneNumber ::= NumericString (SIZE(1..16))
END
@@ -1,73 +0,0 @@
EtsiTs102941BaseTypes
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) version2(2) }
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
HashedId8, Time32, PublicEncryptionKey, PublicVerificationKey, Signature
FROM
IEEE1609dot2BaseTypes {iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) base(1) base-types(2) major-version-2(2)}
CertificateId, SubjectAssurance, SequenceOfPsidSsp, SequenceOfPsidGroupPermissions,
ValidityPeriod, GeographicRegion
FROM
IEEE1609dot2 {iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) base (1) schema (1) major-version-2(2)}
EtsiTs103097Data, EtsiTs103097Data-Encrypted, EtsiTs103097Data-Signed, EtsiTs103097Data-SignedExternalPayload
FROM
EtsiTs103097Module {itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(103097) securedMessageV1(0)}
;
CertificateFormat::= INTEGER {
ts103097v131 (1)
}(1..255)
CertificateSubjectAttributes ::= SEQUENCE {
id CertificateId OPTIONAL,
validityPeriod ValidityPeriod OPTIONAL,
region GeographicRegion OPTIONAL,
assuranceLevel SubjectAssurance OPTIONAL,
appPermissions SequenceOfPsidSsp OPTIONAL,
certIssuePermissions SequenceOfPsidGroupPermissions OPTIONAL,
...
}(WITH COMPONENTS { ..., appPermissions PRESENT} | WITH COMPONENTS { ..., certIssuePermissions PRESENT})
EcSignature::= CHOICE {
encryptedEcSignature EtsiTs103097Data-Encrypted{EtsiTs103097Data-SignedExternalPayload},
ecSignature EtsiTs103097Data-SignedExternalPayload
}
PublicKeys ::= SEQUENCE {
verificationKey PublicVerificationKey,
encryptionKey PublicEncryptionKey OPTIONAL
}
Version ::= INTEGER {v1(1)}
EtsiTs103097Data-Unsecured {ToBeSentDataContent} ::= EtsiTs103097Data
(WITH COMPONENTS {...,
content (WITH COMPONENTS {
unsecuredData (CONTAINING ToBeSentDataContent)
})
})
EtsiTs103097Data-Encrypted-Unicast {ToBeEncryptedDataContent} ::= EtsiTs103097Data-Encrypted { EtsiTs103097Data-Unsecured{ToBeEncryptedDataContent}}
(WITH COMPONENTS {...,
content (WITH COMPONENTS {
encryptedData (WITH COMPONENTS {...,
recipients (SIZE(1))
})
})
})
EtsiTs103097Data-SignedAndEncrypted-Unicast {ToBesignedAndEncryptedDataContent} ::= EtsiTs103097Data-Encrypted {EtsiTs103097Data-Signed {ToBesignedAndEncryptedDataContent}}
(WITH COMPONENTS {...,
content (WITH COMPONENTS {
encryptedData (WITH COMPONENTS {...,
recipients (SIZE(1))
})
})
})
END
@@ -1,77 +0,0 @@
/*************************************************************************************
This file contains the EtsiTs102941Messages module containing all possible PKI messages.
It should be used when all PKI messages needs to be implemented (for example, for CA development)
**************************************************************************************/
EtsiTs102941MessagesCa
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) messagesCa(0) version2(2) }
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Data-Signed,
--EtsiTs103097Data-Encrypted,
EtsiTs103097Data-SignedExternalPayload
--EtsiTs103097Data-SignedAndEncrypted
FROM EtsiTs103097Module { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(103097) securedMessageV1(0) }
Version, EtsiTs103097Data-Encrypted-Unicast, EtsiTs103097Data-SignedAndEncrypted-Unicast
FROM EtsiTs102941BaseTypes { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) version2(2) }
InnerEcRequestSignedForPop, InnerEcResponse
FROM EtsiTs102941TypesEnrolment { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) enrolment(4) version2(2) }
InnerAtRequest, InnerAtResponse
FROM EtsiTs102941TypesAuthorization { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authorization(5) version2(2) }
ToBeSignedCrl, ToBeSignedTlmCtl, ToBeSignedRcaCtl
FROM EtsiTs102941TrustLists { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) trustLists(6) version2(2) }
AuthorizationValidationRequest, AuthorizationValidationResponse
FROM EtsiTs102941TypesAuthorizationValidation { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authValidation(7) version2(2) }
CaCertificateRequest
FROM EtsiTs102941TypesCaManagement { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) caManagement(8) version2(2) }
;
/************
-- Messages
************/
EnrolmentRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{enrolmentRequest PRESENT})})}
EnrolmentResponseMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{enrolmentResponse PRESENT})})}
AuthorizationRequestMessage ::= EtsiTs103097Data-Encrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationRequest PRESENT})})}
AuthorizationRequestMessageWithPop ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationRequest PRESENT})})}
AuthorizationResponseMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationResponse PRESENT})})}
CertificateRevocationListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateRevocationList PRESENT})})}
TlmCertificateTrustListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateTrustListTlm PRESENT})})}
RcaCertificateTrustListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateTrustListRca PRESENT})})}
AuthorizationValidationRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationValidationRequest PRESENT})})}
AuthorizationValidationResponseMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationValidationResponse PRESENT})})}
CaCertificateRequestMessage ::= EtsiTs103097Data-Signed {EtsiTs102941Data(WITH COMPONENTS{..., content (WITH COMPONENTS{caCertificateRequest PRESENT})})}
CaCertificateRekeyingMessage ::= EtsiTs103097Data-Signed {EtsiTs103097Data-Signed {EtsiTs102941Data(WITH COMPONENTS{..., content (WITH COMPONENTS{caCertificateRequest PRESENT})})}}
/************
-- EtsiTs102941Data
************/
EtsiTs102941Data::= SEQUENCE {
version Version (v1),
content EtsiTs102941DataContent
}
EtsiTs102941DataContent ::= CHOICE {
enrolmentRequest InnerEcRequestSignedForPop,
enrolmentResponse InnerEcResponse,
authorizationRequest InnerAtRequest,
authorizationResponse InnerAtResponse,
certificateRevocationList ToBeSignedCrl,
certificateTrustListTlm ToBeSignedTlmCtl,
certificateTrustListRca ToBeSignedRcaCtl,
authorizationValidationRequest AuthorizationValidationRequest,
authorizationValidationResponse AuthorizationValidationResponse,
caCertificateRequest CaCertificateRequest,
...
}
END
@@ -1,138 +0,0 @@
EtsiTs102941TrustLists
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) trustLists(6) version2(2) }
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Certificate, EtsiTs103097Data-SignedAndEncrypted, EtsiTs103097Data-Signed
FROM EtsiTs103097Module {itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(103097) securedMessageV1(0)}
HashedId8, Time32, Version --, CertificateAuthorityConstraints
FROM EtsiTs102941BaseTypes {itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) version2(2)}
;
/************
-- CRL
************/
ToBeSignedCrl ::= SEQUENCE {
version Version,
thisUpdate Time32,
nextUpdate Time32,
entries SEQUENCE OF CrlEntry,
...
}
CrlEntry ::= HashedId8
/************
-- TLM CTL
************/
ToBeSignedTlmCtl ::= CtlFormat (FullCtl | DeltaCtl) (WITH COMPONENTS {...,
ctlCommands ( WITH COMPONENT(
( WITH COMPONENTS {...,
add ( WITH COMPONENTS {...,
ea ABSENT,
aa ABSENT
})
})
))
})
/************
-- RCA CTL
************/
ToBeSignedRcaCtl ::= CtlFormat (FullCtl | DeltaCtl) ( WITH COMPONENTS {...,
ctlCommands ( WITH COMPONENT(
( WITH COMPONENTS {...,
add ( WITH COMPONENTS {...,
rca ABSENT,
tlm ABSENT
})
})
))
})
/************
-- CTL
************/
FullCtl::= CtlFormat ( WITH COMPONENTS {...,
isFullCtl ( TRUE ),
ctlCommands ( WITH COMPONENT(
( WITH COMPONENTS {...,
delete ABSENT
})
))
})
DeltaCtl::= CtlFormat (WITH COMPONENTS {...,
isFullCtl(FALSE)
})
CtlFormat ::= SEQUENCE {
version Version,
nextUpdate Time32,
isFullCtl BOOLEAN,
ctlSequence INTEGER (0..255),
ctlCommands SEQUENCE OF CtlCommand,
...
}
CtlCommand ::= CHOICE {
add CtlEntry,
delete CtlDelete,
...
}
CtlEntry ::= CHOICE {
rca RootCaEntry,
ea EaEntry,
aa AaEntry,
dc DcEntry,
tlm TlmEntry,
...
}
CtlDelete ::= CHOICE {
cert HashedId8,
dc DcDelete,
...
}
TlmEntry::= SEQUENCE {
selfSignedTLMCertificate EtsiTs103097Certificate,
linkTLMCertificate EtsiTs103097Certificate OPTIONAL,
accessPoint Url
}
RootCaEntry ::= SEQUENCE {
selfsignedRootCa EtsiTs103097Certificate,
linkRootCaCertificate EtsiTs103097Certificate OPTIONAL
}
EaEntry ::= SEQUENCE {
eaCertificate EtsiTs103097Certificate,
aaAccessPoint Url,
itsAccessPoint Url OPTIONAL
}
AaEntry ::= SEQUENCE {
aaCertificate EtsiTs103097Certificate,
accessPoint Url
}
DcEntry ::= SEQUENCE {
url Url,
cert SEQUENCE OF HashedId8
}
DcDelete ::= Url
Url::= IA5String
END
@@ -1,80 +0,0 @@
EtsiTs102941TypesAuthorization
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authorization(5) version2(2) }
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Certificate, EtsiTs103097Data-Signed
FROM EtsiTs103097Module { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(103097) securedMessageV1(0)}
CertificateFormat, CertificateSubjectAttributes, EcSignature, HashedId8, PublicKeys, Version
FROM EtsiTs102941BaseTypes { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) version2(2) }
;
/************
-- AuthorizationRequest/Response
************/
AuthorizationResponseCode ::= ENUMERATED {
ok(0),
-- ITS->AA
its-aa-cantparse, -- valid for any structure
its-aa-badcontenttype, -- not encrypted, not signed, not authorizationrequest
its-aa-imnottherecipient, -- the "recipients" of the outermost encrypted data doesn't include me
its-aa-unknownencryptionalgorithm, -- either kexalg or contentencryptionalgorithm
its-aa-decryptionfailed, -- works for ECIES-HMAC and AES-CCM
its-aa-keysdontmatch, -- HMAC keyTag verification fails
its-aa-incompleterequest, -- some elements are missing
its-aa-invalidencryptionkey, -- the responseEncryptionKey is bad
its-aa-outofsyncrequest, -- signingTime is outside acceptable limits
its-aa-unknownea, -- the EA identified by eaId is unknown to me
its-aa-invalidea, -- the EA certificate is revoked
its-aa-deniedpermissions, -- I, the AA, deny the requested permissions
-- AA->EA
aa-ea-cantreachea, -- the EA is unreachable (network error?)
-- EA->AA
ea-aa-cantparse, -- valid for any structure
ea-aa-badcontenttype, -- not encrypted, not signed, not authorizationrequest
ea-aa-imnottherecipient, -- the "recipients" of the outermost encrypted data doesn't include me
ea-aa-unknownencryptionalgorithm, -- either kexalg or contentencryptionalgorithm
ea-aa-decryptionfailed, -- works for ECIES-HMAC and AES-CCM
-- TODO: to be continued...
invalidaa, -- the AA certificate presented is invalid/revoked/whatever
invalidaasignature, -- the AA certificate presented can't validate the request signature
wrongea, -- the encrypted signature doesn't designate me as the EA
unknownits, -- can't retrieve the EC/ITS in my DB
invalidsignature, -- signature verification of the request by the EC fails
invalidencryptionkey, -- signature is good, but the key is bad
deniedpermissions, -- permissions not granted
deniedtoomanycerts, -- parallel limit
... }
InnerAtRequest ::= SEQUENCE {
publicKeys PublicKeys,
hmacKey OCTET STRING (SIZE(32)),
sharedAtRequest SharedAtRequest,
ecSignature EcSignature,
...
}
SharedAtRequest ::= SEQUENCE {
eaId HashedId8,
keyTag OCTET STRING (SIZE(16)),
certificateFormat CertificateFormat,
requestedSubjectAttributes CertificateSubjectAttributes (WITH COMPONENTS{..., certIssuePermissions ABSENT}),
...
}
InnerAtResponse ::= SEQUENCE {
requestHash OCTET STRING (SIZE(16)),
responseCode AuthorizationResponseCode,
certificate EtsiTs103097Certificate OPTIONAL,
...
}
(WITH COMPONENTS { responseCode (ok), certificate PRESENT }
| WITH COMPONENTS { responseCode (ALL EXCEPT ok), certificate ABSENT }
)
END
@@ -1,58 +0,0 @@
EtsiTs102941TypesAuthorizationValidation
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authValidation(7) version2(2) }
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Certificate
FROM EtsiTs103097Module { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(103097) securedMessageV1(0) }
CertificateFormat, CertificateSubjectAttributes,EcSignature, HashedId8, PublicKeys, Version
FROM EtsiTs102941BaseTypes { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) version2(2) }
SharedAtRequest
FROM EtsiTs102941TypesAuthorization { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authorization(5) version2(2) }
;
/************
-- AuthorizationValidationRequest/Response
************/
AuthorizationValidationResponseCode ::= ENUMERATED {
ok(0),
cantparse, -- valid for any structure
badcontenttype, -- not encrypted, not signed, not permissionsverificationrequest
imnottherecipient, -- the "recipients" of the outermost encrypted data doesn't include me
unknownencryptionalgorithm, -- either kexalg or contentencryptionalgorithm
decryptionfailed, -- works for ECIES-HMAC and AES-CCM
invalidaa, -- the AA certificate presented is invalid/revoked/whatever
invalidaasignature, -- the AA certificate presented can't validate the request signature
wrongea, -- the encrypted signature doesn't designate me as the EA
unknownits, -- can't retrieve the EC/ITS in my DB
invalidsignature, -- signature verification of the request by the EC fails
invalidencryptionkey, -- signature is good, but the responseEncryptionKey is bad
deniedpermissions, -- requested permissions not granted
deniedtoomanycerts, -- parallel limit
deniedrequest, -- any other reason?
...
}
AuthorizationValidationRequest ::= SEQUENCE {
sharedAtRequest SharedAtRequest,
ecSignature EcSignature,
...
}
AuthorizationValidationResponse ::= SEQUENCE {
requestHash OCTET STRING (SIZE(16)),
responseCode AuthorizationValidationResponseCode,
confirmedSubjectAttributes CertificateSubjectAttributes (WITH COMPONENTS{..., certIssuePermissions ABSENT}) OPTIONAL,
...
}
(WITH COMPONENTS { responseCode (ok), confirmedSubjectAttributes PRESENT }
| WITH COMPONENTS { responseCode (ALL EXCEPT ok), confirmedSubjectAttributes ABSENT }
)
END
@@ -1,26 +0,0 @@
EtsiTs102941TypesCaManagement
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) caManagement(8) version2(2) }
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Certificate, EtsiTs103097Data-Signed
FROM EtsiTs103097Module {itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(103097) securedMessageV1(0) }
PublicKeys, CertificateSubjectAttributes
FROM EtsiTs102941BaseTypes { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) version2(2) }
;
/************
-- CA certificate request
************/
CaCertificateRequest ::= SEQUENCE {
publicKeys PublicKeys,
requestedSubjectAttributes CertificateSubjectAttributes,
...
}
END
@@ -1,58 +0,0 @@
EtsiTs102941TypesEnrolment
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) enrolment(4) version2(2) }
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Certificate, EtsiTs103097Data-Signed
FROM EtsiTs103097Module { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(103097) securedMessageV1(0) }
CertificateFormat, CertificateSubjectAttributes, EcSignature, HashedId8, PublicKeys, Version
FROM EtsiTs102941BaseTypes { itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) version2(2) }
;
/************
-- EnrolmentRequest/Response
************/
EnrolmentResponseCode ::= ENUMERATED {
ok(0),
cantparse, -- valid for any structure
badcontenttype, -- not encrypted, not signed, not enrolmentrequest
imnottherecipient, -- the "recipients" doesn't include me
unknownencryptionalgorithm, -- either kexalg or contentencryptionalgorithm
decryptionfailed, -- works for ECIES-HMAC and AES-CCM
unknownits, -- can't retrieve the ITS from the itsId
invalidsignature, -- signature verification of the request fails
invalidencryptionkey, -- signature is good, but the responseEncryptionKey is bad
baditsstatus, -- revoked, not yet active
incompleterequest, -- some elements are missing
deniedpermissions, -- requested permissions are not granted
invalidkeys, -- either the verification_key of the encryption_key is bad
deniedrequest, -- any other reason?
...
}
InnerEcRequestSignedForPop::= EtsiTs103097Data-Signed{InnerEcRequest}
InnerEcRequest ::= SEQUENCE {
itsId OCTET STRING,
certificateFormat CertificateFormat,
publicKeys PublicKeys,
requestedSubjectAttributes CertificateSubjectAttributes (WITH COMPONENTS{..., certIssuePermissions ABSENT}),
...
}
InnerEcResponse ::= SEQUENCE {
requestHash OCTET STRING (SIZE(16)),
responseCode EnrolmentResponseCode,
certificate EtsiTs103097Certificate OPTIONAL,
...
}
(WITH COMPONENTS { responseCode (ok), certificate PRESENT }
| WITH COMPONENTS { responseCode (ALL EXCEPT ok), certificate ABSENT }
)
END
@@ -1,100 +0,0 @@
EtsiTs103097Module {
itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(103097) v1(0)
}
DEFINITIONS AUTOMATIC TAGS ::= BEGIN
IMPORTS
Ieee1609Dot2Data, ExplicitCertificate
FROM IEEE1609dot2 {
iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) base (1) schema (1) major-version-2(2)
};
EtsiTs103097Certificate ::= ExplicitCertificate (WITH COMPONENTS {...,
toBeSigned (WITH COMPONENTS {...,
id (WITH COMPONENTS {...,
linkageData ABSENT,
binaryId ABSENT
}),
certRequestPermissions ABSENT,
canRequestRollover ABSENT
})
})
SingleEtsiTs103097Certificate ::= SEQUENCE {
only EtsiTs103097Certificate
}
EtsiTs103097Data ::= Ieee1609Dot2Data (WITH COMPONENTS {...,
content (WITH COMPONENTS {...,
signedData (WITH COMPONENTS {..., -- constraints on signed data headers
tbsData (WITH COMPONENTS {
headerInfo (WITH COMPONENTS {...,
generationTime PRESENT,
p2pcdLearningRequest ABSENT,
missingCrlIdentifier ABSENT
})
}),
signer (WITH COMPONENTS {..., -- constraints on the certificate
certificate (SIZE(1))
})
}),
encryptedData (WITH COMPONENTS {..., -- constraints on encrypted data headers
recipients (WITH COMPONENT (
(WITH COMPONENTS {...,
pskRecipInfo ABSENT,
symmRecipInfo ABSENT,
rekRecipInfo ABSENT
})
))
}),
signedCertificateRequest ABSENT
})
})
EtsiTs103097Data-Signed {ToBeSignedDataContent} ::= EtsiTs103097Data (WITH COMPONENTS {...,
content (WITH COMPONENTS {
signedData (WITH COMPONENTS {...,
tbsData (WITH COMPONENTS {
payload (WITH COMPONENTS {
data (WITH COMPONENTS {...,
content (WITH COMPONENTS {
unsecuredData (CONTAINING ToBeSignedDataContent)
})
}) PRESENT
})
})
})
})
})
EtsiTs103097Data-SignedExternalPayload ::= EtsiTs103097Data (WITH COMPONENTS {...,
content (WITH COMPONENTS {
signedData (WITH COMPONENTS {...,
tbsData (WITH COMPONENTS {
payload (WITH COMPONENTS {
extDataHash (WITH COMPONENTS {
sha256HashedData PRESENT
}) PRESENT
})
})
})
})
})
EtsiTs103097Data-Encrypted {ToBeEncryptedDataContent} ::= EtsiTs103097Data (WITH COMPONENTS {...,
content (WITH COMPONENTS {
encryptedData (WITH COMPONENTS {...,
ciphertext (WITH COMPONENTS {...,
aes128ccm (WITH COMPONENTS {...,
ccmCiphertext (CONSTRAINED BY { -- ccm encryption of -- ToBeEncryptedDataContent})
})
})
})
})
})
EtsiTs103097Data-SignedAndEncrypted {ToBesignedAndEncryptedDataContent} ::= EtsiTs103097Data-Encrypted {EtsiTs103097Data-Signed {ToBesignedAndEncryptedDataContent}}
END
@@ -1,40 +0,0 @@
-- @brief Specification of the In vehicle information Message ETSI TS 103 301
-- Latest version available at @url https://forge.etsi.org/rep/ITS/asn1/is_ts103301.git
IVIM-PDU-Descriptions {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts103301 (103301) ivim (2) version2 (2)
}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
-- @brief Include ISO 19321
-- Include references from @url https://standards.iso.org/iso/ts/19321/ed-2/en/ISO19321IVIv2.asn
IviStructure
FROM IVI { iso (1) standard (0) ivi (19321) version2 (2) }
-- @brief Include ETSI TS 102 894-2
-- Include references from @url https://forge.etsi.org/rep/ITS/ITS_ASN1/blob/master/CDD_TS102894-2/ITS-Container.asn
ItsPduHeader
FROM ITS-Container { itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) version (2)};
-- In vehicle information Message Message
-- @brief In vehicle information Message Root
-- This DF includes DEs for the IVIM protocolVersion, the IVI message type identifier _messageID_,
-- the station identifier _stationID_ of the originating ITS-S and the IVI data from ISO TS 19321.
IVIM ::= SEQUENCE {
-- @details header
-- The DE _protocolVersion_ is used to select the appropriate protocol decoder at the receiving ITS-S.
-- It shall be set to 2.
-- The DE _messageID_ shall be ivim(6).
header ItsPduHeader,
-- @details ivi
-- The DE _ivi_ contains the IVI data as defined in ISO TS 19321.
ivi IviStructure
}
END
@@ -1,39 +0,0 @@
-- @brief Specification of the Map (lane topology) extended Message ETSI TS 103 301
-- Latest version available at @url https://forge.etsi.org/rep/ITS/asn1/is_ts103301.git
MAPEM-PDU-Descriptions {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts103301 (103301) mapem (1) version2 (2)
}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
-- @brief Include ISO TS 19091
-- Include references from @url https://standards.iso.org/iso/ts/19091/ed-2/en/ISO-TS-19091-addgrp-C-2018.asn
MapData FROM DSRC { iso (1) standard (0) signalizedIntersection (19091) profilec(2) dsrc (2) version2 (2) }
-- @brief Include ETSI TS 102 894-2
-- Include references from @url https://forge.etsi.org/rep/ITS/ITS_ASN1/blob/master/CDD_TS102894-2/ITS-Container.asn
ItsPduHeader
FROM ITS-Container { itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) version (2)};
-- Map (lane topology) extended Message
-- @brief Map (lane topology) extended Message Root
-- This DF includes DEs for the MAPEM: protocolVersion, the MAPEM message type identifier _messageID_,
-- the station identifier _stationID_ of the originating ITS-S and the Map data from ISO TS 19091.
MAPEM ::= SEQUENCE {
-- @details header
-- The DE _protocolVersion_ is used to select the appropriate protocol decoder at the receiving ITS-S.
-- It shall be set to 2.
-- The DE _messageID_ shall be mapem(5).
header ItsPduHeader,
-- @details map
-- The DE _map_ contains the MAP data as defined in ISO TS 19091.
map MapData
}
END
@@ -1,40 +0,0 @@
-- @brief Specification of the RTCM corrections extended Message ETSI TS 103 301
-- Latest version available at @url https://forge.etsi.org/rep/ITS/asn1/is_ts103301.git
RTCMEM-PDU-Descriptions {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts103301 (103301) rtcmem (5) version1 (1)
}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
-- @brief Include ISO 19091
-- Include references from @url https://standards.iso.org/iso/ts/19091/ed-2/en/ISO-TS-19091-addgrp-C-2018.asn
RTCMcorrections
FROM DSRC { iso (1) standard (0) signalizedIntersection (19091) profilec(2) dsrc (2) version2 (2) }
-- @brief Include ETSI TS 102 894-2
-- Include references from @url https://forge.etsi.org/rep/ITS/ITS_ASN1/blob/master/CDD_TS102894-2/ITS-Container.asn
ItsPduHeader
FROM ITS-Container { itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) version (2)};
-- RTCM corrections extended extended Message
-- @brief RTCM corrections extended extended Message Root
-- This DF includes DEs for the RTCMEM: protocolVersion, the RTCMEM message type identifier _messageID_,
-- the station identifier _stationID_ of the originating ITS-S and the RTCM corrections as os ISO TS 19091.
RTCMEM ::= SEQUENCE {
-- @details header
-- The DE _protocolVersion_ is used to select the appropriate protocol decoder at the receiving ITS-S.
-- It shall be set to 1.
-- The DE _messageID_ shall be rtcmem(13).
header ItsPduHeader,
-- @details rtcmc
-- The DE _rtcmc_ contains the RTCM corrections data as defined in ISO TS 19091.
rtcmc RTCMcorrections
}
END
@@ -1,40 +0,0 @@
-- @brief Specification of the Signal phase and timing extended Message ETSI TS 103 301
-- Latest version available at @url https://forge.etsi.org/rep/ITS/asn1/is_ts103301.git
SPATEM-PDU-Descriptions {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts103301 (103301) spatem (0) version2 (2)
}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
-- @brief Include ISO TS 19091
-- Include references from @url https://standards.iso.org/iso/ts/19091/ed-2/en/ISO-TS-19091-addgrp-C-2018.asn
SPAT
FROM DSRC { iso (1) standard (0) signalizedIntersection (19091) profilec(2) dsrc (2) version2 (2) }
-- @brief Include ETSI TS 102 894-2
-- Include references from @url https://forge.etsi.org/rep/ITS/ITS_ASN1/blob/master/CDD_TS102894-2/ITS-Container.asn
ItsPduHeader
FROM ITS-Container { itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) version (2) };
-- Signal phase and timing extended Message
-- @brief Signal phase and timing extended Message Root
-- This DF includes DEs for the SPATEM: protocolVersion, the SPATEM message type identifier _messageID_,
-- the station identifier _stationID_ of the originating ITS-S and the SPaT data from ISO TS 19091.
SPATEM ::= SEQUENCE {
-- @details header
-- The DE _protocolVersion_ is used to select the appropriate protocol decoder at the receiving ITS-S.
-- It shall be set to 2.
-- The DE _messageID_ shall be spatem(4).
header ItsPduHeader,
-- @details spat
-- The DE _spat_ contains the SPaT data as defined in ISO TS 19091.
spat SPAT
}
END
@@ -1,40 +0,0 @@
-- @brief Specification of the Signal request extended Message ETSI TS 103 301
-- Latest version available at @url https://forge.etsi.org/rep/ITS/asn1/is_ts103301.git
SREM-PDU-Descriptions {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts103301 (103301) srem (3) version2 (2)
}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
-- @brief Include ISO 19091
-- Include references from @url https://standards.iso.org/iso/ts/19091/ed-2/en/ISO-TS-19091-addgrp-C-2018.asn
SignalRequestMessage
FROM DSRC { iso (1) standard (0) signalizedIntersection (19091) profilec(2) dsrc (2) version2 (2) }
-- @brief Include ETSI TS 102 894-2
-- Include references from @url https://forge.etsi.org/rep/ITS/ITS_ASN1/blob/master/CDD_TS102894-2/ITS-Container.asn
ItsPduHeader
FROM ITS-Container { itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) version (2)};
-- Signal request extended Message Message
-- @brief Signal request extended Message Root
-- This DF includes DEs for the SREM: protocolVersion, the SREM message type identifier _messageID_,
-- the station identifier _stationID_ of the originating ITS-S and the signal request data ISO TS 19091.
SREM ::= SEQUENCE {
-- @details header
-- The DE _protocolVersion_ is used to select the appropriate protocol decoder at the receiving ITS-S.
-- It shall be set to 2.
-- The DE _messageID_ shall be srem(9).
header ItsPduHeader,
-- @details srm
-- The DE _srm_ contains the Signal request data as defined in ISO TS 19091.
srm SignalRequestMessage
}
END
@@ -1,40 +0,0 @@
-- @brief Specification of the Signal status extended Message ETSI TS 103 301
-- Latest version available at @url https://forge.etsi.org/rep/ITS/asn1/is_ts103301.git
SSEM-PDU-Descriptions {
itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts103301 (103301) ssem (4) version2 (2)
}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
-- @brief Include ISO 19091
-- Include references from @url https://standards.iso.org/iso/ts/19091/ed-2/en/ISO-TS-19091-addgrp-C-2018.asn
SignalStatusMessage
FROM DSRC { iso (1) standard (0) signalizedIntersection (19091) profilec(2) dsrc (2) version2 (2) }
-- @brief Include ETSI TS 102 894-2
-- Include references from @url https://forge.etsi.org/rep/ITS/ITS_ASN1/blob/master/CDD_TS102894-2/ITS-Container.asn
ItsPduHeader
FROM ITS-Container { itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) version (2)};
-- Signal status extended Message
-- @brief Signal status extended Message Root
-- This DF includes DEs for the SSEM: protocolVersion, the SSEM message type identifier _messageID_ and
-- the station identifier _stationID_ of the originating ITS-S and the signal status data ISO TS 19091.
SSEM ::= SEQUENCE {
-- @details header
-- The DE _protocolVersion_ is used to select the appropriate protocol decoder at the receiving ITS-S.
-- It shall be set to 2.
-- The DE _messageID_ shall be ssem(10).
header ItsPduHeader,
-- @details ssm
-- The DE _ssm_ contains the Signal status data as defined in ISO TS 19091.
ssm SignalStatusMessage
}
END
@@ -1,44 +0,0 @@
# ETSI ITS ASN.1
The ASN.1 files in this directory are a slightly patched subset of the files published by ETSI for ITS Release 2.
You can find the original files on [ETSI Forge](https://forge.etsi.org/rep/ITS/asn1). Our changes are documented below.
These files are distributed under a permissive license:
> Copyright 2019 ETSI
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
> 1. Redistributions of source code must retain the above copyright notice,
> this list of conditions and the following disclaimer.
> 2. Redistributions in binary form must reproduce the above copyright notice,
> this list of conditions and the following disclaimer in the documentation
> and/or other materials provided with the distribution.
> 3. Neither the name of the copyright holder nor the names of its contributors
> may be used to endorse or promote products derived from this software without
> specific prior written permission.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
> IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
> INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
> DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
> LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
> OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
> OF THE POSSIBILITY OF SUCH DAMAGE.
# Changes
## CDD
Removed obsolete `ActionID` and `StationID` data elements. The generated file names clash with those of `ActionId` and `StationId` on case-insensitive filesystems.
## CPM
Updated CDD imports.
## VAM
Updated CDD imports.
File diff suppressed because it is too large Load Diff
@@ -1,58 +0,0 @@
EtsiTs102941BaseTypes
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) major-version-3(3) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
HashedId8, Time32, PublicEncryptionKey, PublicVerificationKey, Signature
FROM
Ieee1609Dot2BaseTypes {iso(1) identified-organization(3) ieee(111)
standards-association-numbered-series-standards(2) wave-stds(1609)
dot2(2) base(1) base-types(2) major-version-2(2) minor-version-3(3)}
WITH SUCCESSORS
CertificateId, SubjectAssurance, SequenceOfPsidSsp, SequenceOfPsidGroupPermissions,
ValidityPeriod, GeographicRegion, HashedData
FROM
Ieee1609Dot2 {iso(1) identified-organization(3) ieee(111)
standards-association-numbered-series-standards(2) wave-stds(1609)
dot2(2) base (1) schema (1) major-version-2(2) minor-version-4(4)}
WITH SUCCESSORS
EtsiTs103097Data, EtsiTs103097Data-Encrypted, EtsiTs103097Data-Signed,
EtsiTs103097Data-SignedExternalPayload,
EtsiTs103097Data-Unsecured, EtsiTs103097Data-Encrypted-Unicast, EtsiTs103097Data-SignedAndEncrypted-Unicast
FROM
EtsiTs103097Module {itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) secHeaders(103097) core(1) major-version-3(3) minor-version-1(1)}
WITH SUCCESSORS
;
CertificateFormat::= INTEGER {
ts103097v131 (1)
}(1..255)
CertificateSubjectAttributes ::= SEQUENCE {
id CertificateId OPTIONAL,
validityPeriod ValidityPeriod OPTIONAL,
region GeographicRegion OPTIONAL,
assuranceLevel SubjectAssurance OPTIONAL,
appPermissions SequenceOfPsidSsp OPTIONAL,
certIssuePermissions SequenceOfPsidGroupPermissions OPTIONAL,
...
}(WITH COMPONENTS { ..., appPermissions PRESENT} |
WITH COMPONENTS { ..., certIssuePermissions PRESENT})
EcSignature::= CHOICE {
encryptedEcSignature EtsiTs103097Data-Encrypted{EtsiTs103097Data-SignedExternalPayload},
ecSignature EtsiTs103097Data-SignedExternalPayload
}
PublicKeys ::= SEQUENCE {
verificationKey PublicVerificationKey,
encryptionKey PublicEncryptionKey OPTIONAL
}
Version ::= INTEGER {v1(1)}
END
@@ -1,127 +0,0 @@
/*************************************************************************************
This file contains the EtsiTs102941Messages module containing all possible PKI messages.
It should be used when all PKI messages needs to be implemented (for example, for CA development)
**************************************************************************************/
EtsiTs102941MessagesCa
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) messagesCa(0) major-version-3(3) minor-version-3(3)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Data-Signed,
EtsiTs103097Data-SignedExternalPayload,
EtsiTs103097Data-Encrypted-Unicast,
EtsiTs103097Data-SignedAndEncrypted-Unicast
FROM EtsiTs103097Module
{itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) secHeaders(103097) core(1) major-version-3(3) minor-version-1(1)}
WITH SUCCESSORS
Version
FROM EtsiTs102941BaseTypes
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
InnerEcRequestSignedForPop, InnerEcResponse
FROM EtsiTs102941TypesEnrolment
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) enrolment(4) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
EtsiTs102941ButterflyAuthorizationRequest-X509Signed, InnerAtRequest, InnerAtResponse
FROM EtsiTs102941TypesAuthorization
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authorization(5) major-version-3(3) minor-version-2(2) }
WITH SUCCESSORS
ToBeSignedCrl, ToBeSignedTlmCtl, ToBeSignedRcaCtl
FROM EtsiTs102941TrustLists
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) trustLists(6) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
AuthorizationValidationRequest, AuthorizationValidationResponse
FROM EtsiTs102941TypesAuthorizationValidation
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authValidation(7) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
CaCertificateRequest
FROM EtsiTs102941TypesCaManagement
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) caManagement(8) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
ToBeSignedLinkCertificate, ToBeSignedLinkCertificateTlm, ToBeSignedLinkCertificateRca
FROM EtsiTs102941TypesLinkCertificate
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) linkCertificate(9) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
EeRaCertRequest, RaEeCertInfo, EeRaDownloadRequest
FROM Ieee1609Dot2Dot1EeRaInterface
{iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) extension-standards(255) dot1(1) interfaces(1) ee-ra(11) major-version-3(3) minor-version-1(1)}
WITH SUCCESSORS
RaAcaCertRequest, AcaRaCertResponse
FROM Ieee1609Dot2Dot1AcaRaInterface
{iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) extension-standards(255) dot1(1) interfaces(1) aca-ra(4) major-version-3(3) minor-version-1(1)}
WITH SUCCESSORS
;
/************
-- Messages
************/
EnrolmentRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{enrolmentRequest PRESENT})})}
EnrolmentResponseMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{enrolmentResponse PRESENT})})}
AuthorizationRequestMessage ::= EtsiTs103097Data-Encrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationRequest PRESENT})})}
AuthorizationRequestMessageWithPop ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationRequest PRESENT})})}
AuthorizationResponseMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationResponse PRESENT})})}
CertificateRevocationListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateRevocationList PRESENT})})}
TlmCertificateTrustListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateTrustListTlm PRESENT})})}
RcaCertificateTrustListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateTrustListRca PRESENT})})}
AuthorizationValidationRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationValidationRequest PRESENT})})}
AuthorizationValidationResponseMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationValidationResponse PRESENT})})}
CaCertificateRequestMessage ::= EtsiTs103097Data-Signed {EtsiTs102941Data(WITH COMPONENTS{..., content (WITH COMPONENTS{caCertificateRequest PRESENT})})}
CaCertificateRekeyingMessage ::= EtsiTs103097Data-Signed {EtsiTs103097Data-Signed {EtsiTs102941Data(WITH COMPONENTS{..., content (WITH COMPONENTS{caCertificateRequest PRESENT})})}}
TlmLinkCertificateMessage ::= EtsiTs103097Data-Signed {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{linkCertificateTlm PRESENT})})}
RcaSingleSignedLinkCertificateMessage ::= EtsiTs103097Data-Signed {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{singleSignedLinkCertificateRca PRESENT})})}
RcaDoubleSignedLinkCertificateMessage ::= EtsiTs103097Data-Signed {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{doubleSignedlinkCertificateRca PRESENT})})}
ButterflyAuthorizationRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{butterflyAuthorizationRequest PRESENT})})}
X509SignedButterflyAuthorizationRequestMessage ::= EtsiTs103097Data-Encrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{x509SignedbutterflyAuthorizationRequest PRESENT})})}
ButterflyAuthorizationResponseMessage ::= EtsiTs103097Data-Signed {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{butterflyAuthorizationResponse PRESENT})})}
ButterflyAtDownloadRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{butterflyAtDownloadRequest PRESENT})})}
ButterflyCertRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{butterflyCertificateRequest PRESENT})})}
ButterflyCertResponseMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{butterflyCertificateResponse PRESENT})})}
/************
-- EtsiTs102941Data
************/
EtsiTs102941Data::= SEQUENCE {
version Version (v1),
content EtsiTs102941DataContent
}
EtsiTs102941DataContent ::= CHOICE {
enrolmentRequest InnerEcRequestSignedForPop,
enrolmentResponse InnerEcResponse,
authorizationRequest InnerAtRequest,
authorizationResponse InnerAtResponse,
certificateRevocationList ToBeSignedCrl,
certificateTrustListTlm ToBeSignedTlmCtl,
certificateTrustListRca ToBeSignedRcaCtl,
authorizationValidationRequest AuthorizationValidationRequest,
authorizationValidationResponse AuthorizationValidationResponse,
caCertificateRequest CaCertificateRequest,
...,
linkCertificateTlm ToBeSignedLinkCertificateTlm,
singleSignedLinkCertificateRca ToBeSignedLinkCertificateRca,
doubleSignedlinkCertificateRca RcaSingleSignedLinkCertificateMessage,
/* Extension for butterfly key provisioning */
[[butterflyAuthorizationRequest EeRaCertRequest,
x509SignedbutterflyAuthorizationRequest EtsiTs102941ButterflyAuthorizationRequest-X509Signed,
butterflyAuthorizationResponse RaEeCertInfo,
butterflyCertificateRequest RaAcaCertRequest,
butterflyCertificateResponse AcaRaCertResponse,
butterflyAtDownloadRequest EeRaDownloadRequest]]
}
END
@@ -1,106 +0,0 @@
/*************************************************************************************
This file contains the EtsiTs102941MessagesItss-OptionalPrivacy module providing the
same subset of messages as the EtsiTs102941MessagesItss module.
It should never be used together with the EtsiTs102941MessagesCA and EtsiTs102941MessagesItss
This module allows the usage of unencrypted EC signature for AA requests.
**************************************************************************************/
EtsiTs102941MessagesItss-OptionalPrivacy
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) messagesItssOp(2) major-version-3(3) minor-version-3(3)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Data-Signed,
EtsiTs103097Data-Encrypted-Unicast,
EtsiTs103097Data-SignedAndEncrypted-Unicast
FROM EtsiTs103097Module
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) secHeaders(103097) core(1) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
Version
FROM EtsiTs102941BaseTypes
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
InnerEcRequestSignedForPop, InnerEcResponse
FROM EtsiTs102941TypesEnrolment
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) enrolment(4) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
EtsiTs102941ButterflyAuthorizationRequest-X509Signed, InnerAtRequest, InnerAtResponse
FROM EtsiTs102941TypesAuthorization
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authorization(5) major-version-3(3) minor-version-2(2) }
WITH SUCCESSORS
ToBeSignedCrl, ToBeSignedTlmCtl, ToBeSignedRcaCtl
FROM EtsiTs102941TrustLists
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) trustLists(6) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
ToBeSignedLinkCertificate, ToBeSignedLinkCertificateTlm
FROM EtsiTs102941TypesLinkCertificate
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) linkCertificate(9) major-version-3(3) minor-version-1(1)}
WITH SUCCESSORS
EeRaCertRequest, RaEeCertInfo, EeRaDownloadRequest
FROM Ieee1609Dot2Dot1EeRaInterface
{ iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) extension-standards(255) dot1(1) interfaces(1) ee-ra(11) major-version-3(3) minor-version-1(1)}
WITH SUCCESSORS
;
/************
-- Messages
************/
EnrolmentRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{enrolmentRequest PRESENT})})}
EnrolmentResponseMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{enrolmentResponse PRESENT})})}
AuthorizationRequestMessage ::= EtsiTs103097Data-Encrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationRequest PRESENT})})}
AuthorizationRequestMessageWithPop ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationRequest PRESENT})})}
AuthorizationResponseMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationResponse PRESENT})})}
CertificateRevocationListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateRevocationList PRESENT})})}
TlmCertificateTrustListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateTrustListTlm PRESENT})})}
RcaCertificateTrustListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateTrustListRca PRESENT})})}
TlmLinkCertificateMessage ::= EtsiTs103097Data-Signed {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{linkCertificateTlm PRESENT})})}
ButterflyAuthorizationRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{butterflyAuthorizationRequest PRESENT})})}
X509SignedButterflyAuthorizationRequestMessage ::= EtsiTs103097Data-Encrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{x509SignedbutterflyAuthorizationRequest PRESENT})})}
ButterflyAuthorizationResponseMessage ::= EtsiTs103097Data-Signed {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{butterflyAuthorizationResponse PRESENT})})}
ButterflyAtDownloadRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{butterflyAtDownloadRequest PRESENT})})}
/************
-- EtsiTs102941Data
************/
EtsiTs102941Data::= SEQUENCE {
version Version (v1),
content EtsiTs102941DataContent
}
EtsiTs102941DataContent ::= CHOICE {
enrolmentRequest InnerEcRequestSignedForPop,
enrolmentResponse InnerEcResponse,
authorizationRequest InnerAtRequest,
authorizationResponse InnerAtResponse,
certificateRevocationList ToBeSignedCrl,
certificateTrustListTlm ToBeSignedTlmCtl,
certificateTrustListRca ToBeSignedRcaCtl,
authorizationValidationRequest NULL,
authorizationValidationResponse NULL,
caCertificateRequest NULL,
...,
linkCertificateTlm ToBeSignedLinkCertificateTlm,
singleSignedLinkCertificateRca NULL,
doubleSignedlinkCertificateRca NULL,
/* Extension for butterfly key provisioning */
[[butterflyAuthorizationRequest EeRaCertRequest,
x509SignedbutterflyAuthorizationRequest EtsiTs102941ButterflyAuthorizationRequest-X509Signed,
butterflyAuthorizationResponse RaEeCertInfo,
butterflyCertificateRequest NULL,
butterflyCertificateResponse NULL,
butterflyAtDownloadRequest EeRaDownloadRequest]]
}
END
@@ -1,112 +0,0 @@
/*************************************************************************************
This file contains the EtsiTs102941MessagesItss module providing the ITS-S subset
of messages defined in the module EtsiTs102941MessagesCA
It should never be imported together with the module EtsiTs102941MessagesCA.
Use the EtsiTs102941MessagesCA if all possible PKI message types are needed.
This module blocks the usage of unencrypted EC signature for AA requests.
**************************************************************************************/
EtsiTs102941MessagesItss
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) messagesItss(1) major-version-3(3) minor-version-3(3)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Data-Signed,
EtsiTs103097Data-Encrypted-Unicast,
EtsiTs103097Data-SignedAndEncrypted-Unicast
FROM EtsiTs103097Module
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) secHeaders(103097) core(1) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
Version
FROM EtsiTs102941BaseTypes
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
InnerEcRequestSignedForPop, InnerEcResponse
FROM EtsiTs102941TypesEnrolment
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) enrolment(4) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
EtsiTs102941ButterflyAuthorizationRequest-X509Signed, InnerAtRequest, InnerAtResponse
FROM EtsiTs102941TypesAuthorization
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authorization(5) major-version-3(3) minor-version-2(2) }
WITH SUCCESSORS
ToBeSignedCrl, ToBeSignedTlmCtl, ToBeSignedRcaCtl
FROM EtsiTs102941TrustLists
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) trustLists(6) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
ToBeSignedLinkCertificate, ToBeSignedLinkCertificateTlm
FROM EtsiTs102941TypesLinkCertificate
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) linkCertificate(9) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
EeRaCertRequest, RaEeCertInfo, EeRaDownloadRequest
FROM Ieee1609Dot2Dot1EeRaInterface
{ iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) extension-standards(255) dot1(1) interfaces(1) ee-ra(11) major-version-3(3) minor-version-1(1)}
WITH SUCCESSORS
;
/************
-- Messages
************/
EnrolmentRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{enrolmentRequest PRESENT})})}
EnrolmentResponseMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{enrolmentResponse PRESENT})})}
AuthorizationRequestMessage ::= EtsiTs103097Data-Encrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationRequest PRESENT})})}
AuthorizationRequestMessageWithPop ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationRequest PRESENT})})}
AuthorizationResponseMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{authorizationResponse PRESENT})})}
CertificateRevocationListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateRevocationList PRESENT})})}
TlmCertificateTrustListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateTrustListTlm PRESENT})})}
RcaCertificateTrustListMessage ::= EtsiTs103097Data-Signed{EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{certificateTrustListRca PRESENT})})}
TlmLinkCertificateMessage ::= EtsiTs103097Data-Signed {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{linkCertificateTlm PRESENT})})}
ButterflyAuthorizationRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{butterflyAuthorizationRequest PRESENT})})}
X509SignedButterflyAuthorizationRequestMessage ::= EtsiTs103097Data-Encrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{x509SignedbutterflyAuthorizationRequest PRESENT})})}
ButterflyAuthorizationResponseMessage ::= EtsiTs103097Data-Signed {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{butterflyAuthorizationResponse PRESENT})})}
ButterflyAtDownloadRequestMessage ::= EtsiTs103097Data-SignedAndEncrypted-Unicast {EtsiTs102941Data (WITH COMPONENTS{..., content (WITH COMPONENTS{butterflyAtDownloadRequest PRESENT})})}
/************
-- EtsiTs102941Data
************/
EtsiTs102941Data::= SEQUENCE {
version Version (v1),
content EtsiTs102941DataContent
}
EtsiTs102941DataContent ::= CHOICE {
enrolmentRequest InnerEcRequestSignedForPop,
enrolmentResponse InnerEcResponse,
authorizationRequest InnerAtRequest,
authorizationResponse InnerAtResponse,
certificateRevocationList ToBeSignedCrl,
certificateTrustListTlm ToBeSignedTlmCtl,
certificateTrustListRca ToBeSignedRcaCtl,
authorizationValidationRequest NULL,
authorizationValidationResponse NULL,
caCertificateRequest NULL,
...,
linkCertificateTlm ToBeSignedLinkCertificateTlm,
singleSignedLinkCertificateRca NULL,
doubleSignedlinkCertificateRca NULL,
/* Extension for butterfly key provisioning */
[[butterflyAuthorizationRequest EeRaCertRequest,
x509SignedbutterflyAuthorizationRequest EtsiTs102941ButterflyAuthorizationRequest-X509Signed,
butterflyAuthorizationResponse RaEeCertInfo,
butterflyCertificateRequest NULL,
butterflyCertificateResponse NULL,
butterflyAtDownloadRequest EeRaDownloadRequest]]
} (WITH COMPONENTS{...,
authorizationRequest (WITH COMPONENTS{...,
ecSignature (WITH COMPONENTS{...,
encryptedEcSignature PRESENT
})
})
})
END
@@ -1,3 +0,0 @@
The TS 102941 v2.2.1 PKI ASN1 files are copied from its [ETSI repository](https://forge.etsi.org/rep/ITS/asn1/pki_ts102941/-/tree/v2.2.1).
See [ETSI License](https://forge.etsi.org/rep/ITS/asn1/pki_ts102941/-/blob/v2.2.1/LICENSE) for copyright details.
@@ -1,143 +0,0 @@
EtsiTs102941TrustLists
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) trustLists(6) major-version-3(3) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Certificate, EtsiTs103097Data-SignedAndEncrypted, EtsiTs103097Data-Signed
FROM
EtsiTs103097Module
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) secHeaders(103097) core(1) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
HashedId8, Time32, Version --, CertificateAuthorityConstraints
FROM EtsiTs102941BaseTypes
{itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
;
/************
-- CRL
************/
ToBeSignedCrl ::= SEQUENCE {
version Version,
thisUpdate Time32,
nextUpdate Time32,
entries SEQUENCE OF CrlEntry,
...
}
CrlEntry ::= HashedId8
/************
-- TLM CTL
************/
ToBeSignedTlmCtl ::= CtlFormat (FullCtl | DeltaCtl) (WITH COMPONENTS {...,
ctlCommands ( WITH COMPONENT(
( WITH COMPONENTS {...,
add ( WITH COMPONENTS {...,
ea ABSENT,
aa ABSENT
})
})
))
})
/************
-- RCA CTL
************/
ToBeSignedRcaCtl ::= CtlFormat (FullCtl | DeltaCtl) ( WITH COMPONENTS {...,
ctlCommands ( WITH COMPONENT(
( WITH COMPONENTS {...,
add ( WITH COMPONENTS {...,
rca ABSENT,
tlm ABSENT
})
})
))
})
/************
-- CTL
************/
FullCtl::= CtlFormat ( WITH COMPONENTS {...,
isFullCtl ( TRUE ),
ctlCommands ( WITH COMPONENT(
( WITH COMPONENTS {...,
delete ABSENT
})
))
})
DeltaCtl::= CtlFormat (WITH COMPONENTS {...,
isFullCtl(FALSE)
})
CtlFormat ::= SEQUENCE {
version Version,
nextUpdate Time32,
isFullCtl BOOLEAN,
ctlSequence INTEGER (0..255),
ctlCommands SEQUENCE OF CtlCommand,
...
}
CtlCommand ::= CHOICE {
add CtlEntry,
delete CtlDelete,
...
}
CtlEntry ::= CHOICE {
rca RootCaEntry,
ea EaEntry,
aa AaEntry,
dc DcEntry,
tlm TlmEntry,
...
}
CtlDelete ::= CHOICE {
cert HashedId8,
dc DcDelete,
...
}
TlmEntry::= SEQUENCE {
selfSignedTLMCertificate EtsiTs103097Certificate,
successorTo EtsiTs103097Certificate OPTIONAL,
accessPoint Url
}
RootCaEntry ::= SEQUENCE {
selfsignedRootCa EtsiTs103097Certificate,
successorTo EtsiTs103097Certificate OPTIONAL
}
EaEntry ::= SEQUENCE {
eaCertificate EtsiTs103097Certificate,
aaAccessPoint Url,
itsAccessPoint Url OPTIONAL
}
AaEntry ::= SEQUENCE {
aaCertificate EtsiTs103097Certificate,
accessPoint Url
}
DcEntry ::= SEQUENCE {
url Url,
cert SEQUENCE OF HashedId8
}
DcDelete ::= Url
Url::= IA5String
END
@@ -1,105 +0,0 @@
EtsiTs102941TypesAuthorization
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authorization(5) major-version-3(3) minor-version-3(3)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Certificate,
EtsiTs103097Data-Signed
FROM EtsiTs103097Module
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) secHeaders(103097) core(1) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
CertificateFormat, CertificateSubjectAttributes, EcSignature, HashedId8, PublicKeys, Version
FROM EtsiTs102941BaseTypes
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
EeRaInterfacePdu
FROM Ieee1609Dot2Dot1EeRaInterface
{ iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) extension-standards(255) dot1(1) interfaces(1) ee-ra(11) major-version-3(3) minor-version-1(1)}
WITH SUCCESSORS
Ieee1609Dot2Data-SignedX509AuthenticatedCertRequest, ScmsPdu-Scoped, SignerSingleX509Cert
FROM Ieee1609Dot2Dot1Protocol
{ iso(1) identified-organization(3) ieee(111) standards-association-numbered-series-standards(2) wave-stds(1609) dot2(2) extension-standards(255) dot1(1) interfaces(1) protocol(17) major-version-3(3) minor-version-1(1)}
WITH SUCCESSORS
;
/************
-- AuthorizationRequest/Response
************/
AuthorizationResponseCode ::= ENUMERATED {
ok(0),
-- ITS->AA
its-aa-cantparse, -- valid for any structure
its-aa-badcontenttype, -- not encrypted, not signed, not authorizationrequest
its-aa-imnottherecipient, -- the "recipients" of the outermost encrypted data doesn't include me
its-aa-unknownencryptionalgorithm, -- either kexalg or contentencryptionalgorithm
its-aa-decryptionfailed, -- works for ECIES-HMAC and AES-CCM
its-aa-keysdontmatch, -- HMAC keyTag verification fails
its-aa-incompleterequest, -- some elements are missing
its-aa-invalidencryptionkey, -- the responseEncryptionKey is bad
its-aa-outofsyncrequest, -- signingTime is outside acceptable limits
its-aa-unknownea, -- the EA identified by eaId is unknown to me
its-aa-invalidea, -- the EA certificate is revoked
its-aa-deniedpermissions, -- I, the AA, deny the requested permissions
-- AA->EA
aa-ea-cantreachea, -- the EA is unreachable (network error?)
-- EA->AA
ea-aa-cantparse, -- valid for any structure
ea-aa-badcontenttype, -- not encrypted, not signed, not authorizationrequest
ea-aa-imnottherecipient, -- the "recipients" of the outermost encrypted data doesn't include me
ea-aa-unknownencryptionalgorithm, -- either kexalg or contentencryptionalgorithm
ea-aa-decryptionfailed, -- works for ECIES-HMAC and AES-CCM
-- TODO: to be continued...
invalidaa, -- the AA certificate presented is invalid/revoked/whatever
invalidaasignature, -- the AA certificate presented can't validate the request signature
wrongea, -- the encrypted signature doesn't designate me as the EA
unknownits, -- can't retrieve the EC/ITS in my DB
invalidsignature, -- signature verification of the request by the EC fails
invalidencryptionkey, -- signature is good, but the key is bad
deniedpermissions, -- permissions not granted
deniedtoomanycerts, -- parallel limit
... }
InnerAtRequest ::= SEQUENCE {
publicKeys PublicKeys,
hmacKey OCTET STRING (SIZE(32)),
sharedAtRequest SharedAtRequest,
ecSignature EcSignature,
...
}
SharedAtRequest ::= SEQUENCE {
eaId HashedId8,
keyTag OCTET STRING (SIZE(16)),
certificateFormat CertificateFormat,
requestedSubjectAttributes CertificateSubjectAttributes (WITH COMPONENTS{..., certIssuePermissions ABSENT}),
...
}
InnerAtResponse ::= SEQUENCE {
requestHash OCTET STRING (SIZE(16)),
responseCode AuthorizationResponseCode,
certificate EtsiTs103097Certificate OPTIONAL,
...
}
(WITH COMPONENTS { responseCode (ok), certificate PRESENT }
| WITH COMPONENTS { responseCode (ALL EXCEPT ok), certificate ABSENT }
)
EtsiTs102941ButterflyAuthorizationRequest-X509Signed ::= Ieee1609Dot2Data-SignedX509AuthenticatedCertRequest {
ScmsPdu-Scoped {
EeRaInterfacePdu (WITH COMPONENTS {
eeRaCertRequest})
},
SignerSingleX509Cert
}
END
@@ -1,63 +0,0 @@
EtsiTs102941TypesAuthorizationValidation
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authValidation(7) major-version-3(3) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Certificate
FROM EtsiTs103097Module
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) secHeaders(103097) core(1) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
CertificateFormat, CertificateSubjectAttributes,EcSignature, HashedId8, PublicKeys, Version
FROM EtsiTs102941BaseTypes
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
SharedAtRequest
FROM EtsiTs102941TypesAuthorization
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) authorization(5) major-version-3(3) minor-version-2(2) }
WITH SUCCESSORS
;
/************
-- AuthorizationValidationRequest/Response
************/
AuthorizationValidationResponseCode ::= ENUMERATED {
ok(0),
cantparse, -- valid for any structure
badcontenttype, -- not encrypted, not signed, not permissionsverificationrequest
imnottherecipient, -- the "recipients" of the outermost encrypted data doesn't include me
unknownencryptionalgorithm, -- either kexalg or contentencryptionalgorithm
decryptionfailed, -- works for ECIES-HMAC and AES-CCM
invalidaa, -- the AA certificate presented is invalid/revoked/whatever
invalidaasignature, -- the AA certificate presented can't validate the request signature
wrongea, -- the encrypted signature doesn't designate me as the EA
unknownits, -- can't retrieve the EC/ITS in my DB
invalidsignature, -- signature verification of the request by the EC fails
invalidencryptionkey, -- signature is good, but the responseEncryptionKey is bad
deniedpermissions, -- requested permissions not granted
deniedtoomanycerts, -- parallel limit
deniedrequest, -- any other reason?
... }
AuthorizationValidationRequest ::= SEQUENCE {
sharedAtRequest SharedAtRequest,
ecSignature EcSignature,
...
}
AuthorizationValidationResponse ::= SEQUENCE {
requestHash OCTET STRING (SIZE(16)),
responseCode AuthorizationValidationResponseCode,
confirmedSubjectAttributes CertificateSubjectAttributes (WITH COMPONENTS{..., certIssuePermissions ABSENT}) OPTIONAL,
...
}
(WITH COMPONENTS { responseCode (ok), confirmedSubjectAttributes PRESENT }
| WITH COMPONENTS { responseCode (ALL EXCEPT ok), confirmedSubjectAttributes ABSENT }
)
END
@@ -1,31 +0,0 @@
EtsiTs102941TypesCaManagement
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) caManagement(8) major-version-3(3) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Certificate, EtsiTs103097Data-Signed
FROM
EtsiTs103097Module
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) secHeaders(103097) core(1) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
PublicKeys, CertificateSubjectAttributes
FROM EtsiTs102941BaseTypes
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
;
/************
-- CA certificate request
************/
CaCertificateRequest ::= SEQUENCE {
publicKeys PublicKeys,
requestedSubjectAttributes CertificateSubjectAttributes,
...
}
END
@@ -1,62 +0,0 @@
EtsiTs102941TypesEnrolment
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) enrolment(4) major-version-3(3) minor-version-1(1) }
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
EtsiTs103097Certificate,
EtsiTs103097Data-Signed
FROM EtsiTs103097Module
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) secHeaders(103097) core(1) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
CertificateFormat, CertificateSubjectAttributes, EcSignature, HashedId8, PublicKeys, Version
FROM EtsiTs102941BaseTypes
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) major-version-3(3) minor-version-1(1) }
WITH SUCCESSORS
;
/************
-- EnrolmentRequest/Response
************/
EnrolmentResponseCode ::= ENUMERATED {
ok(0),
cantparse, -- valid for any structure
badcontenttype, -- not encrypted, not signed, not enrolmentrequest
imnottherecipient, -- the "recipients" doesn't include me
unknownencryptionalgorithm, -- either kexalg or contentencryptionalgorithm
decryptionfailed, -- works for ECIES-HMAC and AES-CCM
unknownits, -- can't retrieve the ITS from the itsId
invalidsignature, -- signature verification of the request fails
invalidencryptionkey, -- signature is good, but the responseEncryptionKey is bad
baditsstatus, -- revoked, not yet active
incompleterequest, -- some elements are missing
deniedpermissions, -- requested permissions are not granted
invalidkeys, -- either the verification_key of the encryption_key is bad
deniedrequest, -- any other reason?
... }
InnerEcRequestSignedForPop::= EtsiTs103097Data-Signed{InnerEcRequest}
InnerEcRequest ::= SEQUENCE {
itsId OCTET STRING,
certificateFormat CertificateFormat,
publicKeys PublicKeys,
requestedSubjectAttributes CertificateSubjectAttributes (WITH COMPONENTS{..., certIssuePermissions ABSENT}),
...
}
InnerEcResponse ::= SEQUENCE {
requestHash OCTET STRING (SIZE(16)),
responseCode EnrolmentResponseCode,
certificate EtsiTs103097Certificate OPTIONAL,
...
}
(WITH COMPONENTS { responseCode (ok), certificate PRESENT }
| WITH COMPONENTS { responseCode (ALL EXCEPT ok), certificate ABSENT }
)
END
@@ -1,30 +0,0 @@
EtsiTs102941TypesLinkCertificate
{ itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) linkCertificate(9) major-version-3(3) minor-version-1(1) }
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
Time32, HashedData
FROM EtsiTs102941BaseTypes
{itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) ts(102941) baseTypes(3) major-version-3(3) minor-version-1(1)}
WITH SUCCESSORS
;
/************
-- Link certificate messages
************/
ToBeSignedLinkCertificate ::= SEQUENCE {
expiryTime Time32,
certificateHash HashedData,
...
}
ToBeSignedLinkCertificateTlm ::= ToBeSignedLinkCertificate
ToBeSignedLinkCertificateRca ::= ToBeSignedLinkCertificate
END
@@ -1,52 +0,0 @@
EtsiTs103097ExtensionModule
{itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) secHeaders(103097) extension(2) major-version-1(1) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::= BEGIN
IMPORTS
HashedId8,
Time32
FROM Ieee1609Dot2BaseTypes {iso(1) identified-organization(3) ieee(111)
standards-association-numbered-series-standards(2) wave-stds(1609)
dot2(2) base(1) base-types(2) major-version-2 (2) minor-version-3 (3)}
WITH SUCCESSORS
;
ExtensionModuleVersion::= INTEGER(1)
Extension {EXT-TYPE : ExtensionTypes} ::= SEQUENCE {
id EXT-TYPE.&extId({ExtensionTypes}),
content EXT-TYPE.&ExtContent({ExtensionTypes}{@.id})
}
EXT-TYPE ::= CLASS {
&extId ExtId,
&ExtContent
} WITH SYNTAX {&ExtContent IDENTIFIED BY &extId}
ExtId ::= INTEGER(0..255)
EtsiOriginatingHeaderInfoExtension ::= Extension{{EtsiTs103097HeaderInfoExtensions}}
EtsiTs103097HeaderInfoExtensionId ::= ExtId
etsiTs102941CrlRequestId EtsiTs103097HeaderInfoExtensionId ::= 1 --'01'H
etsiTs102941DeltaCtlRequestId EtsiTs103097HeaderInfoExtensionId ::= 2 --'02'H
EtsiTs103097HeaderInfoExtensions EXT-TYPE ::= {
{ EtsiTs102941CrlRequest IDENTIFIED BY etsiTs102941CrlRequestId } |
{ EtsiTs102941DeltaCtlRequest IDENTIFIED BY etsiTs102941DeltaCtlRequestId },
...
}
EtsiTs102941CrlRequest::= SEQUENCE {
issuerId HashedId8,
lastKnownUpdate Time32 OPTIONAL
}
EtsiTs102941CtlRequest::= SEQUENCE {
issuerId HashedId8,
lastKnownCtlSequence INTEGER (0..255) OPTIONAL
}
EtsiTs102941DeltaCtlRequest::= EtsiTs102941CtlRequest
END
@@ -1,123 +0,0 @@
EtsiTs103097Module
{itu-t(0) identified-organization(4) etsi(0) itsDomain(5) wg5(5) secHeaders(103097) core(1) major-version-3(3) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::= BEGIN
IMPORTS
Ieee1609Dot2Data, Certificate
FROM Ieee1609Dot2 {iso(1) identified-organization(3) ieee(111)
standards-association-numbered-series-standards(2) wave-stds(1609)
dot2(2) base(1) schema(1) major-version-2(2) minor-version-4(4)}
WITH SUCCESSORS
ExtensionModuleVersion
FROM EtsiTs103097ExtensionModule {itu-t(0) identified-organization(4)
etsi(0) itsDomain(5) wg5(5) secHeaders(103097) extension(2) major-version-1(1) minor-version-1(1)}
;
EtsiTs103097Certificate::= Certificate (WITH COMPONENTS{...,
toBeSigned (WITH COMPONENTS{...,
id (WITH COMPONENTS{...,
linkageData ABSENT,
binaryId ABSENT
}),
certRequestPermissions ABSENT,
canRequestRollover ABSENT
})
})
EtsiTs103097Data::=Ieee1609Dot2Data (WITH COMPONENTS {...,
content (WITH COMPONENTS {...,
signedData (WITH COMPONENTS {..., -- constraints on signed data headers
tbsData (WITH COMPONENTS {
headerInfo (WITH COMPONENTS {...,
generationTime PRESENT,
p2pcdLearningRequest ABSENT,
missingCrlIdentifier ABSENT
})
}),
signer (WITH COMPONENTS {..., --constraints on the certificate
certificate ((WITH COMPONENT (EtsiTs103097Certificate))^(SIZE(1)))
})
}),
encryptedData (WITH COMPONENTS {..., -- constraints on encrypted data headers
recipients (WITH COMPONENT (
(WITH COMPONENTS {...,
pskRecipInfo ABSENT,
symmRecipInfo ABSENT,
rekRecipInfo ABSENT
})
))
}),
signedCertificateRequest ABSENT
})
})
EtsiTs103097Data-Unsecured {ToBeSentDataContent} ::= EtsiTs103097Data (WITH COMPONENTS {...,
content (WITH COMPONENTS {
unsecuredData (CONTAINING ToBeSentDataContent)
})
})
EtsiTs103097Data-Signed {ToBeSignedDataContent} ::= EtsiTs103097Data (WITH COMPONENTS {...,
content (WITH COMPONENTS {
signedData (WITH COMPONENTS {...,
tbsData (WITH COMPONENTS {
payload (WITH COMPONENTS {
data (WITH COMPONENTS {...,
content (WITH COMPONENTS {
unsecuredData (CONTAINING ToBeSignedDataContent)
})
}) PRESENT
})
})
})
})
})
EtsiTs103097Data-SignedExternalPayload ::= EtsiTs103097Data (WITH COMPONENTS {...,
content (WITH COMPONENTS {
signedData (WITH COMPONENTS {...,
tbsData (WITH COMPONENTS {
payload (WITH COMPONENTS {
extDataHash (WITH COMPONENTS {
sha256HashedData PRESENT
}) PRESENT
})
})
})
})
})
EtsiTs103097Data-Encrypted {ToBeEncryptedDataContent} ::= EtsiTs103097Data (WITH COMPONENTS {...,
content (WITH COMPONENTS {
encryptedData (WITH COMPONENTS {...,
ciphertext (WITH COMPONENTS {...,
aes128ccm (WITH COMPONENTS {...,
ccmCiphertext (CONSTRAINED BY {-- ccm encryption of -- ToBeEncryptedDataContent})
})
})
})
})
})
EtsiTs103097Data-SignedAndEncrypted {ToBesignedAndEncryptedDataContent} ::= EtsiTs103097Data-Encrypted {EtsiTs103097Data-Signed {ToBesignedAndEncryptedDataContent}}
EtsiTs103097Data-Encrypted-Unicast {ToBeEncryptedDataContent} ::= EtsiTs103097Data-Encrypted { EtsiTs103097Data-Unsecured{ToBeEncryptedDataContent}} (WITH COMPONENTS {...,
content (WITH COMPONENTS {
encryptedData (WITH COMPONENTS {...,
recipients (SIZE(1))
})
})
})
EtsiTs103097Data-SignedAndEncrypted-Unicast {ToBesignedAndEncryptedDataContent} ::= EtsiTs103097Data-Encrypted {EtsiTs103097Data-Signed {ToBesignedAndEncryptedDataContent}} (WITH COMPONENTS {...,
content (WITH COMPONENTS {
encryptedData (WITH COMPONENTS {...,
recipients (SIZE(1))
})
})
})
END
@@ -1,229 +0,0 @@
-- Draft V0.0.4_2.2.1 - VAM TS 103 300-1 ASN.1 module
-- Based on the official version available at @url https://forge.etsi.org/rep/ITS/asn1/vam-ts103300_3/-/tree/v2.1.1
-- Modified to import from the CDD module V2.1.1
VAM-PDU-Descriptions {itu-t(0) identified-organization(4) etsi(0) itsDomain(5)
wg1(1) 103300 vam(1) major-version-3(3) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
AccelerationChangeIndication, BasicContainer, CartesianAngle, ClusterBreakupInfo,
ClusterJoinInfo, ClusterLeaveInfo, Curvature, CurvatureCalculationMode,
DeltaTimeQuarterSecond, GenerationDeltaTime, HeadingChangeIndication, ItsPduHeader, LanePosition,
LateralAcceleration, LongitudinalAcceleration, PathHistory, PathPredicted,
SequenceOfSafeDistanceIndication, SequenceOfTrajectoryInterceptionIndication,
SequenceOfTrajectoryInterceptionIndication, Speed, StabilityChangeIndication, StationId,
TrajectoryInterceptionIndication, VerticalAcceleration, VruClusterInformation, VruDeviceUsage,
VruEnvironment, VruExteriorLights, GeneralizedLanePosition, VruProfileAndSubprofile, VruMovementControl,
VruSizeClass, YawRate, Wgs84Angle
FROM ETSI-ITS-CDD {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) 102894 cdd (2) major-version-3 (4) minor-version-3 (3) }
WITH SUCCESSORS
;
VAM ::= SEQUENCE {
header ItsPduHeaderVam,
vam VruAwareness
}
/**
* @details ItsPduHeaderVam
* The ITS PDU header for the VAM.
*
* This DF includes DEs for the VAM _protocolVersion_, the VAM message type identifier _messageID_
* and the station identifier _stationID_ of the originating ITS-S.
*
* @category: Communication information
* @revision: V2.2.1
*/
ItsPduHeaderVam ::= ItsPduHeader(WITH COMPONENTS {..., protocolVersion(3), messageId(vam)})
/**
* @details VruAwareness
* VAM payload.
*
* It includes the time stamp of the VAM and the VAM different containers
*
* @category: Communication information
* @revision: V2.2.1
*/
VruAwareness ::= SEQUENCE {
generationDeltaTime GenerationDeltaTime,
vamParameters VamParameters
}
/**
* @details VamParameters
* The VAM payload includes the @ref BasicContainer and @ref VruHighFrequencyContainer.
* The VAM payload may also include additional containers: @ref VruLowFrequencyContainer,
* @ref VruClusterInformationContainer, @ref VruClusterOperationContainer and @ref VruMotionPredictionContainer.
* The selection of the additional containers depends on the dissemination criteria,
* e.g. _vruCluster_ or _MotionDynamicPrediction_ availability.
*
* @category: Communication information
* @revision: V2.2.1
**/
VamParameters ::= SEQUENCE {
basicContainer BasicContainer,
vruHighFrequencyContainer VruHighFrequencyContainer,
vruLowFrequencyContainer VruLowFrequencyContainer OPTIONAL,
vruClusterInformationContainer VruClusterInformationContainer OPTIONAL,
vruClusterOperationContainer VruClusterOperationContainer OPTIONAL,
vruMotionPredictionContainer VruMotionPredictionContainer OPTIONAL,
...
}
/**
* @details VruHighFrequencyContainer
* The VRU HF container of the VAM contains potentially fast-changing status information of the VRU ITS-S.
* It includes the following components (setting indications are specified in clause 7.3.3 of TS 103 300-3):
*
* @field heading: heading and heading confidence of the originating VRU with regards to the true north.
* @field speed: speed in moving direction and speed confidence of the originating VRU.
* @field longitudinalAcceleration: longitudinal acceleration of the originating VRU.
* @field curvature: related to the actual trajectory of the originating VRU vehicle.
_(recommended for VRU Profile 2)_
* @field curvatureCalculationMode: indicates whether vehicle yaw-rate is used in the calculation of
* the curvature of the VRU vehicle ITS-S that originates the VAM. _(recommended for VRU Profile 2)_
* @field yawRate: yaw rate of originating VRU vehicle. _(recommended for VRU Profile 2)_
* @field lateralAcceleration: originating VRU lateral acceleration in the street plane.
* This field shall be present if the data is available at the originating ITS-S. _(recommended for VRU Profile 2)_
* @field verticalAcceleration: vertical acceleration of the originating VRU.
* This field shall be present if the data is available at the originating ITS-S.
* @field vruLanePosition: lane position of the referencePosition of a VRU, which is either a VRU-specific non-traffic lane
* or a standard traffic lane. This field shall be present if the data is available at the originating ITS-S.
* @field environment: provides contextual awareness of the VRU among other road users.
* This field shall be present only if the data is available at the originating ITS-S.
* @field movementControl: indicates the mechanism used by the VRU to control the longitudinal movement of the VRU vehicle.
* This field shall be present only if the data is available at the originating ITS-S. _(recommended for VRU Profile 2)_
* @field orientation : complements the dimensions of the VRU vehicle by defining the angle of the VRU vehicle longitudinal
* axis with regards to the WGS84 north. _(recommended for VRU Profile 2)_
* @field rollAngle: provides the angle and angle accuracy between the ground plane and the current orientation of a vehicle's
* y-axis with respect to the ground plane about the x-axis according to the ISO 8855.
* This field shall be present only if the data is available at the originating ITS-S. _(recommended for VRU Profile 2)_
* @field deviceUsage: provides indications from the personal device about the potential
* activity of the VRU. This field shall be present only if the data is available at the originating ITS-S.
* _(recommended for VRU Profile 1)_
*
* @category: VRU information
* @revision: V2.2.1
*/
VruHighFrequencyContainer ::= SEQUENCE {
heading Wgs84Angle,
speed Speed,
longitudinalAcceleration LongitudinalAcceleration,
curvature Curvature OPTIONAL,
curvatureCalculationMode CurvatureCalculationMode OPTIONAL,
yawRate YawRate OPTIONAL,
lateralAcceleration LateralAcceleration OPTIONAL,
verticalAcceleration VerticalAcceleration OPTIONAL,
vruLanePosition GeneralizedLanePosition OPTIONAL,
environment VruEnvironment OPTIONAL,
movementControl VruMovementControl OPTIONAL,
orientation Wgs84Angle OPTIONAL,
rollAngle CartesianAngle OPTIONAL,
deviceUsage VruDeviceUsage OPTIONAL,
...
}
/**
* @details VruLowFrequencyContainer
* The VRU LF container of the VAM contains potentially slow-changing information of the VRU ITS-S.
* It is mandatory with higher periodicity as specified in clause 6.2 or when VRU cluster operation container is present.
* It includes the following components (setting indications are specified in clause 7.3.4 of TS 103 300-3):
*
* @field profileAndSubprofile: profile of the ITS-S that originates the VAM, including sub-profile information.
* @field sizeClass: information about the size of the VRU.
* @field exteriorLights: status of the most important exterior lights switches of the VRU ITS-S that originates the VAM.
* _(conditional mandatory as specified in clause 7.3.4 of TS 103 300-3)_
*
* @category: VRU information
* @revision: V2.2.1
*/
VruLowFrequencyContainer ::= SEQUENCE {
profileAndSubprofile VruProfileAndSubprofile,
sizeClass VruSizeClass OPTIONAL,
exteriorLights VruExteriorLights OPTIONAL,
...
}
/**
* @details VruClusterInformationContainer
* The VRU Cluster Information container of the VAM provides the information/parameters relevant to the VRU cluster.
* It is mandatory if the VAM is transmitted by VRU cluster leader.
* It includes the following components (setting indications are specified in clause 7.3.5 of TS 103 300-3):
*
* @field vruClusterInformation: set of parammeters releated to the VRU cluster.
* When transmitted by a VRU ITS-S, the clusterId and clusterBoundingBoxShape fields inside this DF shall be present.
* The clusterBoundingBoxShape is positioned with respect to the position sent in the BasicContainer.
*
* @category: VRU information
* @revision: V2.2.1
*/
VruClusterInformationContainer::= SEQUENCE{
vruClusterInformation VruClusterInformation (WITH COMPONENTS{..., clusterId, clusterBoundingBoxShape PRESENT}),
...
}
/**
* @details VruClusterOperationContainer
* The VRU Cluster Operation container of the VAM provides information relevant to change of cluster state and composition.
* It is mandatory if the VAM is transmitted by a VRU joining, leaving or breaking up a cluster.
* It includes the following components (setting indications are specified in clause 7.3.5 of TS 103 300-3).
* At least one of the fields below shall be present if the container is present in the VAM:
*
* @field clusterJoinInfo: indicates the intent of an individual VAM transmitter to join a cluster.
* @field clusterLeaveInfo : indicates that an individual VAM transmitter has recently left the VRU cluster.
* @field clusterBreakupInfo: indicates the intent of a cluster VAM transmitter to stop sending cluster VAMs.
* @field clusterIdChangeTimeInfo: indicates the intent of a cluster VAM transmitter to change cluster ID.
*
* @category: VRU information
* @revision: V2.2.1
*/
VruClusterOperationContainer ::= SEQUENCE {
clusterJoinInfo ClusterJoinInfo OPTIONAL,
clusterLeaveInfo ClusterLeaveInfo OPTIONAL,
clusterBreakupInfo ClusterBreakupInfo OPTIONAL,
clusterIdChangeTimeInfo DeltaTimeQuarterSecond OPTIONAL,
...
}
/**
* @details VruMotionPredictionContainer
* The VRU Motion Prediction container of the VAM carries the past and future motion state information of the VRU.
* It includes the following components (setting indications are specified in clause 7.3.6 of TS 103 300-3).
* At least one of the fields below shall be present if the container is present in the VAM:
*
* @field pathHistory: represents the VRU's recent movement over some past time and/or distance.
* It consists of a list of path points.
* @field pathPrediction: provides the set of predicted locations of the ITS-S, confidence values
* and the corresponding future time instants.
* @field safeDistance: provides indication of safe distance between an ego-VRU and up to 8 other ITS-S
* or entity on the road to indicate whether the ego-VRU is at a safe distance (that is less likely to
* physically collide) from another ITS-S or entity on the road.
* @field trajectoryInterceptionIndication: provides the indication for possible trajectory interception
* with up to 8 VRUs or other objects on the road..
* @field accelerationChangeIndication: provides an acceleration change indication of the VRU.
* When present this DF indicates an anticipated change in the VRU speed for period of actionDeltaTime.
* @field headingChangeIndication: provides additional data elements associated to heading change indicators
* such as a change of travel direction (left or right).
* The direction change action is performed for a period of actionDeltaTime.
* @field stabilityChangeIndication: provides an estimation of the VRU stability.
*
* @category: GeoReference information, VRU information
* @revision: V2.2.1
*/
VruMotionPredictionContainer ::= SEQUENCE {
pathHistory PathHistory OPTIONAL,
pathPrediction PathPredicted OPTIONAL,
safeDistance SequenceOfSafeDistanceIndication OPTIONAL,
trajectoryInterceptionIndication SequenceOfTrajectoryInterceptionIndication OPTIONAL,
accelerationChangeIndication AccelerationChangeIndication OPTIONAL,
headingChangeIndication HeadingChangeIndication OPTIONAL,
stabilityChangeIndication StabilityChangeIndication OPTIONAL,
...
}
END
@@ -1,38 +0,0 @@
-- Draft V0.0.4_2.2.1 - TS 103 300-3
-- Based on the official version available at @url https://forge.etsi.org/rep/ITS/asn1/vam-ts103300_3/-/tree/v2.1.1
-- Modified to import from the CDD module V2.1.1
-- This module defines a special container for motorcycles, to be integrated into the
-- Cooperative Awareness Message (CAM) defined in ETSI TS 103 900
VRU-Motorcyclist-Special-Container {itu-t(0) identified-organization(4) etsi(0) itsDomain(5)
wg1(1) 103300 motorcyclist-special-container(2) version2(2)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
PathPredicted, SequenceOfSafeDistanceIndication,
StabilityChangeIndication, Wgs84Angle, CartesianAngle,
VruSizeClass, VruSubProfileMotorcyclist
FROM ETSI-ITS-CDD
{itu-t(0) identified-organization(4) etsi(0)
itsDomain(5) wg1(1) 102894 cdd(2) major-version-3(4) minor-version-3(3)}
;
MotorcylistSpecialContainer ::= SEQUENCE {
motorcyclist VruSubProfileMotorcyclist,
sizeClass VruSizeClass,
rollAngle CartesianAngle OPTIONAL,
orientation Wgs84Angle OPTIONAL,
safeDistance SequenceOfSafeDistanceIndication OPTIONAL,
pathPrediction PathPredicted OPTIONAL,
stabilityChangeIndication StabilityChangeIndication OPTIONAL,
...
}
END
@@ -1,54 +0,0 @@
CPM-OriginatingStationContainers {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (103324) originatingStationContainers (2) major-version-1 (1) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
CartesianAngle, MapReference, Speed, StationType, TrailerData, Wgs84Angle
FROM ETSI-ITS-CDD {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) major-version-4 (4) minor-version-3 (3)}
;
/**
* This DF represents the Originating Vehicle Container
*
* It shall include the following components:
*
* @field orientationAngle: the angle and angle accuracy of the absolute orientation of the disseminating vehicle in the WGS84 coordinate system with respect to true North.
*
* @field pitchAngle: the optional angle and angle accuracy between the ground plane and the current orientation of the vehicle's x-axis with respect to the ground plane about the y-axis according to the ISO 8855.
*
* @field rollAngle: the optional angle and angle accuracy between the ground plane and the current orientation of a vehicle's y-axis with respect to the ground plane about the x-axis according to the ISO 8855
*
* @field trailerData: information about the trailer dimensions and orientation in case a trailer is present.
*
*/
OriginatingVehicleContainer ::= SEQUENCE {
orientationAngle Wgs84Angle,
pitchAngle CartesianAngle OPTIONAL,
rollAngle CartesianAngle OPTIONAL,
trailerDataSet TrailerDataSet OPTIONAL,
...
}
/**
* This DF represents the Originating RSU Container.
*
* It shall include the following components:
*
* @field mapReference: identifies the MAPEM containing the topology information reference in the Perceived Object Container
*
*/
OriginatingRsuContainer ::= SEQUENCE{
mapReference MapReference OPTIONAL,
...
}
/**
* This DF represents a list of trailer data.
*
*/
TrailerDataSet::= SEQUENCE SIZE(1..8,...) OF TrailerData (WITH COMPONENTS {... ,frontOverhang ABSENT, rearOverhang ABSENT, trailerWidth ABSENT})
END
@@ -1,171 +0,0 @@
CPM-PDU-Descriptions { itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (103324) cpm (1) major-version-1 (1) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
ItsPduHeader, MessageRateHz, MessageSegmentationInfo, OrdinalNumber1B, ReferencePosition, StationType, TimestampIts
FROM ETSI-ITS-CDD {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) major-version-4 (4) minor-version-3 (3)}
WITH SUCCESSORS
OriginatingRsuContainer, OriginatingVehicleContainer
FROM CPM-OriginatingStationContainers {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (103324) originatingStationContainers (2) major-version-1 (1) minor-version-1(1)}
WITH SUCCESSORS
SensorInformationContainer
FROM CPM-SensorInformationContainer {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (103324) sensorInformationContainer (3) major-version-1 (1) minor-version-1(1)}
WITH SUCCESSORS
PerceptionRegionContainer
FROM CPM-PerceptionRegionContainer {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (103324) perceptionRegionContainer (5) major-version-1 (1) minor-version-1(1)}
WITH SUCCESSORS
PerceivedObjectContainer
FROM CPM-PerceivedObjectContainer {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (103324) perceivedObjectContainer (4) major-version-1 (1) minor-version-1(1)}
WITH SUCCESSORS
;
/**
* This DF represents the Collective Perception Message (CPM) and is the top level Protocol Data Unit.
*
* It shall include the following components:
*
* @field header: the common message header for the facilities layer message.
*
* @field payload: the payload of the message.
*
*/
CollectivePerceptionMessage ::= SEQUENCE {
header ItsPduHeader (WITH COMPONENTS {... , protocolVersion (2), messageId(cpm)}),
payload CpmPayload
}
/**
* This DF represents the payload of the CPM.
*
* It shall include the following components:
*
* @field managementContainer: the management container.
*
* @field cpmContainers: the list of CPM containers, including its container type identifier and including either one or none of originatingVehicleContainer and/or originatingRsuContainer.
*
*/
CpmPayload ::= SEQUENCE {
managementContainer ManagementContainer,
cpmContainers ConstraintWrappedCpmContainers,
...
}
/**
* This DF represents the management container of the CPM.
* The management container provides basic information about the originating ITS-S, which are not specific to a specific type of station.
*
* It shall include the following components:
*
* @field referenceTime: the reference time for all time related information in the CPM.
*
* @field messageRateRange: the planned or expected range of the CPM generation rate.
*
* @field segmentationInfo: information regarding the message segmentation on facility layer.
*
* @field referencePosition: the reference position for all position related information in the CPM.
*
*/
ManagementContainer ::= SEQUENCE {
referenceTime TimestampIts,
referencePosition ReferencePosition,
segmentationInfo MessageSegmentationInfo OPTIONAL,
messageRateRange MessageRateRange OPTIONAL,
...
}
/**
* This information object class is an abstract template to instantiate containers.
*
* It shall include the following components:
*
* @field &id: the identifier of the container type.
*
* @field &Type: the container content.
*
*/
CPM-CONTAINER-ID-AND-TYPE ::= CLASS {
&id CpmContainerId UNIQUE,
&Type
} WITH SYNTAX {&Type IDENTIFIED BY &id}
/**
* This DE represents the identifier of the container type.
*
*/
CpmContainerId ::= INTEGER (1..16)
/**
* These value assignements represent specific values of the container type identifier.
*/
originatingVehicleContainer CpmContainerId ::= 1
originatingRsuContainer CpmContainerId ::= 2
sensorInformationContainer CpmContainerId ::= 3
perceptionRegionContainer CpmContainerId ::= 4
perceivedObjectContainer CpmContainerId ::= 5
/**
* This information object set represents the association between the container type and the container content.
*/
CpmContainers CPM-CONTAINER-ID-AND-TYPE ::= {
{OriginatingVehicleContainer IDENTIFIED BY originatingVehicleContainer} |
{OriginatingRsuContainer IDENTIFIED BY originatingRsuContainer} |
{SensorInformationContainer IDENTIFIED BY sensorInformationContainer} |
{PerceptionRegionContainer IDENTIFIED BY perceptionRegionContainer} |
{PerceivedObjectContainer IDENTIFIED BY perceivedObjectContainer},
...
}
/**
* This DF represents a CPM container preceded by its type identifier and a lenght indicator.
*
* It shall include the following components:
*
* @field containerId: the identifier of the container type.
*
* @field containerData: the container content consistent with the container type.
*
*/
WrappedCpmContainer ::= SEQUENCE {
containerId CPM-CONTAINER-ID-AND-TYPE.&id( {CpmContainers} ),
containerData CPM-CONTAINER-ID-AND-TYPE.&Type( {CpmContainers}{@containerId} )
}
/**
* This DF represents a list of CPM containers, each with their type identifier.
*/
WrappedCpmContainers::= SEQUENCE SIZE(1..8,...) OF WrappedCpmContainer
/**
* This DF represents a list of CPM containers, each with their type identifier with an additional constraint.
*/
ConstraintWrappedCpmContainers ::= WrappedCpmContainers
((WITH COMPONENT (WITH COMPONENTS {..., containerId (ALL EXCEPT 1)})) |
(WITH COMPONENT (WITH COMPONENTS {..., containerId (ALL EXCEPT 2)})))
/**
* This DF represents the planned or expected range of the message generation rate.
*
* It shall include the following components:
*
* @field messageRateMin: the minimum planned or expected message rate.
*
* @field messageRateMax: the maximum planned or expected message rate.
*
*/
MessageRateRange::= SEQUENCE{
messageRateMin MessageRateHz,
messageRateMax MessageRateHz
}
END
@@ -1,34 +0,0 @@
CPM-PerceivedObjectContainer {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (103324) perceivedObjectContainer (4) major-version-1 (1) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
CardinalNumber1B, PerceivedObject
FROM ETSI-ITS-CDD {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) major-version-4 (4) minor-version-3 (3)}
;
/**
* This DF represents the Perceived Object Container
*
* It shall include the following components:
*
* @field numberOfPerceivedObjects: the total number of perceived objects at the time of generating the message.
*
* @field perceivedObjects: the list of perceived objects.
*
*/
PerceivedObjectContainer ::= SEQUENCE {
numberOfPerceivedObjects CardinalNumber1B,
perceivedObjects PerceivedObjects,
...
}
/** @brief Perceived Objects
* This DF provides a list of perceived objects represented in the coordinate system in which the y-axis corresponds to the North direction, the x-axis to the East direction, and the z- axis to the vertical direction.
*/
PerceivedObjects::= SEQUENCE SIZE(0..255, ...) OF PerceivedObject (WITH COMPONENTS {... ,objectId PRESENT})
END
@@ -1,54 +0,0 @@
CPM-PerceptionRegionContainer {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (103324) perceptionRegionContainer (5) major-version-1 (1) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
CardinalNumber1B, DeltaTimeMilliSecondSigned, Identifier2B, Shape, ConfidenceLevel, SensorType, SequenceOfIdentifier1B
FROM ETSI-ITS-CDD {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) major-version-4 (4) minor-version-3 (3)}
;
/**
* This DF represents the Perception Region Container as a list of perception region information objects.
*/
PerceptionRegionContainer ::= SEQUENCE SIZE(1..256, ...) OF PerceptionRegion
/**
* This DF represents the actual perception capabilities available to the transmitting ITS-S, offering additional (often dynamic) details to the information provided in the sensor information container.
*
* It shall include the following components:
* @field measurementDeltaTime: difference between the time of estimation of the perception region and the reference time. Positive values indicates that the provided information refers to a point in time after the reference time.
*
* @field perceptionRegionConfidence: the perception confidence.
*
* @field perceptionRegionShape: specification of the shape of the perception region.
*
* @field shadowingApplies: indicates if the standard shadowing approach applies to the described perception region.
*
* @field sensorIdList: the optional list of identifiers of the sensors which are involved in perceiving the region.
*
* @field numberOfPerceivedObjects: the optional number of perceived objects contained in the perception region specified in the component perceptionRegionShape.
*
* @field perceivedObjectIds: the optional list of identifiers of the objects specified in the Perceived Object Container that are contained in the perception region specified in the component perceptionRegionShape.
*
*/
PerceptionRegion ::= SEQUENCE {
measurementDeltaTime DeltaTimeMilliSecondSigned,
perceptionRegionConfidence ConfidenceLevel,
perceptionRegionShape Shape,
shadowingApplies BOOLEAN,
sensorIdList SequenceOfIdentifier1B OPTIONAL,
numberOfPerceivedObjects CardinalNumber1B OPTIONAL,
perceivedObjectIds PerceivedObjectIds OPTIONAL,
...
}
/**
* This DF represents a list of identifiers of perceived objects.
*/
PerceivedObjectIds::= SEQUENCE SIZE(0..255, ...) OF Identifier2B
END
@@ -1,47 +0,0 @@
CPM-SensorInformationContainer {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (103324) sensorInformationContainer (3) major-version-1 (1) minor-version-1(1)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
Shape, ConfidenceLevel, Identifier1B, SensorType
FROM ETSI-ITS-CDD {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) major-version-4 (4) minor-version-3 (3) }
;
/**
* This DF represents the Sensor Information Container as a list of information objects about the sensors or data fusion systems from which the station provides information about detected objects.
*/
SensorInformationContainer ::= SEQUENCE SIZE(1..128, ...) OF SensorInformation
/**
* This DF represents the characteristics of a single sensor or data fusion system.
*
* It shall include the following components:
*
* @field sensorId: identifier of the sensor or data fusion system used to relate the perceived object in the Perceived Object Container to the sensor that detected it.
* this identifier shall uniquely identify the sensor inside the CPM, but may be changed from one CPM to the next.
*
* @field sensorType: the type of the sensor.
*
* @field perceptionRegionShape: the perception region of the sensor.
*
* @field perceptionRegionConfidence: the homogeneous perception region confidence that can be assumed for the entire perception region shape of this sensor.
*
* @field shadowingApplies: indicates if the standard shadowing approach applies to the described perception region.
*
*/
SensorInformation ::= SEQUENCE {
sensorId Identifier1B,
sensorType SensorType,
perceptionRegionShape Shape OPTIONAL,
perceptionRegionConfidence ConfidenceLevel OPTIONAL,
shadowingApplies BOOLEAN,
...
}
END
@@ -1,4 +0,0 @@
The TS 103324 v2.1.1 CPM ASN1 files are copied from its [ETSI repository](https://forge.etsi.org/rep/ITS/asn1/cpm_ts103324/-/tree/v2.1.1).
For compatibility with CDD 2.2.1, the CDD import lines are manually patched from "major-version-3" to "major-version-4".
See [ETSI License](https://forge.etsi.org/rep/ITS/asn1/cpm_ts103324/-/blob/v2.1.1/LICENSE) for copyright details.
@@ -1,406 +0,0 @@
DENM-PDU-Description {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) denmPduRelease2 (103831)
major-version-2 (2) minor-version-3 (3)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
ActionId, ActionIdList, CauseCodeV2, ClosedLanes, DangerousGoodsExtended, DeltaReferencePosition, DeltaTimeMilliSecondPositive, DeltaTimeSecond,
EnergyStorageType, EventZone, GeneralizedLanePositions, HeightLonCarr, InformationQuality, ItsPduHeader, IvimReferences, LanePosition,
LightBarSirenInUse, ItineraryPath, MapReferences, MetaInformation, MultiplicativeFactor, NumberOfOccupants, ObjectFace, OccupiedLanesWithConfidence,
PerceivedObject, Position1d, PosLonCarr, PosCentMass, PositionOfPillars, PositioningSolutionType, PosFrontAx, PositionOfOccupants, PathPredictedList,
RequestResponseIndication, ReferencePosition, RestrictedTypes, RoadConfigurationSectionList, RoadType, StandardLength3b,StandardLength12b,
StationType, Speed, SpeedLimit, StationId, StationarySince, TimestampIts, Traces, TracesExtended, TrafficDirection, TrafficRule, TurningRadius,
Temperature, VehicleMass, VehicleIdentification, Wgs84Angle, WheelBaseVehicle
FROM ETSI-ITS-CDD {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2)
major-version-4 (4) minor-version-3 (3)} WITH SUCCESSORS
;
/**
* This type represents the DENM PDU.
*
* It shall include the following components:
*
* @field header: the header of the DENM PDU.
*
* @field denm: the payload of the DENM PDU.
*/
DENM ::= SEQUENCE {
header ItsPduHeader (WITH COMPONENTS {... , protocolVersion (2), messageId(denm)}),
denm DenmPayload
}
/**
* This type represents the DENM payload.
*
* It shall include the following components:
*
* @field management: the Management Container.
*
* @field situation: the optional Situation Container.
*
* @field location: the optional Location Container.
*
* @field alacarte: the optional ALaCarte Container .
*
*/
DenmPayload ::= SEQUENCE {
management ManagementContainer,
situation SituationContainer OPTIONAL,
location LocationContainer OPTIONAL,
alacarte AlacarteContainer OPTIONAL
}
((WITH COMPONENTS {..., management (WITH COMPONENTS {..., termination ABSENT}), situation PRESENT, location PRESENT}) |
(WITH COMPONENTS {..., management (WITH COMPONENTS {..., termination PRESENT}), situation ABSENT, location ABSENT, alacarte ABSENT}))
/**
* This type represents the Management Container.
*
* It shall include the following components:
*
* @field actionId: the identifier of the DENM.
*
* @field detectionTime: the time at which the event is detected.
*
* @field referenceTime: the time at which a new DENM, an update DENM or a cancellation DENM is generated
*
* @field termination: the optional termination type of the DENM.
*
* @field eventPosition: the geographical position used in the definition of the awareness area / relevance zone, see clause 6.1.3.
*
* @field awarenessDistance: the optional radius of the circular awareness area, with centre at the event position or at any of the
* event history points as defined in clause 6.1.3.1.
*
* @field trafficDirection: the optional traffic direction along which the receiving ITS-S may encounter the event,
* as defined in clause 6.1.3.
*
* @field validityDuration: the validity duration of a DENM. This component represents a time offset in the unit of second since detectionTime.
*
* @field transmissionInterval: the optional time interval for DENM transmission as defined by the originating ITS-S.
* If the component is not present in DENM, a default value defaultValidity is assumed.
*
* @field stationType: the station type information of the originating ITS-S.
*
*/
ManagementContainer ::= SEQUENCE {
actionId ActionId,
detectionTime TimestampIts,
referenceTime TimestampIts,
termination Termination OPTIONAL,
eventPosition ReferencePosition,
awarenessDistance StandardLength3b OPTIONAL,
trafficDirection TrafficDirection OPTIONAL,
validityDuration DeltaTimeSecond DEFAULT defaultValidity,
transmissionInterval DeltaTimeMilliSecondPositive OPTIONAL,
stationType StationType,
...
}
/**
* This type represents the situation container.
*
* It shall include the following components:
*
* @field informationQuality: the quality level of the information provided by the ITS-S application of the originating ITS-S.
* It indicates the probability of the detected event being truly existent at the event position.
*
* @field eventType: the event type, including direct cause and sub cause.
*
* @field linkedCause: the optional type of a linked event co-existing at the same time and the same place (same event zone),
* including direct cause and sub cause of the linked event, for which no other DENM is sent out.
*
* @field eventZone: an optional list of EventPoint, using the position indicated in the component eventPosition of the Management container
* as the reference position for the first EventPoint.
*
* @field linkedDenms: the optional list of DF ActionId, pointing to DENMs that are semantically connected because applying to consecutive
* event zones at the same time.
*
* @field eventEnd: the end position of the event along the road that is affected by the event w.r.t. the component eventPosition of the
* Management container. This end position is represented by the length of the event along the road.
*
* @field eventEndFactor: an optional component that applies to the component eventEnd. It is a factor to be multiplied with the value given in eventEnd.
*
* @field perceivedEvent: an optional component that, in case of vehicle ITS-S, indicates the nature of the information about the event.
*
*/
SituationContainer ::= SEQUENCE {
informationQuality InformationQuality,
eventType CauseCodeV2,
linkedCause CauseCodeV2 OPTIONAL,
eventZone EventZone OPTIONAL,
...,
[[ linkedDenms ActionIdList OPTIONAL,
eventEnd Position1d OPTIONAL ]], -- extension in minor-version-2
[[ eventEndFactor MultiplicativeFactor OPTIONAL,
perceivedEvent PerceivedEvent OPTIONAL ]] -- extension in minor-version-3
}
((WITH COMPONENTS {..., eventZone PRESENT, eventEnd ABSENT, eventEndFactor ABSENT}) |
(WITH COMPONENTS {..., eventZone ABSENT, eventEnd PRESENT}) |
(WITH COMPONENTS {..., eventZone ABSENT, eventEnd ABSENT, eventEndFactor ABSENT}))
/**
* This type represents the Location Container.
*
* It shall include the following components:
*
* @field eventSpeed: optional speed of a detected dynamic event and the confidence of the speed information.
*
* @field eventPositionHeading: the optional heading of the event and the confidence of the heading information.
*
* @field detectionZonesToEventPosition: the detection zone information approaching the event position, see clause 6.1.3.3.
*
* @field roadType: the optional road type information at the event position.
*
* @field lanePositions: the optional lane(s) where the event is located, at the position indicated by the component eventPosition
* of the Management container and for a given reference direction.
*
* @field occupiedLanes: the optional lane(s) that are fully or partially occupied by the event, at the position indicated by the
* component eventPosition of the Management container and for a given reference direction.
*
* @field linkedIvims: the optional list of DF IvimReference, pointing to IVIMs that are semantically connected because providing information
* applying to the road segment(s) covered by the components detectionZonesToEventPosition, detectionZonesToSpecifiedEventPoint and
* the SituationContainer component eventZone.
*
* @field linkedMapem: the optional list of DF Mapreference, pointing to MAPEMs that are semantically connected because providing information
* applying to the road segment(s) covered by the component detectionZonesToEventPosition, detectionZonesToSpecifiedEventPoint and
* the SituationContainer component eventZone.
*
* @field detectionZonesToSpecifiedEventPoint: the optional detection zone information approaching towards a
* specified event point, see clause 6.1.3.3.
*
* @field predictedPaths: the optional list of future paths or trajectories that the event may move along or zones that the event may occupy.
*
*/
LocationContainer ::= SEQUENCE {
eventSpeed Speed OPTIONAL,
eventPositionHeading Wgs84Angle OPTIONAL,
detectionZonesToEventPosition Traces,
roadType RoadType OPTIONAL,
...,
[[ lanePositions GeneralizedLanePositions OPTIONAL,
occupiedLanes OccupiedLanesWithConfidence OPTIONAL,
linkedIvims IvimReferences OPTIONAL,
linkedMapems MapReferences OPTIONAL,
detectionZonesToSpecifiedEventPoint TracesExtended OPTIONAL,
predictedPaths PathPredictedList OPTIONAL ]] -- extension in minor-version-2
}
/**
* This type contains detailed information about the vehicle in which the originating ITS-S is mounted, for mitigating the consequences
* of a collision.
*
* It shall include the following components:
*
* @field heightLonCarrLeft: the height of the left longitudinal carrier of the vehicle from base to top.
*
* @field heightLonCarrRight: the height of the right longitudinal carrier of the vehicle from base to top.
*
* @field posLonCarrLeft: the position of the left longitudinal carrier of vehicle.
*
* @field posLonCarrRight: the position of the right longitudinal carrier of vehicle.
*
* @field positionOfPillars: information about the vertical support of the vehicle in which the originating ITS-S is mounted. It shall be
* included for passenger vehicles only.
*
* @field posCentMass: the position of the centre of mass of the vehicle.
*
* @field wheelBaseVehicle: the wheel base of the vehicle.
*
* @field turningRadius: the turning radius of the vehicle.
*
* @field posFrontAx: the position of the front axle of the vehicle.
*
* @field positionOfOccupants: indicates whether an in-vehicle seat is occupied at the moment of generation of the message.
*
* @field vehicleMass: the mass of the unloaded vehicle
*
* @field requestResponseIndication: indicates whether the originating ITS-S transmitting the impactReduction component is requesting
* the receiving ITS-S to provide also its impactReduction component.
*
*/
ImpactReductionContainer ::= SEQUENCE {
heightLonCarrLeft HeightLonCarr,
heightLonCarrRight HeightLonCarr,
posLonCarrLeft PosLonCarr,
posLonCarrRight PosLonCarr,
positionOfPillars PositionOfPillars,
posCentMass PosCentMass,
wheelBaseVehicle WheelBaseVehicle,
turningRadius TurningRadius,
posFrontAx PosFrontAx,
positionOfOccupants PositionOfOccupants,
vehicleMass VehicleMass,
requestResponseIndication RequestResponseIndication
}
/**
* This type contains detailed information about an object with which a vehicle and/or the traffic is likely to collide.
*
* It shall include the following components:
*
* @field perceivedPreCrashObject: information about a perceived object in the East, North, Up reference frame.
*
* @field objectStationId: the optional station Id of the object for which the information is provided.
*
* @field timeToCollision: the optional estimated time to collision of a vehicle with the object.
*
* @field impactSection: indication of the object's section where the impact will most likely occur.
* When the target object is likely to be a vehicle, then this component should be present, otherwise it should not be provided.
*
* @field estimatedBrakingDistance: the optional estimated distance in which the vehicle would need to come to a complete hold,
* if no obstruction was in the way.
*/
PreCrashContainer ::= SEQUENCE {
perceivedPreCrashObject PerceivedObject,
objectStationId StationId OPTIONAL,
timeToCollision DeltaTimeMilliSecondPositive OPTIONAL,
impactSection ObjectFace OPTIONAL,
estimatedBrakingDistance StandardLength12b OPTIONAL,
...
}
/**
* This type contains detailed information about the configuration of road section(s) that are geographically related to the event.
*
* It shall include the following components:
*
* @field roadConfigurationConfidence: information about the source of the road configuration and the confidence in the information.
*
* @field roadConfigurationSectionList: a list of road configuration information per applicable road section.
*
*/
RoadConfigurationContainer ::= SEQUENCE {
roadConfigurationConfidence MetaInformation,
roadConfigurationSectionList RoadConfigurationSectionList,
...
}
/**
* This type contains detailed information of a roadwork zone and specific access conditions.
*
* It shall include the following components:
*
* @field lightBarSirenInUse: optionally indicates whether a roadwork vehicle has switched on the light bar or siren.
* It is used when the roadwork involves a specific roadwork vehicle
*
* @field closedLanes: optionally indicates whether the roadwork has caused the closure of one or several driving lanes.
* Optionally, it may indicate whether a hard shoulder lane is closed to traffic or can be used for specific usage (e.g. for stopping).
*
* @field restriction: the optional type(s) of vehicles that are restricted to access the road work zone.
* More than one vehicle types may be provided by this component if the restriction apply to multiple vehicle types.
*
* @field speedLimit: the optional speed limitation applied to the roadwork zone.
*
* @field incidentIndication: the optional incident related to the roadworks to provide additional information of the roadworks zone.
*
* @field recommendedPath: the optional recommended itinerary in order to contour the roadworks zone. If present, a recommended path
* shall be a list of path points in the order from the starting point closest to the roadworks zone to the end point of the recommended path.
*
* @field startingPointSpeedLimit: the optional effective starting position of a speed limit being applied to the roadwork zone,
* with respect to the component eventPosition on the Management Container.
* This component shall be present if the speed limit is applied at a certain distance prior to the roadwork zone starting position.
*
* @field trafficFlowRule: optionally indicates the side of the road to which the traffic should flow around a roadwork.
*
* @field referenceDenms: an optional sequence of actionIds for different DENMs that describe the same event.
* If it is available, it indicates the actionIds of all other DENMs describing this event.
*
*/
RoadWorksContainerExtended ::= SEQUENCE {
lightBarSirenInUse LightBarSirenInUse OPTIONAL,
closedLanes ClosedLanes OPTIONAL,
restriction RestrictedTypes OPTIONAL,
speedLimit SpeedLimit OPTIONAL,
incidentIndication CauseCodeV2 OPTIONAL,
recommendedPath ItineraryPath OPTIONAL,
startingPointSpeedLimit DeltaReferencePosition OPTIONAL,
trafficFlowRule TrafficRule OPTIONAL,
referenceDenms ActionIdList OPTIONAL
}
/**
* This type contains detailed information about a stationary vehicle.
*
* It shall include the following components:
*
* @field stationarySince: the optional time duration of the stationary vehicle being stationary.
*
* @field stationaryCause: optional additional information to describe causes of the stationary vehicle event such as human problem.
*
* @field carryingDangerousGoods: optional information on the type of dangerous goods, the required emergency action and other information.
*
* @field numberOfOccupants: the optional estimated number of occupants involved in the stationary vehicle event.
*
* @field vehicleIdentification: the optional identification of the stationary vehicle.
*
* @field energyStorageType: the optional vehicle energy storage type information of the stationary vehicle, such as electric, diesel, etc.
*
*/
StationaryVehicleContainer ::= SEQUENCE {
stationarySince StationarySince OPTIONAL,
stationaryCause CauseCodeV2 OPTIONAL,
carryingDangerousGoods DangerousGoodsExtended OPTIONAL,
numberOfOccupants NumberOfOccupants OPTIONAL,
vehicleIdentification VehicleIdentification OPTIONAL,
energyStorageType EnergyStorageType OPTIONAL
}
/**
* This type represents the A La Carte Container.
*
* It shall include the following components:
*
* @field lanePosition: the optional lane position of the event.
*
* @field impactReduction: optional vehicle data for collision mitigation.
*
* @field externalTemperature: optional the ambient temperature at the event position.
*
* @field roadWorks: optional information of the roadwork zone.
*
* @field positioningSolution: optionally indicates the technical solution being used by the originating ITS-S to estimate the event position.
*
* @field stationaryVehicle: optional information about a stationary vehicle.
*
* @field roadConfiguration: optional information about the configuration of the road.
*
* @field precrash: the optional information about perceived objects that represent hazards and/or could be subject of collisions.
*
*/
AlacarteContainer ::= SEQUENCE {
lanePosition LanePosition OPTIONAL,
impactReduction ImpactReductionContainer OPTIONAL,
externalTemperature Temperature OPTIONAL,
roadWorks RoadWorksContainerExtended OPTIONAL,
positioningSolution PositioningSolutionType OPTIONAL,
stationaryVehicle StationaryVehicleContainer OPTIONAL,
...,
[[ roadConfiguration RoadConfigurationContainer OPTIONAL,
preCrash PreCrashContainer OPTIONAL ]] -- extension in minor-version-2
}
/**
* This type specifies the default value for DENM validity duration used for DENM protocol operation.
*/
defaultValidity INTEGER ::= 600
/**
* This DE indicates whether a vehicle has perceived an event (e.g. a stationary vehicle using camera sensors) or whether it is itself the cause of the event.
* the value shall be set to:
* - True - in case the vehicle has perceived the event
* - False - in case the vehicle itself is the cause of the event (e.g. vehicle is stationary) or part of the cause (e.g. traffic jam) or is directly exposed to the environmental condition (e.g. adverse weather)
*/
PerceivedEvent::= BOOLEAN
/**
* This indicates the termination type of generated DENM, i.e. if it is a cancellation DENM or a negation DENM
*/
Termination ::= ENUMERATED {isCancellation(0), isNegation (1)}
END
@@ -1,542 +0,0 @@
--! @options: no-fields-header
CAM-PDU-Descriptions {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) camPduRelease2 (103900)
major-version-2 (2) minor-version-3 (3)}
DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
IMPORTS
ItsPduHeader, CauseCodeV2, ReferencePosition, AccelerationControl, Curvature, CurvatureCalculationMode, Heading, LanePosition,
EmergencyPriority, EmbarkationStatus, Speed, DriveDirection, AccelerationComponent, StationType, ExteriorLights, DangerousGoodsBasic,
SpecialTransportType, LightBarSirenInUse, VehicleRole, VehicleLength, VehicleWidth, Path, RoadworksSubCauseCode, ClosedLanes,
TrafficRule, SpeedLimit, SteeringWheelAngle, PerformanceClass, YawRate, PtActivation, ProtectedCommunicationZonesRSU,
CenDsrcTollingZone, GenerationDeltaTime, BasicContainer, BrakeControl, VehicleHeight2, WiperStatus, GeneralizedLanePositions,
PathPredictedList, CartesianAngle, Wgs84Angle, StabilityChangeIndication, VruSubProfileBicyclist, VruMovementControl,
BasicLaneConfiguration, PolygonalLine, MetaInformation, ConfidenceLevels, VehicleMovementControl
FROM ETSI-ITS-CDD {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) 102894 cdd (2)
major-version-4 (4) minor-version-3 (3)} WITH SUCCESSORS
;
-- The root data frame for cooperative awareness messages
/**
* This type represents the CAM PDU.
*
* It shall include the following componenets:
*
* @field header: the header of the CAM PDU.
*
* @field cam: the payload of the CAM PDU.
*/
CAM ::= SEQUENCE {
header ItsPduHeader (WITH COMPONENTS {... , protocolVersion (2), messageId(cam)}),
cam CamPayload
}
/**
* This type represents the CAM payload.
*
* It shall include the following components:
*
* @field generationDeltaTime: Time corresponding to the time of the reference position in the CAM, considered as time of the CAM generation.
*
* @field camParameters: The sequence of CAM mandatory and optional container.
*
*/
CamPayload ::= SEQUENCE {
generationDeltaTime GenerationDeltaTime,
camParameters CamParameters
}
/**
* @field basicContainer: the mandatory basic container of the CAM.
*
* @field highFrequencyContainer: the mandatory container represents the high frequency of the CAM.
*
* @field lowFrequencyContainer: the optional conatainer represents the low frequency of the CAM.
*
* @field specialVehicleContainer: The special container of the CAM shall be present as defined in clause 6.1.2.
* The content of the container shall be set according to the value of the vehicleRole component as specified in Table 5.
*
* @field extensionContainers: the list of CAM extension containers, including its container type identifier and the container itself.
*/
CamParameters ::= SEQUENCE {
basicContainer BasicContainer,
highFrequencyContainer HighFrequencyContainer,
lowFrequencyContainer LowFrequencyContainer OPTIONAL,
specialVehicleContainer SpecialVehicleContainer OPTIONAL,
...,
extensionContainers WrappedExtensionContainers OPTIONAL
}
/**
* This type represents the high frequency container.
*
* It shall include the following components:
*
* @field basicVehicleContainerHighFrequency: The mandatory high frequency container of the CAM when the originating ITS-S is of the type vehicle ITS-S.
*
* @field rsuContainerHighFrequency: The mandatory high frequency container of CAM when the type of the originating ITS-S is RSU ITS-S.
*/
HighFrequencyContainer ::= CHOICE {
basicVehicleContainerHighFrequency BasicVehicleContainerHighFrequency,
rsuContainerHighFrequency RSUContainerHighFrequency,
...
}
/**
* This type represents the low frequency container.
*
* It shall include the following components:
*
* The low frequency container of the CAM when the originating ITS-S is of the type vehicle ITS-S. It shall be present as defined in clause 6.1.2.
*/
LowFrequencyContainer ::= CHOICE {
basicVehicleContainerLowFrequency BasicVehicleContainerLowFrequency (WITH COMPONENTS {..., pathHistory (SIZE (0..23))}),
...
}
/**
* This type represent the Special Vehicle Container.
*
* It shall include the following components:
*
* @field publicTransportContainer: If the vehicleRole component is set to publicTransport(1) this container shall be present.
*
* @field specialTransportContainer: If the vehicleRole component is set to specialTransport(2) this container shall be present.
*
* @field dangerousGoodsContainer: If the vehicleRole component is set to dangerousGoods(3) this container shall be present.
*
* @field roadWorksContainerBasic: If the vehicleRole component is set to roadWork(4) this container shall be present.
*
* @field rescueContainer: If the vehicleRole component is set to rescue(5) this container shall be present.
*
* @field emergencyContainer: If the vehicleRole component is set to emergency(6) this container shall be present.
*
* @field safetyCarContainer: If the vehicleRole component is set to safetyCar(7) this container shall be present.
*/
SpecialVehicleContainer ::= CHOICE {
publicTransportContainer PublicTransportContainer,
specialTransportContainer SpecialTransportContainer,
dangerousGoodsContainer DangerousGoodsContainer,
roadWorksContainerBasic RoadWorksContainerBasic,
rescueContainer RescueContainer,
emergencyContainer EmergencyContainer,
safetyCarContainer SafetyCarContainer,
...
}
/**
* This type contains detaild information of the Basic Vehicle Container High Frequency.
*
* It shall include the following components:
*
* @field heading: It represent the heading and heading accuracy of the vehicle movement of the originating ITS-S with regards to the true north.
* The heading accuracy provided in the heading Confidence value shall provide the accuracy of the measured vehicle heading with a confidence level
* of 95 %. Otherwise, the value of the headingConfidence shall be set to unavailable.
*
* @field speed: It represent driving speed and speed accuracy of the originating ITS-S. The speed accuracy provided in the speedConfidence shall
* provide the accuracy of the speed value with a confidence level of 95 %. Otherwise, the speedConfidence shall be set to unavailable.
*
* @field driveDirection: This component represent the vehicle drive direction (forward or backward) of the originating ITS-S.
*
* @field vehicleLength: This component represent the vehicle length value and vehicle length confidence indication of the vehicle ITS-S that
* originate the CAM.
*
* @field vehicleWidth: This component represents the Vehicle Width of the vehicle ITS-S that originates the CAM excluding side mirrors and possible
* similar extensions.
*
* @field longitudinalAcceleration: It represent the vehicle Longitudinal Acceleration of the originating ITS-S in the centre of the mass of the
* empty vehicle. It shall include the measured vehicle longitudinal acceleration and its accuracy value with the confidence level of 95 %.
* Otherwise, the longitudinalAccelerationConfidence shall be set to unavailable.
*
* @field curvature: this component reppresent the actual trajectory of the vehicle.
*
* @field curvatureCalculationMode: It indicates whether vehicle yaw-rate is used in the calculation of the curvature of the vehicle ITS-S that
* originates the CAM.
*
* @field yawRate: It denotes the vehicle rotation around the centre of mass of the empty vehicle. The leading sign denotes the direction of
* rotation. The value is negative if the motion is clockwise when viewing from the top.
* yawRateConfidence denotes the accuracy for the 95 % confidence level for the measured yawRateValue. Otherwise, the value of yawRateConfidence
* shall be set to unavailable.
*
* @field accelerationControl: an optional component which represents the current status of the vehcile mechnanisms controlling the longitudinal movement of the vehcile ITS-S
* (e.g. brake pedal, gas pedal, etc. engaged) that originate the CAM.
*
* @field lanePosition: an optional component which represents the lanePosition of the referencePosition of a vehicle. This component shall be present if the data is
* available at the originating ITS-S.
*
* @field steeringWheelAngle: an optional component which indicates the steering wheel angle and accuracy as measured at the vehicle ITS-S that originates the CAM.
*
* @field lateralAcceleration: an optional component which represents the vehicle lateral acceleration of the originating ITS-S in the centre of the mass of the empty vehicle.
* It shall include the measured vehicle lateral acceleration and its accuracy value with the confidence level of 95%.
*
* @field verticalAcceleration: an optional component which indicates the originating ITS-S in the centre of the mass of the empty vehicle.
*
* @field performanceClass: an optional component characterizes the maximum age of the CAM data elements with regard to the generation delta time.
*
* @field cenDsrcTollingZone: an optional component which represents the information about the position of a CEN DSRC Tolling Station operating in the 5,8 GHz frequency band.
*/
BasicVehicleContainerHighFrequency ::= SEQUENCE {
heading Heading,
speed Speed,
driveDirection DriveDirection,
vehicleLength VehicleLength,
vehicleWidth VehicleWidth,
longitudinalAcceleration AccelerationComponent,
curvature Curvature,
curvatureCalculationMode CurvatureCalculationMode,
yawRate YawRate,
accelerationControl AccelerationControl OPTIONAL,
lanePosition LanePosition OPTIONAL,
steeringWheelAngle SteeringWheelAngle OPTIONAL,
lateralAcceleration AccelerationComponent OPTIONAL,
verticalAcceleration AccelerationComponent OPTIONAL,
performanceClass PerformanceClass OPTIONAL,
cenDsrcTollingZone CenDsrcTollingZone OPTIONAL
}
/**
* This type contains detaild information of the Basic Vehicle Container Low Frequency.
*
* It shall include the following components:
*
* @field vehicleRole: represent the role of the vehicle ITS-S that originates the CAM. Only values 0 to 7 shall be used.
*
* @field exteriorLights: represent the status of the most important exterior lights switches of the vehicle ITS-S that originates the CAM.
*
* @field pathHistory: which represents the vehicle's recent movement over some past time and/or distance. It consists of a list of path points,
* each represented as DF PathPoint. The list of path points may consist of up to 23 elements.
*/
BasicVehicleContainerLowFrequency ::= SEQUENCE {
vehicleRole VehicleRole,
exteriorLights ExteriorLights,
pathHistory Path
}
/**
* This type contains detaild information of the Public Transport Container.
*
* It shall include the following components:
*
* @field embarkationStatus: It indicates whether the passenger embarkation is currently ongoing.
*
* @field ptActivation: an optional component used for controlling traffic lights, barriers, bollards, etc.
*/
PublicTransportContainer ::= SEQUENCE {
embarkationStatus EmbarkationStatus,
ptActivation PtActivation OPTIONAL
}
/**
* This type contains detaild information of the Special Transport Container.
*
* It shall include the following components:
*
* @field specialTransportType: which indicates whether the originating ITS-S is mounted on a special transport vehicle with heavy or oversized load
* or both. It shall be present if the data is available in originating ITS-S.
*
* @field lightBarSirenInUse: indicates whether light-bar or a siren is in use by the vehicle originating the CAM.
*/
SpecialTransportContainer ::= SEQUENCE {
specialTransportType SpecialTransportType,
lightBarSirenInUse LightBarSirenInUse
}
/**
* This type contains detaild information of the Dangerous Goods Container.
*
* It shall include the following components:
*
* @field dangerousGoodsBasic: identifies the type of the dangerous goods transported by the vehicle that originates the CAM. It shall be present if
* the data is available in the originating ITS S.
*/
DangerousGoodsContainer ::= SEQUENCE {
dangerousGoodsBasic DangerousGoodsBasic
}
/**
* This type contains detaild information of the Road Works Container Basic.
*
* It shall include the following components:
*
* @field roadworksSubCauseCode: The optional component, in case the originating ITS-S is mounted to a vehicle ITS-S participating to roadwork. It
* provides information on the type of roadwork that it is currently undertaking. This component shall be present if the data is available in
* originating ITS S.
*
* @field lightBarSirenInUse: it indicates whether light-bar or a siren is in use by the vehicle originating the CAM.
*
* @field closedLanes: an optional component which provides information about the opening/closure status of the lanes ahead. Lanes are counted from
* the inside boarder of the road. If a lane is closed to traffic, the corresponding bit shall be set to 1.
*/
RoadWorksContainerBasic ::= SEQUENCE {
roadworksSubCauseCode RoadworksSubCauseCode OPTIONAL,
lightBarSirenInUse LightBarSirenInUse,
closedLanes ClosedLanes OPTIONAL
}
/**
* This type contains detaild information of the Rescue Container.
*
* It shall include the following components:
*
* @field lightBarSirenInUse: it indicates whether light-bar or a siren is in use by the vehicle originating the CAM.
*/
RescueContainer ::= SEQUENCE {
lightBarSirenInUse LightBarSirenInUse
}
/**
* This type contains detaild information of the Emergency Container.
*
* It shall include the following components:
*
* @field lightBarSirenInUse: it indicates whether light-bar or a siren is in use by the vehicle originating the CAM.
*
* @field incidentIndication: the optional incident related to the roadworks to provide additional information of the roadworks zone.
*
* @field emergencyPriority: the optional component represent right of way indicator of the vehicle ITS-S that originates the CAM PDU.
*/
EmergencyContainer ::= SEQUENCE {
lightBarSirenInUse LightBarSirenInUse,
incidentIndication CauseCodeV2 OPTIONAL,
emergencyPriority EmergencyPriority OPTIONAL
}
/**
* This type contains detaild information of the Safety Car Container.
*
* It shall include the following components:
*
* @field lightBarSirenInUse: it indicates whether light-bar or a siren is in use by the vehicle originating the CAM.
*
* @field incidentIndication: the optional incident related to the roadworks to provide additional information of the roadworks zone.
*
* @field trafficRule: an optional rule indicates whether vehicles are allowed to overtake a safety car that is originating this CAM.
*
* @field speedLimit: an optional speed indicates whether a speed limit is applied to vehicles following the safety car.
*/
SafetyCarContainer ::= SEQUENCE {
lightBarSirenInUse LightBarSirenInUse,
incidentIndication CauseCodeV2 OPTIONAL,
trafficRule TrafficRule OPTIONAL,
speedLimit SpeedLimit OPTIONAL
}
/**
* This type contains detaild information of the RSU Container High Frequency.
*
* It shall include the following components:
*
* @field protectedCommunicationZonesRSU: an optional Information about position of a CEN DSRC Tolling Station operating in the 5,8 GHz frequency
* band. If this information is provided by RSUs a receiving vehicle ITS-S is prepared to adopt mitigation techniques when being in the vicinity of
* CEN DSRC tolling stations.
*/
RSUContainerHighFrequency ::= SEQUENCE {
protectedCommunicationZonesRSU ProtectedCommunicationZonesRSU OPTIONAL,
...
}
/**
* This information object class is an abstract template to instantiate containers.
*
* It shall include the following components:
*
* @field &id: the identifier of the container type.
*
* @field &Type: the container content.
*
*/
EXTENSION-CONTAINER-ID-AND-TYPE ::= CLASS {
&id ExtensionContainerId UNIQUE,
&Type
} WITH SYNTAX {&Type IDENTIFIED BY &id}
/**
* This DE represents the identifier of the container type.
*/
ExtensionContainerId ::= INTEGER (1..16,...)
/**
* These value assignments represent specific values of the container type identifier.
*/
twoWheelerContainer ExtensionContainerId ::= 1
eHorizonLocationSharingContainer ExtensionContainerId ::= 2
veryLowFrequencyContainer ExtensionContainerId ::= 3
pathPredictionContainer ExtensionContainerId ::= 4
generalizedLanePositionsContainer ExtensionContainerId ::= 5
vehicleMovementControlContainer ExtensionContainerId ::= 6
/**
* This information object set represents the association between the container type and the container content.
*/
ExtensionContainers EXTENSION-CONTAINER-ID-AND-TYPE ::= {
{TwoWheelerContainer IDENTIFIED BY twoWheelerContainer} |
{EHorizonLocationSharingContainer IDENTIFIED BY eHorizonLocationSharingContainer} |
{VeryLowFrequencyContainer IDENTIFIED BY veryLowFrequencyContainer} |
{PathPredictionContainer IDENTIFIED BY pathPredictionContainer} |
{GeneralizedLanePositionsContainer IDENTIFIED BY generalizedLanePositionsContainer} |
{VehicleMovementControlContainer IDENTIFIED BY vehicleMovementControlContainer},
...
}
/**
* This DF represents a CAM container preceded by its type identifier and a length indicator.
*
* It shall include the following components:
*
* @field containerId: the identifier of the container type.
*
* @field containerData: the container content consistent with the container type.
*
*/
WrappedExtensionContainer ::= SEQUENCE {
containerId EXTENSION-CONTAINER-ID-AND-TYPE.&id( {ExtensionContainers} ),
containerData EXTENSION-CONTAINER-ID-AND-TYPE.&Type( {ExtensionContainers}{@containerId} )
}
/**
* This DF represents a list of CAM containers, each with their type identifier.
*/
WrappedExtensionContainers ::= SEQUENCE SIZE(1..8,...) OF WrappedExtensionContainer
/**
* This type contains detailed information about two wheelers. It is meant to use for StationType
* cyclist, moped and motorcycle.
*
* It shall include the following components:
*
* @field typeSpecificInformation: this data field contains type specific information about two wheelers.
*
* @field rollAngle: this data field describes the roll angle of the two wheeler.
*
* @field orientation: this data field describes the orientation of the two wheeler.
*
* @field stabilityChangeIndication: this data field describes if the two wheeler is about to lose control.
*
*/
TwoWheelerContainer ::= SEQUENCE {
typeSpecificInformation TwoWheelerTypeSpecificInformation OPTIONAL,
rollAngle CartesianAngle OPTIONAL,
orientation Wgs84Angle OPTIONAL,
stabilityChangeIndication StabilityChangeIndication OPTIONAL,
...
}
/**
* This type contains type specific information about a two wheeler.
*
* It includes one of the following components:
*
* @field cyclist: it contains cyclist-specific information.
*
*/
TwoWheelerTypeSpecificInformation ::= CHOICE {
cyclist CyclistTypeSpecificInformation,
...
}
/**
* This type contains type-specific information about cyclists.
*
* It shall include the following components:
*
* @field vruSubProfileBicyclist: it indicates the detailed type of the cyclist.
*
* @field vruMovementControl: it includes information about the movement control of the bicycle.
*
*/
CyclistTypeSpecificInformation ::= SEQUENCE {
vruSubProfileBicyclist VruSubProfileBicyclist (unavailable | bicyclist | e-scooter | pedelec | speed-pedelec | roadbike | childrensbike) OPTIONAL,
vruMovementControl VruMovementControl OPTIONAL,
...
}
/**
* This type contains contextual, map-based location information.
* eHorizon is defined as a tool to convey the part of the road network and its characteristics derived from map data located in front of and behind the vehicle along the road.
*
* It shall include the following components:
*
* @field segmentAhead: the road segment that the vehicle is predicted to reach, starting from the reference position.
* At least one node must be filled in so that the current map position can be calculated on the receiver side.
*
* @field nodeProbabilities: confidence values for each node in segmentAhead, indicating how confident we are that the ITS station will reach that point.
*
* @field segmentBehind: the road segment that the vehicle has passed, based on the collected data, starting from the reference position.
* At least one node must be filled in so that the current map position can be calculated on the receiver side.
*
* @field laneLevelDetails: provides information about the configuration of the road at the position indicated by the component referencePosition of the Basic Container and for a given reference direction.
*
* @field segmentSource: it represents the origin of the map-specific data.
*
*/
EHorizonLocationSharingContainer ::= SEQUENCE {
segmentAhead PolygonalLine,
nodeProbabilities ConfidenceLevels OPTIONAL,
segmentBehind PolygonalLine,
laneLevelDetails BasicLaneConfiguration OPTIONAL,
segmentSource MetaInformation (WITH COMPONENTS {..., confidenceValue ABSENT}) OPTIONAL,
...
}
/**
* This type represents the very low frequency container.
*
* It shall include the following components:
*
* @field vehicleHeight: this component represents the height of the vehicle that originates the CAM.
*
* @field wiperStatus: this component represents the status of the wipers of the vehicle that originates the CAM, at the time indicated by generationDeltaTime.
*
* @field brakeControl: this component represents the status of the brake control system of the vehicle that originates the CAM,
* at the time indicated by generationDeltaTime and during the period 10 seconds before that time.
*/
VeryLowFrequencyContainer ::= SEQUENCE {
vehicleHeight VehicleHeight2 OPTIONAL,
wiperStatus WiperStatus OPTIONAL,
brakeControl BrakeControl OPTIONAL,
...
}
/**
* This type represents the path prediction container.
*
* Contains information about the possible future paths of ITS station.
*/
PathPredictionContainer ::= SEQUENCE {
pathPredictedList PathPredictedList,
...
}
/**
* This type represents the generalized lane positions container.
*
* Contains detailed information about the transversal position of the ITS station with respect to the road and potentially about the lane type.
*/
GeneralizedLanePositionsContainer ::= SEQUENCE {
generalizedLanePositions GeneralizedLanePositions,
...
}
/**
* This type represents the vehicle movement control container.
*
* Contains information about the current vehicle movement control status
* of ITS station.
* This contains:
* - brake and acceleration pedal position status information
* - mechanism for lateral, longitudinal movements dimensions
* of the vehicle
*/
VehicleMovementControlContainer ::= SEQUENCE {
vehicleMovementControl VehicleMovementControl,
...
}
END
@@ -1,68 +0,0 @@
POIM-CommonContainers {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) poi(103916) common(99) major-version-1 (1) minor-version-1 (1)}
DEFINITIONS AUTOMATIC TAGS ::= BEGIN
IMPORTS
Identifier2B, IvimReferences, MapReferences, Provider, TimestampIts
FROM ETSI-ITS-CDD {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) major-version-4 (4) minor-version-1 (1)} WITH SUCCESSORS
PoiType
FROM POIM-PDU-Description {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) poi(103916) poim(0) major-version-1 (1) minor-version-1 (1)}
;
/**
* This type represents the common management container of the POI Information Blocks.
*
* It shall include the following components:
*
* @field serviceProviderId: identifier of the organization that provided the POI Information Block.
*
* @field blockIdentificationNumber: identifier of the POI Information Block, as assigned by the organization identified in serviceProviderId.
*
* @field timestamp: timestamp of the generation or last change of the POI Information Block
*
* @field linkedIvims: the optional list of references to IVIMs that are semantically connected because providing signage information applying to the POI.
*
* @field linkedMapem: the optional list of references to MAPEMs that are semantically connected because providing information applying to the POI.
*
*/
PoiBlockManagementContainer::= SEQUENCE {
serviceProviderId Provider,
blockIdentificationNumber Identifier2B,
timestamp TimestampIts,
linkedIvims IvimReferences OPTIONAL,
linkedMapems MapReferences OPTIONAL,
...
}
/**
* This type can be used to reference to other related POI Information Blocks.
*
* It shall include the following components:
*
* @field poiInfoBlockType: the type of related POI Information Blocks
*
* @field serviceProviderId: identifier of the organization that provided the related POI Information Block.
*
* @field blockIdentificationNumber: identifier of the related POI Information Block, as assigned by the organization identified in serviceProviderId.
*
* @field timestamp: optional timestamp of the generation or last change of the related POI Information Block
*
*/
RelatedPoiInfoBlock::= SEQUENCE{
poiInfoBlockType PoiType,
serviceProviderId Provider,
blockIdentificationNumber Identifier2B,
timestamp TimestampIts OPTIONAL,
...
}
/**
* This DF shall contain a list of @ref RelatedPoiInfoBlock and represents a commmon container to refer to other POI Information Blocks.
*
*/
RelatedPoiInfoBlockContainer::= SEQUENCE OF RelatedPoiInfoBlock
END
@@ -1,85 +0,0 @@
POIM-PDU-Description {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) poi(103916) poim(0) major-version-1 (1) minor-version-1 (1)}
DEFINITIONS AUTOMATIC TAGS ::= BEGIN
IMPORTS
ItsPduHeader
FROM ETSI-ITS-CDD {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) major-version-4 (4) minor-version-1 (1)} WITH SUCCESSORS
ParkingAvailabilityBlock
FROM POIM-ParkingAvailability {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) poi(103916) parkingAvailability(1) major-version-1 (1) minor-version-1 (1)} WITH SUCCESSORS
-- other block imports from separate standards to be added here when needed.
;
/**
* This type represents the POIM PDU.
*
* It shall include the following components:
*
* @field header: the header of the POIM PDU.
*
* @field payload: the payload of the POIM PDU.
*/
POIM ::= SEQUENCE {
header ItsPduHeader (WITH COMPONENTS {... , protocolVersion (1), messageId(poim)}),
payload WrappedPoiInformationBlocks
}
/**
* This DF represents a list of POI information blocks, each with their type identifier.
*/
WrappedPoiInformationBlocks::= SEQUENCE (SIZE (1..8,...)) OF WrappedPoiInformationBlock
/**
* This information object class is an abstract template to instantiate POI Information Blocks.
*
* It shall include the following components:
*
* @field &id: the identifier of the block type.
*
* @field &Type: the block content.
*
*/
BLOCK-TYPE ::= CLASS {
&id PoiType,
&Content
} WITH SYNTAX {&Content IDENTIFIED BY &id}
/**
* This DE represents the identifier of the container type.
*
*/
PoiType::= INTEGER(1..128,...)
/**
* These value assignements represent specific values of the block type identifier.
*/
parkingAvailability PoiType ::= 1
/**
* This information object set represents the association between the block type and the block content.
*/
PoiInformationSet BLOCK-TYPE ::= {
{ParkingAvailabilityBlock IDENTIFIED BY parkingAvailability}, ...
}
/**
* This DF represents a POI Information Block preceded by its type identifier and a lenght indicator.
*
* It shall include the following components:
*
* @field poiType: the identifier of the POI Information Block type.
*
* @field poiBlock: the POI Information Block.
*
*/
WrappedPoiInformationBlock ::= SEQUENCE {
poiType BLOCK-TYPE.&id({PoiInformationSet}),
poiBlock BLOCK-TYPE.&Content({PoiInformationSet}{@poiType})
}
END
@@ -1,743 +0,0 @@
POIM-ParkingAvailability {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) poi(103916) parkingAvailability(1) major-version-1 (1) minor-version-1 (1)}
DEFINITIONS AUTOMATIC TAGS ::= BEGIN
IMPORTS
AltitudeValue, ConfidenceLevel, DangerousGoodsBasic, DeltaLatitude, DeltaLongitude, DeltaReferencePosition, EuVehicleCategoryCode, GeoPosition, Identifier2B, Latitude, Longitude,
SpecialTransportType, TimestampIts, VehicleHeight, Wgs84Angle
FROM ETSI-ITS-CDD {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) ts (102894) cdd (2) major-version-4 (4) minor-version-1 (1)} WITH SUCCESSORS
PoiBlockManagementContainer, RelatedPoiInfoBlockContainer
FROM POIM-CommonContainers {itu-t (0) identified-organization (4) etsi (0) itsDomain (5) wg1 (1) poi(103916) common(99) major-version-1 (1) minor-version-1 (1)} WITH SUCCESSORS
;
-- Parking Availability Block for usage within the Point of Interest Message (POIM):
/**
* This type represents the POI Information Block concerning a parking availability at a given facility.
*
* It shall include the following components:
*
* @field managementContainer: the management container of the parking availability POI Information Block.
*
* @field placeInfo: general information about the parking facility.
*
* @field aggregatedStatus: aggregated status information about the parking facility.
*
* @field detailedStatus: detailed topology and status information about the parking areas and spaces.
*
* @field relatedPoi: reference to other relevant POI Information Blocks.
*
*/
ParkingAvailabilityBlock::= SEQUENCE{
managementContainer PoiBlockManagementContainer,
placeInfo GeneralParkingPlaceContainer,
aggregatedStatus ParkingStatusContainer,
detailedStatus ParkingSupplyCapabilitiesContainer OPTIONAL,
relatedPoi RelatedPoiInfoBlockContainer OPTIONAL,
...
}
-- POIM-PA Containers:
/**
* This type represents the General Parking Facility Container.
*
* It shall include the following components:
*
* @field position: the position of the parking facility.
*
* @field name: the name of the parking facility.
*
* @field openingPeriods: the optional opening periods of the parking facility.
*
* @field address: the address of the parking facility.
*
* @field phoneNumber: the phone number of the operator of the parking facility.
*
* @field website: the URL of the website of the operator of the parking facility.
*
*/
GeneralParkingPlaceContainer::= SEQUENCE{
position GeoPosition,
name UTF8String (SIZE (1..31)),
openingPeriods Periods OPTIONAL,
address UTF8String (SIZE (1..255)) OPTIONAL,
phoneNumber UTF8String (SIZE (1..31)) OPTIONAL,
website UTF8String (SIZE (1..31)) OPTIONAL,
...
}
/**
* This type represents the Parking Status Container.
*
* It shall include the following components:
*
* @field currentFacilityStatus: the current status of the facility.
*
* @field currentOccupancy: the optional current occupancy of the entire facility.
* This component is only representative if the entire facility has areas and spaces of the same type. e.g. personal car parking spaces only.
*
* @field detailedStatus: the optional detailed parking availability status of the facility.
*
*/
ParkingStatusContainer::= SEQUENCE {
currentFacilityStatus OpeningStatus,
currentOccupancy OccupancyInformation OPTIONAL,
detailedStatus ParkingStatusList OPTIONAL,
...
}
/**
* This type represents the ParkingSupplyCapabilitiesContainer and is a list of @ref ParkingAreas.
*
*/
ParkingSupplyCapabilitiesContainer::= SEQUENCE OF ParkingAreas
-- DFs for parking availability:
/**
* This DF indicates a geographical position with reference to a reference position.
*
* It shall include the following components:
*
* @field deltaLatitude: the delta latitude of the geographical position.
*
* @field deltaLongitude: the delta longitude of the geographical position.
*
*/
DeltaPosition::=SEQUENCE{
deltaLatitude DeltaLatitude,
deltaLongitude DeltaLongitude
}
/**
* This DF shall contain an an ordered sequence of @ref DeltaPosition with respect to the previous position
* with the first instance referring to a reference position and with the order implicitly defining a direction.
*
*/
DeltaPositions::= SEQUENCE (SIZE (1..32, ...)) OF DeltaPosition
/**
* This DF indicates a geographical position.
*
* It shall include the following components:
*
* @field latitude: the latitude of the geographical position.
*
* @field longitude: the longitude of the geographical position.
*
* @field altitude: the altitude of the geographical position.
*
*/
-- Redundant with GeoPosition from CDD (TS102894-2v221-CDD.asn)
-- GeoPosition::= SEQUENCE{
-- latitude Latitude,
-- longitude Longitude,
-- altitude AltitudeValue DEFAULT unavailable
-- }
/**
* This DF indicates a the load type of the vehicle.
*
* It shall include the following components:
*
* @field goodsType: optional information about the goods being transported.
*
* @field dangerousGoodsType: the optional type of dangerous goods according to the European Agreement concerning the International Carriage of Dangerous Goods by Road (ADR).
*
* @field specialTransportType: the type of special transport according to local regulations.
*
*/
LoadType::= SEQUENCE{
goodsType GoodsType OPTIONAL,
dangerousGoodsType DangerousGoodsBasic OPTIONAL,
specialTransportType SpecialTransportType,
...
}
/**
* This DF represents occupancy information.
*
* It shall include the following components:
*
* @field rate: the current occupancy in percentage, nominally = ((totalSpaces-freeSpaces)/totalSpaces) * 100.
*
* @field trend: the current trend of the occupancy.
*
* @field freeSpaces: the number of spaces that are actually available.
*
* @field totalSpaces: the total number of spaces.
*
* @field confidence: the confidence level of the above information.
*
*/
OccupancyInformation::= SEQUENCE{
rate OccupancyRate,
trend OccupancyTrend,
freeSpaces NumberOfSpaces,
totalSpaces NumberOfSpaces,
confidence ConfidenceLevel,
...
}
/**
* This DF provides information about the parking capabilities and availabilities of a parking identified area.
*
* @note:a parking area possibly groups all spaces with the same characteristics.
*
* It shall include the following components:
*
* @field id: the identifier of the parking area within the parking place/facility.
*
* @field currentOccupancy: the current occupancy information of the parking area.
*
* @field location: the location of the parking area.
*
* @field level: the level under/over ground floor of the parking area.
*
* @field arrangementType: the common arrangement of the parking spaces within the area.
* the value ´mixed´ shall be used if there is no common arrangement. In this case the arrangement of the single spaces may be defined in the component spacesDetailed.
*
* @field boundary: the optional physical boundary of the parking area as a polygon using the component location as reference position.
*
* @field orientation: the optional common orientation of the parking spaces within the area.
* This is component shall be absent if there is no common orientation. In this case the orientation of the single spaces may be defined in the component spacesDetailed.
*
* @field accessViaLane: the optional identifier of a lane that provides access to the parking area.
*
* @field allowedVehicleCategories: the categories of vehicles that are allowed in the parking area.
*
* @field notAllowedEngineCharacteristics: the optional characteristic(s) of engines that are not allowed in the parking area.
*
* @field allowedLoadTypes: the optional load type(s) that are allowed in the parking area.
*
* @field notallowedLoadTypes: the optional load type(s) that are not allowed in the parking area.
*
* @field heightRestriction: the optional applicable vehicle height restriction.
*
* @field reservationType: the optional parking reservation type(s) associated to the parking area.
* This is component shall be absent if there is no common reservation type. In this case the reservation type of the single spaces may be defined in the component spacesDetailed.
*
* @field pricingInformation: the optional pricing information applicable to the parking area.
*
* @field paymentInformation: the optional payment information applicable to the parking area.
*
* @field bookingContactInfo: the optional booking contact information applicable to the parking area.
*
* @field accessCharachteristics: the optional access information to the area.
*
* @field freeSpacesPosition: the optional list of positions of free spaces.
*
* @field freeSpacesId: the optional list of Ids of free spaces.
*
* @field spacesBasic: the optional basic information about none, some or all parking spaces within the area.
*
* @field spacesDetailed: the optional detailed information about none, some or all parking spaces within the area.
*
*/
ParkingArea::=SEQUENCE{
id Identifier2B,
currentOccupancy OccupancyInformation,
location GeoPosition,
level INTEGER(-8..16,...) DEFAULT 0,
arrangementType ParkingAreaArrangementType,
boundary DeltaPositions OPTIONAL,
orientation Wgs84Angle OPTIONAL,
accessViaLane Identifier2B OPTIONAL,
allowedVehicleCategories SEQUENCE (SIZE(1..4,...)) OF EuVehicleCategoryCode,
notAllowedEngineCharacteristics SEQUENCE (SIZE(1..4,...)) OF EngineCharacteristicsFromIso OPTIONAL,
allowedLoadTypes SEQUENCE (SIZE(1..4,...)) OF LoadType OPTIONAL,
notAllowedLoadTypes SEQUENCE (SIZE(1..4,...)) OF LoadType OPTIONAL,
heightRestriction VehicleHeight OPTIONAL,
reservationType SEQUENCE (SIZE(1..4,...)) OF ParkingReservationType OPTIONAL,
pricingInformation UTF8String OPTIONAL,
paymentInformation UTF8String OPTIONAL,
bookingContactInfo UTF8String OPTIONAL,
accessCharachteristics UTF8String OPTIONAL,
freeSpacesPosition SEQUENCE OF DeltaPosition OPTIONAL,
freeSpacesId SEQUENCE OF Identifier2B OPTIONAL,
spacesBasic ParkingSpacesBasic OPTIONAL,
spacesDetailed ParkingSpacesDetailed OPTIONAL,
...
}
/**
* This DF shall contain a list of @ref ParkingArea.
*
*/
ParkingAreas::= SEQUENCE OF ParkingArea
/**
* This DF indicates the allowed type of occupancy of a parking space/area in terms of time and/or usage.
*
* The following options are available:
*
* @field unknown: indicates that the allowed type of occupancy is unknown.
*
* @field unlimitedOccupancy: indicates that it can be occupied without limits.
*
* @field onlyWhileCharging: indicates that it can be occupied only while charging an electric vehicle.
*
* @field limitedDuration: indicates that it can be occupied for a limited and indicated duration in minutes.
*
* @field onlyWhileChargingLimitedDuration: indicates that it can be occupied only while charging an electric vehicle and only for a limited and indicated duration in minutes.
*
* @field parkingAllowedUntil: indicates that it can be occupied only until an indicated moment in time.
*
* @field forcedParkingUntil: indicates that it can be occupied and departure is possible only after an indicated moment in time.
*
*/
ParkingOccupancyInfo::= CHOICE {
unknown NULL,
unlimitedOccupancy NULL,
onlyWhileCharging NULL,
limitedDuration INTEGER,
onlyWhileChargingLimitedDuration INTEGER,
parkingAllowedUntil TimestampIts,
forcedParkingUntil TimestampIts,
...
}
/**
* This DF provides basic information about the parking capabilities and availabilities of a single parking space.
*
* It shall include the following components:
*
* @field id: the unqiue identifier of the parking space within the parking area.
*
* @field location: the optional location of the geometrical center of the parking space w.r.t. the location of the parking area.
*
* @field status: the actual status of the parking space.
*
*/
ParkingSpaceBasic ::= SEQUENCE{
id Identifier2B,
location DeltaReferencePosition OPTIONAL,
status ParkingSpaceStatus
}
/**
* This DF provides detailed information about the parking capabilities and availabilities of a single parking space.
*
* It is an extension of @ref ParkingSpaceBasic and it shall additionally include the following additional components:
*
* @field arrangementType: the optional arrangement of the parking space w.r.t. other spaces.
* This is component, if present, overrides the common arrangementType defined in the @ref ParkingArea.
*
* @field boundary: the optional physical boundary of the parking space as a polygon w.r.t. the location of the parking space.
*
* @field orientation: the optional orientation of the parking space.
* This is component, if present, overrides the common orientation defined in the @ref ParkingArea.
*
* @field occupancyRule: the occupancy rule applicable to the parking space.
*
* @field chargingStationId: the optional identitfier of a charging station that serves the parking space.
*
* @field accessViaLane: the optional identifier of a lane that provides access to the parking space.
*
* @field accessViaParkingSpaces: the optional identifier(s) of a parking spaces that provide access to the parking space.
*
* @field reservationType: the optional parking reservation type(s) associated to the parking space.
* This is component, if present, overrides the common reservationType defined in the @ref ParkingArea.
*
*/
ParkingSpaceDetailed ::= SEQUENCE{
COMPONENTS OF ParkingSpaceBasic,
arrangementType ParkingAreaArrangementType OPTIONAL,
boundary DeltaPositions OPTIONAL,
orientation Wgs84Angle OPTIONAL,
occupancyRule ParkingOccupancyInfo,
chargingStationId Identifier2B OPTIONAL,
accessViaLane Identifier2B OPTIONAL,
accessViaParkingSpaces SEQUENCE (SIZE(0..7)) OF Identifier2B OPTIONAL,
reservationType SEQUENCE (SIZE(1..4,...)) OF ParkingReservationType OPTIONAL,
...
}
/**
* This DF shall contain a list of @ref ParkingSpaceBasic.
*
*/
ParkingSpacesBasic ::= SEQUENCE OF ParkingSpaceBasic
/**
* This DF shall contain a list of @ref ParkingSpaceDetailed.
*
*/
ParkingSpacesDetailed ::= SEQUENCE OF ParkingSpaceDetailed
/**
* This DF indicates the status of parking space.
*
* The following options are available:
*
* @field unknown: indicates that the status is unknown.
*
* @field free: indicates that the parking space is free and hence available to be used.
*
* @field freeUntil: indicates that the parking space is free to be used until an indicated moment in time.
*
* @field fullyOccupied: indicates that the parking space is interely occupied.
*
* @field partiallyOccupied: indicates that the parking space that allows parking of multiple vehicles is occupied by an indicated percentage.
*
* @field occupiedUntil: indicates that the parking space is entirely occupied until an indicated moment in time.
*
* @field reservedUntil: indicates that the parking space is reserved (but not necessarily occupied) until an indicated moment in time.
*
* @field accessBlocked: indicates that the parking space cannot accessed, e.g. due to obstructing vehicles.
*
* @field retrictedUsage: indicates that the parking space is available but that it is not an official parking space and that there are some phyiscal restrictions applicable,
* such as parking behind or in front of other vehicles that, depending on the situation, may then have problems entering or leaving their respective parking spaces.
*
*/
ParkingSpaceStatus::= CHOICE {
unknown NULL,
free NULL,
freeUntil TimestampIts,
fullyOccupied NULL,
partiallyOccupied INTEGER(0..100),
occupiedUntil TimestampIts,
reservedUntil TimestampIts,
accessBlocked NULL,
retrictedUsage NULL,
...
}
/**
* This DF provides information about the status of a defined type of parking.
*
* It shall include the following components:
*
* @field vehicleTypes: the optional types of vehicle for which the parking status is provided.
*
* @field reservationTypes: the optional reservation types for which the parking status is provided.
*
* @field loadTypes: the optional load types for which the parking status is provided.
*
* @field parkingFeeTypes: the optional fee types for which the parking status is provided.
*
* @field occupancyTypes: the optional occupancy types for which the parking status is provided.
*
* @field currentOccupancy: the occupancy for the logical AND combination of the components defined above.
*
*/
ParkingStatus ::= SEQUENCE {
vehicleTypes SEQUENCE (SIZE(1..4,...)) OF EuVehicleCategoryCode OPTIONAL,
reservationTypes SEQUENCE (SIZE(1..4,...)) OF ParkingReservationType OPTIONAL,
loadTypes SEQUENCE (SIZE(1..4,...)) OF LoadType OPTIONAL,
parkingFeeTypes SEQUENCE (SIZE(1..4,...)) OF ParkingFeeType OPTIONAL,
occupancyTypes SEQUENCE (SIZE(1..4,...)) OF ParkingOccupancyInfo OPTIONAL,
currentOccupancy OccupancyInformation,
...
}
((WITH COMPONENTS {..., vehicleTypes PRESENT}) |
(WITH COMPONENTS {..., reservationTypes PRESENT}) |
(WITH COMPONENTS {..., loadTypes PRESENT}) |
(WITH COMPONENTS {..., parkingFeeTypes PRESENT}) |
(WITH COMPONENTS {..., occupancyTypes PRESENT}))
/**
* This DF shall contain a list of @ref ParkingStatus.
*
*/
ParkingStatusList::= SEQUENCE (SIZE(1..16,...)) OF ParkingStatus
/**
* This DF indicates a period occuring on defined days of the week.
*
* It shall include the following components:
*
* @field days: the applicable day(s) of the week.
*
* @field opening: the opening time in local time.
*
* @field closing: the closing time in local time.
*
*/
Period ::= SEQUENCE {
days DayOfWeek,
opening TimeOfDay,
closing TimeOfDay
}
/**
* This DF shall contain a list of up to 8 @ref Period.
*
*/
Periods ::= SEQUENCE (SIZE(1..8,...)) OF Period
/**
* This DF indicates the time of the day.
*
* It shall include the following components:
*
* @field hours: the number of hours after midnight.
*
* @field mins: the number of minutes after the hour.
*
*/
TimeOfDay::= SEQUENCE {
hours INTEGER (0..23),
mins INTEGER (0..59)
}
-- DEs for parking availability:
/**
* This DE indicates the applicable day(s) of the week.
*
*/
DayOfWeek::= BIT STRING {unused(0), monday(1), tuesday(2), wednesday(3), thursday(4), friday(5), saturday(6), sunday(7)} (SIZE (8))
/**
* This DE indicates the type of goods of a vehicle.
*
* The value shall be set to:
* - `0` to indicate: Transport of ammunition.
* - `1` to indicate: Transport of chemicals of unspecified type.
* - `2` to indicate: Empty load.
* - `3` to indicate: Transport of fuel of unspecified type.
* - `4` to indicate: Transport of glass.
* - `5` to indicate: Transport of materials classified as of dangerous or hazardous nature.
* - `6` to indicate: Transport of liquids of an unspecified nature.
* - `7` to indicate: Transport of livestock.
* - `8` to indicate: Transport of materials classed as being of a danger to people or animals.
* - `9` to indicate: Transport of materials classed as being potentially dangerous to the environment.
* - `10` to indicate: Transport of materials classed as being dangerous when exposed to water.
* - `11` to indicate: Transport of fresh products or produce that will significantly degrade in quality or freshness over a short period of time.
* - `12` to indicate: Transport of pharmaceutical materials.
* - `13` to indicate: Transport of vehicles of any type.
* - `14` to indicate: Refrigerated transport.
*
*/
GoodsType::= INTEGER {
ammunition (0),
chemicals (1),
empty (2),
fuel (3),
glass (4),
dangerous (5),
liquid (6),
liveStock (7),
dangerousForPeople (8),
dangerousForTheEnvironment (9),
dangerousForWater (10),
perishableProducts (11),
pharmaceutical (12),
vehicles (13),
refrigerated (14)
-- other values reserved for future use
} (0..15,...)
/**
* This DE represents a number of parking spaces.
*
* The value shall be set to:
* - `n` (`n > 0` and `n < 16383`) : for the number of spaces.
* - `16383` : in case the number of spaces is not available.
*
*/
NumberOfSpaces ::= INTEGER {
unavailable (16383)
} (1..16383)
/**
* This DE represents an occupancy rate in percentage.
*
* The value shall be set to:
* - `n` (`n > 0` and `n < 101`) : for the occupany rate in %,
* - `101` : in case the occupany rate is not available.
*
*/
OccupancyRate ::= INTEGER {
unavailable (101)
} (1..101)
/**
* This DE indicates the trend of occupancy.
*
* The value shall be set to:
* - `0` to indicate that occupancy is decreasing,
* - `1` to indicate that occupancy is decreasing quickly,
* - `2` to indicate that occupancy is decreasing slowly,
* - `3` to indicate that occupancy is increasing,
* - `4` to indicate that occupancy is increasing quickly,
* - `5` to indicate that occupancy is increasing slowly,
* - `6` to indicate that occupancy is stable,
* - `7` to indicate that occupancy is variable with no clear trend,
* - `15` to indicate that occupancy is unknown,
* - 8-14 - reserved for future usage.
*
*/
OccupancyTrend ::= INTEGER {
decreasing (0),
decreasingQuickly (1),
decreasingSlowly (2),
increasing (3),
increasingQuickly (4),
increasingSlowly (5),
stable (6),
variable (7),
unknown (15)
} (0..15)
/**
* This DE indicates the opening status.
*
* The value shall be set to:
* - `0` to indicate that the opening status is: closed,
* - `1` to indicate that the opening status is: open,
* - `2` to indicate that the opening status is: open for subscribers only,
* - `3` to indicate that the opening status is: open but currently full, i.e. no parking spaces available,
* - `15` to indicate that occupancy is unknown,
* - 4-14 - reserved for future usage.
*
*/
OpeningStatus ::= INTEGER {
closed (0),
open (1),
subscribersOnly (2),
full (3),
unknown (15)
} (0..15)
/**
* This DE indicates the arrangement of parking space in a parking area.
*
* The value shall be set to:
* - `0` to indicate that the parking spaces are arranged in a line and parallel to a road or curb,
* - `1` to indicate that the parking spaces are arranged side-by-side and diagonally to a curb,
* - `2` to indicate that the parking spaces are arranged side-by-side and perpendicularly to a curb,
* - `3` to indicate that the parking spaces are arranged so that vehicles form a queue,
* - `4` to indicate that the parking spaces are arranged in a mixed fashion,
* - 5-7 - reserved for future usage.
*
*/
ParkingAreaArrangementType ::= INTEGER {
parallelParkingSpace(0),
diagonalParkingSpace(1),
perpendicularParkingSpace(2),
queueParking (3),
mixed(4),
unknown(7)
} (0..7)
/**
* This DE indicates the type of a reservation of a parking space/area.
*
* The value shall be set to:
* - `0` to indicate that it is reserved to disabled persons,
* - `1` to indicate that it is reserved to pregnant women,
* - `2` to indicate that it is reserved to women,
* - `3` to indicate that it is reserved to parents with small children,
* - `4` to indicate that it is reserved for loading and unloading of goods,
* - `5` to indicate that it is reserved for manual charging of electric vehicles,
* - `6` to indicate that it is reserved for automated charging of electric vehicles,
* - `7` to indicate that it is reserved for vehicles carrying out refrigerated transport of goods,
* - `8` to indicate that it is reserved for VIPs,
* - `9` to indicate that it is reserved for pre-booked reservations only,
* - `10` to indicate that it is not reserved and can still be reserved,
* - `11` to indicate that it cannot be reserved,
* - `12` to indicate that it reserved for drop-off and pick-up of vehicles for automated valet parking,
* - `13` to indicate that it is reserved for vehicles with a permit,
* - 14-31 - reserved for future usage.
*
*/
ParkingReservationType ::= INTEGER {
disabled(0),
pregnant(1),
womenOnly(2),
parentAndChild(3),
loadAndOffloadGoods(4),
manualElectricVehicleCharging (5),
automatedElectricVehicleCharging (6),
refriferatedTransport(7),
vip(8),
preBooking (9),
freeToBeReserved (10),
reservationNotPossible (11),
automatedValetparking (12),
permit (13)
}(0..31)
/**
* This DE indicates the applicable type of parking fee.
*
* The value shall be set to:
* - `0` to indicate that the applicable fee is time dependent, e.g. an hourly fee,
* - `1` to indicate that ithe applicable fee is flat for a certain period, e.g. a flat fee for daily parking,
* - `2` to indicate that the applicable fee is included in the subscription, e.g. a monthly subscription,
* - 3-7 - reserved for future usage.
*
*/
ParkingFeeType::= INTEGER {
timeDependentFee(0),
flatFee(1),
subscription(2)
}(0..7)
EngineCharacteristicsFromIso ::= INTEGER {
noEntry (0),
noEngine (1),
petrolUnleaded (2),
petrolLeaded (3),
diesel (4),
lpg (5),
battery (6),
solar (7),
hybrid (8),
hydrogen (9),
multiFuel (10),
bivalentPetrolLpg (11),
bivalentPetrolCng (12),
combinedPetrolElectric (13),
cng (14),
lng (15),
combinedDieselElectric (16),
combinedHydrogenElectric (17),
bivalentHydrogenPetrol (18),
bivalentHydrogenPetrolElectricEngine (19),
fuelCellHydrogen (20),
fuelCellPetrol (21),
fuelCellMethanol (22),
fuelCellEthanol (23),
fuelCellDiesel (24),
combinedMultiFuelElectricEngine (25),
combinedCngElectricEngine (26),
combinedLngElectricEngine (27),
petrolEthanol (28),
combinedLpgElectricEngine (29),
hybridPetrolExternalBattery (30),
hybridDieselExternalBattery (31),
hybridLpgExternalBattery (32),
hybridHydrogenExternalBattery (33),
hybridMultiFuelExternalBattery (34),
hybridCngExternalBattery (35),
hybridLngExternalBattery (36),
hybridBivalentHydrogenPetrolExternalBattery (37),
hydrogenCng (38),
hydrogenLng (39),
hybridHydrogenCngExternalBattery (40),
hybridHydrogenLngExternalBattery (41),
ethanol (42),
hybridFuelCellHydrogen (43),
hybridFuelCellHydrogenExternalBattery (44),
dualFuelLngDiesel (45),
electricExternal (46),
biogas (47),
bioDiesel (48),
bioPetrol (49),
bivalentPetrolBiogas (50),
combinedBiogasElectricEngine (51),
dualFuelCngDiesel (52),
-- (53-254) are reserved for future CEN and ISO use
other (255)
} (0..255)
END

Some files were not shown because too many files have changed in this diff Show More