Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3fc9bf66c | ||
|
|
6e4d293c3a | ||
|
|
d107534eb2 | ||
|
|
2f60623e18 | ||
|
|
a08494b56a | ||
|
|
d2fd222a62 | ||
|
|
7285fa19b7 | ||
|
|
21e01499d8 | ||
|
|
01204a2c22 |
+17
@@ -46,3 +46,20 @@ sdkconfig.old
|
|||||||
# Office lock files. Word/Excel create these beside a document while it is open
|
# Office lock files. Word/Excel create these beside a document while it is open
|
||||||
# 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).
|
||||||
|
# Restore with: esptool --chip esp32c5 -p COM<N> write-flash 0 <image>
|
||||||
|
/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/
|
||||||
|
|
||||||
|
# The colleague's standalone ESP32-C5 VRU station (its own repository, HAW GitLab
|
||||||
|
# urban-mobility-lab/microbu/microbu-esp32c5). Kept beside the project on the lab laptop, for
|
||||||
|
# reference and because the V2X2MAP bridge runs from its tools/, but not part of this repository:
|
||||||
|
# obu-firmware only needs its vanetza-idf, which is copied to obu-firmware/external/vanetza-idf.
|
||||||
|
/microbu-esp32c5/
|
||||||
|
|
||||||
|
# draw.io keeps a backup beside an open diagram.
|
||||||
|
*.drawio.bkp
|
||||||
|
|||||||
@@ -5,6 +5,195 @@ 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 `tools/wireshark/psid-vru.lua` from the colleague's microbu-esp32c5 repository
|
||||||
|
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
|
||||||
@@ -28,6 +217,27 @@ Partial check possible with one board and no phone: flash it, `idf.py -p COMx mo
|
|||||||
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
|
||||||
@@ -57,6 +267,58 @@ optional, but shows what was on air at the time.
|
|||||||
shorter).
|
shorter).
|
||||||
- [ ] The heartbeat's oversize counter still counts over-long messages (e.g. road SPATEMs).
|
- [ ] The heartbeat's oversize counter still counts over-long messages (e.g. road SPATEMs).
|
||||||
|
|
||||||
|
### CiT One custom CAM injection over `v2x/tx/v2/cam` (added 2026-09-14)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
- [x] Install MSYS2 UCRT64 gcc (done 2026-09-11: gcc 16.2.0, GNU Make 4.4.1; chosen over WSL,
|
- [x] Install MSYS2 UCRT64 gcc (done 2026-09-11: gcc 16.2.0, GNU Make 4.4.1; chosen over WSL,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?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" />
|
||||||
@@ -10,6 +11,15 @@
|
|||||||
|
|
||||||
<!-- 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,10 +26,12 @@ 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.UsbSerialState
|
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState
|
||||||
|
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
|
||||||
@@ -40,6 +42,7 @@ 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
|
||||||
@@ -86,7 +89,10 @@ 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 usbSerialState by mqttViewModel.usbSerialState.collectAsState()
|
val esp32LinkState by mqttViewModel.esp32LinkState.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.
|
||||||
@@ -104,6 +110,13 @@ 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 ->
|
||||||
@@ -119,11 +132,17 @@ 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)
|
||||||
@@ -132,14 +151,17 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
StatusTopBar(
|
if (!isFullBleed) {
|
||||||
state = state,
|
StatusTopBar(
|
||||||
mqttConnectionState = mqttConnectionState,
|
state = state,
|
||||||
isEsp32 = obuHardware == ObuHardware.ESP32_C5,
|
mqttConnectionState = mqttConnectionState,
|
||||||
usbSerialState = usbSerialState,
|
isEsp32 = obuHardware == ObuHardware.ESP32_C5,
|
||||||
)
|
esp32LinkState = esp32LinkState,
|
||||||
|
esp32Bluetooth = esp32Transport == Esp32Transport.BLE,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bottomBar = { BottomNavBar(navController) },
|
bottomBar = { if (!isFullBleed) BottomNavBar(navController) },
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
@@ -152,7 +174,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
mqttConnectionState = mqttConnectionState,
|
mqttConnectionState = mqttConnectionState,
|
||||||
activeTransport = activeTransport,
|
activeTransport = activeTransport,
|
||||||
obuHardware = obuHardware,
|
obuHardware = obuHardware,
|
||||||
usbSerialState = usbSerialState,
|
esp32LinkState = esp32LinkState,
|
||||||
|
esp32Bluetooth = esp32Transport == Esp32Transport.BLE,
|
||||||
usbCableConnected = usbConnected,
|
usbCableConnected = usbConnected,
|
||||||
obuStationTypeWarning = obuStationTypeWarning,
|
obuStationTypeWarning = obuStationTypeWarning,
|
||||||
obuStationType = obuStationType,
|
obuStationType = obuStationType,
|
||||||
@@ -191,7 +214,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
state = state,
|
state = state,
|
||||||
mqttConnectionState = mqttConnectionState,
|
mqttConnectionState = mqttConnectionState,
|
||||||
obuConnected = if (obuHardware == ObuHardware.ESP32_C5)
|
obuConnected = if (obuHardware == ObuHardware.ESP32_C5)
|
||||||
usbSerialState == UsbSerialState.CONNECTED
|
esp32LinkState == Esp32LinkState.CONNECTED
|
||||||
else
|
else
|
||||||
mqttConnectionState == MqttConnectionState.CONNECTED,
|
mqttConnectionState == MqttConnectionState.CONNECTED,
|
||||||
tripServiceState = tripServiceState,
|
tripServiceState = tripServiceState,
|
||||||
@@ -253,7 +276,19 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composable(Screen.MqttViewer.route) {
|
composable(Screen.MqttViewer.route) {
|
||||||
MqttTopicViewerScreen(viewModel = mqttViewModel)
|
MqttTopicViewerScreen(
|
||||||
|
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(
|
||||||
@@ -281,6 +316,12 @@ 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,7 +21,13 @@ 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,
|
||||||
* typically indoors before any satellite fix since boot, the wall clock is used unchanged.
|
* the last error measured is kept, because the wall clock's error changes slowly while GNSS time
|
||||||
|
* 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.
|
||||||
@@ -36,6 +42,9 @@ 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()
|
||||||
@@ -44,8 +53,9 @@ 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, gnssNow, systemNow)
|
return ItsTime.onGnssTime(systemMs, lastErrorMs?.let { systemNow + it }, systemNow)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun noteSource(gnssNow: Long?, systemNow: Long) {
|
private fun noteSource(gnssNow: Long?, systemNow: Long) {
|
||||||
@@ -55,6 +65,9 @@ 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.UsbSerialState
|
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState
|
||||||
import com.hawhamburg.micr0bu.data.transport.UsbSerialTransport
|
import com.hawhamburg.micr0bu.data.transport.Esp32Link
|
||||||
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 usbSerialTransport: UsbSerialTransport,
|
private val esp32Link: Esp32Link,
|
||||||
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 {
|
||||||
usbSerialTransport.incomingFrames.collect { frame ->
|
esp32Link.incomingFrames.collect { frame ->
|
||||||
if (frame.type != SerialFrameType.V2X_RX) return@collect
|
if (frame.type != SerialFrameType.V2X_RX) return@collect
|
||||||
if (usbSerialTransport.state.value != UsbSerialState.CONNECTED) return@collect
|
if (esp32Link.state.value != Esp32LinkState.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 {
|
||||||
usbSerialTransport.state.collect { state ->
|
esp32Link.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 != UsbSerialState.CONNECTED) {
|
if (currentHardware == ObuHardware.ESP32_C5 && state != Esp32LinkState.CONNECTED) {
|
||||||
engine.reset()
|
engine.reset()
|
||||||
_rsuStations.value = emptyMap()
|
_rsuStations.value = emptyMap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
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
|
||||||
@@ -32,6 +35,10 @@ 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 ->
|
||||||
@@ -42,6 +49,37 @@ 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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,466 @@
|
|||||||
|
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()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
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,8 +57,9 @@ 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]` (7 bytes). Counters are free-running totals since firmware boot and
|
* [rxCrcErrors:2 LE][capabilities:1][rxQueueDrops:2 LE]` (10 bytes; the last two fields are an
|
||||||
* saturate at 0xFFFF rather than wrapping.
|
* optional tail — see [capabilities] and [rxQueueDrops]). Counters are free-running totals since
|
||||||
|
* firmware boot and saturate at 0xFFFF rather than wrapping.
|
||||||
*
|
*
|
||||||
* Exists so the phone can tell "link alive, no traffic" from "link dead", and so firmware-side
|
* 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
|
||||||
@@ -79,6 +80,15 @@ 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
|
||||||
@@ -99,6 +109,7 @@ 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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
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,9 +35,6 @@ 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"
|
||||||
|
|
||||||
@@ -63,6 +60,11 @@ 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
|
||||||
@@ -111,8 +113,8 @@ class UsbSerialTransport @Inject constructor(
|
|||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
private val _state = MutableStateFlow(UsbSerialState.DISCONNECTED)
|
private val _state = MutableStateFlow(Esp32LinkState.DISCONNECTED)
|
||||||
val state: StateFlow<UsbSerialState> = _state.asStateFlow()
|
val state: StateFlow<Esp32LinkState> = _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]. */
|
||||||
@@ -150,7 +152,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 = UsbSerialState.ERROR
|
_state.value = Esp32LinkState.ERROR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
|
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
|
||||||
@@ -174,7 +176,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 == UsbSerialState.CONNECTED) {
|
if (_state.value == Esp32LinkState.CONNECTED) {
|
||||||
Log.i(TAG, "connect(): already connected, no-op")
|
Log.i(TAG, "connect(): already connected, no-op")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -197,7 +199,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 = UsbSerialState.DISCONNECTED
|
_state.value = Esp32LinkState.DISCONNECTED
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (espDriver == null) {
|
if (espDriver == null) {
|
||||||
@@ -211,14 +213,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 = UsbSerialState.DEVICE_ATTACHED
|
_state.value = Esp32LinkState.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 = UsbSerialState.PERMISSION_REQUESTED
|
_state.value = Esp32LinkState.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,
|
||||||
@@ -269,14 +271,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 = UsbSerialState.ERROR
|
_state.value = Esp32LinkState.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 = UsbSerialState.ERROR
|
_state.value = Esp32LinkState.ERROR
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +289,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 = UsbSerialState.ERROR
|
_state.value = Esp32LinkState.ERROR
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,13 +332,15 @@ 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
|
||||||
}
|
}
|
||||||
@@ -347,7 +351,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 = UsbSerialState.ERROR
|
_state.value = Esp32LinkState.ERROR
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
ioManager = manager
|
ioManager = manager
|
||||||
@@ -360,13 +364,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 = UsbSerialState.CONNECTED
|
_state.value = Esp32LinkState.CONNECTED
|
||||||
startWatchdog()
|
startWatchdog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flips the link to [UsbSerialState.ERROR] once the firmware's 1 Hz STATUS heartbeat has been
|
* Flips the link to [Esp32LinkState.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.
|
||||||
*/
|
*/
|
||||||
@@ -375,14 +379,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 != UsbSerialState.CONNECTED) continue
|
if (_state.value != Esp32LinkState.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 = UsbSerialState.ERROR
|
_state.value = Esp32LinkState.ERROR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -443,7 +447,29 @@ 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 = UsbSerialState.ERROR
|
_state.value = Esp32LinkState.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,7 +485,7 @@ class UsbSerialTransport @Inject constructor(
|
|||||||
openDeviceName = null
|
openDeviceName = null
|
||||||
_linkStatus.value = null
|
_linkStatus.value = null
|
||||||
_consecutiveWriteFailures.value = 0
|
_consecutiveWriteFailures.value = 0
|
||||||
_state.value = UsbSerialState.DISCONNECTED
|
_state.value = Esp32LinkState.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.UsbSerialTransport], and UPER-decodes whatever the
|
* to [com.hawhamburg.micr0bu.data.transport.Esp32Link], 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).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
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(...)` → `UsbSerialTransport` (write).
|
* `PhoneCamBuilder.build(...)` → `RealAsn1UperCodec.encodeCam(...)` → `Esp32Link` (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
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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,7 +6,10 @@ 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.transport.UsbSerialTransport
|
import com.hawhamburg.micr0bu.data.mqtt.ObuHardwarePreferences
|
||||||
|
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
|
||||||
@@ -21,6 +24,7 @@ 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
|
||||||
@@ -49,7 +53,8 @@ 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 usbSerialTransport: UsbSerialTransport,
|
private val esp32Link: Esp32Link,
|
||||||
|
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)
|
||||||
@@ -114,7 +119,8 @@ 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)
|
||||||
if (usbSerialTransport.sendCamTx(bytes, pv)) {
|
val signed = prefs.signOutgoingFlow.first()
|
||||||
|
if (esp32Link.send(OutgoingIts(OutgoingMessage.CAM, bytes, pv, gnss.accuracyM, signed))) {
|
||||||
_sentCount.update { it + 1 }
|
_sentCount.update { it + 1 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,18 @@ 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.UsbSerialTransport
|
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.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
|
||||||
@@ -45,12 +50,19 @@ 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 usbSerialTransport: UsbSerialTransport,
|
private val esp32Link: Esp32Link,
|
||||||
private val codec: RealAsn1UperCodec,
|
private val codec: RealAsn1UperCodec,
|
||||||
private val pseudonymManager: PseudonymManager,
|
private val pseudonymManager: PseudonymManager,
|
||||||
) {
|
) {
|
||||||
@@ -67,6 +79,10 @@ 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
|
||||||
@@ -85,6 +101,7 @@ 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
|
||||||
@@ -102,24 +119,66 @@ 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) {
|
||||||
// Asked for per CAM rather than once per trip: that is what lets a pseudonym
|
when (outgoing) {
|
||||||
// rotation fall cleanly between two frames instead of inside one.
|
OutgoingMessage.CAM -> sendCam(gnss)
|
||||||
val pseudonym = pseudonymManager.current()
|
OutgoingMessage.VAM -> sendVamIfDue(gnss)
|
||||||
// Stamped on GNSS time rather than the phone clock; see GnssTimeSource. Only the
|
}
|
||||||
// outgoing CAM is: acceleration below still differences wall-clock samples.
|
|
||||||
val fix = gnss.copy(timestamp = GnssTimeSource.correct(gnss.timestamp))
|
|
||||||
val cam = PhoneCamBuilder.build(fix, latestGyroZ, pseudonym.stationId, longitudinalAccel(gnss))
|
|
||||||
val bytes = codec.encodeCam(cam)
|
|
||||||
usbSerialTransport.sendCamTx(bytes, GnPositionVector.fromCam(cam, gnss.accuracyM, pseudonym.mac))
|
|
||||||
}
|
}
|
||||||
delay((1000.0 / currentRateHz(gnss)).toLong())
|
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
|
||||||
|
// rotation fall cleanly between two frames instead of inside one.
|
||||||
|
val pseudonym = pseudonymManager.current()
|
||||||
|
// Stamped on GNSS time rather than the phone clock; see GnssTimeSource. Only the
|
||||||
|
// outgoing CAM is: acceleration below still differences wall-clock samples.
|
||||||
|
val fix = gnss.copy(timestamp = GnssTimeSource.correct(gnss.timestamp))
|
||||||
|
val cam = PhoneCamBuilder.build(fix, latestGyroZ, pseudonym.stationId, longitudinalAccel(gnss))
|
||||||
|
val bytes = codec.encodeCam(cam)
|
||||||
|
esp32Link.send(OutgoingIts(OutgoingMessage.CAM, bytes,
|
||||||
|
GnPositionVector.fromCam(cam, gnss.accuracyM, pseudonym.mac), gnss.accuracyM, signOutgoing))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Along-track acceleration in m/s², from the change in GNSS speed since the previous fix.
|
* Along-track acceleration in m/s², from the change in GNSS speed since the previous fix.
|
||||||
*
|
*
|
||||||
@@ -158,6 +217,9 @@ 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,6 +20,8 @@ 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
|
||||||
@@ -39,7 +41,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.UsbSerialState
|
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState
|
||||||
import com.hawhamburg.micr0bu.viewmodel.SensorUiState
|
import com.hawhamburg.micr0bu.viewmodel.SensorUiState
|
||||||
|
|
||||||
private val GreenActive = Color(0xFF4CAF50)
|
private val GreenActive = Color(0xFF4CAF50)
|
||||||
@@ -53,7 +55,9 @@ fun StatusTopBar(
|
|||||||
state: SensorUiState,
|
state: SensorUiState,
|
||||||
mqttConnectionState: MqttConnectionState,
|
mqttConnectionState: MqttConnectionState,
|
||||||
isEsp32: Boolean = false,
|
isEsp32: Boolean = false,
|
||||||
usbSerialState: UsbSerialState = UsbSerialState.DISCONNECTED,
|
esp32LinkState: Esp32LinkState = Esp32LinkState.DISCONNECTED,
|
||||||
|
/** The ESP32-C5 is reached over BLE rather than its USB port (Settings). */
|
||||||
|
esp32Bluetooth: Boolean = false,
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
@@ -93,7 +97,7 @@ fun StatusTopBar(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
|
|
||||||
ObuStatusIcon(mqttConnectionState, isEsp32, usbSerialState)
|
ObuStatusIcon(mqttConnectionState, isEsp32, esp32LinkState, esp32Bluetooth = esp32Bluetooth)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
@@ -106,20 +110,21 @@ 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 glyph there rather than the
|
* report the OBU as offline while CAMs were streaming in. Uses a USB or Bluetooth glyph there
|
||||||
* Wi-Fi one, since that is literally what the connection is.
|
* rather than the 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,
|
||||||
usbSerialState: UsbSerialState,
|
esp32LinkState: Esp32LinkState,
|
||||||
|
esp32Bluetooth: Boolean,
|
||||||
) {
|
) {
|
||||||
val state = if (isEsp32) usbSerialState.asConnectionState() else mqttState
|
val state = if (isEsp32) esp32LinkState.asConnectionState() else mqttState
|
||||||
|
val linkUp = state == MqttConnectionState.CONNECTED || state == MqttConnectionState.CONNECTING
|
||||||
val icon = when {
|
val icon = when {
|
||||||
isEsp32 && state == MqttConnectionState.CONNECTED -> Icons.Default.Usb
|
isEsp32 && esp32Bluetooth -> if (linkUp) Icons.Default.Bluetooth else Icons.Default.BluetoothDisabled
|
||||||
isEsp32 && state == MqttConnectionState.CONNECTING -> Icons.Default.Usb
|
isEsp32 -> if (linkUp) Icons.Default.Usb else Icons.Default.UsbOff
|
||||||
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
|
||||||
@@ -194,10 +199,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 UsbSerialState.asConnectionState(): MqttConnectionState = when (this) {
|
private fun Esp32LinkState.asConnectionState(): MqttConnectionState = when (this) {
|
||||||
UsbSerialState.CONNECTED -> MqttConnectionState.CONNECTED
|
Esp32LinkState.CONNECTED -> MqttConnectionState.CONNECTED
|
||||||
UsbSerialState.DEVICE_ATTACHED,
|
Esp32LinkState.DEVICE_ATTACHED,
|
||||||
UsbSerialState.PERMISSION_REQUESTED -> MqttConnectionState.CONNECTING
|
Esp32LinkState.PERMISSION_REQUESTED -> MqttConnectionState.CONNECTING
|
||||||
UsbSerialState.ERROR -> MqttConnectionState.ERROR
|
Esp32LinkState.ERROR -> MqttConnectionState.ERROR
|
||||||
UsbSerialState.DISCONNECTED -> MqttConnectionState.DISCONNECTED
|
Esp32LinkState.DISCONNECTED -> MqttConnectionState.DISCONNECTED
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ 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)
|
||||||
@@ -83,6 +85,7 @@ 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,7 +44,9 @@ 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.UsbSerialState
|
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState
|
||||||
|
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)
|
||||||
@@ -65,7 +67,11 @@ 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 usbSerialState by viewModel.usbSerialState.collectAsState()
|
val esp32LinkState by viewModel.esp32LinkState.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
|
||||||
@@ -229,22 +235,22 @@ fun ConnectionSetupScreen(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// ── ESP32-C5 real connection card (Phase 03) ────────────────────────
|
// ── ESP32-C5 real connection card (Phase 03) ────────────────────────
|
||||||
// Backed by UsbSerialTransport (native USB Serial/JTAG CDC-ACM link) - see that
|
// Backed by Esp32Link: USB (UsbSerialTransport, native USB Serial/JTAG CDC-ACM - see
|
||||||
// class's KDoc for the VID/PID (0x303A/0x1001) and native-vs-UART-bridge port note.
|
// its KDoc for the VID/PID and native-vs-UART-bridge port note) or BLE (BleLinkTransport).
|
||||||
val isEspConnected = usbSerialState == UsbSerialState.CONNECTED
|
val isEspConnected = esp32LinkState == Esp32LinkState.CONNECTED
|
||||||
val isEspBusy = usbSerialState == UsbSerialState.DEVICE_ATTACHED ||
|
val isEspBusy = esp32LinkState == Esp32LinkState.DEVICE_ATTACHED ||
|
||||||
usbSerialState == UsbSerialState.PERMISSION_REQUESTED
|
esp32LinkState == Esp32LinkState.PERMISSION_REQUESTED
|
||||||
val espContainerColor = when {
|
val espContainerColor = when {
|
||||||
isEspConnected -> UsbGreenBg
|
isEspConnected -> UsbGreenBg
|
||||||
isEspBusy -> UsbAmberBg
|
isEspBusy -> UsbAmberBg
|
||||||
else -> UsbGrayBg
|
else -> UsbGrayBg
|
||||||
}
|
}
|
||||||
val (espColor, espStateLabel) = when (usbSerialState) {
|
val (espColor, espStateLabel) = when (esp32LinkState) {
|
||||||
UsbSerialState.CONNECTED -> UsbGreen to stringResource(R.string.conn_esp32_state_connected)
|
Esp32LinkState.CONNECTED -> UsbGreen to stringResource(R.string.conn_esp32_state_connected)
|
||||||
UsbSerialState.DEVICE_ATTACHED -> UsbAmber to stringResource(R.string.conn_esp32_state_device_attached)
|
Esp32LinkState.DEVICE_ATTACHED -> UsbAmber to stringResource(R.string.conn_esp32_state_device_attached)
|
||||||
UsbSerialState.PERMISSION_REQUESTED -> UsbAmber to stringResource(R.string.conn_esp32_state_permission_requested)
|
Esp32LinkState.PERMISSION_REQUESTED -> UsbAmber to stringResource(R.string.conn_esp32_state_permission_requested)
|
||||||
UsbSerialState.ERROR -> Color(0xFFFF5252) to stringResource(R.string.conn_esp32_state_error)
|
Esp32LinkState.ERROR -> Color(0xFFFF5252) to stringResource(R.string.conn_esp32_state_error)
|
||||||
UsbSerialState.DISCONNECTED -> UsbGray to stringResource(R.string.conn_esp32_state_disconnected)
|
Esp32LinkState.DISCONNECTED -> UsbGray to stringResource(R.string.conn_esp32_state_disconnected)
|
||||||
}
|
}
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
@@ -257,7 +263,7 @@ fun ConnectionSetupScreen(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Usb,
|
if (esp32Transport == Esp32Transport.BLE) Icons.Default.Bluetooth else Icons.Default.Usb,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = espColor,
|
tint = espColor,
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
@@ -285,11 +291,36 @@ 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) {
|
if (isEspConnected || isEspBusy) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { viewModel.disconnectUsbSerial() },
|
onClick = { viewModel.disconnectEsp32() },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
contentColor = Color(0xFFFF5252),
|
contentColor = Color(0xFFFF5252),
|
||||||
@@ -297,13 +328,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))
|
||||||
Text(stringResource(R.string.conn_disconnect))
|
// While connecting (BLE retries until it succeeds) this cancels the attempt.
|
||||||
|
Text(stringResource(if (isEspConnected) R.string.conn_disconnect else R.string.conn_esp32_cancel))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button(
|
Button(
|
||||||
onClick = { viewModel.connectUsbSerial() },
|
onClick = { viewModel.connectEsp32() },
|
||||||
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.UsbSerialState
|
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState
|
||||||
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,7 +78,9 @@ 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,
|
||||||
usbSerialState: UsbSerialState = UsbSerialState.DISCONNECTED,
|
esp32LinkState: Esp32LinkState = Esp32LinkState.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,
|
||||||
@@ -281,14 +283,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) usbSerialState == UsbSerialState.CONNECTED else mqttConnected
|
val obuConnected = if (isEsp32) esp32LinkState == Esp32LinkState.CONNECTED else mqttConnected
|
||||||
val transportIcon = when (activeTransport) {
|
val transportIcon = if (isEsp32 && esp32Bluetooth) Icons.Default.Bluetooth else 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 = when (activeTransport) {
|
val transportInactiveIcon = if (isEsp32 && esp32Bluetooth) Icons.Default.BluetoothDisabled else 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
|
||||||
@@ -324,12 +326,12 @@ fun DashboardScreen(
|
|||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = if (isEsp32) {
|
text = if (isEsp32) {
|
||||||
when (usbSerialState) {
|
when (esp32LinkState) {
|
||||||
UsbSerialState.CONNECTED -> stringResource(R.string.conn_esp32_state_connected)
|
Esp32LinkState.CONNECTED -> stringResource(R.string.conn_esp32_state_connected)
|
||||||
UsbSerialState.DEVICE_ATTACHED -> stringResource(R.string.conn_esp32_state_device_attached)
|
Esp32LinkState.DEVICE_ATTACHED -> stringResource(R.string.conn_esp32_state_device_attached)
|
||||||
UsbSerialState.PERMISSION_REQUESTED -> stringResource(R.string.conn_esp32_state_permission_requested)
|
Esp32LinkState.PERMISSION_REQUESTED -> stringResource(R.string.conn_esp32_state_permission_requested)
|
||||||
UsbSerialState.ERROR -> stringResource(R.string.conn_esp32_state_error)
|
Esp32LinkState.ERROR -> stringResource(R.string.conn_esp32_state_error)
|
||||||
UsbSerialState.DISCONNECTED -> stringResource(R.string.dash_tap_to_connect)
|
Esp32LinkState.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)
|
||||||
@@ -362,15 +364,16 @@ 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 = activeTransport == TransportType.USB_SERIAL,
|
active = !esp32Bluetooth,
|
||||||
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 = activeTransport == TransportType.BLUETOOTH,
|
active = isEsp32 && esp32Bluetooth,
|
||||||
dimmed = true, // Phase 03 — production BT transport still under discussion
|
// Only the ESP32-C5 has a BLE link; the CiT One has none.
|
||||||
|
dimmed = !isEsp32,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ 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
|
||||||
@@ -69,7 +70,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.UsbSerialState
|
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState
|
||||||
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
|
||||||
@@ -87,11 +88,15 @@ import java.util.Date
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View toggle for [TopicListPane]: decoded CAM/DENM traffic (LIST), the raw MQTT topic list
|
* View toggle for [TopicListPane]: decoded CAM/DENM traffic (LIST) or the raw MQTT topic list
|
||||||
* (TOPICS, CiT One only - there is no broker on the ESP32-C5 path), or the V2X live map
|
* (TOPICS, CiT One only - there is no broker on the ESP32-C5 path).
|
||||||
* (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, MAP }
|
private enum class TopicViewMode { LIST, TOPICS }
|
||||||
|
|
||||||
private val timeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
|
private val timeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
|
||||||
|
|
||||||
@@ -116,6 +121,7 @@ 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()
|
||||||
@@ -130,7 +136,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 usbSerialState by viewModel.usbSerialState.collectAsState()
|
val esp32LinkState by viewModel.esp32LinkState.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()
|
||||||
@@ -155,7 +161,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) usbSerialState.asConnectionState() else connectionState
|
val effectiveState = if (isEsp32) esp32LinkState.asConnectionState() else connectionState
|
||||||
val isConnected = effectiveState == MqttConnectionState.CONNECTED
|
val isConnected = effectiveState == MqttConnectionState.CONNECTED
|
||||||
val isConnecting = effectiveState == MqttConnectionState.CONNECTING
|
val isConnecting = effectiveState == MqttConnectionState.CONNECTING
|
||||||
|
|
||||||
@@ -193,14 +199,25 @@ 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.disconnectUsbSerial()
|
if (isConnected || isConnecting) viewModel.disconnectEsp32()
|
||||||
else viewModel.connectUsbSerial()
|
else viewModel.connectEsp32()
|
||||||
} else {
|
} else {
|
||||||
if (isConnected || isConnecting) viewModel.disconnect() else viewModel.connect()
|
if (isConnected || isConnecting) viewModel.disconnect() else viewModel.connect()
|
||||||
}
|
}
|
||||||
@@ -236,7 +253,7 @@ fun MqttTopicViewerScreen(
|
|||||||
isEsp32 = isEsp32,
|
isEsp32 = isEsp32,
|
||||||
denmEvents = denmEvents,
|
denmEvents = denmEvents,
|
||||||
spatIntersections = spatIntersections,
|
spatIntersections = spatIntersections,
|
||||||
usbSerialState = usbSerialState,
|
esp32LinkState = esp32LinkState,
|
||||||
camPingerActive = camPingerActive,
|
camPingerActive = camPingerActive,
|
||||||
camPingerSentCount = camPingerSentCount,
|
camPingerSentCount = camPingerSentCount,
|
||||||
camPingerHasFix = camPingerHasFix,
|
camPingerHasFix = camPingerHasFix,
|
||||||
@@ -277,7 +294,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(),
|
||||||
usbSerialState: UsbSerialState = UsbSerialState.DISCONNECTED,
|
esp32LinkState: Esp32LinkState = Esp32LinkState.DISCONNECTED,
|
||||||
camPingerActive: Boolean = false,
|
camPingerActive: Boolean = false,
|
||||||
camPingerSentCount: Int = 0,
|
camPingerSentCount: Int = 0,
|
||||||
camPingerHasFix: Boolean = false,
|
camPingerHasFix: Boolean = false,
|
||||||
@@ -321,7 +338,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 = usbSerialState == UsbSerialState.CONNECTED,
|
usbConnected = esp32LinkState == Esp32LinkState.CONNECTED,
|
||||||
pingerActive = camPingerActive,
|
pingerActive = camPingerActive,
|
||||||
sentCount = camPingerSentCount,
|
sentCount = camPingerSentCount,
|
||||||
hasFix = camPingerHasFix,
|
hasFix = camPingerHasFix,
|
||||||
@@ -335,49 +352,38 @@ private fun TopicListPane(
|
|||||||
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f))
|
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── List / Topics / Map toggle ────────────────────────────────────────────────────
|
// ── List / Topics 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). It is hidden on the ESP32-C5 path, where there is
|
// to", not instead of, the topic list).
|
||||||
// no broker and `topics` is permanently empty.
|
//
|
||||||
Row(
|
// The whole row is hidden on the ESP32-C5 path: there is no broker there, `topics` is
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp),
|
// permanently empty, and a toggle offering a single choice is just noise.
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
if (!isEsp32) {
|
||||||
) {
|
Row(
|
||||||
ViewModeButton(
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
label = stringResource(R.string.mqtt_view_list),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
selected = viewMode == TopicViewMode.LIST,
|
) {
|
||||||
) { viewMode = TopicViewMode.LIST }
|
ViewModeButton(
|
||||||
|
label = stringResource(R.string.mqtt_view_list),
|
||||||
|
selected = 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 / live map ───────────────────────────
|
// ── Decoded traffic / raw topics ──────────────────────────────────────
|
||||||
// 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.MAP) {
|
if (shownMode == TopicViewMode.LIST) {
|
||||||
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,
|
||||||
@@ -1213,11 +1219,12 @@ 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.txFailures, s.oversizeDrops, s.rxCrcErrors, s.rxQueueDrops,
|
||||||
),
|
),
|
||||||
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 ||
|
||||||
ErrorRed else MaterialTheme.colorScheme.onSurfaceVariant,
|
s.rxQueueDrops > 0
|
||||||
|
) ErrorRed else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1466,10 +1473,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 UsbSerialState.asConnectionState(): MqttConnectionState = when (this) {
|
private fun Esp32LinkState.asConnectionState(): MqttConnectionState = when (this) {
|
||||||
UsbSerialState.CONNECTED -> MqttConnectionState.CONNECTED
|
Esp32LinkState.CONNECTED -> MqttConnectionState.CONNECTED
|
||||||
UsbSerialState.DEVICE_ATTACHED,
|
Esp32LinkState.DEVICE_ATTACHED,
|
||||||
UsbSerialState.PERMISSION_REQUESTED -> MqttConnectionState.CONNECTING
|
Esp32LinkState.PERMISSION_REQUESTED -> MqttConnectionState.CONNECTING
|
||||||
UsbSerialState.ERROR -> MqttConnectionState.ERROR
|
Esp32LinkState.ERROR -> MqttConnectionState.ERROR
|
||||||
UsbSerialState.DISCONNECTED -> MqttConnectionState.DISCONNECTED
|
Esp32LinkState.DISCONNECTED -> MqttConnectionState.DISCONNECTED
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ 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
|
||||||
@@ -165,13 +167,19 @@ 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). Everything
|
// OBU Hardware selector — CiT One / ESP32-C5 (Phase 03, Section 13). The transport
|
||||||
// below (transport, USB-C options) only really applies to CiT One; ESP32-C5 uses
|
// cards below apply to the CiT One; the ESP32-C5 has its own card (USB-C or BLE,
|
||||||
// USB Serial exclusively and has no transport choice to make here.
|
// CAM or VAM, signing).
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.settings_obu_hardware),
|
stringResource(R.string.settings_obu_hardware),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
@@ -212,6 +220,48 @@ 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
|
||||||
@@ -611,6 +661,40 @@ 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,12 +1,13 @@
|
|||||||
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
|
||||||
@@ -30,25 +31,37 @@ 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 view (Phase 03, Section 13) — plots the ego bike's own position plus
|
* V2X Monitor live map (Phase 03, Section 13) — the map body behind [V2xMapScreen], plotting the
|
||||||
* every currently-tracked remote road user's last-known CAM position, in addition to (not
|
* ego bike's own position, every currently-tracked remote road user's last-known CAM position,
|
||||||
* replacing) the raw topic list already on this screen. Reuses the same osmdroid pattern as
|
* every live hazard (DENM) and every signalised intersection heard over SPATEM.
|
||||||
* [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).
|
|
||||||
*
|
*
|
||||||
* Remote markers are colored by that station's most severe active alert level, if any, so a
|
* Marker vocabulary, one shape per message type so the map reads without a legend:
|
||||||
* glance at the map shows not just "who's nearby" but "who's a warning right now" — the same
|
* - CAM — teardrop pin, tinted by that station's most severe active alert level
|
||||||
* severity coloring already used by [UseCaseAlertPanel].
|
* - DENM — hazard warning triangle
|
||||||
|
* - 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(
|
||||||
@@ -56,6 +69,9 @@ 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
|
||||||
@@ -71,8 +87,17 @@ 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 ->
|
||||||
@@ -89,106 +114,236 @@ fun V2xLiveMapView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxSize()) {
|
AndroidView(
|
||||||
Text(
|
factory = { ctx ->
|
||||||
text = stringResource(R.string.v2x_map_remote_count, remotes.size),
|
initOsmForV2xMap(ctx)
|
||||||
style = MaterialTheme.typography.labelMedium,
|
MapView(ctx).apply {
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
setTileSource(TileSourceFactory.MAPNIK)
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
setMultiTouchControls(true)
|
||||||
)
|
// Raster tiles are authored for ~160 dpi; without this they are upscaled by the
|
||||||
Spacer(Modifier.height(4.dp))
|
// display density and labels come out soft on a modern phone.
|
||||||
|
isTilesScaledToDpi = true
|
||||||
AndroidView(
|
// The floating +/- buttons sit exactly where the rider's thumb lands and
|
||||||
factory = { ctx ->
|
// duplicate pinch-zoom. Pinch and double-tap still work.
|
||||||
initOsmForV2xMap(ctx)
|
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
|
||||||
MapView(ctx).apply {
|
setMinZoomLevel(4.0)
|
||||||
setTileSource(TileSourceFactory.MAPNIK)
|
setMaxZoomLevel(20.0)
|
||||||
setMultiTouchControls(true)
|
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
|
||||||
mapViewRef.value = this
|
// 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
|
||||||
update = { mv ->
|
}
|
||||||
mv.overlays.clear()
|
},
|
||||||
|
update = { mv ->
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
// Own position: a centred "you are here" dot, not a pin. Own position is a fact
|
// Intersections we can actually place: SPATEM carries signal state but no geometry
|
||||||
// about the viewer rather than one of the tracked objects, and when both used
|
// (that is MAPEM's job), so the only position available is the sending RSU's own CAM.
|
||||||
// osmdroid's identical default pin the two were indistinguishable at a glance.
|
val locatedSpats = spats.mapNotNull { spat ->
|
||||||
mv.overlays.add(
|
remotes[spat.stationId]?.let { rsu -> spat to rsu }
|
||||||
Marker(mv).apply {
|
}
|
||||||
position = ownGeoPoint
|
// An RSU drawn as a traffic light must not also be drawn as a CAM pin underneath it:
|
||||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
// two markers on one point, the lower one unreachable.
|
||||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_map_own)
|
val spatStationIds = locatedSpats.map { (spat, _) -> spat.stationId }.toSet()
|
||||||
title = context.getString(R.string.v2x_map_own_label)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
remotes.forEach { (stationId, cam) ->
|
val live = mutableSetOf<String>()
|
||||||
val level = alertByStation[stationId]
|
|
||||||
val label = when (level) {
|
// Own position: a centred "you are here" dot, not a pin. Own position is a fact about
|
||||||
AlertLevel.WARNING -> context.getString(R.string.v2x_map_remote_warning, stationId)
|
// the viewer rather than one of the tracked objects, and when both used osmdroid's
|
||||||
AlertLevel.AWARENESS -> context.getString(R.string.v2x_map_remote_awareness, stationId)
|
// identical default pin the two were indistinguishable at a glance.
|
||||||
AlertLevel.INFO -> context.getString(R.string.v2x_map_remote_info, stationId)
|
markers.marker(mv, KEY_OWN, live).apply {
|
||||||
null -> context.getString(R.string.v2x_map_remote_plain, stationId)
|
position = ownGeoPoint
|
||||||
}
|
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||||
// Teardrop pin anchored at its tip, tinted by severity. Now that these are
|
icon = icons.own
|
||||||
// custom drawables, per-instance tinting is possible - severity no longer
|
title = context.getString(R.string.v2x_map_own_label)
|
||||||
// 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.
|
remotes.forEach { (stationId, cam) ->
|
||||||
val pin = ContextCompat.getDrawable(context, R.drawable.ic_map_remote_station)
|
if (stationId in spatStationIds) return@forEach
|
||||||
?.mutate()
|
val level = alertByStation[stationId]
|
||||||
?.apply { setTint(level.toMarkerColor()) }
|
val label = when (level) {
|
||||||
mv.overlays.add(
|
AlertLevel.WARNING -> context.getString(R.string.v2x_map_remote_warning, stationId)
|
||||||
Marker(mv).apply {
|
AlertLevel.AWARENESS -> context.getString(R.string.v2x_map_remote_awareness, stationId)
|
||||||
position = GeoPoint(cam.latitude, cam.longitude)
|
AlertLevel.INFO -> context.getString(R.string.v2x_map_remote_info, stationId)
|
||||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
null -> context.getString(R.string.v2x_map_remote_plain, stationId)
|
||||||
icon = pin
|
}
|
||||||
title = label
|
// Teardrop pin anchored at its tip, tinted by severity, so severity no longer
|
||||||
}
|
// depends on tapping the marker to read its label.
|
||||||
|
markers.marker(mv, "$KEY_CAM$stationId", live).apply {
|
||||||
|
position = GeoPoint(cam.latitude, cam.longitude)
|
||||||
|
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||||
|
icon = icons.remotePin(level)
|
||||||
|
title = label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hazards and signals are added after the vehicle pins, so they draw on top: a hazard
|
||||||
|
// hidden behind a CAM pin defeats the point of showing it.
|
||||||
|
denms.forEach { denm ->
|
||||||
|
markers.marker(mv, "$KEY_DENM${denm.dedupKey}", live).apply {
|
||||||
|
position = GeoPoint(denm.latitude, denm.longitude)
|
||||||
|
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||||
|
icon = icons.denm
|
||||||
|
title = denm.causeCode?.let {
|
||||||
|
context.getString(
|
||||||
|
R.string.v2x_map_denm_labeled,
|
||||||
|
it,
|
||||||
|
denm.subCauseCode ?: 0,
|
||||||
|
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) } ?: "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DENM hazard pins, added last so they draw on top of vehicle markers - a hazard
|
// Drop markers for stations, hazards and intersections that have expired, then rebuild
|
||||||
// hidden behind a CAM pin defeats the point of showing it.
|
// the overlay list in draw order from the cached Markers. Reordering moves references;
|
||||||
denms.forEach { denm ->
|
// nothing here allocates a Marker or decodes a Drawable.
|
||||||
mv.overlays.add(
|
markers.keys.retainAll { key ->
|
||||||
Marker(mv).apply {
|
(key in live).also { kept -> if (!kept) markers[key]?.closeInfoWindow() }
|
||||||
position = GeoPoint(denm.latitude, denm.longitude)
|
}
|
||||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
mv.overlays.clear()
|
||||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_denm_warning)
|
markers.entries
|
||||||
title = denm.causeCode?.let {
|
.sortedBy { (key, _) -> key.drawOrder() }
|
||||||
context.getString(
|
.forEach { (_, marker) -> mv.overlays.add(marker) }
|
||||||
R.string.v2x_map_denm_labeled,
|
|
||||||
it,
|
|
||||||
denm.subCauseCode ?: 0,
|
|
||||||
denm.stationId,
|
|
||||||
)
|
|
||||||
} ?: context.getString(R.string.v2x_map_denm_plain, denm.stationId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
mv.controller.animateTo(ownGeoPoint)
|
// setCenter, not animateTo: an animation restarted on every CAM never finishes, which
|
||||||
mv.invalidate()
|
// 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.
|
||||||
// osmdroid's onDetach() permanently tears the MapView down: afterwards its
|
if (followOwn) {
|
||||||
// MapViewRepository holds a null MapView, so constructing a Marker against it throws
|
if (wasFollowing.value) mv.controller.setCenter(ownGeoPoint)
|
||||||
// NullPointerException from deep inside InfoWindow's constructor.
|
else mv.controller.animateTo(ownGeoPoint)
|
||||||
//
|
}
|
||||||
// This used to run in the DisposableEffect's onDispose, which is NOT safe: that effect
|
wasFollowing.value = followOwn
|
||||||
// is keyed on the lifecycle owner and disposes independently of this AndroidView, so
|
|
||||||
// the update block above could still run against an already-detached MapView and
|
mv.invalidate()
|
||||||
// rebuild its markers. It crashed the app on 2026-08-17 once DENMs started arriving,
|
},
|
||||||
// because every incoming message recomposes this view and there are far more updates
|
// osmdroid's onDetach() permanently tears the MapView down: afterwards its
|
||||||
// to land in that window than there used to be.
|
// MapViewRepository holds a null MapView, so constructing a Marker against it throws
|
||||||
//
|
// NullPointerException from deep inside InfoWindow's constructor.
|
||||||
// onRelease is the callback that actually means "this View is gone": Compose
|
//
|
||||||
// guarantees no further update after it.
|
// This used to run in the DisposableEffect's onDispose, which is NOT safe: that effect
|
||||||
onRelease = { it.onDetach() },
|
// is keyed on the lifecycle owner and disposes independently of this AndroidView, so
|
||||||
modifier = Modifier.fillMaxSize(),
|
// the update block above could still run against an already-detached MapView and
|
||||||
)
|
// rebuild its markers. It crashed the app on 2026-08-17 once DENMs started arriving,
|
||||||
|
// because every incoming message recomposes this view and there are far more updates
|
||||||
|
// to land in that window than there used to be.
|
||||||
|
//
|
||||||
|
// onRelease is the callback that actually means "this View is gone": Compose
|
||||||
|
// guarantees no further update after it.
|
||||||
|
onRelease = {
|
||||||
|
markers.clear()
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,5 +383,10 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
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,8 +13,11 @@ 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.UsbSerialState
|
import com.hawhamburg.micr0bu.data.transport.Esp32LinkState
|
||||||
import com.hawhamburg.micr0bu.data.transport.UsbSerialTransport
|
import com.hawhamburg.micr0bu.data.transport.Esp32Link
|
||||||
|
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
|
||||||
@@ -45,7 +48,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 usbSerialTransport: UsbSerialTransport,
|
private val esp32Link: Esp32Link,
|
||||||
private val camPinger: CamPinger,
|
private val camPinger: CamPinger,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -80,14 +83,42 @@ 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 USB-serial link state (Phase 03) — see [UsbSerialTransport]. */
|
/** ESP32-C5 link state, over USB or BLE per [esp32Transport] — see [Esp32Link]. */
|
||||||
val usbSerialState: StateFlow<UsbSerialState> = usbSerialTransport.state
|
val esp32LinkState: StateFlow<Esp32LinkState> = esp32Link.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?> = usbSerialTransport.linkStatus
|
val espLinkStatus: StateFlow<EspLinkStatus?> = esp32Link.linkStatus
|
||||||
|
|
||||||
/** Non-zero means CAMs are being built and dropped — see [UsbSerialTransport.sendCamTx]. */
|
/** Non-zero means CAMs are being built and dropped — see [Esp32Link.send]. */
|
||||||
val camSendFailures: StateFlow<Int> = usbSerialTransport.consecutiveWriteFailures
|
val camSendFailures: StateFlow<Int> = esp32Link.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-
|
||||||
@@ -352,10 +383,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 USB-serial link — separate from [connect]/[disconnect],
|
/** Connect/disconnect the ESP32-C5 link (USB or BLE per [esp32Transport]) — separate from
|
||||||
* which drive the CiT One's MQTT-over-USB-C/Wi-Fi path. See [ConnectionSetupScreen]. */
|
* [connect]/[disconnect], which drive the CiT One's MQTT-over-USB-C/Wi-Fi path. */
|
||||||
fun connectUsbSerial() = usbSerialTransport.connect()
|
fun connectEsp32() = esp32Link.connect()
|
||||||
fun disconnectUsbSerial() = usbSerialTransport.disconnect()
|
fun disconnectEsp32() = esp32Link.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 }
|
||||||
@@ -389,7 +420,7 @@ class MqttViewModel @Inject constructor(
|
|||||||
super.onCleared()
|
super.onCleared()
|
||||||
repo.disconnect()
|
repo.disconnect()
|
||||||
camPinger.stop()
|
camPinger.stop()
|
||||||
// Deliberately NOT usbSerialTransport.disconnect(): the transport is an app-scoped
|
// Deliberately NOT esp32Link.disconnect(): the link 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
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
@@ -142,12 +142,18 @@
|
|||||||
<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>
|
||||||
@@ -183,7 +189,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">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_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_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>
|
||||||
@@ -197,7 +203,6 @@
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -313,4 +318,18 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -143,12 +143,18 @@
|
|||||||
<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>
|
||||||
@@ -184,7 +190,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">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_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_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>
|
||||||
@@ -198,7 +204,6 @@
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -259,7 +264,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</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_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>
|
||||||
@@ -320,4 +325,18 @@
|
|||||||
|
|
||||||
<!-- 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,6 +166,21 @@ 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(
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# 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`, from the colleague's own repository
|
||||||
|
(not part of this one; nothing is pushed there). The C-ITS library it needs is copied into this
|
||||||
|
repository as `obu-firmware/external/vanetza-idf` (their commit cf4b99f, unchanged), so
|
||||||
|
obu-firmware builds from a plain clone. 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 (colleague's repository, `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.
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 660 KiB |
@@ -1,7 +1,7 @@
|
|||||||
# OBU transmit firmware - Phase 2 (in progress: HLN-SV DENM beacon)
|
# OBU transmit firmware - Phase 2 (in progress: HLN-SV DENM beacon)
|
||||||
|
|
||||||
Started. See `docs/04-transmit-setup.md` in the project root for build/flash
|
Build and flash steps are under "Build and flash" below. (`docs/04-transmit-setup.md`,
|
||||||
steps and how to validate this against your own sniffer.
|
referenced here and in the sources, is not in the repo.)
|
||||||
|
|
||||||
## Toolchain: use a dedicated terminal (ESP-IDF 5.5.4)
|
## Toolchain: use a dedicated terminal (ESP-IDF 5.5.4)
|
||||||
|
|
||||||
@@ -29,6 +29,123 @@ referenced an older source path under `micrOBU_workspace/v2x-obu-esp32c5/`,
|
|||||||
which makes `idf.py fullclean` refuse to run). If that error reappears, delete
|
which makes `idf.py fullclean` refuse to run). If that error reappears, delete
|
||||||
`build/` manually rather than fighting it.
|
`build/` manually rather than fighting it.
|
||||||
|
|
||||||
|
## Build and flash
|
||||||
|
|
||||||
|
The firmware needs no button, phone or serial connection to start. On every power-up or reset,
|
||||||
|
`app_main` sets up the radio and starts `tx_task`, which starts driving the simulated route and
|
||||||
|
sending CAMs on 5900 MHz. A board flashed with this image starts beaconing on its own as soon as it
|
||||||
|
gets power.
|
||||||
|
|
||||||
|
In a fresh PowerShell window:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:IDF_PYTHON_ENV_PATH = $null; $env:IDF_PATH = $null
|
||||||
|
. C:\Espressif\frameworks\esp-idf-v5.5.4\export.ps1
|
||||||
|
cd C:\Users\Ashin\AndroidStudioProjects\MicrOBU\obu-cam-transmistter
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Check what the board is running before you flash it.** Both this project and `obu-firmware`
|
||||||
|
name their image `obu_firmware.bin`, so the file name tells you nothing. Read the app
|
||||||
|
descriptor instead (replace `COMx` with the board's port):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m esptool --chip esp32c5 -p COMx read_flash 0x10000 0x100 $env:TEMP\desc.bin
|
||||||
|
$b = [IO.File]::ReadAllBytes("$env:TEMP\desc.bin")
|
||||||
|
function S($o,$n){ [Text.Encoding]::ASCII.GetString($b,$o,$n).Trim([char]0) }
|
||||||
|
"time=" + (S 0x70 16) + " date=" + (S 0x80 16) + " idf=" + (S 0x90 32)
|
||||||
|
```
|
||||||
|
|
||||||
|
`idf=v6.1...` means the board runs the production OBU (`obu-firmware`). Don't flash this beacon
|
||||||
|
over it. `idf=v5.5.4` means this transmitter, or another bench image.
|
||||||
|
|
||||||
|
2. **Build:**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
idf.py build
|
||||||
|
```
|
||||||
|
|
||||||
|
A rebuild after small changes takes about 1-2 minutes. The output is
|
||||||
|
`build\obu_firmware.bin`.
|
||||||
|
|
||||||
|
3. **Flash** (the board must be on its UART bridge port or its native USB port):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
idf.py -p COMx flash
|
||||||
|
```
|
||||||
|
|
||||||
|
The flash is good when esptool prints `Hash of data verified.`, then resets the board.
|
||||||
|
|
||||||
|
4. **Check that it's transmitting:**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
idf.py -p COMx monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
(Exit with `Ctrl+]`.) About 1.4 s after reset you should see:
|
||||||
|
|
||||||
|
```
|
||||||
|
W obu-tx: OCB @ 5900 MHz - CAM beacon armed, driving a 103-point street loop
|
||||||
|
I obu-tx: CAM sent (119 bytes) @ 5900 MHz genDeltaT=1087 pos=53.5531770,10.0220980 50.0 km/h heading 77.0 pt1
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, a `CAM sent` line appears about 3 times a second, with the position, speed and
|
||||||
|
heading changing. These lines only mean each frame was
|
||||||
|
handed to the radio. To confirm the frames actually went out, capture them with a second
|
||||||
|
ESP32-C5 running the receiver firmware.
|
||||||
|
|
||||||
|
Don't open the console of the production OBU (COM3) while the phone is attached. Opening the port
|
||||||
|
resets that board and drops the phone's USB link. The beacon boards have no phone attached, so
|
||||||
|
this doesn't apply to them.
|
||||||
|
|
||||||
|
## Simulated drive
|
||||||
|
|
||||||
|
The beacon pretends to be a car driving a loop through St. Georg / Berliner Tor in Hamburg, on
|
||||||
|
the real streets. The route comes from six waypoints, which you set in `tools/make_route.py`.
|
||||||
|
|
||||||
|
**Changing the route:** edit `WAYPOINTS` in `tools/make_route.py`, then run
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -3.11 tools/make_route.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The script asks the OSRM demo server (router.project-osrm.org) for a legal driving route through the
|
||||||
|
waypoints in order and back to the first, then writes three files:
|
||||||
|
|
||||||
|
- `main/route_points.h`: the street geometry, thinned to points no more than 1.5 m off the line
|
||||||
|
(currently 103 points).
|
||||||
|
- `tools/route_osrm.json`: OSRM's raw answer. `--offline` rebuilds the header from it without the
|
||||||
|
network.
|
||||||
|
- `tools/route_map.html`: the route on an OpenStreetMap map. Open it in a browser and check it
|
||||||
|
before building.
|
||||||
|
|
||||||
|
Then build and flash as above. Route data (c) OpenStreetMap contributors, ODbL; routing by OSRM.
|
||||||
|
|
||||||
|
**Things to know about the routing:**
|
||||||
|
- OSRM follows one-way streets and turn bans, so the loop can be longer than the waypoints suggest.
|
||||||
|
The current one is 5.2 km, with two turn-round detours: a loop via Borgfelder Straße and
|
||||||
|
Anckelmannsplatz between wp2 and wp3, and one round Nagelsweg, Norderstraße and Repsoldstraße
|
||||||
|
between wp5 and wp6. To avoid a detour, move the waypoint on either side of it.
|
||||||
|
- Each waypoint is sent with the direction towards the next one. Without it, points on divided
|
||||||
|
roads (Beim Strohhause, for example) snap to the carriageway going the other way, and the loop
|
||||||
|
grows to 8.4 km of U-turns.
|
||||||
|
|
||||||
|
**How the car drives** (`main/route.c`):
|
||||||
|
- **Speed:** it cruises at 50 km/h (`CRUISE_MPS` in `main/main.c`). Each bend gets a speed limit
|
||||||
|
from its radius, keeping sideways acceleration at 2 m/s², so a 90° junction turn is taken at
|
||||||
|
about 15 km/h and a gentle curve barely slows the car. It never drops below 10 km/h
|
||||||
|
(`MIN_CORNER_MPS`). It brakes at 2 m/s² and accelerates at 1.5 m/s², planning braking across as
|
||||||
|
many points as a bend needs. A lap takes about 7.7 min, averaging 40 km/h.
|
||||||
|
- **Heading:** the compass bearing of the current straight piece. It changes gradually through
|
||||||
|
curves but jumps at sharp junction turns.
|
||||||
|
- **When CAMs are sent:** following ETSI EN 302 637-2, the state is checked every 100 ms. A CAM goes
|
||||||
|
out when the heading changed by more than 4°, the position by more than 4 m, or the speed by more
|
||||||
|
than 0.5 m/s since the last one, and at least once a second. That's about 3 CAMs a second at
|
||||||
|
50 km/h.
|
||||||
|
- **What's filled in:** position, speed and heading go into both the CAM and the GeoNetworking
|
||||||
|
source position vector. `genDeltaT` is milliseconds since boot.
|
||||||
|
- **Testing:** `route.c` only uses standard headers, so you can compile it on the PC with MSYS2 gcc
|
||||||
|
and simulate a lap.
|
||||||
|
|
||||||
## CAM encoding
|
## CAM encoding
|
||||||
|
|
||||||
`main/cam.c` IS compiled here (unlike `obu-firmware`'s copy, which is a
|
`main/cam.c` IS compiled here (unlike `obu-firmware`'s copy, which is a
|
||||||
@@ -44,10 +161,12 @@ hazard-light GPIO is grounded. No location/alacarte containers.
|
|||||||
- `main/main.c` - entry point, the `phy_11p_set`/`phy_change_channel(5900,...)`
|
- `main/main.c` - entry point, the `phy_11p_set`/`phy_change_channel(5900,...)`
|
||||||
register hack, GPIO polling, TX loop
|
register hack, GPIO polling, TX loop
|
||||||
- `main/denm.c` / `.h` - ASN.1 UPER encoding of a minimal DENM
|
- `main/denm.c` / `.h` - ASN.1 UPER encoding of a minimal DENM
|
||||||
|
- `main/route.c` / `.h` - simulated drive round the route loop
|
||||||
|
- `main/route_points.h` - the route, generated by `tools/make_route.py`
|
||||||
- `main/geonet.c` / `.h` - GeoNetworking Basic/Common/SHB headers + BTP-B
|
- `main/geonet.c` / `.h` - GeoNetworking Basic/Common/SHB headers + BTP-B
|
||||||
- `main/dot11p.c` / `.h` - 802.11 OCB (QoS Data, broadcast) frame + LLC/SNAP
|
- `main/dot11p.c` / `.h` - 802.11 OCB (QoS Data, broadcast) frame + LLC/SNAP
|
||||||
|
|
||||||
Known gaps, tracked as TODOs in the source: no real GNSS (lat/long hardcoded
|
Known gaps, tracked as TODOs in the source: no real GNSS (the CAM position comes
|
||||||
0), no real time source (detectionTime/referenceTime hardcoded 0, decodes as
|
from the simulated drive above), no real time source (detectionTime/referenceTime hardcoded 0, decodes as
|
||||||
2004-01-01), fixed (non-rotating) pseudonym MAC, SHB instead of GeoBroadcast
|
2004-01-01), fixed (non-rotating) pseudonym MAC, SHB instead of GeoBroadcast
|
||||||
(no multi-hop forwarding), unsecured (no IEEE 1609.2 signing).
|
(no multi-hop forwarding), unsecured (no IEEE 1609.2 signing).
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# wifi_patches.c is intentionally NOT in this list anymore - superseded by
|
# wifi_patches.c is intentionally NOT in this list anymore - superseded by
|
||||||
# tx_custom.c (see that file for why). Left on disk, unused, for history.
|
# tx_custom.c (see that file for why). Left on disk, unused, for history.
|
||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "main.c" "denm.c" "cam.c" "geonet.c" "dot11p.c" "tx_custom.c"
|
SRCS "main.c" "denm.c" "cam.c" "geonet.c" "dot11p.c" "tx_custom.c" "route.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
REQUIRES esp_event esp_netif nvs_flash driver esp_phy
|
REQUIRES esp_event esp_timer esp_netif nvs_flash driver esp_phy
|
||||||
PRIV_REQUIRES esp_wifi
|
PRIV_REQUIRES esp_wifi
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
int geonet_wrap_shb(const uint8_t *its_payload, int its_len,
|
int geonet_wrap_shb(const uint8_t *its_payload, int its_len,
|
||||||
const uint8_t mac[6], uint8_t station_type,
|
const uint8_t mac[6], uint8_t station_type,
|
||||||
int32_t latitude_tenmicrodeg, int32_t longitude_tenmicrodeg,
|
int32_t latitude_tenmicrodeg, int32_t longitude_tenmicrodeg,
|
||||||
|
uint16_t speed_cm_s, uint16_t heading_ddeg,
|
||||||
uint16_t btp_dest_port,
|
uint16_t btp_dest_port,
|
||||||
uint8_t *out, size_t out_len)
|
uint8_t *out, size_t out_len)
|
||||||
{
|
{
|
||||||
@@ -68,12 +69,12 @@ int geonet_wrap_shb(const uint8_t *its_payload, int its_len,
|
|||||||
uint32_t lon_u = (uint32_t)longitude_tenmicrodeg;
|
uint32_t lon_u = (uint32_t)longitude_tenmicrodeg;
|
||||||
*p++ = (uint8_t)(lon_u >> 24); *p++ = (uint8_t)(lon_u >> 16);
|
*p++ = (uint8_t)(lon_u >> 24); *p++ = (uint8_t)(lon_u >> 16);
|
||||||
*p++ = (uint8_t)(lon_u >> 8); *p++ = (uint8_t)(lon_u);
|
*p++ = (uint8_t)(lon_u >> 8); *p++ = (uint8_t)(lon_u);
|
||||||
// PAI(1 bit) + Speed(15 bits), packed into 2 bytes: 0 = PAI false,
|
// PAI(1 bit) + Speed(15 bits, signed, 0.01 m/s), packed into 2 bytes. PAI stays 0: the
|
||||||
// speed 0 - which is actually correct semantics for a STATIONARY
|
// position has no accuracy estimate behind it.
|
||||||
// vehicle beacon, not just a placeholder.
|
uint16_t spd = speed_cm_s > 0x7FFF ? 0x7FFF : speed_cm_s;
|
||||||
*p++ = 0x00; *p++ = 0x00;
|
*p++ = (uint8_t)(spd >> 8); *p++ = (uint8_t)(spd & 0xFF);
|
||||||
// Heading (16 bits, 0.1 degree units): 0 = due north / unavailable
|
// Heading (16 bits, 0.1 degree units, clockwise from north)
|
||||||
*p++ = 0x00; *p++ = 0x00;
|
*p++ = (uint8_t)(heading_ddeg >> 8); *p++ = (uint8_t)(heading_ddeg & 0xFF);
|
||||||
// Reserved (4 bytes) - clause 9.8.4: the SHB extended header is the 24-byte Source Position
|
// Reserved (4 bytes) - clause 9.8.4: the SHB extended header is the 24-byte Source Position
|
||||||
// Vector FOLLOWED BY a 4-byte reserved field (media-dependent data), 28 bytes in total. These
|
// Vector FOLLOWED BY a 4-byte reserved field (media-dependent data), 28 bytes in total. These
|
||||||
// four bytes were missing, which is why a standards-compliant receiver read our CAM payload's
|
// four bytes were missing, which is why a standards-compliant receiver read our CAM payload's
|
||||||
|
|||||||
@@ -31,6 +31,10 @@
|
|||||||
// working (same extended header shape as CAM). Fine for a single-vehicle
|
// working (same extended header shape as CAM). Fine for a single-vehicle
|
||||||
// beacon; revisit if you need real multi-hop forwarding later.
|
// beacon; revisit if you need real multi-hop forwarding later.
|
||||||
//
|
//
|
||||||
|
// `speed_cm_s` (0.01 m/s) and `heading_ddeg` (0.1 deg) also go into the Source Long Position
|
||||||
|
// Vector; pass the same values as the CAM's high-frequency container. Speed is a 15-bit field, so
|
||||||
|
// values above 32767 are clamped.
|
||||||
|
//
|
||||||
// `btp_dest_port` is the BTP-B destination port for the service being carried
|
// `btp_dest_port` is the BTP-B destination port for the service being carried
|
||||||
// (ETSI TS 103 248): 2001 = CAM, 2002 = DENM, 2003 = MAPEM, 2004 = SPATEM, ...
|
// (ETSI TS 103 248): 2001 = CAM, 2002 = DENM, 2003 = MAPEM, 2004 = SPATEM, ...
|
||||||
//
|
//
|
||||||
@@ -38,6 +42,7 @@
|
|||||||
int geonet_wrap_shb(const uint8_t *its_payload, int its_len,
|
int geonet_wrap_shb(const uint8_t *its_payload, int its_len,
|
||||||
const uint8_t mac[6], uint8_t station_type,
|
const uint8_t mac[6], uint8_t station_type,
|
||||||
int32_t latitude_tenmicrodeg, int32_t longitude_tenmicrodeg,
|
int32_t latitude_tenmicrodeg, int32_t longitude_tenmicrodeg,
|
||||||
|
uint16_t speed_cm_s, uint16_t heading_ddeg,
|
||||||
uint16_t btp_dest_port,
|
uint16_t btp_dest_port,
|
||||||
uint8_t *out, size_t out_len);
|
uint8_t *out, size_t out_len);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <math.h>
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
#include "esp_event.h"
|
#include "esp_event.h"
|
||||||
@@ -14,13 +18,17 @@
|
|||||||
#include "geonet.h"
|
#include "geonet.h"
|
||||||
#include "dot11p.h"
|
#include "dot11p.h"
|
||||||
#include "tx_custom.h"
|
#include "tx_custom.h"
|
||||||
|
#include "route.h"
|
||||||
|
#include "route_points.h"
|
||||||
|
|
||||||
static const char *TAG = "obu-tx";
|
static const char *TAG = "obu-tx";
|
||||||
|
|
||||||
// CAM beacon: transmit a Cooperative Awareness Message every TX_INTERVAL_MS,
|
// CAM beacon for a simulated car driving round a block in Hamburg (see route.c). The CAM
|
||||||
// unconditionally (no hazard-light gating - CAM is a continuous beacon, unlike
|
// generation rules follow ETSI EN 302 637-2 clause 6.1.3: every CHECK_INTERVAL_MS the car's state
|
||||||
// the event-triggered DENM). Matches the working Rust reference
|
// is compared with the last CAM sent, and a new CAM goes out when the heading changed by more than
|
||||||
// (esp32-c_its-companion, feat/tx-cam), which beacons CAM on 5900 MHz.
|
// 4 degrees, the position by more than 4 m, the speed by more than 0.5 m/s, or 1 s has passed.
|
||||||
|
// There is no hazard-light gating - CAM is a continuous beacon, unlike the event-triggered DENM.
|
||||||
|
// Transmits on 5900 MHz like the working Rust reference (esp32-c_its-companion, feat/tx-cam).
|
||||||
|
|
||||||
// ISOLATION TEST for whether tx_custom.c is the blocker.
|
// ISOLATION TEST for whether tx_custom.c is the blocker.
|
||||||
// 1 = transmit via the STANDARD, well-tested esp_wifi_80211_tx() using a
|
// 1 = transmit via the STANDARD, well-tested esp_wifi_80211_tx() using a
|
||||||
@@ -55,15 +63,19 @@ static const char *TAG = "obu-tx";
|
|||||||
#define VEHICLE_LENGTH_DM 40 // VehicleLengthValue, 10cm steps (4.0 m)
|
#define VEHICLE_LENGTH_DM 40 // VehicleLengthValue, 10cm steps (4.0 m)
|
||||||
#define VEHICLE_WIDTH_DM 18 // VehicleWidth, 10cm steps (1.8 m)
|
#define VEHICLE_WIDTH_DM 18 // VehicleWidth, 10cm steps (1.8 m)
|
||||||
#define BTP_PORT_CAM 2001 // BTP-B destination port for CAM (ETSI TS 103 248)
|
#define BTP_PORT_CAM 2001 // BTP-B destination port for CAM (ETSI TS 103 248)
|
||||||
#define TX_INTERVAL_MS 1000 // CAM beacon period (1 Hz; ITS allows 1-10 Hz)
|
#define CHECK_INTERVAL_MS 100 // T_CheckCamGen: how often the generation rules are evaluated
|
||||||
|
#define CAM_MAX_INTERVAL_MS 1000 // T_GenCamMax: a CAM goes out at least this often
|
||||||
|
#define CAM_HEADING_DDEG 40 // > 4 degrees heading change triggers a CAM
|
||||||
|
#define CAM_POSITION_M 4.0 // > 4 m position change triggers a CAM
|
||||||
|
#define CAM_SPEED_CM_S 50 // > 0.5 m/s speed change triggers a CAM
|
||||||
|
|
||||||
// Bench location, hardcoded since there's no GNSS module wired in yet and
|
// ---- Simulated drive ----
|
||||||
// the unit is genuinely stationary here: 53°33'16.8"N 10°01'20.6"E, in
|
// route_points (main/route_points.h) is the street geometry of a driving loop through six waypoints
|
||||||
// 1/10-microdegree units (decimal_degrees * 10,000,000). Replace with real
|
// in St. Georg, generated by tools/make_route.py from OpenStreetMap via OSRM. To change the route,
|
||||||
// GNSS output once you have a fix source; until then this beats 0/0
|
// edit WAYPOINTS in that script and rerun it. No GNSS is wired in; replace with real fixes once
|
||||||
// ("Null Island"), which is an obvious placeholder-tell on any map.
|
// there is one.
|
||||||
#define BENCH_LATITUDE_TENMICRODEG 535546667
|
#define CRUISE_MPS (50.0 / 3.6) // 50 km/h, the urban limit
|
||||||
#define BENCH_LONGITUDE_TENMICRODEG 100223889
|
#define MIN_CORNER_MPS (10.0 / 3.6) // slowest the car goes, for hairpins and U-turns
|
||||||
|
|
||||||
// Single source of truth for the pseudonym/link-layer address: used both as
|
// Single source of truth for the pseudonym/link-layer address: used both as
|
||||||
// the 802.11 source MAC (Addr2) and as GN_ADDR's MID field, since the GN
|
// the 802.11 source MAC (Addr2) and as GN_ADDR's MID field, since the GN
|
||||||
@@ -78,33 +90,28 @@ static const uint8_t pseudonym_mac[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x01};
|
|||||||
extern void phy_11p_set(int enable, int unused);
|
extern void phy_11p_set(int enable, int unused);
|
||||||
extern void phy_change_channel(int freq_mhz, int bw_mode, int sec_chan_offset, int unused);
|
extern void phy_change_channel(int freq_mhz, int bw_mode, int sec_chan_offset, int unused);
|
||||||
|
|
||||||
static void send_cam(void)
|
static void send_cam(const route_state_t *car, uint16_t gen_delta)
|
||||||
{
|
{
|
||||||
// GenerationDeltaTime is TimestampIts mod 65536 (ms). No RTC/GNSS time here,
|
|
||||||
// so use a free-running ms counter that advances one beacon-interval per
|
|
||||||
// send. It wraps at 65536, which is exactly the field's defined behaviour.
|
|
||||||
static uint16_t gen_delta = 0;
|
|
||||||
|
|
||||||
uint8_t frame[300];
|
uint8_t frame[300];
|
||||||
cam_fields_t fields = {
|
cam_fields_t fields = {
|
||||||
.station_id = STATION_ID,
|
.station_id = STATION_ID,
|
||||||
.station_type = STATION_TYPE,
|
.station_type = STATION_TYPE,
|
||||||
.generation_delta_time = gen_delta,
|
.generation_delta_time = gen_delta,
|
||||||
.latitude_tenmicrodeg = BENCH_LATITUDE_TENMICRODEG,
|
.latitude_tenmicrodeg = car->latitude_tenmicrodeg,
|
||||||
.longitude_tenmicrodeg = BENCH_LONGITUDE_TENMICRODEG,
|
.longitude_tenmicrodeg = car->longitude_tenmicrodeg,
|
||||||
.speed_cm_s = 0, // stationary
|
.speed_cm_s = car->speed_cm_s,
|
||||||
.heading_ddeg = 3601, // HeadingValue unavailable (no heading source)
|
.heading_ddeg = car->heading_ddeg,
|
||||||
.vehicle_length_dm = VEHICLE_LENGTH_DM,
|
.vehicle_length_dm = VEHICLE_LENGTH_DM,
|
||||||
.vehicle_width_dm = VEHICLE_WIDTH_DM,
|
.vehicle_width_dm = VEHICLE_WIDTH_DM,
|
||||||
};
|
};
|
||||||
gen_delta += TX_INTERVAL_MS;
|
|
||||||
|
|
||||||
uint8_t cam_payload[96];
|
uint8_t cam_payload[96];
|
||||||
int cam_len = cam_encode(&fields, cam_payload, sizeof(cam_payload));
|
int cam_len = cam_encode(&fields, cam_payload, sizeof(cam_payload));
|
||||||
|
|
||||||
uint8_t gn_payload[160];
|
uint8_t gn_payload[160];
|
||||||
int gn_len = geonet_wrap_shb(cam_payload, cam_len, pseudonym_mac, STATION_TYPE,
|
int gn_len = geonet_wrap_shb(cam_payload, cam_len, pseudonym_mac, STATION_TYPE,
|
||||||
BENCH_LATITUDE_TENMICRODEG, BENCH_LONGITUDE_TENMICRODEG,
|
car->latitude_tenmicrodeg, car->longitude_tenmicrodeg,
|
||||||
|
car->speed_cm_s, car->heading_ddeg,
|
||||||
BTP_PORT_CAM, gn_payload, sizeof(gn_payload));
|
BTP_PORT_CAM, gn_payload, sizeof(gn_payload));
|
||||||
|
|
||||||
// qos=false for the standard-TX path (esp_wifi_80211_tx accepts only non-QoS
|
// qos=false for the standard-TX path (esp_wifi_80211_tx accepts only non-QoS
|
||||||
@@ -127,7 +134,10 @@ static void send_cam(void)
|
|||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "esp_wifi_80211_tx (standard) failed: %d", err);
|
ESP_LOGW(TAG, "esp_wifi_80211_tx (standard) failed: %d", err);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGI(TAG, "CAM sent via STANDARD tx (%d bytes) @ %d MHz genDeltaT=%u", frame_len, TX_FREQ_MHZ, gen_delta);
|
ESP_LOGI(TAG, "CAM sent (%d bytes) @ %d MHz genDeltaT=%u pos=%.7f,%.7f %.1f km/h heading %.1f pt%d",
|
||||||
|
frame_len, TX_FREQ_MHZ, gen_delta,
|
||||||
|
car->latitude_tenmicrodeg / 1e7, car->longitude_tenmicrodeg / 1e7,
|
||||||
|
car->speed_cm_s * 0.036, car->heading_ddeg / 10.0, car->segment + 1);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// tx_custom path: submits to the driver's internal HMAC TX path,
|
// tx_custom path: submits to the driver's internal HMAC TX path,
|
||||||
@@ -151,12 +161,49 @@ static void send_cam(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool cam_due(const route_state_t *car, const route_state_t *last, int64_t since_last_ms)
|
||||||
|
{
|
||||||
|
if (since_last_ms >= CAM_MAX_INTERVAL_MS) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int dh = abs((int)car->heading_ddeg - (int)last->heading_ddeg);
|
||||||
|
if (dh > 1800) {
|
||||||
|
dh = 3600 - dh;
|
||||||
|
}
|
||||||
|
if (dh > CAM_HEADING_DDEG) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (abs((int)car->speed_cm_s - (int)last->speed_cm_s) > CAM_SPEED_CM_S) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Flat-earth distance is plenty for a 4 m threshold.
|
||||||
|
double north_m = (car->latitude_tenmicrodeg - last->latitude_tenmicrodeg) * 0.0111194930;
|
||||||
|
double east_m = (car->longitude_tenmicrodeg - last->longitude_tenmicrodeg) * 0.0111194930
|
||||||
|
* cos(car->latitude_tenmicrodeg / 1e7 * M_PI / 180.0);
|
||||||
|
return north_m * north_m + east_m * east_m > CAM_POSITION_M * CAM_POSITION_M;
|
||||||
|
}
|
||||||
|
|
||||||
static void tx_task(void *arg)
|
static void tx_task(void *arg)
|
||||||
{
|
{
|
||||||
|
route_state_t car;
|
||||||
|
route_state_t last_sent;
|
||||||
|
int64_t last_sent_ms = 0;
|
||||||
|
bool sent_any = false;
|
||||||
|
TickType_t wake = xTaskGetTickCount();
|
||||||
|
|
||||||
|
route_step(0.0, &car);
|
||||||
while (1) {
|
while (1) {
|
||||||
// CAM is a continuous beacon - send every interval, unconditionally.
|
int64_t now_ms = esp_timer_get_time() / 1000;
|
||||||
send_cam();
|
if (!sent_any || cam_due(&car, &last_sent, now_ms - last_sent_ms)) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(TX_INTERVAL_MS));
|
// GenerationDeltaTime is TimestampIts mod 65536 (ms). No real clock here, so use
|
||||||
|
// milliseconds since boot, which advances at the right rate.
|
||||||
|
send_cam(&car, (uint16_t)now_ms);
|
||||||
|
last_sent = car;
|
||||||
|
last_sent_ms = now_ms;
|
||||||
|
sent_any = true;
|
||||||
|
}
|
||||||
|
vTaskDelayUntil(&wake, pdMS_TO_TICKS(CHECK_INTERVAL_MS));
|
||||||
|
route_step(CHECK_INTERVAL_MS / 1000.0, &car);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,8 +302,13 @@ void app_main(void)
|
|||||||
phy_change_channel(TX_FREQ_MHZ, 1, 0, 0);
|
phy_change_channel(TX_FREQ_MHZ, 1, 0, 0);
|
||||||
ESP_LOGI(TAG, "phy_change_channel returned");
|
ESP_LOGI(TAG, "phy_change_channel returned");
|
||||||
|
|
||||||
ESP_LOGW(TAG, "OCB @ %d MHz - CAM beacon armed, transmitting every %d ms",
|
if (route_init(route_points, sizeof(route_points) / sizeof(route_points[0]),
|
||||||
TX_FREQ_MHZ, TX_INTERVAL_MS);
|
CRUISE_MPS, MIN_CORNER_MPS) != 0) {
|
||||||
|
ESP_LOGE(TAG, "route_init failed - check route_points");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "OCB @ %d MHz - CAM beacon armed, driving a %d-point street loop",
|
||||||
|
TX_FREQ_MHZ, (int)(sizeof(route_points) / sizeof(route_points[0])));
|
||||||
|
|
||||||
xTaskCreate(tx_task, "tx_task", 4096, NULL, 5, NULL);
|
xTaskCreate(tx_task, "tx_task", 4096, NULL, 5, NULL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#include "route.h"
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
#define DEG_TO_RAD (M_PI / 180.0)
|
||||||
|
#define METRES_PER_DEG 111194.93 // mean Earth radius 6371 km; a block is small enough for a flat projection
|
||||||
|
#define ACCEL_MPS2 1.5 // pulling away from a corner
|
||||||
|
#define DECEL_MPS2 2.0 // braking ahead of a corner
|
||||||
|
#define LATERAL_MPS2 2.0 // sideways acceleration a normal driver takes a bend at
|
||||||
|
#define STRAIGHT_DEG 3.0 // kinks gentler than this are digitising noise, not bends
|
||||||
|
#define BEND_SPAN_M 15.0 // longest segment counted towards a bend's radius (see route_init)
|
||||||
|
|
||||||
|
static const route_point_t *s_pts;
|
||||||
|
static int s_n;
|
||||||
|
static double s_len[ROUTE_MAX_POINTS]; // segment i runs from point i to point (i+1) % n
|
||||||
|
static double s_bearing_deg[ROUTE_MAX_POINTS];
|
||||||
|
static double s_corner_mps[ROUTE_MAX_POINTS]; // speed limit at point i, where segment i starts
|
||||||
|
static double s_cruise_mps;
|
||||||
|
static int s_seg;
|
||||||
|
static double s_pos_m; // distance along the current segment
|
||||||
|
|
||||||
|
static double segment_speed(int seg, double pos_m)
|
||||||
|
{
|
||||||
|
double v = s_cruise_mps;
|
||||||
|
double pull_away = sqrt(s_corner_mps[seg] * s_corner_mps[seg] + 2.0 * ACCEL_MPS2 * pos_m);
|
||||||
|
int next = (seg + 1) % s_n;
|
||||||
|
double braking = sqrt(s_corner_mps[next] * s_corner_mps[next]
|
||||||
|
+ 2.0 * DECEL_MPS2 * (s_len[seg] - pos_m));
|
||||||
|
if (pull_away < v) v = pull_away;
|
||||||
|
if (braking < v) v = braking;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
int route_init(const route_point_t *points, int n, double cruise_mps, double min_corner_mps)
|
||||||
|
{
|
||||||
|
if (n < 2 || n > ROUTE_MAX_POINTS) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
s_pts = points;
|
||||||
|
s_n = n;
|
||||||
|
s_cruise_mps = cruise_mps;
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
const route_point_t *a = &points[i];
|
||||||
|
const route_point_t *b = &points[(i + 1) % n];
|
||||||
|
double mid_lat = (a->latitude_tenmicrodeg + (double)b->latitude_tenmicrodeg) / 2e7;
|
||||||
|
double north_m = (b->latitude_tenmicrodeg - a->latitude_tenmicrodeg) / 1e7 * METRES_PER_DEG;
|
||||||
|
double east_m = (b->longitude_tenmicrodeg - a->longitude_tenmicrodeg) / 1e7 * METRES_PER_DEG
|
||||||
|
* cos(mid_lat * DEG_TO_RAD);
|
||||||
|
s_len[i] = sqrt(north_m * north_m + east_m * east_m);
|
||||||
|
if (s_len[i] < 0.01) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
double bearing = atan2(east_m, north_m) / DEG_TO_RAD;
|
||||||
|
s_bearing_deg[i] = bearing < 0 ? bearing + 360.0 : bearing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speed limit at each point from how tight the bend there is. A polyline bend of angle theta
|
||||||
|
// between segments of length L approximates an arc of radius L / theta, and a car takes a
|
||||||
|
// radius R at sqrt(a_lat * R). L is capped at BEND_SPAN_M: at a junction the two streets can be
|
||||||
|
// hundreds of metres long, but the car still turns within the width of the crossing.
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
double turn = fabs(s_bearing_deg[i] - s_bearing_deg[(i + n - 1) % n]);
|
||||||
|
if (turn > 180.0) {
|
||||||
|
turn = 360.0 - turn;
|
||||||
|
}
|
||||||
|
double v = cruise_mps;
|
||||||
|
if (turn > STRAIGHT_DEG) {
|
||||||
|
double span = s_len[(i + n - 1) % n] < s_len[i] ? s_len[(i + n - 1) % n] : s_len[i];
|
||||||
|
if (span > BEND_SPAN_M) {
|
||||||
|
span = BEND_SPAN_M;
|
||||||
|
}
|
||||||
|
v = sqrt(LATERAL_MPS2 * span / (turn * DEG_TO_RAD));
|
||||||
|
}
|
||||||
|
if (v > cruise_mps) v = cruise_mps;
|
||||||
|
if (v < min_corner_mps) v = min_corner_mps;
|
||||||
|
s_corner_mps[i] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A point's limit also has to respect the bends after it (the car must be able to brake for
|
||||||
|
// them within the segments in between) and before it (it can only have sped up so much since).
|
||||||
|
// segment_speed only looks at the two ends of a segment, so settle this here. Limits only ever
|
||||||
|
// go down, so repeating the two passes until nothing changes terminates.
|
||||||
|
for (int changed = 1; changed;) {
|
||||||
|
changed = 0;
|
||||||
|
for (int k = 0; k < 2 * n; k++) {
|
||||||
|
int i = (2 * n - 1 - k) % n; // backwards: braking
|
||||||
|
int next = (i + 1) % n;
|
||||||
|
double v = sqrt(s_corner_mps[next] * s_corner_mps[next] + 2.0 * DECEL_MPS2 * s_len[i]);
|
||||||
|
if (v < s_corner_mps[i] - 1e-9) { s_corner_mps[i] = v; changed = 1; }
|
||||||
|
}
|
||||||
|
for (int k = 0; k < 2 * n; k++) {
|
||||||
|
int i = k % n; // forwards: accelerating
|
||||||
|
int next = (i + 1) % n;
|
||||||
|
double v = sqrt(s_corner_mps[i] * s_corner_mps[i] + 2.0 * ACCEL_MPS2 * s_len[i]);
|
||||||
|
if (v < s_corner_mps[next] - 1e-9) { s_corner_mps[next] = v; changed = 1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s_seg = 0;
|
||||||
|
s_pos_m = 0.0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void route_step(double dt_s, route_state_t *out)
|
||||||
|
{
|
||||||
|
// Advance with the speed at the start of the step; at 100 ms steps the error is well under a metre.
|
||||||
|
double d = segment_speed(s_seg, s_pos_m) * dt_s;
|
||||||
|
while (s_pos_m + d >= s_len[s_seg]) {
|
||||||
|
d -= s_len[s_seg] - s_pos_m;
|
||||||
|
s_seg = (s_seg + 1) % s_n;
|
||||||
|
s_pos_m = 0.0;
|
||||||
|
}
|
||||||
|
s_pos_m += d;
|
||||||
|
|
||||||
|
const route_point_t *a = &s_pts[s_seg];
|
||||||
|
const route_point_t *b = &s_pts[(s_seg + 1) % s_n];
|
||||||
|
double f = s_pos_m / s_len[s_seg];
|
||||||
|
out->latitude_tenmicrodeg = (int32_t)lround(a->latitude_tenmicrodeg
|
||||||
|
+ f * (b->latitude_tenmicrodeg - a->latitude_tenmicrodeg));
|
||||||
|
out->longitude_tenmicrodeg = (int32_t)lround(a->longitude_tenmicrodeg
|
||||||
|
+ f * (b->longitude_tenmicrodeg - a->longitude_tenmicrodeg));
|
||||||
|
out->speed_cm_s = (uint16_t)lround(segment_speed(s_seg, s_pos_m) * 100.0);
|
||||||
|
out->heading_ddeg = (uint16_t)(lround(s_bearing_deg[s_seg] * 10.0) % 3600);
|
||||||
|
out->segment = s_seg;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#ifndef ROUTE_H
|
||||||
|
#define ROUTE_H
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// Simulated drive around a closed loop of waypoints, so the beacon looks like a
|
||||||
|
// car going round the block instead of a parked one.
|
||||||
|
//
|
||||||
|
// The car follows straight lines between the points and goes from the last one
|
||||||
|
// back to the first, forever. For street-following, the points are the street
|
||||||
|
// geometry from tools/make_route.py (main/route_points.h). Speed is a function
|
||||||
|
// of where the car is on a segment: it cruises, brakes ahead of each bend down to
|
||||||
|
// the speed that bend allows, and accelerates away after it. Heading is the
|
||||||
|
// bearing of the current segment.
|
||||||
|
//
|
||||||
|
// Uses only standard headers, so it also compiles on the host for testing.
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int32_t latitude_tenmicrodeg; // 1/10 microdegree, same units as the CAM
|
||||||
|
int32_t longitude_tenmicrodeg;
|
||||||
|
} route_point_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int32_t latitude_tenmicrodeg;
|
||||||
|
int32_t longitude_tenmicrodeg;
|
||||||
|
uint16_t speed_cm_s; // SpeedValue units (0.01 m/s)
|
||||||
|
uint16_t heading_ddeg; // HeadingValue units (0.1 deg, 0..3599, 0 = north, clockwise)
|
||||||
|
int segment; // index of the route point the car last passed
|
||||||
|
} route_state_t;
|
||||||
|
|
||||||
|
#define ROUTE_MAX_POINTS 512 // keep in step with MAX_POINTS in tools/make_route.py
|
||||||
|
|
||||||
|
// `points` must stay valid for as long as the route is used. Returns 0, or -1
|
||||||
|
// if n is out of range (2..ROUTE_MAX_POINTS) or a segment has zero length.
|
||||||
|
// min_corner_mps is the slowest the car ever goes (hairpins, U-turns).
|
||||||
|
int route_init(const route_point_t *points, int n, double cruise_mps, double min_corner_mps);
|
||||||
|
|
||||||
|
// Moves the car on by dt_s seconds and writes its new position into `out`.
|
||||||
|
void route_step(double dt_s, route_state_t *out);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// GENERATED by tools/make_route.py - do not edit by hand; change WAYPOINTS there and rerun.
|
||||||
|
// Route data (c) OpenStreetMap contributors, ODbL. Routing by OSRM.
|
||||||
|
//
|
||||||
|
// Driving loop through 6 waypoints: 5179 m (legs 147 m, 1837 m, 381 m, 819 m, 1234 m, 761 m), 103 points after
|
||||||
|
// simplifying to 1.5 m. Streets: Beim Strohhause, Berlinertordamm, Berliner Tor, Bei der Hauptfeuerwache, Westphalensweg, Berliner Tor, Berlinertordamm, Borgfelder Straße, Anckelmannstraße, Anckelmannsplatz, Bürgerweide, Wallstraße, Lübeckertordamm, Steindamm, Kreuzweg, Adenauerallee, Nagelsweg, Norderstraße, Repsoldstraße, Kurt-Schumacher-Allee, Kreuzweg, Adenauerallee, Kurt-Schumacher-Allee, Beim Strohhause.
|
||||||
|
#ifndef ROUTE_POINTS_H
|
||||||
|
#define ROUTE_POINTS_H
|
||||||
|
#include "route.h"
|
||||||
|
|
||||||
|
static const route_point_t route_points[] = {
|
||||||
|
{ 535531770, 100220980 },
|
||||||
|
{ 535534470, 100240600 },
|
||||||
|
{ 535535790, 100244160 },
|
||||||
|
{ 535536400, 100244520 },
|
||||||
|
{ 535537130, 100244160 },
|
||||||
|
{ 535538480, 100242840 },
|
||||||
|
{ 535538720, 100242140 },
|
||||||
|
{ 535540390, 100240200 },
|
||||||
|
{ 535548880, 100233930 },
|
||||||
|
{ 535549470, 100235130 },
|
||||||
|
{ 535554140, 100253280 },
|
||||||
|
{ 535553570, 100254580 },
|
||||||
|
{ 535546070, 100251700 },
|
||||||
|
{ 535543130, 100250020 },
|
||||||
|
{ 535542570, 100249130 },
|
||||||
|
{ 535542760, 100247800 },
|
||||||
|
{ 535539980, 100244600 },
|
||||||
|
{ 535538720, 100242140 },
|
||||||
|
{ 535538140, 100241960 },
|
||||||
|
{ 535535720, 100243380 },
|
||||||
|
{ 535535300, 100244470 },
|
||||||
|
{ 535535090, 100246580 },
|
||||||
|
{ 535536830, 100264460 },
|
||||||
|
{ 535537600, 100270920 },
|
||||||
|
{ 535538520, 100275220 },
|
||||||
|
{ 535541110, 100290830 },
|
||||||
|
{ 535540170, 100291550 },
|
||||||
|
{ 535539890, 100294430 },
|
||||||
|
{ 535539580, 100295330 },
|
||||||
|
{ 535532290, 100299300 },
|
||||||
|
{ 535527980, 100302450 },
|
||||||
|
{ 535525130, 100291880 },
|
||||||
|
{ 535524220, 100289760 },
|
||||||
|
{ 535522900, 100287820 },
|
||||||
|
{ 535522890, 100285300 },
|
||||||
|
{ 535522610, 100282810 },
|
||||||
|
{ 535520620, 100276050 },
|
||||||
|
{ 535519820, 100271130 },
|
||||||
|
{ 535519670, 100269030 },
|
||||||
|
{ 535520220, 100267120 },
|
||||||
|
{ 535520930, 100262840 },
|
||||||
|
{ 535521850, 100260020 },
|
||||||
|
{ 535522840, 100258210 },
|
||||||
|
{ 535527790, 100257880 },
|
||||||
|
{ 535538650, 100261370 },
|
||||||
|
{ 535551660, 100266470 },
|
||||||
|
{ 535555790, 100268750 },
|
||||||
|
{ 535559620, 100271540 },
|
||||||
|
{ 535561120, 100272050 },
|
||||||
|
{ 535562430, 100271250 },
|
||||||
|
{ 535563830, 100269390 },
|
||||||
|
{ 535569610, 100260190 },
|
||||||
|
{ 535579530, 100242810 },
|
||||||
|
{ 535584090, 100237660 },
|
||||||
|
{ 535585850, 100234670 },
|
||||||
|
{ 535584970, 100230060 },
|
||||||
|
{ 535584000, 100227260 },
|
||||||
|
{ 535580730, 100222030 },
|
||||||
|
{ 535579100, 100218590 },
|
||||||
|
{ 535574500, 100208430 },
|
||||||
|
{ 535570560, 100199010 },
|
||||||
|
{ 535568130, 100194820 },
|
||||||
|
{ 535564910, 100187620 },
|
||||||
|
{ 535563990, 100184630 },
|
||||||
|
{ 535562540, 100181160 },
|
||||||
|
{ 535559780, 100176310 },
|
||||||
|
{ 535542180, 100136840 },
|
||||||
|
{ 535539720, 100133430 },
|
||||||
|
{ 535537350, 100132010 },
|
||||||
|
{ 535534760, 100131390 },
|
||||||
|
{ 535523840, 100132690 },
|
||||||
|
{ 535524980, 100155090 },
|
||||||
|
{ 535524890, 100156360 },
|
||||||
|
{ 535524440, 100157650 },
|
||||||
|
{ 535523030, 100158530 },
|
||||||
|
{ 535517610, 100158580 },
|
||||||
|
{ 535504440, 100168810 },
|
||||||
|
{ 535501360, 100156400 },
|
||||||
|
{ 535499250, 100144880 },
|
||||||
|
{ 535498470, 100133880 },
|
||||||
|
{ 535498630, 100126150 },
|
||||||
|
{ 535499110, 100121980 },
|
||||||
|
{ 535504260, 100117230 },
|
||||||
|
{ 535505720, 100115500 },
|
||||||
|
{ 535507630, 100112310 },
|
||||||
|
{ 535508400, 100111560 },
|
||||||
|
{ 535510000, 100110940 },
|
||||||
|
{ 535511390, 100119560 },
|
||||||
|
{ 535512440, 100122280 },
|
||||||
|
{ 535514510, 100132770 },
|
||||||
|
{ 535515020, 100134120 },
|
||||||
|
{ 535516630, 100135630 },
|
||||||
|
{ 535518260, 100136100 },
|
||||||
|
{ 535523950, 100134870 },
|
||||||
|
{ 535524980, 100155090 },
|
||||||
|
{ 535524890, 100156360 },
|
||||||
|
{ 535524440, 100157650 },
|
||||||
|
{ 535523470, 100158460 },
|
||||||
|
{ 535518560, 100158480 },
|
||||||
|
{ 535519070, 100162430 },
|
||||||
|
{ 535522260, 100178080 },
|
||||||
|
{ 535527880, 100197650 },
|
||||||
|
{ 535530060, 100209020 },
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
"""Turn the beacon's waypoints into a street-following route for main/route_points.h.
|
||||||
|
|
||||||
|
Asks the OSRM demo server (router.project-osrm.org, OpenStreetMap data) for a driving route that
|
||||||
|
visits WAYPOINTS in order and returns to the first, thins the street geometry out, and writes:
|
||||||
|
|
||||||
|
main/route_points.h the C array the firmware drives (commit this)
|
||||||
|
tools/route_osrm.json the raw OSRM answer, so the header can be regenerated offline (--offline)
|
||||||
|
tools/route_map.html the route over an OpenStreetMap map, to check it before flashing
|
||||||
|
|
||||||
|
Each waypoint also gets a bearing (the direction towards the next waypoint), so OSRM snaps it onto
|
||||||
|
the carriageway going that way. Without it, points on divided roads land on the wrong side and
|
||||||
|
every leg grows a U-turn detour.
|
||||||
|
|
||||||
|
Usage (Python 3.8+, standard library only):
|
||||||
|
py tools/make_route.py fetch from OSRM, then write all three files
|
||||||
|
py -3 tools/make_route.py --offline rebuild from the saved route_osrm.json
|
||||||
|
|
||||||
|
Route data (c) OpenStreetMap contributors, ODbL. Routing by OSRM.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import pathlib
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
# (latitude, longitude) in decimal degrees, in driving order. The route closes back to the first.
|
||||||
|
WAYPOINTS = [
|
||||||
|
(53.553309, 10.022043),
|
||||||
|
(53.553611, 10.024146),
|
||||||
|
(53.556164, 10.027121),
|
||||||
|
(53.558310, 10.023362),
|
||||||
|
(53.554062, 10.013515),
|
||||||
|
(53.551540, 10.013398),
|
||||||
|
]
|
||||||
|
BEARING_TOLERANCE_DEG = 60 # how far the road's direction may differ from the waypoint bearing
|
||||||
|
SIMPLIFY_M = 1.5 # drop points that move the line by less than this
|
||||||
|
MAX_POINTS = 512 # must match ROUTE_MAX_POINTS in main/route.h
|
||||||
|
|
||||||
|
HERE = pathlib.Path(__file__).resolve().parent
|
||||||
|
PROJECT = HERE.parent
|
||||||
|
OSRM_JSON = HERE / "route_osrm.json"
|
||||||
|
HEADER = PROJECT / "main" / "route_points.h"
|
||||||
|
MAP_HTML = HERE / "route_map.html"
|
||||||
|
METRES_PER_DEG = 111194.93
|
||||||
|
|
||||||
|
|
||||||
|
def to_xy(lat, lon, lat0):
|
||||||
|
return (lon * METRES_PER_DEG * math.cos(math.radians(lat0)), lat * METRES_PER_DEG)
|
||||||
|
|
||||||
|
|
||||||
|
def bearing(a, b):
|
||||||
|
north = b[0] - a[0]
|
||||||
|
east = (b[1] - a[1]) * math.cos(math.radians(a[0]))
|
||||||
|
return math.degrees(math.atan2(east, north)) % 360
|
||||||
|
|
||||||
|
|
||||||
|
def fetch():
|
||||||
|
n = len(WAYPOINTS)
|
||||||
|
pts = WAYPOINTS + [WAYPOINTS[0]]
|
||||||
|
bearings = [round(bearing(WAYPOINTS[i], WAYPOINTS[(i + 1) % n])) for i in range(n)]
|
||||||
|
bearings.append(bearings[0])
|
||||||
|
url = ("https://router.project-osrm.org/route/v1/driving/"
|
||||||
|
+ ";".join(f"{lon},{lat}" for lat, lon in pts)
|
||||||
|
+ "?overview=full&geometries=geojson&steps=true&bearings="
|
||||||
|
+ ";".join(f"{b},{BEARING_TOLERANCE_DEG}" for b in bearings))
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "MicrOBU-route-tool"})
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
data = json.load(resp)
|
||||||
|
if data.get("code") != "Ok":
|
||||||
|
raise SystemExit(f"OSRM error: {data.get('code')} {data.get('message')}")
|
||||||
|
OSRM_JSON.write_text(json.dumps(data, indent=1), encoding="utf-8")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def simplify(xy, tol):
|
||||||
|
"""Douglas-Peucker, iterative. Keeps the first and last point."""
|
||||||
|
keep = [False] * len(xy)
|
||||||
|
keep[0] = keep[-1] = True
|
||||||
|
stack = [(0, len(xy) - 1)]
|
||||||
|
while stack:
|
||||||
|
a, b = stack.pop()
|
||||||
|
(ax, ay), (bx, by) = xy[a], xy[b]
|
||||||
|
dx, dy = bx - ax, by - ay
|
||||||
|
seg2 = dx * dx + dy * dy
|
||||||
|
worst, worst_d = -1, tol
|
||||||
|
for i in range(a + 1, b):
|
||||||
|
px, py = xy[i]
|
||||||
|
if seg2 == 0:
|
||||||
|
d = math.hypot(px - ax, py - ay)
|
||||||
|
else:
|
||||||
|
t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / seg2))
|
||||||
|
d = math.hypot(px - ax - t * dx, py - ay - t * dy)
|
||||||
|
if d > worst_d:
|
||||||
|
worst, worst_d = i, d
|
||||||
|
if worst >= 0:
|
||||||
|
keep[worst] = True
|
||||||
|
stack += [(a, worst), (worst, b)]
|
||||||
|
return [i for i, k in enumerate(keep) if k]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--offline", action="store_true", help="use the saved route_osrm.json")
|
||||||
|
args = ap.parse_args()
|
||||||
|
data = json.loads(OSRM_JSON.read_text(encoding="utf-8")) if args.offline else fetch()
|
||||||
|
route = data["routes"][0]
|
||||||
|
|
||||||
|
# GeoJSON is [lon, lat]; round to the CAM's 1/10-microdegree grid and drop repeats.
|
||||||
|
raw = []
|
||||||
|
for lon, lat in route["geometry"]["coordinates"]:
|
||||||
|
p = (round(lat * 1e7), round(lon * 1e7))
|
||||||
|
if not raw or p != raw[-1]:
|
||||||
|
raw.append(p)
|
||||||
|
if raw[0] == raw[-1]:
|
||||||
|
raw.pop()
|
||||||
|
closed = raw + [raw[0]]
|
||||||
|
lat0 = closed[0][0] / 1e7
|
||||||
|
xy = [to_xy(p[0] / 1e7, p[1] / 1e7, lat0) for p in closed]
|
||||||
|
pts = [closed[i] for i in simplify(xy, SIMPLIFY_M)][:-1]
|
||||||
|
if len(pts) > MAX_POINTS:
|
||||||
|
raise SystemExit(f"{len(pts)} points is more than MAX_POINTS={MAX_POINTS}; raise SIMPLIFY_M")
|
||||||
|
|
||||||
|
streets = []
|
||||||
|
for leg in route["legs"]:
|
||||||
|
for step in leg["steps"]:
|
||||||
|
if step["name"] and (not streets or streets[-1] != step["name"]):
|
||||||
|
streets.append(step["name"])
|
||||||
|
legs = ", ".join(f"{round(l['distance'])} m" for l in route["legs"])
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"// GENERATED by tools/make_route.py - do not edit by hand; change WAYPOINTS there and rerun.",
|
||||||
|
"// Route data (c) OpenStreetMap contributors, ODbL. Routing by OSRM.",
|
||||||
|
"//",
|
||||||
|
f"// Driving loop through {len(WAYPOINTS)} waypoints: {round(route['distance'])} m "
|
||||||
|
f"(legs {legs}), {len(pts)} points after",
|
||||||
|
f"// simplifying to {SIMPLIFY_M} m. Streets: {', '.join(streets)}.",
|
||||||
|
"#ifndef ROUTE_POINTS_H",
|
||||||
|
"#define ROUTE_POINTS_H",
|
||||||
|
'#include "route.h"',
|
||||||
|
"",
|
||||||
|
"static const route_point_t route_points[] = {",
|
||||||
|
]
|
||||||
|
lines += [f" {{ {lat}, {lon} }}," for lat, lon in pts]
|
||||||
|
lines += ["};", "", "#endif", ""]
|
||||||
|
HEADER.write_text("\n".join(lines), encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
MAP_HTML.write_text(f"""<!doctype html><meta charset="utf-8"><title>Beacon route</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<style>html,body,#m{{height:100%;margin:0}}</style><div id="m"></div><script>
|
||||||
|
const pts={json.dumps([[p[0] / 1e7, p[1] / 1e7] for p in pts])};
|
||||||
|
const wps={json.dumps(WAYPOINTS)};
|
||||||
|
const m=L.map('m');
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png',{{maxZoom:19,
|
||||||
|
attribution:'© OpenStreetMap contributors'}}).addTo(m);
|
||||||
|
const line=L.polyline(pts.concat([pts[0]]),{{color:'#d33',weight:4}}).addTo(m);
|
||||||
|
wps.forEach((w,i)=>L.marker(w,{{title:'wp'+(i+1)}}).bindTooltip('wp'+(i+1),{{permanent:true}}).addTo(m));
|
||||||
|
L.circleMarker(pts[0],{{radius:7,color:'#060'}}).bindTooltip('start').addTo(m);
|
||||||
|
m.fitBounds(line.getBounds(),{{padding:[20,20]}});
|
||||||
|
</script>
|
||||||
|
""", encoding="utf-8")
|
||||||
|
|
||||||
|
print(f"{round(route['distance'])} m, legs {legs}")
|
||||||
|
print(f"{len(route['geometry']['coordinates'])} OSRM points -> {len(pts)} after simplifying")
|
||||||
|
print(f"wrote {HEADER.relative_to(PROJECT)}, {OSRM_JSON.relative_to(PROJECT)}, "
|
||||||
|
f"{MAP_HTML.relative_to(PROJECT)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html><meta charset="utf-8"><title>Beacon route</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<style>html,body,#m{height:100%;margin:0}</style><div id="m"></div><script>
|
||||||
|
const pts=[[53.553177, 10.022098], [53.553447, 10.02406], [53.553579, 10.024416], [53.55364, 10.024452], [53.553713, 10.024416], [53.553848, 10.024284], [53.553872, 10.024214], [53.554039, 10.02402], [53.554888, 10.023393], [53.554947, 10.023513], [53.555414, 10.025328], [53.555357, 10.025458], [53.554607, 10.02517], [53.554313, 10.025002], [53.554257, 10.024913], [53.554276, 10.02478], [53.553998, 10.02446], [53.553872, 10.024214], [53.553814, 10.024196], [53.553572, 10.024338], [53.55353, 10.024447], [53.553509, 10.024658], [53.553683, 10.026446], [53.55376, 10.027092], [53.553852, 10.027522], [53.554111, 10.029083], [53.554017, 10.029155], [53.553989, 10.029443], [53.553958, 10.029533], [53.553229, 10.02993], [53.552798, 10.030245], [53.552513, 10.029188], [53.552422, 10.028976], [53.55229, 10.028782], [53.552289, 10.02853], [53.552261, 10.028281], [53.552062, 10.027605], [53.551982, 10.027113], [53.551967, 10.026903], [53.552022, 10.026712], [53.552093, 10.026284], [53.552185, 10.026002], [53.552284, 10.025821], [53.552779, 10.025788], [53.553865, 10.026137], [53.555166, 10.026647], [53.555579, 10.026875], [53.555962, 10.027154], [53.556112, 10.027205], [53.556243, 10.027125], [53.556383, 10.026939], [53.556961, 10.026019], [53.557953, 10.024281], [53.558409, 10.023766], [53.558585, 10.023467], [53.558497, 10.023006], [53.5584, 10.022726], [53.558073, 10.022203], [53.55791, 10.021859], [53.55745, 10.020843], [53.557056, 10.019901], [53.556813, 10.019482], [53.556491, 10.018762], [53.556399, 10.018463], [53.556254, 10.018116], [53.555978, 10.017631], [53.554218, 10.013684], [53.553972, 10.013343], [53.553735, 10.013201], [53.553476, 10.013139], [53.552384, 10.013269], [53.552498, 10.015509], [53.552489, 10.015636], [53.552444, 10.015765], [53.552303, 10.015853], [53.551761, 10.015858], [53.550444, 10.016881], [53.550136, 10.01564], [53.549925, 10.014488], [53.549847, 10.013388], [53.549863, 10.012615], [53.549911, 10.012198], [53.550426, 10.011723], [53.550572, 10.01155], [53.550763, 10.011231], [53.55084, 10.011156], [53.551, 10.011094], [53.551139, 10.011956], [53.551244, 10.012228], [53.551451, 10.013277], [53.551502, 10.013412], [53.551663, 10.013563], [53.551826, 10.01361], [53.552395, 10.013487], [53.552498, 10.015509], [53.552489, 10.015636], [53.552444, 10.015765], [53.552347, 10.015846], [53.551856, 10.015848], [53.551907, 10.016243], [53.552226, 10.017808], [53.552788, 10.019765], [53.553006, 10.020902]];
|
||||||
|
const wps=[[53.553309, 10.022043], [53.553611, 10.024146], [53.556164, 10.027121], [53.55831, 10.023362], [53.554062, 10.013515], [53.55154, 10.013398]];
|
||||||
|
const m=L.map('m');
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,
|
||||||
|
attribution:'© OpenStreetMap contributors'}).addTo(m);
|
||||||
|
const line=L.polyline(pts.concat([pts[0]]),{color:'#d33',weight:4}).addTo(m);
|
||||||
|
wps.forEach((w,i)=>L.marker(w,{title:'wp'+(i+1)}).bindTooltip('wp'+(i+1),{permanent:true}).addTo(m));
|
||||||
|
L.circleMarker(pts[0],{radius:7,color:'#060'}).bindTooltip('start').addTo(m);
|
||||||
|
m.fitBounds(line.getBounds(),{padding:[20,20]});
|
||||||
|
</script>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,64 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.22)
|
||||||
|
# obu-firmware: the ESP32-C5 half of the MicrOBU station, on the vanetza-idf C-ITS stack.
|
||||||
|
#
|
||||||
|
# Since 2026-09-23 this is a port of the colleague's standalone VRU station
|
||||||
|
# (microbu-esp32c5/firmware, in their own repository, not part of this one). From it:
|
||||||
|
# BTP/GeoNetworking and the TS 103 097 security entity (vanetza-idf), the station-link message
|
||||||
|
# layer over native USB Serial/JTAG and BLE GATT, the NVS credential store, and the C5 radio adapter.
|
||||||
|
# Added here for this project: the raw receive path of the previous firmware (gn_unwrap.c, forwarded
|
||||||
|
# as link opcode V2X_RX) so unsigned and non-demo-signed traffic still reaches the phone, the
|
||||||
|
# unsigned transmit path of the previous firmware (geonet.c), and BLE pausing while USB is in use.
|
||||||
|
# See NOTES.md.
|
||||||
|
#
|
||||||
|
# ESP-IDF 6.0.2 exactly: the C5 radio's private Wi-Fi driver ABI (otm_tx_custom.c) is pinned to it
|
||||||
|
# by vanetza-idf's radio_c5.cmake and has only been validated there. The previous C firmware was
|
||||||
|
# built with IDF 6.1; see FLASHING.md for the export script of each.
|
||||||
|
#
|
||||||
|
# vanetza-idf lives in external/vanetza-idf, copied from the colleague's repository (their
|
||||||
|
# external/vanetza-idf at commit cf4b99f, unchanged; see its PROVENANCE.md). Override with
|
||||||
|
# -DVANETZA_IDF_DIR=... to build against another checkout.
|
||||||
|
if(NOT VANETZA_IDF_DIR)
|
||||||
|
set(VANETZA_IDF_DIR "${CMAKE_CURRENT_LIST_DIR}/external/vanetza-idf")
|
||||||
|
endif()
|
||||||
|
if(NOT EXISTS "${VANETZA_IDF_DIR}/idf_component.yml")
|
||||||
|
message(FATAL_ERROR "vanetza-idf not found at ${VANETZA_IDF_DIR}: obu-firmware/external/vanetza-idf "
|
||||||
|
"is part of this repository; check it is complete, or pass "
|
||||||
|
"-DVANETZA_IDF_DIR=<path to a vanetza-idf checkout>")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Provenance guard for main/otm_tx_custom.c, copied unchanged from microbu-esp32c5/firmware/CMakeLists.txt:
|
||||||
|
# this checked-in copy is what microbu::C5Radio::request() links against for every transmission.
|
||||||
|
# Fail configure if either the pinned upstream or this file drifts from the reviewed revision.
|
||||||
|
set(_otm_upstream "${VANETZA_IDF_DIR}/ports/esp_idf/third_party/otm/main/tx_custom.c")
|
||||||
|
if(EXISTS "${_otm_upstream}")
|
||||||
|
file(READ "${_otm_upstream}" _otm_upstream_text)
|
||||||
|
string(REPLACE "\r\n" "\n" _otm_upstream_text "${_otm_upstream_text}")
|
||||||
|
string(SHA256 _otm_upstream_hash "${_otm_upstream_text}")
|
||||||
|
if(NOT _otm_upstream_hash STREQUAL "cb1dccfef96912ca59275e8a9102f41f56925b94a19d4a4629082aaeb779be1b")
|
||||||
|
message(FATAL_ERROR "OpenTrafficMap upstream tx_custom.c differs from the reviewed source revision (674e3412) -- review before updating main/otm_tx_custom.c and this hash")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
set(_otm_checked_in "${CMAKE_CURRENT_LIST_DIR}/main/otm_tx_custom.c")
|
||||||
|
file(READ "${_otm_checked_in}" _otm_checked_in_text)
|
||||||
|
string(REPLACE "\r\n" "\n" _otm_checked_in_text "${_otm_checked_in_text}")
|
||||||
|
string(SHA256 _otm_checked_in_hash "${_otm_checked_in_text}")
|
||||||
|
if(NOT _otm_checked_in_hash STREQUAL "114693af99ce3866cfc767066d484e45822eaf15335d94a03626b60ac5777277")
|
||||||
|
message(FATAL_ERROR "main/otm_tx_custom.c differs from the reviewed copy -- review the change, then update this hash")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
list(APPEND EXTRA_COMPONENT_DIRS "${VANETZA_IDF_DIR}")
|
||||||
|
set(SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults")
|
||||||
|
set(COMPONENTS main)
|
||||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
# No longer need -Wl,-zmuldefs here - that was only for main/wifi_patches.c's
|
|
||||||
# symbol-override attempt (which didn't work anyway; see docs/04-transmit-setup.md),
|
|
||||||
# and that file is no longer part of the build. Superseded by main/tx_custom.c,
|
|
||||||
# which bypasses the gate at a different layer instead of trying to override it.
|
|
||||||
|
|
||||||
project(obu_firmware)
|
project(obu_firmware)
|
||||||
|
add_compile_options(-Wno-error -Wno-cpp -Wno-error=implicit-function-declaration)
|
||||||
|
idf_component_get_property(vanetza_lib vanetza-idf COMPONENT_LIB)
|
||||||
|
if(vanetza_lib)
|
||||||
|
target_compile_options(${vanetza_lib} PRIVATE -Wno-error=implicit-function-declaration -Wno-error=cpp)
|
||||||
|
endif()
|
||||||
|
# GCC 15's stricter -Warray-bounds false-positives on NimBLE's fixed-size bond-store arrays
|
||||||
|
# (upstream Apache Mynewt code); demote to a warning so the component still builds.
|
||||||
|
idf_component_get_property(bt_lib bt COMPONENT_LIB)
|
||||||
|
if(bt_lib)
|
||||||
|
target_compile_options(${bt_lib} PRIVATE -Wno-error=array-bounds)
|
||||||
|
endif()
|
||||||
|
|||||||
+101
-27
@@ -2,35 +2,93 @@
|
|||||||
|
|
||||||
## Two toolchains - use a dedicated terminal for each
|
## Two toolchains - use a dedicated terminal for each
|
||||||
|
|
||||||
This project builds against the receiver-firmware's pinned ESP-IDF **6.1**.
|
Since 2026-09-23 this project builds against ESP-IDF **6.0.2** exactly
|
||||||
The separate `obu-cam-transmistter` project builds against the global ESP-IDF
|
(`C:\Espressif\frameworks\esp-idf-v6.0.2`): it is the vanetza-idf port (see
|
||||||
**5.5.4**. Exporting both in one PowerShell window fails: the second export
|
NOTES.md), and vanetza-idf's `radio_c5.cmake` refuses any other version because
|
||||||
|
the raw TX path pokes private Wi-Fi driver structures only validated there. The
|
||||||
|
previous C firmware used the receiver firmware's IDF **6.1**; the separate
|
||||||
|
`obu-cam-transmistter` project builds against the global ESP-IDF **5.5.4**.
|
||||||
|
Exporting two of them in one PowerShell window fails: the second export
|
||||||
inherits the first's `IDF_PYTHON_ENV_PATH` and reports every Python dependency
|
inherits the first's `IDF_PYTHON_ENV_PATH` and reports every Python dependency
|
||||||
as unmet. Don't run `install.bat` to "fix" that - open a fresh terminal, or
|
as unmet. Don't run `install.bat` to "fix" that - open a fresh terminal, or
|
||||||
clear the state with `$env:IDF_PYTHON_ENV_PATH = $null; $env:IDF_PATH = $null`.
|
clear the state with `$env:IDF_PYTHON_ENV_PATH = $null; $env:IDF_PATH = $null`.
|
||||||
|
|
||||||
## Every new PowerShell session
|
The build is self-contained: the C-ITS library comes from
|
||||||
|
`obu-firmware/external/vanetza-idf`, a copy of `external/vanetza-idf` from the
|
||||||
|
colleague's microbu-esp32c5 repository (their commit cf4b99f, unchanged). Pass
|
||||||
|
`-DVANETZA_IDF_DIR=<path>` to `idf.py` to build against another checkout. The
|
||||||
|
first build downloads `espressif/esp-boost` into `managed_components/`.
|
||||||
|
|
||||||
Activate the toolchain (obu-firmware has no esp-idf of its own — reuse the
|
Nothing is fetched from or pushed to the colleague's repository (HAW GitLab,
|
||||||
receiver firmware's already-installed checkout):
|
urban-mobility-lab/microbu/microbu-esp32c5). To take a newer vanetza-idf from
|
||||||
|
it, copy their `external/vanetza-idf` over this folder, rebuild and test, and
|
||||||
|
commit it here. The rest of their tree (their own VAM firmware, PKI tooling,
|
||||||
|
station-link Python tools, the V2X2MAP bridge) is not part of this repository.
|
||||||
|
|
||||||
|
## Every new PowerShell session
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
||||||
C:\Users\Ashin\Documents\micrOBU_workspace\its-g5-receiver-firmware\esp-idf\export.ps1
|
$env:IDF_TOOLS_PATH = "C:\Espressif"
|
||||||
idf.py --version
|
C:\Espressif\frameworks\esp-idf-v6.0.2\export.ps1
|
||||||
|
idf.py --version # v6.0.2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build & flash
|
## Build & flash
|
||||||
|
|
||||||
|
Flash over the board's **UART-bridge port** (CH343; COM3 for the production OBU
|
||||||
|
on the bench), not the native port the phone uses.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd C:\Users\Ashin\AndroidStudioProjects\MicrOBU\obu-firmware
|
cd C:\Users\Ashin\AndroidStudioProjects\MicrOBU\obu-firmware
|
||||||
idf.py set-target esp32c5 # only needed once per clean build folder
|
idf.py set-target esp32c5 # only needed once per clean build folder
|
||||||
idf.py build
|
idf.py build
|
||||||
idf.py -p COM5 -b 921600 flash monitor
|
idf.py -p COM3 -b 921600 flash monitor
|
||||||
```
|
```
|
||||||
|
|
||||||
Swap `COM5` for whatever port the ESP32-C5 enumerates as (Device Manager →
|
Swap `COM3` for whatever port the ESP32-C5's bridge enumerates as (Device Manager →
|
||||||
Ports). `monitor` opens the serial console after flashing — `Ctrl+]` to exit.
|
Ports). `monitor` opens the serial console after flashing — `Ctrl+]` to exit.
|
||||||
|
Use `flash` (bootloader + partition table + app), not `app-flash`, whenever the
|
||||||
|
partition table may differ from what the board has.
|
||||||
|
|
||||||
|
### First flash onto a board with the previous firmware
|
||||||
|
|
||||||
|
The partition table changed with the port (NVS 24 KB → 80 KB, app 0x10000 →
|
||||||
|
0x20000), and the old NVS is full of Wi-Fi settings the previous firmware left
|
||||||
|
behind. Erase the whole new NVS range once, then do a full flash:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m esptool --chip esp32c5 -p COM3 -b 921600 erase-region 0x9000 0x15000
|
||||||
|
idf.py -p COM3 -b 921600 flash
|
||||||
|
```
|
||||||
|
|
||||||
|
Without the erase, the board cannot store the BLE bond and the phone has to pair
|
||||||
|
on every connection (2026-09-23). After it:
|
||||||
|
|
||||||
|
- the boot log shows `0 bonded phone(s) in NVS`, `BLE advertising started as
|
||||||
|
'micrOBU-XXXX'` and `station task ready`; the radio stays off until the app
|
||||||
|
connects;
|
||||||
|
- on the first Connect the app provisions the demo credentials once (the card
|
||||||
|
says "Provisioning the demo credentials"); they stay in NVS from then on;
|
||||||
|
- a phone that was paired with the board before must forget `micrOBU-XXXX` in
|
||||||
|
Android's Bluetooth settings and pair again (passkey 123456).
|
||||||
|
|
||||||
|
Erasing NVS later (e.g. after a partition change) has the same effects.
|
||||||
|
|
||||||
|
## Going back to the previous firmware
|
||||||
|
|
||||||
|
The previous, unsigned C firmware (frame types 0x01-0x05, CAM over USB only)
|
||||||
|
can come back two ways. The app detects it and falls back to its protocol.
|
||||||
|
|
||||||
|
- **Image:** `firmware-backups/` in the repository root (gitignored, on the lab
|
||||||
|
laptop only) holds a full-flash image of the COM3 board as it was before the
|
||||||
|
port, with the esptool command in its README.txt. It restores the old
|
||||||
|
partition table and NVS too.
|
||||||
|
- **Source:** commit `7285fa1` is the last one before the port. Check it out in
|
||||||
|
a separate worktree and build it with the receiver firmware's IDF 6.1
|
||||||
|
(`its-g5-receiver-firmware\esp-idf\export.ps1`). Erase the NVS range as above
|
||||||
|
first, then `flash` (full, since the partition table differs); erasing after
|
||||||
|
flashing would wipe the start of the old app, which sits at 0x10000.
|
||||||
|
|
||||||
## If the build fails
|
## If the build fails
|
||||||
|
|
||||||
@@ -50,9 +108,9 @@ Ports). `monitor` opens the serial console after flashing — `Ctrl+]` to exit.
|
|||||||
The board has two USB-C ports — use the right one:
|
The board has two USB-C ports — use the right one:
|
||||||
|
|
||||||
- **Native USB-C port** (labeled for JTAG/native USB, up to 12 Mbps) — this
|
- **Native USB-C port** (labeled for JTAG/native USB, up to 12 Mbps) — this
|
||||||
is where the phone plugs in via USB-OTG. The CAM serial link
|
is where the phone plugs in via USB-OTG. The station link
|
||||||
(`serial_link.c`) runs over the ESP32-C5's native USB Serial/JTAG
|
(`serial_link.cpp`: station-link messages as frame type 0x10 in the 0xAA55
|
||||||
peripheral on this port, enumerating as a CDC-ACM device under Espressif's
|
framing) runs over the ESP32-C5's native USB Serial/JTAG peripheral on this port, enumerating as a CDC-ACM device under Espressif's
|
||||||
VID/PID (0x303A/0x1001).
|
VID/PID (0x303A/0x1001).
|
||||||
- **UART-bridge port** (labeled for flashing) — this is what you use for
|
- **UART-bridge port** (labeled for flashing) — this is what you use for
|
||||||
`idf.py flash monitor` from your PC. Leave the phone unplugged from this
|
`idf.py flash monitor` from your PC. Leave the phone unplugged from this
|
||||||
@@ -69,10 +127,10 @@ at all — that isolates a bad/charge-only OTG cable from an app-side issue.
|
|||||||
|
|
||||||
Work down this list — each step isolates the layer below it.
|
Work down this list — each step isolates the layer below it.
|
||||||
|
|
||||||
1. **Flash and install together.** `SERIAL_LINK_MAX_PAYLOAD` is 512 on both sides.
|
1. **Flash and install together.** The app detects the firmware generation by
|
||||||
A phone at 512 talking to firmware still at 160 (or vice versa) silently
|
its heartbeat and speaks either protocol, but only an app from 2026-09-23 on
|
||||||
rejects every large frame at the `length exceeds max, resync` branch. Never
|
knows the station-link protocol; messages are at most 512 octets on both
|
||||||
update one side alone.
|
sides.
|
||||||
2. **Does Android see the device at all?** Plug the phone into the **native**
|
2. **Does Android see the device at all?** Plug the phone into the **native**
|
||||||
USB-C port, hit Connect, and read logcat for `UsbSerialTransport`. It logs
|
USB-C port, hit Connect, and read logcat for `UsbSerialTransport`. It logs
|
||||||
every attached device *and* each device's interfaces. Empty list = cable /
|
every attached device *and* each device's interfaces. Empty list = cable /
|
||||||
@@ -81,10 +139,12 @@ Work down this list — each step isolates the layer below it.
|
|||||||
composite device — expect CDC control (class 2) + CDC data (class 10) +
|
composite device — expect CDC control (class 2) + CDC data (class 10) +
|
||||||
vendor-specific JTAG (class 255) in that dump. Compare against the `ports=`
|
vendor-specific JTAG (class 255) in that dump. Compare against the `ports=`
|
||||||
count on the `matched device` line.
|
count on the `matched device` line.
|
||||||
4. **Is the link alive?** The firmware sends a STATUS heartbeat at 1 Hz
|
4. **Is the link alive?** The firmware sends a STATUS at 1 Hz regardless of
|
||||||
regardless of radio traffic, and the app marks the link ERROR after ~3.5 s of
|
radio traffic, and the app marks the link ERROR after ~3.5 s of silence.
|
||||||
silence. Connected-and-staying-connected means device→host actually works.
|
Connected-and-staying-connected means device→host actually works. The board
|
||||||
5. **If it connects but no CAM_RX ever arrives** — suspect DTR. The app now
|
starts its radio only after the app's STATION_CONFIGURE, so nothing is
|
||||||
|
received before Connect.
|
||||||
|
5. **If it connects but no V2X_RX ever arrives** — suspect DTR. The app now
|
||||||
asserts DTR/RTS on open (`openDevice()` in `UsbSerialTransport.kt`), because
|
asserts DTR/RTS on open (`openDevice()` in `UsbSerialTransport.kt`), because
|
||||||
`CdcAcmSerialDriver` doesn't do it by default and the ESP32's USB Serial/JTAG
|
`CdcAcmSerialDriver` doesn't do it by default and the ESP32's USB Serial/JTAG
|
||||||
endpoint may gate TX on the host opening the CDC line. **This is still
|
endpoint may gate TX on the host opening the CDC line. **This is still
|
||||||
@@ -93,14 +153,28 @@ Work down this list — each step isolates the layer below it.
|
|||||||
next to the VID/PID note, so nobody has to guess again.
|
next to the VID/PID note, so nobody has to guess again.
|
||||||
6. **Watch the counters, not just "Sent: N".** The CAM Pinger card shows
|
6. **Watch the counters, not just "Sent: N".** The CAM Pinger card shows
|
||||||
consecutive write failures (phone side) and the firmware's tx-failure /
|
consecutive write failures (phone side) and the firmware's tx-failure /
|
||||||
oversize-drop / CRC-error totals from the heartbeat. A rising `tx fail` means
|
RX-queue-drop / CRC-error totals from the heartbeat; the connection card
|
||||||
CAMs reach the ESP32 but `esp_wifi_80211_tx` rejects them — a radio problem,
|
shows tickets, signed and refused counts. A rising `tx fail` means messages
|
||||||
not a link problem.
|
reach the ESP32 but the radio refuses them — a radio problem, not a link
|
||||||
|
problem. A rising `refused` with signing on usually means no ticket (NVS
|
||||||
|
erased: reconnect so the app provisions again).
|
||||||
|
|
||||||
|
## Connecting over Bluetooth instead
|
||||||
|
|
||||||
|
Settings > Connection > ESP32-C5 > link: Bluetooth, then Connect. The board
|
||||||
|
advertises as `micrOBU-XXXX` (last two bytes of its BT MAC; `micrOBU-4AFA` on
|
||||||
|
COM3), but only while nothing uses its native USB port. Android asks to pair
|
||||||
|
the first time: passkey **123456** (fixed in `simple_ble.cpp`). The bond is kept
|
||||||
|
on both sides; the board keeps one bond, so pairing a second phone or a PC
|
||||||
|
replaces the first. Log lines on the console start with `cits_ble:`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- No `git submodule update` needed here — obu-firmware has no pinned
|
- No `git submodule update` needed here — obu-firmware has no pinned
|
||||||
submodule of its own, unlike its-g5-receiver-firmware.
|
submodule of its own, unlike its-g5-receiver-firmware.
|
||||||
- Don't use the global "ESP-IDF 5.5 PowerShell" shortcut — always export from
|
- Don't use the global "ESP-IDF 5.5 PowerShell" shortcut or the receiver
|
||||||
the receiver-firmware's pinned checkout, since this firmware's undocumented
|
firmware's 6.1 checkout — export from `esp-idf-v6.0.2`, the version the
|
||||||
PHY/driver internals were verified against that specific build.
|
vanetza-idf radio's undocumented driver internals were verified against.
|
||||||
|
- The console (ESP_LOG, boot messages, panics) stays on the UART-bridge port.
|
||||||
|
Opening it resets the board; do that only while nothing else depends on the
|
||||||
|
session.
|
||||||
|
|||||||
+55
-1
@@ -1,4 +1,58 @@
|
|||||||
# OBU transmit firmware - Phase 2 (in progress: HLN-SV DENM beacon)
|
# obu-firmware
|
||||||
|
|
||||||
|
## Since 2026-09-23: signed ITS on vanetza-idf
|
||||||
|
|
||||||
|
This firmware is a port of the colleague's standalone ESP32-C5 VRU station
|
||||||
|
(`microbu-esp32c5/firmware`, in their own repository; only its `external/vanetza-idf` is copied
|
||||||
|
here, to `obu-firmware/external/vanetza-idf`).
|
||||||
|
From it: the vanetza-idf C-ITS stack (BTP, GeoNetworking, the TS 103 097 security entity with
|
||||||
|
credentials in NVS), the station-link protocol v1 (`link_protocol.*`, `link_service.*`) over the
|
||||||
|
native USB port (`serial_link.*`, frame type 0x10 in the same 0xAA55 framing as before) and over BLE
|
||||||
|
GATT (`simple_ble.*`), and the radio adapter (`c5_radio.*`, `otm_tx_custom.c`). Build with ESP-IDF
|
||||||
|
**6.0.2**; see FLASHING.md.
|
||||||
|
|
||||||
|
The phone builds CAM or VAM, configures the station, provisions credentials, sends PoTi and hands
|
||||||
|
each message over as a BTP-DATA.request; the firmware adds GN/BTP, signs with the authorization
|
||||||
|
ticket, and transmits. The phone side is `Esp32Link.kt` in the app.
|
||||||
|
|
||||||
|
Changed or added for this project (search for `MicrOBU:` in the sources):
|
||||||
|
|
||||||
|
- **Reception stays as it was.** vanetza-idf decapsulates with `itsGnSnDecapResultHandling =
|
||||||
|
STRICT`, so it drops unsigned traffic (the bench sim car) and everything signed under a root other
|
||||||
|
than the provisioned demo root (every RSU). Every captured frame therefore also goes through the
|
||||||
|
previous firmware's `gn_unwrap.c` and reaches the phone as link opcode `V2X_RX` (0x85), whose body
|
||||||
|
is exactly the old `SERIAL_MSG_V2X_RX` payload (`Station::forward_raw`). The app's receive side is
|
||||||
|
unchanged.
|
||||||
|
- **Unsigned transmission is still possible.** The colleague's station refuses unsecured requests.
|
||||||
|
Here a request with GN security profile 1 goes out with the previous firmware's `geonet.c` header
|
||||||
|
and the position of the last PoTi (`Station::unsecured_request`); the app's "Sign outgoing
|
||||||
|
messages" setting decides per message.
|
||||||
|
- **Console on UART0.** ESP_LOG stays on the CH343 bridge port (COM3 on the bench); the native port
|
||||||
|
carries only link frames. The colleague's single-port board routes the log into LOG frames there
|
||||||
|
(`CONFIG_MICROBU_LOG_OVER_LINK`, off here).
|
||||||
|
- **BLE pauses while USB is in use** (`CONFIG_MICROBU_BLE_USB_IDLE_MS`, 3 s): BLE and ITS-G5 share
|
||||||
|
the C5's one RF front end. Whether a live BLE connection disturbs 5.9 GHz is still unmeasured
|
||||||
|
(TODO.md).
|
||||||
|
- A serial write no longer stalls the station task when no USB host is present (BLE-only use).
|
||||||
|
- A notification longer than the ATT MTU is dropped instead of silently truncated.
|
||||||
|
- The Wi-Fi RX callback's 2.4 KB capture buffer is static rather than on the driver task's stack
|
||||||
|
(the previous firmware's commit 04b0076 fixed the same risk).
|
||||||
|
- Manual country policy and the TX-power read-back from the previous firmware's radio bring-up.
|
||||||
|
- The activity LED (GPIO27 on the colleague's XIAO board) is off unless configured.
|
||||||
|
|
||||||
|
Credentials: the app ships `assets/demo-chain.vcr`, a throwaway chain generated 2026-09-23 with the
|
||||||
|
colleague's `vidf_issue` (root `6E7D0374FB021901`, AA `B3312F29844299E0`, AT `B80B49387A4C12EB`
|
||||||
|
valid two years, permissions psid 36 SSP `010000` and psid 638 SSP `01`). The colleague's own demo
|
||||||
|
chain only grants psid 638 and so cannot sign CAMs. Not EU-registered: receivers that verify against
|
||||||
|
the EU trust list drop what it signs.
|
||||||
|
|
||||||
|
Time: the signature's generationTime comes from the PoTi timestamp, which follows the app's
|
||||||
|
`ItsTime` convention (UTC-based, no leap seconds). The colleague's VBS adds the 5 leap seconds.
|
||||||
|
Which one is right is the open question documented in `ItsTime.kt`.
|
||||||
|
|
||||||
|
## Earlier notes (Phase 2, superseded)
|
||||||
|
|
||||||
|
### OBU transmit firmware - Phase 2 (in progress: HLN-SV DENM beacon)
|
||||||
|
|
||||||
Started. See `docs/04-transmit-setup.md` in the project root for build/flash
|
Started. See `docs/04-transmit-setup.md` in the project root for build/flash
|
||||||
steps and how to validate this against your own sniffer.
|
steps and how to validate this against your own sniffer.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
dependencies:
|
||||||
|
espressif/esp-boost:
|
||||||
|
component_hash: 45cfa63ade2ad7c489203ab2ddf3f33be50ec9cab752b6eb17a79f06aee8f8f0
|
||||||
|
dependencies:
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=5.3'
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com/
|
||||||
|
type: service
|
||||||
|
version: 0.4.1
|
||||||
|
idf:
|
||||||
|
source:
|
||||||
|
type: idf
|
||||||
|
version: 6.0.2
|
||||||
|
direct_dependencies:
|
||||||
|
- espressif/esp-boost
|
||||||
|
- idf
|
||||||
|
manifest_hash: 3fca1283d556ade5b08c63857e23cb3b08582f1d1c24bb7c1d5e374f438415d7
|
||||||
|
target: esp32c5
|
||||||
|
version: 3.0.0
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
build
|
||||||
|
.cache/
|
||||||
|
build-*/
|
||||||
|
build-*.log
|
||||||
|
sdkconfig
|
||||||
|
sdkconfig.old
|
||||||
|
sdkconfig.*.old
|
||||||
|
managed_components/
|
||||||
|
dependencies.lock
|
||||||
|
__pycache__/
|
||||||
|
conanfile.pyc
|
||||||
|
doxygen/html/
|
||||||
|
www/
|
||||||
|
build.asn1/
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
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")
|
||||||
+2983
File diff suppressed because it is too large
Load Diff
+1
@@ -0,0 +1 @@
|
|||||||
|
rsource "ports/esp_idf/Kconfig"
|
||||||
+853
@@ -0,0 +1,853 @@
|
|||||||
|
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
@@ -0,0 +1,26 @@
|
|||||||
|
# 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
@@ -0,0 +1,153 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*************************************************************************************
|
||||||
|
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
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- @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
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
-- @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
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- @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
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- @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
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- @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
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- @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
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 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
@@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*************************************************************************************
|
||||||
|
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
|
||||||
Vendored
+106
@@ -0,0 +1,106 @@
|
|||||||
|
/*************************************************************************************
|
||||||
|
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
|
||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
/*************************************************************************************
|
||||||
|
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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
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
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
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
|
||||||
Vendored
+63
@@ -0,0 +1,63 @@
|
|||||||
|
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
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
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
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
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
|
||||||
+229
@@ -0,0 +1,229 @@
|
|||||||
|
-- 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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user