From e62ab8374b4e6d78c148bdecb0e1fd7f4882d4ed Mon Sep 17 00:00:00 2001 From: David Gomes <10091092+davidgomesdev@users.noreply.github.com> Date: Sun, 9 Jun 2024 19:10:52 +0100 Subject: [PATCH] feat(server): add bounce effect --- server/src/graphql/schema_input.rs | 30 +++++- server/src/graphql/schema_mutation.rs | 49 ++++++++- server/src/graphql/schema_query.rs | 1 + server/src/graphql/schema_response.rs | 1 + server/src/monitoring/metrics.rs | 11 +- server/src/ps_move/api.rs | 2 +- server/src/ps_move/controller.rs | 18 ++-- server/src/ps_move/effects.rs | 110 ++++++++++++++------ server/src/spawn_tasks.rs | 2 +- server/src/tasks/controllers_list_update.rs | 2 +- server/src/tasks/effects_update.rs | 2 +- server/src/tasks/models.rs | 2 +- server/src/tasks/mutations_handler.rs | 6 +- 13 files changed, 176 insertions(+), 60 deletions(-) diff --git a/server/src/graphql/schema_input.rs b/server/src/graphql/schema_input.rs index ceeab31d..2f668943 100644 --- a/server/src/graphql/schema_input.rs +++ b/server/src/graphql/schema_input.rs @@ -26,7 +26,7 @@ pub(super) struct StaticLedEffectInput { pub duration: Option, #[graphql(description = "Name of the effect.")] pub name: Option, - #[graphql(description = "Hue/color (min 0.0, max 360.0)")] + #[graphql(description = "Hue/color (min 0, max 360)")] pub hue: i32, #[graphql(description = "Saturation (min 0.0, max 1.0)")] pub saturation: f64, @@ -44,7 +44,7 @@ pub(super) struct BreathingLedEffectInput { pub duration: Option, #[graphql(description = "Name of the effect.")] pub name: Option, - #[graphql(description = "Hue/color (min 0.0, max 360.0)")] + #[graphql(description = "Hue/color (min 0, max 360)")] pub hue: i32, #[graphql(description = "Saturation (min 0.0, max 1.0)")] pub saturation: f64, @@ -88,7 +88,7 @@ pub(super) struct BlinkLedEffectInput { pub duration: Option, #[graphql(description = "Name of the effect.")] pub name: Option, - #[graphql(description = "Hue/color (min 0.0, max 360.0)")] + #[graphql(description = "Hue/color (min 0, max 360)")] pub hue: i32, #[graphql(description = "Saturation (min 0.0, max 1.0)")] pub saturation: f64, @@ -101,14 +101,14 @@ pub(super) struct BlinkLedEffectInput { #[derive(GraphQLInputObject, Debug)] pub(super) struct CandleLedEffectInput { #[graphql( - description = "If specified, must not be empty, and applies the effect only on these controller addresses." + description = "If specified, must not be empty, and applies the effect only on these controller addresses." )] pub controllers: Option>, #[graphql(description = "Duration of effect, in milliseconds, if specified.")] pub duration: Option, #[graphql(description = "Name of the effect.")] pub name: Option, - #[graphql(description = "Hue/color (min 0.0, max 360.0)")] + #[graphql(description = "Hue/color (min 0, max 360)")] pub hue: i32, #[graphql(description = "Saturation (min 0.0, max 1.0)")] pub saturation: f64, @@ -122,6 +122,26 @@ pub(super) struct CandleLedEffectInput { pub interval: Option, } +#[derive(GraphQLInputObject, Debug)] +pub(super) struct BounceLedEffectInput { + #[graphql( + description = "If specified, must not be empty, and applies the effect only on these controller addresses." + )] + pub controllers: Option>, + #[graphql(description = "Duration of effect, in milliseconds, if specified.")] + pub duration: Option, + #[graphql(description = "Name of the effect.")] + pub name: Option, + #[graphql(description = "Hue colors to bounce (each min 0, max 360)")] + pub hues: Vec, + #[graphql(description = "To Saturation (min 0.0, max 1.0)")] + pub saturation: f64, + #[graphql(description = "To value (min 0.0, max 1.0)")] + pub value: f64, + #[graphql(description = "Step ratio, percentage of change from one color to the other. (min 0.0=stay in from, max 1.0=stay in to)")] + pub step: f64 +} + #[derive(GraphQLInputObject, Debug)] pub(super) struct StaticRumbleEffectInput { #[graphql( diff --git a/server/src/graphql/schema_mutation.rs b/server/src/graphql/schema_mutation.rs index fea8b148..c2f19f75 100644 --- a/server/src/graphql/schema_mutation.rs +++ b/server/src/graphql/schema_mutation.rs @@ -286,7 +286,7 @@ impl MutationRoot { } #[graphql( - description = "Randomly set brightness between min value and max value, simulating a candle/flame." + description = "Randomly set brightness between min value and max value, simulating a candle/flame." )] fn set_led_candle(ctx: &Context, input: CandleLedEffectInput) -> FieldResult { tracing::info!( @@ -363,6 +363,53 @@ impl MutationRoot { ) } + #[graphql( + description = "Bounces from one color to the other." + )] + fn set_led_bounce(ctx: &Context, input: BounceLedEffectInput) -> FieldResult { + tracing::info!( + "Received led bounce effect ({})", + input + .name + .clone() + .map_or(String::from("unnamed"), |name| format!("'{name}'")) + ); + tracing::debug!("Effect input: {input:?}"); + + if input.name.map_or(false, |name| name.is_empty()) { + return Err(FieldError::new("Name can't be empty!", Value::Null)); + } + + if !input.hues.iter().all(|hue| (0..=360).contains(hue)) { + return Err(FieldError::new( + "Hue must be between 0 and 360!", + Value::Null, + )); + } + + if !(0.0..=1.0).contains(&input.saturation) { + return Err(FieldError::new( + "Saturation must be between 0.0 and 1.0!", + Value::Null, + )); + } + + if !(0.0..=1.0).contains(&input.value) { + return Err(FieldError::new( + "Min value must between 0.0 and 1.0!", + Value::Null, + )); + } + + let effect = LedEffectKind::new_bounce(input.hues.iter().map(|hue| *hue as f32).collect(), input.saturation as f32, input.value as f32, input.step as f32); + + process_led_effect_mutation( + ctx, + LedEffect::from(effect, input.duration), + input.controllers, + ) + } + #[graphql(description = "Turn rumble off.")] fn set_rumble_off( ctx: &Context, diff --git a/server/src/graphql/schema_query.rs b/server/src/graphql/schema_query.rs index d4693c66..18644472 100644 --- a/server/src/graphql/schema_query.rs +++ b/server/src/graphql/schema_query.rs @@ -37,6 +37,7 @@ impl QueryRoot { api::LedEffectKind::Rainbow { .. } => { graphql::LedEffectType::Rainbow } api::LedEffectKind::Blink { .. } => { graphql::LedEffectType::Blink } api::LedEffectKind::Candle { .. } => { graphql::LedEffectType::Candle } + api::LedEffectKind::Bounce { .. } => { graphql::LedEffectType::Bounce } }, current_rumble_effect: match ctl.rumble_effect.kind { api::RumbleEffectKind::Off => { graphql::RumbleEffectType::Off } diff --git a/server/src/graphql/schema_response.rs b/server/src/graphql/schema_response.rs index cb5f1b44..2bf1f652 100644 --- a/server/src/graphql/schema_response.rs +++ b/server/src/graphql/schema_response.rs @@ -31,6 +31,7 @@ pub(super) enum LedEffectType { Rainbow, Blink, Candle, + Bounce, } #[derive(GraphQLEnum)] diff --git a/server/src/monitoring/metrics.rs b/server/src/monitoring/metrics.rs index 3e304bf5..0c75a79c 100644 --- a/server/src/monitoring/metrics.rs +++ b/server/src/monitoring/metrics.rs @@ -27,13 +27,10 @@ pub async fn metrics_handler() -> Result { if let Err(e) = encoder.encode(&prometheus::gather(), &mut buffer) { eprintln!("could not encode prometheus metrics: {}", e); }; - let result = match String::from_utf8(buffer.clone()) { - Ok(v) => v, - Err(e) => { - eprintln!("prometheus metrics could not be from_utf8'd: {}", e); - String::default() - } - }; + let result = String::from_utf8(buffer.clone()).unwrap_or_else(|e| { + eprintln!("prometheus metrics could not be from_utf8'd: {}", e); + String::default() + }); buffer.clear(); Ok(result) diff --git a/server/src/ps_move/api.rs b/server/src/ps_move/api.rs index bb11fe7d..eb8502c6 100644 --- a/server/src/ps_move/api.rs +++ b/server/src/ps_move/api.rs @@ -167,7 +167,7 @@ impl PsMoveApi { } if connection_type == ConnectionType::Usb { - usb_path = path.clone(); + usb_path.clone_from(&path); bt_address = if cfg!(windows) { self.get_bt_address_on_windows(&path) } else { diff --git a/server/src/ps_move/controller.rs b/server/src/ps_move/controller.rs index 11d32567..29a9ecc2 100644 --- a/server/src/ps_move/controller.rs +++ b/server/src/ps_move/controller.rs @@ -79,9 +79,9 @@ impl PsMoveController { } if self.connection_type == ConnectionType::Usb { - self.info.bt_path = other.info.bt_path.clone(); + self.info.bt_path.clone_from(&other.info.bt_path); } else if self.connection_type == ConnectionType::Bluetooth { - self.info.usb_path = other.info.usb_path.clone(); + self.info.usb_path.clone_from(&other.info.usb_path); } self.connection_type = ConnectionType::UsbAndBluetooth; } @@ -94,10 +94,10 @@ impl PsMoveController { } pub fn revert_led_effect(&mut self) { - let current_effect = self.led_effect; + let current_effect = self.led_effect.clone(); let current_led = self.setting.led; - self.led_effect = self.last_led_effect; + self.led_effect = self.last_led_effect.clone(); self.setting.led = self.setting.last_led; self.last_led_effect = current_effect; @@ -110,8 +110,8 @@ impl PsMoveController { info!("Last led effect '{}' of '{}' has already expired, setting to off", last_led_effect, self.bt_address); let off_effect = LedEffect::off(); - self.led_effect = off_effect; - self.setting.led = off_effect.kind.get_initial_hsv() + self.setting.led = off_effect.kind.get_initial_hsv(); + self.led_effect = off_effect } } @@ -119,13 +119,11 @@ impl PsMoveController { } pub fn set_led_effect(&mut self, effect: LedEffect) { - self.last_led_effect = self.led_effect; + self.last_led_effect = self.led_effect.clone(); self.setting.last_led = self.setting.led; - let kind = effect.kind; - + self.setting.led = effect.kind.get_initial_hsv(); self.led_effect = effect; - self.setting.led = kind.get_initial_hsv(); } pub fn set_led_effect_with_hsv(&mut self, effect: LedEffect, hsv: Hsv) { diff --git a/server/src/ps_move/effects.rs b/server/src/ps_move/effects.rs index 0a0d9de2..696f3c8d 100644 --- a/server/src/ps_move/effects.rs +++ b/server/src/ps_move/effects.rs @@ -1,8 +1,9 @@ use std::fmt; use std::fmt::Formatter; +use std::sync::Arc; use lazy_static::lazy_static; -use palette::{Hsv, ShiftHue}; +use palette::{Hsv, Mix, ShiftHue}; use rand::distributions::{Distribution, Uniform}; use rand::thread_rng; use strum_macros::Display; @@ -16,7 +17,7 @@ lazy_static! { const MAX_HUE_VALUE: f32 = 360.0; -#[derive(Clone, Copy)] +#[derive(Clone)] pub struct LedEffect { pub kind: LedEffectKind, pub start: Instant, @@ -51,7 +52,7 @@ impl LedEffect { /// Creates an expiring `LedEffect` if `duration_millis` is present, /// otherwise a non-expiring one pub fn from(kind: LedEffectKind, duration_millis: Option) -> LedEffect { - duration_millis.map_or(LedEffect::new(kind), |millis| { + duration_millis.map_or(LedEffect::new(kind.clone()), |millis| { if millis < 0 { panic!("Negative milliseconds as duration not allowed!") } @@ -79,7 +80,7 @@ impl fmt::Display for LedEffect { } } -#[derive(Clone, Copy, Display, Debug, PartialEq)] +#[derive(Clone, Display, Debug, PartialEq)] pub enum LedEffectKind { Off, Static { @@ -95,7 +96,7 @@ pub enum LedEffectKind { Rainbow { saturation: f32, value: f32, - time_to_complete: f32, + step: f32, }, Blink { hsv: Hsv, @@ -111,6 +112,12 @@ pub enum LedEffectKind { interval: i32, last_change: Instant, }, + Bounce { + colors: Arc>, + step: f32, + progress: f32, + next_color_index: usize, + }, } impl LedEffectKind { @@ -146,7 +153,7 @@ impl LedEffectKind { LedEffectKind::Rainbow { saturation, value, - time_to_complete: step, + step, } } @@ -156,7 +163,7 @@ impl LedEffectKind { min_value: f32, max_value: f32, variability: f32, - interval: Option + interval: Option, ) -> LedEffectKind { let value_range = max_value - min_value; let value_sample = Uniform::new_inclusive( @@ -175,48 +182,69 @@ impl LedEffectKind { } } + pub fn new_bounce( + hues: Vec, + saturation: f32, + value: f32, + step: f32, + ) -> LedEffectKind { + LedEffectKind::Bounce { + colors: Arc::new(hues.iter() + .map(|hue| Hsv::from_components((*hue, saturation, value))) + .collect()), + step, + progress: 0.0, + next_color_index: 1, + } + } + pub fn get_initial_hsv(&self) -> Hsv { - match *self { + match self { LedEffectKind::Off => Hsv::from_components((0.0, 0.0, 0.0)), LedEffectKind::Static { hsv } | LedEffectKind::Blink { hsv, - interval: _, - last_blink: _, - } => hsv, + .. + } => *hsv, LedEffectKind::Breathing { initial_hsv, peak, .. } => { - if peak < initial_hsv.value { + if *peak < initial_hsv.value { tracing::error!("Peak must be higher than initial value") } - initial_hsv + *initial_hsv } LedEffectKind::Rainbow { saturation, value, - time_to_complete: step, + step, } => { - if step > 360.0 { + if *step > 360.0 { tracing::error!("Step can't be higher than 360 (max hue)") } - Hsv::from_components((0.0, saturation, value)) + Hsv::from_components((0.0, *saturation, *value)) } LedEffectKind::Candle { hue, saturation, min_value, .. - } => Hsv::from_components((hue, saturation, min_value)), + } => Hsv::from_components((*hue, *saturation, *min_value)), + LedEffectKind::Bounce { + colors, + .. + } => { + colors[0] + } } } pub fn get_updated_hsv(&mut self, current_hsv: Hsv) -> Hsv { - match *self { + match self { LedEffectKind::Off => *LED_OFF, - LedEffectKind::Static { hsv } => hsv, + LedEffectKind::Static { hsv } => *hsv, LedEffectKind::Breathing { initial_hsv, time_to_peak, @@ -224,31 +252,31 @@ impl LedEffectKind { ref mut inhaling, ref mut last_update, } => Self::get_updated_breathing_hsv( - initial_hsv, + *initial_hsv, last_update, - time_to_peak as f32, - peak, + *time_to_peak as f32, + *peak, inhaling, ), LedEffectKind::Rainbow { - time_to_complete, + step, .. } => { // no need to use [saturation] and [value], // since it was already set in the beginning similar to breathing, // the step is relative to the max possible value - current_hsv.shift_hue(time_to_complete) + current_hsv.shift_hue(*step) } LedEffectKind::Blink { hsv, interval, last_blink: ref mut start, } => { - if start.elapsed() > interval / 2 { + if start.elapsed() > *interval / 2 { *start = Instant::now(); if current_hsv.value == 0.0 { - hsv + *hsv } else { *LED_OFF } @@ -265,18 +293,42 @@ impl LedEffectKind { interval, ref mut last_change } => { - if last_change.elapsed().as_millis() as i32 > interval { + if last_change.elapsed().as_millis() as i32 > *interval { *last_change = Instant::now(); let new_value = value_sample .sample(&mut thread_rng()) - .clamp(min_value, max_value); + .clamp(*min_value, *max_value); - Hsv::from_components((hue, saturation, new_value)) + Hsv::from_components((*hue, *saturation, new_value)) } else { current_hsv } } + LedEffectKind::Bounce { + colors, + step, + progress, + next_color_index, + } => { + let target_color = colors[*next_color_index]; + let new_color = current_hsv.mix(target_color, *progress); + + *progress += *step; + + // the new color never reaches exactly the target hue (the float "99.99994" problem) + if (new_color.hue.into_degrees() - target_color.hue.into_degrees()).abs() < 2.0 { + *next_color_index += 1; + + if *next_color_index + 1 > colors.len() { + *next_color_index = 0 + } + + *progress = 0.0; + } + + new_color + } } } diff --git a/server/src/spawn_tasks.rs b/server/src/spawn_tasks.rs index 3af43b07..fd78a4d8 100644 --- a/server/src/spawn_tasks.rs +++ b/server/src/spawn_tasks.rs @@ -47,7 +47,7 @@ pub async fn run_move( let api = PsMoveApi::new(); let shutdown_flag = Arc::new(AtomicBool::new(false)); - let initial_effect = Arc::new(Mutex::new(InitialLedState::from(*ON_STARTUP_EFFECT))); + let initial_effect = Arc::new(Mutex::new(InitialLedState::from(ON_STARTUP_EFFECT.clone()))); let (send, recv) = mpsc::channel::<()>(1); { diff --git a/server/src/tasks/controllers_list_update.rs b/server/src/tasks/controllers_list_update.rs index b10a3f0e..b35b9710 100644 --- a/server/src/tasks/controllers_list_update.rs +++ b/server/src/tasks/controllers_list_update.rs @@ -69,7 +69,7 @@ pub async fn run( let initial_state = initial_state.lock().await; new_controllers.into_iter().for_each(|mut controller| { - let initial_effect = initial_state.effect; + let initial_effect = initial_state.effect.clone(); let effect = if initial_effect.is_off() { tracing::info!( diff --git a/server/src/tasks/effects_update.rs b/server/src/tasks/effects_update.rs index 9dd4e4d6..e46c923a 100644 --- a/server/src/tasks/effects_update.rs +++ b/server/src/tasks/effects_update.rs @@ -1,5 +1,5 @@ use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::{Duration}; use tokio::sync::Mutex; use tokio::task::JoinHandle; diff --git a/server/src/tasks/models.rs b/server/src/tasks/models.rs index efddf89b..744618c0 100644 --- a/server/src/tasks/models.rs +++ b/server/src/tasks/models.rs @@ -14,7 +14,7 @@ pub enum EffectTarget { Only { bt_addresses: Vec }, } -#[derive(Clone, Copy)] +#[derive(Clone)] pub enum EffectChangeType { RevertLed, Led { effect: LedEffect }, diff --git a/server/src/tasks/mutations_handler.rs b/server/src/tasks/mutations_handler.rs index e6f7441f..a86d2608 100644 --- a/server/src/tasks/mutations_handler.rs +++ b/server/src/tasks/mutations_handler.rs @@ -24,7 +24,7 @@ pub async fn run( EffectTarget::All => { tracing::info!("Setting effect '{effect}' for all controllers"); controllers.iter_mut().for_each(|controller| { - mutate_controller_effect(controller, effect); + mutate_controller_effect(controller, effect.clone()); tracing::debug!( "Controller '{}' set to {effect}", controller.bt_address @@ -33,7 +33,7 @@ pub async fn run( if let EffectChangeType::Led { effect } = effect { let mut initial_state = initial_state.lock().await; - *initial_state = InitialLedState::from(effect); + *initial_state = InitialLedState::from(effect.clone()); tracing::debug!("Set '{effect}' as initial effect."); } } @@ -53,7 +53,7 @@ pub async fn run( ); }, |controller| { - mutate_controller_effect(controller, effect); + mutate_controller_effect(controller, effect.clone()); tracing::info!( "Controller '{}' set to {effect}", controller.bt_address