1
Fork 0

added combat logger

A refactor might be beneficial here, now data is being
sent to multiple streams which feels a tad off
This commit is contained in:
Andy Killorin 2025-03-08 11:40:07 -05:00
parent e352c5b048
commit 95c5b50dd3
Signed by: ank
GPG key ID: 23F9463ECB67FE8C
6 changed files with 297 additions and 11 deletions

View file

@ -39,13 +39,13 @@ pub struct SensorData {
pub accel: Option<Vector3<f32>>, pub accel: Option<Vector3<f32>>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TelemetryPacket { pub struct TelemetryPacket {
pub sensors: SensorData, pub sensors: SensorData,
pub cam_state: CamState, pub cam_state: CamState,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum CamState { pub enum CamState {
Firing, Firing,
Charged, Charged,

73
interface/Cargo.lock generated
View file

@ -201,6 +201,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@ -737,6 +743,20 @@ dependencies = [
"libc", "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]] [[package]]
name = "clang-sys" name = "clang-sys"
version = "1.8.1" version = "1.8.1"
@ -1183,6 +1203,18 @@ dependencies = [
"serde", "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]] [[package]]
name = "endi" name = "endi"
version = "1.1.0" version = "1.1.0"
@ -1714,6 +1746,29 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "1.5.0" version = "1.5.0"
@ -1890,6 +1945,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atomic_float", "atomic_float",
"chrono",
"common", "common",
"cpal", "cpal",
"eframe", "eframe",
@ -2738,6 +2794,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8"
dependencies = [ dependencies = [
"cobs", "cobs",
"embedded-io 0.4.0",
"embedded-io 0.6.1",
"heapless", "heapless",
"serde", "serde",
] ]
@ -3938,6 +3996,15 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.54.0" version = "0.54.0"
@ -3983,6 +4050,12 @@ dependencies = [
"syn 2.0.96", "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]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.1.2" version = "0.1.2"

View file

@ -7,9 +7,9 @@ edition = "2021"
anyhow = "1.0.95" anyhow = "1.0.95"
cpal = "0.15.3" cpal = "0.15.3"
pitch-detection = "0.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"} 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"] } tokio = { version = "1.43.0", features = ["full"] }
common = {path = "../common"} common = {path = "../common"}
heapless = "0.7.0" heapless = "0.7.0"
@ -19,3 +19,4 @@ egui_extras = { version = "0.30", features = ["default", "image"] }
egui-toast = "0.16.0" egui-toast = "0.16.0"
home = "0.5.11" home = "0.5.11"
atomic_float = "1.1.0" atomic_float = "1.1.0"
chrono = "0.4.40"

View file

@ -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<CombatData>) -> 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(())
}

View file

@ -3,6 +3,7 @@ use std::{fmt::format, ops::ControlFlow, result, sync::{atomic::Ordering, Arc},
use anyhow::{Context, Ok, Result}; use anyhow::{Context, Ok, Result};
use atomic_float::AtomicF32; use atomic_float::AtomicF32;
use combatlog::{combat_logger, CombatData};
use common::{ControlPacket, TelemetryPacket}; use common::{ControlPacket, TelemetryPacket};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use egui_toast::{Toast, ToastKind}; use egui_toast::{Toast, ToastKind};
@ -13,10 +14,13 @@ use tokio::{io::{AsyncReadExt, AsyncWriteExt, BufWriter, WriteHalf}, net::{tcp::
mod gui; mod gui;
mod storage_dir; mod storage_dir;
mod combatlog;
pub const POWER_THRESHOLD: AtomicF32 = AtomicF32::new(DEFAULT_VOLUME_THRESHOLD); pub const POWER_THRESHOLD: AtomicF32 = AtomicF32::new(DEFAULT_VOLUME_THRESHOLD);
fn main() -> Result<()> { fn main() -> Result<()> {
let (logging_sender, combatlog) = mpsc::channel(64);
// assumes pulseaudio system with f32 samples and 2204 sample packets // assumes pulseaudio system with f32 samples and 2204 sample packets
dbg!(cpal::available_hosts()); dbg!(cpal::available_hosts());
let host = cpal::default_host(); let host = cpal::default_host();
@ -27,7 +31,6 @@ fn main() -> Result<()> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let device = host.devices().unwrap().find(|d|d.name().unwrap() == "pulse").context("no pulse")?; let device = host.devices().unwrap().find(|d|d.name().unwrap() == "pulse").context("no pulse")?;
let config = device.default_input_config()?; let config = device.default_input_config()?;
dbg!(config.sample_format()); dbg!(config.sample_format());
@ -86,8 +89,15 @@ fn main() -> Result<()> {
let spawner = executor.handle().clone(); let spawner = executor.handle().clone();
thread::spawn(move || { thread::spawn(move || {
spawner.block_on(async { 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 { 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 { if let Err(_) = toast_sender.send(Toast::new().text(format!("{e:?}")).kind(ToastKind::Error)).await {
break; break;
}; };
@ -108,7 +118,7 @@ fn main() -> Result<()> {
/// frequency, volume /// frequency, volume
type Detection = (Option<f32>, f32); type Detection = (Option<f32>, f32);
async fn connect(toast_sender: mpsc::Sender<Toast>, notes: broadcast::Receiver<Detection>, data_sender: watch::Sender<GUIData>) -> Result<()>{ async fn connect(toast_sender: mpsc::Sender<Toast>, notes: broadcast::Receiver<Detection>, data_sender: watch::Sender<GUIData>, logging_sender: mpsc::Sender<CombatData>) -> Result<()>{
toast_sender.send(Toast::new().text("connecting to bot").kind(ToastKind::Info)).await?; toast_sender.send(Toast::new().text("connecting to bot").kind(ToastKind::Info)).await?;
let cruisecontrol = TcpStream::connect("192.168.1.2:1234").await?; let cruisecontrol = TcpStream::connect("192.168.1.2:1234").await?;
println!("connected"); println!("connected");
@ -116,13 +126,13 @@ async fn connect(toast_sender: mpsc::Sender<Toast>, notes: broadcast::Receiver<D
cruisecontrol.set_nodelay(true)?; cruisecontrol.set_nodelay(true)?;
let (telem, control) = cruisecontrol.into_split(); let (telem, control) = cruisecontrol.into_split();
tokio::spawn(telemetry_handler(telem, data_sender.clone())); tokio::spawn(telemetry_handler(telem, data_sender.clone(), logging_sender.clone()));
controller(notes, control, data_sender.clone()).await?; controller(notes, control, data_sender.clone(), logging_sender.clone()).await?;
Ok(()) Ok(())
} }
async fn telemetry_handler(mut telem: OwnedReadHalf, gui: watch::Sender<GUIData>) -> Result<()> { async fn telemetry_handler(mut telem: OwnedReadHalf, gui: watch::Sender<GUIData>, logging_sender: mpsc::Sender<CombatData>) -> Result<()> {
let mut buf = vec![0; 2048]; let mut buf = vec![0; 2048];
loop { loop {
let len = telem.read_u32().await.context("bad length")? as usize; 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<GUIData>
//println!("telem: {telem:?}"); //println!("telem: {telem:?}");
logging_sender.send(CombatData::Telemetry(telem.clone())).await?;
gui.send_modify(|gui| { gui.send_modify(|gui| {
gui.telemetry = Some(telem); gui.telemetry = Some(telem);
}); });
@ -139,7 +151,7 @@ async fn telemetry_handler(mut telem: OwnedReadHalf, gui: watch::Sender<GUIData>
} }
} }
async fn controller(mut notes: broadcast::Receiver<Detection>, controller: OwnedWriteHalf, gui: watch::Sender<GUIData>) -> Result<()> { async fn controller(mut notes: broadcast::Receiver<Detection>, controller: OwnedWriteHalf, gui: watch::Sender<GUIData>, logging_sender: mpsc::Sender<CombatData>) -> Result<()> {
let mut controller = BufWriter::new(controller); let mut controller = BufWriter::new(controller);
//send_packet(&mut controller, ControlPacket::Arm(true)).await?; //send_packet(&mut controller, ControlPacket::Arm(true)).await?;
//println!("armed flipper"); //println!("armed flipper");
@ -168,6 +180,7 @@ async fn controller(mut notes: broadcast::Receiver<Detection>, controller: Owned
} }
send_packet(&mut controller, control.clone()).await?; send_packet(&mut controller, control.clone()).await?;
logging_sender.send(CombatData::Control(control.clone())).await?;
gui.send_modify(|gui| gui.last_command = Some(control)); gui.send_modify(|gui| gui.last_command = Some(control));
} else { } else {
send_packet(&mut controller, ControlPacket::Twist(0.0, 0.0)).await?; send_packet(&mut controller, ControlPacket::Twist(0.0, 0.0)).await?;

View file

@ -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<PathBuf> {
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<PathBuf> {
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<PathBuf> {
None
}