Watch
0
0
Fork
You've already forked esp32-c_its-companion
0

Compare commits

..
Author SHA1 Message Date
Jannik Beyerstedt
6bfccf7f22 WIP: try rasn tweaks
1.2 instead of 1.9 MB
2026-05-12 22:35:37 +02:00
db4dffa7b3 WIP feat: Add c-its-parser
TODO: Remove CAM again
2026-05-11 22:44:16 +02:00
46 changed files with 270 additions and 34875 deletions

View file

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

4
.gitignore vendored
View file

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

612
Cargo.lock generated
View file

@ -17,92 +17,24 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c583acf993cf4245c4acb0a2cc2ab1f9cc097de73411bb6d3647ff6af2b1013d"
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary-int"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993a810118f8f37e9c4411c86f1c4c940a09a7ab34b7bf2d88d06f50c553fab7"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "assert_float_eq"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd9777fd8234cccd34019d9e26d70c8247f9baf24c8fdc0b56c548cf943af2ed"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "az"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973"
[[package]]
name = "base64"
version = "0.13.1"
@ -137,9 +69,9 @@ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bitvec"
version = "1.1.1"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
@ -166,29 +98,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bt-hci"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "211713d2e9fb4793ce4360a712c0764264aff6be48932ccf02ca2a331c0436a9"
dependencies = [
"btuuid",
"embassy-sync 0.8.0",
"embedded-io 0.7.1",
"embedded-io-async 0.7.0",
"futures-intrusive",
"heapless 0.9.2",
]
[[package]]
name = "btuuid"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f48f1e9b0aad0a4f05d17bdeae0fa20ff798e272a03a6940ca27ad9c5a6ae7"
dependencies = [
"uuid",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
@ -215,9 +124,8 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "c-its-parser"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e94d9990b20498f621f164f23ae198c4365115eb4191ae106352ec74d4a4110"
version = "2.1.0"
source = "git+https://github.com/consider-it/C-ITS-Parser.git?branch=feat%2Frasn-tweaks#f3208f5d2d1e14265809c652ce62d0668d1f3af6"
dependencies = [
"chrono",
"etherparse",
@ -255,27 +163,12 @@ dependencies = [
"num-traits",
]
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "const-default"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa"
[[package]]
name = "convert_case"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cordyceps"
version = "0.3.4"
@ -624,21 +517,6 @@ dependencies = [
"heapless 0.9.2",
]
[[package]]
name = "embassy_gps"
version = "0.1.0"
source = "git+https://github.com/jbeyerstedt/embassy_gps.git?branch=feat%2Frework-gpsfix#75f1a67a69372234330c0c9618a953c415380b99"
dependencies = [
"chrono",
"embassy-futures",
"embassy-time",
"embedded-io-async 0.7.0",
"esp-hal",
"heapless 0.9.2",
"log",
"nmea",
]
[[package]]
name = "embedded-can"
version = "0.4.1"
@ -648,29 +526,6 @@ dependencies = [
"nb 1.1.0",
]
[[package]]
name = "embedded-graphics"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8da660bb0c829b34a56a965490597f82a55e767b91f9543be80ce8ccb416fe"
dependencies = [
"az",
"byteorder",
"embedded-graphics-core",
"float-cmp",
"micromath",
]
[[package]]
name = "embedded-graphics-core"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95743bef3ff70fcba3930246c4e6872882bbea0dcc6da2ca860112e0cd4bd09f"
dependencies = [
"az",
"byteorder",
]
[[package]]
name = "embedded-hal"
version = "0.2.7"
@ -696,16 +551,6 @@ dependencies = [
"embedded-hal 1.0.0",
]
[[package]]
name = "embedded-hal-bus"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "513e0b3a8fb7d3013a8ae17a834283f170deaf7d0eeab0a7c1a36ad4dd356d22"
dependencies = [
"critical-section",
"embedded-hal 1.0.0",
]
[[package]]
name = "embedded-io"
version = "0.6.1"
@ -772,45 +617,12 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "env_filter"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "esp-alloc"
version = "0.10.0"
@ -988,7 +800,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23fbff98b06a96b6ce3791ecec5c668524052a068e23aacd23afe17ddba844ce"
dependencies = [
"allocator-api2",
"bt-hci",
"cfg-if",
"docsplay",
"document-features",
@ -1184,22 +995,13 @@ dependencies = [
name = "esp32-c_its-companion"
version = "0.1.0"
dependencies = [
"assert_float_eq",
"bt-hci",
"c-its-parser",
"chrono",
"critical-section",
"crossbeam-queue",
"embassy-executor",
"embassy-futures",
"embassy-sync 0.7.2",
"embassy-time",
"embassy_gps",
"embedded-graphics",
"embedded-hal-bus",
"embedded-io 0.7.1",
"embedded-io-async 0.7.0",
"env_logger",
"esp-alloc",
"esp-backtrace",
"esp-bootloader-esp-idf",
@ -1207,29 +1009,7 @@ dependencies = [
"esp-println",
"esp-radio",
"esp-rtos",
"esp32-cits-core",
"geo-types",
"log",
"mipidsi",
"num-traits",
"profont",
"prost",
"prost-build",
"static_cell",
"trouble-host",
"ublox",
]
[[package]]
name = "esp32-cits-core"
version = "0.1.0"
source = "git+https://git.hamburg.ccc.de/jtbx/esp32-cits-core?branch=main#a6d0ea6ca2da130fde2b84060a34ef45e772a459"
dependencies = [
"c-its-parser",
"chrono",
"esp-hal",
"esp-radio",
"geo-types",
]
[[package]]
@ -1297,40 +1077,19 @@ dependencies = [
[[package]]
name = "etherparse"
version = "0.21.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17304d06addb3283cdc4bd528e42dd95e73c8ee2d6492ffce415e93660885449"
checksum = "3ac016aaf11dfe643edcd088a166234bfcb72e7f06691abfcf21e9af524037f4"
dependencies = [
"arrayvec",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -1358,52 +1117,12 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-intrusive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
]
[[package]]
name = "futures-io"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-sink"
version = "0.3.32"
@ -1423,10 +1142,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-sink",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
@ -1471,11 +1188,13 @@ dependencies = [
[[package]]
name = "geonetworking"
version = "0.4.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6da6fde770112587d5f5f7af439546e35bd35aa77a9e3e47e9968056783fe50d"
checksum = "ea4f48e7b1cdf91ad77ec1e4c5617aee3666a929288f4a3f11e473b67d3a6314"
dependencies = [
"arbitrary-int",
"bitvec",
"bitvec-nom2",
"bytes",
"nom",
"num",
"num-traits",
@ -1596,12 +1315,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.13.0"
@ -1641,18 +1354,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "js-sys"
version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@ -1683,27 +1384,12 @@ version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
@ -1738,35 +1424,12 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "micromath"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "mipidsi"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790ebd28bd67addbccf41b1c0c188c26bb9f5bdcd91d4d6da9bd558e20d97a1d"
dependencies = [
"embedded-graphics-core",
"embedded-hal 1.0.0",
"heapless 0.8.0",
]
[[package]]
name = "multimap"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
[[package]]
name = "nb"
version = "0.1.3"
@ -1782,20 +1445,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
[[package]]
name = "nmea"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2086c773d18da556c05ca235596d163c00379027189bafe2209ae40ebd19717c"
dependencies = [
"arrayvec",
"cfg-if",
"chrono",
"heapless 0.8.0",
"nom",
"num-traits",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -1913,29 +1562,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "petgraph"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
"indexmap",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
@ -2006,66 +1638,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "profont"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016681370a9dd6e7ddb4c1a959922fd59dc45e5ebaa5ff5b13090267898ced34"
dependencies = [
"embedded-graphics",
]
[[package]]
name = "prost"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
dependencies = [
"heck",
"itertools",
"log",
"multimap",
"petgraph",
"prettyplease",
"prost",
"prost-types",
"regex",
"syn 2.0.117",
"tempfile",
]
[[package]]
name = "prost-derive"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "prost-types"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a"
dependencies = [
"prost",
]
[[package]]
name = "quote"
version = "1.0.45"
@ -2108,8 +1680,7 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rasn"
version = "0.28.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88235e585f2f3fc26ad809c79a84dd77e6a7203eb7d12e5bcaab89fc036d3d90"
source = "git+https://github.com/consider-it/rasn.git?branch=feat%2Ffeature-flags#2f7833af4339cac3be1775c2cd262e735f5e8ad0"
dependencies = [
"bitvec",
"bitvec-nom2",
@ -2123,7 +1694,6 @@ dependencies = [
"num-traits",
"once_cell",
"rasn-derive",
"serde_json",
"snafu",
"xml-no-std",
]
@ -2131,8 +1701,7 @@ dependencies = [
[[package]]
name = "rasn-derive"
version = "0.28.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11dcd4dab09ceadedb3e2bedf340deb583f0e25bba9ad966b5655c2fb62b1392"
source = "git+https://github.com/consider-it/rasn.git?branch=feat%2Ffeature-flags#2f7833af4339cac3be1775c2cd262e735f5e8ad0"
dependencies = [
"proc-macro2",
"rasn-derive-impl",
@ -2142,8 +1711,7 @@ dependencies = [
[[package]]
name = "rasn-derive-impl"
version = "0.28.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f121c276d3f2fba8caabc870288de3f81abdb0bb94d5510651e58e1e00a7b9e7"
source = "git+https://github.com/consider-it/rasn.git?branch=feat%2Ffeature-flags#2f7833af4339cac3be1775c2cd262e735f5e8ad0"
dependencies = [
"either",
"itertools",
@ -2153,18 +1721,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
@ -2178,9 +1734,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.11"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "riscv"
@ -2254,19 +1810,6 @@ dependencies = [
"svgbobdoc",
]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@ -2285,12 +1828,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.28"
@ -2368,12 +1905,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
@ -2422,15 +1953,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_cell"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23"
dependencies = [
"portable-atomic",
]
[[package]]
name = "strsim"
version = "0.11.1"
@ -2499,18 +2021,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "termcolor"
version = "1.4.1"
@ -2620,68 +2130,12 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "trouble-host"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df7817cead4b83dfbeeaa59736ecbc97c30b21dc3bfc0cc2ce8a6687f70b37a"
dependencies = [
"bt-hci",
"embassy-futures",
"embassy-sync 0.7.2",
"embassy-time",
"embedded-io 0.7.1",
"futures",
"heapless 0.9.2",
"rand_core 0.6.4",
"static_cell",
"trouble-host-macros",
"zerocopy",
]
[[package]]
name = "trouble-host-macros"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d809ae05f510cf8f1d863749281eb28e049d9ae4ad52d92ea7a0ac068a0e17c"
dependencies = [
"convert_case",
"darling 0.20.11",
"proc-macro2",
"quote",
"syn 2.0.117",
"uuid",
]
[[package]]
name = "typenum"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "ublox"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bcc96bd937066775fb87034e1dbf6d7165a8975c74ddec1105e31b4a050538b"
dependencies = [
"bitflags",
"chrono",
"num-traits",
"ublox_derive",
]
[[package]]
name = "ublox_derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5eda7d1c1151a40ae85c7c065a19959909573535034e18e1015b738993cd2c0"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "ufmt-write"
version = "0.1.0"
@ -2694,12 +2148,6 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]]
name = "unicode-width"
version = "0.1.14"
@ -2718,12 +2166,6 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.23.1"
@ -2731,8 +2173,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
"getrandom",
"js-sys",
"wasm-bindgen",
]
[[package]]
@ -3038,26 +2478,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "zerocopy"
version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "zmij"
version = "1.0.21"

View file

@ -3,134 +3,56 @@ edition = "2024"
name = "esp32-c_its-companion"
rust-version = "1.88"
version = "0.1.0"
default-run = "esp32-c_its-companion"
[[bin]]
name = "esp32-c_its-companion"
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-futures",
"dep:embassy-sync",
"dep:embassy-time",
"dep:embedded-io",
"dep:embedded-io-async",
"dep:esp-alloc",
"dep:esp-backtrace",
"dep:esp-println",
"dep:esp-radio",
"dep:bt-hci",
"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"]
# enable I/O via BLE
ble = ["_gnss"]
# internal feature, that any UART I/O is enabled
_uart = []
# internal feature, that any GNSS source is enabled
_gnss = []
[dependencies]
esp-hal = { version = "~1.1.0", optional = true, features = ["esp32c5", "log-04", "unstable"] }
esp-hal = { version = "~1.1.0", features = ["esp32c5", "log-04", "unstable"] }
esp-rtos = { version = "0.3.0", optional = true, features = [
esp-rtos = { version = "0.3.0", features = [
"embassy",
"esp-alloc",
"esp-radio",
"embassy",
"esp32c5",
"log-04",
] }
esp-bootloader-esp-idf = { version = "0.5.0", optional = true, features = ["esp32c5", "log-04"] }
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32c5", "log-04"] }
log = "0.4.27"
critical-section = "1.2.0"
bt-hci = { version = "0.8.0", optional = true }
embassy-executor = { version = "0.10.0", optional = true, features = ["log"] }
embassy-futures = { version = "0.1.2", optional = true }
embassy-sync = { version = "0.7", optional = true }
embassy-time = { version = "0.5.0", optional = true, features = ["log"] }
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 = [
embassy-executor = { version = "0.10.0", features = ["log"] }
embassy-time = { version = "0.5.0", features = ["log"] }
embedded-io = "0.7.1"
embedded-io-async = "0.7.0"
esp-alloc = "0.10.0"
esp-backtrace = { version = "0.19.0", features = [
"esp32c5",
"panic-handler",
"println",
] }
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", optional = true, features = [
esp-println = { version = "0.17.0", features = ["esp32c5", "log-04"] }
esp-radio = { version = "0.18.0", features = [
"esp-alloc",
"esp32c5",
"log-04",
"unstable",
"wifi",
"sniffer",
"ble",
] }
trouble-host = { version = "0.6.0", features = ["gatt", "central", "derive", "default-packet-pool"] }
static_cell = "2"
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 = [
c-its-parser = { version = "2.1.0", git = "https://github.com/consider-it/C-ITS-Parser.git", branch = "feat/rasn-tweaks", default-features = false, features = [
"time",
"geo",
"libm",
"transport",
"mapem",
"spatem",
"cam", # TODO: for initial setup only
] }
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", 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
@ -146,6 +68,14 @@ lto = 'fat'
opt-level = 's'
strip = "debuginfo"
## cargo build --release && espflash save-image --chip esp32c5 target/riscv32imac-unknown-none-elf/release/esp32-c_its-companion out.bin
# default rasn, debug 2, strip default: 1,904,496 byte
# default rasn, debug 2, strip true: 1,904,592 byte
# default rasn, debug default, strip default: 1,905,312 byte
# only UPER, debug 2, strip default: 1,229,840 byte
# only UPER, debug 2, strip true: 1,229,760 byte
# only UPER, debug default, strip default: 1,231,136 byte
[profile.release-dbg]
inherits = "release"
strip = false

View file

@ -1,68 +1,7 @@
# ESP32-C5 C-ITS Companion/ Visualizer
# ESP32-C5 C-ITS Visualizer
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 240*280px screen (connected on GPIO8 (SCL/ SCK), GPIO10 (SDA/ MOSI), GPIO7 (reset), GPIO23 (DC), GPIO24 (CS), 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).
### BLE
Enable BLE I/O protocol according to [docs/ble-protocol.md](./docs/ble-protocol.md).
Enabling `denm` feature or `cam` feature is recommended, otherwise no data will be received.
Note: When changing the set of supported messages on the BLE interface, you may need to turn bluetooth off and on again on your client device.
Otherwise the client may not re-scan the service's characteristics and still use the previous set of characteristics.
## Usage
### Toolchain Installation
@ -77,7 +16,7 @@ rustup target add riscv32imac-unknown-none-elf
cargo install espflash --locked
```
(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 -o ble-trouble`.)
(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
@ -85,37 +24,8 @@ rustup component add --toolchain nightly rustfmt
```
### 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 with UART")
- `esp,ble,cam,denm,cam_tx` ("smartphone companion with BLE")
Connect your ESP32-C5 and run:
```
# default configuration
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,23 +1,7 @@
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();
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
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 main() {
linker_be_nice();
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
println!("cargo:rustc-link-arg=-Tlinkall.x");
}
fn linker_be_nice() {

View file

@ -1,124 +0,0 @@
# 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-4002-ada8-f99a02ee315c | `RawNotify` | Raw message notification
c0b70001-d4f4-4003-ada8-f99a02ee315c | `RawMsgRx` | Raw CAM message (de-duplicated/ rate-limited)
c0b70001-d4f4-4004-ada8-f99a02ee315c | `CamEvent` | Latest received CAM
c0b70001-d4f4-4005-ada8-f99a02ee315c | `RawMsgRx` | Raw DENM message (de-duplicated/ rate-limited)
c0b70001-d4f4-4006-ada8-f99a02ee315c | `DenmEvent` | Latest received DENM
c0b70001-d4f4-4007-ada8-f99a02ee315c | `RawMsgRx` | Raw MAPEM message (de-duplicated/ rate-limited)
c0b70001-d4f4-4008-ada8-f99a02ee315c | `RawMsgRx` | Raw SPATEM message (de-duplicated/ rate-limited)
Since a modern BLE notification can only fit up tp 251 bytes of payload, the raw messages can only be polled.
Please subscribe to `c0b70001-d4f4-4002-ada8-f99a02ee315c` and use the message ID to determine from which service to read the incoming value.
## 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)).
Maximum message size (assuming realistic value ranges):
field name | tag size | (max.) data size
----------------- | -------- | ---------
timestamp_ms | 1 | 8 (I64)
position | 1 | 1 (LEN(10)) + sub-fields below
\+ latitude_deg | 1 | 4 (I32)
\+ longitude_deg | 1 | 4 (I32)
heading | 1 | 2 (VARINT(3600))
speed | 1 | 2 (VARINT(~163 m/s))
**SUM** | 6 + | 21 = 27 byte
### 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.
Maximum message size (assuming realistic value ranges):
field name | tag size | (max.) data size
----------------- | -------- | ---------
timestamp | 1 | 8 (I64)
validity_end_ts | 1 | 8 (I64)
station_id | 1 | 8 (I64)
seq_num | 1 | 3 (VARINT(65535))
event_position | 1 | 1 (LEN(10)) + sub-fields below
\+ latitude_deg | 1 | 4 (I32)
\+ longitude_deg | 1 | 4 (I32)
event_heading_deg | 1 | 1 (VARINT(360))
direction | 1 | 1 (VARINT(3))
cause_code | 1 | 1 (VARINT(127))
sub_cause_code | 1 | 1 (VARINT(127))
**SUM** | 11 + | 40 = 51 byte
Note: Cause code and sub-cause code are technically 0..255 in the ASN.1 spec, but that's mainly to ensure backwards-compatibility of the CHOICE with the old wire format.
Realistically only cause codes up to 100 are defined and common sub-cause codes use values up to 22.
### CAM Event
Each received (low-frequency) CAM will be "published" as a `CamEvent` message (see [c_its-messages.proto](./c_its-messages.proto)).
Maximum message size (assuming realistic value ranges):
field name | tag size | (max.) data size
--------------------- | -------- | ---------
station_id | 1 | 8 (I64)
station_type | 1 | 1 (VARINT(15))
position | 1 | 1 (LEN(10)) + sub-fields below
\+ latitude_deg | 1 | 4 (I32)
\+ longitude_deg | 1 | 4 (I32)
vehicle_role | 1 | 1 (VARINT(15))
vehicle_data | 1 | 1 (LEN(11)) + sub-fields below
\+ heading_deg | 1 | 2 (VARINT(360))
\+ speed | 1 | 2 (VARINT(~163 m/s))
\+ vehicle_length_dm | 1 | 2 (VARINT(1023))
\+ vehicle_width_dm | 1 | 1 (VARINT(62))
**SUM** | 11 + | 27 = 38 byte
### 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)
Maximum message size (assuming realistic value ranges):
field name | tag size | (max.) data size
--------------------- | -------- | ---------
msg_type | 1 | 1 (VARINT(21))
payload | 1 | 2+1394 (LEN(1394) + payload)
**SUM** | 2 + | 1397 = 1399 byte
### Raw Message Notification
Maximum message size (assuming realistic value ranges):
field name | tag size | (max.) data size
--------------------- | -------- | ---------
msg_type | 1 | 1 (VARINT(21))
**SUM** | 1 + | 1 = 2 byte

View file

@ -1,126 +0,0 @@
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
}
message RawNotify {
required uint32 message_type = 1; // ITS `MessageId` integer value
}
// ------------------------------
// 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 (in seconds)
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;
}
}

View file

@ -1,20 +0,0 @@
# 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`

View file

@ -1,74 +0,0 @@
#!/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
ble,cam,denm,cam_tx
uart,ble,cam,denm
spat,gnss_ubx,screen
spat,gnss_ubx,screen,cam_tx
)
features_more=(
spat,gnss_ubx,screen,ble
spat,gnss_ubx,screen,ble,cam_tx
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.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because it is too large Load diff

View file

@ -1,138 +0,0 @@
{
"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

@ -1,744 +0,0 @@
{
"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

@ -1 +0,0 @@
{"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

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

View file

@ -1,542 +0,0 @@
(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

@ -1,558 +0,0 @@
(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

@ -1,445 +0,0 @@
(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

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

View file

@ -1,6 +0,0 @@
#!/bin/sh
## Builds common config for smartphone connected device
## -> receive CAM and DENMs via BLE and send CAMs based on position data received from BLE
cargo run --release --no-default-features -F esp,cam,denm,ble,cam_tx

View file

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

View file

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

View file

@ -1,6 +0,0 @@
#!/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

View file

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

View file

@ -7,32 +7,23 @@ 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()
);
}
info!(
"Got CAM from {} at {:.3}°N, {:.3}°E, speed {:.2} km/h",
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(),
obu.speed.speed_value.as_kmh()
);
}
HighFrequencyContainer::rsuContainerHighFrequency(rsu) => {
info!(

View file

@ -1,465 +0,0 @@
//! 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);
}
}

View file

@ -1,44 +0,0 @@
//! 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");
}
}

View file

@ -1,746 +1,4 @@
//! 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 timing: Option<TimingData>,
}
#[cfg(feature = "spat")]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[allow(clippy::struct_field_names)]
pub struct TimingData {
pub min_end_sec: i16,
pub likely_end_sec: Option<i16>,
pub max_end_sec: Option<i16>,
}
#[cfg(feature = "spat")]
impl TimingData {
pub fn new(value: &map::SignalGroup, now: chrono::DateTime<chrono::Utc>) -> Option<Self> {
let min = value
.min_end_time()
.map(|val| Self::time_to_duration_sec(now, val));
let likely_end_sec = value
.likely_time()
.map(|val| Self::time_to_duration_sec(now, val));
let max_end_sec = value
.max_end_time()
.map(|val| Self::time_to_duration_sec(now, val));
min.map(|min_end_sec| Self {
min_end_sec,
likely_end_sec,
max_end_sec,
})
}
fn time_to_duration_sec(
now: chrono::DateTime<chrono::Utc>,
time_point: chrono::DateTime<chrono::Utc>,
) -> i16 {
use core::ops::Sub as _;
use num_traits::Float as _;
let dur = time_point.sub(now).as_seconds_f32();
#[allow(clippy::cast_possible_truncation)]
let duration_sec = dur.round() as i16;
duration_sec
}
}
#[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")]
#[allow(clippy::too_many_lines)]
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(maneuver) = sig.maneuvers() {
let phase = sig
.phase()
.map(core::convert::Into::into)
.unwrap_or_default();
let timing = TimingData::new(sig, self.time.and_utc());
Some(SignalGroupData {
phase,
maneuver,
timing,
})
} 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));
}
}

View file

@ -1,117 +1,22 @@
//! C-ITS Traffic Light Data (MAPEM/ SPATEM) Evaluation/ Cache
//! C-ITS Traffic Light Data (MAPEM/ SPATEM) Evaluation
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 c_its_parser::standards::{
mapem_2_2_1::mapem_pdu_descriptions, spatem_2_2_1::spatem_pdu_descriptions,
};
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
}
pub fn handle_mapem(etsi: &mapem_pdu_descriptions::MAPEM) {
// TODO: proof of concept only -- replace with actual evaluation
if let Some(map) = &etsi.map.intersections {
for int in &map.0 {
println!("got MAPEM for intersection {}", int.id.id.0);
}
}
}
pub fn handle_spatem(etsi: &spatem_pdu_descriptions::SPATEM) {
// TODO: proof of concept only -- replace with actual evaluation
for int in &etsi.spat.intersections.0 {
println!("got SPATEM for intersection {}", int.id.id.0);
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,416 +0,0 @@
#![allow(
clippy::needless_borrows_for_generic_args,
reason = "gatt_service creates false positives"
)]
use core::cell::RefCell;
use critical_section::Mutex;
use crossbeam_queue::ArrayQueue;
use esp_backtrace as _;
use log::{info, warn};
use trouble_host::prelude::*;
use crate::io;
pub const CONTROLLER_SLOTS: usize = 20;
const CONNECTIONS_MAX: usize = 1;
const L2CAP_CHANNELS_MAX: usize = 1;
pub static CLIENT_CONNECTED: Mutex<RefCell<bool>> = Mutex::new(RefCell::new(false));
pub static TX_DATA: Mutex<RefCell<Option<ArrayQueue<BleSendable>>>> =
Mutex::new(RefCell::new(None));
pub static RX_DATA: Mutex<RefCell<Option<io::msg::PositionState>>> = Mutex::new(RefCell::new(None));
pub enum BleSendable {
#[cfg(feature = "cam")]
CamRaw(io::msg::RawMsgRx),
#[cfg(feature = "cam")]
Cam(io::msg::CamEvent),
#[cfg(feature = "denm")]
DenmRaw(io::msg::RawMsgRx),
#[cfg(feature = "denm")]
Denm(io::msg::DenmEvent),
#[cfg(feature = "spat")]
MapemRaw(io::msg::RawMsgRx),
#[cfg(feature = "spat")]
SpatemRaw(io::msg::RawMsgRx),
}
#[gatt_server]
struct Server {
c_its_events: CITSEvents,
}
#[gatt_service(uuid = "c0b70000-d4f4-4000-ada8-f99a02ee315c")]
struct CITSEvents {
#[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "Position", read, value = "PositionState proto msg", type = &'static str)]
#[characteristic(uuid = "c0b70001-d4f4-4001-ada8-f99a02ee315c", write)]
position: [u8; 27],
#[cfg(any(feature = "cam", feature = "denm", feature = "spat"))]
#[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "Raw_Notify", read, value = "Raw Message Notification", type = &'static str)]
#[characteristic(uuid = "c0b70001-d4f4-4002-ada8-f99a02ee315c", read, notify, value = [0u8; io::msg::RawNotify::PROTO_SIZE])]
raw_notify: [u8; io::msg::RawNotify::PROTO_SIZE],
#[cfg(feature = "cam")]
// #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "CAM", read, value = "RawMsgRx proto msg", type = &'static str)]
#[characteristic(uuid = "c0b70001-d4f4-4003-ada8-f99a02ee315c", read, value = [0u8; io::msg::RawMsgRx::PROTO_SIZE])]
cam_raw: [u8; io::msg::RawMsgRx::PROTO_SIZE],
#[cfg(feature = "cam")]
#[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "CAM_Event", read, value = "CamEvent proto msg", type = &'static str)]
#[characteristic(uuid = "c0b70001-d4f4-4004-ada8-f99a02ee315c", read, notify, value = [0u8; io::msg::CamEvent::PROTO_SIZE])]
cam_event: [u8; io::msg::CamEvent::PROTO_SIZE],
#[cfg(feature = "denm")]
// #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "DENM", read, value = "RawMsgRx proto msg", type = &'static str)]
#[characteristic(uuid = "c0b70001-d4f4-4005-ada8-f99a02ee315c", read, value = [0u8; io::msg::RawMsgRx::PROTO_SIZE])]
denm_raw: [u8; io::msg::RawMsgRx::PROTO_SIZE],
#[cfg(feature = "denm")]
#[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "DENM_Event", read, value = "DenmEvent proto msg", type = &'static str)]
#[characteristic(uuid = "c0b70001-d4f4-4006-ada8-f99a02ee315c", read, notify, value = [0u8; io::msg::DenmEvent::PROTO_SIZE])]
denm_event: [u8; io::msg::DenmEvent::PROTO_SIZE],
#[cfg(feature = "spat")]
// #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "MAPEM", read, value = "RawMsgRx proto msg", type = &'static str)]
#[characteristic(uuid = "c0b70001-d4f4-4007-ada8-f99a02ee315c", read, value = [0u8; io::msg::RawMsgRx::PROTO_SIZE])]
mapem_raw: [u8; io::msg::RawMsgRx::PROTO_SIZE],
#[cfg(feature = "spat")]
// #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "SPATEM", read, value = "RawMsgRx proto msg", type = &'static str)]
#[characteristic(uuid = "c0b70001-d4f4-4008-ada8-f99a02ee315c", read, value = [0u8; io::msg::RawMsgRx::PROTO_SIZE])]
spatem_raw: [u8; io::msg::RawMsgRx::PROTO_SIZE],
}
/// Creates the BLE controller
pub fn make_controller(
bt_peripheral: esp_hal::peripherals::BT<'static>,
) -> bt_hci::controller::ExternalController<
esp_radio::ble::controller::BleConnector<'static>,
CONTROLLER_SLOTS,
> {
critical_section::with(|cs| {
TX_DATA.borrow(cs).replace(Some(ArrayQueue::new(2)));
});
let transport = esp_radio::ble::controller::BleConnector::new(
bt_peripheral,
esp_radio::ble::Config::default(),
)
.unwrap();
bt_hci::controller::ExternalController::<_, { CONTROLLER_SLOTS }>::new(transport)
}
/// Run the BLE stack.
#[embassy_executor::task]
pub async fn run(
controller: bt_hci::controller::ExternalController<
esp_radio::ble::controller::BleConnector<'static>,
CONTROLLER_SLOTS,
>,
name: alloc::string::String,
) {
let mut resources: HostResources<DefaultPacketPool, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> =
HostResources::new();
let stack = trouble_host::new(controller, &mut resources);
let Host {
mut peripheral,
runner,
..
} = stack.build();
let mut features = alloc::vec![];
if cfg!(feature = "cam") {
features.push("cam");
}
if cfg!(feature = "denm") {
features.push("denm");
}
if cfg!(feature = "spat") {
features.push("mapem");
features.push("spatem");
}
info!(
"Starting advertising and GATT service for {}",
features.join(",")
);
let server = Server::new_with_config(GapConfig::Peripheral(PeripheralConfig {
name: &name,
appearance: &appearance::UNKNOWN,
}))
.unwrap();
let _ = embassy_futures::join::join(ble_task(runner), async {
loop {
match advertise(&mut peripheral, &server).await {
Ok(conn) => {
info!("BLE: New client connected");
// set up tasks when the connection is established to a central, so they don't run when no one is connected.
let a = gatt_events_task(&server, &conn);
let b = publishing_task(&server, &conn);
// run until any task ends (usually because the connection has been closed), then return to advertising state.
embassy_futures::select::select(a, b).await;
critical_section::with(|cs| {
let value_ref = CLIENT_CONNECTED.borrow(cs);
let _ = value_ref.replace(false);
});
}
Err(e) => {
panic!("BLE ADV: fatal error: {e:?}");
}
}
}
})
.await;
}
pub fn update(payload: BleSendable) {
critical_section::with(|cs| {
// only add to queue when someone is connected
if *CLIENT_CONNECTED.borrow(cs).borrow() {
let queue_rc = TX_DATA.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).is_err() {
log::error!("BLE TX queue is full");
}
}
});
}
/// Create an advertiser to use to connect to a BLE Central, and wait for it to connect.
async fn advertise<'values, 'server, C: Controller>(
peripheral: &mut Peripheral<'values, C, DefaultPacketPool>,
server: &'server Server<'values>,
) -> Result<GattConnection<'values, 'server, DefaultPacketPool>, BleHostError<C::Error>> {
let mut advertiser_data = [0; 35];
let service_uuid = Uuid::from(0xc0b7_0000_d4f4_4000_ada8_f99a_02ee_315c_u128)
.as_raw()
.try_into()
.unwrap();
let len = AdStructure::encode_slice(
&[
AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
AdStructure::ServiceUuids128(&[service_uuid]),
],
&mut advertiser_data[..],
)?;
let advertiser = peripheral
.advertise(
&trouble_host::advertise::AdvertisementParameters {
timeout: Some(embassy_time::Duration::from_secs(2)),
..Default::default()
},
Advertisement::ConnectableScannableUndirected {
adv_data: &advertiser_data[..len],
scan_data: &[],
},
)
.await?;
// info!("[adv] advertising {len} bytes");
let conn = advertiser.accept().await?.with_attribute_server(server)?;
// info!("[adv] connection established");
Ok(conn)
}
async fn ble_task<C: Controller, P: PacketPool>(mut runner: Runner<'_, C, P>) {
loop {
if let Err(e) = runner.run().await {
panic!("BLE Task: fatal error {e:?}");
}
}
}
/// Stream Events until the connection closes.
///
/// This function will handle the GATT events and process them.
/// This is how we interact with read and write requests.
async fn gatt_events_task<P: PacketPool>(
server: &Server<'_>,
conn: &GattConnection<'_, '_, P>,
) -> Result<(), Error> {
let reason = loop {
match conn.next().await {
GattConnectionEvent::Disconnected { reason } => break reason,
GattConnectionEvent::Gatt { event } => {
match &event {
GattEvent::Write(event)
if event.handle() == server.c_its_events.position.handle =>
{
use prost::Message as _;
// parse and send to main thread
match io::msg::PositionState::decode(event.data()) {
Ok(data) => {
critical_section::with(|cs| {
let rx_data_ref = RX_DATA.borrow(cs);
let _ = rx_data_ref.replace(Some(data));
});
}
Err(err) => log::error!("BLE: Failed to parse input: {err:?}"),
}
}
_ => {}
}
// This step is also performed at drop(), but writing it explicitly is necessary
// in order to ensure reply is sent.
match event.accept() {
Ok(reply) => reply.send().await,
Err(e) => warn!("BLE: error sending response: {e:?}"),
}
}
_ => {} // ignore other Gatt Connection Events
}
};
info!("BLE: disconnected b/c {reason:?}");
Ok(())
}
#[allow(clippy::too_many_lines)]
async fn publishing_task<P: PacketPool>(server: &Server<'_>, conn: &GattConnection<'_, '_, P>) {
critical_section::with(|cs| {
let value_ref = CLIENT_CONNECTED.borrow(cs);
let _ = value_ref.replace(true);
});
loop {
embassy_time::Timer::after(embassy_time::Duration::from_millis(10)).await;
let mut new_data = None;
critical_section::with(|cs| {
let queue_rc = TX_DATA.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);
}
});
match new_data {
#[cfg(feature = "cam")]
Some(BleSendable::Cam(cam_event)) => match cam_event.serialize_ble_proto() {
Ok(value) => {
if let Err(err) = server.c_its_events.cam_event.notify(conn, &value).await {
warn!("BLE: failed notifying connection of new CAM event: {err:?}");
}
}
Err(err) => warn!("BLE: CAM event {err}"),
},
#[cfg(feature = "denm")]
Some(BleSendable::Denm(denm_event)) => match denm_event.serialize_ble_proto() {
Ok(value) => {
if let Err(err) = server.c_its_events.denm_event.notify(conn, &value).await {
warn!("BLE: failed notifying connection of new DENM event: {err:?}");
}
}
Err(err) => warn!("BLE: DENM event {err}"),
},
#[cfg(feature = "cam")]
Some(BleSendable::CamRaw(raw_msg)) => match raw_msg.serialize_ble_proto() {
Ok(value) => {
if let Err(err) = server.c_its_events.cam_raw.set(server, &value) {
warn!("BLE: failed setting new raw CAM value: {err:?}");
}
if let Err(err) = send_raw_notify(
server,
conn,
c_its_parser::standards::extensions::ItsMessageId::Cam,
)
.await
{
warn!("BLE: {err}");
}
}
Err(err) => warn!("BLE: Raw CAM too big: {err}"),
},
#[cfg(feature = "denm")]
Some(BleSendable::DenmRaw(raw_msg)) => match raw_msg.serialize_ble_proto() {
Ok(value) => {
if let Err(err) = server.c_its_events.denm_raw.set(server, &value) {
warn!("BLE: failed setting new raw DENM value: {err:?}");
}
if let Err(err) = send_raw_notify(
server,
conn,
c_its_parser::standards::extensions::ItsMessageId::Denm,
)
.await
{
warn!("BLE: {err}");
}
}
Err(err) => warn!("BLE: Raw DENM {err}"),
},
#[cfg(feature = "spat")]
Some(BleSendable::MapemRaw(raw_msg)) => match raw_msg.serialize_ble_proto() {
Ok(value) => {
if let Err(err) = server.c_its_events.mapem_raw.set(server, &value) {
warn!("BLE: failed setting new raw MAPEM value: {err:?}");
}
if let Err(err) = send_raw_notify(
server,
conn,
c_its_parser::standards::extensions::ItsMessageId::Mapem,
)
.await
{
warn!("BLE: {err}");
}
}
Err(err) => warn!("BLE: Raw MAPEM {err}"),
},
#[cfg(feature = "spat")]
Some(BleSendable::SpatemRaw(raw_msg)) => match raw_msg.serialize_ble_proto() {
Ok(value) => {
if let Err(err) = server.c_its_events.spatem_raw.set(server, &value) {
warn!("BLE: failed setting new raw SPATEM value: {err:?}");
}
if let Err(err) = send_raw_notify(
server,
conn,
c_its_parser::standards::extensions::ItsMessageId::Spatem,
)
.await
{
warn!("BLE: {err}");
}
}
Err(err) => warn!("BLE: Raw SPATEM {err}"),
},
None => {}
}
}
}
async fn send_raw_notify<P: PacketPool>(
server: &Server<'_>,
conn: &GattConnection<'_, '_, P>,
msg_id: c_its_parser::standards::extensions::ItsMessageId,
) -> Result<(), alloc::string::String> {
let notify_buf = io::msg::RawNotify::from(msg_id)
.serialize_ble_proto()
.expect("BLE: Raw notify too big");
server
.c_its_events
.raw_notify
.notify(conn, &notify_buf)
.await
.map_err(|err| alloc::format!("failed notifying connection of new raw {msg_id:?}: {err:?}"))
}

View file

@ -1,258 +0,0 @@
//! 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
});
}
}

View file

@ -1,446 +0,0 @@
//! 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);
}
}
}
}
}

849
src/io.rs
View file

@ -1,849 +0,0 @@
//! UART and 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"));
}
#[cfg(feature = "uart")]
use c_its_parser::gn as geonetworking;
#[cfg(feature = "uart")]
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)),
}
}
}
#[cfg(feature = "uart")]
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 {
#[cfg(feature = "uart")]
pub fn serialize_uart_proto(&self) -> alloc::vec::Vec<u8> {
serialize_serial_msg(&Into::<msg::SerialOutputMsg>::into(self.clone()))
}
}
#[cfg(feature = "denm")]
impl msg::DenmEvent {
#[cfg(feature = "ble")]
pub const PROTO_SIZE: usize = 51;
#[cfg(feature = "ble")]
pub fn serialize_ble_proto(&self) -> Result<[u8; Self::PROTO_SIZE], alloc::string::String> {
use prost::Message as _;
let mut data = [0u8; Self::PROTO_SIZE];
let mut mut_buf = &mut data[..];
if let Err(err) = self.encode(&mut mut_buf) {
Err(alloc::format!("Buffer too small: {err:?}"))
} else {
Ok(data)
}
}
}
#[cfg(feature = "cam")]
impl msg::CamEvent {
#[cfg(feature = "ble")]
pub const PROTO_SIZE: usize = 38;
#[cfg(feature = "ble")]
pub fn serialize_ble_proto(&self) -> Result<[u8; Self::PROTO_SIZE], alloc::string::String> {
use prost::Message as _;
let mut data = [0u8; Self::PROTO_SIZE];
let mut mut_buf = &mut data[..];
if let Err(err) = self.encode(&mut mut_buf) {
Err(alloc::format!("Buffer too small: {err:?}"))
} else {
Ok(data)
}
}
}
#[cfg(any(feature = "cam", feature = "denm", feature = "spat"))]
impl msg::RawMsgRx {
#[cfg(feature = "ble")]
pub const PROTO_SIZE: usize = 1400;
#[cfg(feature = "ble")]
pub fn serialize_ble_proto(&self) -> Result<[u8; Self::PROTO_SIZE], alloc::string::String> {
use prost::Message as _;
let mut data = [0u8; Self::PROTO_SIZE];
let mut mut_buf = &mut data[..];
if let Err(err) = self.encode(&mut mut_buf) {
Err(alloc::format!("Buffer too small: {err:?}"))
} else {
Ok(data)
}
}
}
#[cfg(all(
feature = "ble",
any(feature = "cam", feature = "denm", feature = "spat")
))]
impl From<c_its_parser::standards::extensions::ItsMessageId> for msg::RawNotify {
fn from(value: c_its_parser::standards::extensions::ItsMessageId) -> Self {
let message_type = value.as_u8().into();
Self { message_type }
}
}
#[cfg(any(feature = "cam", feature = "denm", feature = "spat"))]
impl msg::RawNotify {
#[cfg(feature = "ble")]
pub const PROTO_SIZE: usize = 2;
#[cfg(feature = "ble")]
pub fn serialize_ble_proto(self) -> Result<[u8; Self::PROTO_SIZE], alloc::string::String> {
use prost::Message as _;
let mut data = [0u8; Self::PROTO_SIZE];
let mut mut_buf = &mut data[..];
if let Err(err) = self.encode(&mut mut_buf) {
Err(alloc::format!("Buffer too small: {err:?}"))
} else {
Ok(data)
}
}
}
#[cfg(feature = "uart")]
#[derive(Debug, Default)]
pub struct Parser {
buffer: alloc::vec::Vec<u8>,
}
#[cfg(feature = "uart")]
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) => log::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
}
}
#[cfg(feature = "uart")]
fn serialize_serial_msg<T>(msg: &T) -> alloc::vec::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
}
#[cfg(any(feature = "uart", feature = "ble"))]
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,
}
}
}
#[cfg(feature = "uart")]
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,
}
}
}
#[cfg(feature = "uart")]
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,
}
}
}
#[cfg(feature = "uart")]
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,
> {
use alloc::string::ToString as _;
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<
(
alloc::vec::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))
}
}
#[cfg(feature = "uart")]
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(feature = "cam")]
impl From<&alloc::boxed::Box<c_its_parser::standards::cam_1_4_1::cam_pdu_descriptions::CAM>>
for msg::CamEvent
{
fn from(
value: &alloc::boxed::Box<c_its_parser::standards::cam_1_4_1::cam_pdu_descriptions::CAM>,
) -> Self {
use c_its_parser::standards::cam_1_4_1::cam_pdu_descriptions;
let station_id = value.header.station_id.0;
let station_type = u32::from(value.cam.cam_parameters.basic_container.station_type.0);
let position = value
.cam
.cam_parameters
.basic_container
.reference_position
.clone()
.into();
let vehicle_role = value
.cam
.cam_parameters
.low_frequency_container
.as_ref()
.and_then(|v| match v {
cam_pdu_descriptions::LowFrequencyContainer::basicVehicleContainerLowFrequency(
basic_vehicle_container_low_frequency,
) => Some(basic_vehicle_container_low_frequency.vehicle_role as u32),
_ => None,
});
let vehicle_data = match &value.cam.cam_parameters.high_frequency_container {
cam_pdu_descriptions::HighFrequencyContainer::basicVehicleContainerHighFrequency(
basic_vehicle_container_high_frequency,
) => Some(basic_vehicle_container_high_frequency.into()),
_ => None,
};
Self {
station_id,
station_type,
position,
vehicle_role,
vehicle_data,
}
}
}
#[cfg(feature = "cam")]
impl From<&c_its_parser::standards::cam_1_4_1::cam_pdu_descriptions::BasicVehicleContainerHighFrequency> for msg::VehicleContainer {
fn from(value: &c_its_parser::standards::cam_1_4_1::cam_pdu_descriptions::BasicVehicleContainerHighFrequency) -> Self {
let heading_deg = if value.heading.heading_value.is_unavailable() {
None
} else {
Some(u32::from(value.heading.heading_value.0))
};
let speed = if value.speed.speed_value.is_unavailable() {
None
} else {
Some(u32::from(value.speed.speed_value.0))
};
let vehicle_length_dm = if value.vehicle_length.vehicle_length_value.is_unavailable() {
None
} else {
Some(u32::from(value.vehicle_length.vehicle_length_value.0))
};
let vehicle_width_dm = if value.vehicle_width.is_unavailable() {
None
} else {
Some(u32::from(value.vehicle_width.0))
};
Self { heading_deg, speed, vehicle_length_dm, vehicle_width_dm }
}
}
#[cfg(feature = "denm")]
impl From<&alloc::boxed::Box<c_its_parser::standards::denm_2_2_1::denm_pdu_description::DENM>>
for msg::DenmEvent
{
fn from(
value: &alloc::boxed::Box<c_its_parser::standards::denm_2_2_1::denm_pdu_description::DENM>,
) -> Self {
let denm_mgmt = &value.denm.management;
let station_id = value.header.station_id.0; // TODO: or from action ID?
let ref_time = chrono::DateTime::<chrono::Utc>::from(denm_mgmt.reference_time.clone());
let end_time = ref_time + chrono::Duration::seconds(denm_mgmt.validity_duration.0.into());
#[allow(clippy::cast_sign_loss)]
let timestamp = ref_time.timestamp_millis() as u64;
#[allow(clippy::cast_sign_loss)]
let validity_end_ts = end_time.timestamp_millis() as u64;
let seq_num = denm_mgmt.action_id.sequence_number.0.into();
let event_position = Some(denm_mgmt.event_position.clone().into());
let event_heading_deg = value
.denm
.location
.as_ref()
.and_then(|v| v.event_position_heading.clone())
.map(|v| {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let conv = v.value.as_deg() as u32;
conv
});
let direction = denm_mgmt
.traffic_direction
.map(|v| msg::TrafficDir::from(v) as i32);
let cc_tuple = value
.denm
.situation
.as_ref()
.map(|v| v.event_type.cc_and_scc.to_u8_tuple());
let cause_code = cc_tuple.map(|v| v.0.into());
let sub_cause_code = cc_tuple.map(|v| v.1.into());
Self {
timestamp,
validity_end_ts,
station_id,
seq_num,
event_position,
event_heading_deg,
direction,
cause_code,
sub_cause_code,
}
}
}
#[cfg(feature = "cam")]
impl From<c_its_parser::standards::cdd_1_3_1_1::its_container::ReferencePosition>
for msg::Position
{
fn from(value: c_its_parser::standards::cdd_1_3_1_1::its_container::ReferencePosition) -> Self {
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let latitude_deg = value.latitude.as_deg() as f32;
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let longitude_deg = value.longitude.as_deg() as f32;
Self {
latitude_deg,
longitude_deg,
}
}
}
#[cfg(feature = "denm")]
impl From<c_its_parser::standards::cdd_2_2_1::etsi_its_cdd::ReferencePosition> for msg::Position {
fn from(value: c_its_parser::standards::cdd_2_2_1::etsi_its_cdd::ReferencePosition) -> Self {
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let latitude_deg = value.latitude.as_deg() as f32;
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let longitude_deg = value.longitude.as_deg() as f32;
Self {
latitude_deg,
longitude_deg,
}
}
}
#[cfg(feature = "denm")]
impl From<c_its_parser::standards::cdd_2_2_1::etsi_its_cdd::TrafficDirection> for msg::TrafficDir {
fn from(value: c_its_parser::standards::cdd_2_2_1::etsi_its_cdd::TrafficDirection) -> Self {
use c_its_parser::standards::cdd_2_2_1::etsi_its_cdd::TrafficDirection;
match value {
TrafficDirection::allTrafficDirections => Self::AllDirections,
TrafficDirection::sameAsReferenceDirection_upstreamOfReferencePosition => {
Self::Upstream
}
TrafficDirection::sameAsReferenceDirection_downstreamOfReferencePosition => {
Self::Downstream
}
TrafficDirection::oppositeToReferenceDirection => Self::Opposite,
}
}
}
#[cfg(any(feature = "cam", feature = "denm", feature = "spat"))]
impl TryFrom<&c_its_parser::ItsMessage<'_>> for msg::RawMsgRx {
type Error = alloc::string::String;
fn try_from(value: &c_its_parser::ItsMessage<'_>) -> Result<Self, Self::Error> {
let msg_id = c_its_parser::standards::extensions::ItsMessageId::from(value);
let payload = match value {
#[cfg(feature = "denm")]
c_its_parser::ItsMessage::DenmV1 {
geonetworking: _,
transport: _,
etsi,
} => etsi.encode_to_uper(),
#[cfg(feature = "denm")]
c_its_parser::ItsMessage::DenmV2 {
geonetworking: _,
transport: _,
etsi,
} => etsi.encode_to_uper(),
#[cfg(any(feature = "cam", feature = "cam_tx"))]
c_its_parser::ItsMessage::Cam {
geonetworking: _,
transport: _,
etsi,
} => etsi.encode_to_uper(),
#[cfg(feature = "spat")]
c_its_parser::ItsMessage::Spatem {
geonetworking: _,
transport: _,
etsi,
} => etsi.encode_to_uper(),
#[cfg(feature = "spat")]
c_its_parser::ItsMessage::Mapem {
geonetworking: _,
transport: _,
etsi,
} => etsi.encode_to_uper(),
}?;
Ok(Self {
msg_type: msg_id.as_u8().into(),
payload,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::init_test_env_logger;
#[cfg(feature = "uart")]
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)),
}
}
}
#[cfg(feature = "uart")]
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
// PositionState: 09a0bbe2a29f010000120a0d00002842150000b841
#[cfg(feature = "uart")]
#[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()
);
}
}

File diff suppressed because it is too large Load diff

98
src/radio.rs Normal file
View file

@ -0,0 +1,98 @@
//! IEEE 802.11p C-ITS Capture with ESP32-C5
// -----------------------------
// missing interface definitions
// -----------------------------
use alloc::format;
use alloc::string::String;
use esp_radio::wifi::sniffer;
pub type Channel = core::ffi::c_int;
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
unsafe fn phy_change_channel(
primary: Channel,
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
unsafe fn phy_11p_set(enable: u8, zero: u8);
}
/// Enum values for `wifi_promiscuous_pkt_type_t` used in `esp_radio::wifi::sniffer::PromiscuousPkt::frame_type`
///
/// Original value is actually `core::ffi::c_uint`, but that can't be used in `#[repr()]` attributes,
/// so we're using `u8` as the smallest suitable type here that can be casted to a bigger type.
#[repr(u8)]
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub(crate) enum PromiscuousPktType {
/// 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 PromiscuousPktType {
#[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 {
core::ffi::c_uint::from(self.as_repr())
}
/// Determines if
pub fn matches(self, frame_type: core::ffi::c_uint) -> bool {
self.as_frame_type() == frame_type
}
}
/// Configure WiFi Sniffer to a certain 802.11p channel
pub fn setup_wifi_sniffer(
channel: Channel,
sniffer: &mut sniffer::Sniffer,
callback: fn(sniffer::PromiscuousPkt<'_>),
) -> Result<(), String> {
sniffer
.set_promiscuous_mode(true)
.map_err(|err| format!("Failed to enable promiscuous mode: {err:?}"))?;
sniffer.set_receive_cb(callback);
// convert channel number to frequency in MHz
let primary = 5000 + (channel * 5);
unsafe {
phy_11p_set(1, 0);
match phy_change_channel(primary, 1, 0, 0) {
0 => Ok(()),
ret => Err(format!("Failed to change channel: {ret}")),
}
}
}

View file

@ -1,663 +0,0 @@
//! Screen Output
use embedded_graphics::prelude::*;
use embedded_graphics::{geometry, mono_font, pixelcolor, primitives, text};
use crate::applogic;
pub struct DisplaySize {
width: u16,
height: u16,
offset_top: i32,
corner_radius: i32,
}
pub struct Handler<D>
where
D: DrawTarget<Color = pixelcolor::Rgb565>,
{
target: D,
size: DisplaySize,
// internal state
prev_state: ScreenState,
hb_state: bool,
gnss_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 MID_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> =
mono_font::MonoTextStyle::new(&profont::PROFONT_18_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);
const BIG_HEIGHT_THLD: u16 = 172;
pub fn new(target: D, display_size: DisplaySize) -> Self {
Self {
target,
size: display_size,
prev_state: ScreenState::Uninitialized,
hb_state: false,
gnss_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.size.width) - update_ind_dia - self.size.corner_radius;
text::Text::with_alignment(
"HB",
geometry::Point::new(circle_x_pos - 5, 10 + self.size.offset_top),
Self::SMALL_TEXT_STYLE,
text::Alignment::Right,
)
.draw(&mut self.target)?;
primitives::Circle::new(
geometry::Point::new(circle_x_pos, 2 + self.size.offset_top),
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(())
}
// on big screns: uses full screen width and height from 250 to 280 pixels
pub fn update_gnss(
&mut self,
pvt: &ublox::nav_pvt::proto27::NavPvtRef,
) -> Result<(), D::Error> {
use alloc::string::ToString as _;
// this only applies to big screens
if !self.is_big_height() {
return Ok(());
}
let num_sat = pvt.num_satellites();
let fix_str = match pvt.fix_type() {
ublox::GnssFixType::NoFix
| ublox::GnssFixType::DeadReckoningOnly
| ublox::GnssFixType::TimeOnlyFix => {
alloc::format!("{num_sat:2} sat")
}
ublox::GnssFixType::Fix2D => "2D fix".to_string(),
ublox::GnssFixType::Fix3D | ublox::GnssFixType::GPSPlusDeadReckoning => {
"3D fix".to_string()
}
_ => " ".to_string(),
};
// clear area and write information
// note: text position is x baseline, not top left corner
primitives::Rectangle::new(
geometry::Point::new(0, 250 + self.size.offset_top),
geometry::Size::new(self.size.width.into(), 20),
)
.into_styled(Self::BK_STYLE)
.draw(&mut self.target)?;
let colon = if self.gnss_hb_state { ":" } else { " " };
let message = alloc::format!(
"{fix_str}{colon} {:2.0}km/h, {:3.0}°",
pvt.ground_speed_2d() * 3.6,
pvt.heading_motion()
);
text::Text::with_alignment(
&message,
geometry::Point::new(self.size.corner_radius / 2, 265 + self.size.offset_top),
Self::SMALL_TEXT_STYLE,
text::Alignment::Left,
)
.draw(&mut self.target)?;
self.gnss_hb_state = !self.gnss_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.size.width.into(), self.size.height.into()),
)
.into_styled(Self::BK_STYLE)
.draw(&mut self.target)?;
Ok(())
}
fn is_big_height(&self) -> bool {
self.size.height > Self::BIG_HEIGHT_THLD
}
#[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.size.width) / 2, 40 + self.size.offset_top),
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(
"Searching...",
geometry::Point::new(i32::from(self.size.width) / 2, 130 + self.size.offset_top),
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.size.width) / 2, 130 + self.size.offset_top),
Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
Ok(())
}
// uses full screen width and height
// - from 0 to 105+67 = 172 pixels for small screens
// - from 0 to 105+67+20 = 192 pixels for big screens
#[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();
let is_big_height = self.is_big_height();
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
let sig_total_height = 67 + if is_big_height { 20 } else { 0 };
primitives::Rectangle::new(
geometry::Point::new(0, 105 + self.size.offset_top),
geometry::Size::new(self.size.width.into(), sig_total_height),
)
.into_styled(Self::BK_STYLE)
.draw(&mut self.target)?;
}
// do nothing more when no signals
if num_signals == 0 {
return Ok(());
}
// draw intersection ID
text::Text::new(
&alloc::format!("#{}", data.intersection_id),
geometry::Point::new(self.size.corner_radius + 5, 10 + self.size.offset_top),
Self::SMALL_TEXT_STYLE,
)
.draw(&mut self.target)?;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let sig_width = i32::from(self.size.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 + self.size.offset_top),
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.size.offset_top),
Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
// draw timing indicator(s)
if let Some(timing) = &sig.timing {
if let (Some(likely), Some(max)) = (timing.likely_end_sec, timing.max_end_sec) {
text::Text::with_alignment(
&alloc::format!("{likely}s"),
geometry::Point::new(centerline, 160 + self.size.offset_top),
Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
// only for big screen
if is_big_height {
// TODO: show as diff to likely time?
text::Text::with_alignment(
&alloc::format!("{}-{max}", timing.min_end_sec),
geometry::Point::new(centerline, 185 + self.size.offset_top),
Self::MID_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
}
} else {
text::Text::with_alignment(
&alloc::format!("{}s", timing.min_end_sec),
geometry::Point::new(centerline, 160 + self.size.offset_top),
Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
}
}
}
// if is_big_height {
// // TODO: Add ETA
// }
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.size.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 + self.size.offset_top),
circle_dia.cast_unsigned(),
)
.into_styled(*style)
.draw(&mut self.target)?;
}
// draw welcome text
let centerline = i32::from(self.size.width) / 2;
text::Text::with_alignment(
"C-ITS Signal Phase\nWaiting for GNSS...",
geometry::Point::new(centerline, 130 + self.size.offset_top),
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(unused, 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<'_>,
>,
DisplaySize,
),
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_HIDDEN_X: u16 = 34; // somehow this display has 34 px of invisible space on the left (in native orientation)
let size = DisplaySize {
width: 320,
height: 172,
offset_top: 0,
corner_radius: 10,
};
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}"))?;
// Note: height and width are swapped b/c of 90° display rotation
let display = mipidsi::Builder::new(
mipidsi::models::ST7789,
mipidsi::interface::SpiInterface::new(spi_device, dc_output, spi_buffer),
)
.display_size(size.height + DISPLAY_HIDDEN_X, size.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, size))
}
/// Creates an [`mipidsi::Display`], width, height tuple for an ST7789 240*280px screen in portrait orientation
///
/// # Errors
/// Human-readable fatal errors
#[allow(unused, clippy::too_many_arguments, clippy::type_complexity)]
pub fn make_large_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<'_>,
>,
DisplaySize,
),
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_HIDDEN_Y: u16 = 20; // somehow this display has 20 px of invisible space on the top (in native orientation)
let size = DisplaySize {
width: 240,
height: 280 + DISPLAY_HIDDEN_Y,
offset_top: DISPLAY_HIDDEN_Y.into(),
corner_radius: 30,
};
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(size.width, size.height + DISPLAY_HIDDEN_Y)
.reset_pin(rst_output)
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg0))
.init(&mut display_delay)
.map_err(|err| alloc::format!("Failed to initialize display: {err:?}"))?;
Ok((display, size))
}
/// 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))
}

View file

@ -1,24 +0,0 @@
#![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();
}

View file

@ -1,465 +0,0 @@
//! 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,
];

View file

@ -1,106 +0,0 @@
//! 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),
})
}
}