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 @@
+