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