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