Krolyxon
Commit d55b50b

Branches

Refs →

budctlcratesbudctl-guisrcbuds.rs

318 lines · 10.4 KB · Rust

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
4use budctl_core::bt::BdAddr;
5use budctl_core::status::{
6 BassBand, Battery, Charge, DualDevice, EqBand, EqPreset, EqPresets, Gesture, GestureChoice, NoiseControl, Status,
7 Switch, TouchControls,
8};
9
10/// A bass level for each band of bass boost, where one is known.
11#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
12pub struct BassLevels {
13 low: Option<i8>,
14 medium: Option<i8>,
15 high: Option<i8>,
16}
17
18impl BassLevels {
19 pub fn get(self, band: BassBand) -> Option<i8> {
20 match band {
21 BassBand::Low => self.low,
22 BassBand::Medium => self.medium,
23 BassBand::High => self.high,
24 }
25 }
26
27 pub fn set(&mut self, band: BassBand, level: Option<i8>) {
28 let slot = match band {
29 BassBand::Low => &mut self.low,
30 BassBand::Medium => &mut self.medium,
31 BassBand::High => &mut self.high,
32 };
33 *slot = level;
34 }
35}
36
37/// The buds the window shows.
38pub struct Buds {
39 pub name: String,
40 pub connected: bool,
41 pub status: Status,
42 /// The EQ presets, when the buds list them.
43 pub eq: Option<EqPresets>,
44 /// The touch controls, when the buds report them.
45 pub touch: Option<TouchControls>,
46 /// The levels each band of bass boost takes, when the buds have bass levels to set.
47 pub bass_range: Option<(i8, i8)>,
48 /// The bass level of each band, as last set from this window. The buds do not report them.
49 pub bass: BassLevels,
50 /// The devices dual connection keeps, when the buds list them.
51 pub dual: Option<Vec<DualDevice>>,
52}
53
54impl Buds {
55 /// The name to show for the EQ preset in use.
56 pub fn eq_name(&self) -> String {
57 eq_name(self.status.eq.as_deref(), self.eq.as_ref())
58 }
59
60 /// Records that `now` is the EQ preset in use.
61 pub fn set_eq(&mut self, now: String) {
62 if let Some(eq) = &mut self.eq {
63 eq.current = Some(now.clone());
64 }
65 self.status.eq = Some(now);
66 }
67
68 /// Takes in a custom preset as the buds report it after a write: saved, and in use. A new
69 /// one joins the list.
70 pub fn set_custom(&mut self, preset: EqPreset) {
71 let now = key(&preset);
72 if let Some(eq) = self.eq.as_mut() {
73 match eq.presets.iter_mut().find(|p| p.custom && p.id == preset.id) {
74 Some(shown) => *shown = preset,
75 None => eq.presets.push(preset),
76 }
77 }
78 self.set_eq(now);
79 }
80
81 /// Takes in the whole preset list, as the buds report it.
82 pub fn set_presets(&mut self, eq: EqPresets) {
83 self.status.eq.clone_from(&eq.current);
84 self.eq = Some(eq);
85 }
86
87 /// The custom presets' names, as the buds store them.
88 pub fn custom_names(&self) -> impl Iterator<Item = (u8, &str)> {
89 self.eq
90 .iter()
91 .flat_map(|eq| eq.presets.iter())
92 .filter(|p| p.custom)
93 .map(|p| (p.id, p.name.as_str()))
94 }
95
96 /// The preset with this id.
97 pub fn preset(&self, id: u8) -> Option<&EqPreset> {
98 self.eq.as_ref()?.presets.iter().find(|p| p.id == id)
99 }
100
101 /// What a gesture of a bud (`left`, `right`) does now.
102 pub fn action(&self, side: &str, gesture: &str) -> Option<&str> {
103 self.touch
104 .as_ref()?
105 .gestures
106 .iter()
107 .find(|g| g.side == side && g.gesture == gesture && g.group.is_none())
108 .map(|g| g.action.as_str())
109 }
110}
111
112/// The made-up buds of `--demo`.
113pub fn demo() -> Buds {
114 let charge = |level| Some(Charge { level, charging: false });
115 let switch = |key, label, on| Switch { key, label, on };
116 let builtin = |id, name: &str| EqPreset {
117 name: name.to_owned(),
118 id,
119 custom: false,
120 bands: Vec::new(),
121 range: None,
122 };
123 let band = |hz, gain| EqBand { hz, gain };
124 let gesture = |side: &str, gesture: &str, action: &str| Gesture {
125 side: side.to_owned(),
126 gesture: gesture.to_owned(),
127 action: action.to_owned(),
128 group: None,
129 };
130 let choice = |gesture: &str, actions: &[&str]| GestureChoice {
131 gesture: gesture.to_owned(),
132 actions: actions.iter().map(|&a| a.to_owned()).collect(),
133 };
134 let names = |names: &[&str]| names.iter().map(|&n| n.to_owned()).collect::<Vec<_>>();
135 Buds {
136 name: "realme Buds T310".to_owned(),
137 connected: true,
138 status: Status {
139 battery: Battery {
140 left: charge(80),
141 right: charge(80),
142 case: None,
143 },
144 noise_control: Some(NoiseControl {
145 mode: "anc".to_owned(),
146 level: Some("deep".to_owned()),
147 }),
148 eq: Some("custom-4".to_owned()),
149 bass_level: None,
150 switches: vec![
151 switch("game", "Game mode", false),
152 switch("dual", "Dual connection", true),
153 switch("wind", "Wind-noise reduction", true),
154 switch("spatial", "3D sound", true),
155 switch("bass-boost", "Bass boost", true),
156 ],
157 codec: Some("SBC".to_owned()),
158 firmware: Some("1.1.0.43".to_owned()),
159 warnings: Vec::new(),
160 },
161 eq: Some(EqPresets {
162 current: Some("custom-4".to_owned()),
163 presets: vec![
164 builtin(0, "pure-raw"),
165 builtin(1, "deep-bass"),
166 builtin(2, "pure-vocals"),
167 builtin(3, "pure-bass"),
168 EqPreset {
169 name: "Custom1".to_owned(),
170 id: 4,
171 custom: true,
172 bands: vec![
173 band(50, 3),
174 band(250, 1),
175 band(1000, 0),
176 band(4000, -1),
177 band(8000, 1),
178 band(16000, 2),
179 ],
180 range: Some((-6, 6)),
181 },
182 ],
183 warnings: Vec::new(),
184 }),
185 touch: Some(TouchControls {
186 gestures: vec![
187 gesture("left", "double-tap", "play-pause"),
188 gesture("left", "triple-tap", "previous"),
189 gesture("left", "hold", "noise-cycle"),
190 gesture("right", "double-tap", "play-pause"),
191 gesture("right", "triple-tap", "next"),
192 gesture("right", "hold", "noise-cycle"),
193 ],
194 hold_cycle: Some(names(&["off", "anc"])),
195 choices: vec![
196 choice("double-tap", &["none", "play-pause", "next", "voice-assistant"]),
197 choice(
198 "triple-tap",
199 &[
200 "none",
201 "previous",
202 "next",
203 "voice-assistant",
204 "volume-up",
205 "volume-down",
206 "game-mode",
207 ],
208 ),
209 choice("hold", &["none", "noise-cycle", "voice-assistant", "game-mode"]),
210 ],
211 cycle_modes: names(&["off", "transparency", "anc"]),
212 warnings: Vec::new(),
213 }),
214 bass_range: Some((-5, 5)),
215 bass: BassLevels::default(),
216 dual: Some(vec![
217 DualDevice {
218 address: BdAddr::new([0x2B, 0x1A, 0x00, 0xAA, 0xBB, 0xCC]),
219 name: "arch".to_owned(),
220 connected: true,
221 this_computer: true,
222 main_audio: false,
223 playing: false,
224 },
225 DualDevice {
226 address: BdAddr::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]),
227 name: "Pixel 8".to_owned(),
228 connected: true,
229 this_computer: false,
230 main_audio: true,
231 playing: true,
232 },
233 ]),
234 }
235}
236
237/// What the window shows before the real buds answer.
238pub fn unknown() -> Buds {
239 Buds {
240 name: "Your buds".to_owned(),
241 connected: false,
242 status: Status::default(),
243 eq: None,
244 touch: None,
245 bass_range: None,
246 bass: BassLevels::default(),
247 dual: None,
248 }
249}
250
251/// The name a preset is selected by, as in [`Status::eq`]: a built-in one's own name,
252/// `custom-<id>` for a custom one.
253pub fn key(preset: &EqPreset) -> String {
254 if preset.custom {
255 format!("custom-{}", preset.id)
256 } else {
257 preset.name.clone()
258 }
259}
260
261/// A preset's name to show: a custom one's stored name, a built-in one's in words.
262pub fn shown_name(preset: &EqPreset) -> String {
263 if preset.custom {
264 preset.name.clone()
265 } else {
266 words(&preset.name)
267 }
268}
269
270/// The name to show for the EQ preset in use: a custom preset's own name (`custom-4` →
271/// "Custom1"), else the preset's name in words (`pure-bass` → "Pure bass").
272pub fn eq_name(current: Option<&str>, presets: Option<&EqPresets>) -> String {
273 let Some(current) = current else {
274 return "Unknown".to_owned();
275 };
276 presets
277 .and_then(|eq| eq.presets.iter().find(|p| key(p) == current))
278 .map_or_else(|| words(current), shown_name)
279}
280
281/// `pure-bass` → "Pure bass".
282pub fn words(name: &str) -> String {
283 let words = name.replace('-', " ");
284 let mut letters = words.chars();
285 letters
286 .next()
287 .map(|first| first.to_uppercase().chain(letters).collect())
288 .unwrap_or_default()
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn eq_names() {
297 let buds = demo();
298 assert_eq!(eq_name(Some("custom-4"), buds.eq.as_ref()), "Custom1");
299 assert_eq!(eq_name(Some("custom-4"), None), "Custom 4");
300 assert_eq!(eq_name(Some("pure-bass"), buds.eq.as_ref()), "Pure bass");
301 assert_eq!(eq_name(None, None), "Unknown");
302 assert_eq!(buds.eq_name(), "Custom1");
303 }
304
305 #[test]
306 fn eq_changes_show() {
307 let mut buds = demo();
308 buds.set_eq("deep-bass".to_owned());
309 assert_eq!(buds.eq_name(), "Deep bass");
310 assert_eq!(buds.eq.as_ref().unwrap().current.as_deref(), Some("deep-bass"));
311
312 let mut custom = buds.preset(4).unwrap().clone();
313 custom.bands[0].gain = -6;
314 buds.set_custom(custom);
315 assert_eq!(buds.status.eq.as_deref(), Some("custom-4"));
316 assert_eq!(buds.preset(4).unwrap().bands[0].gain, -6);
317 }
318}