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

Compare commits

...
Author SHA1 Message Date
Jannik Beyerstedt
105d3123f4 FIXUP: use user LED 2026-08-03 11:45:28 +02:00
759e6e0d04 WIP: try to debug V2X rx dropouts 2026-08-03 11:25:09 +02:00
Jannik Beyerstedt
666499c240 Move core ESP32 V2X to own repo 2026-07-31 16:06:20 +02:00
2db0dc0116 gnss: Add support for ublox GNSS 2026-07-31 13:20:21 +02:00
ac6a3e398e Hardware: Add PCB layout 2026-07-31 13:20:13 +02:00
Jannik Beyerstedt
71161d1d99 Add scripts to run/ flash the supported feature-sets 2026-07-31 12:13:54 +02:00
Jannik Beyerstedt
2c50668e7e 802.11-TX: Reduce transmit power, add notes about legal requirements 2026-07-31 12:04:46 +02:00
Jannik Beyerstedt
4d7bef58a1 Implement UART I/O protocol 2026-07-27 13:24:24 +02:00
Jannik Beyerstedt
ba71e718df proto/PositionState: Add time for geonetworking 2026-07-27 12:41:51 +02:00
6604670490 main: Add UART I/O helpers 2026-07-27 11:03:24 +02:00
Jannik Beyerstedt
7bbb47a844 cam_tx: Fill path history 2026-07-26 21:24:24 +02:00
2379a7eefc Enable V2X transmission, add optional stand-alone CAM transmission 2026-07-24 12:12:11 +02:00
Jannik Beyerstedt
1d4e82335d feat(docs): Explain supported feature combinations, add test script 2026-07-24 11:52:50 +02:00
Jannik Beyerstedt
7668c9bb7b proto/RawMsgTx: Add station type for source position vector 2026-07-24 11:51:00 +02:00
Jannik Beyerstedt
baaee5f6ff logging: Only print to jtag-serial
Otherwise esp-println will use UART0 when no USB-JTAG/Serial is connected.
2026-07-23 10:11:46 +02:00
Jannik Beyerstedt
fc3581d0d1 main: Fix feature flags 2026-07-22 16:15:58 +02:00
Jannik Beyerstedt
21db09ad7f build: Add "io" module for BLE/ UART messages 2026-07-22 15:39:33 +02:00
Jannik Beyerstedt
29a5d86e43 docs: Add BLE and UART protocol specification 2026-07-22 15:14:15 +02:00
58e5d5ec75 v2x: Reduce SPAT rate 2026-07-21 17:31:08 +02:00
e178c740db v2x: drop hopped messages and unsupported message types 2026-07-21 17:30:05 +02:00
f32887df61 applogic: Move to generic position state interface 2026-07-20 22:38:33 +02:00
Jannik Beyerstedt
055b03850b wifi: Move setup to radio module 2026-07-20 13:26:00 +02:00
Jannik Beyerstedt
d7eeb67538 screen: Move setup to module 2026-07-20 11:36:05 +02:00
35 changed files with 29532 additions and 470 deletions

4
.gitignore vendored
View file

@ -24,3 +24,7 @@ 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

186
Cargo.lock generated
View file

@ -73,6 +73,12 @@ 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"
@ -131,9 +137,9 @@ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bitvec"
version = "1.0.1"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837"
dependencies = [
"funty",
"radium",
@ -186,9 +192,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "c-its-parser"
version = "2.2.5"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993244dbda1234597ca13fe2e2565fc6f2ece585040404faa011482b55ed544e"
checksum = "7e94d9990b20498f621f164f23ae198c4365115eb4191ae106352ec74d4a4110"
dependencies = [
"chrono",
"etherparse",
@ -763,6 +769,16 @@ 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"
@ -1155,11 +1171,27 @@ dependencies = [
"esp-println",
"esp-radio",
"esp-rtos",
"esp32-cits-core",
"geo-types",
"log",
"mipidsi",
"num-traits",
"profont",
"prost",
"prost-build",
"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]]
@ -1227,19 +1259,31 @@ dependencies = [
[[package]]
name = "etherparse"
version = "0.20.1"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ac016aaf11dfe643edcd088a166234bfcb72e7f06691abfcf21e9af524037f4"
checksum = "17304d06addb3283cdc4bd528e42dd95e73c8ee2d6492ffce415e93660885449"
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"
@ -1347,13 +1391,11 @@ dependencies = [
[[package]]
name = "geonetworking"
version = "0.2.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4f48e7b1cdf91ad77ec1e4c5617aee3666a929288f4a3f11e473b67d3a6314"
checksum = "6da6fde770112587d5f5f7af439546e35bd35aa77a9e3e47e9968056783fe50d"
dependencies = [
"bitvec",
"bitvec-nom2",
"bytes",
"arbitrary-int",
"nom",
"num",
"num-traits",
@ -1549,6 +1591,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"
@ -1612,6 +1660,12 @@ dependencies = [
"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"
@ -1770,6 +1824,17 @@ 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"
@ -1849,6 +1914,57 @@ 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"
@ -2037,6 +2153,19 @@ 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"
@ -2248,6 +2377,18 @@ 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"
@ -2363,6 +2504,29 @@ 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"

View file

@ -12,10 +12,10 @@ path = "./src/main.rs"
[[bin]]
name = "test"
path = "./src/test.rs"
required-features = ["std"]
required-features = ["std", "cam"]
[features]
default = ["esp", "spat", "gnss", "screen"]
default = ["esp", "spat", "gnss_ubx", "screen"]
# build for ESP (needed to build test for `host-tuple`)
esp = [
@ -30,13 +30,19 @@ esp = [
"dep:esp-backtrace",
"dep:esp-println",
"dep:esp-radio",
"esp32-cits-core/esp32c5",
"esp32-cits-core/log-04",
]
std = ["c-its-parser/std"]
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 = ["dep:embassy_gps"]
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
@ -45,7 +51,13 @@ spat = ["c-its-parser/spatem", "c-its-parser/mapem"]
spat_debug = []
# enable ST7789 based display
screen = ["dep:mipidsi", "dep:embedded-graphics", "dep:embedded-hal-bus", "dep:profont"]
# enable I/O via UART
uart = ["_uart", "_gnss", "dep:prost"]
# internal feature, that any UART I/O is enabled
_uart = []
# internal feature, that any GNSS source is enabled
_gnss = []
[dependencies]
esp-hal = { version = "~1.1.0", optional = true, features = ["esp32c5", "log-04", "unstable"] }
@ -72,7 +84,7 @@ esp-backtrace = { version = "0.19.0", optional = true, features = [
"panic-handler",
"println",
] }
esp-println = { version = "0.17.0", optional = true, features = ["esp32c5", "log-04"] }
esp-println = { version = "0.17.0", optional = true, default-features = false, features = ["colors", "critical-section", "jtag-serial", "esp32c5", "log-04"] }
esp-radio = { version = "0.18.0", optional = true, features = [
"esp-alloc",
"esp32c5",
@ -83,9 +95,11 @@ esp-radio = { version = "0.18.0", optional = true, features = [
] }
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 }
c-its-parser = { version = "2.2.4", default-features = false, features = [
# needs newer GN with removed bitvec dependency
c-its-parser = { version = "2.4.0", default-features = false, features = [
"time",
"geo",
"libm",
@ -97,11 +111,16 @@ geo-types = { version = "0.7.19", default-features = false }
mipidsi = { version = "0.10.0", optional = true }
num-traits = { version = "0.2.19", default-features = false, features = ["libm"] }
profont = { version = "0.7.0", optional = true }
prost = { version = "0.14", optional = true, default-features = false, features = ["derive"] }
ublox = { version = "0.10", optional = true, default-features = false, features = ["alloc", "ubx_proto27"] }
[dev-dependencies]
assert_float_eq = "1.2.0"
env_logger = "0.11.10"
[build-dependencies]
prost-build = "0.14"
# For fine tuning these settings, please refer to https://doc.rust-lang.org/cargo/reference/profiles.html
[profile.dev]

View file

@ -1,19 +1,20 @@
# ESP32-C5 C-ITS Visualizer
# ESP32-C5 C-ITS Companion/ 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)
- Seeed XIAO L67K (connected on GPIO12 (RX), GPIO11 (TX), GPIO1 (wakeup) and GPIO25 (reset))
only available with `gnss` feature (which is included in `spat` feature)
- U-Blox M10Q GNSS (connected on GPIO12 (RX), GPIO11 (TX), and GPIO25 (reset))
or Seeed XIAO L67K (connected on GPIO12 (RX), GPIO11 (TX), GPIO1 (wakeup) and GPIO25 (reset))
only available with `gnss_ubx` or `gnss_l67k` feature (which is included in `spat` feature)
- Some ST7789 172*320px screen (connected on GPIO8 (SCL/ SCK), GPIO10 (SDA/ MOSI), GPIO7 (reset), GPIO23 (DC), GPIO24 (CS), TODO GPIO0 (BL))
only available with `screen` feature (which is included in `spat` feature)
## Features
### GNSS
Enable L67K GNSS module and in main application logic/ state.
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.
@ -28,6 +29,32 @@ Shall "publish" the most relevant information of a DENMs via BLE
### CAM
Just prints a line for each received CAM.
### CAM-TX
Generates and transmits a CAM when a GNSS update was received.
Depends on the `gnss` feature.
Note: In germany, using the 5875 to 5915 MHz frequency band is only permitted for C-ITS use cases which implement transmit power reduction measures with at least 30 dB reduction range (or equivalent measures) and have a maximum EIRP of 33 dBm.
This is based on EU regulation 2014/53/EU, so it applies to other EU countries in similar ways.
The relevant harmonized standard is ETSI EN 302 571 which mandates (excerpt of most relevant points):
- a maximum power spectral density of 23 dBm/Mhz EIRP
- transmit power control (TCP) in a range from 3 dBm up to the maximum output power of the equipment
* used for CEN DSRC and HDR DSRC interference mitigation by implementing measures from ETSI TS 102 792
+ Mode A or B: Output power less than 10 dBm EIRP and less than -65 or -45 dBm/MHz EIRP in CEN DSRC band -> **needs to be measured**
+ Mode C: Maximum transmission time of less than 1 ms and at least a few 100 ms between transmission (realistically) -> **can't be ensured with big messages**
+ Mode D: "any" realistically possible transmission time, but at least 500 ms between transmissions for ~5 ITS stations/ 1000ms for 10 ITS stations -> **safe to assume when we only send CAMs at 1 Hz**
+ But modes C and D also need to:
- use protected zone information from CAM messages
- use a protected zone database, e.g. from www.asecap-pzdb.com
* used for dynamic congestion control (DCC)
+ mandatory mechanism described in ETSI EN 302 571, chapter 4.2.10
This is **not** implemented in this application!
**So legally, you're not allowed to send with this.**
### UART
Enable UART I/O protocol according to [docs/uart-protocol.md](./docs/uart-protocol.md).
## Usage
@ -52,13 +79,21 @@ 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 though feature flags:
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`: Enable signal phase (and intersection map) data
- `spat_debug`: Enable additional debug information for the SPAT/ GLOSA use case
- `denm`: Enable DENMs
- `cam`: Enable CAMs
- `cam_tx`: Enable CAM generation and transmission (needs time and position source)
Officially supported feature combinations are:
- `esp,spat,gnss_ubx,screen` (default features, "standalone")
- `esp,spat,gnss_ubx,screen,cam_tx` ("standalone with CAM")
- `esp,spat,gnss_ubx,screen,cam_tx,cam,denm`
- `esp,uart,cam,denm` ("smartphone companion")
Connect your ESP32-C5 and run:

View file

@ -1,6 +1,6 @@
use std::env;
fn main() {
fn main() -> std::io::Result<()> {
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
if target_arch == "riscv32" {
@ -8,6 +8,16 @@ fn main() {
// 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 linker_be_nice() {

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

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

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

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

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

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

70
feat-permut.sh Executable file
View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because it is too large Load diff

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

6
run-smartphone.sh Executable file
View file

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

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

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

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

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

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

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

View file

@ -6,7 +6,7 @@ use alloc::rc::Rc;
use alloc::vec::Vec;
use log::info;
#[cfg(any(feature = "gnss", feature = "spat"))]
#[cfg(feature = "spat")]
use log::warn;
#[cfg(feature = "spat")]
use log::{debug, error};
@ -18,6 +18,8 @@ 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")]
@ -47,11 +49,7 @@ pub struct State {
time: chrono::NaiveDateTime,
#[allow(unused)]
position: geo_types::Point,
#[allow(unused)]
heading_deg: Option<f32>,
#[allow(unused)]
speed_mps: Option<f32>,
pos: esp32_cits_core::PosVel,
#[cfg(feature = "spat")]
last_prune: chrono::NaiveDateTime,
@ -122,6 +120,33 @@ pub enum SignalPhase {
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 {
@ -179,9 +204,7 @@ impl State {
Self {
initialized: false,
time: chrono::NaiveDateTime::default(),
position: geo_types::Point::default(),
heading_deg: None,
speed_mps: None,
pos: esp32_cits_core::PosVel::default(),
#[cfg(feature = "spat")]
last_prune: chrono::NaiveDateTime::default(),
#[cfg(feature = "spat")]
@ -191,23 +214,11 @@ impl State {
}
}
#[cfg(all(feature = "esp", feature = "gnss"))]
pub fn update_with_gpsfix(&mut self, fix: &embassy_gps::types::GpsFix) {
self.position = geo_types::Point::new(fix.longitude_deg, fix.latitude_deg);
if let Some(heading) = fix.true_course_deg {
self.heading_deg = Some(heading);
} else {
warn!("State: Course missing from GpsFix");
}
if let Some(speed) = fix.speed_over_ground {
self.speed_mps = Some(speed);
} else {
warn!("State: Speed missing from GpsFix");
}
if let Some(time) = fix.get_timestamp() {
#[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 {
@ -217,18 +228,19 @@ impl State {
}
}
#[cfg(feature = "gnss")]
#[allow(unused)]
pub fn print_fix(&self) -> alloc::string::String {
alloc::format!(
"{:?}, {:.4} N, {:.4} E, {:.0}°, {:.2} m/s",
self.time,
self.position.y(),
self.position.x(),
self.heading_deg.unwrap_or_default(),
self.speed_mps.unwrap_or_default(),
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 {
@ -252,7 +264,7 @@ impl State {
#[cfg(feature = "spat")]
pub fn run_glosa(&mut self) -> GlosaOutput {
let Some(own_heading) = self.heading_deg else {
let Some(own_heading) = self.pos.heading_deg else {
use alloc::string::ToString;
return GlosaOutput::Error("Heading unknown".to_string());
@ -268,7 +280,7 @@ impl State {
);
if let Some(search_result) =
Self::find_intersection(&self.position, own_heading, &self.tlc_cache)
Self::find_intersection(&self.pos.position, own_heading, &self.tlc_cache)
{
info!(
"GLOSA: Approach has signal groups: {:?}",
@ -315,7 +327,7 @@ impl State {
&intersection,
info.lane_id,
info.approach_id,
&self.position,
&self.pos.position,
) {
info!("GLOSA: Intersection was left -> back to idle");
(
@ -619,6 +631,11 @@ impl State {
pub fn time(&self) -> chrono::prelude::NaiveDateTime {
self.time
}
#[allow(unused)]
pub fn pos(&self) -> &esp32_cits_core::PosVel {
&self.pos
}
}
#[cfg(test)]
@ -670,13 +687,13 @@ mod tests {
let own_pos_passed = geo_types::Point::new(9.977217, 53.5560602);
let own_heading = 90.;
state.heading_deg = Some(own_heading);
state.pos.heading_deg = Some(own_heading);
state.position = own_pos_infront;
state.pos.position = own_pos_infront;
state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Idle));
state.position = own_pos_inside;
state.pos.position = own_pos_inside;
state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Locked(_)));
if let GlosaState::Locked(info) = &state.glosa_state {
@ -684,7 +701,7 @@ mod tests {
assert!(info.lane_id == 1 || info.lane_id == 2);
}
state.position = own_pos_inside;
state.pos.position = own_pos_inside;
state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Locked(_)));
if let GlosaState::Locked(info) = &state.glosa_state {
@ -692,7 +709,7 @@ mod tests {
assert!(info.lane_id == 1 || info.lane_id == 2);
}
state.position = own_pos_passed;
state.pos.position = own_pos_passed;
state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Idle));
}

515
src/io.rs Normal file
View file

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

View file

@ -7,48 +7,81 @@
)]
// #![deny(clippy::large_stack_frames)]
// we can't use UART I/O and GNSS at the same time
#[cfg(all(feature = "uart", feature = "gnss_ubx", feature = "gnss_l67k"))]
compile_error!(
"feature \"uart\" and features \"gnss_ubx\" or \"gnss_l67k\" cannot be enabled at the same time"
);
// autonomous CAM generation need a time and position source (aka _gnss)
#[cfg(all(feature = "cam_tx", not(feature = "_gnss")))]
compile_error!("feature \"cam_tx\" needs a time and position source (either GNSS or UART)");
// we can't have both GNSS drivers enabled at the same time
#[cfg(all(feature = "gnss_ubx", feature = "gnss_l67k"))]
compile_error!("feature \"gnss_ubx\" and feature \"gnss_l67k\" cannot be enabled at the same time");
use alloc::string::ToString as _;
use alloc::vec::Vec;
use core::cell::RefCell;
#[cfg(feature = "spat")]
use core::cell::UnsafeCell;
use c_its_parser::gn as geonetworking;
#[cfg(feature = "cam_tx")]
use c_its_parser::standards::extensions::ItsStationType;
use critical_section::Mutex;
use crossbeam_queue::ArrayQueue;
use embassy_executor::Spawner;
#[cfg(feature = "gnss")]
#[cfg(feature = "gnss_l67k")]
use embassy_gps::gps::l76k;
use esp_backtrace as _;
use esp_hal::clock::CpuClock;
use esp_hal::timer::timg::TimerGroup;
#[cfg(feature = "screen")]
use esp_hal::{delay, gpio, spi, time};
#[cfg(all(target_arch = "riscv32", feature = "spat"))]
use esp_println::println;
use esp_radio::wifi;
use log::{debug, error, info, warn};
use esp32_cits_core::radio;
use geonetworking::Decode as _;
use log::{error, info, warn};
extern crate alloc;
mod applogic;
#[cfg(feature = "spat")]
mod cache;
mod geo_alg;
mod radio;
#[cfg(feature = "uart")]
mod io;
#[cfg(feature = "screen")]
mod screen;
#[cfg(feature = "gnss_ubx")]
mod ubx;
const WIFI_CHANNEL: radio::Channel = 180;
#[cfg(feature = "screen")]
pub const DISPLAY_WIDTH: u16 = 320;
#[cfg(feature = "screen")]
pub const DISPLAY_HEIGHT: u16 = 172;
const WIFI_TXPOWER: i8 = 10; // dBm, 2..20
#[cfg(feature = "spat")]
const SPAT_RATE_LIMIT: u8 = 5; // keep every n-th message
#[cfg(any(feature = "cam_tx", feature = "uart"))]
const GN_IS_MOBILE: bool = true;
#[cfg(feature = "cam_tx")]
const OWN_STATION_TYPE: ItsStationType = ItsStationType::Cyclist;
#[cfg(feature = "cam_tx")]
const OWN_VEHICLE_WIDTH: u8 = 5; // in 10cm steps!
#[cfg(feature = "cam_tx")]
const OWN_VEHICLE_LENGTH: u16 = 20; // in 10cm steps!
#[cfg(feature = "_uart")]
const SERIAL_FIFO_SIZE_THLD: u8 = 100; // hardware has max. 128 byte
// This creates a default app-descriptor required by the esp-idf bootloader.
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!();
#[cfg(feature = "gnss")]
static GNSS_UPDATE: Mutex<RefCell<Option<embassy_gps::types::GpsFix>>> =
Mutex::new(RefCell::new(None));
#[cfg(feature = "_gnss")]
static GNSS_UPDATE: Mutex<RefCell<Option<applogic::GnssFix>>> = Mutex::new(RefCell::new(None));
static WIFI_RX_QUEUE: Mutex<RefCell<Option<ArrayQueue<Vec<u8>>>>> = Mutex::new(RefCell::new(None));
static mut USER_LED: Option<esp_hal::gpio::Output<'static>> = None;
#[cfg(feature = "_uart")]
static UART_RX_BUF: Mutex<RefCell<Option<Vec<u8>>>> = Mutex::new(RefCell::new(None));
#[cfg(feature = "_uart")]
static SERIAL: Mutex<RefCell<Option<esp_hal::uart::Uart<'static, esp_hal::Blocking>>>> =
Mutex::new(RefCell::new(None));
#[allow(
clippy::large_stack_frames,
@ -84,7 +117,7 @@ async fn main(spawner: Spawner) -> ! {
info!("Embassy initialized!");
// ST7789 172*320px screen
// Setup ST7789 screen
// SCL: D8/ GPIO8 (SPI_SCK)
// SDA: D10/ GPIO10 (SPI_MOSI)
// RES: D3/ GPIO7
@ -95,59 +128,67 @@ async fn main(spawner: Spawner) -> ! {
let mut spi_buffer = [0_u8; 512];
#[cfg(feature = "screen")]
let mut screen = {
let (spi, cs) = setup_st7789_spi(
// 172*320px in landscape orientation
let (display, width, height) = screen::make_small_display(
peripherals.GPIO8,
peripherals.GPIO10,
peripherals.GPIO7,
peripherals.GPIO23,
peripherals.GPIO24,
peripherals.GPIO9,
peripherals.GPIO0,
peripherals.SPI2,
&mut spi_buffer,
)
.expect("Failed to build SPI config");
let output_config = gpio::OutputConfig::default();
let dc_output = gpio::Output::new(peripherals.GPIO23, gpio::Level::Low, output_config);
let rst_output = gpio::Output::new(peripherals.GPIO7, gpio::Level::High, output_config);
let _ = gpio::Output::new(peripherals.GPIO0, gpio::Level::High, output_config); // backlight
.expect("Fatal error building screen");
let mut display_delay = delay::Delay::new();
let mut screen = screen::Handler::new(display, width, height);
let spi_device = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs)
.expect("Failed to initialize SPI device");
let _ = screen
.update(screen::ScreenState::Initial)
.inspect_err(log_screen_error);
let display = mipidsi::Builder::new(
mipidsi::models::ST7789,
mipidsi::interface::SpiInterface::new(spi_device, dc_output, &mut spi_buffer),
)
.display_size(DISPLAY_HEIGHT + 34, DISPLAY_WIDTH) // somehow this display has 34 px of invisible space on the left (in native orientation)
.reset_pin(rst_output)
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg90))
.init(&mut display_delay)
.expect("Failed to initialize display");
screen::Handler::new(display, DISPLAY_WIDTH, DISPLAY_HEIGHT)
screen
};
#[cfg(feature = "screen")]
let _ = screen
.update(screen::ScreenState::Initial)
.inspect_err(log_screen_error);
// Setup UART I/O channel
#[cfg(feature = "uart")]
setup_uart0(
115_200,
Some(b'\n'),
peripherals.GPIO12,
peripherals.GPIO11,
peripherals.UART0,
serial_handler,
)
.expect("Failed to initialize UART");
// Setup WiFi
// sadly, it doesn't work any more when we move `esp_radio::wifi::new` to `setup_wifi_sniffer`
let (mut wifi_controller, interfaces) = esp_radio::wifi::new(
peripherals.WIFI,
esp_radio::wifi::ControllerConfig::default(),
)
.expect("Failed to initialize Wi-Fi controller");
if let Err(err) = wifi_controller.set_band_mode(esp_radio::wifi::BandMode::_5G) {
error!("Failed to set WiFi to 5GHz only: {err}");
}
if let Err(err) = wifi_controller.set_max_tx_power(WIFI_TXPOWER * 4) {
error!("Failed to set WiFi TX power: {err}");
}
unsafe {
USER_LED = Some(esp_hal::gpio::Output::new(
peripherals.GPIO27,
esp_hal::gpio::Level::Low,
esp_hal::gpio::OutputConfig::default(),
));
}
critical_section::with(|cs| {
WIFI_RX_QUEUE.borrow(cs).replace(Some(ArrayQueue::new(2)));
});
let mut sniffer = interfaces.sniffer;
radio::setup_wifi_sniffer(WIFI_CHANNEL, &mut sniffer, handle_frame)
let mut wlan_iface = interfaces.sniffer;
radio::setup_wifi_sniffer(WIFI_CHANNEL, &mut wlan_iface, handle_frame)
.expect("Fatal error initializing 802.11p sniffer");
info!("WiFi promiscuous mode on channel {WIFI_CHANNEL} running");
@ -155,14 +196,15 @@ async fn main(spawner: Spawner) -> ! {
let mac = esp_hal::efuse::base_mac_address();
info!("WiFi MAC: {mac}");
// Setup GNSS:
// Seeed XIAO L67K:
// RX: D7/ GPIO12
// TX: D6/ GPIO11
// WAKEUP: D0/ GPIO1 -> HIGH for active, LOW for Sleep
// RESET: D2/ GPIO25 -> HIGH for normal, LOW for Reset
#[cfg(feature = "gnss")]
#[cfg(feature = "gnss_l67k")]
spawner.spawn(
gnss_task(
gnss_l67k_task(
peripherals.GPIO1,
peripherals.GPIO25,
peripherals.UART0,
@ -171,14 +213,167 @@ async fn main(spawner: Spawner) -> ! {
)
.expect("Failed to spawn GNSS task"),
);
#[cfg(feature = "gnss")]
// U-Blox module:
// RX: D7/ GPIO12
// TX: D6/ GPIO11
// RESET: D2/ GPIO25 -> HIGH for normal, LOW for Reset
#[cfg(feature = "gnss_ubx")]
{
setup_uart0(
9600,
Some(b'\n'),
peripherals.GPIO12,
peripherals.GPIO11,
peripherals.UART0,
serial_handler,
)
.expect("Failed to initialize UART for GNSS");
// do some settings (UART receive and parsing runs in main loop)
spawner
.spawn(gnss_ubx_config(peripherals.GPIO25).expect("Failed to spawn GNSS config task"));
}
#[cfg(feature = "_gnss")]
info!("GNSS task started, waiting for GNSS fix...");
#[cfg(any(feature = "cam_tx", feature = "uart"))]
let (mut seq_no, mac_bytes) = {
// unwrap is fine since [`esp_hal::efuse::MacAddress`] is just an `[u8; 6]`
let mac_bytes = mac.as_bytes().try_into().unwrap();
(0, mac_bytes)
};
let mut state = applogic::State::new();
#[cfg(feature = "cam_tx")]
let mut cam = {
let station_id = make_station_id(mac);
info!("V2X Station ID: {station_id}");
applogic::cam_tx::CamState::new(
station_id,
OWN_STATION_TYPE,
OWN_VEHICLE_LENGTH,
OWN_VEHICLE_WIDTH,
)
};
#[cfg(feature = "uart")]
let mut uart_input = io::Parser::default();
#[cfg(feature = "gnss_ubx")]
let mut ubx_parser = ublox::Parser::default();
loop {
embassy_time::Timer::after(embassy_time::Duration::from_millis(1)).await;
#[cfg(feature = "gnss")]
#[cfg(feature = "_uart")]
{
let mut new_data = None;
critical_section::with(|cs| {
let mut buf_rc = UART_RX_BUF.borrow(cs).borrow_mut();
// unwrap is fine b/c we stored something in it before
let buf = buf_rc.as_mut().unwrap();
// we move the buffer to new_data to do the processing outside of the critical section lock
if !buf.is_empty() {
new_data = Some(core::mem::take(buf));
}
});
if let Some(data) = new_data {
#[cfg(feature = "uart")]
{
for msg in uart_input.parse(&data) {
match msg.payload {
Some(io::msg::serial_input_msg::Payload::Position(fix)) => {
let pos_state = fix.into();
critical_section::with(|cs| {
let gnss_update_ref = GNSS_UPDATE.borrow(cs);
gnss_update_ref.replace(Some(pos_state));
});
}
Some(io::msg::serial_input_msg::Payload::TxMsg(tx_request)) => {
use esp32_cits_core::tx::GnItsPayload as _;
info!(
"UART: Got new TX message: Type {} with {} bytes",
tx_request.message_type,
tx_request.payload.len()
);
let own_position = state.pos();
let time = state.time().and_utc();
match tx_request.encode_packet(
mac_bytes,
own_position,
time,
GN_IS_MOBILE,
&mut seq_no,
) {
Ok(data) => {
if let Err(err) = radio::send_80211_bcast_frame(
&mut wlan_iface,
&mac_bytes.clone(),
&data,
) {
warn!("Failed to send 802.11 frame: {err}");
}
}
Err(err) => warn!("Failed to create GN message: {err}"),
}
}
None => {}
}
}
}
#[cfg(feature = "gnss_ubx")]
{
let mut it = ubx_parser.consume_ubx(&data);
loop {
match it.next() {
Some(Ok(ublox::UbxPacket::Proto27(packet_ref))) => match &packet_ref {
ublox::proto27::PacketRef::MonVer(packet) => {
println!("{}", ubx::pretty_print_mon_ver(packet));
}
ublox::proto27::PacketRef::MonComms(packet) => {
println!("{}", ubx::pretty_print_mon_comms(packet));
}
ublox::proto27::PacketRef::MonRf(packet) => {
println!("{}", ubx::pretty_print_mon_rf(packet));
}
ublox::proto27::PacketRef::NavSat(packet) => {
println!("{}", ubx::pretty_print_nav_sat(packet));
}
ublox::proto27::PacketRef::NavPvt(pvt) => {
match applogic::GnssFix::try_from(pvt) {
Ok(pos_state) => {
critical_section::with(|cs| {
let gnss_update_ref = GNSS_UPDATE.borrow(cs);
gnss_update_ref.replace(Some(pos_state));
});
}
Err(err) => warn!("{err}"),
}
}
_ => {
println!("UBX: {packet_ref:?}");
}
},
Some(Err(_)) => {
// Received a malformed packet
}
None => {
// The internal buffer is now empty
break;
}
}
}
}
}
}
#[cfg(feature = "_gnss")]
{
let mut new_position = false;
critical_section::with(|cs| {
@ -187,6 +382,33 @@ async fn main(spawner: Spawner) -> ! {
if let Some(fix) = gnss_update_ref.replace(None) {
state.update_with_gpsfix(&fix);
new_position = true;
#[cfg(feature = "cam_tx")]
{
use esp32_cits_core::tx::GnItsPayload as _;
let time = state.time().and_utc();
cam.update(time, fix.clone().into());
match cam.encode_packet(
mac_bytes,
&fix.into(),
time,
GN_IS_MOBILE,
&mut seq_no,
) {
Ok(data) => {
if let Err(err) = radio::send_80211_bcast_frame(
&mut wlan_iface,
&mac_bytes.clone(),
&data,
) {
warn!("Failed to send 802.11 frame: {err}");
}
}
Err(err) => warn!("Failed to create CAM: {err}"),
}
}
}
});
@ -227,12 +449,12 @@ async fn main(spawner: Spawner) -> ! {
}
});
if let Some(data) = new_data {
match c_its_parser::de::decode(&data, c_its_parser::Headers::IEEE802LlcGnBtp) {
match c_its_parser::de::decode(&data, c_its_parser::Headers::None) {
Ok(msg) => {
use c_its_parser::ItsMessage;
use c_its_parser::standards::extensions::ItsMessageId;
// use c_its_parser::standards::extensions::ItsMessageId;
debug!("Got new {:?} message", ItsMessageId::from(&msg));
// info!("Got new {:?} message", ItsMessageId::from(&msg));
match msg {
#[cfg(feature = "spat")]
@ -252,12 +474,15 @@ async fn main(spawner: Spawner) -> ! {
}
}
#[cfg(feature = "cam")]
#[cfg(any(feature = "cam", feature = "cam_tx"))]
ItsMessage::Cam {
geonetworking: _,
transport: _,
etsi,
} => applogic::cam::handle_cam(&etsi),
} => {
#[cfg(feature = "cam")]
applogic::cam::handle_cam(&etsi);
}
#[cfg(feature = "denm")]
ItsMessage::DenmV2 {
@ -276,8 +501,9 @@ async fn main(spawner: Spawner) -> ! {
}
}
Err(err) => {
// will give false-positives when a message is received which wasn't enabled
debug!("Failed to parse V2X message: {err}");
// we already filtered out all unsupported message types in the wifi rx callback
// so these are actual errors
warn!("Failed to parse V2X message: {err}");
}
}
}
@ -291,50 +517,197 @@ async fn main(spawner: Spawner) -> ! {
clippy::needless_pass_by_value,
reason = "adhering to callback interface"
)]
fn handle_frame(frame: wifi::sniffer::PromiscuousPkt<'_>) {
// Ignore frames with errors and non-data frames
if frame.rx_cntl.rx_state != 0 {
warn!("Received frame has RX error: {}", frame.rx_cntl.rx_state);
return;
}
#[allow(clippy::too_many_lines)]
fn handle_frame(frame: esp_radio::wifi::sniffer::PromiscuousPkt<'_>) {
#[cfg(feature = "spat")]
const PRUNE_INTERVAL: chrono::Duration = chrono::Duration::seconds(60);
#[cfg(feature = "spat")]
static mut RATELIMIT_CACHE_LAST_PRUNE: UnsafeCell<chrono::NaiveDateTime> =
UnsafeCell::new(chrono::NaiveDateTime::MIN);
if !radio::PromiscuousPktType::Data.matches(frame.frame_type) {
debug!("Received frame is not a DATA frame: {}", frame.frame_type);
return;
}
#[cfg(feature = "spat")]
static mut SPAT_RATELIMIT_CACHE: UnsafeCell<cache::Cache<u32, RatelimitData>> = UnsafeCell::new(
cache::Cache::<u32, RatelimitData>::new(chrono::Duration::seconds(20)),
);
// Ignore non-broadcast frames
if frame.len < (4 + 6) {
warn!(
"Received frame too small to be a valid broadcast frame: {} bytes",
frame.len
);
return;
}
let dest_mac = &frame.data[4..10];
if dest_mac != [0xff; 6] {
warn!("Received frame is not broadcast frame: Dest MAC {dest_mac:02x?}");
return;
}
#[cfg(feature = "spat")]
let now = {
let time = esp_hal::time::Instant::now().duration_since_epoch();
// unwrap is fine since value range is u64 microseconds
chrono::DateTime::from_timestamp(time.as_secs().cast_signed(), 0)
.unwrap()
.naive_utc()
};
debug!("Received frame with {} bytes", frame.len);
// Send data to main thread
critical_section::with(|cs| {
let queue_rc = WIFI_RX_QUEUE.borrow(cs).borrow();
// unwrap is fine b/c we stored something in it before
let queue = queue_rc.as_ref().unwrap();
if queue.push(frame.data.to_vec()).is_err() {
error!("V2X RX queue is full");
match esp32_cits_core::rx::is_broadcast_frame(&frame) {
Err(err) => {
warn!("{err}");
return;
}
});
Ok(false) => {
// drop frame
return;
}
Ok(true) => {
// continue
}
}
// info!("Received frame with {} bytes", frame.len);
esp_println::print!("."); // TODO: debug only!
unsafe {
#[allow(static_mut_refs)]
if let Some(ref mut led) = USER_LED.as_mut() {
led.toggle();
}
}
// parse GN headers
match c_its_parser::pcap::remove_wlan_headers(frame.data)
.map_err(|err| alloc::format!("Failed to parse WLAN headers: {err}"))
.and_then(|data| {
geonetworking::Packet::decode(data)
.map_err(|err| alloc::format!("Failed to parse GN headers: {err:?}"))
}) {
Err(err) => {
warn!("{err}");
}
Ok(packet) => {
// drop hopped (for now)
if packet.decoded.is_hopped() {
return;
}
// "parse" ITS PDU Header
if let Ok((payload, message_id, station_id)) = get_its_header(&packet) {
// drop all unsupported message IDs and rate-limit SPATEMs
if match message_id {
#[cfg(feature = "denm")]
c_its_parser::standards::extensions::ItsMessageId::Denm => false,
#[cfg(feature = "cam")]
c_its_parser::standards::extensions::ItsMessageId::Cam => false,
#[cfg(feature = "spat")]
c_its_parser::standards::extensions::ItsMessageId::Spatem => {
// reduce SPAT rate per station ID
let mut drop_msg = false;
unsafe {
#[allow(
static_mut_refs,
reason = "this function will only run consecutive"
)]
let spat_cache = SPAT_RATELIMIT_CACHE.get();
(*spat_cache).update_or_init(now, station_id, |i| {
i.msg_count = i.msg_count.wrapping_add(1);
if (i.msg_count % SPAT_RATE_LIMIT) > 0 {
drop_msg = true;
}
});
}
drop_msg
}
#[cfg(feature = "spat")]
c_its_parser::standards::extensions::ItsMessageId::Mapem => false,
_ => true,
} {
return;
}
// Send data to main thread
critical_section::with(|cs| {
let queue_rc = WIFI_RX_QUEUE.borrow(cs).borrow();
// unwrap is fine b/c we stored something in it before
let queue = queue_rc.as_ref().unwrap();
if queue.push(payload.to_vec()).is_err() {
error!("V2X RX queue is full");
}
});
// Send data to UART
#[cfg(feature = "uart")]
{
let message = io::msg::RawMsgRx {
msg_type: message_id.as_u8().into(),
payload: frame.data.to_vec(),
}
.serialize_uart_proto();
send_uart0(&message);
}
}
}
}
#[cfg(feature = "spat")]
unsafe {
#[allow(static_mut_refs, reason = "this function will only run consecutive")]
let last_prune_time = RATELIMIT_CACHE_LAST_PRUNE.get();
if *last_prune_time + PRUNE_INTERVAL < now {
#[allow(static_mut_refs, reason = "this function will only run consecutive")]
let spat_cache = SPAT_RATELIMIT_CACHE.get();
(*spat_cache).prune(now);
*last_prune_time = now;
}
}
}
#[cfg(feature = "gnss")]
#[cfg(feature = "spat")]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct RatelimitData {
station_id: u32,
msg_count: u8,
}
#[cfg(feature = "spat")]
impl cache::Cachable<u32> for RatelimitData {
fn key(&self) -> u32 {
self.station_id
}
}
#[cfg(feature = "spat")]
impl cache::Initable<Self, u32> for RatelimitData {
fn init(key: u32) -> Self {
Self {
station_id: key,
..Default::default()
}
}
}
fn get_its_header<'p>(
packet: &geonetworking::Decoded<geonetworking::Packet<'p>>,
) -> Result<
(
&'p [u8],
c_its_parser::standards::extensions::ItsMessageId,
u32,
),
alloc::string::String,
> {
let payload = packet.decoded.btp_payload()?;
if payload.len() < 6 {
return Err("ITS payload too small for ItsPduHeader".to_string());
}
if let Ok(message_id) = payload[1].try_into() {
let station_id_buf: [u8; 4] = payload[2..6].try_into().unwrap();
let station_id = u32::from_be_bytes(station_id_buf);
Ok((payload, message_id, station_id))
} else {
Err("Unknown message ID in ItsPduHeader".to_string())
}
}
#[cfg(feature = "gnss_l67k")]
#[allow(clippy::large_stack_frames, reason = "GNSS driver needs some space")]
#[embassy_executor::task]
async fn gnss_task(
async fn gnss_l67k_task(
gps_wakeup: esp_hal::peripherals::GPIO1<'static>,
gps_reset: esp_hal::peripherals::GPIO25<'static>,
uart: esp_hal::peripherals::UART0<'static>,
@ -365,9 +738,17 @@ async fn gnss_task(
fix_drop_message = !fix_drop_message;
if fix_drop_message {
// send to main thread
let pos_state = applogic::GnssFix {
time: fix.get_timestamp(),
latitude_deg: fix.latitude_deg,
longitude_deg: fix.longitude_deg,
heading_deg: fix.true_course_deg,
speed_mps: fix.speed_over_ground,
};
critical_section::with(|cs| {
let gnss_update_ref = GNSS_UPDATE.borrow(cs);
gnss_update_ref.replace(Some(fix));
gnss_update_ref.replace(Some(pos_state));
});
}
} else {
@ -376,34 +757,36 @@ async fn gnss_task(
}
}
/// Builds the SPI interface for the ST7789 display
///
/// Returns (spi master, chip-select) tuple or SPI config error
#[cfg(feature = "screen")]
fn setup_st7789_spi(
scl: esp_hal::peripherals::GPIO8<'static>,
sda: esp_hal::peripherals::GPIO10<'static>,
cs: esp_hal::peripherals::GPIO24<'static>,
miso: esp_hal::peripherals::GPIO9<'static>,
spi: esp_hal::peripherals::SPI2<'static>,
) -> Result<
(
spi::master::Spi<'static, esp_hal::Blocking>,
gpio::Output<'static>,
),
spi::master::ConfigError,
> {
let spi_config = spi::master::Config::default()
.with_frequency(time::Rate::from_mhz(40))
.with_mode(spi::Mode::_0);
let spi: spi::master::Spi<'_, esp_hal::Blocking> = spi::master::Spi::new(spi, spi_config)?
.with_sck(scl)
.with_mosi(sda)
.with_miso(miso);
#[cfg(feature = "gnss_ubx")]
#[embassy_executor::task]
async fn gnss_ubx_config(reset_pin: esp_hal::peripherals::GPIO25<'static>) {
// reset the module
let mut gnss_reset = esp_hal::gpio::Output::new(
reset_pin,
esp_hal::gpio::Level::Low,
esp_hal::gpio::OutputConfig::default(),
);
embassy_time::Timer::after(embassy_time::Duration::from_millis(2)).await;
gnss_reset.set_high();
let cs_output = gpio::Output::new(cs, gpio::Level::High, gpio::OutputConfig::default());
embassy_time::Timer::after(embassy_time::Duration::from_secs(1)).await;
Ok((spi, cs_output))
info!("UBX: Enabling ubx protocol with NAV-PVT and NAV-SAT");
let buffer = ublox::packets::cfg_val::CfgValSetBuilder {
version: 1,
layers: ublox::packets::cfg_val::CfgLayerSet::RAM,
reserved1: 0,
cfg_data: &[
ublox::cfg_val::CfgVal::Uart1OutProtNmea(false),
ublox::cfg_val::CfgVal::Uart1OutProtUbx(true),
// CFG-MSGOUT-UBX_NAV_PVT_UART1
ublox::cfg_val::CfgVal::MsgOutUbxNavPvtUart1(1),
// CFG-MSGOUT-UBX_NAV_SAT_UART1
// ublox::cfg_val::CfgVal::MsgOutUbxNavSatUart1(10),
],
}
.into_packet_vec();
send_uart0(&buffer);
}
#[cfg(feature = "screen")]
@ -413,3 +796,111 @@ where
{
error!("graphics failed: {error:?}");
}
#[cfg(feature = "_uart")]
/// Configures UART0 with an interrupt handler and saves it to `SERIAL`
fn setup_uart0(
baud: u32,
cmd_char: Option<u8>,
gpio12: esp_hal::peripherals::GPIO12<'static>,
gpio11: esp_hal::peripherals::GPIO11<'static>,
uart0: esp_hal::peripherals::UART0<'static>,
handler: esp_hal::interrupt::InterruptHandler,
) -> Result<(), alloc::string::String> {
critical_section::with(|cs| {
UART_RX_BUF
.borrow(cs)
.replace(Some(alloc::vec::Vec::with_capacity(255)));
});
let mut uart = esp_hal::uart::Uart::new(
uart0,
esp_hal::uart::Config::default()
.with_baudrate(baud)
.with_rx(
esp_hal::uart::RxConfig::default()
.with_fifo_full_threshold(u16::from(SERIAL_FIFO_SIZE_THLD))
.with_timeout(50),
),
)
.map_err(|err| alloc::format!("UART0 config error: {err}"))?
.with_rx(gpio12)
.with_tx(gpio11);
if let Some(char) = cmd_char {
uart.set_at_cmd(
esp_hal::uart::AtCmdConfig::default()
.with_cmd_char(char)
.with_char_num(1)
.with_pre_idle_count(0) // command start can be right after other data
.with_post_idle_count(0) // we don't expect a gap between the command start and subsequent text
.with_gap_timeout(5), // needs to be >0 to work
);
}
uart.set_interrupt_handler(handler);
uart.listen(
esp_hal::uart::UartInterrupt::AtCmd
| esp_hal::uart::UartInterrupt::RxTimeout
| esp_hal::uart::UartInterrupt::RxFifoFull, // just to be save
);
critical_section::with(|cs| SERIAL.borrow_ref_mut(cs).replace(uart));
Ok(())
}
#[cfg(feature = "_uart")]
#[esp_hal::handler]
fn serial_handler() {
critical_section::with(|cs| {
let mut serial = SERIAL.borrow_ref_mut(cs);
let mut uart_buf = UART_RX_BUF.borrow_ref_mut(cs);
if let (Some(serial), Some(uart_buf)) = (serial.as_mut(), uart_buf.as_mut()) {
let mut buf = [0u8; SERIAL_FIFO_SIZE_THLD as usize];
match serial.read_buffered(&mut buf) {
Ok(0) => {}
Ok(len) => {
uart_buf.extend_from_slice(&buf[..len]);
}
Err(err) => error!("Failed to read from UART: {err}"),
}
serial.clear_interrupts(
esp_hal::uart::UartInterrupt::RxFifoFull
| esp_hal::uart::UartInterrupt::RxTimeout
| esp_hal::uart::UartInterrupt::AtCmd,
);
}
});
}
#[cfg(feature = "_uart")]
fn send_uart0(data: &[u8]) {
critical_section::with(|cs| {
let mut serial = SERIAL.borrow_ref_mut(cs);
if let Some(serial) = serial.as_mut()
&& let Err(err) = serial.write(data)
{
error!("Failed to write to UART: {err:?}");
}
});
}
#[cfg(all(feature = "esp", feature = "cam_tx"))]
/// Creates a station ID from the WLAN MAC address
///
/// Will use `0xC173` as static prefix and uses last 16 bit from MAC address for the remainder.
///
/// # Panics
/// Won't panic in practive b/c of pre-conditions
#[must_use]
pub fn make_station_id(mac: esp_hal::efuse::MacAddress) -> u32 {
// unwrap is fine here since we can be sure that the MacAddress is 6 byte
// and a slice of 2 bytes can be converted to [u8; 2]
let mac_last_16bit = u16::from_be_bytes(mac.as_bytes().get(4..6).unwrap().try_into().unwrap());
0xC173_0000 | u32::from(mac_last_16bit)
}

View file

@ -1,291 +0,0 @@
//! IEEE 802.11p C-ITS Capture with ESP32-C5
#![allow(unused)]
#[cfg(feature = "esp")]
use esp_radio::wifi::sniffer;
// -----------------------------
// missing interface definitions
// -----------------------------
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
/// - `half_rate`: 5 MHz mode
unsafe fn phy_11p_set(enable: u8, half_rate: 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 we are the same frame type as `esp_radio::wifi::sniffer::PromiscuousPkt::frame_type`
pub fn matches(self, frame_type: core::ffi::c_uint) -> bool {
self.as_frame_type() == frame_type
}
}
#[cfg(feature = "esp")]
/// 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<(), alloc::string::String> {
sniffer
.set_promiscuous_mode(true)
.map_err(|err| alloc::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(alloc::format!("Failed to change channel: {ret}")),
}
}
}
#[cfg(feature = "esp")]
/// Sends 802.11p broadcast frame
///
/// Expects `data` to contain GN header (BTP header and ITS payload) to be a valid C-ITS/ ITS-G5 packet/ frame.
///
/// # Errors
/// Fails when MAC is not the right size or returns frame send error
pub fn send_80211_bcast_frame(
sniffer: &mut sniffer::Sniffer,
mac_addr: &[u8],
data: &[u8],
) -> Result<(), alloc::string::String> {
const HEADER_SIZE: usize = 24 + 8;
let mac = mac_addr
.try_into()
.map_err(|err| alloc::format!("Unexpected MAC address size: {err}"))?;
// assemble frame
let mut frame_buf = alloc::vec::Vec::with_capacity(HEADER_SIZE + data.len());
frame_buf.extend_from_slice(&DataFrameHeader::new_80211p(mac).to_binary());
frame_buf.extend_from_slice(&LlcHeader::new_80211p().to_binary());
frame_buf.extend_from_slice(data);
// send
sniffer
.send_raw_frame(true, &frame_buf, true)
.map_err(|err| alloc::format!("Failed to send 802.11p frame: {err:?}"))
}
/// IEEE 802.11p frame header
#[derive(Debug, Default)]
struct DataFrameHeader {
/// byte 0: subtype in upper nibble, type in lower nibble
/// "normal" data frame: 0x08, QoS data frame: 0x88
pub type_and_subtype: u8,
/// byte 1: frame contol field, 0x00 for 802.11p
pub fcf_flags: u8,
/// byte 2..3: duration in micros, usually 0x0000 for 802.11p (toDS = 0, fromDS = 0)
pub duration: u16,
/// byte 4..9: receiver/ destination for 802.11p
pub address_1: [u8; 6],
/// byte 10..15: transmitter/ source for 802.11p
pub address_2: [u8; 6],
/// byte 16..21: BSSID for 802.11p
pub address_3: [u8; 6],
/// byte 22..24: upper 12 bits: seq. no, lower 4 bits: fragment no
pub sequence_control: u16,
// Note: address 4 is not used in 802.11p
// pub qos: Option<[u8; 2]>, // QoS frames are supported by radio library
// Note: 802.11p does not use HT mode(s)
}
impl DataFrameHeader {
const BCAST_MAC: [u8; 6] = [0xFF; 6];
/// Create an 802.11p (non-QoS) data frame header with a source MAC address
pub(crate) fn new_80211p(source_mac: &[u8; 6]) -> Self {
Self {
type_and_subtype: 0x08,
address_1: Self::BCAST_MAC,
address_2: *source_mac,
address_3: Self::BCAST_MAC,
..Default::default()
}
}
pub(crate) fn to_binary(&self) -> [u8; 24] {
let mut buf = [0u8; 24];
buf[0] = self.type_and_subtype;
buf[1] = self.fcf_flags;
Self::write_u16(&mut buf, 2, self.duration);
Self::write_mac_addr(&mut buf, 4, &self.address_1);
Self::write_mac_addr(&mut buf, 10, &self.address_2);
Self::write_mac_addr(&mut buf, 16, &self.address_3);
Self::write_u16(&mut buf, 22, self.sequence_control);
buf
}
fn write_u16(buf: &mut [u8; 24], offset: usize, data: u16) {
for (idx, byte) in data.to_be_bytes().iter().enumerate() {
buf[idx + offset] = *byte;
}
}
fn write_mac_addr(buf: &mut [u8; 24], offset: usize, data: &[u8; 6]) {
for (idx, byte) in data.iter().enumerate() {
buf[idx + offset] = *byte;
}
}
}
/// IEEE 802.11p LLC header
#[derive(Debug, Default)]
struct LlcHeader {
/// byte 0: SNAP (0xaa) in 802.11p
pub dsap: u8, // SNAP in
/// byte 1: SNAP (0xaa) in 802.11p
pub lsap: u8, // SNAP in
/// byte 2: SNAP (0xaa) in 802.11p
pub control_1: u8, // 0x03
// Note: optional second control byte not used in 802.11p
// SNAP fields
/// set to 0 since ethertype is used for protocol ID in 802.11p
pub oui: [u8; 3],
/// ethertype GeoNetworking (0x8947) in 802.11p
pub protocol_id: u16,
}
impl LlcHeader {
pub(crate) fn new_80211p() -> Self {
Self {
dsap: 0xaa,
lsap: 0xaa,
control_1: 0x03,
oui: [0; 3],
protocol_id: 0x8947,
}
}
pub(crate) fn to_binary(&self) -> [u8; 8] {
let mut buf: [u8; 8] = [
self.dsap,
self.lsap,
self.control_1,
0x00,
0x00,
0x00,
0x00,
0x00,
];
buf[3] = self.oui[0];
buf[4] = self.oui[1];
buf[5] = self.oui[2];
buf[6] = self.protocol_id.to_be_bytes()[0];
buf[7] = self.protocol_id.to_be_bytes()[1];
buf
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn data_frame_test() {
// build a typical IEEE 802.11p frame
const OWN_MAC: [u8; 6] = [0x12, 0x34, 0x56, 0x78, 0xab, 0xcd];
const REF_HEADER: [u8; 24] = [
0x08, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x12, 0x34, 0x56, 0x78,
0xab, 0xcd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
];
let frame = DataFrameHeader::new_80211p(&OWN_MAC);
assert_eq!(0x08, frame.type_and_subtype); // data frame, subtype 0
assert_eq!(0x0000, frame.fcf_flags);
assert_eq!(0x0000, frame.duration);
assert_eq!([0xFF; 6], frame.address_1);
assert_eq!(OWN_MAC, frame.address_2);
assert_eq!([0xFF; 6], frame.address_3);
assert_eq!(0x0000, frame.sequence_control);
assert_eq!(REF_HEADER, frame.to_binary());
}
#[test]
fn llc_test() {
// build a typical IEEE 802.11p LLC header
const REF_HEADER: [u8; 8] = [0xaa, 0xaa, 0x03, 0x00, 0x00, 0x00, 0x89, 0x47];
let llc = LlcHeader::new_80211p();
assert_eq!(0xaa, llc.dsap);
assert_eq!(0xaa, llc.lsap);
assert_eq!(0x03, llc.control_1);
assert_eq!([0x00; 3], llc.oui);
assert_eq!(0x8947, llc.protocol_id);
assert_eq!(REF_HEADER, llc.to_binary());
}
}

View file

@ -348,3 +348,123 @@ fn maneuver_to_str(value: applogic::v2x::map::Maneuvers) -> alloc::string::Strin
if value.right_allowed { ">" } else { " " },
)
}
/// Creates an [`mipidsi::Display`], width, height tuple for an ST7789 172*320px screen in landscape orientation
///
/// # Errors
/// Human-readable fatal errors
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub fn make_small_display<IoScl, IoSda, IoRes, IoDc, IoCs, IoBl, Spi>(
scl: IoScl,
sda: IoSda,
res: IoRes,
dc: IoDc,
cs: IoCs,
bl: IoBl,
spi: Spi,
spi_buffer: &mut [u8],
) -> Result<
(
mipidsi::Display<
mipidsi::interface::SpiInterface<
'_,
embedded_hal_bus::spi::ExclusiveDevice<
esp_hal::spi::master::Spi<'_, esp_hal::Blocking>,
esp_hal::gpio::Output<'_>,
embedded_hal_bus::spi::NoDelay,
>,
esp_hal::gpio::Output<'_>,
>,
mipidsi::models::ST7789,
esp_hal::gpio::Output<'_>,
>,
u16,
u16,
),
alloc::string::String,
>
where
IoScl: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
IoSda: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
IoRes:
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
IoDc:
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
IoCs:
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
IoBl:
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
Spi: esp_hal::spi::master::Instance + 'static,
{
const DISPLAY_WIDTH: u16 = 320;
const DISPLAY_HEIGHT: u16 = 172;
const DISPLAY_HIDDEN_X: u16 = 34; // somehow this display has 34 px of invisible space on the left (in native orientation)
let (spi, cs) = setup_st7789_spi(scl, sda, cs, spi)
.map_err(|err| alloc::format!("Failed to build SPI config: {err}"))?;
let output_config = esp_hal::gpio::OutputConfig::default();
let dc_output = esp_hal::gpio::Output::new(dc, esp_hal::gpio::Level::Low, output_config);
let rst_output = esp_hal::gpio::Output::new(res, esp_hal::gpio::Level::High, output_config);
let _ = esp_hal::gpio::Output::new(bl, esp_hal::gpio::Level::High, output_config); // enable backlight
let mut display_delay = esp_hal::delay::Delay::new();
let spi_device = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs)
.map_err(|err| alloc::format!("Failed to initialize SPI device: {err}"))?;
let display = mipidsi::Builder::new(
mipidsi::models::ST7789,
mipidsi::interface::SpiInterface::new(spi_device, dc_output, spi_buffer),
)
.display_size(DISPLAY_HEIGHT + DISPLAY_HIDDEN_X, DISPLAY_WIDTH)
.reset_pin(rst_output)
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg90))
.init(&mut display_delay)
.map_err(|err| alloc::format!("Failed to initialize display: {err:?}"))?;
Ok((display, DISPLAY_WIDTH, DISPLAY_HEIGHT))
}
/// Builds the SPI interface for the ST7789 display
///
/// Returns (spi master, chip-select) tuple
///
/// # Errors
/// Returns SPI config error when SPI setup failed
fn setup_st7789_spi<IoScl, IoSda, IoCs, Spi>(
scl: IoScl,
sda: IoSda,
cs: IoCs,
spi: Spi,
) -> Result<
(
esp_hal::spi::master::Spi<'static, esp_hal::Blocking>,
esp_hal::gpio::Output<'static>,
),
esp_hal::spi::master::ConfigError,
>
where
IoScl: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
IoSda: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
IoCs:
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
Spi: esp_hal::spi::master::Instance + 'static,
{
let spi_config = esp_hal::spi::master::Config::default()
.with_frequency(esp_hal::time::Rate::from_mhz(40))
.with_mode(esp_hal::spi::Mode::_0);
let spi: esp_hal::spi::master::Spi<'_, esp_hal::Blocking> =
esp_hal::spi::master::Spi::new(spi, spi_config)?
.with_sck(scl)
.with_mosi(sda);
let cs_output = esp_hal::gpio::Output::new(
cs,
esp_hal::gpio::Level::High,
esp_hal::gpio::OutputConfig::default(),
);
Ok((spi, cs_output))
}

View file

@ -6,7 +6,7 @@ extern crate alloc;
mod applogic;
mod cache;
mod geo_alg;
mod radio;
mod io;
mod testdata;
fn main() {

View file

@ -455,3 +455,11 @@ pub const CAM: &[u8] = &[
0x00, 0x06, 0xc6, 0x70, 0x00, 0xdf, 0x80, 0x8d, 0x5f, 0xde, 0x66, 0x27, 0x00, 0x07, 0x3c, 0x04,
0x76, 0xfd, 0x67, 0x31, 0x9c, 0x00, 0x58, 0x60, 0x41, 0x37, 0xd3, 0x15, 0x89, 0xc0, 0x06, 0xdc,
];
#[cfg(any(feature = "cam_tx", feature = "uart"))]
pub const CAM_SMALL: &[u8] = &[
0x2, 0x2, 0xc1, 0x73, 0x25, 0x68, 0x17, 0x28, 0x40, 0x29, 0xd5, 0xb3, 0x40, 0xf, 0x1f, 0xea,
0xf0, 0x1f, 0xff, 0xff, 0xfc, 0x23, 0xb7, 0x74, 0x3e, 0x0, 0x0, 0xf, 0xc0, 0x0, 0x7e, 0x81,
0x38, 0x25, 0x7, 0x37, 0xfe, 0xeb, 0xff, 0xf6, 0x0, 0x0, 0xd, 0xff, 0xff, 0x7f, 0xff, 0xf1,
0xce, 0x40, 0x40, 0x0,
];

106
src/ubx.rs Normal file
View file

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