
BLE client implementing scanning with rich filters, GATT discovery, serialized request queue, subscriptions/notifications, pairing management, managed sessions with reconnection/resubscription, chunked transfers, diagnostics and testing support.
A Kotlin Multiplatform BLE client for Windows x64 (JVM + C++/WinRT) and
Android 8.0+ (API 26+). The shared API is in the gpt.ble.manager package.
This project is created and developed with the help of ChatGPT. ChatGPT assists with implementation, refactoring, tests, and documentation. Project maintainers remain responsible for accepting changes and releasing versions.
The main code is licensed under MIT. The project is actively developed.
This working tree contains 0.3.0 in development (0.3.0-local). See the
0.3.0 API guide for local integration, migration notes, and
examples of the new APIs. Maven dependency examples below refer to the 0.2.3 coordinates;
they do not provide the new 0.3.0 functionality.
StateFlow, notifications through SharedFlow.| Platform | Implementation | Notes |
|---|---|---|
| Windows x64 | Kotlin/JVM, JNI, C++20/WinRT | DLL bundled in the JAR; Windows negotiates MTU |
| Android API 26+ | Android Bluetooth API | The application requests runtime permissions |
Peripheral/GATT server mode, iOS, Linux, and macOS are outside the current API's scope.
Tool and dependency versions are defined in
gradle/libs.versions.toml: Kotlin 2.4.20,
Android Gradle Plugin 9.4.1, JDK 21, and compile SDK 37. The Gradle version is set in
gradle-wrapper.properties.
A complete Windows and Android build requires:
ANDROID_HOME or sdk.dir
in your local local.properties.From the repository root in PowerShell:
.\gradlew.bat allTests assembleGradle searches for CMake in PATH, its standard installation, and Visual Studio.
To override its location:
.\gradlew.bat :gpt-ble-manager:buildWindowsNative -PcmakeExecutable="C:/tools/cmake/bin/cmake.exe"Android can be built separately on a system supported by AGP:
bash ./gradlew :gpt-ble-manager:assembleAndroidMain :gpt-ble-manager:testAndroidHostTest.
The Windows artifact requires its DLL to be built on Windows.
Add Maven Central to your application's settings:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}Kotlin Multiplatform:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.graycatdeveloper:gpt-ble-manager:0.2.3")
}
}
}A regular JVM application on Windows:
dependencies {
implementation("io.github.graycatdeveloper:gpt-ble-manager-windows:0.2.3")
}The Windows dependency includes the native DLL. Visual Studio, CMake, and the Windows SDK are only needed when building the library from source.
In the Windows source set:
import gpt.ble.manager.BleManager
import gpt.ble.manager.windows.WindowsBleManager
val manager: BleManager = WindowsBleManager()In the Android source set:
import gpt.ble.manager.android.AndroidBleManager
val manager = AndroidBleManager(context.applicationContext)
val permissions = manager.requiredPermissions()
// The Activity requests missing permissions before startScan/connect.Android 12+ requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT; Android 8–11 requires
ACCESS_FINE_LOCATION and location services to be enabled. The library does not
request runtime permissions itself. The OS may display its own pairing UI.
The manifest uses neverForLocation. Applications that derive location from BLE
must configure their merged manifest accordingly. This flag may also restrict
discovery of some beacons.
See Android Bluetooth permissions.
Example for commonMain; pass your device's name prefix:
import gpt.ble.manager.AdapterState
import gpt.ble.manager.BleConnection
import gpt.ble.manager.BleManager
import gpt.ble.manager.ScanOptions
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.withTimeout
suspend fun connectToDevice(manager: BleManager, namePrefix: String): BleConnection {
check(manager.refreshAdapterState() == AdapterState.Ready)
manager.startScan(ScanOptions(namePrefix = namePrefix))
val device = try {
withTimeout(30_000) {
manager.devices.mapNotNull { devices -> devices.firstOrNull() }.first()
}
} finally {
manager.stopScan()
}
return manager.connect(device, timeoutMillis = 20_000)
}Use an existing manager from a suspend function:
try {
val connection = connectToDevice(manager, "ExampleSensor")
try {
println(connection.readDeviceName())
connection.discoverServices().forEach { service ->
println("${service.uuid}: ${service.characteristics.size} characteristics")
}
} finally {
connection.close()
}
} finally {
manager.close()
}Create a new manager after manager.close(). Remote disconnection is reflected in
connection.state and connection.disconnectReason.
A name may arrive in a separate scan response after the first unnamed packet.
namePrefix is case-sensitive and is applied after merging the data.
Source priority: Advertisement → GATT → System.
device.name remains null until a name is known; only displayName falls back to
the address for display. An address returned by the OS as a name is discarded.
With ScanOptions(includeKnownDevices = true), results also include Windows-known
devices or bonded Android LE/dual-mode devices. Such a record may be unreachable:
check seenInCurrentScan and nullable rssi. Read the GATT name using
readDeviceName() after connecting; scanning does not automatically connect to
every unnamed device.
Obtain characteristics from connection.discoverServices(). UUIDs may repeat:
id, serviceId, and connectionId establish identity.
Objects from a previous connection cannot be reused.
val services = connection.discoverServices()
val readable = services.flatMap { it.characteristics }.first { it.canRead }
val bytes = connection.read(readable).toByteArray()Start collecting connection.notifications before calling subscribe(characteristic).
Select a characteristic with canNotify or canIndicate; disable the subscription
with SubscriptionMode.Disabled. Use subscribe for CCCD 2902.
See GattExample.kt
for listener/CCCD ordering.
A write accepts at most MTU - 3 bytes. Command fragmentation depends on the device
protocol. Windows returns the actually negotiated MTU; on Android, requestMtu
asks the OS to change it.
val state = manager.getPairingState(device)
val paired = manager.pair(device)
val unpaired = manager.unpair(device)Close this manager's connection to the device before pair/unpair. Windows supports
ConfirmOnly; use system settings for PIN/passkey scenarios. Android unpair requires
API 36+ and a CompanionDeviceManager association owned by the application;
otherwise it returns BleError.Unsupported. No hidden Android APIs are used.
Paired does not guarantee access to every GATT service. BleException.code holds
a portable error category, while cause retains the platform cause. A GATT timeout
or cancellation terminates the session to prevent a late response from completing
the next request. Cancelling pair/unpair does not guarantee cancellation of an
OS operation already in progress.
.\gradlew.bat :sample-windows:run
.\gradlew.bat :sample-windows:run --args="ExampleSensor --known --name-only"
.\gradlew.bat :sample-windows:run --args="AA:BB:CC:DD:EE:01 --direct --inspect"--known includes system records, --prefix=<name> filters the scan, and --inspect
reads the catalog and standard attributes. --notify=<uuid> checks notifications
for five seconds. --pairing-state only reads the state; --pair and --unpair
change it. The example name and address are placeholders; replace them with your
device's values.
Include the OS, library version, reproduction steps, BleException.code, and stack
trace when reporting an issue. Remove device addresses and other personal data
before sharing logs.
The main source code and documentation are distributed under the MIT License. Gradle Wrapper retains its Apache-2.0 license; see NOTICE and the Wrapper license text. Dependencies retain their own licenses.
Windows console issues: encoding and native error diagnostics.
A Kotlin Multiplatform BLE client for Windows x64 (JVM + C++/WinRT) and
Android 8.0+ (API 26+). The shared API is in the gpt.ble.manager package.
This project is created and developed with the help of ChatGPT. ChatGPT assists with implementation, refactoring, tests, and documentation. Project maintainers remain responsible for accepting changes and releasing versions.
The main code is licensed under MIT. The project is actively developed.
This working tree contains 0.3.0 in development (0.3.0-local). See the
0.3.0 API guide for local integration, migration notes, and
examples of the new APIs. Maven dependency examples below refer to the 0.2.3 coordinates;
they do not provide the new 0.3.0 functionality.
StateFlow, notifications through SharedFlow.| Platform | Implementation | Notes |
|---|---|---|
| Windows x64 | Kotlin/JVM, JNI, C++20/WinRT | DLL bundled in the JAR; Windows negotiates MTU |
| Android API 26+ | Android Bluetooth API | The application requests runtime permissions |
Peripheral/GATT server mode, iOS, Linux, and macOS are outside the current API's scope.
Tool and dependency versions are defined in
gradle/libs.versions.toml: Kotlin 2.4.20,
Android Gradle Plugin 9.4.1, JDK 21, and compile SDK 37. The Gradle version is set in
gradle-wrapper.properties.
A complete Windows and Android build requires:
ANDROID_HOME or sdk.dir
in your local local.properties.From the repository root in PowerShell:
.\gradlew.bat allTests assembleGradle searches for CMake in PATH, its standard installation, and Visual Studio.
To override its location:
.\gradlew.bat :gpt-ble-manager:buildWindowsNative -PcmakeExecutable="C:/tools/cmake/bin/cmake.exe"Android can be built separately on a system supported by AGP:
bash ./gradlew :gpt-ble-manager:assembleAndroidMain :gpt-ble-manager:testAndroidHostTest.
The Windows artifact requires its DLL to be built on Windows.
Add Maven Central to your application's settings:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}Kotlin Multiplatform:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.graycatdeveloper:gpt-ble-manager:0.2.3")
}
}
}A regular JVM application on Windows:
dependencies {
implementation("io.github.graycatdeveloper:gpt-ble-manager-windows:0.2.3")
}The Windows dependency includes the native DLL. Visual Studio, CMake, and the Windows SDK are only needed when building the library from source.
In the Windows source set:
import gpt.ble.manager.BleManager
import gpt.ble.manager.windows.WindowsBleManager
val manager: BleManager = WindowsBleManager()In the Android source set:
import gpt.ble.manager.android.AndroidBleManager
val manager = AndroidBleManager(context.applicationContext)
val permissions = manager.requiredPermissions()
// The Activity requests missing permissions before startScan/connect.Android 12+ requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT; Android 8–11 requires
ACCESS_FINE_LOCATION and location services to be enabled. The library does not
request runtime permissions itself. The OS may display its own pairing UI.
The manifest uses neverForLocation. Applications that derive location from BLE
must configure their merged manifest accordingly. This flag may also restrict
discovery of some beacons.
See Android Bluetooth permissions.
Example for commonMain; pass your device's name prefix:
import gpt.ble.manager.AdapterState
import gpt.ble.manager.BleConnection
import gpt.ble.manager.BleManager
import gpt.ble.manager.ScanOptions
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.withTimeout
suspend fun connectToDevice(manager: BleManager, namePrefix: String): BleConnection {
check(manager.refreshAdapterState() == AdapterState.Ready)
manager.startScan(ScanOptions(namePrefix = namePrefix))
val device = try {
withTimeout(30_000) {
manager.devices.mapNotNull { devices -> devices.firstOrNull() }.first()
}
} finally {
manager.stopScan()
}
return manager.connect(device, timeoutMillis = 20_000)
}Use an existing manager from a suspend function:
try {
val connection = connectToDevice(manager, "ExampleSensor")
try {
println(connection.readDeviceName())
connection.discoverServices().forEach { service ->
println("${service.uuid}: ${service.characteristics.size} characteristics")
}
} finally {
connection.close()
}
} finally {
manager.close()
}Create a new manager after manager.close(). Remote disconnection is reflected in
connection.state and connection.disconnectReason.
A name may arrive in a separate scan response after the first unnamed packet.
namePrefix is case-sensitive and is applied after merging the data.
Source priority: Advertisement → GATT → System.
device.name remains null until a name is known; only displayName falls back to
the address for display. An address returned by the OS as a name is discarded.
With ScanOptions(includeKnownDevices = true), results also include Windows-known
devices or bonded Android LE/dual-mode devices. Such a record may be unreachable:
check seenInCurrentScan and nullable rssi. Read the GATT name using
readDeviceName() after connecting; scanning does not automatically connect to
every unnamed device.
Obtain characteristics from connection.discoverServices(). UUIDs may repeat:
id, serviceId, and connectionId establish identity.
Objects from a previous connection cannot be reused.
val services = connection.discoverServices()
val readable = services.flatMap { it.characteristics }.first { it.canRead }
val bytes = connection.read(readable).toByteArray()Start collecting connection.notifications before calling subscribe(characteristic).
Select a characteristic with canNotify or canIndicate; disable the subscription
with SubscriptionMode.Disabled. Use subscribe for CCCD 2902.
See GattExample.kt
for listener/CCCD ordering.
A write accepts at most MTU - 3 bytes. Command fragmentation depends on the device
protocol. Windows returns the actually negotiated MTU; on Android, requestMtu
asks the OS to change it.
val state = manager.getPairingState(device)
val paired = manager.pair(device)
val unpaired = manager.unpair(device)Close this manager's connection to the device before pair/unpair. Windows supports
ConfirmOnly; use system settings for PIN/passkey scenarios. Android unpair requires
API 36+ and a CompanionDeviceManager association owned by the application;
otherwise it returns BleError.Unsupported. No hidden Android APIs are used.
Paired does not guarantee access to every GATT service. BleException.code holds
a portable error category, while cause retains the platform cause. A GATT timeout
or cancellation terminates the session to prevent a late response from completing
the next request. Cancelling pair/unpair does not guarantee cancellation of an
OS operation already in progress.
.\gradlew.bat :sample-windows:run
.\gradlew.bat :sample-windows:run --args="ExampleSensor --known --name-only"
.\gradlew.bat :sample-windows:run --args="AA:BB:CC:DD:EE:01 --direct --inspect"--known includes system records, --prefix=<name> filters the scan, and --inspect
reads the catalog and standard attributes. --notify=<uuid> checks notifications
for five seconds. --pairing-state only reads the state; --pair and --unpair
change it. The example name and address are placeholders; replace them with your
device's values.
Include the OS, library version, reproduction steps, BleException.code, and stack
trace when reporting an issue. Remove device addresses and other personal data
before sharing logs.
The main source code and documentation are distributed under the MIT License. Gradle Wrapper retains its Apache-2.0 license; see NOTICE and the Wrapper license text. Dependencies retain their own licenses.
Windows console issues: encoding and native error diagnostics.