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_bis_sync(input: &str) -> HashMap<SubgroupIndex, BisSync> {
155    let mut map = HashMap::new();
156    for t in input.split(',') {
157        let parts: Vec<_> = t.split('-').collect();
158        if parts.len() != 2 {
159            eprintln!(
160                "invalid big-bis sync info {t}. should be in <Ith_BIG>-<BIS_INDEX> format, will be ignored"
161            );
162            continue;
163        }
164        let Ok(ith_big) = parse_int(parts[0]) else {
165            eprintln!("Failed to parse big index from '{}', ignoring.", parts[0]);
166            continue;
167        };
168        match parse_int::<u8>(parts[1]) {
169            Ok(bis_index) => {
170                let entry = map.entry(ith_big).or_insert(BisSync::no_sync());
171                if let Err(e) = entry.synchronize_to_index(bis_index) {
172                    eprintln!("Failed to set sync to BIS index: {e:?}");
173                }
174            }
175            Err(_) if parts[1] == "OFF" => {
176                map.insert(ith_big, BisSync::no_sync());
177            }
178            Err(e) => {
179                eprintln!("{e:?} - BIS index should be a number from 1-31, ignoring {}", parts[1]);
180            }
181        }
182    }
183    map
184}
185
186/// Converts a passcode string into a 16-byte broadcast code.
187/// The string is UTF-8 encoded and then padded with zeros on the right to a
188/// total length of 16 bytes. This result is a little-endian byte array
189/// equivalent to a 128-bit value.
190fn passcode_to_broadcast_code(passcode: &str) -> Result<[u8; 16], String> {
191    if passcode.is_empty() {
192        return Err("invalid broadcast code: passcode cannot be empty".to_string());
193    }
194    let code = passcode.as_bytes();
195    if code.len() > 16 {
196        return Err(format!(
197            "invalid broadcast code: '{}'. should be at max length 16, but was {}",
198            passcode,
199            code.len()
200        ));
201    }
202    let mut broadcast_code = [0u8; 16];
203    broadcast_code[..code.len()].copy_from_slice(code);
204    Ok(broadcast_code)
205}
206
207impl<T: bt_gatt::GattTypes + 'static, R: GetPeerAddr> CommandRunner for AssistantDebug<T, R>
208where
209    <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
210{
211    type Set = AssistantCmd;
212
213    fn run(
214        &self,
215        cmd: Self::Set,
216        args: Vec<String>,
217    ) -> impl futures::Future<Output = Result<(), impl std::error::Error>> {
218        let help_subcommands: HashSet<&str> = HashSet::from(["help", "-h", "--help"]);
219        async move {
220            if args.len() >= 1 && help_subcommands.contains(args[0].as_str()) {
221                eprintln!("usage: {}", cmd.help_simple());
222                return Ok(());
223            }
224            match cmd {
225                AssistantCmd::Info => {
226                    let known = self.assistant.known_broadcast_sources();
227                    println!("Known Broadcast Sources:");
228                    for (id, s) in known {
229                        println!("({id:?}): {s:?}");
230                    }
231                }
232                AssistantCmd::Connect => {
233                    if self.connected_peer.lock().is_some() {
234                        eprintln!(
235                            "peer already connected. Call `disconnect` first: {}",
236                            AssistantCmd::Disconnect.help_simple()
237                        );
238                        return Ok(());
239                    }
240                    if args.len() != 1 {
241                        eprintln!("usage: {}", AssistantCmd::Connect.help_simple());
242                        return Ok(());
243                    }
244
245                    let Ok(peer_id) = parse_peer_id(&args[0]) else {
246                        eprintln!("invalid peer id: {}", args[0]);
247                        return Ok(());
248                    };
249
250                    let peer = self.assistant.connect_to_scan_delegator(peer_id).await;
251                    match peer {
252                        Ok(peer) => {
253                            *self.connected_peer.lock() = Some(Arc::new(peer));
254                        }
255                        Err(e) => {
256                            eprintln!("failed to connect to scan delegator: {e:?}");
257                        }
258                    };
259                }
260                AssistantCmd::Disconnect => {
261                    if self.connected_peer.lock().take().is_none() {
262                        eprintln!("not connected to a scan delegator");
263                    }
264                }
265                AssistantCmd::SendBroadcastCode => {
266                    if args.len() != 2 {
267                        eprintln!("usage: {}", AssistantCmd::SendBroadcastCode.help_simple());
268                        return Ok(());
269                    }
270
271                    let Ok(broadcast_id) = parse_broadcast_id(&args[0]) else {
272                        eprintln!("invalid broadcast id: {}", args[0]);
273                        return Ok(());
274                    };
275
276                    let broadcast_code = match passcode_to_broadcast_code(&args[1]) {
277                        Ok(code) => code,
278                        Err(e) => {
279                            eprintln!("{e:?}");
280                            return Ok(());
281                        }
282                    };
283
284                    self.with_peer(|peer| async move {
285                        peer.send_broadcast_code(broadcast_id, broadcast_code).await
286                    })
287                    .await;
288                }
289                AssistantCmd::AddBroadcastSource => {
290                    if args.len() < 3 {
291                        eprintln!("usage: {}", AssistantCmd::AddBroadcastSource.help_simple());
292                        return Ok(());
293                    }
294
295                    let Ok(source_peer_id) = parse_peer_id(&args[0]) else {
296                        eprintln!("invalid peer id: {}", args[0]);
297                        return Ok(());
298                    };
299
300                    let Ok(sid_val) = parse_int::<u8>(&args[1]) else {
301                        eprintln!("invalid advertising sid: {}", args[1]);
302                        return Ok(());
303                    };
304                    let advertising_sid = AdvertisingSetId(sid_val);
305
306                    let pa_sync: PaSync = match args[2].parse() {
307                        Ok(sync) => sync,
308                        Err(e) => {
309                            eprintln!("invalid pa_sync: {e:?}");
310                            return Ok(());
311                        }
312                    };
313
314                    let bis_sync =
315                        if args.len() == 4 { parse_bis_sync(&args[3]) } else { HashMap::new() };
316
317                    self.with_peer(|peer| async move {
318                        peer.add_broadcast_source(
319                            source_peer_id,
320                            advertising_sid,
321                            &self.peer_addr_getter,
322                            pa_sync,
323                            bis_sync,
324                        )
325                        .await
326                    })
327                    .await;
328                }
329                AssistantCmd::UpdatePaSync => {
330                    if args.len() < 2 {
331                        eprintln!("usage: {}", AssistantCmd::UpdatePaSync.help_simple());
332                        return Ok(());
333                    }
334
335                    let Ok(broadcast_id) = parse_broadcast_id(&args[0]) else {
336                        eprintln!("invalid broadcast id: {}", args[0]);
337                        return Ok(());
338                    };
339
340                    let pa_sync: PaSync = match args[1].parse() {
341                        Ok(sync) => sync,
342                        Err(e) => {
343                            eprintln!("invalid pa_sync: {e:?}");
344                            return Ok(());
345                        }
346                    };
347
348                    let bis_sync =
349                        if args.len() == 3 { parse_bis_sync(&args[2]) } else { HashMap::new() };
350
351                    self.with_peer(|peer| async move {
352                        peer.update_broadcast_source_sync(broadcast_id, pa_sync, bis_sync).await
353                    })
354                    .await;
355                }
356                AssistantCmd::RemoveBroadcastSource => {
357                    if args.len() != 1 {
358                        eprintln!("usage: {}", AssistantCmd::RemoveBroadcastSource.help_simple());
359                        return Ok(());
360                    }
361
362                    let Ok(broadcast_id) = parse_broadcast_id(&args[0]) else {
363                        eprintln!("invalid broadcast id: {}", args[0]);
364                        return Ok(());
365                    };
366
367                    self.with_peer(|peer| async move {
368                        peer.remove_broadcast_source(broadcast_id).await
369                    })
370                    .await;
371                }
372                AssistantCmd::RemoteScanStarted => {
373                    self.with_peer(|peer: Arc<Peer<T>>| async move {
374                        peer.inform_remote_scan_started().await
375                    })
376                    .await;
377                }
378                AssistantCmd::RemoteScanStopped => {
379                    self.with_peer(|peer| async move { peer.inform_remote_scan_stopped().await })
380                        .await;
381                }
382                #[cfg(feature = "debug")]
383                AssistantCmd::ForceDiscoverBroadcastSource => {
384                    if args.len() != 4 {
385                        eprintln!(
386                            "usage: {}",
387                            AssistantCmd::ForceDiscoverBroadcastSource.help_simple()
388                        );
389                        return Ok(());
390                    }
391
392                    let Ok(source_peer_id) = parse_peer_id(&args[0]) else {
393                        eprintln!("invalid peer id: {}", args[0]);
394                        return Ok(());
395                    };
396
397                    let Ok(address) = parse_bd_addr(&args[1]) else {
398                        eprintln!("invalid address: {}", args[1]);
399                        return Ok(());
400                    };
401
402                    let address_type: AddressType = match args[2].parse() {
403                        Ok(t) => t,
404                        Err(e) => {
405                            eprintln!("invalid address type: {e:?}");
406                            return Ok(());
407                        }
408                    };
409
410                    let Ok(raw_ad_sid) = parse_int::<u8>(&args[3]) else {
411                        eprintln!("invalid advertising sid: {}", args[3]);
412                        return Ok(());
413                    };
414                    let advertising_sid = AdvertisingSetId(raw_ad_sid);
415
416                    match self.assistant.force_discover_broadcast_source(
417                        source_peer_id,
418                        address,
419                        address_type,
420                        advertising_sid,
421                    ) {
422                        Ok(source) => {
423                            println!("broadcast source after additional info: {source:?}")
424                        }
425                        Err(e) => {
426                            eprintln!("failed to enter in broadcast source information: {e:?}")
427                        }
428                    }
429                }
430                #[cfg(feature = "debug")]
431                AssistantCmd::ForceDiscoverSourceMetadata => {
432                    if args.len() < 3 {
433                        eprintln!(
434                            "usage: {}",
435                            AssistantCmd::ForceDiscoverSourceMetadata.help_simple()
436                        );
437                        return Ok(());
438                    }
439
440                    let Ok(source_peer_id) = parse_peer_id(&args[0]) else {
441                        eprintln!("invalid peer id: {}", args[0]);
442                        return Ok(());
443                    };
444
445                    let Ok(raw_ad_sid) = parse_int::<u8>(&args[1]) else {
446                        eprintln!("invalid advertising sid: {}", args[1]);
447                        return Ok(());
448                    };
449                    let advertising_sid = AdvertisingSetId(raw_ad_sid);
450
451                    let mut all_big_metadata = Vec::new();
452                    for i in 2..args.len() {
453                        let raw_metadata: Vec<u8> = args[i]
454                            .split(',')
455                            .map(|t| parse_int(t))
456                            .filter_map(Result::ok)
457                            .collect();
458
459                        if raw_metadata.len() > 0 {
460                            let (decoded_metadata, consumed_len) =
461                                Metadata::decode_all(raw_metadata.as_slice());
462                            if consumed_len != raw_metadata.len() {
463                                eprintln!("Metadata length is not valid");
464                                return Ok(());
465                            }
466                            all_big_metadata.push(
467                                decoded_metadata.into_iter().filter_map(Result::ok).collect(),
468                            );
469                        } else {
470                            all_big_metadata.push(vec![]);
471                        }
472                    }
473
474                    match self.assistant.force_discover_broadcast_source_metadata(
475                        source_peer_id,
476                        advertising_sid,
477                        all_big_metadata,
478                    ) {
479                        Ok(source) => println!("broadcast source with metadata: {source:?}"),
480                        Err(e) => eprintln!("failed to enter in broadcast source metadata: {e:?}"),
481                    }
482                }
483                #[cfg(feature = "debug")]
484                AssistantCmd::ForceDiscoverEmptySourceMetadata => {
485                    if args.len() != 3 {
486                        eprintln!(
487                            "usage: {}",
488                            AssistantCmd::ForceDiscoverEmptySourceMetadata.help_simple()
489                        );
490                        return Ok(());
491                    }
492
493                    let Ok(source_peer_id) = parse_peer_id(&args[0]) else {
494                        eprintln!("invalid peer id: {}", args[0]);
495                        return Ok(());
496                    };
497
498                    let Ok(raw_ad_sid) = parse_int::<u8>(&args[1]) else {
499                        eprintln!("invalid advertising sid: {}", args[1]);
500                        return Ok(());
501                    };
502                    let advertising_sid = AdvertisingSetId(raw_ad_sid);
503
504                    let Ok(num_big) = parse_int::<usize>(&args[2]) else {
505                        eprintln!("invalid # of bigs: {}", args[2]);
506                        return Ok(());
507                    };
508
509                    let mut all_big_metadata = Vec::new();
510                    for _i in 0..num_big {
511                        all_big_metadata.push(vec![]);
512                    }
513
514                    match self.assistant.force_discover_broadcast_source_metadata(
515                        source_peer_id,
516                        advertising_sid,
517                        all_big_metadata,
518                    ) {
519                        Ok(source) => println!("broadcast source with metadata: {source:?}"),
520                        Err(e) => {
521                            eprintln!("failed to enter in empty broadcast source metadata: {e:?}")
522                        }
523                    }
524                }
525                #[cfg(not(feature = "debug"))]
526                c => eprintln!("unknown command: {c:?}"),
527            }
528            Ok::<(), Error>(())
529        }
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    #[test]
538    fn test_parse_peer_id() {
539        // In hex string.
540        assert_eq!(parse_peer_id("0x678abc").expect("should be ok"), PeerId(0x678abc));
541        // Decimal equivalent.
542        assert_eq!(parse_peer_id("6785724").expect("should be ok"), PeerId(0x678abc));
543
544        // Invalid peer id.
545        let _ = parse_peer_id("0123zzz").expect_err("should fail");
546    }
547
548    #[test]
549    fn test_parse_bd_addr() {
550        assert_eq!(
551            parse_bd_addr("3c:80:f1:ed:32:2c").expect("should be ok"),
552            [0x2c, 0x32, 0xed, 0xf1, 0x80, 0x3c]
553        );
554        // Address with 5 parts is invalid.
555        let _ = parse_bd_addr("3c:80:f1:ed:32").expect_err("should fail");
556        // Address with 6 parts but one of them empty is invalid.
557        let _ = parse_bd_addr("3c:80:f1::32:2c").expect_err("should fail");
558        let _ = parse_bd_addr(":80:f1:ed:32:2c").expect_err("should fail");
559        let _ = parse_bd_addr("3c:80:f1:ed:32:").expect_err("should fail");
560        // Address not delimited by : is invalid.
561        let _ = parse_bd_addr("3c.80.f1.ed.32.2c").expect_err("should fail");
562    }
563
564    #[test]
565    fn test_parse_broadcast_id() {
566        assert_eq!(parse_broadcast_id("0xABCD").expect("should work"), 0xABCD.try_into().unwrap());
567        assert_eq!(parse_broadcast_id("123456").expect("should work"), 123456.try_into().unwrap());
568
569        // Invalid string cannot be parsed.
570        let _ = parse_broadcast_id("0xABYZ").expect_err("should fail");
571
572        // Broadcast ID is actually a 3 byte long number.
573        let _ = parse_broadcast_id("16777216").expect_err("should fail");
574    }
575
576    #[test]
577    fn test_parse_bis_sync() {
578        // Basic case with multiple BIGs and BIS indices.
579        let bis_sync = parse_bis_sync("0-1,0-2,1-1");
580        assert_eq!(bis_sync.len(), 2);
581        assert_eq!(bis_sync.get(&0), Some(&BisSync::sync(vec![1, 2]).unwrap()));
582        assert_eq!(bis_sync.get(&1), Some(&BisSync::sync(vec![1]).unwrap()));
583
584        // Case with "OFF" to disable sync for a BIG.
585        let bis_sync = parse_bis_sync("0-1,1-OFF,0-2");
586        assert_eq!(bis_sync.len(), 2);
587        assert_eq!(bis_sync.get(&0), Some(&BisSync::sync(vec![1, 2]).unwrap()));
588        assert_eq!(bis_sync.get(&1), Some(&BisSync::no_sync()));
589
590        // Case where sync is set and then turned off for the same BIG.
591        let bis_sync = parse_bis_sync("0-5,0-OFF");
592        assert_eq!(bis_sync.len(), 1);
593        assert_eq!(bis_sync.get(&0), Some(&BisSync::no_sync()));
594
595        // Will ignore invalid values.
596        let bis_sync = parse_bis_sync("0-1,0-2,1:1,1-1-1,");
597        assert_eq!(bis_sync.len(), 1);
598        assert_eq!(bis_sync.get(&0), Some(&BisSync::sync(vec![1, 2]).unwrap()));
599
600        let bis_sync = parse_bis_sync("hellothisistoallynotvalid");
601        assert_eq!(bis_sync.len(), 0);
602    }
603
604    #[test]
605    fn test_passcode_to_broadcast_code() {
606        // UTF-8 string that is less than 16 bytes.
607        // Source of truth test case from Bluetooth Spec.
608        let code = "Børne House";
609        let expected = [
610            0x42, 0xc3, 0xb8, 0x72, 0x6e, 0x65, 0x20, 0x48, 0x6f, 0x75, 0x73, 0x65, 0x00, 0x00,
611            0x00, 0x00,
612        ];
613        let actual = passcode_to_broadcast_code(code).expect("should succeed");
614        assert_eq!(actual, expected);
615        assert_eq!(u128::from_le_bytes(actual), 0x00000000_6573756F_4820656E_72B8C342);
616
617        // Valid ASCII passcode, exactly 16 bytes.
618        let code = "1234567890123456";
619        let expected = [
620            0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34,
621            0x35, 0x36,
622        ];
623        assert_eq!(passcode_to_broadcast_code(code).unwrap(), expected);
624
625        // Invalid passcode, over 16 bytes.
626        let code = "12345678901234567";
627        assert!(passcode_to_broadcast_code(code).is_err());
628
629        // Empty passcode should be an error.
630        let code = "";
631        assert!(passcode_to_broadcast_code(code).is_err());
632    }
633}