@robotical/ricjs
Version:
Javascript/TS library for Robotical RIC
312 lines • 13.3 kB
JavaScript
"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