diff --git a/Makefile b/Makefile index cd397ee..1e1308a 100644 --- a/Makefile +++ b/Makefile @@ -24,12 +24,25 @@ psql: ## Connect to the PSQL DB format-api: ## Format code @cd api && cargo fmt -build-api: ## Build the project +build-api: ## Build the API project @cd api && cargo build run-api: ## Run the API project @cd api && cargo run -p api +################## +# ADS-B Commands # +################## + +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 + ################# # UI Commands # ################# diff --git a/README.md b/README.md index 24e1e24..147db9a 100755 --- a/README.md +++ b/README.md @@ -81,6 +81,12 @@ The following resources were used to help decode METARS. ### OpenMapTiles [Generate Vector Tiles](https://openmaptiles.org/docs/generate/generate-openmaptiles/) +### ADS-B +- https://blog.exploit.org/ads-b-guide-demodulation-and-decoding/ +- https://mode-s.org/1090mhz/index.html +- https://planewave.github.io/posts/rtlsdr/ +- http://jasonplayne.com:8080/# + ### Other data - https://www.faa.gov/air_traffic/weather/asos diff --git a/adsb/Cargo.toml b/adsb/Cargo.toml new file mode 100644 index 0000000..f91765a --- /dev/null +++ b/adsb/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +members = [ + "adsb_lib", + "adsb_recv", + "adsb_sim" +] +resolver = "2" +package.version = "0.1.0" \ No newline at end of file diff --git a/adsb/README.md b/adsb/README.md new file mode 100644 index 0000000..130192f --- /dev/null +++ b/adsb/README.md @@ -0,0 +1,7 @@ +## Simulation Mode +`cargo run -p adsb_sim --` + +`cargo run -p adsb_recv -- --sim` + +## Decode +`cargo run -p adsb_recv -- --decode 8D4840D6202CC371C32CE0576098` diff --git a/adsb/adsb_lib/Cargo.toml b/adsb/adsb_lib/Cargo.toml new file mode 100644 index 0000000..bdc6fa5 --- /dev/null +++ b/adsb/adsb_lib/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "adsb_lib" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/adsb/adsb_lib/src/lib.rs b/adsb/adsb_lib/src/lib.rs new file mode 100644 index 0000000..adcbd77 --- /dev/null +++ b/adsb/adsb_lib/src/lib.rs @@ -0,0 +1,119 @@ +use std::io::Result; + +pub trait RtlDevice { + /// Send a control message to the device + fn control_send(&mut self, b_request: u8, data: &[u8]) -> Result<()>; + /// Receive a control message from a device + fn control_recv(&mut self, b_request: u8, length: usize) -> 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]) -> Result; +} + +pub fn run(device: &mut S) -> 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!(); + } +} \ No newline at end of file diff --git a/adsb/adsb_recv/Cargo.toml b/adsb/adsb_recv/Cargo.toml new file mode 100644 index 0000000..3d0e052 --- /dev/null +++ b/adsb/adsb_recv/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "adsb_recv" +version = "0.1.0" +edition = "2021" + +[dependencies] +adsb_lib = { path = "../adsb_lib" } +rusb = "0.9.4" +clap = { version = "4.5.37", features = ["derive"] } \ No newline at end of file diff --git a/adsb/adsb_recv/src/adsb.rs b/adsb/adsb_recv/src/adsb.rs new file mode 100644 index 0000000..345c23b --- /dev/null +++ b/adsb/adsb_recv/src/adsb.rs @@ -0,0 +1,498 @@ +use std::fmt::Display; +use std::io::{Error, ErrorKind, Result}; + +#[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 { + if frame.len() != 14 { + return Err(Error::new( + ErrorKind::InvalidInput, + 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( + ErrorKind::Unsupported, + 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 + }) + } + + 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()))); + } + let s = data.iter() + .map(|b| format!("{:02X}", b)) + .collect::(); + Ok(s) + } + + 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()))); + } + 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, on‑ground (can set CA=7) + Level2OnGround, + /// 5: Level 2+ transponder, airborne (can set CA=7) + Level2Airborne, + /// 6: Level 2+ transponder, either on‑ground or airborne (can set CA=7) + Level2Either, + /// 7: Downlink Request = 0, or Flight Status = 2,3,4,5 + DownlinkRequestOrFlightStatus, +} + +impl TryFrom for Capability { + type Error = Error; + + fn try_from(value: u8) -> Result { + 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( + ErrorKind::InvalidData, + 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), + AirbornePositionBaro(AirbornePositionBaro), + AirborneVelocities(AirborneVelocities), + AirbornePositionGNSS(AirbornePositionGNSS), + Reserved(u8), + AircraftStatus(AircraftStatus), + TargetState(TargetState), + AircraftOperationStatus(AircraftOperationStatus), +} + +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()), + )); + } + // 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::AirbornePositionBaro(AirbornePositionBaro::decode(data)?), + 19 => ADSBMessage::AirborneVelocities(AirborneVelocities::decode(data)?), + 20..=22 => ADSBMessage::AirbornePositionGNSS(AirbornePositionGNSS::decode(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( + ErrorKind::InvalidData, + 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 { + // 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 { + Ok(Self {}) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AirbornePositionBaro { + +} + +impl AirbornePositionBaro { + pub fn decode(data: &[u8]) -> Result { + Ok(Self {}) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AirborneVelocities { + +} + +impl AirborneVelocities { + pub fn decode(data: &[u8]) -> Result { + Ok(Self {}) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AirbornePositionGNSS { + +} + +impl AirbornePositionGNSS { + pub fn decode(data: &[u8]) -> Result { + Ok(Self {}) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AircraftStatus { + +} + +impl AircraftStatus { + pub fn decode(data: &[u8]) -> Result { + Ok(Self {}) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TargetState { + +} + +impl TargetState { + pub fn decode(data: &[u8]) -> Result { + Ok(Self {}) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AircraftOperationStatus { + +} + +impl AircraftOperationStatus { + pub fn decode(data: &[u8]) -> Result { + 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); + } +} \ No newline at end of file diff --git a/adsb/adsb_recv/src/main.rs b/adsb/adsb_recv/src/main.rs new file mode 100644 index 0000000..99bffdd --- /dev/null +++ b/adsb/adsb_recv/src/main.rs @@ -0,0 +1,84 @@ +mod tcp_rtl; +mod rusb_rtl; +mod adsb; + +use std::io::{Error, ErrorKind, Result}; +use clap::Parser; +use adsb_lib::run; +use crate::adsb::ADSBFrame; +use crate::rusb_rtl::RusbRtl; +use crate::tcp_rtl::TcpRtl; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct ReceiverArgs { + #[arg(long)] + sim: bool, + #[arg(long, default_value = "127.0.0.1:9999", requires = "sim")] + addr: String, + #[arg(long)] + decode: Option, +} + +fn main() -> Result<()> { + let args = ReceiverArgs::parse(); + + if let Some(mut hexString) = args.decode { + if let Some(stripped) = hexString.strip_prefix("0x") { + hexString = stripped.to_string(); + } + let buf = hex_to_bytes(&hexString)?; + let frame = ADSBFrame::decode(&buf)?; + + println!("{}", frame); + return Ok(()); + } + + if args.sim { + println!("Starting in SIMULATION mode, connecting to {}", args.addr); + let mut device = TcpRtl::connect(&args.addr)?; + run(&mut device) + } else { + println!("Starting in REAL RTL‑SDR mode"); + let mut device = RusbRtl::open()?; + run(&mut device) + } +} + +fn hex_to_bytes(s: &str) -> 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()))); + } + + 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( + ErrorKind::InvalidInput, + 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) + )) + }; + out.push((hi << 4) | lo); + } + Ok(out) +} + +fn hex_val(b: u8) -> Option { + 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, + } +} \ No newline at end of file diff --git a/adsb/adsb_recv/src/rusb_rtl.rs b/adsb/adsb_recv/src/rusb_rtl.rs new file mode 100644 index 0000000..b364d10 --- /dev/null +++ b/adsb/adsb_recv/src/rusb_rtl.rs @@ -0,0 +1,100 @@ +use std::io::{Error, ErrorKind, Result}; +use std::time::Duration; +use rusb::{request_type, Context, DeviceHandle, Direction, Recipient, RequestType, UsbContext}; +use adsb_lib::RtlDevice; + +// 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 RusbRtl { + handle: DeviceHandle, +} + +impl RusbRtl { + /// Open the USB device, claim interface 0, and return a wrapper + pub fn open() -> Result { + // Create a new libusb context + let mut ctx = Context::new().map_err(|err| Error::new(ErrorKind::Other, err))?; + + // Find and open the RTL-SDR by VID/PID + let mut 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, + Duration::from_secs(1)) + .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, + Duration::from_secs(1))?; + + // Truncate to the actual length returned by the device + buffer.truncate(n); + Ok(buffer) + } +} + +impl RtlDevice for RusbRtl { + 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, Duration::from_secs(1)) + .map_err(|err| Error::new(ErrorKind::Other, err)) + } +} diff --git a/adsb/adsb_recv/src/tcp_rtl.rs b/adsb/adsb_recv/src/tcp_rtl.rs new file mode 100644 index 0000000..950ad0c --- /dev/null +++ b/adsb/adsb_recv/src/tcp_rtl.rs @@ -0,0 +1,118 @@ +use std::io::{Error, ErrorKind, Read, Result, Write}; +use std::net::TcpStream; +use adsb_lib::RtlDevice; + +// 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 TcpRtl { + socket: TcpStream, +} + +impl TcpRtl { + /// Connect to a remote RTL-SDR server at the given address + pub fn connect(addr: &str) -> Result { + let socket = TcpStream::connect(addr)?; + Ok(TcpRtl { socket }) + } + + /// 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 RtlDevice for TcpRtl { + 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) + } +} \ No newline at end of file diff --git a/adsb/adsb_sim/Cargo.toml b/adsb/adsb_sim/Cargo.toml new file mode 100644 index 0000000..608eb6f --- /dev/null +++ b/adsb/adsb_sim/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "adsb_sim" +version = "0.1.0" +edition = "2021" + +[dependencies] +adsb_lib = { path = "../adsb_lib" } +clap = { version = "4.5.37", features = ["derive"] } \ No newline at end of file diff --git a/adsb/adsb_sim/src/main.rs b/adsb/adsb_sim/src/main.rs new file mode 100644 index 0000000..092bd79 --- /dev/null +++ b/adsb/adsb_sim/src/main.rs @@ -0,0 +1,189 @@ +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::thread; +use std::time::Duration; +use clap::Parser; + +// 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() { + // Parse command‐line arguments + let args = SimulationArgs::parse(); + + // Build the bind address, e.g. "127.0.0.1:9999" + let bind_address = format!("{}:{}", args.host, args.port); + println!("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 in listener.incoming() { + match incoming { + Ok(client_stream) => { + // Spawn a thread per client + thread::spawn(move || handle_client_connection(client_stream)); + } + Err(err) => eprintln!("Error accepting connection: {}", err), + } + } +} + +/// Handle a single client connection +fn handle_client_connection(mut connection: TcpStream) { + // Track a "current frequency" + let mut current_frequency_hz: u32 = 0; + + loop { + // Read the 4-byte header: [tag:1][bRequest:1][length:2] + 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; + + // Read the optional payload + let mut payload_buffer = vec![0u8; payload_length]; + if payload_length > 0 { + if connection.read_exact(&mut payload_buffer).is_err() { + break; + } + } + + // Dispatch based on the framing tag + match message_tag { + TAG_CONTROL_OUT => { + // Simulate accepting a CONTROL_OUT (e.g. SET_FREQ) + if b_request == 0x02 && payload_buffer.len() == 4 { + current_frequency_hz = u32::from_le_bytes([ + payload_buffer[0], + payload_buffer[1], + payload_buffer[2], + payload_buffer[3] + ]); + println!("SET_FREQ -> {} Hz", current_frequency_hz); + } + // Acknowledge with a single byte = 0 (OK) + connection.write_all(&[0u8]).ok(); + }, + TAG_CONTROL_IN => { + dbg!(message_tag); + // Simulate a CONTROL_IN reply with a fixed pattern + + // Status byte + let _ = connection.write_all(&[0u8]); + + // 2-byte little-endian length + let length_u16 = payload_length as u16; + let _ = connection.write_all(&length_u16.to_le_bytes()); + + // Payload (0x42 repeated) + let reply = vec![0x42; payload_length]; + let _ = connection.write_all(&reply).ok(); + }, + TAG_BULK => { + dbg!(message_tag); + // Generate a ADS-B IQ burst + let iq_samples = generate_adsb_iq_samples(); + let length_u32 = (iq_samples.len() as u32).to_le_bytes(); + + // Send status byte = 0 (OK) + let _ = connection.write_all(&[0u8]); + + // Send 4-byte little-endian length (bulk uses u32) + let _ = connection.write_all(&length_u32); + + // Send the IQ payload + let _ = connection.write_all(&iq_samples); + + // Throttle a bit to simulate real USB/bulk behavior + thread::sleep(Duration::from_millis(10)); + }, + _unknown_tag => { + // On any unrecognized tag, break out + break + }, + } + } + + println!("Connection closed"); +} + +/// 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/api/Cargo.toml b/api/Cargo.toml index 3857b10..e7c3f83 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" authors = ["Ben Sherriff "] repository = "https://gitea.bensherriff.com/bsherriff/aviation" readme = "../README.md" -license = "GPL-3.0-or-later" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/api/src/users/model.rs b/api/src/users/model.rs index 99ca22d..8d89c20 100644 --- a/api/src/users/model.rs +++ b/api/src/users/model.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{Postgres, QueryBuilder}; +use uuid::Uuid; use crate::{account::hash, error::ApiResult}; use crate::db; @@ -8,7 +9,7 @@ pub const ADMIN_ROLE: &str = "ADMIN"; pub const USER_ROLE: &str = "USER"; const TABLE_NAME: &str = "users"; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct RegisterRequest { pub email: String, pub password: String, @@ -20,49 +21,59 @@ impl RegisterRequest { pub fn to_user(self) -> ApiResult { let password_hash = hash(&self.password)?; Ok(User { + id: Uuid::new_v4(), email: self.email.to_lowercase(), + emailVerified: false, password_hash, role: USER_ROLE.to_string(), first_name: self.first_name, last_name: self.last_name, + avatar: None, updated_at: Utc::now(), created_at: Utc::now(), }) } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct LoginRequest { pub email: String, pub password: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize)] pub struct UserResponse { - pub email: String, + pub id: Uuid, + pub email_verified: bool, pub role: String, pub first_name: String, pub last_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar: Option, } impl From for UserResponse { fn from(user: User) -> Self { UserResponse { - email: user.email, + id: user.id, + email_verified: user.emailVerified, role: user.role, first_name: user.first_name, last_name: user.last_name, + avatar: user.avatar, } } } -#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Deserialize, sqlx::FromRow)] pub struct UpdateUser { + pub id: Uuid, pub email: Option, pub password: Option, pub role: Option, pub first_name: Option, pub last_name: Option, + pub avatar: Option, } impl UpdateUser { @@ -123,13 +134,16 @@ impl UpdateUser { } } -#[derive(Serialize, Deserialize, sqlx::FromRow, Debug)] +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct User { + pub id: Uuid, pub email: String, + pub emailVerified: bool, pub password_hash: String, pub role: String, pub first_name: String, pub last_name: String, + pub avatar: Option, pub updated_at: DateTime, pub created_at: DateTime, }