{"id":26078307,"url":"https://github.com/danielepantaleone/blueconnect","last_synced_at":"2025-04-11T23:51:30.550Z","repository":{"id":258532446,"uuid":"858288886","full_name":"danielepantaleone/BlueConnect","owner":"danielepantaleone","description":"A modern approach to Bluetooth LE connectivity built around CoreBluetooth ","archived":false,"fork":false,"pushed_at":"2025-04-07T08:51:04.000Z","size":4147,"stargazers_count":9,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-10T15:28:22.849Z","etag":null,"topics":["bluetooth","combine","ios","swift","swift-concurrency","xcode"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danielepantaleone.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-09-16T16:34:06.000Z","updated_at":"2025-04-07T08:19:52.000Z","dependencies_parsed_at":"2024-10-26T19:30:31.700Z","dependency_job_id":null,"html_url":"https://github.com/danielepantaleone/BlueConnect","commit_stats":null,"previous_names":["danielepantaleone/blueconnect"],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielepantaleone%2FBlueConnect","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielepantaleone%2FBlueConnect/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielepantaleone%2FBlueConnect/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielepantaleone%2FBlueConnect/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danielepantaleone","download_url":"https://codeload.github.com/danielepantaleone/BlueConnect/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248497890,"owners_count":21113984,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bluetooth","combine","ios","swift","swift-concurrency","xcode"],"created_at":"2025-03-09T03:57:44.845Z","updated_at":"2025-04-11T23:51:30.528Z","avatar_url":"https://github.com/danielepantaleone.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"![](https://github.com/danielepantaleone/BlueConnect/blob/master/Banner.png?raw=true)\n\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdanielepantaleone%2FBlueConnect%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/danielepantaleone/BlueConnect)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdanielepantaleone%2FBlueConnect%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/danielepantaleone/BlueConnect)\n![Cocoapods Version](https://img.shields.io/cocoapods/v/BlueConnect)\n![GitHub Release](https://img.shields.io/github/v/release/danielepantaleone/BlueConnect)\n![GitHub License](https://img.shields.io/github/license/danielepantaleone/BlueConnect)\n![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/danielepantaleone/BlueConnect/swift-tests.yml)\n\nBlueConnect is a Swift framework built on top of CoreBluetooth, designed to simplify interaction with Bluetooth Low Energy (BLE) peripherals. \nBy wrapping Core Bluetooth functionalities, BlueConnect provides a modern approach to BLE communication. \nIt leverages asynchronous programming models, allowing you to interact with peripherals using either traditional callbacks or Swift concurrency with async/await. \n\nAdditionally, BlueConnect supports event notifications through Combine publishers, offering a more streamlined and reactive way to handle BLE events. \nBy leveraging Swift protocols, BlueConnect also facilitates unit testing, making it easier to build testable libraries and apps that interact with BLE peripherals. \nThis combination of asynchronous communication, event-driven architecture, and testability ensures a highly flexible and modern BLE development experience.\n\n## Table of contents\n\n* [Feature highlights](#feature-highlights)\n* [Usage](#usage)\n    * [Scanning for peripherals](#scanning-for-peripherals)\n    * [Connecting a peripheral](#connecting-a-peripheral)\n    * [Disconnecting a peripheral](#disconnecting-a-peripheral)\n    * [Reading connected peripheral RSSI](#reading-connected-peripheral-rssi)\n    * [Reading a characteristic](#reading-a-characteristic)\n    * [Writing a characteristic](#writing-a-characteristic)\n    * [Enabling notify on a characteristic](#enabling-notify-on-a-characteristic)\n* [Providing unit tests in your codebase](#providing-unit-tests-in-your-codebase)\n* [Installation](#installation)\n    * [Cocoapods](#cocoapods)\n    * [Swift package manager](#swift-package-manager)\n* [Documentation](https://danielepantaleone.github.io/BlueConnect/documentation/blueconnect/)\n* [Contributing](#contributing)\n* [License](#license)\n\n## Feature Highlights\n\n- [x] Supports both iOS and macOS.\n- [x] Completely covered by unit tests.\n- [x] Replaces the delegate-based interface of **CBCentralManager**, **CBPeripheralManager** and **CBPeripheral** with closures and Swift concurrency (async/await).\n- [x] Delivers event notifications via Combine publishers for **CBCentralManager**, **CBPeripheralManager** and **CBPeripheral**.\n- [x] Includes connection timeout handling for **CBPeripheral**.\n- [x] Includes characteristic operations timeout handling for **CBPeripheral** (discovery, read, write, set notify).\n- [x] Provides direct interaction with **CBPeripheral** characteristics with no need to manage **CBPeripheral** data.\n- [x] Provides an optional cache policy for **CBPeripheral** data retrieval, ideal for scenarios where characteristic data remains static over time.\n- [x] Provides automatic service/characteristic discovery when characteristic operations are requested (read, write, set notify).\n- [x] Provides notification via Combine publisher when advertising is stopped on the **CBPeripheralManager**.\n- [x] Correct routing of **CBCentralManager** disconnection events towards connection failure publisher and callbacks if the connection didn't happen at all.\n- [x] Facilitates unit testing by supporting BLE central and peripheral mocks, enabling easier testing for libraries and apps that interact with BLE peripherals.\n\n## Usage\n\nBlueConnect delegates its functionality to several proxies:\n\n- **BleCentralManagerProxy**: A wrapper around **CBCentralManager**, responsible for connecting, disconnecting, and \nscanning for peripherals. \n- **BlePeripheralManagerProxy**: A wrapper around **CBPeripheralManager**, responsible for advertising BLE services, \nmanaging local services and characteristics, and reacting to BLE centrals requests. \nIt publishes events using both asynchronous methods (via callbacks or Swift concurrency) and Combine publishers.\n- **BlePeripheralProxy**: A wrapper around **CBPeripheral** that handles communication with BLE peripherals and manages \ndata transmission. \nLike the central manager proxy, it publishes events through asynchronous methods and Combine publishers.\n\nSince communication with BLE peripherals requires encoding and decoding raw data, BlueConnect simplifies this \ninteraction by offering various proxy protocols that wrap around **BlePeripheralProxy**. You can create custom proxies \nby conforming to these protocols, enabling you to perform operations like reading, writing, and enabling notifications \non BLE peripheral characteristics:\n\n- **BleCharacteristicProxy**: The base proxy for discovering characteristics.\n- **BleCharacteristicReadProxy**: A proxy for reading data from a characteristic.\n- **BleCharacteristicWriteProxy**: A proxy for writing data to a characteristic.\n- **BleCharacteristicWriteWithoutResponseProxy**: A proxy for writing data to a characteristic without awaiting a \nresponse from the BLE peripheral.\n- **BleCharacteristicNotifyProxy**: A proxy for enabling notifications on a characteristic.\n\n### Scanning for peripherals\n\nYou can start scanning for BLE peripherals by calling `scanForPeripherals` on the **BleCentralManagerProxy**. \nThis method allows you to provide BLE scan options, which are passed directly to the underlying **CBCentralManager**. \nYou can also specify an optional timeout (defaulting to 60 seconds if not provided). \nThe method returns a publisher that you can use to listen for discovered BLE peripherals, along with completion or \nfailure events.\n\n```swift\nimport BlueConnect\nimport Combine\nimport CoreBluetooth\n\nvar subscriptions: Set\u003cAnyCancellable\u003e = []\nlet centralManagerProxy = BleCentralManagerProxy()\n\ndo {\n    try await centralManagerProxy.waitUntilReady()\n    centralManagerProxy.scanForPeripherals(timeout: .seconds(30))\n        .receive(on: DispatchQueue.main)\n        .sink(\n            receiveCompletion: { completion in\n                // This is called when the peripheral scan is completed or upon scan failure.\n                switch completion {\n                    case .finished:\n                        print(\"peripheral scan completed successfully\")\n                    case .failure(let error):\n                        print(\"peripheral scan terminated with error: \\(error)\")\n                }\n            },\n            receiveValue: { peripheral, advertisementData, RSSI in \n                // This is called multiple times for every discovered peripheral.\n                print(\"peripheral '\\(peripheral.identifier)' was discovered\")\n            }\n        )\n        .store(in: \u0026subscriptions)\n} catch {\n    print(\"peripheral scan failed with error: \\(error)\")\n}\n```\n\nThe peripheral scan will automatically stop if a timeout is specified. However, you can also manually stop the scan at \nany time by calling `stopScan` on the **BleCentralManagerProxy**.\n\n### Connecting a peripheral\n\nTo connect to a BLE peripheral, use the `connect` method on the **BleCentralManagerProxy**. \nYou can provide connection options that will be forwarded to the underlying **CBCentralManager**. \nAdditionally, you have the option to specify a timeout (defaulting to no timeout if not provided).\nThe establishment of the connection will also be notified through the Combine publishers, allowing you \nto react to the connection status.\n\n```swift\nimport BlueConnect\nimport Combine\nimport CoreBluetooth\n\nvar subscriptions: Set\u003cAnyCancellable\u003e = []\nlet centralManagerProxy = BleCentralManagerProxy()\n\n// You can optionally subscribe a publisher to be notified when a connection is established.\ncentralManagerProxy.didConnectPublisher\n    .receive(on: DispatchQueue.main)\n    .sink { peripheral in \n        print(\"peripheral '\\(peripheral.identifier)' connected\")\n    }\n    .store(in: \u0026subscriptions)\n\n// You can optionally subscribe a publisher to be notified when a connection attempt fails.\ncentralManagerProxy.didFailToConnectPublisher\n    .receive(on: DispatchQueue.main)\n    .sink { peripheral, error in \n        print(\"peripheral '\\(peripheral.identifier)' failed to connect with error: \\(error)\")\n    }\n    .store(in: \u0026subscriptions)\n\ndo {\n    // The following will try to establish a connection to a BLE peripheral for at most 60 seconds.\n    // If the connection cannot be established within the specified amount of time, the connection \n    // attempt is dropped and notified by raising an appropriate error.\n    try await centralManagerProxy.waitUntilReady()\n    try await centralManagerProxy.connect(\n        peripheral: peripheral,\n        options: nil,\n        timeout: .seconds(60))\n    print(\"peripheral '\\(peripheral.identifier)' connected\")\n} catch {\n    print(\"peripheral connection failed with error: \\(error)\")\n}\n```\n\n### Disconnecting a peripheral\n\nTo disconnect a connected BLE peripheral, use the `disconnect` method on the **BleCentralManagerProxy**. The \ndisconnection event will be notified through the Combine publisher, enabling you to respond to changes in the \nconnection status.\n\n```swift\nimport BlueConnect\nimport Combine\nimport CoreBluetooth\n\nvar subscriptions: Set\u003cAnyCancellable\u003e = []\nlet centralManagerProxy = BleCentralManagerProxy()\n\n// You can optionally subscribe a publisher to be notified when a peripheral is disconnected.\ncentralManagerProxy.didDisconnectPublisher\n    .receive(on: DispatchQueue.main)\n    .sink { peripheral in \n        print(\"peripheral '\\(peripheral.identifier)' disconnected\")\n    }\n    .store(in: \u0026subscriptions)\n\ndo {\n    // The following will disconnect a BLE peripheral.\n    try await centralManagerProxy.waitUntilReady()\n    try await centralManagerProxy.disconnect(peripheral: peripheral)\n    print(\"peripheral '\\(peripheral.identifier)' disconnected\")\n} catch {\n    print(\"peripheral disconnection failed with error: \\(error)\")\n}\n```\n\n### Reading connected peripheral RSSI\n\nTo read connected peripheral RSSI you can use the `readRSSI` method of the `BlePeripheralProxy`.\n\n```swift\nimport BlueConnect\nimport Combine\nimport CoreBluetooth\n\nvar subscriptions: Set\u003cAnyCancellable\u003e = []\nlet peripheralProxy = BlePeripheralProxy(peripheral: peripheral)\n\n// You can optionally subscribe a publisher to be triggered when the RSSI value is read.\nperipheralProxy.didUpdateRSSIPublisher\n    .receive(on: DispatchQueue.main)\n    .sink { value in \n        print(\"RSSI: \\(value)\")\n    }\n    .store(in: \u0026subscriptions)\n\ndo {\n    // The following will read the RSSI value from a connected peripheral.\n    let value = try await peripheralProxy.readRSSI(timeout: .seconds(10))\n    print(\"RSSI: \\(value)\")\n} catch {\n    print(\"failed to read peripheral RSSI with error: \\(error)\")\n}\n```\n\n### Reading a characteristic\n\nTo read a characteristic, you can create your own proxy by conforming to the **BleCharacteristicReadProxy** protocol, \nwhich provides the necessary functionality for reading data from a characteristic.\n\n```swift\nimport BlueConnect\nimport Combine\nimport CoreBluetooth\n\n// Declare your type conforming to the BleCharacteristicReadProxy protocol.\nstruct SerialNumberProxy: BleCharacteristicReadProxy {\n    \n    typealias ValueType = String\n    \n    let characteristicUUID: CBUUID = CBUUID(string: \"2A25\")\n    let serviceUUID: CBUUID = CBUUID(string: \"180A\")\n\n    weak var peripheralProxy: BlePeripheralProxy?\n    \n    init(peripheralProxy: BlePeripheralProxy) {\n        self.peripheralProxy = peripheralProxy\n    }\n    \n    func decode(_ data: Data) throws -\u003e String {\n        return String(decoding: data, as: UTF8.self)\n    }\n        \n}\n\nvar subscriptions: Set\u003cAnyCancellable\u003e = []\nlet peripheralProxy = BlePeripheralProxy(peripheral: peripheral)\nlet serialNumberProxy = SerialNumberProxy(peripheralProxy: peripheralProxy)\n\n// You can optionally subscribe a publisher to be notified when data is read from the characteristic.\n// The publisher sink method won't be triggered when reading data from local cache.\nserialNumberProxy.didUpdateValuePublisher\n    .receive(on: DispatchQueue.main)\n    .sink { serialNumber in \n        print(\"serial number is \\(serialNumber)\")\n     }\n    .store(in: \u0026subscriptions)\n\ndo {\n    // The following will read the serial number of the characteristic.\n    // If the serial number characteristic, or the service backing the characteristic, has not been discovered yet, \n    // a silent discovery is performed before attempting to read data from the characteristic.\n    let serialNumber = try await serialNumberProxy.read(cachePolicy: .always, timeout: .seconds(10))\n    print(\"serial number is \\(serialNumber)\")\n} catch {\n    print(\"failed to read serial number with error: \\(error)\")\n}\n```\n\n### Writing a characteristic\n\nTo write a characteristic, you can create your own proxy by conforming to the **BleCharacteristicWriteProxy** protocol, \nwhich provides the necessary functionality for writing data to a characteristic.\n\n```swift\nimport BlueConnect\nimport Combine\nimport CoreBluetooth\n\n// Declare your type conforming to the BleCharacteristicWriteProxy protocol.\nstruct PinProxy: BleCharacteristicWriteProxy {\n    \n    typealias ValueType = String\n    \n    let characteristicUUID: CBUUID = CBUUID(string: \"5A8F2E01-58D9-4B0B-83B8-843402E49293\")\n    let serviceUUID: CBUUID = CBUUID(string: \"C5405A74-7C07-4702-A631-9D5EBF007DAE\")\n\n    weak var peripheralProxy: BlePeripheralProxy?\n    \n    init(peripheralProxy: BlePeripheralProxy) {\n        self.peripheralProxy = peripheralProxy\n    }\n    \n    func encode(_ value: String) throws -\u003e Data {\n        return Data(value.utf8)\n    }\n        \n}\n\nvar subscriptions: Set\u003cAnyCancellable\u003e = []\nlet peripheralProxy = BlePeripheralProxy(peripheral: peripheral)\nlet pinProxy = PinProxy(peripheralProxy: peripheralProxy)\n\n// You can optionally subscribe a publisher to be notified when data is written to the characteristic.\npinProxy.didWriteValuePublisher\n    .receive(on: DispatchQueue.main)\n    .sink {  \n        print(\"data was written to the characteristic\")\n     }\n    .store(in: \u0026subscriptions)\n\ndo {\n    // The following will write the PIN to the PIN characteristic.\n    // If the PIN characteristic, or the service backing the PIN characteristic, has not been discovered yet, \n    // a silent discovery is performed before attempting to write data to the characteristic.\n    try await pinProxy.write(value: \"1234\", timeout: .seconds(10))\n    print(\"data was written to the characteristic\")\n} catch {\n    print(\"failed to write data to the characteristic with error: \\(error)\")\n}\n```\n\n### Enabling notify on a characteristic\n\nTo be notified when characteristic data is updated, you can create your own proxy by conforming to the \n**BleCharacteristicNotifyProxy** and **BleCharacteristicReadProxy** protocols. The **BleCharacteristicNotifyProxy**\nprovides the necessary functionality to enable data notify on the characteristic while the **BleCharacteristicReadProxy** provides the necessary functionality for receiving data from a characteristic.\n\n```swift\nimport BlueConnect\nimport Combine\nimport CoreBluetooth\n\n// Declare your type conforming to the BleCharacteristicNotifyProxy and BleCharacteristicReadProxy protocols.\n// You can omit BleCharacteristicReadProxy if you are not interested in receiving characteristic data and you just want\n// to toggle the notification status for a characteristic.\nstruct HeartRateProxy: BleCharacteristicReadProxy, BleCharacteristicNotifyProxy {\n    \n    typealias ValueType = Int\n    \n    let characteristicUUID: CBUUID = CBUUID(string: \"2A37\")\n    let serviceUUID: CBUUID = CBUUID(string: \"180D\")\n\n    weak var peripheralProxy: BlePeripheralProxy?\n    \n    init(peripheralProxy: BlePeripheralProxy) {\n        self.peripheralProxy = peripheralProxy\n    }\n    \n    func decode(_ data: Data) throws -\u003e Int {\n        return Int(data.first ?? 0x00)\n    }\n        \n}\n\nvar subscriptions: Set\u003cAnyCancellable\u003e = []\nlet peripheralProxy = BlePeripheralProxy(peripheral: peripheral)\nlet heartRateProxy = HeartRateProxy(peripheralProxy: peripheralProxy)\n\n// You can optionally subscribe a publisher to be triggered when the notify flag is changed.\nheartRateProxy.didUpdateNotificationStatePublisher\n    .receive(on: DispatchQueue.main)\n    .sink { enabled in \n        print(\"notification enabled: \\(enabled)\")\n    }\n    .store(in: \u0026subscriptions)\n\n// You can optionally subscribe a publisher to be notified when data is received from the characteristic.\nheartRateProxy.didUpdateValuePublisher\n    .receive(on: DispatchQueue.main)\n    .sink { heartRate in \n        print(\"heart rate is \\(heartRate)\")\n     }\n    .store(in: \u0026subscriptions)\n\ndo {\n    // The following will enable data notify on the Heart Rate characteristic\n    // If the Heart Rate characteristic, or the service backing the Heart Rate characteristic, has not \n    // been discovered yet, a silent discovery is performed before attempting to enable data notify on the\n    // characteristic.\n    try await heartRateProxy.setNotify(enabled: true, timeout: .seconds(10))\n    print(\"notify enabled on the characteristic\")\n} catch {\n    print(\"failed to enable notify on the characteristic with error: \\(error)\")\n}\n```\n\n## Providing unit tests in your codebase\n\nBy leveraging the power of **BleCentralManagerProxy**, **BlePeripheralManagerProxy** and **BlePeripheralProxy**, you can easily create mocks for your codebase, allowing you to run unit tests in a controlled environment. \nThis is made possible because **BleCentralManagerProxy**, **BlePeripheralManagerProxy** and **BlePeripheralProxy** rely on protocols during initialization:\n\n- **BleCentralManager**: A protocol that defines all public methods of **CBCentralManager**. **CBCentralManager** itself conforms to this protocol.\n- **BlePeripheralManager**: A protocol that defines all public methods of **CBPeripheralManager**. **CBPeripheralManager** itself conforms to this protocol.\n- **BlePeripheral**: A protocol that defines all public methods of **CBPeripheral**. **CBPeripheral** itself conforms to this protocol.\n\nYou can create mock versions of your central manager and peripheral(s) and supply them during the initialization of \n**BleCentralManagerProxy**, **BlePeripheralManagerProxy** and **BlePeripheralProxy**. \nThis can be easily achieved by using a dependency injection (DI) container such as [Factory](https://github.com/hmlongco/Factory?tab=readme-ov-file#mocking).\n\n- An example of a mocked central manager can be found [here](https://github.com/danielepantaleone/BlueConnect/blob/master/Tests/BlueConnectTests/CentralManager/MockBleCentralManager.swift).\n- An example of a mocked peripheral manager can be found [here](https://github.com/danielepantaleone/BlueConnect/blob/master/Tests/BlueConnectTests/PeripheralManager/MockBlePeripheralManager.swift).\n- An example of a mocked peripheral can be found [here](https://github.com/danielepantaleone/BlueConnect/blob/master/Tests/BlueConnectTests/Peripheral/MockBlePeripheral.swift).\n\n## Installation\n\n### Cocoapods\n\n```ruby\npod 'BlueConnect', '~\u003e 1.3.5'\n```\n\n### Swift Package Manager\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/danielepantaleone/BlueConnect.git\", .upToNextMajor(from: \"1.3.5\"))\n]\n```\n\n## Contributing\n\nIf you like this project, you can contribute by:\n\n- Submitting a bug report via an [issue](https://github.com/danielepantaleone/BlueConnect/issues)\n- Contributing code through a [pull request](https://github.com/danielepantaleone/BlueConnect/pulls)\n\n## License\n\n```\nMIT License\n\nCopyright (c) 2025 Daniele Pantaleone\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielepantaleone%2Fblueconnect","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanielepantaleone%2Fblueconnect","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielepantaleone%2Fblueconnect/lists"}