From 5a3f06f89060df98fe4c2dff74c7c95aba6cd545 Mon Sep 17 00:00:00 2001 From: Mark Haslinghuis Date: Fri, 28 Jun 2024 20:02:32 +0200 Subject: [PATCH] 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 --- locales/en/messages.json | 15 +- src/components/port-picker/PortPicker.vue | 7 +- src/components/port-picker/PortsInput.vue | 16 + src/js/msp.js | 12 +- src/js/port_handler.js | 67 +++- src/js/protocols/bluetooth.js | 361 ++++++++++++++++++++++ src/js/serial_backend.js | 63 ++-- src/js/serial_shim.js | 4 +- src/js/webSerial.js | 5 +- 9 files changed, 512 insertions(+), 38 deletions(-) create mode 100644 src/js/protocols/bluetooth.js diff --git a/locales/en/messages.json b/locales/en/messages.json index 3f6c55db38..2cdae9aa93 100755 --- a/locales/en/messages.json +++ b/locales/en/messages.json @@ -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" }, diff --git a/src/components/port-picker/PortPicker.vue b/src/components/port-picker/PortPicker.vue index 778d0d4029..add7a2e23f 100644 --- a/src/components/port-picker/PortPicker.vue +++ b/src/components/port-picker/PortPicker.vue @@ -12,6 +12,7 @@ /> [], }, @@ -52,6 +53,10 @@ export default { type: Array, default: () => [], }, + connectedUsbDevices: { + type: Array, + default: () => [], + }, showVirtualOption: { type: Boolean, default: true, diff --git a/src/components/port-picker/PortsInput.vue b/src/components/port-picker/PortsInput.vue index 2be759251a..876bef8011 100644 --- a/src/components/port-picker/PortsInput.vue +++ b/src/components/port-picker/PortsInput.vue @@ -30,6 +30,13 @@ > {{ $t("portsSelectVirtual") }} + +
@@ -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); } diff --git a/src/js/msp.js b/src/js/msp.js index 42c214bf92..dac5a20339 100644 --- a/src/js/msp.js +++ b/src/js/msp.js @@ -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) { diff --git a/src/js/port_handler.js b/src/js/port_handler.js index 136f407abf..3bec35ad21 100644 --- a/src/js/port_handler.js +++ b/src/js/port_handler.js @@ -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; }; diff --git a/src/js/protocols/bluetooth.js b/src/js/protocols/bluetooth.js new file mode 100644 index 0000000000..dd71d875b7 --- /dev/null +++ b/src/js/protocols/bluetooth.js @@ -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(); diff --git a/src/js/serial_backend.js b/src/js/serial_backend.js index 1870001e5f..6a7e40bb54 100644 --- a/src/js/serial_backend.js +++ b/src/js/serial_backend.js @@ -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 diff --git a/src/js/serial_shim.js b/src/js/serial_shim.js index eeb8608756..dc0eaa0199 100644 --- a/src/js/serial_shim.js +++ b/src/js/serial_shim.js @@ -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; diff --git a/src/js/webSerial.js b/src/js/webSerial.js index 68b8d3fc8f..c0de6e9d38 100644 --- a/src/js/webSerial.js +++ b/src/js/webSerial.js @@ -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); }