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 // http://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 //! Counterpart to the Python example `run_scanner.py`.
16 //!
17 //! Device deduplication is done here rather than relying on the controller's filtering to provide
18 //! for additional features, like the ability to make deduplication time-bounded.
19
20 use bumble::{
21 adv::CommonDataType,
22 wrapper::{
23 core::AdvertisementDataUnit,
24 device::Device,
25 hci::{packets::AddressType, Address},
26 transport::Transport,
27 },
28 };
29 use clap::Parser as _;
30 use itertools::Itertools;
31 use owo_colors::{OwoColorize, Style};
32 use pyo3::PyResult;
33 use std::{
34 collections,
35 sync::{Arc, Mutex},
36 time,
37 };
38
39 #[pyo3_asyncio::tokio::main]
main() -> PyResult<()>40 async fn main() -> PyResult<()> {
41 env_logger::builder()
42 .filter_level(log::LevelFilter::Info)
43 .init();
44
45 let cli = Cli::parse();
46
47 let transport = Transport::open(cli.transport).await?;
48
49 let address = Address::new("F0:F1:F2:F3:F4:F5", AddressType::RandomDeviceAddress)?;
50 let mut device = Device::with_hci("Bumble", address, transport.source()?, transport.sink()?)?;
51
52 // in practice, devices can send multiple advertisements from the same address, so we keep
53 // track of a timestamp for each set of data
54 let seen_advertisements = Arc::new(Mutex::new(collections::HashMap::<
55 Vec<u8>,
56 collections::HashMap<Vec<AdvertisementDataUnit>, time::Instant>,
57 >::new()));
58
59 let seen_adv_clone = seen_advertisements.clone();
60 device.on_advertisement(move |_py, adv| {
61 let rssi = adv.rssi()?;
62 let data_units = adv.data()?.data_units()?;
63 let addr = adv.address()?;
64
65 let show_adv = if cli.filter_duplicates {
66 let addr_bytes = addr.as_le_bytes()?;
67
68 let mut seen_adv_cache = seen_adv_clone.lock().unwrap();
69 let expiry_duration = time::Duration::from_secs(cli.dedup_expiry_secs);
70
71 let advs_from_addr = seen_adv_cache.entry(addr_bytes).or_default();
72 // we expect cache hits to be the norm, so we do a separate lookup to avoid cloning
73 // on every lookup with entry()
74 let show = if let Some(prev) = advs_from_addr.get_mut(&data_units) {
75 let expired = prev.elapsed() > expiry_duration;
76 *prev = time::Instant::now();
77 expired
78 } else {
79 advs_from_addr.insert(data_units.clone(), time::Instant::now());
80 true
81 };
82
83 // clean out anything we haven't seen in a while
84 advs_from_addr.retain(|_, instant| instant.elapsed() <= expiry_duration);
85
86 show
87 } else {
88 true
89 };
90
91 if !show_adv {
92 return Ok(());
93 }
94
95 let addr_style = if adv.is_connectable()? {
96 Style::new().yellow()
97 } else {
98 Style::new().red()
99 };
100
101 let (type_style, qualifier) = match adv.address()?.address_type()? {
102 AddressType::PublicIdentityAddress | AddressType::PublicDeviceAddress => {
103 (Style::new().cyan(), "")
104 }
105 _ => {
106 if addr.is_static()? {
107 (Style::new().green(), "(static)")
108 } else if addr.is_resolvable()? {
109 (Style::new().magenta(), "(resolvable)")
110 } else {
111 (Style::new().default_color(), "")
112 }
113 }
114 };
115
116 println!(
117 ">>> {} [{:?}] {qualifier}:\n RSSI: {}",
118 addr.as_hex()?.style(addr_style),
119 addr.address_type()?.style(type_style),
120 rssi,
121 );
122
123 data_units.into_iter().for_each(|(code, data)| {
124 let matching = CommonDataType::for_type_code(code).collect::<Vec<_>>();
125 let code_str = if matching.is_empty() {
126 format!("0x{}", hex::encode_upper([code.into()]))
127 } else {
128 matching
129 .iter()
130 .map(|t| format!("{}", t))
131 .join(" / ")
132 .blue()
133 .to_string()
134 };
135
136 // use the first matching type's formatted data, if any
137 let data_str = matching
138 .iter()
139 .filter_map(|t| {
140 t.format_data(&data).map(|formatted| {
141 format!(
142 "{} {}",
143 formatted,
144 format!("(raw: 0x{})", hex::encode_upper(&data)).dimmed()
145 )
146 })
147 })
148 .next()
149 .unwrap_or_else(|| format!("0x{}", hex::encode_upper(&data)));
150
151 println!(" [{}]: {}", code_str, data_str)
152 });
153
154 Ok(())
155 })?;
156
157 device.power_on().await?;
158 // do our own dedup
159 device.start_scanning(false).await?;
160
161 // wait until user kills the process
162 tokio::signal::ctrl_c().await?;
163
164 Ok(())
165 }
166
167 #[derive(clap::Parser)]
168 #[command(author, version, about, long_about = None)]
169 struct Cli {
170 /// Bumble transport spec.
171 ///
172 /// <https://google.github.io/bumble/transports/index.html>
173 #[arg(long)]
174 transport: String,
175
176 /// Filter duplicate advertisements
177 #[arg(long, default_value_t = false)]
178 filter_duplicates: bool,
179
180 /// How long before a deduplicated advertisement that hasn't been seen in a while is considered
181 /// fresh again, in seconds
182 #[arg(long, default_value_t = 10, requires = "filter_duplicates")]
183 dedup_expiry_secs: u64,
184 }
185