bt_hfp/sco/
connector.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fuchsia_bluetooth::types::PeerId;
6use futures::{Future, StreamExt};
7use log::{debug, info, warn};
8use std::collections::HashSet;
9use {fidl_fuchsia_bluetooth as fidl_bt, fidl_fuchsia_bluetooth_bredr as bredr};
10
11use crate::codec_id::CodecId;
12
13use super::{ConnectError, Connection};
14
15#[derive(Clone)]
16pub struct Connector {
17    proxy: bredr::ProfileProxy,
18    controller_codecs: HashSet<CodecId>,
19}
20
21fn common_sco_params() -> bredr::ScoConnectionParameters {
22    bredr::ScoConnectionParameters {
23        air_frame_size: Some(60), // Chosen to match legacy usage.
24        // IO parameters are to fit 16-bit PSM Signed audio input expected from the audio chip.
25        io_coding_format: Some(fidl_bt::AssignedCodingFormat::LinearPcm),
26        io_frame_size: Some(16),
27        io_pcm_data_format: Some(fidl_fuchsia_hardware_audio::SampleFormat::PcmSigned),
28        path: Some(bredr::DataPath::Offload),
29        ..Default::default()
30    }
31}
32
33/// If all eSCO parameters fail to setup a connection, these parameters are required to be
34/// supported by all peers.  HFP 1.8 Section 5.7.1.
35fn sco_params_fallback() -> bredr::ScoConnectionParameters {
36    bredr::ScoConnectionParameters {
37        parameter_set: Some(bredr::HfpParameterSet::D1),
38        air_coding_format: Some(fidl_bt::AssignedCodingFormat::Cvsd),
39        // IO bandwidth to match an 8khz audio rate.
40        io_bandwidth: Some(16000),
41        ..common_sco_params()
42    }
43}
44
45fn params_with_data_path(
46    sco_params: bredr::ScoConnectionParameters,
47    in_band_sco: bool,
48) -> bredr::ScoConnectionParameters {
49    bredr::ScoConnectionParameters {
50        path: in_band_sco.then_some(bredr::DataPath::Host).or(Some(bredr::DataPath::Offload)),
51        ..sco_params
52    }
53}
54
55// pub in this crate for tests
56pub(crate) fn parameter_sets_for_codec(
57    codec_id: CodecId,
58    in_band_sco: bool,
59) -> Vec<bredr::ScoConnectionParameters> {
60    use bredr::HfpParameterSet::*;
61    // Parameter sets from the HFP Spec, Section 5.7
62    // Core spec 5.3 requires the air coding and io coding formats both be transparent or neither
63    // should be.
64    // The parameter sets returned also meet this criteria.
65    match codec_id {
66        CodecId::MSBC => {
67            let (air_coding_format, io_bandwidth, io_coding_format) = if in_band_sco {
68                (
69                    Some(fidl_bt::AssignedCodingFormat::Transparent),
70                    Some(16000),
71                    Some(fidl_bt::AssignedCodingFormat::Transparent),
72                )
73            } else {
74                // IO bandwidth to match an 16khz audio rate. (x2 for input + output)
75                (
76                    Some(fidl_bt::AssignedCodingFormat::Msbc),
77                    Some(32000),
78                    Some(fidl_bt::AssignedCodingFormat::LinearPcm),
79                )
80            };
81            let params_fn = |set| bredr::ScoConnectionParameters {
82                parameter_set: Some(set),
83                air_coding_format,
84                io_coding_format,
85                io_bandwidth,
86                ..params_with_data_path(common_sco_params(), in_band_sco)
87            };
88            // TODO(b/200305833): Disable MsbcT1 for now as it results in bad audio
89            //vec![params_fn(T2), params_fn(T1)]
90            vec![params_fn(T2)]
91        }
92        // CVSD parameter sets
93        _ => {
94            let (io_bandwidth, io_frame_size, io_coding_format) = if in_band_sco {
95                (Some(16000), Some(8), Some(fidl_bt::AssignedCodingFormat::Cvsd))
96            } else {
97                (Some(16000), Some(16), Some(fidl_bt::AssignedCodingFormat::LinearPcm))
98            };
99
100            let params_fn = |set| bredr::ScoConnectionParameters {
101                parameter_set: Some(set),
102                io_bandwidth,
103                io_frame_size,
104                io_coding_format,
105                ..params_with_data_path(sco_params_fallback(), in_band_sco)
106            };
107            vec![params_fn(S4), params_fn(S1), params_fn(D1)]
108        }
109    }
110}
111
112#[derive(Debug, Clone, PartialEq, Copy)]
113enum InitiatorRole {
114    Initiate,
115    Accept,
116}
117
118impl Connector {
119    pub fn build(proxy: bredr::ProfileProxy, controller_codecs: HashSet<CodecId>) -> Self {
120        Self { proxy, controller_codecs }
121    }
122
123    async fn setup_sco_connection(
124        profile_proxy: bredr::ProfileProxy,
125        peer_id: PeerId,
126        role: InitiatorRole,
127        params: Vec<bredr::ScoConnectionParameters>,
128    ) -> Result<Connection, ConnectError> {
129        let (connection_proxy, server) =
130            fidl::endpoints::create_proxy::<bredr::ScoConnectionMarker>();
131        profile_proxy.connect_sco(bredr::ProfileConnectScoRequest {
132            peer_id: Some(peer_id.into()),
133            initiator: Some(role == InitiatorRole::Initiate),
134            params: Some(params),
135            connection: Some(server),
136            ..Default::default()
137        })?;
138
139        match connection_proxy.take_event_stream().next().await {
140            Some(Ok(bredr::ScoConnectionEvent::OnConnectionComplete { payload })) => {
141                match payload {
142                    bredr::ScoConnectionOnConnectionCompleteRequest::ConnectedParams(params) => {
143                        let params =
144                            params.try_into().map_err(|_| ConnectError::InvalidArguments)?;
145                        Ok(Connection { peer_id, params, proxy: connection_proxy })
146                    }
147                    bredr::ScoConnectionOnConnectionCompleteRequest::Error(err) => Err(err.into()),
148                    _ => {
149                        warn!("Received unknown ScoConnectionOnConnectionCompleteRequest");
150                        Err(ConnectError::Canceled)
151                    }
152                }
153            }
154            Some(Ok(bredr::ScoConnectionEvent::_UnknownEvent { .. })) => {
155                warn!("Received unknown ScoConnectionEvent");
156                Err(ConnectError::Canceled)
157            }
158            Some(Err(e)) => Err(e.into()),
159            None => Err(ConnectError::Canceled),
160        }
161    }
162
163    fn parameters_for_codecs(&self, codecs: Vec<CodecId>) -> Vec<bredr::ScoConnectionParameters> {
164        codecs
165            .into_iter()
166            .map(|id| parameter_sets_for_codec(id, !self.controller_codecs.contains(&id)))
167            .flatten()
168            .collect()
169    }
170
171    pub fn connect(
172        &self,
173        peer_id: PeerId,
174        codecs: Vec<CodecId>,
175    ) -> impl Future<Output = Result<Connection, ConnectError>> + 'static {
176        let params = self.parameters_for_codecs(codecs);
177        info!(peer_id:%, params:?; "Initiating SCO connection");
178
179        let proxy = self.proxy.clone();
180        async move {
181            for param in params {
182                let result = Self::setup_sco_connection(
183                    proxy.clone(),
184                    peer_id,
185                    InitiatorRole::Initiate,
186                    vec![param.clone()],
187                )
188                .await;
189                match &result {
190                    // Return early if there is a FIDL issue, or we succeeded.
191                    Err(ConnectError::Fidl { .. }) | Ok(_) => return result,
192                    // Otherwise continue to try the next params.
193                    Err(e) => {
194                        debug!(peer_id:%, param:?, e:?; "Connection failed, trying next set..")
195                    }
196                }
197            }
198            info!(peer_id:%; "Exhausted SCO connection parameters");
199            Err(ConnectError::Failed)
200        }
201    }
202
203    pub fn accept(
204        &self,
205        peer_id: PeerId,
206        codecs: Vec<CodecId>,
207    ) -> impl Future<Output = Result<Connection, ConnectError>> + 'static {
208        let params = self.parameters_for_codecs(codecs);
209        info!(peer_id:%, params:?; "Accepting SCO connection");
210
211        let proxy = self.proxy.clone();
212        Self::setup_sco_connection(proxy, peer_id, InitiatorRole::Accept, params)
213    }
214}
215
216#[cfg(test)]
217pub mod test {
218    use super::*;
219
220    use fidl_fuchsia_bluetooth_bredr::HfpParameterSet;
221
222    #[track_caller]
223    pub fn connection_for_codec(
224        peer_id: PeerId,
225        codec_id: CodecId,
226        in_band: bool,
227    ) -> (Connection, bredr::ScoConnectionRequestStream) {
228        let sco_params = parameter_sets_for_codec(codec_id, in_band).pop().unwrap();
229        let (proxy, stream) =
230            fidl::endpoints::create_proxy_and_stream::<bredr::ScoConnectionMarker>();
231        let connection = Connection::build(peer_id, sco_params, proxy);
232        (connection, stream)
233    }
234
235    #[fuchsia::test]
236    fn codec_parameters() {
237        let _exec = fuchsia_async::TestExecutor::new();
238        let all_codecs = vec![CodecId::MSBC, CodecId::CVSD];
239
240        // Out-of-band SCO.
241        let test_profile_server::TestProfileServerEndpoints { proxy: profile_svc, .. } =
242            test_profile_server::TestProfileServer::new(None, None);
243        let sco = Connector::build(profile_svc.clone(), all_codecs.iter().cloned().collect());
244        let res = sco.parameters_for_codecs(all_codecs.clone());
245        assert_eq!(res.len(), 4);
246        assert_eq!(
247            res,
248            vec![
249                bredr::ScoConnectionParameters {
250                    parameter_set: Some(HfpParameterSet::T2),
251                    air_coding_format: Some(fidl_bt::AssignedCodingFormat::Msbc),
252                    io_bandwidth: Some(32000),
253                    path: Some(bredr::DataPath::Offload),
254                    ..common_sco_params()
255                },
256                bredr::ScoConnectionParameters {
257                    parameter_set: Some(HfpParameterSet::S4),
258                    path: Some(bredr::DataPath::Offload),
259                    ..sco_params_fallback()
260                },
261                bredr::ScoConnectionParameters {
262                    parameter_set: Some(HfpParameterSet::S1),
263                    path: Some(bredr::DataPath::Offload),
264                    ..sco_params_fallback()
265                },
266                bredr::ScoConnectionParameters {
267                    path: Some(bredr::DataPath::Offload),
268                    ..sco_params_fallback()
269                },
270            ]
271        );
272
273        // All In-band SCO.
274        let sco = Connector::build(profile_svc.clone(), HashSet::new());
275        let res = sco.parameters_for_codecs(all_codecs.clone());
276        assert_eq!(res.len(), 4);
277        assert_eq!(
278            res,
279            vec![
280                bredr::ScoConnectionParameters {
281                    parameter_set: Some(HfpParameterSet::T2),
282                    air_coding_format: Some(fidl_bt::AssignedCodingFormat::Transparent),
283                    io_bandwidth: Some(16000),
284                    io_coding_format: Some(fidl_bt::AssignedCodingFormat::Transparent),
285                    path: Some(bredr::DataPath::Host),
286                    ..common_sco_params()
287                },
288                bredr::ScoConnectionParameters {
289                    parameter_set: Some(HfpParameterSet::S4),
290                    io_coding_format: Some(fidl_bt::AssignedCodingFormat::Cvsd),
291                    io_frame_size: Some(8),
292                    path: Some(bredr::DataPath::Host),
293                    ..sco_params_fallback()
294                },
295                bredr::ScoConnectionParameters {
296                    parameter_set: Some(HfpParameterSet::S1),
297                    io_coding_format: Some(fidl_bt::AssignedCodingFormat::Cvsd),
298                    io_frame_size: Some(8),
299                    path: Some(bredr::DataPath::Host),
300                    ..sco_params_fallback()
301                },
302                bredr::ScoConnectionParameters {
303                    io_coding_format: Some(fidl_bt::AssignedCodingFormat::Cvsd),
304                    io_frame_size: Some(8),
305                    path: Some(bredr::DataPath::Host),
306                    ..sco_params_fallback()
307                },
308            ]
309        );
310
311        // Mix of in-band and offloaded SCO
312        let only_cvsd_set = [CodecId::CVSD].iter().cloned().collect();
313        let sco = Connector::build(profile_svc.clone(), only_cvsd_set);
314        let res = sco.parameters_for_codecs(all_codecs);
315        assert_eq!(res.len(), 4);
316        assert_eq!(
317            res,
318            vec![
319                bredr::ScoConnectionParameters {
320                    parameter_set: Some(HfpParameterSet::T2),
321                    air_coding_format: Some(fidl_bt::AssignedCodingFormat::Transparent),
322                    io_bandwidth: Some(16000),
323                    io_coding_format: Some(fidl_bt::AssignedCodingFormat::Transparent),
324                    path: Some(bredr::DataPath::Host),
325                    ..common_sco_params()
326                },
327                bredr::ScoConnectionParameters {
328                    parameter_set: Some(HfpParameterSet::S4),
329                    path: Some(bredr::DataPath::Offload),
330                    ..sco_params_fallback()
331                },
332                bredr::ScoConnectionParameters {
333                    parameter_set: Some(HfpParameterSet::S1),
334                    path: Some(bredr::DataPath::Offload),
335                    ..sco_params_fallback()
336                },
337                bredr::ScoConnectionParameters {
338                    path: Some(bredr::DataPath::Offload),
339                    ..sco_params_fallback()
340                },
341            ]
342        );
343    }
344}