mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 10:19:26 -07:00
PR chage requests, revision to GPS logging feature, code cleanup
This commit is contained in:
committed by
Will Greenberg
parent
dbe102e366
commit
0b91a6e5d3
@@ -12,7 +12,9 @@ use futures::{StreamExt, TryStreamExt, future};
|
||||
use log::{debug, error, info, warn};
|
||||
use rayhunter::Device;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
|
||||
use crate::gps::GpsRecord;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::sync::{RwLock, oneshot};
|
||||
use tokio_stream::wrappers::LinesStream;
|
||||
@@ -57,6 +59,7 @@ pub struct DiagTask {
|
||||
min_space_to_start_mb: u64,
|
||||
min_space_to_continue_mb: u64,
|
||||
gps_mode: u8,
|
||||
gps_fixed_coords: Option<(f64, f64)>,
|
||||
state: DiagState,
|
||||
max_type_seen: EventType,
|
||||
bytes_since_space_check: usize,
|
||||
@@ -106,6 +109,7 @@ impl DiagTask {
|
||||
min_space_to_start_mb: u64,
|
||||
min_space_to_continue_mb: u64,
|
||||
gps_mode: u8,
|
||||
gps_fixed_coords: Option<(f64, f64)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
ui_update_sender,
|
||||
@@ -115,6 +119,7 @@ impl DiagTask {
|
||||
min_space_to_start_mb,
|
||||
min_space_to_continue_mb,
|
||||
gps_mode,
|
||||
gps_fixed_coords,
|
||||
state: DiagState::Stopped,
|
||||
max_type_seen: EventType::Informational,
|
||||
bytes_since_space_check: 0,
|
||||
@@ -155,6 +160,26 @@ impl DiagTask {
|
||||
return Err(msg);
|
||||
}
|
||||
};
|
||||
// For fixed-mode sessions, write the configured coordinates to the sidecar
|
||||
// immediately so the per-session GPS is stored durably and isn't affected
|
||||
// by future config changes or GPS API calls.
|
||||
if self.gps_mode == 1 {
|
||||
if let Some((lat, lon)) = self.gps_fixed_coords {
|
||||
if let Some((entry_idx, _)) = qmdl_store.get_current_entry() {
|
||||
if let Ok(mut gps_file) = qmdl_store.open_entry_gps_for_append(entry_idx).await
|
||||
{
|
||||
let record = GpsRecord {
|
||||
unix_ts: 0,
|
||||
lat,
|
||||
lon,
|
||||
};
|
||||
if let Ok(json) = serde_json::to_string(&record) {
|
||||
let _ = gps_file.write_all(format!("{json}\n").as_bytes()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.stop_current_recording().await;
|
||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
||||
let analysis_writer = match AnalysisWriter::new(analysis_file, &self.analyzer_config).await
|
||||
@@ -385,6 +410,7 @@ pub fn run_diag_read_thread(
|
||||
min_space_to_start_mb: u64,
|
||||
min_space_to_continue_mb: u64,
|
||||
gps_mode: u8,
|
||||
gps_fixed_coords: Option<(f64, f64)>,
|
||||
) {
|
||||
task_tracker.spawn(async move {
|
||||
info!("Using configuration for device: {0:?}", device);
|
||||
@@ -402,6 +428,7 @@ pub fn run_diag_read_thread(
|
||||
min_space_to_start_mb,
|
||||
min_space_to_continue_mb,
|
||||
gps_mode,
|
||||
gps_fixed_coords,
|
||||
);
|
||||
qmdl_file_tx
|
||||
.send(DiagDeviceCtrlMessage::StartRecording { response_tx: None })
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
@@ -14,13 +16,18 @@ where
|
||||
use serde::de;
|
||||
use serde_json::Value;
|
||||
match Value::deserialize(deserializer)? {
|
||||
Value::Number(n) => n.as_i64()
|
||||
Value::Number(n) => n
|
||||
.as_i64()
|
||||
.or_else(|| n.as_f64().map(|f| f as i64))
|
||||
.ok_or_else(|| de::Error::custom("timestamp out of range")),
|
||||
Value::String(s) => s.trim().parse::<f64>()
|
||||
Value::String(s) => s
|
||||
.trim()
|
||||
.parse::<f64>()
|
||||
.map(|f| f as i64)
|
||||
.map_err(|_| de::Error::custom("timestamp must be a numeric value")),
|
||||
_ => Err(de::Error::custom("timestamp must be a number or numeric string")),
|
||||
_ => Err(de::Error::custom(
|
||||
"timestamp must be a number or numeric string",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,14 +39,30 @@ pub struct GpsData {
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
impl GpsData {
|
||||
pub fn to_datetime(&self) -> DateTime<FixedOffset> {
|
||||
DateTime::from_timestamp(self.timestamp, 0)
|
||||
.unwrap_or_default()
|
||||
.fixed_offset()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GpsRecord {
|
||||
pub unix_ts: u32,
|
||||
pub unix_ts: i64,
|
||||
pub lat: f64,
|
||||
pub lon: f64,
|
||||
}
|
||||
|
||||
/// Reads all GPS records from a sidecar file, skipping malformed lines.
|
||||
impl GpsRecord {
|
||||
pub fn to_datetime(&self) -> DateTime<FixedOffset> {
|
||||
DateTime::from_timestamp(self.unix_ts, 0)
|
||||
.unwrap_or_default()
|
||||
.fixed_offset()
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads all GPS records from a sidecar NDJSON file, skipping malformed lines.
|
||||
pub async fn load_gps_records(file: tokio::fs::File) -> Vec<GpsRecord> {
|
||||
let reader = BufReader::new(file);
|
||||
let mut lines = reader.lines();
|
||||
@@ -69,9 +92,18 @@ pub async fn post_gps(
|
||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||
if let Some((entry_idx, _)) = qmdl_store.get_current_entry() {
|
||||
if let Ok(mut file) = qmdl_store.open_entry_gps_for_append(entry_idx).await {
|
||||
let record = GpsRecord { unix_ts: chrono::Utc::now().timestamp() as u32, lat: gps_data.latitude, lon: gps_data.longitude };
|
||||
if let Ok(json) = serde_json::to_string(&record) {
|
||||
let _ = file.write_all(format!("{json}\n").as_bytes()).await;
|
||||
let record = GpsRecord {
|
||||
unix_ts: Utc::now().timestamp(),
|
||||
lat: gps_data.latitude,
|
||||
lon: gps_data.longitude,
|
||||
};
|
||||
match serde_json::to_string(&record) {
|
||||
Ok(json) => {
|
||||
if let Err(e) = file.write_all(format!("{json}\n").as_bytes()).await {
|
||||
error!("failed to write GPS record to sidecar: {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => error!("failed to serialize GPS record: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,9 +111,7 @@ pub async fn post_gps(
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
pub async fn get_gps(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<Json<GpsData>, StatusCode> {
|
||||
pub async fn get_gps(State(state): State<Arc<ServerState>>) -> Result<Json<GpsData>, StatusCode> {
|
||||
let gps = state.gps_state.read().await;
|
||||
match gps.as_ref() {
|
||||
Some(data) => Ok(Json(data.clone())),
|
||||
|
||||
@@ -21,10 +21,10 @@ use crate::battery::run_battery_notification_worker;
|
||||
use crate::config::{parse_args, parse_config};
|
||||
use crate::diag::run_diag_read_thread;
|
||||
use crate::error::RayhunterError;
|
||||
use crate::gps::{get_gps, post_gps};
|
||||
use crate::notifications::{NotificationService, run_notification_worker};
|
||||
use crate::pcap::get_pcap;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
use crate::gps::{get_gps, post_gps};
|
||||
use crate::server::{
|
||||
ServerState, debug_set_display_state, get_config, get_qmdl, get_time, get_wifi_status, get_zip,
|
||||
scan_wifi, serve_static, set_config, set_time_offset, test_notification,
|
||||
@@ -220,6 +220,10 @@ async fn run_with_config(
|
||||
|
||||
if !config.debug_mode {
|
||||
info!("Starting Diag Thread");
|
||||
let gps_fixed_coords = match (config.gps_fixed_latitude, config.gps_fixed_longitude) {
|
||||
(Some(lat), Some(lon)) => Some((lat, lon)),
|
||||
_ => None,
|
||||
};
|
||||
run_diag_read_thread(
|
||||
&task_tracker,
|
||||
config.device.clone(),
|
||||
@@ -233,6 +237,7 @@ async fn run_with_config(
|
||||
config.min_space_to_start_recording_mb,
|
||||
config.min_space_to_continue_recording_mb,
|
||||
config.gps_mode,
|
||||
gps_fixed_coords,
|
||||
);
|
||||
info!("Starting UI");
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ pub(crate) async fn load_gps_records_for_entry(
|
||||
) -> Vec<GpsRecord> {
|
||||
// Always try the per-session sidecar first — it reflects what was actually
|
||||
// recorded regardless of what the current gps_mode config is.
|
||||
let entry_gps_mode;
|
||||
{
|
||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||
if let Ok(file) = qmdl_store.open_entry_gps(entry_index).await {
|
||||
@@ -83,19 +84,33 @@ pub(crate) async fn load_gps_records_for_entry(
|
||||
return records;
|
||||
}
|
||||
}
|
||||
// Capture the entry's recorded GPS mode before releasing the lock.
|
||||
entry_gps_mode = qmdl_store
|
||||
.manifest
|
||||
.entries
|
||||
.get(entry_index)
|
||||
.and_then(|e| e.gps_mode);
|
||||
}
|
||||
// Sidecar missing or empty — fall back to current config.
|
||||
if state.config.gps_mode == 1 {
|
||||
let guard = state.gps_state.read().await;
|
||||
return guard
|
||||
.as_ref()
|
||||
.map(|g| vec![GpsRecord { unix_ts: 0, lat: g.latitude, lon: g.longitude }])
|
||||
.unwrap_or_default();
|
||||
// Sidecar missing or empty — fall back using the entry's own recorded GPS mode,
|
||||
// not the current config, so old fixed-mode sessions still get coordinates even
|
||||
// if the mode has since been changed. Use the configured fixed coords directly
|
||||
// rather than gps_state, which can be overwritten by API calls or be None.
|
||||
if entry_gps_mode == Some(1) {
|
||||
if let (Some(lat), Some(lon)) = (
|
||||
state.config.gps_fixed_latitude,
|
||||
state.config.gps_fixed_longitude,
|
||||
) {
|
||||
return vec![GpsRecord {
|
||||
unix_ts: 0,
|
||||
lat,
|
||||
lon,
|
||||
}];
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn find_nearest_gps(records: &[GpsRecord], packet_unix_ts: u32) -> Option<GpsPoint> {
|
||||
fn find_nearest_gps(records: &[GpsRecord], packet_unix_ts: i64) -> Option<GpsPoint> {
|
||||
if records.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -106,9 +121,79 @@ fn find_nearest_gps(records: &[GpsRecord], packet_unix_ts: u32) -> Option<GpsPoi
|
||||
&records[records.len() - 1]
|
||||
} else {
|
||||
let (before, after) = (&records[idx - 1], &records[idx]);
|
||||
if packet_unix_ts - before.unix_ts <= after.unix_ts - packet_unix_ts { before } else { after }
|
||||
let before_delta = packet_unix_ts - before.unix_ts;
|
||||
let after_delta = after.unix_ts - packet_unix_ts;
|
||||
if before_delta <= after_delta {
|
||||
before
|
||||
} else {
|
||||
after
|
||||
}
|
||||
};
|
||||
Some(GpsPoint { latitude: record.lat, longitude: record.lon, unix_ts: record.unix_ts })
|
||||
Some(GpsPoint {
|
||||
latitude: record.lat,
|
||||
longitude: record.lon,
|
||||
unix_ts: record.unix_ts,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn rec(unix_ts: i64, lat: f64, lon: f64) -> GpsRecord {
|
||||
GpsRecord { unix_ts, lat, lon }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_returns_none() {
|
||||
assert!(find_nearest_gps(&[], 100).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_record_always_returned() {
|
||||
let records = vec![rec(100, 1.0, 2.0)];
|
||||
assert_eq!(find_nearest_gps(&records, 0).unwrap().unix_ts, 100);
|
||||
assert_eq!(find_nearest_gps(&records, 200).unwrap().unix_ts, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_before_all_records_returns_first() {
|
||||
let records = vec![rec(100, 1.0, 2.0), rec(200, 3.0, 4.0)];
|
||||
assert_eq!(find_nearest_gps(&records, 50).unwrap().unix_ts, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_after_all_records_returns_last() {
|
||||
let records = vec![rec(100, 1.0, 2.0), rec(200, 3.0, 4.0)];
|
||||
assert_eq!(find_nearest_gps(&records, 300).unwrap().unix_ts, 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exact_match() {
|
||||
let records = vec![rec(100, 1.0, 2.0), rec(200, 3.0, 4.0), rec(300, 5.0, 6.0)];
|
||||
assert_eq!(find_nearest_gps(&records, 200).unwrap().unix_ts, 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_closer_to_before() {
|
||||
// packet at 130: delta to before(100)=30, delta to after(200)=70 → picks before
|
||||
let records = vec![rec(100, 1.0, 2.0), rec(200, 3.0, 4.0)];
|
||||
assert_eq!(find_nearest_gps(&records, 130).unwrap().unix_ts, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_closer_to_after() {
|
||||
// packet at 170: delta to before(100)=70, delta to after(200)=30 → picks after
|
||||
let records = vec![rec(100, 1.0, 2.0), rec(200, 3.0, 4.0)];
|
||||
assert_eq!(find_nearest_gps(&records, 170).unwrap().unix_ts, 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_equidistant_prefers_before() {
|
||||
// packet at 150: delta to before(100)=50, delta to after(200)=50 → tie, picks before
|
||||
let records = vec![rec(100, 1.0, 2.0), rec(200, 3.0, 4.0)];
|
||||
assert_eq!(find_nearest_gps(&records, 150).unwrap().unix_ts, 100);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_pcap_data<R, W>(
|
||||
@@ -135,8 +220,7 @@ where
|
||||
Ok(msg) => {
|
||||
let maybe_gsmtap_msg = gsmtap_parser::parse(msg)?;
|
||||
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
|
||||
let packet_unix_ts =
|
||||
timestamp.to_datetime().timestamp().max(0) as u32;
|
||||
let packet_unix_ts = timestamp.to_datetime().timestamp();
|
||||
let gps = find_nearest_gps(&gps_records, packet_unix_ts);
|
||||
pcap_writer
|
||||
.write_gsmtap_message(gsmtap_msg, timestamp, gps.as_ref())
|
||||
|
||||
@@ -105,9 +105,7 @@ impl ManifestEntry {
|
||||
}
|
||||
|
||||
pub fn get_gps_filepath<P: AsRef<Path>>(&self, path: P) -> PathBuf {
|
||||
let mut filepath = path.as_ref().join(&self.name);
|
||||
filepath.set_extension("gps.ndjson");
|
||||
filepath
|
||||
path.as_ref().join(format!("{}-gps.ndjson", self.name))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -388,8 +388,13 @@ pub async fn get_zip(
|
||||
.take(qmdl_size_bytes as u64)
|
||||
};
|
||||
|
||||
if let Err(e) =
|
||||
generate_pcap_data(&mut entry_writer, qmdl_file_for_pcap, qmdl_size_bytes, gps_records).await
|
||||
if let Err(e) = generate_pcap_data(
|
||||
&mut entry_writer,
|
||||
qmdl_file_for_pcap,
|
||||
qmdl_size_bytes,
|
||||
gps_records,
|
||||
)
|
||||
.await
|
||||
{
|
||||
// if we fail to generate the PCAP file, we should still continue and give the
|
||||
// user the QMDL.
|
||||
|
||||
@@ -70,12 +70,10 @@
|
||||
>
|
||||
</p>
|
||||
{/if}
|
||||
{#if entry.gps_mode !== undefined}
|
||||
<p>
|
||||
<b>GPS Mode:</b>
|
||||
{entry.gps_mode === 0 ? 'Disabled' : entry.gps_mode === 1 ? 'Fixed coordinates' : 'API endpoint'}
|
||||
</p>
|
||||
{/if}
|
||||
<p>
|
||||
<b>GPS Mode:</b>
|
||||
{(entry.gps_mode ?? 0) === 0 ? 'Disabled' : entry.gps_mode === 1 ? 'Fixed coordinates' : 'API endpoint'}
|
||||
</p>
|
||||
</div>
|
||||
{#if metadata && metadata.analyzers}
|
||||
<div>
|
||||
|
||||
@@ -30,7 +30,7 @@ pub struct GpsPoint {
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
/// Unix timestamp of the GPS fix. 0 means fixed/synthetic (no real GPS time).
|
||||
pub unix_ts: u32,
|
||||
pub unix_ts: i64,
|
||||
}
|
||||
|
||||
pub struct GsmtapPcapWriter<T>
|
||||
@@ -149,9 +149,15 @@ where
|
||||
let mut options = vec![];
|
||||
if let Some(p) = gps {
|
||||
let comment = if p.unix_ts == 0 {
|
||||
format!("GPS fixed lat={:.7} lon={:.7}", p.latitude, p.longitude)
|
||||
format!(
|
||||
r#"{{"latitude":{:.7},"longitude":{:.7}}}"#,
|
||||
p.latitude, p.longitude
|
||||
)
|
||||
} else {
|
||||
format!("GPS lat={:.7} lon={:.7} ts={}", p.latitude, p.longitude, p.unix_ts)
|
||||
format!(
|
||||
r#"{{"latitude":{:.7},"longitude":{:.7},"timestamp":{}}}"#,
|
||||
p.latitude, p.longitude, p.unix_ts
|
||||
)
|
||||
};
|
||||
options.push(EnhancedPacketOption::Comment(Cow::Owned(comment)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user