Connect the ACK characteristic to Android GATT and JNI

This commit is contained in:
evgeny committed 2026-10-29 22:13:46 +01:00
1 parent e7adcbe139
commit d4182c22cd
2 files changed
+47 -29

No files matched your search

@@ -22,6 +22,7 @@ object BleRadio {
private val rxUuid = UUID.fromString("f61a9021-4abc-45ac-98e1-761df4c13811")
private val txUuid = UUID.fromString("f61a9022-4abc-45ac-98e1-761df4c13811")
private val inviteUuid = UUID.fromString("f61a9023-4abc-45ac-98e1-761df4c13811")
private val ackUuid = UUID.fromString("f61a9024-4abc-45ac-98e1-761df4c13811")
private val cccUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
private val thread by lazy { HandlerThread("vibechat-ble").apply { start() } }
private val worker by lazy { Handler(thread.looper) }
@@ -39,6 +40,7 @@ object BleRadio {
private var server: BluetoothGattServer? = null
private var tx: BluetoothGattCharacteristic? = null
private var invite: BluetoothGattCharacteristic? = null
private var ack: BluetoothGattCharacteristic? = null
private var scan: ScanCallback? = null
private var scanner: BluetoothLeScanner? = null
private var advertisement: AdvertiseCallback? = null
@@ -51,7 +53,8 @@ object BleRadio {
private val incomingMtu = mutableMapOf<String, Int>()
private data class Link(val token: Long, val device: BluetoothDevice, val outgoing: Boolean,
var gatt: BluetoothGatt? = null, var rx: BluetoothGattCharacteristic? = null,
var invite: BluetoothGattCharacteristic? = null, var subscriptions: Int = 0, var channel: Int = 0,
var invite: BluetoothGattCharacteristic? = null, var ack: BluetoothGattCharacteristic? = null,
var subscriptions: Int = 0, var channel: Int = 0,
var mtu: Int = 23, var opened: Boolean = false, var pending: ByteArray? = null, var writing: Boolean = false,
var busySince: Long = 0,
var priorityGatt: BluetoothGatt? = null) // Клиентский handle того же ACL: server API не имеет requestConnectionPriority.
@@ -90,6 +93,7 @@ object BleRadio {
}
}
private fun log(level: String, text: String) { LogManager.addLog(level, "discovery", "BLE $text") }
private fun wireChannel(uuid: UUID): Int = when (uuid) { rxUuid, txUuid -> 0; inviteUuid -> 1; ackUuid -> 2; else -> -1 }
private fun emit(type: Int, token: Long = 0, arg: Int = 0, text: String = "", data: ByteArray = byteArrayOf()) {
nativeEvent(generation, token, type, arg, text, data)
}
@@ -124,7 +128,7 @@ object BleRadio {
5 -> {
val link = checkNotNull(links[token]) { "send unknown link=$token" }
check(link.pending == null) { "send already pending link=$token" }
check(arg in 0..1) { "Invalid BLE channel=$arg" }
check(arg in 0..2) { "Invalid BLE channel=$arg" }
link.channel = arg; link.pending = bytes
pump()
}
@@ -174,7 +178,7 @@ object BleRadio {
preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) = post {
val expected = if (descriptor.characteristic.uuid == txUuid) BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
else BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
val valid = incoming && descriptor.uuid == cccUuid && descriptor.characteristic.uuid in listOf(txUuid, inviteUuid) &&
val valid = incoming && descriptor.uuid == cccUuid && descriptor.characteristic.uuid in listOf(txUuid, inviteUuid, ackUuid) &&
!preparedWrite && offset == 0 && value.contentEquals(expected)
var link = links.values.find { !it.outgoing && it.device.address == device.address }
val accepted = valid && (link != null || links.size < 16)
@@ -182,7 +186,7 @@ object BleRadio {
if (accepted && link == null) {
link = Link(--sequence, device, false, mtu = incomingMtu[device.address] ?: 23)
links[link.token] = link
emit(9, link.token, text = device.address) // Core ограничивает время ожидания обеих CCC.
emit(9, link.token, text = device.address) // Core ограничивает время ожидания всех трёх CCC.
preferFastPhy(device, null)
tuneIncomingPriority(link)
} else if (!accepted) {
@@ -192,9 +196,9 @@ object BleRadio {
if (link != null) close(link, "subscription disabled")
}
if (accepted && link != null) {
link.subscriptions = link.subscriptions or if (descriptor.characteristic.uuid == txUuid) 1 else 2
link.subscriptions = link.subscriptions or (1 shl wireChannel(descriptor.characteristic.uuid))
log("DEBUG", "Server subscribed link=${link.token} characteristic=${descriptor.characteristic.uuid} mode=${value[0]}")
if (link.subscriptions == 3 && !link.opened) {
if (link.subscriptions == 7 && !link.opened) {
link.opened = true; emit(2, link.token, link.mtu, device.address)
}
}
@@ -202,9 +206,9 @@ object BleRadio {
override fun onCharacteristicWriteRequest(device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic,
preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) = post {
val link = links.values.find { !it.outgoing && it.device.address == device.address }
val valid = link != null && link.opened && characteristic.uuid in listOf(rxUuid, inviteUuid) &&
!preparedWrite && responseNeeded == (characteristic.uuid == inviteUuid) && offset == 0
if (valid) emit(3, link!!.token, arg = if (characteristic.uuid == inviteUuid) 1 else 0, data = value.copyOf())
val valid = link != null && link.opened && characteristic.uuid in listOf(rxUuid, inviteUuid, ackUuid) &&
!preparedWrite && responseNeeded == (characteristic.uuid != rxUuid) && offset == 0
if (valid) emit(3, link!!.token, arg = wireChannel(characteristic.uuid), data = value.copyOf())
if (responseNeeded) respond(device, requestId, if (valid) 0 else BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED)
if (!valid) log("WARN", "Write rejected address=${device.address} characteristic=${characteristic.uuid} " +
"bytes=${value.size} response=$responseNeeded prepared=$preparedWrite offset=$offset")
@@ -248,6 +252,12 @@ object BleRadio {
addDescriptor(BluetoothGattDescriptor(cccUuid, BluetoothGattDescriptor.PERMISSION_WRITE))
}
service.addCharacteristic(invite)
ack = BluetoothGattCharacteristic(ackUuid,
BluetoothGattCharacteristic.PROPERTY_WRITE or BluetoothGattCharacteristic.PROPERTY_INDICATE,
BluetoothGattCharacteristic.PERMISSION_WRITE).apply {
addDescriptor(BluetoothGattDescriptor(cccUuid, BluetoothGattDescriptor.PERMISSION_WRITE))
}
service.addCharacteristic(ack)
check(server!!.addService(service)) { "addService refused" }
}
}
@@ -381,11 +391,13 @@ object BleRadio {
val ccc = characteristic?.getDescriptor(cccUuid)
link.rx = service?.getCharacteristic(rxUuid)
link.invite = service?.getCharacteristic(inviteUuid)
link.ack = service?.getCharacteristic(ackUuid)
log("DEBUG", "Client discovery link=$token status=$status service=${service != null} " +
"rx_properties=${link.rx?.properties} tx_properties=${characteristic?.properties} invite_properties=${link.invite?.properties}")
if (status != 0 || ccc == null || link.rx == null || link.invite == null) {
"rx_properties=${link.rx?.properties} tx_properties=${characteristic?.properties} " +
"invite_properties=${link.invite?.properties} ack_properties=${link.ack?.properties}")
if (status != 0 || ccc == null || link.rx == null || link.invite == null || link.ack == null) {
close(link, "service discovery failed status=$status service=${service != null} CCC=${ccc != null} " +
"RX=${link.rx != null} INVITE=${link.invite != null}"); return@dispatch
"RX=${link.rx != null} INVITE=${link.invite != null} ACK=${link.ack != null}"); return@dispatch
}
if (link.rx!!.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE == 0) {
close(link, "RX lacks Write Without Response properties=${link.rx!!.properties}"); return@dispatch
@@ -393,6 +405,12 @@ object BleRadio {
if (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY == 0) {
close(link, "TX lacks Notification properties=${characteristic.properties}"); return@dispatch
}
val controlProperties = BluetoothGattCharacteristic.PROPERTY_WRITE or BluetoothGattCharacteristic.PROPERTY_INDICATE
for (control in listOf(checkNotNull(link.invite), checkNotNull(link.ack))) {
if (control.properties and controlProperties != controlProperties || control.getDescriptor(cccUuid) == null) {
close(link, "Control characteristic invalid uuid=${control.uuid} properties=${control.properties}"); return@dispatch
}
}
if (!gatt.setCharacteristicNotification(characteristic, true)) {
close(link, "DATA local subscription refused characteristic=${characteristic.uuid}"); return@dispatch
}
@@ -403,29 +421,29 @@ object BleRadio {
}
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) = dispatch(gatt) {
if (status != 0) close(link, "CCC status=$status")
else if (descriptor.characteristic.uuid == txUuid) {
val characteristic = link.invite
else if (descriptor.characteristic.uuid in listOf(txUuid, inviteUuid)) {
val characteristic = if (descriptor.characteristic.uuid == txUuid) link.invite else link.ack
val ccc = characteristic?.getDescriptor(cccUuid)
if (ccc == null || !gatt.setCharacteristicNotification(characteristic, true)) {
close(link, "invitation subscription unavailable"); return@dispatch
close(link, "control subscription unavailable after=${descriptor.characteristic.uuid}"); return@dispatch
}
val result = if (Build.VERSION.SDK_INT >= 33) gatt.writeDescriptor(ccc, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)
else { ccc.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE; if (gatt.writeDescriptor(ccc)) 0 else -1 }
if (result != 0) close(link, "INVITE CCC write refused status=$result")
} else if (descriptor.characteristic.uuid == inviteUuid) { link.opened = true; emit(2, token, link.mtu, address) }
if (result != 0) close(link, "Control CCC write refused uuid=${characteristic?.uuid} status=$result")
} else if (descriptor.characteristic.uuid == ackUuid) { link.opened = true; emit(2, token, link.mtu, address) }
else close(link, "unexpected CCC completion")
}
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) = dispatch(gatt) {
complete(link, status)
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) = dispatch(gatt) {
if (characteristic.uuid in listOf(txUuid, inviteUuid))
emit(3, token, arg = if (characteristic.uuid == inviteUuid) 1 else 0, data = value.copyOf())
if (characteristic.uuid in listOf(txUuid, inviteUuid, ackUuid))
emit(3, token, arg = wireChannel(characteristic.uuid), data = value.copyOf())
}
@Deprecated("API < 33")
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) = dispatch(gatt) {
if (Build.VERSION.SDK_INT < 33 && characteristic.uuid in listOf(txUuid, inviteUuid))
emit(3, token, arg = if (characteristic.uuid == inviteUuid) 1 else 0, data = characteristic.value.copyOf())
if (Build.VERSION.SDK_INT < 33 && characteristic.uuid in listOf(txUuid, inviteUuid, ackUuid))
emit(3, token, arg = wireChannel(characteristic.uuid), data = characteristic.value.copyOf())
}
}
link.gatt = checkNotNull(link.device.connectGatt(context, false, callback, BluetoothDevice.TRANSPORT_LE,
@@ -439,8 +457,8 @@ object BleRadio {
if (!link.outgoing && writer != null) continue
val bytes = checkNotNull(link.pending)
val status = if (link.outgoing) {
val characteristic = checkNotNull(if (link.channel == 1) link.invite else link.rx)
val type = if (link.channel == 1) BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
val characteristic = checkNotNull(when (link.channel) { 0 -> link.rx; 1 -> link.invite; else -> link.ack })
val type = if (link.channel != 0) BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
else BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
if (Build.VERSION.SDK_INT >= 33) link.gatt!!.writeCharacteristic(characteristic, bytes, type)
else {
@@ -448,11 +466,11 @@ object BleRadio {
if (link.gatt!!.writeCharacteristic(characteristic)) 0 else -1
}
} else {
val characteristic = checkNotNull(if (link.channel == 1) invite else tx)
if (Build.VERSION.SDK_INT >= 33) server!!.notifyCharacteristicChanged(link.device, characteristic, link.channel == 1, bytes)
val characteristic = checkNotNull(when (link.channel) { 0 -> tx; 1 -> invite; else -> ack })
if (Build.VERSION.SDK_INT >= 33) server!!.notifyCharacteristicChanged(link.device, characteristic, link.channel != 0, bytes)
else {
characteristic.value = bytes
if (server!!.notifyCharacteristicChanged(link.device, characteristic, link.channel == 1)) 0 else -1
if (server!!.notifyCharacteristicChanged(link.device, characteristic, link.channel != 0)) 0 else -1
}
}
if (status == 0) {
@@ -520,7 +538,7 @@ object BleRadio {
advertisement?.let { callback -> advertiser?.let { owner -> cleanup.retain(callback, "advertisement") { owner.stopAdvertising(callback) } } }
server?.let { owner -> cleanup.retain(owner, "GATT server") { owner.close() } }
scan = null; scanner = null; advertisement = null; advertiser = null; server = null
tx = null; invite = null; writer = null; beacon = byteArrayOf(); incomingMtu.clear()
tx = null; invite = null; ack = null; writer = null; beacon = byteArrayOf(); incomingMtu.clear()
drainCleanup()
}
private val cleanupRetry = Runnable {
@@ -135,7 +135,7 @@ static void drain(int fd, void* arg) {
if (l) ble_backend_opened(b->owner, l->id, e.arg);
else command(4, b->generation, e.token, NULL, NULL, 0, 0);
break;
case 3: if (l) ble_backend_received(b->owner, l->id, e.arg, e.data, e.len); break;
case 3: if (l) ble_backend_received(b->owner, l->id, (enum ble_wire_channel)e.arg, e.data, e.len); break;
case 4: if (l) ble_backend_sent(b->owner, l->id, e.arg == 0); break;
case 5: if (l) ble_backend_closed(b->owner, l->id, e.text); break;
case 6: ble_backend_broadcast(b->owner, &ep, e.data, e.len, e.arg); break;
@@ -213,7 +213,7 @@ void ble_backend_close(struct ble_backend* b, ble_link_id id) {
struct android_link* l = by_id(b, id); if (!l) return;
command(4, b->generation, l->token, NULL, NULL, 0, 0); memset(l, 0, sizeof(*l));
}
int ble_backend_send(struct ble_backend* b, ble_link_id id, enum ble_channel channel, const uint8_t* data, size_t len) {
int ble_backend_send(struct ble_backend* b, ble_link_id id, enum ble_wire_channel channel, const uint8_t* data, size_t len) {
struct android_link* l = by_id(b, id);
if (!l) { DEBUG_ERROR(CAT, "BLE Android send unknown link"); return BLE_ERROR; }
return command(5, b->generation, l->token, NULL, data, len, channel);