From 95c5b50dd3460be0fd6d1955595e46909792b76e Mon Sep 17 00:00:00 2001 From: Andy Killorin <37423245+Speedy6451@users.noreply.github.com> Date: Sat, 8 Mar 2025 11:40:07 -0500 Subject: [PATCH] added combat logger A refactor might be beneficial here, now data is being sent to multiple streams which feels a tad off --- common/src/lib.rs | 4 +- interface/Cargo.lock | 73 ++++++++++++++++ interface/Cargo.toml | 5 +- interface/src/combatlog.rs | 42 ++++++++++ interface/src/main.rs | 27 ++++-- interface/src/storage_dir.rs | 157 +++++++++++++++++++++++++++++++++++ 6 files changed, 297 insertions(+), 11 deletions(-) create mode 100644 interface/src/combatlog.rs create mode 100644 interface/src/storage_dir.rs diff --git a/common/src/lib.rs b/common/src/lib.rs index 13791c5..16ce42f 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -39,13 +39,13 @@ pub struct SensorData { pub accel: Option>, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct TelemetryPacket { pub sensors: SensorData, pub cam_state: CamState, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub enum CamState { Firing, Charged, diff --git a/interface/Cargo.lock b/interface/Cargo.lock index 0b8b141..04315c5 100644 --- a/interface/Cargo.lock +++ b/interface/Cargo.lock @@ -201,6 +201,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -737,6 +743,20 @@ dependencies = [ "libc", ] +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1183,6 +1203,18 @@ dependencies = [ "serde", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "endi" version = "1.1.0" @@ -1714,6 +1746,29 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1890,6 +1945,7 @@ version = "0.1.0" dependencies = [ "anyhow", "atomic_float", + "chrono", "common", "cpal", "eframe", @@ -2738,6 +2794,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" dependencies = [ "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", "heapless", "serde", ] @@ -3938,6 +3996,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.54.0" @@ -3983,6 +4050,12 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + [[package]] name = "windows-result" version = "0.1.2" diff --git a/interface/Cargo.toml b/interface/Cargo.toml index 8ebdd93..b4c2952 100644 --- a/interface/Cargo.toml +++ b/interface/Cargo.toml @@ -7,9 +7,9 @@ edition = "2021" anyhow = "1.0.95" cpal = "0.15.3" pitch-detection = "0.3" -postcard = "1.1.1" +postcard = { version = "1.1.1", features = ["alloc", "use-std"] } rust-music-theory = {git = "https://github.com/the-drunk-coder/rust-music-theory", rev = "a062d65"} -serde = "1.0.217" +serde = { version = "1.0.217", features = ["derive"] } tokio = { version = "1.43.0", features = ["full"] } common = {path = "../common"} heapless = "0.7.0" @@ -19,3 +19,4 @@ egui_extras = { version = "0.30", features = ["default", "image"] } egui-toast = "0.16.0" home = "0.5.11" atomic_float = "1.1.0" +chrono = "0.4.40" diff --git a/interface/src/combatlog.rs b/interface/src/combatlog.rs new file mode 100644 index 0000000..21371c9 --- /dev/null +++ b/interface/src/combatlog.rs @@ -0,0 +1,42 @@ +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use anyhow::Ok; +use common::{ControlPacket, TelemetryPacket}; +use serde::{Deserialize, Serialize}; +use tokio::{fs::File, io::AsyncWriteExt, sync::mpsc::Receiver, time::Instant}; + +use crate::storage_dir::storage; + + +#[derive(Serialize, Deserialize)] +pub enum CombatData { + Telemetry(TelemetryPacket), + Control(ControlPacket), +} + +type CombatLogRow = (CombatData, u128); + +pub async fn combat_logger(mut data: Receiver) -> anyhow::Result<()> { + let mut path = storage(); + let time = chrono::offset::Utc::now(); + let formatted = time.to_rfc3339_opts(chrono::SecondsFormat::Secs, false); + path.push(format!("{formatted}.combatlog")); + let mut file = File::options().create(true).append(true).open(path).await?; + + let mut save = Instant::now(); + + while let Some(packet) = data.recv().await { + let time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_micros(); + let vec = postcard::to_stdvec(&(packet, time))?; + + file.write_u64(vec.len() as u64).await?; + file.write_all(&vec).await?; + + if save.elapsed() > Duration::from_secs(30) { + save = Instant::now(); + file.sync_all().await?; + } + } + + Ok(()) +} diff --git a/interface/src/main.rs b/interface/src/main.rs index a799877..ce49c5e 100644 --- a/interface/src/main.rs +++ b/interface/src/main.rs @@ -3,6 +3,7 @@ use std::{fmt::format, ops::ControlFlow, result, sync::{atomic::Ordering, Arc}, use anyhow::{Context, Ok, Result}; use atomic_float::AtomicF32; +use combatlog::{combat_logger, CombatData}; use common::{ControlPacket, TelemetryPacket}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use egui_toast::{Toast, ToastKind}; @@ -13,10 +14,13 @@ use tokio::{io::{AsyncReadExt, AsyncWriteExt, BufWriter, WriteHalf}, net::{tcp:: mod gui; mod storage_dir; +mod combatlog; pub const POWER_THRESHOLD: AtomicF32 = AtomicF32::new(DEFAULT_VOLUME_THRESHOLD); fn main() -> Result<()> { + let (logging_sender, combatlog) = mpsc::channel(64); + // assumes pulseaudio system with f32 samples and 2204 sample packets dbg!(cpal::available_hosts()); let host = cpal::default_host(); @@ -27,7 +31,6 @@ fn main() -> Result<()> { #[cfg(target_os = "linux")] let device = host.devices().unwrap().find(|d|d.name().unwrap() == "pulse").context("no pulse")?; - let config = device.default_input_config()?; dbg!(config.sample_format()); @@ -86,8 +89,15 @@ fn main() -> Result<()> { let spawner = executor.handle().clone(); thread::spawn(move || { spawner.block_on(async { + let log_toasts = toast_sender.clone(); + tokio::spawn(async move { + if let Err(e) = combat_logger(combatlog).await { + let _ = log_toasts.clone().send(Toast::new().text(format!("logger crashed: {e:?}")).kind(ToastKind::Error)).await; + } + }); + loop { - if let Err(e) = connect(toast_sender.clone(), notes.resubscribe(), data_sender.clone()).await { + if let Err(e) = connect(toast_sender.clone(), notes.resubscribe(), data_sender.clone(), logging_sender.clone()).await { if let Err(_) = toast_sender.send(Toast::new().text(format!("{e:?}")).kind(ToastKind::Error)).await { break; }; @@ -108,7 +118,7 @@ fn main() -> Result<()> { /// frequency, volume type Detection = (Option, f32); -async fn connect(toast_sender: mpsc::Sender, notes: broadcast::Receiver, data_sender: watch::Sender) -> Result<()>{ +async fn connect(toast_sender: mpsc::Sender, notes: broadcast::Receiver, data_sender: watch::Sender, logging_sender: mpsc::Sender) -> Result<()>{ toast_sender.send(Toast::new().text("connecting to bot").kind(ToastKind::Info)).await?; let cruisecontrol = TcpStream::connect("192.168.1.2:1234").await?; println!("connected"); @@ -116,13 +126,13 @@ async fn connect(toast_sender: mpsc::Sender, notes: broadcast::Receiver) -> Result<()> { +async fn telemetry_handler(mut telem: OwnedReadHalf, gui: watch::Sender, logging_sender: mpsc::Sender) -> Result<()> { let mut buf = vec![0; 2048]; loop { let len = telem.read_u32().await.context("bad length")? as usize; @@ -132,6 +142,8 @@ async fn telemetry_handler(mut telem: OwnedReadHalf, gui: watch::Sender //println!("telem: {telem:?}"); + logging_sender.send(CombatData::Telemetry(telem.clone())).await?; + gui.send_modify(|gui| { gui.telemetry = Some(telem); }); @@ -139,7 +151,7 @@ async fn telemetry_handler(mut telem: OwnedReadHalf, gui: watch::Sender } } -async fn controller(mut notes: broadcast::Receiver, controller: OwnedWriteHalf, gui: watch::Sender) -> Result<()> { +async fn controller(mut notes: broadcast::Receiver, controller: OwnedWriteHalf, gui: watch::Sender, logging_sender: mpsc::Sender) -> Result<()> { let mut controller = BufWriter::new(controller); //send_packet(&mut controller, ControlPacket::Arm(true)).await?; //println!("armed flipper"); @@ -168,6 +180,7 @@ async fn controller(mut notes: broadcast::Receiver, controller: Owned } send_packet(&mut controller, control.clone()).await?; + logging_sender.send(CombatData::Control(control.clone())).await?; gui.send_modify(|gui| gui.last_command = Some(control)); } else { send_packet(&mut controller, ControlPacket::Twist(0.0, 0.0)).await?; diff --git a/interface/src/storage_dir.rs b/interface/src/storage_dir.rs new file mode 100644 index 0000000..d5e3649 --- /dev/null +++ b/interface/src/storage_dir.rs @@ -0,0 +1,157 @@ +// copied from eframe (private item) +// https://docs.rs/eframe/latest/src/eframe/native/file_storage.rs.html#17 + +use std::path::PathBuf; + +use eframe::egui; + +pub fn storage() -> PathBuf { + storage_dir("cruisecontrol").unwrap() +} + +pub fn storage_dir(app_id: &str) -> Option { + + use egui::os::OperatingSystem as OS; + + use std::env::var_os; + + match OS::from_target_os() { + + OS::Nix => var_os("XDG_DATA_HOME") + + .map(PathBuf::from) + + .filter(|p| p.is_absolute()) + + .or_else(|| home::home_dir().map(|p| p.join(".local").join("share"))) + + .map(|p| { + + p.join( + + app_id + + .to_lowercase() + + .replace(|c: char| c.is_ascii_whitespace(), ""), + + ) + + }), + + OS::Mac => home::home_dir().map(|p| { + + p.join("Library") + + .join("Application Support") + + .join(app_id.replace(|c: char| c.is_ascii_whitespace(), "-")) + + }), + + OS::Windows => roaming_appdata().map(|p| p.join(app_id).join("data")), + + OS::Unknown | OS::Android | OS::IOS => None, + + } + +} + +// Adapted from + +// https://github.com/rust-lang/cargo/blob/6e11c77384989726bb4f412a0e23b59c27222c34/crates/home/src/windows.rs#L19-L37 + +#[cfg(all(windows, not(target_vendor = "uwp")))] + +#[allow(unsafe_code)] + +fn roaming_appdata() -> Option { + + use std::ffi::OsString; + + use std::os::windows::ffi::OsStringExt; + + use std::ptr; + + use std::slice; + + + use windows_sys::Win32::Foundation::S_OK; + + use windows_sys::Win32::System::Com::CoTaskMemFree; + + use windows_sys::Win32::UI::Shell::{ + + FOLDERID_RoamingAppData, SHGetKnownFolderPath, KF_FLAG_DONT_VERIFY, + + }; + + + extern "C" { + + fn wcslen(buf: *const u16) -> usize; + + } + + let mut path_raw = ptr::null_mut(); + + + // SAFETY: SHGetKnownFolderPath allocates for us, we don't pass any pointers to it. + + // See https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath + + let result = unsafe { + + SHGetKnownFolderPath( + + &FOLDERID_RoamingAppData, + + KF_FLAG_DONT_VERIFY as u32, + + std::ptr::null_mut(), + + &mut path_raw, + + ) + + }; + + + let path = if result == S_OK { + + // SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a nullterminated string for us. + + let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) }; + + Some(PathBuf::from(OsString::from_wide(path_slice))) + + } else { + + None + + }; + + + // SAFETY: + + // This memory got allocated by SHGetKnownFolderPath, we didn't touch anything in the process. + + // A null ptr is a no-op for `CoTaskMemFree`, so in case this failed we're still good. + + // https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cotaskmemfree + + unsafe { CoTaskMemFree(path_raw.cast()) }; + + + path + +} + + +#[cfg(any(not(windows), target_vendor = "uwp"))] + +fn roaming_appdata() -> Option { + + None + +}