-
Notifications
You must be signed in to change notification settings - Fork 2
/
serial.js
345 lines (325 loc) · 15.7 KB
/
serial.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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
/* Copyright (c) 2019 Parallax Inc., All Rights Reserved. */
//TODO Study effects of sudden USB port disappearance and try to handle gracefully
//TODO Enhance to protect against (or support) downloading to multiple active ports simultaneously (involves loadPropeller, talkToProp, and hearFromProp)
//TODO Revisit promisify and see if it will clean up code significantly
//TODO Study .bind for opportunities to save scope context of private functions
/***********************************************************
* Serial Support Functions *
***********************************************************/
//TODO Consider enhancing error to indicate if the port is already open (this would only be for developer mistakes though)
function openPort(sock, portName, baudrate, connMode) {
/* Return a promise to open wired or wireless port at portName with baudrate and connect to browser sock. If wireless, the port is opened
as a Telnet-based debug service.
sock can be null to open port without an associated browser socket
portName is the string name of the wired or wireless port
baudrate is optional; defaults to initialBaudrate
connMode is the current point of the connection; 'debug', 'programming'
Resolves (with nothing); rejects with Error*/
return new Promise(function(resolve, reject) {
baudrate = baudrate ? parseInt(baudrate) : initialBaudrate;
var port = findPort(byName, portName);
if (port) {
if (port.isWired) { /*Wired port*/
if (port.connId) {
//Already open; ensure correct baudrate, socket, and connMode, then resolve.
updatePort(port, {bSocket: sock, mode: connMode, baud: baudrate})
.then(function() {resolve()})
.catch(function(e) {reject(e)});
} else {
//Not already open; attempt to open it
chrome.serial.connect(port.path, {bitrate: baudrate, dataBits: 'eight', parityBit: 'no', stopBits: 'one', ctsFlowControl: false},
function (openInfo) {
if (!chrome.runtime.lastError) {
// No error; update serial port object
updatePort(port, {connId: openInfo.connectionId, bSocket: sock, mode: connMode});
port.baud = baudrate; //Update baud; does not use updatePort() to avoid unnecessary port activity
log("Port " + portName + " open with ID " + openInfo.connectionId + " at " + baudrate + " baud", mDbug);
resolve();
} else {
// Error
reject(Error(notice(neCanNotOpenPort, [port.name])));
}
}
);
}
} else { /*Wireless port*/
openSocket(port, false)
.then(updatePort(port, {bSocket: sock, mode: connMode, baud: baudrate})
.then(function() {resolve()})
.catch(function (e) {reject(e)}));
}
} else {
// Error; port record not found
reject(Error(notice(neCanNotFindPort, [portName])));
}
});
}
function openSocket(port, command) {
/* Open Propeller command (HTTP) or debug (Telnet) socket on port
port is the port's object
command is true to open HTTP-based command service and false to open Telnet-based Debug service
Resolves with object describing socket type*/
return new Promise(function(resolve, reject) {
let p = (command) ? {socket: "phSocket", portNum: 80} : {socket: "ptSocket", portNum: 23};
if (port[p.socket]) { // Already open; resolve
resolve(p);
} else { // No ph or pt socket yet; create one and connect to it
chrome.sockets.tcp.create(function (info) {
updatePort(port, {[p.socket]: info.socketId});
chrome.sockets.tcp.connect(port[p.socket], port.ip, p.portNum, function () {
//TODO Handle connect result
chrome.sockets.tcp.setNoDelay(info.socketId, true, function(result) {
if (result < 0) {log("Warning: unable to disable Nagle timer", mDbug)}
resolve(p);
});
});
});
}
});
}
//TODO !!! This is no longer a pure-wired-serial function; decide what to do long-term
function closePort(port, command) {
/* Close the port.
port is the port object
command [ignored unless wireless] must be true to close socket to Wi-Fi Module's HTTP-based command service and false to close socket to Propeller via Telnet service
Resolves (with nothing); rejects with Error*/
return new Promise(function(resolve, reject) {
function socketClose(socket) {
// Nullify port's HTTP or Telnet socket reference
let sID = port[socket];
if (sID) {
updatePort(port, {[socket]: null});
// Disconnect and/or close socket (if necessary)
chrome.sockets.tcp.getInfo(sID, function(info) {
log("Closing wireless port socket " + sID, mDbug);
if (info.connected) {
chrome.sockets.tcp.disconnect(sID, function() {
chrome.sockets.tcp.close(sID, function() {
resolve();
})
})
} else {
chrome.sockets.tcp.close(sID, function() {
resolve();
})
}
});
} else {
reject(Error(notice(neCanNotClosePort, [port.name])));
}
}
if (port) {
if (port.isWired) {
// Wired port
if (port.connId) {
chrome.serial.disconnect(port.connId, function (closeResult) {
if (closeResult) {
log("Closed port " + port.name + " (id " + port.connId + ")", mDbug);
// Clear connection id to indicate port is closed
updatePort(port, {connId: null});
resolve();
} else {
log("Could not close port " + port.name + " (id " + port.connId + ")", mDbug);
reject(Error(notice(neCanNotClosePort, [port.name])));
}
});
}
} else {
// Wireless port
socketClose((command) ? "phSocket" : "ptSocket");
}
}
});
}
//TODO !!! This is no longer a pure-wired-serial function; decide what to do long-term
function changeBaudrate(port, baudrate) {
/* Return a promise that changes the port's baudrate.
port is the port's object
baudrate is optional; defaults to finalBaudrate
Resolves (with nothing); rejects with Error*/
return new Promise(function(resolve, reject) {
baudrate = baudrate ? parseInt(baudrate) : finalBaudrate;
if (port.baud !== baudrate) {
// Need to change current baudrate
log("Changing " + port.name + " to " + baudrate + " baud", mDbug);
if (port.isWired) {
chrome.serial.update(port.connId, {'bitrate': baudrate}, function (updateResult) {
if (updateResult) {
port.baud = baudrate; //Update baud; does not use updatePort() to avoid circular reference
resolve();
} else {
reject(Error(notice(neCanNotSetBaudrate, [port.name, baudrate])));
}
});
} else {
//TODO Need to check for errors.
resetPropComm(port, 1500, sgWXResponse, notice(neCanNotSetBaudrate, [port.name, baudrate]), true);
openSocket(port, true)
.then(function(p) {
let postStr = "POST /wx/setting?name=baud-rate&value=" + baudrate + " HTTP/1.1\r\n\r\n";
chrome.sockets.tcp.send(port.phSocket, str2ab(postStr), function () {
propComm.response
.then(function() {port.baud = baudrate; return resolve();}) //Update baud; does not use updatePort() because of circular reference //!!!
.catch(function(e) {return reject(e);})
});
})
.catch(function(e) {return reject(e)});
}
} else {
// Port is already set to baudrate
resolve();
}
});
}
function setControl(port, options) {
/* Return a promise that sets/clears the control option(s).
port is the open port's object*/
return new Promise(function(resolve, reject) {
chrome.serial.setControlSignals(port.connId, options, function(controlResult) {
if (controlResult) {
resolve();
} else {
reject(Error(notice(000, ["Can not set port " + port.name + "'s options" + (options.hasOwnProperty('dtr') ? " - DTR: " + options.dtr : "")])));
}
});
});
}
function flush(port) {
/* Return a promise that empties the transmit and receive buffers
port is the open port's object*/
return new Promise(function(resolve, reject) {
chrome.serial.flush(port.connId, function(flushResult) {
if (flushResult) {
resolve();
} else {
reject(Error(notice(000, ["Can not flush port " + port.name + "'s transmit/receive buffer"])));
}
});
});
}
function unPause(port) {
/* Return a promise that unpauses the port
port is the open port's object*/
return new Promise(function(resolve) {
chrome.serial.setPaused(port.connId, false, function() {
resolve();
});
});
}
function ageWiredPorts() {
// Age wired ports, remove those that haven't been seen for some time from the list
ports.forEach(function(p) {
if (p.isWired && !--p.life) {deletePort(byName, p.name)}
})
}
//TODO !!! This is no longer a pure-wired-serial function; decide what to do long-term
//TODO Check send callback
//TODO Reject with error objects as needed
function send(port, data, command) {
/* Return a promise that transmits data on port. Port must already be open if wired, may be open or not if wireless.
port is the port's object
data is an ArrayBuffer
command [ignored unless wireless] is true to send to Wi-Fi Module's HTTP-based command service and false to send to Propeller via Telnet service*/
return new Promise(function(resolve, reject) {
if (port.isWired) { // Wired port
if (platform !== pfMac) {
//Any platform other than Mac? Send as one full packet (any size)
//log('send(): '+new Uint8Array(data), mDeep) //.subarray(0, 4)+'/'+txView.subarray(4, 8), mDeep);
chrome.serial.send(port.connId, data, function () {
resolve();
});
} else {
//Mac platform? Split into smaller chucks (1,024-byte or less) so Mac can transmit properly
let idx, last, chunk = 1024, macPackets = []; //Support vars plus array of small buffers to handle "baffling" limited transmission size on a Mac
for (idx = 0, last = data.byteLength; idx < last; idx += chunk) {
let size = Math.min(chunk, last-idx); //Make size <= 1024
macPackets.push(new ArrayBuffer(size)); //Add pre-sized element to macPackets then set data into that element
(new Uint8Array(macPackets[macPackets.length-1])).set((new Uint8Array(data)).slice(idx, idx+size), 0);
}
//Transmit all packets, one after another
let transmit = function() {
if (macPackets.length) {
chrome.serial.send(port.connId, macPackets.shift(), transmit);
} else {
resolve();
}
};
transmit();
}
} else { // Wireless port
openSocket(port, command)
.then(function (p) {
chrome.sockets.tcp.send(port[p.socket], data, function () {
//TODO handle send result
resolve();
});
})
.catch(function (e) {reject(e)});
}
});
}
//TODO !!! This is no longer a pure-wired-serial function; decide what to do long-term
function debugReceiver(info) {
// Wired and wireless receive listener- routes debug data from Propeller to connected browser when necessary
let wired = (info.hasOwnProperty("connectionId"));
let port = wired ? findPort(byCID, info.connectionId) : findPort(byPTID, info.socketId);
if (port) {
if (port.mode === 'debug' && port.bSocket) {
// send to terminal in browser tab
let offset = 0;
do {
let byteCount = Math.min(info.data.byteLength-offset, serPacketMax-port.packet.len);
port.packet.bufView.set(new Uint8Array(info.data).slice(offset, offset+byteCount), port.packet.len);
port.packet.len += byteCount;
offset += byteCount;
if (port.packet.len === serPacketMax) {
sendDebugPacket(port);
} else if (port.packet.timer === null) {
port.packet.timer = setTimeout(sendDebugPacket, serPacketMaxTxTime, port)
}
} while (offset < info.data.byteLength);
}
}
function sendDebugPacket(port) {
if (port.packet.timer !== null) {
clearTimeout(port.packet.timer);
port.packet.timer = null;
}
if (port.mode === 'debug' && port.bSocket) {
port.bSocket.send(JSON.stringify({type: 'serial-terminal', packetID: port.packet.id++, msg: btoa(ab2str(port.packet.bufView.slice(0, port.packet.len)))}));
}
port.packet.len = 0;
}
}
//TODO !!! This is no longer a pure-wired-serial function; decide what to do long-term
function serialError(info) {
// Wired and wireless serial error listener.
if (info.hasOwnProperty("connectionId")) {
switch (info.error) {
case "disconnected":
case "device_lost" :
case "system_error": deletePort(byCID, info.connectionId);
}
// log("Error: PortID "+info.connectionId+" "+info.error, mDeep);
} else {
switch (info.resultCode) {
case -100: //Port closed
//Find port by Propeller Telnet ID or HTTP ID and clear record
let port = findPort(byPTID, info.socketId);
if (port) {
updatePort(port, {ptSocket: null});
} else {
port = findPort(byPHID, info.socketId);
if (port) {updatePort(port, {phSocket: null})}
}
if (port) {
log("SocketID " + info.socketId + " connection closed" + ((port) ? " for port " + port.name + "." : "."), mDeep);
}
break;
default:
log("Error: SocketID " + info.socketId + " Code " + info.resultCode, mDeep);
}
}
}
chrome.serial.onReceive.addListener(debugReceiver);
chrome.serial.onReceiveError.addListener(serialError);