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

Is there a difference between readValue and subscribing to characteristUpdatePublisher when reading a single value from a characteristic? #64

Open
tylerjames opened this issue Nov 27, 2024 · 3 comments

Comments

@tylerjames
Copy link

tylerjames commented Nov 27, 2024

Sorry, this is more of a question and maybe a problem.

I have a scenario where I first write to a characteristic with a request. My firmware interprets the request and posts a response to the same characteristic. Some of our hardware is more memory constrained than others so sometimes the response will be a single message and sometimes it will be broken up into multiple messages.

When the peripheral posts multiple messages I can usually receive them all by subscribing to the characteristicUpdatePublisher but if it posts only a single message then the update publisher does not seem to receive it.

In the past, to combat this, I've had to add a readValue after my writeValue to see if the whole response has been posted to the characteristic. But I feel like this shouldn't really be necessary. Maybe the value is written before my line that awaits the response and then the response is missed?

Is this how things should be working and is this the right way to deal with it?

If it helps my general structure is like this:

do {
  let values = await characteristicValueUpdatedPublisher
    .filter { $0.characteristic.uuid == MyService.commandCharacteristicId }
    .compactMap {
      $0.value
    }
    .compactMap { String(data: $0, encoding: .utf8) }
    .buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest)
    .values
    .eraseToStream()
  
  try await setNotifyValue(
    true,
    forCharacteristicWithCBUUID: MyService.commandCharacteristicId,
    ofServiceWithCBUUID: MyService.serviceId
  )
  
  try await writeValue(
    request,
    forCharacteristicWithCBUUID: MyService.commandCharacteristicId,
    ofServiceWithCBUUID: MyService.serviceId
  )
  
  // It could just hang here if there is only one response
  let response = await gatherMultipartResponse(fromStream: values)
  

Thanks.

@nhannam
Copy link

nhannam commented Nov 30, 2024

In a project I'm working on I'm using a similar pattern (although I call setNotify before subscribing, and never do explicit reads) and don't seem to be losing notifications from the peripheral.

There's a couple of things I think are worth mentionig:

  • I have all the bluetooth interactions performed by an actor to ensure they are done one at a time and to offload work from the main actor, however I seemed to have problems with seeing unexpected responses to some requests. I ended up putting this down to interleaving of requests as a result of actor reentrancy. I've now placed all these requests behind an AsyncChannel and this seems to have cured my problems (although there's been other refactoring, so I can't say 100% that is what fixed it)
  • I still feel like there's a bit of a fudge in there because characteristicValueUpdatedPublisher is marked @mainactor whichreally isn't great when you are trying to do the work in the background. I've had to mark AsyncBluetooth as preconcurrency.

@tylerjames
Copy link
Author

Is it possible that the issue is due to how Combine works?

In my example above I've created the stream but I'm not actually subscribed to it until after I call writeValue?

My await gatherMultipartResponse(fromStream: values) contains a for await value in values line that I assume subscribes to the underlying publisher.

Is there maybe a better way to structure this sequence of calls so that I'm for sure subscribed to the publisher before the writeValue happens?

@manolofdez
Copy link
Owner

manolofdez commented Dec 19, 2024

Hi,

@tylerjames yes, any value sent before subscribing to characteristicValueUpdatedPublisher will be lost, and your example could potentially hang if the value was already sent. One thing you can do to get out of this is use async let before you write to the characteristic. This will continue execution until the value is read. Something like:

do {
  let values = await characteristicValueUpdatedPublisher
    .filter { $0.characteristic.uuid == MyService.commandCharacteristicId }
    .compactMap {
      $0.value
    }
    .compactMap { String(data: $0, encoding: .utf8) }
    .buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest)
    .values
    .eraseToStream()

  // This 👇🏼
  async let response = gatherMultipartResponse(fromStream: values)
  
  try await setNotifyValue(
    true,
    forCharacteristicWithCBUUID: MyService.commandCharacteristicId,
    ofServiceWithCBUUID: MyService.serviceId
  )
  
  try await writeValue(
    request,
    forCharacteristicWithCBUUID: MyService.commandCharacteristicId,
    ofServiceWithCBUUID: MyService.serviceId
  )
  
  // Use the response
  return await response

  // ...

Let me know if that works!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants