0
0
Fork 0

Compare commits

...

53 commits

Author SHA1 Message Date
Jannik Beyerstedt
666499c240 Move core ESP32 V2X to own repo 2026-07-31 16:06:20 +02:00
2db0dc0116 gnss: Add support for ublox GNSS 2026-07-31 13:20:21 +02:00
ac6a3e398e Hardware: Add PCB layout 2026-07-31 13:20:13 +02:00
Jannik Beyerstedt
71161d1d99 Add scripts to run/ flash the supported feature-sets 2026-07-31 12:13:54 +02:00
Jannik Beyerstedt
2c50668e7e 802.11-TX: Reduce transmit power, add notes about legal requirements 2026-07-31 12:04:46 +02:00
Jannik Beyerstedt
4d7bef58a1 Implement UART I/O protocol 2026-07-27 13:24:24 +02:00
Jannik Beyerstedt
ba71e718df proto/PositionState: Add time for geonetworking 2026-07-27 12:41:51 +02:00
6604670490 main: Add UART I/O helpers 2026-07-27 11:03:24 +02:00
Jannik Beyerstedt
7bbb47a844 cam_tx: Fill path history 2026-07-26 21:24:24 +02:00
2379a7eefc Enable V2X transmission, add optional stand-alone CAM transmission 2026-07-24 12:12:11 +02:00
Jannik Beyerstedt
1d4e82335d feat(docs): Explain supported feature combinations, add test script 2026-07-24 11:52:50 +02:00
Jannik Beyerstedt
7668c9bb7b proto/RawMsgTx: Add station type for source position vector 2026-07-24 11:51:00 +02:00
Jannik Beyerstedt
baaee5f6ff logging: Only print to jtag-serial
Otherwise esp-println will use UART0 when no USB-JTAG/Serial is connected.
2026-07-23 10:11:46 +02:00
Jannik Beyerstedt
fc3581d0d1 main: Fix feature flags 2026-07-22 16:15:58 +02:00
Jannik Beyerstedt
21db09ad7f build: Add "io" module for BLE/ UART messages 2026-07-22 15:39:33 +02:00
Jannik Beyerstedt
29a5d86e43 docs: Add BLE and UART protocol specification 2026-07-22 15:14:15 +02:00
58e5d5ec75 v2x: Reduce SPAT rate 2026-07-21 17:31:08 +02:00
e178c740db v2x: drop hopped messages and unsupported message types 2026-07-21 17:30:05 +02:00
f32887df61 applogic: Move to generic position state interface 2026-07-20 22:38:33 +02:00
Jannik Beyerstedt
055b03850b wifi: Move setup to radio module 2026-07-20 13:26:00 +02:00
Jannik Beyerstedt
d7eeb67538 screen: Move setup to module 2026-07-20 11:36:05 +02:00
3ae65edcbe cache: Add method to update or initialize items 2026-07-19 17:25:05 +02:00
044a735778 radio: Enable TX of non-QoS frames 2026-07-19 17:16:06 +02:00
fc0c9b61da screen, spat: Add more information and update indicator 2026-07-19 17:02:47 +02:00
c1315f10aa spat: Process SPATEMs for all intersections in cache
Otherwise it can take a while until SPAT data is available after the intersection was "locked".
2026-07-17 13:00:46 +00:00
cc31d6226e spat: ensure that "our" intersection is kept in cache
Sometimes we may not receive V2X data for longer than the TLC cache time.
This leads to no SPAT updates in the GLOSA algorithm since that uses a cached reference to the intersection instead of querying the tlc cache every time.
2026-07-17 13:00:46 +00:00
Jannik Beyerstedt
a62d25e651 chore(denm): Update c-its-parser and use conversions from there 2026-07-16 09:46:14 +02:00
6c9cbbd6e6 applogic: Fix maneuver ordering 2026-06-28 00:35:54 +02:00
f0339f6261 applogic: Fix feature flags 2026-06-28 00:27:24 +02:00
c6f9aea38c screen: Fix feature flags to make tests work again 2026-06-28 00:25:32 +02:00
dc547b8178 spat: Increase TLC cache lifetime 2026-06-20 23:24:08 +02:00
26a8d0d18a main: Make critical_sections as short as possible 2026-06-20 20:18:21 +02:00
e024251260 screen: clear sceen on first GNSS fix 2026-06-20 20:17:17 +02:00
696bc97ca5 add missing feature flags 2026-06-20 20:03:44 +02:00
1b504b1ee2 spat: fix get_search_angle 2026-06-20 19:54:55 +02:00
c771afe744 spat: Use likely time, if available 2026-06-20 19:54:55 +02:00
712550160f screen: Increase SPI speed 2026-06-20 19:54:55 +02:00
47bb1f1284 screen: Show placeholder when no signal groups available 2026-06-20 19:54:46 +02:00
ff45df8204 spat: Show signal groups without timing information 2026-06-20 19:54:25 +02:00
f732585989 screen: Initialize before GNSS and WiFI 2026-06-19 22:01:42 +02:00
b2ec98da2e spat: Add screen 2026-06-17 23:27:49 +02:00
a7f6440e75 spat: decrease "stop line passed" distance 2026-06-17 23:11:45 +02:00
002f0bb08b gnss: Only process every second fix event
We always get two "fix" events per second right after each other.
So let's drop one of them to not run GLOSA twice for each position update.
2026-06-17 23:08:04 +02:00
57a6b7983d main: Add feature flag for GNSS module 2026-06-15 22:42:15 +02:00
7463adb038 Evaluate remaining red/green time for upcoming intersection 2026-06-15 22:36:35 +02:00
933106b7ea build: run unit tests locally 2026-06-13 12:07:26 +02:00
9a6491c393 denm: Use conversion from c-its-parser 2026-06-13 12:03:12 +02:00
9bf8b8e4de feat: Add some message parsing 2026-05-27 20:55:55 +02:00
919065f377 feat: Add L76K GNSS 2026-05-27 19:40:43 +02:00
c589605407 chore: Add rustfmt config 2026-05-11 22:22:39 +02:00
93f3cce9e4 refactor: Move sniffer to own module 2026-05-11 22:11:27 +02:00
dc309bc095 refactor: Move main.rs
Otherwise adding more modules will let cargo think they are multiple binaries
2026-05-11 22:09:43 +02:00
ae71fb51ef chore: Use embassy runtime 2026-05-11 22:07:49 +02:00
44 changed files with 34610 additions and 183 deletions

View file

@ -1 +1,2 @@
stack-size-threshold = 1024 stack-size-threshold = 1024
doc-valid-idents = ["WiFi", "GeoNetworking", "QoS", ".."]

4
.gitignore vendored
View file

@ -24,3 +24,7 @@ target/
# Ignore .DS_Store file in mac # Ignore .DS_Store file in mac
**/.DS_Store **/.DS_Store
# Ignore KiCad backups and lock files
hardware/c_its-companion/c_its-companion-backups/
*.kicad_pro.lck

1343
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,34 +3,89 @@ edition = "2024"
name = "esp32-c_its-companion" name = "esp32-c_its-companion"
rust-version = "1.88" rust-version = "1.88"
version = "0.1.0" version = "0.1.0"
default-run = "esp32-c_its-companion"
[[bin]] [[bin]]
name = "esp32-c_its-companion" name = "esp32-c_its-companion"
path = "./src/bin/main.rs" path = "./src/main.rs"
[[bin]]
name = "test"
path = "./src/test.rs"
required-features = ["std", "cam"]
[features]
default = ["esp", "spat", "gnss_ubx", "screen"]
# build for ESP (needed to build test for `host-tuple`)
esp = [
"dep:esp-hal",
"dep:esp-rtos",
"dep:esp-bootloader-esp-idf",
"dep:embassy-executor",
"dep:embassy-time",
"dep:embedded-io",
"dep:embedded-io-async",
"dep:esp-alloc",
"dep:esp-backtrace",
"dep:esp-println",
"dep:esp-radio",
"esp32-cits-core/esp32c5",
"esp32-cits-core/log-04",
]
std = ["c-its-parser/std", "esp32-cits-core/std"]
# enable u-blox GNSS module
gnss_ubx = ["_gnss", "_uart", "dep:ublox"]
# enable L67K GNSS module
gnss_l67k = ["_gnss", "dep:embassy_gps"]
# echo CAM data on serial
cam = ["c-its-parser/cam"]
# send CAM autonomously
cam_tx = ["c-its-parser/cam"]
# echo DENM events on serial and BLE
denm = ["c-its-parser/denm"]
# evaluate SPAT information
spat = ["c-its-parser/spatem", "c-its-parser/mapem"]
# enable some debug printing (without enabling debug logging of the ESP RTOS)
spat_debug = []
# enable ST7789 based display
screen = ["dep:mipidsi", "dep:embedded-graphics", "dep:embedded-hal-bus", "dep:profont"]
# enable I/O via UART
uart = ["_uart", "_gnss", "dep:prost"]
# internal feature, that any UART I/O is enabled
_uart = []
# internal feature, that any GNSS source is enabled
_gnss = []
[dependencies] [dependencies]
esp-hal = { version = "~1.1.0", features = ["esp32c5", "log-04", "unstable"] } esp-hal = { version = "~1.1.0", optional = true, features = ["esp32c5", "log-04", "unstable"] }
esp-rtos = { version = "0.3.0", features = [ esp-rtos = { version = "0.3.0", optional = true, features = [
"embassy",
"esp-alloc", "esp-alloc",
"esp-radio", "esp-radio",
"esp32c5", "esp32c5",
"log-04", "log-04",
] } ] }
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32c5", "log-04"] } esp-bootloader-esp-idf = { version = "0.5.0", optional = true, features = ["esp32c5", "log-04"] }
log = "0.4.27" log = "0.4.27"
critical-section = "1.2.0" critical-section = "1.2.0"
embedded-io = "0.7.1" embassy-executor = { version = "0.10.0", optional = true, features = ["log"] }
esp-alloc = "0.10.0" embassy-time = { version = "0.5.0", optional = true, features = ["log"] }
esp-backtrace = { version = "0.19.0", features = [ embedded-io = { version = "0.7.1", optional = true }
embedded-io-async = { version = "0.7.0", optional = true }
esp-alloc = { version = "0.10.0", optional = true }
esp-backtrace = { version = "0.19.0", optional = true, features = [
"esp32c5", "esp32c5",
"panic-handler", "panic-handler",
"println", "println",
] } ] }
esp-println = { version = "0.17.0", features = ["esp32c5", "log-04"] } esp-println = { version = "0.17.0", optional = true, default-features = false, features = ["colors", "critical-section", "jtag-serial", "esp32c5", "log-04"] }
esp-radio = { version = "0.18.0", features = [ esp-radio = { version = "0.18.0", optional = true, features = [
"esp-alloc", "esp-alloc",
"esp32c5", "esp32c5",
"log-04", "log-04",
@ -39,6 +94,33 @@ esp-radio = { version = "0.18.0", features = [
"sniffer", "sniffer",
] } ] }
embassy_gps = { version = "0.1.0", optional = true, default-features = false, features = ["esp", "log-04"], git = "https://github.com/jbeyerstedt/embassy_gps.git", branch = "feat/rework-gpsfix" }
esp32-cits-core = { version = "0.1.0", default-features = false, git = "https://git.hamburg.ccc.de/jtbx/esp32-cits-core", branch = "main" }
embedded-hal-bus = { version = "0.3.0", optional = true }
embedded-graphics = { version = "0.8.2", optional = true }
# needs newer GN with removed bitvec dependency
c-its-parser = { version = "2.4.0", default-features = false, features = [
"time",
"geo",
"libm",
"transport",
] }
chrono = { version = "0.4.44", default-features = false, features = ["alloc"] }
crossbeam-queue = { version = "0.3.12", default-features = false, features = ["alloc"] }
geo-types = { version = "0.7.19", default-features = false }
mipidsi = { version = "0.10.0", optional = true }
num-traits = { version = "0.2.19", default-features = false, features = ["libm"] }
profont = { version = "0.7.0", optional = true }
prost = { version = "0.14", optional = true, default-features = false, features = ["derive"] }
ublox = { version = "0.10", optional = true, default-features = false, features = ["alloc", "ubx_proto27"] }
[dev-dependencies]
assert_float_eq = "1.2.0"
env_logger = "0.11.10"
[build-dependencies]
prost-build = "0.14"
# For fine tuning these settings, please refer to https://doc.rust-lang.org/cargo/reference/profiles.html # For fine tuning these settings, please refer to https://doc.rust-lang.org/cargo/reference/profiles.html
[profile.dev] [profile.dev]

View file

@ -1,13 +1,67 @@
# ESP32-C5 C-ITS Visualizer # ESP32-C5 C-ITS Companion/ Visualizer
Displays different C-ITS receive-only use cases for cheap. Displays different C-ITS receive-only use cases for cheap.
## Hardware Requirements
The code is currently tailored to:
- Seeed XIAO ESP32-C5 (8MB PSRAM)
- U-Blox M10Q GNSS (connected on GPIO12 (RX), GPIO11 (TX), and GPIO25 (reset))
or Seeed XIAO L67K (connected on GPIO12 (RX), GPIO11 (TX), GPIO1 (wakeup) and GPIO25 (reset))
only available with `gnss_ubx` or `gnss_l67k` feature (which is included in `spat` feature)
- Some ST7789 172*320px screen (connected on GPIO8 (SCL/ SCK), GPIO10 (SDA/ MOSI), GPIO7 (reset), GPIO23 (DC), GPIO24 (CS), TODO GPIO0 (BL))
only available with `screen` feature (which is included in `spat` feature)
## Features
### GNSS
Enable u-blox or L67K GNSS module and in main application logic/ state.
### SPAT
Takes the current position and heading from the GNSS receiver and displays the signal phases of the upcoming intersection.
The upcoming intersection is selected when the own position is "inside" some ingress approach of the intersection.
The display will show the current phase and remaining time for all signal groups of the bike-able lanes in the approach.
### DENM
TODO: Not implemented yet!
Shall "publish" the most relevant information of a DENMs via BLE
### CAM
Just prints a line for each received CAM.
### CAM-TX
Generates and transmits a CAM when a GNSS update was received.
Depends on the `gnss` feature.
Note: In germany, using the 5875 to 5915 MHz frequency band is only permitted for C-ITS use cases which implement transmit power reduction measures with at least 30 dB reduction range (or equivalent measures) and have a maximum EIRP of 33 dBm.
This is based on EU regulation 2014/53/EU, so it applies to other EU countries in similar ways.
The relevant harmonized standard is ETSI EN 302 571 which mandates (excerpt of most relevant points):
- a maximum power spectral density of 23 dBm/Mhz EIRP
- transmit power control (TCP) in a range from 3 dBm up to the maximum output power of the equipment
* used for CEN DSRC and HDR DSRC interference mitigation by implementing measures from ETSI TS 102 792
+ Mode A or B: Output power less than 10 dBm EIRP and less than -65 or -45 dBm/MHz EIRP in CEN DSRC band -> **needs to be measured**
+ Mode C: Maximum transmission time of less than 1 ms and at least a few 100 ms between transmission (realistically) -> **can't be ensured with big messages**
+ Mode D: "any" realistically possible transmission time, but at least 500 ms between transmissions for ~5 ITS stations/ 1000ms for 10 ITS stations -> **safe to assume when we only send CAMs at 1 Hz**
+ But modes C and D also need to:
- use protected zone information from CAM messages
- use a protected zone database, e.g. from www.asecap-pzdb.com
* used for dynamic congestion control (DCC)
+ mandatory mechanism described in ETSI EN 302 571, chapter 4.2.10
This is **not** implemented in this application!
**So legally, you're not allowed to send with this.**
### UART
Enable UART I/O protocol according to [docs/uart-protocol.md](./docs/uart-protocol.md).
## Usage ## Usage
### Toolchain Installation ### Toolchain Installation
Install the toolchain, targe and tools like this: Install the toolchain, targe and tools like this:
``` ```shell
rustup toolchain install nightly --component rust-src rustup toolchain install nightly --component rust-src
rustup target add riscv32imac-unknown-none-elf rustup target add riscv32imac-unknown-none-elf
@ -16,11 +70,44 @@ rustup target add riscv32imac-unknown-none-elf
cargo install espflash --locked cargo install espflash --locked
``` ```
(Project was setup using esp-generate with the options `--chip esp32c5 -o esp32c5-wroom-1 -o alloc -o vscode -o log -o unstable-hal -o wifi -o esp-backtrace`.) (Project was setup using esp-generate with the options `--chip esp32c5 -o esp32c5-wroom-1-psram -o alloc -o log -o unstable-hal -o wifi -o esp-backtrace -o embassy`.)
To use the proper rustfmt features, enable the nightly toolchain:
```shell
rustup component add --toolchain nightly rustfmt
```
### Build and Flash ### Build and Flash
In default configuration only signal phases (SPAT) data is displayed for the upcoming intersection.
Additional features are available through feature flags:
- `esp`: Run on ESP (required as feature flag to enable unit tests on host)
- `std`: Run on system with `std` library
- `spat`: Enable signal phase (and intersection map) data
- `spat_debug`: Enable additional debug information for the SPAT/ GLOSA use case
- `denm`: Enable DENMs
- `cam`: Enable CAMs
- `cam_tx`: Enable CAM generation and transmission (needs time and position source)
Officially supported feature combinations are:
- `esp,spat,gnss_ubx,screen` (default features, "standalone")
- `esp,spat,gnss_ubx,screen,cam_tx` ("standalone with CAM")
- `esp,spat,gnss_ubx,screen,cam_tx,cam,denm`
- `esp,uart,cam,denm` ("smartphone companion")
Connect your ESP32-C5 and run: Connect your ESP32-C5 and run:
``` ```
# default configuration
cargo run --release cargo run --release
# for e.g. also enable DENM feature
cargo run --release -F denm
```
### Run Unit Tests on Host
The unit tests in this project are meant to be run on the host:
```
cargo test --target host-tuple --bin test --no-default-features -F std,spat
``` ```

View file

@ -1,9 +1,25 @@
fn main() { use std::env;
fn main() -> std::io::Result<()> {
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
if target_arch == "riscv32" {
linker_be_nice(); linker_be_nice();
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link) // make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
println!("cargo:rustc-link-arg=-Tlinkall.x"); println!("cargo:rustc-link-arg=-Tlinkall.x");
} }
let descriptor_path =
std::path::PathBuf::from(env::var("OUT_DIR").unwrap()).join("proto_descriptor.bin");
prost_build::Config::new()
.file_descriptor_set_path(&descriptor_path)
.compile_well_known_types()
.compile_protos(&["docs/c_its-messages.proto"], &["docs/"])?;
Ok(())
}
fn linker_be_nice() { fn linker_be_nice() {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
if args.len() > 1 { if args.len() > 1 {

53
docs/ble-protocol.md Normal file
View file

@ -0,0 +1,53 @@
# Bluetooth LE Protocol
Some data is made available via BLE so that a smartphone app can be used to display it in other use cases.
## General
The BLE protocol contains some standardized services, but can also transport custom services.
Each service is compromised of a set of characteristics which can be read or clients can even subscribe for changes.
Sadly we can't use any of the standardized service/ profiles to transmit V2X data.
Even notifications about DENMs can't be transmitted since the Alert Notification Profile is only meant for the companion device to query notifications from the phone, but not the other way around.
So we will only use the Device Information Service to announce ourselves and use a custom service for everything else.
## V2X BLE Service
V2X data is exposed via the service UUID `c0b70000-d4f4-4000-ada8-f99a02ee315c` (randomly generated UUID, with byte 3 and 4 set to zero) using the following characteristics:
UUID | Type | Description
------------------------------------- | --------------- | -----------
c0b70001-d4f4-4001-ada8-f99a02ee315c | `PositionState` | Set position from external source
c0b70001-d4f4-4001-ada8-f99a02ee315c | `RawMsgRx` | Raw CAM message (de-duplicated/ rate-limited)
c0b70001-d4f4-4002-ada8-f99a02ee315c | `CamEvent` | Latest received CAM
c0b70001-d4f4-4003-ada8-f99a02ee315c | `RawMsgRx` | Raw DENM message (de-duplicated/ rate-limited)
c0b70001-d4f4-4004-ada8-f99a02ee315c | `DenmEvent` | Latest received DENM
c0b70001-d4f4-4005-ada8-f99a02ee315c | `RawMsgRx` | Raw MAPEM message (de-duplicated/ rate-limited)
c0b70001-d4f4-4006-ada8-f99a02ee315c | `RawMsgRx` | Raw SPATEM message (de-duplicated/ rate-limited)
## Message Format
Messages are encoded according to the [Protocol Buffers](https://protobuf.dev/) specification since it provides decent binary size and has parser libraries for many programming languages.
All data types are defined in one `.proto` file: [c_its-messages.proto](./c_its-messages.proto).
### Position State Input
When there's no GNSS receiver connected to the ESP, position, heading and speed information can be set using a `PositionState` message (see [c_its-messages.proto](./c_its-messages.proto)).
### DENM Event
Each received DENM will be "published" as a `DenmEvent` message (see [c_its-messages.proto](./c_its-messages.proto)).
Repetitions of the same message (e.g. no changes to the validity timeframe and location) may be dropped by the BLE server.
### CAM Event
Each received (low-frequency) CAM will be "published" as a `CamEvent` message (see [c_its-messages.proto](./c_its-messages.proto)).
### Raw Received Message
Received V2X messages will be "published" as a `RawMsgRx` message (see [c_its-messages.proto](./c_its-messages.proto)).
Repetitions of the same message should be dropped by the BLE server.
Consecutive messages with no relevant changes may be dropped by the BLE server while keeping some minimal publishing rate, e.g.:
- only publish MAPEMs every 10 seconds (since content doesn't change, but client may be restarted after first MAPEM from an intersection was received)
- drop high-frequency CAMs (only publish "full" CAMs which include the low-frequency container)

122
docs/c_its-messages.proto Normal file
View file

@ -0,0 +1,122 @@
syntax = "proto2";
package c_its_io;
// ------------------------------
// Data Types
// ------------------------------
message Position {
required float latitude_deg = 1;
required float longitude_deg = 2;
}
enum TrafficDir {
// for all directions of traffic
ALL_DIRECTIONS = 0;
// for the direction of traffic according to the reference direction, and the portion of traffic upstream of the reference position
UPSTREAM = 1;
// for the direction of traffic according to the reference direction, and the portion of traffic downstream of the reference position
DOWNSTREAM = 2;
// for the direction of traffic opposite to the reference direction
OPPOSITE = 3;
}
message VehicleContainer {
optional uint32 heading_deg = 1; // vehicle heading in degrees, if not "unavailable"
optional uint32 speed = 2; // vehicle speed in 1/100 m/s, if not "unavailable"
optional uint32 vehicle_length_dm = 3; // vehicle length in 1/10 m, if not "unavailable"
optional uint32 vehicle_width_dm = 4; // vehicle width in 1/10 m, if not "unavailable"
}
// GeoNetworking Destination Area Shape
enum GNAreaShape {
ETSI_AREASHAPE_CIRCLE = 0;
ETSI_AREASHAPE_RECTANGLE = 1;
ETSI_AREASHAPE_ELLIPSIS = 2;
}
// GeoNetworking Destination Area/ Source Position
message ITSPosition {
required int32 latitude = 1; // Latitude in 1/10th of micro degrees
required int32 longitude = 2; // Longitude in 1/10th of micro degrees
}
// GeoNetworking Destination Area
message GNArea {
optional GNAreaShape area = 1;
optional ITSPosition position = 2;
optional int32 distA = 3; // Distance A of the geometric shape in meters
optional int32 distB = 4; // Distance B of the geometric shape in meters, mandatory for rectangle and ellipsis
optional int32 angle = 5; // Angle of the geometric shape in degrees, mandatory for rectangle and ellipsis
}
// GeoNetworking Transport
enum GNTransport {
GN_TRANSPORT_GAC = 3; // Geographically-Scoped Anycast
GN_TRANSPORT_GBC = 4; // Geographically-Scoped broadcast
GN_TRANSPORT_TSB = 5; // Topologically-scoped broadcast
GN_TRANSPORT_SHB = 7; // Single hop broadcast
}
message RawMsgTx {
required bytes payload = 1; // as UPER
required uint32 message_type = 2; // ITS `MessageId` integer value
required GNTransport gnTransport = 3;
required GNArea gnArea = 4;
required uint32 hopLimit = 5;
optional uint32 stationType = 6; // for source position vector
}
// ------------------------------
// Messages
// ------------------------------
message PositionState {
required sfixed64 timestamp_ms = 1; // current UNIX time in milliseconds
required Position position = 2;
optional uint32 heading = 3; // heading in 1/10 deg
optional uint32 speed = 4; // speed in 1/100 m/s
}
message DenmEvent {
required fixed64 timestamp = 1; // DENM reference time as UNIX time
required fixed64 validity_end_ts = 2; // UNIX time when event validity ends (from denm.management.validityDuration)
required fixed32 station_id = 3; // unique ID of the event when combined with `seq_num`
required uint32 seq_num = 4; // unique ID of the event when combined with `station_id`
optional Position event_position = 5; // event location (from denm.management.eventPosition)
optional uint32 event_heading_deg = 6; // event heading (from denm.location.eventPositionHeading)
optional TrafficDir direction = 7; // relevance traffic direction (from denm.management.trafficDirection)
optional uint32 cause_code = 8;
optional uint32 sub_cause_code = 9;
}
message CamEvent {
required fixed32 station_id = 1; // Station ID of the vehicle
required uint32 station_type = 2; // ITS `StationType`
required Position position = 3; // vehicle position
optional uint32 vehicle_role = 4; // ITS `VehicleRole`
optional VehicleContainer vehicle_data = 5; // speed, etc unless CAM contains an RSU container
}
message RawMsgRx {
required uint32 msg_type = 1; // ITS message type (`MessageId` from PDU header)
required bytes payload = 2; // UPER message with Geonetworking and BTP headers
}
message SerialOutputMsg {
oneof payload {
DenmEvent denm = 1;
RawMsgRx rx_msg = 2;
}
}
message SerialInputMsg {
oneof payload {
PositionState position = 1;
RawMsgTx tx_msg = 2;
}
}

20
docs/uart-protocol.md Normal file
View file

@ -0,0 +1,20 @@
# UART Protocol
If enabled using the `TODO` feature flag, all received messages are transmitted on UART using the protocol described in this document.
The serial communication can also be used to send V2X data.
## Message Format
The UART communication is using a custom framing protocol to encapsulate binary messages.
The payload is:
- a `SerialOutputMsg` for transmissions from the ESP to the host or
- a `SerialInputMsg` for transmissions from the host to the ESP
in binary [Proto](https://protobuf.dev/) serialization according to the [c_its-messages.proto](./c_its-messages.proto) definition.
Each transmission:
- starts with a Start of Frame (SOF) sequence: `0x56, 0x32, 0x58, 0x2B` ("V2X+")
- followed by a 16-bit big-endian length field
- followed by the payload
- followed by an End of Frame (EOF) sequence `0x0d, 0x0a`

70
feat-permut.sh Executable file
View file

@ -0,0 +1,70 @@
#!/bin/bash
## Build/ Test/ Clippy all supported feature permutations
set -eu
mode=${1:-help}
wasm_target="--chrome --headless"
extended=${2:-false}
features_base=(
uart,cam,denm
spat,gnss_ubx,screen
spat,gnss_ubx,screen,cam_tx
)
features_more=(
spat,gnss_ubx,screen,cam,denm
spat,gnss_ubx,screen,cam_tx,cam,denm
)
features_test=(
spat,cam,denm,uart
)
runBuild () {
for feat in "${features[@]}"; do
echo "cargo build for $feat ..."
cargo build --release --no-default-features -F esp,$feat
done
}
runTest () {
for feat in "${features_test[@]}"; do
echo "cargo test for $feat ..."
cargo test --target host-tuple --bin test --no-default-features -F std,$feat
done
}
runClippy () {
for feat in "${features[@]}"; do
echo "clippy for $feat ..."
cargo clippy --no-default-features -F esp,$feat -- -Wclippy::pedantic
done
}
if [ $extended == "--more" ]; then
echo "running with extended feature list"
features=( "${features_base[@]}" "${features_more[@]}" )
else
echo "running standard feature list"
features=( "${features_base[@]}" )
fi
case $mode in
build)
runBuild
;;
clippy)
runClippy
;;
test)
runTest
;;
all)
runBuild
runClippy
runTest
;;
*)
echo "Usage: $0 [build | clippy | test | all] [--more]"
esac

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,138 @@
{
"board": {
"active_layer": 0,
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
"hidden_nets": [],
"high_contrast_mode": 0,
"net_color_mode": 1,
"opacity": {
"images": 0.6,
"pads": 1.0,
"shapes": 1.0,
"tracks": 1.0,
"vias": 1.0,
"zones": 0.6
},
"prototype_zone_fills": false,
"selection_filter": {
"dimensions": true,
"footprints": true,
"graphics": true,
"keepouts": true,
"lockedItems": false,
"otherItems": true,
"pads": true,
"text": true,
"tracks": true,
"vias": true,
"zones": true
},
"visible_items": [
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings",
"drc_exclusions",
"locked_item_shadows",
"conflict_shadows",
"shapes",
"board_outline_area",
"ly_points"
],
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
"zone_display_mode": 0
},
"git": {
"integration_disabled": false,
"repo_type": "",
"repo_username": "",
"ssh_key": ""
},
"meta": {
"filename": "c_its-companion.kicad_prl",
"version": 5
},
"net_inspector_panel": {
"col_hidden": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
],
"col_order": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
],
"col_widths": [
156,
141,
103,
71,
103,
103,
103,
74,
103,
103
],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
"filter_by_netclass": true,
"filter_text": "",
"group_by_constraint": false,
"group_by_netclass": false,
"show_time_domain_details": false,
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": 0
},
"open_jobsets": [],
"project": {
"files": []
},
"schematic": {
"hierarchy_collapsed": [],
"selection_filter": {
"graphics": true,
"images": true,
"labels": true,
"lockedItems": false,
"otherItems": true,
"pins": true,
"ruleAreas": true,
"symbols": true,
"text": true,
"wires": true
}
}
}

View file

@ -0,0 +1,744 @@
{
"board": {
"3dviewports": [
{
"name": "ISO-back",
"ww": 1.0,
"wx": 23.768348693847656,
"wy": 1.82621169090271,
"wz": -13.010103225708008,
"xw": 0.0,
"xx": -0.6424655318260193,
"xy": 0.5901128053665161,
"xz": -0.4888812303543091,
"yw": 0.0,
"yx": 0.7663145065307617,
"yy": 0.49455612897872925,
"yz": -0.41009292006492615,
"zw": 0.0,
"zx": -0.00022190232994034886,
"zy": -0.638107419013977,
"zz": -0.7699472904205322
},
{
"name": "ISO-front",
"ww": 1.0,
"wx": 4.608643054962158,
"wy": 18.730253219604492,
"wz": -27.170215606689453,
"xw": 0.0,
"xx": 0.6427879333496094,
"xy": -0.5868241190910339,
"xz": 0.49240362644195557,
"yw": 0.0,
"yx": 0.7660443186759949,
"yy": 0.4924042820930481,
"yz": -0.4131760001182556,
"zw": 0.0,
"zx": -2.1759660739917308e-08,
"zy": 0.6427874565124512,
"zz": 0.7660446763038635
}
],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_barcodes": false,
"apply_defaults_to_fp_dimensions": false,
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.8,
"height": 1.27,
"width": 1.27
},
"silk_line_width": 0.1,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.1,
"silk_text_upright": false,
"zones": {
"border_display_style": 2,
"border_hatch_pitch": 0.5,
"corner_radius": 0.0,
"corner_smoothing": 0,
"fill_mode": 0,
"hatch_gap": 1.5,
"hatch_orientation": 0.0,
"hatch_smoothing_level": 0,
"hatch_smoothing_value": 0.1,
"hatch_thickness": 1.0,
"min_clearance": 0.5,
"min_island_area": 10.0,
"min_thickness": 0.25,
"pad_connection": 1,
"remove_islands": 0,
"thermal_relief_gap": 0.5,
"thermal_relief_spoke_width": 0.5
}
},
"diff_pair_dimensions": [
{
"gap": 0.0,
"via_gap": 0.0,
"width": 0.0
}
],
"drc_exclusions": [],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_field_mismatch": "warning",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_to_hole": "warning",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"missing_tuning_profile": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "error",
"padstack": "warning",
"pth_inside_courtyard": "error",
"shorting_items": "error",
"silk_edge_clearance": "warning",
"silk_over_copper": "warning",
"silk_overlap": "warning",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "error",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_not_centered_on_via": "ignore",
"track_on_post_machined_layer": "error",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"tuning_profile_track_geometries": "ignore",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.0,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.5,
"min_groove_width": 0.0,
"min_hole_clearance": 0.25,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.3,
"min_track_width": 0.2,
"min_via_annular_width": 0.1,
"min_via_diameter": 0.5,
"solder_mask_to_copper_clearance": 0.005,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [
0.0
],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [
{
"diameter": 0.0,
"drill": 0.0
}
],
"zones_allow_external_fillets": false
},
"ipc2581": {
"bom_rev": "",
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": "",
"sch_revision": "v0.1"
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"component_class_settings": {
"assignments": [],
"meta": {
"version": 0
},
"sheet_component_classes": {
"enabled": false
}
},
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"field_name_whitespace": "warning",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"ground_pin_not_ground": "warning",
"hier_label_mismatch": "error",
"isolated_pin_label": "warning",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"stacked_pin_name": "warning",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "c_its-companion.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2,
"tuning_profile": "",
"via_diameter": 0.6,
"via_drill": 0.3,
"wire_width": 6
},
{
"name": "Power",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 0,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.5,
"tuning_profile": ""
}
],
"meta": {
"version": 5
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": [
{
"netclass": "Power",
"pattern": "GND"
},
{
"netclass": "Power",
"pattern": "+3V3"
},
{
"netclass": "Power",
"pattern": "/BAT*"
},
{
"netclass": "Power",
"pattern": "/V_*"
}
]
},
"pcbnew": {
"last_paths": {
"idf": "",
"netlist": "",
"plot": "",
"specctra_dsn": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"annotation": {
"method": 0,
"sort_order": 0
},
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "Default Editing",
"sort_asc": true,
"sort_field": "Reference"
},
"bus_aliases": {},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"hop_over_size_choice": 0,
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"page_layout_descr_file": "",
"plot_directory": "",
"reuse_designators": true,
"subpart_first_id": 65,
"subpart_id_separator": 0,
"top_level_sheets": [
{
"filename": "c_its-companion.kicad_sch",
"name": "Root",
"uuid": "9ad468b0-d7e4-4d4a-b652-c4a81a952e30"
}
],
"used_designators": "C1,#PWR1-10,J1-2,SW1,TP1,U1-2,#FLG1-3",
"variants": []
},
"sheets": [
[
"9ad468b0-d7e4-4d4a-b652-c4a81a952e30",
"Root"
]
],
"text_variables": {},
"tuning_profiles": {
"meta": {
"version": 0
},
"tuning_profiles_impedance_geometric": []
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
{"ARCHIVE_NAME": "", "EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false, "OPEN BROWSER": true, "BACKUP_OPT": true}

View file

@ -0,0 +1,4 @@
(fp_lib_table
(version 7)
(lib (name "library") (type "KiCad") (uri "${KIPRJMOD}/library") (options "") (descr ""))
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,542 @@
(footprint "XIAO-ESP32-C5-DIP"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(property "Reference" "REF**"
(at -0.275 -10.275 0)
(unlocked yes)
(layer "F.SilkS")
(uuid "7bab359d-5e5b-4dff-9db4-e7c8562ce635")
(effects
(font
(size 0.635 0.635)
(thickness 0.1016)
)
)
)
(property "Value" "XIAO-ESP32-C5-DIP"
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(uuid "79cabf23-4ebc-4d37-9172-bb2cf4ea975f")
(effects
(font
(size 0.635 0.635)
(thickness 0.1016)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "663dc686-104e-47e1-9669-e1228477fa6f")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "a25b2a4a-94ce-41bd-8341-b98204aa9142")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(attr smd)
(fp_line
(start -10.414 -6.985)
(end -10.414 6.985)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "e213e2f5-659f-4cdb-b390-b284746621aa")
)
(fp_line
(start -8.509 -8.89)
(end 8.636 -8.89)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "f58afa0e-63f9-4096-9d17-7cf8e5de7759")
)
(fp_line
(start -8.509 8.89)
(end 8.636 8.89)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "9c6409b2-5f1b-4299-85bf-d07c96f13a2a")
)
(fp_line
(start 10.541 -4.495)
(end 11.551272 -4.491272)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "4c45fdff-98df-401f-bf6d-768af24b67a9")
)
(fp_line
(start 10.541 6.985)
(end 10.541 -6.985)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "eb7ba2c6-3729-4940-9c6b-4ac369cf3075")
)
(fp_line
(start 10.541 6.985)
(end 10.541 -6.985)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "6f680559-bbef-46de-9ef5-efda7c2411e9")
)
(fp_line
(start 11.551 4.504)
(end 10.541 4.504)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "eddbec16-11c2-42ad-90d6-191cfc991c10")
)
(fp_line
(start 12.051 -3.991272)
(end 12.051 4.004)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "e973f8d1-81df-437d-bffe-4f734e1ac18e")
)
(fp_arc
(start -10.414 -6.985)
(mid -9.856038 -8.332038)
(end -8.509 -8.89)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "88e5d6ea-6b70-4830-9817-980fd2b32f79")
)
(fp_arc
(start -8.509 8.89)
(mid -9.856038 8.332038)
(end -10.414 6.985)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "f108f05c-d5f6-4de1-bc6f-4c31ef53e967")
)
(fp_arc
(start 8.636 -8.89)
(mid 9.983038 -8.332038)
(end 10.541 -6.985)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "c6870bc4-897a-485a-b951-3feda1bdd32e")
)
(fp_arc
(start 10.541 6.985)
(mid 9.983038 8.332038)
(end 8.636 8.89)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "e1d8f660-1495-43db-8a6b-7fdc7dc42e35")
)
(fp_arc
(start 11.551272 -4.491272)
(mid 11.904644 -4.344724)
(end 12.051 -3.991272)
(stroke
(width 0.127)
(type default)
)
(layer "F.SilkS")
(uuid "a9b2ba2a-ad25-4191-9070-ebc0333a9101")
)
(fp_arc
(start 12.051 4.004)
(mid 11.904553 4.357553)
(end 11.551 4.504)
(stroke
(width 0.127)
(type default)
)
(layer "F.SilkS")
(uuid "45e349ac-e285-4bed-a05b-ae5a80336af0")
)
(fp_circle
(center 9.37 -7.857)
(end 9.624 -7.857)
(stroke
(width 0.5)
(type solid)
)
(fill yes)
(layer "F.SilkS")
(uuid "6839324b-c75a-49ba-8c0c-16e2191166eb")
)
(fp_circle
(center 10.25 -9)
(end 10.504 -9)
(stroke
(width 0.5)
(type solid)
)
(fill yes)
(layer "F.SilkS")
(uuid "6e5f4857-2ad9-4493-8234-6091a6821232")
)
(fp_rect
(start -10.425 -8.9)
(end 10.55 8.9)
(stroke
(width 0.05)
(type default)
)
(fill no)
(layer "F.CrtYd")
(uuid "b53697bc-d105-40e2-af66-708b16a7f26c")
)
(fp_rect
(start -10.425 -8.9)
(end 10.55 8.9)
(stroke
(width 0.1)
(type default)
)
(fill no)
(layer "F.Fab")
(uuid "a45a327e-f036-4877-8d89-39f4f4b0c715")
)
(fp_circle
(center 9.376 -7.854)
(end 9.63 -7.854)
(stroke
(width 0.5)
(type solid)
)
(fill yes)
(layer "F.Fab")
(uuid "12a1812e-f866-498e-9d6e-50e427087f34")
)
(pad "1" smd roundrect
(at 7.62 -8.455 90)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "82bbfb55-8baa-4218-ac89-ab07d82ef7f8")
)
(pad "1" thru_hole circle
(at 7.62 -7.62 90)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "f2fffd4f-460c-4b4d-b954-811e935ed1d6")
)
(pad "2" smd roundrect
(at 5.08 -8.455 90)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "18ef9914-2d15-494c-9020-2334abcc1460")
)
(pad "2" thru_hole circle
(at 5.08 -7.62 90)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "4732cc3f-e641-4870-a9b3-ae13729c7bba")
)
(pad "3" smd roundrect
(at 2.54 -8.455 90)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "15338a55-8514-4218-a2c1-2cae003104c3")
)
(pad "3" thru_hole circle
(at 2.54 -7.62 90)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "0cef5e55-d8ff-4394-9cc1-cc318608b0d9")
)
(pad "4" smd roundrect
(at 0 -8.455 90)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "191a259d-5773-4a80-91de-baa69611fab8")
)
(pad "4" thru_hole circle
(at 0 -7.62 90)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "200b8da4-9395-48a5-b7ac-92a6a147c215")
)
(pad "5" smd roundrect
(at -2.54 -8.455 90)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "52012467-1c46-4a8e-aa82-6f6ce69bd2ca")
)
(pad "5" thru_hole circle
(at -2.54 -7.62 90)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "2b50ccce-3634-47d1-bf7d-a3db4f3f9a9f")
)
(pad "6" smd roundrect
(at -5.08 -8.455 90)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "04aeaf95-142a-40f6-97a3-04453fc50824")
)
(pad "6" thru_hole circle
(at -5.08 -7.62 90)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "7636de9d-d8e5-49c9-9fff-0cd76515690d")
)
(pad "7" smd roundrect
(at -7.62 -8.455 90)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "e997fd0b-233a-4656-afad-cb2f8b9d87c9")
)
(pad "7" thru_hole circle
(at -7.62 -7.62 90)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "9d2c0d0c-05dd-477e-8384-b14eec83fc2e")
)
(pad "8" thru_hole circle
(at -7.62 7.62 270)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "1e50c66f-49b8-4b03-9889-8cc6ca45ad8e")
)
(pad "8" smd roundrect
(at -7.62 8.455 270)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "e7b72803-b5f0-433c-8313-83247355fb43")
)
(pad "9" thru_hole circle
(at -5.08 7.62 270)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "808bf9dc-1c26-4a77-93c9-e4c6d5eccbb2")
)
(pad "9" smd roundrect
(at -5.08 8.455 270)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "532de3b6-bd30-47c5-b70e-0bca6abbd997")
)
(pad "10" thru_hole circle
(at -2.54 7.62 270)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "a792a2ac-537b-4136-8f92-b81f7ce8f753")
)
(pad "10" smd roundrect
(at -2.54 8.455 270)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "6e3f5c75-6143-468e-bb07-d89020c03f59")
)
(pad "11" thru_hole circle
(at 0 7.62 270)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "fb78046a-9b91-4893-b010-827957cc07be")
)
(pad "11" smd roundrect
(at 0 8.455 270)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "5249f425-bece-432d-8076-28e1b8b386e3")
)
(pad "12" thru_hole circle
(at 2.54 7.62 270)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "0b8ccc97-5100-4dcf-8500-e99789ee0b4c")
)
(pad "12" smd roundrect
(at 2.54 8.455 270)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "82f1bbd1-708b-4d2d-9113-7ba3c2313670")
)
(pad "13" thru_hole circle
(at 5.08 7.62 270)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "03882061-01af-484b-b620-9e92a3daaf68")
)
(pad "13" smd roundrect
(at 5.08 8.455 270)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "954f51d1-3d75-42fb-a02a-7389b30718d9")
)
(pad "14" thru_hole circle
(at 7.62 7.62 270)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "c6d1af47-da13-4409-9ab1-55d358cc57eb")
)
(pad "14" smd roundrect
(at 7.62 8.455 270)
(size 2.432 1.524)
(layers "F.Cu" "F.Mask")
(roundrect_rratio 0.1)
(thermal_bridge_angle 45)
(uuid "6b725fd4-d6f0-46bf-b3e0-7ec07cc05622")
)
(embedded_fonts no)
(model "${AMZPATH}/3dmodels/AMz3D.3dshapes/XIAO-nRF52840 v15.step"
(hide yes)
(offset
(xyz -1.778 -6.096 0.381)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz -90 0 0)
)
)
(model "${AMZPATH}/3dmodels/AMz3D.3dshapes/Seeeduino XIAO SAMD21 v4.step"
(offset
(xyz 0 0 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz 0 0 0)
)
)
(model "${AMZPATH}/3dmodels/AMz3D.3dshapes/Seeed Studio XIAO RP2040 v26.step"
(hide yes)
(offset
(xyz 0 0 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz 0 0 90)
)
)
(model "${AMZPATH}/3dmodels/AMz3D.3dshapes/Seeed Studio XIAO-ESP32-C3.step"
(hide yes)
(offset
(xyz -1.7018 -6.096 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz -90 0 0)
)
)
(model "${AMZPATH}/3dmodels/AMz3D.3dshapes/XIAO-ESP32S3 v2.step"
(hide yes)
(offset
(xyz -1.778 -6.1722 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz -90 0 0)
)
)
)

View file

@ -0,0 +1,558 @@
(footprint "XIAO-ESP32-C5-SMD-DIP"
(version 20260206)
(generator "pcbnew")
(generator_version "10.0")
(layer "F.Cu")
(property "Reference" "REF**"
(at 7.312 -21.336 0)
(layer "F.SilkS")
(uuid "95a6896c-2eab-4f28-adc5-7fd64550efd1")
(effects
(font
(size 0.78232 0.78232)
(thickness 0.10668)
)
(justify left bottom)
)
)
(property "Value" "Val**"
(at 8.89 -10.795 0)
(layer "F.Fab")
(uuid "ead571aa-8f25-4413-9dd8-3ecea3d8b1b5")
(effects
(font
(size 0.536448 0.536448)
(thickness 0.073152)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "e0e614f2-f99b-42a7-87d5-6d40f74c047a")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "074d8848-2ba7-4146-8c49-a0cf65e2aa82")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(duplicate_pad_numbers_are_jumpers no)
(fp_line
(start 0 -1.987)
(end 0 -19.132)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "e72c3c07-5bc8-4f5f-97cd-36412308e250")
)
(fp_line
(start 1.905 -0.082)
(end 15.875 -0.082)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "e3e0ab25-f18a-4068-92e8-92a17b4caf05")
)
(fp_line
(start 4.595 -21.037)
(end 4.598728 -22.047272)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "4838598f-afa6-445e-b237-7544ae7e9968")
)
(fp_line
(start 5.098728 -22.547)
(end 13.094 -22.547)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "101b5417-f570-447c-b9f6-42639e5eff3c")
)
(fp_line
(start 13.594 -22.047)
(end 13.594 -21.037)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "37ee6532-c85a-4899-82db-921e02cce4af")
)
(fp_line
(start 15.875 -21.037)
(end 1.905 -21.037)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "f94b2798-d11f-47d7-8f2c-02478417b6f1")
)
(fp_line
(start 17.78 -1.987)
(end 17.78 -19.132)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "8c3282cc-86c2-4cf3-a698-856916df5f40")
)
(fp_arc
(start 0 -19.136)
(mid 0.557962 -20.483038)
(end 1.905 -21.041)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "2a1b788b-cdcf-4a49-acac-d187cf344ffc")
)
(fp_arc
(start 1.905 -0.082)
(mid 0.557962 -0.639962)
(end 0 -1.987)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "677c2a8e-0212-4651-99b4-af42d29d0bd1")
)
(fp_arc
(start 4.598728 -22.047272)
(mid 4.745276 -22.400644)
(end 5.098728 -22.547)
(stroke
(width 0.127)
(type default)
)
(layer "F.SilkS")
(uuid "b38fc005-51b4-4609-b7e3-78320e82da0d")
)
(fp_arc
(start 13.094 -22.547)
(mid 13.447553 -22.400553)
(end 13.594 -22.047)
(stroke
(width 0.127)
(type default)
)
(layer "F.SilkS")
(uuid "46a43512-01fe-440e-b1e8-31a50057cbf8")
)
(fp_arc
(start 15.875 -21.037)
(mid 17.222038 -20.479038)
(end 17.78 -19.132)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "f77dffb2-d331-43db-9018-069e9fa47c57")
)
(fp_arc
(start 17.78 -1.987)
(mid 17.222038 -0.639962)
(end 15.875 -0.082)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "535b0a70-da0d-4557-a5dd-d25a51ee5468")
)
(fp_circle
(center -0.21 -20.621)
(end -0.21 -20.875)
(stroke
(width 0.5)
(type solid)
)
(fill yes)
(layer "F.SilkS")
(uuid "c4f8c64e-4c5d-41c1-8b14-aca8fcf77968")
)
(fp_circle
(center 1.064 -19.904)
(end 1.064 -20.158)
(stroke
(width 0.5)
(type solid)
)
(fill yes)
(layer "F.SilkS")
(uuid "163ea809-1cd6-43c9-968e-14c093715603")
)
(fp_rect
(start 17.8 0)
(end 0 -21)
(stroke
(width 0.05)
(type default)
)
(fill no)
(layer "F.CrtYd")
(uuid "477d102d-f2d0-4f67-ad93-9f60da51bd76")
)
(fp_rect
(start 0 -21)
(end 17.8 0)
(stroke
(width 0.1)
(type default)
)
(fill no)
(layer "F.Fab")
(uuid "e7107e2f-485b-49f6-9f5d-2537ab0ef458")
)
(fp_circle
(center 1.084 -19.9)
(end 1.084 -20.154)
(stroke
(width 0.5)
(type solid)
)
(fill yes)
(layer "F.Fab")
(uuid "089122fe-193e-4cef-a9b8-54d37cb7fa99")
)
(pad "1" smd roundrect
(at 0.835 -18.12)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "351f5f05-8bc3-4b53-9daf-3207bd71876b")
)
(pad "1" thru_hole circle
(at 1.26 -18.14 180)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "6a16c3b2-8d4e-4efe-a84e-dbc418c7fe79")
)
(pad "2" smd roundrect
(at 0.835 -15.58)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "cfdee4aa-d600-484f-9a5c-2062a00bde1d")
)
(pad "2" thru_hole circle
(at 1.26 -15.6 180)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "69193d02-4e31-4519-a90d-949c3f48e92c")
)
(pad "3" smd roundrect
(at 0.835 -13.04)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "52a69e1c-edab-4079-a491-6bcb12907f61")
)
(pad "3" thru_hole circle
(at 1.26 -13.06 180)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "a3aed707-7e8b-4b1b-ba96-06cfb9154fcb")
)
(pad "4" smd roundrect
(at 0.835 -10.5)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "1e864c46-432c-460c-af69-13a1c9d0fd2f")
)
(pad "4" thru_hole circle
(at 1.26 -10.52 180)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "eee65312-1942-42a1-a13a-2a28a114ec95")
)
(pad "5" smd roundrect
(at 0.835 -7.96)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "da854e17-c3b3-4a83-9d47-a379e4d7efa5")
)
(pad "5" thru_hole circle
(at 1.26 -7.98 180)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "febdd29f-f361-4b8e-b2b5-7502d3e5fdea")
)
(pad "6" smd roundrect
(at 0.835 -5.42)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "3fde7f90-983d-44c1-939e-47d36175373f")
)
(pad "6" thru_hole circle
(at 1.26 -5.44 180)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "323ef0a5-3407-4ac3-b76f-8ac19e044d1e")
)
(pad "7" smd roundrect
(at 0.835 -2.88)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "ebfd6bef-ea17-4635-877b-58b57c784dec")
)
(pad "7" thru_hole circle
(at 1.26 -2.9 180)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "2b179783-65b0-4d7c-850b-74c9fa3a7a64")
)
(pad "8" thru_hole circle
(at 16.5 -2.9)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "5cbc8aa6-fdfd-49e6-b018-da1d4ff342aa")
)
(pad "8" smd roundrect
(at 17 -2.88)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "19eb14d0-5713-47b7-86f2-4bccede8ba54")
)
(pad "9" thru_hole circle
(at 16.5 -5.44)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "e7f197b5-1fc8-47f6-9f49-60ffd6fa2335")
)
(pad "9" smd roundrect
(at 17 -5.42)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "4c3b83bc-a000-4a70-887c-6b775bc40ee1")
)
(pad "10" thru_hole circle
(at 16.5 -7.98)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "4df7fd17-71d5-4f8f-a3f2-68c6537a767f")
)
(pad "10" smd roundrect
(at 17 -7.96)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "223d5c33-d2a3-4398-8d3f-544bd6345a86")
)
(pad "11" thru_hole circle
(at 16.5 -10.52)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "dad67976-a604-485a-bb37-2d67ea771b2c")
)
(pad "11" smd roundrect
(at 17 -10.5)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "1f04f1d2-ce82-4256-b122-36f63f189efe")
)
(pad "12" thru_hole circle
(at 16.5 -13.06)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "c55d522b-9f8b-4fa4-afaa-880f3b1c9c5f")
)
(pad "12" smd roundrect
(at 17 -13.04)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "31818d88-bbc0-4c22-b4ed-d6884ef71ebb")
)
(pad "13" thru_hole circle
(at 16.5 -15.6)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "e46165bc-b289-44b0-b7f6-6759bba3d4b8")
)
(pad "13" smd roundrect
(at 17 -15.58)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "3fbf02f8-81da-4fba-bc95-9e47b9abaa05")
)
(pad "14" thru_hole circle
(at 16.5 -18.14)
(size 1.524 1.524)
(drill 0.889)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "71ca3093-fd4e-46c3-9980-6d986d711719")
)
(pad "14" smd roundrect
(at 17 -18.12)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "ce7a6ff3-5c3a-4077-bf86-beedc24debb3")
)
(pad "15" smd circle
(at 7.6166 -19.1415)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "7273149b-9aea-4a23-9b4e-db1e0d6c76fc")
)
(pad "16" smd circle
(at 10.1566 -19.1415)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "0aef11c3-14ad-4cb5-9b05-95a892988ce8")
)
(pad "17" smd circle
(at 7.6166 -16.6015)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "158a5cbb-af86-4fa6-8d58-6831485085c5")
)
(pad "18" smd circle
(at 10.1566 -16.6015)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "3b7bcd8a-c97c-46f6-9faa-4e525d03d6e8")
)
(pad "19" smd circle
(at 7.6166 -14.0615)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "ad4ba963-211d-4878-a4cb-255a615c14de")
)
(pad "20" smd circle
(at 10.1566 -14.0615)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "8c7e088d-0846-4d7d-9143-5ce3fdb0c4fd")
)
(pad "21" smd circle
(at 7.6166 -11.5215)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "c6bbe198-c8c7-4b83-a36e-6a6fdeb3b94f")
)
(pad "22" smd circle
(at 10.064 -11.5783)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "4f1e574b-3b71-4a23-b765-d779726fd3c6")
)
(pad "23" smd roundrect
(at 7.3086 -4.9683 270)
(size 2.5 1.1)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(thermal_bridge_angle 45)
(uuid "08d345bf-1b32-499f-94bb-44332eb5115d")
)
(pad "24" smd roundrect
(at 9.8486 -4.9683 270)
(size 2.5 1.1)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(thermal_bridge_angle 45)
(uuid "8b3ca1bb-eee0-4c3d-8f3b-eefea4a68bbb")
)
(pad "25" smd roundrect
(at 9.949 -8.412 270)
(size 3.1 1.9)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(thermal_bridge_angle 45)
(uuid "45570478-e499-45ab-bb97-539cabc4e177")
)
(embedded_fonts no)
)

View file

@ -0,0 +1,445 @@
(footprint "XIAO-ESP32-C5-SMD"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(property "Reference" "REF**"
(at 7.312 -21.336 0)
(layer "F.SilkS")
(uuid "a9f5f5dd-597c-45dd-b31d-a35c3963eed2")
(effects
(font
(size 0.78232 0.78232)
(thickness 0.10668)
)
(justify left bottom)
)
)
(property "Value" "Val**"
(at 8.89 -10.795 0)
(layer "F.Fab")
(uuid "2d66aca7-e0b3-44df-838e-be285890792c")
(effects
(font
(size 0.536448 0.536448)
(thickness 0.073152)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "5173b428-b87b-497c-9fd1-0d444c2ced85")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "ea7181c4-fdca-44f1-ad15-d0627f50a68e")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(fp_line
(start 0 -1.987)
(end 0 -19.132)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "df0486b4-d04f-4201-a301-beede6e15862")
)
(fp_line
(start 1.905 -0.082)
(end 15.875 -0.082)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "f54abd8d-62ed-4b05-8e37-f29acdadc0f8")
)
(fp_line
(start 4.595 -21.037)
(end 4.598728 -22.047272)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "fead6daa-b9f4-4bd1-a131-b5d2cd3184de")
)
(fp_line
(start 5.098728 -22.547)
(end 13.094 -22.547)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "b90ae92d-7c1a-4689-bf64-c3528a3ea3ee")
)
(fp_line
(start 13.594 -22.047)
(end 13.594 -21.037)
(stroke
(width 0.127)
(type solid)
)
(layer "F.SilkS")
(uuid "b540fbcc-8e06-44d7-95f6-8b142e4c90fb")
)
(fp_line
(start 15.875 -21.037)
(end 1.905 -21.037)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "2b80542c-5088-4f89-ae32-3400cf894ca1")
)
(fp_line
(start 17.78 -1.987)
(end 17.78 -19.132)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "4cd8add0-8a8c-468d-ba6f-b86787c288af")
)
(fp_arc
(start 0 -19.136)
(mid 0.557962 -20.483038)
(end 1.905 -21.041)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "4d9bf7a5-4ab4-48bb-b8ac-0a5ce4523642")
)
(fp_arc
(start 1.905 -0.082)
(mid 0.557962 -0.639962)
(end 0 -1.987)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "d09661c8-1ad3-40e9-8323-9dabc8f3f98b")
)
(fp_arc
(start 4.598728 -22.047272)
(mid 4.745276 -22.400644)
(end 5.098728 -22.547)
(stroke
(width 0.127)
(type default)
)
(layer "F.SilkS")
(uuid "7a58f637-14d7-4804-b127-f1d029b5d1e6")
)
(fp_arc
(start 13.094 -22.547)
(mid 13.447553 -22.400553)
(end 13.594 -22.047)
(stroke
(width 0.127)
(type default)
)
(layer "F.SilkS")
(uuid "d0bc8663-987f-4e2e-b464-096a547b822f")
)
(fp_arc
(start 15.875 -21.037)
(mid 17.222038 -20.479038)
(end 17.78 -19.132)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "77004ea5-53a4-40ab-b35c-7bdd22107e34")
)
(fp_arc
(start 17.78 -1.987)
(mid 17.222038 -0.639962)
(end 15.875 -0.082)
(stroke
(width 0.1)
(type solid)
)
(layer "F.SilkS")
(uuid "94189dc1-14b5-4f17-b9d3-0163ed99e583")
)
(fp_circle
(center -0.21 -20.621)
(end -0.21 -20.875)
(stroke
(width 0.5)
(type solid)
)
(fill yes)
(layer "F.SilkS")
(uuid "dc606cc5-10bc-4635-81d1-fa27b1e6b4e2")
)
(fp_circle
(center 1.064 -19.904)
(end 1.064 -20.158)
(stroke
(width 0.5)
(type solid)
)
(fill yes)
(layer "F.SilkS")
(uuid "2b5e2829-6d01-441f-9983-fc4a273ff483")
)
(fp_rect
(start 17.8 0)
(end 0 -21)
(stroke
(width 0.05)
(type default)
)
(fill no)
(layer "F.CrtYd")
(uuid "e428eb56-00a2-4249-bb91-d2e5823b2899")
)
(fp_rect
(start 0 -21)
(end 17.8 0)
(stroke
(width 0.1)
(type default)
)
(fill no)
(layer "F.Fab")
(uuid "2133d927-559d-4615-ab92-732772c02d94")
)
(fp_circle
(center 1.084 -19.9)
(end 1.084 -20.154)
(stroke
(width 0.5)
(type solid)
)
(fill yes)
(layer "F.Fab")
(uuid "50e04a65-8ff2-4d1f-be98-2db48101e90a")
)
(pad "1" smd roundrect
(at 0.835 -18.12)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "c68f6b12-dcf5-46b1-b159-2eef9023b0ac")
)
(pad "2" smd roundrect
(at 0.835 -15.58)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "525b09fc-9099-4274-98c3-45f84d0591ab")
)
(pad "3" smd roundrect
(at 0.835 -13.04)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "99cc76a7-0a78-4868-871b-c214ecac5525")
)
(pad "4" smd roundrect
(at 0.835 -10.5)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "8038b67b-b3bf-4a3e-b790-40857963c957")
)
(pad "5" smd roundrect
(at 0.835 -7.96)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "84711cb2-d222-464e-bcd5-f76af75cc612")
)
(pad "6" smd roundrect
(at 0.835 -5.42)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "aabcfc3b-fa72-4e48-95d1-d53474158cc0")
)
(pad "7" smd roundrect
(at 0.835 -2.88)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "3e086061-1f3f-4b1e-9354-920126d40249")
)
(pad "8" smd roundrect
(at 17 -2.88)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "974a7e29-b5d3-475c-b481-2bc14f5b3495")
)
(pad "9" smd roundrect
(at 17 -5.42)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "a553cc0b-22bc-49aa-9fb7-680c812e8abd")
)
(pad "10" smd roundrect
(at 17 -7.96)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "b650936d-c436-4d93-abb7-7957add5cfb1")
)
(pad "11" smd roundrect
(at 17 -10.5)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "177728f1-7b02-41a6-9de4-bdaee546f6d9")
)
(pad "12" smd roundrect
(at 17 -13.04)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "eb6539ef-4d84-4ae5-bc4e-237dae21b9ef")
)
(pad "13" smd roundrect
(at 17 -15.58)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "c8744fbe-93c9-4c54-84bc-435357de679f")
)
(pad "14" smd roundrect
(at 17 -18.12)
(size 2.75 2)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(solder_mask_margin 0.0508)
(thermal_bridge_angle 45)
(uuid "fd579d63-bbef-4609-9252-aad3f944a314")
)
(pad "15" smd circle
(at 7.6166 -19.1415)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "ad890579-1e55-4b71-9d59-080a59598896")
)
(pad "16" smd circle
(at 10.1566 -19.1415)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "25d6fdae-01ac-4ca4-8702-55badc8bcbc8")
)
(pad "17" smd circle
(at 7.6166 -16.6015)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "e73ae295-736f-4acf-8ee1-0c120ebcebea")
)
(pad "18" smd circle
(at 10.1566 -16.6015)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "7adfba87-f861-42a5-bb73-c3f9fac53180")
)
(pad "19" smd circle
(at 7.6166 -14.0615)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "619dc2a5-519d-48ac-9c78-8c438df2a14d")
)
(pad "20" smd circle
(at 10.1566 -14.0615)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "aa8b72d0-17bf-47d4-94b1-df9199638a29")
)
(pad "21" smd circle
(at 7.6166 -11.5215)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "8539b5cc-db2c-4fa4-9ead-b842a1885293")
)
(pad "22" smd circle
(at 10.064 -11.5783)
(size 1.7 1.7)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "313edcfe-ad25-4ea7-b786-d8290dfb479a")
)
(pad "23" smd roundrect
(at 7.3086 -4.9683 270)
(size 2.5 1.1)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(thermal_bridge_angle 45)
(uuid "144d3c3d-6207-433c-9e61-4f75722af865")
)
(pad "24" smd roundrect
(at 9.8486 -4.9683 270)
(size 2.5 1.1)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(thermal_bridge_angle 45)
(uuid "3780a4ef-348c-4241-b04b-2cb4f7127fa2")
)
(pad "25" smd roundrect
(at 9.949 -8.412 270)
(size 3.1 1.9)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.25)
(thermal_bridge_angle 45)
(uuid "00be3957-5a21-4898-8292-59f5f447623f")
)
(embedded_fonts no)
)

View file

@ -0,0 +1,4 @@
(sym_lib_table
(version 7)
(lib (name "Seeed_Studio_XIAO_Series") (type "KiCad") (uri "${KIPRJMOD}/library/Seeed_Studio_XIAO_Series.kicad_sym") (options "") (descr ""))
)

6
run-smartphone.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
## Builds common config for smartphone connected device
## -> only receive CAM and DENMs via UART (TODO: and BLE)
cargo run --release --no-default-features -F esp,cam,denm,uart

6
run-standalone-cam.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
## Builds common config for standalone device with automatic CAM transmission
## -> enable SPAT/ GLOSA with GNSS and a screen + CAM transmission
cargo run --release -F cam_tx

6
run-standalone-no_cam.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
## Builds common config for standalone device
## -> enable SPAT/ GLOSA with GNSS and a screen
cargo run --release

6
rustfmt.toml Normal file
View file

@ -0,0 +1,6 @@
unstable_features = true
format_code_in_doc_comments = true
group_imports = "StdExternalCrate"
imports_granularity = "Module"
imports_layout = "HorizontalVertical"

65
src/applogic/cam.rs Normal file
View file

@ -0,0 +1,65 @@
//! C-ITS CAM Evaluation
use c_its_parser::standards::cam_1_4_1::cam_pdu_descriptions::{self, HighFrequencyContainer};
use log::info;
pub fn handle_cam(etsi: &cam_pdu_descriptions::CAM) {
// only POC output
match &etsi.cam.cam_parameters.high_frequency_container {
HighFrequencyContainer::basicVehicleContainerHighFrequency(obu) => {
let station_id = etsi.header.station_id.0;
let lat_deg = etsi
.cam
.cam_parameters
.basic_container
.reference_position
.latitude
.as_deg();
let lon_deg = etsi
.cam
.cam_parameters
.basic_container
.reference_position
.longitude
.as_deg();
if obu.speed.speed_value.is_unavailable() {
info!(
"Got CAM from {station_id} at {lat_deg:.3}°N, {lon_deg:.3}°E, speed unavailable",
);
} else {
info!(
"Got CAM from {station_id} at {lat_deg:.3}°N, {lon_deg:.3}°E, speed {:.2} km/h",
obu.speed.speed_value.as_kmh()
);
}
}
HighFrequencyContainer::rsuContainerHighFrequency(rsu) => {
info!(
"Got RSU from {} at {:.3}°N, {:.3}°E, {} protected zones",
etsi.header.station_id.0,
etsi.cam
.cam_parameters
.basic_container
.reference_position
.latitude
.as_deg(),
etsi.cam
.cam_parameters
.basic_container
.reference_position
.longitude
.as_deg(),
rsu.protected_communication_zones_rsu
.as_ref()
.map(|d| d.0.len())
.unwrap_or_default()
);
}
_ => {
// enum is non-exhaustive, so we need a default case
// but this should never be reached in pactical applications
}
}
}

465
src/applogic/cam_tx.rs Normal file
View file

@ -0,0 +1,465 @@
//! C-ITS CAM Transmission
use alloc::vec::Vec;
use c_its_parser::gn as geonetworking;
use c_its_parser::standards::cam_1_4_1::cam_pdu_descriptions;
use c_its_parser::standards::cdd_1_3_1_1::its_container;
use c_its_parser::standards::extensions;
const PH_MAX_LENGHT_M: f32 = 500.;
const PH_MAX_ITEMS: usize = 23;
const PH_MIN_DIST_M: f32 = 20.;
/// Stand-alone CAM generator
///
/// Generates CAM messages from time and [`PosVel`] updates
///
/// # Send a message
/// ```
/// use esp32_cits_core::tx::GnItsPayload as _;
///
/// let (mut seq_no, mac_bytes) = {
/// // unwrap is fine since [`esp_hal::efuse::MacAddress`] is just an `[u8; 6]`
/// let mac_bytes = mac.as_bytes().try_into().unwrap();
///
/// (0, mac_bytes)
/// };
///
/// let mut cam = {
/// let station_id = make_station_id(mac);
/// info!("V2X Station ID: {station_id}");
///
/// applogic::cam_tx::CamState::new(
/// station_id,
/// OWN_STATION_TYPE,
/// OWN_VEHICLE_LENGTH,
/// OWN_VEHICLE_WIDTH,
/// )
/// };
///
/// cam.update(time, fix.clone().into());
/// match cam.encode_packet(mac_bytes, pos_vel, time, GN_IS_MOBILE, &mut seq_no) {
/// Ok(data) => {
/// if let Err(err) =
/// radio::send_80211_bcast_frame(&mut wlan_iface, &mac_bytes.clone(), &data)
/// {
/// warn!("Failed to send 802.11 frame: {err}");
/// }
/// }
/// Err(err) => warn!("Failed to create CAM: {err}"),
/// }
/// ```
#[derive(Debug)]
pub struct CamState {
station_id: u32,
station_type: its_container::StationType,
vehicle_length_dm: u16, // vehicle length is in 10cm steps
vehicle_width_dm: u8, // vehicle width is in 10cm steps
time: chrono::DateTime<chrono::Utc>,
pos_state: esp32_cits_core::PosVel,
/// database for path history, oldest item at the back of the vector
path_history: Vec<PathPoint>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct PathPoint {
pos: geo_types::Point,
time: chrono::DateTime<chrono::Utc>,
/// distance to previous point. Ignore on last point!
dist_m: f32,
}
impl PathPoint {
fn new_from_pos(pos: &esp32_cits_core::PosVel, time: chrono::DateTime<chrono::Utc>) -> Self {
Self {
pos: pos.position,
time,
dist_m: 0.,
}
}
fn new_from_pos_with_dist(
pos: &esp32_cits_core::PosVel,
time: chrono::DateTime<chrono::Utc>,
dist_m: f32,
) -> Self {
Self {
pos: pos.position,
time,
dist_m,
}
}
}
impl CamState {
pub fn new(
station_id: u32,
station_type: c_its_parser::standards::extensions::ItsStationType,
vehicle_length_dm: u16,
vehicle_width_dm: u8,
) -> Self {
Self {
station_id,
station_type: station_type.into(),
vehicle_length_dm,
vehicle_width_dm,
time: chrono::DateTime::<chrono::Utc>::default(),
pos_state: esp32_cits_core::PosVel::default(),
path_history: Vec::with_capacity(PH_MAX_ITEMS + 1),
}
}
pub fn update(&mut self, time: chrono::DateTime<chrono::Utc>, pos: esp32_cits_core::PosVel) {
self.time = time;
self.pos_state = pos;
// Update path history
Self::update_ph(&mut self.path_history, time, &self.pos_state);
}
// this is a function, not a method for easier testing
fn update_ph(
path_history: &mut Vec<PathPoint>,
time: chrono::DateTime<chrono::Utc>,
pos: &esp32_cits_core::PosVel,
) {
// Update path history
if path_history.is_empty() {
// prime with current position if empty
path_history.push(PathPoint::new_from_pos(pos, time));
} else {
// unwrap is fine since we checked for non-empty before
let prev = path_history.first().unwrap();
// only add a point, if we moved more than PH_MIN_DIST_M
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let dist = crate::geo_alg::haversine_dist(&prev.pos, &pos.position) as f32;
if dist > PH_MIN_DIST_M {
// new items shall be at the front of the vector
path_history.insert(0, PathPoint::new_from_pos_with_dist(pos, time, dist));
}
}
// check length (PH_MAX_LENGHT_M and PH_MAX_ITEMS points max)
if path_history.len() > PH_MAX_ITEMS {
let _ = path_history.pop();
}
let (_, cutoff_idx) =
path_history
.iter()
.enumerate()
.fold((0., None), |(len, cutoff_idx), (idx, item)| {
if cutoff_idx.is_none() {
let new_len = len + item.dist_m;
let new_cutoff_idx = if len > PH_MAX_LENGHT_M {
Some(idx)
} else {
None
};
(new_len, new_cutoff_idx)
} else {
(len, cutoff_idx)
}
});
if let Some(idx) = cutoff_idx {
path_history.drain(idx..);
}
}
fn make_ph(&self) -> Vec<its_container::PathPoint> {
let mut ph = alloc::vec![];
self.path_history.iter().fold(
PathPoint::new_from_pos(&self.pos_state, self.time),
|prev_point, pt| {
// calculate delta position and append to `ph`
let delta_time = pt.time - prev_point.time;
let delta_lat = pt.pos.y() - prev_point.pos.y();
let delta_lon = pt.pos.x() - prev_point.pos.x();
// delta time is in 1/100 second / 10 ms steps
let path_delta_time = its_container::PathDeltaTime(
(delta_time.num_milliseconds().abs() / 10)
.min(65_535) // clamp to max value
.into(),
);
let delta_latitude = its_container::DeltaLatitude::from_deg(delta_lat);
let delta_longitude = its_container::DeltaLongitude::from_deg(delta_lon);
let delta_altitude = its_container::DeltaAltitude::unavailable();
let ppoint = its_container::PathPoint::new(
its_container::DeltaReferencePosition::new(
delta_latitude,
delta_longitude,
delta_altitude,
),
Some(path_delta_time),
);
ph.push(ppoint);
*pt
},
);
ph
}
#[allow(clippy::too_many_lines)]
fn make_cam(&self) -> Result<cam_pdu_descriptions::CAM, alloc::string::String> {
let header = its_container::ItsPduHeader::new(
2,
extensions::ItsMessageId::Cam.as_u8(),
its_container::StationID(self.station_id),
);
let generation_delta_time = {
let its_timestamp = c_its_parser::time_utils::TimestampIts::from(self.time);
#[allow(clippy::cast_possible_truncation)]
let ts_mod = (its_timestamp.0 % 65_536) as u16;
cam_pdu_descriptions::GenerationDeltaTime(ts_mod)
};
let heading = match self.pos_state.heading_deg {
Some(value) => its_container::Heading {
heading_value: its_container::HeadingValue::from_deg(value)
.map_err(|err| alloc::format!("Failed to make HeadingValue: {err}"))?,
heading_confidence: its_container::HeadingConfidence(127), // unavailable for now
},
None => its_container::Heading {
heading_value: its_container::HeadingValue(0),
heading_confidence: its_container::HeadingConfidence(127), // unavailable for now
},
};
let speed = match self.pos_state.speed_mps {
Some(value) => its_container::Speed {
speed_value: its_container::SpeedValue::from_mps(value)
.map_err(|err| alloc::format!("Failed to make SpeedValue: {err}"))?,
speed_confidence: its_container::SpeedConfidence(127), // unavailable for now
},
None => its_container::Speed {
speed_value: its_container::SpeedValue(0),
speed_confidence: its_container::SpeedConfidence(127), // unavailable for now
},
};
let reference_position = its_container::ReferencePosition {
latitude: its_container::Latitude::from_deg(self.pos_state.position.y()),
longitude: its_container::Longitude::from_deg(self.pos_state.position.x()),
position_confidence_ellipse: its_container::PosConfidenceEllipse {
semi_major_confidence: its_container::SemiAxisLength(4095), // unavailable for now
semi_minor_confidence: its_container::SemiAxisLength(4095), // unavailable for now
semi_major_orientation: its_container::HeadingValue::unavailable(),
},
altitude: its_container::Altitude {
altitude_value: its_container::AltitudeValue(800_001), // unavailable for now
altitude_confidence: its_container::AltitudeConfidence::unavailable,
},
};
let basic_container = cam_pdu_descriptions::BasicContainer::new(
self.station_type.clone(),
reference_position,
);
let high_frequency_container =
cam_pdu_descriptions::HighFrequencyContainer::basicVehicleContainerHighFrequency(
cam_pdu_descriptions::BasicVehicleContainerHighFrequency {
heading,
speed,
drive_direction: its_container::DriveDirection::unavailable,
vehicle_length: its_container::VehicleLength {
vehicle_length_value: its_container::VehicleLengthValue(
self.vehicle_length_dm,
),
vehicle_length_confidence_indication:
its_container::VehicleLengthConfidenceIndication::unavailable,
},
vehicle_width: its_container::VehicleWidth(self.vehicle_width_dm),
longitudinal_acceleration: its_container::LongitudinalAcceleration {
longitudinal_acceleration_value:
its_container::LongitudinalAccelerationValue::unavailable(),
longitudinal_acceleration_confidence: its_container::AccelerationConfidence(
102, // unavailable
),
},
curvature: its_container::Curvature {
curvature_value: its_container::CurvatureValue::unavailable(),
curvature_confidence: its_container::CurvatureConfidence::unavailable,
},
curvature_calculation_mode:
its_container::CurvatureCalculationMode::unavailable,
yaw_rate: its_container::YawRate {
yaw_rate_value: its_container::YawRateValue::unavailable(),
yaw_rate_confidence: its_container::YawRateConfidence::unavailable,
},
acceleration_control: None,
lane_position: None,
steering_wheel_angle: None,
lateral_acceleration: None,
vertical_acceleration: None,
performance_class: None,
cen_dsrc_tolling_zone: None,
},
);
let low_frequency_container =
cam_pdu_descriptions::LowFrequencyContainer::basicVehicleContainerLowFrequency(
cam_pdu_descriptions::BasicVehicleContainerLowFrequency {
vehicle_role: its_container::VehicleRole::default,
exterior_lights: its_container::ExteriorLights::default(),
path_history: its_container::PathHistory(self.make_ph()),
},
);
let cam_parameters = cam_pdu_descriptions::CamParameters::new(
basic_container,
high_frequency_container,
Some(low_frequency_container),
None,
);
Ok(cam_pdu_descriptions::CAM::new(
header,
cam_pdu_descriptions::CoopAwareness::new(generation_delta_time, cam_parameters),
))
}
}
impl esp32_cits_core::tx::GnItsPayload for CamState {
fn make_eh(
&self,
address: [u8; 6],
own_position: &esp32_cits_core::PosVel,
time: chrono::DateTime<chrono::Utc>,
_seq_no: &mut u16,
) -> Result<
(
geonetworking::en302636_4_1::ExtendedHeader,
Option<geonetworking::en302636_4_1::AreaType>,
u8,
),
alloc::string::String,
> {
let station_type = geonetworking::en302636_4_1::StationType::try_from(self.station_type.0)
.unwrap_or_default();
// CAM always uses SHB, hop-limit 1
Ok((
esp32_cits_core::tx::gn::make_shb_eh(address, station_type, own_position, time)?,
None,
1,
))
}
fn make_payload(
&self,
) -> Result<(Vec<u8>, c_its_parser::standards::extensions::ItsMessageId), alloc::string::String>
{
let msg = self.make_cam()?;
let uper = msg.encode_to_uper()?;
Ok((uper, c_its_parser::standards::extensions::ItsMessageId::Cam))
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
use crate::init_test_env_logger;
fn posstate_from_pos(position: geo_types::Point) -> esp32_cits_core::PosVel {
esp32_cits_core::PosVel {
position,
heading_deg: None,
speed_mps: None,
}
}
#[test]
fn cam_path_history_update() {
init_test_env_logger();
let time = chrono::DateTime::from_timestamp(0, 0).unwrap();
let p1 = geo_types::Point::new(10., 53.5);
let p2 = geo_types::Point::new(10., 53.5 + 0.000045); // ~5m further (< PH_MIN_DIST_M)
let p3 = geo_types::Point::new(10., 53.5 + 0.00027); // ~30m further (> PH_MIN_DIST_M)
let ps1 = posstate_from_pos(p1);
let pp1 = PathPoint::new_from_pos(&ps1, time);
let ps2 = posstate_from_pos(p2);
let pp2 = PathPoint::new_from_pos(&ps2, time);
let ps3 = posstate_from_pos(p3);
let pp3 = PathPoint::new_from_pos_with_dist(&ps3, time, 30.02267);
let mut path_history: Vec<PathPoint> = vec![];
// add first point, expect one point in PH
CamState::update_ph(&mut path_history, time, &posstate_from_pos(p1));
assert_eq!(1, path_history.len());
assert_eq!(&pp1, path_history.first().unwrap());
// add insignificant point, expect still one point in PH
CamState::update_ph(&mut path_history, time, &posstate_from_pos(p2));
assert_eq!(1, path_history.len());
assert_eq!(&pp1, path_history.first().unwrap());
// add significantly moved point, expect [pp3, pp1] vector
CamState::update_ph(&mut path_history, time, &posstate_from_pos(p3));
assert_eq!(2, path_history.len());
assert_eq!(&pp3, path_history.first().unwrap());
assert_eq!(&pp1, path_history.last().unwrap());
}
#[test]
fn cam_path_history_max_len() {
init_test_env_logger();
let time = chrono::DateTime::from_timestamp(0, 0).unwrap();
let p1 = geo_types::Point::new(10., 53.5);
let p2 = geo_types::Point::new(10., 53.5 + 0.00023); // at ~25m from p1 (> PH_MIN_DIST_M)
let p3 = geo_types::Point::new(10., 53.5 + 0.00445); // at ~495m from p1 (slightly less than PH_MAX_LENGHT_M)
let p4 = geo_types::Point::new(10., 53.5 + 0.00477); // at ~530m from p1 (slightly more than PH_MAX_LENGHT_M)
let ps1 = posstate_from_pos(p1);
let pp1 = PathPoint::new_from_pos(&ps1, time);
let ps2 = posstate_from_pos(p2);
let pp2 = PathPoint::new_from_pos(&ps2, time);
let ps3 = posstate_from_pos(p3);
let pp3 = PathPoint::new_from_pos(&ps3, time);
let ps4 = posstate_from_pos(p4);
let pp4 = PathPoint::new_from_pos(&ps4, time);
let mut path_history: Vec<PathPoint> = vec![];
// prime with p3, p4 (should give a length of ~470m)
CamState::update_ph(&mut path_history, time, &posstate_from_pos(p4));
CamState::update_ph(&mut path_history, time, &posstate_from_pos(p3));
assert_eq!(2, path_history.len());
// Add p2, should drop p4 from vector because of > PH_MAX_LENGHT_M
CamState::update_ph(&mut path_history, time, &posstate_from_pos(p2));
assert_eq!(2, path_history.len());
assert_eq!(p2, path_history.get(0).unwrap().pos);
assert_eq!(469.24323, path_history.get(0).unwrap().dist_m);
assert_eq!(p3, path_history.get(1).unwrap().pos);
assert_eq!(35.582424, path_history.get(1).unwrap().dist_m);
// Add p1, should not drop anything from vector because still < PH_MAX_LENGHT_M
CamState::update_ph(&mut path_history, time, &posstate_from_pos(p1));
assert_eq!(3, path_history.len());
assert_eq!(p1, path_history.get(0).unwrap().pos);
assert_eq!(25.57487, path_history.get(0).unwrap().dist_m);
assert_eq!(p2, path_history.get(1).unwrap().pos);
assert_eq!(469.24323, path_history.get(1).unwrap().dist_m);
assert_eq!(p3, path_history.get(2).unwrap().pos);
assert_eq!(35.582424, path_history.get(2).unwrap().dist_m);
}
}

44
src/applogic/denm.rs Normal file
View file

@ -0,0 +1,44 @@
//! C-ITS DENM Evaluation
use c_its_parser::standards::denm_2_2_1::denm_pdu_description;
use log::info;
pub fn handle_denm(etsi: &denm_pdu_description::DENM) {
let mgmt = &etsi.denm.management;
if mgmt.termination.is_some() {
return;
}
if let (Some(sit), Some(loc)) = (&etsi.denm.situation, &etsi.denm.location) {
// let timestamp: chrono::DateTime<chrono::Utc> = mgmt.reference_time.clone().into();
let end_time = chrono::DateTime::<chrono::Utc>::from(mgmt.reference_time.clone())
+ chrono::Duration::seconds(mgmt.validity_duration.0.into());
let station_id = &mgmt.action_id.originating_station_id.0;
let seq_num = mgmt.action_id.sequence_number.0;
let lat_deg = mgmt.event_position.latitude.as_deg();
let lon_deg = mgmt.event_position.longitude.as_deg();
let heading = loc
.event_position_heading
.as_ref()
.map(|v| v.value.as_deg());
// let direction = mgmt.traffic_direction;
let (cc, scc) = sit.event_type.cc_and_scc.to_u8_tuple();
if let Some(hdg) = heading {
info!(
"DENN: {station_id}/{seq_num} CC {cc}/{scc} at {lat_deg:.3}°N, {lon_deg:.3}°E, hdg {hdg:.1}° until {end_time:?}"
);
} else {
info!(
"DENN: {station_id}/{seq_num} CC {cc}/{scc} at {lat_deg:.3}°N, {lon_deg:.3}°E until {end_time:?}"
);
}
} else {
info!("DENM: One of situation or location container not available");
}
}

716
src/applogic/mod.rs Normal file
View file

@ -0,0 +1,716 @@
//! Application Logic
#[cfg(feature = "spat")]
use alloc::rc::Rc;
#[cfg(feature = "spat")]
use alloc::vec::Vec;
use log::info;
#[cfg(feature = "spat")]
use log::warn;
#[cfg(feature = "spat")]
use log::{debug, error};
#[cfg(feature = "spat")]
use crate::{applogic::v2x::map, geo_alg};
pub mod v2x;
#[cfg(feature = "cam")]
pub mod cam;
#[cfg(any(feature = "cam_tx", test))]
pub mod cam_tx;
#[cfg(feature = "denm")]
pub mod denm;
#[cfg(feature = "spat")]
pub mod tlc;
#[cfg(feature = "spat")]
const TLC_CACHE_LIFETIME: chrono::Duration = chrono::Duration::minutes(1);
#[cfg(feature = "spat")]
const PRUNE_INTERVAL: chrono::Duration = chrono::Duration::seconds(5);
#[cfg(feature = "spat")]
const GLOSA_SEARCH_DIST_M: f32 = 400.;
#[cfg(feature = "spat")]
const GLOSA_SEARCH_ANGLE_PARAMS: [(f32, f32); 2] = [(400., 45.), (50., 180.)]; // far and near (distance, angle) tuple
#[cfg(feature = "spat")]
const GLOSA_LANE_HEADING_DIFF_DEG: f32 = 35.;
#[cfg(feature = "spat")]
const GLOSA_AT_LANE_OFFSET_M: f64 = 10.;
#[cfg(feature = "spat")]
const GLOSA_LANE_LEFT_OFFSET_M: f64 = 20.;
#[cfg(feature = "spat")]
const GLOSA_STOP_LINE_PASSED_M: f64 = 5.;
#[allow(unused)]
pub struct State {
initialized: bool,
time: chrono::NaiveDateTime,
#[allow(unused)]
pos: esp32_cits_core::PosVel,
#[cfg(feature = "spat")]
last_prune: chrono::NaiveDateTime,
#[cfg(feature = "spat")]
tlc_cache: tlc::Data,
// GLOSA
#[cfg(feature = "spat")]
#[allow(clippy::struct_field_names)]
glosa_state: GlosaState,
}
#[cfg(feature = "spat")]
#[derive(Debug)]
pub enum GlosaOutput {
Error(alloc::string::String),
Searching(GlosaSearching),
Found(GlosaFound),
Locked(GlosaSignalInfo),
}
#[cfg(feature = "spat")]
#[derive(Debug, PartialEq, Eq)]
pub struct GlosaSearching {
pub known_intersection_ids: Vec<u16>,
}
#[cfg(feature = "spat")]
#[derive(Debug, PartialEq, Eq)]
pub struct GlosaFound {
pub intersection_id: u16,
pub approach_id: Option<u8>,
}
#[cfg(feature = "spat")]
impl From<&TlData> for GlosaFound {
fn from(value: &TlData) -> Self {
Self {
intersection_id: value.intersection_id,
approach_id: value.approach_id,
}
}
}
#[cfg(feature = "spat")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlosaSignalInfo {
pub intersection_id: u16,
pub signal_groups: Vec<SignalGroupData>,
}
#[cfg(feature = "spat")]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct SignalGroupData {
pub phase: SignalPhase,
pub maneuver: map::Maneuvers,
pub end_sec: Option<i16>,
}
#[cfg(feature = "spat")]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum SignalPhase {
#[default]
Unknown,
Red,
RedYellow,
Green,
Yellow,
}
#[allow(unused)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct GnssFix {
pub time: Option<chrono::NaiveDateTime>,
pub latitude_deg: f64,
pub longitude_deg: f64,
pub heading_deg: Option<f32>,
pub speed_mps: Option<f32>,
}
impl From<&GnssFix> for esp32_cits_core::PosVel {
fn from(value: &GnssFix) -> Self {
let position = geo_types::Point::new(value.longitude_deg, value.latitude_deg);
Self {
position,
heading_deg: value.heading_deg,
speed_mps: value.speed_mps,
}
}
}
impl From<GnssFix> for esp32_cits_core::PosVel {
fn from(value: GnssFix) -> Self {
(&value).into()
}
}
#[cfg(feature = "spat")]
impl core::fmt::Display for SignalPhase {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SignalPhase::Unknown => write!(f, "__"),
SignalPhase::Red => write!(f, "RE"),
SignalPhase::RedYellow => write!(f, "RY"),
SignalPhase::Green => write!(f, "GN"),
SignalPhase::Yellow => write!(f, "YE"),
}
}
}
#[cfg(feature = "spat")]
impl From<c_its_parser::standards::dsrc_2_2_1::etsi_its_dsrc::MovementPhaseState> for SignalPhase {
fn from(value: c_its_parser::standards::dsrc_2_2_1::etsi_its_dsrc::MovementPhaseState) -> Self {
use c_its_parser::standards::dsrc_2_2_1::etsi_its_dsrc::MovementPhaseState;
match value {
MovementPhaseState::stop_And_Remain => SignalPhase::Red,
MovementPhaseState::pre_Movement => SignalPhase::RedYellow,
MovementPhaseState::permissive_Movement_Allowed
| MovementPhaseState::protected_Movement_Allowed => SignalPhase::Green,
MovementPhaseState::permissive_clearance | MovementPhaseState::protected_clearance => {
SignalPhase::Yellow
}
_ => SignalPhase::Unknown,
}
}
}
#[cfg(feature = "spat")]
#[derive(Debug, Default)]
enum GlosaState {
#[default]
Idle,
Locked(TlData),
}
/// Where we are and pre-processed information about the current intersection
#[cfg(feature = "spat")]
#[derive(Debug)]
struct TlData {
intersection_id: u16, // mostly for debug printing
intersection: Rc<core::cell::RefCell<v2x::map::Intersection>>,
lane_id: u8,
approach_id: Option<u8>,
signal_groups: Vec<u8>,
}
impl State {
pub fn new() -> Self {
Self {
initialized: false,
time: chrono::NaiveDateTime::default(),
pos: esp32_cits_core::PosVel::default(),
#[cfg(feature = "spat")]
last_prune: chrono::NaiveDateTime::default(),
#[cfg(feature = "spat")]
tlc_cache: tlc::Data::new(TLC_CACHE_LIFETIME),
#[cfg(feature = "spat")]
glosa_state: GlosaState::default(),
}
}
#[allow(unused)]
#[cfg(feature = "esp")]
pub fn update_with_gpsfix(&mut self, fix: &GnssFix) {
self.pos = fix.into();
if let Some(time) = fix.time {
self.time = time;
if !self.initialized {
info!("GNSS fix: {}", self.print_fix());
}
self.initialized = true;
}
}
#[allow(unused)]
pub fn print_fix(&self) -> alloc::string::String {
alloc::format!(
"{:?}, {:.4} N, {:.4} E, {:.0}°, {:.2} m/s",
self.time,
self.pos.position.y(),
self.pos.position.x(),
self.pos.heading_deg.unwrap_or_default(),
self.pos.speed_mps.unwrap_or_default(),
)
}
#[allow(clippy::unused_self)]
pub fn prune(&mut self) {
#[cfg(feature = "spat")]
if self.last_prune + PRUNE_INTERVAL < self.time {
self.tlc_cache.prune(self.time);
info!("Known int.: {:?}", self.tlc_cache.known_intersections());
self.last_prune = self.time;
}
}
#[cfg(feature = "spat")]
pub fn handle_mapem(&mut self, etsi: &tlc::MAPEM) {
self.tlc_cache.handle_mapem(self.time, etsi);
}
#[cfg(feature = "spat")]
pub fn handle_spatem(&mut self, etsi: &tlc::SPATEM) {
self.tlc_cache.handle_spatem(self.time, etsi);
}
#[cfg(feature = "spat")]
pub fn run_glosa(&mut self) -> GlosaOutput {
let Some(own_heading) = self.pos.heading_deg else {
use alloc::string::ToString;
return GlosaOutput::Error("Heading unknown".to_string());
};
let known_intersection_ids = self.tlc_cache.known_intersections();
let (state_update, output_data) = match &self.glosa_state {
// search for upcoming intersection
GlosaState::Idle => {
debug!(
"GLOSA: Searching with {} known intersections",
known_intersection_ids.len()
);
if let Some(search_result) =
Self::find_intersection(&self.pos.position, own_heading, &self.tlc_cache)
{
info!(
"GLOSA: Approach has signal groups: {:?}",
search_result.signal_groups
);
let glosa_found_data = (&search_result).into();
(
Some(GlosaState::Locked(search_result)),
GlosaOutput::Found(glosa_found_data),
)
} else {
(
None,
GlosaOutput::Searching(GlosaSearching {
known_intersection_ids,
}),
)
}
}
GlosaState::Locked(info) => {
debug!(
"GLOSA: Re-evaluating intersection {:4} (at lane {:3})",
info.intersection_id, info.lane_id
);
// ensure that "our" intersection is kept in TLC cache
if self
.tlc_cache
.keep_intersection(info.intersection_id, self.time)
.is_err()
{
info!("GLOSA: Intersection not known any more");
(
Some(GlosaState::Idle),
GlosaOutput::Searching(GlosaSearching {
known_intersection_ids,
}),
)
} else {
// determine if intersection is left
let intersection = info.intersection.borrow();
if Self::is_intersection_left(
&intersection,
info.lane_id,
info.approach_id,
&self.pos.position,
) {
info!("GLOSA: Intersection was left -> back to idle");
(
Some(GlosaState::Idle),
GlosaOutput::Searching(GlosaSearching {
known_intersection_ids,
}),
)
} else {
// get signal groups which are in our approach
let mut approach_sig_grps = intersection
.signal_groups()
.iter()
.filter(|i| {
info.signal_groups
.iter()
.find(|sg_id| **sg_id == i.id())
.is_some()
})
.collect::<Vec<_>>();
// sort signal groups by maneuver
approach_sig_grps.sort_by_key(|v| v.maneuvers());
// display signal phases
let sig_grps = approach_sig_grps
.iter()
.filter_map(|sig| {
if let Some(manv) = sig.maneuvers() {
use core::ops::Sub;
use num_traits::Float;
let phase = sig
.phase()
.map(core::convert::Into::into)
.unwrap_or_default();
let end_sec = sig
.likely_time()
.or_else(|| sig.min_end_time())
.map(|time| {
let dur =
time.sub(self.time.and_utc()).as_seconds_f32();
#[allow(clippy::cast_possible_truncation)]
let end_sec = dur.round() as i16;
end_sec
});
Some(SignalGroupData {
phase,
maneuver: manv,
end_sec,
})
} else {
None
}
})
.collect::<Vec<_>>();
(
None,
GlosaOutput::Locked(GlosaSignalInfo {
intersection_id: intersection.id(),
signal_groups: sig_grps,
}),
) // keep state, return signal groups
}
}
}
};
// can't update the state from within the match statement, so do it here
if let Some(new) = state_update {
self.glosa_state = new;
}
output_data
}
#[cfg(feature = "spat")]
#[allow(clippy::too_many_lines)]
fn find_intersection(
own_pos: &geo_types::Point,
own_heading: f32,
tlc_cache: &tlc::Data,
) -> Option<TlData> {
// filter known intersections by distance (and bearing)
let upcoming_intersections = tlc_cache.intersections_matching(|i| {
let int_pos = i.center();
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let distance = geo_alg::haversine_dist(own_pos, &int_pos) as f32;
let bearing = geo_alg::haversine_bearing(own_pos, &int_pos);
let bearing_diff = geo_alg::deg_180(own_heading - bearing);
distance < GLOSA_SEARCH_DIST_M && bearing_diff.abs() < Self::get_search_angle(distance)
});
info!(
"GLOSA: upcoming int. count: {}",
upcoming_intersections.len()
);
// determine relevant approach
for int in upcoming_intersections {
let intersection = int.borrow();
let lanes = intersection.ingress_lanes();
let own_pos_cart = geo_alg::lonlat_to_dcart(&intersection.ref_point(), own_pos);
// find lanes with matching heading
let relevant_lanes = lanes
.iter()
.filter(|i| {
let lane_heading = i.get_heading();
let heading_diff = geo_alg::deg_180(own_heading - lane_heading);
debug!(
" - Lane {:4}/{:03} has heading {lane_heading:4.0} -> diff {heading_diff:4.0}°",
intersection.id(),
i.id()
);
heading_diff.abs() < GLOSA_LANE_HEADING_DIFF_DEG
})
.collect::<Vec<_>>();
info!(
"GLOSA: Int {:4} has {} relevant lanes",
intersection.id(),
relevant_lanes.len()
);
for lane in &relevant_lanes {
info!(
" - lane {:03} from appr. {:?}, usages {:?}",
lane.id(),
lane.approach_id(),
lane.usages()
);
}
// determine in which lane/ approach we are
let current_lane = relevant_lanes.iter().find(|lane| {
#[cfg(feature = "spat_debug")]
info!("GLOSA: Searching for current lane");
match geo_alg::LinePosition::for_ingress_lane(own_pos_cart, lane.nodes()) {
Ok(pos) => {
#[cfg(feature = "spat_debug")]
info!(
" - Lane {:4}/{:03} {:?} with {:.0} m offset",
intersection.id(),
lane.id(),
pos.semantic,
pos.perpendicular_dist,
);
if pos.perpendicular_dist < GLOSA_AT_LANE_OFFSET_M
&& pos.semantic == geo_alg::LinePositionType::InBetween
{
info!(
"GLOSA: At lane {:4}/{:03} -> locking intersection",
intersection.id(),
lane.id()
);
true
} else {
false
}
}
Err(err) => {
warn!("Lane {:04}: Failed to determine position: {err}", lane.id());
false
}
}
});
if let Some(lane) = current_lane {
// we found some lane which we are "inside" -> lane search can be aborted here
// but we should extract some information which is needed for future processing first
// collect all signal groups from the current approach
let mut signal_groups = if let Some(own_approach) = lane.approach_id() {
let approach_lanes = relevant_lanes.iter().filter(|lane| {
if let Some(approach) = lane.approach_id() {
approach == own_approach
} else {
false
}
});
approach_lanes
.flat_map(|lane| {
lane.connections()
.iter()
.filter_map(v2x::map::Connection::signal_group)
})
.collect::<Vec<_>>()
} else {
error!(
"GLOSA: Lane has no approach ID, can't determine all possibly relevant signal groups"
);
// just fall back to only the current lane's signal groups
lane.connections()
.iter()
.filter_map(v2x::map::Connection::signal_group)
.collect::<Vec<_>>()
};
signal_groups.sort_unstable();
signal_groups.dedup();
return Some(TlData {
intersection_id: intersection.id(),
intersection: int.clone(),
lane_id: lane.id(),
approach_id: lane.approach_id(),
signal_groups,
});
}
}
None
}
#[cfg(feature = "spat")]
fn get_search_angle(distance: f32) -> f32 {
let (dist_far, angle_far) = GLOSA_SEARCH_ANGLE_PARAMS[0];
let (dist_near, angle_near) = GLOSA_SEARCH_ANGLE_PARAMS[1];
if distance > dist_far {
angle_far
} else if distance < dist_near {
angle_near
} else {
// interpolate angle from near to far
let diff_dist = dist_far - dist_near;
let diff_angle = angle_far - angle_near;
angle_near + (diff_angle / diff_dist) * (distance - dist_near)
}
}
/// Determines if intersection was left
///
/// TODO: return if left via stop line or otherwise
#[cfg(feature = "spat")]
fn is_intersection_left(
intersection: &v2x::map::Intersection,
lane_id: u8,
approach_id: Option<u8>,
own_pos: &geo_types::Point,
) -> bool {
// get all lanes from the current approach
let lanes = if let Some(approach) = approach_id {
intersection.approach_lanes(approach)
} else if let Some(lane) = intersection.lane(lane_id) {
alloc::vec![lane]
} else {
alloc::vec![]
};
// find minimal distance to any lane where we are "inside"
let own_pos_cart = geo_alg::lonlat_to_dcart(&intersection.ref_point(), own_pos);
let min_perp_dist = lanes.iter().fold(f64::MAX, |min_dist, i| {
match geo_alg::LinePosition::for_ingress_lane(own_pos_cart, i.nodes()) {
Ok(pos) => {
// check if we left the lane via the stop line, but only count stop line as passed after X meters
if pos.semantic == geo_alg::LinePositionType::InBetween
|| (pos.semantic == geo_alg::LinePositionType::BeforeStart
&& pos.parallel_dist.abs() < GLOSA_STOP_LINE_PASSED_M)
{
// still at the lane -> update min_dist
pos.perpendicular_dist
} else {
// not at the lane -> take old min_dist
min_dist
}
}
Err(err) => {
warn!("Lane {:04}: Failed to determine position: {err}", i.id());
min_dist
}
}
});
min_perp_dist > GLOSA_LANE_LEFT_OFFSET_M
}
// -- getters --
#[allow(unused)]
pub fn initialized(&self) -> bool {
self.initialized
}
#[allow(unused)]
pub fn time(&self) -> chrono::prelude::NaiveDateTime {
self.time
}
#[allow(unused)]
pub fn pos(&self) -> &esp32_cits_core::PosVel {
&self.pos
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::init_test_env_logger;
#[cfg(feature = "spat")]
use crate::testdata::{MAPEM_HH_560, MAPEM_HH_1328};
#[cfg(feature = "spat")]
#[test]
fn glosa() {
init_test_env_logger();
// prime TLC cache
let mut state = State::new();
let time_now = chrono::NaiveDateTime::default();
let etsi = if let c_its_parser::ItsMessage::Mapem {
geonetworking: _,
transport: _,
etsi,
} =
c_its_parser::de::decode(MAPEM_HH_1328, c_its_parser::Headers::None).unwrap()
{
etsi
} else {
panic!("Unexpected C-ITS message")
};
state.tlc_cache.handle_mapem(time_now, &etsi);
let etsi = if let c_its_parser::ItsMessage::Mapem {
geonetworking: _,
transport: _,
etsi,
} =
c_its_parser::de::decode(MAPEM_HH_560, c_its_parser::Headers::None).unwrap()
{
etsi
} else {
panic!("Unexpected C-ITS message")
};
state.tlc_cache.handle_mapem(time_now, &etsi);
// test GLOSA
let own_pos_infront = geo_types::Point::new(9.9752864, 53.5564028);
let own_pos_inside = geo_types::Point::new(9.9760764, 53.5563621);
let own_pos_passed = geo_types::Point::new(9.977217, 53.5560602);
let own_heading = 90.;
state.pos.heading_deg = Some(own_heading);
state.pos.position = own_pos_infront;
state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Idle));
state.pos.position = own_pos_inside;
state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Locked(_)));
if let GlosaState::Locked(info) = &state.glosa_state {
assert_eq!(1328, info.intersection_id);
assert!(info.lane_id == 1 || info.lane_id == 2);
}
state.pos.position = own_pos_inside;
state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Locked(_)));
if let GlosaState::Locked(info) = &state.glosa_state {
assert_eq!(1328, info.intersection_id);
assert!(info.lane_id == 1 || info.lane_id == 2);
}
state.pos.position = own_pos_passed;
state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Idle));
}
}

117
src/applogic/tlc.rs Normal file
View file

@ -0,0 +1,117 @@
//! C-ITS Traffic Light Data (MAPEM/ SPATEM) Evaluation/ Cache
use alloc::rc::Rc;
use alloc::vec::Vec;
pub use c_its_parser::standards::mapem_2_2_1::mapem_pdu_descriptions::MAPEM;
pub use c_its_parser::standards::spatem_2_2_1::spatem_pdu_descriptions::SPATEM;
use chrono::Datelike;
#[cfg(all(target_arch = "riscv32", feature = "spat_debug"))]
use esp_println::println;
use super::v2x::map;
use crate::cache::{Cache, CacheError};
pub struct Data {
intersections: Cache<u16, map::Intersection>,
}
impl Data {
pub fn new(cache_lifetime: chrono::Duration) -> Self {
Self {
intersections: Cache::<u16, map::Intersection>::new(cache_lifetime),
}
}
pub fn known_intersections(&self) -> Vec<u16> {
self.intersections.keys()
}
pub fn intersections_matching<P>(
&self,
predicate: P,
) -> Vec<Rc<core::cell::RefCell<map::Intersection>>>
where
P: Fn(&map::Intersection) -> bool,
{
self.intersections.get_filtered(predicate)
}
pub fn handle_mapem(&mut self, time_now: chrono::NaiveDateTime, etsi: &MAPEM) {
let layer_id = etsi.map.layer_id.as_ref().map(|v| v.0);
if let Some(map) = &etsi.map.intersections {
for geom in &map.0 {
let int_id = geom.id.id.0;
if self.intersections.contains(&int_id) {
// update with new map layers
// we can ignore the result, since we're sure the item exists
let _ = self.intersections.update(time_now, int_id, |i| {
if !i.is_complete() {
i.add_layer(layer_id, geom);
#[cfg(feature = "spat_debug")]
if i.is_complete() {
Self::pretty_print_intersection(&i);
}
}
});
} else {
// create new intersection
let intersection = map::Intersection::new(layer_id, geom);
#[cfg(feature = "spat_debug")]
if intersection.is_complete() {
Self::pretty_print_intersection(&intersection);
}
// we can ignore the result, since we're sure the item does not exist
let _ = self.intersections.insert(time_now, intersection);
}
}
}
}
#[cfg(feature = "spat_debug")]
fn pretty_print_intersection(intersection: &map::Intersection) {
println!("TLC: {intersection}");
println!("TLC: Signal Groups");
for grp in intersection.signal_groups() {
println!(" {grp}");
}
}
pub fn prune(&mut self, time_now: chrono::NaiveDateTime) {
self.intersections.prune(time_now);
}
/// Bumps the timestamp of a certain intersection to keep it cached
pub fn keep_intersection(
&mut self,
intersection_id: u16,
time_now: chrono::NaiveDateTime,
) -> Result<(), CacheError<u16>> {
self.intersections.update(time_now, intersection_id, |_| {})
}
pub fn handle_spatem(&mut self, time_now: chrono::NaiveDateTime, etsi: &SPATEM) {
for int in &etsi.spat.intersections.0 {
let int_id = int.id.id.0;
if self.intersections.contains(&int_id) {
// update with SPAT data
// we can ignore the result, since we're sure the item exists
let _ = self.intersections.update(time_now, int_id, |i| {
if i.is_complete() {
// add SPAT information to intersection
i.add_spat(int, time_now.year());
}
});
} else {
// drop message since we don't have (full) MAP yet
}
}
}
}

1053
src/applogic/v2x/map.rs Normal file

File diff suppressed because it is too large Load diff

4
src/applogic/v2x/mod.rs Normal file
View file

@ -0,0 +1,4 @@
//! Types for V2X data handling
#[cfg(feature = "spat")]
pub mod map;

View file

@ -1,148 +0,0 @@
#![no_std]
#![no_main]
#![deny(
clippy::mem_forget,
reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
holding buffers for the duration of a data transfer."
)]
#![deny(clippy::large_stack_frames)]
use esp_backtrace as _;
use esp_hal::clock::CpuClock;
use esp_hal::main;
use esp_hal::timer::timg::TimerGroup;
use log::{debug, error, info, warn};
extern crate alloc;
const WIFI_CHANNEL: core::ffi::c_int = 180;
unsafe extern "C" {
/// Changes the 802.11 PHY channel
///
/// C function signature: `void phy_change_channel(int,int,int,int)`
///
/// Options:
/// - `primary`: Channel frequency in MHz
/// - `ignored1`: Ignored (set to 1)
/// - `ignored2`: Ignored (set to 0)
/// - `ht_mode`: Probably HT mode
pub unsafe fn phy_change_channel(
primary: core::ffi::c_int,
ignored1: core::ffi::c_int,
ignored2: core::ffi::c_int,
ht_mode: core::ffi::c_int,
) -> core::ffi::c_int;
/// Enables 802.11p mode on the ESP32-C5
///
/// C function signature: `void phy_11p_set(int,int)`
///
/// Options:
/// - `enable`: Boolean option to enable 802.11p mode
/// - `zero`: Unknown, but need to be zero
pub unsafe fn phy_11p_set(enable: u8, zero: u8);
}
#[repr(u8)]
#[derive(Debug)]
#[allow(dead_code)]
enum EspWifiPromiscuousPktType {
/// Management frame, indicates 'buf' argument is wifi_promiscuous_pkt_t
MGMT = 0,
/// Control frame, indicates 'buf' argument is wifi_promiscuous_pkt_t
CTRL = 1,
/// Data frame, indicates 'buf' argument is wifi_promiscuous_pkt_t
DATA = 2,
/// Other type, such as MIMO etc. 'buf' argument is wifi_promiscuous_pkt_t but the payload is zero length.
MISC = 3,
}
impl EspWifiPromiscuousPktType {
#[allow(dead_code)]
pub fn as_repr(self) -> u8 {
self as u8
}
/// Converts to underlying type of `wifi_promiscuous_pkt_type_t` (used by `esp_radio::wifi::sniffer::PromiscuousPkt::frame_type`)
pub fn as_frame_type(self) -> core::ffi::c_uint {
(self as u8) as core::ffi::c_uint
}
}
// This creates a default app-descriptor required by the esp-idf bootloader.
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!();
#[allow(
clippy::large_stack_frames,
reason = "it's not unusual to allocate larger buffers etc. in main"
)]
#[main]
fn main() -> ! {
// generator version: 1.3.0
// generator parameters: --chip esp32c5 -o esp32c5-wroom-1 -o alloc -o vscode -o log -o unstable-hal -o wifi -o esp-backtrace
esp_println::logger::init_logger_from_env();
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
// The following pins are used to bootstrap the chip. They are available
// for use, but check the datasheet of the module for more information on them.
// - GPIO2
// - GPIO3
// - GPIO7
// - GPIO25
// - GPIO26
// - GPIO27
// - GPIO28
esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 65536);
let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_interrupt =
esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);
let (_wifi_controller, interfaces) = esp_radio::wifi::new(
peripherals.WIFI,
esp_radio::wifi::ControllerConfig::default(),
)
.expect("Failed to initialize Wi-Fi controller");
let mut sniffer = interfaces.sniffer;
sniffer
.set_promiscuous_mode(true)
.expect("Failed to enable promiscuous mode");
sniffer.set_receive_cb(|packet| {
// Ignore frames with errors and non-data frames
if packet.rx_cntl.rx_state != 0 {
warn!("Received frame has RX error: {}", packet.rx_cntl.rx_state);
return;
}
if packet.frame_type != EspWifiPromiscuousPktType::DATA.as_frame_type() {
debug!("Received frame is not a DATA frame: {}", packet.frame_type);
return;
}
// TODO: handle 802.11 frame
info!("Received frame with {} bytes", packet.len);
});
unsafe {
phy_11p_set(1, 0);
match phy_change_channel(5000 + (WIFI_CHANNEL * 5), 1, 0, 0) {
0 => {}
ret => {
error!("ERROR: Failed to change channel: {ret}");
}
}
}
info!("WiFi promiscuous mode on channel {WIFI_CHANNEL} running");
loop {
// nothing to do here, all data is handled in RX callback for now
}
}

258
src/cache.rs Normal file
View file

@ -0,0 +1,258 @@
//! Data Cache
//!
//! Store items for some time
use alloc::rc::Rc;
use alloc::vec::Vec;
use core::ops::Sub;
pub trait Cachable<K>
where
K: core::cmp::PartialEq,
{
fn key(&self) -> K;
}
pub trait Initable<T, K>
where
T: Cachable<K>,
K: core::cmp::PartialEq,
{
fn init(key: K) -> T;
}
struct CacheItem<K, T>
where
T: Cachable<K>,
K: core::cmp::PartialEq,
{
timestamp: chrono::NaiveDateTime,
data: Rc<core::cell::RefCell<T>>,
phantom: core::marker::PhantomData<K>, // rust wants this
}
impl<K, T> core::fmt::Display for CacheItem<K, T>
where
T: Cachable<K>,
K: core::cmp::PartialEq + core::fmt::Display,
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"CacheItem({}, {:?})",
self.data.borrow().key(),
self.timestamp
)
}
}
impl<K, T> CacheItem<K, T>
where
T: Cachable<K>,
K: core::cmp::PartialEq,
{
fn new(time_now: chrono::NaiveDateTime, data: T) -> Self {
Self {
timestamp: time_now,
data: Rc::new(core::cell::RefCell::new(data)),
phantom: core::marker::PhantomData,
}
}
}
#[derive(Debug, Clone)]
pub enum CacheError<K>
where
K: core::fmt::Display,
{
KeyNotFound(K),
InsertKeyAlreadyPresent(K),
}
impl<K> core::fmt::Display for CacheError<K>
where
K: core::fmt::Display,
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CacheError::KeyNotFound(key) => write!(f, "Key '{key}' doesn't exist in cache"),
CacheError::InsertKeyAlreadyPresent(key) => {
write!(f, "Insert failed b/c key '{key}' already exists in cache")
}
}
}
}
/// Item cache
///
/// Stores items for a certain time.
/// Items need to implement `Cachable` so that an itentifier (key) can be retreived from them.
pub struct Cache<K, T>
where
T: Cachable<K>,
K: core::cmp::PartialEq,
{
lifetime: chrono::Duration,
items: Vec<CacheItem<K, T>>,
}
impl<K, T> core::fmt::Display for Cache<K, T>
where
T: Cachable<K>,
K: core::cmp::PartialEq + core::fmt::Display,
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Cache({}, {} items)", self.lifetime, self.items.len())
}
}
#[allow(unused)]
impl<K, T> Cache<K, T>
where
T: Cachable<K> + Clone,
K: core::cmp::PartialEq + core::fmt::Display,
{
/// Creates a new cache with a certain item lifetime
pub const fn new(lifetime: chrono::Duration) -> Self {
Self {
lifetime,
items: alloc::vec![],
}
}
/// Determines if a key is in the cache
pub fn contains(&mut self, key: &K) -> bool {
self.items
.iter()
.find(|i| i.data.borrow().key() == *key)
.is_some()
}
/// Determines the index of a certain key
fn find(&mut self, key: &K) -> Option<usize> {
self.items
.iter()
.position(|i| i.data.borrow().key() == *key)
}
/// Retrieves all known keys
pub fn keys(&self) -> Vec<K> {
self.items.iter().map(|i| i.data.borrow().key()).collect()
}
/// Adds a new item to the cache
///
/// Call [`Self::update`] to update the contents of the cached item
/// or just to bump the timestamp.
///
/// # Errors
/// Fails if the specified key already exist
pub fn insert(
&mut self,
time_now: chrono::NaiveDateTime,
data: T,
) -> Result<(), CacheError<K>> {
let key = data.key();
if self.contains(&key) {
Err(CacheError::InsertKeyAlreadyPresent(key))
} else {
self.items.push(CacheItem::new(time_now, data));
Ok(())
}
}
/// Retrieves an item from the cache
///
/// Returns `None` if no item exists for this key
pub fn get(&mut self, key: &K) -> Option<Rc<core::cell::RefCell<T>>> {
self.items.iter().find_map(|i| {
if i.data.borrow().key() == *key {
Some(i.data.clone())
} else {
None
}
})
}
/// Retrieves items from the cache which match the predicate
pub fn get_filtered<P>(&self, predicate: P) -> Vec<Rc<core::cell::RefCell<T>>>
where
P: Fn(&T) -> bool,
{
self.items
.iter()
.filter_map(|i| {
let data = i.data.borrow();
if predicate(&data) {
Some(i.data.clone())
} else {
None
}
})
.collect()
}
/// Updates an item in the cache using the provided function
///
/// This can also be used to just bump the timestamp of the cached item by providing an empty closure.
///
/// # Errors
/// Fails if the specified key does not exist in the cache
pub fn update<F>(
&mut self,
time_now: chrono::NaiveDateTime,
key: K,
func: F,
) -> Result<(), CacheError<K>>
where
F: Fn(&mut T),
K: core::fmt::Display,
{
// find item
let Some(pos) = self.find(&key) else {
return Err(CacheError::KeyNotFound(key));
};
// unwrap is fine since we fetched the position before
let item = self.items.get_mut(pos).unwrap();
let mut data = item.data.borrow_mut();
func(&mut data);
item.timestamp = time_now;
Ok(())
}
/// Updates an item in the cache using the provided function or create a new item with default values
pub fn update_or_init<F>(&mut self, time_now: chrono::NaiveDateTime, key: K, mut func: F)
where
F: FnMut(&mut T),
K: core::fmt::Display,
T: Initable<T, K>,
{
if let Some(pos) = self.find(&key) {
// update existsting item
// unwrap is fine since we fetched the position before
let item = self.items.get_mut(pos).unwrap();
let mut data = item.data.borrow_mut();
func(&mut data);
item.timestamp = time_now;
} else {
// insert new item
self.items.push(CacheItem::new(time_now, T::init(key)));
}
}
/// Cleans items which timed out from the cache
///
/// Call this regularly to prune old items.
pub fn prune(&mut self, time_now: chrono::NaiveDateTime) {
self.items.retain(|i| {
let age = time_now.sub(i.timestamp);
age < self.lifetime
});
}
}

446
src/geo_alg.rs Normal file
View file

@ -0,0 +1,446 @@
//! Geo Algorithms
//!
//! Since the `geo` crate is not `no_std`, we need to implement the most important algorithms on our own
use alloc::string::{String, ToString};
use core::ops::{Mul, Sub};
use log::warn;
#[cfg(not(feature = "std"))]
use num_traits::float::Float;
const EARTH_RADIUS: f64 = 6_371_008.8; // mean radius of GRS80 ellipsoid
const EARTH_CIRCUMFERENCE: f64 = 40_030_228.884; // calculated from `EARTH_RADIUS * 2 * PI`
/// Haversine distance between two geo coordinates (in meters)
#[allow(unused)]
pub fn haversine_dist(a: &geo_types::Point, b: &geo_types::Point) -> f64 {
let theta1 = a.y().to_radians();
let theta2 = b.y().to_radians();
let delta_theta = (b.y() - a.y()).to_radians();
let delta_lambda = (b.x() - a.x()).to_radians();
let a = (delta_theta / 2.).sin().powi(2)
+ theta1.cos() * theta2.cos() * (delta_lambda / 2.).sin().powi(2);
let c = 2. * a.sqrt().asin();
EARTH_RADIUS * c
}
/// Haversine bearing from point `a` to point `b`
#[allow(unused)]
pub fn haversine_bearing(a: &geo_types::Point, b: &geo_types::Point) -> f32 {
let (lng_a, lat_a) = (a.x().to_radians(), a.y().to_radians());
let (lng_b, lat_b) = (b.x().to_radians(), b.y().to_radians());
let delta_lng = lng_b - lng_a;
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let s = (lat_b.cos() * delta_lng.sin()) as f32;
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let c = (lat_a.cos() * lat_b.sin() - lat_a.sin() * lat_b.cos() * delta_lng.cos()) as f32;
let degrees = f32::atan2(s, c).to_degrees();
(degrees + 360.0) % 360.0
}
/// Cartesian distance between to X/Y coordinates (in unit of the coordinates)
#[allow(unused)]
pub fn cartesian_dist(a: geo_types::Point, b: geo_types::Point) -> f64 {
let vector = b.sub(a);
cartesian_norm(vector)
}
/// Cartesian bearing from X/Y point `a` to X/Y point `b`
#[allow(unused)]
pub fn cartesian_bearing(a: geo_types::Point, b: geo_types::Point) -> f64 {
let vector = b.sub(a);
cartesian_theta(vector)
}
/// Cartesian vector norm (length)
#[allow(unused)]
pub fn cartesian_norm(vec: geo_types::Point) -> f64 {
f64::sqrt(vec.x() * vec.x() + vec.y() * vec.y())
}
/// Cartesian vector direction (polar coordinate angle)
#[allow(unused)]
pub fn cartesian_theta(vec: geo_types::Point) -> f64 {
let theta_rad = f64::atan2(vec.y(), vec.x());
theta_rad * 180. / core::f64::consts::PI
}
/// Normalizes an angle (in degrees) to (-180, 180] interval
#[allow(unused)]
pub fn deg_180(angle_deg: f32) -> f32 {
let full_circle_deg = 360.0;
let angle = angle_deg % full_circle_deg;
if angle > full_circle_deg / 2. {
return angle - full_circle_deg;
}
if angle <= -full_circle_deg / 2. {
return angle + full_circle_deg;
}
angle
}
/// Normalizes an angle (in degrees) to [0, 360) interval
#[allow(unused)]
pub fn deg_360(angle_deg: f32) -> f32 {
let full_circle_deg = 360.0;
let angle = angle_deg % full_circle_deg;
if (angle < 0.) {
return angle + full_circle_deg;
}
angle
}
/// Converts a geo-position (lon/lat) to cartesian coordinates relative to a reference positions
///
/// Both `ref_pos` and `position` are expected to be lon/lat in degrees.
/// The output will be in a local X/Y coordinate system with X pointing north and Y pointing east.
#[allow(unused)]
pub fn lonlat_to_dcart(
ref_pos: &geo_types::Point,
position: &geo_types::Point,
) -> geo_types::Point {
let pos_diff = position.sub(*ref_pos);
let dlat_deg = pos_diff.y();
let dlon_deg = pos_diff.x();
// latitude (north/south) degree per meter is independent from longitude
let dx = dlat_deg / 360. * EARTH_CIRCUMFERENCE;
// longitude (east/west) degree per meter has a different value depending on the latitude
#[allow(clippy::cast_possible_truncation)]
let ref_lat_rad = ref_pos.to_radians().y();
let dy = dlon_deg / 360. * (EARTH_CIRCUMFERENCE * ref_lat_rad.cos());
geo_types::Point::new(dx, dy)
}
/// Converts a X/Y coordinate (NED relative to a reference position) to a geo-position
///
/// The `ref_pos` is expected to be lon/lat in degrees and the output will be the same.
/// The `position` is expected to be in a local X/Y coordinate system with X pointing north and Y pointing east.
#[allow(unused)]
pub fn dcart_to_lonlat(
ref_pos: &geo_types::Point,
position: &geo_types::Point,
) -> geo_types::Point {
// note that we're using NED while c-its-parser uses ITS-G5 style ENU X/Y coordinates
// so we need to swap X and Y coordinates
#[allow(clippy::cast_possible_truncation)]
let dx = position.y() as f32;
#[allow(clippy::cast_possible_truncation)]
let dy = position.x() as f32;
c_its_parser::geo_utils::point_from_dxy(dx, dy, ref_pos)
}
/// Describes how a position is related to a line segment
#[derive(Debug, Default)]
#[allow(unused)]
pub struct LinePosition {
/// Own position relative to the line segment
pub semantic: LinePositionType,
/// Distance to center line (always positive)
pub parallel_dist: f64,
/// Distance to first node (negative if `BeforeStart`)
pub perpendicular_dist: f64,
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
#[allow(unused)]
pub enum LinePositionType {
#[default]
Undefined,
BeforeStart,
InBetween,
AfterEnd,
}
#[allow(unused)]
impl LinePosition {
pub fn new(parallel_dist: f64, perpendicular_dist: f64, after_end: bool) -> Self {
let semantic = if after_end {
LinePositionType::AfterEnd
} else if parallel_dist < 0. {
LinePositionType::BeforeStart
} else {
LinePositionType::InBetween
};
Self {
semantic,
parallel_dist,
perpendicular_dist,
}
}
/// Determines `point` position relative to a line segment (in X/Y coordinates)
///
/// The `ls` shall consist of 2 points!
///
/// # Algorithm
/// The line segment can be converted to a vector which describes an infinite line.
/// So we can calculate the (perpendicular) projection of the point on that line:
/// ```text
/// [Case A] (point) | [Case B] (point)
/// | | |
/// | | |
/// (node1)---(node2)-----(q) | (node1)------(q)------(node2)
/// ```
///
/// This will already give us the perpendicular and parallel distance.
/// Since we came from a finite line segment and want to know if the point is bejond the segment,
/// we also need to compare the parallel distance with the length of the vector.
/// So both case are the same mathematically.
///
/// # Errors
/// Fails if points are the same
pub fn for_ls(point: geo_types::Point, line: &geo_types::Line) -> Result<Self, String> {
let node1 = line.start_point();
let node2 = line.end_point();
let line_vec = node2.sub(node1);
let line_vec_length = cartesian_norm(line_vec);
// input validation
if line_vec_length < f64::EPSILON {
return Err("Points are the same".to_string());
}
// calculate projection of `p` onto the line
// ```math
// # n needs to be a unit vector
// n = (node2 - node1) / |node2 - node1|
// # vector from node1 to the point
// p = point - node1
// # projection is the unit-vector multipled by the dot-product of p and n
// q = (p {dot} n) * n
// # perpendicular length is calculated from the vector q->p
// QP = q - p
// ```
let p = point.sub(node1);
let n = line_vec.mul(1. / line_vec_length);
let q = n.mul(p.dot(n));
let qp_length = cartesian_norm(q.sub(p));
// calculate parallel distance of `q` to `node1`.
// This can be achieved by the dot-product since `n` is a unit vector and
// the dot-product of two paralle vectors is the same as the multiplication of the vector norms.
let q_dist = n.dot(q);
// generate output
let after_end = q_dist > line_vec_length;
let line_pos = Self::new(q_dist, qp_length, after_end);
Ok(line_pos)
}
/// Determines `point` position relative to nodes of an ingress `Lane`
///
/// Keep in mind, that the lane's node list starts at the stop line and is extending away from the intersection.
/// But we are driving the other way around!
///
/// # Errors
/// Returns a human-readable string if position can't be determined
pub fn for_ingress_lane(
point: geo_types::Point,
nodes: &geo_types::LineString,
) -> Result<Self, String> {
let mut result = Self::default();
let mut lines = nodes.lines();
// check, that the stop line is not behind us
let line = lines
.next()
.ok_or("Lane has less than two nodes".to_string())?;
let pos = Self::for_ls(point, &line)?;
if pos.semantic == LinePositionType::BeforeStart {
// we have crossed the stop line -> abort search for position inside the lane
return Ok(pos);
}
if pos.semantic == LinePositionType::InBetween {
// we're at this segment -> search finished
return Ok(pos);
}
// otherwise: continue with next segment (and store this segment's length)
let mut lane_length = cartesian_norm(line.delta().into());
let mut last_pos = LinePosition::default();
// continue with remaining lines
for line in lines {
let line_len = cartesian_norm(line.delta().into());
// ignored line, if points are too close <10cm
if line_len < 0.1 {
warn!("Lane nodes are just {line_len:.1} m apart -> ignoring");
continue;
}
let pos = Self::for_ls(point, &line)?;
match pos.semantic {
LinePositionType::BeforeStart => {
// we're at the outer arc of the lane right at the border between line segments
// -> set parallel distance to zero and finish search
return Ok(LinePosition::new(
lane_length,
pos.perpendicular_dist,
false,
));
}
LinePositionType::InBetween => {
// we're at this segment
// -> add segment's distance to accumulated lane length and finish search
return Ok(LinePosition::new(
lane_length + pos.parallel_dist,
pos.perpendicular_dist,
false,
));
}
_ => {
// continue searching, add segment's distance to lane length accumulator
lane_length += line_len;
last_pos = pos;
}
}
}
// if we came this far, we're after the lane's end (actually the start in driving direction)
// -> add parallel distance to accumulated lane length
Ok(LinePosition::new(
lane_length + last_pos.parallel_dist,
last_pos.perpendicular_dist,
true,
))
}
}
#[cfg(test)]
mod tests {
use crate::geo_alg::{LinePosition, cartesian_bearing};
#[test]
fn test_cart_bearing() {
let start = geo_types::Point::new(0., 0.);
assert_float_eq::assert_float_absolute_eq!(
0.,
cartesian_bearing(start, geo_types::Point::new(10., 0.))
);
assert_float_eq::assert_float_absolute_eq!(
90.,
cartesian_bearing(start, geo_types::Point::new(0., 10.))
);
assert_float_eq::assert_float_absolute_eq!(
45.,
cartesian_bearing(start, geo_types::Point::new(10., 10.))
);
assert_float_eq::assert_float_absolute_eq!(
135.,
cartesian_bearing(start, geo_types::Point::new(-10., 10.))
);
assert_float_eq::assert_float_absolute_eq!(
-135.,
cartesian_bearing(start, geo_types::Point::new(-10., -10.))
);
}
#[test]
fn linepos_for_ls_validation() {
// identical nodes shall result in an error
let point = geo_types::Point::new(0., 0.);
let node1 = geo_types::Point::new(0., 0.);
let node2 = geo_types::Point::new(10., -5.);
let ls = geo_types::Line::new(node1, node1);
assert!(LinePosition::for_ls(point, &ls).is_err());
let ls = geo_types::Line::new(node2, node2);
assert!(LinePosition::for_ls(point, &ls).is_err());
}
#[test]
fn linepos_for_ls_test() {
use crate::geo_alg::LinePositionType;
// test on X axis
{
let node1 = geo_types::Point::new(0., 0.);
let node2 = geo_types::Point::new(5., 0.);
let ls = geo_types::Line::new(node1, node2);
// test some positions
let test_tuples = vec![
(node1.x() - 1000., LinePositionType::BeforeStart),
(node1.x() - 1., LinePositionType::BeforeStart),
(node1.x(), LinePositionType::InBetween),
(node1.x() + 1., LinePositionType::InBetween),
(node2.x() - 1., LinePositionType::InBetween),
(node2.x(), LinePositionType::InBetween),
(node2.x() + 1., LinePositionType::AfterEnd),
(node2.x() + 1000., LinePositionType::AfterEnd),
];
let y_values = vec![(node1.y() - 1.), (node1.y()), (node1.y() + 1.)];
// y position should only affect the perpendicular distance
for y in &y_values {
// x position affects the position type and parallel distance
for (x, semantic) in &test_tuples {
let point = geo_types::Point::new(*x, *y);
let result = LinePosition::for_ls(point, &ls).unwrap();
assert_eq!(semantic, &result.semantic);
assert_eq!((y - node1.y()).abs(), result.perpendicular_dist);
assert_eq!(x - node1.x(), result.parallel_dist);
}
}
}
// test on Y axis
{
let node1 = geo_types::Point::new(3., 5.);
let node2 = geo_types::Point::new(3., 100.);
let ls = geo_types::Line::new(node1, node2);
// test some positions
let test_tuples = vec![
(node1.y() - 1000., LinePositionType::BeforeStart),
(node1.y() - 1., LinePositionType::BeforeStart),
(node1.y(), LinePositionType::InBetween),
(node1.y() + 1., LinePositionType::InBetween),
(node2.y() - 1., LinePositionType::InBetween),
(node2.y(), LinePositionType::InBetween),
(node2.y() + 1., LinePositionType::AfterEnd),
(node2.y() + 1000., LinePositionType::AfterEnd),
];
let x_values = vec![(node1.x() - 1.), (node1.x()), (node1.x() + 1.)];
// x position should only affect the perpendicular distance
for x in &x_values {
// y position affects the position type and parallel distance
for (y, semantic) in &test_tuples {
let point = geo_types::Point::new(*x, *y);
let result = LinePosition::for_ls(point, &ls).unwrap();
assert_eq!(semantic, &result.semantic);
assert_eq!((x - node1.x()).abs(), result.perpendicular_dist);
assert_eq!(y - node1.y(), result.parallel_dist);
}
}
}
}
}

515
src/io.rs Normal file
View file

@ -0,0 +1,515 @@
//! UART/ BLE I/O Protocol
pub mod msg {
#![allow(clippy::all, clippy::pedantic, clippy::nursery, dead_code)]
include!(concat!(env!("OUT_DIR"), "/c_its_io.rs"));
}
use alloc::string::ToString;
use alloc::vec::Vec;
use c_its_parser::gn as geonetworking;
use log::error;
impl From<msg::RawMsgRx> for msg::SerialOutputMsg {
fn from(value: msg::RawMsgRx) -> Self {
use msg::serial_output_msg::Payload;
Self {
payload: Some(Payload::RxMsg(value)),
}
}
}
impl From<msg::DenmEvent> for msg::SerialOutputMsg {
fn from(value: msg::DenmEvent) -> Self {
use msg::serial_output_msg::Payload;
Self {
payload: Some(Payload::Denm(value)),
}
}
}
impl msg::RawMsgRx {
pub fn serialize_uart_proto(&self) -> Vec<u8> {
serialize_serial_msg(&Into::<msg::SerialOutputMsg>::into(self.clone()))
}
}
impl msg::DenmEvent {
#[allow(unused)]
pub fn serialize_uart_proto(&self) -> Vec<u8> {
serialize_serial_msg(&Into::<msg::SerialOutputMsg>::into(*self))
}
}
#[derive(Debug, Default)]
pub struct Parser {
buffer: alloc::vec::Vec<u8>,
}
impl Parser {
const SOF_LEN: u16 = 4;
const SOF1: u8 = 0x56;
const SOF2: u8 = 0x32;
const SOF3: u8 = 0x58;
const SOF4: u8 = 0x2B;
const EOF_LEN: u16 = 2;
#[allow(unused)]
const EOF1: u8 = 0x0d;
#[allow(unused)]
const EOF2: u8 = 0x0a;
const LENGTH_LEN: u16 = 2;
/// Parses incoming data and return list of messages
pub fn parse(&mut self, input: &[u8]) -> alloc::vec::Vec<msg::SerialInputMsg> {
use prost::Message as _;
// combine with existing data
self.buffer.append(&mut input.to_vec());
// find start of frame and parse
let mut output = alloc::vec![];
while let Some(pos) = Self::find_sof(&self.buffer) {
// drop everything before the SOF
self.buffer.drain(..pos);
// get length field and check buffer size
if let Some(len_bytes) = self
.buffer
.get(usize::from(Self::SOF_LEN)..usize::from(Self::SOF_LEN + Self::LENGTH_LEN))
{
// unwrap is fine since we queried 2 bytes before, so we can also convert to 2-byte slice
let msg_size = u16::from_be_bytes(len_bytes.try_into().unwrap());
let required_buf_size = Self::SOF_LEN + Self::LENGTH_LEN + msg_size + Self::EOF_LEN;
if let Some(payload) = self.buffer.get(
usize::from(Self::SOF_LEN + Self::LENGTH_LEN)
..usize::from(required_buf_size - 2),
) {
match msg::SerialInputMsg::decode(payload) {
Ok(data) => {
output.push(data);
}
Err(err) => error!("Failed to parse input: {err:?}"),
}
// consume buffer
self.buffer.drain(..usize::from(required_buf_size));
} else {
break;
}
}
}
output
}
/// Returns the start index of the SOF sequence, if present
fn find_sof(input: &[u8]) -> Option<usize> {
// search for SOF
if let Some(sof1_pos) = input.iter().position(|i| *i == Self::SOF1) {
// drop everything before sof1_pos and check if remainder continues with SOF sequence
let (_, msg) = input.split_at(sof1_pos);
if msg.starts_with(&[Self::SOF1, Self::SOF2, Self::SOF3, Self::SOF4]) {
// found the SOF sequence
return Some(sof1_pos);
}
}
// if we reach this position, SOF sequence wasn't found
None
}
}
fn serialize_serial_msg<T>(msg: &T) -> Vec<u8>
where
T: prost::Message,
{
let mut msg_buf = msg.encode_to_vec();
let sof = [0x56u8, 0x32, 0x58, 0x2B];
let eof = [0x0du8, 0x0a];
#[allow(clippy::cast_possible_truncation)]
let len: [u8; 2] = (msg_buf.len() as u16).to_be_bytes();
let mut out = sof.to_vec();
out.extend_from_slice(&len);
out.append(&mut msg_buf);
out.extend_from_slice(&eof);
out
}
impl From<msg::PositionState> for crate::applogic::GnssFix {
fn from(value: msg::PositionState) -> Self {
let time =
chrono::DateTime::from_timestamp_millis(value.timestamp_ms).map(|v| v.naive_utc());
let latitude_deg = value.position.latitude_deg.into();
let longitude_deg = value.position.longitude_deg.into();
let heading_deg = value.heading.map(|v| {
#[allow(clippy::cast_precision_loss)]
let val = (v as f32) / 10.;
val
});
let speed_mps = value.speed.map(|v| {
#[allow(clippy::cast_precision_loss)]
let val = (v as f32) / 100.;
val
});
Self {
time,
latitude_deg,
longitude_deg,
heading_deg,
speed_mps,
}
}
}
impl From<msg::PositionState> for esp32_cits_core::PosVel {
fn from(value: msg::PositionState) -> Self {
let position = geo_types::Point::new(
value.position.longitude_deg.into(),
value.position.latitude_deg.into(),
);
let heading_deg = value.heading.map(|v| {
#[allow(clippy::cast_precision_loss)]
let val = (v as f32) / 10.;
val
});
let speed_mps = value.speed.map(|v| {
#[allow(clippy::cast_precision_loss)]
let val = (v as f32) / 100.;
val
});
Self {
position,
heading_deg,
speed_mps,
}
}
}
impl From<msg::ItsPosition> for esp32_cits_core::PosVel {
fn from(value: msg::ItsPosition) -> Self {
let latitude_deg = f64::from(value.latitude) / 10_000_000.;
let longitude_deg = f64::from(value.longitude) / 10_000_000.;
let position = geo_types::Point::new(longitude_deg, latitude_deg);
Self {
position,
heading_deg: None,
speed_mps: None,
}
}
}
impl esp32_cits_core::tx::GnItsPayload for msg::RawMsgTx {
fn make_eh(
&self,
address: [u8; 6],
own_position: &esp32_cits_core::PosVel,
time: chrono::DateTime<chrono::Utc>,
seq_no: &mut u16,
) -> Result<
(
c_its_parser::gn::en302636_4_1::ExtendedHeader,
Option<c_its_parser::gn::en302636_4_1::AreaType>,
u8,
),
alloc::string::String,
> {
let station_type = self
.station_type
.and_then(|v| {
#[allow(clippy::cast_possible_truncation)]
let raw_val = v as u8;
geonetworking::en302636_4_1::StationType::try_from(raw_val).ok()
})
.unwrap_or_default();
#[allow(clippy::cast_possible_truncation)]
let hop_limit = self.hop_limit as u8;
Ok(match self.gn_transport() {
msg::GnTransport::Gac => {
if self.gn_area.position.is_none() {
return Err("Failed to create GAC: gnArea.position missing".to_string());
}
*seq_no += 1;
let (atype, geoanycast) = Self::make_geobcast(
address,
station_type,
own_position,
time,
self.gn_area,
*seq_no,
)?;
let eh = geonetworking::en302636_4_1::ExtendedHeader::GAC(geoanycast);
(eh, Some(atype), hop_limit)
}
msg::GnTransport::Gbc => {
if self.gn_area.position.is_none() {
return Err("Failed to create GAC: gnArea.position missing".to_string());
}
*seq_no += 1;
let (atype, geoanycast) = Self::make_geobcast(
address,
station_type,
own_position,
time,
self.gn_area,
*seq_no,
)?;
let eh = geonetworking::en302636_4_1::ExtendedHeader::GBC(geoanycast);
(eh, Some(atype), hop_limit)
}
msg::GnTransport::Tsb => {
*seq_no += 1;
(
esp32_cits_core::tx::gn::make_tsb_eh(
address,
station_type,
own_position,
time,
*seq_no,
)?,
None,
hop_limit,
)
}
msg::GnTransport::Shb => (
esp32_cits_core::tx::gn::make_shb_eh(address, station_type, own_position, time)?,
None,
1,
),
})
}
fn make_payload(
&self,
) -> Result<(Vec<u8>, c_its_parser::standards::extensions::ItsMessageId), alloc::string::String>
{
#[allow(clippy::cast_possible_truncation)]
let message_id = (self.message_type as u8).try_into()?;
Ok((self.payload.clone(), message_id))
}
}
impl msg::RawMsgTx {
fn make_geobcast(
address: [u8; 6],
station_type: geonetworking::en302636_4_1::StationType,
own_position: &esp32_cits_core::PosVel,
time: chrono::DateTime<chrono::Utc>,
gn_area: msg::GnArea,
sequence_number: u16,
) -> Result<
(
c_its_parser::gn::en302636_4_1::AreaType,
geonetworking::en302636_4_1::GeoBroadcast,
),
alloc::string::String,
> {
let source_position_vector =
esp32_cits_core::tx::gn::make_lpv(address, station_type, own_position, time)?;
let (atype, dist_a, dist_b, angle) = match gn_area.area() {
msg::GnAreaShape::EtsiAreashapeCircle => (
c_its_parser::gn::en302636_4_1::AreaType::Circular,
gn_area.dist_a,
None,
None,
),
msg::GnAreaShape::EtsiAreashapeRectangle => (
c_its_parser::gn::en302636_4_1::AreaType::Rectangular,
gn_area.dist_a,
gn_area.dist_b,
gn_area.angle,
),
msg::GnAreaShape::EtsiAreashapeEllipsis => (
c_its_parser::gn::en302636_4_1::AreaType::Ellipsoidal,
gn_area.dist_a,
gn_area.dist_b,
gn_area.angle,
),
};
#[allow(clippy::cast_possible_truncation)]
let latitude_deg = own_position.position.y() as f32;
#[allow(clippy::cast_possible_truncation)]
let longitude_deg = own_position.position.x() as f32;
let distance_a = dist_a
.map(|v| {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let val = v as u16;
val
})
.unwrap_or_default();
let distance_b = dist_b
.map(|v| {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let val = v as u16;
val
})
.unwrap_or_default();
let angle = angle
.map(|v| {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let val = v as u16;
val
})
.unwrap_or_default();
let gac = geonetworking::en302636_4_1::GeoAnycast::try_from_values(
sequence_number,
source_position_vector,
latitude_deg,
longitude_deg,
distance_a,
distance_b,
angle,
)
.map_err(|err| alloc::format!("{err}"))?;
Ok((atype, gac))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::init_test_env_logger;
impl From<msg::RawMsgTx> for msg::SerialInputMsg {
fn from(value: msg::RawMsgTx) -> Self {
use msg::serial_input_msg::Payload;
Self {
payload: Some(Payload::TxMsg(value)),
}
}
}
impl From<msg::PositionState> for msg::SerialInputMsg {
fn from(value: msg::PositionState) -> Self {
use msg::serial_input_msg::Payload;
Self {
payload: Some(Payload::Position(value)),
}
}
}
// manual example data:
// SerialInputMsg(RawMsgTx): 5632582b0040123e0a340202c173256817284029d5b3400f1feaf01ffffffc23b7743e00000fc0007e8138250737feebfff600000dffff7ffff1ce40400010011807220028020d0a
// SerialInputMsg(PositionState): 5632582b00170a1509a0bbe2a29f010000120a0d00002842150000b8410d0a
#[test]
fn uart_parser() {
// generate some test UART input messages
let raw_tx = msg::RawMsgTx {
payload: crate::testdata::CAM_SMALL.to_vec(),
message_type: 1,
gn_transport: msg::GnTransport::Shb.into(),
gn_area: msg::GnArea {
area: None,
position: None,
dist_a: None,
dist_b: None,
angle: None,
},
hop_limit: 2,
station_type: None,
};
let raw_tx_buf = serialize_serial_msg::<msg::SerialInputMsg>(&raw_tx.clone().into());
let pos_state = msg::PositionState {
timestamp_ms: 1785144196000,
position: msg::Position {
latitude_deg: 42.,
longitude_deg: 23.,
},
heading: None,
speed: None,
};
let pos_state_buf = serialize_serial_msg::<msg::SerialInputMsg>(&pos_state.clone().into());
let mut parser = Parser::default();
// feed individual complete messages to parser
let output = parser.parse(&raw_tx_buf);
assert_eq!(1, output.len());
assert_eq!(
&msg::SerialInputMsg::from(raw_tx.clone()),
output.get(0).unwrap()
);
let output = parser.parse(&pos_state_buf);
assert_eq!(1, output.len());
assert_eq!(
&msg::SerialInputMsg::from(pos_state.clone()),
output.get(0).unwrap()
);
// feed multiple complete messages to parser
let mut combined_buf = pos_state_buf.clone();
combined_buf.extend_from_slice(&raw_tx_buf);
let output = parser.parse(&combined_buf);
assert_eq!(2, output.len());
assert_eq!(
&msg::SerialInputMsg::from(pos_state.clone()),
output.get(0).unwrap()
);
assert_eq!(
&msg::SerialInputMsg::from(raw_tx.clone()),
output.get(1).unwrap()
);
// feed partial messages to parser
let buf1 = &raw_tx_buf[0..3];
let buf2 = &raw_tx_buf[3..];
let output = parser.parse(buf1);
assert_eq!(0, output.len());
let output = parser.parse(buf2);
assert_eq!(1, output.len());
assert_eq!(
&msg::SerialInputMsg::from(raw_tx.clone()),
output.get(0).unwrap()
);
// feed messages with garbage in between
let mut buf1 = vec![0xabu8, 0xcd]; // prepend some garbage
buf1.extend_from_slice(&raw_tx_buf[0..3]);
let mut buf2 = (&raw_tx_buf[3..]).to_vec();
buf2.extend_from_slice(&[0xfeu8, 0x42]); // append some garbage
buf2.extend_from_slice(&pos_state_buf); // append second message
buf2.extend_from_slice(&[0x11u8, 0x22]); // append some more garbage
let output = parser.parse(&buf1);
assert_eq!(0, output.len());
let output = parser.parse(&buf2);
assert_eq!(2, output.len());
assert_eq!(
&msg::SerialInputMsg::from(raw_tx.clone()),
output.get(0).unwrap()
);
assert_eq!(
&msg::SerialInputMsg::from(pos_state.clone()),
output.get(1).unwrap()
);
}
}

891
src/main.rs Normal file
View file

@ -0,0 +1,891 @@
#![no_std]
#![no_main]
#![deny(
clippy::mem_forget,
reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
holding buffers for the duration of a data transfer."
)]
// #![deny(clippy::large_stack_frames)]
// we can't use UART I/O and GNSS at the same time
#[cfg(all(feature = "uart", feature = "gnss_ubx", feature = "gnss_l67k"))]
compile_error!(
"feature \"uart\" and features \"gnss_ubx\" or \"gnss_l67k\" cannot be enabled at the same time"
);
// autonomous CAM generation need a time and position source (aka _gnss)
#[cfg(all(feature = "cam_tx", not(feature = "_gnss")))]
compile_error!("feature \"cam_tx\" needs a time and position source (either GNSS or UART)");
// we can't have both GNSS drivers enabled at the same time
#[cfg(all(feature = "gnss_ubx", feature = "gnss_l67k"))]
compile_error!("feature \"gnss_ubx\" and feature \"gnss_l67k\" cannot be enabled at the same time");
use alloc::string::ToString as _;
use alloc::vec::Vec;
use core::cell::RefCell;
#[cfg(feature = "spat")]
use core::cell::UnsafeCell;
use c_its_parser::gn as geonetworking;
#[cfg(feature = "cam_tx")]
use c_its_parser::standards::extensions::ItsStationType;
use critical_section::Mutex;
use crossbeam_queue::ArrayQueue;
use embassy_executor::Spawner;
#[cfg(feature = "gnss_l67k")]
use embassy_gps::gps::l76k;
use esp_backtrace as _;
use esp_hal::clock::CpuClock;
use esp_hal::timer::timg::TimerGroup;
#[cfg(all(target_arch = "riscv32", feature = "spat"))]
use esp_println::println;
use esp32_cits_core::radio;
use geonetworking::Decode as _;
use log::{error, info, warn};
extern crate alloc;
mod applogic;
mod cache;
mod geo_alg;
#[cfg(feature = "uart")]
mod io;
#[cfg(feature = "screen")]
mod screen;
#[cfg(feature = "gnss_ubx")]
mod ubx;
const WIFI_CHANNEL: radio::Channel = 180;
const WIFI_TXPOWER: i8 = 10; // dBm, 2..20
#[cfg(feature = "spat")]
const SPAT_RATE_LIMIT: u8 = 5; // keep every n-th message
#[cfg(any(feature = "cam_tx", feature = "uart"))]
const GN_IS_MOBILE: bool = true;
#[cfg(feature = "cam_tx")]
const OWN_STATION_TYPE: ItsStationType = ItsStationType::Cyclist;
#[cfg(feature = "cam_tx")]
const OWN_VEHICLE_WIDTH: u8 = 5; // in 10cm steps!
#[cfg(feature = "cam_tx")]
const OWN_VEHICLE_LENGTH: u16 = 20; // in 10cm steps!
#[cfg(feature = "_uart")]
const SERIAL_FIFO_SIZE_THLD: u8 = 100; // hardware has max. 128 byte
// This creates a default app-descriptor required by the esp-idf bootloader.
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!();
#[cfg(feature = "_gnss")]
static GNSS_UPDATE: Mutex<RefCell<Option<applogic::GnssFix>>> = Mutex::new(RefCell::new(None));
static WIFI_RX_QUEUE: Mutex<RefCell<Option<ArrayQueue<Vec<u8>>>>> = Mutex::new(RefCell::new(None));
#[cfg(feature = "_uart")]
static UART_RX_BUF: Mutex<RefCell<Option<Vec<u8>>>> = Mutex::new(RefCell::new(None));
#[cfg(feature = "_uart")]
static SERIAL: Mutex<RefCell<Option<esp_hal::uart::Uart<'static, esp_hal::Blocking>>>> =
Mutex::new(RefCell::new(None));
#[allow(
clippy::large_stack_frames,
reason = "it's not unusual to allocate larger buffers etc. in main"
)]
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
// generator version: 1.3.0
// generator parameters: --chip esp32c5 -o esp32c5-wroom-1-psram -o alloc -o log -o unstable-hal -o wifi -o esp-backtrace -o embassy
esp_println::logger::init_logger_from_env();
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
// The following pins are used to bootstrap the chip. They are available
// for use, but check the datasheet of the module for more information on them.
// - GPIO2
// - GPIO3
// - GPIO7
// - GPIO25
// - GPIO26
// - GPIO27
// - GPIO28
esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 65536);
esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram);
let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_interrupt =
esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);
info!("Embassy initialized!");
// Setup ST7789 screen
// SCL: D8/ GPIO8 (SPI_SCK)
// SDA: D10/ GPIO10 (SPI_MOSI)
// RES: D3/ GPIO7
// DC: D4/ GPIO23 (display clear)
// CS: D5/ GPIO24 (chip select)
// BL: D1/ GPIO0 (backlight)
#[cfg(feature = "screen")]
let mut spi_buffer = [0_u8; 512];
#[cfg(feature = "screen")]
let mut screen = {
// 172*320px in landscape orientation
let (display, width, height) = screen::make_small_display(
peripherals.GPIO8,
peripherals.GPIO10,
peripherals.GPIO7,
peripherals.GPIO23,
peripherals.GPIO24,
peripherals.GPIO0,
peripherals.SPI2,
&mut spi_buffer,
)
.expect("Fatal error building screen");
let mut screen = screen::Handler::new(display, width, height);
let _ = screen
.update(screen::ScreenState::Initial)
.inspect_err(log_screen_error);
screen
};
// Setup UART I/O channel
#[cfg(feature = "uart")]
setup_uart0(
115_200,
Some(b'\n'),
peripherals.GPIO12,
peripherals.GPIO11,
peripherals.UART0,
serial_handler,
)
.expect("Failed to initialize UART");
// Setup WiFi
// sadly, it doesn't work any more when we move `esp_radio::wifi::new` to `setup_wifi_sniffer`
let (mut wifi_controller, interfaces) = esp_radio::wifi::new(
peripherals.WIFI,
esp_radio::wifi::ControllerConfig::default(),
)
.expect("Failed to initialize Wi-Fi controller");
if let Err(err) = wifi_controller.set_band_mode(esp_radio::wifi::BandMode::_5G) {
error!("Failed to set WiFi to 5GHz only: {err}");
}
if let Err(err) = wifi_controller.set_max_tx_power(WIFI_TXPOWER * 4) {
error!("Failed to set WiFi TX power: {err}");
}
critical_section::with(|cs| {
WIFI_RX_QUEUE.borrow(cs).replace(Some(ArrayQueue::new(2)));
});
let mut wlan_iface = interfaces.sniffer;
radio::setup_wifi_sniffer(WIFI_CHANNEL, &mut wlan_iface, handle_frame)
.expect("Fatal error initializing 802.11p sniffer");
info!("WiFi promiscuous mode on channel {WIFI_CHANNEL} running");
let mac = esp_hal::efuse::base_mac_address();
info!("WiFi MAC: {mac}");
// Setup GNSS:
// Seeed XIAO L67K:
// RX: D7/ GPIO12
// TX: D6/ GPIO11
// WAKEUP: D0/ GPIO1 -> HIGH for active, LOW for Sleep
// RESET: D2/ GPIO25 -> HIGH for normal, LOW for Reset
#[cfg(feature = "gnss_l67k")]
spawner.spawn(
gnss_l67k_task(
peripherals.GPIO1,
peripherals.GPIO25,
peripherals.UART0,
peripherals.GPIO12,
peripherals.GPIO11,
)
.expect("Failed to spawn GNSS task"),
);
// U-Blox module:
// RX: D7/ GPIO12
// TX: D6/ GPIO11
// RESET: D2/ GPIO25 -> HIGH for normal, LOW for Reset
#[cfg(feature = "gnss_ubx")]
{
setup_uart0(
9600,
Some(b'\n'),
peripherals.GPIO12,
peripherals.GPIO11,
peripherals.UART0,
serial_handler,
)
.expect("Failed to initialize UART for GNSS");
// do some settings (UART receive and parsing runs in main loop)
spawner
.spawn(gnss_ubx_config(peripherals.GPIO25).expect("Failed to spawn GNSS config task"));
}
#[cfg(feature = "_gnss")]
info!("GNSS task started, waiting for GNSS fix...");
#[cfg(any(feature = "cam_tx", feature = "uart"))]
let (mut seq_no, mac_bytes) = {
// unwrap is fine since [`esp_hal::efuse::MacAddress`] is just an `[u8; 6]`
let mac_bytes = mac.as_bytes().try_into().unwrap();
(0, mac_bytes)
};
let mut state = applogic::State::new();
#[cfg(feature = "cam_tx")]
let mut cam = {
let station_id = make_station_id(mac);
info!("V2X Station ID: {station_id}");
applogic::cam_tx::CamState::new(
station_id,
OWN_STATION_TYPE,
OWN_VEHICLE_LENGTH,
OWN_VEHICLE_WIDTH,
)
};
#[cfg(feature = "uart")]
let mut uart_input = io::Parser::default();
#[cfg(feature = "gnss_ubx")]
let mut ubx_parser = ublox::Parser::default();
loop {
embassy_time::Timer::after(embassy_time::Duration::from_millis(1)).await;
#[cfg(feature = "_uart")]
{
let mut new_data = None;
critical_section::with(|cs| {
let mut buf_rc = UART_RX_BUF.borrow(cs).borrow_mut();
// unwrap is fine b/c we stored something in it before
let buf = buf_rc.as_mut().unwrap();
// we move the buffer to new_data to do the processing outside of the critical section lock
if !buf.is_empty() {
new_data = Some(core::mem::take(buf));
}
});
if let Some(data) = new_data {
#[cfg(feature = "uart")]
{
for msg in uart_input.parse(&data) {
match msg.payload {
Some(io::msg::serial_input_msg::Payload::Position(fix)) => {
let pos_state = fix.into();
critical_section::with(|cs| {
let gnss_update_ref = GNSS_UPDATE.borrow(cs);
gnss_update_ref.replace(Some(pos_state));
});
}
Some(io::msg::serial_input_msg::Payload::TxMsg(tx_request)) => {
use esp32_cits_core::tx::GnItsPayload as _;
info!(
"UART: Got new TX message: Type {} with {} bytes",
tx_request.message_type,
tx_request.payload.len()
);
let own_position = state.pos();
let time = state.time().and_utc();
match tx_request.encode_packet(
mac_bytes,
own_position,
time,
GN_IS_MOBILE,
&mut seq_no,
) {
Ok(data) => {
if let Err(err) = radio::send_80211_bcast_frame(
&mut wlan_iface,
&mac_bytes.clone(),
&data,
) {
warn!("Failed to send 802.11 frame: {err}");
}
}
Err(err) => warn!("Failed to create GN message: {err}"),
}
}
None => {}
}
}
}
#[cfg(feature = "gnss_ubx")]
{
let mut it = ubx_parser.consume_ubx(&data);
loop {
match it.next() {
Some(Ok(ublox::UbxPacket::Proto27(packet_ref))) => match &packet_ref {
ublox::proto27::PacketRef::MonVer(packet) => {
println!("{}", ubx::pretty_print_mon_ver(packet));
}
ublox::proto27::PacketRef::MonComms(packet) => {
println!("{}", ubx::pretty_print_mon_comms(packet));
}
ublox::proto27::PacketRef::MonRf(packet) => {
println!("{}", ubx::pretty_print_mon_rf(packet));
}
ublox::proto27::PacketRef::NavSat(packet) => {
println!("{}", ubx::pretty_print_nav_sat(packet));
}
ublox::proto27::PacketRef::NavPvt(pvt) => {
match applogic::GnssFix::try_from(pvt) {
Ok(pos_state) => {
critical_section::with(|cs| {
let gnss_update_ref = GNSS_UPDATE.borrow(cs);
gnss_update_ref.replace(Some(pos_state));
});
}
Err(err) => warn!("{err}"),
}
}
_ => {
println!("UBX: {packet_ref:?}");
}
},
Some(Err(_)) => {
// Received a malformed packet
}
None => {
// The internal buffer is now empty
break;
}
}
}
}
}
}
#[cfg(feature = "_gnss")]
{
let mut new_position = false;
critical_section::with(|cs| {
let gnss_update_ref = GNSS_UPDATE.borrow(cs);
if let Some(fix) = gnss_update_ref.replace(None) {
state.update_with_gpsfix(&fix);
new_position = true;
#[cfg(feature = "cam_tx")]
{
use esp32_cits_core::tx::GnItsPayload as _;
let time = state.time().and_utc();
cam.update(time, fix.clone().into());
match cam.encode_packet(
mac_bytes,
&fix.into(),
time,
GN_IS_MOBILE,
&mut seq_no,
) {
Ok(data) => {
if let Err(err) = radio::send_80211_bcast_frame(
&mut wlan_iface,
&mac_bytes.clone(),
&data,
) {
warn!("Failed to send 802.11 frame: {err}");
}
}
Err(err) => warn!("Failed to create CAM: {err}"),
}
}
}
});
if new_position {
info!("{}", state.print_fix());
// run GLOSA algorithm
#[cfg(feature = "spat")]
{
let glosa_data = state.run_glosa();
if let applogic::GlosaOutput::Locked(data) = &glosa_data {
for sig in &data.signal_groups {
let duration_str = sig
.end_sec
.map(|v| alloc::format!("for {v} s"))
.unwrap_or_default();
println!("GLOSA: {} {} {duration_str}", sig.maneuver, sig.phase);
}
}
#[cfg(feature = "screen")]
let _ = screen
.update(glosa_data.into())
.inspect_err(log_screen_error);
}
}
}
let mut new_data = None;
critical_section::with(|cs| {
let queue_rc = WIFI_RX_QUEUE.borrow(cs).borrow();
// unwrap is fine b/c we stored something in it before
let queue = queue_rc.as_ref().unwrap();
if let Some(data) = queue.pop() {
new_data = Some(data);
}
});
if let Some(data) = new_data {
match c_its_parser::de::decode(&data, c_its_parser::Headers::None) {
Ok(msg) => {
use c_its_parser::ItsMessage;
// use c_its_parser::standards::extensions::ItsMessageId;
// info!("Got new {:?} message", ItsMessageId::from(&msg));
match msg {
#[cfg(feature = "spat")]
ItsMessage::Mapem {
geonetworking: _,
transport: _,
etsi,
} => state.handle_mapem(&etsi),
#[cfg(feature = "spat")]
ItsMessage::Spatem {
geonetworking: _,
transport: _,
etsi,
} => {
if state.initialized() {
state.handle_spatem(&etsi);
}
}
#[cfg(any(feature = "cam", feature = "cam_tx"))]
ItsMessage::Cam {
geonetworking: _,
transport: _,
etsi,
} => {
#[cfg(feature = "cam")]
applogic::cam::handle_cam(&etsi);
}
#[cfg(feature = "denm")]
ItsMessage::DenmV2 {
geonetworking: _,
transport: _,
etsi,
} => applogic::denm::handle_denm(&etsi),
#[cfg(feature = "denm")]
ItsMessage::DenmV1 {
geonetworking: _,
transport: _,
etsi: _,
} => {
// irrelevant since all received DENMs should be parsed as v2
}
}
}
Err(err) => {
// we already filtered out all unsupported message types in the wifi rx callback
// so these are actual errors
warn!("Failed to parse V2X message: {err}");
}
}
}
// maintenance
state.prune();
}
}
#[allow(
clippy::needless_pass_by_value,
reason = "adhering to callback interface"
)]
#[allow(clippy::too_many_lines)]
fn handle_frame(frame: esp_radio::wifi::sniffer::PromiscuousPkt<'_>) {
#[cfg(feature = "spat")]
const PRUNE_INTERVAL: chrono::Duration = chrono::Duration::seconds(60);
#[cfg(feature = "spat")]
static mut RATELIMIT_CACHE_LAST_PRUNE: UnsafeCell<chrono::NaiveDateTime> =
UnsafeCell::new(chrono::NaiveDateTime::MIN);
#[cfg(feature = "spat")]
static mut SPAT_RATELIMIT_CACHE: UnsafeCell<cache::Cache<u32, RatelimitData>> = UnsafeCell::new(
cache::Cache::<u32, RatelimitData>::new(chrono::Duration::seconds(20)),
);
#[cfg(feature = "spat")]
let now = {
let time = esp_hal::time::Instant::now().duration_since_epoch();
// unwrap is fine since value range is u64 microseconds
chrono::DateTime::from_timestamp(time.as_secs().cast_signed(), 0)
.unwrap()
.naive_utc()
};
match esp32_cits_core::rx::is_broadcast_frame(&frame) {
Err(err) => {
warn!("{err}");
return;
}
Ok(false) => {
// drop frame
return;
}
Ok(true) => {
// continue
}
}
// info!("Received frame with {} bytes", frame.len);
// parse GN headers
match c_its_parser::pcap::remove_wlan_headers(frame.data)
.map_err(|err| alloc::format!("Failed to parse WLAN headers: {err}"))
.and_then(|data| {
geonetworking::Packet::decode(data)
.map_err(|err| alloc::format!("Failed to parse GN headers: {err:?}"))
}) {
Err(err) => {
warn!("{err}");
}
Ok(packet) => {
// drop hopped (for now)
if packet.decoded.is_hopped() {
return;
}
// "parse" ITS PDU Header
if let Ok((payload, message_id, station_id)) = get_its_header(&packet) {
// drop all unsupported message IDs and rate-limit SPATEMs
if match message_id {
#[cfg(feature = "denm")]
c_its_parser::standards::extensions::ItsMessageId::Denm => false,
#[cfg(feature = "cam")]
c_its_parser::standards::extensions::ItsMessageId::Cam => false,
#[cfg(feature = "spat")]
c_its_parser::standards::extensions::ItsMessageId::Spatem => {
// reduce SPAT rate per station ID
let mut drop_msg = false;
unsafe {
#[allow(
static_mut_refs,
reason = "this function will only run consecutive"
)]
let spat_cache = SPAT_RATELIMIT_CACHE.get();
(*spat_cache).update_or_init(now, station_id, |i| {
i.msg_count = i.msg_count.wrapping_add(1);
if (i.msg_count % SPAT_RATE_LIMIT) > 0 {
drop_msg = true;
}
});
}
drop_msg
}
#[cfg(feature = "spat")]
c_its_parser::standards::extensions::ItsMessageId::Mapem => false,
_ => true,
} {
return;
}
// Send data to main thread
critical_section::with(|cs| {
let queue_rc = WIFI_RX_QUEUE.borrow(cs).borrow();
// unwrap is fine b/c we stored something in it before
let queue = queue_rc.as_ref().unwrap();
if queue.push(payload.to_vec()).is_err() {
error!("V2X RX queue is full");
}
});
// Send data to UART
#[cfg(feature = "uart")]
{
let message = io::msg::RawMsgRx {
msg_type: message_id.as_u8().into(),
payload: frame.data.to_vec(),
}
.serialize_uart_proto();
send_uart0(&message);
}
}
}
}
#[cfg(feature = "spat")]
unsafe {
#[allow(static_mut_refs, reason = "this function will only run consecutive")]
let last_prune_time = RATELIMIT_CACHE_LAST_PRUNE.get();
if *last_prune_time + PRUNE_INTERVAL < now {
#[allow(static_mut_refs, reason = "this function will only run consecutive")]
let spat_cache = SPAT_RATELIMIT_CACHE.get();
(*spat_cache).prune(now);
*last_prune_time = now;
}
}
}
#[cfg(feature = "spat")]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct RatelimitData {
station_id: u32,
msg_count: u8,
}
#[cfg(feature = "spat")]
impl cache::Cachable<u32> for RatelimitData {
fn key(&self) -> u32 {
self.station_id
}
}
#[cfg(feature = "spat")]
impl cache::Initable<Self, u32> for RatelimitData {
fn init(key: u32) -> Self {
Self {
station_id: key,
..Default::default()
}
}
}
fn get_its_header<'p>(
packet: &geonetworking::Decoded<geonetworking::Packet<'p>>,
) -> Result<
(
&'p [u8],
c_its_parser::standards::extensions::ItsMessageId,
u32,
),
alloc::string::String,
> {
let payload = packet.decoded.btp_payload()?;
if payload.len() < 6 {
return Err("ITS payload too small for ItsPduHeader".to_string());
}
if let Ok(message_id) = payload[1].try_into() {
let station_id_buf: [u8; 4] = payload[2..6].try_into().unwrap();
let station_id = u32::from_be_bytes(station_id_buf);
Ok((payload, message_id, station_id))
} else {
Err("Unknown message ID in ItsPduHeader".to_string())
}
}
#[cfg(feature = "gnss_l67k")]
#[allow(clippy::large_stack_frames, reason = "GNSS driver needs some space")]
#[embassy_executor::task]
async fn gnss_l67k_task(
gps_wakeup: esp_hal::peripherals::GPIO1<'static>,
gps_reset: esp_hal::peripherals::GPIO25<'static>,
uart: esp_hal::peripherals::UART0<'static>,
uart_rx: esp_hal::peripherals::GPIO12<'static>,
uart_tx: esp_hal::peripherals::GPIO11<'static>,
) {
use embassy_gps::gps::GpsFsm;
let mut gps = l76k::esp::L76kFsm::new_sep(
l76k::esp::GpsHw {
reinit: gps_reset,
standby: gps_wakeup,
},
|| {
esp_hal::uart::Uart::new(uart, esp_hal::uart::Config::default().with_baudrate(9600))
.expect("Failed to create UART for GNSS")
.with_rx(uart_rx)
.with_tx(uart_tx)
.into_async()
},
)
.await;
let mut fix_drop_message = false;
loop {
if let Ok(Some(embassy_gps::types::GpsEvent::Fix(fix))) = gps.step().await {
// drop every second message as we always get each fix twice
fix_drop_message = !fix_drop_message;
if fix_drop_message {
// send to main thread
let pos_state = applogic::GnssFix {
time: fix.get_timestamp(),
latitude_deg: fix.latitude_deg,
longitude_deg: fix.longitude_deg,
heading_deg: fix.true_course_deg,
speed_mps: fix.speed_over_ground,
};
critical_section::with(|cs| {
let gnss_update_ref = GNSS_UPDATE.borrow(cs);
gnss_update_ref.replace(Some(pos_state));
});
}
} else {
// FSM will recover automatically from errors and ignore other event types
}
}
}
#[cfg(feature = "gnss_ubx")]
#[embassy_executor::task]
async fn gnss_ubx_config(reset_pin: esp_hal::peripherals::GPIO25<'static>) {
// reset the module
let mut gnss_reset = esp_hal::gpio::Output::new(
reset_pin,
esp_hal::gpio::Level::Low,
esp_hal::gpio::OutputConfig::default(),
);
embassy_time::Timer::after(embassy_time::Duration::from_millis(2)).await;
gnss_reset.set_high();
embassy_time::Timer::after(embassy_time::Duration::from_secs(1)).await;
info!("UBX: Enabling ubx protocol with NAV-PVT and NAV-SAT");
let buffer = ublox::packets::cfg_val::CfgValSetBuilder {
version: 1,
layers: ublox::packets::cfg_val::CfgLayerSet::RAM,
reserved1: 0,
cfg_data: &[
ublox::cfg_val::CfgVal::Uart1OutProtNmea(false),
ublox::cfg_val::CfgVal::Uart1OutProtUbx(true),
// CFG-MSGOUT-UBX_NAV_PVT_UART1
ublox::cfg_val::CfgVal::MsgOutUbxNavPvtUart1(1),
// CFG-MSGOUT-UBX_NAV_SAT_UART1
// ublox::cfg_val::CfgVal::MsgOutUbxNavSatUart1(10),
],
}
.into_packet_vec();
send_uart0(&buffer);
}
#[cfg(feature = "screen")]
fn log_screen_error<E>(error: &E)
where
E: core::fmt::Debug,
{
error!("graphics failed: {error:?}");
}
#[cfg(feature = "_uart")]
/// Configures UART0 with an interrupt handler and saves it to `SERIAL`
fn setup_uart0(
baud: u32,
cmd_char: Option<u8>,
gpio12: esp_hal::peripherals::GPIO12<'static>,
gpio11: esp_hal::peripherals::GPIO11<'static>,
uart0: esp_hal::peripherals::UART0<'static>,
handler: esp_hal::interrupt::InterruptHandler,
) -> Result<(), alloc::string::String> {
critical_section::with(|cs| {
UART_RX_BUF
.borrow(cs)
.replace(Some(alloc::vec::Vec::with_capacity(255)));
});
let mut uart = esp_hal::uart::Uart::new(
uart0,
esp_hal::uart::Config::default()
.with_baudrate(baud)
.with_rx(
esp_hal::uart::RxConfig::default()
.with_fifo_full_threshold(u16::from(SERIAL_FIFO_SIZE_THLD))
.with_timeout(50),
),
)
.map_err(|err| alloc::format!("UART0 config error: {err}"))?
.with_rx(gpio12)
.with_tx(gpio11);
if let Some(char) = cmd_char {
uart.set_at_cmd(
esp_hal::uart::AtCmdConfig::default()
.with_cmd_char(char)
.with_char_num(1)
.with_pre_idle_count(0) // command start can be right after other data
.with_post_idle_count(0) // we don't expect a gap between the command start and subsequent text
.with_gap_timeout(5), // needs to be >0 to work
);
}
uart.set_interrupt_handler(handler);
uart.listen(
esp_hal::uart::UartInterrupt::AtCmd
| esp_hal::uart::UartInterrupt::RxTimeout
| esp_hal::uart::UartInterrupt::RxFifoFull, // just to be save
);
critical_section::with(|cs| SERIAL.borrow_ref_mut(cs).replace(uart));
Ok(())
}
#[cfg(feature = "_uart")]
#[esp_hal::handler]
fn serial_handler() {
critical_section::with(|cs| {
let mut serial = SERIAL.borrow_ref_mut(cs);
let mut uart_buf = UART_RX_BUF.borrow_ref_mut(cs);
if let (Some(serial), Some(uart_buf)) = (serial.as_mut(), uart_buf.as_mut()) {
let mut buf = [0u8; SERIAL_FIFO_SIZE_THLD as usize];
match serial.read_buffered(&mut buf) {
Ok(0) => {}
Ok(len) => {
uart_buf.extend_from_slice(&buf[..len]);
}
Err(err) => error!("Failed to read from UART: {err}"),
}
serial.clear_interrupts(
esp_hal::uart::UartInterrupt::RxFifoFull
| esp_hal::uart::UartInterrupt::RxTimeout
| esp_hal::uart::UartInterrupt::AtCmd,
);
}
});
}
#[cfg(feature = "_uart")]
fn send_uart0(data: &[u8]) {
critical_section::with(|cs| {
let mut serial = SERIAL.borrow_ref_mut(cs);
if let Some(serial) = serial.as_mut()
&& let Err(err) = serial.write(data)
{
error!("Failed to write to UART: {err:?}");
}
});
}
#[cfg(all(feature = "esp", feature = "cam_tx"))]
/// Creates a station ID from the WLAN MAC address
///
/// Will use `0xC173` as static prefix and uses last 16 bit from MAC address for the remainder.
///
/// # Panics
/// Won't panic in practive b/c of pre-conditions
#[must_use]
pub fn make_station_id(mac: esp_hal::efuse::MacAddress) -> u32 {
// unwrap is fine here since we can be sure that the MacAddress is 6 byte
// and a slice of 2 bytes can be converted to [u8; 2]
let mac_last_16bit = u16::from_be_bytes(mac.as_bytes().get(4..6).unwrap().try_into().unwrap());
0xC173_0000 | u32::from(mac_last_16bit)
}

470
src/screen.rs Normal file
View file

@ -0,0 +1,470 @@
//! Screen Output
use embedded_graphics::prelude::*;
use embedded_graphics::{geometry, mono_font, pixelcolor, primitives, text};
use crate::applogic;
pub struct Handler<D>
where
D: DrawTarget<Color = pixelcolor::Rgb565>,
{
target: D,
width: u16,
height: u16,
// internal state
prev_state: ScreenState,
hb_state: bool,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub enum ScreenState {
#[default]
Uninitialized,
/// aka. "no GNSS fix" or "splash screen"
Initial,
Error(alloc::string::String),
#[cfg(feature = "spat")]
GlosaSearching(applogic::GlosaSearching),
#[cfg(feature = "spat")]
GlosaFound(applogic::GlosaFound),
#[cfg(feature = "spat")]
GlosaLocked(applogic::GlosaSignalInfo),
}
#[cfg(feature = "spat")]
impl From<applogic::GlosaOutput> for ScreenState {
fn from(value: applogic::GlosaOutput) -> Self {
match value {
applogic::GlosaOutput::Searching(glosa_searching) => {
Self::GlosaSearching(glosa_searching)
}
applogic::GlosaOutput::Found(glosa_found) => Self::GlosaFound(glosa_found),
applogic::GlosaOutput::Locked(glosa_signal_info) => {
Self::GlosaLocked(glosa_signal_info)
}
applogic::GlosaOutput::Error(err_str) => Self::Error(err_str),
}
}
}
impl<D> Handler<D>
where
D: DrawTarget<Color = pixelcolor::Rgb565>,
{
const COLOR_BG: pixelcolor::Rgb565 = pixelcolor::Rgb565::BLACK;
const COLOR_FG: pixelcolor::Rgb565 = pixelcolor::Rgb565::WHITE;
const BK_STYLE: primitives::PrimitiveStyle<pixelcolor::Rgb565> =
primitives::PrimitiveStyleBuilder::new()
.fill_color(Self::COLOR_BG)
.build();
const RD_STYLE: primitives::PrimitiveStyle<pixelcolor::Rgb565> =
primitives::PrimitiveStyleBuilder::new()
.fill_color(pixelcolor::Rgb565::CSS_RED)
.build();
const YE_STYLE: primitives::PrimitiveStyle<pixelcolor::Rgb565> =
primitives::PrimitiveStyleBuilder::new()
.fill_color(pixelcolor::Rgb565::new(31, 55, 0))
.build();
const GN_STYLE: primitives::PrimitiveStyle<pixelcolor::Rgb565> =
primitives::PrimitiveStyleBuilder::new()
.fill_color(pixelcolor::Rgb565::new(10, 50, 0))
.build();
const BLANK_SIGNAL_STYLE: primitives::PrimitiveStyle<pixelcolor::Rgb565> =
primitives::PrimitiveStyleBuilder::new()
.fill_color(pixelcolor::Rgb565::CSS_GRAY)
.build();
const DEFAULT_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> =
mono_font::MonoTextStyle::new(&profont::PROFONT_24_POINT, Self::COLOR_FG);
const SMALL_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> =
mono_font::MonoTextStyle::new(&profont::PROFONT_14_POINT, Self::COLOR_FG);
pub fn new(target: D, width: u16, height: u16) -> Self {
Self {
target,
width,
height,
prev_state: ScreenState::Uninitialized,
hb_state: false,
}
}
pub fn update(&mut self, state: ScreenState) -> Result<(), D::Error> {
if self.prev_state != state {
match &state {
ScreenState::Uninitialized | ScreenState::Initial => self.draw_slash_screen()?,
ScreenState::GlosaSearching(_glosa_searching) => self.draw_glosa_searching()?,
ScreenState::GlosaFound(glosa_found) => self.draw_glosa_found(glosa_found)?,
ScreenState::GlosaLocked(glosa_data) => {
let prev_glosa_data =
if let ScreenState::GlosaLocked(prev_glosa_data) = &self.prev_state {
Some(prev_glosa_data.clone())
} else {
None
};
self.draw_glosa(glosa_data, prev_glosa_data)?;
}
ScreenState::Error(msg) => self.draw_message(msg)?,
}
}
// Update indicator/ heartbeat
{
let update_ind_dia = 10;
let circle_x_pos = i32::from(self.width) - update_ind_dia - 10;
text::Text::with_alignment(
"HB",
geometry::Point::new(circle_x_pos - 5, 10),
Self::SMALL_TEXT_STYLE,
text::Alignment::Right,
)
.draw(&mut self.target)?;
primitives::Circle::new(
geometry::Point::new(circle_x_pos, 2),
update_ind_dia.cast_unsigned(),
)
.into_styled(if self.hb_state {
Self::GN_STYLE
} else {
Self::BK_STYLE
})
.draw(&mut self.target)?;
}
self.prev_state = state;
self.hb_state = !self.hb_state;
Ok(())
}
fn clear(&mut self) -> Result<(), D::Error>
where
D: DrawTarget<Color = pixelcolor::Rgb565>,
{
primitives::Rectangle::new(
geometry::Point::new(0, 0),
geometry::Size::new(self.width.into(), self.height.into()),
)
.into_styled(Self::BK_STYLE)
.draw(&mut self.target)?;
Ok(())
}
#[allow(unused)]
pub fn test_img(&mut self) -> Result<(), D::Error> {
mipidsi::TestImage::new().draw(&mut self.target)?;
Ok(())
}
#[allow(unused)]
pub fn draw_message(&mut self, msg: &str) -> Result<(), D::Error> {
// clear whole display and draw text
self.clear()?;
text::Text::with_alignment(
msg,
geometry::Point::new(i32::from(self.width) / 2, 40),
Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
Ok(())
}
#[allow(unused)]
fn draw_glosa_searching(&mut self) -> Result<(), D::Error> {
// clear whole display and draw text
self.clear()?;
text::Text::with_alignment(
"No upcoming SPAT",
geometry::Point::new(i32::from(self.width) / 2, 130),
Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
Ok(())
}
#[allow(unused)]
fn draw_glosa_found(&mut self, data: &applogic::GlosaFound) -> Result<(), D::Error> {
// clear whole display and draw text
self.clear()?;
let text = if let Some(appr) = data.approach_id {
alloc::format!("Int. {}\nApproach {appr}", data.intersection_id)
} else {
alloc::format!("Int. {}\nno approach ID available", data.intersection_id)
};
text::Text::with_alignment(
&text,
geometry::Point::new(i32::from(self.width) / 2, 130),
Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
Ok(())
}
#[allow(unused)]
fn draw_glosa(
&mut self,
data: &applogic::GlosaSignalInfo,
prev_data: Option<applogic::GlosaSignalInfo>,
) -> Result<(), D::Error> {
let num_signals = data.signal_groups.len();
if let Some(prev) = prev_data
&& prev.signal_groups.len() != num_signals
{
// clear everything if number of signals has changed
self.clear();
} else {
// only clear maneuver and timing area otherwise
primitives::Rectangle::new(
geometry::Point::new(0, 105),
geometry::Size::new(self.width.into(), 67),
)
.into_styled(Self::BK_STYLE)
.draw(&mut self.target)?;
}
// draw intersection ID
text::Text::new(
&alloc::format!("#{}", data.intersection_id),
geometry::Point::new(15, 10),
Self::SMALL_TEXT_STYLE,
)
.draw(&mut self.target)?;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let sig_width = i32::from(self.width) / (num_signals as i32);
for (idx, sig) in data.signal_groups.iter().enumerate() {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let offset = (idx as i32) * sig_width;
let circle_dia = 50;
let circle_margin = (sig_width - circle_dia) / 2;
let centerline = offset + (sig_width / 2);
// draw phase color
let style = match sig.phase {
applogic::SignalPhase::Unknown => Self::BLANK_SIGNAL_STYLE,
applogic::SignalPhase::Red => Self::RD_STYLE,
applogic::SignalPhase::Green => Self::GN_STYLE,
applogic::SignalPhase::RedYellow | applogic::SignalPhase::Yellow => Self::YE_STYLE,
};
primitives::Circle::new(
geometry::Point::new(offset + circle_margin, 15),
circle_dia.cast_unsigned(),
)
.into_styled(style)
.draw(&mut self.target)?;
// TODO: draw lane type?
// draw maneuver
text::Text::with_alignment(
&maneuver_to_str(sig.maneuver),
geometry::Point::new(centerline, 130),
Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
// draw timing indicator
if let Some(end_sec) = sig.end_sec {
text::Text::with_alignment(
&alloc::format!("{end_sec}s"),
geometry::Point::new(centerline, 160),
Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
}
}
Ok(())
}
#[allow(unused)]
fn draw_slash_screen(&mut self) -> Result<(), D::Error> {
let text_style = mono_font::MonoTextStyle::new(&profont::PROFONT_18_POINT, Self::COLOR_FG);
self.clear()?;
let sig_width = i32::from(self.width) / 3;
let circle_dia = 50;
let circle_margin = (sig_width - circle_dia) / 2;
for (idx, style) in [Self::RD_STYLE, Self::YE_STYLE, Self::GN_STYLE]
.iter()
.enumerate()
{
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let offset = (idx as i32) * sig_width;
primitives::Circle::new(
geometry::Point::new(offset + circle_margin, 15),
circle_dia.cast_unsigned(),
)
.into_styled(*style)
.draw(&mut self.target)?;
}
// draw welcome text
let centerline = i32::from(self.width) / 2;
text::Text::with_alignment(
"C-ITS Signal Phase Display\nWaiting for GNSS fix...",
geometry::Point::new(centerline, 130),
text_style,
text::Alignment::Center,
)
.draw(&mut self.target)?;
Ok(())
}
}
fn maneuver_to_str(value: applogic::v2x::map::Maneuvers) -> alloc::string::String {
alloc::format!(
"{}{}{}",
if value.left_allowed { "<" } else { " " },
if value.staight_allowed { "^" } else { " " },
if value.right_allowed { ">" } else { " " },
)
}
/// Creates an [`mipidsi::Display`], width, height tuple for an ST7789 172*320px screen in landscape orientation
///
/// # Errors
/// Human-readable fatal errors
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub fn make_small_display<IoScl, IoSda, IoRes, IoDc, IoCs, IoBl, Spi>(
scl: IoScl,
sda: IoSda,
res: IoRes,
dc: IoDc,
cs: IoCs,
bl: IoBl,
spi: Spi,
spi_buffer: &mut [u8],
) -> Result<
(
mipidsi::Display<
mipidsi::interface::SpiInterface<
'_,
embedded_hal_bus::spi::ExclusiveDevice<
esp_hal::spi::master::Spi<'_, esp_hal::Blocking>,
esp_hal::gpio::Output<'_>,
embedded_hal_bus::spi::NoDelay,
>,
esp_hal::gpio::Output<'_>,
>,
mipidsi::models::ST7789,
esp_hal::gpio::Output<'_>,
>,
u16,
u16,
),
alloc::string::String,
>
where
IoScl: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
IoSda: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
IoRes:
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
IoDc:
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
IoCs:
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
IoBl:
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
Spi: esp_hal::spi::master::Instance + 'static,
{
const DISPLAY_WIDTH: u16 = 320;
const DISPLAY_HEIGHT: u16 = 172;
const DISPLAY_HIDDEN_X: u16 = 34; // somehow this display has 34 px of invisible space on the left (in native orientation)
let (spi, cs) = setup_st7789_spi(scl, sda, cs, spi)
.map_err(|err| alloc::format!("Failed to build SPI config: {err}"))?;
let output_config = esp_hal::gpio::OutputConfig::default();
let dc_output = esp_hal::gpio::Output::new(dc, esp_hal::gpio::Level::Low, output_config);
let rst_output = esp_hal::gpio::Output::new(res, esp_hal::gpio::Level::High, output_config);
let _ = esp_hal::gpio::Output::new(bl, esp_hal::gpio::Level::High, output_config); // enable backlight
let mut display_delay = esp_hal::delay::Delay::new();
let spi_device = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs)
.map_err(|err| alloc::format!("Failed to initialize SPI device: {err}"))?;
let display = mipidsi::Builder::new(
mipidsi::models::ST7789,
mipidsi::interface::SpiInterface::new(spi_device, dc_output, spi_buffer),
)
.display_size(DISPLAY_HEIGHT + DISPLAY_HIDDEN_X, DISPLAY_WIDTH)
.reset_pin(rst_output)
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg90))
.init(&mut display_delay)
.map_err(|err| alloc::format!("Failed to initialize display: {err:?}"))?;
Ok((display, DISPLAY_WIDTH, DISPLAY_HEIGHT))
}
/// Builds the SPI interface for the ST7789 display
///
/// Returns (spi master, chip-select) tuple
///
/// # Errors
/// Returns SPI config error when SPI setup failed
fn setup_st7789_spi<IoScl, IoSda, IoCs, Spi>(
scl: IoScl,
sda: IoSda,
cs: IoCs,
spi: Spi,
) -> Result<
(
esp_hal::spi::master::Spi<'static, esp_hal::Blocking>,
esp_hal::gpio::Output<'static>,
),
esp_hal::spi::master::ConfigError,
>
where
IoScl: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
IoSda: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
IoCs:
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
Spi: esp_hal::spi::master::Instance + 'static,
{
let spi_config = esp_hal::spi::master::Config::default()
.with_frequency(esp_hal::time::Rate::from_mhz(40))
.with_mode(esp_hal::spi::Mode::_0);
let spi: esp_hal::spi::master::Spi<'_, esp_hal::Blocking> =
esp_hal::spi::master::Spi::new(spi, spi_config)?
.with_sck(scl)
.with_mosi(sda);
let cs_output = esp_hal::gpio::Output::new(
cs,
esp_hal::gpio::Level::High,
esp_hal::gpio::OutputConfig::default(),
);
Ok((spi, cs_output))
}

24
src/test.rs Normal file
View file

@ -0,0 +1,24 @@
#![cfg(not(target_arch = "riscv32"))]
#![allow(unused)]
extern crate alloc;
mod applogic;
mod cache;
mod geo_alg;
mod io;
mod testdata;
fn main() {
// env_logger::init();
// dummy only
}
/// Initializes the env-logger to be used inside tests
fn init_test_env_logger() {
let _ = env_logger::builder()
.format_timestamp(None)
.filter_level(log::LevelFilter::Debug)
.is_test(true)
.try_init();
}

465
src/testdata.rs Normal file
View file

@ -0,0 +1,465 @@
//! Central provider for V2X test messages
//!
//! All messages are without any headers (so they start with the ItsPduHeader)
#![cfg(all(not(target_arch = "riscv32"), any(feature = "spat", feature = "cam")))]
#![allow(unused)]
/// Intersection "Sievekingplatz-Südfahrbahn/Ziviljustizgebäude"
///
/// Pedestrian crossing over a one-way street (going from west to east)
///
/// Possible start point (heading 90° to 135°):
/// - in front: `geo_types::Point::new(9.9752864, 53.5564028)`
/// - at lane: `geo_types::Point::new(9.9760764, 53.5563621)`
/// - passed: `geo_types::Point::new(9.977217, 53.5560602)`
#[cfg(feature = "spat")]
pub const MAPEM_HH_1328: &[u8] = &[
0x2, 0x5, 0x0, 0x12, 0x19, 0x8c, 0x8, 0x0, 0x3, 0x9, 0xcd, 0x83, 0x42, 0xfc, 0x9a, 0x94, 0xef,
0xb1, 0x67, 0x71, 0x8b, 0x36, 0x4e, 0x2e, 0x31, 0x60, 0xb9, 0xa4, 0x0, 0xc, 0x14, 0xc0, 0x20,
0xab, 0x21, 0xda, 0x5c, 0xe2, 0x78, 0x72, 0x74, 0x5, 0x14, 0x18, 0x90, 0x4, 0x50, 0x80, 0x0,
0x0, 0x85, 0xc, 0x5c, 0xa8, 0xa0, 0x0, 0x80, 0x23, 0xc1, 0x42, 0x7e, 0xa8, 0x58, 0x46, 0x1d,
0xa0, 0x11, 0xef, 0x4d, 0xf0, 0x45, 0xa5, 0x56, 0x8, 0x2d, 0xa6, 0x2, 0x30, 0xc9, 0x26, 0x11,
0xb7, 0xc5, 0x90, 0x84, 0xca, 0x16, 0xc1, 0x60, 0x70, 0x0, 0x2, 0x2, 0x20, 0x6, 0x44, 0x40,
0x0, 0x0, 0x0, 0x8f, 0x5, 0xf8, 0x83, 0xed, 0x8c, 0x19, 0x20, 0x10, 0xa1, 0x0, 0x0, 0x1, 0xa,
0x23, 0x2a, 0x63, 0x40, 0x1, 0x9, 0xb9, 0xa7, 0x40, 0x25, 0xec, 0x21, 0x17, 0xad, 0x3c, 0x8,
0xef, 0x66, 0xfc, 0x47, 0xa1, 0x2e, 0x82, 0x39, 0x99, 0x2b, 0x11, 0xd2, 0xc7, 0x8, 0x8f, 0x32,
0x18, 0x4, 0x70, 0x50, 0x8c, 0x2b, 0x5, 0x80, 0x0, 0x10, 0x25, 0x82, 0x40, 0x0, 0x8, 0x18,
0x80, 0x29, 0x11, 0x0, 0x0, 0x0, 0x1, 0x17, 0x14, 0x80, 0x3f, 0x70, 0xf8, 0x88, 0x2, 0x11,
0x10, 0x0, 0x0, 0x0, 0x11, 0x23, 0x38, 0x83, 0xe5, 0x8c, 0x90, 0x20, 0x33, 0x2, 0x4, 0x0, 0x0,
0x1, 0x43, 0xd6, 0xe2, 0x8, 0x0, 0x20, 0x72, 0x5a, 0xd0, 0x50, 0x38, 0x10, 0x20, 0x20, 0x3b,
0x2, 0x4, 0x0, 0x0, 0x1, 0x20, 0x23, 0x20, 0xa0, 0x0, 0x82, 0x37, 0x94, 0xc1, 0x40, 0xc0, 0x40,
0xa0,
];
/// Intersection "Johannisbollwerk/Ditmar-Koel-Straße"
///
/// Has a very complex geometry with multiple stop lines after each other.
/// Main traffic direction is east-west, has separate bike lanes.
///
/// Possible start point (heading 90°): TODO
#[cfg(feature = "spat")]
pub const MAPEM_HH_2053: &[u8] = &[
0x2, 0x5, 0x0, 0x12, 0x43, 0x84, 0x8, 0x0, 0x3, 0x9, 0xcd, 0x83, 0x42, 0xfc, 0x9a, 0x94, 0xef,
0xb2, 0x61, 0x71, 0x93, 0x6, 0xac, 0xee, 0x31, 0x66, 0xb9, 0x94, 0x0, 0xc, 0x20, 0x14, 0x10,
0xab, 0x1e, 0x9c, 0x96, 0xe2, 0x77, 0x51, 0x1e, 0x5, 0x14, 0xc8, 0x90, 0x1c, 0xd0, 0x88, 0x0,
0x0, 0x15, 0x8f, 0x5d, 0xb6, 0x88, 0x0, 0x20, 0xc, 0x8e, 0xb0, 0x1a, 0x4a, 0xb8, 0x2c, 0xa,
0x0, 0x0, 0x40, 0x44, 0x0, 0x84, 0x88, 0x80, 0x0, 0x1, 0x9, 0xdd, 0xc0, 0x41, 0xdd, 0x54, 0xb0,
0x59, 0xe9, 0x39, 0x4, 0x80, 0x64, 0x84, 0x0, 0x0, 0x0, 0xaa, 0x39, 0xc9, 0x5d, 0x0, 0x4, 0x5c,
0x8c, 0x91, 0x84, 0xd8, 0x75, 0x3d, 0x21, 0x58, 0x34, 0x0, 0x3, 0x1, 0x2c, 0x8, 0x80, 0x0,
0x81, 0x4, 0x1, 0x90, 0x88, 0x0, 0x0, 0x1, 0x18, 0xe6, 0x61, 0xa8, 0x13, 0xaf, 0x1d, 0xc0,
0x98, 0xc4, 0x44, 0x80, 0x84, 0x84, 0x0, 0x0, 0x0, 0xaa, 0x11, 0x44, 0xe9, 0x0, 0x4, 0x5c,
0x8c, 0x91, 0x84, 0xd8, 0xdd, 0x3c, 0x50, 0x58, 0x2c, 0x0, 0x3, 0x1, 0x88, 0x2, 0xa1, 0x10,
0x0, 0x0, 0x2, 0x31, 0xa3, 0x3e, 0x34, 0x27, 0x7a, 0x38, 0x81, 0x38, 0x8a, 0xc9, 0x0, 0x45,
0x8, 0x0, 0x0, 0x1, 0x48, 0x1a, 0xd6, 0x68, 0x0, 0x22, 0xcf, 0xe9, 0xc7, 0x1e, 0x92, 0xe7,
0x46, 0x2b, 0x6, 0x80, 0x0, 0x30, 0x55, 0x82, 0xc0, 0x0, 0x18, 0x31, 0x21, 0x34, 0x21, 0x0,
0x0, 0x0, 0x2b, 0x4c, 0x89, 0xc, 0x90, 0x0, 0x44, 0x3d, 0xb5, 0x5a, 0x12, 0x39, 0x48, 0xc1,
0x65, 0x10, 0x0, 0x26, 0xe, 0x20, 0x50, 0xe4, 0x40, 0x0, 0x0, 0x8, 0xda, 0x2, 0x25, 0x30, 0xe3,
0x85, 0x9c, 0x1e, 0x1e, 0x92, 0x89, 0x9, 0x61, 0x8, 0x0, 0x0, 0x1, 0x5a, 0x3e, 0x86, 0x18,
0x80, 0x2, 0x21, 0xcb, 0xaa, 0x80, 0x94, 0x8a, 0x1c, 0xb, 0x29, 0x80, 0x1, 0x30, 0x81, 0x2,
0x97, 0x22, 0x0, 0x0, 0x0, 0x46, 0xc5, 0xb0, 0x9a, 0x7, 0xc, 0x2d, 0xe0, 0xf3, 0x54, 0x88,
0x48, 0x4f, 0x8, 0x4, 0x20, 0x0, 0x0, 0x15, 0xa2, 0x28, 0x44, 0x88, 0x80, 0x2b, 0x50, 0x87,
0x5e, 0xa8, 0xc2, 0x1f, 0xaa, 0x0, 0x2c, 0xaa, 0x0, 0x4, 0xc2, 0x44, 0xa, 0x9c, 0x80, 0x84,
0x0, 0x0, 0x2, 0xb5, 0xec, 0x1, 0x40, 0x11, 0x6a, 0xe, 0x75, 0x55, 0xc1, 0xdb, 0x69, 0xc4,
0x90, 0x7d, 0x90, 0x8, 0x40, 0x0, 0x0, 0x6d, 0x5e, 0x40, 0x79, 0xb, 0x44, 0x1, 0x54, 0x46,
0x7e, 0x8, 0x37, 0x88, 0x6f, 0x23, 0xd0, 0x44, 0x91, 0x34, 0x45, 0x5f, 0x6f, 0x8e, 0x65, 0x5,
0x90, 0x40, 0x0, 0x48, 0x50, 0x81, 0x2, 0x90, 0x10, 0x80, 0x0, 0x0, 0x58, 0x59, 0x12, 0xe1,
0x80, 0x8a, 0x88, 0x37, 0x26, 0x78, 0x1c, 0xf3, 0x20, 0x48, 0x2c, 0xc8, 0x40, 0x0, 0x0, 0x1b,
0x57, 0x80, 0x1e, 0x86, 0x10, 0x0, 0x46, 0x70, 0x58, 0x42, 0x88, 0xa4, 0x23, 0xcc, 0x44, 0x4f,
0x38, 0x44, 0x0, 0xae, 0xe2, 0x21, 0x58, 0xa4, 0x0, 0x4, 0x85, 0xac, 0x90, 0x80, 0x4, 0x83,
0x84, 0x5, 0x14, 0x88, 0x0, 0x0, 0x1, 0x21, 0x96, 0x6b, 0xd8, 0x3, 0x6f, 0x67, 0xc1, 0xaa,
0x35, 0x24, 0x82, 0xec, 0x84, 0x0, 0x0, 0x1, 0xb5, 0x77, 0xb1, 0xed, 0xd9, 0x0, 0x4, 0x67,
0x6c, 0x83, 0xd0, 0x8b, 0x2a, 0x32, 0x44, 0x1c, 0x54, 0x6a, 0x40, 0x5c, 0xad, 0xb3, 0x15, 0x8a,
0xc0, 0x0, 0x48, 0x62, 0xcb, 0xd0, 0x0, 0x44, 0x34, 0x40, 0x55, 0x48, 0x80, 0x0, 0x0, 0x12,
0x1b, 0xf0, 0xc6, 0x68, 0x2d, 0xd7, 0x8, 0x19, 0xe3, 0x4c, 0x20, 0x5f, 0x44, 0x44, 0x0, 0x0,
0x9, 0xf, 0x23, 0x6c, 0x98, 0x15, 0xac, 0xb6, 0xa, 0x62, 0x52, 0x10, 0x24, 0x92, 0x20, 0x0,
0x0, 0x4, 0x84, 0x6b, 0x2a, 0xd6, 0x1c, 0x8a, 0xcf, 0x3, 0x90, 0xdc, 0xc9, 0x21, 0x1c, 0xa0,
0x10, 0x80, 0x0, 0x0, 0x58, 0x4a, 0x82, 0xe0, 0xc8, 0x80, 0x2b, 0x50, 0x19, 0x7, 0x6c, 0x8,
0x4, 0x82, 0xb, 0x2f, 0x20, 0x1, 0x60, 0xf2, 0x41, 0x25, 0x42, 0x0, 0x0, 0x1, 0x98, 0x22, 0xb3,
0x96, 0xe8, 0x0, 0x20, 0x80, 0xac, 0x80, 0x73, 0x37, 0x90, 0x75, 0x54, 0xbe, 0xf, 0xdd, 0x81,
0x60, 0xb2, 0xe0, 0x20, 0x6c, 0x59, 0x42, 0x2c, 0xb7, 0xd5, 0x0, 0x10, 0x96, 0x28, 0x20, 0x1,
0x42, 0xb, 0x2f, 0x20, 0x0, 0xb1, 0x15, 0x8a, 0x88, 0x0, 0x50, 0xa9, 0x20, 0x9a, 0xa0, 0x10,
0x80, 0x0, 0x0, 0xd8, 0x5, 0xcc, 0x2e, 0xe8, 0x80, 0x2a, 0x88, 0x75, 0xc4, 0x84, 0xf, 0x81,
0x92, 0xc1, 0xf5, 0x11, 0x1c, 0x59, 0x84, 0xf8, 0x40, 0xb1, 0xa8, 0x0, 0xc, 0x12, 0x10, 0x1a,
0x62, 0x2, 0x10, 0x0, 0x0, 0xb, 0x57, 0x61, 0xa0, 0x42, 0x1, 0x15, 0x11, 0x1e, 0x53, 0xdb,
0x88, 0xc7, 0x21, 0xa0, 0x90, 0x41, 0x50, 0x80, 0x0, 0x0, 0x36, 0x1, 0xc7, 0x3, 0xea, 0x0, 0x8,
0x75, 0x7c, 0x79, 0xf, 0xfb, 0x95, 0xe1, 0xef, 0x70, 0xd0, 0x59, 0xd6, 0xf5, 0xe0, 0xb1, 0x98,
0x0, 0xc, 0x13, 0x10, 0x19, 0x62, 0x20, 0x0, 0x0, 0x4, 0xab, 0xb4, 0x4f, 0xff, 0x82, 0x3b,
0x97, 0xb6, 0x11, 0x77, 0xc1, 0x99, 0x20, 0x8a, 0xa1, 0x0, 0x0, 0x0, 0x8c, 0x4, 0x11, 0xf4,
0xc4, 0x0, 0x10, 0xec, 0x88, 0xfe, 0x2c, 0x2c, 0x89, 0xe0, 0xf0, 0x38, 0x5e, 0xac, 0x74, 0x7c,
0x24, 0x0, 0x40, 0x4f, 0x6f, 0xc8, 0x2c, 0x62, 0x0, 0x3, 0x5, 0x4, 0x6, 0x18, 0x88, 0x0, 0x0,
0x1, 0x2a, 0xed, 0xc3, 0xf5, 0xc0, 0x8e, 0xa1, 0xed, 0x84, 0x5b, 0xd0, 0x66, 0x8, 0x10, 0xc0,
0x81, 0x0, 0x0, 0x0, 0x53, 0x6a, 0x8a, 0x3a, 0x0, 0x8, 0x1d, 0x86, 0x94, 0x14, 0x12, 0xe, 0x2c,
0x8, 0x12, 0xc0, 0x81, 0x0, 0x0, 0x0, 0x53, 0xeb, 0x9c, 0x8a, 0x0, 0x8, 0x22, 0x89, 0x6c, 0x14,
0x10, 0xe, 0x2e, 0x8, 0x14, 0x80, 0x42, 0x0, 0x0, 0x0, 0x52, 0xdd, 0x8e, 0xf2, 0x0, 0x8, 0x20,
0xd6, 0x74, 0x14, 0x16, 0x10, 0x30, 0x0, 0x16, 0x40, 0x42, 0x0, 0x0, 0x0, 0x49, 0x4e, 0x21,
0x48, 0x0, 0x20, 0x7c, 0xe6, 0x30, 0x40, 0xc6, 0x4, 0x8, 0x0, 0x0, 0x2, 0x82, 0xce, 0x9a, 0x10,
0x0, 0x40, 0xd4, 0x45, 0xe0, 0xa0, 0xd0, 0x41, 0x90, 0x40, 0xd6, 0x4, 0x8, 0x0, 0x0, 0x2, 0x46,
0x74, 0x3c, 0x40, 0x1, 0x4, 0xb0, 0xe8, 0x82, 0x83, 0x1, 0x6, 0x81, 0x3, 0x90, 0x8, 0x40, 0x0,
0x0, 0x9, 0xc, 0xcb, 0xe9, 0x0, 0x4, 0x12, 0xd3, 0xaa, 0xa, 0xf, 0x5, 0x1b, 0x0, 0xf, 0x20,
0x21, 0x0, 0x0, 0x0, 0x2c, 0x7b, 0xf1, 0xcf, 0x40, 0x1, 0x3, 0x3b, 0x7, 0x2, 0xd, 0xa0, 0x20,
0x40, 0x0, 0x0, 0x1a, 0xbd, 0xaf, 0x2, 0xfa, 0x80, 0x2, 0x7, 0xca, 0x63, 0x5, 0xe, 0x6, 0x8e,
0x0, 0xe, 0x10, 0x20, 0x40, 0x0, 0x0, 0x1a, 0xbe, 0x96, 0xfc, 0xb4, 0x80, 0x2, 0x8, 0x39, 0x9d,
0x4, 0x1d, 0x40, 0x40, 0x80, 0x0, 0x0, 0x35, 0x7e, 0x35, 0xf0, 0x9d, 0x0, 0x4, 0xf, 0x8c, 0xc6,
0xa, 0x1e, 0xe, 0x1d, 0x0, 0x1e, 0x20, 0x40, 0x80, 0x0, 0x0, 0x30, 0x1, 0x86, 0x20, 0x50, 0x0,
0x41, 0x7, 0xb3, 0xa0, 0x84, 0x2c, 0x8, 0x10, 0x0, 0x0, 0x6, 0x9, 0xca, 0xb9, 0xea, 0x0, 0x8,
0x19, 0xd8, 0x30, 0x14, 0x44, 0x2e, 0x3c, 0x8, 0x44, 0xc0, 0x81, 0x0, 0x0, 0x0, 0x60, 0xfe,
0xcb, 0x6e, 0xa0, 0x0, 0x82, 0x63, 0x7d, 0x1, 0x44, 0x22, 0xe3, 0xe0, 0x85, 0x6c, 0x8, 0x10,
0x0, 0x0, 0x5, 0xaf, 0x58, 0x78, 0xc8, 0x0, 0x20, 0x8b, 0x65, 0x90, 0x51, 0x60, 0xa1, 0x0,
0x21, 0x63, 0x2, 0x4, 0x0, 0x0, 0x1, 0x6a, 0x5a, 0x6, 0xe2, 0x0, 0x8, 0x1d, 0x36, 0x9c, 0x14,
0x56, 0x28, 0x42, 0x8, 0x5a, 0xc0, 0x42, 0x0, 0x0, 0x0, 0x5a, 0x4c, 0xc2, 0xc6, 0x80, 0x2, 0x7,
0x45, 0xa8, 0x5, 0x17, 0xa, 0x91, 0x2, 0x17, 0x30, 0x10, 0x80, 0x0, 0x0, 0x16, 0xac, 0x12,
0x2b, 0x20, 0x0, 0x82, 0x2f, 0x96, 0x1, 0x45, 0xa2, 0xa4, 0x60, 0x86, 0x8, 0x8, 0x10, 0x0, 0x0,
0x6, 0x0, 0x3c, 0xc4, 0x5a, 0x0, 0x8, 0x20, 0xe6, 0x74, 0x14, 0x62, 0x1e, 0x48, 0x0, 0x62,
0x40, 0x81, 0x0, 0x0, 0x0, 0x6a, 0xfc, 0xd3, 0xe1, 0x5a, 0x0, 0x8, 0x1f, 0x29, 0x8c, 0x10,
0xc9, 0x1, 0x2, 0x0, 0x0, 0x0, 0xd5, 0xf4, 0xa7, 0xe6, 0xa4, 0x0, 0x10, 0x41, 0xac, 0xe8, 0x28,
0xcc, 0x40, 0x94, 0x0, 0xcc, 0x81, 0x2, 0x0, 0x0, 0x0, 0xd5, 0xed, 0xe8, 0x18, 0xd4, 0x0, 0x10,
0x3e, 0x73, 0x18,
];
/// Intersection "Feldstraße/Glacischaussee"
///
/// Has a very complex geometry with multiple stop lines after each other.
/// Main traffic direction is east-west, has separate bike lanes.
///
/// Possible start point (heading 90°): TODO
#[cfg(feature = "spat")]
pub const MAPEM_HH_560: &[u8] = &[
0x2, 0x5, 0x0, 0x1, 0x61, 0x2b, 0x8, 0x0, 0x3, 0x9, 0x4d, 0x83, 0x42, 0xfc, 0x9a, 0x94, 0xef,
0xb0, 0x6b, 0x7d, 0xab, 0x66, 0x17, 0xd8, 0xb3, 0x5c, 0xda, 0x0, 0x6, 0x4, 0x60, 0x18, 0x55,
0x91, 0x1, 0x73, 0x71, 0x3b, 0xe2, 0xcb, 0x2, 0x58, 0x56, 0x48, 0x1c, 0x28, 0x44, 0x0, 0x0,
0x32, 0xbb, 0xbd, 0x9a, 0x91, 0x0, 0x61, 0x90, 0x6e, 0x15, 0x40, 0x3c, 0x5c, 0xac, 0x39, 0x63,
0x28, 0x8e, 0xcc, 0x98, 0x51, 0xe, 0x55, 0xe1, 0x2, 0xd0, 0x3d, 0xd2, 0xe, 0x4, 0xe8, 0x22,
0xb0, 0xa2, 0x0, 0x1, 0x7, 0x58, 0x49, 0x0, 0x0, 0x84, 0x8, 0x5, 0x21, 0x11, 0x0, 0x0, 0x2,
0x21, 0x1f, 0x51, 0xf0, 0x9e, 0xd9, 0x70, 0x12, 0xab, 0x42, 0xc4, 0x2, 0x50, 0x8c, 0x80, 0x0,
0x1, 0x10, 0x5e, 0xa0, 0x20, 0x4f, 0x4c, 0xba, 0x9, 0x52, 0xa2, 0xe4, 0x81, 0x8, 0x84, 0x40,
0x0, 0x2, 0xad, 0x36, 0xa3, 0x6c, 0x40, 0x1, 0x2, 0x93, 0x4b, 0x4, 0x38, 0xd7, 0x80, 0x2, 0x16,
0x50, 0x1, 0xff, 0x2c, 0x0, 0x2f, 0xd6, 0x13, 0xb, 0x6f, 0x1, 0x60, 0x84, 0x0, 0x82, 0x2, 0x20,
0x8, 0x64, 0x44, 0x0, 0x0, 0x9, 0x1b, 0x9, 0x2c, 0xd0, 0x4, 0xf0, 0x82, 0x25, 0x1c, 0x3f, 0xd2,
0x40, 0xd1, 0x42, 0x20, 0x0, 0x1, 0xd7, 0xa0, 0xe6, 0xac, 0xa2, 0x0, 0xc3, 0x21, 0xc1, 0xfa,
0xd8, 0x58, 0xf5, 0xb9, 0x2, 0xc1, 0x7d, 0x4e, 0xe, 0x68, 0xf1, 0x3, 0xa9, 0x26, 0x13, 0x81,
0x8c, 0xef, 0x60, 0xdb, 0x75, 0xa0, 0xea, 0xdd, 0xa4, 0xb, 0x4, 0x80, 0x0, 0x20, 0x22, 0x40,
0xb1, 0x42, 0x20, 0x0, 0x1, 0x97, 0xce, 0x86, 0x7c, 0x22, 0x0, 0xc3, 0x20, 0xcd, 0x78, 0x81,
0x63, 0xde, 0xe5, 0xb, 0x8, 0xf5, 0x74, 0x39, 0xef, 0xd1, 0x8b, 0x2f, 0x7e, 0x90, 0x55, 0x57,
0x82, 0x41, 0xca, 0x7d, 0xdc, 0x16, 0x22, 0x80, 0x0, 0x61, 0xc2, 0x2, 0x24, 0x44, 0x40, 0x0,
0x0, 0xb3, 0x4b, 0x65, 0x8e, 0x81, 0x21, 0x90, 0xe0, 0x57, 0xc6, 0xb, 0xe9, 0xef, 0x24, 0xc,
0x14, 0x22, 0x0, 0x0, 0x19, 0x7b, 0x78, 0x69, 0x4a, 0x20, 0xc, 0x32, 0x1b, 0x4b, 0x87, 0x85,
0x8f, 0x7b, 0x92, 0x2c, 0x1d, 0xd5, 0x50, 0xe7, 0x2f, 0x2a, 0x2c, 0xbb, 0xfa, 0x11, 0x64, 0x6f,
0x83, 0x2, 0x81, 0xb1, 0xc1, 0x62, 0x48, 0x0, 0x6, 0x1e, 0x20, 0x24, 0x44, 0x44, 0x0, 0x0, 0xb,
0x34, 0xa5, 0x54, 0x3c, 0x12, 0x19, 0x7, 0xee, 0xf2, 0x2, 0x99, 0x7b, 0xc9, 0x1, 0x51, 0x8,
0x80, 0x0, 0x7, 0x51, 0x5d, 0x86, 0x5a, 0x20, 0xc, 0x32, 0x0, 0x62, 0x8c, 0x11, 0xe2, 0x4c,
0xd0, 0x42, 0x65, 0xb3, 0x9, 0x3a, 0xaa, 0xc1, 0xa, 0xb8, 0x18, 0x26, 0x2, 0xb0, 0x88, 0xc6,
0x67, 0xb8, 0x23, 0x96, 0x95, 0x82, 0xc3, 0xd0, 0x0, 0x10, 0x24, 0x40, 0x3c, 0x48, 0x88, 0x0,
0x0, 0x11, 0xfc, 0x8d, 0xab, 0xa0, 0xcd, 0x78, 0x81, 0x5f, 0x56, 0x9b, 0x12, 0x3, 0x22, 0x11,
0x0, 0x0, 0x14, 0xb7, 0x45, 0x19, 0x21, 0x0, 0x4, 0x44, 0x19, 0x3b, 0xa2, 0x3b, 0x49, 0xa0,
0x8, 0x40, 0xb7, 0x1, 0x26, 0x15, 0x6c, 0x47, 0x6f, 0x49, 0x42, 0x3d, 0xba, 0x89, 0x11, 0x52,
0x56, 0x78, 0xce, 0xf9, 0x3c, 0xc1, 0xb, 0x1d, 0x3a, 0x88, 0x77, 0xa8, 0x3c, 0x44, 0xc5, 0x39,
0x80, 0xb1, 0x18, 0x0, 0x5, 0xa, 0x24, 0x7, 0x44, 0x22, 0x0, 0x0, 0x29, 0x6d, 0xa7, 0x28, 0xda,
0x0, 0x8, 0x89, 0xf6, 0x6a, 0x44, 0x71, 0x53, 0x50, 0x10, 0xc3, 0x6d, 0x82, 0x46, 0x2c, 0xa8,
0x8e, 0xa2, 0x94, 0x84, 0x7a, 0x94, 0xca, 0x22, 0xbf, 0xae, 0x81, 0x9e, 0x3e, 0x78, 0x82, 0x17,
0x2a, 0x6f, 0x10, 0xf2, 0xd0, 0x60, 0x89, 0x96, 0x72, 0xc1, 0x62, 0x50, 0x0, 0xa, 0x16, 0x48,
0x6, 0x68, 0x44, 0x0, 0x0, 0x33, 0x24, 0x13, 0x3d, 0xf1, 0x10, 0x6, 0x19, 0x1, 0x56, 0x4a, 0x4,
0x84, 0xc, 0x80, 0x99, 0x12, 0x30, 0x5, 0x81, 0x2c, 0x9, 0x6, 0x1, 0xc2, 0x15, 0xd0, 0xa5, 0x9,
0x18, 0x0, 0x22, 0xb1, 0x22, 0x0, 0x6, 0x3, 0x58, 0x89, 0x0, 0x3, 0x2, 0x12, 0x0, 0x9a, 0x11,
0x0, 0x0, 0xe, 0xc8, 0x3b, 0x8f, 0xac, 0x44, 0x1, 0x86, 0x42, 0x88, 0xf, 0xb0, 0x51, 0xa1,
0xff, 0x9, 0x20, 0xc, 0x41, 0x26, 0x60, 0xd8, 0x28, 0x4c, 0x97, 0x5, 0x39, 0x1b, 0x91, 0x2d,
0xc1, 0x75, 0x5, 0x30, 0x9c, 0x50, 0x58, 0x4a, 0x0, 0x3, 0x82, 0x92, 0x1, 0x1a, 0x11, 0x0, 0x0,
0xc, 0xc8, 0x9b, 0x4f, 0x8f, 0x44, 0x1, 0x86, 0x42, 0x5f, 0x8a, 0x48, 0x3, 0xb0, 0x1c, 0x24,
0xa8, 0x37, 0x88, 0xd9, 0xc5, 0xe4, 0x69, 0x7b, 0x38, 0x49, 0xf, 0x9b, 0x5, 0xdc, 0xac, 0xcd,
0x2a, 0x16, 0x20, 0xb0, 0xf8, 0x0, 0x8, 0x6, 0x24, 0x10, 0x24, 0x12, 0x0, 0x0, 0x15, 0x9a,
0x65, 0xaf, 0x56, 0x88, 0x3, 0xc, 0x83, 0xf2, 0x7b, 0x5, 0xb9, 0xe, 0xc6, 0x2e, 0x1, 0x6b,
0xe1, 0x6c, 0xf3, 0x28, 0xb, 0x63, 0x56, 0xec, 0x5c, 0x8a, 0x98, 0x20, 0xb0, 0x98, 0x0, 0x9,
0xc, 0x24, 0x2d, 0x44, 0x2, 0x10, 0x0, 0x0, 0x3a, 0xd9, 0xca, 0x42, 0xc4, 0x40, 0x16, 0x20,
0x0, 0x34, 0xec, 0x3, 0xe, 0x65, 0x1, 0x15, 0x3e, 0x84, 0x3a, 0xd6, 0xe1, 0x3, 0x15, 0xb9,
0x8c, 0xa6, 0x16, 0x3d, 0x0, 0xab, 0x7d, 0x80, 0x1, 0xc3, 0x81, 0x65, 0xd0, 0x0, 0xa, 0x1a,
0x20, 0x5c, 0x44, 0x4, 0x20, 0x0, 0x0, 0x11, 0xe4, 0x58, 0x4a, 0x20, 0xfc, 0x9e, 0xc0, 0x66,
0xaf, 0xa0, 0x20, 0x9b, 0x2, 0x4, 0x0, 0x0, 0x1, 0x7e, 0x98, 0x22, 0x8a, 0x0, 0x8, 0x1d, 0x6,
0xa0, 0x4, 0x28, 0x2, 0x8, 0x28, 0xc0, 0x81, 0x0, 0x0, 0x0, 0x66, 0x20, 0x8c, 0x7b, 0x20, 0x0,
0x82, 0x26, 0x97, 0x40, 0x42, 0x60, 0x40, 0x82, 0xac, 0x8, 0x10, 0x0, 0x0, 0x5, 0xb9, 0x28,
0x6f, 0xe8, 0x0, 0x20, 0x71, 0x1b, 0x0, 0x10, 0xb0, 0x18, 0x20, 0xb3, 0x2, 0x4, 0x0, 0x0, 0x1,
0x56, 0xc2, 0x4b, 0x68, 0x0, 0x20, 0x8f, 0x25, 0x0, 0x10, 0xa8, 0x20, 0x20, 0xbb, 0x2, 0x4,
0x0, 0x0, 0x1, 0x7e, 0x63, 0x57, 0xea, 0x0, 0x8, 0x26, 0x37, 0xcc, 0x4, 0x30, 0xa, 0x8, 0x30,
0xc0, 0x81, 0x0, 0x0, 0x0, 0x5e, 0x5, 0x57, 0xac, 0x80, 0x2, 0x6, 0x76, 0xd, 0x1, 0xb, 0x83,
0x2, 0xc, 0xb0, 0x20, 0x40, 0x0, 0x0, 0x13, 0xeb, 0xc2, 0x9a, 0x0, 0x8, 0x22, 0x56, 0x8c, 0x4,
0x34, 0xe, 0x8, 0x34, 0xc0, 0x81, 0x0, 0x0, 0x0, 0x4e, 0xf3, 0x45, 0x28, 0x0, 0x20, 0x76, 0xe5,
0xd0, 0x10, 0xc8, 0x40, 0x20, 0xdb, 0x2, 0x4, 0x0, 0x0, 0x1, 0x6f, 0xd8, 0xd, 0x32, 0x0, 0x8,
0x19, 0xd8, 0x34, 0x4, 0x38, 0x12, 0x8, 0x38, 0xc0, 0x81, 0x0, 0x0, 0x0, 0x5d, 0x90, 0xc1,
0x90, 0x80, 0x2, 0x9, 0x8d, 0xf3, 0x1, 0xd, 0x85, 0x2, 0xf, 0xa0, 0x10, 0x80, 0x0, 0x0, 0x15,
0xa2, 0x24, 0x2e, 0x80, 0x2, 0x8, 0xee, 0x50, 0x1, 0x10, 0x5, 0x80, 0x10, 0x10, 0x10, 0x80,
0x0, 0x0, 0x17, 0x0, 0x81, 0x9d, 0xa0, 0x0, 0x81, 0xc8, 0x6b, 0x41, 0x8, 0x50, 0x8, 0x40, 0x0,
0x0, 0xb, 0x8a, 0xd8, 0xd8, 0x50, 0x0, 0x40, 0xcf, 0x42, 0x0, 0x22, 0x20, 0xc0, 0x2, 0x22, 0x2,
0x10, 0x0, 0x0, 0x2, 0xef, 0x9c, 0x24, 0xe4, 0x0, 0x10, 0x4c, 0x4f, 0x80, 0x21, 0x1a, 0x1, 0x8,
0x0, 0x0, 0x1, 0x5a, 0xd2, 0xde, 0x48, 0x0, 0x20, 0x80, 0x26, 0x40, 0x11, 0x20, 0x68, 0x1,
0x21, 0x1, 0x8, 0x0, 0x0, 0x1, 0x5a, 0xce, 0x9a, 0x68, 0x0, 0x20, 0x80, 0x19, 0xc0, 0x42, 0x54,
0x2, 0x10, 0x0, 0x0, 0x2, 0xa4, 0x6c, 0xcc, 0x10, 0x0, 0x41, 0x2a, 0x39, 0x40, 0x22, 0x60,
0xe0, 0x2, 0x62, 0x2, 0x10, 0x0, 0x0, 0x2, 0x47, 0x69, 0x47, 0x40, 0x1, 0x3, 0x49, 0x13, 0x2,
0x13, 0xa0, 0x10, 0x80, 0x0, 0x0, 0x17, 0xe0, 0x4, 0x82, 0x20, 0x0, 0x82, 0x63, 0x7d, 0x0,
0x45, 0x1, 0xe0, 0x5, 0x4, 0x4, 0x20, 0x0, 0x0, 0x5, 0x6c, 0xd8, 0xff, 0xa0, 0x0, 0x81, 0x9d,
0x83, 0x1, 0xa, 0x58, 0x10, 0x20, 0x0, 0x0, 0xb, 0x60, 0x30, 0x70, 0x10, 0x0, 0x41, 0x31, 0x42,
0x0, 0x22, 0xa1, 0x0, 0x42, 0xa6, 0x4, 0x8, 0x0, 0x0, 0x2, 0xd3, 0xa2, 0x16, 0x24, 0x0, 0x10,
0x33, 0xcf, 0x80, 0x8, 0xa4, 0x44, 0x10, 0xad, 0x0, 0x84, 0x0, 0x0, 0x0, 0xbd, 0xe7, 0x8f,
0xdd, 0x0, 0x4, 0xe, 0x8b, 0x50, 0x2, 0x2c, 0x12, 0x0, 0x2c, 0x20, 0x21, 0x0, 0x0, 0x0, 0x2f,
0xe0, 0x89, 0xbb, 0x40, 0x1, 0x4, 0x71, 0x29, 0x80,
];
/// Intersection "TODO"
///
/// 3 layers
#[cfg(feature = "spat")]
pub const MAPEM_HH_2349_L31: &[u8] = &[
0x2, 0x5, 0x0, 0x12, 0x25, 0xf4, 0x18, 0x0, 0x3e, 0x6, 0xd, 0x91, 0x21, 0x93, 0x3b, 0x8c, 0x99,
0xb4, 0x73, 0x71, 0x8b, 0x6, 0xe, 0x60, 0x0, 0x61, 0x25, 0xa1, 0x5, 0x58, 0xef, 0x69, 0x97,
0x13, 0xed, 0x54, 0x20, 0x28, 0xa3, 0x68, 0x4, 0x49, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba,
0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x99, 0x2, 0x4, 0x8, 0x0, 0x0, 0xab,
0x72, 0xf, 0xe5, 0xc0, 0x7d, 0xa6, 0x34, 0x41, 0xf4, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd,
0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xcd, 0x1, 0x2, 0x4, 0x0, 0x0,
0x55, 0xb8, 0x57, 0xf1, 0x50, 0x3e, 0x73, 0x18, 0x28, 0x80, 0x3c, 0x51, 0x10, 0x6d, 0x38, 0xdd,
0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32,
0x40, 0x40, 0x81, 0x0, 0x0, 0x10, 0x8c, 0x36, 0xc6, 0x1, 0x31, 0xc1, 0xc0, 0xa1, 0xc0, 0x91,
0x24, 0x1, 0xe4, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4,
0x10, 0x79, 0x6d, 0x40, 0xcc, 0x81, 0x2, 0x4, 0x0, 0x0, 0x55, 0xaf, 0x28, 0x32, 0xd0, 0x3e,
0x73, 0x1a, 0x0, 0xd2, 0x71, 0xba, 0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7,
0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x64, 0x40, 0x81, 0x2, 0x0, 0x0, 0x20, 0x21, 0xcd, 0x23, 0x81,
0x9c, 0x7e, 0x51, 0x7, 0x53, 0x8d, 0xd7, 0x97, 0x44, 0x9, 0xe3, 0x75, 0xe5, 0xd1, 0x3a, 0xd,
0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3, 0x34, 0x4, 0x8, 0x10, 0x0, 0x1, 0x56, 0xd5, 0x20, 0xe, 0xc1,
0x6, 0xb3, 0xa0, 0xa1, 0xe0, 0xe1, 0x34, 0x41, 0x94, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd,
0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xc9, 0x1, 0x2, 0x4, 0x0, 0x0,
0x40, 0xe9, 0x9a, 0x78, 0x4, 0xc9, 0x3, 0x82, 0x86, 0x83, 0x4, 0x50, 0x6, 0x13, 0x8d, 0xd7,
0x97, 0x44, 0x9, 0xe3, 0x75, 0xe5, 0xd1, 0x3a, 0xd, 0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3, 0x22,
0x2, 0x8, 0x10, 0x0, 0x1, 0x8, 0xf2, 0x70, 0xe4, 0x13, 0x1c, 0x22, 0x88, 0x2e, 0x9c, 0x6e,
0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda, 0x82, 0xf, 0x2d, 0xa8, 0x19,
0x20, 0x10, 0x40, 0x80, 0x0, 0x8, 0x2f, 0xb3, 0x77, 0x0, 0x67, 0x5e, 0xf0, 0x50, 0xc0, 0x58,
0x82, 0x0, 0xb2, 0x71, 0xba, 0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7, 0x6a,
0x8, 0x3c, 0xb6, 0xa0, 0x64, 0x40, 0x41, 0x2, 0x0, 0x0, 0x20, 0x73, 0x2d, 0xbe, 0x82, 0x64,
0x81, 0x51, 0x5, 0x53, 0x8d, 0xd7, 0x97, 0x44, 0x9, 0xe3, 0x75, 0xe5, 0xd1, 0x3a, 0xd, 0x3b,
0x50, 0x41, 0xe5, 0xb5, 0x3, 0x24, 0x2, 0x8, 0x10, 0x0, 0x1, 0x0, 0x69, 0x6d, 0x60, 0xc, 0xe3,
0xf6, 0xa, 0x16, 0xa, 0xf, 0x40, 0x14, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97,
0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0x88, 0x10, 0x20, 0x40, 0x0, 0x4, 0xe,
0xb5, 0xa7, 0x50, 0x4c, 0x90, 0x2b, 0x20, 0xa, 0xd1, 0xb0, 0xe8, 0xe5, 0xcf, 0xa7, 0x2c, 0xba,
0x73, 0x65, 0xdc, 0x81, 0x8a, 0xd, 0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3, 0x11, 0x40, 0x20, 0x0,
0x2, 0x88, 0x69, 0x7c, 0x9a, 0x43, 0xec, 0xa4, 0x90, 0x1, 0x0, 0xc0, 0x0, 0xd1, 0xe3, 0x90,
0x68, 0xf2, 0x0, 0x34, 0x59, 0x18, 0x1a, 0x1c, 0x98, 0x8, 0x2, 0x0, 0x16, 0x86, 0xc9, 0xe0,
0x31, 0x99, 0x20, 0x18, 0xbc, 0x9e, 0x9c, 0x5b, 0x29, 0x10, 0x1, 0x41, 0x61, 0x90, 0x0, 0x2,
0x2, 0x80, 0x20, 0x9c, 0x6e, 0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda,
0x82, 0xf, 0x2d, 0xa8, 0x19, 0x90, 0x20, 0x40, 0x80, 0x0, 0xa, 0xb6, 0xbd, 0x0, 0x6c, 0x8,
0x35, 0x9d, 0x50, 0xc, 0x5a, 0x36, 0x1d, 0x1c, 0xb9, 0xf4, 0xe5, 0x97, 0x4e, 0x6c, 0xbb, 0x90,
0x34, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x66, 0x64, 0x4, 0x0, 0x0, 0x9, 0x55, 0x73,
0x60, 0x6b, 0xc2, 0x13, 0x5c, 0x90, 0x86, 0xc9, 0xce, 0x59, 0x0, 0x96, 0x8d, 0x87, 0x47, 0x2e,
0x7d, 0x39, 0x65, 0xd3, 0x9b, 0x2e, 0xe4, 0xc, 0x90, 0x69, 0xda, 0x82, 0xf, 0x2d, 0xa8, 0x18,
0x8a, 0x10, 0x0, 0x0, 0xe, 0x43, 0x56, 0xe3, 0x87, 0x1f, 0xbb, 0xe5, 0x46, 0x1c, 0x3, 0x18,
0x41, 0xff, 0x4d, 0xe0, 0x80, 0x20, 0x0, 0xff, 0xa9, 0xd8, 0xd, 0x6e, 0x53, 0x6, 0xb3, 0x2d,
0xa7, 0x58, 0xcb, 0xa4, 0x0, 0x50, 0x58, 0x5c, 0x0, 0x0, 0x81, 0x20, 0x9, 0x27, 0x1b, 0xaf,
0x2e, 0x88, 0x13, 0xc6, 0xeb, 0xcb, 0xa2, 0x74, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a, 0x6, 0x44,
0x8, 0x10, 0x20, 0x0, 0x2, 0x11, 0x7e, 0xd9, 0x90, 0x26, 0x38, 0x31, 0x0, 0x81, 0x38, 0xdd,
0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x33,
0x20, 0x40, 0x81, 0x0, 0x0, 0x15, 0x6f, 0x79, 0xf1, 0x88, 0x10, 0x6b, 0x3a, 0x80, 0x38, 0x9c,
0x6e, 0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda, 0x82, 0xf, 0x2d, 0xa8,
0x19, 0x10, 0x20, 0x40, 0x80, 0x0, 0x8, 0x31, 0x1b, 0x57, 0x40, 0x67, 0x5f, 0x25, 0x0, 0xb5,
0xa3, 0x61, 0xd1, 0xcb, 0x9f, 0x4e, 0x59, 0x74, 0xe6, 0xcb, 0xb9, 0x3, 0x34, 0x1a, 0x76, 0xa0,
0x83, 0xcb, 0x6a, 0x6, 0x66, 0x44, 0x0, 0x0, 0x0, 0x95, 0x57, 0xe2, 0x0, 0xfc, 0x46, 0x24,
0xec, 0x62, 0x2e, 0xc7, 0x57, 0x44, 0x21, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97,
0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0xd0, 0x10, 0x20, 0x40, 0x0, 0x5, 0x5b,
0xd9, 0x7c, 0x72, 0x4, 0x14, 0xce, 0x82, 0x88, 0x83, 0x45, 0x54, 0x2, 0x16, 0x8d, 0x87, 0x47,
0x2e, 0x7d, 0x39, 0x65, 0xd3, 0x9b, 0x2e, 0xe4, 0xc, 0xd0, 0x69, 0xda, 0x82, 0xf, 0x2d, 0xa8,
0x19, 0x11, 0x10, 0x0, 0x0, 0x2, 0x40, 0xab, 0x99, 0x11, 0xc, 0x3d, 0x3b, 0x22, 0x87, 0x13,
0xc5, 0x44, 0xd, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed,
0x41, 0x7, 0x96, 0xd4, 0xc, 0x58, 0x10, 0x20, 0x40, 0x0, 0x4, 0x2e, 0x8a, 0x53, 0x20, 0x3e,
0xb3, 0x18, 0x28, 0x38, 0x14, 0x29, 0x10, 0x39, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e,
0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x31, 0x60, 0x40, 0x81, 0x0, 0x0, 0x10,
0xc6, 0x17, 0xad, 0x41, 0x5, 0xb3, 0xa0, 0xa0, 0xd0, 0x50, 0xb4, 0x40, 0xf4, 0xe3, 0x75, 0xe5,
0xd1, 0x2, 0x78, 0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xcd, 0x1,
0x2, 0x4, 0x0, 0x0, 0x55, 0xaf, 0xc8, 0x33, 0x20, 0x3e, 0x73, 0x18, 0x28, 0x40, 0x34, 0x31,
0x90, 0xd, 0x68, 0xd8, 0x74, 0x72, 0xe7, 0xd3, 0x96, 0x5d, 0x39, 0xb2, 0xee, 0x40, 0xcd, 0x6,
0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x88, 0xa1, 0x0, 0x0, 0x2, 0x44, 0x36, 0x2e, 0x23, 0x61,
0x79, 0xf4, 0x9a, 0x83, 0xcf, 0x94, 0x1, 0xe7, 0x4a, 0xc0, 0xf3, 0x65, 0xc0, 0x79, 0x73, 0x10,
0xf5, 0xf5, 0x26, 0x64, 0x46, 0x30, 0x58, 0x23, 0x6, 0x9, 0x32, 0x18, 0x4, 0x8b, 0x13, 0x82,
0x3b, 0x8d, 0x1, 0x18, 0x48, 0x0, 0x96, 0x69, 0x30, 0x40, 0x10, 0x0, 0x23, 0x9, 0x0, 0x11,
0xdc, 0x6a, 0x9, 0x12, 0x2a, 0x4, 0x97, 0xe, 0x2, 0x0, 0x80, 0x51, 0x18, 0x41, 0xa8, 0x0, 0xa0,
0xb0, 0x84, 0x0, 0x2, 0x3, 0x44, 0x11, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97,
0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0x90, 0x10, 0x20, 0x40, 0x0, 0x4, 0x18,
0x69, 0xad, 0xe0, 0x33, 0xaf, 0xa0, 0x28, 0x48, 0x20, 0x35, 0x10, 0x4d, 0x38, 0xdd, 0x79, 0x74,
0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, 0x40, 0x40,
0x81, 0x0, 0x0, 0x10, 0x10, 0xa6, 0x95, 0x80, 0xce, 0x3f, 0x60, 0xa1, 0x40, 0x90, 0xe0,
];
#[cfg(feature = "spat")]
pub const MAPEM_HH_2349_L32: &[u8] = &[
0x2, 0x5, 0x0, 0x12, 0x25, 0xf4, 0x18, 0x0, 0x40, 0x6, 0xd, 0x91, 0x21, 0x93, 0x3b, 0x8c, 0x99,
0xb4, 0x73, 0x71, 0x8b, 0x6, 0xe, 0x60, 0x0, 0x61, 0x25, 0xa1, 0x5, 0x58, 0xef, 0x69, 0x97,
0x13, 0xed, 0x54, 0x20, 0x28, 0xa3, 0x48, 0x84, 0x29, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba,
0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x9a, 0x2, 0x4, 0x8, 0x0, 0x0, 0xab,
0x7b, 0x2f, 0x8e, 0x40, 0x82, 0x99, 0xd0, 0x51, 0x10, 0x68, 0xaa, 0x0, 0xf2, 0x71, 0xba, 0xf2,
0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x66, 0x40,
0x81, 0x2, 0x0, 0x0, 0x2a, 0xd7, 0x94, 0x19, 0x68, 0x1f, 0x39, 0x8d, 0x0, 0x71, 0x38, 0xdd,
0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32,
0x20, 0x40, 0x81, 0x0, 0x0, 0x10, 0x62, 0x36, 0xae, 0x80, 0xce, 0xbe, 0x48, 0x3, 0x49, 0xc6,
0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81,
0x91, 0x2, 0x4, 0x8, 0x0, 0x0, 0x80, 0x87, 0x34, 0x8e, 0x6, 0x71, 0xf9, 0x44, 0x19, 0x4e, 0x37,
0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc,
0x90, 0x10, 0x20, 0x40, 0x0, 0x4, 0xe, 0x99, 0xa7, 0x80, 0x4c, 0x90, 0x38, 0x28, 0x68, 0x30,
0x45, 0x0, 0x61, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5,
0x4, 0x1e, 0x5b, 0x50, 0x32, 0x20, 0x20, 0x81, 0x0, 0x0, 0x10, 0x8f, 0x27, 0xe, 0x41, 0x31,
0xc2, 0x28, 0x82, 0xe9, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d,
0xa8, 0x20, 0xf2, 0xda, 0x81, 0x92, 0x1, 0x4, 0x8, 0x0, 0x0, 0x82, 0xfb, 0x37, 0x70, 0x6, 0x75,
0xef, 0x5, 0xc, 0x5, 0x88, 0x20, 0xb, 0x27, 0x1b, 0xaf, 0x2e, 0x88, 0x13, 0xc6, 0xeb, 0xcb,
0xa2, 0x74, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a, 0x6, 0x44, 0x4, 0x10, 0x20, 0x0, 0x2, 0x7,
0x32, 0xdb, 0xe8, 0x26, 0x48, 0x15, 0x10, 0x55, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e,
0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, 0x40, 0x20, 0x81, 0x0, 0x0, 0x10,
0x6, 0x96, 0xd6, 0x0, 0xce, 0x3f, 0x60, 0xa1, 0x60, 0xa0, 0xf5, 0x0, 0xb5, 0xa3, 0x61, 0xd1,
0xcb, 0x9f, 0x4e, 0x59, 0x74, 0xe6, 0xcb, 0xb9, 0x3, 0x34, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a,
0x6, 0x66, 0x44, 0x0, 0x0, 0x0, 0x95, 0x57, 0xe2, 0x0, 0xfc, 0x46, 0x24, 0xec, 0x62, 0x2e,
0xc7, 0x57, 0x40, 0x12, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34,
0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0x88, 0x10, 0x20, 0x40, 0x0, 0x4, 0x22, 0xfd, 0xb3, 0x20,
0x4c, 0x70, 0x62, 0x20, 0xea, 0x71, 0xba, 0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41,
0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x66, 0x80, 0x81, 0x2, 0x0, 0x0, 0x2a, 0xda, 0xa4, 0x1,
0xd8, 0x20, 0xd6, 0x74, 0x14, 0x3c, 0x1c, 0x26, 0xa0, 0xa, 0xb4, 0x6c, 0x3a, 0x39, 0x73, 0xe9,
0xcb, 0x2e, 0x9c, 0xd9, 0x77, 0x20, 0x6a, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xc4, 0x48,
0x80, 0x0, 0x0, 0x11, 0x81, 0x6e, 0x2, 0x81, 0xcc, 0x71, 0x88, 0x5c, 0x25, 0x20, 0x4c, 0x80,
0xeb, 0x46, 0xc3, 0xa3, 0x97, 0x3e, 0x9c, 0xb2, 0xe9, 0xcd, 0x97, 0x72, 0x6, 0x28, 0x34, 0xed,
0x41, 0x7, 0x96, 0xd4, 0xc, 0x89, 0x8, 0x0, 0x0, 0xf, 0x21, 0x6, 0xc, 0xfb, 0xa, 0x3e, 0x44,
0xf0, 0x10, 0xc, 0x0, 0x18, 0xb2, 0x5d, 0x83, 0x13, 0xcb, 0xa8, 0xa2, 0x58, 0xba, 0x11, 0x16,
0x8e, 0xa0, 0x8c, 0x18, 0x20, 0x7, 0xfc, 0x10, 0x5a, 0x76, 0x7, 0xd1, 0x3b, 0xb, 0xa1, 0x1b,
0x40, 0x80, 0x20, 0x0, 0x3f, 0xc, 0xe8, 0x1f, 0xe6, 0x78, 0x10, 0x23, 0x3a, 0x8, 0x25, 0x9e,
0x44, 0x6a, 0x3b, 0x20, 0x2, 0x8a, 0xc1, 0x48, 0x0, 0x10, 0x1d, 0x60, 0x84, 0x0, 0x8, 0x10,
0x80, 0x44, 0x9c, 0x6e, 0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda, 0x82,
0xf, 0x2d, 0xa8, 0x19, 0x90, 0x20, 0x40, 0x80, 0x0, 0xa, 0xb7, 0x20, 0xfe, 0x5c, 0x7, 0xda,
0x63, 0x44, 0x11, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed,
0x41, 0x7, 0x96, 0xd4, 0xc, 0x90, 0x10, 0x20, 0x40, 0x0, 0x4, 0x18, 0x69, 0xad, 0xe0, 0x33,
0xaf, 0xa0, 0x28, 0x48, 0x20, 0x35, 0x10, 0x6d, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e,
0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, 0x40, 0x40, 0x81, 0x0, 0x0, 0x10,
0x8c, 0x36, 0xc6, 0x1, 0x31, 0xc1, 0xc0, 0xa1, 0xc0, 0x91, 0x25, 0x0, 0x45, 0xa3, 0x61, 0xd1,
0xcb, 0x9f, 0x4e, 0x59, 0x74, 0xe6, 0xcb, 0xb9, 0x3, 0x44, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a,
0x6, 0x22, 0x44, 0x0, 0x0, 0x0, 0x8c, 0x3, 0xf1, 0x64, 0xf, 0xed, 0x92, 0xe2, 0xc5, 0xf8, 0xba,
0x64, 0x6, 0x5a, 0x36, 0x1d, 0x1c, 0xb9, 0xf4, 0xe5, 0x97, 0x4e, 0x6c, 0xbb, 0x90, 0x32, 0x41,
0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x64, 0x48, 0x40, 0x0, 0x0, 0x29, 0x6, 0xee, 0x66, 0xf4,
0x91, 0x7b, 0x93, 0xc8, 0x8, 0x6, 0x0, 0x4c, 0x7f, 0x2, 0x68, 0x0, 0xa1, 0x8e, 0x0, 0x44, 0x51,
0xa4, 0x3, 0xe5, 0x88, 0x34, 0x38, 0x3, 0x5, 0x85, 0xa0, 0x0, 0x38, 0x4a, 0x20, 0x6a, 0x71,
0xba, 0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0,
0x62, 0xc0, 0x81, 0x2, 0x0, 0x0, 0x21, 0x74, 0x52, 0x99, 0x1, 0xf5, 0x98, 0xc1, 0x41, 0xc0,
0xa1, 0x48, 0x2, 0x89, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d,
0xa8, 0x20, 0xf2, 0xda, 0x81, 0x91, 0x2, 0x4, 0x8, 0x0, 0x0, 0x81, 0xd6, 0xb4, 0xea, 0x9, 0x92,
0x5, 0x44, 0x1f, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed,
0x41, 0x7, 0x96, 0xd4, 0xc, 0xd0, 0x10, 0x20, 0x40, 0x0, 0x5, 0x5b, 0x85, 0x7f, 0x15, 0x3,
0xe7, 0x31, 0x82, 0x88, 0x3, 0xc5, 0x11, 0x3, 0x93, 0x8d, 0xd7, 0x97, 0x44, 0x9, 0xe3, 0x75,
0xe5, 0xd1, 0x3a, 0xd, 0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3, 0x16, 0x4, 0x8, 0x10, 0x0, 0x1, 0xc,
0x61, 0x7a, 0xd4, 0x10, 0x5b, 0x3a, 0xa, 0xd, 0x5, 0xb, 0x40, 0x20, 0x4e, 0x37, 0x5e, 0x5d,
0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0xc8, 0x10,
0x20, 0x40, 0x0, 0x5, 0x5b, 0xde, 0x7c, 0x62, 0x4, 0x1a, 0xce, 0xa2, 0x7, 0xa7, 0x1b, 0xaf,
0x2e, 0x88, 0x13, 0xc6, 0xeb, 0xcb, 0xa2, 0x74, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a, 0x6, 0x68,
0x8, 0x10, 0x20, 0x0, 0x2, 0xad, 0x7e, 0x41, 0x99, 0x1, 0xf3, 0x98, 0xc1, 0x42, 0x1, 0xa1,
0x88, 0x2, 0x9, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8,
0x20, 0xf2, 0xda, 0x81, 0x99, 0x2, 0x4, 0x8, 0x0, 0x0, 0xab, 0x6b, 0xd0, 0x6, 0xc0, 0x83, 0x59,
0xd4, 0x41, 0x34, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4,
0x10, 0x79, 0x6d, 0x40, 0xc9, 0x1, 0x2, 0x4, 0x0, 0x0, 0x40, 0x42, 0x9a, 0x56, 0x3, 0x38, 0xfd,
0x82, 0x85, 0x2, 0x43, 0x80,
];
#[cfg(feature = "spat")]
pub const MAPEM_HH_2349_L33: &[u8] = &[
0x2, 0x5, 0x0, 0x12, 0x25, 0xf4, 0x18, 0x0, 0x42, 0x6, 0xd, 0x91, 0x21, 0x93, 0x3b, 0x8c, 0x99,
0xb4, 0x73, 0x71, 0x8b, 0x6, 0xe, 0x60, 0x0, 0x61, 0x25, 0xa1, 0x5, 0x58, 0xef, 0x69, 0x97,
0x13, 0xed, 0x54, 0x20, 0x28, 0xa3, 0x48, 0x84, 0x29, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba,
0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x9a, 0x2, 0x4, 0x8, 0x0, 0x0, 0xab,
0x7b, 0x2f, 0x8e, 0x40, 0x82, 0x99, 0xd0, 0x51, 0x10, 0x68, 0xaa, 0x0, 0xf2, 0x71, 0xba, 0xf2,
0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x66, 0x40,
0x81, 0x2, 0x0, 0x0, 0x2a, 0xd7, 0x94, 0x19, 0x68, 0x1f, 0x39, 0x8d, 0x10, 0x75, 0x38, 0xdd,
0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x33,
0x40, 0x40, 0x81, 0x0, 0x0, 0x15, 0x6d, 0x52, 0x0, 0xec, 0x10, 0x6b, 0x3a, 0xa, 0x1e, 0xe,
0x13, 0x44, 0x1b, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed,
0x41, 0x7, 0x96, 0xd4, 0xc, 0x90, 0x10, 0x20, 0x40, 0x0, 0x4, 0x23, 0xd, 0xb1, 0x80, 0x4c,
0x70, 0x70, 0x28, 0x70, 0x24, 0x49, 0x0, 0x69, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e,
0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, 0x20, 0x40, 0x81, 0x0, 0x0, 0x10,
0x10, 0xe6, 0x91, 0xc0, 0xce, 0x3f, 0x28, 0x83, 0x29, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba,
0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x92, 0x2, 0x4, 0x8, 0x0, 0x0, 0x81,
0xd3, 0x34, 0xf0, 0x9, 0x92, 0x7, 0x5, 0xd, 0x6, 0x8, 0xa2, 0xb, 0xa7, 0x1b, 0xaf, 0x2e, 0x88,
0x13, 0xc6, 0xeb, 0xcb, 0xa2, 0x74, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a, 0x6, 0x48, 0x4, 0x10,
0x20, 0x0, 0x2, 0xb, 0xec, 0xdd, 0xc0, 0x19, 0xd7, 0xbc, 0x14, 0x30, 0x16, 0x20, 0x80, 0x2c,
0x9c, 0x6e, 0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda, 0x82, 0xf, 0x2d,
0xa8, 0x19, 0x10, 0x10, 0x40, 0x80, 0x0, 0x8, 0x1c, 0xcb, 0x6f, 0xa0, 0x99, 0x20, 0x54, 0x41,
0x54, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, 0x10, 0x79,
0x6d, 0x40, 0xc9, 0x0, 0x82, 0x4, 0x0, 0x0, 0x40, 0x1a, 0x5b, 0x58, 0x3, 0x38, 0xfd, 0x82,
0x85, 0x82, 0x83, 0xd0, 0x7, 0x13, 0x8d, 0xd7, 0x97, 0x44, 0x9, 0xe3, 0x75, 0xe5, 0xd1, 0x3a,
0xd, 0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3, 0x22, 0x4, 0x8, 0x10, 0x0, 0x1, 0x6, 0x23, 0x6a, 0xe8,
0xc, 0xeb, 0xe4, 0xc8, 0x12, 0xb4, 0x6c, 0x3a, 0x39, 0x73, 0xe9, 0xcb, 0x2e, 0x9c, 0xd9, 0x77,
0x20, 0x64, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xcc, 0xd0, 0x80, 0x0, 0x0, 0x12, 0xad,
0x1e, 0x3f, 0x1c, 0x4, 0xed, 0x3b, 0xc2, 0xa7, 0xc3, 0x3c, 0xe1, 0x82, 0xc1, 0x20, 0x0, 0xc,
0x11, 0x40, 0x21, 0x68, 0xd8, 0x74, 0x72, 0xe7, 0xd3, 0x96, 0x5d, 0x39, 0xb2, 0xee, 0x40, 0xcd,
0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x91, 0x11, 0x0, 0x0, 0x0, 0x24, 0xa, 0xb9, 0x91,
0x10, 0xc3, 0xd3, 0xb2, 0x28, 0x71, 0x3c, 0x54, 0x41, 0x34, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78,
0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xc9, 0x1, 0x2, 0x4, 0x0,
0x0, 0x40, 0x42, 0x9a, 0x56, 0x3, 0x38, 0xfd, 0x82, 0x85, 0x2, 0x43, 0x90, 0x6, 0x13, 0x8d,
0xd7, 0x97, 0x44, 0x9, 0xe3, 0x75, 0xe5, 0xd1, 0x3a, 0xd, 0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3,
0x22, 0x2, 0x8, 0x10, 0x0, 0x1, 0x8, 0xf2, 0x70, 0xe4, 0x13, 0x1c, 0x22, 0xa0, 0x8, 0xb4, 0x6c,
0x3a, 0x39, 0x73, 0xe9, 0xcb, 0x2e, 0x9c, 0xd9, 0x77, 0x20, 0x68, 0x83, 0x4e, 0xd4, 0x10, 0x79,
0x6d, 0x40, 0xc4, 0x48, 0x80, 0x0, 0x0, 0x11, 0x80, 0x7e, 0x2c, 0x81, 0xfd, 0xb2, 0x5c, 0x58,
0xbf, 0x17, 0x48, 0x2, 0x49, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6,
0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x91, 0x2, 0x4, 0x8, 0x0, 0x0, 0x84, 0x5f, 0xb6, 0x64, 0x9,
0x8e, 0xc, 0x44, 0xd, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34,
0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0x58, 0x10, 0x20, 0x40, 0x0, 0x4, 0x2e, 0x8a, 0x53, 0x20,
0x3e, 0xb3, 0x18, 0x28, 0x38, 0x14, 0x29, 0x0, 0x51, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37,
0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, 0x20, 0x40, 0x81, 0x0, 0x0,
0x10, 0x3a, 0xd6, 0x9d, 0x41, 0x32, 0x40, 0xa8, 0x83, 0xe9, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1,
0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x9a, 0x2, 0x4, 0x8, 0x0, 0x0,
0xab, 0x70, 0xaf, 0xe2, 0xa0, 0x7c, 0xe6, 0x30, 0x51, 0x0, 0x78, 0xa2, 0x20, 0x72, 0x71, 0xba,
0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x62,
0xc0, 0x81, 0x2, 0x0, 0x0, 0x21, 0x8c, 0x2f, 0x5a, 0x82, 0xb, 0x67, 0x41, 0x41, 0xa0, 0xa1,
0x68, 0x4, 0x9, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8,
0x20, 0xf2, 0xda, 0x81, 0x99, 0x2, 0x4, 0x8, 0x0, 0x0, 0xab, 0x7b, 0xcf, 0x8c, 0x40, 0x83,
0x59, 0xd4, 0x40, 0xf4, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e,
0xd4, 0x10, 0x79, 0x6d, 0x40, 0xcd, 0x1, 0x2, 0x4, 0x0, 0x0, 0x55, 0xaf, 0xc8, 0x33, 0x20,
0x3e, 0x73, 0x18, 0x28, 0x40, 0x34, 0x31, 0x90, 0x29, 0x68, 0xd8, 0x74, 0x72, 0xe7, 0xd3, 0x96,
0x5d, 0x39, 0xb2, 0xee, 0x40, 0xc5, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x99, 0xa1, 0x0,
0x0, 0x0, 0x45, 0x5a, 0x6e, 0x7c, 0xf8, 0x9, 0xda, 0x77, 0x80, 0x80, 0x60, 0x2, 0xa7, 0xbf,
0xbc, 0xf1, 0xa, 0xc1, 0x60, 0x0, 0xc, 0x15, 0x61, 0x4, 0x0, 0xc, 0xc, 0x80, 0x20, 0x9c, 0x6e,
0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda, 0x82, 0xf, 0x2d, 0xa8, 0x19,
0x90, 0x20, 0x40, 0x80, 0x0, 0xa, 0xb6, 0xbd, 0x0, 0x6c, 0x8, 0x35, 0x9d, 0x40, 0x22, 0x4e,
0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4,
0xc, 0xc8, 0x10, 0x20, 0x40, 0x0, 0x5, 0x5b, 0x90, 0x7f, 0x2e, 0x3, 0xed, 0x31, 0xa8, 0x2,
0xad, 0x1b, 0xe, 0x8e, 0x5c, 0xfa, 0x72, 0xcb, 0xa7, 0x36, 0x5d, 0xc8, 0x1a, 0xa0, 0xd3, 0xb5,
0x4, 0x1e, 0x5b, 0x50, 0x31, 0x12, 0x20, 0x0, 0x0, 0x4, 0x60, 0x5b, 0x80, 0xa0, 0x73, 0x1c,
0x62, 0x17, 0x9, 0x48, 0x12, 0x20, 0x8a, 0x71, 0xba, 0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba,
0x27, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x64, 0x80, 0x81, 0x2, 0x0, 0x0, 0x20, 0xc3,
0x4d, 0x6f, 0x1, 0x9d, 0x7d, 0x1, 0x42, 0x41, 0x1, 0xa0,
];
// More data:
// - 34 (Neuer Pferdemarkt/Neuer Kamp): 2 layers, quite complex bike crossings
/// Just some random SPATEM
#[cfg(feature = "spat")]
pub const SPATEM: &[u8] = &[
0x02, 0x04, 0x00, 0x00, 0x30, 0x16, 0x00, 0x38, 0x4a, 0x6c, 0x1a, 0x17, 0xe4, 0xd4, 0xa7, 0x7d,
0x93, 0x5b, 0x8c, 0x9a, 0xb0, 0x63, 0x71, 0x92, 0xe6, 0x50, 0x00, 0x30, 0x9c, 0x50, 0x40, 0x00,
0x0e, 0xa7, 0x1d, 0x4f, 0xf0, 0x10, 0x00, 0x22, 0x8c, 0xe2, 0xf9, 0x44, 0xff, 0x63, 0x5b, 0x63,
0x48, 0x71, 0xad, 0xb1, 0xae, 0xa1, 0xae, 0xa7, 0x80, 0x10, 0xa1, 0xb8, 0xbe, 0x29, 0x40, 0xa0,
0xd6, 0xd8, 0xd1, 0x5c, 0x6b, 0x6c, 0x6c, 0x0c, 0x6c, 0x0d, 0xe0, 0x06, 0x28, 0x0e, 0x2d, 0xfa,
0x68, 0xf6, 0x35, 0xb6, 0x34, 0x57, 0x1a, 0xdb, 0x1b, 0x03, 0x1b, 0x03, 0x78, 0x02, 0x0a, 0x1b,
0x8d, 0x8b, 0x8d, 0xc0, 0x0d, 0xa7, 0x0d, 0x11, 0xc6, 0xd3, 0x86, 0xd4, 0xc6, 0xd4, 0xde, 0x00,
0xa2, 0x8a, 0xe3, 0x5f, 0x23, 0x6c, 0x43, 0x66, 0x03, 0x43, 0x71, 0xb3, 0x01, 0xbb, 0x71, 0xba,
0xd1, 0x80, 0x30, 0xa0, 0x38, 0xaf, 0x79, 0x86, 0x50, 0xd6, 0xd8, 0xd1, 0x5c, 0x6b, 0x6c, 0x6c,
0x0c, 0x6c, 0x0c, 0x60, 0x0e, 0x28, 0xce, 0x35, 0xfc, 0x36, 0xce, 0x36, 0x6a, 0x34, 0x87, 0x1b,
0x35, 0x1b, 0x44, 0x1b, 0x44, 0x78, 0x04, 0x0a, 0x1b, 0x8d, 0x8b, 0x8d, 0xc0, 0x0d, 0xa7, 0x0d,
0x15, 0xc6, 0xd3, 0x86, 0xe3, 0xc6, 0xe1, 0x46, 0x01, 0x22, 0x80, 0xe2, 0xdf, 0xa5, 0xcd, 0x03,
0x5b, 0x63, 0x45, 0x71, 0xad, 0xb1, 0xb1, 0xc1, 0xb0, 0xd1, 0x80,
];
/// Some random CAM
#[cfg(feature = "cam")]
pub const CAM: &[u8] = &[
0x02, 0x02, 0xde, 0x14, 0x0c, 0xe5, 0xc7, 0xc0, 0x40, 0x5a, 0xb2, 0x3d, 0x82, 0xce, 0x27, 0x81,
0xe9, 0xa2, 0x78, 0x27, 0x4b, 0xc6, 0x33, 0xfa, 0x54, 0x58, 0x7c, 0xa0, 0xa2, 0x7e, 0x83, 0x02,
0x96, 0x8a, 0x97, 0x33, 0xff, 0x82, 0x00, 0x1a, 0x10, 0x3f, 0xe0, 0x14, 0x39, 0x80, 0x10, 0x6e,
0x00, 0x75, 0x80, 0x11, 0x58, 0xce, 0x00, 0x02, 0xf0, 0x3a, 0xdc, 0x08, 0xc4, 0xc8, 0x00, 0x01,
0x57, 0x81, 0xd6, 0x20, 0x46, 0x96, 0x33, 0x80, 0x0a, 0xbc, 0x0e, 0xdb, 0x02, 0x39, 0x31, 0x9c,
0x00, 0x55, 0xe0, 0x75, 0x08, 0x11, 0x85, 0x90, 0x00, 0x02, 0xaf, 0x03, 0xa0, 0xc0, 0x91, 0x2c,
0x80, 0x00, 0x16, 0x78, 0x1c, 0x9e, 0x05, 0x65, 0x64, 0x00, 0x00, 0xc3, 0xc0, 0xe0, 0x90, 0x2d,
0xbb, 0x19, 0xc0, 0x06, 0xde, 0x05, 0x8d, 0x81, 0x02, 0x18, 0xce, 0x00, 0x35, 0xf0, 0x15, 0x5c,
0x00, 0x06, 0xc6, 0x70, 0x00, 0xdf, 0x80, 0x8d, 0x5f, 0xde, 0x66, 0x27, 0x00, 0x07, 0x3c, 0x04,
0x76, 0xfd, 0x67, 0x31, 0x9c, 0x00, 0x58, 0x60, 0x41, 0x37, 0xd3, 0x15, 0x89, 0xc0, 0x06, 0xdc,
];
#[cfg(any(feature = "cam_tx", feature = "uart"))]
pub const CAM_SMALL: &[u8] = &[
0x2, 0x2, 0xc1, 0x73, 0x25, 0x68, 0x17, 0x28, 0x40, 0x29, 0xd5, 0xb3, 0x40, 0xf, 0x1f, 0xea,
0xf0, 0x1f, 0xff, 0xff, 0xfc, 0x23, 0xb7, 0x74, 0x3e, 0x0, 0x0, 0xf, 0xc0, 0x0, 0x7e, 0x81,
0x38, 0x25, 0x7, 0x37, 0xfe, 0xeb, 0xff, 0xf6, 0x0, 0x0, 0xd, 0xff, 0xff, 0x7f, 0xff, 0xf1,
0xce, 0x40, 0x40, 0x0,
];

106
src/ubx.rs Normal file
View file

@ -0,0 +1,106 @@
//! u-blox helpers
#![allow(unused)]
use alloc::vec::Vec;
pub fn pretty_print_mon_ver(packet: &ublox::mon_ver::MonVerRef) -> alloc::string::String {
alloc::format!(
"UBX-MON-VER: SW version: {} HW version: {}; Extensions: {:?}",
packet.software_version(),
packet.hardware_version(),
packet.extension().collect::<Vec<&str>>()
)
}
pub fn pretty_print_mon_comms(packet: &ublox::mon_comms::MonCommsRef) -> alloc::string::String {
alloc::format!(
"UBX-MON-COMMS: {} ports ({:?}), {} tx err, info {:?}",
packet.n_ports(),
packet.prot_ids(),
packet.tx_errors(),
packet
.ports()
.map(|v| alloc::format!("{v:?}"))
.collect::<Vec<_>>()
)
}
pub fn pretty_print_mon_rf(packet: &ublox::mon_rf::MonRfRef) -> alloc::string::String {
alloc::format!(
"UBX-MON-RF: {:?}",
packet
.blocks()
.map(|v| alloc::format!("{v:?}"))
.collect::<Vec<_>>()
)
}
pub fn pretty_print_nav_sat(packet: &ublox::nav_sat::NavSatRef) -> alloc::string::String {
// sort by quality flags
let mut sat_infos = packet.svs().collect::<Vec<_>>();
sat_infos.sort_unstable_by_key(|v| v.flags_raw() & 0x7);
sat_infos.reverse();
let sat_info_strings = sat_infos
.iter()
.map(|v| {
alloc::format!(
"- {}/{:3}: E {:3.0}, A {:3.0}, CNO {:2.1} dBHz, {:?}, {:?}",
v.gnss_id(),
v.sv_id(),
v.elev(),
v.azim(),
v.cno(),
v.flags().quality_ind(),
v.flags().health(),
)
})
.collect::<Vec<_>>();
alloc::format!(
"UBX-NAV-SAT: num {:?}\n{}",
packet.num_svs(),
sat_info_strings.join("\n")
)
}
impl<'a> TryFrom<&ublox::nav_pvt::proto27::NavPvtRef<'a>> for crate::applogic::GnssFix {
type Error = alloc::string::String;
fn try_from(value: &ublox::nav_pvt::proto27::NavPvtRef<'a>) -> Result<Self, Self::Error> {
let has_time = value.fix_type() == ublox::GnssFixType::Fix3D
|| value.fix_type() == ublox::GnssFixType::GPSPlusDeadReckoning
|| value.fix_type() == ublox::GnssFixType::TimeOnlyFix;
let has_posvel = value.fix_type() == ublox::GnssFixType::Fix3D
|| value.fix_type() == ublox::GnssFixType::GPSPlusDeadReckoning;
if !has_posvel || !has_time {
return Err(alloc::format!(
"NAV-PVT without valid fix: {:?}",
value.fix_type()
));
}
let pos: ublox::PositionLLA = value.into();
let vel: ublox::Velocity = value.into();
let time: chrono::DateTime<chrono::Utc> = value
.try_into()
.map_err(|err| alloc::format!("Failed to convert time: {err}"))?;
let latitude_deg = pos.lat;
let longitude_deg = pos.lon;
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let heading_deg = vel.heading as f32;
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let speed_mps = vel.speed as f32;
Ok(Self {
time: Some(time.naive_utc()),
latitude_deg,
longitude_deg,
heading_deg: Some(heading_deg),
speed_mps: Some(speed_mps),
})
}
}