{"id":13348956,"url":"https://github.com/JuulLabs/kable","last_synced_at":"2025-03-12T08:31:33.435Z","repository":{"id":36954134,"uuid":"293227216","full_name":"JuulLabs/kable","owner":"JuulLabs","description":"Kotlin Asynchronous Bluetooth Low-Energy","archived":false,"fork":false,"pushed_at":"2024-10-24T17:20:47.000Z","size":1405,"stargazers_count":821,"open_issues_count":39,"forks_count":83,"subscribers_count":21,"default_branch":"main","last_synced_at":"2024-10-24T18:36:18.688Z","etag":null,"topics":["android","apple","bluetooth-low-energy","core-bluetooth","javascript","kotlin-coroutines","kotlin-multiplatform","web-bluetooth"],"latest_commit_sha":null,"homepage":"https://juullabs.github.io/kable","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JuulLabs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-09-06T07:38:51.000Z","updated_at":"2024-10-24T17:20:49.000Z","dependencies_parsed_at":"2023-09-26T20:55:44.513Z","dependency_job_id":"aa60ea22-b5fe-40db-829c-25edf9372c4d","html_url":"https://github.com/JuulLabs/kable","commit_stats":null,"previous_names":[],"tags_count":59,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JuulLabs%2Fkable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JuulLabs%2Fkable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JuulLabs%2Fkable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JuulLabs%2Fkable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JuulLabs","download_url":"https://codeload.github.com/JuulLabs/kable/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243184643,"owners_count":20250031,"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":["android","apple","bluetooth-low-energy","core-bluetooth","javascript","kotlin-coroutines","kotlin-multiplatform","web-bluetooth"],"created_at":"2024-07-29T20:02:03.968Z","updated_at":"2025-03-12T08:31:33.400Z","avatar_url":"https://github.com/JuulLabs.png","language":"Kotlin","readme":"![badge][badge-android]\n![badge][badge-ios]\n![badge][badge-js]\n![badge][badge-mac]\n[![Slack](https://img.shields.io/badge/Slack-%23juul--libraries-ECB22E.svg?logo=data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTQgNTQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMTkuNzEyLjEzM2E1LjM4MSA1LjM4MSAwIDAgMC01LjM3NiA1LjM4NyA1LjM4MSA1LjM4MSAwIDAgMCA1LjM3NiA1LjM4Nmg1LjM3NlY1LjUyQTUuMzgxIDUuMzgxIDAgMCAwIDE5LjcxMi4xMzNtMCAxNC4zNjVINS4zNzZBNS4zODEgNS4zODEgMCAwIDAgMCAxOS44ODRhNS4zODEgNS4zODEgMCAwIDAgNS4zNzYgNS4zODdoMTQuMzM2YTUuMzgxIDUuMzgxIDAgMCAwIDUuMzc2LTUuMzg3IDUuMzgxIDUuMzgxIDAgMCAwLTUuMzc2LTUuMzg2IiBmaWxsPSIjMzZDNUYwIi8+PHBhdGggZD0iTTUzLjc2IDE5Ljg4NGE1LjM4MSA1LjM4MSAwIDAgMC01LjM3Ni01LjM4NiA1LjM4MSA1LjM4MSAwIDAgMC01LjM3NiA1LjM4NnY1LjM4N2g1LjM3NmE1LjM4MSA1LjM4MSAwIDAgMCA1LjM3Ni01LjM4N20tMTQuMzM2IDBWNS41MkE1LjM4MSA1LjM4MSAwIDAgMCAzNC4wNDguMTMzYTUuMzgxIDUuMzgxIDAgMCAwLTUuMzc2IDUuMzg3djE0LjM2NGE1LjM4MSA1LjM4MSAwIDAgMCA1LjM3NiA1LjM4NyA1LjM4MSA1LjM4MSAwIDAgMCA1LjM3Ni01LjM4NyIgZmlsbD0iIzJFQjY3RCIvPjxwYXRoIGQ9Ik0zNC4wNDggNTRhNS4zODEgNS4zODEgMCAwIDAgNS4zNzYtNS4zODcgNS4zODEgNS4zODEgMCAwIDAtNS4zNzYtNS4zODZoLTUuMzc2djUuMzg2QTUuMzgxIDUuMzgxIDAgMCAwIDM0LjA0OCA1NG0wLTE0LjM2NWgxNC4zMzZhNS4zODEgNS4zODEgMCAwIDAgNS4zNzYtNS4zODYgNS4zODEgNS4zODEgMCAwIDAtNS4zNzYtNS4zODdIMzQuMDQ4YTUuMzgxIDUuMzgxIDAgMCAwLTUuMzc2IDUuMzg3IDUuMzgxIDUuMzgxIDAgMCAwIDUuMzc2IDUuMzg2IiBmaWxsPSIjRUNCMjJFIi8+PHBhdGggZD0iTTAgMzQuMjQ5YTUuMzgxIDUuMzgxIDAgMCAwIDUuMzc2IDUuMzg2IDUuMzgxIDUuMzgxIDAgMCAwIDUuMzc2LTUuMzg2di01LjM4N0g1LjM3NkE1LjM4MSA1LjM4MSAwIDAgMCAwIDM0LjI1bTE0LjMzNi0uMDAxdjE0LjM2NEE1LjM4MSA1LjM4MSAwIDAgMCAxOS43MTIgNTRhNS4zODEgNS4zODEgMCAwIDAgNS4zNzYtNS4zODdWMzQuMjVhNS4zODEgNS4zODEgMCAwIDAtNS4zNzYtNS4zODcgNS4zODEgNS4zODEgMCAwIDAtNS4zNzYgNS4zODciIGZpbGw9IiNFMDFFNUEiLz48L2c+PC9zdmc+\u0026labelColor=611f69)](https://kotlinlang.slack.com/messages/juul-libraries/)\n\n# Kable\n\n**K**otlin **A**synchronous **B**luetooth **L**ow **E**nergy provides a simple Coroutines-powered API for interacting\nwith Bluetooth Low Energy devices.\n\nUsage is demonstrated with the [SensorTag sample app].\n\n## UUIDs\n\nUUIDs (Universally Unique Identifiers) are used to uniquely identify various components of a\nBluetooth Low Energy device. The Bluetooth Base UUID (`00000000-0000-1000-8000-00805F9B34FB`) allows\nfor short form (16-bit or 32-bit) UUIDs which are reserved for standard, predefined components\n(e.g. 0x180D for \"Heart Rate Service\", or 0x2A37 for \"Heart Rate Measurement\").\n128-bit UUIDs outside of the Bluetooth Base UUID are typically used for custom applications.\n\nThe `Bluetooth.BaseUuid` is provided to simplify defining 16-bit or 32-bit UUIDs. Simply add (`+`)\na 16-bit or 32-bit UUID (in [`Int`] or [`Long`] form) to the Bluetooth Base UUID to get a \"full\"\n[`Uuid`] representation; for example:\n\n```kotlin\nval uuid16bit = 0x180D\nval heartRateServiceUuid = Bluetooth.BaseUuid + uuid16bit\nprintln(heartRateServiceUuid) // Output: 0000180d-0000-1000-8000-00805f9b34fb\n```\n\nWeb Bluetooth named UUIDs may also be used to acquire [`Uuid`]s via the following [`Uuid`] extension\nfunctions:\n\n- `Uuid.service(name: String)`\n- `Uuid.characteristic(name: String)`\n- `Uuid.descriptor(name: String)`\n\nFor example:\n\n```kotlin\nval heartRateServiceUuid = Uuid.service(\"heart_rate\")\nprintln(heartRateServiceUuid) // Output: 0000180d-0000-1000-8000-00805f9b34fb\n```\n\n\u003e [!NOTE]\n\u003e List of known UUID names can be found in [`Uuid.kt`](https://github.com/JuulLabs/kable/blob/main/kable-core/src/commonMain/kotlin/Uuid.kt).\n\nAdditional example shorthand notations:\n\n| Shorthand                         | Canonical UUID                         |\n|-----------------------------------|----------------------------------------|\n| `Bluetooth.BaseUuid + 0x180D`     | `0000180D-0000-1000-8000-00805F9B34FB` |\n| `Bluetooth.BaseUuid + 0x12345678` | `12345678-0000-1000-8000-00805F9B34FB` |\n| `Uuid.service(\"blood_pressure\")`  | `00001810-0000-1000-8000-00805F9B34FB` |\n| `Uuid.characteristic(\"altitude\")` | `00002AB3-0000-1000-8000-00805F9B34FB` |\n| `Uuid.descriptor(\"valid_range\")`  | `00002906-0000-1000-8000-00805F9B34FB` |\n\n## Scanning\n\nTo scan for nearby peripherals, the [`Scanner`] provides an [`advertisements`] [`Flow`] which is a stream of\n[`Advertisement`] objects representing advertisements seen from nearby peripherals. [`Advertisement`] objects contain\ninformation such as the peripheral's name and RSSI (signal strength).\n\nThe [`Scanner`] may be configured via the following DSL (shown are defaults, when not specified):\n\n```kotlin\nval scanner = Scanner {\n    filters {\n        match {\n            name = Filter.Name.Exact(\"My device\")\n        }\n    }\n    logging {\n        engine = SystemLogEngine\n        level = Warnings\n        format = Multiline\n    }\n}\n```\n\nScan results can be filtered by providing a list of [`Filter`]s via the `filters` DSL.\nThe following filters are supported:\n\n| Filter             |    Android    |     Apple     | JavaScript |\n|--------------------|:-------------:|:-------------:|:----------:|\n| `Service`          |       ✓       | ✓\u003csup\u003e2\u003c/sup\u003e |     ✓      |\n| `Name`             |       ✓       | ✓\u003csup\u003e1\u003c/sup\u003e |     ✓      |\n| `NamePrefix`       | ✓\u003csup\u003e1\u003c/sup\u003e | ✓\u003csup\u003e1\u003c/sup\u003e |     ✓      |\n| `Address`          |       ✓       |               |            |\n| `ManufacturerData` |       ✓       | ✓\u003csup\u003e1\u003c/sup\u003e |     ✓      |\n\n✓\u0026nbsp; Supported natively  \n✓\u003csup\u003e1\u003c/sup\u003e Support provided by Kable via flow filter  \n✓\u003csup\u003e2\u003c/sup\u003e Supported natively if the only filter type used, otherwise falls back to flow filter  \n\n\u003e [!TIP]\n\u003e When a filter is supported natively, the system will often be able to perform scan optimizations. If feasible, it is\n\u003e recommended to provide only `Filter.Service` filters (and at least one) — as it is natively supported on all platforms.\n\nWhen filters are specified, only [`Advertisement`]s that match at least one [`Filter`] will be emitted. For example, if\nyou had the following peripherals nearby when performing a scan:\n\n| ID | Name        | Services                                                                           |\n|:--:|-------------|------------------------------------------------------------------------------------|\n| D1 | \"SensorTag\" | `0000aa80-0000-1000-8000-00805f9b34fb`                                             |\n| D2 |             | `f484e2db-2efa-4b58-96be-f89372a3ef82`                                             |\n| D3 | \"Example\"   | `8d7798c7-15bd-493f-a935-785305946870`,\u003cbr/\u003e`67bebb9e-6372-4de6-a7bf-e0384583929e` |\n\nTo have peripherals D1 and D3 emitted during a scan, you could use the following `filters`:\n\n```kotlin\nval scanner = Scanner {\n    filters {\n        match {\n            services = listOf(Bluetooth.BaseUuid + 0xaa80) // SensorTag\n        }\n        match {\n            name = Filter.Name.Prefix(\"Ex\")\n        }\n    }\n}\n```\n\nScanning begins when the [`advertisements`] [`Flow`] is collected and stops when the [`Flow`] collection is terminated.\nA [`Flow`] terminal operator (such as [`first`]) may be used to scan until (for example) the first advertisement is\nfound matching the specified filters: \n\n```kotlin\nval advertisement = Scanner {\n    filters {\n        match {\n            name = Filter.Name.Exact(\"Example\")\n        }\n    }\n}.advertisements.first()\n```\n\n### Android\n\nAndroid offers additional settings to customize scanning. They are available via the `scanSettings` property in the\n[`Scanner`] builder DSL. Simply set `scanSettings` property to an Android [`ScanSettings`] object, for example:\n\n```kotlin\nval scanner = Scanner {\n    scanSettings = ScanSettings.Builder()\n        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)\n        .build()\n}\n```\n\n\u003e [!NOTE]\n\u003e _The `scanSettings` property is only available on Android and is considered a Kable obsolete API, meaning it will be\n\u003e removed when a DSL specific API becomes available._\n\n### JavaScript\n\n_Scanning for nearby peripherals is supported, but only available on Chrome 79+ with \"Experimental Web Platform\nfeatures\" enabled via:_ `chrome://flags/#enable-experimental-web-platform-features`\n\n## Peripheral\n\nOnce an [`Advertisement`] is obtained, it can be converted to a [`Peripheral`] via the `Peripheral` builder function:\n\n```kotlin\nval peripheral = Peripheral(advertisement) {\n    // Configure peripheral.\n}\n```\n\n[`Peripheral`] objects represent actions that can be performed against a remote peripheral, such as connection\nhandling and I/O operations. [`Peripheral`] objects provide a [`CoroutineScope`] `scope` property, and coroutines\ncan be `launch`ed from it:\n\n```kotlin\nperipheral.scope.launch {\n    // Long running task that will be cancelled when peripheral is disposed\n    // (i.e. `peripheral.close()` is called).\n}\n```\n\n\u003e [!IMPORTANT]\n\u003e When a [`Peripheral`] is no longer needed, it should be disposed via `close`:\n\u003e\n\u003e ```kotlin\n\u003e peripheral.close()\n\u003e ```\n\u003e\n\u003e Once a [`Peripheral`] is disposed (via `close`) it can no longer be used (e.g. calling `connect` will throw\n\u003e `IllegalStateException`).\n\n\u003e [!TIP]\n\u003e `launch`ed coroutines from a `Peripheral` object's `scope` are permitted to run until `Peripheral.dispose()`\n\u003e is called (i.e. can span across reconnects); for tasks that should only run for the duration of a single connection\n\u003e (i.e. shutdown on disconnect), `launch` via the `CoroutineScope` returned from `Peripheral.connect` instead.\n\n### Configuration\n\n#### Logging\n\nBy default, Kable only logs a small number of warnings when unexpected failures occur. To aid in debugging, additional\nlogging may be enabled and configured via the `logging` DSL, for example:\n\n```kotlin\nval peripheral = Peripheral(advertisement) {\n    logging {\n        level = Events // or Data\n    }\n}\n```\n\nThe available log levels are:\n\n- `Warnings`: Logs warnings when unexpected failures occur _(default)_\n- `Events`: Same as `Warnings` plus logs all events (e.g. writing to a characteristic)\n- `Data`: Same as `Events` plus string representation of I/O data\n\nAvailable logging settings are as follows (all settings are optional; shown are defaults, when not specified):\n\n```kotlin\nval peripheral = Peripheral(advertisement) {\n    logging {\n        engine = SystemLogEngine\n        level = Warnings\n        format = Multiline\n        data = Hex\n    }\n}\n```\n\nThe format of the logs can be either `Compact` (on a single line per log) or `Multiline` (spanning multiple lines for\ndetails):\n\n| `Compact` | `Multiline` _(default)_ |\n|-----------|-------------------------|\n| \u003cpre\u003eexample message(detail1=value1, detail2=value2, ...)\u003c/pre\u003e | \u003cpre\u003eexample message\u003cbr/\u003e  detail1: value1\u003cbr/\u003e  detail2: value2\u003cbr/\u003e  ...\u003c/pre\u003e |\n\nDisplay format of I/O data may be customized, either by configuring the `Hex` representation, or by providing a\n`DataProcessor`, for example:\n\n```kotlin\nval peripheral = Peripheral(advertisement) {\n    logging {\n        data = Hex {\n            separator = \" \"\n            lowerCase = false\n        }\n\n        // or...\n\n        data = DataProcessor { bytes, _, _, _, _ -\u003e\n            // todo: Convert `bytes` to desired String representation, for example:\n            bytes.joinToString { byte -\u003e byte.toString() } // Show data as integer representation of bytes.\n        }\n    }\n}\n```\n\n_I/O data is only shown in logs when logging `level` is set to `Data`._\n\nWhen logging, the identity of the peripheral is prefixed on log messages to differentiate messages when multiple\nperipherals are logging. The identifier (for the purposes of logging) can be set via the `identifier` property:\n\n```kotlin\nval peripheral = Peripheral(advertisement) {\n    logging {\n        identifier = \"Example\"\n    }\n}\n```\n\nThe default (when not specified, or set to `null`) is to use the platform specific peripheral identifier:\n\n- Android: Hardware (MAC) address (e.g. \"00:11:22:AA:BB:CC\")\n- Apple: The UUID associated with the peer\n- JavaScript: A `DOMString` that uniquely identifies a device\n\n#### Service Discovery\n\nAll platforms support an `onServicesDiscovered` action (that is executed after service discovery but before observations\nare wired up):\n\n```kotlin\nval peripheral = Peripheral(advertisement) {\n    onServicesDiscovered {\n        // Perform any desired I/O operations.\n    }\n}\n```\n\n_Exceptions thrown in `onServicesDiscovered` are propagated to the `Peripheral`'s [`connect`] call._\n\n### Android\n\nOn Android targets, additional configuration options are available (all configuration directives are optional):\n\n```kotlin\nval peripheral = Peripheral(advertisement) {\n    autoConnectIf { false } // default\n    onServicesDiscovered {\n        requestMtu(...)\n    }\n    transport = Transport.Le // default\n    phy = Phy.Le1M // default\n}\n```\n\n#### `autoConnect`\n\nPer the [`connectGatt`] documentation, `autoConnect` determines:\n\n\u003e Whether to directly connect to the remote device (`false`) or to automatically connect as soon as the remote device\n\u003e becomes available (`true`).\n\nWith respect to [`connect`]ing:\n\n| `autoConnect` value | [`connect`] timeout |\n|:-------------------:|:-------------------:|\n|       `false`       |     ~30 seconds     |\n|       `true`        |        Never        |\n\nPer [answer](https://stackoverflow.com/a/50273724) to \"What exactly does Android's Bluetooth `autoConnect` parameter do?\":\n\n\u003e Direct connect has a different scan interval and scan window at a higher duty than auto connect, meaning it will\n\u003e dedicate more radio time to listen for connectable advertisements for the remote device, i.e. the connection will be\n\u003e established faster.\n\nOne possible strategy for a fast initial connection attempt that falls back to lower battery usage connection attempts is:\n\n```kotlin\nval autoConnect = MutableStateFlow(false)\n\nval peripheral = Peripheral {\n    autoConnectIf { autoConnect.value }\n}\n\nwhile (peripheral.state.value !is Connected) {\n    autoConnect.value = try {\n        peripheral.connect()\n        false\n    } catch (e: Exception) {\n        if (e is CancellationException) throw e\n        true\n    }\n}\n```\n\n### JavaScript\n\nOn JavaScript, rather than processing a stream of advertisements, a specific peripheral can be requested using the\n[`requestPeripheral`] function. Criteria ([`Options`]) such as expected service UUIDs on the peripheral and/or the\nperipheral's name may be specified. When [`requestPeripheral`] is called with the specified options, the browser shows\nthe user a list of peripherals matching the criteria. The peripheral chosen by the user is then returned (as a\n[`Peripheral`] object). If user cancels the dialog, then [`requestPeripheral`] returns `null`.\n\n```kotlin\nval options = Options {\n    filters {\n        match {\n            name = Filter.Name.Prefix(\"Example\")\n        }\n    }\n    optionalServices = listOf(\n        Uuid.parse(\"f000aa80-0451-4000-b000-000000000000\"),\n        Uuid.parse(\"f000aa81-0451-4000-b000-000000000000\"),\n    )\n}\nval peripheral = requestPeripheral(options)\n```\n\n\u003e After the user selects a device to pair with this origin, the origin is allowed to access any service whose UUID was\n\u003e listed in the services list in any element of `options.filters` or in `options.optionalServices`.\n\u003e \n\u003e This implies that if developers filter just by name, they must use `optionalServices` to get access to any services.\n\n— [Web Bluetooth: 4. Device Discovery](https://webbluetoothcg.github.io/web-bluetooth/#device-discovery)\n\n## Connectivity\n\nOnce a [`Peripheral`] object is acquired, a connection can be established via the [`connect`] function. The [`connect`]\nmethod suspends until a connection is established and ready (or a failure occurs). A connection is considered ready when\nconnected, services have been discovered, and observations (if any) have been re-wired. _Service discovery occurs\nautomatically upon connection._\n\n\u003e [!TIP]\n\u003e Multiple concurrent calls to [`connect`] will all suspend until connection is ready.\n\n```kotlin\nperipheral.connect()\n```\n\n\u003e [!TIP]\n\u003e The [`connect`] function returns a [`CoroutineScope`] that can be used to `launch` tasks that\n\u003e should run until peripheral disconnects. When [`disconnect`] is called, any coroutines\n\u003e `launch`ed from the [`CoroutineScope`] returned by [`connect`] will be cancelled prior to\n\u003e performing the underlying disconnect process.\n\n\u003e [!TIP]\n\u003e The connection [`CoroutineScope`] is also available as the `scope` property on the [`Connected`]\n\u003e [`State`][connection-state].\n\nTo disconnect, the [`disconnect`] function will disconnect an active connection, or cancel an in-flight connection\nattempt. The [`disconnect`] function suspends until the peripheral has settled on a disconnected state.\n\n```kotlin\nperipheral.disconnect()\n```\n\n#### State\n\nThe connection state of a [`Peripheral`] can be monitored via its [`state`] [`Flow`].\n\n```kotlin\nperipheral.state.collect { state -\u003e\n    // Display and/or process the connection state.\n}\n```\n\nThe [`state`] will typically transition through the following [`State`][connection-state]s:\n\n![Connection states](artwork/connection-states.png)\n\n\u003e [!NOTE]\n\u003e [`Disconnecting`] state is skipped on Apple and JavaScript when connection closure is initiated by peripheral (or\n\u003e peripheral goes out-of-range).\n\n### I/O\n\nBluetooth Low Energy devices are organized into a tree-like structure of services, characteristics and descriptors;\nwhereas characteristics and descriptors have the capability of being read from, or written to.\n\nFor example, a peripheral might have the following structure:\n\n- Service S1 (`0x1815` or `00001815-0000-1000-8000-00805f9b34fb`)\n    - Characteristic C1\n        - Descriptor D1\n        - Descriptor D2\n    - Characteristic C2 (`0x2a56` or `00002a56-0000-1000-8000-00805f9b34fb`)\n        - Descriptor D3 (`gatt.client_characteristic_configuration` or `00002902-0000-1000-8000-00805f9b34fb`)\n- Service S2\n    - Characteristic C3\n\nTo access a characteristic or descriptor, use the [`characteristicOf`] or [`descriptorOf`] functions, respectively.\nThese functions lazily search for the first match (based on UUID) in the GATT profile when performing I/O.\n\n_When performing I/O operations on a characteristic ([`read`], [`write`], [`observe`]), the properties of the\ncharacteristic are taken into account when finding the first match. For example, when performing a [`write`] with a\n[`WriteType`] of [`WithoutResponse`], the first characteristic matching the expected UUID **and** having the\n[`writeWithoutResponse`] property will be used._\n\nIn the above example, to lazily access \"Descriptor D3\":\n\n```kotlin\nval descriptor = descriptorOf(\n    service = Bluetooth.BaseUuid + 0x1815,\n    characteristic = Bluetooth.BaseUuid + 0x2A56,\n    descriptor = Uuid.descriptor(\"gatt.client_characteristic_configuration\"),\n)\n```\n\nAlternatively, a characteristic or descriptor may be obtained by traversing the [`Peripheral.services`]. This is useful\nwhen multiple characteristics or descriptors have the same UUID. Objects obtained from the [`Peripheral.services`] hold\nstrong references to the underlying platform types, so special care must be taken to properly remove references to\nobjects retrieved from [`Peripheral.services`] when no longer needed.\n\nTo access \"Descriptor D3\" using a discovered descriptor:\n\n```kotlin\nval services = peripheral.services.value ?: error(\"Services have not been discovered\")\nval descriptor = services\n    .first { it.serviceUuid == Uuid.parse(\"00001815-0000-1000-8000-00805f9b34fb\") }\n    .characteristics\n    .first { it.characteristicUuid == Uuid.parse(\"00002a56-0000-1000-8000-00805f9b34fb\") }\n    .descriptors\n    .first { it.descriptorUuid == Uuid.parse(\"00002902-0000-1000-8000-00805f9b34fb\") }\n```\n\n\u003e [!TIP]\n\u003e Shorthand notations are available for UUIDs. The accessing \"Descriptor D3\" example could be written as:\n\u003e\n\u003e ```kotlin\n\u003e val services = peripheral.services.value ?: error(\"Services have not been discovered\")\n\u003e val descriptor = services\n\u003e   .first { it.serviceUuid == Bluetooth.BaseUuid + 0x1815 }\n\u003e   .characteristics\n\u003e   .first { it.characteristicUuid == Bluetooth.BaseUuid + 0x2A56 }\n\u003e   .descriptors\n\u003e   .first { it.descriptorUuid == Uuid.descriptor(\"gatt.client_characteristic_configuration\") }\n\u003e ```\n\n\u003e [!TIP]\n\u003e This example uses a similar search algorithm as `descriptorOf`, but other search methods may be utilized. For example,\n\u003e properties of the characteristic could be queried to find a specific characteristic that is expected to be the parent of\n\u003e the sought after descriptor. When searching for a specific characteristic, descriptors can be read that may identity the\n\u003e sought after characteristic.\n\nWhen connected, data can be read from, or written to, characteristics and/or descriptors via [`read`] and [`write`]\nfunctions.\n\n```kotlin\nval data = peripheral.read(characteristic)\n\nperipheral.write(descriptor, byteArrayOf(1, 2, 3))\n```\n\n\u003e [!NOTE]\n\u003e _The [`read`] and [`write`] functions throw [`NotConnectedException`] until a connection is established._\n\n### Observation\n\nBluetooth Low Energy provides the capability of subscribing to characteristic changes by means of notifications and/or\nindications, whereas a characteristic change on a connected peripheral is \"pushed\" to the central via a characteristic\nnotification and/or indication which carries the new value of the characteristic.\n\nCharacteristic change notifications/indications can be observed/subscribed to via the [`observe`] function which returns\na [`Flow`] of the new characteristic data.\n\n```kotlin\nval observation = peripheral.observe(characteristic)\nobservation.collect { data -\u003e\n    // Process data.\n}\n```\n\nWhen used with [`characteristicOf`], the [`observe`] function can be called (and its returned [`Flow`] can be collected)\nprior to a connection being established. Once a connection is established then characteristic changes will stream from\nthe [`Flow`]. If the connection drops, the [`Flow`] will remain active, and upon reconnecting it will resume streaming\ncharacteristic changes.\n\nA [`Characteristic`] may also be obtained via the [`Peripheral.services`] property and used with the [`observe`]\nfunction. As before, if the connection drops, the [`Flow`] will remain active, upon reconnecting the same underlying\nplatform characteristic will be used to to resume streaming characteristic changes.\n\nFailures related to notifications/indications are propagated via the [`observe`] [`Flow`], for example, if the\nassociated characteristic is invalid or cannot be found, then a `NoSuchElementException` is propagated via the\n[`observe`] [`Flow`]. An [`observationExceptionHandler`] may be registered with the [`Peripheral`] to control which\nfailures are propagated through (and terminate) the [`observe`] [`Flow`], for example:\n\n```kotlin\nPeripheral(advertisement) {\n    observationExceptionHandler { cause -\u003e\n        // Log failure instead of propagating associated `observe` flow.\n        println(\"Observation failure suppressed: $cause\")\n    }\n}\n```\n\nIn scenarios where an I/O operation needs to be performed upon subscribing to the [`observe`] [`Flow`], an\n`onSubscription` action may be specified:\n\n```kotlin\nval observation = peripheral.observe(characteristic) {\n    // Perform desired I/O operations upon collecting from the `observe` Flow, for example:\n    peripheral.write(descriptor, \"ping\".toByteArray())\n}\nobservation.collect { data -\u003e\n    // Process data.\n}\n```\n\nIn the above example, `\"ping\"` will be written to the `descriptor` when:\n\n- [Connection][`connect`] is established (while the returned [`Flow`] is active); and\n- _After_ the observation is spun up (i.e. after enabling notifications or indications)\n\nThe `onSubscription` action is useful in situations where an initial operation is needed when starting an observation\n(such as writing a configuration to the peripheral and expecting the response to come back in the form of a\ncharacteristic change).\n\n## Background Support\n\nTo enable [background support] on Apple, configure the `CentralManager` _before_ using most of Kable's functionality:\n\n```kotlin\nCentralManager.configure {\n    stateRestoration = true // `false` by default.\n}\n```\n\nThe `CentralManager` is initialized on first use (e.g. scanning or creating a peripheral), attempts to `configure` it\nafter initialization will result in an `IllegalStateException` being thrown.\n\n## Setup\n\n### Android Permissions\n\nKable [declares permissions for common use cases](kable-core/src/androidMain/AndroidManifest.xml), but your app's\nconfiguration may need to be adjusted under the following conditions:\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\"\u003eYour app...\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003ccode\u003eAndroidManifest.xml\u003c/code\u003e additions\u003c/td\u003e\n\u003c/tr\u003e\n\n\u003ctr\u003e\n\u003ctd\u003e\n\n[Obtains the user's location (e.g. maps)](https://developer.android.com/training/location/permissions#foreground)\n\n\u003c/td\u003e\n\u003ctd\u003e\n\n```xml\n\u003cuses-permission\n    android:name=\"android.permission.ACCESS_COARSE_LOCATION\"\n    tools:node=\"replace\"/\u003e\n\u003cuses-permission\n    android:name=\"android.permission.ACCESS_FINE_LOCATION\"\n    tools:node=\"replace\"/\u003e\n```\n\n\u003c/td\u003e\n\u003c/tr\u003e\n\n\u003ctr\u003e\n\u003ctd\u003e\n\n[Derives the user's location from Bluetooth Low Energy scans](https://developer.android.com/guide/topics/connectivity/bluetooth/permissions#declare-android12-or-higher)\n\n\u003c/td\u003e\n\u003ctd\u003e\n\n```xml\n\u003cuses-permission\n    android:name=\"android.permission.BLUETOOTH_SCAN\"\n    tools:node=\"replace\"/\u003e\n```\n\n\u003c/td\u003e\n\u003c/tr\u003e\n\n\u003ctr\u003e\n\u003ctd\u003e\n\n[Performs background Bluetooth Low Energy scans](https://developer.android.com/training/location/permissions#background)\n\n\u003c/td\u003e\n\u003ctd\u003e\n\n```xml\n\u003cuses-permission\n    android:name=\"android.permission.ACCESS_BACKGROUND_LOCATION\"\n    android:maxSdkVersion=\"30\"/\u003e\n```\n\n\u003c/td\u003e\n\u003c/tr\u003e\n\n\u003ctr\u003e\n\u003ctd\u003e\n\n[Requires Bluetooth Low Energy (and won't function without it)](https://developer.android.com/guide/topics/connectivity/bluetooth/permissions#features)\n\n\u003c/td\u003e\n\u003ctd\u003e\n\n```xml\n\u003cuses-feature\n    android:name=\"android.hardware.bluetooth_le\"\n    android:required=\"true\"/\u003e\n```\n\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n### Gradle\n\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.juul.kable/kable-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.juul.kable/kable-core)\n\nKable can be configured via Gradle Kotlin DSL as follows:\n\n```kotlin\nplugins {\n    id(\"com.android.application\") // or id(\"com.android.library\")\n    kotlin(\"multiplatform\")\n}\n\nrepositories {\n    mavenCentral()\n}\n\nkotlin {\n    androidTarget()\n    js().browser()\n    macosX64()\n    iosX64()\n    iosArm64()\n\n    sourceSets {\n        commonMain.dependencies {\n            api(\"org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}\")\n            implementation(\"com.juul.kable:kable-core:${kableVersion}\")\n        }\n\n        androidMain.dependencies {\n            implementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}\")\n        }\n    }\n}\n\nandroid {\n    // ...\n}\n```\n\n# License\n\n```\nCopyright 2020 JUUL Labs, Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n\n\n[Coroutine scope]: https://kotlinlang.org/docs/reference/coroutines/coroutine-context-and-dispatchers.html#coroutine-scope\n[Coroutines with multithread support for Kotlin/Native]: https://github.com/Kotlin/kotlinx.coroutines/issues/462\n[SensorTag sample app]: https://github.com/JuulLabs/sensortag\n[`Advertisement`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-advertisement/index.html\n[`Characteristic`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-characteristic/index.html\n[`Connected`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-state/-connected/index.html\n[`CoroutineScope.peripheral`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/peripheral.html\n[`CoroutineScope`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/\n[`Disconnected`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-state/-disconnected/index.html\n[`Disconnecting`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-state/-disconnecting/index.html\n[`Filter`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-filter/index.html\n[`Flow`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/\n[`Int`]: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-int/\n[`Long`]: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-long/\n[`NotConnectedException`]: https://juullabs.github.io/kable/kable-exceptions/com.juul.kable/-not-connected-exception/index.html\n[`Options`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-options/index.html\n[`Peripheral.disconnect`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-peripheral/disconnect.html\n[`Peripheral.services`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-peripheral/services.html\n[`Peripheral`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-peripheral/index.html\n[`ScanSettings`]: https://developer.android.com/reference/kotlin/android/bluetooth/le/ScanSettings\n[`Scanner`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-scanner.html\n[`Uuid`]: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.uuid/-uuid/\n[`WithoutResponse`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-write-type/-without-response/index.html\n[`WriteType`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-write-type/index.html\n[`advertisements`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-scanner/advertisements.html\n[`characteristicOf`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/characteristic-of.html\n[`connectGatt`]: https://developer.android.com/reference/android/bluetooth/BluetoothDevice#connectGatt(android.content.Context,%20boolean,%20android.bluetooth.BluetoothGattCallback)\n[`connect`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-peripheral/connect.html\n[`descriptorOf`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/descriptor-of.html\n[`disconnect`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-peripheral/disconnect.html\n[`first`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/first.html\n[`observationExceptionHandler`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-peripheral-builder/observation-exception-handler.html\n[`observe`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-peripheral/observe.html\n[`read`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-peripheral/read.html\n[`requestPeripheral`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/request-peripheral.html\n[`state`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-peripheral/state.html\n[`writeWithoutResponse`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/write-without-response.html\n[`write`]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-peripheral/write.html\n[background support]: https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html\n[badge-android]: http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat\n[badge-ios]: http://img.shields.io/badge/platform-ios-CDCDCD.svg?style=flat\n[badge-js]: http://img.shields.io/badge/platform-js-F8DB5D.svg?style=flat\n[badge-jvm]: http://img.shields.io/badge/platform-jvm-DB413D.svg?style=flat\n[badge-linux]: http://img.shields.io/badge/platform-linux-2D3F6C.svg?style=flat\n[badge-mac]: http://img.shields.io/badge/platform-macos-111111.svg?style=flat\n[badge-tvos]: http://img.shields.io/badge/platform-tvos-808080.svg?style=flat\n[badge-wasm]: https://img.shields.io/badge/platform-wasm-624FE8.svg?style=flat\n[badge-watchos]: http://img.shields.io/badge/platform-watchos-C0C0C0.svg?style=flat\n[badge-windows]: http://img.shields.io/badge/platform-windows-4D76CD.svg?style=flat\n[connection-state]: https://juullabs.github.io/kable/kable-core/com.juul.kable/-state/index.html\n","funding_links":[],"categories":["Kotlin","KMM"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FJuulLabs%2Fkable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FJuulLabs%2Fkable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FJuulLabs%2Fkable/lists"}