yanshouwang / bluetooth_low_energy

A Flutter plugin for controlling the bluetooth low energy.
https://pub.dev/packages/bluetooth_low_energy
MIT License
50 stars 16 forks source link

PeripheralManager.characteristicRead and GattCharacteristicReadEventArgs #45

Closed ekuleshov closed 5 months ago

ekuleshov commented 10 months ago

The PeripheralManager.characteristicRead stream spawns GattCharacteristicReadEventArgs events when connected "central" device is sending a read request to a certain characteristic.

But I can't figure out how to return an app-specific data in response to that read request.

There is PeripheralManager.writeCharacteristic() method which sends an updated characteristic value to one or more subscribed centrals, using a notification or indication. But that requires additional orchestration, e.g. have central subscribe for notifications and use some command from central (e.g. a write event) to initiate transmission.

It looks like the value is set in the GattCharacteristic at the time GattService.characteristics are created and then gatt service is registered using PeripheralManager.instance.addService(), but it is unclear how to change value in those chars because the GattCharacteristic has no setter for value, but even then it is too late to update value in those chars at the time characteristicRead event is triggered.

Perhaps you could add a value setter to the GattCharacteristic or better allow to specify a callback there that would allow to pull the up to date value from any custom source.

yanshouwang commented 10 months ago

This is by design.

I think developers should not care about how to response the read and write request because the sendRespone method is not easy to use, look at the Android and iOS document, the requestId, status and offset parameters made this method too complicated to be called, and the method is different on each platforms.

So I decide to send response by the plugin itself. if you want to change the characteristic's value, just call the writeCharacteristic method, this method will change the characteristc's value no matter it's subscribed or not, that means if you call readCharacteristic in the central side, you will receive the last value set by the writeCharacteristic method or the initial value if you didn't call this method, in this way, developers don't need to call the sendRespone method any more.

The characteristcRead event is just to tell developers that the characteristic is read by centrals, you can ignore this event if you don't care about this event.

In short, you just need to call writeCharacteristic method if you want to change the characteristic's value, central can receive that value by read or notify.

=========== EDIT if you want just change characteristic's value and don't want to notify other centrals, call the writeCharacteristic without the central parameter, if you want to notify others, you need to call this method with the central parameter one or more times(you can only notify one central each time).

ekuleshov commented 10 months ago

@yanshouwang I understand and appreciate the simplicity.

However there is an extra cost and additional complexity adds up for changing those char values from different data sources.

Take a battery service as an example. Instead of simply pulling the current battery level at the time battery char is read - we have to subscribe to battery level change and call writeCharacteristic() with updated values even if the battery value char will be never be read.

And when you have a few of such services - maintaining subscriptions to their value updates and updating chars up from gets costly.

So, allowing to provide a value callback on the GattCharacteristic and GattDescriptor would allow the app developer to provide custom data sources for all those values.

yanshouwang commented 10 months ago

If I add the sendResponse method, instead of listen custom data source, you need to listen the characteristicRead stream, and I really don't recommand developers to make this themself.

But if you really want to call the writeCharacteristic just when the characteristic is read, as the stream can't block the response, I think maybe I can provide a PeripheralManagerInterceptor mixin example to intercept the read event and do the writeCharacteristic at that time.

==== Edit Another way is make the characteristicRead stream to a callback so developers can intercept the read response, need to consider how to implement this with minimum change.

ekuleshov commented 10 months ago

...if you really want to call the writeCharacteristic...

That is not what I'm suggesting. Right now chars and descriptors are created like this, even if there are no values available:

GattCharacteristic(
    uuid: _charUuid,
    properties: [ GattCharacteristicProperty.read, GattCharacteristicProperty.notify ],
    value: Uint8List.fromList([]), // <--- char value
    descriptors: [
        GattDescriptor(uuid: _charUuid, value: Uint8List.fromList([])) // <--- descriptor value
    ],
  );

You could add an optional value provider parameters, so it would allow to hook up any custom values

GattCharacteristic(
    uuid: _charUuid,
    properties: [ GattCharacteristicProperty.read, GattCharacteristicProperty.notify ],
    provider: () async { // <--- char value provider
      ...this (potentially async) code will be called to get value when char is read
    }, 
    ...
ekuleshov commented 10 months ago

Another way is make the characteristicRead stream to a callback so developers can intercept the read response, need to consider how to implement this with minimum change.

That may work too. Internally you already transforming/truncating value before passing it to central.

Currently we have to listen like this

PeripheralManager.instance.characteristicRead.listen(_onCharRead);

Perhaps you could add another method that would return the same read stream but allow to provide custom values:

PeripheralManager.instance.characteristicReadMapped((GattCharacteristicReadEventArgs args) {
    return <mapped value>;
  }).listen(_onCharRead);
yanshouwang commented 10 months ago

@ekuleshov You can do this by override the GattCharacteristic class.

Just add bluetooth_low_energy_platform_interface dependency, and extend the MyGattCharacteristic class and override the value property, then you can create your own GattCharacteristic.

ekuleshov commented 10 months ago

@yanshouwang but isn't MyGattCharacteristic being instantiated by a various platform-specific implementations?

Also, having to depend in bluetooth_low_energy_platform_interface is not ideal for a regular plugin consumer app and going to be fragile and likely break when platform interface changes.

yanshouwang commented 10 months ago

@yanshouwang but isn't MyGattCharacteristic being instantiated by a various platform-specific implementations?

Also, having to depend in bluetooth_low_energy_platform_interface is not ideal for a regular plugin consumer app and going to be fragile and likely break when platform interface changes.

The MyGattCharacteristic doesn't have platform-specific implementations when used with PeripheralManager, When you create the GattCharacteristic, the factory constructor returns a new MyGattCharacteristic instance, so you can safely extends this class on the peripheral side.

Anyone can depend the platform_interface plugin or the platform plugin without concern, you can even provide you own PeripheralManager implementation in this way. The api is stable(no breaking changes) until the main version changed.

yanshouwang commented 10 months ago

Take a battery service as an example. Instead of simply pulling the current battery level at the time battery char is read - we have to subscribe to battery level change and call writeCharacteristic() with updated values even if the battery value char will be never be read.

And when you have a few of such services - maintaining subscriptions to their value updates and updating chars up from gets costly.

There is another thing to be noticed, if you read the battery level just when the central read, it will spend extra time to read the value from the battery plugin as this is an async function, so it's better to store the battery value directly in the characteristic itself, so the PeripheralManager can get the battery level and send response immediately.

Anyway, I can expose the MyGattCharacteristic from the platform_interface so you can extend it without depend on the platform_interface, I hide this class just because I don't want the characteristic's value to be modified directly, it should be read and write by the PeripheralManager class.

ekuleshov commented 10 months ago

I also don't really want to modify values. I would prefer to be able to specify a data provider.

I'm avare of the overhead of pulling char values. Though some use cases are harder to implement another way. E.g. think of a service that returns a counter how many times it been read, or a service that returns a random number for every call.

yanshouwang commented 10 months ago

I don't want to make breaking changes for this, In the current version, the read/write value is just stored in the characteristic itself as intended, what you want will break current API.

Also you can look at Apple's document about the descriptor value, there even doesn't have a descriptor read or write callback, the read and write descriptor response is handled by system, we don't even kown when the descriptor is read or write, we just maintain the descriptor's value when it's changed.

ekuleshov commented 10 months ago

I don't want to make breaking changes for this, In the current version, the read/write value is just stored in the characteristic itself as intended, what you want will break current API.

U understand about the breaking changes, though making a required property optional and adding additional optional properties is not a breaking change.

Also I see it mentioned there that you can call respond and provide a value in response to didReceiveRead call on CBPeripheralManagerDelegate.

Here are a few examples using that delegate API:

https://shinesolutions.com/2021/08/31/working-with-core-bluetooth/#:~:text=to%20our%20peripheral!-,However%2C%20in%20contrast%20to%20the%20central%2C%20all%20the%20peripheral%20has%20to,%7D,-Learning%20the%20hard

https://uynguyen.github.io/2018/02/21/Play-Central-And-Peripheral-Roles-With-CoreBluetooth/#:~:text=From%20the%20peripheral%20side%2C%20you%20will%20receive%20a%20read%20request%20inside%20the%20method

yanshouwang commented 10 months ago

I don't want to make breaking changes for this, In the current version, the read/write value is just stored in the characteristic itself as intended, what you want will break current API.

U understand about the breaking changes, though making a required property optional and adding additional optional properties is not a breaking change.

Also I see it mentioned there that you can call respond and provide a value in response to didReceiveRead call on CBPeripheralManagerDelegate.

Here are a few examples using that delegate API:

https://shinesolutions.com/2021/08/31/working-with-core-bluetooth/#:~:text=to%20our%20peripheral!-,However%2C%20in%20contrast%20to%20the%20central%2C%20all%20the%20peripheral%20has%20to,%7D,-Learning%20the%20hard

https://uynguyen.github.io/2018/02/21/Play-Central-And-Peripheral-Roles-With-CoreBluetooth/#:~:text=From%20the%20peripheral%20side%2C%20you%20will%20receive%20a%20read%20request%20inside%20the%20method

It's not so easy for me to do that...

Obviously we can respond characteristcs read and write requests, but what I mean is that we can't resond descriptors read and write requests on iOS platform, I want to keep the characteristic's read/write API the same as the descriptor's.

It's not just add something, It's a mechanism issue

ekuleshov commented 10 months ago

Understood. Hope you will consider adding support for this in the future.

github-actions[bot] commented 9 months ago

This issue is stale because it has been open for 30 days with no activity.

github-actions[bot] commented 8 months ago

This issue was closed because it has been inactive for 14 days since being marked as stale.

yanshouwang commented 6 months ago

New PeripheralManager API is designed with interface-6.0.0-dev.16

The new API contains GATTCharacteristic.mutable() and GATTCharacteristic.immutable factory methods, characteristicReadRequested and characteristicWriteRequested events and corresponding respond method.

I think the new API can resolve this issue.

yanshouwang commented 6 months ago

The 6.0.0-dev.0 has released.

ekuleshov commented 5 months ago

New PeripheralManager API is designed with interface-6.0.0-dev.16

The new API contains GATTCharacteristic.mutable() and GATTCharacteristic.immutable factory methods, characteristicReadRequested and characteristicWriteRequested events and corresponding respond method.

I think the new API can resolve this issue.

I'm struggling with converting my 5.x code to the new 6.x APIs.

I have a service/char that receives commands and the app need to respond to another service/char with the dynamically created data for a received command.

I can't figure out how to send a response to a different service when processing a PeripheralManager.characteristicWriteRequested() event.

Also the PeripheralManager.getState() method is not listed in the 6.x migration notes.

yanshouwang commented 5 months ago

I'm struggling with converting my 5.x code to the new 6.x APIs.

I have a service/char that receives commands and the app need to respond to another service/char with the dynamically created data for a received command.

I can't figure out how to send a response to a different service when processing a PeripheralManager.characteristicWriteRequested() event.

Also the PeripheralManager.getState() method is not listed in the 6.x migration notes.

You can't respond if the service is not read or written by remote devices. You must respond to a request.

The getState method just moved to state field.

ekuleshov commented 5 months ago

You can't respond if the service is not read or written by remote devices. You must respond to a request.

In 5.x API I simply used the PeripheralManager.writeCharacteristic() to send/notify on a different GATT char using the central instance from the incoming "write" request.

The getState method just moved to state field.

I know. Yet it is not in the migration notes.

yanshouwang commented 5 months ago

You can't respond if the service is not read or written by remote devices. You must respond to a request.

In 5.x API I simply used the PeripheralManager.writeCharacteristic() to send/notify on a different GATT char using the central instance from the incoming "write" request.

The getState method just moved to state field.

I know. Yet it is not in the migration notes.

You can use the notifyCharacteristic method to notify central device the characteristic changed when the central is notifying. And you must respond the read or write request when received a read or write request.

I'll add that to the migration doc later

ekuleshov commented 5 months ago

You can use the notifyCharacteristic method to notify central device the characteristic changed when the central is notifying. And you must respond the read or write request when received a read or write request.

Respond to read/write request how? The requesting device is getting UNKNOWN_GATT_ERROR 241 with 6.x peripheral.

yanshouwang commented 5 months ago

Respond to read/write request how? The requesting device is getting UNKNOWN_GATT_ERROR 241 with 6.x peripheral.

Use the respondReadReuqestWithValue or the respondWriteRequest method. There is a sample code in the migration doc. And you can run the example to see how it works