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 @@
     />
     <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,
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") }}
         </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);
       }
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);
             }