| 1 | //! The buds the window shows: made-up ones for `--demo` (roughly what the owner's T310 reported |
| 2 | //! at the last checkpoints), or an empty pair until the real buds answer. |
| 3 | |
| 4 | use budctl_core::status::{ |
| 5 | Battery, Charge, EqBand, EqPreset, EqPresets, Gesture, GestureChoice, NoiseControl, Status, Switch, TouchControls, |
| 6 | }; |
| 7 | |
| 8 | /// The buds the window shows. |
| 9 | pub struct Buds { |
| 10 | pub name: String, |
| 11 | pub connected: bool, |
| 12 | pub status: Status, |
| 13 | /// The EQ presets, when the buds list them. |
| 14 | pub eq: Option<EqPresets>, |
| 15 | /// The touch controls, when the buds report them. |
| 16 | pub touch: Option<TouchControls>, |
| 17 | /// The levels the bass level takes, when the buds have one to set. |
| 18 | pub bass_range: Option<(i8, i8)>, |
| 19 | /// The bass level last set from this window. The buds do not report it. |
| 20 | pub bass: Option<i8>, |
| 21 | } |
| 22 | |
| 23 | impl Buds { |
| 24 | /// The name to show for the EQ preset in use. |
| 25 | pub fn eq_name(&self) -> String { |
| 26 | eq_name(self.status.eq.as_deref(), self.eq.as_ref()) |
| 27 | } |
| 28 | |
| 29 | /// Records that `now` is the EQ preset in use. |
| 30 | pub fn set_eq(&mut self, now: String) { |
| 31 | if let Some(eq) = &mut self.eq { |
| 32 | eq.current = Some(now.clone()); |
| 33 | } |
| 34 | self.status.eq = Some(now); |
| 35 | } |
| 36 | |
| 37 | /// Takes in a custom preset as the buds report it after a write: saved, and in use. |
| 38 | pub fn set_custom(&mut self, preset: EqPreset) { |
| 39 | let now = key(&preset); |
| 40 | if let Some(shown) = self |
| 41 | .eq |
| 42 | .as_mut() |
| 43 | .and_then(|eq| eq.presets.iter_mut().find(|p| p.custom && p.id == preset.id)) |
| 44 | { |
| 45 | *shown = preset; |
| 46 | } |
| 47 | self.set_eq(now); |
| 48 | } |
| 49 | |
| 50 | /// The preset with this id. |
| 51 | pub fn preset(&self, id: u8) -> Option<&EqPreset> { |
| 52 | self.eq.as_ref()?.presets.iter().find(|p| p.id == id) |
| 53 | } |
| 54 | |
| 55 | /// What a gesture of a bud (`left`, `right`) does now. |
| 56 | pub fn action(&self, side: &str, gesture: &str) -> Option<&str> { |
| 57 | self.touch |
| 58 | .as_ref()? |
| 59 | .gestures |
| 60 | .iter() |
| 61 | .find(|g| g.side == side && g.gesture == gesture && g.group.is_none()) |
| 62 | .map(|g| g.action.as_str()) |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | /// The made-up buds of `--demo`. |
| 67 | pub fn demo() -> Buds { |
| 68 | let charge = |level| Some(Charge { level, charging: false }); |
| 69 | let switch = |key, label, on| Switch { key, label, on }; |
| 70 | let builtin = |id, name: &str| EqPreset { |
| 71 | name: name.to_owned(), |
| 72 | id, |
| 73 | custom: false, |
| 74 | bands: Vec::new(), |
| 75 | range: None, |
| 76 | }; |
| 77 | let band = |hz, gain| EqBand { hz, gain }; |
| 78 | let gesture = |side: &str, gesture: &str, action: &str| Gesture { |
| 79 | side: side.to_owned(), |
| 80 | gesture: gesture.to_owned(), |
| 81 | action: action.to_owned(), |
| 82 | group: None, |
| 83 | }; |
| 84 | let choice = |gesture: &str, actions: &[&str]| GestureChoice { |
| 85 | gesture: gesture.to_owned(), |
| 86 | actions: actions.iter().map(|&a| a.to_owned()).collect(), |
| 87 | }; |
| 88 | let names = |names: &[&str]| names.iter().map(|&n| n.to_owned()).collect::<Vec<_>>(); |
| 89 | Buds { |
| 90 | name: "realme Buds T310".to_owned(), |
| 91 | connected: true, |
| 92 | status: Status { |
| 93 | battery: Battery { |
| 94 | left: charge(80), |
| 95 | right: charge(80), |
| 96 | case: None, |
| 97 | }, |
| 98 | noise_control: Some(NoiseControl { |
| 99 | mode: "anc".to_owned(), |
| 100 | level: Some("deep".to_owned()), |
| 101 | }), |
| 102 | eq: Some("custom-4".to_owned()), |
| 103 | bass_level: None, |
| 104 | switches: vec![ |
| 105 | switch("game", "Game mode", false), |
| 106 | switch("dual", "Dual connection", true), |
| 107 | switch("wind", "Wind-noise reduction", true), |
| 108 | switch("spatial", "3D sound", true), |
| 109 | switch("bass-boost", "Bass boost", true), |
| 110 | ], |
| 111 | codec: Some("SBC".to_owned()), |
| 112 | firmware: Some("1.1.0.43".to_owned()), |
| 113 | warnings: Vec::new(), |
| 114 | }, |
| 115 | eq: Some(EqPresets { |
| 116 | current: Some("custom-4".to_owned()), |
| 117 | presets: vec![ |
| 118 | builtin(0, "pure-raw"), |
| 119 | builtin(1, "deep-bass"), |
| 120 | builtin(2, "pure-vocals"), |
| 121 | builtin(3, "pure-bass"), |
| 122 | EqPreset { |
| 123 | name: "Custom1".to_owned(), |
| 124 | id: 4, |
| 125 | custom: true, |
| 126 | bands: vec![ |
| 127 | band(50, 3), |
| 128 | band(250, 1), |
| 129 | band(1000, 0), |
| 130 | band(4000, -1), |
| 131 | band(8000, 1), |
| 132 | band(16000, 2), |
| 133 | ], |
| 134 | range: Some((-6, 6)), |
| 135 | }, |
| 136 | ], |
| 137 | warnings: Vec::new(), |
| 138 | }), |
| 139 | touch: Some(TouchControls { |
| 140 | gestures: vec![ |
| 141 | gesture("left", "double-tap", "play-pause"), |
| 142 | gesture("left", "triple-tap", "previous"), |
| 143 | gesture("left", "hold", "noise-cycle"), |
| 144 | gesture("right", "double-tap", "play-pause"), |
| 145 | gesture("right", "triple-tap", "next"), |
| 146 | gesture("right", "hold", "noise-cycle"), |
| 147 | ], |
| 148 | hold_cycle: Some(names(&["off", "anc"])), |
| 149 | choices: vec![ |
| 150 | choice("double-tap", &["none", "play-pause", "next", "voice-assistant"]), |
| 151 | choice( |
| 152 | "triple-tap", |
| 153 | &[ |
| 154 | "none", |
| 155 | "previous", |
| 156 | "next", |
| 157 | "voice-assistant", |
| 158 | "volume-up", |
| 159 | "volume-down", |
| 160 | "game-mode", |
| 161 | ], |
| 162 | ), |
| 163 | choice("hold", &["none", "noise-cycle", "voice-assistant", "game-mode"]), |
| 164 | ], |
| 165 | cycle_modes: names(&["off", "transparency", "anc"]), |
| 166 | warnings: Vec::new(), |
| 167 | }), |
| 168 | bass_range: Some((-5, 5)), |
| 169 | bass: None, |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | /// What the window shows before the real buds answer. |
| 174 | pub fn unknown() -> Buds { |
| 175 | Buds { |
| 176 | name: "Your buds".to_owned(), |
| 177 | connected: false, |
| 178 | status: Status::default(), |
| 179 | eq: None, |
| 180 | touch: None, |
| 181 | bass_range: None, |
| 182 | bass: None, |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | /// The name a preset is selected by, as in [`Status::eq`]: a built-in one's own name, |
| 187 | /// `custom-<id>` for a custom one. |
| 188 | pub fn key(preset: &EqPreset) -> String { |
| 189 | if preset.custom { |
| 190 | format!("custom-{}", preset.id) |
| 191 | } else { |
| 192 | preset.name.clone() |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | /// A preset's name to show: a custom one's stored name, a built-in one's in words. |
| 197 | pub fn shown_name(preset: &EqPreset) -> String { |
| 198 | if preset.custom { |
| 199 | preset.name.clone() |
| 200 | } else { |
| 201 | words(&preset.name) |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | /// The name to show for the EQ preset in use: a custom preset's own name (`custom-4` → |
| 206 | /// "Custom1"), else the preset's name in words (`pure-bass` → "Pure bass"). |
| 207 | pub fn eq_name(current: Option<&str>, presets: Option<&EqPresets>) -> String { |
| 208 | let Some(current) = current else { |
| 209 | return "Unknown".to_owned(); |
| 210 | }; |
| 211 | presets |
| 212 | .and_then(|eq| eq.presets.iter().find(|p| key(p) == current)) |
| 213 | .map_or_else(|| words(current), shown_name) |
| 214 | } |
| 215 | |
| 216 | /// `pure-bass` → "Pure bass". |
| 217 | pub fn words(name: &str) -> String { |
| 218 | let words = name.replace('-', " "); |
| 219 | let mut letters = words.chars(); |
| 220 | letters |
| 221 | .next() |
| 222 | .map(|first| first.to_uppercase().chain(letters).collect()) |
| 223 | .unwrap_or_default() |
| 224 | } |
| 225 | |
| 226 | #[cfg(test)] |
| 227 | mod tests { |
| 228 | use super::*; |
| 229 | |
| 230 | #[test] |
| 231 | fn eq_names() { |
| 232 | let buds = demo(); |
| 233 | assert_eq!(eq_name(Some("custom-4"), buds.eq.as_ref()), "Custom1"); |
| 234 | assert_eq!(eq_name(Some("custom-4"), None), "Custom 4"); |
| 235 | assert_eq!(eq_name(Some("pure-bass"), buds.eq.as_ref()), "Pure bass"); |
| 236 | assert_eq!(eq_name(None, None), "Unknown"); |
| 237 | assert_eq!(buds.eq_name(), "Custom1"); |
| 238 | } |
| 239 | |
| 240 | #[test] |
| 241 | fn eq_changes_show() { |
| 242 | let mut buds = demo(); |
| 243 | buds.set_eq("deep-bass".to_owned()); |
| 244 | assert_eq!(buds.eq_name(), "Deep bass"); |
| 245 | assert_eq!(buds.eq.as_ref().unwrap().current.as_deref(), Some("deep-bass")); |
| 246 | |
| 247 | let mut custom = buds.preset(4).unwrap().clone(); |
| 248 | custom.bands[0].gain = -6; |
| 249 | buds.set_custom(custom); |
| 250 | assert_eq!(buds.status.eq.as_deref(), Some("custom-4")); |
| 251 | assert_eq!(buds.preset(4).unwrap().bands[0].gain, -6); |
| 252 | } |
| 253 | } |