diff options
author | blckmn <blackman@xtra.com.au> | 2022-10-30 10:50:53 +0300 |
---|---|---|
committer | blckmn <blackman@xtra.com.au> | 2022-10-30 10:50:53 +0300 |
commit | 6aae7957813e6469bb87849245eca7582e1c639c (patch) | |
tree | 761beb9f4217064f57b87568b0f9942c887c78d3 | |
parent | a49a6b98ba926bd08298f640ae5b1617a665a36c (diff) |
Setup for the cloud build implementation
-rw-r--r-- | locales/en/messages.json | 35 | ||||
-rw-r--r-- | src/css/tabs/firmware_flasher.less | 8 | ||||
-rw-r--r-- | src/js/ConfigInserter.js | 107 | ||||
-rw-r--r-- | src/js/FirmwareCache.js | 242 | ||||
-rw-r--r-- | src/js/jenkins_loader.js | 160 | ||||
-rw-r--r-- | src/js/main.js | 6 | ||||
-rw-r--r-- | src/js/release_loader.js | 129 | ||||
-rw-r--r-- | src/js/tabs/firmware_flasher.js | 792 | ||||
-rw-r--r-- | src/main.html | 4 | ||||
-rw-r--r-- | src/tabs/firmware_flasher.html | 219 |
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;" |