Krolyxon
Commit a41a513

Branches

Refs →

budctlcratesbudctl-guisrcbuds.rs

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