Skip to content

Commit

Permalink
1.04: For packet uploads, add ability to ste chunk size, report progr…
Browse files Browse the repository at this point in the history
…ess or even skip searching for acks

Add example of uploading a zip
  • Loading branch information
gfwilliams committed Sep 18, 2024
1 parent 7a78944 commit 7785078
Show file tree
Hide file tree
Showing 2 changed files with 209 additions and 10 deletions.
145 changes: 145 additions & 0 deletions examples/uartUploadZIP.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<html>
<!-- This example uploads the entire contents of a zip file to Espruino using 2v25's packet
upload system. It uses `fs:true` which causes Espruino to upload to the attached SD card.
You can omit this to upload to internal storage instead. -->
<head>
</head>
<body>
<script src="../uart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script>

<button onclick="connectAndUploadZIP()">Upload a ZIP file</button>
<button onclick="UART.close();">Disconnect</button>

<div>
<span id="status"></span>
<div id="progress" style="width:100px;border:1px solid black;padding:2px;display:none;"><div id="progressInner" style="background-color:red;width:25%">&nbsp;</div></div>
</div>
<script>
console.log("Use UART.debug=3 for full debug info");

function setStatus(txt, progress) {
document.getElementById("status").innerText = txt;
if (progress===undefined)
document.getElementById("progress").style.display="none";
else {
document.getElementById("progress").style.display="inline-block";
document.getElementById("progressInner").style.width=Math.round(progress*100)+"%";
}
}


function uploadZIP() {
fileOpenDialog({
id:"backup",
type:"arraybuffer",
mimeType:".zip,application/zip"}, function(data) {
if (data===undefined) return;
var promise = Promise.resolve();
var zip = new JSZip();
var cmds = "";
zip.loadAsync(data).then(function(zip) {
console.log(`Reading ZIP`);
zip.forEach(function (path, file){
promise = promise
.then(() => {
setStatus("Decompressing"+path);
return file.async("string");
}).then(data => {
if (data.length==0) {
console.log("Can't restore files of length 0, ignoring "+path);
} else {
console.log("Uploading", path);
setStatus("Uploading "+path, 0);
return UART.getConnection().espruinoSendFile(path,data,{
fs:true,
noACK:true,
chunkSize:1024*7, // 8k packet size limit in protocol
progress: (n,chunks) => setStatus("Uploading "+path, n/chunks)
}).then(() => {
console.log("Uploaded.");
setStatus("");
});
}
});
});
return promise;
})
});
}

function connectAndUploadZIP() {
if (UART.getConnection()) {
uploadZIP();
} else {
UART.write("reset()\n", function() { // or connect
uploadZIP();
});
}
}

// just copied from EspruinoTools to let us pop up a dialog
function fileOpenDialog(options, callback) {
function readerLoaded(e,files,i,options,fileLoader) {
/* Doing reader.readAsText(file) interprets the file as UTF8
which we don't want. */
var result;
if (options.type=="text") {
var a = new Uint8Array(e.target.result);
result = "";
for (var j=0;j<a.length;j++)
result += String.fromCharCode(a[j]);
} else
result = e.target.result;
fileLoader.callback(result, files[i].type, files[i].name);


// If there's a file left to load
if (i < files.length - 1 && options.multi) {
// Load the next file
setupReader(files, i+1,options,fileLoader);
} else {
fileLoader.callback = undefined;
}
}
function setupReader(files,i,options,fileLoader) {
var reader = new FileReader();
reader.onload = function(e) {
readerLoaded(e,files,i,options,fileLoader)
};
if (options.type=="text" || options.type=="arraybuffer") reader.readAsArrayBuffer(files[i]);
else throw new Error("fileOpenDialog: unknown type "+options.type);
}
options = options||{};
options.type = options.type||"text";
options.id = options.id||"default";
var loaderId = options.id+"FileLoader";
var fileLoader = document.getElementById(loaderId);
if (!fileLoader) {
fileLoader = document.createElement("input");
fileLoader.setAttribute("id", loaderId);
fileLoader.setAttribute("type", "file");
fileLoader.setAttribute("style", "z-index:-2000;position:absolute;top:0px;left:0px;display:none;");
if (options.multi)
fileLoader.setAttribute("multiple","multiple");
if (options.mimeType)
fileLoader.setAttribute("accept",options.mimeType);
fileLoader.addEventListener('click', function(e) {
e.target.value = ''; // handle repeated upload of the same file
});
fileLoader.addEventListener('change', function(e) {
if (!fileLoader.callback) return;

var files = e.target.files;
setupReader(files,0,options,fileLoader);

}, false);
document.body.appendChild(fileLoader);
}
fileLoader.callback = callback;
fileLoader.click();
}

</script>
</body>
</html>
74 changes: 64 additions & 10 deletions uart.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,25 @@ As of Espruino 2v25 you can also send data packets:
UART.getConnection().espruinoSendFile("test.txt","This is a test of sending data to Espruino").then(_=>console.log("Done"))
UART.getConnection().espruinoSendFile("test.txt","This is a test of sending data to Espruino's SD card",{fs:true}).then(_=>console.log("Done"))
UART.debug=3;
var s = ""; for (var i=0;i<256;i++) s+=String.fromCharCode(i);
var ss = ""; for (var i=0;i<256;i++) ss+=s;
UART.getConnection().espruinoSendFile("testbin",ss,{fs:true})
UART.debug=3;
var s = ""; for (var i=0;i<256;i++) s+=String.fromCharCode(i);
UART.getConnection().espruinoSendFile("testbin",s,{fs:true})
UART.debug=3;
var s = ""; for (var i=0;i<256;i++) s+=String.fromCharCode(i)
UART.getConnection().espruinoSendFile("testbin",s)
ChangeLog:
...
1.04: For packet uploads, add ability to ste chunk size, report progress or even skip searching for acks
1.03: Added options for restricting what devices appear
Improve Web Serial Disconnection - didn't work before
1.02: Added better on/emit/removeListener handling
Expand All @@ -72,6 +88,7 @@ To do:
* move 'connection.received' handling into removeListener and add an upper limit (100k?)
* add a 'line' event for each line of data that's received
* add espruinoEval method using the new packet system
* move XON/XOFF handling into Connection.rxDataHandler
*/
(function (root, factory) {
Expand Down Expand Up @@ -157,8 +174,13 @@ To do:
connection.emit('data', data);
}

/* Send a packet of type "RESPONSE/EVAL/EVENT/FILE_SEND/DATA" to Espruino */
espruinoSendPacket(pkType, data) {
/* Send a packet of type "RESPONSE/EVAL/EVENT/FILE_SEND/DATA" to Espruino
options = {
noACK : bool (don't wait to acknowledgement)
}
*/
espruinoSendPacket(pkType, data, options) {
options = options || {};
if ("string"!=typeof data) throw new Error("'data' must be a String");
if (data.length>0x1FFF) throw new Error("'data' too long");
const PKTYPES = {
Expand All @@ -183,27 +205,59 @@ To do:
tidy();
reject();
}
connection.parsePackets = true;
connection.on("ack",onACK);
connection.on("nak",onNAK);
if (!options.noACK) {
connection.parsePackets = true;
connection.on("ack",onACK);
connection.on("nak",onNAK);
}
let flags = data.length | PKTYPES[pkType];
write(String.fromCharCode(/*DLE*/16,/*SOH*/1,(flags>>8)&0xFF,flags&0xFF)+data);
connection.write(String.fromCharCode(/*DLE*/16,/*SOH*/1,(flags>>8)&0xFF,flags&0xFF)+data, function() {
// write complete
if (options.noACK) {
resolve(); // if not listening for acks, just resolve immediately
}
});
});
}
/* Send a file to Espruino using 2v25 packets */
/* Send a file to Espruino using 2v25 packets.
options = { // mainly passed to Espruino
fs : true // optional -> write using require("fs") (to SD card)
noACK : bool // (don't wait to acknowledgements)
chunkSize : int // size of chunks to send (default 1024) for safety this depends on how big your device's input buffer is if there isn't flow control
progress : (chunkNo,chunkCount)=>{} // callback to report upload progress
} */
espruinoSendFile(filename, data, options) {
if ("string"!=typeof data) throw new Error("'data' must be a String");
let CHUNK = 1024;
options = options||{};
options.fn = filename;
options.s = data.length;
let packetOptions = {};
let progressHandler = (chunkNo,chunkCount)=>{};
if (options.noACK) {
delete options.noACK;
packetOptions.noACK = true;
}
if (options.chunkSize) {
CHUNK = options.chunkSize;
delete options.chunkSize;
}
if (options.progress) {
progressHandler = options.progress;
delete options.progress;
}
let connection = this;
let packetCount = 0, packetTotal = Math.ceil(data.length/CHUNK)+1;
// always ack the FILE_SEND
progressHandler(0, packetTotal);
return connection.espruinoSendPacket("FILE_SEND",JSON.stringify(options)).then(sendData);
// but if noACK don't ack for data
function sendData() {
progressHandler(++packetCount, packetTotal);
if (data.length==0) return Promise.resolve();
const CHUNK = 512;
let packet = data.substring(0, CHUNK);
data = data.substring(CHUNK);
return connection.espruinoSendPacket("DATA", packet).then(sendData);
return connection.espruinoSendPacket("DATA", packet, packetOptions).then(sendData);
}
}
};
Expand Down Expand Up @@ -669,7 +723,7 @@ To do:
// ----------------------------------------------------------

var uart = {
version : "1.03",
version : "1.04",
/// Are we writing debug information? 0 is no, 1 is some, 2 is more, 3 is all.
debug : 1,
/// Should we use flow control? Default is true
Expand Down

0 comments on commit 7785078

Please sign in to comment.