xref: /aosp_15_r20/tools/netsim/rust/daemon/src/bluetooth/beacon.rs (revision cf78ab8cffb8fc9207af348f23af247fb04370a6)
1 // Copyright 2023 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 use super::advertise_data::{AdvertiseData, AdvertiseDataBuilder};
16 use super::advertise_settings::{
17     AdvertiseMode, AdvertiseSettings, AdvertiseSettingsBuilder, TxPowerLevel,
18 };
19 use super::chip::{rust_bluetooth_add, RustBluetoothChipCallbacks};
20 use crate::devices::chip::{ChipIdentifier, FacadeIdentifier};
21 use crate::devices::device::{AddChipResult, DeviceIdentifier};
22 use crate::devices::devices_handler::add_chip;
23 use crate::ffi::ffi_bluetooth;
24 use crate::wireless;
25 use cxx::{let_cxx_string, UniquePtr};
26 use log::{error, info, warn};
27 use netsim_packets::link_layer::{
28     Address, AddressType, LeLegacyAdvertisingPduBuilder, LeScanResponseBuilder, PacketType,
29 };
30 use netsim_proto::common::ChipKind;
31 use netsim_proto::model::chip::Bluetooth;
32 use netsim_proto::model::chip::{
33     ble_beacon::AdvertiseData as AdvertiseDataProto,
34     ble_beacon::AdvertiseSettings as AdvertiseSettingsProto, BleBeacon as BleBeaconProto,
35 };
36 use netsim_proto::model::chip_create::{
37     BleBeaconCreate as BleBeaconCreateProto, Chip as BuiltinProto,
38 };
39 use netsim_proto::model::{ChipCreate as ChipCreateProto, DeviceCreate as DeviceCreateProto};
40 use pdl_runtime::Packet;
41 use protobuf::{Message, MessageField};
42 use std::alloc::System;
43 use std::sync::{Mutex, OnceLock, RwLock};
44 use std::time::{Duration, Instant};
45 use std::{collections::HashMap, ptr::null};
46 
47 static EMPTY_ADDRESS: OnceLock<Address> = OnceLock::new();
48 
get_empty_address() -> &'static Address49 fn get_empty_address() -> &'static Address {
50     EMPTY_ADDRESS.get_or_init(|| Address::try_from(0u64).unwrap())
51 }
52 
53 // A singleton that contains a hash map from chip id to RustBluetoothChip.
54 // It's used by `BeaconChip` to access `RustBluetoothChip` to call send_link_layer_packet().
55 static BT_CHIPS: OnceLock<
56     RwLock<HashMap<ChipIdentifier, Mutex<UniquePtr<ffi_bluetooth::RustBluetoothChip>>>>,
57 > = OnceLock::new();
58 
get_bt_chips( ) -> &'static RwLock<HashMap<ChipIdentifier, Mutex<UniquePtr<ffi_bluetooth::RustBluetoothChip>>>>59 fn get_bt_chips(
60 ) -> &'static RwLock<HashMap<ChipIdentifier, Mutex<UniquePtr<ffi_bluetooth::RustBluetoothChip>>>> {
61     BT_CHIPS.get_or_init(|| RwLock::new(HashMap::new()))
62 }
63 
64 // Used to find beacon chip based on it's id from static methods.
65 static BEACON_CHIPS: OnceLock<RwLock<HashMap<ChipIdentifier, Mutex<BeaconChip>>>> = OnceLock::new();
66 
get_beacon_chips() -> &'static RwLock<HashMap<ChipIdentifier, Mutex<BeaconChip>>>67 pub(crate) fn get_beacon_chips() -> &'static RwLock<HashMap<ChipIdentifier, Mutex<BeaconChip>>> {
68     BEACON_CHIPS.get_or_init(|| RwLock::new(HashMap::new()))
69 }
70 
71 /// BeaconChip class.
72 pub struct BeaconChip {
73     device_name: String,
74     chip_id: ChipIdentifier,
75     address: Address,
76     advertise_settings: AdvertiseSettings,
77     advertise_data: AdvertiseData,
78     scan_response_data: AdvertiseData,
79     advertise_last: Option<Instant>,
80     advertise_start: Option<Instant>,
81 }
82 
83 impl BeaconChip {
new( device_name: String, chip_id: ChipIdentifier, address: String, ) -> Result<Self, String>84     pub fn new(
85         device_name: String,
86         chip_id: ChipIdentifier,
87         address: String,
88     ) -> Result<Self, String> {
89         Ok(BeaconChip {
90             chip_id,
91             device_name: device_name.clone(),
92             address: str_to_addr(&address)?,
93             advertise_settings: AdvertiseSettings::builder().build(),
94             advertise_data: AdvertiseData::builder(device_name.clone(), TxPowerLevel::default())
95                 .build()
96                 .unwrap(),
97             scan_response_data: AdvertiseData::builder(device_name, TxPowerLevel::default())
98                 .build()
99                 .unwrap(),
100             advertise_last: None,
101             advertise_start: None,
102         })
103     }
104 
from_proto( device_name: String, chip_id: ChipIdentifier, beacon_proto: &BleBeaconCreateProto, ) -> Result<Self, String>105     pub fn from_proto(
106         device_name: String,
107         chip_id: ChipIdentifier,
108         beacon_proto: &BleBeaconCreateProto,
109     ) -> Result<Self, String> {
110         let advertise_settings = AdvertiseSettings::from_proto(&beacon_proto.settings)?;
111         let advertise_data = AdvertiseData::from_proto(
112             device_name.clone(),
113             beacon_proto
114                 .settings
115                 .tx_power
116                 .as_ref()
117                 .map(TxPowerLevel::try_from)
118                 .transpose()?
119                 .unwrap_or_default(),
120             &beacon_proto.adv_data,
121         )?;
122         let scan_response_data = AdvertiseData::from_proto(
123             device_name.clone(),
124             advertise_settings.tx_power_level,
125             &beacon_proto.scan_response,
126         )?;
127 
128         let address = if beacon_proto.address == String::default() {
129             // Safe to unwrap here because chip_id is a u32 which is less than 6 bytes
130             u64::from(chip_id.0).try_into().unwrap()
131         } else {
132             str_to_addr(&beacon_proto.address)?
133         };
134 
135         Ok(BeaconChip {
136             device_name,
137             chip_id,
138             address,
139             advertise_settings,
140             advertise_data,
141             scan_response_data,
142             advertise_last: None,
143             advertise_start: None,
144         })
145     }
146 
send_link_layer_le_packet(&self, packet: &[u8], tx_power: i8)147     pub fn send_link_layer_le_packet(&self, packet: &[u8], tx_power: i8) {
148         let binding = get_bt_chips().read().unwrap();
149         if let Some(rust_bluetooth_chip) = binding.get(&self.chip_id) {
150             rust_bluetooth_chip
151                 .lock()
152                 .expect("Failed to acquire lock on RustBluetoothChip")
153                 .pin_mut()
154                 .send_link_layer_le_packet(packet, tx_power);
155         } else {
156             warn!("Failed to get RustBluetoothChip for unknown chip id: {}", self.chip_id);
157         };
158     }
159 }
160 
161 // BEACON_CHIPS has ownership of all the BeaconChips, so we need a separate class to hold the callbacks.
162 // This class will be owned by rootcanal.
163 pub struct BeaconChipCallbacks {
164     chip_id: ChipIdentifier,
165 }
166 
167 impl RustBluetoothChipCallbacks for BeaconChipCallbacks {
tick(&mut self)168     fn tick(&mut self) {
169         let guard = get_beacon_chips().read().unwrap();
170         let mut beacon = guard.get(&self.chip_id);
171         if beacon.is_none() {
172             error!("could not find bluetooth beacon with chip id {}", self.chip_id);
173             return;
174         }
175         let mut beacon = beacon.unwrap().lock().expect("Failed to acquire lock on BeaconChip");
176         if let (Some(start), Some(timeout)) =
177             (beacon.advertise_start, beacon.advertise_settings.timeout)
178         {
179             if start.elapsed() > timeout {
180                 return;
181             }
182         }
183 
184         if let Some(last) = beacon.advertise_last {
185             if last.elapsed() <= beacon.advertise_settings.mode.interval {
186                 return;
187             }
188         } else {
189             beacon.advertise_start = Some(Instant::now())
190         }
191 
192         beacon.advertise_last = Some(Instant::now());
193 
194         let packet = LeLegacyAdvertisingPduBuilder {
195             advertising_type: beacon.advertise_settings.get_packet_type(),
196             advertising_data: beacon.advertise_data.to_bytes(),
197             advertising_address_type: AddressType::Public,
198             target_address_type: AddressType::Public,
199             source_address: beacon.address,
200             destination_address: *get_empty_address(),
201         }
202         .build()
203         .encode_to_vec()
204         .unwrap();
205         beacon.send_link_layer_le_packet(&packet, beacon.advertise_settings.tx_power_level.dbm);
206     }
207 
receive_link_layer_packet( &mut self, source_address: String, destination_address: String, packet_type: u8, packet: &[u8], )208     fn receive_link_layer_packet(
209         &mut self,
210         source_address: String,
211         destination_address: String,
212         packet_type: u8,
213         packet: &[u8],
214     ) {
215         let guard = get_beacon_chips().read().unwrap();
216         let beacon = guard.get(&self.chip_id);
217         if beacon.is_none() {
218             error!("could not find bluetooth beacon with chip id {}", self.chip_id);
219             return;
220         }
221         let beacon = beacon.unwrap().lock().expect("Failed to acquire lock on BeaconChip");
222 
223         if beacon.advertise_settings.scannable
224             && destination_address == addr_to_str(beacon.address)
225             && packet_type == u8::from(PacketType::LeScan)
226         {
227             let packet = LeScanResponseBuilder {
228                 advertising_address_type: AddressType::Public,
229                 source_address: beacon.address,
230                 destination_address: beacon.address,
231                 scan_response_data: beacon.scan_response_data.to_bytes(),
232             }
233             .build()
234             .encode_to_vec()
235             .unwrap();
236 
237             beacon.send_link_layer_le_packet(&packet, beacon.advertise_settings.tx_power_level.dbm);
238         }
239     }
240 }
241 
242 /// Add a beacon device in rootcanal.
243 ///
244 /// Called by `devices/chip.rs`.
245 ///
246 /// Similar to `bluetooth_add()`.
247 #[cfg(not(test))]
ble_beacon_add( device_name: String, chip_id: ChipIdentifier, chip_proto: &ChipCreateProto, ) -> Result<FacadeIdentifier, String>248 pub fn ble_beacon_add(
249     device_name: String,
250     chip_id: ChipIdentifier,
251     chip_proto: &ChipCreateProto,
252 ) -> Result<FacadeIdentifier, String> {
253     let beacon_proto = match &chip_proto.chip {
254         Some(BuiltinProto::BleBeacon(beacon_proto)) => beacon_proto,
255         _ => return Err(String::from("failed to create ble beacon: unexpected chip type")),
256     };
257 
258     let beacon_chip = BeaconChip::from_proto(device_name, chip_id, beacon_proto)?;
259     if get_beacon_chips().write().unwrap().insert(chip_id, Mutex::new(beacon_chip)).is_some() {
260         return Err(format!(
261             "failed to create a bluetooth beacon chip with id {chip_id}: chip id already exists.",
262         ));
263     }
264 
265     let callbacks: Box<dyn RustBluetoothChipCallbacks> = Box::new(BeaconChipCallbacks { chip_id });
266     let add_rust_device_result = rust_bluetooth_add(
267         chip_id,
268         callbacks,
269         String::from("beacon"),
270         beacon_proto.address.clone(),
271     );
272     let rust_chip = add_rust_device_result.rust_chip;
273     let facade_id = add_rust_device_result.facade_id;
274     info!("Creating HCI facade_id: {} for chip_id: {}", facade_id, chip_id);
275     get_bt_chips().write().unwrap().insert(chip_id, Mutex::new(rust_chip));
276     Ok(FacadeIdentifier(facade_id))
277 }
278 
279 #[cfg(not(test))]
ble_beacon_remove( chip_id: ChipIdentifier, facade_id: FacadeIdentifier, ) -> Result<(), String>280 pub fn ble_beacon_remove(
281     chip_id: ChipIdentifier,
282     facade_id: FacadeIdentifier,
283 ) -> Result<(), String> {
284     let removed_beacon = get_beacon_chips().write().unwrap().remove(&chip_id);
285     let removed_radio = get_bt_chips().write().unwrap().remove(&chip_id);
286     if removed_beacon.is_none() || removed_radio.is_none() {
287         Err(format!("failed to delete ble beacon chip: chip with id {chip_id} does not exist"))
288     } else {
289         ffi_bluetooth::bluetooth_remove_rust_device(facade_id.0);
290         Ok(())
291     }
292 }
293 
ble_beacon_patch( facade_id: FacadeIdentifier, chip_id: ChipIdentifier, patch: &BleBeaconProto, ) -> Result<(), String>294 pub fn ble_beacon_patch(
295     facade_id: FacadeIdentifier,
296     chip_id: ChipIdentifier,
297     patch: &BleBeaconProto,
298 ) -> Result<(), String> {
299     let mut guard = get_beacon_chips().write().unwrap();
300     let mut beacon = guard
301         .get_mut(&chip_id)
302         .ok_or(format!("could not find bluetooth beacon with chip id {chip_id} for patching"))?
303         .get_mut()
304         .unwrap();
305 
306     if patch.address != String::default() {
307         beacon.address = str_to_addr(&patch.address)?;
308         #[cfg(not(test))]
309         ffi_bluetooth::bluetooth_set_rust_device_address(
310             facade_id.0,
311             u64::from(beacon.address).to_le_bytes()[..6].try_into().unwrap(),
312         );
313     }
314 
315     if let Some(patch_settings) = patch.settings.as_ref() {
316         if let Some(interval) = patch_settings.interval.as_ref() {
317             beacon.advertise_settings.mode = interval.into();
318         }
319 
320         if let Some(tx_power) = patch_settings.tx_power.as_ref() {
321             beacon.advertise_settings.tx_power_level = tx_power.try_into()?
322         }
323 
324         beacon.advertise_settings.scannable =
325             patch_settings.scannable || beacon.advertise_settings.scannable;
326 
327         if patch_settings.timeout != u64::default() {
328             beacon.advertise_settings.timeout = Some(Duration::from_millis(patch_settings.timeout));
329         }
330     }
331 
332     if let Some(patch_adv_data) = patch.adv_data.as_ref() {
333         let mut builder = AdvertiseData::builder(
334             beacon.device_name.clone(),
335             beacon.advertise_settings.tx_power_level,
336         );
337 
338         if patch_adv_data.include_device_name || beacon.advertise_data.include_device_name {
339             builder.include_device_name();
340         }
341 
342         if patch_adv_data.include_tx_power_level || beacon.advertise_data.include_tx_power_level {
343             builder.include_tx_power_level();
344         }
345 
346         if !patch_adv_data.manufacturer_data.is_empty() {
347             builder.manufacturer_data(patch_adv_data.manufacturer_data.clone());
348         } else if let Some(manufacturer_data) = beacon.advertise_data.manufacturer_data.as_ref() {
349             builder.manufacturer_data(manufacturer_data.clone());
350         }
351 
352         beacon.advertise_data = builder.build()?;
353     }
354 
355     Ok(())
356 }
357 
ble_beacon_get( chip_id: ChipIdentifier, _facade_id: FacadeIdentifier, ) -> Result<BleBeaconProto, String>358 pub fn ble_beacon_get(
359     chip_id: ChipIdentifier,
360     _facade_id: FacadeIdentifier,
361 ) -> Result<BleBeaconProto, String> {
362     let guard = get_beacon_chips().read().unwrap();
363     let beacon = guard
364         .get(&chip_id)
365         .ok_or(format!("could not get bluetooth beacon with chip id {chip_id}"))?
366         .lock()
367         .expect("Failed to acquire lock on BeaconChip");
368     #[cfg(not(test))]
369     let bt = {
370         let bluetooth_bytes = ffi_bluetooth::bluetooth_get_cxx(_facade_id.0);
371         Some(Bluetooth::parse_from_bytes(&bluetooth_bytes).unwrap())
372     };
373     #[cfg(test)]
374     let bt = Some(netsim_proto::model::chip::Bluetooth::new());
375     Ok(BleBeaconProto {
376         bt: bt.into(),
377         address: addr_to_str(beacon.address),
378         settings: MessageField::some((&beacon.advertise_settings).try_into()?),
379         adv_data: MessageField::some((&beacon.advertise_data).into()),
380         ..Default::default()
381     })
382 }
383 
addr_to_str(addr: Address) -> String384 fn addr_to_str(addr: Address) -> String {
385     let bytes = u64::from(addr).to_le_bytes();
386     bytes[..5]
387         .iter()
388         .rfold(format!("{:02x}", bytes[5]), |addr, byte| addr + &format!(":{:02x}", byte))
389 }
390 
str_to_addr(addr: &str) -> Result<Address, String>391 fn str_to_addr(addr: &str) -> Result<Address, String> {
392     if addr == String::default() {
393         Ok(*get_empty_address())
394     } else {
395         if addr.len() != 17 {
396             return Err(String::from("failed to parse address: address was not the right length"));
397         }
398         let addr = addr.replace(':', "");
399         u64::from_str_radix(&addr, 16)
400             .map_err(|_| String::from("failed to parse address: invalid hex"))?
401             .try_into()
402             .map_err(|_| {
403                 String::from("failed to parse address: address must be smaller than 6 bytes")
404             })
405     }
406 }
407 
408 #[cfg(test)]
409 pub mod tests {
410     use std::ops::Add;
411     use std::sync::atomic::{AtomicU32, Ordering};
412     use std::thread;
413 
414     use netsim_proto::model::chip::ble_beacon::{
415         advertise_settings::{AdvertiseTxPower as AdvertiseTxPowerProto, Tx_power as TxPowerProto},
416         AdvertiseData as AdvertiseDataProto,
417     };
418 
419     use super::*;
420     // using ble_beacon_add from mocked.rs
421     use crate::bluetooth::ble_beacon_add;
422 
423     static TEST_GUID_GENERATOR: AtomicU32 = AtomicU32::new(0);
424 
next_id() -> ChipIdentifier425     fn next_id() -> ChipIdentifier {
426         ChipIdentifier(TEST_GUID_GENERATOR.fetch_add(1, Ordering::SeqCst))
427     }
428 
new_test_beacon_with_settings(settings: AdvertiseSettingsProto) -> ChipIdentifier429     fn new_test_beacon_with_settings(settings: AdvertiseSettingsProto) -> ChipIdentifier {
430         let id = next_id();
431 
432         let add_result = ble_beacon_add(
433             format!("test-device-{:?}", thread::current().id()),
434             id,
435             &ChipCreateProto {
436                 name: format!("test-beacon-chip-{:?}", thread::current().id()),
437                 chip: Some(BuiltinProto::BleBeacon(BleBeaconCreateProto {
438                     address: String::from("00:00:00:00:00:00"),
439                     settings: MessageField::some(settings),
440                     ..Default::default()
441                 })),
442                 ..Default::default()
443             },
444         );
445         assert!(add_result.is_ok(), "{}", add_result.unwrap_err());
446 
447         id
448     }
449 
cleanup_beacon(chip_id: ChipIdentifier)450     fn cleanup_beacon(chip_id: ChipIdentifier) {
451         get_beacon_chips().write().unwrap().remove(&chip_id);
452     }
453 
454     #[test]
test_beacon_get()455     fn test_beacon_get() {
456         let interval = Duration::from_millis(9999);
457         let settings = AdvertiseSettingsProto {
458             interval: Some(AdvertiseMode::new(interval).try_into().unwrap()),
459             ..Default::default()
460         };
461 
462         let id = new_test_beacon_with_settings(settings);
463 
464         let beacon = ble_beacon_get(id, FacadeIdentifier(0));
465         assert!(beacon.is_ok(), "{}", beacon.unwrap_err());
466         let beacon = beacon.unwrap();
467 
468         let interval_after_get =
469             beacon.settings.interval.as_ref().map(AdvertiseMode::from).unwrap().interval;
470 
471         assert_eq!(interval, interval_after_get);
472         cleanup_beacon(id);
473     }
474 
475     #[test]
test_beacon_patch()476     fn test_beacon_patch() {
477         let settings = AdvertiseSettingsProto {
478             interval: Some(AdvertiseMode::new(Duration::from_millis(0)).try_into().unwrap()),
479             ..Default::default()
480         };
481 
482         let id = new_test_beacon_with_settings(settings);
483 
484         let interval = Duration::from_millis(33);
485         let tx_power = TxPowerProto::TxPowerLevel(AdvertiseTxPowerProto::MEDIUM.into());
486         let scannable = true;
487         let patch_result = ble_beacon_patch(
488             FacadeIdentifier(0),
489             id,
490             &BleBeaconProto {
491                 settings: MessageField::some(AdvertiseSettingsProto {
492                     interval: Some(
493                         AdvertiseMode::new(Duration::from_millis(33)).try_into().unwrap(),
494                     ),
495                     scannable,
496                     tx_power: Some(tx_power.clone()),
497                     ..Default::default()
498                 }),
499                 ..Default::default()
500             },
501         );
502         assert!(patch_result.is_ok(), "{}", patch_result.unwrap_err());
503 
504         let beacon_proto = ble_beacon_get(id, FacadeIdentifier(0));
505         assert!(beacon_proto.is_ok(), "{}", beacon_proto.unwrap_err());
506         let beacon_proto = beacon_proto.unwrap();
507         let interval_after_patch =
508             beacon_proto.settings.interval.as_ref().map(AdvertiseMode::from).unwrap().interval;
509 
510         assert_eq!(interval, interval_after_patch);
511         assert_eq!(tx_power, *beacon_proto.settings.tx_power.as_ref().unwrap());
512         assert_eq!(scannable, beacon_proto.settings.scannable);
513         cleanup_beacon(id);
514     }
515 
516     #[test]
test_beacon_patch_default()517     fn test_beacon_patch_default() {
518         let settings =
519             AdvertiseSettingsProto { timeout: 1234, scannable: true, ..Default::default() };
520 
521         let id = new_test_beacon_with_settings(settings.clone());
522 
523         let patch_result = ble_beacon_patch(FacadeIdentifier(0), id, &BleBeaconProto::default());
524         assert!(patch_result.is_ok(), "{}", patch_result.unwrap_err());
525 
526         let beacon_proto = ble_beacon_get(id, FacadeIdentifier(0));
527         assert!(beacon_proto.is_ok(), "{}", beacon_proto.unwrap_err());
528         let beacon_proto = beacon_proto.unwrap();
529 
530         let settings_after_patch = beacon_proto.settings.unwrap();
531         assert_eq!(settings.timeout, settings_after_patch.timeout);
532         assert_eq!(settings.scannable, settings_after_patch.scannable);
533     }
534 
535     #[test]
test_str_to_addr_succeeds()536     fn test_str_to_addr_succeeds() {
537         let addr = str_to_addr("be:ac:12:34:00:0f");
538         assert_eq!(Address::try_from(0xbe_ac_12_34_00_0f).unwrap(), addr.unwrap());
539     }
540 
541     #[test]
test_empty_str_to_addr_succeeds()542     fn test_empty_str_to_addr_succeeds() {
543         let addr = str_to_addr("00:00:00:00:00:00");
544         assert_eq!(Address::try_from(0).unwrap(), addr.unwrap());
545     }
546 
547     #[test]
test_str_to_addr_fails()548     fn test_str_to_addr_fails() {
549         let addr = str_to_addr("hi mom!");
550         assert!(addr.is_err());
551     }
552 
553     #[test]
test_invalid_str_to_addr_fails()554     fn test_invalid_str_to_addr_fails() {
555         let addr = str_to_addr("56:78:9a:bc:de:fg");
556         assert!(addr.is_err());
557     }
558 
559     #[test]
test_long_str_to_addr_fails()560     fn test_long_str_to_addr_fails() {
561         let addr = str_to_addr("55:55:55:55:55:55:55:55");
562         assert!(addr.is_err());
563     }
564 
565     #[test]
test_short_str_to_addr_fails()566     fn test_short_str_to_addr_fails() {
567         let addr = str_to_addr("ab:cd");
568         assert!(addr.is_err());
569     }
570 
571     #[test]
test_addr_to_str_succeeds()572     fn test_addr_to_str_succeeds() {
573         let addr: u64 = 0xbe_ac_12_34_00_0f;
574         assert_eq!("be:ac:12:34:00:0f", addr_to_str(addr.try_into().unwrap()))
575     }
576 
577     #[test]
test_empty_addr_to_str_succeeds()578     fn test_empty_addr_to_str_succeeds() {
579         let addr: u64 = 0;
580         assert_eq!("00:00:00:00:00:00", addr_to_str(addr.try_into().unwrap()))
581     }
582 
583     #[test]
test_small_addr_to_str_succeeds()584     fn test_small_addr_to_str_succeeds() {
585         let addr: u64 = 123;
586         assert_eq!("00:00:00:00:00:7b", addr_to_str(addr.try_into().unwrap()))
587     }
588 }
589