diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/BleRadio.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/BleRadio.kt index 140c2f03..579cb655 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/BleRadio.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/BleRadio.kt @@ -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() 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 { diff --git a/tools/chatgui-android/jni_bridge/android_ble.c b/tools/chatgui-android/jni_bridge/android_ble.c index 60fa0054..b56e2f6c 100644 --- a/tools/chatgui-android/jni_bridge/android_ble.c +++ b/tools/chatgui-android/jni_bridge/android_ble.c @@ -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);