Refactor to break out scheduler

This commit is contained in:
2025-10-23 20:23:03 -04:00
parent 84312d0b50
commit a9dc5ffdc1
66 changed files with 5796 additions and 705 deletions

475
crates/adsb/Cargo.lock generated Normal file
View 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
View 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
View 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`

View File

@@ -0,0 +1,3 @@
[toolchain]
channel = "stable"
components = ["rustfmt", "clippy"]

3
crates/adsb/rustfmt.toml Normal file
View File

@@ -0,0 +1,3 @@
indent_style = "Block"
reorder_imports = false
tab_spaces = 2

View 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
View 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
View 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
View 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
View 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
View 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");
}
}