diff --git a/Makefile b/Makefile index e0e2745..b4af527 100644 --- a/Makefile +++ b/Makefile @@ -40,11 +40,8 @@ format-adsb: ## Format code build-adsb: ## Build the ADS-B project @cd adsb && cargo build -run-sim: ## Run the ADS-B Simulator - @cd adsb/adsb_sim && cargo run -p adsb_sim - -run-recv: ## Run the ADS-B Receiver - @cd adsb/adsb_recv && cargo run -p adsb_recv -- --sim +run-adsb: ## Run the ADS-B Receiver + @cd adsb && cargo run -- -c -D ################# # UI Commands # diff --git a/adsb/Cargo.toml b/adsb/Cargo.toml index 41b8578..69c012f 100644 --- a/adsb/Cargo.toml +++ b/adsb/Cargo.toml @@ -1,9 +1,11 @@ -[workspace] -members = [ - "adsb_lib", - "squawk", - "squawk_sim" -] -resolver = "2" -package.version = "0.1.0" -package.edition = "2024" \ No newline at end of file +[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" diff --git a/adsb/README.md b/adsb/README.md index f748787..262d729 100644 --- a/adsb/README.md +++ b/adsb/README.md @@ -1,3 +1,8 @@ +# ADSB +Debug using `export LIBUSB_DEBUG=4` + +`lsusb -v -d 0bda:2832` + ## Simulation Mode `cargo run -p adsb_sim --` diff --git a/adsb/adsb_lib/Cargo.toml b/adsb/adsb_lib/Cargo.toml deleted file mode 100644 index 948022c..0000000 --- a/adsb/adsb_lib/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -name = "adsb_lib" -version = "0.1.0" -edition = "2024" \ No newline at end of file diff --git a/adsb/adsb_lib/src/device.rs b/adsb/adsb_lib/src/device.rs deleted file mode 100644 index 00f9d71..0000000 --- a/adsb/adsb_lib/src/device.rs +++ /dev/null @@ -1,110 +0,0 @@ -pub trait Device { - /// Send a control message to the device - fn control_send(&mut self, b_request: u8, data: &[u8]) -> std::io::Result<()>; - /// Receive a control message from a device - fn control_recv(&mut self, b_request: u8, length: usize) -> std::io::Result>; - /// Read a chunk of raw IQ samples from the bulk-in endpoint - /// - /// # Arguments - /// * `buffer` - the slice to fill with received data - /// - /// # Returns - /// Number of bytes actually read - fn read_bulk(&mut self, buffer: &mut [u8]) -> std::io::Result; -} - -pub fn run(device: &mut S) -> std::io::Result<()> { - // RESET - device.control_send(0x00, &[])?; - // SET_FREQ - device.control_send(0x02, &1_090_000_000u32.to_le_bytes())?; - // SET_SR - device.control_send(0x03, &2_400_000u32.to_le_bytes())?; - // AGC on - device.control_send(0x04, &[1])?; - - // Precompute the preamble pattern in “half-bit” units (16 samples) - let preamble_halfbit_pattern = [1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0]; - - // Create a big buffer to hold raw I/Q bytes - let mut iq_buffer = [0u8; 16_384]; - - loop { - // Read one bulk transfer's worth of I/Q data - let bytes_read = device.read_bulk(&mut iq_buffer)?; - if bytes_read < 32 { - // Must be at least 16 I/Q pairs - continue; - } - - let raw = &iq_buffer[..bytes_read]; - - // Build a vector of "bit-samples" by thresholding I - // raw is [I0,Q0,I1,Q1,...], so step by 2 - let mut halfbit_samples = Vec::with_capacity(raw.len() / 2); - for pair in raw.chunks_exact(2) { - let i_sample = pair[0] as u16; - // Threshold at 200 - halfbit_samples.push(if i_sample > 200 { 1 } else { 0 }); - } - - // Scan for the 16-sample preamble - let mut data_start_index = None; - for idx in 0..halfbit_samples.len().saturating_sub(16) { - if &halfbit_samples[idx..idx + 16] == preamble_halfbit_pattern { - data_start_index = Some(idx + 16); - break; - } - } - - let data_start = match data_start_index { - Some(i) => i, - None => continue, // No preamble found in this chunk - }; - - // Collect 112 ADS-B bits, each manchester-encoded into 2 half-bits - // 224 half-bit samples total - let required_samples = 112 * 2; - if data_start + required_samples > halfbit_samples.len() { - // Not enough in this buffer - continue; - } - let manchester_slice = &halfbit_samples[data_start..data_start + required_samples]; - - // Manchester-decode pairs back into plain bits - let mut adsb_bits = Vec::with_capacity(112); - for window in manchester_slice.chunks_exact(2) { - match window { - [1, 0] => adsb_bits.push(0), - [0, 1] => adsb_bits.push(1), - _ => { - // Failed manchester pattern - adsb_bits.clear(); - break; - } - } - } - - if adsb_bits.len() != 112 { - // Data is malformed - continue; - } - - // Pack 112 bits into 14 bytes (MSB first in each byte) - let mut adsb_payload = [0u8; 14]; - for (bit_index, &bit_value) in adsb_bits.iter().enumerate() { - let byte_index = bit_index / 8; - let bit_in_byte = 7 - (bit_index % 8); - if bit_value == 1 { - adsb_payload[byte_index] |= 1 << bit_in_byte; - } - } - - // Print out the 14-byte payload in hex - print!("ADS-B payload: "); - for byte in &adsb_payload { - print!("{:02X} ", byte); - } - println!(); - } -} diff --git a/adsb/squawk/Cargo.toml b/adsb/squawk/Cargo.toml deleted file mode 100644 index 3681de2..0000000 --- a/adsb/squawk/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "squawk" -version = "0.1.0" -edition = "2024" - -[dependencies] -adsb_lib = { path = "../adsb_lib" } -rusb = "0.9.4" -clap = { version = "4.5.37", features = ["derive"] } -log = "0.4.27" -env_logger = "0.11.8" diff --git a/adsb/squawk/src/main.rs b/adsb/squawk/src/main.rs deleted file mode 100644 index b8d8d20..0000000 --- a/adsb/squawk/src/main.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod rusb_device; -mod tcp_device; - -use crate::rusb_device::RusbDevice; -use crate::tcp_device::TcpDevice; -use adsb_lib::adsb_frame::ADSBFrame; -use adsb_lib::{hex_to_bytes, device::run}; -use clap::Parser; -use std::io::Result; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = "An ADS-B Receiver")] -struct ReceiverArgs { - /// Hex-string to decode - #[arg(long)] - decode: Option, - - /// Connect to the network - #[arg(long)] - net: bool, - /// Network listen address (requires --net) - #[arg(long, requires = "net", hide = true)] - addr: Option, - /// Network listen port (requires --net) - #[arg(long, requires = "net", hide = true)] - port: Option, - - /// Connect to the USB device - #[arg(long)] - usb: bool, - - /// Enable debug logging - #[arg(short, long, action)] - debug: bool, -} - -fn main() -> Result<()> { - let args = ReceiverArgs::parse(); - - let default_filter = if args.debug { - "warn,squawk=debug" - } else { - "warn,squawk=info" - }; - - env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", default_filter)); - - // Handle decode mode - if let Some(mut hex_string) = args.decode { - if let Some(stripped) = hex_string.strip_prefix("0x") { - hex_string = stripped.to_string(); - } - let buf = hex_to_bytes(&hex_string)?; - let frame = ADSBFrame::decode(&buf)?; - - log::info!("{}", frame); - return Ok(()); - } - - // Handle net mode - if args.net { - let host = args.addr.unwrap_or_else(|| "127.0.0.1".into()); - let port = args.port.unwrap_or(9999); - let addr = format!("{host}:{port}"); - - log::info!("Connecting to network {}", addr); - let mut device = TcpDevice::connect(&addr)?; - device.run() - } - // Handle usb mode - else if args.usb { - log::info!("Connecting to device"); - let mut device = RusbDevice::open()?; - run(&mut device) - } else { - log::warn!("No connection specified"); - Ok(()) - } -} diff --git a/adsb/squawk/src/rusb_device.rs b/adsb/squawk/src/rusb_device.rs deleted file mode 100644 index dc418f1..0000000 --- a/adsb/squawk/src/rusb_device.rs +++ /dev/null @@ -1,107 +0,0 @@ -use adsb_lib::device::Device; -use rusb::{request_type, Context, DeviceHandle, Direction, Recipient, RequestType, UsbContext}; -use std::io::{Error, ErrorKind, Result}; -use std::time::Duration; - -// USB identifiers for RTL-SDR -const VENDOR_ID: u16 = 0x0BDA; -const PRODUCT_ID: u16 = 0x2832; -// Bulk-IN endpoint (0x80 | 0x01) -const DATA_ENDPOINT_ADDRESS: u8 = 0x81; -const USB_TRANSFER_TIMEOUT: Duration = Duration::from_secs(1); - -/// rusb/libusb implementation of `RtlDevice` -pub struct RusbDevice { - handle: DeviceHandle, -} - -impl RusbDevice { - /// Open the USB device, claim interface 0, and return a wrapper - pub fn open() -> Result { - // Create a new libusb context - let ctx = Context::new().map_err(|err| Error::new(ErrorKind::Other, err))?; - - // Find and open the RTL-SDR by VID/PID - let handle = ctx - .open_device_with_vid_pid(VENDOR_ID, PRODUCT_ID) - .ok_or_else(|| Error::new(ErrorKind::NotFound, "Device not found"))?; - - // Claim interface 0 - handle - .claim_interface(0) - .map_err(|err| Error::new(ErrorKind::Other, err))?; - - Ok(Self { handle }) - } - - /// Send a CONTROL-OUT request to the device - /// - /// # Arguments - /// * `request` - the bRequest byte identifying the operation - /// * `data_payload` - a slice of bytes to send in the data stage - fn control_out(&mut self, request: u8, data: &[u8]) -> rusb::Result<()> { - // bmRequestType: OUT | VENDOR | DEVICE - let bm_request_type = request_type(Direction::Out, RequestType::Vendor, Recipient::Device); - - // Perform the control transfer. - self - .handle - .write_control( - bm_request_type, - request, - 0, // wValue - 0, // wIndex - data, - USB_TRANSFER_TIMEOUT, - ) - .map(|_bytes_written| ()) - } - - /// Perform a CONTROL_IN request and read back up to `response_length` bytes - /// - /// # Arguments - /// * `request` - the bRequest byte identifying the operation - /// * `response_length` - the maximum number of bytes to read - fn control_in(&mut self, request: u8, response_length: usize) -> rusb::Result> { - // bmRequestType: IN | VENDOR | DEVICE - let bm_request_type = request_type(Direction::In, RequestType::Vendor, Recipient::Device); - - // Allocate a buffer for the incoming data - let mut buffer = vec![0u8; response_length]; - - // Read the control response into the buffer - let n = self.handle.read_control( - bm_request_type, - request, - 0, // wValue - 0, // wIndex - &mut buffer, - USB_TRANSFER_TIMEOUT, - )?; - - // Truncate to the actual length returned by the device - buffer.truncate(n); - Ok(buffer) - } -} - -impl Device for RusbDevice { - fn control_send(&mut self, b_request: u8, data: &[u8]) -> Result<()> { - self - .control_out(b_request, data) - .map_err(|err| Error::new(ErrorKind::Other, err)) - } - - fn control_recv(&mut self, b_request: u8, length: usize) -> Result> { - self - .control_in(b_request, length) - .map_err(|err| Error::new(ErrorKind::Other, err)) - } - - fn read_bulk(&mut self, buffer: &mut [u8]) -> Result { - self - .handle - .read_bulk(DATA_ENDPOINT_ADDRESS, buffer, USB_TRANSFER_TIMEOUT) - .map_err(|err| Error::new(ErrorKind::Other, err)) - } -} diff --git a/adsb/squawk/src/tcp_device.rs b/adsb/squawk/src/tcp_device.rs deleted file mode 100644 index c46762d..0000000 --- a/adsb/squawk/src/tcp_device.rs +++ /dev/null @@ -1,161 +0,0 @@ -use adsb_lib::device::Device; -use std::io::{Error, ErrorKind, Read, Result, Write}; -use std::net::TcpStream; -use adsb_lib::adsb_frame::ADSBFrame; - -// Tags for framing requests/responses over the TCP socket -const TAG_CTRL_OUT: u8 = 0x10; -const TAG_CTRL_IN: u8 = 0x11; -const TAG_BULK: u8 = 0x20; - -/// A TCP-based implementation of `RtlDevice` -pub struct TcpDevice { - socket: TcpStream, -} - -impl TcpDevice { - /// Connect to a remote RTL-SDR server at the given address - pub fn connect(addr: &str) -> Result { - let socket = TcpStream::connect(addr)?; - Ok(TcpDevice { socket }) - } - - pub fn run(&mut self) -> Result<()> { - let request_len: u16 = 14; - loop { - // Send header: [tag][bRequest=0][length:2 bytes LE] - let mut hdr = [0u8; 4]; - hdr[0] = TAG_BULK; - hdr[1] = 0; - hdr[2..4].copy_from_slice(&request_len.to_le_bytes()); - self.socket.write_all(&hdr)?; - - // Read status - let mut status = [0u8; 1]; - self.socket.read_exact(&mut status)?; - if status[0] != 0 { - log::error!("Remote reported error status {}", status[0]); - break; - } - - // Read 4-byte payload length (LE) - let mut len_bytes = [0u8; 4]; - self.socket.read_exact(&mut len_bytes)?; - let actual_len = u32::from_le_bytes(len_bytes) as usize; - - // Read payload (I/Q pairs) - let mut iq = vec![0u8; actual_len]; - self.socket.read_exact(&mut iq)?; - - // Extract I-samples (even indices) and print as ADS‑B hex - let mut adsb = Vec::with_capacity(actual_len / 2); - for chunk in iq.chunks_exact(2) { - adsb.push(chunk[0]); - } - - let frame = ADSBFrame::decode(&adsb)?; - log::info!("{}", frame); - } - - Ok(()) - } - - /// Send a framed message - /// - /// 1 byte: tag - /// 1 byte: bRequest - /// 2 bytes: payload length (big endian) - /// N bytes: optional payload data - fn send_message(&mut self, message_tag: u8, b_request: u8, data: &[u8]) -> Result<()> { - let payload_length = data.len() as u16; - - // Build the 4-byte header - let mut header = [0u8; 4]; - header[0] = message_tag; - header[1] = b_request; - header[2..4].copy_from_slice(&payload_length.to_be_bytes()); - - // Send header + payload - self.socket.write_all(&header)?; - if payload_length > 0 { - self.socket.write_all(&data)? - } - Ok(()) - } - - /// Read one status byte. Expect 0 => OK, non-zero => error - fn receive_status_ok(&mut self) -> Result<()> { - let mut status_byte = [0u8; 1]; - self.socket.read_exact(&mut status_byte)?; - if status_byte[0] != 0 { - Err(Error::new(ErrorKind::Other, "Remote reported error")) - } else { - Ok(()) - } - } - - /// Read a length-prefixed payload - /// - /// 1 byte: tag (ignored) - /// 2 bytes: length (little-endian) - /// N bytes: payload data - fn receive_length_prefixed_payload(&mut self) -> Result> { - // Discard tag - let mut tag_bytes = [0u8; 1]; - self.socket.read_exact(&mut tag_bytes)?; - - // Read 2-byte little-endian length - let mut length_bytes = [0u8; 2]; - self.socket.read_exact(&mut length_bytes)?; - let payload_length = u16::from_le_bytes(length_bytes) as usize; - - // Read exactly payload_length bytes - let mut buffer = vec![0u8; payload_length]; - self.socket.read_exact(&mut buffer)?; - Ok(buffer) - } -} - -impl Device for TcpDevice { - fn control_send(&mut self, b_request: u8, data: &[u8]) -> Result<()> { - self.send_message(TAG_CTRL_OUT, b_request, data)?; - self.receive_status_ok() - } - - fn control_recv(&mut self, b_request: u8, _length: usize) -> Result> { - self.send_message(TAG_CTRL_IN, b_request, &[])?; - self.receive_length_prefixed_payload() - } - - fn read_bulk(&mut self, buffer: &mut [u8]) -> Result { - // Number of bytes expected - let requested_byte_count = buffer.len() as u16; - - // Prepare a 4-byte header slot - let mut header = [0u8; 4]; - header[0] = TAG_BULK; - header[1] = 0; // bRequest=0 for bulk - header[2..4].copy_from_slice(&requested_byte_count.to_le_bytes()); - - // Write the 4-byte header from the peer - let _ = self.socket.write_all(&header)?; - - // Read and check the status byte - let mut status_byte = [0u8]; - self.socket.read_exact(&mut status_byte)?; - if status_byte[0] != 0 { - return Err(Error::new(ErrorKind::Other, "Remote reported error")); - } - - // Read the 4-byte payload length - let mut length_bytes = [0u8; 4]; - self.socket.read_exact(&mut length_bytes)?; - let actual_payload_length = u32::from_le_bytes(length_bytes) as usize; - - // Read the payload - self - .socket - .read_exact(&mut buffer[..actual_payload_length])?; - Ok(actual_payload_length) - } -} diff --git a/adsb/squawk_sim/Cargo.toml b/adsb/squawk_sim/Cargo.toml deleted file mode 100644 index 91a762a..0000000 --- a/adsb/squawk_sim/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "squawk_sim" -version = "0.1.0" -edition = "2024" - -[dependencies] -adsb_lib = { path = "../adsb_lib" } -clap = { version = "4.5.37", features = ["derive"] } -log = "0.4.27" -env_logger = "0.11.8" diff --git a/adsb/squawk_sim/src/main.rs b/adsb/squawk_sim/src/main.rs deleted file mode 100644 index b53035a..0000000 --- a/adsb/squawk_sim/src/main.rs +++ /dev/null @@ -1,180 +0,0 @@ -use clap::Parser; -use std::io::{Read, Write, Result}; -use std::net::{TcpListener, TcpStream}; -use std::thread; -use std::time::Duration; - -// Framing tags -const TAG_CONTROL_OUT: u8 = 0x10; -const TAG_CONTROL_IN: u8 = 0x11; -const TAG_BULK: u8 = 0x20; - -const ADSB_MESSAGE: [u8; 14] = [ - 0x8D, 0x48, 0x40, 0xD6, 0x20, 0x2C, 0xC3, 0x71, 0xC3, 0x2C, 0xE0, 0x57, 0x60, 0x98, -]; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct SimulationArgs { - /// Host/IP to bind the TCP listener on - #[arg(long, default_value = "127.0.0.1")] - host: String, - - /// TCP port to bind the listener on - #[arg(long, default_value = "9999")] - port: u16, -} - -fn main() { - env_logger::init_from_env( - env_logger::Env::default().filter_or("RUST_LOG", "warn,squawk_sim=info"), - ); - let args = SimulationArgs::parse(); - - // Build the bind address, e.g. "127.0.0.1:9999" - let bind_address = format!("{}:{}", args.host, args.port); - log::info!("Listening on {}", bind_address); - - // Start listening for incoming TCP connections - let listener = TcpListener::bind(&bind_address) - .unwrap_or_else(|err| panic!("failed to bind {}: {}", bind_address, err)); - - // Accept connections in a loop - for incoming_connection in listener.incoming() { - match incoming_connection { - Ok(client_stream) => { - // Spawn a thread per client - thread::spawn(move || { - if let Err(err) = handle_client_connection(client_stream) { - log::error!("connection error: {}", err); - } - }); - } - Err(err) => log::error!("error accepting connection: {}", err), - } - } -} - -/// Handle a single client connection -fn handle_client_connection(mut connection: TcpStream) -> Result<()> { - log::info!("Connection established"); - loop { - // Read the 4-byte header: [tag:1][bRequest:1][length:2 LE] - let mut header_buffer = [0u8; 4]; - if connection.read_exact(&mut header_buffer).is_err() { - // Client closed on error - break; - } - - let message_tag = header_buffer[0]; - let _b_request = header_buffer[1]; - let payload_length = u16::from_le_bytes([header_buffer[2], header_buffer[3]]) as usize; - - log::trace!( - "Received message '{:02x}' with payload length {}", - message_tag, - payload_length - ); - - // Read the optional payload - // let mut payload_buffer = vec![0u8; payload_length]; - // if payload_length > 0 { - // if connection.read(&mut payload_buffer).is_err() { - // log::error!("error reading payload buffer"); - // break; - // } - // } - - // Dispatch based on the framing tag - match message_tag { - TAG_CONTROL_OUT => { - log::trace!("Received control out"); - // Acknowledge with a status OK - connection.write_all(&[0u8])?; - } - TAG_CONTROL_IN => { - log::trace!("Received control in"); - // STATUS(1) + LENGTH(2) + dummy payload - connection.write_all(&[0x00])?; - connection.write_all(&(payload_length as u16).to_le_bytes())?; - connection.write_all(&vec![0x42; payload_length])?; - } - TAG_BULK => { - log::trace!("Received bulk message"); - let iq = generate_adsb_iq(); - // STATUS(1) + LENGTH(4) + IQ data - connection.write_all(&[0x00])?; - connection.write_all(&(iq.len() as u32).to_le_bytes())?; - connection.write_all(&iq)?; - - // Throttle a bit to simulate real USB/bulk behavior - thread::sleep(Duration::from_millis(10)); - } - _unknown_tag => { - log::warn!("Unknown message tag {}", _unknown_tag); - break; - } - } - } - - log::info!("Connection closed"); - Ok(()) -} - -fn generate_adsb_iq() -> Vec { - let mut v = Vec::with_capacity(ADSB_MESSAGE.len() * 2); - for &b in &ADSB_MESSAGE { - v.push(b); // I - v.push(0x80); // Q fixed - } - v -} - -/// Build one preamble (8 bits) + 112 data bits -/// Sampled at 2 Mhz (1 sample per half-bit). Interleaved I/Q bytes -fn _generate_adsb_iq_samples() -> Vec { - // Preamble bits (1us per bit at 2 Mhz -> 2 samples per bit) - // Preamble is 8 bits: 1,0,1,0,1,0,0,0 - let preamble_bits = [1, 0, 1, 0, 1, 0, 0, 0]; - - // Manchester encode the 112 data bits - // bit=0 -> [1,0], bit=1 -> [0,1] (half-bit intervals) - let mut manchester_bits = Vec::with_capacity(112 * 2); - for &byte in ADSB_MESSAGE.iter() { - for bit_idx in (0..8).rev() { - let bit = (byte >> bit_idx) & 1; - if bit == 0 { - manchester_bits.push(1); - manchester_bits.push(0); - } else { - manchester_bits.push(0); - manchester_bits.push(1); - } - } - } - - // Concatenate preamble + data - let mut full_bitstream = Vec::with_capacity(preamble_bits.len() * 2 + manchester_bits.len()); - - // Preamble: each '1' or '0' is one microsecond = 2 samples - for &pb in preamble_bits.iter() { - // Push two identical half-bits = 2 samples - full_bitstream.push(pb); - full_bitstream.push(pb); - } - - // Data: already in half-bit units = 1 sample per element - full_bitstream.extend(manchester_bits); - - // Build interleaved I/Q samples - // I = 128 + 127*(bit), Q = 128 - let mut iq = Vec::with_capacity(full_bitstream.len() * 2); - for &level in full_bitstream.iter() { - let i_sample = if level == 1 { 255u8 } else { 128u8 }; - let q_sample = 128u8; - iq.push(i_sample); - iq.push(q_sample); - } - - iq -} diff --git a/adsb/src/device.rs b/adsb/src/device.rs new file mode 100644 index 0000000..9b2ae39 --- /dev/null +++ b/adsb/src/device.rs @@ -0,0 +1,248 @@ +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 std::time::Duration; + +const TIMEOUT: Duration = Duration::from_secs(1); + +/// rusb/libusb implementation of `RtlSdrDevice` +pub struct RtlSdrDevice { + /// Device handle + handle: DeviceHandle, + device_desc: DeviceDescriptor, + device: Device, +} + +impl RtlSdrDevice { + /// List devices + pub fn list() -> Result<()> { + for device in DeviceList::new()?.iter() { + let device_desc = device.device_descriptor()?; + + println!( + "Bus: {:03}, Device: {:03} VID: 0x{:04X}, PID: 0x{:04X}", + device.bus_number(), + device.address(), + device_desc.vendor_id(), + device_desc.product_id() + ); + + let handle = device.open()?; + println!("{}", device_info(&handle, &device_desc, " ", true)?); + } + + Ok(()) + } + + /// Open the USB device and return a wrapper + pub fn open(vid: u16, pid: u16) -> Result { + // 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()?; + return Ok(Self { + handle, + device_desc, + device, + }); + } + } + Err(Error::new("No valid device found")) + } + + pub fn read(&mut self, transfer_type: TransferType) -> Result<()> { + log::debug!( + "Reading active configuration: {} ({:?})", + self.handle.active_configuration()?, + transfer_type + ); + + log::debug!( + "{}", + device_info(&self.handle, &self.device_desc, "", false)? + ); + + // Read endpoint + match Endpoint::find_readable(&self.device, &self.device_desc, transfer_type) { + Some(endpoint) => endpoint.read(&mut self.handle)?, + None => log::warn!("No readable {:?} endpoint", transfer_type), + } + + Ok(()) + } +} + +fn device_info( + handle: &DeviceHandle, + device_desc: &DeviceDescriptor, + offset: &str, + full: bool, +) -> Result { + let languages = handle.read_languages(TIMEOUT)?; + let descriptor_type = device_desc.descriptor_type(); + let mut output = String::new(); + if full { + output = format!("{}Device Descriptor ({})\n", offset, descriptor_type).to_string(); + } + if !languages.is_empty() { + for language in languages { + let manufacturer = handle.read_manufacturer_string(language, device_desc, TIMEOUT)?; + let product = handle.read_product_string(language, device_desc, TIMEOUT)?; + let serial_number = handle.read_serial_number_string(language, device_desc, TIMEOUT)?; + 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 + )) + } + } + } + Ok(output) +} + +#[derive(Debug)] +struct Endpoint { + config: u8, + interface: u8, + setting: u8, + address: u8, + transfer_type: TransferType, +} + +impl Endpoint { + pub fn find_readable( + device: &Device, + device_desc: &DeviceDescriptor, + transfer_type: TransferType, + ) -> Option { + 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(Self { + config: config_desc.number(), + interface: interface_desc.interface_number(), + setting: interface_desc.setting_number(), + address: endpoint_desc.address(), + transfer_type, + }); + } + } + } + } + } + None + } + + fn read(&self, handle: &mut DeviceHandle) -> Result<()> { + log::debug!("Reading from endpoint: {:?}", self); + let running = Arc::new(AtomicBool::new(true)); + { + let running = running.clone(); + ctrlc::set_handler(move || { + running.store(false, Ordering::SeqCst); + })?; + } + + // Detach the kernel driver if applicable + let has_kernel_driver = match handle.kernel_driver_active(self.interface) { + Ok(true) => { + log::debug!("Detaching active kernel driver"); + handle.detach_kernel_driver(self.interface).ok(); + true + } + _ => false, + }; + + self.configure_endpoint(handle)?; + + let mut buffer = [0u8; 4096]; + while running.load(Ordering::SeqCst) { + let length = match self.transfer_type { + TransferType::Interrupt => handle + .read_interrupt(self.address, &mut buffer, TIMEOUT) + .map_err(|err| { + Error::new(format!("Unable to read interrupt from endpoint: {:?}", err)) + })?, + TransferType::Bulk => handle + .read_bulk(self.address, &mut buffer, TIMEOUT) + .map_err(|err| Error::new(format!("Unable to read bulk from endpoint: {:?}", err)))?, + _ => 0, + }; + log::debug!("Received: {:?}", &buffer[..length]); + } + + // Attach the kernel driver if applicable + if has_kernel_driver { + log::debug!("Attaching active kernel driver"); + handle.attach_kernel_driver(self.interface).ok(); + } + + log::debug!("Exiting USB read"); + Ok(()) + } + + fn configure_endpoint(&self, handle: &mut DeviceHandle) -> Result<()> { + log::debug!("Configuring endpoint: {:?}", self); + + // Switch to ADS-B mode + // let request_type = request_type(Direction::Out, RequestType::Vendor, Recipient::Interface); + // handle.write_control( + // request_type, + // 0x42, + // 0x0002, + // 0, + // &[], + // TIMEOUT, + // )?; + + handle + .set_active_configuration(self.config) + .map_err(|err| Error::new(format!("Failed to set active configuration: {:?}", err)))?; + handle + .claim_interface(self.interface) + .map_err(|err| Error::new(format!("Failed to claim interface: {:?}", err)))?; + handle + .set_alternate_setting(self.interface, self.setting) + .map_err(|err| Error::new(format!("Failed to set alternate setting: {:?}", err)))?; + Ok(()) + } +} diff --git a/adsb/src/error.rs b/adsb/src/error.rs new file mode 100644 index 0000000..e60ec11 --- /dev/null +++ b/adsb/src/error.rs @@ -0,0 +1,38 @@ +use std::{fmt, result}; + +pub type Result = result::Result; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Error { + RusbError(rusb::Error), + Other(String), +} + +impl Error { + pub fn new>(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 for Error { + fn from(err: rusb::Error) -> Self { + Error::RusbError(err) + } +} + +impl From for Error { + fn from(err: ctrlc::Error) -> Self { + Error::Other(err.to_string()) + } +} diff --git a/adsb/adsb_lib/src/adsb_frame.rs b/adsb/src/frame.rs similarity index 92% rename from adsb/adsb_lib/src/adsb_frame.rs rename to adsb/src/frame.rs index f1974eb..9cea41c 100644 --- a/adsb/adsb_lib/src/adsb_frame.rs +++ b/adsb/src/frame.rs @@ -1,6 +1,6 @@ use crate::hex_to_bytes; use std::fmt::Display; -use std::io::{Error, ErrorKind, Result}; +use crate::error::{Result, Error}; #[derive(Debug)] pub struct ADSBFrame { @@ -23,10 +23,10 @@ impl ADSBFrame { /// [ DF:5 ][ CA:3 ][ ICAO:24 ][ ME:56 ][ PI:24 ] pub fn decode(frame: &[u8]) -> Result { if frame.len() != 14 { - return Err(Error::new( - ErrorKind::InvalidInput, - format!("expected 14 bytes, received {}", frame.len()), - )); + return Err(Error::new(format!( + "expected 14 bytes, received {}", + frame.len() + ))); } let mut raw_frame = "".to_string(); @@ -37,13 +37,10 @@ impl ADSBFrame { // Decode the downlink format by discarding the lower 3 bits let downlink_format = &frame[0] >> 3; if downlink_format != 17 { - return Err(Error::new( - ErrorKind::Unsupported, - format!( - "downlink format {} is not currently supported", - downlink_format - ), - )); + 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 @@ -70,10 +67,10 @@ impl ADSBFrame { fn decode_icao(data: &[u8]) -> Result { if data.len() != 3 { - return Err(Error::new( - ErrorKind::InvalidInput, - format!("ICAO must be 3 bytes, received {}", data.len()), - )); + return Err(Error::new(format!( + "ICAO must be 3 bytes, received {}", + data.len() + ))); } let s = data .iter() @@ -84,10 +81,10 @@ impl ADSBFrame { fn decode_parity(data: &[u8]) -> Result { if data.len() != 3 { - return Err(Error::new( - ErrorKind::InvalidInput, - format!("parity must be 3 bytes, received {}", data.len()), - )); + 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) @@ -138,10 +135,7 @@ impl TryFrom for Capability { 6 => Capability::Level2Either, 7 => Capability::DownlinkRequestOrFlightStatus, _ => { - return Err(Error::new( - ErrorKind::InvalidData, - format!("invalid CA value: {}", value), - )); + return Err(Error::new(format!("invalid CA value: {}", value))); } }; Ok(capability) @@ -175,10 +169,10 @@ pub enum ADSBMessage { impl ADSBMessage { pub fn decode(data: &[u8]) -> Result { if data.len() != 7 { - return Err(Error::new( - ErrorKind::InvalidInput, - format!("ME field must be 7 bytes, received {}", data.len()), - )); + 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; @@ -195,10 +189,10 @@ impl ADSBMessage { 29 => ADSBMessage::TargetState(TargetState::decode(data)?), 31 => ADSBMessage::AircraftOperationStatus(AircraftOperationStatus::decode(data)?), _ => { - return Err(Error::new( - ErrorKind::InvalidData, - format!("unsupported ADS-B type_code {}", type_code), - )); + return Err(Error::new(format!( + "unsupported ADS-B type_code {}", + type_code + ))); } }; diff --git a/adsb/adsb_lib/src/lib.rs b/adsb/src/hex.rs similarity index 50% rename from adsb/adsb_lib/src/lib.rs rename to adsb/src/hex.rs index b958696..0149d55 100644 --- a/adsb/adsb_lib/src/lib.rs +++ b/adsb/src/hex.rs @@ -1,15 +1,12 @@ -use std::io::{Error, ErrorKind, Result}; +use crate::error::Error; -pub mod adsb_frame; -pub mod device; - -pub fn hex_to_bytes(s: &str) -> Result> { +pub fn hex_to_bytes(s: &str) -> crate::error::Result> { let bytes = s.as_bytes(); if bytes.len() % 2 != 0 { - return Err(Error::new( - ErrorKind::InvalidInput, - format!("hex string must have even length, got {}", bytes.len()), - )); + return Err(Error::new(format!( + "hex string must have even length, got {}", + bytes.len() + ))); } let mut out = Vec::with_capacity(bytes.len() / 2); @@ -17,19 +14,19 @@ pub fn hex_to_bytes(s: &str) -> Result> { let hi = match hex_val(chunk[0]) { Some(hi) => hi, None => { - return Err(Error::new( - ErrorKind::InvalidInput, - format!("invalid hex char '{}'", chunk[0] as char), - )); + 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( - ErrorKind::InvalidInput, - format!("invalid hex char '{}'", chunk[1] as char), - )); + return Err(Error::new(format!( + "invalid hex char '{}'", + chunk[1] as char + ))); } }; out.push((hi << 4) | lo); diff --git a/adsb/src/main.rs b/adsb/src/main.rs new file mode 100644 index 0000000..799aa68 --- /dev/null +++ b/adsb/src/main.rs @@ -0,0 +1,68 @@ +mod device; +mod error; +mod frame; +mod hex; + +use error::Result; +use crate::device::RtlSdrDevice; +use clap::Parser; +use rusb::TransferType; +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, + + /// Connect to the USB device + #[arg(short = 'c', long, action)] + connect: bool, + + /// List USB devices + #[arg(short = 'l', long, action)] + list: bool, + + /// Enable debug logging + #[arg(short = 'D', long, action)] + debug: bool, +} + +fn main() -> Result<()> { + let args = ReceiverArgs::parse(); + + let default_filter = if args.debug { + "warn,adsb=debug" + } else { + "warn,adsb=info" + }; + + env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", default_filter)); + + // Handle connection + if args.connect { + log::info!("Connecting to device"); + let mut device = RtlSdrDevice::open(0x0BDA, 0x2832)?; + device.read(TransferType::Bulk) + } + // List devices + else if args.list { + RtlSdrDevice::list() + } + // 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 buf = hex_to_bytes(&hex_string)?; + let frame = ADSBFrame::decode(&buf)?; + + log::info!("{}", frame); + Ok(()) + } else { + log::warn!("No connection specified"); + Ok(()) + } +}