-
Notifications
You must be signed in to change notification settings - Fork 96
/
Copy pathserial_web_bluetooth.js
258 lines (243 loc) · 10.3 KB
/
serial_web_bluetooth.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
(function() {
var isSupportedByBrowser = false;
if (typeof navigator != "undefined" &&
navigator.bluetooth) {
if (navigator.bluetooth.getAvailability)
navigator.bluetooth.getAvailability().then(x=>isSupportedByBrowser=x);
else
isSupportedByBrowser=true;
}
/**
* @param {boolean} ignoreSettings
* @returns {{warning?: string, error?: string} | boolean}
*/
function getStatus(ignoreSettings) {
/* If BLE is handled some other way (eg noble), then it can be disabled here */
if (Espruino.Core.Serial.NO_WEB_BLUETOOTH) {
OK = false;
return {warning:`Disabled by another Serial service in Web IDE (Serial.NO_WEB_BLUETOOTH)`};
}
if (typeof navigator == "undefined") {
return {warning:"Not running in a browser"};
}
if (!navigator.bluetooth) {
if (Espruino.Core.Utils.isAppleDevice())
return {error:`Safari on iOS has no Web Bluetooth support. You need <a href="https://itunes.apple.com/us/app/webble/id1193531073" target="_blank">to use the WebBLE app</a>`};
else if (Espruino.Core.Utils.isChrome() && Espruino.Core.Utils.isLinux())
return {error:`Chrome on Linux requires <code>chrome://flags/#enable-experimental-web-platform-features</code> to be enabled.`};
else if (Espruino.Core.Utils.isFirefox())
return {error:`Firefox doesn't support Web Bluetooth - try using Chrome`};
else
return {error:"No navigator.bluetooth. Do you have a supported browser?"};
}
if (navigator.bluetooth.requestDevice &&
navigator.bluetooth.requestDevice.toString().indexOf('callExtension') >= 0) {
status("info","Using Urish's Windows 10 Web Bluetooth Polyfill");
} else if (navigator.platform.indexOf("Win")>=0 &&
(navigator.userAgent.indexOf("Chrome/")>=0)) {
var chromeVer = navigator.userAgent.match(/Chrome\/(\d+)/);
if (chromeVer && chromeVer[1]<68) {
return {error:"Web Bluetooth available, but Windows Web Bluetooth is broken in <68"};
}
}
if (window && window.location && window.location.protocol=="http:" &&
window.location.hostname!="localhost") {
return {error:"Serving off HTTP (not HTTPS)"};
}
if (!isSupportedByBrowser) {
return {error:"Web Bluetooth API available, but not supported by this Browser"};
}
if (!ignoreSettings && !Espruino.Config.WEB_BLUETOOTH)
return {warning:`"Web Bluetooth" disabled in settings`};
return true;
}
var OK = true;
var NORDIC_SERVICE = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
var NORDIC_TX = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
var NORDIC_RX = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
var NORDIC_DEFAULT_TX_LENGTH = 20; ///< default value for maxPacketLength
var testedCompatibility = false;
/// List of previously paired devices that we could reconnect to without the chooser
var pairedDevices = [];
var btServer = undefined;
var connectionDisconnectCallback;
var txCharacteristic;
var rxCharacteristic;
var maxPacketLength; ///< packet length (MTU-3) for the currently active connection
var txInProgress = false;
function init() {
Espruino.Core.Config.add("WEB_BLUETOOTH", {
section : "Communications",
name : "Connect over Bluetooth LE (Web Bluetooth)",
descriptionHTML : 'Allow connection to Espruino via BLE with the Nordic UART implementation',
type : "boolean",
defaultValue : true,
});
Espruino.Core.Config.add("WEB_BLUETOOTH_FILTER", {
section : "Communications",
name : "Bluetooth LE (Web Bluetooth) Filter",
descriptionHTML : 'If non-empty, only Bluetooth devices with names that start with this value will be shown in the Web Bluetooth device list (otherwise any supported device is shown). For example entering <code>Puck.js</code> will return only Puck.js devices.',
type : "string",
defaultValue : "",
});
// If we're ok and have the getDevices extension, use it to remember previously paired devices
if (getStatus(true)===true && navigator.bluetooth.getDevices) {
console.log("BT> bluetooth.getDevices exists - grab known devices");
navigator.bluetooth.getDevices().then(devices=>{
pairedDevices = devices;
});
}
}
function getPorts(callback) {
if (!testedCompatibility) {
testedCompatibility = true;
/* Check compatibility here - the Web Bluetooth Polyfill for windows
loads after everything else, so we can't check when this page is
parsed.*/
if (getStatus(true)!==true)
OK = false;
}
if (Espruino.Config.WEB_BLUETOOTH && OK) {
var list = [{path:'Web Bluetooth', description:'Bluetooth Low Energy', type : "bluetooth", promptsUser:true}];
pairedDevices.forEach(function(btDevice) {
list.push({path:btDevice.name, description:'Web Bluetooth device', type : "bluetooth"});
});
callback(list, true/*instantPorts*/);
} else
callback(undefined, true/*instantPorts*/);
}
function setMaxPacketLength(n) {
maxPacketLength = n;
SerialDevice.maxWriteLength = n;
}
function openSerial(serialPort, openCallback, receiveCallback, disconnectCallback) {
connectionDisconnectCallback = disconnectCallback;
var btDevice;
var btService;
var promise;
// Check for pre-paired devices
btDevice = pairedDevices.find(dev=>dev.name == serialPort);
if (btDevice) {
console.log("BT> Pre-paired Web Bluetooth device already found");
promise = Promise.resolve(btDevice);
} else {
var filters = [];
if (Espruino.Config.WEB_BLUETOOTH_FILTER && Espruino.Config.WEB_BLUETOOTH_FILTER.trim().length) {
filters.push({ namePrefix: Espruino.Config.WEB_BLUETOOTH_FILTER.trim() });
} else {
Espruino.Core.Utils.recognisedBluetoothDevices().forEach(function(namePrefix) {
filters.push({ namePrefix: namePrefix });
});
filters.push({ services: [ NORDIC_SERVICE ] });
}
console.log("BT> Starting device chooser");
promise = navigator.bluetooth.requestDevice({
filters: filters,
optionalServices: [ NORDIC_SERVICE ]});
}
promise.then(function(device) {
btDevice = device;
Espruino.Core.Status.setStatus("Connecting to "+btDevice.name);
console.log('BT> Device Name: ' + btDevice.name);
console.log('BT> Device ID: ' + btDevice.id);
// Was deprecated: Should use getPrimaryServices for this in future
//console.log('BT> Device UUIDs: ' + device.uuids.join('\n' + ' '.repeat(21)));
btDevice.addEventListener('gattserverdisconnected', function() {
console.log("BT> Disconnected (gattserverdisconnected)");
closeSerial();
}, {once:true});
return btDevice.gatt.connect();
}).then(function(server) {
Espruino.Core.Status.setStatus("Connected to BLE");
console.log("BT> Connected");
btServer = server;
return server.getPrimaryService(NORDIC_SERVICE);
}).then(function(service) {
Espruino.Core.Status.setStatus("Configuring BLE...");
console.log("BT> Got service");
btService = service;
return btService.getCharacteristic(NORDIC_RX);
}).then(function (characteristic) {
Espruino.Core.Status.setStatus("Configuring BLE....");
rxCharacteristic = characteristic;
setMaxPacketLength(NORDIC_DEFAULT_TX_LENGTH); // set default packet length
console.log("BT> RX characteristic:"+JSON.stringify(rxCharacteristic));
rxCharacteristic.addEventListener('characteristicvaluechanged', function(event) {
// In Chrome 50+, a DataView is returned instead of an ArrayBuffer.
var value = event.target.value.buffer;
if (value.byteLength > maxPacketLength) {
console.log("BT> Received packet of length "+value.byteLength+" - assuming increased MTU");
setMaxPacketLength(value.byteLength);
}
//console.log("BT> RX:"+JSON.stringify(Espruino.Core.Utils.arrayBufferToString(value)));
receiveCallback(value);
});
return rxCharacteristic.startNotifications();
}).then(function() {
Espruino.Core.Status.setStatus("Configuring BLE....");
return btService.getCharacteristic(NORDIC_TX);
}).then(function (characteristic) {
Espruino.Core.Status.setStatus("Configuring BLE.....");
txCharacteristic = characteristic;
console.log("BT> TX characteristic:"+JSON.stringify(txCharacteristic));
}).then(function() {
Espruino.Core.Status.setStatus("Configuring BLE.....");
txInProgress = false;
Espruino.Core.Serial.setSlowWrite(false, true); // hack - leave throttling up to this implementation
if (!pairedDevices.includes(btDevice))
pairedDevices.push(btDevice);
setTimeout(function() {
Espruino.Core.Status.setStatus("BLE configured. Receiving data...");
openCallback({ portName : btDevice.name });
}, 500);
}).catch(function(error) {
console.log('BT> ERROR: ' + error);
closeSerial({error:error.toString()});
});
}
function closeSerial(err) {
if (btServer) {
btServer.disconnect();
btServer = undefined;
txCharacteristic = undefined;
rxCharacteristic = undefined;
setMaxPacketLength(NORDIC_DEFAULT_TX_LENGTH); // set default
}
if (connectionDisconnectCallback) {
connectionDisconnectCallback(err);
connectionDisconnectCallback = undefined;
}
}
// Throttled serial write
function writeSerial(data, callback) {
if (!txCharacteristic) return;
if (data.length>maxPacketLength) {
console.error("BT> TX length >"+maxPacketLength);
return callback();
}
if (txInProgress) {
console.error("BT> already sending!");
return callback();
}
txInProgress = true;
txCharacteristic.writeValue(Espruino.Core.Utils.stringToArrayBuffer(data)).then(function() {
txInProgress = false;
callback();
}).catch(function(error) {
console.log('BT> SEND ERROR: ' + error);
closeSerial();
});
}
// ----------------------------------------------------------
var SerialDevice = {
"name" : "Web Bluetooth",
"init" : init,
"getStatus": getStatus,
"getPorts": getPorts,
"open": openSerial,
"write": writeSerial,
"close": closeSerial,
"maxWriteLength" : maxPacketLength,
};
Espruino.Core.Serial.devices.push(SerialDevice);
})();