Skip to content

Commit 7aae568

Browse files
committed
Improve
1 parent 9360bec commit 7aae568

File tree

3 files changed

+207
-20
lines changed

3 files changed

+207
-20
lines changed

android/app/src/main/java/betaflight/configurator/protocols/bluetooth/BetaflightBluetoothPlugin.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,14 @@ public void write(PluginCall call) {
300300
return;
301301
}
302302

303+
int properties = characteristic.getProperties();
304+
boolean supportsWrite = (properties & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0;
305+
boolean supportsWriteNoResponse = (properties & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0;
306+
if (!supportsWrite && !supportsWriteNoResponse) {
307+
call.reject("Characteristic does not support write operations");
308+
return;
309+
}
310+
303311
byte[] payload;
304312
try {
305313
payload = decodePayload(value, encoding);
@@ -308,7 +316,15 @@ public void write(PluginCall call) {
308316
return;
309317
}
310318

311-
int writeType = withoutResponse ? BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE : BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
319+
int writeType;
320+
if (withoutResponse) {
321+
writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE;
322+
} else if (!supportsWrite && supportsWriteNoResponse) {
323+
writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE;
324+
Log.d(TAG, "Characteristic " + characteristicUuid + " does not support acknowledged writes; falling back to no response mode");
325+
} else {
326+
writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
327+
}
312328

313329
boolean submitted = submitWrite(gatt, characteristic, payload, writeType);
314330
if (!submitted) {
@@ -924,7 +940,17 @@ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
924940
logGattLayout("Services discovered", gatt);
925941
JSArray services = new JSArray();
926942
for (BluetoothGattService service : gatt.getServices()) {
927-
services.put(service.getUuid().toString());
943+
JSObject servicePayload = new JSObject();
944+
servicePayload.put("uuid", service.getUuid().toString());
945+
JSArray characteristics = new JSArray();
946+
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
947+
JSObject characteristicPayload = new JSObject();
948+
characteristicPayload.put("uuid", characteristic.getUuid().toString());
949+
characteristicPayload.put("properties", characteristic.getProperties());
950+
characteristics.put(characteristicPayload);
951+
}
952+
servicePayload.put("characteristics", characteristics);
953+
services.put(servicePayload);
928954
}
929955
JSObject payload = new JSObject();
930956
payload.put("deviceId", connectedDeviceId);

src/js/protocols/CapacitorBluetooth.js

Lines changed: 179 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ const logHead = "[CAPACITOR-BLUETOOTH]";
99

1010
const toLowerUuid = (uuid) => uuid?.toLowerCase?.() ?? uuid;
1111

12+
const GATT_PROPERTIES = {
13+
READ: 0x02,
14+
WRITE_NO_RESPONSE: 0x04,
15+
WRITE: 0x08,
16+
NOTIFY: 0x10,
17+
INDICATE: 0x20,
18+
};
19+
1220
const toUint8Array = (data) => {
1321
if (data instanceof Uint8Array) {
1422
return data;
@@ -82,6 +90,9 @@ class CapacitorBluetooth extends EventTarget {
8290
this.disconnectHandled = true;
8391
this.nativeListeners = [];
8492
this.nativeListenersReady = false;
93+
this.discoveredServices = new Map();
94+
this.pendingServiceResolvers = new Map();
95+
this.serviceResolutionTimeoutMs = 8000;
8596

8697
this.handleNotification = this.handleNotification.bind(this);
8798
this.handleRemoteDisconnect = this.handleRemoteDisconnect.bind(this);
@@ -213,13 +224,11 @@ class CapacitorBluetooth extends EventTarget {
213224
this.device = requestedDevice;
214225
this.logHead = logHead;
215226

216-
const deviceDescription = this.resolveDeviceDescription(requestedDevice.device); // || { serviceUuid: info.service };
227+
const deviceDescription = this.resolveDeviceDescription(requestedDevice.device) ?? null;
217228
if (!deviceDescription) {
218-
console.error(`${logHead} Unsupported device: missing known service UUID`);
219-
this.openRequested = false;
220-
gui_log(i18n.getMessage("bluetoothConnectionError", ["Unsupported device"]));
221-
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
222-
return false;
229+
console.warn(
230+
`${logHead} No static profile for device ${requestedDevice.device?.name ?? requestedDevice.path}`,
231+
);
223232
}
224233

225234
this.deviceDescription = deviceDescription;
@@ -235,6 +244,17 @@ class CapacitorBluetooth extends EventTarget {
235244
void this.handleRemoteDisconnect(deviceId);
236245
});
237246

247+
const effectiveDescription = await this.waitForDeviceCharacteristics(
248+
requestedDevice.device.deviceId,
249+
deviceDescription,
250+
);
251+
252+
if (!this.hasCompleteCharacteristics(effectiveDescription)) {
253+
throw new Error("Unable to determine BLE service and characteristics for device");
254+
}
255+
256+
this.deviceDescription = effectiveDescription;
257+
238258
gui_log(i18n.getMessage("bluetoothConnected", [requestedDevice.device.name]));
239259
await this.startNotifications();
240260

@@ -258,7 +278,7 @@ class CapacitorBluetooth extends EventTarget {
258278
gui_log(i18n.getMessage("bluetoothConnectionError", [error]));
259279
this.openRequested = false;
260280
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
261-
this.cleanupConnectionState();
281+
this.cleanupConnectionState(requestedDevice.device?.deviceId ?? null);
262282
return false;
263283
}
264284
}
@@ -333,7 +353,7 @@ class CapacitorBluetooth extends EventTarget {
333353

334354
this.closeRequested = true;
335355

336-
const targetDeviceId = this.device?.device?.deviceId ?? this.connectionId ?? "unknown";
356+
const targetDeviceId = this.device?.device?.deviceId ?? this.connectionId ?? null;
337357

338358
try {
339359
await this.stopNotifications();
@@ -407,17 +427,18 @@ class CapacitorBluetooth extends EventTarget {
407427
}
408428

409429
this.disconnectHandled = true;
430+
const activeDeviceId = deviceId ?? this.device?.device?.deviceId ?? null;
410431

411-
console.warn(`${logHead} Device ${deviceId} disconnected`);
432+
console.warn(`${logHead} Device ${activeDeviceId ?? "unknown"} disconnected`);
412433
await this.stopNotifications();
413434
console.log(
414435
`${logHead} Connection with ID: ${this.connectionId} closed, Sent: ${this.bytesSent} bytes, Received: ${this.bytesReceived} bytes`,
415436
);
416-
this.cleanupConnectionState();
437+
this.cleanupConnectionState(activeDeviceId);
417438
this.dispatchEvent(new CustomEvent("disconnect", { detail: true }));
418439
}
419440

420-
cleanupConnectionState() {
441+
cleanupConnectionState(deviceId = null) {
421442
this.connected = false;
422443
this.connectionId = null;
423444
this.bitrate = 0;
@@ -435,6 +456,148 @@ class CapacitorBluetooth extends EventTarget {
435456
this.openRequested = false;
436457
this.closeRequested = false;
437458
this.disconnectHandled = true;
459+
if (deviceId) {
460+
this.clearPendingServiceResolver(deviceId);
461+
}
462+
}
463+
464+
hasCompleteCharacteristics(description) {
465+
return Boolean(description?.serviceUuid && description?.writeCharacteristic && description?.readCharacteristic);
466+
}
467+
468+
async waitForDeviceCharacteristics(deviceId, fallbackDescription) {
469+
if (!deviceId) {
470+
return fallbackDescription ?? null;
471+
}
472+
473+
const cached = this.getCachedDeviceDescription(deviceId, fallbackDescription);
474+
if (this.hasCompleteCharacteristics(cached)) {
475+
return cached;
476+
}
477+
478+
return this.createPendingServicePromise(deviceId, cached ?? fallbackDescription ?? null);
479+
}
480+
481+
getCachedDeviceDescription(deviceId, fallbackDescription) {
482+
const services = this.discoveredServices.get(deviceId);
483+
if (!services) {
484+
return fallbackDescription ?? null;
485+
}
486+
const derived = this.buildDescriptionFromServices(services);
487+
if (!derived) {
488+
return fallbackDescription ?? null;
489+
}
490+
return this.mergeDeviceDescription(fallbackDescription, derived);
491+
}
492+
493+
mergeDeviceDescription(baseDescription, overrideDescription) {
494+
if (!baseDescription) {
495+
return overrideDescription ? { ...overrideDescription } : null;
496+
}
497+
if (!overrideDescription) {
498+
return { ...baseDescription };
499+
}
500+
return { ...baseDescription, ...overrideDescription };
501+
}
502+
503+
createPendingServicePromise(deviceId, fallbackDescription) {
504+
const existing = this.pendingServiceResolvers.get(deviceId);
505+
if (existing) {
506+
return existing.promise;
507+
}
508+
509+
const pending = {
510+
fallback: fallbackDescription ? { ...fallbackDescription } : null,
511+
};
512+
pending.promise = new Promise((resolve) => {
513+
pending.resolve = (description) => {
514+
clearTimeout(pending.timeout);
515+
this.pendingServiceResolvers.delete(deviceId);
516+
if (description) {
517+
resolve(description);
518+
} else {
519+
resolve(pending.fallback);
520+
}
521+
};
522+
});
523+
pending.timeout = setTimeout(() => pending.resolve(null), this.serviceResolutionTimeoutMs);
524+
this.pendingServiceResolvers.set(deviceId, pending);
525+
return pending.promise;
526+
}
527+
528+
buildDescriptionFromServices(services = []) {
529+
for (const service of services) {
530+
if (!service) {
531+
continue;
532+
}
533+
const serviceUuid = toLowerUuid(service.uuid ?? service.serviceUuid);
534+
if (!serviceUuid) {
535+
continue;
536+
}
537+
const characteristics = Array.isArray(service.characteristics) ? service.characteristics : [];
538+
if (characteristics.length === 0) {
539+
continue;
540+
}
541+
const writeCandidate = characteristics.find((characteristic) =>
542+
this.characteristicSupportsWrite(characteristic?.properties),
543+
);
544+
if (!writeCandidate?.uuid) {
545+
continue;
546+
}
547+
const notifyCandidate =
548+
characteristics.find((characteristic) =>
549+
this.characteristicSupportsNotify(characteristic?.properties),
550+
) || writeCandidate;
551+
if (!notifyCandidate?.uuid) {
552+
continue;
553+
}
554+
return {
555+
serviceUuid,
556+
writeCharacteristic: toLowerUuid(writeCandidate.uuid),
557+
readCharacteristic: toLowerUuid(notifyCandidate.uuid),
558+
};
559+
}
560+
return null;
561+
}
562+
563+
characteristicSupportsWrite(properties = 0) {
564+
return (properties & (GATT_PROPERTIES.WRITE | GATT_PROPERTIES.WRITE_NO_RESPONSE)) !== 0;
565+
}
566+
567+
characteristicSupportsNotify(properties = 0) {
568+
return (properties & (GATT_PROPERTIES.NOTIFY | GATT_PROPERTIES.INDICATE)) !== 0;
569+
}
570+
571+
handleServicesEvent(event) {
572+
const deviceId = event?.deviceId;
573+
const services = event?.services;
574+
if (!deviceId || !Array.isArray(services)) {
575+
return;
576+
}
577+
578+
this.discoveredServices.set(deviceId, services);
579+
const derived = this.buildDescriptionFromServices(services);
580+
const pending = this.pendingServiceResolvers.get(deviceId);
581+
if (pending) {
582+
pending.resolve(this.mergeDeviceDescription(pending.fallback, derived));
583+
}
584+
585+
if (derived && this.device?.device?.deviceId === deviceId) {
586+
this.deviceDescription = this.mergeDeviceDescription(this.deviceDescription, derived);
587+
}
588+
}
589+
590+
clearPendingServiceResolver(deviceId) {
591+
if (!deviceId) {
592+
return;
593+
}
594+
const pending = this.pendingServiceResolvers.get(deviceId);
595+
if (pending) {
596+
clearTimeout(pending.timeout);
597+
this.pendingServiceResolvers.delete(deviceId);
598+
pending.resolve(null);
599+
}
600+
this.discoveredServices.delete(deviceId);
438601
}
439602

440603
attachNativeListeners() {
@@ -480,9 +643,13 @@ class CapacitorBluetooth extends EventTarget {
480643

481644
registerListener("connectionState", (event) => {
482645
if (event?.connected === false) {
483-
void this.handleRemoteDisconnect(event.deviceId ?? "unknown");
646+
void this.handleRemoteDisconnect(event.deviceId ?? null);
484647
}
485648
});
649+
650+
registerListener("services", (event) => {
651+
this.handleServicesEvent(event);
652+
});
486653
}
487654
}
488655

src/js/protocols/devices.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,6 @@ export const bluetoothDevices = [
4848
writeCharacteristic: "0000db33-0000-1000-8000-00805f9b34fb",
4949
readCharacteristic: "0000db34-0000-1000-8000-00805f9b34fb",
5050
},
51-
{
52-
name: "SpeedyBee F7V3",
53-
serviceUuid: "0000abf0-0000-1000-8000-00805f9b34fb",
54-
writeCharacteristic: "0000abf1-0000-1000-8000-00805f9b34fb",
55-
readCharacteristic: "0000abf2-0000-1000-8000-00805f9b34fb",
56-
},
5751
];
5852

5953
export const serialDevices = [

0 commit comments

Comments
 (0)