Adding base for adsb

This commit is contained in:
2025-04-21 19:15:11 -04:00
parent 06f9a96498
commit 95e4b8abf3
15 changed files with 1185 additions and 9 deletions

View File

@@ -24,12 +24,25 @@ psql: ## Connect to the PSQL DB
format-api: ## Format code format-api: ## Format code
@cd api && cargo fmt @cd api && cargo fmt
build-api: ## Build the project build-api: ## Build the API project
@cd api && cargo build @cd api && cargo build
run-api: ## Run the API project run-api: ## Run the API project
@cd api && cargo run -p api @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 # # UI Commands #
################# #################

View File

@@ -81,6 +81,12 @@ The following resources were used to help decode METARS.
### OpenMapTiles ### OpenMapTiles
[Generate Vector Tiles](https://openmaptiles.org/docs/generate/generate-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 ### Other data
- https://www.faa.gov/air_traffic/weather/asos - https://www.faa.gov/air_traffic/weather/asos

8
adsb/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[workspace]
members = [
"adsb_lib",
"adsb_recv",
"adsb_sim"
]
resolver = "2"
package.version = "0.1.0"

7
adsb/README.md Normal file
View File

@@ -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`

4
adsb/adsb_lib/Cargo.toml Normal file
View File

@@ -0,0 +1,4 @@
[package]
name = "adsb_lib"
version = "0.1.0"
edition = "2021"

119
adsb/adsb_lib/src/lib.rs Normal file
View File

@@ -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<Vec<u8>>;
/// 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<usize>;
}
pub fn run<S: RtlDevice>(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 “halfbit” 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!();
}
}

View File

@@ -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"] }

498
adsb/adsb_recv/src/adsb.rs Normal file
View File

@@ -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<ADSBFrame> {
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<String> {
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::<String>();
Ok(s)
}
fn decode_parity(data: &[u8]) -> Result<u32> {
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 ADSB spec
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Capability {
/// 0: Level 1 transponder
Level1,
/// 13: Reserved
Reserved(u8),
/// 4: Level 2+ transponder, onground (can set CA=7)
Level2OnGround,
/// 5: Level 2+ transponder, airborne (can set CA=7)
Level2Airborne,
/// 6: Level 2+ transponder, either onground 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(
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<ADSBMessage> {
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 ADSB 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 AirbornePositionBaro {
}
impl AirbornePositionBaro {
pub fn decode(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 AirbornePositionGNSS {
}
impl AirbornePositionGNSS {
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);
}
}

View File

@@ -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<String>,
}
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 RTLSDR mode");
let mut device = RusbRtl::open()?;
run(&mut device)
}
}
fn hex_to_bytes(s: &str) -> Result<Vec<u8>> {
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<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,
}
}

View File

@@ -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<Context>,
}
impl RusbRtl {
/// Open the USB device, claim interface 0, and return a wrapper
pub fn open() -> Result<Self> {
// 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<Vec<u8>> {
// 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<Vec<u8>> {
self.control_in(b_request, length)
.map_err(|err| Error::new(ErrorKind::Other, err))
}
fn read_bulk(&mut self, buffer: &mut [u8]) -> Result<usize> {
self.handle
.read_bulk(DATA_ENDPOINT_ADDRESS, buffer, Duration::from_secs(1))
.map_err(|err| Error::new(ErrorKind::Other, err))
}
}

View File

@@ -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<Self> {
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<Vec<u8>> {
// 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<Vec<u8>> {
self.send_message(TAG_CTRL_IN, b_request, &[])?;
self.receive_length_prefixed_payload()
}
fn read_bulk(&mut self, buffer: &mut [u8]) -> Result<usize> {
// 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)
}
}

8
adsb/adsb_sim/Cargo.toml Normal file
View File

@@ -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"] }

189
adsb/adsb_sim/src/main.rs Normal file
View File

@@ -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 commandline 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<u8> {
// 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
}

View File

@@ -5,7 +5,6 @@ edition = "2021"
authors = ["Ben Sherriff <ben@bensherriff.com>"] authors = ["Ben Sherriff <ben@bensherriff.com>"]
repository = "https://gitea.bensherriff.com/bsherriff/aviation" repository = "https://gitea.bensherriff.com/bsherriff/aviation"
readme = "../README.md" 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,6 +1,7 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Postgres, QueryBuilder}; use sqlx::{Postgres, QueryBuilder};
use uuid::Uuid;
use crate::{account::hash, error::ApiResult}; use crate::{account::hash, error::ApiResult};
use crate::db; use crate::db;
@@ -8,7 +9,7 @@ pub const ADMIN_ROLE: &str = "ADMIN";
pub const USER_ROLE: &str = "USER"; pub const USER_ROLE: &str = "USER";
const TABLE_NAME: &str = "users"; const TABLE_NAME: &str = "users";
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize)]
pub struct RegisterRequest { pub struct RegisterRequest {
pub email: String, pub email: String,
pub password: String, pub password: String,
@@ -20,49 +21,59 @@ impl RegisterRequest {
pub fn to_user(self) -> ApiResult<User> { pub fn to_user(self) -> ApiResult<User> {
let password_hash = hash(&self.password)?; let password_hash = hash(&self.password)?;
Ok(User { Ok(User {
id: Uuid::new_v4(),
email: self.email.to_lowercase(), email: self.email.to_lowercase(),
emailVerified: false,
password_hash, password_hash,
role: USER_ROLE.to_string(), role: USER_ROLE.to_string(),
first_name: self.first_name, first_name: self.first_name,
last_name: self.last_name, last_name: self.last_name,
avatar: None,
updated_at: Utc::now(), updated_at: Utc::now(),
created_at: Utc::now(), created_at: Utc::now(),
}) })
} }
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize)]
pub struct LoginRequest { pub struct LoginRequest {
pub email: String, pub email: String,
pub password: String, pub password: String,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize)]
pub struct UserResponse { pub struct UserResponse {
pub email: String, pub id: Uuid,
pub email_verified: bool,
pub role: String, pub role: String,
pub first_name: String, pub first_name: String,
pub last_name: String, pub last_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<String>,
} }
impl From<User> for UserResponse { impl From<User> for UserResponse {
fn from(user: User) -> Self { fn from(user: User) -> Self {
UserResponse { UserResponse {
email: user.email, id: user.id,
email_verified: user.emailVerified,
role: user.role, role: user.role,
first_name: user.first_name, first_name: user.first_name,
last_name: user.last_name, last_name: user.last_name,
avatar: user.avatar,
} }
} }
} }
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] #[derive(Debug, Deserialize, sqlx::FromRow)]
pub struct UpdateUser { pub struct UpdateUser {
pub id: Uuid,
pub email: Option<String>, pub email: Option<String>,
pub password: Option<String>, pub password: Option<String>,
pub role: Option<String>, pub role: Option<String>,
pub first_name: Option<String>, pub first_name: Option<String>,
pub last_name: Option<String>, pub last_name: Option<String>,
pub avatar: Option<String>,
} }
impl UpdateUser { impl UpdateUser {
@@ -123,13 +134,16 @@ impl UpdateUser {
} }
} }
#[derive(Serialize, Deserialize, sqlx::FromRow, Debug)] #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct User { pub struct User {
pub id: Uuid,
pub email: String, pub email: String,
pub emailVerified: bool,
pub password_hash: String, pub password_hash: String,
pub role: String, pub role: String,
pub first_name: String, pub first_name: String,
pub last_name: String, pub last_name: String,
pub avatar: Option<String>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
} }