1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-15 20:35:23 +03:00

Setup for the cloud build implementation

This commit is contained in:
blckmn 2022-10-30 18:50:53 +11:00
parent a49a6b98ba
commit 6aae795781
10 changed files with 570 additions and 1142 deletions

View file

@ -3068,7 +3068,9 @@
"sdcardStatusUnknown": { "sdcardStatusUnknown": {
"message": "Unknown state $1" "message": "Unknown state $1"
}, },
"firmwareFlasherBranch": {
"message": "Select commit"
},
"firmwareFlasherReleaseSummaryHead": { "firmwareFlasherReleaseSummaryHead": {
"message": "Release info" "message": "Release info"
}, },
@ -3090,17 +3092,17 @@
"firmwareFlasherReleaseTarget": { "firmwareFlasherReleaseTarget": {
"message": "Target:" "message": "Target:"
}, },
"firmwareFlasherReleaseFile": { "firmwareFlasherReleaseMCU": {
"message": "Binary:" "message": "MCU:"
}, },
"firmwareFlasherUnifiedTargetName": { "firmwareFlasherCloudBuildDetails": {
"message": "Unified Target:" "message": "Cloud Build Details:"
}, },
"firmwareFlasherUnifiedTargetFileUrl": { "firmwareFlasherCloudBuildLogUrl": {
"message": "Show config." "message": "Show Log."
}, },
"firmwareFlasherUnifiedTargetDate": { "firmwareFlasherCloudBuildStatus": {
"message": "Date:" "message": "Status:"
}, },
"firmwareFlasherReleaseFileUrl": { "firmwareFlasherReleaseFileUrl": {
"message": "Download manually." "message": "Download manually."
@ -6674,5 +6676,20 @@
"presetsReviewOptionsWarning": { "presetsReviewOptionsWarning": {
"message": "Please, review the list of options before picking this preset.", "message": "Please, review the list of options before picking this preset.",
"description": "Dialog text to prompt user to review options for the 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"
} }
} }

View file

@ -123,7 +123,13 @@
} }
} }
} }
.release_info { .build_configuration {
.select2-selection__choice {
margin: auto;
color: #3f4241;
}
}
.release_info, .build_configuration {
display: none; display: none;
.title { .title {
line-height: 20px; line-height: 20px;

View file

@ -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;
};

View file

@ -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,
};
})();

View file

@ -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);
};

View file

@ -638,6 +638,12 @@ function notifyOutdatedVersion(releaseData) {
if (result.checkForConfiguratorUnstableVersions) { if (result.checkForConfiguratorUnstableVersions) {
showUnstableReleases = true; showUnstableReleases = true;
} }
if (releaseData === undefined) {
console.log('No releaseData');
return false;
}
const versions = releaseData.filter(function (version) { const versions = releaseData.filter(function (version) {
const semVerVersion = semver.parse(version.tag_name); const semVerVersion = semver.parse(version.tag_name);
if (semVerVersion && (showUnstableReleases || semVerVersion.prerelease.length === 0)) { if (semVerVersion && (showUnstableReleases || semVerVersion.prerelease.length === 0)) {

129
src/js/release_loader.js Normal file
View file

@ -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);
}
}

File diff suppressed because it is too large Load diff

View file

@ -110,7 +110,7 @@
<script type="text/javascript" src="./js/Features.js"></script> <script type="text/javascript" src="./js/Features.js"></script>
<script type="text/javascript" src="./js/Beepers.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/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/Analytics.js"></script>
<script type="text/javascript" src="./js/GitHubApi.js"></script> <script type="text/javascript" src="./js/GitHubApi.js"></script>
<script type="module" src="./js/main.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/SourcesDialog.js"></script>
<script type="text/javascript" src="./tabs/presets/SourcesDialog/SourcePanel.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="./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="./js/LogoManager.js"></script>
<script type="text/javascript" src="./node_modules/jquery-textcomplete/dist/jquery.textcomplete.min.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/CliAutoComplete.js"></script>
<script type="text/javascript" src="./js/DarkTheme.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/TuningSliders.js"></script>
<script type="text/javascript" src="./js/phones_ui.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> <script type="text/javascript" src="./node_modules/jquery-touchswipe/jquery.touchSwipe.min.js"></script>

View file

@ -1,68 +1,104 @@
<div class="tab-firmware_flasher toolbar_fixed_bottom"> <div class="tab-firmware_flasher toolbar_fixed_bottom">
<div class="content_wrapper"> <div class="content_wrapper">
<div class="options gui_box"> <div class="options gui_box" style="float: left; width: 460px; ">
<div class="spacer"> <div class="spacer" style="margin-bottom: 10px;">
<div class="margin-bottom">
<table class="cf_table" style="margin-top: 10px;"> <table class="cf_table" style="margin-top: 10px;">
<tr class="option"> <tr class="option">
<td><label> <input class="show_development_releases toggle" type="checkbox" /> <span <td>
i18n="firmwareFlasherShowDevelopmentReleases"></span> <label>
</label></td> <input class="show_development_releases toggle" type="checkbox" />
<td><span class="description" i18n="firmwareFlasherShowDevelopmentReleasesDescription"></span></td> <span i18n="firmwareFlasherShowDevelopmentReleases"></span>
</label>
<div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherShowDevelopmentReleasesDescription"></div>
</td>
<td>
</td>
</tr> </tr>
<tr class="expert_mode option"> <tr class="expert_mode option">
<td><label><input class="expert_mode toggle" type="checkbox" /><span i18n="expertMode"></span> <td>
</label></td> <label>
<td><span class="description" i18n="expertModeDescription"></span></td> <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>
<tr class="build_type"> <tr class="build_type">
<td> <td>
<select name="build_type"> <select name="build_type">
<!-- options generated at runtime --> <!-- options generated at runtime -->
</select> </select>
<div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherOnlineSelectBuildType"></div>
</td>
<td>
</td> </td>
<td><span class="description" i18n="firmwareFlasherOnlineSelectBuildType"></span></td>
</tr> </tr>
<tr> <tr>
<td class="board-select"><select name="board"> <td class="board-select">
<select name="board">
<option value="0" i18n="firmwareFlasherOptionLoading">Loading ...</option> <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"> <td class="board-description">
<div class="btn default_btn"> <div class="btn default_btn">
<a class="detect-board disabled" href="#" i18n="firmwareFlasherDetectBoardButton"></a> <a class="detect-board disabled" href="#" i18n="firmwareFlasherDetectBoardButton"></a>
</div> </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="firmwareFlasherOnlineSelectBoardHint"></div>
<div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherDetectBoardDescriptionHint"></div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td><select name="firmware_version"> <td>
<select name="firmware_version">
<option value="0" i18n="firmwareFlasherOptionLoading">Loading ...</option> <option value="0" i18n="firmwareFlasherOptionLoading">Loading ...</option>
</select></td> </select>
<td><span class="description" i18n="firmwareFlasherOnlineSelectFirmwareVersionDescription"></span></td> <div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherOnlineSelectFirmwareVersionDescription"></div>
</td>
<td>
</td>
</tr> </tr>
<tr> <tr>
<td><label> <input class="updating toggle" type="checkbox" /> <span <td>
i18n="firmwareFlasherNoReboot"></span> <label>
</label></td> <input class="updating toggle" type="checkbox" />
<td><span class="description" i18n="firmwareFlasherNoRebootDescription"></span></td> <span i18n="firmwareFlasherNoReboot"></span>
</label>
<div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherNoRebootDescription"></div>
</td>
<td>
</td>
</tr> </tr>
<tr class="option flash_on_connect_wrapper"> <tr class="option flash_on_connect_wrapper">
<td><label> <input class="flash_on_connect toggle" type="checkbox" /> <span <td>
i18n="firmwareFlasherFlashOnConnect"></span></label></td> <label>
<input class="flash_on_connect toggle" type="checkbox" />
<td><span class="description" i18n="firmwareFlasherFlashOnConnectDescription"></span></td> <span i18n="firmwareFlasherFlashOnConnect"></span>
</label>
<div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherFlashOnConnectDescription"></div>
</td>
<td>
</td>
</tr> </tr>
<tr class="option"> <tr class="option">
<td><label> <input class="erase_chip toggle" type="checkbox" /> <span <td>
i18n="firmwareFlasherFullChipErase"></span> <label>
</label></td> <input class="erase_chip toggle" type="checkbox" />
<td><span class="description" i18n="firmwareFlasherFullChipEraseDescription"></span></td> <span i18n="firmwareFlasherFullChipErase"></span>
</label>
<div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherFullChipEraseDescription"></div>
</td>
<td>
</td>
</tr> </tr>
<tr class="option manual_baud_rate noboarder"> <tr class="option manual_baud_rate noboarder">
<td><label> <input class="flash_manual_baud toggle" type="checkbox" /> <span <td>
i18n="firmwareFlasherManualBaud"></span> <select id="flash_manual_baud_rate" <label>
i18n_title="firmwareFlasherBaudRate"> <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="921600">921600</option>
<option value="460800">460800</option> <option value="460800">460800</option>
<option value="256000" selected="selected">256000</option> <option value="256000" selected="selected">256000</option>
@ -72,27 +108,101 @@
<option value="38400">38400</option> <option value="38400">38400</option>
<option value="28800">28800</option> <option value="28800">28800</option>
<option value="19200">19200</option> <option value="19200">19200</option>
</select> </select>
</label></td> </label>
<td><span class="description" i18n="firmwareFlasherManualBaudDescription"></span></td> <div class="helpicon cf_tip_wide" i18n_title="firmwareFlasherManualBaudDescription"></div>
</td>
<td>
</td>
</tr> </tr>
</table> </table>
</div>
</div> </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="clear-both"></div>
<div class="git_info"> <div class="git_info">
<div class="title" i18n="firmwareFlasherGithubInfoHead"></div> <div class="title" i18n="firmwareFlasherGithubInfoHead"></div>
<p> <p>
<strong i18n="firmwareFlasherHash"></strong> <a i18n_title="firmwareFlasherUrl" class="hash" href="#" <strong i18n="firmwareFlasherHash"></strong>
target="_blank"></a><br /> <strong i18n="firmwareFlasherCommiter"></strong> <span class="committer"></span><br /> <a i18n_title="firmwareFlasherUrl" class="hash" href="#" target="_blank"></a><br />
<strong i18n="firmwareFlasherDate"></strong> <span class="date"></span><br /> <strong <strong i18n="firmwareFlasherCommiter"></strong> <span class="committer"></span><br />
i18n="firmwareFlasherMessage"></strong> <span class="message"></span> <strong i18n="firmwareFlasherDate"></strong> <span class="date"></span><br />
<strong i18n="firmwareFlasherMessage"></strong> <span class="message"></span>
</p> </p>
</div> </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="release_info gui_box">
<div class="darkgrey_box gui_box_titlebar"> <div class="darkgrey_box gui_box_titlebar">
<div class="spacer_box_title" style="text-align: center;" <div class="spacer_box_title" style="text-align: center;" i18n="firmwareFlasherReleaseSummaryHead">
i18n="firmwareFlasherReleaseSummaryHead"></div> </div>
</div> </div>
<div class="spacer" style="margin-bottom: 10px;"> <div class="spacer" style="margin-bottom: 10px;">
<div class="margin-bottom"> <div class="margin-bottom">
@ -107,37 +217,26 @@
<strong i18n="firmwareFlasherReleaseVersion"></strong> <strong i18n="firmwareFlasherReleaseVersion"></strong>
<a i18n_title="firmwareFlasherReleaseVersionUrl" class="name" href="#" target="_blank"></a> <a i18n_title="firmwareFlasherReleaseVersionUrl" class="name" href="#" target="_blank"></a>
<br /> <br />
<strong i18n="firmwareFlasherReleaseFile"></strong> <strong i18n="firmwareFlasherReleaseMCU"></strong>
<a i18n_title="firmwareFlasherReleaseFileUrl" class="file" href="#" target="_blank"></a> <span id="targetMCU"></span>
<br /> <br />
<strong i18n="firmwareFlasherReleaseDate"></strong> <strong i18n="firmwareFlasherReleaseDate"></strong>
<span class="date"></span> <span class="date"></span>
<br /> <br />
</div> </div>
<div class="margin-bottom" id="unifiedTargetInfo"> <div class="margin-bottom" id="cloudTargetInfo">
<strong i18n="firmwareFlasherUnifiedTargetName"></strong> <strong i18n="firmwareFlasherCloudBuildDetails"></strong>
<a i18n_title="firmwareFlasherUnifiedTargetFileUrl" id="unifiedTargetFile" href="#" target="_blank"></a> <a i18n_title="firmwareFlasherCloudBuildLogUrl" id="cloudTargetLog" href="#" target="_blank"></a>
<br /> <br />
<strong i18n="firmwareFlasherUnifiedTargetDate"></strong> <strong i18n="firmwareFlasherCloudBuildStatus"></strong>
<span id="unifiedTargetDate"></span> <span id="cloudTargetStatus"></span>
<br /> <br />
</div> </div>
<strong i18n="firmwareFlasherReleaseNotes"></strong> <strong i18n="firmwareFlasherReleaseNotes"></strong>
<div class=notes></div> <div class=notes></div>
</div> </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 gui_note">
<div class="gui_box_titlebar"> <div class="gui_box_titlebar">
<div class="spacer_box_title" style="text-align: center;" <div class="spacer_box_title" style="text-align: center;"