From d2295da637ae6e16b0bbf8fd558515fb9ade78ac Mon Sep 17 00:00:00 2001 From: Mark Haslinghuis Date: Tue, 24 Sep 2024 13:45:05 +0200 Subject: [PATCH] Add websocket support --- .../port-picker/PortOverrideOption.vue | 39 +++-- src/js/data_storage.js | 1 + src/js/protocols/websocket.js | 145 ++++++++++++++++++ src/js/serial_backend.js | 12 +- src/js/serial_shim.js | 14 +- 5 files changed, 194 insertions(+), 17 deletions(-) create mode 100644 src/js/protocols/websocket.js diff --git a/src/components/port-picker/PortOverrideOption.vue b/src/components/port-picker/PortOverrideOption.vue index e3f0c95fe86..7480e4ac404 100644 --- a/src/components/port-picker/PortOverrideOption.vue +++ b/src/components/port-picker/PortOverrideOption.vue @@ -1,8 +1,7 @@ diff --git a/src/js/data_storage.js b/src/js/data_storage.js index efb37b3498e..38b391d387f 100644 --- a/src/js/data_storage.js +++ b/src/js/data_storage.js @@ -11,6 +11,7 @@ const CONFIGURATOR = { connectionValid: false, connectionValidCliOnly: false, + manualMode: false, virtualMode: false, virtualApiVersion: '0.0.1', cliActive: false, diff --git a/src/js/protocols/websocket.js b/src/js/protocols/websocket.js new file mode 100644 index 00000000000..418eda0a153 --- /dev/null +++ b/src/js/protocols/websocket.js @@ -0,0 +1,145 @@ +class WebsocketSerial extends EventTarget { + constructor() { + super(); + + this.connected = false; + this.connectionInfo = null; + + this.bitrate = 0; + this.bytesSent = 0; + this.bytesReceived = 0; + this.failed = 0; + + this.logHead = "[WEBSOCKET] "; + + this.address = "ws://localhost:5761"; + + this.ws = null; + + this.connect = this.connect.bind(this); + } + + handleReceiveBytes(info) { + this.bytesReceived += info.detail.byteLength; + } + + handleDisconnect() { + this.disconnect(); + } + + createPort(url) { + this.address = url; + return { + path: url, + displayName: `Betaflight SITL`, + vendorId: 0, + productId: 0, + port: 0, + }; + } + + getConnectedPort() { + return { + path: this.address, + displayName: `Betaflight SITL`, + vendorId: 0, + productId: 0, + port: 0, + }; + } + + async getDevices() { + return []; + } + + async blob2uint(blob) { + const buffer = await new Response(blob).arrayBuffer(); + return new Uint8Array(buffer); + } + + waitForConnection(socket) { + return new Promise((resolve) => { + const interval = setInterval(() => { + if (socket.connected) { + clearInterval(interval); // Stop checking + resolve(); // Resolve the promise + } + }, 100); // Check every 100ms, adjust as needed + }); + } + + async connect(path, options) { + this.address = path; + console.log(`${this.logHead} Connecting to ${this.address}`); + + this.ws = new WebSocket(this.address, "wsSerial"); + let socket = this; + + this.ws.onopen = function(e) { + console.log(`${socket.logHead} Connected: `, e); + socket.connected = true; + socket.dispatchEvent( + new CustomEvent("connect", { detail: { + socketId: socket.address, + }}), + ); + }; + + await this.waitForConnection(socket); + + this.ws.onclose = function(e) { + console.log(`${socket.logHead} Connection closed: `, e); + + socket.disconnect(() => { + socket.dispatchEvent(new CustomEvent("disconnect", this.disconnect.bind(this))); + }); + }; + + this.ws.onerror = function(e) { + console.error(`${socket.logHead} Connection error: `, e); + + socket.disconnect(() => { + socket.dispatchEvent(new CustomEvent("disconnect", this.disconnect.bind(this))); + }); + }; + + this.ws.onmessage = async function(msg) { + let uint8Chunk = await socket.blob2uint(msg.data); + socket.dispatchEvent( + new CustomEvent("receive", { detail: uint8Chunk }), + ); + }; + } + + async disconnect() { + this.connected = false; + this.bytesReceived = 0; + this.bytesSent = 0; + + if (this.ws) { + try { + this.ws.close(); + } catch (e) { + console.error(`${this.logHead}Failed to close socket: ${e}`); + } + } + } + + async send(data, cb) { + if (this.ws) { + try { + this.ws.send(data); + this.bytesSent += data.byteLength; + } + catch(e) { + console.error(`${this.logHead}Failed to send data e: ${e}`); + } + } + + return { + bytesSent: data.byteLength, + }; + } +} + +export default new WebsocketSerial(); diff --git a/src/js/serial_backend.js b/src/js/serial_backend.js index bc95ca4ffc1..bc96fdd6868 100644 --- a/src/js/serial_backend.js +++ b/src/js/serial_backend.js @@ -107,7 +107,6 @@ function connectDisconnect() { GUI.configuration_loaded = false; const baudRate = PortHandler.portPicker.selectedBauds; - const selectedPort = portName; if (!isConnected) { // prevent connection when we do not have permission @@ -124,6 +123,7 @@ function connectDisconnect() { CONFIGURATOR.virtualMode = selectedPort === 'virtual'; CONFIGURATOR.bluetoothMode = selectedPort.startsWith('bluetooth'); + CONFIGURATOR.manualMode = selectedPort === 'manual'; if (CONFIGURATOR.virtualMode) { CONFIGURATOR.virtualApiVersion = PortHandler.portPicker.virtualMspVersion; @@ -131,6 +131,16 @@ function connectDisconnect() { // Hack to get virtual working on the web serial = serialShim(); serial.connect(onOpenVirtual); + } else if (selectedPort === 'manual') { + serial = serialShim(); + // Explicitly disconnect the event listeners before attaching the new ones. + serial.removeEventListener('connect', connectHandler); + serial.addEventListener('connect', connectHandler); + + serial.removeEventListener('disconnect', disconnectHandler); + serial.addEventListener('disconnect', disconnectHandler); + + serial.connect(portName, { baudRate }); } else { CONFIGURATOR.virtualMode = false; serial = serialShim(); diff --git a/src/js/serial_shim.js b/src/js/serial_shim.js index dee494a6392..bcec5bf1041 100644 --- a/src/js/serial_shim.js +++ b/src/js/serial_shim.js @@ -1,6 +1,18 @@ import CONFIGURATOR from "./data_storage"; import serialWeb from "./webSerial.js"; import BT from "./protocols/bluetooth.js"; +import websocketSerial from "./protocols/websocket.js"; import virtualSerial from "./virtualSerial.js"; -export let serialShim = () => CONFIGURATOR.virtualMode ? virtualSerial: CONFIGURATOR.bluetoothMode ? BT : serialWeb; +export const serialShim = () => { + if (CONFIGURATOR.virtualMode) { + return virtualSerial; + } + if (CONFIGURATOR.manualMode) { + return websocketSerial; + } + if (CONFIGURATOR.bluetoothMode) { + return BT; + } + return serialWeb; +} \ No newline at end of file