Refactor to break out scheduler
This commit is contained in:
475
crates/adsb/Cargo.lock
generated
Normal file
475
crates/adsb/Cargo.lock
generated
Normal file
@@ -0,0 +1,475 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adsb"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"ctrlc",
|
||||
"env_logger",
|
||||
"log",
|
||||
"rusb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libusb1-sys"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rusb"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libusb1-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
11
crates/adsb/Cargo.toml
Normal file
11
crates/adsb/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "adsb"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rusb = "0.9.4"
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
log = "0.4.27"
|
||||
env_logger = "0.11.8"
|
||||
ctrlc = "3.4.6"
|
||||
10
crates/adsb/README.md
Normal file
10
crates/adsb/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# ADSB
|
||||
Debug using `export LIBUSB_DEBUG=4`
|
||||
|
||||
`lsusb -v -d 0bda:2832`
|
||||
|
||||
## Simulation Mode
|
||||
`cargo run -- --connect`
|
||||
|
||||
## Decode
|
||||
`cargo run -- --decode 8D4840D6202CC371C32CE0576098`
|
||||
3
crates/adsb/rust-toolchain.toml
Normal file
3
crates/adsb/rust-toolchain.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
components = ["rustfmt", "clippy"]
|
||||
3
crates/adsb/rustfmt.toml
Normal file
3
crates/adsb/rustfmt.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
indent_style = "Block"
|
||||
reorder_imports = false
|
||||
tab_spaces = 2
|
||||
62
crates/adsb/src/constants.rs
Normal file
62
crates/adsb/src/constants.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use std::fmt::Display;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceInfo {
|
||||
/// Vendor ID
|
||||
pub vid: u16,
|
||||
/// Product ID
|
||||
pub pid: u16,
|
||||
}
|
||||
|
||||
impl Display for DeviceInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "VID: 0x{:04X} PID: 0x{:04X}", self.vid, self.pid)
|
||||
}
|
||||
}
|
||||
|
||||
// Devices
|
||||
pub const DEVICE_RTL2832U: DeviceInfo = DeviceInfo {
|
||||
vid: 0x0BDA,
|
||||
pid: 0x2832,
|
||||
};
|
||||
|
||||
pub const TIMEOUT: Duration = Duration::from_secs(1);
|
||||
pub const FIR_LENGTH: usize = 16;
|
||||
|
||||
// Request Types
|
||||
pub const REQ_CTRL_OUT: u8 =
|
||||
rusb::constants::LIBUSB_ENDPOINT_OUT | rusb::constants::LIBUSB_REQUEST_TYPE_VENDOR;
|
||||
|
||||
// Blocks
|
||||
pub const BLOCK_DEMOD: u16 = 0;
|
||||
pub const BLOCK_USB: u16 = 1;
|
||||
pub const BLOCK_SYS: u16 = 2;
|
||||
pub const BLOCK_TUN: u16 = 3;
|
||||
pub const BLOCK_ROM: u16 = 4;
|
||||
pub const BLOCK_IRB: u16 = 5;
|
||||
pub const BLOCK_IIC: u16 = 6;
|
||||
|
||||
// Registers
|
||||
pub const DEMOD_CTL: u16 = 0x3000;
|
||||
pub const DEMOD_CTL_1: u16 = 0x300b;
|
||||
|
||||
// USB
|
||||
pub const USB_EPA_CTL: u16 = 0x2148;
|
||||
pub const USB_SYSCTL: u16 = 0x2000;
|
||||
pub const USB_EPA_MAXPKT: u16 = 0x2158;
|
||||
|
||||
/// ADS-B downlink frequency (1090 MHz)
|
||||
pub const ADSB_FREQUENCY_HZ: u32 = 1_090_000_000;
|
||||
|
||||
/// RTL-SDR sample rate in samples/second.
|
||||
pub const SAMPLE_RATE_HZ: u32 = 2_048_000;
|
||||
|
||||
pub const DEFAULT_FIR: &'static [i32; FIR_LENGTH] = &[
|
||||
-54, -36, -41, -40, -32, -14, 14, 53, 101, 156, 215, 273, 327, 372, 404, 421,
|
||||
];
|
||||
// pub const DEFAULT_BUFFER_LENGTH: usize = 4096;
|
||||
pub const DEFAULT_BUFFER_LENGTH: usize = 64;
|
||||
pub const DEFAULT_RTL_XTAL_FREQ: u32 = 28_800_000;
|
||||
pub const MIN_RTL_XTAL_FREQ: u32 = DEFAULT_RTL_XTAL_FREQ - 1000;
|
||||
pub const MAX_RTL_XTAL_FREQ: u32 = DEFAULT_RTL_XTAL_FREQ + 1000;
|
||||
550
crates/adsb/src/device.rs
Normal file
550
crates/adsb/src/device.rs
Normal file
@@ -0,0 +1,550 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Display;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use rusb::{
|
||||
Context, Device, DeviceDescriptor, DeviceHandle, DeviceList, Direction, TransferType, UsbContext,
|
||||
};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::constants::{
|
||||
ADSB_FREQUENCY_HZ, BLOCK_SYS, BLOCK_USB, DEFAULT_BUFFER_LENGTH, DEFAULT_FIR,
|
||||
DEFAULT_RTL_XTAL_FREQ, DEMOD_CTL, DEMOD_CTL_1, REQ_CTRL_OUT, SAMPLE_RATE_HZ, TIMEOUT,
|
||||
USB_EPA_CTL, USB_EPA_MAXPKT, USB_SYSCTL,
|
||||
};
|
||||
|
||||
/// rusb/libusb implementation of `RtlSdrDevice`
|
||||
pub struct RtlSdrDevice {
|
||||
/// Device handle
|
||||
handle: DeviceHandle<Context>,
|
||||
endpoint: Endpoint,
|
||||
frequency: u32,
|
||||
rate: u32,
|
||||
}
|
||||
|
||||
impl RtlSdrDevice {
|
||||
/// Display RTL SDR information
|
||||
pub fn info(vid: u16, pid: u16) {
|
||||
let device_list = match DeviceList::new() {
|
||||
Ok(d) => d,
|
||||
Err(err) => {
|
||||
eprintln!("Unable to get device list: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
for device in device_list.iter() {
|
||||
match device.device_descriptor() {
|
||||
Ok(device_desc) => {
|
||||
if vid != device_desc.vendor_id() || pid != device_desc.product_id() {
|
||||
continue;
|
||||
}
|
||||
println!(
|
||||
"Bus: {:03}, Device: {:03} VID: 0x{:04X}, PID: 0x{:04X}",
|
||||
device.bus_number(),
|
||||
device.address(),
|
||||
device_desc.vendor_id(),
|
||||
device_desc.product_id()
|
||||
);
|
||||
|
||||
match device.open() {
|
||||
Ok(handle) => {
|
||||
println!("{}", device_info(&handle, &device_desc, " ", true));
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(" Unable to open device: {:?}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Unable to get device descriptor: {:?}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the RTL SDR device and return a wrapper
|
||||
pub fn open(vid: u16, pid: u16) -> Result<Self> {
|
||||
// Create a new libusb context
|
||||
let ctx = Context::new().map_err(|_| Error::new("Unable to create libusb context"))?;
|
||||
|
||||
for device in ctx.devices()?.iter() {
|
||||
let device_desc = match device.device_descriptor() {
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if device_desc.vendor_id() == vid && device_desc.product_id() == pid {
|
||||
let handle = device.open()?;
|
||||
|
||||
log::debug!("{}", device_info(&handle, &device_desc, "", false));
|
||||
|
||||
// Find the endpoint
|
||||
let endpoint = match Endpoint::find(&device_desc, &device, TransferType::Bulk) {
|
||||
Some(e) => e,
|
||||
None => return Err(Error::new("Unable to find endpoint on device")),
|
||||
};
|
||||
log::debug!("Found readable endpoint: {}", endpoint.to_string());
|
||||
|
||||
let mut sdr = Self::new(handle, endpoint);
|
||||
|
||||
sdr.initialize()?;
|
||||
|
||||
return Ok(sdr);
|
||||
}
|
||||
}
|
||||
Err(Error::new("No valid device found"))
|
||||
}
|
||||
|
||||
/// Close the RTL SDR device
|
||||
pub fn close(&self) -> Result<()> {
|
||||
log::debug!("Closing device...");
|
||||
self.attach_kernel_driver(self.endpoint.interface);
|
||||
self.handle.release_interface(self.endpoint.interface)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process the USB data
|
||||
pub fn process(&mut self, running: Arc<AtomicBool>) -> Result<()> {
|
||||
log::debug!(
|
||||
"Reading from active configuration: {}",
|
||||
self.handle.active_configuration()?
|
||||
);
|
||||
|
||||
// Read endpoint
|
||||
let mut buffer = [0u8; DEFAULT_BUFFER_LENGTH];
|
||||
while running.load(Ordering::SeqCst) {
|
||||
let s = self.read(&mut buffer)?;
|
||||
log::debug!("Read: {}", s);
|
||||
}
|
||||
|
||||
self.close()
|
||||
}
|
||||
|
||||
fn new(handle: DeviceHandle<Context>, endpoint: Endpoint) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
endpoint,
|
||||
frequency: 0,
|
||||
rate: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self, buffer: &mut [u8; DEFAULT_BUFFER_LENGTH]) -> Result<String> {
|
||||
let length = match self.endpoint.transfer_type {
|
||||
TransferType::Interrupt => self
|
||||
.handle
|
||||
.read_interrupt(self.endpoint.address, buffer, TIMEOUT)
|
||||
.map_err(|err| Error::new(format!("Unable to read interrupt from endpoint: {:?}", err)))?,
|
||||
TransferType::Bulk => self
|
||||
.handle
|
||||
.read_bulk(self.endpoint.address, buffer, TIMEOUT)
|
||||
.map_err(|err| Error::new(format!("Unable to read bulk from endpoint: {:?}", err)))?,
|
||||
_ => 0,
|
||||
};
|
||||
log::trace!("Received {} bytes", length);
|
||||
let s = match String::from_utf8_lossy(&buffer[..length]) {
|
||||
Cow::Borrowed(s) => s.to_string(),
|
||||
Cow::Owned(s) => s,
|
||||
};
|
||||
Ok(s.to_string())
|
||||
}
|
||||
|
||||
fn initialize(&mut self) -> Result<()> {
|
||||
// Configure the device for the endpoint
|
||||
self.set_active_configuration(self.endpoint.config)?;
|
||||
self.claim_interface(self.endpoint.interface)?;
|
||||
self.set_alternate_setting(self.endpoint.interface, self.endpoint.setting)?;
|
||||
self.detach_kernel_driver(self.endpoint.interface);
|
||||
|
||||
self.test_write()?;
|
||||
|
||||
self.initialize_baseband()?;
|
||||
|
||||
self.set_i2c_repeater(true)?;
|
||||
|
||||
// Reset the internal USB buffer
|
||||
self.reset_buffer()?;
|
||||
|
||||
// Set the center-frequency in Hz
|
||||
self.set_center_frequency(ADSB_FREQUENCY_HZ)?;
|
||||
|
||||
// Set the sample rate
|
||||
self.set_sample_rate(SAMPLE_RATE_HZ)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_active_configuration(&self, configuration: u8) -> Result<()> {
|
||||
self
|
||||
.handle
|
||||
.set_active_configuration(configuration)
|
||||
.map_err(|err| Error::new(format!("Failed to set active configuration: {:?}", err)))
|
||||
}
|
||||
|
||||
fn claim_interface(&self, interface: u8) -> Result<()> {
|
||||
self
|
||||
.handle
|
||||
.claim_interface(interface)
|
||||
.map_err(|err| Error::new(format!("Failed to claim interface: {:?}", err)))
|
||||
}
|
||||
|
||||
fn set_alternate_setting(&self, interface: u8, setting: u8) -> Result<()> {
|
||||
self
|
||||
.handle
|
||||
.set_alternate_setting(interface, setting)
|
||||
.map_err(|err| Error::new(format!("Failed to set alternate setting: {:?}", err)))
|
||||
}
|
||||
|
||||
/// Attempt to write a test message, and reset the device on a failure
|
||||
fn test_write(&self) -> Result<()> {
|
||||
log::trace!("Testing write to device...");
|
||||
let length = ctrl_write_register(&self.handle, BLOCK_USB, USB_SYSCTL, 0x89, 1)?;
|
||||
if length == 0 {
|
||||
log::info!("Resetting device");
|
||||
self
|
||||
.handle
|
||||
.reset()
|
||||
.map_err(|err| Error::new(format!("Failed to reset device: {:?}", err)))?;
|
||||
} else {
|
||||
log::trace!("Test write was successful");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reset_buffer(&self) -> Result<()> {
|
||||
log::trace!("Resetting buffer...");
|
||||
ctrl_write_register(&self.handle, BLOCK_USB, USB_EPA_CTL, 0x1002, 2)
|
||||
.map_err(|err| Error::new(format!("Failed to reset the internal buffer: {:?}", err)))?;
|
||||
ctrl_write_register(&self.handle, BLOCK_USB, USB_EPA_CTL, 0x0000, 2)
|
||||
.map_err(|err| Error::new(format!("Failed to reset the internal buffer: {:?}", err)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reset_demod(&self) -> Result<()> {
|
||||
log::trace!("Resetting demod...");
|
||||
demod_ctrl_write_register(&self.handle, 1, 0x01, 0x14, 1)
|
||||
.map_err(|err| Error::new(format!("Failed to reset the internal demod: {:?}", err)))?;
|
||||
demod_ctrl_write_register(&self.handle, 1, 0x01, 0x10, 1)
|
||||
.map_err(|err| Error::new(format!("Failed to reset the internal demod: {:?}", err)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn initialize_baseband(&self) -> Result<()> {
|
||||
// Initialize the USB
|
||||
ctrl_write_register(&self.handle, BLOCK_USB, USB_SYSCTL, 0x09, 1)?;
|
||||
ctrl_write_register(&self.handle, BLOCK_USB, USB_EPA_MAXPKT, 0x0002, 2)?;
|
||||
ctrl_write_register(&self.handle, BLOCK_USB, USB_EPA_CTL, 0x1002, 2)?;
|
||||
|
||||
// Power on demod
|
||||
ctrl_write_register(&self.handle, BLOCK_SYS, DEMOD_CTL_1, 0x22, 1)?;
|
||||
ctrl_write_register(&self.handle, BLOCK_SYS, DEMOD_CTL, 0xe8, 1)?;
|
||||
|
||||
// Reset demod
|
||||
self.reset_demod()?;
|
||||
|
||||
// Disable spectrum inversion and adjust channel rejection
|
||||
ctrl_write_register(&self.handle, 1, 0x15, 0x00, 1)?;
|
||||
ctrl_write_register(&self.handle, 1, 0x16, 0x00, 2)?;
|
||||
|
||||
// Clear DDC shift and IF registers
|
||||
for i in 0..5 {
|
||||
demod_ctrl_write_register(&self.handle, 1, 0x16 + i, 0x00, 1)?;
|
||||
}
|
||||
self.set_fir(DEFAULT_FIR)?;
|
||||
|
||||
// info!("Enable SDR mode, disable DAGC (bit 5)");
|
||||
demod_ctrl_write_register(&self.handle, 0, 0x19, 0x05, 1)?;
|
||||
|
||||
// info!("Init FSM state-holding register");
|
||||
demod_ctrl_write_register(&self.handle, 1, 0x93, 0xf0, 1)?;
|
||||
demod_ctrl_write_register(&self.handle, 1, 0x94, 0x0f, 1)?;
|
||||
|
||||
// Disable AGC (en_dagc, bit 0) (seems to have no effect)
|
||||
demod_ctrl_write_register(&self.handle, 1, 0x11, 0x00, 1)?;
|
||||
|
||||
// Disable RF and IF AGC loop
|
||||
demod_ctrl_write_register(&self.handle, 1, 0x04, 0x00, 1)?;
|
||||
|
||||
// Disable PID filter
|
||||
demod_ctrl_write_register(&self.handle, 0, 0x61, 0x60, 1)?;
|
||||
|
||||
// opt_adc_iq = 0, default ADC_I/ADC_Q datapath
|
||||
demod_ctrl_write_register(&self.handle, 0, 0x06, 0x80, 1)?;
|
||||
|
||||
// Enable Zero-IF mode, DC cancellation, and IQ estimation/compensation
|
||||
demod_ctrl_write_register(&self.handle, 1, 0xb1, 0x1b, 1)?;
|
||||
|
||||
// Disable 4.096 MHz clock output on pin TP_CK0
|
||||
demod_ctrl_write_register(&self.handle, 0, 0x0d, 0x83, 1)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_center_frequency(&mut self, frequency: u32) -> Result<()> {
|
||||
log::trace!("Setting center_frequency to {}Hz", frequency);
|
||||
self.frequency = frequency;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_sample_rate(&mut self, rate: u32) -> Result<()> {
|
||||
log::trace!("Setting sample_rate to {}Hz", rate);
|
||||
if rate <= 225_000 || rate > 3_200_000 || (rate > 300000 && rate <= 900000) {
|
||||
return Err(Error::new(format!("Invalid sample rate: {} Hz", rate)));
|
||||
}
|
||||
|
||||
let rsamp_ratio =
|
||||
((DEFAULT_RTL_XTAL_FREQ as u128 * 2_u128.pow(22) / rate as u128) & 0x0ffffffc) as u128;
|
||||
log::trace!(
|
||||
"Sample rate: {}, xtal: {}, rsamp_ratio: {}",
|
||||
rate,
|
||||
DEFAULT_RTL_XTAL_FREQ,
|
||||
rsamp_ratio
|
||||
);
|
||||
let real_resamp_ratio = rsamp_ratio | ((rsamp_ratio & 0x08000000) << 1);
|
||||
let real_rate =
|
||||
(DEFAULT_RTL_XTAL_FREQ as u128 * 2_u128.pow(22)) as f64 / real_resamp_ratio as f64;
|
||||
if rate as f64 != real_rate {
|
||||
log::trace!("Exact sample rate is {} Hz", real_rate);
|
||||
}
|
||||
|
||||
self.rate = real_rate as u32;
|
||||
|
||||
let mut tmp: u16 = (rsamp_ratio >> 16) as u16;
|
||||
demod_ctrl_write_register(&self.handle, 1, 0x9f, tmp, 2)?;
|
||||
tmp = (rsamp_ratio & 0xffff) as u16;
|
||||
demod_ctrl_write_register(&self.handle, 1, 0xa1, tmp, 2)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_fir(&self, fir: &[i32; 16]) -> Result<()> {
|
||||
log::trace!("Setting fir to {:?}", fir);
|
||||
const TMP_LEN: usize = 20;
|
||||
let mut tmp: [u8; TMP_LEN] = [0; TMP_LEN];
|
||||
// First 8 values are i8
|
||||
for i in 0..8 {
|
||||
let val = fir[i];
|
||||
if val < -128 || val > 127 {
|
||||
panic!("i8 FIR coefficient out of bounds! {}", val);
|
||||
}
|
||||
tmp[i] = val as u8;
|
||||
}
|
||||
// Next 12 are i12, so don't line up with byte boundaries and need to unpack
|
||||
// 12 i12 values from 4 pairs of bytes in fir. Example:
|
||||
// fir: 4b5, 7f8, 3e8, 619
|
||||
// tmp: 4b, 57, f8, 3e, 86, 19
|
||||
for i in (0..8).step_by(2) {
|
||||
let val0 = fir[8 + i];
|
||||
let val1 = fir[8 + i + 1];
|
||||
if val0 < -2048 || val0 > 2047 {
|
||||
panic!("i12 FIR coefficient out of bounds: {}", val0)
|
||||
} else if val1 < -2048 || val1 > 2047 {
|
||||
panic!("i12 FIR coefficient out of bounds: {}", val1)
|
||||
}
|
||||
tmp[8 + i * 3 / 2] = (val0 >> 4) as u8;
|
||||
tmp[8 + i * 3 / 2 + 1] = ((val0 << 4) | ((val1 >> 8) & 0x0f)) as u8;
|
||||
tmp[8 + i * 3 / 2 + 2] = val1 as u8;
|
||||
}
|
||||
|
||||
for i in 0..TMP_LEN {
|
||||
demod_ctrl_write_register(&self.handle, 1, 0x1c + i as u16, tmp[i] as u16, 1)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_i2c_repeater(&self, enabled: bool) -> Result<()> {
|
||||
let value = match enabled {
|
||||
true => 0x18,
|
||||
false => 0x10,
|
||||
};
|
||||
|
||||
demod_ctrl_write_register(&self.handle, 1, 0x01, value, 1)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn detach_kernel_driver(&self, interface: u8) {
|
||||
// Detach the kernel driver if applicable
|
||||
if let Ok(true) = self.handle.kernel_driver_active(interface) {
|
||||
log::trace!("Detaching active kernel driver");
|
||||
self.handle.detach_kernel_driver(interface).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_kernel_driver(&self, interface: u8) {
|
||||
// Attach the kernel driver if applicable
|
||||
if let Ok(true) = self.handle.kernel_driver_active(interface) {
|
||||
log::trace!("Attaching active kernel driver");
|
||||
self.handle.attach_kernel_driver(interface).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Endpoint {
|
||||
config: u8,
|
||||
interface: u8,
|
||||
setting: u8,
|
||||
address: u8,
|
||||
transfer_type: TransferType,
|
||||
}
|
||||
|
||||
impl Display for Endpoint {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Config: {}, Interface: {}, Setting: {}, Address: 0x{:04X}, Transfer Type: {:?}",
|
||||
self.config, self.interface, self.setting, self.address, self.transfer_type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
fn find<T: UsbContext>(
|
||||
device_desc: &DeviceDescriptor,
|
||||
device: &Device<T>,
|
||||
transfer_type: TransferType,
|
||||
) -> Option<Self> {
|
||||
for n in 0..device_desc.num_configurations() {
|
||||
let config_desc = match device.config_descriptor(n) {
|
||||
Ok(c) => c,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
for interface in config_desc.interfaces() {
|
||||
for interface_desc in interface.descriptors() {
|
||||
for endpoint_desc in interface_desc.endpoint_descriptors() {
|
||||
if endpoint_desc.direction() == Direction::In
|
||||
&& endpoint_desc.transfer_type() == transfer_type
|
||||
{
|
||||
return Some(Endpoint {
|
||||
config: config_desc.number(),
|
||||
interface: interface_desc.interface_number(),
|
||||
setting: interface_desc.setting_number(),
|
||||
address: endpoint_desc.address(),
|
||||
transfer_type,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn device_info<T: UsbContext>(
|
||||
handle: &DeviceHandle<T>,
|
||||
device_desc: &DeviceDescriptor,
|
||||
offset: &str,
|
||||
full: bool,
|
||||
) -> String {
|
||||
let languages = match handle.read_languages(TIMEOUT) {
|
||||
Ok(l) => l,
|
||||
Err(err) => {
|
||||
return format!("{} Unable to get languages: {:?}", offset, err);
|
||||
}
|
||||
};
|
||||
let descriptor_type = device_desc.descriptor_type();
|
||||
let mut output = String::new();
|
||||
if full {
|
||||
output = format!("{}Device Descriptor ({})\n", offset, descriptor_type);
|
||||
}
|
||||
if !languages.is_empty() {
|
||||
for language in languages {
|
||||
let manufacturer = handle
|
||||
.read_manufacturer_string(language, device_desc, TIMEOUT)
|
||||
.unwrap_or_else(|err| err.to_string());
|
||||
let product = handle
|
||||
.read_product_string(language, device_desc, TIMEOUT)
|
||||
.unwrap_or_else(|err| err.to_string());
|
||||
let serial_number = handle
|
||||
.read_serial_number_string(language, device_desc, TIMEOUT)
|
||||
.unwrap_or_else(|err| err.to_string());
|
||||
output.push_str(&format!(
|
||||
"{}{}Manufacturer: {}, Product: {}, Serial Number: {}",
|
||||
offset, offset, manufacturer, product, serial_number
|
||||
));
|
||||
|
||||
if full {
|
||||
let length = device_desc.length();
|
||||
let version = format!(
|
||||
" v{}.{}.{}",
|
||||
device_desc.usb_version().major(),
|
||||
device_desc.usb_version().minor(),
|
||||
device_desc.usb_version().sub_minor()
|
||||
);
|
||||
output.push_str(&format!(
|
||||
"\n{}{}Length: {}, USB:{}\n",
|
||||
offset, offset, length, version,
|
||||
));
|
||||
let class = device_desc.class_code();
|
||||
let sub_class = device_desc.sub_class_code();
|
||||
let protocol = device_desc.protocol_code();
|
||||
let max_packet_size = device_desc.max_packet_size();
|
||||
output.push_str(&format!(
|
||||
"{}{}Class: {:#04x}, Subclass: {:#04x}, Protocol: {:#04x}, Max Packet Size: {}",
|
||||
offset, offset, class, sub_class, protocol, max_packet_size
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn ctrl_write_register<T: UsbContext>(
|
||||
handle: &DeviceHandle<T>,
|
||||
block: u16,
|
||||
address: u16,
|
||||
value: u16,
|
||||
length: usize,
|
||||
) -> rusb::Result<usize> {
|
||||
assert!(length == 1 || length == 2);
|
||||
|
||||
let data: [u8; 2] = value.to_be_bytes();
|
||||
let buffer = if length == 1 { &data[1..2] } else { &data };
|
||||
let index = (block << 8) | 0x10;
|
||||
log::trace!(
|
||||
"Received block {}, address 0x{:04X}, value 0x{:04X}, length {} \
|
||||
- writing control register: {} 0x{:04X} 0x{:04X} {:?}",
|
||||
block,
|
||||
address,
|
||||
value,
|
||||
length,
|
||||
REQ_CTRL_OUT,
|
||||
address,
|
||||
index,
|
||||
buffer
|
||||
);
|
||||
|
||||
handle.write_control(REQ_CTRL_OUT, 0x00, address, index, buffer, TIMEOUT)
|
||||
}
|
||||
|
||||
fn demod_ctrl_write_register<T: UsbContext>(
|
||||
handle: &DeviceHandle<T>,
|
||||
page: u16,
|
||||
address: u16,
|
||||
value: u16,
|
||||
length: usize,
|
||||
) -> rusb::Result<usize> {
|
||||
assert!(length == 1 || length == 2);
|
||||
|
||||
let data: [u8; 2] = value.to_be_bytes();
|
||||
let buffer = if length == 1 { &data[1..2] } else { &data };
|
||||
let index = 0x10 | page;
|
||||
let address = (address << 8) | 0x20;
|
||||
log::trace!(
|
||||
"Received page {}, address 0x{:04X}, value 0x{:04X}, length {} \
|
||||
- writing control register: {} 0x{:04X} 0x{:04X} {:?}",
|
||||
page,
|
||||
address,
|
||||
value,
|
||||
length,
|
||||
REQ_CTRL_OUT,
|
||||
address,
|
||||
index,
|
||||
buffer
|
||||
);
|
||||
|
||||
handle.write_control(REQ_CTRL_OUT, 0x00, address, index, buffer, TIMEOUT)
|
||||
}
|
||||
44
crates/adsb/src/error.rs
Normal file
44
crates/adsb/src/error.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use std::{fmt, result};
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Error {
|
||||
RusbError(rusb::Error),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new<S: Into<String>>(msg: S) -> Self {
|
||||
Error::Other(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
|
||||
match self {
|
||||
Error::RusbError(err) => write!(f, "USB Error: {}", err),
|
||||
Error::Other(err) => write!(f, "{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<rusb::Error> for Error {
|
||||
fn from(err: rusb::Error) -> Self {
|
||||
Error::RusbError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ctrlc::Error> for Error {
|
||||
fn from(err: ctrlc::Error) -> Self {
|
||||
Error::Other(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::str::Utf8Error> for Error {
|
||||
fn from(err: std::str::Utf8Error) -> Self {
|
||||
Error::Other(err.to_string())
|
||||
}
|
||||
}
|
||||
473
crates/adsb/src/frame.rs
Normal file
473
crates/adsb/src/frame.rs
Normal file
@@ -0,0 +1,473 @@
|
||||
use crate::hex_to_bytes;
|
||||
use std::fmt::Display;
|
||||
use crate::error::{Result, Error};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ADSBFrame {
|
||||
pub raw_frame: String,
|
||||
/// Downlink format (DF, 5 bits)
|
||||
pub downlink_format: u8,
|
||||
/// Transponder capability (CA, 3 bits)
|
||||
pub capability: Capability,
|
||||
/// Unique aircraft number (ICAO, 24 bits)
|
||||
pub icao: String,
|
||||
/// Message (ME, 56 bits)
|
||||
pub message: ADSBMessage,
|
||||
/// Parity/Interrogator ID/Checksum (PI, 24 bits)
|
||||
pub parity: u32,
|
||||
}
|
||||
|
||||
impl ADSBFrame {
|
||||
/// Parse exactly 14 bytes (112 bits) of raw ADS-B ES data into its fields
|
||||
///
|
||||
/// [ DF:5 ][ CA:3 ][ ICAO:24 ][ ME:56 ][ PI:24 ]
|
||||
pub fn decode(frame: &[u8]) -> Result<ADSBFrame> {
|
||||
if frame.len() != 14 {
|
||||
return Err(Error::new(format!(
|
||||
"expected 14 bytes, received {}",
|
||||
frame.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut raw_frame = "".to_string();
|
||||
for byte in frame {
|
||||
raw_frame.push_str(&format!("{:02x}", byte).to_uppercase());
|
||||
}
|
||||
|
||||
// Decode the downlink format by discarding the lower 3 bits
|
||||
let downlink_format = &frame[0] >> 3;
|
||||
if downlink_format != 17 {
|
||||
return Err(Error::new(format!(
|
||||
"downlink format {} is not currently supported",
|
||||
downlink_format
|
||||
)));
|
||||
}
|
||||
|
||||
// Decode the capability by masking off everything but the lower 3 bits
|
||||
let capability_value = &frame[0] & 0b0000_0111;
|
||||
let capability = Capability::try_from(capability_value)?;
|
||||
|
||||
let icao = Self::decode_icao(&frame[1..=3])?;
|
||||
let message = ADSBMessage::decode(&frame[4..=10])?;
|
||||
let parity = Self::decode_parity(&frame[11..])?;
|
||||
|
||||
Ok(Self {
|
||||
raw_frame,
|
||||
downlink_format,
|
||||
capability,
|
||||
icao,
|
||||
message,
|
||||
parity,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Result<Vec<u8>> {
|
||||
Ok(hex_to_bytes(&self.raw_frame)?)
|
||||
}
|
||||
|
||||
fn decode_icao(data: &[u8]) -> Result<String> {
|
||||
if data.len() != 3 {
|
||||
return Err(Error::new(format!(
|
||||
"ICAO must be 3 bytes, received {}",
|
||||
data.len()
|
||||
)));
|
||||
}
|
||||
let s = data
|
||||
.iter()
|
||||
.map(|b| format!("{:02X}", b))
|
||||
.collect::<String>();
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn decode_parity(data: &[u8]) -> Result<u32> {
|
||||
if data.len() != 3 {
|
||||
return Err(Error::new(format!(
|
||||
"parity must be 3 bytes, received {}",
|
||||
data.len()
|
||||
)));
|
||||
}
|
||||
let p = ((data[0] as u32) << 16) | ((data[1] as u32) << 8) | (data[2] as u32);
|
||||
Ok(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ADSBFrame {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Frame: {}\
|
||||
\nDF: {}\
|
||||
\nCA: {:?}\
|
||||
\nICAO: {}\
|
||||
\nME: {:?}\
|
||||
\nPI: {}",
|
||||
self.raw_frame, self.downlink_format, &self.capability, self.icao, &self.message, self.parity
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transponder Capability (CA) codes from the ADS-B spec
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Capability {
|
||||
/// 0: Level 1 transponder
|
||||
Level1,
|
||||
/// 1-3: Reserved
|
||||
Reserved(u8),
|
||||
/// 4: Level 2+ transponder, ground (can set CA=7)
|
||||
Level2OnGround,
|
||||
/// 5: Level 2+ transponder, airborne (can set CA=7)
|
||||
Level2Airborne,
|
||||
/// 6: Level 2+ transponder, either ground or airborne (can set CA=7)
|
||||
Level2Either,
|
||||
/// 7: Downlink Request = 0, or Flight Status = 2,3,4,5
|
||||
DownlinkRequestOrFlightStatus,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Capability {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self> {
|
||||
let capability = match value {
|
||||
0 => Capability::Level1,
|
||||
1..=3 => Capability::Reserved(value),
|
||||
4 => Capability::Level2OnGround,
|
||||
5 => Capability::Level2Airborne,
|
||||
6 => Capability::Level2Either,
|
||||
7 => Capability::DownlinkRequestOrFlightStatus,
|
||||
_ => {
|
||||
return Err(Error::new(format!("invalid CA value: {}", value)));
|
||||
}
|
||||
};
|
||||
Ok(capability)
|
||||
}
|
||||
}
|
||||
|
||||
// fn get_bits(data: &[u8], from: usize, len: usize) -> u32 {
|
||||
// let mut val = 0;
|
||||
// for bit in 0..len {
|
||||
// let idx = from + bit;
|
||||
// let byte = data[idx / 8];
|
||||
// let shift = 7 - (idx % 8);
|
||||
// let bit_val = ((byte >> shift) & 0x01) as u32;
|
||||
// val = (val << 1) | bit_val;
|
||||
// }
|
||||
// val
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ADSBMessage {
|
||||
AircraftIdentification(AircraftIdentification),
|
||||
SurfacePosition(SurfacePosition),
|
||||
AirbornePosition(AirbornePosition),
|
||||
AirborneVelocities(AirborneVelocities),
|
||||
Reserved(u8),
|
||||
AircraftStatus(AircraftStatus),
|
||||
TargetState(TargetState),
|
||||
AircraftOperationStatus(AircraftOperationStatus),
|
||||
}
|
||||
|
||||
impl ADSBMessage {
|
||||
pub fn decode(data: &[u8]) -> Result<ADSBMessage> {
|
||||
if data.len() != 7 {
|
||||
return Err(Error::new(format!(
|
||||
"ME field must be 7 bytes, received {}",
|
||||
data.len()
|
||||
)));
|
||||
}
|
||||
// First 5 bits is the type code
|
||||
let type_code = data[0] >> 3;
|
||||
let message = match type_code {
|
||||
1..=4 => {
|
||||
ADSBMessage::AircraftIdentification(AircraftIdentification::decode(type_code, data)?)
|
||||
}
|
||||
5..=8 => ADSBMessage::SurfacePosition(SurfacePosition::decode(data)?),
|
||||
9..=18 => ADSBMessage::AirbornePosition(AirbornePosition::decode(type_code, data)?),
|
||||
19 => ADSBMessage::AirborneVelocities(AirborneVelocities::decode(data)?),
|
||||
20..=22 => ADSBMessage::AirbornePosition(AirbornePosition::decode(type_code, data)?),
|
||||
23..=27 => ADSBMessage::Reserved(type_code),
|
||||
28 => ADSBMessage::AircraftStatus(AircraftStatus::decode(data)?),
|
||||
29 => ADSBMessage::TargetState(TargetState::decode(data)?),
|
||||
31 => ADSBMessage::AircraftOperationStatus(AircraftOperationStatus::decode(data)?),
|
||||
_ => {
|
||||
return Err(Error::new(format!(
|
||||
"unsupported ADS-B type_code {}",
|
||||
type_code
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AircraftIdentification {
|
||||
type_code: u8,
|
||||
emitter_category: u8,
|
||||
wake_vortex_category: WakeVortexCategory,
|
||||
callsign: String,
|
||||
}
|
||||
|
||||
impl AircraftIdentification {
|
||||
pub fn decode(type_code: u8, data: &[u8]) -> Result<Self> {
|
||||
// Byte 0: [ TC(5 bits) | emitter_category (3 bits) ]
|
||||
let emitter_category = data[0] & 0x07;
|
||||
|
||||
// 56 bit buffer for message
|
||||
let mut bits: u64 = 0;
|
||||
for &b in data {
|
||||
bits = (bits << 8) | b as u64;
|
||||
}
|
||||
|
||||
let mut callsign = String::with_capacity(8);
|
||||
for i in 0..8 {
|
||||
let shift = 48 - 6 * (i + 1);
|
||||
let raw6 = ((bits >> shift) & 0x3F) as u8;
|
||||
let ch = match raw6 {
|
||||
1..=26 => (b'A' + (raw6 - 1)) as char,
|
||||
48..=57 => (b'0' + (raw6 - 48)) as char,
|
||||
32 => ' ',
|
||||
_ => continue,
|
||||
};
|
||||
callsign.push(ch);
|
||||
}
|
||||
|
||||
// trim any trailing spaces
|
||||
let callsign = callsign.trim_end().to_string();
|
||||
|
||||
Ok(Self {
|
||||
type_code,
|
||||
emitter_category,
|
||||
wake_vortex_category: WakeVortexCategory::from_tc_ca(type_code, emitter_category),
|
||||
callsign,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WakeVortexCategory {
|
||||
NoInfo,
|
||||
SurfaceEmergencyVehicle,
|
||||
SurfaceServiceVehicle,
|
||||
GroundObstruction,
|
||||
Glider,
|
||||
LighterThanAir,
|
||||
Parachutist,
|
||||
Ultralight,
|
||||
Reserved,
|
||||
UnmannedAerialVehicle,
|
||||
SpaceVehicle,
|
||||
Light,
|
||||
Medium1,
|
||||
Medium2,
|
||||
HighVortex,
|
||||
Heavy,
|
||||
HighPerformance,
|
||||
Rotorcraft,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl WakeVortexCategory {
|
||||
pub fn from_tc_ca(type_code: u8, emitter_category: u8) -> Self {
|
||||
match (type_code, emitter_category) {
|
||||
(_, 0) => WakeVortexCategory::NoInfo,
|
||||
(2, 1) => WakeVortexCategory::SurfaceEmergencyVehicle,
|
||||
(2, 3) => WakeVortexCategory::SurfaceServiceVehicle,
|
||||
(2, 4..=7) => WakeVortexCategory::GroundObstruction,
|
||||
(3, 1) => WakeVortexCategory::Glider,
|
||||
(3, 2) => WakeVortexCategory::LighterThanAir,
|
||||
(3, 3) => WakeVortexCategory::Parachutist,
|
||||
(3, 4) => WakeVortexCategory::Ultralight,
|
||||
(3, 5) => WakeVortexCategory::Reserved,
|
||||
(3, 6) => WakeVortexCategory::UnmannedAerialVehicle,
|
||||
(3, 7) => WakeVortexCategory::SpaceVehicle,
|
||||
(4, 1) => WakeVortexCategory::Light,
|
||||
(4, 2) => WakeVortexCategory::Medium1,
|
||||
(4, 3) => WakeVortexCategory::Medium2,
|
||||
(4, 4) => WakeVortexCategory::HighVortex,
|
||||
(4, 5) => WakeVortexCategory::Heavy,
|
||||
(4, 6) => WakeVortexCategory::HighPerformance,
|
||||
(4, 7) => WakeVortexCategory::Rotorcraft,
|
||||
_ => WakeVortexCategory::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SurfacePosition {}
|
||||
|
||||
impl SurfacePosition {
|
||||
pub fn decode(_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AirbornePosition {}
|
||||
|
||||
impl AirbornePosition {
|
||||
pub fn decode(_type_code: u8, _data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AirborneVelocities {}
|
||||
|
||||
impl AirborneVelocities {
|
||||
pub fn decode(_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AircraftStatus {}
|
||||
|
||||
impl AircraftStatus {
|
||||
pub fn decode(_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TargetState {}
|
||||
|
||||
impl TargetState {
|
||||
pub fn decode(_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AircraftOperationStatus {}
|
||||
|
||||
impl AircraftOperationStatus {
|
||||
pub fn decode(_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_decode_df_17_aircraft_information() {
|
||||
let input = [
|
||||
0x8D, 0x48, 0x40, 0xD6, 0x20, 0x2C, 0xC3, 0x71, 0xC3, 0x1C, 0x32, 0xCE, 0x05, 0x76,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "4840D6");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 0);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::NoInfo);
|
||||
assert_eq!(id.callsign, "KLM10102");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 13501814);
|
||||
|
||||
let input = [
|
||||
0x8D, 0x48, 0x40, 0xD6, 0x20, 0x2C, 0xC3, 0x71, 0xC3, 0x2C, 0xE0, 0x57, 0x60, 0x98,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "4840D6");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 0);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::NoInfo);
|
||||
assert_eq!(id.callsign, "KLM1023");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 5726360);
|
||||
|
||||
let input = [
|
||||
0x8D, 0x7C, 0x71, 0x81, 0x21, 0x5D, 0x01, 0xA0, 0x82, 0x08, 0x20, 0x4D, 0x8B, 0xF1,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "7C7181");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 1);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Light);
|
||||
assert_eq!(id.callsign, "WPF");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 5082097);
|
||||
|
||||
let input = [
|
||||
0x8D, 0x7C, 0x77, 0x45, 0x22, 0x61, 0x51, 0xA0, 0x82, 0x08, 0x20, 0x5C, 0xE9, 0xC2,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "7C7745");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 2);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Medium1);
|
||||
assert_eq!(id.callsign, "XUF");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 6089154);
|
||||
|
||||
let input = [
|
||||
0x8D, 0x7C, 0x80, 0xAD, 0x23, 0x58, 0xF6, 0xB1, 0xE3, 0x5C, 0x60, 0xFF, 0x19, 0x25,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "7C80AD");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 3);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Medium2);
|
||||
assert_eq!(id.callsign, "VOZ1851");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 16718117);
|
||||
|
||||
let input = [
|
||||
0x8D, 0x7C, 0x14, 0x65, 0x25, 0x44, 0x60, 0x74, 0xDF, 0x58, 0x20, 0x73, 0x8E, 0x90,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "7C1465");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 5);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Heavy);
|
||||
assert_eq!(id.callsign, "QFA475");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 7573136);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_df_17_operation_status() {
|
||||
let input = [
|
||||
0x8D, 0x89, 0x65, 0xD2, 0xF8, 0x21, 0x00, 0x02, 0x00, 0x49, 0xB8, 0x94, 0xA4, 0x5F,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
dbg!(frame);
|
||||
}
|
||||
}
|
||||
44
crates/adsb/src/hex.rs
Normal file
44
crates/adsb/src/hex.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::error::Error;
|
||||
|
||||
pub fn hex_to_bytes(s: &str) -> crate::error::Result<Vec<u8>> {
|
||||
let bytes = s.as_bytes();
|
||||
if bytes.len() % 2 != 0 {
|
||||
return Err(Error::new(format!(
|
||||
"hex string must have even length, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(bytes.len() / 2);
|
||||
for chunk in bytes.chunks(2) {
|
||||
let hi = match hex_val(chunk[0]) {
|
||||
Some(hi) => hi,
|
||||
None => {
|
||||
return Err(Error::new(format!(
|
||||
"invalid hex char '{}'",
|
||||
chunk[0] as char
|
||||
)));
|
||||
}
|
||||
};
|
||||
let lo = match hex_val(chunk[1]) {
|
||||
Some(lo) => lo,
|
||||
None => {
|
||||
return Err(Error::new(format!(
|
||||
"invalid hex char '{}'",
|
||||
chunk[1] as char
|
||||
)));
|
||||
}
|
||||
};
|
||||
out.push((hi << 4) | lo);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn hex_val(b: u8) -> Option<u8> {
|
||||
match b {
|
||||
b'0'..=b'9' => Some(b - b'0'),
|
||||
b'a'..=b'f' => Some(b - b'a' + 10),
|
||||
b'A'..=b'F' => Some(b - b'A' + 10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
98
crates/adsb/src/main.rs
Normal file
98
crates/adsb/src/main.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
mod constants;
|
||||
mod device;
|
||||
mod error;
|
||||
mod frame;
|
||||
mod hex;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use crate::device::RtlSdrDevice;
|
||||
use clap::Parser;
|
||||
use crate::constants::DEVICE_RTL2832U;
|
||||
use crate::frame::ADSBFrame;
|
||||
use crate::hex::hex_to_bytes;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about = "An ADS-B Receiver")]
|
||||
struct ReceiverArgs {
|
||||
/// Hex-string to decode
|
||||
#[arg(short = 'd', long)]
|
||||
decode: Option<String>,
|
||||
|
||||
/// Connect to the USB device
|
||||
#[arg(short = 'c', long, action)]
|
||||
connect: bool,
|
||||
|
||||
/// Display ADS-B/Mode-S receiver info
|
||||
#[arg(short = 'i', long, action)]
|
||||
info: bool,
|
||||
|
||||
/// Enable debug logging
|
||||
#[arg(short = 'D', long, action = clap::ArgAction::Count)]
|
||||
debug: u8,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = ReceiverArgs::parse();
|
||||
|
||||
let default_filter = match args.debug {
|
||||
0 => "warn,adsb=info", // no -D
|
||||
1 => "warn,adsb=debug", // -D
|
||||
_ => "trace,adsb=trace", // -DD or more
|
||||
};
|
||||
|
||||
env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", default_filter));
|
||||
|
||||
let device_info = DEVICE_RTL2832U;
|
||||
|
||||
// Handle connection
|
||||
if args.connect {
|
||||
log::info!("Connecting to {:?}", device_info);
|
||||
let mut device = match RtlSdrDevice::open(device_info.vid, device_info.pid) {
|
||||
Ok(d) => d,
|
||||
Err(err) => {
|
||||
log::error!("Unable to open RTL SDR device: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
log::debug!("Connected to {:?}", device_info.to_string());
|
||||
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
if let Err(err) = ctrlc::set_handler({
|
||||
let running = running.clone();
|
||||
move || running.store(false, Ordering::SeqCst)
|
||||
}) {
|
||||
log::error!("Error setting Ctrl-C handler: {}", err);
|
||||
running.store(false, Ordering::SeqCst);
|
||||
};
|
||||
|
||||
if let Err(err) = device.process(running) {
|
||||
log::error!("Failed to read from device: {}", err);
|
||||
if let Err(err) = device.close() {
|
||||
log::error!("Failed to close device: {}", err);
|
||||
};
|
||||
};
|
||||
}
|
||||
// Display dongle info
|
||||
else if args.info {
|
||||
RtlSdrDevice::info(device_info.vid, device_info.pid);
|
||||
}
|
||||
// Handle decode mode
|
||||
else if let Some(mut hex_string) = args.decode {
|
||||
if let Some(stripped) = hex_string.strip_prefix("0x") {
|
||||
hex_string = stripped.to_string();
|
||||
}
|
||||
let buffer = match hex_to_bytes(&hex_string) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(err) => {
|
||||
eprintln!("Unable to convert hex to bytes: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Ok(frame) = ADSBFrame::decode(&buffer) {
|
||||
println!("{:?}", frame);
|
||||
};
|
||||
} else {
|
||||
eprintln!("No connection specified");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user