Struct bluest::Device

source ·
pub struct Device(/* private fields */);
Expand description

A Bluetooth LE device

Implementations§

source§

impl Device

source

pub fn id(&self) -> DeviceId

This device’s unique identifier

Examples found in repository?
examples/reconnect.rs (line 37)
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
async fn main() -> Result<(), Box<dyn Error>> {
    use tracing_subscriber::prelude::*;
    use tracing_subscriber::{fmt, EnvFilter};

    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .from_env_lossy(),
        )
        .init();

    let device_id = {
        let adapter = Adapter::default().await.unwrap();
        adapter.wait_available().await?;

        info!("looking for device");
        let device = adapter
            .discover_devices(&[btuuid::services::BATTERY])
            .await?
            .next()
            .await
            .ok_or("Failed to discover device")??;
        info!(
            "found device: {} ({:?})",
            device.name().as_deref().unwrap_or("(unknown)"),
            device.id()
        );

        device.id()
    };

    info!("Time passes...");
    tokio::time::sleep(Duration::from_secs(5)).await;

    {
        let adapter = Adapter::default().await.unwrap();
        adapter.wait_available().await?;

        info!("re-opening previously found device");
        let device = adapter.open_device(&device_id).await?;
        info!(
            "re-opened device: {} ({:?})",
            device.name().as_deref().unwrap_or("(unknown)"),
            device.id()
        );
    }

    Ok(())
}
More examples
Hide additional examples
examples/blinky.rs (line 40)
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
async fn main() -> Result<(), Box<dyn Error>> {
    use tracing_subscriber::prelude::*;
    use tracing_subscriber::{fmt, EnvFilter};

    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .from_env_lossy(),
        )
        .init();

    let adapter = Adapter::default().await.ok_or("Bluetooth adapter not found")?;
    adapter.wait_available().await?;

    info!("looking for device");
    let device = adapter
        .discover_devices(&[NORDIC_LED_AND_BUTTON_SERVICE])
        .await?
        .next()
        .await
        .ok_or("Failed to discover device")??;
    info!(
        "found device: {} ({:?})",
        device.name().as_deref().unwrap_or("(unknown)"),
        device.id()
    );

    adapter.connect_device(&device).await?;
    info!("connected!");

    let service = match device
        .discover_services_with_uuid(NORDIC_LED_AND_BUTTON_SERVICE)
        .await?
        .get(0)
    {
        Some(service) => service.clone(),
        None => return Err("service not found".into()),
    };
    info!("found LED and button service");

    let characteristics = service.characteristics().await?;
    info!("discovered characteristics");

    let button_characteristic = characteristics
        .iter()
        .find(|x| x.uuid() == BLINKY_BUTTON_STATE_CHARACTERISTIC)
        .ok_or("button characteristic not found")?;

    let button_fut = async {
        info!("enabling button notifications");
        let mut updates = button_characteristic.notify().await?;
        info!("waiting for button changes");
        while let Some(val) = updates.next().await {
            info!("Button state changed: {:?}", val?);
        }
        Ok(())
    };

    let led_characteristic = characteristics
        .iter()
        .find(|x| x.uuid() == BLINKY_LED_STATE_CHARACTERISTIC)
        .ok_or("led characteristic not found")?;

    let blink_fut = async {
        info!("blinking LED");
        tokio::time::sleep(Duration::from_secs(1)).await;
        loop {
            led_characteristic.write(&[0x01]).await?;
            info!("LED on");
            tokio::time::sleep(Duration::from_secs(1)).await;
            led_characteristic.write(&[0x00]).await?;
            info!("LED off");
            tokio::time::sleep(Duration::from_secs(1)).await;
        }
    };

    type R = Result<(), Box<dyn Error>>;
    let button_fut = async move {
        let res: R = button_fut.await;
        error!("Button task exited: {:?}", res);
    };
    let blink_fut = async move {
        let res: R = blink_fut.await;
        error!("Blink task exited: {:?}", res);
    };

    future::zip(blink_fut, button_fut).await;

    Ok(())
}
source

pub fn name(&self) -> Result<String>

The local name for this device, if available

This can either be a name advertised or read from the device, or a name assigned to the device by the OS.

Panics

On Linux, this method will panic if there is a current Tokio runtime and it is single-threaded or if there is no current Tokio runtime and creating one fails.

Examples found in repository?
examples/pair.rs (line 21)
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
    async fn confirm(&self, device: &Device) -> Result<(), PairingRejected> {
        tokio::task::block_in_place(move || {
            println!("Do you want to pair with {:?}? (Y/n)", device.name().unwrap());
            let mut buf = String::new();
            std::io::stdin()
                .read_line(&mut buf)
                .map_err(|_| PairingRejected::default())?;
            let response = buf.trim();
            if response.is_empty() || response == "y" || response == "Y" {
                Ok(())
            } else {
                Err(PairingRejected::default())
            }
        })
    }

    async fn confirm_passkey(&self, device: &Device, passkey: Passkey) -> Result<(), PairingRejected> {
        tokio::task::block_in_place(move || {
            println!(
                "Is the passkey \"{}\" displayed on {:?}? (Y/n)",
                passkey,
                device.name().unwrap()
            );
            let mut buf = String::new();
            std::io::stdin()
                .read_line(&mut buf)
                .map_err(|_| PairingRejected::default())?;
            let response = buf.trim();
            if response.is_empty() || response == "y" || response == "Y" {
                Ok(())
            } else {
                Err(PairingRejected::default())
            }
        })
    }

    async fn request_passkey(&self, device: &Device) -> Result<Passkey, PairingRejected> {
        tokio::task::block_in_place(move || {
            println!("Please enter the 6-digit passkey for {:?}: ", device.name().unwrap());
            let mut buf = String::new();
            std::io::stdin()
                .read_line(&mut buf)
                .map_err(|_| PairingRejected::default())?;
            buf.trim().parse().map_err(|_| PairingRejected::default())
        })
    }

    fn display_passkey(&self, device: &Device, passkey: Passkey) {
        println!("The passkey is \"{}\" for {:?}.", passkey, device.name().unwrap());
    }
More examples
Hide additional examples
examples/scan.rs (line 31)
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
async fn main() -> Result<(), Box<dyn Error>> {
    use tracing_subscriber::prelude::*;
    use tracing_subscriber::{fmt, EnvFilter};

    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .from_env_lossy(),
        )
        .init();

    let adapter = Adapter::default().await.ok_or("Bluetooth adapter not found")?;
    adapter.wait_available().await?;

    info!("starting scan");
    let mut scan = adapter.scan(&[]).await?;
    info!("scan started");
    while let Some(discovered_device) = scan.next().await {
        info!(
            "{}{}: {:?}",
            discovered_device.device.name().as_deref().unwrap_or("(unknown)"),
            discovered_device
                .rssi
                .map(|x| format!(" ({}dBm)", x))
                .unwrap_or_default(),
            discovered_device.adv_data.services
        );
    }

    Ok(())
}
examples/reconnect.rs (line 36)
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
async fn main() -> Result<(), Box<dyn Error>> {
    use tracing_subscriber::prelude::*;
    use tracing_subscriber::{fmt, EnvFilter};

    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .from_env_lossy(),
        )
        .init();

    let device_id = {
        let adapter = Adapter::default().await.unwrap();
        adapter.wait_available().await?;

        info!("looking for device");
        let device = adapter
            .discover_devices(&[btuuid::services::BATTERY])
            .await?
            .next()
            .await
            .ok_or("Failed to discover device")??;
        info!(
            "found device: {} ({:?})",
            device.name().as_deref().unwrap_or("(unknown)"),
            device.id()
        );

        device.id()
    };

    info!("Time passes...");
    tokio::time::sleep(Duration::from_secs(5)).await;

    {
        let adapter = Adapter::default().await.unwrap();
        adapter.wait_available().await?;

        info!("re-opening previously found device");
        let device = adapter.open_device(&device_id).await?;
        info!(
            "re-opened device: {} ({:?})",
            device.name().as_deref().unwrap_or("(unknown)"),
            device.id()
        );
    }

    Ok(())
}
examples/blinky.rs (line 39)
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
async fn main() -> Result<(), Box<dyn Error>> {
    use tracing_subscriber::prelude::*;
    use tracing_subscriber::{fmt, EnvFilter};

    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .from_env_lossy(),
        )
        .init();

    let adapter = Adapter::default().await.ok_or("Bluetooth adapter not found")?;
    adapter.wait_available().await?;

    info!("looking for device");
    let device = adapter
        .discover_devices(&[NORDIC_LED_AND_BUTTON_SERVICE])
        .await?
        .next()
        .await
        .ok_or("Failed to discover device")??;
    info!(
        "found device: {} ({:?})",
        device.name().as_deref().unwrap_or("(unknown)"),
        device.id()
    );

    adapter.connect_device(&device).await?;
    info!("connected!");

    let service = match device
        .discover_services_with_uuid(NORDIC_LED_AND_BUTTON_SERVICE)
        .await?
        .get(0)
    {
        Some(service) => service.clone(),
        None => return Err("service not found".into()),
    };
    info!("found LED and button service");

    let characteristics = service.characteristics().await?;
    info!("discovered characteristics");

    let button_characteristic = characteristics
        .iter()
        .find(|x| x.uuid() == BLINKY_BUTTON_STATE_CHARACTERISTIC)
        .ok_or("button characteristic not found")?;

    let button_fut = async {
        info!("enabling button notifications");
        let mut updates = button_characteristic.notify().await?;
        info!("waiting for button changes");
        while let Some(val) = updates.next().await {
            info!("Button state changed: {:?}", val?);
        }
        Ok(())
    };

    let led_characteristic = characteristics
        .iter()
        .find(|x| x.uuid() == BLINKY_LED_STATE_CHARACTERISTIC)
        .ok_or("led characteristic not found")?;

    let blink_fut = async {
        info!("blinking LED");
        tokio::time::sleep(Duration::from_secs(1)).await;
        loop {
            led_characteristic.write(&[0x01]).await?;
            info!("LED on");
            tokio::time::sleep(Duration::from_secs(1)).await;
            led_characteristic.write(&[0x00]).await?;
            info!("LED off");
            tokio::time::sleep(Duration::from_secs(1)).await;
        }
    };

    type R = Result<(), Box<dyn Error>>;
    let button_fut = async move {
        let res: R = button_fut.await;
        error!("Button task exited: {:?}", res);
    };
    let blink_fut = async move {
        let res: R = blink_fut.await;
        error!("Blink task exited: {:?}", res);
    };

    future::zip(blink_fut, button_fut).await;

    Ok(())
}
source

pub async fn name_async(&self) -> Result<String>

The local name for this device, if available

This can either be a name advertised or read from the device, or a name assigned to the device by the OS.

source

pub async fn is_connected(&self) -> bool

The connection status for this device

source

pub async fn is_paired(&self) -> Result<bool>

The pairing status for this device

source

pub async fn pair(&self) -> Result<()>

Attempt to pair this device using the system default pairing UI

Platform specific
MacOS/iOS

Device pairing is performed automatically by the OS when a characteristic requiring security is accessed. This method is a no-op.

Windows

This will fail unless it is called from a UWP application.

source

pub async fn pair_with_agent<T: PairingAgent + 'static>( &self, agent: &T ) -> Result<()>

Attempt to pair this device using the system default pairing UI

Platform specific

On MacOS/iOS, device pairing is performed automatically by the OS when a characteristic requiring security is accessed. This method is a no-op.

Examples found in repository?
examples/pair.rs (line 101)
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
async fn main() -> Result<(), Box<dyn Error>> {
    use tracing_subscriber::prelude::*;
    use tracing_subscriber::{fmt, EnvFilter};

    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .from_env_lossy(),
        )
        .init();

    let adapter = Adapter::default().await.ok_or("Bluetooth adapter not found")?;
    adapter.wait_available().await?;

    let discovered_device = {
        info!("starting scan");
        let mut scan = adapter.scan(&[btuuid::services::HUMAN_INTERFACE_DEVICE]).await?;
        info!("scan started");
        scan.next().await.ok_or("scan terminated")?
    };

    info!("{:?} {:?}", discovered_device.rssi, discovered_device.adv_data);
    let device = discovered_device.device;

    adapter.connect_device(&device).await?;
    info!("connected!");

    device.pair_with_agent(&StdioPairingAgent).await?;
    info!("paired!");

    adapter.disconnect_device(&device).await?;
    info!("disconnected!");

    Ok(())
}
source

pub async fn unpair(&self) -> Result<()>

Disconnect and unpair this device from the system

Platform specific

Not supported on MacOS/iOS.

source

pub async fn discover_services(&self) -> Result<Vec<Service>>

Discover the primary services of this device.

source

pub async fn discover_services_with_uuid( &self, uuid: Uuid ) -> Result<Vec<Service>>

Discover the primary service(s) of this device with the given Uuid.

Examples found in repository?
examples/blinky.rs (line 47)
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
async fn main() -> Result<(), Box<dyn Error>> {
    use tracing_subscriber::prelude::*;
    use tracing_subscriber::{fmt, EnvFilter};

    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .from_env_lossy(),
        )
        .init();

    let adapter = Adapter::default().await.ok_or("Bluetooth adapter not found")?;
    adapter.wait_available().await?;

    info!("looking for device");
    let device = adapter
        .discover_devices(&[NORDIC_LED_AND_BUTTON_SERVICE])
        .await?
        .next()
        .await
        .ok_or("Failed to discover device")??;
    info!(
        "found device: {} ({:?})",
        device.name().as_deref().unwrap_or("(unknown)"),
        device.id()
    );

    adapter.connect_device(&device).await?;
    info!("connected!");

    let service = match device
        .discover_services_with_uuid(NORDIC_LED_AND_BUTTON_SERVICE)
        .await?
        .get(0)
    {
        Some(service) => service.clone(),
        None => return Err("service not found".into()),
    };
    info!("found LED and button service");

    let characteristics = service.characteristics().await?;
    info!("discovered characteristics");

    let button_characteristic = characteristics
        .iter()
        .find(|x| x.uuid() == BLINKY_BUTTON_STATE_CHARACTERISTIC)
        .ok_or("button characteristic not found")?;

    let button_fut = async {
        info!("enabling button notifications");
        let mut updates = button_characteristic.notify().await?;
        info!("waiting for button changes");
        while let Some(val) = updates.next().await {
            info!("Button state changed: {:?}", val?);
        }
        Ok(())
    };

    let led_characteristic = characteristics
        .iter()
        .find(|x| x.uuid() == BLINKY_LED_STATE_CHARACTERISTIC)
        .ok_or("led characteristic not found")?;

    let blink_fut = async {
        info!("blinking LED");
        tokio::time::sleep(Duration::from_secs(1)).await;
        loop {
            led_characteristic.write(&[0x01]).await?;
            info!("LED on");
            tokio::time::sleep(Duration::from_secs(1)).await;
            led_characteristic.write(&[0x00]).await?;
            info!("LED off");
            tokio::time::sleep(Duration::from_secs(1)).await;
        }
    };

    type R = Result<(), Box<dyn Error>>;
    let button_fut = async move {
        let res: R = button_fut.await;
        error!("Button task exited: {:?}", res);
    };
    let blink_fut = async move {
        let res: R = blink_fut.await;
        error!("Blink task exited: {:?}", res);
    };

    future::zip(blink_fut, button_fut).await;

    Ok(())
}
source

pub async fn services(&self) -> Result<Vec<Service>>

Get previously discovered services.

If no services have been discovered yet, this method will perform service discovery.

Examples found in repository?
examples/connected.rs (line 29)
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
async fn main() -> Result<(), Box<dyn Error>> {
    use tracing_subscriber::prelude::*;
    use tracing_subscriber::{fmt, EnvFilter};

    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .from_env_lossy(),
        )
        .init();

    let adapter = Adapter::default().await.ok_or("Bluetooth adapter not found")?;
    adapter.wait_available().await?;

    info!("getting connected devices");
    let devices = adapter.connected_devices().await?;
    for device in devices {
        info!("found {:?}", device);
        adapter.connect_device(&device).await?;
        let services = device.services().await?;
        for service in services {
            info!("  {:?}", service);
            let characteristics = service.characteristics().await?;
            for characteristic in characteristics {
                info!("    {:?}", characteristic);
                let props = characteristic.properties().await?;
                info!("      props: {:?}", props);
                if props.read {
                    info!("      value: {:?}", characteristic.read().await);
                }
                if props.write_without_response {
                    info!("      max_write_len: {:?}", characteristic.max_write_len());
                }

                let descriptors = characteristic.descriptors().await?;
                for descriptor in descriptors {
                    info!("      {:?}: {:?}", descriptor, descriptor.read().await);
                }
            }
        }
    }
    info!("done");

    Ok(())
}
source

pub async fn services_changed(&self) -> Result<()>

Asynchronously blocks until a GATT services changed packet is received

Platform specific

See Device::service_changed_indications.

source

pub async fn service_changed_indications( &self ) -> Result<impl Stream<Item = Result<ServicesChanged>> + Send + Unpin + '_>

Monitors the device for service changed indications.

Platform specific

On Windows an event is generated whenever the services value is updated. In addition to actual service change indications this occurs when, for example, discover_services is called or when an unpaired device disconnects.

source

pub async fn rssi(&self) -> Result<i16>

Get the current signal strength from the device in dBm.

Platform specific

Returns NotSupported on Windows and Linux.

Trait Implementations§

source§

impl Clone for Device

source§

fn clone(&self) -> Device

Returns a copy of the value. Read more
1.0.0 · source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
source§

impl Debug for Device

source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
source§

impl Display for Device

source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
source§

impl Hash for Device

source§

fn hash<__H: Hasher>(&self, state: &mut __H)

Feeds this value into the given Hasher. Read more
1.3.0 · source§

fn hash_slice<H>(data: &[Self], state: &mut H)where H: Hasher, Self: Sized,

Feeds a slice of this type into the given Hasher. Read more
source§

impl PartialEq for Device

source§

fn eq(&self, other: &Device) -> bool

This method tests for self and other values to be equal, and is used by ==.
1.0.0 · source§

fn ne(&self, other: &Rhs) -> bool

This method tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
source§

impl Eq for Device

source§

impl StructuralEq for Device

source§

impl StructuralPartialEq for Device

Auto Trait Implementations§

Blanket Implementations§

source§

impl<T> Any for Twhere T: 'static + ?Sized,

source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
source§

impl<T> Borrow<T> for Twhere T: ?Sized,

source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
source§

impl<T> BorrowMut<T> for Twhere T: ?Sized,

source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
source§

impl<T> From<T> for T

source§

fn from(t: T) -> T

Returns the argument unchanged.

source§

impl<T> Instrument for T

source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
source§

impl<T, U> Into<U> for Twhere U: From<T>,

source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

source§

impl<T> ToOwned for Twhere T: Clone,

§

type Owned = T

The resulting type after obtaining ownership.
source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
source§

impl<T> ToString for Twhere T: Display + ?Sized,

source§

default fn to_string(&self) -> String

Converts the given value to a String. Read more
source§

impl<T, U> TryFrom<U> for Twhere U: Into<T>,

§

type Error = Infallible

The type returned in the event of a conversion error.
source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
source§

impl<T, U> TryInto<U> for Twhere U: TryFrom<T>,

§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
source§

impl<T> WithSubscriber for T

source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more