Skip to content

Commit 80188d7

Browse files
committed
Merge branch 'packetsend' of github.com:rikkuness/EspruinoTools into rikkuness-packetsend
2 parents c036044 + 5065116 commit 80188d7

File tree

2 files changed

+267
-26
lines changed

2 files changed

+267
-26
lines changed

core/serial.js

Lines changed: 85 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -215,32 +215,50 @@ To add a new serial device, you must add an object to
215215
});
216216
}
217217
}, function(data) { // RECEIEVE DATA
218-
if (!(data instanceof ArrayBuffer)) console.warn("Serial port implementation is not returning ArrayBuffers");
219-
if (Espruino.Config.SERIAL_FLOW_CONTROL) {
220-
var u = new Uint8Array(data);
221-
for (var i=0;i<u.length;i++) {
222-
if (u[i]==17) { // XON
223-
console.log("XON received => resume upload");
224-
flowControlXOFF = false;
225-
if (flowControlTimeout) {
226-
clearTimeout(flowControlTimeout);
227-
flowControlTimeout = undefined;
218+
if (!(data instanceof ArrayBuffer)) console.warn("Serial port implementation is not returning ArrayBuffers")
219+
220+
// Filter incoming data to handle and remove control characters
221+
const filteredData = new Uint8Array(data).filter((v) => {
222+
switch (v) {
223+
case 17: // XON
224+
if (Espruino.Config.SERIAL_FLOW_CONTROL) {
225+
console.log("XON received => resume upload")
226+
flowControlXOFF = false
227+
if (flowControlTimeout) {
228+
clearTimeout(flowControlTimeout)
229+
flowControlTimeout = undefined
230+
}
231+
}
232+
return false
233+
234+
case 19: // XOFF
235+
if (Espruino.Config.SERIAL_FLOW_CONTROL) {
236+
console.log("XOFF received => pause upload")
237+
flowControlXOFF = true
238+
if (flowControlTimeout) clearTimeout(flowControlTimeout)
239+
flowControlTimeout = setTimeout(function () {
240+
console.log(
241+
`XOFF timeout (${FLOW_CONTROL_RESUME_TIMEOUT}s) => resume upload anyway`
242+
)
243+
flowControlXOFF = false
244+
flowControlTimeout = undefined
245+
}, FLOW_CONTROL_RESUME_TIMEOUT)
228246
}
229-
}
230-
if (u[i]==19) { // XOFF
231-
console.log("XOFF received => pause upload");
232-
flowControlXOFF = true;
233-
if (flowControlTimeout)
234-
clearTimeout(flowControlTimeout);
235-
flowControlTimeout = setTimeout(function() {
236-
console.log(`XOFF timeout (${FLOW_CONTROL_RESUME_TIMEOUT}s) => resume upload anyway`);
237-
flowControlXOFF = false;
238-
flowControlTimeout = undefined;
239-
}, FLOW_CONTROL_RESUME_TIMEOUT);
240-
}
247+
return false
248+
249+
case 6: // ACK
250+
emit("ack")
251+
return false
252+
253+
case 21: // NACK
254+
emit("nack")
255+
return false
241256
}
242-
}
243-
if (readListener) readListener(data);
257+
258+
return true
259+
})
260+
261+
if (readListener) readListener(filteredData.buffer)
244262
}, function(error) { // DISCONNECT
245263
currentDevice = undefined;
246264
if (writeTimeout!==undefined)
@@ -412,6 +430,45 @@ To add a new serial device, you must add an object to
412430
}
413431
};
414432

433+
/**
434+
* Simplified events system.
435+
* @typedef {"close"|"data"|"open"|"error"|"ack"|"nack"|"packet"} PacketEvent
436+
* @typedef {(...any) => void} PacketEventListener
437+
*/
438+
439+
/** @type {Object.<PacketEvent, PacketEventListener} */
440+
var pkListeners = {};
441+
442+
/**
443+
* Act on events using a simplified events listener
444+
* @param {PacketEvent} evt
445+
* @param {PacketEventListener} cb
446+
*/
447+
function on(evt, cb) {
448+
let e = "on" + evt;
449+
if (!pkListeners[e]) pkListeners[e] = [];
450+
pkListeners[e].push(cb);
451+
}
452+
453+
/**
454+
* Emit event on the event handler, will call all registered callbacks for {evt} and pass {data}
455+
* @param {PacketEvent} evt
456+
* @param {...any} data
457+
*/
458+
function emit(evt, ...data) {
459+
let e = "on" + evt;
460+
if (pkListeners[e]) pkListeners[e].forEach(fn => fn(...data));
461+
}
462+
463+
/**
464+
* Remove a {PacketEvent} listener
465+
* @param {PacketEvent} evt
466+
* @param {PacketEventListener} callback
467+
*/
468+
function removeListener(evt, callback) {
469+
let e = "on" + evt;
470+
if (pkListeners[e]) pkListeners[e] = pkListeners[e].filter(fn => fn != callback);
471+
}
415472

416473
// ----------------------------------------------------------
417474
Espruino.Core.Serial = {
@@ -442,6 +499,9 @@ To add a new serial device, you must add an object to
442499
},
443500
"setBinary": function(isOn) {
444501
sendingBinary = isOn;
445-
}
502+
},
503+
504+
// Packet events system
505+
on, emit, removeListener
446506
};
447507
})();

core/utils.js

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,181 @@
467467
}
468468
}
469469

470+
/**
471+
* Packet types mapped to their wire values
472+
* @typedef {Object} PacketTypes
473+
* @property {number} RESPONSE - Response to an EVAL packet
474+
* @property {number} EVAL - Execute and return the result as RESPONSE packet
475+
* @property {number} EVENT - Parse as JSON and create `E.on('packet', ...)` event
476+
* @property {number} FILE_SEND - Called before DATA, with {fn:"filename",s:123}
477+
* @property {number} DATA - Sent after FILE_SEND with blocks of data for the file
478+
* @property {number} FILE_RECV - Receive a file - returns a series of PT_TYPE_DATA packets, with a final zero length packet to end
479+
*/
480+
const pkTypes = Object.freeze({
481+
RESPONSE: 0,
482+
EVAL: 0x2000,
483+
EVENT: 0x4000,
484+
FILE_SEND: 0x6000,
485+
DATA: 0x8000,
486+
FILE_RECV: 0xA000
487+
})
488+
489+
/**
490+
* Creates a new packet for transfer using the packet protocol
491+
* @param {number} pkType The packet type being sent, from `PacketTypes`
492+
* @param {string} data Data to be appended to the end of the packet (max length 8191 bytes)
493+
* @returns {string}
494+
*/
495+
function createPacket(pkType, data) {
496+
497+
// Check the packet type is one of the known types
498+
if (!Object.hasOwn(pkTypes, pkType)) throw new Error(`'pkType' '${pkType}' not one of ${Object.keys(pkTypes)}`);
499+
500+
// Check the data is a string type and length is in bounds
501+
if (typeof data !== 'string') throw new Error("data must be a String");
502+
if (data.length <= 0 || data.length > 0x1FFF) throw new Error('data length is out of bounds, max 8191 bytes');
503+
504+
// Create packet heading using packet type and data length
505+
const heading = pkTypes[pkType] | data.length
506+
507+
return String.fromCharCode(
508+
16, // DLE (Data Link Escape)
509+
1, // SOH (Start of Heading)
510+
(heading >> 8) &0xFF, // Upper byte of heading
511+
heading & 0xFF // Lower byte of heading
512+
) + data; // Data blob
513+
}
514+
515+
/**
516+
* Take an input buffer and look for the initial control characters and then attempt to parse a
517+
* complete data packet from the buffer. Any complete packet is sent via `emit("packet")` and then
518+
* stripped from `buffer` modifiying it.
519+
* @param {Uint8Array} buffer
520+
* @returns {Uint8Array}
521+
*/
522+
function parsePacketsFromBuffer(buffer) {
523+
524+
// Find DLE
525+
const dle = buffer.findIndex(v => v === 0x10)
526+
if (dle < 0) return buffer
527+
528+
// Check for SOH
529+
if (buffer.at(dle + 1) !== 0x1) {
530+
// console.warn("DLE not followed by SOH")
531+
// TODO: Not stripping out this invalid control will cause a loop
532+
buffer.set([undefined], dle) // Remove this DLE
533+
return buffer
534+
}
535+
536+
// Check there's still space for headers
537+
if (buffer.at(dle + 2) === undefined || buffer.at(dle + 3) === undefined) {
538+
console.warn("NO SPACE FOR HEADERS")
539+
return buffer
540+
}
541+
const upper = buffer.at(dle + 2)
542+
const lower = buffer.at(dle + 3)
543+
544+
// Parse heading from 2 bytes after control headers
545+
const heading = new Number(upper << 8) | new Number(lower)
546+
const pkLen = heading & 0x1FFF
547+
const pkTyp = heading & 0xE000
548+
549+
// Ignoring heading bytes, check if there's enough bytes in the buffer to satisfy pkLen
550+
if (buffer.length < dle + 4 + pkLen) {
551+
return buffer
552+
}
553+
554+
// Pick out a packet from the buffer and emit it via the event handler
555+
const packet = buffer.subarray(dle, dle + 4 + pkLen)
556+
console.log("Packet recieved... type:", pkTyp, "length:", pkLen)
557+
Espruino.Core.Serial.emit('packet', pkTyp, packet.subarray(4, packet.length))
558+
559+
// Fill the buffer region of the packet that was sent with undefined
560+
buffer.fill(undefined, 0, dle + packet.length)
561+
562+
// Return the input buffer but with the stripped packet filtered out
563+
return buffer.filter(v => v !== undefined)
564+
}
565+
566+
/**
567+
* Send a packet
568+
* @param {number} pkType
569+
* @param {string} data
570+
* @param {() => void} callback
571+
*/
572+
function sendPacket(pkType, data, callback) {
573+
574+
function onAck() {
575+
// TODO: What do we actually need to do in the event of an ack
576+
// tidy()
577+
// callback()
578+
}
579+
580+
function onNack(err) {
581+
tidy()
582+
callback(err)
583+
}
584+
585+
let allData
586+
function onPacket(rxPkType, data) {
587+
tidy()
588+
const packetData = String.fromCharCode(...data)
589+
590+
// TODO: Depending on the rx type and tx type match up packet types, wait for x number of data
591+
if (pkTypes[pkType] === pkTypes.EVAL && rxPkType === pkTypes.RESPONSE) {
592+
callback(packetData)
593+
594+
// If the packet type is data, we need to wait for the 0 length `DATA` packet and then send all of the data joined together
595+
} else if (pkTypes[pkType] === pkTypes.FILE_RECV && rxPkType === pkTypes.DATA) {
596+
if (data.length === 0) {
597+
callback(allData)
598+
console.log("zero packet")
599+
} else {
600+
console.log("appending data", String.fromCharCode(...data))
601+
allData += String.fromCharCode(...data)
602+
}
603+
}else {
604+
callback("nodata")
605+
}
606+
}
607+
608+
// Tidy up the event listeners from this packet task
609+
function tidy() {
610+
Espruino.Core.Serial.removeListener("ack", onAck)
611+
Espruino.Core.Serial.removeListener("nack", onNack)
612+
Espruino.Core.Serial.removeListener("packet", onPacket)
613+
}
614+
615+
// Attach event handlers for this packet event
616+
Espruino.Core.Serial.on("ack", onAck)
617+
Espruino.Core.Serial.on("nack", onNack)
618+
Espruino.Core.Serial.on("packet", onPacket)
619+
620+
// Write packet to serial port
621+
Espruino.Core.Serial.write(createPacket(pkType, data), undefined, function () {
622+
// TODO: Add 1 sec timeout
623+
624+
let dataBuffer = new Uint8Array()
625+
626+
// Each time data comes in, expand the buffer and add the new data to it
627+
// TODO: This seems problematic if there are subsequent/concurrent calls
628+
Espruino.Core.Serial.startListening((data) => {
629+
const newBuffer = new Uint8Array(data)
630+
631+
const tempBuffer = new Uint8Array(dataBuffer.length + newBuffer.length)
632+
tempBuffer.set(dataBuffer, 0)
633+
tempBuffer.set(newBuffer, dataBuffer.length)
634+
635+
dataBuffer = tempBuffer
636+
637+
// Now we've added more data to the buffer, try to parse out any packets
638+
dataBuffer = parsePacketsFromBuffer(dataBuffer)
639+
})
640+
})
641+
}
642+
470643
/**
644+
* Download a file - storageFile or normal file
471645
* @param {string} fileName Path to file to download
472646
* @param {(content?: string) => void} callback Call back with contents of file, or undefined if no content
473647
*/
@@ -484,6 +658,10 @@ while (d!==undefined) {console.log(btoa(d));d=f.read(${CHUNKSIZE});}
484658
}, options);
485659
}
486660

661+
function downloadFileV2(fileName, fs, callback) {
662+
sendPacket("FILE_RECV", JSON.stringify({ fn: fileName, fs }), callback)
663+
}
664+
487665
/**
488666
* Get the JS needed to upload a file
489667
* @param {string} fileName Path to file to upload
@@ -1116,8 +1294,10 @@ while (d!==undefined) {console.log(btoa(d));d=f.read(${CHUNKSIZE});}
11161294
countBrackets : countBrackets,
11171295
getEspruinoPrompt : getEspruinoPrompt,
11181296
executeExpression : function(expr,callback) { executeExpression(expr,callback,{exprPrintsResult:false}); },
1297+
executeExpressionV2: function(expr,callback) { sendPacket("EVAL",expr,callback); /* TODO: Callback and parseRJSON */ },
11191298
executeStatement : function(statement,callback) { executeExpression(statement,callback,{exprPrintsResult:true}); },
11201299
downloadFile : downloadFile, // (fileName, callback)
1300+
downloadFileV2 : downloadFileV2,
11211301
getUploadFileCode : getUploadFileCode, //(fileName, contents);
11221302
uploadFile : uploadFile, // (fileName, contents, callback)
11231303
versionToFloat : versionToFloat,
@@ -1143,6 +1323,7 @@ while (d!==undefined) {console.log(btoa(d));d=f.read(${CHUNKSIZE});}
11431323
asUTF8Bytes : asUTF8Bytes,
11441324
isASCII : isASCII,
11451325
btoa : btoa,
1146-
atob : atob
1326+
atob : atob,
1327+
createPacket : createPacket
11471328
};
11481329
}());

0 commit comments

Comments
 (0)