Skip to content

Commit 51c5bc0

Browse files
committed
add BleCharacteristic.write
1 parent e639ff0 commit 51c5bc0

File tree

8 files changed

+116
-21
lines changed

8 files changed

+116
-21
lines changed

android/src/main/kotlin/com/viam/ble/BlePlugin.kt

+12
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,18 @@ class BlePlugin :
196196
periph.readCharacteristic(serviceId, charId)
197197
}, result)
198198
}
199+
"centralManagerWriteCharacteristic" -> {
200+
val macAddress: String = requireNotNullArgument(call, DEVICE_ID_PARAM_NAME)
201+
val serviceId: String = requireNotNullArgument(call, SERVICE_ID_PARAM_NAME)
202+
val charId: String = requireNotNullArgument(call, CHARACTERISTIC_ID_PARAM_NAME)
203+
val data: ByteArray = requireNotNullArgument(call, DATA_PARAM_NAME)
204+
flutterMethodCallHandler(call.method, {
205+
val periph =
206+
centralManager.getPeripheral(macAddress)
207+
?: throw Exception("peripheral $macAddress not found")
208+
periph.writeCharacteristic(serviceId, charId, data)
209+
}, result)
210+
}
199211
"centralManagerConnectToChannel" -> {
200212
val macAddress: String = requireNotNullArgument(call, DEVICE_ID_PARAM_NAME)
201213
val psm: Int = requireNotNullArgument(call, PSM_PARAM_NAME)

android/src/main/kotlin/com/viam/ble/CentralManager.kt

+15-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import android.content.Context
99
import android.os.ParcelUuid
1010
import android.util.Log
1111
import com.viam.ble.BlePlugin.Companion.TAG
12+
import kotlinx.coroutines.CancellableContinuation
1213
import kotlinx.coroutines.CoroutineScope
1314
import kotlinx.coroutines.Dispatchers
1415
import kotlinx.coroutines.delay
@@ -130,15 +131,22 @@ class CentralManager(
130131
peripheralsMutex.withLock {
131132
peripherals[macAddress] = periph
132133
}
133-
suspendCancellableCoroutine { cont ->
134-
CoroutineScope(Dispatchers.Main).launch {
135-
val contextStrong = context.get()
136-
if (contextStrong == null) {
137-
cont.resumeWithException(Exception("application context no longer available"))
138-
return@launch
134+
try {
135+
suspendCancellableCoroutine { cont: CancellableContinuation<Unit> ->
136+
CoroutineScope(Dispatchers.Main).launch {
137+
val contextStrong = context.get()
138+
if (contextStrong == null) {
139+
cont.resumeWithException(Exception("application context no longer available"))
140+
return@launch
141+
}
142+
periph.connect(contextStrong, cont)
139143
}
140-
periph.connect(contextStrong, cont)
141144
}
145+
} catch (e: Throwable) {
146+
peripheralsMutex.withLock {
147+
peripherals.remove(macAddress)
148+
}
149+
throw e
142150
}
143151
return@withContext periph.discoveredServices
144152
}

android/src/main/kotlin/com/viam/ble/L2CAPChannel.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ class L2CAPChannel(
1616
@Volatile
1717
private var isClosed = false
1818

19-
suspend fun write(message: ByteArray) {
19+
suspend fun write(data: ByteArray) {
2020
if (isClosed) {
2121
throw Exception("channel closed")
2222
}
2323
withContext(Dispatchers.IO) {
2424
writeMutex.withLock {
25-
socket.outputStream.write(message)
25+
socket.outputStream.write(data)
2626
}
2727
}
2828
}

android/src/main/kotlin/com/viam/ble/Peripheral.kt

+37-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.viam.ble
22

3+
import android.annotation.SuppressLint
34
import android.bluetooth.BluetoothDevice
45
import android.bluetooth.BluetoothGatt
56
import android.bluetooth.BluetoothGattCallback
67
import android.bluetooth.BluetoothGattCharacteristic
8+
import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
79
import android.bluetooth.BluetoothManager
810
import android.bluetooth.BluetoothProfile
911
import android.content.Context
@@ -103,6 +105,21 @@ class Peripheral(
103105
return char.value ?: throw Exception("characteristic $charId not yet read")
104106
}
105107

108+
@SuppressLint("MissingPermission")
109+
fun writeCharacteristic(
110+
serviceId: String,
111+
charId: String,
112+
data: ByteArray,
113+
): Boolean {
114+
val svcUUID = UUID.fromString(serviceId)
115+
val charUUID = UUID.fromString(charId)
116+
val svc = gatt?.getService(svcUUID) ?: throw Exception("service $serviceId not found")
117+
val char = svc.getCharacteristic(charUUID) ?: throw Exception("characteristic $charId not found")
118+
char.setValue(data)
119+
char.writeType = WRITE_TYPE_DEFAULT
120+
return gatt?.writeCharacteristic(char) ?: throw Exception("gatt not available")
121+
}
122+
106123
@Throws(SecurityException::class)
107124
suspend fun close() {
108125
if (isClosed) {
@@ -230,27 +247,39 @@ class Peripheral(
230247
currentCharCont?.resume(Unit)
231248
currentCharCont = null
232249
discoveredSvcDoneCount++
250+
val charUUID = characteristic.uuid.toString().lowercase()
251+
val charSvcUUID =
252+
characteristic.service.uuid
253+
.toString()
254+
.lowercase()
233255
if (status != BluetoothGatt.GATT_SUCCESS) {
234-
Log.d(TAG, "error getting characteristic ${characteristic.uuid}: $status")
256+
Log.d(TAG, "error getting characteristic $charUUID: $status")
235257
} else {
236258
if (!discoveredCharacteristics.containsKey(characteristic.service.uuid.toString())) {
237-
discoveredCharacteristics[characteristic.service.uuid.toString()] = mutableSetOf()
259+
discoveredCharacteristics[charSvcUUID] = mutableSetOf()
238260
}
239-
discoveredCharacteristics[characteristic.service.uuid.toString()]?.add(characteristic.uuid.toString())
261+
discoveredCharacteristics[charSvcUUID]?.add(charUUID)
240262
}
241263
if (discoveredSvcDoneCount == neededCharDoneCount) {
242264
_discoveredServices =
243265
gatt.services
244-
.filter { discoveredCharacteristics.containsKey(it.uuid.toString()) }
266+
.filter { discoveredCharacteristics.containsKey(it.uuid.toString().lowercase()) }
245267
.map { svc ->
246268
hashMapOf(
247-
"id" to svc.uuid.toString(),
269+
"id" to svc.uuid.toString().lowercase(),
248270
"characteristics" to
249271
svc.characteristics
250-
.filter { discoveredCharacteristics[it.service.uuid.toString()]!!.contains(it.uuid.toString()) }
251-
.map { char ->
272+
.filter {
273+
discoveredCharacteristics[
274+
it.service.uuid
275+
.toString()
276+
.lowercase(),
277+
]!!.contains(
278+
it.uuid.toString().lowercase(),
279+
)
280+
}.map { char ->
252281
hashMapOf(
253-
"id" to char.uuid.toString(),
282+
"id" to char.uuid.toString().lowercase(),
254283
)
255284
},
256285
)

ios/Classes/BlePlugin.swift

+31-2
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,10 @@ public class BlePlugin: NSObject, FlutterPlugin {
190190
return
191191
}
192192
let service = periph.actualPeripheral.services?.first(where: { elem in
193-
return elem.uuid.uuidString == svcId
193+
return elem.uuid.uuidString.lowercased() == svcId.lowercased()
194194
})
195195
let characteristic = service?.characteristics?.first(where: { char in
196-
return char.uuid.uuidString == charId
196+
return char.uuid.uuidString.lowercased() == charId.lowercased()
197197
})
198198

199199
if let characteristic {
@@ -208,6 +208,35 @@ public class BlePlugin: NSObject, FlutterPlugin {
208208
throwInvalidArguments(call, result)
209209
}
210210
return
211+
case "centralManagerWriteCharacteristic":
212+
if let arguments = call.arguments as? [String: Any],
213+
let deviceId = arguments[kDeviceIdParamName] as? String,
214+
let svcId = arguments[kServiceIdParamName] as? String,
215+
let charId = arguments[kCharacteristicIdParamName] as? String,
216+
let data = arguments[kDataParamName] as? FlutterStandardTypedData {
217+
guard let periph = try CentralManager.singleton.getPeripheral(deviceId) else {
218+
throwInvalidArguments(call, result, "peripheral not found")
219+
return
220+
}
221+
let service = periph.actualPeripheral.services?.first(where: { elem in
222+
return elem.uuid.uuidString.lowercased() == svcId.lowercased()
223+
})
224+
let characteristic = service?.characteristics?.first(where: { char in
225+
return char.uuid.uuidString.lowercased() == charId.lowercased()
226+
})
227+
228+
if let characteristic {
229+
flutterMethodCallHandler(
230+
methodName: call.method,
231+
call: { [weak periph] in return periph?
232+
.writeCharacteristic(characteristic, data.data) }, result: result)
233+
return
234+
}
235+
result(createFltuterError(call.method, withMessage: "failed to find given characteristic to write"))
236+
} else {
237+
throwInvalidArguments(call, result)
238+
}
239+
return
211240
case "centralManagerDisconnectFromDevice":
212241
if let arguments = call.arguments as? [String: Any],
213242
let deviceId = arguments[kDeviceIdParamName] as? String {

ios/Classes/CentralManager.swift

+1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ extension CentralManager: CBCentralManagerDelegate {
154154

155155
public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
156156
if let periph = (peripheralsLock.withLock { peripherals[peripheral.identifier.uuidString] }) {
157+
peripheralsLock.withLock { peripherals[peripheral.identifier.uuidString] = nil }
157158
periph.failedConnecting(withError: error ?? RuntimeError("failed to connect to peripheral"))
158159
}
159160
}

ios/Classes/Peripheral.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ public class Peripheral: NSObject {
7272
}
7373
}
7474

75+
public func writeCharacteristic(_ characteristic: CBCharacteristic, _ data: Data) {
76+
self.actualPeripheral.writeValue(data, for: characteristic, type: CBCharacteristicWriteType.withResponse)
77+
}
78+
7579
public func connectToL2CAPChannel(psm: UInt16) async throws -> Int {
7680
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Int, Error>) in
7781
channelsLock.withLock {
@@ -220,9 +224,9 @@ extension Peripheral: CBPeripheralDelegate {
220224
}
221225
if discoveredSvcDoneCount == neededSvcDoneCount {
222226
discoveredServices = peripheral.services?.map({ CBService in
223-
return ["id": CBService.uuid.uuidString,
227+
return ["id": CBService.uuid.uuidString.lowercased(),
224228
"characteristics": CBService.characteristics?.map({ CBCharacteristic in
225-
return ["id": CBCharacteristic.uuid.uuidString]
229+
return ["id": CBCharacteristic.uuid.uuidString.lowercased()]
226230
}) ?? []]
227231
}) ?? []
228232
contLocks.withLock {

lib/src/ble.dart

+12
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ class BleCharacteristic {
138138

139139
/// Reads the latest value of the characteristic.
140140
Future<Uint8List?> read() => _BlePlatform.instance.centralManagerReadCharacteristic(deviceId, serviceId, id);
141+
142+
/// Writes the given data to the characteristic.
143+
Future<void> write(Uint8List data) => _BlePlatform.instance.centralManagerWriteCharacteristic(deviceId, serviceId, id, data);
141144
}
142145

143146
/// The various states of the BLE adapter.
@@ -330,6 +333,15 @@ class _BlePlatform extends PlatformInterface {
330333
{deviceIdParamName: deviceId, serviceIdParamName: serviceId, characteristicIdParamName: characteristicId});
331334
}
332335

336+
Future<void> centralManagerWriteCharacteristic(String deviceId, String serviceId, String characteristicId, Uint8List data) async {
337+
return methodChannel.invokeMethod<void>('centralManagerWriteCharacteristic', {
338+
deviceIdParamName: deviceId,
339+
serviceIdParamName: serviceId,
340+
characteristicIdParamName: characteristicId,
341+
dataParamName: data,
342+
});
343+
}
344+
333345
Future<L2CapChannel> centralManagerConnectToChannel(String deviceId, int psm) async {
334346
final cid = await methodChannel.invokeMethod<int>('centralManagerConnectToChannel', {deviceIdParamName: deviceId, psmParamName: psm});
335347
if (cid == null) {

0 commit comments

Comments
 (0)