mirror of
https://github.com/betaflight/betaflight-configurator.git
synced 2025-07-24 16:55:24 +03:00
Add bluetooth support (#4024)
* Add bluetooth support * Add new files * CLI now works over BT * Fix MSP over bluetooth * Cleanup * More cleanup * Fix bind for disconnect * Rename port to device * Reboot does not trigger event gattserverdisconnected * Fix dual permission request * Increase minimum MSP timeout for PWA * Small refactor * Reboot * Do not crash when bluetooth flag is disabled * Cleanup excessive logging * Abstract navigator
This commit is contained in:
parent
91dab8750e
commit
5a3f06f890
9 changed files with 512 additions and 38 deletions
|
@ -84,9 +84,22 @@
|
|||
"description": "Configure a Virtual Flight Controller without the need of a physical FC."
|
||||
},
|
||||
"portsSelectPermission": {
|
||||
"message": "--- I can't find my device ---",
|
||||
"message": "--- I can't find my USB device ---",
|
||||
"description": "Option in the port selection dropdown to allow the user to give permissions to the system to access the device."
|
||||
},
|
||||
"portsSelectPermissionBluetooth": {
|
||||
"message": "--- I can't find my Bluetooth device---",
|
||||
"description": "Option in the port selection dropdown to allow the user to give permissions to the system to access a Bluetooth device."
|
||||
},
|
||||
"bluetoothConnected": {
|
||||
"message": "Connected to Bluetooth device: $1"
|
||||
},
|
||||
"bluetoothConnectionType": {
|
||||
"message": "Bluetooth connection type: $1"
|
||||
},
|
||||
"bluetoothConnectionError": {
|
||||
"message": "Bluetooth error: $1"
|
||||
},
|
||||
"virtualMSPVersion": {
|
||||
"message": "Virtual Firmware Version"
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
/>
|
||||
<PortsInput
|
||||
:value="value"
|
||||
:connected-bluetooth-devices="connectedBluetoothDevices"
|
||||
:connected-serial-devices="connectedSerialDevices"
|
||||
:connected-usb-devices="connectedUsbDevices"
|
||||
:disabled="disabled"
|
||||
|
@ -44,7 +45,7 @@ export default {
|
|||
autoConnect: true,
|
||||
}),
|
||||
},
|
||||
connectedUsbDevices: {
|
||||
connectedBluetoothDevices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
@ -52,6 +53,10 @@ export default {
|
|||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
connectedUsbDevices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showVirtualOption: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
|
|
@ -30,6 +30,13 @@
|
|||
>
|
||||
{{ $t("portsSelectVirtual") }}
|
||||
</option>
|
||||
<option
|
||||
v-for="connectedBluetoothDevice in connectedBluetoothDevices"
|
||||
:key="connectedBluetoothDevice.path"
|
||||
:value="connectedBluetoothDevice.path"
|
||||
>
|
||||
{{ connectedBluetoothDevice.displayName }}
|
||||
</option>
|
||||
<option
|
||||
v-for="connectedSerialDevice in connectedSerialDevices"
|
||||
:key="connectedSerialDevice.path"
|
||||
|
@ -47,6 +54,9 @@
|
|||
<option value="requestpermission">
|
||||
{{ $t("portsSelectPermission") }}
|
||||
</option>
|
||||
<option value="requestpermissionbluetooth">
|
||||
{{ $t("portsSelectPermissionBluetooth") }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="auto-connect-and-baud">
|
||||
|
@ -114,6 +124,10 @@ export default {
|
|||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
connectedBluetoothDevices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@ -154,6 +168,8 @@ export default {
|
|||
onChangePort(event) {
|
||||
if (event.target.value === 'requestpermission') {
|
||||
EventBus.$emit('ports-input:request-permission');
|
||||
} else if (event.target.value === 'requestpermissionbluetooth') {
|
||||
EventBus.$emit('ports-input:request-permission-bluetooth');
|
||||
} else {
|
||||
EventBus.$emit('ports-input:change', event.target.value);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import GUI from "./gui.js";
|
||||
import CONFIGURATOR from "./data_storage.js";
|
||||
import serialNWJS from "./serial.js";
|
||||
import serialWeb from "./webSerial.js";
|
||||
import { isWeb } from "./utils/isWeb.js";
|
||||
import { serialShim } from "./serial_shim.js";
|
||||
|
||||
const serial = serialShim();
|
||||
let serial = serialShim();
|
||||
|
||||
const MSP = {
|
||||
symbols: {
|
||||
|
@ -57,7 +54,7 @@ const MSP = {
|
|||
packet_error: 0,
|
||||
unsupported: 0,
|
||||
|
||||
MIN_TIMEOUT: 100,
|
||||
MIN_TIMEOUT: 200,
|
||||
MAX_TIMEOUT: 2000,
|
||||
timeout: 200,
|
||||
|
||||
|
@ -309,7 +306,10 @@ const MSP = {
|
|||
return bufferOut;
|
||||
},
|
||||
send_message(code, data, callback_sent, callback_msp, doCallbackOnError) {
|
||||
const connected = isWeb() ? serial.connected : serial.connectionId;
|
||||
// Hack to make BT work
|
||||
serial = serialShim();
|
||||
|
||||
const connected = serial.connected;
|
||||
|
||||
if (code === undefined || !connected || CONFIGURATOR.virtualMode) {
|
||||
if (callback_msp) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { get as getConfig } from "./ConfigStorage";
|
|||
import { EventBus } from "../components/eventBus";
|
||||
import serial from "./webSerial";
|
||||
import usb from "./protocols/webusbdfu";
|
||||
import BT from "./protocols/bluetooth";
|
||||
|
||||
const DEFAULT_PORT = 'noselection';
|
||||
const DEFAULT_BAUDS = 115200;
|
||||
|
@ -9,6 +10,8 @@ const DEFAULT_BAUDS = 115200;
|
|||
const PortHandler = new function () {
|
||||
this.currentSerialPorts = [];
|
||||
this.currentUsbPorts = [];
|
||||
this.currentBluetoothPorts = [];
|
||||
|
||||
this.portPicker = {
|
||||
selectedPort: DEFAULT_PORT,
|
||||
selectedBauds: DEFAULT_BAUDS,
|
||||
|
@ -16,7 +19,10 @@ const PortHandler = new function () {
|
|||
virtualMspVersion: "1.46.0",
|
||||
autoConnect: getConfig('autoConnect', false).autoConnect,
|
||||
};
|
||||
|
||||
this.portPickerDisabled = false;
|
||||
|
||||
this.bluetoothAvailable = false;
|
||||
this.dfuAvailable = false;
|
||||
this.portAvailable = false;
|
||||
this.showAllSerialDevices = false;
|
||||
|
@ -27,14 +33,19 @@ const PortHandler = new function () {
|
|||
|
||||
PortHandler.initialize = function () {
|
||||
|
||||
EventBus.$on('ports-input:request-permission-bluetooth', this.askBluetoothPermissionPort.bind(this));
|
||||
EventBus.$on('ports-input:request-permission', this.askSerialPermissionPort.bind(this));
|
||||
EventBus.$on('ports-input:change', this.onChangeSelectedPort.bind(this));
|
||||
|
||||
BT.addEventListener("addedDevice", (event) => this.addedBluetoothDevice(event.detail));
|
||||
BT.addEventListener("removedDevice", (event) => this.addedBluetoothDevice(event.detail));
|
||||
|
||||
serial.addEventListener("addedDevice", (event) => this.addedSerialDevice(event.detail));
|
||||
serial.addEventListener("removedDevice", (event) => this.removedSerialDevice(event.detail));
|
||||
|
||||
usb.addEventListener("addedDevice", (event) => this.addedUsbDevice(event.detail));
|
||||
|
||||
this.addedBluetoothDevice();
|
||||
this.addedSerialDevice();
|
||||
this.addedUsbDevice();
|
||||
};
|
||||
|
@ -73,6 +84,26 @@ PortHandler.removedSerialDevice = function (device) {
|
|||
});
|
||||
};
|
||||
|
||||
PortHandler.addedBluetoothDevice = function (device) {
|
||||
this.updateCurrentBluetoothPortsList()
|
||||
.then(() => {
|
||||
const selectedPort = this.selectActivePort(device);
|
||||
if (!device || selectedPort === device.path) {
|
||||
// Send this event when the port handler auto selects a new device
|
||||
EventBus.$emit('port-handler:auto-select-bluetooth-device', selectedPort);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
PortHandler.removedBluetoothDevice = function (device) {
|
||||
this.updateCurrentBluetoothPortsList()
|
||||
.then(() => {
|
||||
if (this.portPicker.selectedPort === device.path) {
|
||||
this.selectActivePort();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
PortHandler.addedUsbDevice = function (device) {
|
||||
this.updateCurrentUsbPortsList()
|
||||
.then(() => {
|
||||
|
@ -102,6 +133,15 @@ PortHandler.updateCurrentUsbPortsList = async function () {
|
|||
this.currentUsbPorts = orderedPorts;
|
||||
};
|
||||
|
||||
PortHandler.updateCurrentBluetoothPortsList = async function () {
|
||||
if (BT.bluetooth) {
|
||||
const ports = await BT.getDevices();
|
||||
const orderedPorts = this.sortPorts(ports);
|
||||
this.bluetoothAvailable = orderedPorts.length > 0;
|
||||
this.currentBluetoothPorts = orderedPorts;
|
||||
}
|
||||
};
|
||||
|
||||
PortHandler.sortPorts = function(ports) {
|
||||
return ports.sort(function(a, b) {
|
||||
return a.path.localeCompare(b.path, window.navigator.language, {
|
||||
|
@ -111,6 +151,18 @@ PortHandler.sortPorts = function(ports) {
|
|||
});
|
||||
};
|
||||
|
||||
PortHandler.askBluetoothPermissionPort = function() {
|
||||
if (BT.bluetooth) {
|
||||
BT.requestPermissionDevice()
|
||||
.then((port) => {
|
||||
// When giving permission to a new device, the port is selected in the handleNewDevice method, but if the user
|
||||
// selects a device that had already permission, or cancels the permission request, we need to select the port
|
||||
// so do it here too
|
||||
this.selectActivePort(port);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
PortHandler.askSerialPermissionPort = function() {
|
||||
serial.requestPermissionDevice(this.showAllSerialDevices)
|
||||
.then((port) => {
|
||||
|
@ -136,6 +188,11 @@ PortHandler.selectActivePort = function(suggestedDevice) {
|
|||
selectedPort = this.currentUsbPorts.find(device => device === usb.getConnectedPort());
|
||||
}
|
||||
|
||||
// Return the same that is connected to bluetooth
|
||||
if (BT.device) {
|
||||
selectedPort = this.currentBluetoothPorts.find(device => device === BT.getConnectedPort());
|
||||
}
|
||||
|
||||
// Return the suggested device (the new device that has been detected)
|
||||
if (!selectedPort && suggestedDevice) {
|
||||
selectedPort = suggestedDevice.path;
|
||||
|
@ -157,6 +214,14 @@ PortHandler.selectActivePort = function(suggestedDevice) {
|
|||
}
|
||||
}
|
||||
|
||||
// Return some bluetooth port that is recognized by the filter
|
||||
if (!selectedPort) {
|
||||
selectedPort = this.currentBluetoothPorts.find(device => deviceFilter.some(filter => device.displayName.includes(filter)));
|
||||
if (selectedPort) {
|
||||
selectedPort = selectedPort.path;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the virtual port
|
||||
if (!selectedPort && this.showVirtualMode) {
|
||||
selectedPort = "virtual";
|
||||
|
@ -170,7 +235,7 @@ PortHandler.selectActivePort = function(suggestedDevice) {
|
|||
// Return the default port if no other port was selected
|
||||
this.portPicker.selectedPort = selectedPort || DEFAULT_PORT;
|
||||
|
||||
console.log(`Porthandler automatically selected device is '${this.portPicker.selectedPort}'`);
|
||||
console.log(`[PORTHANDLER] automatically selected device is '${this.portPicker.selectedPort}'`);
|
||||
return selectedPort;
|
||||
};
|
||||
|
||||
|
|
361
src/js/protocols/bluetooth.js
Normal file
361
src/js/protocols/bluetooth.js
Normal file
|
@ -0,0 +1,361 @@
|
|||
import { i18n } from "../localization";
|
||||
import { gui_log } from "../gui_log";
|
||||
|
||||
/* Certain flags needs to be enabled in the browser to use BT
|
||||
*
|
||||
* app.commandLine.appendSwitch('enable-web-bluetooth', "true");
|
||||
* app.commandLine.appendSwitch('disable-hid-blocklist')
|
||||
* app.commandLine.appendSwitch('enable-experimental-web-platform-features');
|
||||
*
|
||||
*/
|
||||
|
||||
const bluetoothDevices = [
|
||||
{ name: "CC2541", serviceUuid: '0000ffe0-0000-1000-8000-00805f9b34fb', writeCharacteristic: '0000ffe1-0000-1000-8000-00805f9b34fb', readCharacteristic: '0000ffe1-0000-1000-8000-00805f9b34fb' },
|
||||
{ name: "HC-05", serviceUuid: '00001101-0000-1000-8000-00805f9b34fb', writeCharacteristic: '00001101-0000-1000-8000-00805f9b34fb', readCharacteristic: '00001101-0000-1000-8000-00805f9b34fb' },
|
||||
{ name: "HM-10", serviceUuid: '0000ffe1-0000-1000-8000-00805f9b34fb', writeCharacteristic: '0000ffe1-0000-1000-8000-00805f9b34fb', readCharacteristic: '0000ffe1-0000-1000-8000-00805f9b34fb' },
|
||||
{ name: "HM-11", serviceUuid: '6e400001-b5a3-f393-e0a9-e50e24dcca9e', writeCharacteristic: '6e400003-b5a3-f393-e0a9-e50e24dcca9e', readCharacteristic: '6e400002-b5a3-f393-e0a9-e50e24dcca9e' },
|
||||
{ name: "Nordic NRF", serviceUuid: '6e400001-b5a3-f393-e0a9-e50e24dcca9e', writeCharacteristic: '6e400003-b5a3-f393-e0a9-e50e24dcca9e', readCharacteristic: '6e400002-b5a3-f393-e0a9-e50e24dcca9e' },
|
||||
{ name: "SpeedyBee V1", serviceUuid: '00001000-0000-1000-8000-00805f9b34fb', writeCharacteristic: '00001001-0000-1000-8000-00805f9b34fb', readCharacteristic: '00001002-0000-1000-8000-00805f9b34fb' },
|
||||
{ name: "SpeedyBee V2", serviceUuid: '0000abf0-0000-1000-8000-00805f9b34fb', writeCharacteristic: '0000abf1-0000-1000-8000-00805f9b34fb', readCharacteristic: '0000abf2-0000-1000-8000-00805f9b34fb' },
|
||||
];
|
||||
|
||||
class BT extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (!this.bluetooth && window && window.navigator && window.navigator.bluetooth) {
|
||||
this.bluetooth = navigator.bluetooth;
|
||||
} else {
|
||||
console.error(`${this.logHead} Bluetooth API not available`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.connected = false;
|
||||
this.openRequested = false;
|
||||
this.openCanceled = false;
|
||||
this.closeRequested = false;
|
||||
this.transmitting = false;
|
||||
this.connectionInfo = null;
|
||||
|
||||
this.bitrate = 0;
|
||||
this.bytesSent = 0;
|
||||
this.bytesReceived = 0;
|
||||
this.failed = 0;
|
||||
|
||||
this.logHead = "[BLUETOOTH]";
|
||||
|
||||
this.portCounter = 0;
|
||||
this.devices = [];
|
||||
this.device = null;
|
||||
|
||||
this.connect = this.connect.bind(this);
|
||||
|
||||
this.bluetooth.addEventListener("connect", e => this.handleNewDevice(e.target));
|
||||
this.bluetooth.addEventListener("disconnect", e => this.handleRemovedDevice(e.target));
|
||||
this.bluetooth.addEventListener("gatserverdisconnected", e => this.handleRemovedDevice(e.target));
|
||||
|
||||
this.loadDevices();
|
||||
}
|
||||
|
||||
handleNewDevice(device) {
|
||||
|
||||
const added = this.createPort(device);
|
||||
this.devices.push(added);
|
||||
this.dispatchEvent(new CustomEvent("addedDevice", { detail: added }));
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
handleRemovedDevice(device) {
|
||||
const removed = this.devices.find(port => port.port === device);
|
||||
this.devices = this.devices.filter(port => port.port !== device);
|
||||
this.dispatchEvent(new CustomEvent("removedDevice", { detail: removed }));
|
||||
}
|
||||
|
||||
handleReceiveBytes(info) {
|
||||
this.bytesReceived += info.detail.byteLength;
|
||||
}
|
||||
|
||||
handleDisconnect() {
|
||||
this.disconnect();
|
||||
this.closeRequested = true;
|
||||
}
|
||||
|
||||
getConnectedPort() {
|
||||
return this.device;
|
||||
}
|
||||
|
||||
createPort(device) {
|
||||
return {
|
||||
path: `bluetooth_${this.portCounter++}`,
|
||||
displayName: device.name,
|
||||
vendorId: "unknown",
|
||||
productId: device.id,
|
||||
port: device,
|
||||
};
|
||||
}
|
||||
|
||||
async loadDevices() {
|
||||
const devices = await this.bluetooth.getDevices();
|
||||
|
||||
this.portCounter = 1;
|
||||
this.devices = devices.map(device => this.createPort(device));
|
||||
}
|
||||
|
||||
async requestPermissionDevice() {
|
||||
let newPermissionPort = null;
|
||||
|
||||
const uuids = [];
|
||||
bluetoothDevices.forEach(device => {
|
||||
uuids.push(device.serviceUuid);
|
||||
});
|
||||
|
||||
const options = { acceptAllDevices: true, optionalServices: uuids };
|
||||
|
||||
try {
|
||||
const userSelectedPort = await this.bluetooth.requestDevice(options);
|
||||
newPermissionPort = this.devices.find(port => port.port === userSelectedPort);
|
||||
if (!newPermissionPort) {
|
||||
newPermissionPort = this.handleNewDevice(userSelectedPort);
|
||||
}
|
||||
console.info(`${this.logHead} User selected Bluetooth device from permissions:`, newPermissionPort.path);
|
||||
} catch (error) {
|
||||
console.error(`${this.logHead} User didn't select any Bluetooth device when requesting permission:`, error);
|
||||
}
|
||||
return newPermissionPort;
|
||||
}
|
||||
|
||||
async getDevices() {
|
||||
return this.devices;
|
||||
}
|
||||
|
||||
getAvailability() {
|
||||
this.bluetooth.getAvailability().then((available) => {
|
||||
console.log(`${this.logHead} Bluetooth available:`, available);
|
||||
this.available = available;
|
||||
return available;
|
||||
});
|
||||
}
|
||||
|
||||
async connect(path, options) {
|
||||
this.openRequested = true;
|
||||
this.closeRequested = false;
|
||||
|
||||
this.device = this.devices.find(device => device.path === path).port;
|
||||
|
||||
console.log(`${this.logHead} Opening connection with ID: ${path}, Baud: ${options.baudRate}`);
|
||||
|
||||
this.device.addEventListener('gattserverdisconnected', this.handleDisconnect.bind(this));
|
||||
|
||||
try {
|
||||
console.log(`${this.logHead} Connecting to GATT Server`);
|
||||
|
||||
await this.gattConnect();
|
||||
|
||||
gui_log(i18n.getMessage('bluetoothConnected', [this.device.name]));
|
||||
|
||||
await this.getServices();
|
||||
await this.getCharacteristics();
|
||||
await this.startNotifications();
|
||||
} catch (error) {
|
||||
gui_log(i18n.getMessage('bluetoothConnectionError', [error]));
|
||||
}
|
||||
|
||||
// Bluetooth API doesn't provide a way for getInfo() or similar to get the connection info
|
||||
const connectionInfo = this.device.gatt.connected;
|
||||
|
||||
if (connectionInfo && !this.openCanceled) {
|
||||
this.connected = true;
|
||||
this.connectionId = this.device.port;
|
||||
this.bitrate = options.baudRate;
|
||||
this.bytesReceived = 0;
|
||||
this.bytesSent = 0;
|
||||
this.failed = 0;
|
||||
this.openRequested = false;
|
||||
|
||||
this.device.addEventListener("disconnect", this.handleDisconnect.bind(this));
|
||||
this.addEventListener("receive", this.handleReceiveBytes);
|
||||
|
||||
console.log(
|
||||
`${this.logHead} Connection opened with ID: ${this.connectionId}, Baud: ${options.baudRate}`,
|
||||
);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("connect", { detail: connectionInfo }),
|
||||
);
|
||||
} else if (connectionInfo && this.openCanceled) {
|
||||
this.connectionId = this.device.port;
|
||||
|
||||
console.log(
|
||||
`${this.logHead} Connection opened with ID: ${connectionInfo.connectionId}, but request was canceled, disconnecting`,
|
||||
);
|
||||
// some bluetooth dongles/dongle drivers really doesn't like to be closed instantly, adding a small delay
|
||||
setTimeout(() => {
|
||||
this.openRequested = false;
|
||||
this.openCanceled = false;
|
||||
this.disconnect(() => {
|
||||
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
|
||||
});
|
||||
}, 150);
|
||||
} else if (this.openCanceled) {
|
||||
console.log(
|
||||
`${this.logHead} Connection didn't open and request was canceled`,
|
||||
);
|
||||
this.openRequested = false;
|
||||
this.openCanceled = false;
|
||||
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
|
||||
} else {
|
||||
this.openRequested = false;
|
||||
console.log(`${this.logHead} Failed to open bluetooth port`);
|
||||
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async gattConnect() {
|
||||
this.server = await this.device.gatt?.connect();
|
||||
}
|
||||
|
||||
async getServices() {
|
||||
console.log(`${this.logHead} Get primary services`);
|
||||
|
||||
this.services = await this.server.getPrimaryServices();
|
||||
|
||||
this.service = this.services.find(service => {
|
||||
this.deviceDescription = bluetoothDevices.find(device => device.serviceUuid == service.uuid);
|
||||
return this.deviceDescription;
|
||||
});
|
||||
|
||||
if (!this.deviceDescription) {
|
||||
throw new Error("Unsupported device");
|
||||
}
|
||||
|
||||
gui_log(i18n.getMessage('bluetoothConnectionType', [this.deviceDescription.name]));
|
||||
|
||||
console.log(`${this.logHead} Connected to service:`, this.service.uuid);
|
||||
|
||||
return this.service;
|
||||
}
|
||||
|
||||
async getCharacteristics() {
|
||||
const characteristics = await this.service.getCharacteristics();
|
||||
|
||||
characteristics.forEach(characteristic => {
|
||||
// console.log("Characteristic: ", characteristic);
|
||||
if (characteristic.uuid == this.deviceDescription.writeCharacteristic) {
|
||||
this.writeCharacteristic = characteristic;
|
||||
}
|
||||
|
||||
if (characteristic.uuid == this.deviceDescription.readCharacteristic) {
|
||||
this.readCharacteristic = characteristic;
|
||||
}
|
||||
return this.writeCharacteristic && this.readCharacteristic;
|
||||
});
|
||||
|
||||
if (!this.writeCharacteristic) {
|
||||
throw new Error("Unexpected write characteristic found - should be", this.deviceDescription.writeCharacteristic);
|
||||
}
|
||||
|
||||
if (!this.readCharacteristic) {
|
||||
throw new Error("Unexpected read characteristic found - should be", this.deviceDescription.readCharacteristic);
|
||||
}
|
||||
|
||||
this.readCharacteristic.addEventListener('characteristicvaluechanged', this.handleNotification.bind(this));
|
||||
|
||||
return await this.readCharacteristic.readValue();
|
||||
}
|
||||
|
||||
handleNotification(event) {
|
||||
const buffer = new Uint8Array(event.target.value.byteLength);
|
||||
|
||||
for (let i = 0; i < event.target.value.byteLength; i++) {
|
||||
buffer[i] = event.target.value.getUint8(i);
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent("receive", { detail: buffer }));
|
||||
}
|
||||
|
||||
startNotifications() {
|
||||
if (!this.readCharacteristic) {
|
||||
throw new Error("No read characteristic");
|
||||
}
|
||||
|
||||
if (!this.readCharacteristic.properties.notify) {
|
||||
throw new Error("Read characteristic unable to notify.");
|
||||
}
|
||||
|
||||
return this.readCharacteristic.startNotifications();
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
this.connected = false;
|
||||
this.transmitting = false;
|
||||
this.bytesReceived = 0;
|
||||
this.bytesSent = 0;
|
||||
|
||||
// if we are already closing, don't do it again
|
||||
if (this.closeRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doCleanup = async () => {
|
||||
this.removeEventListener('receive', this.handleReceiveBytes);
|
||||
|
||||
if (this.device) {
|
||||
this.device.removeEventListener("disconnect", this.handleDisconnect.bind(this));
|
||||
this.device.removeEventListener('gattserverdisconnected', this.handleDisconnect);
|
||||
this.readCharacteristic.removeEventListener('characteristicvaluechanged', this.handleNotification.bind(this));
|
||||
|
||||
if (this.device.gatt.connected) {
|
||||
this.device.gatt.disconnect();
|
||||
}
|
||||
|
||||
this.writeCharacteristic = false;
|
||||
this.readCharacteristic = false;
|
||||
this.deviceDescription = false;
|
||||
this.device = null;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await doCleanup();
|
||||
|
||||
console.log(
|
||||
`${this.logHead} Connection with ID: ${this.connectionId} closed, Sent: ${this.bytesSent} bytes, Received: ${this.bytesReceived} bytes`,
|
||||
);
|
||||
|
||||
this.connectionId = false;
|
||||
this.bitrate = 0;
|
||||
this.dispatchEvent(new CustomEvent("disconnect", { detail: true }));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error(
|
||||
`${this.logHead} Failed to close connection with ID: ${this.connectionId} closed, Sent: ${this.bytesSent} bytes, Received: ${this.bytesReceived} bytes`,
|
||||
);
|
||||
this.dispatchEvent(new CustomEvent("disconnect", { detail: false }));
|
||||
} finally {
|
||||
if (this.openCanceled) {
|
||||
this.openCanceled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
if (!this.writeCharacteristic) {
|
||||
return;
|
||||
}
|
||||
|
||||
// There is no writable stream in the bluetooth API
|
||||
this.bytesSent += data.byteLength;
|
||||
|
||||
const dataBuffer = new Uint8Array(data);
|
||||
|
||||
await this.writeCharacteristic.writeValue(dataBuffer);
|
||||
|
||||
return {
|
||||
bytesSent: data.byteLength,
|
||||
resultCode: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new BT();
|
|
@ -24,7 +24,6 @@ import CryptoES from "crypto-es";
|
|||
import $ from 'jquery';
|
||||
import BuildApi from "./BuildApi";
|
||||
|
||||
import { isWeb } from "./utils/isWeb";
|
||||
import { serialShim } from "./serial_shim.js";
|
||||
import { EventBus } from "../components/eventBus";
|
||||
|
||||
|
@ -64,6 +63,18 @@ export function initializeSerialBackend() {
|
|||
}
|
||||
});
|
||||
|
||||
EventBus.$on('port-handler:auto-select-bluetooth-device', function(device) {
|
||||
if (!GUI.connected_to && !GUI.connecting_to && GUI.active_tab !== 'firmware_flasher'
|
||||
&& ((PortHandler.portPicker.autoConnect && !["manual", "virtual"].includes(device))
|
||||
|| Date.now() - rebootTimestamp < REBOOT_CONNECT_MAX_TIME_MS)) {
|
||||
connectDisconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// Using serialShim for serial and bluetooth we don't know which event we need before we connect
|
||||
// Perhaps we should implement a Connection class that handles the connection and events for bluetooth, serial and sockets
|
||||
// TODO: use event gattserverdisconnected for save and reboot and device removal.
|
||||
|
||||
serial.addEventListener("removedDevice", (event) => {
|
||||
if (event.detail.path === GUI.connected_to) {
|
||||
connectDisconnect();
|
||||
|
@ -88,39 +99,39 @@ export function initializeSerialBackend() {
|
|||
|
||||
function connectDisconnect() {
|
||||
const selectedPort = PortHandler.portPicker.selectedPort;
|
||||
let portName;
|
||||
if (selectedPort === 'manual') {
|
||||
portName = PortHandler.portPicker.portOverride;
|
||||
} else {
|
||||
portName = selectedPort;
|
||||
}
|
||||
const portName = selectedPort === 'manual' ? PortHandler.portPicker.portOverride : selectedPort;
|
||||
|
||||
if (!GUI.connect_lock && selectedPort !== 'noselection' && !selectedPort.path?.startsWith('usb_')) {
|
||||
// GUI control overrides the user control
|
||||
|
||||
GUI.configuration_loaded = false;
|
||||
|
||||
const selected_baud = PortHandler.portPicker.selectedBauds;
|
||||
const baudRate = PortHandler.portPicker.selectedBauds;
|
||||
const selectedPort = portName;
|
||||
|
||||
if (!isConnected) {
|
||||
console.log(`Connecting to: ${portName}`);
|
||||
// prevent connection when we do not have permission
|
||||
if (selectedPort.startsWith('requestpermission')) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[SERIAL-BACKEND] Connecting to: ${portName}`);
|
||||
GUI.connecting_to = portName;
|
||||
|
||||
// lock port select & baud while we are connecting / connected
|
||||
PortHandler.portPickerDisabled = true;
|
||||
$('div.connect_controls div.connect_state').text(i18n.getMessage('connecting'));
|
||||
|
||||
const baudRate = selected_baud;
|
||||
if (selectedPort === 'virtual') {
|
||||
CONFIGURATOR.virtualMode = true;
|
||||
CONFIGURATOR.virtualMode = selectedPort === 'virtual';
|
||||
CONFIGURATOR.bluetoothMode = selectedPort.startsWith('bluetooth');
|
||||
|
||||
if (CONFIGURATOR.virtualMode) {
|
||||
CONFIGURATOR.virtualApiVersion = PortHandler.portPicker.virtualMspVersion;
|
||||
|
||||
// Hack to get virtual working on the web
|
||||
serial = serialShim();
|
||||
serial.connect('virtual', {}, onOpenVirtual);
|
||||
} else {
|
||||
CONFIGURATOR.virtualMode = false;
|
||||
serial = serialShim();
|
||||
// Explicitly disconnect the event listeners before attaching the new ones.
|
||||
serial.removeEventListener('connect', connectHandler);
|
||||
|
@ -163,6 +174,7 @@ function finishClose(finishedCallback) {
|
|||
$('#dialogResetToCustomDefaults')[0].close();
|
||||
}
|
||||
|
||||
// serialShim calls the disconnect method for selected connection type.
|
||||
serial.disconnect(onClosed);
|
||||
|
||||
MSP.disconnect_cleanup();
|
||||
|
@ -262,18 +274,16 @@ function onOpen(openInfo) {
|
|||
result = getConfig('expertMode')?.expertMode ?? false;
|
||||
$('input[name="expertModeCheckbox"]').prop('checked', result).trigger('change');
|
||||
|
||||
if(isWeb()) {
|
||||
serial.removeEventListener('receive', read_serial_adapter);
|
||||
serial.addEventListener('receive', read_serial_adapter);
|
||||
} else {
|
||||
serial.onReceive.addListener(read_serial);
|
||||
}
|
||||
// serialShim adds event listener for selected connection type
|
||||
serial.removeEventListener('receive', read_serial_adapter);
|
||||
serial.addEventListener('receive', read_serial_adapter);
|
||||
|
||||
setConnectionTimeout();
|
||||
FC.resetState();
|
||||
mspHelper = new MspHelper();
|
||||
MSP.listen(mspHelper.process_data.bind(mspHelper));
|
||||
MSP.timeout = 250;
|
||||
console.log(`Requesting configuration data`);
|
||||
console.log(`[SERIAL-BACKEND] Requesting configuration data`);
|
||||
|
||||
MSP.send_message(MSPCodes.MSP_API_VERSION, false, false, function () {
|
||||
gui_log(i18n.getMessage('apiVersionReceived', FC.CONFIG.apiVersion));
|
||||
|
@ -642,11 +652,7 @@ function onConnect() {
|
|||
}
|
||||
|
||||
function onClosed(result) {
|
||||
if (result) { // All went as expected
|
||||
gui_log(i18n.getMessage('serialPortClosedOk'));
|
||||
} else { // Something went wrong
|
||||
gui_log(i18n.getMessage('serialPortClosedFail'));
|
||||
}
|
||||
gui_log(i18n.getMessage(result ? 'serialPortClosedOk' : 'serialPortClosedFail'));
|
||||
|
||||
$('#tabs ul.mode-connected').hide();
|
||||
$('#tabs ul.mode-connected-cli').hide();
|
||||
|
@ -760,7 +766,7 @@ function startLiveDataRefreshTimer() {
|
|||
export function reinitializeConnection(callback) {
|
||||
|
||||
// In virtual mode reconnect when autoconnect is enabled
|
||||
if (PortHandler.portPicker.selectedPort === 'virtual' && PortHandler.portPicker.autoConnect) {
|
||||
if (CONFIGURATOR.virtualMode && PortHandler.portPicker.autoConnect) {
|
||||
return setTimeout(function() {
|
||||
$('a.connect').trigger('click');
|
||||
}, 500);
|
||||
|
@ -769,6 +775,11 @@ export function reinitializeConnection(callback) {
|
|||
rebootTimestamp = Date.now();
|
||||
MSP.send_message(MSPCodes.MSP_SET_REBOOT, false, false);
|
||||
|
||||
if (CONFIGURATOR.bluetoothMode) {
|
||||
// Bluetooth devices are not disconnected when rebooting
|
||||
connectDisconnect();
|
||||
}
|
||||
|
||||
gui_log(i18n.getMessage('deviceRebooting'));
|
||||
|
||||
// wait for the device to reboot
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import CONFIGURATOR from "./data_storage";
|
||||
import serialNWJS from "./serial.js";
|
||||
import serialWeb from "./webSerial.js";
|
||||
import { isWeb } from "./utils/isWeb";
|
||||
import BT from "./protocols/bluetooth.js";
|
||||
|
||||
export let serialShim = () => CONFIGURATOR.virtualMode ? serialNWJS : isWeb() ? serialWeb : serialNWJS;
|
||||
export let serialShim = () => CONFIGURATOR.virtualMode ? serialNWJS : CONFIGURATOR.bluetoothMode ? BT : serialWeb;
|
||||
|
|
|
@ -29,7 +29,7 @@ class WebSerial extends EventTarget {
|
|||
this.bytesReceived = 0;
|
||||
this.failed = 0;
|
||||
|
||||
this.logHead = "SERIAL: ";
|
||||
this.logHead = "[SERIAL] ";
|
||||
|
||||
this.portCounter = 0;
|
||||
this.ports = [];
|
||||
|
@ -96,10 +96,13 @@ class WebSerial extends EventTarget {
|
|||
|
||||
async requestPermissionDevice(showAllSerialDevices = false) {
|
||||
let newPermissionPort = null;
|
||||
|
||||
try {
|
||||
const options = showAllSerialDevices ? {} : { filters: webSerialDevices };
|
||||
const userSelectedPort = await navigator.serial.requestPort(options);
|
||||
|
||||
newPermissionPort = this.ports.find(port => port.port === userSelectedPort);
|
||||
|
||||
if (!newPermissionPort) {
|
||||
newPermissionPort = this.handleNewDevice(userSelectedPort);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue