UNPKG

espruino

Version:

Command Line Interface and library for Communications with Espruino JavaScript Microcontrollers

849 lines (806 loc) 34.6 kB
/* Gordon Williams (gw@pur3.co.uk) Common entrypoint for all communications from the IDE. This handles all serial_*.js connection types and passes calls to the correct one. To add a new serial device, you must add an object to Espruino.Core.Serial.devices: Espruino.Core.Serial.devices.push({ "name" : "Test", // Name, when initialising "init" : function() // Gets called at startup "getStatus" : function(ignoreSettings) // Optional - returns: // true - all ok // {error: error_string} // {warning: warning_string} "getPorts": function(callback) // calls 'callback' with an array of ports: callback([{path:"TEST", // path passed to 'open' (and displayed to user) description:"test", // description displayed to user type:"test", // bluetooth|usb|socket - used to show icon in UI // autoconnect : true // automatically conect to this (without the connect menu) // promptsUser : true // this is set if we expect the Web Browser to prompt the user for this item }], true); // instantPorts - will getPorts return all the ports on the first call, or does it need multiple calls (eg. Bluetooth) "open": function(path, openCallback, receiveCallback, disconnectCallback), "write": function(dataAsString, callbackWhenWritten) "close": function(), "maxWriteLength": 20, // optional - the maximum amount of characters that should be given to 'write' at a time }); */ (function() { // List of ports and the devices they map to var portToDevice = undefined; // The current connected device (from Espruino.Core.Serial.devices) var currentDevice = undefined; // called when data received var readListener = undefined; // are we sending binary data? If so, don't automatically insert breaks for stuff like Ctrl-C var sendingBinary = false; // For throttled write var slowWrite = true; // filler to allow us to use EspruinoWebTools' Connection class var uart = { showProgress : false, writeProgress : function(chars, charsMax) { if (chars===undefined) { this.lastProgress = 0; } else { var diff = chars-this.lastProgress; if (chars==0) diff=0; this.lastProgress = chars; if (this.showProgress) Espruino.Core.Status.incrementProgress(diff); } } }; // --------------- var logLevel = 2; function log(level, str) { if (level<=logLevel) console.log("serial:", str); } function ab2str(buf) { return String.fromCharCode.apply(null, new Uint8Array(buf)); } var parseRJSON = data => Espruino.Core.Utils.parseRJSON(data); // --------------- /// Base connection class - BLE/Serial add writeLowLevel/closeLowLevel/etc on top of this class Connection { endpoint = undefined; // Set to the endpoint used for this connection - eg maybe endpoint.name=="Web Bluetooth" // on/emit work for close/data/open/error/ack/nak/packet events on(evt,cb) { let e = "on"+evt; if (!this[e]) this[e]=[]; this[e].push(cb); } // on only works with a single handler emit(evt,data1,data2) { let e = "on"+evt; if (this[e]) this[e].forEach(fn=>fn(data1,data2)); } removeListener(evt,callback) { let e = "on"+evt; if (this[e]) this[e]=this[e].filter(fn=>fn!=callback); } removeAllListeners(evt) { let e = "on"+evt; delete this[e]; } // on("open", () => ... ) connection opened // on("close", () => ... ) connection closed // on("data", (data) => ... ) when data is received (as string) // on("line", (line) => ... ) when a line of data is received (as string), uses /r OR /n for lines // on("packet", (type,data) => ... ) when a packet is received (if .parsePackets=true) // on("ack", () => ... ) when an ACK is received (if .parsePackets=true) // on("nak", () => ... ) when an ACK is received (if .parsePackets=true) // writeLowLevel(string)=>Promise to be provided by implementor // closeLowLevel() to be provided by implementor // cb(dataStr) called if defined isOpen = false; // is the connection actually open? isOpening = true; // in the process of opening a connection? txInProgress = false; // is transmission in progress? txDataQueue = []; // queue of {data,callback,maxLength,resolve} chunkSize = 20; // Default size of chunks to split transmits into (BLE = 20, Serial doesn't care) parsePackets = false; // If set we parse the input stream for Espruino packet data transfers received = ""; // The data we've received so far - this gets reset by .write/eval/etc hadData = false; // used when waiting for a block of data to finish being received flowControlWait = 0; // If this is nonzero, we should hold off sending for that number of milliseconds (wait and decrement each time) rxDataHandlerLastCh = 0; // used by rxDataHandler - last received character rxDataHandlerPacket = undefined; // used by rxDataHandler - used for parsing rxDataHandlerTimeout = undefined; // timeout for unfinished packet rxLine = ""; // current partial line for on("line" event progressAmt = 0; // When sending a file, how many bytes through are we? progressMax = 0; // When sending a file, how long is it in bytes? 0 if not sending a file /// Called when sending data, and we take this (along with progressAmt/progressMax) and create a more detailed progress report updateProgress(chars, charsMax) { if (chars===undefined) return uart.writeProgress(); if (this.progressMax) uart.writeProgress(this.progressAmt+chars, this.progressMax); else uart.writeProgress(chars, charsMax); } /** Called when characters are received. This processes them and passes them on to event listeners */ rxDataHandler(data) { if (!(data instanceof ArrayBuffer)) console.warn("Serial port implementation is not returning ArrayBuffers"); data = ab2str(data); // now a string! log(3, "Received "+JSON.stringify(data)); if (this.parsePackets) { for (var i=0;i<data.length;i++) { let ch = data[i]; // handle packet reception if (this.rxDataHandlerPacket!==undefined) { this.rxDataHandlerPacket += ch; ch = undefined; let flags = (this.rxDataHandlerPacket.charCodeAt(0)<<8) | this.rxDataHandlerPacket.charCodeAt(1); let len = flags & 0x1FFF; let rxLen = this.rxDataHandlerPacket.length; if (rxLen>=2 && rxLen>=(len+2)) { log(3, "Got packet end"); if (this.rxDataHandlerTimeout) { clearTimeout(this.rxDataHandlerTimeout); this.rxDataHandlerTimeout = undefined; } this.emit("packet", flags&0xE000, this.rxDataHandlerPacket.substring(2)); this.rxDataHandlerPacket = undefined; // stop packet reception } } else if (ch=="\x06") { // handle individual control chars log(3, "Got ACK"); this.emit("ack"); ch = undefined; } else if (ch=="\x15") { log(3, "Got NAK"); this.emit("nak"); ch = undefined; } else if (uart.flowControl && ch=="\x11") { // 17 -> XON log(2,"XON received => resume upload"); this.flowControlWait = 0; } else if (uart.flowControl && ch=="\x13") { // 19 -> XOFF log(2,"XOFF received => pause upload (10s)"); this.flowControlWait = 10000; } else if (ch=="\x10") { // DLE - potential start of packet (ignore) this.rxDataHandlerLastCh = "\x10"; ch = undefined; } else if (ch=="\x01" && this.rxDataHandlerLastCh=="\x10") { // SOH log(3, "Got packet start"); this.rxDataHandlerPacket = ""; this.rxDataHandlerTimeout = setTimeout(()=>{ this.rxDataHandlerTimeout = undefined; log(0, "Packet timeout (2s)"); this.rxDataHandlerPacket = undefined; }, 2000); ch = undefined; } if (ch===undefined) { // if we're supposed to remove the char, do it data = data.substring(0,i)+data.substring(i+1); i--; } else this.rxDataHandlerLastCh = ch; } } this.hadData = true; if (data.length>0) { // keep track of received data if (this.received.length < 100000) // ensure we're not creating a memory leak this.received += data; // forward any data if (this.cb) this.cb(data); this.emit('data', data); // look for newlines and send out a 'line' event let lines = (this.rxLine + data).split(/\r\n/); while (lines.length>1) this.emit('line', lines.shift()); this.rxLine = lines[0]; if (this.rxLine.length > 10000) // only store last 10k characters this.rxLine = this.rxLine.slice(-10000); } } /** Called when the connection is opened */ openHandler() { log(1, "Connected"); this.txInProgress = false; this.isOpen = true; this.isOpening = false; this.received = ""; this.hadData = false; this.flowControlWait = 0; this.rxDataHandlerLastCh = 0; this.rxLine = ""; if (!this.isOpen) { this.isOpen = true; this.emit("open"); } // if we had any writes queued, do them now this.write(); } /** Called when the connection is closed - resets any stored info/rejects promises */ closeHandler() { this.isOpening = false; this.txInProgress = false; this.txDataQueue = []; this.hadData = false; if (this.isOpen) { log(1, "Disconnected"); this.isOpen = false; this.emit("close"); } } /** Called to close the connection */ close() { this.closeLowLevel(); this.closeHandler(); } /** Call this to send data, this splits data, handles queuing and flow control, and calls writeLowLevel to actually write the data. * 'callback' can optionally return a promise, in which case writing only continues when the promise resolves * @param {string} data * @param {() => Promise|void} callback * @returns {Promise} */ write(data, callback) { let connection = this; return new Promise((resolve,reject) => { if (data) connection.txDataQueue.push({data:data,callback:callback,maxLength:data.length,resolve:resolve}); if (connection.isOpen && !connection.txInProgress) writeChunk(); function writeChunk() { if (connection.flowControlWait) { // flow control - try again later if (connection.flowControlWait>50) connection.flowControlWait-=50; else { log(2,"Flow Control timeout"); connection.flowControlWait=0; } setTimeout(writeChunk, 50); return; } if (!connection.txDataQueue.length) { // we're finished! connection.txInProgress = false; connection.updateProgress(); return; } connection.txInProgress = true; var chunk, txItem = connection.txDataQueue[0]; connection.updateProgress(txItem.maxLength - (txItem.data?txItem.data.length:0), txItem.maxLength); if (txItem.data.length <= connection.chunkSize) { chunk = txItem.data; txItem.data = undefined; } else { chunk = txItem.data.substr(0,connection.chunkSize); txItem.data = txItem.data.substr(connection.chunkSize); } log(2, "Sending "+ JSON.stringify(chunk)); connection.writeLowLevel(chunk).then(function() { log(3, "Sent"); let promise = undefined; if (!txItem.data) { connection.txDataQueue.shift(); // remove this element if (txItem.callback) promise = txItem.callback(); if (txItem.resolve) txItem.resolve(); } if (!(promise instanceof Promise)) promise = Promise.resolve(); promise.then(writeChunk); // if txItem.callback() returned a promise, wait until it completes before continuing }, function(error) { log(1, 'SEND ERROR: ' + error); connection.updateProgress(); connection.txDataQueue = []; connection.close(); }); } }); } /* Send a packet of type "RESPONSE/EVAL/EVENT/FILE_SEND/DATA" to Espruino options = { noACK : bool (don't wait to acknowledgement - default=false) timeout : int (optional, milliseconds, default=5000) if noACK=false } */ espruinoSendPacket(pkType, data, options) { options = options || {}; if (!options.timeout) options.timeout=5000; if ("string"!=typeof data) throw new Error("'data' must be a String"); if (data.length>0x1FFF) throw new Error("'data' too long"); const PKTYPES = { RESPONSE : 0, // Response to an EVAL packet EVAL : 0x2000, // execute and return the result as RESPONSE packet EVENT : 0x4000, // parse as JSON and create `E.on('packet', ...)` event FILE_SEND : 0x6000, // called before DATA, with {fn:"filename",s:123} DATA : 0x8000, // Sent after FILE_SEND with blocks of data for the file FILE_RECV : 0xA000 // receive a file - returns a series of PT_TYPE_DATA packets, with a final zero length packet to end } if (!(pkType in PKTYPES)) throw new Error("'pkType' not one of "+Object.keys(PKTYPES)); let connection = this; return new Promise((resolve,reject) => { let timeout; function tidy() { if (timeout) { clearTimeout(timeout); timeout = undefined; } connection.removeListener("ack",onACK); connection.removeListener("nak",onNAK); } function onACK(ok) { tidy(); setTimeout(resolve,0); } function onNAK(ok) { tidy(); setTimeout(reject,0,"NAK while sending packet"); } if (!options.noACK) { connection.parsePackets = true; connection.on("ack",onACK); connection.on("nak",onNAK); } let flags = data.length | PKTYPES[pkType]; connection.write(String.fromCharCode(/*DLE*/16,/*SOH*/1,(flags>>8)&0xFF,flags&0xFF)+data, function() { // write complete if (options.noACK) { setTimeout(resolve,0); // if not listening for acks, just resolve immediately } else { timeout = setTimeout(function() { timeout = undefined; tidy(); reject(`Timeout (${options.timeout}ms) while sending packet`); }, options.timeout); } }, err => { tidy(); reject(err); }); }); } /* 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 timeout : int (optional, milliseconds, default=1000) } */ 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 !== undefined) { packetOptions.noACK = !!options.noACK; delete options.noACK; } if (options.chunkSize) { CHUNK = options.chunkSize; delete options.chunkSize; } if (options.progress) { progressHandler = options.progress; delete options.progress; } options.fs = options.fs?1:0; // .fs => use SD card if (!options.fs) delete options.fs; // default=0, so just remove if it's not set let connection = this; let packetCount = 0, packetTotal = Math.ceil(data.length/CHUNK)+1; connection.progressAmt = 0; connection.progressMax = 100 + data.length; // always ack the FILE_SEND progressHandler(0, packetTotal); return connection.espruinoSendPacket("FILE_SEND",JSON.stringify(options)).then(sendData, err=> { connection.progressAmt = 0; connection.progressMax = 0; throw err; }); // but if noACK don't ack for data function sendData() { connection.progressAmt += connection.progressAmt?CHUNK:100; progressHandler(++packetCount, packetTotal); if (data.length==0) { connection.progressAmt = 0; connection.progressMax = 0; return Promise.resolve(); } let packet = data.substring(0, CHUNK); data = data.substring(CHUNK); return connection.espruinoSendPacket("DATA", packet, packetOptions).then(sendData, err=> { connection.progressAmt = 0; connection.progressMax = 0; throw err; }); } } /* Receive a file from Espruino using 2v25 packets. options = { // mainly passed to Espruino fs : true // optional -> write using require("fs") (to SD card) timeout : int // milliseconds timeout (default=2000) progress : (bytes)=>{} // callback to report upload progress } } */ espruinoReceiveFile(filename, options) { options = options||{}; options.fn = filename; if (!options.progress) options.progress = (bytes)=>{}; let connection = this; return new Promise((resolve,reject) => { let fileContents = "", timeout; function scheduleTimeout() { if (timeout) clearTimeout(timeout); timeout = setTimeout(() => { timeout = undefined; cleanup(); reject("espruinoReceiveFile Timeout"); }, options.timeout || 2000); } function cleanup() { connection.removeListener("packet", onPacket); if (timeout) { clearTimeout(timeout); timeout = undefined; } } function onPacket(type,data) { if (type!=0x8000) return; // ignore things that are not DATA packet if (data.length==0) { // 0 length packet = EOF cleanup(); setTimeout(resolve,0,fileContents); } else { fileContents += data; options.progress(fileContents.length); scheduleTimeout(); } } connection.parsePackets = true; connection.on("packet", onPacket); scheduleTimeout(); options.progress(0); connection.espruinoSendPacket("FILE_RECV",JSON.stringify(options)).then(()=>{ // now wait... }, err => { cleanup(); reject(err); }); }); } /* Send a JS expression to be evaluated on Espruino using using 2v25 packets. options = { timeout : int // milliseconds timeout (default=1000) stmFix : bool // if set, this works around an issue in Espruino STM32 2v24 and earlier where USB could get in a state where it only sent small chunks of data at a time }*/ espruinoEval(expr, options) { options = options || {}; if ("string"!=typeof expr) throw new Error("'expr' must be a String"); let connection = this; return new Promise((resolve,reject) => { let prodInterval; function cleanup() { connection.removeListener("packet", onPacket); if (timeout) { clearTimeout(timeout); timeout = undefined; } if (prodInterval) { clearInterval(prodInterval); prodInterval = undefined; } } function onPacket(type,data) { if (type!=0) return; // ignore things that are not a response cleanup(); setTimeout(resolve,0, parseRJSON(data)); } connection.parsePackets = true; connection.on("packet", onPacket); let timeout = setTimeout(() => { timeout = undefined; cleanup(); reject("espruinoEval Timeout"); }, options.timeout || 1000); connection.espruinoSendPacket("EVAL",expr,{noACK:options.stmFix}).then(()=>{ // resolved/rejected with 'packet' event or timeout if (options.stmFix) prodInterval = setInterval(function() { connection.write(" \x08") // space+backspace .catch(err=>{ console.error("Error sending STM fix:",err); cleanup(); }); }, 50); }, err => { cleanup(); reject(err); }); }); } } // End of Connection class function init() { Espruino.Core.Config.add("BAUD_RATE", { section : "Communications", name : "Baud Rate", description : "When connecting over serial, this is the baud rate that is used. 9600 is the default for Espruino", type : {9600:9600,14400:14400,19200:19200,28800:28800,38400:38400,57600:57600,115200:115200}, defaultValue : 9600, }); Espruino.Core.Config.add("SERIAL_IGNORE", { section : "Communications", name : "Ignore Serial Ports", description : "A '|' separated list of serial port paths to ignore, eg `/dev/ttyS*|/dev/*.SOC`", type : "string", defaultValue : "/dev/ttyS*|/dev/*.SOC|/dev/*.MALS" }); Espruino.Core.Config.add("SERIAL_FLOW_CONTROL", { section : "Communications", name : "Software Flow Control", description : "Respond to XON/XOFF flow control characters to throttle data uploads. By default Espruino sends XON/XOFF for USB and Bluetooth (on 2v05+).", type : "boolean", defaultValue : true }); Espruino.Core.Config.add("STORAGE_UPLOAD_METHOD", { section : "Communications", name : "Storage Upload Strategy", description : "On some connections (Serial, >9600 baud) XON/XOFF flow control is too slow to reliably throttle data transfer when writing files, "+ "and data can be lost. By default we add a delay after each write to Storage to help avoid this, but if your connection is stable "+ "you can turn this off and greatly increase write speeds.", type : { 0: "Delay Storage writes", 1: "No delays" }, defaultValue : 0 }); var connection = Espruino.Core.Serial.connection; connection.cb = (dataStr) => { if (readListener) readListener(Espruino.Core.Utils.stringToArrayBuffer(dataStr)); }; connection.writeLowLevel = (dataStr) => { return new Promise(resolve => currentDevice.write(dataStr, resolve)); }; var devices = Espruino.Core.Serial.devices; for (var i=0;i<devices.length;i++) { console.log(" - Initialising Serial "+devices[i].name); if (devices[i].init) devices[i].init(); } } var startListening=function(callback) { var oldListener = readListener; readListener = callback; return oldListener; }; /** * List ports available over all configured devices. * `shouldCallAgain` mean that more devices may appear later on (eg. Bluetooth LE) * @param {(ports, shouldCallAgain) => void} callback */ var getPorts = function (callback) { var newPortToDevice = {}; var devices = Espruino.Core.Serial.devices; if (!devices || devices.length == 0) { portToDevice = newPortToDevice; return callback(ports, false); } // Test to see if a given port path is ignore or not by configuration function isIgnored(path) { if (!Espruino.Config.SERIAL_IGNORE) return false; return Espruino.Config.SERIAL_IGNORE.split("|").some((wildcard) => { const regexp = new RegExp( `^${wildcard.replace(/\./g, "\\.").replace(/\*/g, ".*")}$` ); return path.match(regexp); }); } // Asynchronously call 'getPorts' on all devices and map results back as a series of promises Promise.all( devices.map((device) => new Promise((resolve) => device.getPorts(resolve)).then( (devicePorts, instantPorts) => ({ device: device, shouldCallAgain: !instantPorts, // If the ports are not present now (eg. BLE) then call again value: (devicePorts || []) .filter((port) => !isIgnored(port.path)) // Filter out all the ignored ports .map((port) => { // Map a description for this particular Product/Vendor if (port.usb && port.usb[0] == 0x0483 && port.usb[1] == 0x5740) port.description = "Espruino board"; return port; }), }) ) ) ).then((results) => { portToDevice = results.reduce((acc, promise) => { promise.value.forEach((port) => (acc[port.path] = promise.device)); return acc; }, {}); callback( results .flatMap((result) => result.value) .sort((a, b) => { if (a.unimportant && !b.unimportant) return 1; if (b.unimportant && !a.unimportant) return -1; return 0; }), results.some((result) => result.shouldCallAgain), ); }); }; var openSerial=function(serialPort, connectCallback, disconnectCallback) { Espruino.Core.Serial.setSlowWrite(true); // force slow write to ensure things work ok - we may disable this when versionChecker figures out what board/version we use return openSerialInternal(serialPort, connectCallback, disconnectCallback, 5); } var openSerialInternal=function(serialPort, connectCallback, disconnectCallback, attempts) { /* If openSerial is called, we need to have called getPorts first in order to figure out which one of the serial_ implementations we must call into. */ if (portToDevice === undefined) { portToDevice = {}; // stop recursive calls if something errors return getPorts(function() { openSerialInternal(serialPort, connectCallback, disconnectCallback, attempts); }); } if (!(serialPort in portToDevice)) { if (serialPort.toLowerCase() in portToDevice) { serialPort = serialPort.toLowerCase(); } else { if (attempts>0) { console.log("serial: Port "+JSON.stringify(serialPort)+" not found - checking ports again ("+attempts+" attempts left)"); setTimeout(function() { getPorts(function() { openSerialInternal(serialPort, connectCallback, disconnectCallback, attempts-1); }); }, 500); return; } else { console.error("serial: Port "+JSON.stringify(serialPort)+" not found"); return connectCallback(undefined); } } } Espruino.Core.Serial.connection.isOpen = false; Espruino.Core.Serial.connection.isOpening = true; var portInfo = { port:serialPort }; var connectionInfo = undefined; currentDevice = portToDevice[serialPort]; currentDevice.open(serialPort, function(cInfo) { // CONNECT if (!cInfo) { // Espruino.Core.Notifications.error("Unable to connect"); console.error("Unable to open device (connectionInfo="+cInfo+")"); connectCallback(undefined); connectCallback = undefined; currentDevice = undefined; } else { connectionInfo = cInfo; Espruino.Core.Serial.connection.isOpen = true; Espruino.Core.Serial.connection.isOpening = false; console.log("serial: Connected", cInfo); if (connectionInfo.portName) portInfo.portName = connectionInfo.portName; Espruino.callProcessor("connected", portInfo, function() { connectCallback(cInfo); connectCallback = undefined; }); } }, buf => Espruino.Core.Serial.connection.rxDataHandler(buf), // RECEIEVE DATA function(error) { // DISCONNECT currentDevice = undefined; Espruino.Core.Serial.connection.closeHandler(); sendingBinary = false; if (connectCallback) { // we got a disconnect when we hadn't connected... // Just call connectCallback(undefined), don't bother sending disconnect connectCallback(error); connectCallback = undefined; connectionInfo = undefined; return; } Espruino.callProcessor("disconnected", portInfo, function() { if (disconnectCallback) disconnectCallback(portInfo); disconnectCallback = undefined; }); }); }; var closeSerial=function() { if (currentDevice) { currentDevice.close(); currentDevice = undefined; } else console.error("Close called, but serial port not open"); }; var isConnected = function() { return currentDevice!==undefined; }; var findSplitIdx = function(data, prev, substr, delay, reason) { var match = data.match(substr); // not found if (match===null) return prev; // or previous find was earlier in str var end = match.index + match[0].length; if (end > prev.end) return prev; // found, and earlier prev.start = match.index; prev.end = end; prev.delay = delay; prev.match = match[0]; prev.reason = reason; return prev; } var writeSerialWorker = function(writeData) { var blockSize = 512; if (currentDevice.maxWriteLength) blockSize = currentDevice.maxWriteLength; /* if we're throttling our writes we want to send small * blocks of data at once. We still limit the size of * sent blocks to 512 because on Mac we seem to lose * data otherwise (not on any other platforms!) */ if (slowWrite) blockSize=19; writeData.showStatus &= writeData.data.length>blockSize; uart.showProgress = writeData.showStatus; if (writeData.showStatus) { Espruino.Core.Status.setStatus("Sending...", writeData.data.length); console.log("serial: ---> "+JSON.stringify(writeData.data)); } while (writeData.data.length>0) { let d = undefined; let split = writeData.nextSplit || { start:0, end:writeData.data.length, delay:0 }; // if we get something like Ctrl-C or `reset`, wait a bit for it to complete if (!sendingBinary) { split = findSplitIdx(writeData.data, split, /\x03/, 250, "Ctrl-C"); // Ctrl-C split = findSplitIdx(writeData.data, split, /reset\(\);?\n/, 250, "reset()"); // Reset split = findSplitIdx(writeData.data, split, /load\(\);?\n/, 250, "load()"); // Load split = findSplitIdx(writeData.data, split, /Modules.addCached\("[^\n]*"\);?\n/, 250, "Modules.addCached"); // Adding a module if ((0|Espruino.Config.STORAGE_UPLOAD_METHOD)==0) // only throttle writes if we haven't disabled it split = findSplitIdx(writeData.data, split, /require\("Storage"\).write\([^\n]*\);?\n/, 250, "Storage.write"); // Write chunk of data } if (split.match) console.log("serial: Splitting for "+split.reason+", delay "+split.delay); // Only send some of the data if (writeData.data.length>split.end) { if (slowWrite && split.delay==0) split.delay=50; d = writeData.data.substr(0,split.end); writeData.data = writeData.data.substr(split.end); if (writeData.nextSplit) { writeData.nextSplit.start -= split.end; writeData.nextSplit.end -= split.end; if (writeData.nextSplit.end<=0) writeData.nextSplit = undefined; } } else { d = writeData.data; writeData.data = ""; writeData.nextSplit = undefined; } let isLast = writeData.data.length == 0; // actually write data //console.log("serial: Sending block "+JSON.stringify(d)+", wait "+split.delay+"ms"); Espruino.Core.Serial.connection.chunkSize = blockSize; Espruino.Core.Serial.connection.write(d, function() { // write data, but the callback returns a promise that delays // update status /*if (writeData.showStatus) Espruino.Core.Status.incrementProgress(d.length);*/ return new Promise(resolve => setTimeout(function() { if (isLast && writeData.showStatus) { uart.showProgress = false; Espruino.Core.Status.setStatus("Sent"); if (writeData.callback) writeData.callback(); } resolve(); }, split.delay)); }); } } // Throttled serial write var writeSerial = function(data, showStatus, callback) { if (showStatus===undefined) showStatus=true; writeSerialWorker({data:data,callback:callback,showStatus:showStatus}); }; // ---------------------------------------------------------- Espruino.Core.Serial = { "devices" : [], // List of devices that can provide a serial API "init" : init, "getPorts": getPorts, "open": openSerial, "isConnected": isConnected, "startListening": startListening, "write": writeSerial, // function(data, showStatus, callback) "close": closeSerial, "isSlowWrite": function() { return slowWrite; }, "setSlowWrite": function(isOn, force) { var SERIAL_THROTTLE_SEND = 0|Espruino.Config.SERIAL_THROTTLE_SEND; var reason = ""; if (force) { reason = "(forced)"; } else if (SERIAL_THROTTLE_SEND==1) { reason = "('Throttle Send'='Always')"; isOn = true; } else if (SERIAL_THROTTLE_SEND==2) { reason = "('Throttle Send'='Never')"; isOn = false; } else reason = "('Throttle Send'='Auto')"; console.log(`serial: Set Slow Write = ${isOn} ${reason}`); slowWrite = isOn; }, "setBinary": function(isOn) { sendingBinary = isOn; }, "connection": new Connection(), "debug": function() { logLevel = 3; } // output extra debug info }; })();