Skip to main content

bt_broadcast_assistant/
debug.rs

1// Copyright 2024 Google LLC
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use bt_bap::types::BroadcastId;
6use bt_bass::client::error::Error as BassClientError;
7use bt_bass::client::event::Event as BassEvent;
8use bt_bass::types::{BisSync, PaSync, SubgroupIndex};
9
10#[cfg(any(test, feature = "debug"))]
11use bt_common::core::ltv::LtValue;
12#[cfg(any(test, feature = "debug"))]
13use bt_common::core::AddressType;
14use bt_common::core::AdvertisingSetId;
15use bt_common::debug_command::CommandRunner;
16use bt_common::debug_command::CommandSet;
17use bt_common::gen_commandset;
18#[cfg(any(test, feature = "debug"))]
19use bt_common::generic_audio::metadata_ltv::Metadata;
20use bt_common::PeerId;
21use bt_gatt::pii::GetPeerAddr;
22use std::collections::{HashMap, HashSet};
23
24use futures::stream::FusedStream;
25use futures::Future;
26use futures::Stream;
27use num::Num;
28use parking_lot::Mutex;
29use std::num::ParseIntError;
30use std::sync::Arc;
31
32use crate::assistant::event::*;
33use crate::assistant::peer::Peer;
34use crate::assistant::Error;
35use crate::*;
36
37gen_commandset! {
38    AssistantCmd {
39        Info = ("info", [], [], "Print information from broadcast assistant"),
40        Connect = ("connect", [], ["peer_id"], "Attempt connection to scan delegator"),
41        Disconnect = ("disconnect", [], [], "Disconnect from connected scan delegator"),
42        SendBroadcastCode = ("set-broadcast-code", [], ["broadcast_id", "broadcast_code"], "Attempt to send decryption key for a particular broadcast source to the scan delegator"),
43        AddBroadcastSource = ("add-broadcast-source", [], ["source_peer_id", "advertising_sid", "PaSyncOff|PaSyncPast|PaSyncNoPast", "[bis_sync]"], "Attempt to add a particular broadcast source to the scan delegator"),
44        UpdatePaSync = ("update-pa-sync", [], ["broadcast_id", "PaSyncOff|PaSyncPast|PaSyncNoPast", "[bis_sync]"], "Attempt to update the scan delegator's desired pa sync to a particular broadcast source"),
45        RemoveBroadcastSource = ("remove-broadcast-source", [], ["broadcast_id"], "Attempt to remove a particular broadcast source to the scan delegator"),
46        RemoteScanStarted = ("inform-scan-started", [], [], "Inform the scan delegator that we have started scanning on behalf of it"),
47        RemoteScanStopped = ("inform-scan-stopped", [], [], "Inform the scan delegator that we have stopped scanning on behalf of it"),
48        // TODO(http://b/433285146): Once PA scanning is implemented, remove bottom 3 commands.
49        ForceDiscoverBroadcastSource = ("force-discover-broadcast-source", [], ["source_peer_id", "address", "Public|Random", "advertising_sid"], "Force the broadcast assistant to become aware of the provided broadcast source"),
50        ForceDiscoverSourceMetadata = ("force-discover-source-metadata", [], ["source_peer_id", "advertising_sid", "metadata_big1", "[metadata_big_n]..."], "Force the broadcast assistant to become aware of the provided metadata, each BIG's metadata is comma separated"),
51        ForceDiscoverEmptySourceMetadata = ("force-discover-empty-source-metadata", [], ["source_peer_id", "advertising_sid", "num_big"], "Force the broadcast assistant to become aware of the provided empty metadata, as many as # BIGs specified"),
52    }
53}
54
55pub struct AssistantDebug<T: bt_gatt::GattTypes, R: GetPeerAddr> {
56    assistant: BroadcastAssistant<T>,
57    connected_peer: Mutex<Option<Arc<Peer<T>>>>,
58    peer_addr_getter: R,
59}
60
61impl<T: bt_gatt::GattTypes + 'static, R: GetPeerAddr> AssistantDebug<T, R> {
62    pub fn new(central: T::Central, peer_addr_getter: R) -> Self
63    where
64        <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
65    {
66        Self {
67            assistant: BroadcastAssistant::<T>::new(central),
68            connected_peer: Mutex::new(None),
69            peer_addr_getter,
70        }
71    }
72
73    pub fn start(&mut self) -> Result<EventStream<T>, Error> {
74        let event_stream = self.assistant.start()?;
75        Ok(event_stream)
76    }
77
78    pub fn look_for_scan_delegators(&mut self) -> Result<T::ScanResultStream, Error> {
79        self.assistant.scan_for_scan_delegators()
80    }
81
82    pub fn take_connected_peer_event_stream(
83        &mut self,
84    ) -> Result<impl Stream<Item = Result<BassEvent, BassClientError>> + FusedStream, Error> {
85        let mut lock = self.connected_peer.lock();
86        let Some(peer_arc) = lock.as_mut() else {
87            return Err(Error::Generic(format!("not connected to any scan delegator peer")));
88        };
89        let Some(peer) = Arc::get_mut(peer_arc) else {
90            return Err(Error::Generic(format!(
91                "cannot get mutable peer reference, it is shared elsewhere"
92            )));
93        };
94        peer.take_event_stream().map_err(|e| Error::Generic(format!("{e:?}")))
95    }
96
97    async fn with_peer<F, Fut>(&self, f: F)
98    where
99        F: FnOnce(Arc<Peer<T>>) -> Fut,
100        Fut: Future<Output = Result<(), crate::assistant::peer::Error>>,
101    {
102        let Some(peer) = self.connected_peer.lock().clone() else {
103            eprintln!("not connected to a scan delegator");
104            return;
105        };
106        if let Err(e) = f(peer).await {
107            eprintln!("failed to perform operation: {e:?}");
108        }
109    }
110}
111
112/// Attempt to parse a string into an integer.  If the string begins with 0x,
113/// treat the rest of the string as a hex value, otherwise treat it as decimal.
114pub(crate) fn parse_int<N>(input: &str) -> Result<N, ParseIntError>
115where
116    N: Num<FromStrRadixErr = ParseIntError>,
117{
118    if input.starts_with("0x") {
119        N::from_str_radix(&input[2..], 16)
120    } else {
121        N::from_str_radix(input, 10)
122    }
123}
124
125pub fn parse_peer_id(input: &str) -> Result<PeerId, String> {
126    let raw_id = match parse_int(input) {
127        Err(_) => return Err(format!("falied to parse int from {input}")),
128        Ok(i) => i,
129    };
130
131    Ok(PeerId(raw_id))
132}
133
134#[cfg(any(test, feature = "debug"))]
135/// Returns the bd address in little endian ordering.
136pub fn parse_bd_addr(input: &str) -> Result<[u8; 6], String> {
137    let mut tokens: Vec<u8> =
138        input.split(':').map(|t| u8::from_str_radix(t, 16)).filter_map(Result::ok).collect();
139    if tokens.len() != 6 {
140        return Err(format!("failed to parse bd address from {input}"));
141    }
142    tokens.reverse();
143    tokens.try_into().map_err(|e| format!("{e:?}"))
144}
145
146fn parse_broadcast_id(input: &str) -> Result<BroadcastId, String> {
147    let raw_id: u32 = match parse_int(input) {
148        Err(_) => return Err(format!("falied to parse int from {input}")),
149        Ok(i) => i,
150    };
151    raw_id.try_into().map_err(|e| format!("{e:?}"))
152}
153
154fn parse_advertising_sid(input: &str) -> Result<AdvertisingSetId, String> {
155    let raw_sid: u8 = parse_int(input).map_err(|_| format!("failed to parse int from {input}"))?;
156    AdvertisingSetId::try_from(raw_sid).map_err(|e| format!("{e:?}"))
157}
158
159fn parse_bis_sync(input: &str) -> HashMap<SubgroupIndex, BisSync> {
160    let mut map = HashMap::new();
161    for t in input.split(',') {
162        let parts: Vec<_> = t.split('-').collect();
163        if parts.len() != 2 {
164            eprintln!(
165                "invalid big-bis sync info {t}. should be in <Ith_BIG>-<BIS_INDEX> format, will be ignored"
166            );
167            continue;
168        }
169        let Ok(ith_big) = parse_int(parts[0]) else {
170            eprintln!("Failed to parse big index from '{}', ignoring.", parts[0]);
171            continue;
172        };
173        match parse_int::<u8>(parts[1]) {
174            Ok(bis_index) => {
175                let entry = map.entry(ith_big).or_insert(BisSync::no_sync());
176                if let Err(e) = entry.synchronize_to_index(bis_index) {
177                    eprintln!("Failed to set sync to BIS index: {e:?}");
178                }
179            }
180            Err(_) if parts[1] == "OFF" => {
181                map.insert(ith_big, BisSync::no_sync());
182            }
183            Err(e) => {
184                eprintln!("{e:?} - BIS index should be a number from 1-31, ignoring {}", parts[1]);
185            }
186        }
187    }
188    map
189}
190
191/// Converts a passcode string into a 16-byte broadcast code.
192/// The string is UTF-8 encoded and then padded with zeros on the right to a
193/// total length of 16 bytes. This result is a little-endian byte array
194/// equivalent to a 128-bit value.
195fn passcode_to_broadcast_code(passcode: &str) -> Result<[u8; 16], String> {
196    if passcode.is_empty() {
197        return Err("invalid broadcast code: passcode cannot be empty".to_string());
198    }
199    let code = passcode.as_bytes();
200    if code.len() > 16 {
201        return Err(format!(
202            "invalid broadcast code: '{}'. should be at max length 16, but was {}",
203            passcode,
204            code.len()
205        ));
206    }
207    let mut broadcast_code = [0u8; 16];
208    broadcast_code[..code.len()].copy_from_slice(code);
209    Ok(broadcast_code)
210}
211
212impl<T: bt_gatt::GattTypes + 'static, R: GetPeerAddr> CommandRunner for AssistantDebug<T, R>
213where
214    <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
215{
216    type Set = AssistantCmd;
217
218    fn run(
219        &self,
220        cmd: Self::Set,
221        args: Vec<String>,
222    ) -> impl futures::Future<Output = Result<(), impl std::error::Error>> {
223        let help_subcommands: HashSet<&str> = HashSet::from(["help", "-h", "--help"]);
224        async move {
225            if args.len() >= 1 && help_subcommands.contains(args[0].as_str()) {
226                eprintln!("usage: {}", cmd.help_simple());
227                return Ok(());
228            }
229            match cmd {
230                AssistantCmd::Info => {
231                    let known = self.assistant.known_broadcast_sources();
232                    println!("Known Broadcast Sources:");
233                    for (id, s) in known {
234                        println!("({id:?}): {s:?}");
235                    }
236                }
237                AssistantCmd::Connect => {
238                    if self.connected_peer.lock().is_some() {
239                        eprintln!(
240                            "peer already connected. Call `disconnect` first: {}",
241                            AssistantCmd::Disconnect.help_simple()
242                        );
243                        return Ok(());
244                    }
245                    if args.len() != 1 {
246                        eprintln!("usage: {}", AssistantCmd::Connect.help_simple());
247                        return Ok(());
248                    }
249
250                    let Ok(peer_id) = parse_peer_id(&args[0]) else {
251                        eprintln!("invalid peer id: {}", args[0]);
252                        return Ok(());
253                    };
254
255                    let peer = self.assistant.connect_to_scan_delegator(peer_id).await;
256                    match peer {
257                        Ok(peer) => {
258                            *self.connected_peer.lock() = Some(Arc::new(peer));
259                        }
260                        Err(e) => {
261                            eprintln!("failed to connect to scan delegator: {e:?}");
262                        }
263                    };
264                }
265                AssistantCmd::Disconnect => {
266                    if self.connected_peer.lock().take().is_none() {
267                        eprintln!("not connected to a scan delegator");
268                    }
269                }
270                AssistantCmd::SendBroadcastCode => {
271                    if args.len() != 2 {
272                        eprintln!("usage: {}", AssistantCmd::SendBroadcastCode.help_simple());
273                        return Ok(());
274                    }
275
276                    let Ok(broadcast_id) = parse_broadcast_id(&args[0]) else {
277                        eprintln!("invalid broadcast id: {}", args[0]);
278                        return Ok(());
279                    };
280
281                    let broadcast_code = match passcode_to_broadcast_code(&args[1]) {
282                        Ok(code) => code,
283                        Err(e) => {
284                            eprintln!("{e:?}");
285                            return Ok(());
286                        }
287                    };
288
289                    self.with_peer(|peer| async move {
290                        peer.send_broadcast_code(broadcast_id, broadcast_code).await
291                    })
292                    .await;
293                }
294                AssistantCmd::AddBroadcastSource => {
295                    if args.len() < 3 {
296                        eprintln!("usage: {}", AssistantCmd::AddBroadcastSource.help_simple());
297                        return Ok(());
298                    }
299
300                    let Ok(source_peer_id) = parse_peer_id(&args[0]) else {
301                        eprintln!("invalid peer id: {}", args[0]);
302                        return Ok(());
303                    };
304
305                    let Ok(advertising_sid) = parse_advertising_sid(&args[1]) else {
306                        eprintln!("invalid advertising sid: {}", args[1]);
307                        return Ok(());
308                    };
309
310                    let pa_sync: PaSync = match args[2].parse() {
311                        Ok(sync) => sync,
312                        Err(e) => {
313                            eprintln!("invalid pa_sync: {e:?}");
314                            return Ok(());
315                        }
316                    };
317
318                    let bis_sync =
319                        if args.len() == 4 { parse_bis_sync(&args[3]) } else { HashMap::new() };
320
321                    self.with_peer(|peer| async move {
322                        peer.add_broadcast_source(
323                            source_peer_id,
324                            advertising_sid,
325                            &self.peer_addr_getter,
326                            pa_sync,
327                            bis_sync,
328                        )
329                        .await
330                    })
331                    .await;
332                }
333                AssistantCmd::UpdatePaSync => {
334                    if args.len() < 2 {
335                        eprintln!("usage: {}", AssistantCmd::UpdatePaSync.help_simple());
336                        return Ok(());
337                    }
338
339                    let Ok(broadcast_id) = parse_broadcast_id(&args[0]) else {
340                        eprintln!("invalid broadcast id: {}", args[0]);
341                        return Ok(());
342                    };
343
344                    let pa_sync: PaSync = match args[1].parse() {
345                        Ok(sync) => sync,
346                        Err(e) => {
347                            eprintln!("invalid pa_sync: {e:?}");
348                            return Ok(());
349                        }
350                    };
351
352                    let bis_sync =
353                        if args.len() == 3 { parse_bis_sync(&args[2]) } else { HashMap::new() };
354
355                    self.with_peer(|peer| async move {
356                        peer.update_broadcast_source_sync(broadcast_id, pa_sync, bis_sync).await
357                    })
358                    .await;
359                }
360                AssistantCmd::RemoveBroadcastSource => {
361                    if args.len() != 1 {
362                        eprintln!("usage: {}", AssistantCmd::RemoveBroadcastSource.help_simple());
363                        return Ok(());
364                    }
365
366                    let Ok(broadcast_id) = parse_broadcast_id(&args[0]) else {
367                        eprintln!("invalid broadcast id: {}", args[0]);
368                        return Ok(());
369                    };
370
371                    self.with_peer(|peer| async move {
372                        peer.remove_broadcast_source(broadcast_id).await
373                    })
374                    .await;
375                }
376                AssistantCmd::RemoteScanStarted => {
377                    self.with_peer(|peer: Arc<Peer<T>>| async move {
378                        peer.inform_remote_scan_started().await
379                    })
380                    .await;
381                }
382                AssistantCmd::RemoteScanStopped => {
383                    self.with_peer(|peer| async move { peer.inform_remote_scan_stopped().await })
384                        .await;
385                }
386                #[cfg(feature = "debug")]
387                AssistantCmd::ForceDiscoverBroadcastSource => {
388                    if args.len() != 4 {
389                        eprintln!(
390                            "usage: {}",
391                            AssistantCmd::ForceDiscoverBroadcastSource.help_simple()
392                        );
393                        return Ok(());
394                    }
395
396                    let Ok(source_peer_id) = parse_peer_id(&args[0]) else {
397                        eprintln!("invalid peer id: {}", args[0]);
398                        return Ok(());
399                    };
400
401                    let Ok(address) = parse_bd_addr(&args[1]) else {
402                        eprintln!("invalid address: {}", args[1]);
403                        return Ok(());
404                    };
405
406                    let address_type: AddressType = match args[2].parse() {
407                        Ok(t) => t,
408                        Err(e) => {
409                            eprintln!("invalid address type: {e:?}");
410                            return Ok(());
411                        }
412                    };
413
414                    let Ok(advertising_sid) = parse_advertising_sid(&args[3]) else {
415                        eprintln!("invalid advertising sid: {}", args[3]);
416                        return Ok(());
417                    };
418
419                    match self.assistant.force_discover_broadcast_source(
420                        source_peer_id,
421                        address,
422                        address_type,
423                        advertising_sid,
424                    ) {
425                        Ok(source) => {
426                            println!("broadcast source after additional info: {source:?}")
427                        }
428                        Err(e) => {
429                            eprintln!("failed to enter in broadcast source information: {e:?}")
430                        }
431                    }
432                }
433                #[cfg(feature = "debug")]
434                AssistantCmd::ForceDiscoverSourceMetadata => {
435                    if args.len() < 3 {
436                        eprintln!(
437                            "usage: {}",
438                            AssistantCmd::ForceDiscoverSourceMetadata.help_simple()
439                        );
440                        return Ok(());
441                    }
442
443                    let Ok(source_peer_id) = parse_peer_id(&args[0]) else {
444                        eprintln!("invalid peer id: {}", args[0]);
445                        return Ok(());
446                    };
447
448                    let Ok(advertising_sid) = parse_advertising_sid(&args[1]) else {
449                        eprintln!("invalid advertising sid: {}", args[1]);
450                        return Ok(());
451                    };
452
453                    let mut all_big_metadata = Vec::new();
454                    for i in 2..args.len() {
455                        let raw_metadata: Vec<u8> = args[i]
456                            .split(',')
457                            .map(|t| parse_int(t))
458                            .filter_map(Result::ok)
459                            .collect();
460
461                        if raw_metadata.len() > 0 {
462                            let (decoded_metadata, consumed_len) =
463                                Metadata::decode_all(raw_metadata.as_slice());
464                            if consumed_len != raw_metadata.len() {
465                                eprintln!("Metadata length is not valid");
466                                return Ok(());
467                            }
468                            all_big_metadata.push(
469                                decoded_metadata.into_iter().filter_map(Result::ok).collect(),
470                            );
471                        } else {
472                            all_big_metadata.push(vec![]);
473                        }
474                    }
475
476                    match self.assistant.force_discover_broadcast_source_metadata(
477                        source_peer_id,
478                        advertising_sid,
479                        all_big_metadata,
480                    ) {
481                        Ok(source) => println!("broadcast source with metadata: {source:?}"),
482                        Err(e) => eprintln!("failed to enter in broadcast source metadata: {e:?}"),
483                    }
484                }
485                #[cfg(feature = "debug")]
486                AssistantCmd::ForceDiscoverEmptySourceMetadata => {
487                    if args.len() != 3 {
488                        eprintln!(
489                            "usage: {}",
490                            AssistantCmd::ForceDiscoverEmptySourceMetadata.help_simple()
491                        );
492                        return Ok(());
493                    }
494
495                    let Ok(source_peer_id) = parse_peer_id(&args[0]) else {
496                        eprintln!("invalid peer id: {}", args[0]);
497                        return Ok(());
498                    };
499
500                    let Ok(advertising_sid) = parse_advertising_sid(&args[1]) else {
501                        eprintln!("invalid advertising sid: {}", args[1]);
502                        return Ok(());
503                    };
504
505                    let Ok(num_big) = parse_int::<usize>(&args[2]) else {
506                        eprintln!("invalid # of bigs: {}", args[2]);
507                        return Ok(());
508                    };
509
510                    let mut all_big_metadata = Vec::new();
511                    for _i in 0..num_big {
512                        all_big_metadata.push(vec![]);
513                    }
514
515                    match self.assistant.force_discover_broadcast_source_metadata(
516                        source_peer_id,
517                        advertising_sid,
518                        all_big_metadata,
519                    ) {
520                        Ok(source) => println!("broadcast source with metadata: {source:?}"),
521                        Err(e) => {
522                            eprintln!("failed to enter in empty broadcast source metadata: {e:?}")
523                        }
524                    }
525                }
526                #[cfg(not(feature = "debug"))]
527                c => eprintln!("unknown command: {c:?}"),
528            }
529            Ok::<(), Error>(())
530        }
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    #[test]
539    fn test_parse_peer_id() {
540        // In hex string.
541        assert_eq!(parse_peer_id("0x678abc").expect("should be ok"), PeerId(0x678abc));
542        // Decimal equivalent.
543        assert_eq!(parse_peer_id("6785724").expect("should be ok"), PeerId(0x678abc));
544
545        // Invalid peer id.
546        let _ = parse_peer_id("0123zzz").expect_err("should fail");
547    }
548
549    #[test]
550    fn test_parse_bd_addr() {
551        assert_eq!(
552            parse_bd_addr("3c:80:f1:ed:32:2c").expect("should be ok"),
553            [0x2c, 0x32, 0xed, 0xf1, 0x80, 0x3c]
554        );
555        // Address with 5 parts is invalid.
556        let _ = parse_bd_addr("3c:80:f1:ed:32").expect_err("should fail");
557        // Address with 6 parts but one of them empty is invalid.
558        let _ = parse_bd_addr("3c:80:f1::32:2c").expect_err("should fail");
559        let _ = parse_bd_addr(":80:f1:ed:32:2c").expect_err("should fail");
560        let _ = parse_bd_addr("3c:80:f1:ed:32:").expect_err("should fail");
561        // Address not delimited by : is invalid.
562        let _ = parse_bd_addr("3c.80.f1.ed.32.2c").expect_err("should fail");
563    }
564
565    #[test]
566    fn test_parse_broadcast_id() {
567        assert_eq!(parse_broadcast_id("0xABCD").expect("should work"), 0xABCD.try_into().unwrap());
568        assert_eq!(parse_broadcast_id("123456").expect("should work"), 123456.try_into().unwrap());
569
570        // Invalid string cannot be parsed.
571        let _ = parse_broadcast_id("0xABYZ").expect_err("should fail");
572
573        // Broadcast ID is actually a 3 byte long number.
574        let _ = parse_broadcast_id("16777216").expect_err("should fail");
575    }
576
577    #[test]
578    fn test_parse_bis_sync() {
579        // Basic case with multiple BIGs and BIS indices.
580        let bis_sync = parse_bis_sync("0-1,0-2,1-1");
581        assert_eq!(bis_sync.len(), 2);
582        assert_eq!(bis_sync.get(&0), Some(&BisSync::sync(vec![1, 2]).unwrap()));
583        assert_eq!(bis_sync.get(&1), Some(&BisSync::sync(vec![1]).unwrap()));
584
585        // Case with "OFF" to disable sync for a BIG.
586        let bis_sync = parse_bis_sync("0-1,1-OFF,0-2");
587        assert_eq!(bis_sync.len(), 2);
588        assert_eq!(bis_sync.get(&0), Some(&BisSync::sync(vec![1, 2]).unwrap()));
589        assert_eq!(bis_sync.get(&1), Some(&BisSync::no_sync()));
590
591        // Case where sync is set and then turned off for the same BIG.
592        let bis_sync = parse_bis_sync("0-5,0-OFF");
593        assert_eq!(bis_sync.len(), 1);
594        assert_eq!(bis_sync.get(&0), Some(&BisSync::no_sync()));
595
596        // Will ignore invalid values.
597        let bis_sync = parse_bis_sync("0-1,0-2,1:1,1-1-1,");
598        assert_eq!(bis_sync.len(), 1);
599        assert_eq!(bis_sync.get(&0), Some(&BisSync::sync(vec![1, 2]).unwrap()));
600
601        let bis_sync = parse_bis_sync("hellothisistoallynotvalid");
602        assert_eq!(bis_sync.len(), 0);
603    }
604
605    #[test]
606    fn test_passcode_to_broadcast_code() {
607        // UTF-8 string that is less than 16 bytes.
608        // Source of truth test case from Bluetooth Spec.
609        let code = "Børne House";
610        let expected = [
611            0x42, 0xc3, 0xb8, 0x72, 0x6e, 0x65, 0x20, 0x48, 0x6f, 0x75, 0x73, 0x65, 0x00, 0x00,
612            0x00, 0x00,
613        ];
614        let actual = passcode_to_broadcast_code(code).expect("should succeed");
615        assert_eq!(actual, expected);
616        assert_eq!(u128::from_le_bytes(actual), 0x00000000_6573756F_4820656E_72B8C342);
617
618        // Valid ASCII passcode, exactly 16 bytes.
619        let code = "1234567890123456";
620        let expected = [
621            0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34,
622            0x35, 0x36,
623        ];
624        assert_eq!(passcode_to_broadcast_code(code).unwrap(), expected);
625
626        // Invalid passcode, over 16 bytes.
627        let code = "12345678901234567";
628        assert!(passcode_to_broadcast_code(code).is_err());
629
630        // Empty passcode should be an error.
631        let code = "";
632        assert!(passcode_to_broadcast_code(code).is_err());
633    }
634}