UNPKG

@robotical/ricjs

Version:

Javascript/TS library for Robotical RIC

312 lines 13.3 kB
"use strict"; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // RICJS // Communications Library // // Rob Dobson & Chris Greening 2020-2022 // (C) 2020-2022 // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const RICLog_1 = tslib_1.__importDefault(require("./RICLog")); const RICMsgHandler_1 = require("./RICMsgHandler"); const RICTypes_1 = require("./RICTypes"); class FileBlockTrackInfo { constructor(prom) { this.isDone = false; this.prom = prom; this.prom.then(() => { // RICLog.debug('send complete'); this.isDone = true; }, rej => { RICLog_1.default.debug(`FileBlockTrackInfo send rejected ${rej}`); this.isDone = true; }); } isComplete() { return this.isDone; } get() { return this.prom; } } class RICFileHandler { constructor(msgHandler, commsStats) { // Timeouts this.BLOCK_ACK_TIMEOUT_MS = 30000; // Contents of file to send this._requestedFileBlockSize = 500; this._fileBlockSize = 0; this._requestedBatchAckSize = 10; this._batchAckSize = 0; // File sending flow control this._sendWithoutBatchAcks = false; this._ackedFilePos = 0; this._batchAckReceived = false; this._isCancelled = false; // Message await list this._msgAwaitList = new Array(); this.MAX_OUTSTANDING_FILE_BLOCK_SEND_PROMISES = 1; this._msgHandler = msgHandler; this._commsStats = commsStats; this._fileBlockSize = this._requestedFileBlockSize; this.onOktoMsg = this.onOktoMsg.bind(this); } setRequestedFileBlockSize(blockSize) { this._requestedFileBlockSize = blockSize; } setRequestedBatchAckSize(batchAckSize) { this._requestedBatchAckSize = batchAckSize; } async fileSend(fileName, fileType, fileContents, progressCallback) { this._isCancelled = false; // Send file start message // RICLog.verbose('XXXXXXXXX _sendFileStartMsg start'); if (!await this._sendFileStartMsg(fileName, fileType, fileContents)) return false; // RICLog.verbose('XXXXXXXXX _sendFileStartMsg done'); // Send contents // RICLog.verbose('XXXXXXXXX _sendFileContents start'); if (!await this._sendFileContents(fileContents, progressCallback)) return false; // RICLog.verbose('XXXXXXXXX _sendFileContents done'); // Send file end // RICLog.verbose('XXXXXXXXX _sendFileEndMsg start'); await this._sendFileEndMsg(fileName, fileType, fileContents); // RICLog.verbose('XXXXXXXXX _sendFileEndMsg done'); // Clean up await this.awaitOutstandingMsgPromises(true); // Complete return true; } async fileSendCancel() { // Await outstanding promises await this.awaitOutstandingMsgPromises(true); this._isCancelled = true; } // Send the start message async _sendFileStartMsg(fileName, fileType, fileContents) { // File start command message const reqStr = fileType == RICTypes_1.RICFileSendType.RIC_FIRMWARE_UPDATE ? 'espfwupdate' : 'fileupload'; const fileDest = fileType == RICTypes_1.RICFileSendType.RIC_FIRMWARE_UPDATE ? 'ricfw' : 'fs'; const fileLen = fileContents.length; const cmdMsg = `{"cmdName":"ufStart","reqStr":"${reqStr}","fileType":"${fileDest}","fileName":"${fileName}","fileLen":${fileLen},"batchMsgSize":${this._requestedFileBlockSize},"batchAckSize":${this._requestedBatchAckSize}}`; // Debug RICLog_1.default.debug(`sendFileStartMsg ${cmdMsg}`); // Send let fileStartResp = null; try { fileStartResp = await this._msgHandler.sendRICREST(cmdMsg, RICMsgHandler_1.RICRESTElemCode.RICREST_ELEM_CODE_COMMAND_FRAME); } catch (err) { RICLog_1.default.error(`sendFileStartMsg error ${err}`); return false; } if (fileStartResp.rslt !== 'ok') { RICLog_1.default.error(`sendFileStartMsg error ${fileStartResp.rslt}`); return false; } // Extract params if (fileStartResp.batchMsgSize) { this._fileBlockSize = fileStartResp.batchMsgSize; } else { this._fileBlockSize = this._requestedFileBlockSize; } if (fileStartResp.batchAckSize) { this._batchAckSize = fileStartResp.batchAckSize; } else { this._batchAckSize = this._requestedBatchAckSize; } RICLog_1.default.debug(`_fileSendStartMsg fileBlockSize req ${this._requestedFileBlockSize} resp ${fileStartResp.batchMsgSize} actual ${this._fileBlockSize}`); RICLog_1.default.debug(`_fileSendStartMsg batchAckSize req ${this._requestedBatchAckSize} resp ${fileStartResp.batchAckSize} actual ${this._batchAckSize}`); return true; } async _sendFileEndMsg(fileName, fileType, fileContents) { // File end command message const reqStr = fileType == RICTypes_1.RICFileSendType.RIC_FIRMWARE_UPDATE ? 'espfwupdate' : 'fileupload'; const fileDest = fileType == RICTypes_1.RICFileSendType.RIC_FIRMWARE_UPDATE ? 'ricfw' : 'fs'; const fileLen = fileContents.length; const cmdMsg = `{"cmdName":"ufEnd","reqStr":"${reqStr}","fileType":"${fileDest}","fileName":"${fileName}","fileLen":${fileLen}}`; // Await outstanding promises try { await this.awaitOutstandingMsgPromises(true); } catch (err) { // Ignore } // Send let fileEndResp = null; try { fileEndResp = await this._msgHandler.sendRICREST(cmdMsg, RICMsgHandler_1.RICRESTElemCode.RICREST_ELEM_CODE_COMMAND_FRAME); } catch (err) { RICLog_1.default.error(`sendFileEndMsg error ${err}`); return false; } return fileEndResp.rslt === 'ok'; } async _sendFileCancelMsg() { // File cancel command message const cmdMsg = `{"cmdName":"ufCancel"}`; // Await outstanding promises await this.awaitOutstandingMsgPromises(true); // Send try { return await this._msgHandler.sendRICREST(cmdMsg, RICMsgHandler_1.RICRESTElemCode.RICREST_ELEM_CODE_COMMAND_FRAME); } catch (err) { RICLog_1.default.error(`sendFileCancelMsg error ${err}`); } } async _sendFileContents(fileContents, progressCallback) { if (progressCallback) { progressCallback(0, fileContents.length, 0); } this._batchAckReceived = false; this._ackedFilePos = 0; // Send file blocks let progressUpdateCtr = 0; while (this._ackedFilePos < fileContents.length) { // Sending with or without batches if (this._sendWithoutBatchAcks) { // Debug RICLog_1.default.verbose(`_sendFileContents NO BATCH ACKS ${progressUpdateCtr} blocks total sent ${this._ackedFilePos} block len ${this._fileBlockSize}`); if (!await this._sendFileBlock(fileContents, this._ackedFilePos)) return false; this._ackedFilePos += this._fileBlockSize; progressUpdateCtr++; } else { // NOTE: first batch MUST be of size 1 (not _batchAckSize) because RIC performs a long-running // blocking task immediately after receiving the first message in a firmware // update - although this could be relaxed for non-firmware update file uploads let sendFromPos = this._ackedFilePos; const batchSize = sendFromPos == 0 ? 1 : this._batchAckSize; for (let i = 0; i < batchSize && sendFromPos < fileContents.length; i++) { // Clear old batch acks if (i == batchSize - 1) { this._batchAckReceived = false; } // Debug RICLog_1.default.debug(`_sendFileContents sendblock pos ${sendFromPos} len ${this._fileBlockSize} ackedTo ${this._ackedFilePos} fileLen ${fileContents.length}`); if (!await this._sendFileBlock(fileContents, sendFromPos)) return false; sendFromPos += this._fileBlockSize; } // Wait for response (there is a timeout at the ESP end to ensure a response is always returned // even if blocks are dropped on reception at ESP) - the timeout here is for these responses // being dropped await this.batchAck(this.BLOCK_ACK_TIMEOUT_MS); progressUpdateCtr += this._batchAckSize; } // Show progress if ((progressUpdateCtr >= 20) && progressCallback) { // Update UI progressCallback(this._ackedFilePos, fileContents.length, this._ackedFilePos / fileContents.length); // Debug RICLog_1.default.debug(`_sendFileContents ${progressUpdateCtr} blocks sent OkTo ${this._ackedFilePos} block len ${this._fileBlockSize}`); // Continue progressUpdateCtr = 0; } } return true; } async batchAck(timeout) { // Handle acknowledgement to a batch (OkTo message) return new Promise((resolve, reject) => { const startTime = Date.now(); const checkForAck = async () => { if (this._isCancelled) { RICLog_1.default.debug('checkForAck - cancelling file upload'); this._isCancelled = false; // Send cancel await this._sendFileCancelMsg(); // abort the upload process reject(new Error('Update Cancelled')); return; } if (this._batchAckReceived) { RICLog_1.default.debug(`checkForAck - rx OkTo ${this._ackedFilePos}`); this._batchAckReceived = false; resolve(); return; } else { const now = Date.now(); if (now - startTime > timeout) { RICLog_1.default.warn(`checkForAck - time-out no new ack received`); reject(new Error('Update failed. Please try again.')); return; } setTimeout(checkForAck, 100); } }; checkForAck(); }); } async _sendFileBlock(fileContents, blockStart) { // Calc block start and end const blockEnd = Math.min(fileContents.length, blockStart + this._fileBlockSize); // Check if we need to await a message send promise await this.awaitOutstandingMsgPromises(false); // Send const promRslt = this._msgHandler.sendFileBlock(fileContents.subarray(blockStart, blockEnd), blockStart); if (!promRslt) { return false; } // Record this._commsStats.recordFileBytes(blockEnd - blockStart); // Add to list of pending messages this._msgAwaitList.push(new FileBlockTrackInfo(promRslt)); // Debug // RICLog.debug( // `sendFileBlock start ${blockStart} end ${blockEnd} len ${blockLen}`, // ); return true; } onOktoMsg(fileOkTo) { // Get how far we've progressed in file this._ackedFilePos = fileOkTo; this._batchAckReceived = true; RICLog_1.default.verbose(`onOktoMsg received file up to ${this._ackedFilePos}`); } async awaitOutstandingMsgPromises(all) { // Check if all outstanding promises to be awaited if (all) { for (const promRslt of this._msgAwaitList) { try { await promRslt.get(); } catch (error) { RICLog_1.default.warn(`awaitAll file part send failed ${error}`); } } this._msgAwaitList = []; } else { // RICLog.debug('Await list len', this._msgAwaitList.length); if (this._msgAwaitList.length >= this.MAX_OUTSTANDING_FILE_BLOCK_SEND_PROMISES) { const fileBlockTrackInfo = this._msgAwaitList.shift(); try { if (fileBlockTrackInfo) { await fileBlockTrackInfo.get(); } } catch (error) { RICLog_1.default.warn(`awaitSome file part send failed ${error}`); } } } } } exports.default = RICFileHandler; //# sourceMappingURL=RICFileHandler.js.map