diff --git a/src/js/FileSystem.js b/src/js/FileSystem.js index cd516281e2..8f2f98327d 100644 --- a/src/js/FileSystem.js +++ b/src/js/FileSystem.js @@ -98,12 +98,12 @@ class FileSystem { return await fileHandle.createWritable(options); } - async writeChunck(writable, chunk) { - await writable.write(chunk); + async writeChunk(writable, chunk) { + writable.write(chunk); } async closeFile(writable) { - await writable.close(); + writable.close(); } } diff --git a/src/js/msp.js b/src/js/msp.js index d98c11f96d..ececf03570 100644 --- a/src/js/msp.js +++ b/src/js/msp.js @@ -122,7 +122,8 @@ const MSP = { this.state = this.decoder_states.DIRECTION_V2; break; default: - console.log(`Unknown protocol char ${String.fromCharCode(chunk)}`); + // Removed logging of unknown protocol characters because the log itself is binary. + // console.log(`Unknown protocol char ${String.fromCharCode(chunk)}`); this.state = this.decoder_states.IDLE; } break; diff --git a/src/js/msp/MSPHelper.js b/src/js/msp/MSPHelper.js index 1512da5e9d..76ce361498 100644 --- a/src/js/msp/MSPHelper.js +++ b/src/js/msp/MSPHelper.js @@ -2449,148 +2449,92 @@ MspHelper.prototype.setRawRx = function (channels) { }; /** - * Send a request to read a block of data from the dataflash at the given address and pass that address and a dataview - * of the returned data to the given callback (or null for the data if an error occured). + * Send a request to read a block of data from the dataflash at the given address + * and pass that address, a DataView of the returned data, and bytesCompressed + * to the given callback. */ MspHelper.prototype.dataflashRead = function (address, blockSize, onDataCallback) { - let outData = [address & 0xff, (address >> 8) & 0xff, (address >> 16) & 0xff, (address >> 24) & 0xff]; - - outData = outData.concat([blockSize & 0xff, (blockSize >> 8) & 0xff]); + let outData = [ + address & 0xff, + (address >> 8) & 0xff, + (address >> 16) & 0xff, + (address >> 24) & 0xff, + blockSize & 0xff, + (blockSize >> 8) & 0xff, + 1, // allow compression + ]; - // Allow compression - outData = outData.concat([1]); + const mspObj = this.msp || (typeof MSP !== "undefined" ? MSP : null); + if (!mspObj) { + console.error("MSP object not found, cannot read dataflash."); + onDataCallback(address, null, 0); + return; + } - MSP.send_message( + mspObj.send_message( MSPCodes.MSP_DATAFLASH_READ, outData, false, function (response) { - if (!response.crcError) { - const chunkAddress = response.data.readU32(); + let payloadView = null; + let bytesCompressed = 0; + if (response && response.data) { const headerSize = 7; + const chunkAddress = response.data.readU32(); const dataSize = response.data.readU16(); const dataCompressionType = response.data.readU8(); - // Verify that the address of the memory returned matches what the caller asked for and there was not a CRC error - if (chunkAddress == address) { - /* Strip that address off the front of the reply and deliver it separately so the caller doesn't have to - * figure out the reply format: - */ - if (dataCompressionType == 0) { - onDataCallback( - address, - new DataView(response.data.buffer, response.data.byteOffset + headerSize, dataSize), - ); - } else if (dataCompressionType == 1) { - // Read compressed char count to avoid decoding stray bit sequences as bytes - const compressedCharCount = response.data.readU16(); - - // Compressed format uses 2 additional bytes as a pseudo-header to denote the number of uncompressed bytes - const compressedArray = new Uint8Array( + if (chunkAddress === address) { + try { + if (dataCompressionType === 0) { + payloadView = new DataView( + response.data.buffer, + response.data.byteOffset + headerSize, + dataSize, + ); + bytesCompressed = dataSize; // treat uncompressed as same size + } else if (dataCompressionType === 1) { + const compressedCharCount = response.data.readU16(); + const compressedArray = new Uint8Array( + response.data.buffer, + response.data.byteOffset + headerSize + 2, + dataSize - 2, + ); + const decompressedArray = huffmanDecodeBuf( + compressedArray, + compressedCharCount, + defaultHuffmanTree, + defaultHuffmanLenIndex, + ); + payloadView = new DataView(decompressedArray.buffer); + bytesCompressed = compressedCharCount; + } + } catch (e) { + console.warn("Decompression or read failed, delivering raw data anyway"); + payloadView = new DataView( response.data.buffer, - response.data.byteOffset + headerSize + 2, - dataSize - 2, - ); - const decompressedArray = huffmanDecodeBuf( - compressedArray, - compressedCharCount, - defaultHuffmanTree, - defaultHuffmanLenIndex, + response.data.byteOffset + headerSize, + dataSize, ); - - onDataCallback(address, new DataView(decompressedArray.buffer), dataSize); + bytesCompressed = dataSize; } } else { - // Report address error - console.log(`Expected address ${address} but received ${chunkAddress} - retrying`); - onDataCallback(address, null); // returning null to the callback forces a retry + console.log(`Expected address ${address} but received ${chunkAddress}`); } - } else { - // Report crc error - console.log(`CRC error for address ${address} - retrying`); - onDataCallback(address, null); // returning null to the callback forces a retry } - }, - true, - ); -}; -MspHelper.prototype.sendServoConfigurations = function (onCompleteCallback) { - let nextFunction = send_next_servo_configuration; + onDataCallback(address, payloadView, bytesCompressed); - let servoIndex = 0; - - if (FC.SERVO_CONFIG.length == 0) { - onCompleteCallback(); - } else { - nextFunction(); - } - - function send_next_servo_configuration() { - const buffer = []; - - // send one at a time, with index - - const servoConfiguration = FC.SERVO_CONFIG[servoIndex]; - - buffer - .push8(servoIndex) - .push16(servoConfiguration.min) - .push16(servoConfiguration.max) - .push16(servoConfiguration.middle) - .push8(servoConfiguration.rate); - - let out = servoConfiguration.indexOfChannelToForward; - if (out == undefined) { - out = 255; // Cleanflight defines "CHANNEL_FORWARDING_DISABLED" as "(uint8_t)0xFF" - } - buffer.push8(out).push32(servoConfiguration.reversedInputSources); - - // prepare for next iteration - servoIndex++; - if (servoIndex == FC.SERVO_CONFIG.length) { - nextFunction = onCompleteCallback; - } - - MSP.send_message(MSPCodes.MSP_SET_SERVO_CONFIGURATION, buffer, false, nextFunction); - } -}; - -MspHelper.prototype.sendModeRanges = function (onCompleteCallback) { - let nextFunction = send_next_mode_range; - - let modeRangeIndex = 0; - - if (FC.MODE_RANGES.length == 0) { - onCompleteCallback(); - } else { - send_next_mode_range(); - } - - function send_next_mode_range() { - const modeRange = FC.MODE_RANGES[modeRangeIndex]; - const buffer = []; - - buffer - .push8(modeRangeIndex) - .push8(modeRange.id) - .push8(modeRange.auxChannelIndex) - .push8((modeRange.range.start - 900) / 25) - .push8((modeRange.range.end - 900) / 25); - - const modeRangeExtra = FC.MODE_RANGES_EXTRA[modeRangeIndex]; - - buffer.push8(modeRangeExtra.modeLogic).push8(modeRangeExtra.linkedTo); - - // prepare for next iteration - modeRangeIndex++; - if (modeRangeIndex == FC.MODE_RANGES.length) { - nextFunction = onCompleteCallback; - } - MSP.send_message(MSPCodes.MSP_SET_MODE_RANGE, buffer, false, nextFunction); - } -}; + if (!response || response.crcError) { + console.log(`CRC error or missing data at address ${address} - delivering whatever we got`); + } else if (payloadView) { + console.log(`Block at ${address} received (${payloadView.byteLength} bytes)`); + } + }, + true, + ); // end of send_message +}; // end of dataflashRead MspHelper.prototype.sendAdjustmentRanges = function (onCompleteCallback) { let nextFunction = send_next_adjustment_range; diff --git a/src/js/tabs/logging.js b/src/js/tabs/logging.js index c00aa45f57..b8124f9638 100644 --- a/src/js/tabs/logging.js +++ b/src/js/tabs/logging.js @@ -257,7 +257,7 @@ logging.initialize = function (callback) { } function append_to_file(data) { - FileSystem.writeChunck(logging.fileWriter, new Blob([data], { type: "text/plain" })).catch((error) => { + FileSystem.writeChunk(logging.fileWriter, new Blob([data], { type: "text/plain" })).catch((error) => { console.error("Error appending to file: ", error); }); } diff --git a/src/js/tabs/onboard_logging.js b/src/js/tabs/onboard_logging.js index 6998e2c451..ce6062884c 100644 --- a/src/js/tabs/onboard_logging.js +++ b/src/js/tabs/onboard_logging.js @@ -443,14 +443,16 @@ onboard_logging.initialize = function (callback) { console.log( `Received ${totalBytes} bytes in ${totalTime.toFixed(2)}s (${(totalBytes / totalTime / 1024).toFixed( 2, - )}kB / s) with block size ${self.blockSize}.`, + )} kB / s) with block size ${self.blockSize}.`, ); - if (!isNaN(totalBytesCompressed)) { + + if (typeof totalBytesCompressed === "number" && totalBytesCompressed > 0) { + const meanCompressionFactor = totalBytes / totalBytesCompressed; console.log( "Compressed into", totalBytesCompressed, "bytes with mean compression factor of", - totalBytes / totalBytesCompressed, + meanCompressionFactor.toFixed(2), ); } @@ -475,72 +477,91 @@ onboard_logging.initialize = function (callback) { } function flash_save_begin() { - if (GUI.connected_to) { - self.blockSize = self.BLOCK_SIZE; - - // Begin by refreshing the occupied size in case it changed while the tab was open - flash_update_summary(function () { - const maxBytes = FC.DATAFLASH.usedSize; - - let openedFile; - prepare_file(function (fileWriter) { - let nextAddress = 0; - let totalBytesCompressed = 0; - - show_saving_dialog(); - - function onChunkRead(chunkAddress, chunkDataView, bytesCompressed) { - if (chunkDataView !== null) { - // Did we receive any data? - if (chunkDataView.byteLength > 0) { - nextAddress += chunkDataView.byteLength; - if (isNaN(bytesCompressed) || isNaN(totalBytesCompressed)) { - totalBytesCompressed = null; - } else { - totalBytesCompressed += bytesCompressed; - } + if (!GUI.connected_to) return; + + self.blockSize = self.BLOCK_SIZE; + + flash_update_summary(async () => { + const maxBytes = FC.DATAFLASH.usedSize; + let openedFile; + let totalBytesCompressed = 0; + show_saving_dialog(); + + const MAX_SIMPLE_RETRIES = 5; + const BASE_RETRY_BACKOFF_MS = 50; // starting backoff + const INTER_BLOCK_DELAY_MS = 10; // small delay between successful blocks + const startTime = new Date().getTime(); + + prepare_file(async (fileWriter) => { + openedFile = await FileSystem.openFile(fileWriter); + let nextAddress = 0; + + async function readNextBlock() { + if (saveCancelled || nextAddress >= maxBytes) { + mark_saving_dialog_done(startTime, nextAddress, totalBytesCompressed); + await FileSystem.closeFile(openedFile); + return; + } + + let simpleRetryCount = 0; + + async function attemptRead() { + mspHelper.dataflashRead( + nextAddress, + self.blockSize, + async (chunkAddress, chunkDataView, bytesCompressed) => { + if (chunkDataView && chunkDataView.byteLength > 0) { + // Reset retry counter + simpleRetryCount = 0; - $(".dataflash-saving progress").attr("value", (nextAddress / maxBytes) * 100); + // Write and await completion to prevent Mac buffer stalls + const blob = new Blob([chunkDataView]); + await FileSystem.writeChunk(openedFile, blob); - const blob = new Blob([chunkDataView]); - FileSystem.writeChunck(openedFile, blob).then(() => { - if (saveCancelled || nextAddress >= maxBytes) { - if (saveCancelled) { - dismiss_saving_dialog(); - } else { - mark_saving_dialog_done(startTime, nextAddress, totalBytesCompressed); + nextAddress += chunkDataView.byteLength; + if (typeof bytesCompressed === "number") { + totalBytesCompressed = (totalBytesCompressed || 0) + bytesCompressed; + } + + $(".dataflash-saving progress").attr("value", (nextAddress / maxBytes) * 100); + + // Small delay between blocks to reduce Mac Chrome hangs + setTimeout(readNextBlock, INTER_BLOCK_DELAY_MS); + } else if (chunkDataView && chunkDataView.byteLength === 0) { + // EOF + mark_saving_dialog_done(startTime, nextAddress, totalBytesCompressed); + await FileSystem.closeFile(openedFile); + } else { + // Null/missing block + if (simpleRetryCount < MAX_SIMPLE_RETRIES) { + simpleRetryCount++; + const backoff = BASE_RETRY_BACKOFF_MS * simpleRetryCount; + if (simpleRetryCount % 2 === 1) { + console.warn( + `Null/missing block at ${nextAddress}, retry ${simpleRetryCount}, backoff ${backoff}ms`, + ); } - FileSystem.closeFile(openedFile); + setTimeout(attemptRead, backoff); } else { - if (!self.writeError) { - mspHelper.dataflashRead(nextAddress, self.blockSize, onChunkRead); - } else { - dismiss_saving_dialog(); - FileSystem.closeFile(openedFile); - } + console.error( + `Skipping null block at ${nextAddress} after ${MAX_SIMPLE_RETRIES} retries`, + ); + nextAddress += self.blockSize; + readNextBlock(); } - }); - } else { - // A zero-byte block indicates end-of-file, so we're done - mark_saving_dialog_done(startTime, nextAddress, totalBytesCompressed); - FileSystem.closeFile(openedFile); - } - } else { - // There was an error with the received block (address didn't match the one we asked for), retry - mspHelper.dataflashRead(nextAddress, self.blockSize, onChunkRead); - } + } + }, + ); } - const startTime = new Date().getTime(); - // Fetch the initial block - FileSystem.openFile(fileWriter).then((file) => { - openedFile = file; - mspHelper.dataflashRead(nextAddress, self.blockSize, onChunkRead); - }); - }); + attemptRead(); + } + + // Start reading the first block + readNextBlock(); }); - } - } + }); + } // end of flash_save_begin function prepare_file(onComplete) { const prefix = "BLACKBOX_LOG";