Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BLE, TCP and UDP Support #1498

Merged
merged 3 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,18 +141,41 @@
},

"serialPortOpened": {
"message": "Serial port <span style=\"color: #37a8db\">successfully</span> opened with ID: $1"
"message": "MSP connection <span style=\"color: #37a8db\">successfully</span> opened with ID: $1"
},
"serialPortOpenFail": {
"message": "<span style=\"color: red\">Failed</span> to open serial port"
"message": "<span style=\"color: red\">Failed</span> to open MSP connection"
},
"serialPortClosedOk": {
"message": "Serial port <span style=\"color: #37a8db\">successfully</span> closed"
"message": "MSP connection <span style=\"color: #37a8db\">successfully</span> closed"
},
"serialPortClosedFail": {
"message": "<span style=\"color: red\">Failed</span> to close serial port"
"message": "<span style=\"color: red\">Failed</span> to close MSP connection"
},
"serialPortUnrecoverable": {
"message": "Unrecoverable <span style=\"color: red\">failure</span> of serial connection, disconnecting...'"
},
"connectionConnected": {
"message": "Connected to: $1"
},
"connectionBleType": {
"message": "BLE device type: $1"
},
"connectionBleNotSupported" : {
"message": "<span style=\"color: red\">Connection error:</span> 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 <span style=\"color: #37a8db\">successfully</span> opened with ID: $1"
},
Expand Down
3 changes: 2 additions & 1 deletion eventPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,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',
Expand Down
299 changes: 299 additions & 0 deletions js/connection/connection.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading