GithubHelp home page GithubHelp logo

manolofdez / asyncbluetooth Goto Github PK

View Code? Open in Web Editor NEW
153.0 10.0 28.0 123 KB

A small library that adds concurrency to CoreBluetooth APIs.

License: MIT License

Swift 100.00%
bluetooth corebluetooth ios swift

asyncbluetooth's Introduction

AsyncBluetooth

A small library that adds concurrency to CoreBluetooth APIs.

Features

  • Async/Await APIs
  • Queueing of commands
  • Data conversion to common types
  • Thread safety
  • Convenience APIs for reading/writing without needing to explicitly discover characteristics.
  • Convenience API for waiting until Bluetooth is ready.

Usage

Scanning for a peripheral

Start scanning by calling the central manager's scanForPeripherals function. It returns an AsyncStream you can use to iterate over the discovered peripherals. Once you're satisfied with your scan, you can break from the loop and stop scanning.

let centralManager = CentralManager()

try await centralManager.waitUntilReady()

let scanDataStream = try await centralManager.scanForPeripherals(withServices: nil)
for await scanData in scanDataStream {
    // Check scan data...
}

await centralManager.stopScan()

Connecting to a peripheral

Once you have your peripheral, you can use the central manager to connect to it. Note you must hold a reference to the Peripheral while it's connected.

try await centralManager.connect(peripheral, options: nil)

Subscribe to central manager events

The central manager publishes several events. You can subscribe to them by using the eventPublisher.

centralManager.eventPublisher
    .sink {
        switch $0 {
        case .didConnectPeripheral(let peripheral):
            print("Connected to \(peripheral.identifier)")
        default:
            break
        }
    }
    .store(in: &cancellables)

See CentralManagerEvent to see available events.

Read value from characteristic

You can use convenience functions for reading characteristics. They will find the characteristic by using a UUID, and parse the data into the appropriate type.

let value: String? = try await peripheral.readValue(
    forCharacteristicWithUUID: UUID(uuidString: "")!,
    ofServiceWithUUID: UUID(uuidString: "")!
)

Write value to characteristic

Similar to reading, we have convenience functions for writing to characteristics.

try await peripheral.writeValue(
    value,
    forCharacteristicWithUUID: UUID(uuidString: "")!,
    ofServiceWithUUID: UUID(uuidString: "")!
)

Subscribe to a characteristic

To get notified when a characteristic's value is updated, we provide a publisher you can subscribe to:

let characteristicUUID = CBUUID()
peripheral.characteristicValueUpdatedPublisher
    .filter { $0.uuid == characteristicUUID }
    .map { try? $0.parsedValue() as String? } // replace `String?` with your type
    .sink { value in
        print("Value updated to '\(value)'")
    }
    .store(in: &cancellables)

Remember that you should enable notifications on that characteristic to receive updated values.

try await peripheral.setNotifyValue(true, characteristicUUID, serviceUUID)

Canceling operations

To cancel a specific operation, you can wrap your call in a Task:

let fetchTask = Task {
    do {
        return try await peripheral.readValue(
            forCharacteristicWithUUID: UUID(uuidString: "")!,
            ofServiceWithUUID: UUID(uuidString: "")!
        )
    } catch {
        return ""
    }
}

fetchTask.cancel()

There might also be cases were you want to stop awaiting for all responses. For example, when bluetooth has been powered off. This can be done like so:

centralManager.eventPublisher
    .sink {
        switch $0 {
        case .didUpdateState(let state):
            guard state == .poweredOff else {
                return
            }
            centralManager.cancelAllOperations()
            peripheral.cancelAllOperations()
        default:
            break
        }
    }
    .store(in: &cancellables)

Logging

The library uses os.log to provide logging for several operations. These logs are enabled by default. If you wish to disable them, you can do:

AsyncBluetoothLogging.isEnabled = false

Examples

You can find practical, tasty recipes for how to use AsyncBluetooth in the AsyncBluetooth Cookbook.

Installation

Swift Package Manager

This library can be installed using the Swift Package Manager by adding it to your Package Dependencies.

Requirements

  • iOS 14.0+
  • MacOS 11.0+
  • Swift 5
  • Xcode 13.2.1+

License

Licensed under MIT license.

asyncbluetooth's People

Contributors

bricklife avatar confusedvorlon avatar francescopedronomnys avatar ghislainfrs avatar jiftechnify avatar litso avatar manolofdez avatar mickeyl avatar nervus avatar nouun avatar oliverepper avatar schubter avatar shu223 avatar soltesza avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

asyncbluetooth's Issues

willRestoreState delegate function support

Would it be possible to add support for centralManager(_:willRestoreState:) ?

Currently there is no way to support state restoring because this is not available on the interface. This could work as a publisher.

Task Cancellation

Is Task Cancellation implemented? For me it looks like the manager continues to scan in the background even after I cancelled the task from which the scan was started.

Notify support

Does this library support subscribing to notifications? I can't find anywhere to attach a callback to execute on receiving a notify message. If so, could an example please be provided and placed on the "Usage" page in the readme for this library, as notifications aren't mentioned at all on that page or in the examples cookbook.

Noob question: Why is CentralManager eventPublisher not an AsyncStream?

I see that scanForPeripherals returns an AsyncStream while eventPublisher returns AnyPublisher which is part of the Combine framework. Both these data types represent an asynchronous stream of values.

Is there a reason for this difference? Are there differences in capabilities between these two data types?

Privacy manifest (PrivacyInfo.xcprivacy) missing

Hi, Apple announced that, starting May 1, they would start enforcing that all new apps and updates must declare approved reasons for using specific APIs in a privacy manifest, preventing uploads to TestFlight if the requirement is not met.

These requirements also apply to 3rd party SDK's: Upcoming third-party SDK requirements:

Starting May 1: You’ll need to include approved reasons for the listed APIs used by your app’s code to upload a new or updated app to App Store Connect. If you’re not using an API for an allowed reason, please find an alternative. And if you add a new third-party SDK that’s on the list of commonly used third-party SDKs, these API, privacy manifest, and signature requirements will apply to that SDK. Make sure to use a version of the SDK that includes its privacy manifest and note that signatures are also required when the SDK is added as a binary dependency.
This functionality is a step forward for all apps and we encourage all SDKs to adopt it to better support the apps that depend on them.

At this moment, since AsyncBluetooth is not included in the list of commonly used libraries, it is not necessary to include the PrivacyInfo.xcprivacy, but in the future it will be.

I suggest this utility that can help to create the manifest: iOS Privacy Manifest Maker.

Even if no data is collected from what I understand the manifest is mandatory and should be used the empty template:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSPrivacyCollectedDataTypes</key>
    <array/>
	<key>NSPrivacyAccessedAPITypes</key>
    <array/>
</dict>
</plist>

Crash when chaining operations in different Tasks

version: 1.8.2

Issue:

I have 2 methods: one to register to notifications on some characteristics, and the second to read init ble values.
Each method uses Task { do {} catch {} } blocks to send their ble command.
In that case, calling these two methods one after the other, leads to a crash in PeripheralContext.readCharacteristicValueExecutor:

Capture d’écran 2023-10-25 à 15 28 24

This crash does not occurs if I merge the two methods into one Task { do {} catch {} } block.

=======================================
Here is the sample code with crash:

init() {
     setupBindings()
     setupNotifications()
     readBleValues()
}

func setupBindings() {
    peripheral?.characteristicValueUpdatedPublisher
            .receive(on: RunLoop.main)
            .filter { $0.uuid.uuidString == Uuids.battery.uuidString }
            .sink { [weak self] characteristic in
                  ....
            }.store(in :&cancellables)
}

func  setupNotifications() {
  Task {
      do {
              try await self.peripheral?.setNotifyValue(true, forCharacteristicWithUUID: Uuids.battery,  ofServiceWithUUID: mainSrv)
      } catch {
          ...
     }
}


func  readBleValues() {
     Task {
         do {
               try await peripheral?.readValue(forCharacteristicWithUUID: <another uuid>, ofServiceWithUUID: mainSrv) ==> crashes
         } catch {
            ....
        }
}

=======================================
without crash:


init() {
     setupBindings()
     initNotificationsAndReadValues()
}

func setupBindings() {
      // no change, same as above
}

func  initNotificationsAndReadValues() {
  Task {
      do {
              try await self.peripheral?.setNotifyValue(true, forCharacteristicWithUUID: Uuids.battery,  ofServiceWithUUID: mainSrv)
              try await peripheral?.readValue(forCharacteristicWithUUID: <another uuid>, ofServiceWithUUID: mainSrv) ==> NO CRASHES
     } catch {
          ...
     }
}

Unable to connect to X because a connection attempt is already in progress after turning off/on bluetooth

Hello,

I'm facing this weird issue.
When a peripheral is in state where it's trying to connect, I disable bluetooth and enable it back(basically interrupting the process). The connection is then stuck and this message printed every time I manually try to reconnect:

[centralManager] Unable to connect to 86B377B8-97B1-F21A-7BB4-50C5D7F8943C because a connection attempt is already in progress

The problem is, it's not connecting even after 10 min.
The only way to get the device reconnected is to kill the app.

Here is my reconnection code:

        switch state {
        ....
        case .poweredOn:
            try await reconnect(identifier: <ID_FROM_USERDEFAULTS>)
        @unknown default: break
        }


    @discardableResult
    func reconnect(withIdentifier identifier: UUID) async throws -> Peripheral {
        let restoredPeripheral = try await retrievePeripheral(withIdentifier: identifier)
        try await manager.connect(restoredPeripheral)

        return restoredPeripheral
    }

    func retrievePeripheral(withIdentifier identifier: UUID) async throws -> Peripheral {
        try await manager.waitUntilReady()
        
        var restoredPeripheral: Peripheral

        if let peripheral = manager.retrieveConnectedPeripherals(withServices: [.init(nsuuid: identifier)]).first {
            restoredPeripheral = peripheral
        } else if let peripheral = manager.retrievePeripherals(withIdentifiers: [identifier]).first {
            restoredPeripheral = peripheral
        } else {
            throw DeviceManagerError.unableToRetrievePeripheral(identifier: identifier)
        }

        return restoredPeripheral
    }

I'm not sure if it's a bug, or if I'm doing something wrong. Can't seem to find a way to cancel any pending connections.

API for CBPeripheralManager

I see that there's no wrapper for CBPeripheralManager, are you working on it or need some help with that? I think that it may be very useful to also include it in this package.

setNotifyValue - How to correctly stop notifications?

Hello,

How do I stop the notifications correctly?
My current situation is that when I try to enable - disable - enable notifications, I then get duplicated values. More times I disable/enable, more duplicates I get. So basically if I disable/enable 5 times, I then get the same 5 values returned from the characteristicValueUpdatedPublisher at the same time.

Here is my code:

Start notifications:

            if let service = peripheral.discoveredServices?.first(where: { $0.uuid == Constants.Device.deviceUUID.cbuuid }) {
                try await peripheral.discoverCharacteristics([Constants.Device.sensorUUID.cbuuid], for: service)
                self.characteristic = service.discoveredCharacteristics?.first(where: {
                    $0.uuid == Constants.Device.sensorUUID.cbuuid
                })
                
                if let characteristic {
                    try await peripheral.setNotifyValue(true, for: characteristic)
                    trackingState = .trackingEnabled
                }
            } 
            
            for await characteristic in peripheral.characteristicValueUpdatedPublisher.values {
                guard characteristic.uuid == Constants.Device.sensorUUID.cbuuid else { return }
                let value: Int? = try? characteristic.parsedValue()
            }

Stop notifications:

        guard let peripheral = peripheral, let characteristic, characteristic.isNotifying == true else { return }

        do {
            try await peripheral.setNotifyValue(false, for: characteristic)
            trackingState = .trackingDisabled
        } catch {
            showErrorBanner(error)
        }

Note: I also tried try await peripheral.setNotifyValue(<#T##enabled: Bool##Bool#>, forCharacteristicWithUUID: <#T##UUID#>, ofServiceWithUUID: <#T##UUID#>).

Batching the connection requests

CoreBluetooth devices such as the iPhone or the iPad can only be connected to a certain amount of devices at the same time – I think the limitation is in the range of about 5-12.

If you continue to try connecting to more devices, unfortunate things can happen, i.e. the bluetooth daemon reset, timeouts, etc.

I wonder whether AsyncBluetooth could do anything to help circumventing this by allowing to specify a threshold number. Any more connection request would then either fail or halt until the number of connected devices has decreased again.

A concrete example… in my app I can't filter to scan for a specific service, since it's not standardized. Hence, I need to connect to each and every available device, do some tests and then decide whether the device is eligible for displaying or not. In theory this could mean tens or hundreds of connection attempts in parallel, which is a recipe for desaster.

Suggestion about CBConnectPeripheralOptionEnableAutoReconnect

Hello,
Apple has provided the option to auto-connect with the property CBConnectPeripheralOptionEnableAutoReconnect. However, I tried with your library with this property but I am getting an error "One or more parameters were invalid."

I guess we need to implement a delegate method as well to use this property.
Here is the documentation for it.
https://developer.apple.com/documentation/corebluetooth/cbconnectperipheraloptionenableautoreconnect?language=swift

Can you please include this in the library?

Thanks

How do you get the discover characteristic for service?

Not a bug.

Can you please show me how to get the discovered characteristic?

Thanks!

sample code below:

            if let services = peripheral.discoveredServices {
                for service in services {
                    if service.uuid.uuidString == myServiceUUID {
                        print("found service ", service.uuid.uuidString)
                        try await peripheral.discoverCharacteristics([characteristicUUIDTX], for: service)
                    }
                }

Notifications when a peripheral gets disconnected

I have successfully paired and connected to a given peripheral. After the device has been streaming data for a while, the user disconnected (or turned off) the device. How can my app get notified of events like these?.

Presumably, we need to forward the

func centralManager(
        _ central: CBCentralManager,  
           didDisconnectPeripheral peripheral: CBPeripheral, 
           error: Error?
          )

method from the Central Manager's delegate to a new "app delegate" we need to implement?

How to read characteristics with 16 bit UUID

Hello,
I have to read cahracterististic with UDID "0x2A20" for service with UUID "0x180A". However, the function you have provided is

public func readValue<Value>(
        forCharacteristicWithUUID characteristicUUID: UUID,
        ofServiceWithUUID serviceUUID: UUID
    ) async throws -> Value? where Value: PeripheralDataConvertible 

It takes UUID for characteristics and services but I can't pass them my UUIDs as they are 16 bit but this accepts only 128 bit.
However, we need to provide to corebluetooth CBUUID which also takes 16bit strings.

My question is how can I pass my 16 bit uuids to read values as UUID(uuidString:) returns nil if I pass 16 bit UUID in this uuidString parameter.

Please mark Characteristic init as `public`

Hi there!
I am really loving using this package for my BLE projects; however I have run into several cases where I would like to manually generate a Characteristic struct in my app code, but the init is internal since it is not marked public in the code.

I have so far hacked around this by cloning the repo and importing the code and manually modifying the code, but I would really like and appreciate if this was marked public in the package itself.

I am curious if there is a reason it is not marked public?

Thanks again for this amazing package!

Peripheral.setNotifyValue is not called when we write value manually

Hello,
Is there a way that setNotifyValue for a specific character is called when we change/ write the value manually?
It only gets called when the value is changed from the device.

If not, then please tell me how I can get this update.
The problem is that one characteristic is subscribed on many screens, when I write the value to that characteristic manually, all the places where that characteristic is subscribed, should be notified.

Thanks

[peripheral] Received UpdateValue result for characteristic without a continuation

Getting lots of "[peripheral] Received UpdateValue result for characteristic without a continuation" prints in console. This seems to slow down reads and writes, but eventually everything is working ok. The amount of prints seems to increase when disconnecting and connecting again to a device.

Has this something to do with queueing of commands? Is there are way to flush the queue?

“waitUntilReady” may have a bug.

“waitUntilReady” may have a bug.
Scenario:
If CentralManager is an inherent property of an Actor, when calling waitUntilReady of CentralManager, the state may be incorrect, causing it to wait indefinitely. My solution is to add a 1s delay before the wait to give CentralManager enough initialization time.

Also, if possible, can timeout logic be added to waitUntilReady or can Executor be added with timeout settings?

Feature request: Changing behaviour of the logger

Hi! We'd like to get more control over the logging of this library. Currently it posts a lot of logging in our console, that we'd like to more easily filter out. Furthermore, we'd like to remove the logging altogether in release builds for various reasons. Simply being able to disable logging through a boolean would be enough for our use cases, but I guess that giving the developer the ability to overwrite the logger could also work.

What's your opinion about this? Is this something you can add?

How do I read/write to a peripheral?

As far as I can tell, there are no examples for reading from and writing to a peripheral, and no documentation whatsoever for the writeValue method. I'm having trouble receiving data from my peripheral but I'm not sure if I'm even using the readValue method correctly.

Swift Playgrounds 4 on iPad cannot add AsyncBluetooth

I'd like to use your AsyncBluetooth with Swift Playgrounds 4 on iPad, but I cannot do that. It doesn't seem to be able to import any Swift Package released by tags with alphabetic characters, like v1.1.0, as follows:

IMG_0077

I was able to import my cloned repository re-tagged with 1.1.0 as follows:

IMG_0078

If there is no particular reason, could you use only numbers for tagging?

QUESTION: Why .eventPublisher starts scanning for peripherals.

Hello,
I have a question, why eventPublisher starts scanning for peripherals?

    override init() {
        super.init()

        manager.eventPublisher
            .sink { event in
                print(event)
            }
            .store(in: &cancellables)
    }

LOG:

[CoreBluetooth] API MISUSE: <CBCentralManager: 0x281c10000> has no restore identifier but the delegate implements the centralManager:willRestoreState: method. Restoring will not be supported
[centralManager] Waiting for bluetooth to be ready...
[centralManager] Scanning for peripherals...
[centralManager] Found peripheral 4F380BEB-6D9D-2551-5B23-CB72156C1537
[centralManager] Found peripheral 4F380BEB-6D9D-2551-5B23-CB72156C1537
[centralManager] Found peripheral E3B1A66E-E029-B311-B9CB-90827BD936CD
[centralManager] Found peripheral AC741473-26F7-5C3C-4D86-399F4DB0CDD9
[centralManager] Found peripheral AF1683EB-29A7-2477-0A0C-9CD73A5A9175
.....
[centralManager] Stopping scan...
[centralManager] Stopped scanning peripherals

IGNORE THAT - FORGOT TO REMOVE SOME TESTING CODE 🙄

Crash when switching off bluetooth while scan is ongoing

Version: from 1.7.0 (not tested earlier)

Steps to reproduce:

  • Launch a scan
  • on your device go to the settings to switch off bluetooth

result:

Thread 5: Fatal error: SWIFT TASK CONTINUATION MISUSE: scanForPeripherals(withServices:options:) tried to resume its continuation more than once, throwing operationCancelled!

in CentralManaager().scanForPeripherals(...) in the catch block, line 87

expected:

  • scan is "paused"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.