0
0
Fork 0

Compare commits

..

1 commit

Author SHA1 Message Date
166a01b202 WIP map: De-duplicate lanes on layered MAPEMs
TODO: Actually implement it
2026-07-23 00:13:03 +02:00
32 changed files with 375 additions and 28802 deletions

4
.gitignore vendored
View file

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

59
Cargo.lock generated
View file

@ -73,12 +73,6 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary-int"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993a810118f8f37e9c4411c86f1c4c940a09a7ab34b7bf2d88d06f50c553fab7"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
@ -192,9 +186,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]] [[package]]
name = "c-its-parser" name = "c-its-parser"
version = "2.4.1" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e94d9990b20498f621f164f23ae198c4365115eb4191ae106352ec74d4a4110" checksum = "f9bd739e1f76c017fc9fc6c21b9ff2aee4536bef357075d454e387e94a4d6287"
dependencies = [ dependencies = [
"chrono", "chrono",
"etherparse", "etherparse",
@ -1171,7 +1165,6 @@ dependencies = [
"esp-println", "esp-println",
"esp-radio", "esp-radio",
"esp-rtos", "esp-rtos",
"esp32-cits-core",
"geo-types", "geo-types",
"log", "log",
"mipidsi", "mipidsi",
@ -1179,19 +1172,6 @@ dependencies = [
"profont", "profont",
"prost", "prost",
"prost-build", "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]] [[package]]
@ -1259,9 +1239,9 @@ dependencies = [
[[package]] [[package]]
name = "etherparse" name = "etherparse"
version = "0.21.0" version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17304d06addb3283cdc4bd528e42dd95e73c8ee2d6492ffce415e93660885449" checksum = "3ac016aaf11dfe643edcd088a166234bfcb72e7f06691abfcf21e9af524037f4"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
] ]
@ -1391,11 +1371,13 @@ dependencies = [
[[package]] [[package]]
name = "geonetworking" name = "geonetworking"
version = "0.4.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6da6fde770112587d5f5f7af439546e35bd35aa77a9e3e47e9968056783fe50d" checksum = "6f6d36111f5057d3be7eb4e312a07edac4737fc791e4b0a73d812c6f710161e7"
dependencies = [ dependencies = [
"arbitrary-int", "bitvec",
"bitvec-nom2",
"bytes",
"nom", "nom",
"num", "num",
"num-traits", "num-traits",
@ -2504,29 +2486,6 @@ version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" 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]] [[package]]
name = "ufmt-write" name = "ufmt-write"
version = "0.1.0" version = "0.1.0"

View file

@ -12,10 +12,10 @@ path = "./src/main.rs"
[[bin]] [[bin]]
name = "test" name = "test"
path = "./src/test.rs" path = "./src/test.rs"
required-features = ["std", "cam"] required-features = ["std"]
[features] [features]
default = ["esp", "spat", "gnss_ubx", "screen"] default = ["esp", "spat", "gnss", "screen"]
# build for ESP (needed to build test for `host-tuple`) # build for ESP (needed to build test for `host-tuple`)
esp = [ esp = [
@ -30,19 +30,13 @@ esp = [
"dep:esp-backtrace", "dep:esp-backtrace",
"dep:esp-println", "dep:esp-println",
"dep:esp-radio", "dep:esp-radio",
"esp32-cits-core/esp32c5",
"esp32-cits-core/log-04",
] ]
std = ["c-its-parser/std", "esp32-cits-core/std"] std = ["c-its-parser/std"]
# enable u-blox GNSS module
gnss_ubx = ["_gnss", "_uart", "dep:ublox"]
# enable L67K GNSS module # enable L67K GNSS module
gnss_l67k = ["_gnss", "dep:embassy_gps"] gnss = ["dep:embassy_gps"]
# echo CAM data on serial # echo CAM data on serial
cam = ["c-its-parser/cam"] cam = ["c-its-parser/cam"]
# send CAM autonomously
cam_tx = ["c-its-parser/cam"]
# echo DENM events on serial and BLE # echo DENM events on serial and BLE
denm = ["c-its-parser/denm"] denm = ["c-its-parser/denm"]
# evaluate SPAT information # evaluate SPAT information
@ -51,13 +45,7 @@ spat = ["c-its-parser/spatem", "c-its-parser/mapem"]
spat_debug = [] spat_debug = []
# enable ST7789 based display # enable ST7789 based display
screen = ["dep:mipidsi", "dep:embedded-graphics", "dep:embedded-hal-bus", "dep:profont"] screen = ["dep:mipidsi", "dep:embedded-graphics", "dep:embedded-hal-bus", "dep:profont"]
# enable I/O via UART
uart = ["_uart", "_gnss", "dep:prost"]
# internal feature, that any UART I/O is enabled
_uart = []
# internal feature, that any GNSS source is enabled
_gnss = []
[dependencies] [dependencies]
esp-hal = { version = "~1.1.0", optional = true, features = ["esp32c5", "log-04", "unstable"] } esp-hal = { version = "~1.1.0", optional = true, features = ["esp32c5", "log-04", "unstable"] }
@ -84,7 +72,7 @@ esp-backtrace = { version = "0.19.0", optional = true, features = [
"panic-handler", "panic-handler",
"println", "println",
] } ] }
esp-println = { version = "0.17.0", optional = true, default-features = false, features = ["colors", "critical-section", "jtag-serial", "esp32c5", "log-04"] } esp-println = { version = "0.17.0", optional = true, features = ["esp32c5", "log-04"] }
esp-radio = { version = "0.18.0", optional = true, features = [ esp-radio = { version = "0.18.0", optional = true, features = [
"esp-alloc", "esp-alloc",
"esp32c5", "esp32c5",
@ -95,11 +83,9 @@ 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" } 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-hal-bus = { version = "0.3.0", optional = true }
embedded-graphics = { version = "0.8.2", optional = true } embedded-graphics = { version = "0.8.2", optional = true }
# needs newer GN with removed bitvec dependency c-its-parser = { version = "2.3.0", default-features = false, features = [
c-its-parser = { version = "2.4.0", default-features = false, features = [
"time", "time",
"geo", "geo",
"libm", "libm",
@ -111,8 +97,7 @@ geo-types = { version = "0.7.19", default-features = false }
mipidsi = { version = "0.10.0", optional = true } mipidsi = { version = "0.10.0", optional = true }
num-traits = { version = "0.2.19", default-features = false, features = ["libm"] } num-traits = { version = "0.2.19", default-features = false, features = ["libm"] }
profont = { version = "0.7.0", optional = true } profont = { version = "0.7.0", optional = true }
prost = { version = "0.14", optional = true, default-features = false, features = ["derive"] } prost = { version = "0.14", default-features = false, features = ["derive"] }
ublox = { version = "0.10", optional = true, default-features = false, features = ["alloc", "ubx_proto27"] }
[dev-dependencies] [dev-dependencies]
assert_float_eq = "1.2.0" assert_float_eq = "1.2.0"

View file

@ -5,16 +5,15 @@ Displays different C-ITS receive-only use cases for cheap.
## Hardware Requirements ## Hardware Requirements
The code is currently tailored to: The code is currently tailored to:
- Seeed XIAO ESP32-C5 (8MB PSRAM) - Seeed XIAO ESP32-C5 (8MB PSRAM)
- U-Blox M10Q GNSS (connected on GPIO12 (RX), GPIO11 (TX), and GPIO25 (reset)) - Seeed XIAO L67K (connected on GPIO12 (RX), GPIO11 (TX), GPIO1 (wakeup) and GPIO25 (reset))
or 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)
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)) - 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) only available with `screen` feature (which is included in `spat` feature)
## Features ## Features
### GNSS ### GNSS
Enable u-blox or L67K GNSS module and in main application logic/ state. Enable L67K GNSS module and in main application logic/ state.
### SPAT ### SPAT
Takes the current position and heading from the GNSS receiver and displays the signal phases of the upcoming intersection. Takes the current position and heading from the GNSS receiver and displays the signal phases of the upcoming intersection.
@ -29,32 +28,6 @@ Shall "publish" the most relevant information of a DENMs via BLE
### CAM ### CAM
Just prints a line for each received CAM. Just prints a line for each received CAM.
### CAM-TX
Generates and transmits a CAM when a GNSS update was received.
Depends on the `gnss` feature.
Note: In germany, using the 5875 to 5915 MHz frequency band is only permitted for C-ITS use cases which implement transmit power reduction measures with at least 30 dB reduction range (or equivalent measures) and have a maximum EIRP of 33 dBm.
This is based on EU regulation 2014/53/EU, so it applies to other EU countries in similar ways.
The relevant harmonized standard is ETSI EN 302 571 which mandates (excerpt of most relevant points):
- a maximum power spectral density of 23 dBm/Mhz EIRP
- transmit power control (TCP) in a range from 3 dBm up to the maximum output power of the equipment
* used for CEN DSRC and HDR DSRC interference mitigation by implementing measures from ETSI TS 102 792
+ Mode A or B: Output power less than 10 dBm EIRP and less than -65 or -45 dBm/MHz EIRP in CEN DSRC band -> **needs to be measured**
+ Mode C: Maximum transmission time of less than 1 ms and at least a few 100 ms between transmission (realistically) -> **can't be ensured with big messages**
+ Mode D: "any" realistically possible transmission time, but at least 500 ms between transmissions for ~5 ITS stations/ 1000ms for 10 ITS stations -> **safe to assume when we only send CAMs at 1 Hz**
+ But modes C and D also need to:
- use protected zone information from CAM messages
- use a protected zone database, e.g. from www.asecap-pzdb.com
* used for dynamic congestion control (DCC)
+ mandatory mechanism described in ETSI EN 302 571, chapter 4.2.10
This is **not** implemented in this application!
**So legally, you're not allowed to send with this.**
### UART
Enable UART I/O protocol according to [docs/uart-protocol.md](./docs/uart-protocol.md).
## Usage ## Usage
@ -79,21 +52,13 @@ rustup component add --toolchain nightly rustfmt
### Build and Flash ### Build and Flash
In default configuration only signal phases (SPAT) data is displayed for the upcoming intersection. In default configuration only signal phases (SPAT) data is displayed for the upcoming intersection.
Additional features are available through feature flags: Additional features are available though feature flags:
- `esp`: Run on ESP (required as feature flag to enable unit tests on host) - `esp`: Run on ESP (required as feature flag to enable unit tests on host)
- `std`: Run on system with `std` library - `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 - `denm`: Enable DENMs
- `cam`: Enable CAMs - `cam`: Enable CAMs
- `cam_tx`: Enable CAM generation and transmission (needs time and position source)
Officially supported feature combinations are:
- `esp,spat,gnss_ubx,screen` (default features, "standalone")
- `esp,spat,gnss_ubx,screen,cam_tx` ("standalone with CAM")
- `esp,spat,gnss_ubx,screen,cam_tx,cam,denm`
- `esp,uart,cam,denm` ("smartphone companion")
Connect your ESP32-C5 and run: Connect your ESP32-C5 and run:

View file

@ -35,17 +35,18 @@ enum GNAreaShape {
ETSI_AREASHAPE_CIRCLE = 0; ETSI_AREASHAPE_CIRCLE = 0;
ETSI_AREASHAPE_RECTANGLE = 1; ETSI_AREASHAPE_RECTANGLE = 1;
ETSI_AREASHAPE_ELLIPSIS = 2; ETSI_AREASHAPE_ELLIPSIS = 2;
ETSI_AREASHAPE_UNKNOWN = 15;
} }
// GeoNetworking Destination Area/ Source Position // GeoNetworking Destination Area/ Source Position
message ITSPosition { message ITSPosition {
required int32 latitude = 1; // Latitude in 1/10th of micro degrees optional int32 latitude = 1; // Latitude in 1/10th of micro degrees
required int32 longitude = 2; // Longitude in 1/10th of micro degrees optional int32 longitude = 2; // Longitude in 1/10th of micro degrees
} }
// GeoNetworking Destination Area // GeoNetworking Destination Area
message GNArea { message GNArea {
optional GNAreaShape area = 1; optional GNAreaShape area = 1 [default = ETSI_AREASHAPE_UNKNOWN];
optional ITSPosition position = 2; optional ITSPosition position = 2;
optional int32 distA = 3; // Distance A of the geometric shape in meters 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 distB = 4; // Distance B of the geometric shape in meters, mandatory for rectangle and ellipsis
@ -54,6 +55,8 @@ message GNArea {
// GeoNetworking Transport // GeoNetworking Transport
enum GNTransport { enum GNTransport {
GN_TRANSPORT_DEFAULT = 0;
GN_TRANSPORT_GUC = 2; // GeoUnicast
GN_TRANSPORT_GAC = 3; // Geographically-Scoped Anycast GN_TRANSPORT_GAC = 3; // Geographically-Scoped Anycast
GN_TRANSPORT_GBC = 4; // Geographically-Scoped broadcast GN_TRANSPORT_GBC = 4; // Geographically-Scoped broadcast
GN_TRANSPORT_TSB = 5; // Topologically-scoped broadcast GN_TRANSPORT_TSB = 5; // Topologically-scoped broadcast
@ -67,8 +70,6 @@ message RawMsgTx {
required GNTransport gnTransport = 3; required GNTransport gnTransport = 3;
required GNArea gnArea = 4; required GNArea gnArea = 4;
required uint32 hopLimit = 5; required uint32 hopLimit = 5;
optional uint32 stationType = 6; // for source position vector
} }
// ------------------------------ // ------------------------------
@ -76,8 +77,7 @@ message RawMsgTx {
// ------------------------------ // ------------------------------
message PositionState { message PositionState {
required sfixed64 timestamp_ms = 1; // current UNIX time in milliseconds required Position position = 1;
required Position position = 2;
optional uint32 heading = 3; // heading in 1/10 deg optional uint32 heading = 3; // heading in 1/10 deg
optional uint32 speed = 4; // speed in 1/100 m/s optional uint32 speed = 4; // speed in 1/100 m/s
} }

View file

@ -1,70 +0,0 @@
#!/bin/bash
## Build/ Test/ Clippy all supported feature permutations
set -eu
mode=${1:-help}
wasm_target="--chrome --headless"
extended=${2:-false}
features_base=(
uart,cam,denm
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.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because it is too large Load diff

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ use alloc::rc::Rc;
use alloc::vec::Vec; use alloc::vec::Vec;
use log::info; use log::info;
#[cfg(feature = "spat")] #[cfg(any(feature = "gnss", feature = "spat"))]
use log::warn; use log::warn;
#[cfg(feature = "spat")] #[cfg(feature = "spat")]
use log::{debug, error}; use log::{debug, error};
@ -18,8 +18,6 @@ pub mod v2x;
#[cfg(feature = "cam")] #[cfg(feature = "cam")]
pub mod cam; pub mod cam;
#[cfg(any(feature = "cam_tx", test))]
pub mod cam_tx;
#[cfg(feature = "denm")] #[cfg(feature = "denm")]
pub mod denm; pub mod denm;
#[cfg(feature = "spat")] #[cfg(feature = "spat")]
@ -49,7 +47,7 @@ pub struct State {
time: chrono::NaiveDateTime, time: chrono::NaiveDateTime,
#[allow(unused)] #[allow(unused)]
pos: esp32_cits_core::PosVel, pos: PositionState,
#[cfg(feature = "spat")] #[cfg(feature = "spat")]
last_prune: chrono::NaiveDateTime, last_prune: chrono::NaiveDateTime,
@ -130,7 +128,15 @@ pub struct GnssFix {
pub speed_mps: Option<f32>, pub speed_mps: Option<f32>,
} }
impl From<&GnssFix> for esp32_cits_core::PosVel { #[allow(unused)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct PositionState {
pub position: geo_types::Point,
pub heading_deg: Option<f32>,
pub speed_mps: Option<f32>,
}
impl From<&GnssFix> for PositionState {
fn from(value: &GnssFix) -> Self { fn from(value: &GnssFix) -> Self {
let position = geo_types::Point::new(value.longitude_deg, value.latitude_deg); let position = geo_types::Point::new(value.longitude_deg, value.latitude_deg);
@ -141,7 +147,7 @@ impl From<&GnssFix> for esp32_cits_core::PosVel {
} }
} }
} }
impl From<GnssFix> for esp32_cits_core::PosVel { impl From<GnssFix> for PositionState {
fn from(value: GnssFix) -> Self { fn from(value: GnssFix) -> Self {
(&value).into() (&value).into()
} }
@ -204,7 +210,7 @@ impl State {
Self { Self {
initialized: false, initialized: false,
time: chrono::NaiveDateTime::default(), time: chrono::NaiveDateTime::default(),
pos: esp32_cits_core::PosVel::default(), pos: PositionState::default(),
#[cfg(feature = "spat")] #[cfg(feature = "spat")]
last_prune: chrono::NaiveDateTime::default(), last_prune: chrono::NaiveDateTime::default(),
#[cfg(feature = "spat")] #[cfg(feature = "spat")]
@ -631,11 +637,6 @@ impl State {
pub fn time(&self) -> chrono::prelude::NaiveDateTime { pub fn time(&self) -> chrono::prelude::NaiveDateTime {
self.time self.time
} }
#[allow(unused)]
pub fn pos(&self) -> &esp32_cits_core::PosVel {
&self.pos
}
} }
#[cfg(test)] #[cfg(test)]
@ -687,13 +688,13 @@ mod tests {
let own_pos_passed = geo_types::Point::new(9.977217, 53.5560602); let own_pos_passed = geo_types::Point::new(9.977217, 53.5560602);
let own_heading = 90.; let own_heading = 90.;
state.pos.heading_deg = Some(own_heading); state.heading_deg = Some(own_heading);
state.pos.position = own_pos_infront; state.position = own_pos_infront;
state.run_glosa(); state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Idle)); assert!(matches!(state.glosa_state, GlosaState::Idle));
state.pos.position = own_pos_inside; state.position = own_pos_inside;
state.run_glosa(); state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Locked(_))); assert!(matches!(state.glosa_state, GlosaState::Locked(_)));
if let GlosaState::Locked(info) = &state.glosa_state { if let GlosaState::Locked(info) = &state.glosa_state {
@ -701,7 +702,7 @@ mod tests {
assert!(info.lane_id == 1 || info.lane_id == 2); assert!(info.lane_id == 1 || info.lane_id == 2);
} }
state.pos.position = own_pos_inside; state.position = own_pos_inside;
state.run_glosa(); state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Locked(_))); assert!(matches!(state.glosa_state, GlosaState::Locked(_)));
if let GlosaState::Locked(info) = &state.glosa_state { if let GlosaState::Locked(info) = &state.glosa_state {
@ -709,7 +710,7 @@ mod tests {
assert!(info.lane_id == 1 || info.lane_id == 2); assert!(info.lane_id == 1 || info.lane_id == 2);
} }
state.pos.position = own_pos_passed; state.position = own_pos_passed;
state.run_glosa(); state.run_glosa();
assert!(matches!(state.glosa_state, GlosaState::Idle)); assert!(matches!(state.glosa_state, GlosaState::Idle));
} }

View file

@ -138,6 +138,8 @@ impl Intersection {
self.signal_groups = Self::collect_signal_groups(&self.lanes); self.signal_groups = Self::collect_signal_groups(&self.lanes);
self.center = Self::calculate_center(&self.ref_point, &self.lanes); self.center = Self::calculate_center(&self.ref_point, &self.lanes);
self.is_complete = true; self.is_complete = true;
// TODO: de-duplicate lanes!
} }
} }

466
src/io.rs
View file

@ -5,12 +5,6 @@ pub mod msg {
include!(concat!(env!("OUT_DIR"), "/c_its_io.rs")); 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 { impl From<msg::RawMsgRx> for msg::SerialOutputMsg {
fn from(value: msg::RawMsgRx) -> Self { fn from(value: msg::RawMsgRx) -> Self {
use msg::serial_output_msg::Payload; use msg::serial_output_msg::Payload;
@ -31,145 +25,7 @@ impl From<msg::DenmEvent> for msg::SerialOutputMsg {
} }
} }
impl msg::RawMsgRx { impl From<msg::PositionState> for crate::applogic::PositionState {
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 { fn from(value: msg::PositionState) -> Self {
let position = geo_types::Point::new( let position = geo_types::Point::new(
value.position.longitude_deg.into(), value.position.longitude_deg.into(),
@ -193,323 +49,3 @@ impl From<msg::PositionState> for esp32_cits_core::PosVel {
} }
} }
} }
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,18 +7,6 @@
)] )]
// #![deny(clippy::large_stack_frames)] // #![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::string::ToString as _;
use alloc::vec::Vec; use alloc::vec::Vec;
use core::cell::RefCell; use core::cell::RefCell;
@ -26,19 +14,17 @@ use core::cell::RefCell;
use core::cell::UnsafeCell; use core::cell::UnsafeCell;
use c_its_parser::gn as geonetworking; use c_its_parser::gn as geonetworking;
#[cfg(feature = "cam_tx")]
use c_its_parser::standards::extensions::ItsStationType;
use critical_section::Mutex; use critical_section::Mutex;
use crossbeam_queue::ArrayQueue; use crossbeam_queue::ArrayQueue;
use embassy_executor::Spawner; use embassy_executor::Spawner;
#[cfg(feature = "gnss_l67k")] #[cfg(feature = "gnss")]
use embassy_gps::gps::l76k; use embassy_gps::gps::l76k;
use esp_backtrace as _; use esp_backtrace as _;
use esp_hal::clock::CpuClock; use esp_hal::clock::CpuClock;
use esp_hal::timer::timg::TimerGroup; use esp_hal::timer::timg::TimerGroup;
#[cfg(all(target_arch = "riscv32", feature = "spat"))] #[cfg(all(target_arch = "riscv32", feature = "spat"))]
use esp_println::println; use esp_println::println;
use esp32_cits_core::radio; use esp_radio::wifi;
use geonetworking::Decode as _; use geonetworking::Decode as _;
use log::{error, info, warn}; use log::{error, info, warn};
@ -47,40 +33,22 @@ extern crate alloc;
mod applogic; mod applogic;
mod cache; mod cache;
mod geo_alg; mod geo_alg;
#[cfg(feature = "uart")]
mod io; mod io;
mod radio;
#[cfg(feature = "screen")] #[cfg(feature = "screen")]
mod screen; mod screen;
#[cfg(feature = "gnss_ubx")]
mod ubx;
const WIFI_CHANNEL: radio::Channel = 180; const WIFI_CHANNEL: radio::Channel = 180;
const WIFI_TXPOWER: i8 = 10; // dBm, 2..20
#[cfg(feature = "spat")] #[cfg(feature = "spat")]
const SPAT_RATE_LIMIT: u8 = 5; // keep every n-th message 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. // 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> // 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!(); esp_bootloader_esp_idf::esp_app_desc!();
#[cfg(feature = "_gnss")] #[cfg(feature = "gnss")]
static GNSS_UPDATE: Mutex<RefCell<Option<applogic::GnssFix>>> = Mutex::new(RefCell::new(None)); 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 WIFI_RX_QUEUE: Mutex<RefCell<Option<ArrayQueue<Vec<u8>>>>> = Mutex::new(RefCell::new(None));
#[cfg(feature = "_uart")]
static UART_RX_BUF: Mutex<RefCell<Option<Vec<u8>>>> = Mutex::new(RefCell::new(None));
#[cfg(feature = "_uart")]
static SERIAL: Mutex<RefCell<Option<esp_hal::uart::Uart<'static, esp_hal::Blocking>>>> =
Mutex::new(RefCell::new(None));
#[allow( #[allow(
clippy::large_stack_frames, clippy::large_stack_frames,
@ -149,38 +117,21 @@ async fn main(spawner: Spawner) -> ! {
screen screen
}; };
// Setup UART I/O channel
#[cfg(feature = "uart")]
setup_uart0(
115_200,
Some(b'\n'),
peripherals.GPIO12,
peripherals.GPIO11,
peripherals.UART0,
serial_handler,
)
.expect("Failed to initialize UART");
// Setup WiFi // Setup WiFi
// sadly, it doesn't work any more when we move `esp_radio::wifi::new` to `setup_wifi_sniffer` // 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( let (mut wifi_controller, mut interfaces) = esp_radio::wifi::new(
peripherals.WIFI, peripherals.WIFI,
esp_radio::wifi::ControllerConfig::default(), esp_radio::wifi::ControllerConfig::default(),
) )
.expect("Failed to initialize Wi-Fi controller"); .expect("Failed to initialize Wi-Fi controller");
if let Err(err) = wifi_controller.set_band_mode(esp_radio::wifi::BandMode::_5G) { if let Err(err) = wifi_controller.set_band_mode(esp_radio::wifi::BandMode::_5G) {
error!("Failed to set WiFi to 5GHz only: {err}"); error!("Failed to set WiFi to 5GHz only: {err}");
} }
if let Err(err) = wifi_controller.set_max_tx_power(WIFI_TXPOWER * 4) {
error!("Failed to set WiFi TX power: {err}");
}
critical_section::with(|cs| { critical_section::with(|cs| {
WIFI_RX_QUEUE.borrow(cs).replace(Some(ArrayQueue::new(2))); WIFI_RX_QUEUE.borrow(cs).replace(Some(ArrayQueue::new(2)));
}); });
let mut wlan_iface = interfaces.sniffer; radio::setup_wifi_sniffer(WIFI_CHANNEL, &mut interfaces.sniffer, handle_frame)
radio::setup_wifi_sniffer(WIFI_CHANNEL, &mut wlan_iface, handle_frame)
.expect("Fatal error initializing 802.11p sniffer"); .expect("Fatal error initializing 802.11p sniffer");
info!("WiFi promiscuous mode on channel {WIFI_CHANNEL} running"); info!("WiFi promiscuous mode on channel {WIFI_CHANNEL} running");
@ -188,15 +139,14 @@ async fn main(spawner: Spawner) -> ! {
let mac = esp_hal::efuse::base_mac_address(); let mac = esp_hal::efuse::base_mac_address();
info!("WiFi MAC: {mac}"); info!("WiFi MAC: {mac}");
// Setup GNSS: // Setup GNSS: Seeed XIAO L67K:
// Seeed XIAO L67K:
// RX: D7/ GPIO12 // RX: D7/ GPIO12
// TX: D6/ GPIO11 // TX: D6/ GPIO11
// WAKEUP: D0/ GPIO1 -> HIGH for active, LOW for Sleep // WAKEUP: D0/ GPIO1 -> HIGH for active, LOW for Sleep
// RESET: D2/ GPIO25 -> HIGH for normal, LOW for Reset // RESET: D2/ GPIO25 -> HIGH for normal, LOW for Reset
#[cfg(feature = "gnss_l67k")] #[cfg(feature = "gnss")]
spawner.spawn( spawner.spawn(
gnss_l67k_task( gnss_task(
peripherals.GPIO1, peripherals.GPIO1,
peripherals.GPIO25, peripherals.GPIO25,
peripherals.UART0, peripherals.UART0,
@ -205,167 +155,14 @@ async fn main(spawner: Spawner) -> ! {
) )
.expect("Failed to spawn GNSS task"), .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..."); 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(); 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 { loop {
embassy_time::Timer::after(embassy_time::Duration::from_millis(1)).await; embassy_time::Timer::after(embassy_time::Duration::from_millis(1)).await;
#[cfg(feature = "_uart")] #[cfg(feature = "gnss")]
{
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; let mut new_position = false;
critical_section::with(|cs| { critical_section::with(|cs| {
@ -374,33 +171,6 @@ async fn main(spawner: Spawner) -> ! {
if let Some(fix) = gnss_update_ref.replace(None) { if let Some(fix) = gnss_update_ref.replace(None) {
state.update_with_gpsfix(&fix); state.update_with_gpsfix(&fix);
new_position = true; 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}"),
}
}
} }
}); });
@ -466,15 +236,12 @@ async fn main(spawner: Spawner) -> ! {
} }
} }
#[cfg(any(feature = "cam", feature = "cam_tx"))] #[cfg(feature = "cam")]
ItsMessage::Cam { ItsMessage::Cam {
geonetworking: _, geonetworking: _,
transport: _, transport: _,
etsi, etsi,
} => { } => applogic::cam::handle_cam(&etsi),
#[cfg(feature = "cam")]
applogic::cam::handle_cam(&etsi);
}
#[cfg(feature = "denm")] #[cfg(feature = "denm")]
ItsMessage::DenmV2 { ItsMessage::DenmV2 {
@ -510,7 +277,7 @@ async fn main(spawner: Spawner) -> ! {
reason = "adhering to callback interface" reason = "adhering to callback interface"
)] )]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn handle_frame(frame: esp_radio::wifi::sniffer::PromiscuousPkt<'_>) { fn handle_frame(frame: wifi::sniffer::PromiscuousPkt<'_>) {
#[cfg(feature = "spat")] #[cfg(feature = "spat")]
const PRUNE_INTERVAL: chrono::Duration = chrono::Duration::seconds(60); const PRUNE_INTERVAL: chrono::Duration = chrono::Duration::seconds(60);
#[cfg(feature = "spat")] #[cfg(feature = "spat")]
@ -531,18 +298,27 @@ fn handle_frame(frame: esp_radio::wifi::sniffer::PromiscuousPkt<'_>) {
.naive_utc() .naive_utc()
}; };
match esp32_cits_core::rx::is_broadcast_frame(&frame) { // Ignore frames with errors and non-data frames
Err(err) => { if frame.rx_cntl.rx_state != 0 {
warn!("{err}"); warn!("Received frame has RX error: {}", frame.rx_cntl.rx_state);
return; return;
} }
Ok(false) => { if !radio::PromiscuousPktType::Data.matches(frame.frame_type) {
// drop frame
return; return;
} }
Ok(true) => {
// continue // 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;
} }
// info!("Received frame with {} bytes", frame.len); // info!("Received frame with {} bytes", frame.len);
@ -610,18 +386,6 @@ fn handle_frame(frame: esp_radio::wifi::sniffer::PromiscuousPkt<'_>) {
error!("V2X RX queue is full"); 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);
}
} }
} }
} }
@ -689,10 +453,10 @@ fn get_its_header<'p>(
} }
} }
#[cfg(feature = "gnss_l67k")] #[cfg(feature = "gnss")]
#[allow(clippy::large_stack_frames, reason = "GNSS driver needs some space")] #[allow(clippy::large_stack_frames, reason = "GNSS driver needs some space")]
#[embassy_executor::task] #[embassy_executor::task]
async fn gnss_l67k_task( async fn gnss_task(
gps_wakeup: esp_hal::peripherals::GPIO1<'static>, gps_wakeup: esp_hal::peripherals::GPIO1<'static>,
gps_reset: esp_hal::peripherals::GPIO25<'static>, gps_reset: esp_hal::peripherals::GPIO25<'static>,
uart: esp_hal::peripherals::UART0<'static>, uart: esp_hal::peripherals::UART0<'static>,
@ -742,38 +506,6 @@ async fn gnss_l67k_task(
} }
} }
#[cfg(feature = "gnss_ubx")]
#[embassy_executor::task]
async fn gnss_ubx_config(reset_pin: esp_hal::peripherals::GPIO25<'static>) {
// reset the module
let mut gnss_reset = esp_hal::gpio::Output::new(
reset_pin,
esp_hal::gpio::Level::Low,
esp_hal::gpio::OutputConfig::default(),
);
embassy_time::Timer::after(embassy_time::Duration::from_millis(2)).await;
gnss_reset.set_high();
embassy_time::Timer::after(embassy_time::Duration::from_secs(1)).await;
info!("UBX: Enabling ubx protocol with NAV-PVT and NAV-SAT");
let buffer = ublox::packets::cfg_val::CfgValSetBuilder {
version: 1,
layers: ublox::packets::cfg_val::CfgLayerSet::RAM,
reserved1: 0,
cfg_data: &[
ublox::cfg_val::CfgVal::Uart1OutProtNmea(false),
ublox::cfg_val::CfgVal::Uart1OutProtUbx(true),
// CFG-MSGOUT-UBX_NAV_PVT_UART1
ublox::cfg_val::CfgVal::MsgOutUbxNavPvtUart1(1),
// CFG-MSGOUT-UBX_NAV_SAT_UART1
// ublox::cfg_val::CfgVal::MsgOutUbxNavSatUart1(10),
],
}
.into_packet_vec();
send_uart0(&buffer);
}
#[cfg(feature = "screen")] #[cfg(feature = "screen")]
fn log_screen_error<E>(error: &E) fn log_screen_error<E>(error: &E)
where where
@ -781,111 +513,3 @@ where
{ {
error!("graphics failed: {error:?}"); 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)
}

288
src/radio.rs Normal file
View file

@ -0,0 +1,288 @@
//! IEEE 802.11p C-ITS Capture with ESP32-C5
#![allow(unused)]
// -----------------------------
// 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 esp_radio::wifi::sniffer::Sniffer,
callback: fn(esp_radio::wifi::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 esp_radio::wifi::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

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

View file

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

View file

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