-
Notifications
You must be signed in to change notification settings - Fork 27
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
[Draft, for discussion] Introduce async handler for reading/writing attr data #133
Conversation
I've done an extra change by introducing a new, extra "mega callback" called The major difference between As such, (We might/should make There's currently the inconvenience, that - in the inversion of control code - I have difficulties converting the
|
I like the inversion of control version. I think it's useful to model bluetooth as streams of events, since that matches well with the underlying HCI protocol. Rust also tends to work well with inverted control, since it can often make lifetimes more tractable. I question whether This may require some API changes. For example, you probably don't want to pass a Similarly, in the inversion of control version, it would be nice if |
It might require very deep changes to the For one, I don't see how such an approach can be layered "on top" of the "mega callback" idea, which - for one - uses an owned singleton (
Ditto, as per above. Maybe we can just choose an extreme?
We can also do Extreme2 + Extreme3 as they seem orthogonal to each other. Not 100% sure.
I understand, but this is a small issue to solve compared with the rest, I hope. |
I believe this is aligned with what we're hoping to achieve building on top of #131. For the purposes of that PR we wanted to make as little change to the host as possible, so characteristic data is allocated statically and passed to the AttributeTable. I'm going to try to put together a POC for this - maybe we can look at these two initiatives side-by-side so we can make sure we're aligned on direction? |
I think there are a few general options for the API: First, a synchronous callback that uses a reply object that can be sent to another task for asynchronous handling: fn on_read(&mut self, conn: Connection, attr: Handle, reply: ReadReply);
fn on_write(&mut self, conn: Connection, attr: Handle, data: &[u8], reply: WriteReply); Where fn reply(self, result: Result<&[u8], AttError>); This is what Second, an asynchronous callback: async fn on_read(&self, conn: Connection, attr: Handle, writer: W) -> Result<(), AttError>;
async fn on_write(&self, conn: Connection, attr: Handle, data: &[u8]) -> Result<(), AttError>;
The main issues I see with this version are that Third, an asynchronous callback but with an API like the synchronous callbacks: async fn on_read(&mut self, conn: Connection, attr: Handle, reply: ReadReply);
async fn on_write(&mut self, conn: Connection, attr: Handle, data: &[u8], reply: WriteReply); The advantage of this would be if you just want to do some simple asynchronous logic (like writing to flash or something) and don't care about handling events concurrently you could do it inline. However if you want concurrency, you could send the Fourth, an asynchronous iterator/inversion of control/event based API: async fn next(&mut self) -> GattEvent;
enum GattEvent<'a> {
Read(ReadEvent)
Write(WriteEvent<'a>)
}
impl ReadEvent {
fn reply(self, result: Result<&[u8], AttError>);
}
struct WriteEvent<'a> {
data: &'a [u8]
reply: WriteReply
}
impl<'a> WriteEvent<'a> {
fn into_reply(self) -> WriteReply;
} Converting a I think that any of those options could work, though I would tend to prefer either the third or fourth. I don't think there's a need to support both callback and inversion of control APIs, I think it would be better to pick one and stick to it. |
Thanks for putting a lot of good ideas on the table everyone. My preference are the inversion of control APIs, as I think they are a better/natural fit for async. I don't have any strong opinions on how it's implemented or which exactly which of those variants are chosen, but I would say all options are on the table, and that changing existing APIs are completely fine at this stage. I think the API should be usable without relying on macros, but the option of using macros to simplify the lifetime and generics (i.e. by having macros always using statics and some generic defaults) of the types. |
Disclaimer: this code only type-checks. I've not run it yet. Tests and examples are not fixed and will fail to build.
There might be logical bugs too!
But at least we have some ground for discussion which way we should go.
I'll split the elaboration of the changes into two aspects:
(Easy) Externalize attribute read/writes (and possibly other operations in future) with a user-supplied callback
This change consists of:
'd
lifetime fromAttributeData<'d>
as it no longer serves as attribute data storage (modulo CCCD data and service data), and all the way up the call chain.AttrHandler
(let's put aside the name of this thing for a while) which serves as the user-supplied "mega-callback"async
to methods up the call chain. It also slightly changed the attributes' iteration metaphorGattServer::next
is renamed toGattServer::process
, asGattEvent
is retired. Now that we have a callback, I'm not so sure if and how much a set of "post-factum" events might be useful. If we want the user to stay informed about attributes' processing (or other stuff) which happens without her intervention, we can always extendAttrHandler
with additional, "FYI" methods. Or provide aGattHandler: AttrHandler
trait for GATT-specific notifications.GattEvent
is actually restored with the following, second change described below, but more on that later(A bit more controversial) Inversion of control
There is a new method,
GattServer::split
and a new module,split
(let's put the module name aside for a while, I just wanted to externalize this brand-new code in a separate file for easier review) that tries to outline a processing metaphor different to the "mega callback" one. With that said, it is currently implemented on top and with the help of the "mega callback" one. And relies on the fact that the "mega callback" is async, and can await.It implements something called
GattEvents::next
which does return aGattEvent
instance which pretends to mutably borrow fromGattEvents
to enforce sequential processing of each event. The event is no longer "FYI", but contains either an attribute read, or an attribute write payload that the user has to process (not that we can enforce that). If the user just drops theGattEvent
instance, 0-len data will be returned to the peer, or the incoming attribute data would not be saved in the user's attribute data storage.It relies on something called
ExchangeArea
(can be implemented in many different ways), which is just a rendezvous point, where a hidden "mega callback" (ExchangeArea
itself) pushes the data for an attribute read or write and awaits for the user to process it via pulling events fromGattEvents
.Can this be implemented without the hidden "mega callback" and without paying the price of having
ExchangeData
and its extra synchronization around?Maybe, but that might mean much bigger changes to the Gatt server.