use crate::beast::decode_beast_timestamp;
use flate2::read::GzDecoder;
use rs1090::decode::commb::MessageProcessor;
use rs1090::decode::cpr::{decode_position, AircraftState, Position, UpdateIf};
use rs1090::decode::SensorMetadata;
use rs1090::prelude::*;
use serde::{Deserialize, Serialize};
use sevenz_rust::SevenZReader;
use std::cmp::Reverse;
use std::collections::{BTreeMap, BinaryHeap, HashMap};
use std::io::Read;
use std::path::Path;
use tokio::fs::{self, File};
use tokio::io::AsyncWriteExt;
#[derive(Serialize, Deserialize)]
pub struct JSONEntry {
pub timestamp: f64,
pub rssi: Option<f32>, #[serde(
serialize_with = "rs1090::decode::as_hex",
deserialize_with = "rs1090::decode::from_hex"
)]
pub frame: Vec<u8>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub metadata: Vec<SensorMetadata>,
}
async fn read_input_file(
input_path: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let path = Path::new(input_path);
let extension = path.extension().and_then(|e| e.to_str());
match extension {
Some("7z") => {
let mut archive =
SevenZReader::open(path, sevenz_rust::Password::empty())?;
let mut content = String::new();
archive.for_each_entries(|_entry, reader| {
reader.read_to_string(&mut content)?;
Ok(false) })?;
Ok(content)
}
Some("gz") => {
let file = std::fs::File::open(path)?;
let mut decoder = GzDecoder::new(file);
let mut content = String::new();
decoder.read_to_string(&mut content)?;
Ok(content)
}
_ => {
Ok(tokio::fs::read_to_string(path).await?)
}
}
}
fn parse_beast_csv_line(line: &str) -> Option<JSONEntry> {
let parts: Vec<&str> = line.trim().split(',').collect();
if parts.len() != 2 {
return None;
}
let timestamp: f64 = parts[0].parse().ok()?;
let hex_data = hex::decode(parts[1]).ok()?;
if hex_data.is_empty() || hex_data[0] != 0x1a {
return None;
}
let frame = match hex_data.get(1) {
Some(0x32) if hex_data.len() >= 16 => {
hex_data[9..16].to_vec()
}
Some(0x33) if hex_data.len() >= 23 => {
hex_data[9..23].to_vec()
}
_ => return None,
};
let beast_ts = if hex_data.len() >= 8 {
let ts = decode_beast_timestamp(&hex_data[2..8]);
if ts > 0.0 {
ts
} else {
timestamp
}
} else {
timestamp
};
let _ = beast_ts;
Some(JSONEntry {
timestamp,
rssi: None,
frame,
metadata: vec![],
})
}
async fn process_entries(
mut entries: Vec<JSONEntry>,
aircraft: &mut BTreeMap<ICAO, AircraftState>,
reference: &mut Option<Position>,
update_reference: &UpdateIf,
output_file: &mut Option<File>,
) -> Result<(), Box<dyn std::error::Error>> {
let merged_metadata: Vec<SensorMetadata> = entries
.iter()
.flat_map(|entry| entry.metadata.clone())
.collect();
let json = entries.first_mut().unwrap();
let message = if let Ok((_, msg)) = Message::from_bytes((&json.frame, 0)) {
Some(msg)
} else {
None
};
let mut msg = TimedMessage {
timestamp: json.timestamp,
frame: json.frame.clone(),
message,
metadata: merged_metadata,
decode_time: None,
};
if let Some(message) = &mut msg.message {
match &mut message.df {
ExtendedSquitterADSB(adsb) => decode_position(
&mut adsb.message,
msg.timestamp,
&adsb.icao24,
aircraft,
reference,
update_reference,
),
ExtendedSquitterTisB { cf, .. } => decode_position(
&mut cf.me,
msg.timestamp,
&cf.aa,
aircraft,
reference,
update_reference,
),
_ => {}
}
MessageProcessor::new(message, aircraft)
.sanitize_commb()
.finish();
let json = match serde_json::to_string(&msg) {
Ok(j) => j,
Err(e) => {
eprintln!("Serialization error: {}", e);
eprintln!("Message timestamp: {}", msg.timestamp);
eprintln!("Frame: {}", hex::encode(&msg.frame));
eprintln!("Message: {:?}", msg.message);
panic!("Failed to serialize message");
}
};
if let Some(file) = &mut *output_file {
file.write_all(json.as_bytes()).await?;
file.write_all("\n".as_bytes()).await?;
} else {
println!("{json}");
}
}
Ok(())
}
pub async fn process_file_decode(
inputs: Vec<String>,
reference: Option<Position>,
format: Option<String>,
output_path: Option<String>,
deduplication: u128,
) -> Result<(), Box<dyn std::error::Error>> {
let mut output_file = if let Some(out_path) = output_path {
Some(
fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(out_path)
.await?,
)
} else {
None
};
let mut aircraft: BTreeMap<ICAO, AircraftState> = BTreeMap::new();
let mut reference = reference;
let mut messages = Vec::new();
for input in inputs {
let content_str = read_input_file(&input).await?;
let raw_messages: Vec<&str> = content_str.split('\n').collect();
let detected_format = if let Some(ref fmt) = format {
fmt.as_str()
} else {
let is_csv = raw_messages
.iter()
.find(|line| !line.trim().is_empty())
.map(|first_line| {
!first_line.trim().starts_with('{')
&& first_line.contains(',')
&& first_line.split(',').count() == 2
})
.unwrap_or(false);
if is_csv {
"csv"
} else {
"jsonl"
}
};
let json_objects: Vec<JSONEntry> = if detected_format == "csv" {
raw_messages
.iter()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| parse_beast_csv_line(line))
.collect()
} else {
raw_messages
.iter()
.filter_map(|msg| serde_json::from_str(msg).ok())
.collect()
};
messages.extend(json_objects);
}
messages.sort_by(|a, b| {
a.timestamp
.partial_cmp(&b.timestamp)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut cache: HashMap<Vec<u8>, Vec<JSONEntry>> = HashMap::new();
let mut expiration_heap: BinaryHeap<Reverse<(u128, Vec<u8>)>> =
BinaryHeap::new();
let update_reference = Some(Box::new(|pos: &AirbornePosition| {
pos.alt.is_some_and(|alt| alt < 1000)
})
as Box<dyn Fn(&AirbornePosition) -> bool>);
let mut last_reference_update: f64 = 0.0;
for mut json in messages.into_iter() {
if json.rssi.is_some() {
json.metadata.push(SensorMetadata {
system_timestamp: json.timestamp,
gnss_timestamp: None,
nanoseconds: None,
rssi: json.rssi,
serial: 0,
name: None,
})
}
let timestamp_ms = (json.timestamp * 1e3) as u128;
let frame = json.frame.clone();
if json.timestamp - last_reference_update > 300.0 {
rs1090::decode::cpr::update_global_reference(
&aircraft,
&mut reference,
json.timestamp,
);
last_reference_update = json.timestamp;
}
cache.entry(frame.clone()).or_default().push(json);
if cache[&frame].len() == 1 {
expiration_heap
.push(Reverse((timestamp_ms + deduplication, frame.clone())));
}
while let Some(Reverse((curtime, frame))) = expiration_heap.pop() {
if curtime > timestamp_ms {
expiration_heap.push(Reverse((curtime, frame)));
break;
}
if let Some(entries) = cache.remove(&frame) {
let _ = process_entries(
entries,
&mut aircraft,
&mut reference,
&update_reference,
&mut output_file,
)
.await;
}
}
}
while let Some(Reverse((_curtime, frame))) = expiration_heap.pop() {
if let Some(entries) = cache.remove(&frame) {
let _ = process_entries(
entries,
&mut aircraft,
&mut reference,
&update_reference,
&mut output_file,
)
.await;
}
}
Ok(())
}