Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GATT per-client notifications are currently unsupported #65

Open
alexandrefresnais opened this issue Jan 15, 2023 · 3 comments
Open

GATT per-client notifications are currently unsupported #65

alexandrefresnais opened this issue Jan 15, 2023 · 3 comments
Labels
external caused by bluetoothd or kernel

Comments

@alexandrefresnais
Copy link

Hello here!

Coming back on #62
I was preparing a Nordic UART Service example following your addition to the library but I cannot make it working.
When writing to the notifier of a client, both my phones get notified.

A few months ago, I took a look at BlueZ DBuS API and saw nothing to notify a specific client.
We asked if it was possible in an issue but no one answered (bluez/bluez#409)

So, following the previous issue, do you think this service is really possible with BlueR ?
I have serious belief that it is not currently possible in anyway with BlueZ.

@surban surban self-assigned this Jan 15, 2023
@surban
Copy link
Collaborator

surban commented Jan 15, 2023

BlueZ does provide the device address of the device that started the notification session along with the file descriptor that is used for sending notifications. Thus if you have two CharacteristicWriters I would expect BlueZ to send the data to each corresponding client.

However, I haven't checked how this is implemented in BlueZ. So maybe we get two file descriptors, but both send notifications to all clients. You would have to read the BlueZ source to find out what is going on here.

Could you share your source so that we can have a look at it?

@alexandrefresnais
Copy link
Author

Thanks for your quick reply.

Sure I can give you the code. I have tried to do something very naive.

//! Serves a Bluetooth GATT echo server.

use bluer::{
    adv::Advertisement,
    gatt::{
        local::{
            characteristic_control, Application, Characteristic, CharacteristicControlEvent,
            CharacteristicNotify, CharacteristicNotifyMethod, CharacteristicWrite,
            CharacteristicWriteMethod, Service,
        },
        CharacteristicReader, CharacteristicWriter,
    },
};
use futures::{future, pin_mut, StreamExt};
use std::time::Duration;
use tokio::{
    io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
    time::sleep,
};

/// Nordic UART Service UUIDs for GATT server.
#[allow(dead_code)]
const NUS_UUID: uuid::Uuid = uuid::Uuid::from_u128(0x6e400001b5a3f393e0a9e50e24dcca9e);
#[allow(dead_code)]
const NUS_RX_UUID: uuid::Uuid = uuid::Uuid::from_u128(0x6e400002b5a3f393e0a9e50e24dcca9e);
#[allow(dead_code)]
const NUS_TX_UUID: uuid::Uuid = uuid::Uuid::from_u128(0x6e400003b5a3f393e0a9e50e24dcca9e);

#[tokio::main]
async fn main() -> bluer::Result<()> {
    let session = bluer::Session::new().await?;
    let adapter = session.default_adapter().await?;
    adapter.set_powered(true).await?;

    println!(
        "Advertising on Bluetooth adapter {} with address {}",
        adapter.name(),
        adapter.address().await?
    );
    let le_advertisement = Advertisement {
        service_uuids: vec![NUS_UUID].into_iter().collect(),
        discoverable: Some(true),
        local_name: Some("nus_multiperipheral".to_string()),
        ..Default::default()
    };
    let adv_handle = adapter.advertise(le_advertisement).await?;

    println!(
        "Serving GATT echo service on Bluetooth adapter {}",
        adapter.name()
    );
    let (rx_char_control, rx_char_handle) = characteristic_control();
    let (tx_char_control, tx_char_handle) = characteristic_control();

    let app = Application {
        services: vec![Service {
            uuid: NUS_UUID,
            primary: true,
            characteristics: vec![
                Characteristic {
                    uuid: NUS_RX_UUID,
                    write: Some(CharacteristicWrite {
                        write: true,
                        write_without_response: true,
                        method: CharacteristicWriteMethod::Io,
                        ..Default::default()
                    }),
                    control_handle: rx_char_handle,
                    ..Default::default()
                },
                Characteristic {
                    uuid: NUS_TX_UUID,
                    notify: Some(CharacteristicNotify {
                        notify: true,
                        method: CharacteristicNotifyMethod::Io,
                        ..Default::default()
                    }),
                    control_handle: tx_char_handle,
                    ..Default::default()
                },
            ],
            ..Default::default()
        }],
        ..Default::default()
    };
    let app_handle = adapter.serve_gatt_application(app).await?;

    println!("Echo service ready. Press enter to quit.");
    let stdin = BufReader::new(tokio::io::stdin());
    let mut lines = stdin.lines();

    pin_mut!(rx_char_control);
    pin_mut!(tx_char_control);

    let mut reader_opt1: Option<CharacteristicReader> = None;
    let mut writer_opt1: Option<CharacteristicWriter> = None;

    let mut reader_opt2: Option<CharacteristicReader> = None;
    let mut writer_opt2: Option<CharacteristicWriter> = None;

    let mut read_buf1 = Vec::new();
    let mut read_buf2 = Vec::new();

    loop {
        tokio::select! {
            _ = lines.next_line() => break,
            evt = rx_char_control.next() => {
                match evt {
                    Some(CharacteristicControlEvent::Write(req)) => {
                        if reader_opt1.is_none() {
                            read_buf1 = vec![0; req.mtu()];
                            reader_opt1 = Some(req.accept()?);
                        } else {
                            read_buf2 = vec![0; req.mtu()];
                            reader_opt2 = Some(req.accept()?);
                        }
                    },
                    Some(CharacteristicControlEvent::Notify(_)) => break,
                    None => break,
                }
            },
            evt = tx_char_control.next() => {
                match evt {
                    Some(CharacteristicControlEvent::Notify(notifier)) => {
                        if writer_opt1.is_none() {
                            writer_opt1 = Some(notifier);
                        } else {
                            writer_opt2 = Some(notifier);
                        }
                    },
                    _ => break,
                }
            },
            read_res = async {
                match &mut reader_opt1 {
                    Some(reader) if writer_opt1.is_some() => reader.read(&mut read_buf1).await,
                    _ => future::pending().await,
                }
            } => {
                match read_res {
                    Ok(0) => {
                        println!("Read stream ended");
                        reader_opt1 = None;
                    }
                    Ok(n) => {
                        let value = read_buf1[..n].to_vec();
                        println!("(1) Echoing {} bytes: {:x?} ... {:x?}", value.len(), &value[0..4.min(value.len())], &value[value.len().saturating_sub(4) ..]);
                        if value.len() < 512 {
                            println!();
                        }
                        if let Err(err) = writer_opt1.as_mut().unwrap().write_all(&value).await {
                            println!("Write failed: {}", &err);
                            writer_opt1 = None;
                        }
                    }
                    Err(err) => {
                        println!("Read stream error: {}", &err);
                        reader_opt1 = None;
                    }
                }
            }
            read_res = async {
                match &mut reader_opt2 {
                    Some(reader) if writer_opt2.is_some() => reader.read(&mut read_buf2).await,
                    _ => future::pending().await,
                }
            } => {
                match read_res {
                    Ok(0) => {
                        println!("Read stream ended");
                        reader_opt2 = None;
                    }
                    Ok(n) => {
                        let value = read_buf2[..n].to_vec();
                        println!("(2) Echoing {} bytes: {:x?} ... {:x?}", value.len(), &value[0..4.min(value.len())], &value[value.len().saturating_sub(4) ..]);
                        if value.len() < 512 {
                            println!();
                        }
                        if let Err(err) = writer_opt2.as_mut().unwrap().write_all(&value).await {
                            println!("Write failed: {}", &err);
                            writer_opt2 = None;
                        }
                    }
                    Err(err) => {
                        println!("Read stream error: {}", &err);
                        reader_opt2 = None;
                    }
                }
            }
        }
    }

    println!("Removing service and advertisement");
    drop(app_handle);
    drop(adv_handle);
    sleep(Duration::from_secs(1)).await;

    Ok(())
}

This was tested with BlueZ 5.55, and BlueR 0.15.5, running on a Raspberry Pi CM4.
Tested with two iPhones running NRF Connect.

I really do not think that the notifier is specific to a device. I have also tested the gatt_echo_server example with my two phones. And I am receiving the echo on both phones, whereas the example stores only one notifier (It should be the last one to subscribe shouldn't it ?).

@surban
Copy link
Collaborator

surban commented Jan 16, 2023

Your code looks fine to me.

Yes, I agree. It seems that BlueZ is using one notification session for all connected clients that have enabled notifications. However, the D-Bus API would indeed allow one notification session per client without changes to the API.

BlueR can do nothing about it. You will need to enhance bluetoothd to support this behavior. Since you won't have to modify the API the required changes should be pretty straightforward.

@surban surban added the external caused by bluetoothd or kernel label Jan 16, 2023
@surban surban changed the title Is BlueZ (hence BlueR) really capable of NUS ? GATT per-client notifications are currently unsupported Jan 16, 2023
@surban surban removed their assignment Sep 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
external caused by bluetoothd or kernel
Projects
None yet
Development

No branches or pull requests

2 participants