Format adsb code
This commit is contained in:
248
adsb/src/device.rs
Normal file
248
adsb/src/device.rs
Normal file
@@ -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<Context>,
|
||||
device_desc: DeviceDescriptor,
|
||||
device: Device<Context>,
|
||||
}
|
||||
|
||||
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<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()?;
|
||||
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<T: UsbContext>(
|
||||
handle: &DeviceHandle<T>,
|
||||
device_desc: &DeviceDescriptor,
|
||||
offset: &str,
|
||||
full: bool,
|
||||
) -> Result<String> {
|
||||
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<T: UsbContext>(
|
||||
device: &Device<T>,
|
||||
device_desc: &DeviceDescriptor,
|
||||
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(Self {
|
||||
config: config_desc.number(),
|
||||
interface: interface_desc.interface_number(),
|
||||
setting: interface_desc.setting_number(),
|
||||
address: endpoint_desc.address(),
|
||||
transfer_type,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn read<T: UsbContext>(&self, handle: &mut DeviceHandle<T>) -> 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<T: UsbContext>(&self, handle: &mut DeviceHandle<T>) -> 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(())
|
||||
}
|
||||
}
|
||||
38
adsb/src/error.rs
Normal file
38
adsb/src/error.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
473
adsb/src/frame.rs
Normal file
473
adsb/src/frame.rs
Normal file
@@ -0,0 +1,473 @@
|
||||
use crate::hex_to_bytes;
|
||||
use std::fmt::Display;
|
||||
use crate::error::{Result, Error};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ADSBFrame {
|
||||
pub raw_frame: String,
|
||||
/// Downlink format (DF, 5 bits)
|
||||
pub downlink_format: u8,
|
||||
/// Transponder capability (CA, 3 bits)
|
||||
pub capability: Capability,
|
||||
/// Unique aircraft number (ICAO, 24 bits)
|
||||
pub icao: String,
|
||||
/// Message (ME, 56 bits)
|
||||
pub message: ADSBMessage,
|
||||
/// Parity/Interrogator ID/Checksum (PI, 24 bits)
|
||||
pub parity: u32,
|
||||
}
|
||||
|
||||
impl ADSBFrame {
|
||||
/// Parse exactly 14 bytes (112 bits) of raw ADS-B ES data into its fields
|
||||
///
|
||||
/// [ DF:5 ][ CA:3 ][ ICAO:24 ][ ME:56 ][ PI:24 ]
|
||||
pub fn decode(frame: &[u8]) -> Result<ADSBFrame> {
|
||||
if frame.len() != 14 {
|
||||
return Err(Error::new(format!(
|
||||
"expected 14 bytes, received {}",
|
||||
frame.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut raw_frame = "".to_string();
|
||||
for byte in frame {
|
||||
raw_frame.push_str(&format!("{:02x}", byte).to_uppercase());
|
||||
}
|
||||
|
||||
// Decode the downlink format by discarding the lower 3 bits
|
||||
let downlink_format = &frame[0] >> 3;
|
||||
if downlink_format != 17 {
|
||||
return Err(Error::new(format!(
|
||||
"downlink format {} is not currently supported",
|
||||
downlink_format
|
||||
)));
|
||||
}
|
||||
|
||||
// Decode the capability by masking off everything but the lower 3 bits
|
||||
let capability_value = &frame[0] & 0b0000_0111;
|
||||
let capability = Capability::try_from(capability_value)?;
|
||||
|
||||
let icao = Self::decode_icao(&frame[1..=3])?;
|
||||
let message = ADSBMessage::decode(&frame[4..=10])?;
|
||||
let parity = Self::decode_parity(&frame[11..])?;
|
||||
|
||||
Ok(Self {
|
||||
raw_frame,
|
||||
downlink_format,
|
||||
capability,
|
||||
icao,
|
||||
message,
|
||||
parity,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Result<Vec<u8>> {
|
||||
Ok(hex_to_bytes(&self.raw_frame)?)
|
||||
}
|
||||
|
||||
fn decode_icao(data: &[u8]) -> Result<String> {
|
||||
if data.len() != 3 {
|
||||
return Err(Error::new(format!(
|
||||
"ICAO must be 3 bytes, received {}",
|
||||
data.len()
|
||||
)));
|
||||
}
|
||||
let s = data
|
||||
.iter()
|
||||
.map(|b| format!("{:02X}", b))
|
||||
.collect::<String>();
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn decode_parity(data: &[u8]) -> Result<u32> {
|
||||
if data.len() != 3 {
|
||||
return Err(Error::new(format!(
|
||||
"parity must be 3 bytes, received {}",
|
||||
data.len()
|
||||
)));
|
||||
}
|
||||
let p = ((data[0] as u32) << 16) | ((data[1] as u32) << 8) | (data[2] as u32);
|
||||
Ok(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ADSBFrame {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Frame: {}\
|
||||
\nDF: {}\
|
||||
\nCA: {:?}\
|
||||
\nICAO: {}\
|
||||
\nME: {:?}\
|
||||
\nPI: {}",
|
||||
self.raw_frame, self.downlink_format, &self.capability, self.icao, &self.message, self.parity
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transponder Capability (CA) codes from the ADS-B spec
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Capability {
|
||||
/// 0: Level 1 transponder
|
||||
Level1,
|
||||
/// 1-3: Reserved
|
||||
Reserved(u8),
|
||||
/// 4: Level 2+ transponder, ground (can set CA=7)
|
||||
Level2OnGround,
|
||||
/// 5: Level 2+ transponder, airborne (can set CA=7)
|
||||
Level2Airborne,
|
||||
/// 6: Level 2+ transponder, either ground or airborne (can set CA=7)
|
||||
Level2Either,
|
||||
/// 7: Downlink Request = 0, or Flight Status = 2,3,4,5
|
||||
DownlinkRequestOrFlightStatus,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Capability {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self> {
|
||||
let capability = match value {
|
||||
0 => Capability::Level1,
|
||||
1..=3 => Capability::Reserved(value),
|
||||
4 => Capability::Level2OnGround,
|
||||
5 => Capability::Level2Airborne,
|
||||
6 => Capability::Level2Either,
|
||||
7 => Capability::DownlinkRequestOrFlightStatus,
|
||||
_ => {
|
||||
return Err(Error::new(format!("invalid CA value: {}", value)));
|
||||
}
|
||||
};
|
||||
Ok(capability)
|
||||
}
|
||||
}
|
||||
|
||||
// fn get_bits(data: &[u8], from: usize, len: usize) -> u32 {
|
||||
// let mut val = 0;
|
||||
// for bit in 0..len {
|
||||
// let idx = from + bit;
|
||||
// let byte = data[idx / 8];
|
||||
// let shift = 7 - (idx % 8);
|
||||
// let bit_val = ((byte >> shift) & 0x01) as u32;
|
||||
// val = (val << 1) | bit_val;
|
||||
// }
|
||||
// val
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ADSBMessage {
|
||||
AircraftIdentification(AircraftIdentification),
|
||||
SurfacePosition(SurfacePosition),
|
||||
AirbornePosition(AirbornePosition),
|
||||
AirborneVelocities(AirborneVelocities),
|
||||
Reserved(u8),
|
||||
AircraftStatus(AircraftStatus),
|
||||
TargetState(TargetState),
|
||||
AircraftOperationStatus(AircraftOperationStatus),
|
||||
}
|
||||
|
||||
impl ADSBMessage {
|
||||
pub fn decode(data: &[u8]) -> Result<ADSBMessage> {
|
||||
if data.len() != 7 {
|
||||
return Err(Error::new(format!(
|
||||
"ME field must be 7 bytes, received {}",
|
||||
data.len()
|
||||
)));
|
||||
}
|
||||
// First 5 bits is the type code
|
||||
let type_code = data[0] >> 3;
|
||||
let message = match type_code {
|
||||
1..=4 => {
|
||||
ADSBMessage::AircraftIdentification(AircraftIdentification::decode(type_code, data)?)
|
||||
}
|
||||
5..=8 => ADSBMessage::SurfacePosition(SurfacePosition::decode(data)?),
|
||||
9..=18 => ADSBMessage::AirbornePosition(AirbornePosition::decode(type_code, data)?),
|
||||
19 => ADSBMessage::AirborneVelocities(AirborneVelocities::decode(data)?),
|
||||
20..=22 => ADSBMessage::AirbornePosition(AirbornePosition::decode(type_code, data)?),
|
||||
23..=27 => ADSBMessage::Reserved(type_code),
|
||||
28 => ADSBMessage::AircraftStatus(AircraftStatus::decode(data)?),
|
||||
29 => ADSBMessage::TargetState(TargetState::decode(data)?),
|
||||
31 => ADSBMessage::AircraftOperationStatus(AircraftOperationStatus::decode(data)?),
|
||||
_ => {
|
||||
return Err(Error::new(format!(
|
||||
"unsupported ADS-B type_code {}",
|
||||
type_code
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AircraftIdentification {
|
||||
type_code: u8,
|
||||
emitter_category: u8,
|
||||
wake_vortex_category: WakeVortexCategory,
|
||||
callsign: String,
|
||||
}
|
||||
|
||||
impl AircraftIdentification {
|
||||
pub fn decode(type_code: u8, data: &[u8]) -> Result<Self> {
|
||||
// Byte 0: [ TC(5 bits) | emitter_category (3 bits) ]
|
||||
let emitter_category = data[0] & 0x07;
|
||||
|
||||
// 56 bit buffer for message
|
||||
let mut bits: u64 = 0;
|
||||
for &b in data {
|
||||
bits = (bits << 8) | b as u64;
|
||||
}
|
||||
|
||||
let mut callsign = String::with_capacity(8);
|
||||
for i in 0..8 {
|
||||
let shift = 48 - 6 * (i + 1);
|
||||
let raw6 = ((bits >> shift) & 0x3F) as u8;
|
||||
let ch = match raw6 {
|
||||
1..=26 => (b'A' + (raw6 - 1)) as char,
|
||||
48..=57 => (b'0' + (raw6 - 48)) as char,
|
||||
32 => ' ',
|
||||
_ => continue,
|
||||
};
|
||||
callsign.push(ch);
|
||||
}
|
||||
|
||||
// trim any trailing spaces
|
||||
let callsign = callsign.trim_end().to_string();
|
||||
|
||||
Ok(Self {
|
||||
type_code,
|
||||
emitter_category,
|
||||
wake_vortex_category: WakeVortexCategory::from_tc_ca(type_code, emitter_category),
|
||||
callsign,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WakeVortexCategory {
|
||||
NoInfo,
|
||||
SurfaceEmergencyVehicle,
|
||||
SurfaceServiceVehicle,
|
||||
GroundObstruction,
|
||||
Glider,
|
||||
LighterThanAir,
|
||||
Parachutist,
|
||||
Ultralight,
|
||||
Reserved,
|
||||
UnmannedAerialVehicle,
|
||||
SpaceVehicle,
|
||||
Light,
|
||||
Medium1,
|
||||
Medium2,
|
||||
HighVortex,
|
||||
Heavy,
|
||||
HighPerformance,
|
||||
Rotorcraft,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl WakeVortexCategory {
|
||||
pub fn from_tc_ca(type_code: u8, emitter_category: u8) -> Self {
|
||||
match (type_code, emitter_category) {
|
||||
(_, 0) => WakeVortexCategory::NoInfo,
|
||||
(2, 1) => WakeVortexCategory::SurfaceEmergencyVehicle,
|
||||
(2, 3) => WakeVortexCategory::SurfaceServiceVehicle,
|
||||
(2, 4..=7) => WakeVortexCategory::GroundObstruction,
|
||||
(3, 1) => WakeVortexCategory::Glider,
|
||||
(3, 2) => WakeVortexCategory::LighterThanAir,
|
||||
(3, 3) => WakeVortexCategory::Parachutist,
|
||||
(3, 4) => WakeVortexCategory::Ultralight,
|
||||
(3, 5) => WakeVortexCategory::Reserved,
|
||||
(3, 6) => WakeVortexCategory::UnmannedAerialVehicle,
|
||||
(3, 7) => WakeVortexCategory::SpaceVehicle,
|
||||
(4, 1) => WakeVortexCategory::Light,
|
||||
(4, 2) => WakeVortexCategory::Medium1,
|
||||
(4, 3) => WakeVortexCategory::Medium2,
|
||||
(4, 4) => WakeVortexCategory::HighVortex,
|
||||
(4, 5) => WakeVortexCategory::Heavy,
|
||||
(4, 6) => WakeVortexCategory::HighPerformance,
|
||||
(4, 7) => WakeVortexCategory::Rotorcraft,
|
||||
_ => WakeVortexCategory::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SurfacePosition {}
|
||||
|
||||
impl SurfacePosition {
|
||||
pub fn decode(_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AirbornePosition {}
|
||||
|
||||
impl AirbornePosition {
|
||||
pub fn decode(_type_code: u8, _data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AirborneVelocities {}
|
||||
|
||||
impl AirborneVelocities {
|
||||
pub fn decode(_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AircraftStatus {}
|
||||
|
||||
impl AircraftStatus {
|
||||
pub fn decode(_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TargetState {}
|
||||
|
||||
impl TargetState {
|
||||
pub fn decode(_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AircraftOperationStatus {}
|
||||
|
||||
impl AircraftOperationStatus {
|
||||
pub fn decode(_data: &[u8]) -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_decode_df_17_aircraft_information() {
|
||||
let input = [
|
||||
0x8D, 0x48, 0x40, 0xD6, 0x20, 0x2C, 0xC3, 0x71, 0xC3, 0x1C, 0x32, 0xCE, 0x05, 0x76,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "4840D6");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 0);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::NoInfo);
|
||||
assert_eq!(id.callsign, "KLM10102");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 13501814);
|
||||
|
||||
let input = [
|
||||
0x8D, 0x48, 0x40, 0xD6, 0x20, 0x2C, 0xC3, 0x71, 0xC3, 0x2C, 0xE0, 0x57, 0x60, 0x98,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "4840D6");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 0);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::NoInfo);
|
||||
assert_eq!(id.callsign, "KLM1023");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 5726360);
|
||||
|
||||
let input = [
|
||||
0x8D, 0x7C, 0x71, 0x81, 0x21, 0x5D, 0x01, 0xA0, 0x82, 0x08, 0x20, 0x4D, 0x8B, 0xF1,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "7C7181");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 1);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Light);
|
||||
assert_eq!(id.callsign, "WPF");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 5082097);
|
||||
|
||||
let input = [
|
||||
0x8D, 0x7C, 0x77, 0x45, 0x22, 0x61, 0x51, 0xA0, 0x82, 0x08, 0x20, 0x5C, 0xE9, 0xC2,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "7C7745");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 2);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Medium1);
|
||||
assert_eq!(id.callsign, "XUF");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 6089154);
|
||||
|
||||
let input = [
|
||||
0x8D, 0x7C, 0x80, 0xAD, 0x23, 0x58, 0xF6, 0xB1, 0xE3, 0x5C, 0x60, 0xFF, 0x19, 0x25,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "7C80AD");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 3);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Medium2);
|
||||
assert_eq!(id.callsign, "VOZ1851");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 16718117);
|
||||
|
||||
let input = [
|
||||
0x8D, 0x7C, 0x14, 0x65, 0x25, 0x44, 0x60, 0x74, 0xDF, 0x58, 0x20, 0x73, 0x8E, 0x90,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
assert_eq!(frame.downlink_format, 17);
|
||||
assert_eq!(frame.capability, Capability::Level2Airborne);
|
||||
assert_eq!(frame.icao, "7C1465");
|
||||
match frame.message {
|
||||
ADSBMessage::AircraftIdentification(ref id) => {
|
||||
assert_eq!(id.type_code, 4);
|
||||
assert_eq!(id.emitter_category, 5);
|
||||
assert_eq!(id.wake_vortex_category, WakeVortexCategory::Heavy);
|
||||
assert_eq!(id.callsign, "QFA475");
|
||||
}
|
||||
_ => panic!("expected AircraftIdentification"),
|
||||
}
|
||||
assert_eq!(frame.parity, 7573136);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_df_17_operation_status() {
|
||||
let input = [
|
||||
0x8D, 0x89, 0x65, 0xD2, 0xF8, 0x21, 0x00, 0x02, 0x00, 0x49, 0xB8, 0x94, 0xA4, 0x5F,
|
||||
];
|
||||
let frame = ADSBFrame::decode(&input).unwrap();
|
||||
dbg!(frame);
|
||||
}
|
||||
}
|
||||
44
adsb/src/hex.rs
Normal file
44
adsb/src/hex.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::error::Error;
|
||||
|
||||
pub fn hex_to_bytes(s: &str) -> crate::error::Result<Vec<u8>> {
|
||||
let bytes = s.as_bytes();
|
||||
if bytes.len() % 2 != 0 {
|
||||
return Err(Error::new(format!(
|
||||
"hex string must have even length, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(bytes.len() / 2);
|
||||
for chunk in bytes.chunks(2) {
|
||||
let hi = match hex_val(chunk[0]) {
|
||||
Some(hi) => hi,
|
||||
None => {
|
||||
return Err(Error::new(format!(
|
||||
"invalid hex char '{}'",
|
||||
chunk[0] as char
|
||||
)));
|
||||
}
|
||||
};
|
||||
let lo = match hex_val(chunk[1]) {
|
||||
Some(lo) => lo,
|
||||
None => {
|
||||
return Err(Error::new(format!(
|
||||
"invalid hex char '{}'",
|
||||
chunk[1] as char
|
||||
)));
|
||||
}
|
||||
};
|
||||
out.push((hi << 4) | lo);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn hex_val(b: u8) -> Option<u8> {
|
||||
match b {
|
||||
b'0'..=b'9' => Some(b - b'0'),
|
||||
b'a'..=b'f' => Some(b - b'a' + 10),
|
||||
b'A'..=b'F' => Some(b - b'A' + 10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
68
adsb/src/main.rs
Normal file
68
adsb/src/main.rs
Normal file
@@ -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<String>,
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user