Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/betaflight/betaflight-configurator.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorblckmn <blackman@xtra.com.au>2022-10-30 10:50:53 +0300
committerblckmn <blackman@xtra.com.au>2022-10-30 10:50:53 +0300
commit6aae7957813e6469bb87849245eca7582e1c639c (patch)
tree761beb9f4217064f57b87568b0f9942c887c78d3
parenta49a6b98ba926bd08298f640ae5b1617a665a36c (diff)
Setup for the cloud build implementation
-rw-r--r--locales/en/messages.json35
-rw-r--r--src/css/tabs/firmware_flasher.less8
-rw-r--r--src/js/ConfigInserter.js107
-rw-r--r--src/js/FirmwareCache.js242
-rw-r--r--src/js/jenkins_loader.js160
-rw-r--r--src/js/main.js6
-rw-r--r--src/js/release_loader.js129
-rw-r--r--src/js/tabs/firmware_flasher.js792
-rw-r--r--src/main.html4
-rw-r--r--src/tabs/firmware_flasher.html219
10 files changed, 565 insertions, 1137 deletions
diff --git a/locales/en/messages.json b/locales/en/messages.json
index 083956ee..9645040f 100644
--- a/locales/en/messages.json
+++ b/locales/en/messages.json
@@ -3068,7 +3068,9 @@
"sdcardStatusUnknown": {
"message": "Unknown state $1"
},
-
+ "firmwareFlasherBranch": {
+ "message": "Select commit"
+ },
"firmwareFlasherReleaseSummaryHead": {
"message": "Release info"
},
@@ -3090,17 +3092,17 @@
"firmwareFlasherReleaseTarget": {
"message": "Target:"
},
- "firmwareFlasherReleaseFile": {
- "message": "Binary:"
+ "firmwareFlasherReleaseMCU": {
+ "message": "MCU:"
},
- "firmwareFlasherUnifiedTargetName": {
- "message": "Unified Target:"
+ "firmwareFlasherCloudBuildDetails": {
+ "message": "Cloud Build Details:"
},
- "firmwareFlasherUnifiedTargetFileUrl": {
- "message": "Show config."
+ "firmwareFlasherCloudBuildLogUrl": {
+ "message": "Show Log."
},
- "firmwareFlasherUnifiedTargetDate": {
- "message": "Date:"
+ "firmwareFlasherCloudBuildStatus": {
+ "message": "Status:"
},
"firmwareFlasherReleaseFileUrl": {
"message": "Download manually."
@@ -6674,5 +6676,20 @@
"presetsReviewOptionsWarning": {
"message": "Please, review the list of options before picking this preset.",
"description": "Dialog text to prompt user to review options for the preset"
+ },
+ "firmwareFlasherBuildConfigurationHead": {
+ "message": "Build Configuration"
+ },
+ "firmwareFlasherBuildOptions": {
+ "message": "Other Options"
+ },
+ "firmwareFlasherBuildRadioProtocols": {
+ "message": "Radio Protocols"
+ },
+ "firmwareFlasherBuildTelemetryProtocols": {
+ "message": "Telemetry Protocols"
+ },
+ "firmwareFlasherBuildMotorProtocols": {
+ "message": "Motor Protocols"
}
}
diff --git a/src/css/tabs/firmware_flasher.less b/src/css/tabs/firmware_flasher.less
index 56068935..9a2bca57 100644
--- a/src/css/tabs/firmware_flasher.less
+++ b/src/css/tabs/firmware_flasher.less
@@ -123,7 +123,13 @@
}
}
}
- .release_info {
+ .build_configuration {
+ .select2-selection__choice {
+ margin: auto;
+ color: #3f4241;
+ }
+ }
+ .release_info, .build_configuration {
display: none;
.title {
line-height: 20px;
diff --git a/src/js/ConfigInserter.js b/src/js/ConfigInserter.js
deleted file mode 100644
index 5976656f..00000000
--- a/src/js/ConfigInserter.js
+++ /dev/null
@@ -1,107 +0,0 @@
-'use strict';
-
-const ConfigInserter = function () {
-};
-
-const CUSTOM_DEFAULTS_POINTER_ADDRESS = 0x08002800;
-const BLOCK_SIZE = 16384;
-
-function seek(firmware, address) {
- let index = 0;
- for (; index < firmware.data.length && address >= firmware.data[index].address + firmware.data[index].bytes; index++);
-
- const result = {
- lineIndex: index,
- };
-
- if (firmware.data[index] && address >= firmware.data[index].address) {
- result.byteIndex = address - firmware.data[index].address;
- }
-
- return result;
-}
-
-function readUint32(firmware, index) {
- let result = 0;
- for (let position = 0; position < 4; position++) {
- result += firmware.data[index.lineIndex].data[index.byteIndex++] << (8 * position);
- if (index.byteIndex >= firmware.data[index.lineIndex].bytes) {
- index.lineIndex++;
- index.byteIndex = 0;
- }
- }
-
- return result;
-}
-
-function getCustomDefaultsArea(firmware) {
- const result = {};
-
- const index = seek(firmware, CUSTOM_DEFAULTS_POINTER_ADDRESS);
-
- if (index.byteIndex === undefined) {
- return;
- }
-
- result.startAddress = readUint32(firmware, index);
- result.endAddress = readUint32(firmware, index);
-
- return result;
-}
-
-function generateData(firmware, input, startAddress) {
- let address = startAddress;
-
- const index = seek(firmware, address);
-
- if (index.byteIndex !== undefined) {
- throw new Error('Configuration area in firmware not free.');
- }
-
- // Add 0 terminator
- input = `${input}\0`;
-
- let inputIndex = 0;
- while (inputIndex < input.length) {
- const remaining = input.length - inputIndex;
- const line = {
- address: address,
- bytes: BLOCK_SIZE > remaining ? remaining : BLOCK_SIZE,
- data: [],
- };
-
- if (firmware.data[index.lineIndex] && (line.address + line.bytes) > firmware.data[index.lineIndex].address) {
- throw new Error("Aborting data generation, free area too small.");
- }
-
- for (let i = 0; i < line.bytes; i++) {
- line.data.push(input.charCodeAt(inputIndex++));
- }
-
- address = address + line.bytes;
-
- firmware.data.splice(index.lineIndex++, 0, line);
- }
-
- firmware.bytes_total += input.length;
-}
-
-const CONFIG_LABEL = `Custom defaults inserted in`;
-
-ConfigInserter.prototype.insertConfig = function (firmware, input) {
- console.time(CONFIG_LABEL);
-
- const customDefaultsArea = getCustomDefaultsArea(firmware);
-
- if (!customDefaultsArea || customDefaultsArea.endAddress - customDefaultsArea.startAddress === 0) {
- return false;
- } else if (input.length >= customDefaultsArea.endAddress - customDefaultsArea.startAddress) {
- throw new Error(`Custom defaults area too small (${customDefaultsArea.endAddress - customDefaultsArea.startAddress} bytes), ${input.length + 1} bytes needed.`);
- }
-
- generateData(firmware, input, customDefaultsArea.startAddress);
-
- console.timeEnd(CONFIG_LABEL);
-
- return true;
-};
diff --git a/src/js/FirmwareCache.js b/src/js/FirmwareCache.js
deleted file mode 100644
index cff5c48a..00000000
--- a/src/js/FirmwareCache.js
+++ /dev/null
@@ -1,242 +0,0 @@
-'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;
- SessionStorage.set(obj);
- }
-
- /**
- * @param {Function} callback
- */
- function load(callback) {
- const obj = SessionStorage.get(CACHEKEY);
- 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);
- const obj = SessionStorage.get(cacheKey);
- /** @type {CacheItem} */
- const cached = typeof obj === "object" && obj.hasOwnProperty(cacheKey) ? obj[cacheKey] : null;
- if (cached === null) {
- return undefined;
- }
- SessionStorage.remove(cacheKey);
- onRemoveFromCache(cached.release);
- 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 (!release) {
- return false;
- }
- 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,
- };
- SessionStorage.set(obj);
- 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);
- const obj = SessionStorage.get(cacheKey);
- const cached = typeof obj === "object" && obj.hasOwnProperty(cacheKey) ? obj[cacheKey] : null;
- callback(cached);
- }
-
- /**
- * Remove all cached data
- */
- function invalidate() {
- if (!journalLoaded) {
- console.warn("Cache journal not yet loaded");
- return undefined;
- }
- let cacheKeys = [];
- for (let key of journal.keys()) {
- cacheKeys.push(withCachePrefix(key));
- }
- const obj = SessionStorage.get(cacheKeys);
- if (typeof obj !== "object") {
- return;
- }
- console.log(obj.entries());
- for (let cacheKey of cacheKeys) {
- if (obj.hasOwnProperty(cacheKey)) {
- /** @type {CacheItem} */
- let item = obj[cacheKey];
- onRemoveFromCache(item.release);
- }
- }
- SessionStorage.remove(cacheKeys);
- journal.clear();
- JournalStorage.persist(journal.toJSON());
- }
-
- /**
- * @param {Descriptor} release
- */
- function onPutToCache(release) {
- if (typeof onPutToCacheCallback === "function") {
- onPutToCacheCallback(release);
- }
- console.info(`Release put to cache: ${keyOf(release)}`);
- }
-
- /**
- * @param {Descriptor} release
- */
- function onRemoveFromCache(release) {
- if (typeof onRemoveFromCacheCallback === "function") {
- onRemoveFromCacheCallback(release);
- }
- console.debug(`Cache data removed: ${keyOf(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);
- },
- unload: () => {
- JournalStorage.persist(journal.toJSON());
- journal.clear();
- },
- invalidate: invalidate,
- };
-})();
diff --git a/src/js/jenkins_loader.js b/src/js/jenkins_loader.js
deleted file mode 100644
index 98b2688f..00000000
--- a/src/js/jenkins_loader.js
+++ /dev/null
@@ -1,160 +0,0 @@
-'use strict';
-
-const JenkinsLoader = function (url) {
- this._url = url;
- this._jobs = [];
- this._cacheExpirationPeriod = 3600 * 1000;
-
- this._jobsRequest = '/api/json?tree=jobs[name]';
- this._buildsRequest = '/api/json?tree=builds[number,result,timestamp,artifacts[relativePath],changeSet[items[commitId,msg]]]';
-};
-
-JenkinsLoader.prototype.loadJobs = function (viewName, callback) {
- const self = this;
-
- const viewUrl = `${self._url}/view/${viewName}`;
- const jobsDataTag = `${viewUrl}_JobsData`;
- const cacheLastUpdateTag = `${viewUrl}_JobsLastUpdate`;
-
- const wrappedCallback = jobs => {
- self._jobs = jobs;
- callback(jobs);
- };
-
- const result = SessionStorage.get([cacheLastUpdateTag, jobsDataTag]);
- const jobsDataTimestamp = $.now();
- const cachedJobsData = result[jobsDataTag];
- const cachedJobsLastUpdate = result[cacheLastUpdateTag];
-
- const cachedCallback = () => {
- if (cachedJobsData) {
- GUI.log(i18n.getMessage('buildServerUsingCached', ['jobs']));
- }
-
- wrappedCallback(cachedJobsData ? cachedJobsData : []);
- };
-
- if (!cachedJobsData || !cachedJobsLastUpdate || jobsDataTimestamp - cachedJobsLastUpdate > self._cacheExpirationPeriod) {
- const url = `${viewUrl}${self._jobsRequest}`;
-
- $.get(url, jobsInfo => {
- GUI.log(i18n.getMessage('buildServerLoaded', ['jobs']));
-
- // remove Betaflight prefix, rename Betaflight job to Development
- const jobs = jobsInfo.jobs.map(job => {
- return { title: job.name.replace('Betaflight ', '').replace('Betaflight', 'Development'), name: job.name };
- });
-
- // cache loaded info
- const object = {};
- object[jobsDataTag] = jobs;
- object[cacheLastUpdateTag] = $.now();
- SessionStorage.set(object);
-
- wrappedCallback(jobs);
- }).fail(xhr => {
- GUI.log(i18n.getMessage('buildServerLoadFailed', ['jobs', `HTTP ${xhr.status}`]));
- cachedCallback();
- });
- } else {
- cachedCallback();
- }
-};
-
-JenkinsLoader.prototype.loadBuilds = function (jobName, callback) {
- const self = this;
-
- const jobUrl = `${self._url}/job/${jobName}`;
- const buildsDataTag = `${jobUrl}BuildsData`;
- const cacheLastUpdateTag = `${jobUrl}BuildsLastUpdate`;
-
- const result = SessionStorage.get([cacheLastUpdateTag, buildsDataTag]);
- const buildsDataTimestamp = $.now();
- const cachedBuildsData = result[buildsDataTag];
- const cachedBuildsLastUpdate = result[cacheLastUpdateTag];
-
- const cachedCallback = () => {
- if (cachedBuildsData) {
- GUI.log(i18n.getMessage('buildServerUsingCached', [jobName]));
- }
-
- self._parseBuilds(jobUrl, jobName, cachedBuildsData ? cachedBuildsData : [], callback);
- };
-
- if (!cachedBuildsData || !cachedBuildsLastUpdate || buildsDataTimestamp - cachedBuildsLastUpdate > self._cacheExpirationPeriod) {
- const url = `${jobUrl}${self._buildsRequest}`;
-
- $.get(url, function (buildsInfo) {
- GUI.log(i18n.getMessage('buildServerLoaded', [jobName]));
-
- // filter successful builds
- const builds = buildsInfo.builds.filter(build => build.result == 'SUCCESS')
- .map(build => ({
- number: build.number,
- artifacts: build.artifacts.map(artifact => artifact.relativePath),
- changes: build.changeSet.items.map(item => `* ${item.msg}`).join('<br>\n'),
- timestamp: build.timestamp,
- }));
-
- // cache loaded info
- const object = {};
- object[buildsDataTag] = builds;
- object[cacheLastUpdateTag] = $.now();
- SessionStorage.set(object);
- self._parseBuilds(jobUrl, jobName, builds, callback);
- }).fail(xhr => {
- GUI.log(i18n.getMessage('buildServerLoadFailed', [jobName, `HTTP ${xhr.status}`]));
- cachedCallback();
- });
- } else {
- cachedCallback();
- }
-};
-
-JenkinsLoader.prototype._parseBuilds = function (jobUrl, jobName, builds, callback) {
- // convert from `build -> targets` to `target -> builds` mapping
- const targetBuilds = {};
-
- const targetFromFilenameExpression = /betaflight_([\d.]+)?_?(\w+)(\-.*)?\.(.*)/;
-
- builds.forEach(build => {
- build.artifacts.forEach(relativePath => {
- const match = targetFromFilenameExpression.exec(relativePath);
-
- if (!match) {
- return;
- }
-
- const version = match[1];
- const target = match[2];
- const date = new Date(build.timestamp);
-
- const day = (`0${date.getDate()}`).slice(-2);
- const month = (`0${(date.getMonth() + 1)}`).slice(-2);
- const year = date.getFullYear();
- const hours = (`0${date.getHours()}`).slice(-2);
- const minutes = (`0${date.getMinutes()}`).slice(-2);
-
- const formattedDate = `${day}-${month}-${year} ${hours}:${minutes}`;
-
- const descriptor = {
- 'releaseUrl': `${jobUrl}/${build.number}`,
- 'name' : `${jobName} #${build.number}`,
- 'version' : `${version} #${build.number}`,
- 'url' : `${jobUrl}/${build.number}/artifact/${relativePath}`,
- 'file' : relativePath.split('/').slice(-1)[0],
- 'target' : target,
- 'date' : formattedDate,
- 'notes' : build.changes,
- };
-
- if (targetBuilds[target]) {
- targetBuilds[target].push(descriptor);
- } else {
- targetBuilds[target] = [ descriptor ];
- }
- });
- });
-
- callback(targetBuilds);
-};
diff --git a/src/js/main.js b/src/js/main.js
index 1456988f..cb2eeca7 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -638,6 +638,12 @@ function notifyOutdatedVersion(releaseData) {
if (result.checkForConfiguratorUnstableVersions) {
showUnstableReleases = true;
}
+
+ if (releaseData === undefined) {
+ console.log('No releaseData');
+ return false;
+ }
+
const versions = releaseData.filter(function (version) {
const semVerVersion = semver.parse(version.tag_name);
if (semVerVersion && (showUnstableReleases || semVerVersion.prerelease.length === 0)) {
diff --git a/src/js/release_loader.js b/src/js/release_loader.js
new file mode 100644
index 00000000..cdda8ee2
--- /dev/null
+++ b/src/js/release_loader.js
@@ -0,0 +1,129 @@
+'use strict';
+
+class ReleaseLoader {
+
+ constructor (url) {
+ this._url = url;
+ this._cacheExpirationPeriod = 3600 * 1000;
+ }
+
+ load(url, onSuccess, onFailure) {
+
+ const dataTag = `${url}_Data`;
+ const cacheLastUpdateTag = `${url}_LastUpdate`;
+
+ const result = SessionStorage.get([cacheLastUpdateTag, dataTag]);
+ const dataTimestamp = $.now();
+ const cachedData = result[dataTag];
+ const cachedLastUpdate = result[cacheLastUpdateTag];
+
+ const cachedCallback = () => {
+ if (cachedData) {
+ GUI.log(i18n.getMessage('buildServerUsingCached', [url]));
+ }
+
+ onSuccess(cachedData);
+ };
+
+ if (!cachedData || !cachedLastUpdate || dataTimestamp - cachedLastUpdate > this._cacheExpirationPeriod) {
+ $.get(url, function (info) {
+ GUI.log(i18n.getMessage('buildServerLoaded', [url]));
+
+ // cache loaded info
+ const object = {};
+ object[dataTag] = info;
+ object[cacheLastUpdateTag] = $.now();
+ SessionStorage.set(object);
+ onSuccess(info);
+ }).fail(xhr => {
+ GUI.log(i18n.getMessage('buildServerLoadFailed', [url, `HTTP ${xhr.status}`]));
+ if (onFailure !== undefined) {
+ onFailure();
+ } else {
+ cachedCallback();
+ }
+ });
+ } else {
+ cachedCallback();
+ }
+ }
+
+ loadTargets(callback) {
+
+ const url = `${this._url}/api/targets`;
+ this.load(url, callback);
+ }
+
+ loadTargetReleases(target, callback) {
+
+ const url = `${this._url}/api/targets/${target}`;
+ this.load(url, callback);
+ }
+
+ loadTarget(target, release, onSuccess, onFailure) {
+
+ const url = `${this._url}/api/builds/${release}/${target}`;
+ this.load(url, onSuccess, onFailure);
+ }
+
+ loadTargetHex(path, onSuccess, onFailure) {
+
+ const url = `${this._url}${path}`;
+ $.get(url, function (data) {
+ GUI.log(i18n.getMessage('buildServerLoaded', [path]));
+ onSuccess(data);
+ }).fail(xhr => {
+ GUI.log(i18n.getMessage('buildServerLoadFailed', [path, `HTTP ${xhr.status}`]));
+ if (onFailure !== undefined) {
+ onFailure();
+ }
+ });
+ }
+
+ requestBuild(request, onSuccess, onFailure) {
+
+ const url = `${this._url}/api/builds`;
+ $.ajax({
+ url: url,
+ type: "POST",
+ data: JSON.stringify(request),
+ contentType: "application/json",
+ dataType: "json",
+ success: function(data) {
+ data.url = `/api/builds/${data.key}/hex`;
+ onSuccess(data);
+ },
+ }).fail(xhr => {
+ GUI.log(i18n.getMessage('buildServerLoadFailed', [url, `HTTP ${xhr.status}`]));
+ if (onFailure !== undefined) {
+ onFailure();
+ }
+ });
+ }
+
+ requestBuildStatus(key, onSuccess, onFailure) {
+
+ const url = `${this._url}/api/builds/${key}/status`;
+ $.get(url, function (data) {
+ GUI.log(i18n.getMessage('buildServerLoaded', [url]));
+ onSuccess(data);
+ }).fail(xhr => {
+ GUI.log(i18n.getMessage('buildServerLoadFailed', [url, `HTTP ${xhr.status}`]));
+ if (onFailure !== undefined) {
+ onFailure();
+ }
+ });
+ }
+
+ loadOptions(onSuccess, onFailure) {
+
+ const url = `${this._url}/api/options`;
+ this.load(url, onSuccess, onFailure);
+ }
+
+ loadCommits(release, onSuccess, onFailure) {
+
+ const url = `${this._url}/api/releases/${release}/commits`;
+ this.load(url, onSuccess, onFailure);
+ }
+}
diff --git a/src/js/tabs/firmware_flasher.js b/src/js/tabs/firmware_flasher.js
index 5b1eae2d..0645b3b9 100644
--- a/src/js/tabs/firmware_flasher.js
+++ b/src/js/tabs/firmware_flasher.js
@@ -1,16 +1,13 @@
import { i18n } from '../localization';
const firmware_flasher = {
- releases: null,
- releaseChecker: new ReleaseChecker('firmware', 'https://api.github.com/repos/betaflight/betaflight/releases'),
- jenkinsLoader: new JenkinsLoader('https://ci.betaflight.tech'),
- gitHubApi: new GitHubApi(),
+ targets: null,
+ releaseLoader: new ReleaseLoader('https://build.betaflight.com'),
localFirmwareLoaded: false,
selectedBoard: undefined,
boardNeedsVerification: false,
intel_hex: undefined, // standard intel hex in string format
parsed_hex: undefined, // parsed raw hex in array format
- unifiedTarget: {}, // the Unified Target configuration to be spliced into the configuration
isConfigLocal: false, // Set to true if the user loads one locally
developmentFirmwareLoaded: false, // Is the firmware to be flashed from the development branch?
};
@@ -28,24 +25,9 @@ firmware_flasher.initialize = function (callback) {
self.intel_hex = undefined;
self.parsed_hex = undefined;
- const unifiedSource = 'https://api.github.com/repos/betaflight/unified-targets/contents/configs/default';
-
- function onFirmwareCacheUpdate(release) {
- $('select[name="firmware_version"] option').each(function () {
- const option_e = $(this);
- const optionRelease = option_e.data("summary");
- if (optionRelease && optionRelease.file === release.file) {
- option_e.toggleClass("cached", FirmwareCache.has(release));
- }
- });
- }
-
function onDocumentLoad() {
- FirmwareCache.load();
- FirmwareCache.onPutToCache(onFirmwareCacheUpdate);
- FirmwareCache.onRemoveFromCache(onFirmwareCacheUpdate);
- function parse_hex(str, callback) {
+ function parseHex(str, callback) {
// parsing hex in different thread
const worker = new Worker('./js/workers/hex_parser.js');
@@ -58,75 +40,78 @@ firmware_flasher.initialize = function (callback) {
worker.postMessage(str);
}
- function show_loaded_hex(summary) {
- self.flashingMessage(`<a class="save_firmware" href="#" title="Save Firmware">${i18n.getMessage('firmwareFlasherFirmwareOnlineLoaded', { filename: summary.file, bytes: self.parsed_hex.bytes_total })}</a>`,
- self.FLASH_MESSAGE_TYPES.NEUTRAL);
+ function showLoadedHex(fileName) {
+ if (self.localFirmwareLoaded) {
+ self.flashingMessage(i18n.getMessage('firmwareFlasherFirmwareLocalLoaded', { filename: fileName, bytes: self.parsed_hex.bytes_total }), self.FLASH_MESSAGE_TYPES.NEUTRAL);
+ } else {
+ self.flashingMessage(`<a class="save_firmware" href="#" title="Save Firmware">${i18n.getMessage('firmwareFlasherFirmwareOnlineLoaded', { filename: fileName, bytes: self.parsed_hex.bytes_total })}</a>`,
+ self.FLASH_MESSAGE_TYPES.NEUTRAL);
+ }
self.enableFlashing(true);
+ }
- if (self.unifiedTarget.manufacturerId) {
- $('div.release_info #manufacturer').text(self.unifiedTarget.manufacturerId);
+ function showReleaseNotes(summary) {
+ if (summary.manufacturer) {
+ $('div.release_info #manufacturer').text(summary.manufacturer);
$('div.release_info #manufacturerInfo').show();
} else {
$('div.release_info #manufacturerInfo').hide();
}
- $('div.release_info .target').text(TABS.firmware_flasher.selectedBoard);
- $('div.release_info .name').text(summary.version).prop('href', summary.releaseUrl);
+ $('div.release_info .target').text(summary.target);
+ $('div.release_info .name').text(summary.release).prop('href', summary.releaseUrl);
$('div.release_info .date').text(summary.date);
- $('div.release_info .file').text(summary.file).prop('href', summary.url);
+ $('div.release_info #targetMCU').text(summary.mcu);
- if (Object.keys(self.unifiedTarget).length > 0) {
- $('div.release_info #unifiedTargetInfo').show();
- $('div.release_info #unifiedTargetFile').text(self.unifiedTarget.fileName).prop('href', self.unifiedTarget.fileUrl);
- $('div.release_info #unifiedTargetDate').text(self.unifiedTarget.date);
+ if (summary.cloudBuild) {
+ $('div.release_info #cloudTargetInfo').show();
+ $('div.release_info #cloudTargetLog').text('');
+ $('div.release_info #cloudTargetStatus').text('pending');
} else {
- $('div.release_info #unifiedTargetInfo').hide();
+ $('div.release_info #cloudTargetInfo').hide();
}
- let formattedNotes = summary.notes.replace(/#(\d+)/g, '[#$1](https://github.com/betaflight/betaflight/pull/$1)');
- formattedNotes = marked.parse(formattedNotes);
- formattedNotes = DOMPurify.sanitize(formattedNotes);
- $('div.release_info .notes').html(formattedNotes);
- GUI.addLinksTargetBlank($('div.release_info .notes'));
+ if (summary.notes) {
+ let formattedNotes = summary.notes.replace(/#(\d+)/g, '[#$1](https://github.com/betaflight/betaflight/pull/$1)');
+ formattedNotes = marked.parse(formattedNotes);
+ formattedNotes = DOMPurify.sanitize(formattedNotes);
+ $('div.release_info .notes').html(formattedNotes);
+ GUI.addLinksTargetBlank($('div.release_info .notes'));
+ } else {
+ $('div.release_info .notes').html('Release notes unavailable.');
+ }
- if (self.releases) {
+ if (self.targets) {
$('div.release_info').slideDown();
$('.tab-firmware_flasher .content_wrapper').animate({ scrollTop: $('div.release_info').position().top }, 1000);
}
}
- function process_hex(data, summary) {
+ function processHex(data, key) {
self.intel_hex = data;
- parse_hex(self.intel_hex, function (data) {
+ parseHex(self.intel_hex, function (data) {
self.parsed_hex = data;
if (self.parsed_hex) {
analytics.setFirmwareData(analytics.DATA.FIRMWARE_SIZE, self.parsed_hex.bytes_total);
-
- if (!FirmwareCache.has(summary)) {
- FirmwareCache.put(summary, self.intel_hex);
- }
- show_loaded_hex(summary);
-
+ showLoadedHex(key);
} else {
self.flashingMessage(i18n.getMessage('firmwareFlasherHexCorrupted'), self.FLASH_MESSAGE_TYPES.INVALID);
+ self.enableFlashing(false);
}
});
}
- function onLoadSuccess(data, summary) {
+ function onLoadSuccess(data, key) {
self.localFirmwareLoaded = false;
- // The path from getting a firmware doesn't fill in summary.
- summary = typeof summary === "object"
- ? summary
- : $('select[name="firmware_version"] option:selected').data('summary');
- process_hex(data, summary);
+
+ processHex(data, key);
$("a.load_remote_file").removeClass('disabled');
$("a.load_remote_file").text(i18n.getMessage('firmwareFlasherButtonLoadOnline'));
}
- function populateBoardOptions(builds) {
- if (!builds) {
+ function loadTargetList(targets) {
+ if (!targets || !navigator.onLine) {
$('select[name="board"]').empty().append('<option value="0">Offline</option>');
$('select[name="firmware_version"]').empty().append('<option value="0">Offline</option>');
@@ -141,225 +126,67 @@ firmware_flasher.initialize = function (callback) {
versions_e.empty();
versions_e.append($(`<option value='0'>${i18n.getMessage("firmwareFlasherOptionLabelSelectFirmwareVersion")}</option>`));
-
- const selectTargets = [];
- Object.keys(builds)
- .sort()
+ Object.keys(targets)
+ .sort((a,b) => a.target - b.target)
.forEach(function(target, i) {
- const descriptors = builds[target];
- descriptors.forEach(function(descriptor){
- if ($.inArray(target, selectTargets) === -1) {
- selectTargets.push(target);
- const select_e = $(`<option value='${descriptor.target}'>${descriptor.target}</option>`) ;
- boards_e.append(select_e);
- }
- });
+ const descriptor = targets[target];
+ const select_e = $(`<option value='${descriptor.target}'>${descriptor.target}</option>`);
+ boards_e.append(select_e);
});
- TABS.firmware_flasher.releases = builds;
+ TABS.firmware_flasher.targets = targets;
result = SessionStorage.get('selected_board');
if (result.selected_board) {
- const boardBuilds = builds[result.selected_board];
- $('select[name="board"]').val(boardBuilds ? result.selected_board : 0).trigger('change');
+ const selected = targets[result.selected_board];
+ $('select[name="board"]').val(selected ? result.selected_board : 0).trigger('change');
}
}
- function processBoardOptions(releaseData, showDevReleases) {
- const releases = {};
- let sortedTargets = [];
- const unsortedTargets = [];
- releaseData.forEach(function(release) {
- release.assets.forEach(function(asset) {
- const targetFromFilenameExpression = /betaflight_([\d.]+)?_?(\w+)(\-.*)?\.(.*)/;
- const match = targetFromFilenameExpression.exec(asset.name);
- if ((!showDevReleases && release.prerelease) || !match) {
- return;
- }
- const target = match[2];
- if ($.inArray(target, unsortedTargets) === -1) {
- unsortedTargets.push(target);
- }
- });
- sortedTargets = unsortedTargets.sort();
- });
- sortedTargets.forEach(function(release) {
- releases[release] = [];
- });
- releaseData.forEach(function(release) {
- const versionFromTagExpression = /v?(.*)/;
- const matchVersionFromTag = versionFromTagExpression.exec(release.tag_name);
- const version = matchVersionFromTag[1];
- release.assets.forEach(function(asset) {
- const targetFromFilenameExpression = /betaflight_([\d.]+)?_?(\w+)(\-.*)?\.(.*)/;
- const match = targetFromFilenameExpression.exec(asset.name);
- if ((!showDevReleases && release.prerelease) || !match) {
- return;
- }
- const target = match[2];
- const format = match[4];
- if (format !== 'hex') {
- return;
- }
- const date = new Date(release.published_at);
- const dayOfTheMonth = `0${date.getDate()}`.slice(-2);
- const month = `0${date.getMonth() + 1}`.slice(-2);
- const year = date.getFullYear();
- const hours = `0${date.getHours()}`.slice(-2);
- const minutes = `0${date.getMinutes()}`.slice(-2);
- const formattedDate = `${dayOfTheMonth}-${month}-${year} ${hours}:${minutes}`;
- const descriptor = {
- "releaseUrl": release.html_url,
- "name" : version,
- "version" : version,
- "url" : asset.browser_download_url,
- "file" : asset.name,
- "target" : target,
- "date" : formattedDate,
- "notes" : release.body,
- };
- releases[target].push(descriptor);
- });
- });
- loadUnifiedBuilds(releases);
- }
-
- function supportsUnifiedTargets(version) {
- return semver.gte(version.split(' ')[0], '4.1.0-RC1');
- }
-
- function hasUnifiedTargetBuild(builds) {
- // Find a build that is newer than 4.1.0, return true if found
- return Object.keys(builds).some(function (key) {
- return builds[key].some(function(target) {
- return supportsUnifiedTargets(target.version);
- });
- });
- }
-
- function loadUnifiedBuilds(builds) {
- const expirationPeriod = 3600 * 2; // Two of your earth hours.
- const checkTime = Math.floor(Date.now() / 1000); // Lets deal in seconds.
- if (builds && hasUnifiedTargetBuild(builds)) {
- console.log('loaded some builds for later');
- const storageTag = 'unifiedSourceCache';
- result = SessionStorage.get(storageTag);
- let storageObj = result[storageTag];
- if (!storageObj || !storageObj.lastUpdate || checkTime - storageObj.lastUpdate > expirationPeriod) {
- console.log('go get', unifiedSource);
- $.get(unifiedSource, function(data, textStatus, jqXHR) {
- // Cache the information for later use.
- let newStorageObj = {};
- let newDataObj = {};
- newDataObj.lastUpdate = checkTime;
- newDataObj.data = data;
- newStorageObj[storageTag] = newDataObj;
- SessionStorage.set(newStorageObj);
-
- parseUnifiedBuilds(data, builds);
- }).fail(xhr => {
- console.log('failed to get new', unifiedSource, 'cached data', Math.floor((checkTime - storageObj.lastUpdate) / 60), 'mins old');
- parseUnifiedBuilds(storageObj.data, builds);
- });
+ function buildOptionsList(select_e, options) {
+ select_e.empty();
+ options.forEach((option) => {
+ if (option.default) {
+ select_e.append($(`<option value='${option.value}' selected>${option.name}</option>`));
} else {
- // In the event that the cache is okay
- console.log('unified config cached data', Math.floor((checkTime - storageObj.lastUpdate)/60), 'mins old');
- parseUnifiedBuilds(storageObj.data, builds);
+ select_e.append($(`<option value='${option.value}'>${option.name}</option>`));
}
- } else {
- populateBoardOptions(builds);
- }
+ });
}
- function parseUnifiedBuilds(data, builds) {
- if (!data) {
+ function buildOptions(data) {
+ if (!navigator.onLine) {
return;
}
- let releases = {};
- let unifiedConfigs = {};
- let items = {};
- // Get the legacy builds
- Object.keys(builds).forEach(function (targetName) {
- items[targetName] = { };
- releases[targetName] = builds[targetName];
- });
- // Get the Unified Target configurations
- data.forEach(function(target) {
- const TARGET_REGEXP = /^([^-]{1,4})-(.*).config$/;
- let targetParts = target.name.match(TARGET_REGEXP);
- if (!targetParts) {
- return;
- }
- const targetName = targetParts[2];
- const manufacturerId = targetParts[1];
- items[targetName] = { };
- unifiedConfigs[targetName] = (unifiedConfigs[targetName] || {});
- unifiedConfigs[targetName][manufacturerId] = target;
- });
- const boards_e = $('select[name="board"]');
- const versions_e = $('select[name="firmware_version"]');
- boards_e.empty()
- .append($(`<option value='0'>${i18n.getMessage("firmwareFlasherOptionLabelSelectBoard")}</option>`));
-
- versions_e.empty()
- .append($(`<option value='0'>${i18n.getMessage("firmwareFlasherOptionLabelSelectFirmwareVersion")}</option>`));
- Object.keys(items)
- .sort()
- .forEach(function(target) {
- const select_e = $(`<option value='${target}'>${target}</option>"`);
- boards_e.append(select_e);
- });
- TABS.firmware_flasher.releases = releases;
- TABS.firmware_flasher.unifiedConfigs = unifiedConfigs;
+ buildOptionsList($('select[name="radioProtocols"]'), data.radioProtocols);
+ buildOptionsList($('select[name="telemetryProtocols"]'), data.telemetryProtocols);
+ buildOptionsList($('select[name="options"]'), data.generalOptions);
+ buildOptionsList($('select[name="motorProtocols"]'), data.motorProtocols);
+ }
- result = SessionStorage.get('selected_board');
- if (result.selected_board) {
- const boardReleases = TABS.firmware_flasher.unifiedConfigs[result.selected_board]
- || TABS.firmware_flasher.releases[result.selected_board];
- $('select[name="board"]').val(boardReleases ? result.selected_board : 0).trigger('change');
- }
+ self.releaseLoader.loadOptions(buildOptions);
+
+ let buildTypesToShow;
+ const buildType_e = $('select[name="build_type"]');
+ function buildBuildTypeOptionsList() {
+ buildType_e.empty();
+ buildTypesToShow.forEach(({ tag, title }, index) => {
+ buildType_e.append($(`<option value='${index}'>${tag ? i18n.getMessage(tag) : title}</option>`));
+ });
}
const buildTypes = [
{
tag: 'firmwareFlasherOptionLabelBuildTypeRelease',
- loader: () => self.releaseChecker.loadReleaseData(releaseData => processBoardOptions(releaseData, false)),
},
{
tag: 'firmwareFlasherOptionLabelBuildTypeReleaseCandidate',
- loader: () => self.releaseChecker.loadReleaseData(releaseData => processBoardOptions(releaseData, true)),
+ },
+ {
+ tag: "firmwareFlasherOptionLabelBuildTypeDevelopment",
},
];
- const ciBuildsTypes = self.jenkinsLoader._jobs.map(job => {
- if (job.title === "Development") {
- return {
- tag: "firmwareFlasherOptionLabelBuildTypeDevelopment",
- loader: () => self.jenkinsLoader.loadBuilds(job.name, loadUnifiedBuilds),
- };
- }
- return {
- title: job.title,
- loader: () => self.jenkinsLoader.loadBuilds(job.name, loadUnifiedBuilds),
- };
- });
-
- let buildTypesToShow;
- const buildType_e = $('select[name="build_type"]');
- function buildBuildTypeOptionsList() {
- buildType_e.empty();
- buildTypesToShow.forEach(({ tag, title }, index) => {
- buildType_e.append(
- $(
- `<option value='${index}'>${
- tag ? i18n.getMessage(tag) : title
- }</option>`,
- ),
- );
- });
- buildType_e.val($('select[name="build_type"] option:first').val());
- }
-
function showOrHideBuildTypes() {
const showExtraReleases = $(this).is(':checked');
@@ -374,24 +201,23 @@ firmware_flasher.initialize = function (callback) {
}
const globalExpertMode_e = $('input[name="expertModeCheckbox"]');
- function showOrHideBuildTypeSelect() {
+ function showOrHideExpertMode() {
const expertModeChecked = $(this).is(':checked');
globalExpertMode_e.prop('checked', expertModeChecked).trigger('change');
if (expertModeChecked) {
- buildTypesToShow = buildTypes.concat(ciBuildsTypes);
- buildBuildTypeOptionsList();
- } else {
buildTypesToShow = buildTypes;
- buildBuildTypeOptionsList();
- buildType_e.val(0).trigger('change');
+ } else {
+ buildTypesToShow = buildTypes.slice(0,2);
}
+ buildBuildTypeOptionsList();
+ buildType_e.val(0).trigger('change');
}
const expertMode_e = $('.tab-firmware_flasher input.expert_mode');
expertMode_e.prop('checked', globalExpertMode_e.is(':checked'));
$('input.show_development_releases').change(showOrHideBuildTypes).change();
- expertMode_e.change(showOrHideBuildTypeSelect).change();
+ expertMode_e.change(showOrHideExpertMode).change();
// translate to user-selected language
i18n.localizePage();
@@ -409,10 +235,8 @@ firmware_flasher.initialize = function (callback) {
.append($(`<option value='0'>${i18n.getMessage("firmwareFlasherOptionLoading")}</option>`));
if (!GUI.connect_lock) {
- TABS.firmware_flasher.unifiedConfigs = {};
-
try {
- buildTypesToShow[build_type].loader();
+ self.releaseLoader.loadTargets(loadTargetList);
} catch (err) {
console.error(err);
}
@@ -421,81 +245,37 @@ firmware_flasher.initialize = function (callback) {
ConfigStorage.set({'selected_build_type': build_type});
});
- function populateBuilds(builds, target, manufacturerId, duplicateName, targetVersions, callback) {
- if (targetVersions) {
- targetVersions.forEach(function(descriptor) {
- const versionRegex = /^(\d+.\d+.\d+(?:-\w+)?)(?: #(\d+))?$/;
- const versionParts = descriptor.version.match(versionRegex);
- if (!versionParts) {
- return;
- }
- let version = versionParts[1];
- const buildNumber = versionParts[2] ? `${versionParts[2]}` : '';
-
- const build = { descriptor };
- if (manufacturerId) {
- if (!supportsUnifiedTargets(descriptor.version)) {
- return;
- }
-
- version = `${version}+${buildNumber}${manufacturerId}`;
- build.manufacturerId = manufacturerId;
- build.duplicateName = duplicateName;
- } else {
- version = `${version}+${buildNumber}-legacy`;
- build.isLegacy = true;
- }
- builds[version] = build;
- });
- }
-
- if (callback) {
- callback();
- }
- }
-
- function populateVersions(versions_element, builds, target) {
- const sortVersions = function (a, b) {
- return -semver.compareBuild(a, b);
+ function populateReleases(versions_element, target) {
+ const sortReleases = function (a, b) {
+ return -semver.compareBuild(a.release, b.release);
};
versions_element.empty();
- const targetVersions = Object.keys(builds);
- if (targetVersions.length > 0) {
+ const releases = target.releases;
+ if (releases.length > 0) {
versions_element.append(
$(
`<option value='0'>${i18n.getMessage(
"firmwareFlasherOptionLabelSelectFirmwareVersionFor",
- )} ${target}</option>`,
+ )} ${target.target}</option>`,
),
);
- targetVersions
- .sort(sortVersions)
- .forEach(function(versionName) {
- const version = builds[versionName];
- if (!version.isLegacy && !supportsUnifiedTargets(version.descriptor.version)) {
- return;
- }
-
- let versionLabel;
- if (version.isLegacy && Object.values(builds).some(function (build) {
- return build.descriptor.version === version.descriptor.version && !build.isLegacy;
- })) {
- versionLabel = i18n.getMessage("firmwareFlasherLegacyLabel", { target: version.descriptor.version });
- } else if (!version.isLegacy && Object.values(builds).some(function (build) {
- return build.descriptor.version === version.descriptor.version && build.manufacturerId !== version.manufacturerId && !build.isLegacy;
- })) {
- versionLabel = `${version.descriptor.version} (${version.manufacturerId})`;
- } else {
- versionLabel = version.descriptor.version;
- }
-
- const select_e = $(`<option value='${versionName}'>${version.descriptor.date} - ${versionLabel}</option>`);
- if (FirmwareCache.has(version.descriptor)) {
- select_e.addClass("cached");
- }
- select_e.data('summary', version.descriptor);
+ const build_type = $('select[name="build_type"]').val();
+
+ releases
+ .sort(sortReleases)
+ .filter(r => {
+ return (r.type === 'Unstable' && build_type > 1) ||
+ (r.type === 'ReleaseCandidate' && build_type > 0) ||
+ (r.type === 'Stable');
+ })
+ .forEach(function(release) {
+ const releaseName = release.release;
+
+ const select_e = $(`<option value='${releaseName}'>${releaseName} [${release.label}]</option>`);
+ const summary = `${target}/${release}`;
+ select_e.data('summary', summary);
versions_element.append(select_e);
});
// Assume flashing latest, so default to it.
@@ -503,40 +283,19 @@ firmware_flasher.initialize = function (callback) {
}
}
- function grabBuildNameFromConfig(config) {
- let bareBoard;
- try {
- bareBoard = config.split("\n")[0].split(' ')[3];
- } catch (e) {
- bareBoard = undefined;
- console.log('grabBuildNameFromConfig failed: ', e.message);
- }
- return bareBoard;
- }
-
- function setUnifiedConfig(target, bareBoard, targetConfig, manufacturerId, fileName, fileUrl, date) {
- // a target might request a firmware with the same name, remove configuration in this case.
- if (bareBoard === target) {
- self.unifiedTarget = {};
- } else {
- self.unifiedTarget.config = targetConfig;
- self.unifiedTarget.manufacturerId = manufacturerId;
- self.unifiedTarget.fileName = fileName;
- self.unifiedTarget.fileUrl = fileUrl;
- self.unifiedTarget.date = date;
- self.isConfigLocal = false;
- }
- }
-
function clearBufferedFirmware() {
self.isConfigLocal = false;
- self.unifiedTarget = {};
self.intel_hex = undefined;
self.parsed_hex = undefined;
self.localFirmwareLoaded = false;
}
$('select[name="board"]').select2();
+ $('select[name="radioProtocols"]').select2();
+ $('select[name="telemetryProtocols"]').select2();
+ $('select[name="motorProtocols"]').select2();
+ $('select[name="options"]').select2();
+ $('select[name="commits"]').select2();
$('select[name="board"]').change(function() {
$("a.load_remote_file").addClass('disabled');
@@ -549,15 +308,6 @@ firmware_flasher.initialize = function (callback) {
}
if (!GUI.connect_lock) {
- if (TABS.firmware_flasher.selectedBoard !== target) {
- // We're sure the board actually changed
- if (self.isConfigLocal) {
- console.log('Board changed, unloading local config');
- self.isConfigLocal = false;
- self.unifiedTarget = {};
- }
- }
-
if (target !== '0') {
SessionStorage.set({'selected_board': target});
}
@@ -571,6 +321,7 @@ firmware_flasher.initialize = function (callback) {
$('div.git_info').slideUp();
$('div.release_info').slideUp();
+ $('div.build_configuration').slideUp();
if (!self.localFirmwareLoaded) {
self.enableFlashing(false);
@@ -600,149 +351,11 @@ firmware_flasher.initialize = function (callback) {
),
);
- const builds = [];
-
- const finishPopulatingBuilds = function () {
- if (TABS.firmware_flasher.releases[target]) {
- TABS.firmware_flasher.bareBoard = target;
- populateBuilds(builds, target, undefined, false, TABS.firmware_flasher.releases[target]);
- }
-
- populateVersions(versions_e, builds, target);
- };
-
- if (TABS.firmware_flasher.unifiedConfigs[target]) {
- const storageTag = 'unifiedConfigLast';
- const expirationPeriod = 3600; // One of your earth hours.
- const checkTime = Math.floor(Date.now() / 1000); // Lets deal in seconds.
- result = SessionStorage.get(storageTag);
- let storageObj = result[storageTag];
- const unifiedConfigList = TABS.firmware_flasher.unifiedConfigs[target];
- const manufacturerIds = Object.keys(unifiedConfigList);
- const duplicateName = manufacturerIds.length > 1;
-
- const processManufacturer = function(index) {
- const processNext = function () {
- if (index < manufacturerIds.length - 1) {
- processManufacturer(index + 1);
- } else {
- finishPopulatingBuilds();
- }
- };
-
- const manufacturerId = manufacturerIds[index];
- const targetId = `${target}+${manufacturerId}`;
- // Check to see if the cached configuration is the one we want.
- if (!storageObj || !storageObj.targetId || storageObj.targetId !== targetId
- || !storageObj.lastUpdate || checkTime - storageObj.lastUpdate > expirationPeriod
- || !storageObj.unifiedTarget) {
- const unifiedConfig = unifiedConfigList[manufacturerId];
- // Have to go and try and get the unified config, and then do stuff
- $.get(unifiedConfig.download_url, function(targetConfig) {
- console.log('got unified config');
-
- let config = cleanUnifiedConfigFile(targetConfig);
- if (config !== null) {
- const bareBoard = grabBuildNameFromConfig(config);
- TABS.firmware_flasher.bareBoard = bareBoard;
-
- self.gitHubApi.getFileLastCommitInfo('betaflight/unified-targets', 'master', unifiedConfig.path, function (commitInfo) {
- config = self.injectTargetInfo(config, target, manufacturerId, commitInfo);
-
- setUnifiedConfig(target, bareBoard, config, manufacturerId, unifiedConfig.name, unifiedConfig.download_url, commitInfo.date);
-
- // cache it for later
- let newStorageObj = {};
- newStorageObj[storageTag] = {
- unifiedTarget: self.unifiedTarget,
- targetId: targetId,
- lastUpdate: checkTime,
- };
- SessionStorage.set(newStorageObj);
-
- populateBuilds(builds, target, manufacturerId, duplicateName, TABS.firmware_flasher.releases[bareBoard], processNext);
- });
- } else {
- failLoading(unifiedConfig.download_url);
- }
- }).fail(xhr => {
- failLoading(unifiedConfig.download_url);
- });
- } else {
- console.log('We have the config cached for', targetId);
- const unifiedTarget = storageObj.unifiedTarget;
-
- const bareBoard = grabBuildNameFromConfig(unifiedTarget.config);
- TABS.firmware_flasher.bareBoard = bareBoard;
-
- if (target === bareBoard) {
- self.unifiedTarget = {};
- } else {
- self.unifiedTarget = unifiedTarget;
- }
-
- populateBuilds(builds, target, manufacturerId, duplicateName, TABS.firmware_flasher.releases[bareBoard], processNext);
- }
- };
-
- processManufacturer(0);
- } else {
- self.unifiedTarget = {};
- finishPopulatingBuilds();
- }
+ self.releaseLoader.loadTargetReleases(target, (data) => populateReleases(versions_e, data));
}
}
});
- function failLoading(downloadUrl) {
- //TODO error, populate nothing?
- self.unifiedTarget = {};
- self.isConfigLocal = false;
-
- GUI.log(i18n.getMessage('firmwareFlasherFailedToLoadUnifiedConfig', { remote_file: downloadUrl }));
- }
-
- function flashingMessageLocal(fileName) {
- // used by the a.load_file hook, evaluate the loaded information, and enable flashing if suitable
- if (self.isConfigLocal && !self.parsed_hex) {
- self.flashingMessage(i18n.getMessage('firmwareFlasherLoadedConfig'), self.FLASH_MESSAGE_TYPES.NEUTRAL);
- }
-
- if (self.isConfigLocal && self.parsed_hex && !self.localFirmwareLoaded) {
- self.enableFlashing(true);
- self.flashingMessage(i18n.getMessage('firmwareFlasherFirmwareLocalLoaded', { filename: fileName, bytes: self.parsed_hex.bytes_total }), self.FLASH_MESSAGE_TYPES.NEUTRAL);
- }
-
- if (self.localFirmwareLoaded) {
- self.enableFlashing(true);
- self.flashingMessage(i18n.getMessage('firmwareFlasherFirmwareLocalLoaded', { filename: fileName, bytes: self.parsed_hex.bytes_total }), self.FLASH_MESSAGE_TYPES.NEUTRAL);
- }
- }
-
- function cleanUnifiedConfigFile(input) {
- let output = [];
- let inComment = false;
- for (let i=0; i < input.length; i++) {
- if (input.charAt(i) === "\n" || input.charAt(i) === "\r") {
- inComment = false;
- }
- if (input.charAt(i) === "#") {
- inComment = true;
- }
- if (!inComment && input.charCodeAt(i) > 255) {
- self.flashingMessage(i18n.getMessage('firmwareFlasherConfigCorrupted'), self.FLASH_MESSAGE_TYPES.INVALID);
- GUI.log(i18n.getMessage('firmwareFlasherConfigCorruptedLogMessage'));
- return null;
- }
- if (input.charCodeAt(i) > 255) {
- output.push('_');
- } else {
- output.push(input.charAt(i));
- }
- }
- return output.join('');
- }
-
const portPickerElement = $('div#port-picker #port');
function flashFirmware(firmware) {
const options = {};
@@ -770,7 +383,7 @@ firmware_flasher.initialize = function (callback) {
baud = parseInt($('#flash_manual_baud_rate').val());
}
- analytics.sendEvent(analytics.EVENT_CATEGORIES.FLASHING, 'Flashing', self.unifiedTarget.fileName || null);
+ analytics.sendEvent(analytics.EVENT_CATEGORIES.FLASHING, 'Flashing', self.fileName || null);
STM32.connect(port, baud, firmware, options);
} else {
@@ -778,7 +391,7 @@ firmware_flasher.initialize = function (callback) {
GUI.log(i18n.getMessage('firmwareFlasherNoValidPort'));
}
} else {
- analytics.sendEvent(analytics.EVENT_CATEGORIES.FLASHING, 'Flashing', self.unifiedTarget.fileName || null);
+ analytics.sendEvent(analytics.EVENT_CATEGORIES.FLASHING, 'Flashing', self.fileName || null);
STM32DFU.connect(usbDevices, firmware, options);
}
@@ -859,7 +472,7 @@ firmware_flasher.initialize = function (callback) {
GUI.log(i18n.getMessage('firmwareFlasherDetectBoardQuery'));
- const isLoaded = self.releases ? Object.keys(self.releases).length > 0 : false;
+ const isLoaded = self.targets ? Object.keys(self.targets).length > 0 : false;
if (isLoaded) {
if (!(serial.connected || serial.connectionId)) {
@@ -981,7 +594,7 @@ firmware_flasher.initialize = function (callback) {
accepts: [
{
description: 'target files',
- extensions: ['hex', 'config'],
+ extensions: ['hex'],
},
],
}, function (fileEntry) {
@@ -991,6 +604,7 @@ firmware_flasher.initialize = function (callback) {
// hide github info (if it exists)
$('div.git_info').slideUp();
+ $('div.build_configuration').slideUp();
chrome.fileSystem.getDisplayPath(fileEntry, function (path) {
console.log('Loading file from:', path);
@@ -1006,29 +620,20 @@ firmware_flasher.initialize = function (callback) {
if (file.name.split('.').pop() === "hex") {
self.intel_hex = e.target.result;
- parse_hex(self.intel_hex, function (data) {
+ parseHex(self.intel_hex, function (data) {
self.parsed_hex = data;
if (self.parsed_hex) {
analytics.setFirmwareData(analytics.DATA.FIRMWARE_SIZE, self.parsed_hex.bytes_total);
self.localFirmwareLoaded = true;
- flashingMessageLocal(file.name);
+ showLoadedHex(file.name);
} else {
self.flashingMessage(i18n.getMessage('firmwareFlasherHexCorrupted'), self.FLASH_MESSAGE_TYPES.INVALID);
}
});
} else {
- clearBufferedFirmware();
-
- let config = cleanUnifiedConfigFile(e.target.result);
- if (config !== null) {
- config = self.injectTargetInfo(config, file.name, 'UNKN', { commitHash: 'unknown', date: file.lastModifiedDate.toISOString() });
- self.unifiedTarget.config = config;
- self.unifiedTarget.fileName = file.name;
- self.isConfigLocal = true;
- flashingMessageLocal(file.name);
- }
+ self.flashingMessage(i18n.getMessage('firmwareFlasherHexCorrupted'), self.FLASH_MESSAGE_TYPES.INVALID);
}
}
};
@@ -1042,7 +647,8 @@ firmware_flasher.initialize = function (callback) {
/**
* Lock / Unlock the firmware download button according to the firmware selection dropdown.
*/
- $('select[name="firmware_version"]').change(function(evt){
+ $('select[name="firmware_version"]').change(function(evt) {
+ $('div.build_configuration').slideUp();
$('div.release_info').slideUp();
if (!self.localFirmwareLoaded) {
@@ -1056,26 +662,35 @@ firmware_flasher.initialize = function (callback) {
}
}
- let release = $("option:selected", evt.target).data("summary");
- let isCached = FirmwareCache.has(release);
- if (evt.target.value === "0" || isCached) {
- if (isCached) {
- analytics.setFirmwareData(analytics.DATA.FIRMWARE_SOURCE, 'cache');
+ const release = $("option:selected", evt.target).val();
+ const target = $('select[name="board"] option:selected').val();
- FirmwareCache.get(release, cached => {
- analytics.setFirmwareData(analytics.DATA.FIRMWARE_NAME, release.file);
- console.info("Release found in cache:", release.file);
+ function onTargetDetail(summary) {
+ self.summary = summary;
- self.developmentFirmwareLoaded = buildTypesToShow[$('select[name="build_type"]').val()].tag === 'firmwareFlasherOptionLabelBuildTypeDevelopment';
+ if (summary.cloudBuild === true) {
+ $('div.build_configuration').slideDown();
- onLoadSuccess(cached.hexdata, release);
+ const expertMode = $('.tab-firmware_flasher input.expert_mode').is(':checked');
+ if (!expertMode) {
+ $('div.commitSelection').hide();
+ return;
+ }
+ $('div.commitSelection').show();
+
+ self.releaseLoader.loadCommits(summary.release, (commits) => {
+ const select_e = $('select[name="commits"]');
+ select_e.empty();
+ commits.forEach((commit) => {
+ select_e.append($(`<option value='${commit.sha}'>${commit.message}</option>`));
+ });
});
}
- $("a.load_remote_file").addClass('disabled');
- }
- else {
+
$("a.load_remote_file").removeClass('disabled');
}
+
+ self.releaseLoader.loadTarget(target, release, onTargetDetail);
});
$('a.load_remote_file').click(function (evt) {
@@ -1090,29 +705,109 @@ firmware_flasher.initialize = function (callback) {
return;
}
- function failed_to_load() {
+ function onLoadFailed() {
$('span.progressLabel').attr('i18n','firmwareFlasherFailedToLoadOnlineFirmware').removeClass('i18n-replaced');
$("a.load_remote_file").removeClass('disabled');
$("a.load_remote_file").text(i18n.getMessage('firmwareFlasherButtonLoadOnline'));
i18n.localizePage();
}
- const summary = $('select[name="firmware_version"] option:selected').data('summary');
- if (summary) { // undefined while list is loading or while running offline
- if (self.isConfigLocal && FirmwareCache.has(summary)) {
- // Load the .hex from Cache if available when the user is providing their own config.
- analytics.setFirmwareData(analytics.DATA.FIRMWARE_SOURCE, 'cache');
- FirmwareCache.get(summary, cached => {
- analytics.setFirmwareData(analytics.DATA.FIRMWARE_NAME, summary.file);
- console.info("Release found in cache:", summary.file);
- onLoadSuccess(cached.hexdata, summary);
- });
- return;
+ function updateStatus(status, key) {
+ if (status === 'success' || status === 'fail') {
+ $('div.release_info #cloudTargetLog').text('Build Log').prop('href', `https://build.betaflight.com/api/builds/${key}/log`);
+ }
+ $('div.release_info #cloudTargetStatus').text(status);
+ }
+
+ function requestCloudBuild(summary) {
+ let request = {
+ target: summary.target,
+ release: summary.release,
+ radioProtocols: [],
+ telemetryProtocols: [],
+ motorProtocols: [],
+ options: [],
+ };
+
+ $('select[name="radioProtocols"] option:selected').each(function () {
+ request.radioProtocols.push($(this).val());
+ });
+
+ $('select[name="telemetryProtocols"] option:selected').each(function () {
+ request.telemetryProtocols.push($(this).val());
+ });
+
+ $('select[name="options"] option:selected').each(function () {
+ request.options.push($(this).val());
+ });
+
+ $('select[name="motorProtocols"] option:selected').each(function () {
+ request.motorProtocols.push($(this).val());
+ });
+
+ if (summary.releaseType === "Unstable") {
+ request.commit = $('select[name="commits"] option:selected').val();
}
- analytics.setFirmwareData(analytics.DATA.FIRMWARE_NAME, summary.file);
+
+ self.releaseLoader.requestBuild(request, (info) => {
+ console.info("Build requested:", info);
+
+ analytics.setFirmwareData(analytics.DATA.FIRMWARE_NAME, info.file);
+
+ let retries = 0;
+ self.releaseLoader.requestBuildStatus(info.key, (status) => {
+ if (status.status !== "queued") {
+ updateStatus(status.status, info.key);
+ // will be cached already, no need to wait.
+ if (status.status === 'success') {
+ self.releaseLoader.loadTargetHex(info.url, (hex) => onLoadSuccess(hex, info.file), onLoadFailed);
+ } else {
+ onLoadFailed();
+ }
+ return;
+ }
+
+ const timer = setInterval(() => {
+ self.releaseLoader.requestBuildStatus(info.key, (status) => {
+ if (status.status !== 'queued' || retries > 8) {
+ updateStatus(status.status, info.key);
+ clearInterval(timer);
+ if (status.status === 'success') {
+ self.releaseLoader.loadTargetHex(info.url, (hex) => onLoadSuccess(hex, info.file), onLoadFailed);
+ } else {
+ onLoadFailed();
+ }
+ return;
+ }
+ updateStatus(`${status.status} (${retries})`, info.key);
+ retries = retries + 1;
+ });
+ }, 5000);
+ });
+ }, onLoadFailed);
+ }
+
+ function requestLegacyBuild(summary) {
+ const fileName = summary.file;
+
+ analytics.setFirmwareData(analytics.DATA.FIRMWARE_NAME, fileName);
+ self.releaseLoader.loadTargetHex(summary.url, (hex) => onLoadSuccess(hex, fileName), onLoadFailed);
+ }
+
+ const target = $('select[name="board"] option:selected').val();
+ const release = $('select[name="firmware_version"] option:selected').val();
+
+ if (self.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, onLoadSuccess).fail(failed_to_load);
+
+ showReleaseNotes(self.summary);
+
+ if (self.summary.cloudBuild === true) {
+ self.releaseLoader.loadTarget(target, release, requestCloudBuild, onLoadFailed);
+ } else {
+ self.releaseLoader.loadTarget(target, release, requestLegacyBuild, onLoadFailed);
+ }
} else {
$('span.progressLabel').attr('i18n','firmwareFlasherFailedToLoadOnlineFirmware').removeClass('i18n-replaced');
i18n.localizePage();
@@ -1231,18 +926,6 @@ firmware_flasher.initialize = function (callback) {
if (!GUI.connect_lock) { // button disabled while flashing is in progress
if (self.parsed_hex) {
try {
- if (self.unifiedTarget.config && !self.parsed_hex.configInserted) {
- const configInserter = new ConfigInserter();
-
- if (configInserter.insertConfig(self.parsed_hex, self.unifiedTarget.config)) {
- self.parsed_hex.configInserted = true;
- } else {
- console.log('Firmware does not support custom defaults.');
-
- self.unifiedTarget = {};
- }
- }
-
flashFirmware(self.parsed_hex);
} catch (e) {
console.log(`Flashing failed: ${e.message}`);
@@ -1347,14 +1030,13 @@ firmware_flasher.initialize = function (callback) {
GUI.content_ready(callback);
}
- self.jenkinsLoader.loadJobs('Firmware', () => {
+ self.releaseLoader.loadTargets(() => {
$('#content').load("./tabs/firmware_flasher.html", onDocumentLoad);
});
};
firmware_flasher.cleanup = function (callback) {
PortHandler.flush_callbacks();
- FirmwareCache.unload();
// unbind "global" events
$(document).unbind('keypress');
diff --git a/src/main.html b/src/main.html
index 91824c53..1a9b3cfe 100644
--- a/src/main.html
+++ b/src/main.html
@@ -110,7 +110,7 @@
<script type="text/javascript" src="./js/Features.js"></script>
<script type="text/javascript" src="./js/Beepers.js"></script>
<script type="text/javascript" src="./js/release_checker.js"></script>
- <script type="text/javascript" src="./js/jenkins_loader.js"></script>
+ <script type="text/javascript" src="./js/release_loader.js"></script>
<script type="text/javascript" src="./js/Analytics.js"></script>
<script type="text/javascript" src="./js/GitHubApi.js"></script>
<script type="module" src="./js/main.js"></script>
@@ -127,12 +127,10 @@
<script type="text/javascript" src="./tabs/presets/SourcesDialog/SourcesDialog.js"></script>
<script type="text/javascript" src="./tabs/presets/SourcesDialog/SourcePanel.js"></script>
<script type="text/javascript" src="./tabs/presets/SourcesDialog/PresetSource.js"></script>
- <script type="text/javascript" src="./js/FirmwareCache.js"></script>
<script type="text/javascript" src="./js/LogoManager.js"></script>
<script type="text/javascript" src="./node_modules/jquery-textcomplete/dist/jquery.textcomplete.min.js"></script>
<script type="text/javascript" src="./js/CliAutoComplete.js"></script>
<script type="text/javascript" src="./js/DarkTheme.js"></script>
- <script type="text/javascript" src="./js/ConfigInserter.js"></script>
<script type="text/javascript" src="./js/TuningSliders.js"></script>
<script type="text/javascript" src="./js/phones_ui.js"></script>
<script type="text/javascript" src="./node_modules/jquery-touchswipe/jquery.touchSwipe.min.js"></script>
diff --git a/src/tabs/firmware_flasher.html b/src/tabs/firmware_flasher.html
index 578ae3cd..96fdc7ad 100644
--- a/src/tabs/firmware_flasher.html
+++ b/src/tabs/firmware_flasher.html
@@ -1,68 +1,104 @@
<div class="tab-firmware_flasher toolbar_fixed_bottom">
<div class="content_wrapper">
- <div class="options gui_box">
- <div class="spacer">
+ <div class="options gui_box" style="float: left; width: 460px; ">
+ <div class="spacer" style="margin-bottom: 10px;">
+ <div class="margin-bottom">
<table class="cf_table" style="margin-top: 10px;">
<tr class="option">
- <td><label> <input class="show_development_releases toggle" type="checkbox" /> <span
- i18n="firmwareFlasherShowDevelopmentReleases"></span>
- </label></td>
- <td><span class="description" i18n="firmwareFlasherShowDevelopmentReleasesDescription"></span></td>
+ <td>
+ <label>
+ <input class="show_development_releases toggle" type="checkbox" />
+ <span i18n="firmwareFlasherShowDevelopmentReleases"></span>
+ </label>
+ <div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherShowDevelopmentReleasesDescription"></div>
+ </td>
+ <td>
+ </td>
</tr>
<tr class="expert_mode option">
- <td><label><input class="expert_mode toggle" type="checkbox" /><span i18n="expertMode"></span>
- </label></td>
- <td><span class="description" i18n="expertModeDescription"></span></td>
+ <td>
+ <label>
+ <input class="expert_mode toggle" type="checkbox" />
+ <span i18n="expertMode"></span>
+ </label>
+ <div class="helpicon cf_tip_wide" i18n_title="expertModeDescription"></div>
+ </td>
+ <td>
+ </td>
</tr>
<tr class="build_type">
<td>
<select name="build_type">
<!-- options generated at runtime -->
</select>
+ <div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherOnlineSelectBuildType"></div>
+ </td>
+ <td>
</td>
- <td><span class="description" i18n="firmwareFlasherOnlineSelectBuildType"></span></td>
</tr>
<tr>
- <td class="board-select"><select name="board">
+ <td class="board-select">
+ <select name="board">
<option value="0" i18n="firmwareFlasherOptionLoading">Loading ...</option>
- </select></td>
+ </select>
+ <div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherOnlineSelectBoardDescription"></div>
+ </td>
<td class="board-description">
<div class="btn default_btn">
<a class="detect-board disabled" href="#" i18n="firmwareFlasherDetectBoardButton"></a>
</div>
- <span class="description" i18n="firmwareFlasherOnlineSelectBoardDescription"></span>
<div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherOnlineSelectBoardHint"></div>
- <div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherDetectBoardDescriptionHint"></div>
</td>
</tr>
<tr>
- <td><select name="firmware_version">
+ <td>
+ <select name="firmware_version">
<option value="0" i18n="firmwareFlasherOptionLoading">Loading ...</option>
- </select></td>
- <td><span class="description" i18n="firmwareFlasherOnlineSelectFirmwareVersionDescription"></span></td>
+ </select>
+ <div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherOnlineSelectFirmwareVersionDescription"></div>
+ </td>
+ <td>
+ </td>
</tr>
<tr>
- <td><label> <input class="updating toggle" type="checkbox" /> <span
- i18n="firmwareFlasherNoReboot"></span>
- </label></td>
- <td><span class="description" i18n="firmwareFlasherNoRebootDescription"></span></td>
+ <td>
+ <label>
+ <input class="updating toggle" type="checkbox" />
+ <span i18n="firmwareFlasherNoReboot"></span>
+ </label>
+ <div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherNoRebootDescription"></div>
+ </td>
+ <td>
+ </td>
</tr>
<tr class="option flash_on_connect_wrapper">
- <td><label> <input class="flash_on_connect toggle" type="checkbox" /> <span
- i18n="firmwareFlasherFlashOnConnect"></span></label></td>
-
- <td><span class="description" i18n="firmwareFlasherFlashOnConnectDescription"></span></td>
+ <td>
+ <label>
+ <input class="flash_on_connect toggle" type="checkbox" />
+ <span i18n="firmwareFlasherFlashOnConnect"></span>
+ </label>
+ <div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherFlashOnConnectDescription"></div>
+ </td>
+ <td>
+ </td>
</tr>
<tr class="option">
- <td><label> <input class="erase_chip toggle" type="checkbox" /> <span
- i18n="firmwareFlasherFullChipErase"></span>
- </label></td>
- <td><span class="description" i18n="firmwareFlasherFullChipEraseDescription"></span></td>
+ <td>
+ <label>
+ <input class="erase_chip toggle" type="checkbox" />
+ <span i18n="firmwareFlasherFullChipErase"></span>
+ </label>
+ <div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherFullChipEraseDescription"></div>
+ </td>
+ <td>
+ </td>
</tr>
<tr class="option manual_baud_rate noboarder">
- <td><label> <input class="flash_manual_baud toggle" type="checkbox" /> <span
- i18n="firmwareFlasherManualBaud"></span> <select id="flash_manual_baud_rate"
- i18n_title="firmwareFlasherBaudRate">
+ <td>
+ <label>
+ <input class="flash_manual_baud toggle" type="checkbox" />
+ <span i18n="firmwareFlasherManualBaud"></span>
+ <select id="flash_manual_baud_rate" i18n_title="firmwareFlasherBaudRate">
<option value="921600">921600</option>
<option value="460800">460800</option>
<option value="256000" selected="selected">256000</option>
@@ -72,27 +108,101 @@
<option value="38400">38400</option>
<option value="28800">28800</option>
<option value="19200">19200</option>
- </select>
- </label></td>
- <td><span class="description" i18n="firmwareFlasherManualBaudDescription"></span></td>
+ </select>
+ </label>
+ <div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherManualBaudDescription"></div>
+ </td>
+ <td>
+ </td>
</tr>
</table>
+ </div>
+
+ </div>
+ </div>
+ <div class="gui_box gui_warning" style="max-width: calc(100% - 470px); float: right;">
+ <div class="gui_box_titlebar">
+ <div class="spacer_box_title" style="text-align: center;"
+ i18n="warningTitle">
+ </div>
+ </div>
+ <div class="spacer" style="margin-bottom: 10px;">
+ <p i18n="firmwareFlasherWarningText"></p>
+ <br />
+ <p i18n="firmwareFlasherTargetWarning"></p>
</div>
</div>
+
<div class="clear-both"></div>
<div class="git_info">
<div class="title" i18n="firmwareFlasherGithubInfoHead"></div>
<p>
- <strong i18n="firmwareFlasherHash"></strong> <a i18n_title="firmwareFlasherUrl" class="hash" href="#"
- target="_blank"></a><br /> <strong i18n="firmwareFlasherCommiter"></strong> <span class="committer"></span><br />
- <strong i18n="firmwareFlasherDate"></strong> <span class="date"></span><br /> <strong
- i18n="firmwareFlasherMessage"></strong> <span class="message"></span>
+ <strong i18n="firmwareFlasherHash"></strong>
+ <a i18n_title="firmwareFlasherUrl" class="hash" href="#" target="_blank"></a><br />
+ <strong i18n="firmwareFlasherCommiter"></strong> <span class="committer"></span><br />
+ <strong i18n="firmwareFlasherDate"></strong> <span class="date"></span><br />
+ <strong i18n="firmwareFlasherMessage"></strong> <span class="message"></span>
</p>
</div>
+
+ <div class="build_configuration gui_box">
+ <div class="darkgrey_box gui_box_titlebar">
+ <div class="spacer_box_title" style="text-align: center;" i18n="firmwareFlasherBuildConfigurationHead">
+ </div>
+ </div>
+ <div class="spacer" style="margin-bottom: 10px;">
+ <div class="margin-bottom">
+ <div style="width: 49%; float: left;">
+ <strong i18n="firmwareFlasherBuildRadioProtocols"></strong>
+ <div id="radioProtocolInfo">
+ <select id="radioProtocols" name="radioProtocols" multiple="multiple" class="select2" style="width: 95%; color: #424242">
+ </select>
+ </div>
+ </div>
+ <div style="width: 49%; float: right;">
+ <strong i18n="firmwareFlasherBuildTelemetryProtocols"></strong>
+ <div id="telemetryProtocolInfo">
+ <select id="telemetryProtocols" name="telemetryProtocols" multiple="multiple" class="select2" style="width: 95%; color: #424242">
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="spacer" style="margin-bottom: 10px;">
+ <div class="margin-bottom">
+ <div style="width: 49%; float: left;">
+ <strong i18n="firmwareFlasherBuildOptions"></strong>
+ <div id="optionsInfo">
+ <select id="options" name="options" multiple="multiple" class="select2" style="width: 95%; color: #424242">
+ </select>
+ </div>
+ </div>
+ <div style="width: 49%; float: right;">
+ <strong i18n="firmwareFlasherBuildMotorProtocols"></strong>
+ <div id="motorProtocolInfo">
+ <select id="motorProtocols" name="motorProtocols" multiple="multiple" class="select2" style="width: 95%; color: #424242">
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="commitSelection spacer" style="margin-bottom: 10px;">
+ <div class="margin-bottom">
+ <div style="width: 49%; float: left;">
+ <strong i18n="firmwareFlasherBranch"></strong>
+ <div id="branchInfo">
+ <select id="commits" name="commits" class="select2" style="width: 95%; color: #424242">
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
<div class="release_info gui_box">
<div class="darkgrey_box gui_box_titlebar">
- <div class="spacer_box_title" style="text-align: center;"
- i18n="firmwareFlasherReleaseSummaryHead"></div>
+ <div class="spacer_box_title" style="text-align: center;" i18n="firmwareFlasherReleaseSummaryHead">
+ </div>
</div>
<div class="spacer" style="margin-bottom: 10px;">
<div class="margin-bottom">
@@ -107,37 +217,26 @@
<strong i18n="firmwareFlasherReleaseVersion"></strong>
<a i18n_title="firmwareFlasherReleaseVersionUrl" class="name" href="#" target="_blank"></a>
<br />
- <strong i18n="firmwareFlasherReleaseFile"></strong>
- <a i18n_title="firmwareFlasherReleaseFileUrl" class="file" href="#" target="_blank"></a>
+ <strong i18n="firmwareFlasherReleaseMCU"></strong>
+ <span id="targetMCU"></span>
<br />
<strong i18n="firmwareFlasherReleaseDate"></strong>
<span class="date"></span>
<br />
</div>
- <div class="margin-bottom" id="unifiedTargetInfo">
- <strong i18n="firmwareFlasherUnifiedTargetName"></strong>
- <a i18n_title="firmwareFlasherUnifiedTargetFileUrl" id="unifiedTargetFile" href="#" target="_blank"></a>
+ <div class="margin-bottom" id="cloudTargetInfo">
+ <strong i18n="firmwareFlasherCloudBuildDetails"></strong>
+ <a i18n_title="firmwareFlasherCloudBuildLogUrl" id="cloudTargetLog" href="#" target="_blank"></a>
<br />
- <strong i18n="firmwareFlasherUnifiedTargetDate"></strong>
- <span id="unifiedTargetDate"></span>
+ <strong i18n="firmwareFlasherCloudBuildStatus"></strong>
+ <span id="cloudTargetStatus"></span>
<br />
</div>
<strong i18n="firmwareFlasherReleaseNotes"></strong>
<div class=notes></div>
</div>
</div>
- <div class="gui_box gui_warning">
- <div class="gui_box_titlebar">
- <div class="spacer_box_title" style="text-align: center;"
- i18n="warningTitle">
- </div>
- </div>
- <div class="spacer" style="margin-bottom: 10px;">
- <p i18n="firmwareFlasherWarningText"></p>
- <br />
- <p i18n="firmwareFlasherTargetWarning"></p>
- </div>
- </div>
+
<div class="gui_box gui_note">
<div class="gui_box_titlebar">
<div class="spacer_box_title" style="text-align: center;"