Compare commits
No commits in common. "9e43e6a943d0855b792d9d4f8ad89c7340febfb4" and "706ed9db0302dfdb114064c777ce960b5eb74d14" have entirely different histories.
9e43e6a943
...
706ed9db03
14 changed files with 57 additions and 6431 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
**/target
|
|
5178
auto/Cargo.lock
generated
5178
auto/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,11 +0,0 @@
|
||||||
[lib]
|
|
||||||
crate-type = ["dylib"]
|
|
||||||
|
|
||||||
[package]
|
|
||||||
name = "auto"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
common = {path = "../common"}
|
|
||||||
interface = {path = "../interface"}
|
|
|
@ -1,94 +0,0 @@
|
||||||
use std::{collections::VecDeque, ops::Sub, pin::Pin};
|
|
||||||
|
|
||||||
use common::CamState;
|
|
||||||
use interface::auto::{AutoInterface, Configurable};
|
|
||||||
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub fn entry(interface: AutoInterface) -> Pin<Box<dyn Future<Output = ()>>> {
|
|
||||||
Box::pin(auto(interface))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub static NAME: &'static str = "scanseek v1";
|
|
||||||
|
|
||||||
|
|
||||||
const AUTO_GAP: Configurable = Configurable::new("auto minimum gap").range(0. .. 300.).default(140.)
|
|
||||||
.description("distance (mm) distance measurements must instantaneously drop to indicate a detection. This should line up with the size of the smallest robot you compete against");
|
|
||||||
const AUTO_SELF_OCCLUSION: Configurable = Configurable::new("auto self occlusion").range(0. .. 200.).default(143.)
|
|
||||||
.description("distance (mm) below which measurements are considered noise in the scan phase");
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub static CONFIGS: &[Configurable] = &[
|
|
||||||
AUTO_GAP,
|
|
||||||
AUTO_SELF_OCCLUSION,
|
|
||||||
];
|
|
||||||
|
|
||||||
async fn auto (mut interface: AutoInterface) {
|
|
||||||
let mut tof_l = Stats::new();
|
|
||||||
let mut tof_r = Stats::new();
|
|
||||||
loop {
|
|
||||||
let data = interface.sensor_update().await;
|
|
||||||
let cam = interface.cam_state();
|
|
||||||
let CamState::Charged = cam else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(latest_tof_l) = data.tof_l else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
tof_l.update(latest_tof_l as i16);
|
|
||||||
let Some(latest_tof_r) = data.tof_r else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
tof_r.update(latest_tof_r as i16);
|
|
||||||
|
|
||||||
let auto_gap = interface.conf(&AUTO_GAP);
|
|
||||||
let auto_self_occlusion = interface.conf(&AUTO_SELF_OCCLUSION);
|
|
||||||
|
|
||||||
let detection = |latest: u16, delta: i16| {
|
|
||||||
delta < auto_gap as i16 && latest > auto_self_occlusion as u16
|
|
||||||
};
|
|
||||||
|
|
||||||
if detection(latest_tof_l, tof_l.delta()) || detection(latest_tof_l, tof_l.delta()) {
|
|
||||||
if let Ok(()) = interface.enable() {
|
|
||||||
println!("found, now seek")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct Stats<T> {
|
|
||||||
table: VecDeque<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Stats<T> {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self { table: VecDeque::new() }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_ELEMENTS: usize = 3;
|
|
||||||
|
|
||||||
pub fn update(&mut self, elem: T) {
|
|
||||||
self.table.push_front(elem);
|
|
||||||
if self.table.len() > Self::MAX_ELEMENTS {
|
|
||||||
self.table.pop_back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Ord + Sub<Output = T> + Copy + From<u8>> Stats<T> {
|
|
||||||
pub fn delta(&self) -> T {
|
|
||||||
*self.table.get(0).unwrap_or(&T::from(0)) - *self.table.get(1).unwrap_or(&T::from(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max(&self) -> T {
|
|
||||||
*self.table.iter().max().unwrap_or(&T::from(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn min(&self) -> T {
|
|
||||||
*self.table.iter().max().unwrap_or(&T::from(0))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -39,13 +39,13 @@ pub struct SensorData {
|
||||||
pub accel: Option<Vector3<f32>>,
|
pub accel: Option<Vector3<f32>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct TelemetryPacket {
|
pub struct TelemetryPacket {
|
||||||
pub sensors: SensorData,
|
pub sensors: SensorData,
|
||||||
pub cam_state: CamState,
|
pub cam_state: CamState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub enum CamState {
|
pub enum CamState {
|
||||||
Firing,
|
Firing,
|
||||||
Charged,
|
Charged,
|
||||||
|
|
623
interface/Cargo.lock
generated
623
interface/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -7,19 +7,13 @@ 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 = { version = "1.1.1", features = ["alloc", "use-std"] }
|
postcard = "1.1.1"
|
||||||
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 = { version = "1.0.217", features = ["derive"] }
|
serde = "1.0.217"
|
||||||
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"
|
||||||
|
|
||||||
eframe = { version = "0.30", features = ["persistence"] }
|
eframe = "0.30"
|
||||||
egui_extras = { version = "0.30", features = ["default", "image"] }
|
egui_extras = { version = "0.30", features = ["default", "image"] }
|
||||||
egui-toast = "0.16.0"
|
egui-toast = "0.16.0"
|
||||||
home = "0.5.11"
|
|
||||||
atomic_float = "1.1.0"
|
|
||||||
chrono = "0.4.40"
|
|
||||||
image = "0.25.5"
|
|
||||||
libloading = "0.8.6"
|
|
||||||
emath = "0.31.1"
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
|
@ -1,121 +0,0 @@
|
||||||
use std::{collections::VecDeque, ops::{Deref, Div, Index, Range, Sub}, sync::Arc};
|
|
||||||
|
|
||||||
use common::{CamState, ControlPacket, SensorData, TelemetryPacket};
|
|
||||||
use anyhow::{Context, Ok, Result};
|
|
||||||
use eframe::Storage;
|
|
||||||
use libloading::{Library, Symbol};
|
|
||||||
use tokio::sync::{self, broadcast, mpsc, watch};
|
|
||||||
|
|
||||||
pub struct AutoConfig {
|
|
||||||
turn_gain: f32,
|
|
||||||
auto_fire_distance: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AutoInterface {
|
|
||||||
command_sender: Option<mpsc::Sender<ControlPacket>>,
|
|
||||||
data_receiver: watch::Receiver<TelemetryPacket>,
|
|
||||||
config: &'static dyn Storage,
|
|
||||||
enabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AutoInterface {
|
|
||||||
/// change active command, fails if not in control
|
|
||||||
pub async fn run_command(&self, command: ControlPacket) -> Result<()> {
|
|
||||||
self.command_sender.as_ref().context("no sender")?.send(command).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn sensor_data(&mut self) -> SensorData {
|
|
||||||
self.data_receiver.borrow_and_update().sensors.clone()
|
|
||||||
}
|
|
||||||
pub fn cam_state(&mut self) -> CamState {
|
|
||||||
self.data_receiver.borrow_and_update().cam_state.clone()
|
|
||||||
}
|
|
||||||
pub async fn sensor_update(&mut self) -> SensorData {
|
|
||||||
self.data_receiver.changed().await.unwrap();
|
|
||||||
self.data_receiver.borrow().sensors.clone()
|
|
||||||
|
|
||||||
}
|
|
||||||
/// disable auto
|
|
||||||
pub fn disable(&self) {unimplemented!()}
|
|
||||||
/// request auto enable, fails if the driver does not grant it
|
|
||||||
pub fn enable(&mut self) -> Result<()> {
|
|
||||||
self.enabled = self.command_sender.is_some();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn enabled(&self) -> bool { self.enabled}
|
|
||||||
pub fn conf(&self, key: &'static Configurable) -> f32 {
|
|
||||||
self.config.get_string(&key.name)
|
|
||||||
.map(|s| s.parse().ok())
|
|
||||||
.flatten().unwrap_or(key.default)
|
|
||||||
}
|
|
||||||
fn send_message(&self, message: String) {unimplemented!()}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Configurable {
|
|
||||||
pub name: &'static str,
|
|
||||||
pub description: Option<&'static str>,
|
|
||||||
pub default: f32,
|
|
||||||
pub min: f32,
|
|
||||||
pub max: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Configurable {
|
|
||||||
pub const fn new(name: &'static str) -> Self {
|
|
||||||
Self { name, description: None, default: 0.0, min: 0.0, max: 1.0 }
|
|
||||||
}
|
|
||||||
pub const fn range(self, range: Range<f32>) -> Self {
|
|
||||||
Self { min: range.start, max: range.end, ..self }
|
|
||||||
}
|
|
||||||
pub const fn description(self, description: &'static str) -> Self {
|
|
||||||
Self { description: Some(description), ..self }
|
|
||||||
}
|
|
||||||
pub const fn default(self, default: f32) -> Self {
|
|
||||||
Self { default, ..self }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn name(&'static self) -> &'static str {
|
|
||||||
self.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Configurable {
|
|
||||||
type Target = str;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Configurable {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self { name: Default::default(), description: Default::default(), default: Default::default(), min: 0.0, max: 1.0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A fake trait in the sense that these methods are exposed as symbols, not trait methods
|
|
||||||
pub trait Auto {
|
|
||||||
/// entrypoint
|
|
||||||
async fn run(interface: &AutoInterface);
|
|
||||||
/// register
|
|
||||||
fn default_configs() -> [Configurable];
|
|
||||||
fn name() -> &'static str;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn get_confs() -> Result<()> {
|
|
||||||
let lib = unsafe {Library::new("../auto/target/release/libauto.so")?};
|
|
||||||
|
|
||||||
let configurations: Symbol<&&[Configurable]> = unsafe {lib.get(b"CONFIGS").unwrap()};
|
|
||||||
|
|
||||||
for config in configurations.iter() {
|
|
||||||
println!("co {}", config.name);
|
|
||||||
println!("co {:?}", config.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
let name: Symbol<&&'static str> = unsafe {lib.get(b"NAME").unwrap()};
|
|
||||||
println!("na {:?}", *name);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
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(())
|
|
||||||
}
|
|
|
@ -1,31 +1,16 @@
|
||||||
use std::{cell::OnceCell, collections::HashMap, path::PathBuf, sync::{atomic::Ordering, Arc}, time::Duration};
|
|
||||||
|
use std::cell::OnceCell;
|
||||||
|
|
||||||
use common::{ControlPacket, TelemetryPacket};
|
use common::{ControlPacket, TelemetryPacket};
|
||||||
use eframe::{egui::{self, containers, Align2, Checkbox, Context, IconData, Id, ImageSource, Label, Ui}, Storage};
|
use eframe::egui::{self, containers, Align2, Checkbox, Context, Label};
|
||||||
use image::ImageFormat;
|
use tokio::sync::{mpsc, watch::Receiver};
|
||||||
use tokio::{runtime::Runtime, sync::{mpsc, watch::Receiver}};
|
|
||||||
use egui_toast::{Toast, Toasts};
|
use egui_toast::{Toast, Toasts};
|
||||||
|
|
||||||
use crate::{auto::Configurable, storage_dir::storage_dir, POWER_THRESHOLD};
|
|
||||||
|
|
||||||
pub const GUI: OnceCell<Context> = OnceCell::new();
|
pub const GUI: OnceCell<Context> = OnceCell::new();
|
||||||
|
|
||||||
pub fn gui(data: Receiver<GUIData>, toasts: mpsc::Receiver<Toast>, executor: Runtime) -> eframe::Result {
|
pub fn gui(data: Receiver<GUIData>, toasts: mpsc::Receiver<Toast>) -> eframe::Result {
|
||||||
let icon = egui::include_image!("../assets/lizard.png");
|
|
||||||
|
|
||||||
let icon = image::load_from_memory_with_format(include_bytes!("../assets/lizard.png"), ImageFormat::Png).unwrap();
|
|
||||||
|
|
||||||
let icon = IconData {
|
|
||||||
width: icon.width(),
|
|
||||||
height: icon.height(),
|
|
||||||
rgba: icon.into_rgba8().into_raw(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let options = eframe::NativeOptions {
|
let options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
|
||||||
.with_inner_size([700.0, 480.0])
|
|
||||||
.with_app_id("cruisecontrol")
|
|
||||||
.with_icon(icon),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
|
@ -35,7 +20,7 @@ pub fn gui(data: Receiver<GUIData>, toasts: mpsc::Receiver<Toast>, executor: Run
|
||||||
// This gives us image support:
|
// This gives us image support:
|
||||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||||
|
|
||||||
Ok(Box::new(GUI::with_receivers(data, toasts, executor, cc.storage.unwrap())))
|
Ok(Box::new(GUI::with_receivers(data, toasts)))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -46,86 +31,23 @@ pub struct GUIData {
|
||||||
pub last_command: Option<ControlPacket>,
|
pub last_command: Option<ControlPacket>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub const DEFAULT_VOLUME_THRESHOLD: f32 = 5.0;
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub const DEFAULT_VOLUME_THRESHOLD: f32 = 1.0;
|
|
||||||
const DEFAULT_TURN_GAIN: f32 = 0.3;
|
|
||||||
const DEFAULT_FIRE_DISTANCE: f32 = 55.0;
|
|
||||||
|
|
||||||
struct GUI {
|
struct GUI {
|
||||||
data: Receiver<GUIData>,
|
data: Receiver<GUIData>,
|
||||||
toasts: mpsc::Receiver<Toast>,
|
toasts: mpsc::Receiver<Toast>,
|
||||||
executor: Option<Runtime>,
|
|
||||||
volume_threshold: f32,
|
|
||||||
auto_turn_gain: f32,
|
|
||||||
/// mm
|
|
||||||
auto_fire_distance: f32,
|
|
||||||
selected_auto: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GUI {
|
impl GUI {
|
||||||
fn with_receivers(data: Receiver<GUIData>, toasts: mpsc::Receiver<Toast>, executor: Runtime, storage: &dyn Storage) -> Self {
|
fn with_receivers(data: Receiver<GUIData>, toasts: mpsc::Receiver<Toast>) -> Self {
|
||||||
let volume_threshold: f32 = storage.get_string("volume_threshold").map(|s| s.parse().ok()).flatten().unwrap_or(DEFAULT_VOLUME_THRESHOLD);
|
|
||||||
|
|
||||||
let auto_turn_gain: f32 = storage.get_string("auto_turn_gain").map(|s| s.parse().ok()).flatten().unwrap_or(DEFAULT_TURN_GAIN);
|
|
||||||
|
|
||||||
let auto_fire_distance: f32 = storage.get_string("auto_fire_distance").map(|s| s.parse().ok()).flatten().unwrap_or(DEFAULT_FIRE_DISTANCE);
|
|
||||||
|
|
||||||
let selected_auto: usize = storage.get_string("selected_auto").map(|s| s.parse().ok()).flatten().unwrap_or(0);
|
|
||||||
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
data,
|
data,
|
||||||
toasts,
|
toasts,
|
||||||
executor: Some(executor),
|
|
||||||
volume_threshold,
|
|
||||||
auto_turn_gain,
|
|
||||||
auto_fire_distance,
|
|
||||||
selected_auto,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dupe from auto crate for testing
|
|
||||||
const AUTO_GAP: Configurable = Configurable::new("auto minimum gap").range(0. .. 300.).default(140.)
|
|
||||||
.description("distance (mm) distance measurements must instantaneously drop to indicate a detection. This should line up with the size of the smallest robot you compete against");
|
|
||||||
const AUTO_SELF_OCCLUSION: Configurable = Configurable::new("auto self occlusion").range(0. .. 200.).default(143.)
|
|
||||||
.description("distance (mm) below which measurements are considered noise in the scan phase");
|
|
||||||
|
|
||||||
pub static CONFIGS: &[Configurable] = &[
|
|
||||||
AUTO_GAP,
|
|
||||||
AUTO_SELF_OCCLUSION,
|
|
||||||
];
|
|
||||||
|
|
||||||
impl eframe::App for GUI {
|
impl eframe::App for GUI {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
let _ = GUI.set(ctx.clone());
|
let _ = GUI.set(ctx.clone());
|
||||||
egui::SidePanel::right(Id::new("config")).resizable(false).show(ctx, |ui| {
|
|
||||||
ui.heading("configuration");
|
|
||||||
ui.add(egui::Slider::new(&mut self.volume_threshold, 0.0..=10.0).text("volume threshold"));
|
|
||||||
ui.label("higher accepts less noise (better)");
|
|
||||||
ui.add(egui::Slider::new(&mut self.auto_turn_gain, 0.0..=1.0).text("auto turn kP"));
|
|
||||||
ui.add(egui::Slider::new(&mut self.auto_fire_distance, 30.0..=100.0).text("auto fire distance (mm)"));
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
egui::ComboBox::new("auto selector", "select auto")
|
|
||||||
.show_index(ui, &mut self.selected_auto, 3, |n| ["a","b","c"][n]);
|
|
||||||
ui.button("refresh").clicked();
|
|
||||||
});
|
|
||||||
|
|
||||||
let storage = _frame.storage_mut().unwrap();
|
|
||||||
storage.set_string("volume_threshold", format!("{}",self.volume_threshold));
|
|
||||||
storage.set_string("auto_turn_gain", format!("{}",self.auto_turn_gain));
|
|
||||||
storage.set_string("auto_fire_distance", format!("{}",self.auto_fire_distance));
|
|
||||||
storage.set_string("selected_auto", format!("{}",self.selected_auto));
|
|
||||||
|
|
||||||
POWER_THRESHOLD.store(self.volume_threshold, Ordering::Relaxed);
|
|
||||||
|
|
||||||
for conf in CONFIGS {
|
|
||||||
configurator(ui, conf, storage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
ui.heading("Cruise Control");
|
ui.heading("Cruise Control");
|
||||||
|
|
||||||
|
@ -152,25 +74,5 @@ impl eframe::App for GUI {
|
||||||
}
|
}
|
||||||
|
|
||||||
toasts.show(ctx);
|
toasts.show(ctx);
|
||||||
|
|
||||||
if ctx.input(|i| i.viewport().close_requested()) {
|
|
||||||
self.executor.take().unwrap().shutdown_timeout(Duration::from_millis(100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn configurator(ui: &mut Ui, config: &Configurable, map: &mut dyn Storage) {
|
|
||||||
ui.add(egui::Slider::from_get_set(config.min as f64 ..= config.max as f64, |value| {
|
|
||||||
match value {
|
|
||||||
Some(value) => {
|
|
||||||
map.set_string(&config.name,format!("{value}"));
|
|
||||||
value
|
|
||||||
},
|
|
||||||
None => map.get_string(&config.name).map(|s| s.parse().ok()).flatten().unwrap_or(config.default as f64),
|
|
||||||
}
|
|
||||||
|
|
||||||
}).text(config.name));
|
|
||||||
if let Some(description) = config.description {
|
|
||||||
ui.label(description);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
#![feature(iter_collect_into)]
|
|
||||||
|
|
||||||
use atomic_float::AtomicF32;
|
|
||||||
use gui::DEFAULT_VOLUME_THRESHOLD;
|
|
||||||
|
|
||||||
pub mod gui;
|
|
||||||
pub mod storage_dir;
|
|
||||||
pub mod combatlog;
|
|
||||||
pub mod auto;
|
|
||||||
|
|
||||||
pub const POWER_THRESHOLD: AtomicF32 = AtomicF32::new(DEFAULT_VOLUME_THRESHOLD);
|
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
use std::{fmt::format, ops::ControlFlow, result, sync::{atomic::Ordering, Arc}, thread::{self, sleep}, time::Duration};
|
#![feature(iter_collect_into)]
|
||||||
|
use std::{ops::ControlFlow, result, sync::Arc, thread::{self, sleep}, time::Duration};
|
||||||
|
|
||||||
use anyhow::{Context, Ok, Result};
|
use anyhow::{Context, Ok, Result};
|
||||||
use atomic_float::AtomicF32;
|
|
||||||
use interface::{auto::get_confs, combatlog::{combat_logger, CombatData}, POWER_THRESHOLD};
|
|
||||||
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};
|
||||||
use interface::gui::{gui, GUIData, DEFAULT_VOLUME_THRESHOLD, GUI};
|
use gui::{gui, GUIData, GUI};
|
||||||
use pitch_detection::{detector::{mcleod::McLeodDetector, PitchDetector}, utils};
|
use pitch_detection::{detector::{mcleod::McLeodDetector, PitchDetector}, utils};
|
||||||
use rust_music_theory::note::{Note, NoteLetter, Pitch, Tuning};
|
use rust_music_theory::note::{Note, NoteLetter, Pitch, Tuning};
|
||||||
use tokio::{io::{AsyncReadExt, AsyncWriteExt, BufWriter, WriteHalf}, net::{tcp::{OwnedReadHalf, OwnedWriteHalf}, TcpStream}, spawn, sync::{self, broadcast, mpsc, watch, RwLock}, time::timeout};
|
use tokio::{io::{AsyncReadExt, AsyncWriteExt, BufWriter, WriteHalf}, net::{tcp::{OwnedReadHalf, OwnedWriteHalf}, TcpStream}, spawn, sync::{self, mpsc, watch, RwLock}, time::timeout};
|
||||||
|
|
||||||
|
mod gui;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
get_confs().unwrap();
|
|
||||||
panic!("got too far");
|
|
||||||
|
|
||||||
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();
|
||||||
|
@ -28,46 +23,30 @@ 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());
|
||||||
let rate = config.sample_rate();
|
let rate = config.sample_rate();
|
||||||
|
|
||||||
let (sender, notes) = broadcast::channel(2);
|
let (sender, notes) = mpsc::channel(2);
|
||||||
|
|
||||||
|
const POWER_THRESHOLD: f32 = 5.0;
|
||||||
const CLARITY_THRESHOLD: f32 = 0.85;
|
const CLARITY_THRESHOLD: f32 = 0.85;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
const PACKET_LEN: usize = 2204;
|
const PACKET_LEN: usize = 2204;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
const PACKET_LEN: usize = 1800;
|
const PACKET_LEN: usize = 512;
|
||||||
|
|
||||||
let mut packet = Vec::new();
|
|
||||||
|
|
||||||
let stream = device.build_input_stream(&config.into(),
|
let stream = device.build_input_stream(&config.into(),
|
||||||
move | data: &[f32], _: &_| {
|
move | data: &[f32], _: &_| {
|
||||||
let mut data = data;
|
|
||||||
|
|
||||||
if packet.len() > PACKET_LEN {
|
|
||||||
packet.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.len() <= PACKET_LEN {
|
|
||||||
packet.extend_from_slice(data);
|
|
||||||
if packet.len() > PACKET_LEN {
|
|
||||||
data = &packet;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(data.len() >= PACKET_LEN);
|
assert!(data.len() >= PACKET_LEN);
|
||||||
let data = &data[..PACKET_LEN];
|
let data = &data[..PACKET_LEN];
|
||||||
|
|
||||||
// reinitialized every packet as it is not thread safe
|
// reinitialized every packet as it is not thread safe
|
||||||
let mut detctor = McLeodDetector::new(PACKET_LEN, PACKET_LEN/2);
|
let mut detctor = McLeodDetector::new(PACKET_LEN, PACKET_LEN/2);
|
||||||
let vol = utils::buffer::square_sum(data);
|
let vol = utils::buffer::square_sum(data);
|
||||||
if let Err(e) = sender.send((detctor.get_pitch(data, rate.0 as usize, POWER_THRESHOLD.load(Ordering::Relaxed), CLARITY_THRESHOLD).map(|p|p.frequency),vol)) {
|
sender.blocking_send((detctor.get_pitch(data, rate.0 as usize, POWER_THRESHOLD, CLARITY_THRESHOLD),vol)).unwrap();
|
||||||
println!("channel: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
move |err| {eprintln!("{err}")} ,
|
move |err| {eprintln!("{err}")} ,
|
||||||
|
@ -83,53 +62,32 @@ fn main() -> Result<()> {
|
||||||
let (data_sender, gui_data) =watch::channel(GUIData::default());
|
let (data_sender, gui_data) =watch::channel(GUIData::default());
|
||||||
|
|
||||||
|
|
||||||
let spawner = executor.handle().clone();
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
spawner.block_on(async {
|
let control = executor.block_on(async {
|
||||||
let log_toasts = toast_sender.clone();
|
toast_sender.send(Toast::new().text("connecting to bot").kind(ToastKind::Info)).await.unwrap();
|
||||||
tokio::spawn(async move {
|
let cruisecontrol = TcpStream::connect("192.168.1.2:1234").await?;
|
||||||
if let Err(e) = combat_logger(combatlog).await {
|
println!("connected");
|
||||||
let _ = log_toasts.clone().send(Toast::new().text(format!("logger crashed: {e:?}")).kind(ToastKind::Error)).await;
|
toast_sender.send(Toast::new().text("connected").kind(ToastKind::Success)).await.unwrap();
|
||||||
}
|
cruisecontrol.set_nodelay(true)?;
|
||||||
});
|
let (telem, control) = cruisecontrol.into_split();
|
||||||
|
|
||||||
loop {
|
tokio::spawn(telemetry_handler(telem, data_sender.clone()));
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
Ok(control)
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
executor.block_on(controller(notes, control, data_sender.clone())).unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
println!("launching gui");
|
println!("launching gui");
|
||||||
gui(gui_data, toasts, executor).unwrap();
|
gui(gui_data, toasts).unwrap();
|
||||||
|
|
||||||
drop(stream);
|
drop(stream);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// frequency, volume
|
async fn telemetry_handler(mut telem: OwnedReadHalf, gui: watch::Sender<GUIData>) -> Result<()> {
|
||||||
type Detection = (Option<f32>, f32);
|
|
||||||
|
|
||||||
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?;
|
|
||||||
let cruisecontrol = TcpStream::connect("192.168.1.2:1234").await?;
|
|
||||||
println!("connected");
|
|
||||||
toast_sender.send(Toast::new().text("connected").kind(ToastKind::Success)).await?;
|
|
||||||
cruisecontrol.set_nodelay(true)?;
|
|
||||||
let (telem, control) = cruisecontrol.into_split();
|
|
||||||
|
|
||||||
tokio::spawn(telemetry_handler(telem, data_sender.clone(), logging_sender.clone()));
|
|
||||||
|
|
||||||
controller(notes, control, data_sender.clone(), logging_sender.clone()).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
@ -137,9 +95,7 @@ async fn telemetry_handler(mut telem: OwnedReadHalf, gui: watch::Sender<GUIData>
|
||||||
telem.read_exact(data).await?;
|
telem.read_exact(data).await?;
|
||||||
let telem: TelemetryPacket = postcard::from_bytes(&data)?;
|
let telem: TelemetryPacket = postcard::from_bytes(&data)?;
|
||||||
|
|
||||||
//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);
|
||||||
|
@ -148,7 +104,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>, logging_sender: mpsc::Sender<CombatData>) -> Result<()> {
|
async fn controller(mut notes: mpsc::Receiver<(Option<pitch_detection::Pitch<f32>>, f32)>, controller: OwnedWriteHalf, gui: watch::Sender<GUIData>) -> 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");
|
||||||
|
@ -161,7 +117,7 @@ async fn controller(mut notes: broadcast::Receiver<Detection>, controller: Owned
|
||||||
loop {
|
loop {
|
||||||
let mut control = ControlPacket::Stop;
|
let mut control = ControlPacket::Stop;
|
||||||
|
|
||||||
let result::Result::Ok(note) = timeout(Duration::from_millis(250), notes.recv()).await else {
|
let result::Result::Ok(note) = timeout(Duration::from_millis(95), notes.recv()).await else {
|
||||||
println!("timeout");
|
println!("timeout");
|
||||||
send_packet(&mut controller, ControlPacket::Stop).await?;
|
send_packet(&mut controller, ControlPacket::Stop).await?;
|
||||||
continue;
|
continue;
|
||||||
|
@ -169,15 +125,14 @@ async fn controller(mut notes: broadcast::Receiver<Detection>, controller: Owned
|
||||||
|
|
||||||
let (note,vol) = note.context("channel closed")?;
|
let (note,vol) = note.context("channel closed")?;
|
||||||
if let Some(note) = note {
|
if let Some(note) = note {
|
||||||
dbg!(note);
|
dbg!(note.frequency);
|
||||||
if let ControlFlow::Break(_) = sax_control(&mut control, vol, note) {
|
if let ControlFlow::Break(_) = sax_control(&mut control, vol, ¬e) {
|
||||||
if let ControlFlow::Break(_) = recorder_control(&mut control, vol, note) {
|
if let ControlFlow::Break(_) = recorder_control(&mut control, vol, note) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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?;
|
||||||
|
@ -190,16 +145,16 @@ async fn controller(mut notes: broadcast::Receiver<Detection>, controller: Owned
|
||||||
/// Weapon enabled
|
/// Weapon enabled
|
||||||
const ARMED: bool = true;
|
const ARMED: bool = true;
|
||||||
|
|
||||||
fn sax_control(control: &mut ControlPacket, vol: f32, frequency: f32) -> ControlFlow<()> {
|
fn sax_control(control: &mut ControlPacket, vol: f32, note: &pitch_detection::Pitch<f32>) -> ControlFlow<()> {
|
||||||
if frequency < 150. {
|
if note.frequency < 150. {
|
||||||
println!("too low");
|
println!("too low");
|
||||||
return ControlFlow::Break(());
|
return ControlFlow::Break(());
|
||||||
}
|
}
|
||||||
if frequency > 270. {
|
if note.frequency > 270. {
|
||||||
println!("too high");
|
println!("too high");
|
||||||
return ControlFlow::Break(());
|
return ControlFlow::Break(());
|
||||||
}
|
}
|
||||||
let note = Note::from_freq(frequency, Tuning::EqualTemperament);
|
let note = Note::from_freq(note.frequency, Tuning::EqualTemperament);
|
||||||
//dbg!(note.clarity);
|
//dbg!(note.clarity);
|
||||||
//dbg!(vol);
|
//dbg!(vol);
|
||||||
match note.pitch {
|
match note.pitch {
|
||||||
|
@ -243,16 +198,16 @@ fn sax_control(control: &mut ControlPacket, vol: f32, frequency: f32) -> Control
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn recorder_control(control: &mut ControlPacket, vol: f32, frequency: f32) -> ControlFlow<()> {
|
fn recorder_control(control: &mut ControlPacket, vol: f32, note: pitch_detection::Pitch<f32>) -> ControlFlow<()> {
|
||||||
if frequency < 300. {
|
if note.frequency < 300. {
|
||||||
println!("too low");
|
println!("too low");
|
||||||
return ControlFlow::Break(());
|
return ControlFlow::Break(());
|
||||||
}
|
}
|
||||||
if frequency > 600. {
|
if note.frequency > 600. {
|
||||||
println!("too high");
|
println!("too high");
|
||||||
return ControlFlow::Break(());
|
return ControlFlow::Break(());
|
||||||
}
|
}
|
||||||
let note = Note::from_freq(frequency, Tuning::EqualTemperament);
|
let note = Note::from_freq(note.frequency, Tuning::EqualTemperament);
|
||||||
//dbg!(note.clarity);
|
//dbg!(note.clarity);
|
||||||
//dbg!(vol);
|
//dbg!(vol);
|
||||||
match note.pitch {
|
match note.pitch {
|
||||||
|
|
|
@ -1,157 +0,0 @@
|
||||||
// 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
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in a new issue