diff --git a/manifest.json b/manifest.json index ecc1fbdf..baec36a1 100755 --- a/manifest.json +++ b/manifest.json @@ -34,7 +34,8 @@ {"usbDevices": [ {"vendorId": 1155, "productId": 57105} ]}, - "webview" + "webview", + "unlimitedStorage" ], "sockets": { "tcp": { diff --git a/package.json b/package.json index c7e3cd4f..e86c38fd 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "dependencies": { "i18next": "^10.3.0", "i18next-xhr-backend": "^1.5.1", + "lru_map": "^0.3.3", "marked": "^0.3.12" }, "devDependencies": { diff --git a/src/js/FirmwareCache.js b/src/js/FirmwareCache.js new file mode 100644 index 00000000..dda1b8f1 --- /dev/null +++ b/src/js/FirmwareCache.js @@ -0,0 +1,216 @@ +'use strict'; + +/** + * Caching of previously downloaded firmwares and release descriptions + * + * Depends on LRUMap for which the docs can be found here: + * https://github.com/rsms/js-lru + */ + +/** + * @typedef {object} Descriptor Release descriptor object + * @property {string} releaseUrl + * @property {string} name + * @property {string} version + * @property {string} url + * @property {string} file + * @property {string} target + * @property {string} date + * @property {string} notes + * @property {string} status + * @see buildBoardOptions() in {@link release_checker.js} + */ + +/** + * @typedef {object} CacheItem + * @property {Descriptor} release + * @property {string} hexdata + */ + +/** + * Manages caching of downloaded firmware files + */ +let FirmwareCache = (function () { + + let onPutToCacheCallback, + onRemoveFromCacheCallback; + + let JournalStorage = (function () { + let CACHEKEY = "firmware-cache-journal"; + + /** + * @param {Array} data LRU key-value pairs + */ + function persist(data) { + let obj = {}; + obj[CACHEKEY] = data; + chrome.storage.local.set(obj); + } + + /** + * @param {Function} callback + */ + function load(callback) { + chrome.storage.local.get(CACHEKEY, obj => { + let entries = typeof obj === "object" && obj.hasOwnProperty(CACHEKEY) + ? obj[CACHEKEY] + : []; + callback(entries); + }); + } + + return { + persist: persist, + load: load, + }; + })(); + + let journal = new LRUMap(100), + journalLoaded = false; + + journal.shift = function () { + // remove cached data for oldest release + let oldest = LRUMap.prototype.shift.call(this); + if (oldest === undefined) { + return undefined; + } + let key = oldest[0]; + let cacheKey = withCachePrefix(key); + chrome.storage.local.get(cacheKey, obj => { + /** @type {CacheItem} */ + let cached = typeof obj === "object" && obj.hasOwnProperty(cacheKey) + ? obj[cacheKey] + : null; + chrome.storage.local.remove(cacheKey, () => { + onRemoveFromCache(cached.release); + console.debug("Cache data removed: " + cacheKey); + }); + }); + return oldest; + }; + + /** + * @param {Descriptor} release + * @returns {string} A key used to store a release in the journal + */ + function keyOf(release) { + return release.file; + } + + /** + * @param {string} key + * @returns {string} A key for storing cached data for a release + */ + function withCachePrefix(key) { + return "cache:" + key; + } + + /** + * @param {Descriptor} release + * @returns {boolean} + */ + function has(release) { + if (!journalLoaded) { + console.warn("Cache not yet loaded"); + return false; + } + return journal.has(keyOf(release)); + } + + /** + * @param {Descriptor} release + * @param {string} hexdata + */ + function put(release, hexdata) { + if (!journalLoaded) { + console.warn("Cache journal not yet loaded"); + return; + } + let key = keyOf(release); + if (has(release)) { + console.debug("Firmware is already cached: " + key); + return; + } + journal.set(key, true); + JournalStorage.persist(journal.toJSON()); + let obj = {}; + obj[withCachePrefix(key)] = { + release: release, + hexdata: hexdata, + }; + chrome.storage.local.set(obj, () => { + console.info("Release put to cache: " + key); + onPutToCache(release); + }); + } + + /** + * @param {Descriptor} release + * @param {Function} callback + */ + function get(release, callback) { + if (!journalLoaded) { + console.warn("Cache journal not yet loaded"); + return undefined; + } + let key = keyOf(release); + if (!has(release)) { + console.debug("Firmware is not cached: " + key); + return; + } + let cacheKey = withCachePrefix(key); + chrome.storage.local.get(cacheKey, obj => { + /** @type {CacheItem} */ + let cached = typeof obj === "object" && obj.hasOwnProperty(cacheKey) + ? obj[cacheKey] + : null; + callback(cached); + }); + } + + /** + * @param {Descriptor} release + */ + function onPutToCache(release) { + if (typeof onPutToCacheCallback === "function") { + onPutToCacheCallback(release); + } + } + + /** + * @param {Descriptor} release + */ + function onRemoveFromCache(release) { + if (typeof onRemoveFromCacheCallback === "function") { + onRemoveFromCacheCallback(release); + } + } + + /** + * @param {Array} entries + */ + function onEntriesLoaded(entries) { + let pairs = []; + for (let entry of entries) { + pairs.push([entry.key, entry.value]); + } + journal.assign(pairs); + journalLoaded = true; + console.info("Firmware cache journal loaded; number of entries: " + entries.length); + } + + return { + has: has, + put: put, + get: get, + onPutToCache: callback => onPutToCacheCallback = callback, + onRemoveFromCache: callback => onRemoveFromCacheCallback = callback, + load: () => { + JournalStorage.load(onEntriesLoaded); + }, + flush: () => { + JournalStorage.persist(journal.toJSON()); + journal.clear(); + }, + }; +})(); diff --git a/src/js/tabs/firmware_flasher.js b/src/js/tabs/firmware_flasher.js index c178ca86..92812c15 100755 --- a/src/js/tabs/firmware_flasher.js +++ b/src/js/tabs/firmware_flasher.js @@ -16,7 +16,23 @@ TABS.firmware_flasher.initialize = function (callback) { var intel_hex = false, // standard intel hex in string format parsed_hex = false; // parsed raw hex in array format + /** + * Change boldness of firmware option depending on cache status + * + * @param {Descriptor} release + */ + function onFirmwareCacheUpdate(release) { + $("option[value='{0}']".format(release.version)) + .css("font-weight", FirmwareCache.has(release) + ? "bold" + : "normal"); + } + $('#content').load("./tabs/firmware_flasher.html", function () { + FirmwareCache.load(); + FirmwareCache.onPutToCache(onFirmwareCacheUpdate); + FirmwareCache.onRemoveFromCache(onFirmwareCacheUpdate); + function parse_hex(str, callback) { // parsing hex in different thread var worker = new Worker('./js/workers/hex_parser.js'); @@ -30,6 +46,53 @@ TABS.firmware_flasher.initialize = function (callback) { worker.postMessage(str); } + function process_hex(data, summary) { + intel_hex = data; + + parse_hex(intel_hex, function (data) { + parsed_hex = data; + + if (parsed_hex) { + if (!FirmwareCache.has(summary)) { + FirmwareCache.put(summary, intel_hex); + } + + var url; + + $('span.progressLabel').html('Loaded Online Firmware: (' + parsed_hex.bytes_total + ' bytes)'); + + $('a.flash_firmware').removeClass('disabled'); + + $('div.release_info .target').text(summary.target); + $('div.release_info .name').text(summary.version).prop('href', summary.releaseUrl); + $('div.release_info .date').text(summary.date); + $('div.release_info .status').text(summary.status); + $('div.release_info .file').text(summary.file).prop('href', summary.url); + + var formattedNotes = summary.notes.replace(/#(\d+)/g, '[#$1](https://github.com/betaflight/betaflight/pull/$1)'); + formattedNotes = marked(formattedNotes); + $('div.release_info .notes').html(formattedNotes); + $('div.release_info .notes').find('a').each(function() { + $(this).attr('target', '_blank'); + }); + + $('div.release_info').slideDown(); + + } else { + $('span.progressLabel').text(i18n.getMessage('firmwareFlasherHexCorrupted')); + } + }); + } + + function onLoadSuccess(data, summary) { + summary = typeof summary === "object" + ? summary + : $('select[name="firmware_version"] option:selected').data('summary'); + process_hex(data, summary); + $("a.load_remote_file").removeClass('disabled'); + $("a.load_remote_file").text(i18n.getMessage('firmwareFlasherButtonLoadOnline')); + }; + function buildBoardOptions(releaseData) { if (!releaseData) { $('select[name="board"]').empty().append(''); @@ -160,7 +223,12 @@ TABS.firmware_flasher.initialize = function (callback) { descriptor.target, descriptor.date, descriptor.status - )).data('summary', descriptor); + )) + .css("font-weight", FirmwareCache.has(descriptor) + ? "bold" + : "normal" + ) + .data('summary', descriptor); versions_e.append(select_e); }); @@ -226,7 +294,15 @@ TABS.firmware_flasher.initialize = function (callback) { $('select[name="firmware_version"]').change(function(evt){ $('div.release_info').slideUp(); $('a.flash_firmware').addClass('disabled'); - if (evt.target.value=="0") { + let release = $("option:selected", evt.target).data("summary"); + let isCached = FirmwareCache.has(release); + if (evt.target.value=="0" || isCached) { + if (isCached) { + FirmwareCache.get(release, cached => { + console.info("Release found in cache: " + release.file); + onLoadSuccess(cached.hexdata, release); + }); + } $("a.load_remote_file").addClass('disabled'); } else { @@ -241,40 +317,6 @@ TABS.firmware_flasher.initialize = function (callback) { return; } - function process_hex(data, summary) { - intel_hex = data; - - parse_hex(intel_hex, function (data) { - parsed_hex = data; - - if (parsed_hex) { - var url; - - $('span.progressLabel').html('Loaded Online Firmware: (' + parsed_hex.bytes_total + ' bytes)'); - - $('a.flash_firmware').removeClass('disabled'); - - $('div.release_info .target').text(summary.target); - $('div.release_info .name').text(summary.version).prop('href', summary.releaseUrl); - $('div.release_info .date').text(summary.date); - $('div.release_info .status').text(summary.status); - $('div.release_info .file').text(summary.file).prop('href', summary.url); - - var formattedNotes = summary.notes.replace(/#(\d+)/g, '[#$1](https://github.com/betaflight/betaflight/pull/$1)'); - formattedNotes = marked(formattedNotes); - $('div.release_info .notes').html(formattedNotes); - $('div.release_info .notes').find('a').each(function() { - $(this).attr('target', '_blank'); - }); - - $('div.release_info').slideDown(); - - } else { - $('span.progressLabel').text(i18n.getMessage('firmwareFlasherHexCorrupted')); - } - }); - } - function failed_to_load() { $('span.progressLabel').text(i18n.getMessage('firmwareFlasherFailedToLoadOnlineFirmware')); $('a.flash_firmware').addClass('disabled'); @@ -286,11 +328,7 @@ TABS.firmware_flasher.initialize = function (callback) { if (summary) { // undefined while list is loading or while running offline $("a.load_remote_file").text(i18n.getMessage('firmwareFlasherButtonDownloading')); $("a.load_remote_file").addClass('disabled'); - $.get(summary.url, function (data) { - process_hex(data, summary); - $("a.load_remote_file").removeClass('disabled'); - $("a.load_remote_file").text(i18n.getMessage('firmwareFlasherButtonLoadOnline')); - }).fail(failed_to_load); + $.get(summary.url, onLoadSuccess).fail(failed_to_load); } else { $('span.progressLabel').text(i18n.getMessage('firmwareFlasherFailedToLoadOnlineFirmware')); } @@ -507,6 +545,7 @@ TABS.firmware_flasher.initialize = function (callback) { TABS.firmware_flasher.cleanup = function (callback) { PortHandler.flush_callbacks(); + FirmwareCache.flush(); // unbind "global" events $(document).unbind('keypress'); diff --git a/src/main.html b/src/main.html index 5fdc6bf0..2507775f 100755 --- a/src/main.html +++ b/src/main.html @@ -35,6 +35,7 @@ + @@ -96,6 +97,7 @@ +