Skip to content

Comments

Bluetooth Server#912

Open
Daeda88 wants to merge 92 commits intodevelopfrom
feature/bluetooth-server
Open

Bluetooth Server#912
Daeda88 wants to merge 92 commits intodevelopfrom
feature/bluetooth-server

Conversation

@Daeda88
Copy link
Contributor

@Daeda88 Daeda88 commented Nov 18, 2025

A significant rewrite of the Bluetooth API (2.0) that focusses primarily on supporting Bluetooth Servers.

Changes to Client Side

To make the API as good as possible, some breaking changes had to be made:

  • Service, Characteristic and Descriptor have been renamed to RemoteService, RemoteCharacteristic and RemoteDescriptor respectively. Their old names have now been reserved for an interface that spans both Server (Local) and Client (Remote) implementations.
  • Similarly Device is now named ConnectableDevice. From the server side it is called ConnectedDevice.
  • BaseBluetoothBuilder.create has been deprecated. Use createClient instead.
  • Getting an Attribute from a Flow/List of Attributes by UUID now fails if the UUID is not present. This means that device.services()[uuid] may now fail if called before services have been discovered. Use device.discoveredServices()[uuid] instead if you only want a flow of the service after discovery (this will fail if service is not present), or use devices.services.getOrNull(uuid). This aligns better with other Kotlin APIs
  • RemoteCharacteristic and RemoteDescriptor are no longer Flows of ByteArray. Instead:
    • ReadAction will result in a GattResponse.ReadResponse. If this is ReadSuccess the data read will be available as its value.
    • WriteAction will simply return GattResponse.WriteResponse.
    • Notifications can be enabled by calling RemoteCharacteristic.subscribe {}. The Resulting Subscription must be used to unsubscribe. Calling this method will automatically call enabledNotifications/disableNotifications() if needed.
    • Flow<RemoteCharacteristic?>.value() will automatically subscribe/unsubscribe as the flow is being collected.
    • Flow<RemoteDescriptor?>.value() has been removed.
  • The new Bind api makes it easy to bind an object to a ConnectedDevice, RemoteService, RemoteCharacteristic, or RemoteDescriptor. Simply call object.bind(service) {} Within its closure you can set up observation actions and read or write triggers. The resulting StateFlow will help keep track of any mutations that happened to the object.

Server

  • BaseBluetoothBuilder has a new method createServer to set up a Bluetooth server.
  • The resulting server suspends until Any services and Advertisement set up in its DSL has completed.
  • Close the resulting server once you are dont to stop it.
  • Within the DSL, set up read/write actions to determine the response to a Read/Write request.
  • Within the DSL, make a Characteristic Notifiable by calling notifiable. The resulting LocalCharacteristic.Notifiable has methods for sending notifications to devices. A convenience collectAsNotification/consumeAsNotification api offers the ability to bind flows directly to a characteristic, notifying each subscriber as the flow updates.
  • The BluetoothServer will automatically reboot if Bluetooth is reenabled or permissions are reset. It will restore to the last desired Services and Advertisement.

@ci-splendo
Copy link
Contributor

ci-splendo commented Nov 18, 2025

Code coverage

Total Project Coverage 36.02%

@thoutbeckers thoutbeckers self-assigned this Jan 16, 2026
Copy link
Collaborator

@thoutbeckers thoutbeckers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some small things that can be looked at, other things (efficiency etc) could be for later consideration.

I do also think more extensive tests is of course desirable, but it's worth it to wait for server <-> client mocking perhaps.

fun buildByteArray(order: ByteOrder = ByteOrder.LEAST_SIGNIFICANT_FIRST, block: ByteArrayBuilder.() -> Unit) = ByteArrayBuilderImpl(order).apply(block).bytes

private class ByteArrayBuilderImpl(val order: ByteOrder) : ByteArrayBuilder {
var bytes = byteArrayOf()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation does have some inefficiencies with copying this byte array over and over (both copies in general but in particular object allocation)

I appreciate however that it's behind an interface, and it's probably the best way to create a reference implementation and validate the test suite etc.

The common optimization is to expand the array in chunks, and have a size suggestion for the first chunk. Compare API/implementation of https://docs.oracle.com/javase/8/docs/api/java/io/ByteArrayOutputStream.html. In the implementationcurrentByte would be an index pointer in this case, and when you actually write you check the index range to see if you need to add another chunck (I think it's common to double)

Also, converting other values to ByteArray (like Long.toByteArray) could fairly easily be modified to (also) support writing into an existing an existing ByteArray, again avoiding copies.

If the user size hints the chunk correctly (often in such scenarions it's easy to know) and you take care to only expand the array when there is an actual mutation past the boundry you can construct the byte array with 0 copies. In application code you can add a warning if your eventual size does not match your hint so you can build this robustly too.

Alternetly/additionally, a secondary fixed size builder could be considered (if the size is known ahead of time you might as well fail if it's not that size).

To be clear, I do support keeping this implementation in any case. Whether we add a (usually) more optimal one can be up for debate. It seems a bit nitty but even for small byte structures it's easily an order of magnitude less objects.

I think with the serialization stuff we probably can calculate a good "hint" or even know when the result is fixed size.

@@ -0,0 +1,23 @@
package com.splendo.kaluga.base.utils

infix fun Byte.shl(bitCount: Int) = toUByte().shl(bitCount).toByte()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK these methods are free (no runtime cost), but have you considered just going UByteBuilder by default?

Switch this value to use the location permission on Android when using bluetooth.
*/
const val useBluetoothForLocation = false
const val USE_LOCATION_FOR_BLUETOOTH = true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be false by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted, but note that since the Example also demoes beacons, which do need the location permission, we cannot add neverForLocation and the example will just not scan anything.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't actually derive location from the beacons.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not having neverForLocation is weird though, the example seemed to work without this. Maybe missed commiting it on my side.

val captor = AnyOrNullCaptor<DeviceAction<*>>()
connectionManager.performActionMock.verifyWithin(value = captor)
assertIs<DeviceAction.Notification.Enable>(captor.lastCaptured)
yield()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the cause of having to do these random yields is probably still using BluetoothFlowTest and mainAction, something from the times of not having background dispatchers on iOS.

),
) {
test {
assertEquals(Double.NaN, it)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not the most used API but could probably use a (value) type in a 2.0 API.

) : IOSServerState(),
ServerState.AwaitingBluetoothEnabled {

private val serverQueue = dispatch_queue_create("BluetoothServer", null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not be raised up, maybe as a lazy field? AFAIK these queues will not autoclose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They should auto close. There is also no way of closing them manually as dispatch_release has been deprecated and Apple explicitly says not to use it (anymore)

@Daeda88 Daeda88 mentioned this pull request Jan 29, 2026
@Daeda88
Copy link
Contributor Author

Daeda88 commented Jan 29, 2026

@thoutbeckers the most pressing issues have been fixed here #925

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants