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

PeripheralManager.characteristicRead and GattCharacteristicReadEventArgs #45

Closed
ekuleshov opened this issue Jan 27, 2024 · 24 comments · Fixed by #74
Closed

PeripheralManager.characteristicRead and GattCharacteristicReadEventArgs #45

ekuleshov opened this issue Jan 27, 2024 · 24 comments · Fixed by #74
Labels
enhancement New feature or request

Comments

@ekuleshov
Copy link
Contributor

ekuleshov commented Jan 27, 2024

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
Copy link
Owner

yanshouwang commented Jan 28, 2024

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
Copy link
Contributor Author

@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
Copy link
Owner

yanshouwang commented Jan 28, 2024

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.

@yanshouwang yanshouwang added the enhancement New feature or request label Jan 28, 2024
@ekuleshov
Copy link
Contributor Author

...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
Copy link
Contributor Author

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
Copy link
Owner

@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
Copy link
Contributor Author

@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
Copy link
Owner

@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
Copy link
Owner

yanshouwang commented Jan 28, 2024

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
Copy link
Contributor Author

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
Copy link
Owner

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
Copy link
Contributor Author

ekuleshov commented Jan 28, 2024

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
Copy link
Owner

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
Copy link
Contributor Author

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

Copy link

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

@github-actions github-actions bot added the stale label Feb 28, 2024
Copy link

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

@yanshouwang
Copy link
Owner

yanshouwang commented May 15, 2024

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 yanshouwang reopened this May 15, 2024
@yanshouwang
Copy link
Owner

The 6.0.0-dev.0 has released.

@yanshouwang yanshouwang linked a pull request Jun 3, 2024 that will close this issue
@ekuleshov
Copy link
Contributor Author

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
Copy link
Owner

yanshouwang commented Jun 3, 2024

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
Copy link
Contributor Author

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
Copy link
Owner

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
Copy link
Contributor Author

#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
Copy link
Owner

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

yanshouwang pushed a commit that referenced this issue Jun 4, 2024
yanshouwang pushed a commit that referenced this issue Jun 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants