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