From ab7162980ba6023a3fbd6ee6ec4c96d70364f71c Mon Sep 17 00:00:00 2001 From: Andi Kanzler Date: Thu, 31 Mar 2022 16:18:46 +0200 Subject: [PATCH 1/2] BLE, TCP and UDP Support --- _locales/en/messages.json | 33 +++- eventPage.js | 3 +- gulpfile.js | 6 +- js/connection/connection.js | 299 +++++++++++++++++++++++++++++ js/connection/connectionBle.js | 253 ++++++++++++++++++++++++ js/connection/connectionSerial.js | 139 ++++++++++++++ js/connection/connectionTcp.js | 139 ++++++++++++++ js/connection/connectionUdp.js | 149 ++++++++++++++ js/data_storage.js | 3 +- js/logicConditionsCollection.js | 6 + js/msp/MSPHelper.js | 58 ++++-- js/periodicStatusUpdater.js | 2 +- js/port_handler.js | 22 ++- js/protocols/stm32.js | 18 +- js/serial.js | 309 ------------------------------ js/serial_backend.js | 69 +++++-- js/serial_queue.js | 10 +- main.css | 4 +- main.html | 2 +- manifest.json | 11 +- package-lock.json | 14 +- package.json | 2 +- tabs/cli.js | 24 ++- tabs/sensors.js | 2 +- 24 files changed, 1190 insertions(+), 387 deletions(-) create mode 100644 js/connection/connection.js create mode 100644 js/connection/connectionBle.js create mode 100644 js/connection/connectionSerial.js create mode 100644 js/connection/connectionTcp.js create mode 100644 js/connection/connectionUdp.js delete mode 100644 js/serial.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 9c33a179..afbe6297 100755 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -141,18 +141,41 @@ }, "serialPortOpened": { - "message": "Serial port successfully opened with ID: $1" + "message": "MSP connection successfully opened with ID: $1" }, "serialPortOpenFail": { - "message": "Failed to open serial port" + "message": "Failed to open MSP connection" }, "serialPortClosedOk": { - "message": "Serial port successfully closed" + "message": "MSP connection successfully closed" }, "serialPortClosedFail": { - "message": "Failed to close serial port" + "message": "Failed to close MSP connection" + }, + "serialPortUnrecoverable": { + "message": "Unrecoverable failure of serial connection, disconnecting...'" + }, + "connectionConnected": { + "message": "Connected to: $1" + }, + "connectionBleType": { + "message": "BLE device type: $1" + }, + "connectionBleNotSupported" : { + "message": "Connection error: Firmware doesn't support BLE connections. Abort." + }, + "connectionBleInterrupted": { + "message": "The connection was unexpectedly interrupted." + }, + "connectionBleError": { + "message": "Error while opening BLE device: $1" + }, + "connectionBleCliEnter": { + "message": "Connection over BLE active, output might be slower than usual." + }, + "connectionUdpTimeout": { + "message": "UDP connection timed out." }, - "usbDeviceOpened": { "message": "USB device successfully opened with ID: $1" }, diff --git a/eventPage.js b/eventPage.js index a321e4e9..2843cdbf 100755 --- a/eventPage.js +++ b/eventPage.js @@ -21,10 +21,11 @@ function startApplication() { createdWindow.onClosed.addListener(function () { // automatically close the port when application closes // save connectionId in separate variable before createdWindow.contentWindow is destroyed - var connectionId = createdWindow.contentWindow.serial.connectionId, + var connectionId = createdWindow.contentWindow.CONFIGURATOR.connection.connectionId, valid_connection = createdWindow.contentWindow.CONFIGURATOR.connectionValid, mincommand = createdWindow.contentWindow.MISC.mincommand; + console.log("EP:" + connectionId); if (connectionId && valid_connection) { // code below is handmade MSP message (without pretty JS wrapper), it behaves exactly like MSP.send_message // sending exit command just in case the cli tab was open. diff --git a/gulpfile.js b/gulpfile.js index c1154aaf..42de2a43 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -86,7 +86,11 @@ sources.js = [ './js/msp/MSPHelper.js', './js/msp/MSPchainer.js', './js/port_handler.js', - './js/serial.js', + './js/connection/connection.js', + './js/connection/connectionBle.js', + './js/connection/connectionSerial.js', + './js/connection/connectionTcp.js', + './js/connection/connectionUdp.js', './js/servoMixRule.js', './js/motorMixRule.js', './js/logicCondition.js', diff --git a/js/connection/connection.js b/js/connection/connection.js new file mode 100644 index 00000000..343a5b9f --- /dev/null +++ b/js/connection/connection.js @@ -0,0 +1,299 @@ +'use strict'; + +const ConnectionType = { + Serial: 0, + TCP: 1, + UDP: 2, + BLE: 3 +} + +class Connection { + + constructor() { + this._connectionId = false; + this._openRequested = false; + this._openCanceled = false; + this._bitrate = 0; + this._bytesReceived = 0; + this._bytesSent = 0; + this._transmitting = false; + this._outputBuffer = []; + this._onReceiveListeners = []; + this._onReceiveErrorListeners = []; + + if (this.constructor === Connection) { + throw new TypeError("Abstract class, cannot be instanced."); + } + + if (this.connectImplementation === Connection.prototype.connectImplementation) { + throw new TypeError("connectImplementation is an abstract member and not implemented.") + } + + if (this.disconnectImplementation === Connection.prototype.disconnectImplementation) { + throw new TypeError("disconnectImplementation is an abstract member and not implemented.") + } + + if (this.addOnReceiveCallback === Connection.prototype.addOnReceiveCallback) { + throw new TypeError("addOnReceiveCallback is an abstract member and not implemented.") + } + + if (this.removeOnReceiveCallback === Connection.prototype.removeOnReceiveCallback) { + throw new TypeError("removeOnReceiveCallback is an abstract member and not implemented.") + } + + if (this.addOnReceiveErrorCallback === Connection.prototype.addOnReceiveErrorCallback) { + throw new TypeError("addOnReceiveErrorCallback is an abstract member and not implemented.") + } + + if (this.removeOnReceiveErrorCallback === Connection.prototype.removeOnReceiveErrorCallback) { + throw new TypeError("removeOnReceiveErrorCallback is an abstract member and not implemented.") + } + } + + get connectionId() { + return this._connectionId; + } + + get bitrate() { + return this._bitrate; + } + + get type() { + switch (this.constructor.name) { + case ConnectionSerial.name: + return ConnectionType.Serial; + case ConnectionTcp.name: + return ConnectionType.TCP; + case ConnectionUdp.name: + return ConnectionType.UDP; + case ConnectionBle.name: + return ConnectionType.BLE; + } + } + + static create(type) { + if (Connection.instance && (Connection.instance.type == type || Connection.instance.connectionId)){ + return Connection.instance; + } + + switch (type) { + case ConnectionType.BLE: + Connection.instance = new ConnectionBle(); + break; + case ConnectionType.TCP: + Connection.instance = new ConnectionTcp(); + break; + case ConnectionType.UDP: + Connection.instance = new ConnectionUdp(); + break; + default: + case ConnectionType.Serial: + Connection.instance = new ConnectionSerial(); + break; + } + return Connection.instance; + }; + + connectImplementation(path, options, callback) { + throw new TypeError("Abstract method"); + } + + connect(path, options, callback) { + this._openRequested = true; + this._failed = 0; + this.connectImplementation(path, options, connectionInfo => { + if (connectionInfo && !this._openCanceled) { + this._connectionId = connectionInfo.connectionId; + this._bitrate = connectionInfo.bitrate; + this._bytesReceived = 0; + this._bytesSent = 0; + this._openRequested = false; + + this.addOnReceiveListener((info) => { + this._bytesReceived += info.data.byteLength; + }); + + console.log('Connection opened with ID: ' + connectionInfo.connectionId + ', Baud: ' + connectionInfo.bitrate); + + if (callback) { + callback(connectionInfo); + } + } else if (connectionInfo && this.openCanceled) { + // connection opened, but this connect sequence was canceled + // we will disconnect without triggering any callbacks + this._connectionId = connectionInfo.connectionId; + console.log('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(() => { + if (callback) { + callback(false); + } + }); + }, 150); + } else if (this._openCanceled) { + // connection didn't open and sequence was canceled, so we will do nothing + console.log('Connection didn\'t open and request was canceled'); + this._openRequested = false; + this._openCanceled = false; + if (callback) { + callback(false); + } + } else { + this._openRequested = false; + console.log('Failed to open'); + googleAnalytics.sendException('FailedToOpen', false); + if (callback) { + callback(false); + } + } + }); + } + + disconnectImplementation(callback) { + throw new TypeError("Abstract method"); + } + + disconnect(callback) { + if (this._connectionId) { + this.emptyOutputBuffer(); + this.removeAllListeners(); + + this.disconnectImplementation(result => { + this.checkChromeLastError(); + + if (result) { + console.log('Connection with ID: ' + this._connectionId + ' closed, Sent: ' + this._bytesSent + ' bytes, Received: ' + this._bytesReceived + ' bytes'); + } else { + console.log('Failed to close connection with ID: ' + this._connectionId + ' closed, Sent: ' + this._bytesSent + ' bytes, Received: ' + this._bytesReceived + ' bytes'); + googleAnalytics.sendException('Connection: FailedToClose', false); + } + + this._connectionId = false; + if (callback) { + callback(result); + } + }); + } else { + this._openCanceled = true; + } + } + + sendImplementation(data, callback) { + throw new TypeError("Abstract method"); + } + + send(data, callback) { + this._outputBuffer.push({'data': data, 'callback': callback}); + + var send = () => { + // store inside separate variables in case array gets destroyed + var data = this._outputBuffer[0].data, + callback = this._outputBuffer[0].callback; + + this.sendImplementation(data, sendInfo => { + // track sent bytes for statistics + this._bytesSent += sendInfo.bytesSent; + + // fire callback + if (callback) { + callback(sendInfo); + } + + // remove data for current transmission form the buffer + this._outputBuffer.shift(); + + // if there is any data in the queue fire send immediately, otherwise stop trasmitting + if (this._outputBuffer.length) { + // keep the buffer withing reasonable limits + if (this._outputBuffer.length > 100) { + var counter = 0; + + while (this._outputBuffer.length > 100) { + this._outputBuffer.pop(); + counter++; + } + + console.log('Send buffer overflowing, dropped: ' + counter + ' entries'); + } + send(); + } else { + this._transmitting = false; + } + }); + } + + if (!this._transmitting) { + this._transmitting = true; + send(); + } + } + + abort() { + if (GUI.connected_to || GUI.connecting_to) { + $('a.connect').trigger('click'); + } else { + this.disconnect(); + } + } + + checkChromeLastError() { + if (chrome.runtime.lastError) { + console.error(chrome.runtime.lastError.message); + } + } + + addOnReceiveCallback(callback) { + throw new TypeError("Abstract method"); + } + + removeOnReceiveCallback(callback) { + throw new TypeError("Abstract method"); + } + + addOnReceiveListener(callback) { + this._onReceiveListeners.push(callback); + this.addOnReceiveCallback(callback) + } + + addOnReceiveErrorCallback(callback) { + throw new TypeError("Abstract method"); + } + + removeOnReceiveErrorCallback(callback) { + throw new TypeError("Abstract method"); + } + + addOnReceiveErrorListener(callback) { + this._onReceiveErrorListeners.push(callback); + this.addOnReceiveErrorCallback(callback) + } + + removeAllListeners() { + this._onReceiveListeners.forEach(listener => this.removeOnReceiveCallback(listener)); + this._onReceiveListeners = []; + + this._onReceiveErrorListeners.forEach(listener => this.removeOnReceiveErrorCallback(listener)); + this._onReceiveErrorListeners = []; + } + + emptyOutputBuffer() { + this._outputBuffer = []; + this._transmitting = false; + } + + /** + * Default timeout values + * @returns {number} [ms] + */ + getTimeout() { + if (this._bitrate >= 57600) { + return 3000; + } else { + return 4000; + } + } +} diff --git a/js/connection/connectionBle.js b/js/connection/connectionBle.js new file mode 100644 index 00000000..92abadf2 --- /dev/null +++ b/js/connection/connectionBle.js @@ -0,0 +1,253 @@ +'use strict' + +// BLE 20 bytes buffer +const BLE_WRITE_BUFFER_LENGTH = 20; + +const BleDevices = [ + { + name: "CC2541 based", + serviceUuid: '0000ffe0-0000-1000-8000-00805f9b34fb', + writeCharateristic: '0000ffe1-0000-1000-8000-00805f9b34fb', + readCharateristic: '0000ffe1-0000-1000-8000-00805f9b34fb', + delay: 30, + }, + { + name: "Nordic Semiconductor NRF", + serviceUuid: '6e400001-b5a3-f393-e0a9-e50e24dcca9e', + writeCharateristic: '6e400003-b5a3-f393-e0a9-e50e24dcca9e', + readCharateristic: '6e400002-b5a3-f393-e0a9-e50e24dcca9e', + delay: 30, + }, + { + name: "SpeedyBee Type 2", + serviceUuid: '0000abf0-0000-1000-8000-00805f9b34fb', + writeCharateristic: '0000abf1-0000-1000-8000-00805f9b34fb', + readCharateristic: '0000abf2-0000-1000-8000-00805f9b34fb', + delay: 0, + }, + { + name: "SpeedyBee Type 1", + serviceUuid: '00001000-0000-1000-8000-00805f9b34fb', + writeCharateristic: '00001001-0000-1000-8000-00805f9b34fb', + readCharateristic: '00001002-0000-1000-8000-00805f9b34fb', + delay: 0, + } +]; + +class ConnectionBle extends Connection { + + constructor() { + super(); + + this._readCharacteristic = false; + this._writeCharacteristic = false; + this._device = false; + this._deviceDescription = false; + this._onCharateristicValueChangedListeners = []; + this._onDisconnectListeners = []; + this._reconnects = 0; + this._handleOnCharateristicValueChanged = false; + this._handleDisconnect = false; + } + + get deviceDescription() { + return this._deviceDescription; + } + + async connectImplementation(path, options, callback) { + console.log("Request BLE Device"); + await this.openDevice() + .then(() => { + this.addOnReceiveErrorListener(error => { + GUI.log(chrome.i18n.getMessage('connectionBleInterrupted')); + this.abort(); + }); + + if (callback) { + callback({ + // Dummy values + connectionId: 0xff, + bitrate: 115200 + }); + } + }).catch(error => { + GUI.log(chrome.i18n.getMessage('connectionBleError', [error])); + if (callback) { + callback(false); + } + }); + + return Promise.resolve(); + } + + async openDevice(){ + await this.request() + .then(device => this.connectBle(device)) + .then(() => this.startNotification()); + + return Promise.resolve(); + }; + + request() { + var ids = []; + BleDevices.forEach(device => { + ids.push(device.serviceUuid) + }); + + return navigator.bluetooth.requestDevice({ + acceptAllDevices: true, + optionalServices: ids + }).then(device => { + console.log("Found BLE device: " + device.name); + this._device = device; + this._handleDisconnect = event => { + this._onDisconnectListeners.forEach(listener => { + listener("disconnected"); + }); + }; + + this._device.addEventListener('gattserverdisconnected', this._handleDisconnect); + return this._device; + }); + } + + connectBle(device) { + if (device.gatt.connected && this._readCharacteristic && this._writeCharacteristic) { + return Promise.resolve(); + } + + return device.gatt.connect() + .then(server => { + console.log("Connect to: " + device.name); + GUI.log(chrome.i18n.getMessage('connectionConnected', [device.name])); + return server.getPrimaryServices(); + }).then(services => { + let connectedService = services.find(service => { + this._deviceDescription = BleDevices.find(device => device.serviceUuid == service.uuid); + return this._deviceDescription; + }); + + if (!this._deviceDescription) { + throw new Error("Unsupported device (service UUID mismatch)."); + } + + GUI.log(chrome.i18n.getMessage('connectionBleType', [this._deviceDescription.name])); + return connectedService.getCharacteristics(); + }).then(characteristics => { + characteristics.forEach(characteristic => { + if (characteristic.uuid == this._deviceDescription.writeCharateristic) { + this._writeCharacteristic = characteristic; + } + + if (characteristic.uuid == this._deviceDescription.readCharateristic) { + this._readCharacteristic = characteristic; + } + + return this._writeCharacteristic && this._readCharacteristic; + }); + + if (!this._writeCharacteristic) { + throw new Error("No or unexpected write charateristic found (should be " + this._deviceDescription.writeCharateristic + ")"); + } + + if (!this._readCharacteristic) { + throw new Error("No or unexpected read charateristic found (should be " + this._deviceDescription.readCharateristic + ")"); + } + + this._handleOnCharateristicValueChanged = event => { + let buffer = new Uint8Array(event.target.value.byteLength); + for (var i = 0; i < event.target.value.byteLength; i++) { + buffer[i] = event.target.value.getUint8(i); + } + + this._onCharateristicValueChangedListeners.forEach(listener => { + listener({ + connectionId: 0xFF, + data: buffer + }); + }); + }; + + this._readCharacteristic.addEventListener('characteristicvaluechanged', this._handleOnCharateristicValueChanged) + return Promise.resolve(); + }); + } + + startNotification() { + if (!this._readCharacteristic) { + throw new Error("No read charateristic"); + } + + if (!this._readCharacteristic.properties.notify) { + throw new Error("Read charateristic can't notify."); + } + + return this._readCharacteristic.startNotifications() + .then(() => { + console.log("BLE notifications started."); + }); + } + + disconnectImplementation(callback) { + if (this._device) { + this._device.removeEventListener('gattserverdisconnected', this._handleDisconnect); + this._readCharacteristic.removeEventListener('characteristicvaluechanged', this._handleOnCharateristicValueChanged); + + if (this._device.gatt.connected) { + this._device.gatt.disconnect(); + } + this._device = false; + this._writeCharacteristic = false; + this._readCharacteristic = false; + this._deviceDescription = false; + } + + if (callback) { + callback(true); + } + } + + async sendImplementation (data, callback) {; + if (!this._writeCharacteristic) { + return; + } + + let sent = 0; + let dataBuffer = new Uint8Array(data); + for (var i = 0; i < dataBuffer.length; i += BLE_WRITE_BUFFER_LENGTH) { + var length = BLE_WRITE_BUFFER_LENGTH; + + if (i + BLE_WRITE_BUFFER_LENGTH > dataBuffer.length) { + length = dataBuffer.length % BLE_WRITE_BUFFER_LENGTH; + } + + var outBuffer = dataBuffer.subarray(i, i + length); + sent += outBuffer.length; + await this._writeCharacteristic.writeValue(outBuffer); + } + + if (callback) { + callback({ + bytesSent: sent, + resultCode: 0 + }); + } + + } + + addOnReceiveCallback(callback){ + this._onCharateristicValueChangedListeners.push(callback); + } + + removeOnReceiveCallback(callback){ + this._onCharateristicValueChangedListeners = this._onCharateristicValueChangedListeners.filter(listener => listener !== callback); + } + + addOnReceiveErrorCallback(callback) { + this._onDisconnectListeners.push(callback); + } + + removeOnReceiveErrorCallback(callback) { + this._onDisconnectListeners = this._onDisconnectListeners.filter(listener => listener !== callback); + } +} diff --git a/js/connection/connectionSerial.js b/js/connection/connectionSerial.js new file mode 100644 index 00000000..7140c5f2 --- /dev/null +++ b/js/connection/connectionSerial.js @@ -0,0 +1,139 @@ +'use strict' + +class ConnectionSerial extends Connection { + + constructor() { + super(); + + this._failed = 0; + } + + connectImplementation(path, options, callback) { + chrome.serial.connect(path, options, (connectionInfo) => { + this.checkChromeLastError(); + if (connectionInfo && !this._openCanceled) { + this.addOnReceiveErrorListener(info => { + console.error(info); + googleAnalytics.sendException('Serial: ' + info.error, false); + + switch (info.error) { + case 'system_error': // we might be able to recover from this one + if (!this._failed++) { + chrome.serial.setPaused(this._connectionId, false, function () { + SerialCom.getInfo((info) => { + if (info) { + if (!info.paused) { + console.log('SERIAL: Connection recovered from last onReceiveError'); + googleAnalytics.sendException('Serial: onReceiveError - recovered', false); + + this._failed = 0; + } else { + console.log('SERIAL: Connection did not recover from last onReceiveError, disconnecting'); + GUI.log(chrome.i18n.getMessage('serialPortUnrecoverable')); + googleAnalytics.sendException('Serial: onReceiveError - unrecoverable', false); + + this.abort(); + } + } else { + this.checkChromeLastError(); + } + }); + }); + } + break; + + case 'break': // This occurs on F1 boards with old firmware during reboot + case 'overrun': + case 'frame_error': //Got disconnected + // wait 50 ms and attempt recovery + var error = info.error; + setTimeout(() => { + chrome.serial.setPaused(info.connectionId, false, function() { + SerialCom.getInfo(function (info) { + if (info) { + if (info.paused) { + // assume unrecoverable, disconnect + console.log('SERIAL: Connection did not recover from ' + error + ' condition, disconnecting'); + GUI.log(chrome.i18n.getMessage('serialPortUnrecoverable'));; + googleAnalytics.sendException('Serial: ' + error + ' - unrecoverable', false); + + this.abort(); + } else { + console.log('SERIAL: Connection recovered from ' + error + ' condition'); + googleAnalytics.sendException('Serial: ' + error + ' - recovered', false); + } + } + }); + }); + }, 50); + break; + + case 'timeout': + // TODO + break; + + case 'device_lost': + case 'disconnected': + default: + this.abort(); + } + }); + GUI.log(chrome.i18n.getMessage('connectionConnected', [path])); + } + + if (callback) { + callback(connectionInfo); + } + }); + } + + disconnectImplementation(callback) { + chrome.serial.disconnect(this._connectionId, (result) => { + if (callback) { + callback(result); + } + }); + } + + sendImplementation(data, callback) { + chrome.serial.send(this._connectionId, data, callback); + } + + addOnReceiveCallback(callback){ + chrome.serial.onReceive.addListener(callback); + } + + removeOnReceiveCallback(callback){ + chrome.serial.onReceive.removeListener(callback); + } + + addOnReceiveErrorCallback(callback) { + chrome.serial.onReceiveError.addListener(callback); + } + + removeOnReceiveErrorCallback(callback) { + chrome.serial.onReceiveError.removeListener(callback); + } + + static getDevices(callback) { + chrome.serial.getDevices((devices_array) => { + var devices = []; + devices_array.forEach((device) => { + devices.push(device.path); + }); + callback(devices); + }); + } + + static getInfo(connectionId, callback) { + chrome.serial.getInfo(connectionId, callback); + } + + static getControlSignals(connectionId, callback) { + chrome.serial.getControlSignals(connectionId, callback); + } + + static setControlSignals(connectionId, signals, callback) { + chrome.serial.setControlSignals(connectionId, signals, callback); + } +} diff --git a/js/connection/connectionTcp.js b/js/connection/connectionTcp.js new file mode 100644 index 00000000..48253cbf --- /dev/null +++ b/js/connection/connectionTcp.js @@ -0,0 +1,139 @@ +'use strict' + +const STANDARD_TCP_PORT = 5761; + +class ConnectionTcp extends Connection { + + constructor() { + super(); + + this._connectionIP = ""; + this.connectionPort = 0; + } + + connectImplementation(address, options, callback) { + var addr = address.split(':'); + if (addr.length >= 2) { + this._connectionIP = addr[0]; + this._connectionPort = parseInt(addr[1]) + } else { + this._connectionIP = address[0]; + this._connectionPort = STANDARD_PORT; + } + + chrome.sockets.tcp.create({ + name: "iNavTCP", + bufferSize: 65535 + }, createInfo => { + this.checkChromeLastError(); + if (createInfo && !this._openCanceled) { + chrome.sockets.tcp.connect(createInfo.socketId, this._connectionIP, this._connectionPort, result => { + this.checkChromeLastError(); + + if (result == 0) { + // Disable Nagle's algorithm + chrome.sockets.tcp.setNoDelay(createInfo.socketId, true, noDelayResult => { + this.checkChromeLastError(); + if (noDelayResult < 0) { + console.warn("Unable to set TCP_NODELAY: " + noDelayResult); + if (callback) { + callback(false); + } + } + + this.addOnReceiveErrorListener(info => { + console.error(info); + googleAnalytics.sendException('TCP: ' + info.error, false); + + let message; + switch (info.resultCode) { + case -15: + // connection is lost, cannot write to it anymore, preventing further disconnect attempts + message = 'error: ERR_SOCKET_NOT_CONNECTED'; + console.log(`TCP: ${message}: ${info.resultCode}`); + this._connectionId = false; + return; + case -21: + message = 'error: NETWORK_CHANGED'; + break; + case -100: + message = 'error: CONNECTION_CLOSED'; + break; + case -102: + message = 'error: CONNECTION_REFUSED'; + break; + case -105: + message = 'error: NAME_NOT_RESOLVED'; + break; + case -106: + message = 'error: INTERNET_DISCONNECTED'; + break; + case -109: + message = 'error: ADDRESS_UNREACHABLE'; + break; + } + + let resultMessage = message ? `${message} ${info.resultCode}` : info.resultCode; + console.warn(`TCP: ${resultMessage} ID: ${this._connectionId}`); + + this.abort(); + }); + + GUI.log(chrome.i18n.getMessage('connectionConnected', ["tcp://" + this._connectionIP + ":" + this._connectionPort])); + + if (callback) { + callback({ + bitrate: 115200, + connectionId: createInfo.socketId + }); + } + + }); + } else { + console.error("Unable to open TCP socket: " + result); + if (callback) { + callback(false); + } + } + }); + } else { + console.error("Unable to create TCP socket."); + + if (callback) { + callback(false); + } + } + }); + } + + disconnectImplementation(callback) { + chrome.sockets.tcp.disconnect(this._connectionId); + this.checkChromeLastError(); + this._connectionIP = ""; + this._connectionPort = 0; + + if (callback) { + callback(true); + } + } + + sendImplementation(data, callback) {; + chrome.sockets.tcp.send(this._connectionId, data, callback); + } + + addOnReceiveCallback(callback){ + chrome.sockets.tcp.onReceive.addListener(callback); + } + + removeOnReceiveCallback(callback){ + chrome.sockets.tcp.onReceive.removeListener(callback); + } + + addOnReceiveErrorCallback(callback) { + chrome.sockets.tcp.onReceiveError.addListener(callback); + } + + removeOnReceiveErrorCallback(callback) { + chrome.sockets.tcp.onReceiveError.removeListener(callback); + } +} diff --git a/js/connection/connectionUdp.js b/js/connection/connectionUdp.js new file mode 100644 index 00000000..2f50887f --- /dev/null +++ b/js/connection/connectionUdp.js @@ -0,0 +1,149 @@ +'use strict' + +const STANDARD_UDP_PORT = 5762; + +class ConnectionUdp extends Connection { + constructor() { + super(); + + this._connectionIP = ""; + this._connectionPort = 0; + this._timeoutId = false; + this._isCli = false; + } + + /** + * @param {boolean} value + */ + set isCli(value) { + this._isCli = value; + } + + connectImplementation(address, options, callback) { + var addr = address.split(':'); + if (addr.length >= 2) { + this._connectionIP = addr[0]; + this._connectionPort = parseInt(addr[1]) + } else { + this._connectionIP = address[0]; + this._connectionPort = STANDARD_UDP_PORT; + } + + chrome.sockets.udp.create({ + name: "iNavUDP", + bufferSize: 65535, + }, createInfo => { + this.checkChromeLastError(); + if (createInfo && !this._openCanceled) { + chrome.sockets.udp.bind(createInfo.socketId, "0.0.0.0", this._connectionPort, result => { + this.checkChromeLastError(); + if (result == 0) { + // UDP connections don't trigger an event if they are interrupted, a simple timeout mechanism must suffice here. + this.addOnReceiveCallback(() => { + if (this._timeoutId) { + clearTimeout(this._timeoutId); + } + + this._timeoutId = setTimeout(() => { + if (!this._isCli) { // Disable timeout for CLI + GUI.log(chrome.i18n.getMessage('connectionUdpTimeout')); + this.abort(); + } + }, 10000); + }) + + // Actually useless, but according to chrome documentation also UDP triggers error events ¯\_(ツ)_/¯ + this.addOnReceiveErrorListener(info => { + console.error(info); + googleAnalytics.sendException('UDP: ' + info.error, false); + + let message; + switch (info.resultCode) { + case -15: + // connection is lost, cannot write to it anymore, preventing further disconnect attempts + message = 'error: ERR_SOCKET_NOT_CONNECTED'; + console.log(`UDP: ${message}: ${info.resultCode}`); + this._connectionId = false; + return; + case -21: + message = 'error: NETWORK_CHANGED'; + break; + case -100: + message = 'error: CONNECTION_CLOSED'; + break; + case -102: + message = 'error: CONNECTION_REFUSED'; + break; + case -105: + message = 'error: NAME_NOT_RESOLVED'; + break; + case -106: + message = 'error: INTERNET_DISCONNECTED'; + break; + case -109: + message = 'error: ADDRESS_UNREACHABLE'; + break; + } + + let resultMessage = message ? `${message} ${info.resultCode}` : info.resultCode; + console.warn(`UDP: ${resultMessage} ID: ${this._connectionId}`); + + this.abort(); + }); + + GUI.log(chrome.i18n.getMessage('connectionConnected', ["udp://" + this._connectionIP + ":" + this._connectionPort])); + + if (callback) { + callback({ + bitrate: 115200, + connectionId: createInfo.socketId + }); + } + } else { + console.error("Unable to open UDP socket: " + result); + if (callback) { + callback(false); + } + } + }); + } else { + console.error("Unable to create UDP socket."); + if (callback) { + callback(false); + } + } + }); + } + + disconnectImplementation(callback) { + chrome.sockets.udp.close(this._connectionId); + this.checkChromeLastError(); + this._connectionIP = ""; + this._connectionPort = 0; + clearTimeout(this._timeoutId); + this._timeoutId = false; + if (callback) { + callback(true); + } + } + + sendImplementation(data, callback) {; + chrome.sockets.udp.send(this._connectionId, data, this._connectionIP, this._connectionPort, callback); + } + + addOnReceiveCallback(callback){ + chrome.sockets.udp.onReceive.addListener(callback); + } + + removeOnReceiveCallback(callback){ + chrome.sockets.udp.onReceive.removeListener(callback); + } + + addOnReceiveErrorCallback(callback) { + chrome.sockets.udp.onReceiveError.addListener(callback); + } + + removeOnReceiveErrorCallback(callback) { + chrome.sockets.udp.onReceiveError.removeListener(callback); + } +} diff --git a/js/data_storage.js b/js/data_storage.js index 3de1a4f2..1bfe7b60 100755 --- a/js/data_storage.js +++ b/js/data_storage.js @@ -7,5 +7,6 @@ var CONFIGURATOR = { 'connectionValid': false, 'connectionValidCliOnly': false, 'cliActive': false, - 'cliValid': false + 'cliValid': false, + 'connection': false }; diff --git a/js/logicConditionsCollection.js b/js/logicConditionsCollection.js index 10707f02..7d7bb762 100644 --- a/js/logicConditionsCollection.js +++ b/js/logicConditionsCollection.js @@ -6,6 +6,12 @@ let LogicConditionsCollection = function () { data = [], $container; + let max_logicConditions = 32; + + self.getMaxLogicConditionCount = function () { + return max_logicConditions; + } + self.put = function (element) { data.push(element); }; diff --git a/js/msp/MSPHelper.js b/js/msp/MSPHelper.js index b362d94f..380899c8 100644 --- a/js/msp/MSPHelper.js +++ b/js/msp/MSPHelper.js @@ -463,22 +463,33 @@ var mspHelper = (function (gui) { console.log("Servo mix saved"); break; case MSPCodes.MSP2_INAV_LOGIC_CONDITIONS: - LOGIC_CONDITIONS.flush(); - if (data.byteLength % 14 === 0) { - for (i = 0; i < data.byteLength; i += 14) { - LOGIC_CONDITIONS.put(new LogicCondition( - data.getInt8(i), - data.getInt8(i + 1), - data.getUint8(i + 2), - data.getUint8(i + 3), - data.getInt32(i + 4, true), - data.getUint8(i + 8), - data.getInt32(i + 9, true), - data.getInt8(i + 13) - )); + if (semver.gte(CONFIG.flightControllerVersion, "5.0.0")) { + LOGIC_CONDITIONS.put(new LogicCondition( + data.getInt8(0), + data.getInt8(1), + data.getUint8(2), + data.getUint8(3), + data.getInt32(4, true), + data.getUint8(8), + data.getInt32(9, true), + data.getInt8(13) + )); + } else { + if (data.byteLength % 14 === 0) { + for (i = 0; i < data.byteLength; i += 14) { + LOGIC_CONDITIONS.put(new LogicCondition( + data.getInt8(i), + data.getInt8(i + 1), + data.getUint8(i + 2), + data.getUint8(i + 3), + data.getInt32(i + 4, true), + data.getUint8(i + 8), + data.getInt32(i + 9, true), + data.getInt8(i + 13) + )); + } } } - break; case MSPCodes.MSP2_INAV_LOGIC_CONDITIONS_STATUS: @@ -2259,7 +2270,23 @@ var mspHelper = (function (gui) { }; self.loadLogicConditions = function (callback) { - MSP.send_message(MSPCodes.MSP2_INAV_LOGIC_CONDITIONS, false, false, callback); + LOGIC_CONDITIONS.flush(); + + if (semver.gte(CONFIG.flightControllerVersion, "5.0.0")) { + let idx = 0; + MSP.send_message(MSPCodes.MSP2_INAV_LOGIC_CONDITIONS, [idx], false, nextLogicCondition); + + function nextLogicCondition() { + idx++; + if (idx < LOGIC_CONDITIONS.getMaxLogicConditionCount() - 1) { + MSP.send_message(MSPCodes.MSP2_INAV_LOGIC_CONDITIONS, [idx], false, nextLogicCondition); + } else { + MSP.send_message(MSPCodes.MSP2_INAV_LOGIC_CONDITIONS, [idx], false, callback); + } + } + } else { + MSP.send_message(MSPCodes.MSP2_INAV_LOGIC_CONDITIONS, false, false, callback); + } } self.sendLogicConditions = function (onCompleteCallback) { @@ -3228,5 +3255,6 @@ var mspHelper = (function (gui) { MSP.send_message(MSPCodes.MSP2_INAV_PROGRAMMING_PID_STATUS, false, false, callback); }; + return self; })(GUI); diff --git a/js/periodicStatusUpdater.js b/js/periodicStatusUpdater.js index 213df83c..c60f9f59 100644 --- a/js/periodicStatusUpdater.js +++ b/js/periodicStatusUpdater.js @@ -31,7 +31,7 @@ helper.periodicStatusUpdater = (function () { privateScope.updateView = function () { - var active = ((Date.now() - MSP.analog_last_received_timestamp) < publicScope.getUpdateInterval(serial.bitrate) * 3); + var active = ((Date.now() - MSP.analog_last_received_timestamp) < publicScope.getUpdateInterval(CONFIGURATOR.connection.bitrate) * 3); if (FC.isModeEnabled('ARM')) $(".armedicon").css({ diff --git a/js/port_handler.js b/js/port_handler.js index 28023291..58e464fd 100755 --- a/js/port_handler.js +++ b/js/port_handler.js @@ -19,7 +19,7 @@ PortHandler.initialize = function () { PortHandler.check = function () { var self = this; - serial.getDevices(function(current_ports) { + ConnectionSerial.getDevices(function(current_ports) { // port got removed or initial_ports wasn't initialized yet if (self.array_difference(self.initial_ports, current_ports).length > 0 || !self.initial_ports) { var removed_ports = self.array_difference(self.initial_ports, current_ports); @@ -66,13 +66,16 @@ PortHandler.check = function () { chrome.storage.local.get('last_used_port', function (result) { // if last_used_port was set, we try to select it if (result.last_used_port) { - current_ports.forEach(function(port) { - if (port == result.last_used_port) { - console.log('Selecting last used port: ' + result.last_used_port); - - $('#port').val(result.last_used_port); - } - }); + if (result.last_used_port == "ble" || result.last_used_port == "tcp" || result.last_used_port == "udp") { + $('#port').val(result.last_used_port); + } else { + current_ports.forEach(function(port) { + if (port == result.last_used_port) { + console.log('Selecting last used port: ' + result.last_used_port); + $('#port').val(result.last_used_port); + } + }); + } } else { console.log('Last used port wasn\'t saved "yet", auto-select disabled.'); } @@ -175,6 +178,9 @@ PortHandler.update_port_select = function (ports) { } $('div#port-picker #port').append($("