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 //! Devices and connections to them
16 
17 #[cfg(feature = "unstable_extended_adv")]
18 use crate::wrapper::{
19     hci::packets::{
20         self, AdvertisingEventProperties, AdvertisingFilterPolicy, Enable, EnabledSet,
21         FragmentPreference, LeSetAdvertisingSetRandomAddressBuilder,
22         LeSetExtendedAdvertisingDataBuilder, LeSetExtendedAdvertisingEnableBuilder,
23         LeSetExtendedAdvertisingParametersBuilder, Operation, OwnAddressType, PeerAddressType,
24         PrimaryPhyType, SecondaryPhyType,
25     },
26     ConversionError,
27 };
28 use crate::{
29     adv::AdvertisementDataBuilder,
30     wrapper::{
31         core::AdvertisingData,
32         gatt_client::{ProfileServiceProxy, ServiceProxy},
33         hci::{
34             packets::{Command, ErrorCode, Event},
35             Address, HciCommand, WithPacketType,
36         },
37         host::Host,
38         l2cap::LeConnectionOrientedChannel,
39         transport::{Sink, Source},
40         ClosureCallback, PyDictExt, PyObjectExt,
41     },
42 };
43 use pyo3::{
44     exceptions::PyException,
45     intern,
46     types::{PyDict, PyModule},
47     IntoPy, PyErr, PyObject, PyResult, Python, ToPyObject,
48 };
49 use pyo3_asyncio::tokio::into_future;
50 use std::path;
51 
52 #[cfg(test)]
53 mod tests;
54 
55 /// Represents the various properties of some device
56 pub struct DeviceConfiguration(PyObject);
57 
58 impl DeviceConfiguration {
59     /// Creates a new configuration, letting the internal Python object set all the defaults
new() -> PyResult<DeviceConfiguration>60     pub fn new() -> PyResult<DeviceConfiguration> {
61         Python::with_gil(|py| {
62             PyModule::import(py, intern!(py, "bumble.device"))?
63                 .getattr(intern!(py, "DeviceConfiguration"))?
64                 .call0()
65                 .map(|any| Self(any.into()))
66         })
67     }
68 
69     /// Creates a new configuration from the specified file
load_from_file(&mut self, device_config: &path::Path) -> PyResult<()>70     pub fn load_from_file(&mut self, device_config: &path::Path) -> PyResult<()> {
71         Python::with_gil(|py| {
72             self.0
73                 .call_method1(py, intern!(py, "load_from_file"), (device_config,))
74         })
75         .map(|_| ())
76     }
77 }
78 
79 impl ToPyObject for DeviceConfiguration {
to_object(&self, _py: Python<'_>) -> PyObject80     fn to_object(&self, _py: Python<'_>) -> PyObject {
81         self.0.clone()
82     }
83 }
84 
85 /// Used for tracking what advertising state a device might be in
86 #[derive(PartialEq)]
87 enum AdvertisingStatus {
88     AdvertisingLegacy,
89     AdvertisingExtended,
90     NotAdvertising,
91 }
92 
93 /// A device that can send/receive HCI frames.
94 pub struct Device {
95     obj: PyObject,
96     advertising_status: AdvertisingStatus,
97 }
98 
99 impl Device {
100     #[cfg(feature = "unstable_extended_adv")]
101     const ADVERTISING_HANDLE_EXTENDED: u8 = 0x00;
102 
103     /// Creates a Device. When optional arguments are not specified, the Python object specifies the
104     /// defaults.
new( name: Option<&str>, address: Option<Address>, config: Option<DeviceConfiguration>, host: Option<Host>, generic_access_service: Option<bool>, ) -> PyResult<Self>105     pub fn new(
106         name: Option<&str>,
107         address: Option<Address>,
108         config: Option<DeviceConfiguration>,
109         host: Option<Host>,
110         generic_access_service: Option<bool>,
111     ) -> PyResult<Self> {
112         Python::with_gil(|py| {
113             let kwargs = PyDict::new(py);
114             kwargs.set_opt_item("name", name)?;
115             kwargs.set_opt_item("address", address)?;
116             kwargs.set_opt_item("config", config)?;
117             kwargs.set_opt_item("host", host)?;
118             kwargs.set_opt_item("generic_access_service", generic_access_service)?;
119 
120             PyModule::import(py, intern!(py, "bumble.device"))?
121                 .getattr(intern!(py, "Device"))?
122                 .call((), Some(kwargs))
123                 .map(|any| Self {
124                     obj: any.into(),
125                     advertising_status: AdvertisingStatus::NotAdvertising,
126                 })
127         })
128     }
129 
130     /// Create a Device per the provided file configured to communicate with a controller through an HCI source/sink
from_config_file_with_hci( device_config: &path::Path, source: Source, sink: Sink, ) -> PyResult<Self>131     pub fn from_config_file_with_hci(
132         device_config: &path::Path,
133         source: Source,
134         sink: Sink,
135     ) -> PyResult<Self> {
136         Python::with_gil(|py| {
137             PyModule::import(py, intern!(py, "bumble.device"))?
138                 .getattr(intern!(py, "Device"))?
139                 .call_method1(
140                     intern!(py, "from_config_file_with_hci"),
141                     (device_config, source.0, sink.0),
142                 )
143                 .map(|any| Self {
144                     obj: any.into(),
145                     advertising_status: AdvertisingStatus::NotAdvertising,
146                 })
147         })
148     }
149 
150     /// Create a Device configured to communicate with a controller through an HCI source/sink
with_hci(name: &str, address: Address, source: Source, sink: Sink) -> PyResult<Self>151     pub fn with_hci(name: &str, address: Address, source: Source, sink: Sink) -> PyResult<Self> {
152         Python::with_gil(|py| {
153             PyModule::import(py, intern!(py, "bumble.device"))?
154                 .getattr(intern!(py, "Device"))?
155                 .call_method1(intern!(py, "with_hci"), (name, address.0, source.0, sink.0))
156                 .map(|any| Self {
157                     obj: any.into(),
158                     advertising_status: AdvertisingStatus::NotAdvertising,
159                 })
160         })
161     }
162 
163     /// Sends an HCI command on this Device, returning the command's event result.
164     ///
165     /// When `check_result` is `true`, then an `Err` will be returned if the controller's response
166     /// did not have an event code of "success".
send_command(&self, command: Command, check_result: bool) -> PyResult<Event>167     pub async fn send_command(&self, command: Command, check_result: bool) -> PyResult<Event> {
168         let bumble_hci_command = HciCommand::try_from(command)?;
169         Python::with_gil(|py| {
170             self.obj
171                 .call_method1(
172                     py,
173                     intern!(py, "send_command"),
174                     (bumble_hci_command, check_result),
175                 )
176                 .and_then(|coroutine| into_future(coroutine.as_ref(py)))
177         })?
178         .await
179         .and_then(|event| {
180             Python::with_gil(|py| {
181                 let py_bytes = event.call_method0(py, intern!(py, "__bytes__"))?;
182                 let bytes: &[u8] = py_bytes.extract(py)?;
183                 let event = Event::parse_with_packet_type(bytes)
184                     .map_err(|e| PyErr::new::<PyException, _>(e.to_string()))?;
185                 Ok(event)
186             })
187         })
188     }
189 
190     /// Turn the device on
power_on(&self) -> PyResult<()>191     pub async fn power_on(&self) -> PyResult<()> {
192         Python::with_gil(|py| {
193             self.obj
194                 .call_method0(py, intern!(py, "power_on"))
195                 .and_then(|coroutine| into_future(coroutine.as_ref(py)))
196         })?
197         .await
198         .map(|_| ())
199     }
200 
201     /// Connect to a peer
connect(&self, peer_addr: &str) -> PyResult<Connection>202     pub async fn connect(&self, peer_addr: &str) -> PyResult<Connection> {
203         Python::with_gil(|py| {
204             self.obj
205                 .call_method1(py, intern!(py, "connect"), (peer_addr,))
206                 .and_then(|coroutine| into_future(coroutine.as_ref(py)))
207         })?
208         .await
209         .map(Connection)
210     }
211 
212     /// Register a callback to be called for each incoming connection.
on_connection( &mut self, callback: impl Fn(Python, Connection) -> PyResult<()> + Send + 'static, ) -> PyResult<()>213     pub fn on_connection(
214         &mut self,
215         callback: impl Fn(Python, Connection) -> PyResult<()> + Send + 'static,
216     ) -> PyResult<()> {
217         let boxed = ClosureCallback::new(move |py, args, _kwargs| {
218             callback(py, Connection(args.get_item(0)?.into()))
219         });
220 
221         Python::with_gil(|py| {
222             self.obj
223                 .call_method1(py, intern!(py, "add_listener"), ("connection", boxed))
224         })
225         .map(|_| ())
226     }
227 
228     /// Start scanning
start_scanning(&self, filter_duplicates: bool) -> PyResult<()>229     pub async fn start_scanning(&self, filter_duplicates: bool) -> PyResult<()> {
230         Python::with_gil(|py| {
231             let kwargs = PyDict::new(py);
232             kwargs.set_item("filter_duplicates", filter_duplicates)?;
233             self.obj
234                 .call_method(py, intern!(py, "start_scanning"), (), Some(kwargs))
235                 .and_then(|coroutine| into_future(coroutine.as_ref(py)))
236         })?
237         .await
238         .map(|_| ())
239     }
240 
241     /// Register a callback to be called for each advertisement
on_advertisement( &mut self, callback: impl Fn(Python, Advertisement) -> PyResult<()> + Send + 'static, ) -> PyResult<()>242     pub fn on_advertisement(
243         &mut self,
244         callback: impl Fn(Python, Advertisement) -> PyResult<()> + Send + 'static,
245     ) -> PyResult<()> {
246         let boxed = ClosureCallback::new(move |py, args, _kwargs| {
247             callback(py, Advertisement(args.get_item(0)?.into()))
248         });
249 
250         Python::with_gil(|py| {
251             self.obj
252                 .call_method1(py, intern!(py, "add_listener"), ("advertisement", boxed))
253         })
254         .map(|_| ())
255     }
256 
257     /// Set the advertisement data to be used when [Device::start_advertising] is called.
set_advertising_data(&mut self, adv_data: AdvertisementDataBuilder) -> PyResult<()>258     pub fn set_advertising_data(&mut self, adv_data: AdvertisementDataBuilder) -> PyResult<()> {
259         Python::with_gil(|py| {
260             self.obj.setattr(
261                 py,
262                 intern!(py, "advertising_data"),
263                 adv_data.into_bytes().as_slice(),
264             )
265         })
266         .map(|_| ())
267     }
268 
269     /// Returns the host used by the device, if any
host(&mut self) -> PyResult<Option<Host>>270     pub fn host(&mut self) -> PyResult<Option<Host>> {
271         Python::with_gil(|py| {
272             self.obj
273                 .getattr(py, intern!(py, "host"))
274                 .map(|obj| obj.into_option(Host::from))
275         })
276     }
277 
278     /// Start advertising the data set with [Device.set_advertisement].
279     ///
280     /// When `auto_restart` is set to `true`, then the device will automatically restart advertising
281     /// when a connected device is disconnected.
start_advertising(&mut self, auto_restart: bool) -> PyResult<()>282     pub async fn start_advertising(&mut self, auto_restart: bool) -> PyResult<()> {
283         if self.advertising_status == AdvertisingStatus::AdvertisingExtended {
284             return Err(PyErr::new::<PyException, _>("Already advertising in extended mode. Stop the existing extended advertisement to start a legacy advertisement."));
285         }
286         // Bumble allows (and currently ignores) calling `start_advertising` when already
287         // advertising. Because that behavior may change in the future, we continue to delegate the
288         // handling to bumble.
289 
290         Python::with_gil(|py| {
291             let kwargs = PyDict::new(py);
292             kwargs.set_item("auto_restart", auto_restart)?;
293 
294             self.obj
295                 .call_method(py, intern!(py, "start_advertising"), (), Some(kwargs))
296                 .and_then(|coroutine| into_future(coroutine.as_ref(py)))
297         })?
298         .await
299         .map(|_| ())?;
300 
301         self.advertising_status = AdvertisingStatus::AdvertisingLegacy;
302         Ok(())
303     }
304 
305     /// Start advertising the data set in extended mode, replacing any existing extended adv. The
306     /// advertisement will be non-connectable.
307     ///
308     /// Fails if the device is already advertising in legacy mode.
309     #[cfg(feature = "unstable_extended_adv")]
start_advertising_extended( &mut self, adv_data: AdvertisementDataBuilder, ) -> PyResult<()>310     pub async fn start_advertising_extended(
311         &mut self,
312         adv_data: AdvertisementDataBuilder,
313     ) -> PyResult<()> {
314         // TODO: add tests when local controller object supports extended advertisement commands (github.com/google/bumble/pull/238)
315         match self.advertising_status {
316             AdvertisingStatus::AdvertisingLegacy => return Err(PyErr::new::<PyException, _>("Already advertising in legacy mode. Stop the existing legacy advertisement to start an extended advertisement.")),
317             // Stop the current extended advertisement before advertising with new data.
318             // We could just issue an LeSetExtendedAdvertisingData command, but this approach
319             // allows better future flexibility if `start_advertising_extended` were to change.
320             AdvertisingStatus::AdvertisingExtended => self.stop_advertising_extended().await?,
321             _ => {}
322         }
323 
324         // set extended params
325         let properties = AdvertisingEventProperties {
326             connectable: 0,
327             scannable: 0,
328             directed: 0,
329             high_duty_cycle: 0,
330             legacy: 0,
331             anonymous: 0,
332             tx_power: 0,
333         };
334         let extended_advertising_params_cmd = LeSetExtendedAdvertisingParametersBuilder {
335             advertising_event_properties: properties,
336             advertising_filter_policy: AdvertisingFilterPolicy::AllDevices,
337             advertising_handle: Self::ADVERTISING_HANDLE_EXTENDED,
338             advertising_sid: 0,
339             advertising_tx_power: 0,
340             own_address_type: OwnAddressType::RandomDeviceAddress,
341             peer_address: default_ignored_peer_address(),
342             peer_address_type: PeerAddressType::PublicDeviceOrIdentityAddress,
343             primary_advertising_channel_map: 7,
344             primary_advertising_interval_max: 200,
345             primary_advertising_interval_min: 100,
346             primary_advertising_phy: PrimaryPhyType::Le1m,
347             scan_request_notification_enable: Enable::Disabled,
348             secondary_advertising_max_skip: 0,
349             secondary_advertising_phy: SecondaryPhyType::Le1m,
350         };
351         self.send_command(extended_advertising_params_cmd.into(), true)
352             .await?;
353 
354         // set random address
355         let random_address: packets::Address =
356             self.random_address()?.try_into().map_err(|e| match e {
357                 ConversionError::Python(pyerr) => pyerr,
358                 ConversionError::Native(e) => PyErr::new::<PyException, _>(format!("{e:?}")),
359             })?;
360         let random_address_cmd = LeSetAdvertisingSetRandomAddressBuilder {
361             advertising_handle: Self::ADVERTISING_HANDLE_EXTENDED,
362             random_address,
363         };
364         self.send_command(random_address_cmd.into(), true).await?;
365 
366         // set adv data
367         let advertising_data_cmd = LeSetExtendedAdvertisingDataBuilder {
368             advertising_data: adv_data.into_bytes(),
369             advertising_handle: Self::ADVERTISING_HANDLE_EXTENDED,
370             fragment_preference: FragmentPreference::ControllerMayFragment,
371             operation: Operation::CompleteAdvertisement,
372         };
373         self.send_command(advertising_data_cmd.into(), true).await?;
374 
375         // enable adv
376         let extended_advertising_enable_cmd = LeSetExtendedAdvertisingEnableBuilder {
377             enable: Enable::Enabled,
378             enabled_sets: vec![EnabledSet {
379                 advertising_handle: Self::ADVERTISING_HANDLE_EXTENDED,
380                 duration: 0,
381                 max_extended_advertising_events: 0,
382             }],
383         };
384         self.send_command(extended_advertising_enable_cmd.into(), true)
385             .await?;
386 
387         self.advertising_status = AdvertisingStatus::AdvertisingExtended;
388         Ok(())
389     }
390 
391     /// Stop advertising.
stop_advertising(&mut self) -> PyResult<()>392     pub async fn stop_advertising(&mut self) -> PyResult<()> {
393         Python::with_gil(|py| {
394             self.obj
395                 .call_method0(py, intern!(py, "stop_advertising"))
396                 .and_then(|coroutine| into_future(coroutine.as_ref(py)))
397         })?
398         .await
399         .map(|_| ())?;
400 
401         if self.advertising_status == AdvertisingStatus::AdvertisingLegacy {
402             self.advertising_status = AdvertisingStatus::NotAdvertising;
403         }
404         Ok(())
405     }
406 
407     /// Stop advertising extended.
408     #[cfg(feature = "unstable_extended_adv")]
stop_advertising_extended(&mut self) -> PyResult<()>409     pub async fn stop_advertising_extended(&mut self) -> PyResult<()> {
410         if AdvertisingStatus::AdvertisingExtended != self.advertising_status {
411             return Ok(());
412         }
413 
414         // disable adv
415         let extended_advertising_enable_cmd = LeSetExtendedAdvertisingEnableBuilder {
416             enable: Enable::Disabled,
417             enabled_sets: vec![EnabledSet {
418                 advertising_handle: Self::ADVERTISING_HANDLE_EXTENDED,
419                 duration: 0,
420                 max_extended_advertising_events: 0,
421             }],
422         };
423         self.send_command(extended_advertising_enable_cmd.into(), true)
424             .await?;
425 
426         self.advertising_status = AdvertisingStatus::NotAdvertising;
427         Ok(())
428     }
429 
430     /// Registers an L2CAP connection oriented channel server. When a client connects to the server,
431     /// the `server` callback is passed a handle to the established channel. When optional arguments
432     /// are not specified, the Python module specifies the defaults.
register_l2cap_channel_server( &mut self, psm: u16, server: impl Fn(Python, LeConnectionOrientedChannel) -> PyResult<()> + Send + 'static, max_credits: Option<u16>, mtu: Option<u16>, mps: Option<u16>, ) -> PyResult<()>433     pub fn register_l2cap_channel_server(
434         &mut self,
435         psm: u16,
436         server: impl Fn(Python, LeConnectionOrientedChannel) -> PyResult<()> + Send + 'static,
437         max_credits: Option<u16>,
438         mtu: Option<u16>,
439         mps: Option<u16>,
440     ) -> PyResult<()> {
441         Python::with_gil(|py| {
442             let boxed = ClosureCallback::new(move |py, args, _kwargs| {
443                 server(
444                     py,
445                     LeConnectionOrientedChannel::from(args.get_item(0)?.into()),
446                 )
447             });
448 
449             let kwargs = PyDict::new(py);
450             kwargs.set_item("psm", psm)?;
451             kwargs.set_item("server", boxed.into_py(py))?;
452             kwargs.set_opt_item("max_credits", max_credits)?;
453             kwargs.set_opt_item("mtu", mtu)?;
454             kwargs.set_opt_item("mps", mps)?;
455             self.obj.call_method(
456                 py,
457                 intern!(py, "register_l2cap_channel_server"),
458                 (),
459                 Some(kwargs),
460             )
461         })?;
462         Ok(())
463     }
464 
465     /// Gets the Device's `random_address` property
random_address(&self) -> PyResult<Address>466     pub fn random_address(&self) -> PyResult<Address> {
467         Python::with_gil(|py| {
468             self.obj
469                 .getattr(py, intern!(py, "random_address"))
470                 .map(Address)
471         })
472     }
473 }
474 
475 /// A connection to a remote device.
476 pub struct Connection(PyObject);
477 
478 impl Connection {
479     /// Open an L2CAP channel using this connection. When optional arguments are not specified, the
480     /// Python module specifies the defaults.
open_l2cap_channel( &mut self, psm: u16, max_credits: Option<u16>, mtu: Option<u16>, mps: Option<u16>, ) -> PyResult<LeConnectionOrientedChannel>481     pub async fn open_l2cap_channel(
482         &mut self,
483         psm: u16,
484         max_credits: Option<u16>,
485         mtu: Option<u16>,
486         mps: Option<u16>,
487     ) -> PyResult<LeConnectionOrientedChannel> {
488         Python::with_gil(|py| {
489             let kwargs = PyDict::new(py);
490             kwargs.set_item("psm", psm)?;
491             kwargs.set_opt_item("max_credits", max_credits)?;
492             kwargs.set_opt_item("mtu", mtu)?;
493             kwargs.set_opt_item("mps", mps)?;
494             self.0
495                 .call_method(py, intern!(py, "open_l2cap_channel"), (), Some(kwargs))
496                 .and_then(|coroutine| into_future(coroutine.as_ref(py)))
497         })?
498         .await
499         .map(LeConnectionOrientedChannel::from)
500     }
501 
502     /// Disconnect from device with provided reason. When optional arguments are not specified, the
503     /// Python module specifies the defaults.
disconnect(&mut self, reason: Option<ErrorCode>) -> PyResult<()>504     pub async fn disconnect(&mut self, reason: Option<ErrorCode>) -> PyResult<()> {
505         Python::with_gil(|py| {
506             let kwargs = PyDict::new(py);
507             kwargs.set_opt_item("reason", reason)?;
508             self.0
509                 .call_method(py, intern!(py, "disconnect"), (), Some(kwargs))
510                 .and_then(|coroutine| into_future(coroutine.as_ref(py)))
511         })?
512         .await
513         .map(|_| ())
514     }
515 
516     /// Register a callback to be called on disconnection.
on_disconnection( &mut self, callback: impl Fn(Python, ErrorCode) -> PyResult<()> + Send + 'static, ) -> PyResult<()>517     pub fn on_disconnection(
518         &mut self,
519         callback: impl Fn(Python, ErrorCode) -> PyResult<()> + Send + 'static,
520     ) -> PyResult<()> {
521         let boxed = ClosureCallback::new(move |py, args, _kwargs| {
522             callback(py, args.get_item(0)?.extract()?)
523         });
524 
525         Python::with_gil(|py| {
526             self.0
527                 .call_method1(py, intern!(py, "add_listener"), ("disconnection", boxed))
528         })
529         .map(|_| ())
530     }
531 
532     /// Returns some information about the connection as a [String].
debug_string(&self) -> PyResult<String>533     pub fn debug_string(&self) -> PyResult<String> {
534         Python::with_gil(|py| {
535             let str_obj = self.0.call_method0(py, intern!(py, "__str__"))?;
536             str_obj.gil_ref(py).extract()
537         })
538     }
539 }
540 
541 /// The other end of a connection
542 pub struct Peer(PyObject);
543 
544 impl Peer {
545     /// Wrap a [Connection] in a Peer
new(conn: Connection) -> PyResult<Self>546     pub fn new(conn: Connection) -> PyResult<Self> {
547         Python::with_gil(|py| {
548             PyModule::import(py, intern!(py, "bumble.device"))?
549                 .getattr(intern!(py, "Peer"))?
550                 .call1((conn.0,))
551                 .map(|obj| Self(obj.into()))
552         })
553     }
554 
555     /// Populates the peer's cache of services.
556     ///
557     /// Returns the discovered services.
discover_services(&mut self) -> PyResult<Vec<ServiceProxy>>558     pub async fn discover_services(&mut self) -> PyResult<Vec<ServiceProxy>> {
559         Python::with_gil(|py| {
560             self.0
561                 .call_method0(py, intern!(py, "discover_services"))
562                 .and_then(|coroutine| into_future(coroutine.as_ref(py)))
563         })?
564         .await
565         .and_then(|list| {
566             Python::with_gil(|py| {
567                 list.as_ref(py)
568                     .iter()?
569                     .map(|r| r.map(|h| ServiceProxy(h.to_object(py))))
570                     .collect()
571             })
572         })
573     }
574 
575     /// Returns a snapshot of the Services currently in the peer's cache
services(&self) -> PyResult<Vec<ServiceProxy>>576     pub fn services(&self) -> PyResult<Vec<ServiceProxy>> {
577         Python::with_gil(|py| {
578             self.0
579                 .getattr(py, intern!(py, "services"))?
580                 .as_ref(py)
581                 .iter()?
582                 .map(|r| r.map(|h| ServiceProxy(h.to_object(py))))
583                 .collect()
584         })
585     }
586 
587     /// Build a [ProfileServiceProxy] for the specified type.
588     /// [Peer::discover_services] or some other means of populating the Peer's service cache must be
589     /// called first, or the required service won't be found.
create_service_proxy<P: ProfileServiceProxy>(&self) -> PyResult<Option<P>>590     pub fn create_service_proxy<P: ProfileServiceProxy>(&self) -> PyResult<Option<P>> {
591         Python::with_gil(|py| {
592             let module = py.import(P::PROXY_CLASS_MODULE)?;
593             let class = module.getattr(P::PROXY_CLASS_NAME)?;
594             self.0
595                 .call_method1(py, intern!(py, "create_service_proxy"), (class,))
596                 .map(|obj| obj.into_option(P::wrap))
597         })
598     }
599 }
600 
601 /// A BLE advertisement
602 pub struct Advertisement(PyObject);
603 
604 impl Advertisement {
605     /// Address that sent the advertisement
address(&self) -> PyResult<Address>606     pub fn address(&self) -> PyResult<Address> {
607         Python::with_gil(|py| self.0.getattr(py, intern!(py, "address")).map(Address))
608     }
609 
610     /// Returns true if the advertisement is connectable
is_connectable(&self) -> PyResult<bool>611     pub fn is_connectable(&self) -> PyResult<bool> {
612         Python::with_gil(|py| {
613             self.0
614                 .getattr(py, intern!(py, "is_connectable"))?
615                 .extract::<bool>(py)
616         })
617     }
618 
619     /// RSSI of the advertisement
rssi(&self) -> PyResult<i8>620     pub fn rssi(&self) -> PyResult<i8> {
621         Python::with_gil(|py| self.0.getattr(py, intern!(py, "rssi"))?.extract::<i8>(py))
622     }
623 
624     /// Data in the advertisement
data(&self) -> PyResult<AdvertisingData>625     pub fn data(&self) -> PyResult<AdvertisingData> {
626         Python::with_gil(|py| self.0.getattr(py, intern!(py, "data")).map(AdvertisingData))
627     }
628 }
629 
630 /// Use this address when sending an HCI command that requires providing a peer address, but the
631 /// command is such that the peer address will be ignored.
632 ///
633 /// Internal to bumble, this address might mean "any", but a packets::Address typically gets sent
634 /// directly to a controller, so we don't have to worry about it.
635 #[cfg(feature = "unstable_extended_adv")]
default_ignored_peer_address() -> packets::Address636 fn default_ignored_peer_address() -> packets::Address {
637     packets::Address::try_from(0x0000_0000_0000_u64).unwrap()
638 }
639