Make firmware picker order independent
This commit is contained in:
1 parent
c22da32964
commit
63ab4a1690
4 files changed
+354
-153
No files matched your search
@@ -20,6 +20,8 @@
|
||||
"variant",
|
||||
]);
|
||||
|
||||
const FACET_FIELDS = Object.freeze(FILTER_FIELDS.concat(["install"]));
|
||||
|
||||
const ROLE_LABELS = Object.freeze({
|
||||
companion: "Companion",
|
||||
repeater: "Repeater",
|
||||
@@ -62,11 +64,11 @@
|
||||
});
|
||||
|
||||
const INSTALL_LABELS = Object.freeze({
|
||||
"merged-bin": "USB first install / recovery (.bin)",
|
||||
bin: "Same-profile application update (.bin)",
|
||||
zip: "nRF52 Serial DFU (.zip)",
|
||||
uf2: "UF2 drag-and-drop (.uf2)",
|
||||
hex: "Full wired flash (.hex)",
|
||||
"merged-bin": "Erase & fresh install (merged .bin)",
|
||||
bin: "Update existing install (.bin)",
|
||||
zip: "nRF52 update / Serial DFU (.zip)",
|
||||
uf2: "UF2 install / update (.uf2)",
|
||||
hex: "Erase / recovery flash (.hex)",
|
||||
});
|
||||
|
||||
const INSTALL_ORDER = Object.freeze([
|
||||
@@ -77,6 +79,8 @@
|
||||
"hex",
|
||||
]);
|
||||
|
||||
let pickerInstanceCount = 0;
|
||||
|
||||
function isSafeGithubUrl(value) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
@@ -369,6 +373,32 @@
|
||||
});
|
||||
}
|
||||
|
||||
function profileMatchesFacets(profile, filters, ignoredField) {
|
||||
return FACET_FIELDS.every(function (field) {
|
||||
if (field === ignoredField) return true;
|
||||
const value = filters && filters[field];
|
||||
if (!value) return true;
|
||||
if (field === "install") {
|
||||
return profile.installKinds.includes(value);
|
||||
}
|
||||
return profile[field] === value;
|
||||
});
|
||||
}
|
||||
|
||||
function facetValues(profiles, filters, field) {
|
||||
const compatible = (profiles || []).filter(function (profile) {
|
||||
return profileMatchesFacets(profile, filters, field);
|
||||
});
|
||||
const values = field === "install"
|
||||
? compatible.flatMap(function (profile) {
|
||||
return profile.installKinds;
|
||||
})
|
||||
: compatible.map(function (profile) {
|
||||
return profile[field];
|
||||
});
|
||||
return Array.from(new Set(values.filter(Boolean)));
|
||||
}
|
||||
|
||||
function uniqueValues(profiles, field) {
|
||||
return Array.from(new Set((profiles || []).map(function (profile) {
|
||||
return profile[field];
|
||||
@@ -436,6 +466,7 @@
|
||||
ota: otaOrder,
|
||||
feature: featureOrder,
|
||||
mode: modeOrder,
|
||||
install: INSTALL_ORDER,
|
||||
};
|
||||
const order = orders[field];
|
||||
if (order) {
|
||||
@@ -457,7 +488,7 @@
|
||||
const byKind = {
|
||||
"merged-bin": [
|
||||
"Flash the merged image over a data-capable USB connection.",
|
||||
"Use this path for a first install, recovery, or partition/profile migration.",
|
||||
"Use this erase/fresh-install path for a first install, recovery, or partition/profile migration.",
|
||||
],
|
||||
bin: [
|
||||
"Use this app-only image only when the installed board, role, and partition layout already match.",
|
||||
@@ -515,7 +546,7 @@
|
||||
["OTA", labelFor("ota", profile.ota)],
|
||||
["Feature profile", labelFor("feature", profile.feature)],
|
||||
["Variant", labelFor("variant", profile.variant)],
|
||||
["Install path", labelFor("install", installKind)],
|
||||
["Install operation", labelFor("install", installKind)],
|
||||
["File", asset.name],
|
||||
["Size", formatBytes(asset.size)],
|
||||
["Release", asset.releaseName],
|
||||
@@ -547,30 +578,37 @@
|
||||
|
||||
function initPicker(root) {
|
||||
const repo = root.getAttribute("data-release-repo") || "mikecarper/MeshCore";
|
||||
const form = root.querySelector('[data-role="form"]');
|
||||
const status = root.querySelector('[data-role="status"]');
|
||||
const releaseSetStatus = root.querySelector('[data-role="release-set"]');
|
||||
const result = root.querySelector('[data-role="result"]');
|
||||
const resultList = root.querySelector('[data-role="result-list"]');
|
||||
const missing = root.querySelector('[data-role="missing"]');
|
||||
const installSelect = root.querySelector('[data-field="install"]');
|
||||
const search = root.querySelector('[data-field="asset-search"]');
|
||||
const assetResults = root.querySelector('[data-role="asset-results"]');
|
||||
const selects = {};
|
||||
FILTER_FIELDS.forEach(function (field) {
|
||||
selects[field] = root.querySelector('[data-field="' + field + '"]');
|
||||
const clearButton = form.querySelector('[data-action="clear"]');
|
||||
const controls = {};
|
||||
FACET_FIELDS.forEach(function (field) {
|
||||
controls[field] = form.querySelector('[data-field="' + field + '"]');
|
||||
});
|
||||
let catalog = { releaseSet: null, rows: [], profiles: [] };
|
||||
const filters = {};
|
||||
const groupPrefix = "firmware-picker-" + (++pickerInstanceCount);
|
||||
|
||||
function matchingProfiles(fields) {
|
||||
function matchingProfiles(ignoredField) {
|
||||
return catalog.profiles.filter(function (profile) {
|
||||
return profileMatches(profile, filters, fields);
|
||||
return profileMatchesFacets(profile, filters, ignoredField);
|
||||
});
|
||||
}
|
||||
|
||||
function setOptions(select, field, values, placeholder, selected) {
|
||||
function setSelectOptions(select, field, values, selected) {
|
||||
select.replaceChildren();
|
||||
const blank = createElement("option", placeholder);
|
||||
const placeholders = {
|
||||
hardware: "Any hardware",
|
||||
mode: "Any connection / mode",
|
||||
variant: "Any hardware/profile variant",
|
||||
};
|
||||
const blank = createElement("option", placeholders[field] || "Any");
|
||||
blank.value = "";
|
||||
select.appendChild(blank);
|
||||
values.slice().sort(function (a, b) {
|
||||
@@ -581,93 +619,98 @@
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.disabled = values.length === 0;
|
||||
if (selected && values.includes(selected)) select.value = selected;
|
||||
if (values.length === 1) select.value = values[0];
|
||||
select.value = selected && values.includes(selected) ? selected : "";
|
||||
}
|
||||
|
||||
function rebuildCascade(changedIndex) {
|
||||
if (changedIndex != null && changedIndex >= 0) {
|
||||
FILTER_FIELDS.slice(changedIndex + 1).forEach(function (field) {
|
||||
filters[field] = "";
|
||||
selects[field].value = "";
|
||||
});
|
||||
installSelect.value = "";
|
||||
}
|
||||
|
||||
let priorComplete = true;
|
||||
FILTER_FIELDS.forEach(function (field, index) {
|
||||
const select = selects[field];
|
||||
if (!priorComplete) {
|
||||
setOptions(select, field, [], "Choose earlier options first", "");
|
||||
filters[field] = "";
|
||||
return;
|
||||
}
|
||||
const priorFields = FILTER_FIELDS.slice(0, index);
|
||||
const candidates = matchingProfiles(priorFields);
|
||||
const values = uniqueValues(candidates, field);
|
||||
const previous = filters[field] || select.value;
|
||||
const placeholder = field === "hardware"
|
||||
? "Choose hardware"
|
||||
: "Choose " + field;
|
||||
setOptions(select, field, values, placeholder, previous);
|
||||
filters[field] = select.value;
|
||||
if (!filters[field]) priorComplete = false;
|
||||
});
|
||||
rebuildInstall(priorComplete);
|
||||
render();
|
||||
}
|
||||
|
||||
function rebuildInstall(profileComplete) {
|
||||
if (!profileComplete) {
|
||||
setOptions(
|
||||
installSelect,
|
||||
"install",
|
||||
[],
|
||||
"Choose firmware options first",
|
||||
""
|
||||
);
|
||||
return;
|
||||
}
|
||||
const matches = matchingProfiles(FILTER_FIELDS);
|
||||
const kinds = Array.from(new Set(matches.flatMap(function (profile) {
|
||||
return profile.installKinds;
|
||||
}))).sort(function (a, b) {
|
||||
return INSTALL_ORDER.indexOf(a) - INSTALL_ORDER.indexOf(b);
|
||||
});
|
||||
const previous = installSelect.value;
|
||||
setOptions(
|
||||
installSelect,
|
||||
"install",
|
||||
kinds,
|
||||
"Choose installation method",
|
||||
previous
|
||||
function setRadioOptions(container, field, values, selected) {
|
||||
container.replaceChildren();
|
||||
const options = [{ value: "", label: "Any" }].concat(
|
||||
values.slice().sort(function (a, b) {
|
||||
return optionSort(field, a, b);
|
||||
}).map(function (value) {
|
||||
return { value: value, label: labelFor(field, value) };
|
||||
})
|
||||
);
|
||||
options.forEach(function (option) {
|
||||
const label = createElement("label");
|
||||
label.className = "firmware-picker-radio-option";
|
||||
const input = createElement("input");
|
||||
input.type = "radio";
|
||||
input.name = groupPrefix + "-" + field;
|
||||
input.value = option.value;
|
||||
input.dataset.choiceField = field;
|
||||
input.checked = selected === option.value;
|
||||
label.appendChild(input);
|
||||
label.appendChild(createElement("span", option.label));
|
||||
container.appendChild(label);
|
||||
});
|
||||
const fieldset = container.closest("fieldset");
|
||||
if (fieldset) fieldset.disabled = values.length === 0;
|
||||
}
|
||||
|
||||
function nextMissingField() {
|
||||
return FILTER_FIELDS.find(function (field) {
|
||||
return !filters[field];
|
||||
}) || (!installSelect.value ? "installation method" : "");
|
||||
function setControlOptions(field, values) {
|
||||
const control = controls[field];
|
||||
if (!control) return;
|
||||
if (control.tagName === "SELECT") {
|
||||
setSelectOptions(control, field, values, filters[field] || "");
|
||||
} else {
|
||||
setRadioOptions(control, field, values, filters[field] || "");
|
||||
}
|
||||
}
|
||||
|
||||
function stabilizeSingleChoices() {
|
||||
let changed = true;
|
||||
let passes = 0;
|
||||
while (changed && passes <= FACET_FIELDS.length) {
|
||||
changed = false;
|
||||
passes += 1;
|
||||
FACET_FIELDS.forEach(function (field) {
|
||||
const values = facetValues(catalog.profiles, filters, field);
|
||||
if (filters[field] && !values.includes(filters[field])) {
|
||||
filters[field] = "";
|
||||
changed = true;
|
||||
} else if (!filters[field] && values.length === 1) {
|
||||
filters[field] = values[0];
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function refreshFacets() {
|
||||
stabilizeSingleChoices();
|
||||
FACET_FIELDS.forEach(function (field) {
|
||||
setControlOptions(
|
||||
field,
|
||||
facetValues(catalog.profiles, filters, field)
|
||||
);
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
result.hidden = true;
|
||||
missing.hidden = true;
|
||||
resultList.replaceChildren();
|
||||
const missingField = nextMissingField();
|
||||
const activeFields = FILTER_FIELDS.filter(function (field) {
|
||||
return Boolean(filters[field]);
|
||||
const missingFields = FACET_FIELDS.filter(function (field) {
|
||||
return !filters[field];
|
||||
});
|
||||
const matches = matchingProfiles(activeFields);
|
||||
const matches = matchingProfiles();
|
||||
|
||||
if (missingField) {
|
||||
status.textContent = "Choose " + missingField + ". " +
|
||||
matches.length + " compatible configuration" +
|
||||
(matches.length === 1 ? " remains." : "s remain.");
|
||||
if (missingFields.length) {
|
||||
if (missingFields.length === FACET_FIELDS.length) {
|
||||
status.textContent = "Pick options in any order. " +
|
||||
matches.length + " compatible configurations are available.";
|
||||
} else {
|
||||
status.textContent = matches.length + " compatible configuration" +
|
||||
(matches.length === 1 ? " remains. " : "s remain. ") +
|
||||
"Choose " + missingFields.length + " more option" +
|
||||
(missingFields.length === 1 ? "" : "s") + " in any order.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const installKind = installSelect.value;
|
||||
const installKind = filters.install;
|
||||
const resolved = matches.map(function (profile) {
|
||||
return {
|
||||
profile: profile,
|
||||
@@ -731,13 +774,21 @@
|
||||
assetResults.appendChild(list);
|
||||
}
|
||||
|
||||
FILTER_FIELDS.forEach(function (field, index) {
|
||||
selects[field].addEventListener("change", function () {
|
||||
filters[field] = selects[field].value;
|
||||
rebuildCascade(index);
|
||||
});
|
||||
form.addEventListener("change", function (event) {
|
||||
const target = event.target;
|
||||
const field = target.dataset.choiceField || target.dataset.field;
|
||||
if (!FACET_FIELDS.includes(field)) return;
|
||||
filters[field] = target.value;
|
||||
refreshFacets();
|
||||
});
|
||||
form.addEventListener("reset", function (event) {
|
||||
event.preventDefault();
|
||||
if (!catalog.profiles.length) return;
|
||||
FACET_FIELDS.forEach(function (field) {
|
||||
filters[field] = "";
|
||||
});
|
||||
refreshFacets();
|
||||
});
|
||||
installSelect.addEventListener("change", render);
|
||||
search.addEventListener("input", renderSearch);
|
||||
|
||||
const endpoint = "https://api.github.com/repos/" +
|
||||
@@ -775,9 +826,8 @@
|
||||
link.href = set.url;
|
||||
releaseSetStatus.appendChild(link);
|
||||
}
|
||||
status.textContent =
|
||||
"Catalog loaded. Start by choosing the exact hardware.";
|
||||
rebuildCascade(-1);
|
||||
clearButton.disabled = false;
|
||||
refreshFacets();
|
||||
})
|
||||
.catch(function (error) {
|
||||
status.textContent =
|
||||
@@ -790,6 +840,7 @@
|
||||
|
||||
const api = Object.freeze({
|
||||
FILTER_FIELDS: FILTER_FIELDS,
|
||||
FACET_FIELDS: FACET_FIELDS,
|
||||
ROLE_LABELS: ROLE_LABELS,
|
||||
LOGGING_LABELS: LOGGING_LABELS,
|
||||
OTA_LABELS: OTA_LABELS,
|
||||
@@ -802,6 +853,8 @@
|
||||
parseTargetProfile: parseTargetProfile,
|
||||
buildCatalog: buildCatalog,
|
||||
profileMatches: profileMatches,
|
||||
profileMatchesFacets: profileMatchesFacets,
|
||||
facetValues: facetValues,
|
||||
uniqueValues: uniqueValues,
|
||||
canonicalAsset: canonicalAsset,
|
||||
humanizeHardware: humanizeHardware,
|
||||
|
||||
@@ -32,19 +32,41 @@
|
||||
|
||||
.firmware-picker-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
|
||||
gap: 0.9rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
|
||||
align-items: start;
|
||||
gap: 1rem;
|
||||
margin: 1.2rem 0;
|
||||
}
|
||||
|
||||
.firmware-picker-form label,
|
||||
.firmware-picker-order-note {
|
||||
margin: 1rem 0 0;
|
||||
color: var(--md-default-fg-color--light);
|
||||
}
|
||||
|
||||
.firmware-picker-control {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.firmware-picker-select-control > label,
|
||||
.firmware-picker-radio-control > legend,
|
||||
.firmware-asset-browser label {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.firmware-picker-form select,
|
||||
.firmware-picker-radio-control {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.firmware-picker-radio-control > legend {
|
||||
margin-bottom: 0.35rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.firmware-picker-select-control select,
|
||||
.firmware-asset-browser input {
|
||||
width: 100%;
|
||||
min-height: 2.7rem;
|
||||
@@ -56,6 +78,75 @@
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.firmware-picker-radio-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.firmware-picker-radio-option {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.firmware-picker-radio-option input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.firmware-picker-radio-option span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 2.45rem;
|
||||
padding: 0.45rem 0.72rem;
|
||||
border: 1px solid var(--picker-border);
|
||||
border-radius: 999px;
|
||||
background: var(--md-default-bg-color);
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.firmware-picker-radio-option input:checked + span {
|
||||
border-color: var(--picker-accent);
|
||||
color: white;
|
||||
background: var(--picker-accent);
|
||||
}
|
||||
|
||||
.firmware-picker-radio-option input:focus-visible + span {
|
||||
outline: 0.16rem solid var(--picker-accent);
|
||||
outline-offset: 0.12rem;
|
||||
}
|
||||
|
||||
.firmware-picker-radio-control:disabled {
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.firmware-picker-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.firmware-picker-form-actions button {
|
||||
min-height: 2.35rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border: 1px solid var(--picker-border);
|
||||
border-radius: 0.35rem;
|
||||
color: inherit;
|
||||
background: var(--md-default-bg-color);
|
||||
font: inherit;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.firmware-picker-form-actions button:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.firmware-picker-status {
|
||||
min-height: 1.5rem;
|
||||
margin: 0.75rem 0;
|
||||
|
||||
+62
-54
@@ -1,8 +1,7 @@
|
||||
# Firmware picker
|
||||
|
||||
Choose the exact hardware first. The remaining menus then show only firmware
|
||||
roles and features that were actually built for that hardware in the current
|
||||
release set.
|
||||
Pick the choices in any order. Every selection narrows all the other controls
|
||||
to firmware combinations that were actually built in the current release set.
|
||||
|
||||
The picker reads public release metadata from GitHub. It does not upload device
|
||||
information. Hardware names, target names, and download links come directly
|
||||
@@ -15,67 +14,76 @@ from the published firmware assets.
|
||||
<p>
|
||||
For a new installation, choose the exact board and role, prefer a
|
||||
<strong>FULL / complete profile</strong> when it is available, and select
|
||||
<strong>USB first install / recovery</strong>. Narrower profiles remain
|
||||
available when their reduced transport or feature set is intentional.
|
||||
<strong>Erase & fresh install (merged .bin)</strong>. Narrower profiles
|
||||
remain available when their reduced transport or feature set is intentional.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="firmware-picker-order-note">
|
||||
Pick in any order. Use <strong>Any</strong> to clear one choice, or clear
|
||||
everything with the button below.
|
||||
</p>
|
||||
|
||||
<form class="firmware-picker-form" data-role="form">
|
||||
<label>
|
||||
1. Hardware
|
||||
<select data-field="hardware" disabled>
|
||||
<div class="firmware-picker-control firmware-picker-select-control">
|
||||
<label for="firmware-picker-hardware">Hardware</label>
|
||||
<select id="firmware-picker-hardware" data-field="hardware" disabled>
|
||||
<option value="">Loading hardware...</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
2. Firmware role
|
||||
<select data-field="role" disabled>
|
||||
<option value="">Choose hardware first</option>
|
||||
</select>
|
||||
</label>
|
||||
<fieldset class="firmware-picker-control firmware-picker-radio-control firmware-picker-wide" data-radio-field="install" disabled>
|
||||
<legend>Install operation</legend>
|
||||
<div class="firmware-picker-radio-options" data-field="install">
|
||||
Loading install choices...
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label>
|
||||
3. Logging / MQTT
|
||||
<select data-field="logging" disabled>
|
||||
<option value="">Choose role first</option>
|
||||
</select>
|
||||
</label>
|
||||
<fieldset class="firmware-picker-control firmware-picker-radio-control" data-radio-field="role" disabled>
|
||||
<legend>Firmware role</legend>
|
||||
<div class="firmware-picker-radio-options" data-field="role">
|
||||
Loading roles...
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label>
|
||||
4. OTA capability
|
||||
<select data-field="ota" disabled>
|
||||
<option value="">Choose earlier options first</option>
|
||||
</select>
|
||||
</label>
|
||||
<fieldset class="firmware-picker-control firmware-picker-radio-control" data-radio-field="logging" disabled>
|
||||
<legend>Logging / MQTT</legend>
|
||||
<div class="firmware-picker-radio-options" data-field="logging">
|
||||
Loading logging choices...
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label>
|
||||
5. Connection / bridge mode
|
||||
<select data-field="mode" disabled>
|
||||
<option value="">Choose earlier options first</option>
|
||||
</select>
|
||||
</label>
|
||||
<fieldset class="firmware-picker-control firmware-picker-radio-control" data-radio-field="ota" disabled>
|
||||
<legend>OTA capability</legend>
|
||||
<div class="firmware-picker-radio-options" data-field="ota">
|
||||
Loading OTA choices...
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label>
|
||||
6. Feature profile
|
||||
<select data-field="feature" disabled>
|
||||
<option value="">Choose earlier options first</option>
|
||||
<div class="firmware-picker-control firmware-picker-select-control">
|
||||
<label for="firmware-picker-mode">Connection / bridge mode</label>
|
||||
<select id="firmware-picker-mode" data-field="mode" disabled>
|
||||
<option value="">Loading modes...</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
7. Hardware/profile variant
|
||||
<select data-field="variant" disabled>
|
||||
<option value="">Choose earlier options first</option>
|
||||
</select>
|
||||
</label>
|
||||
<fieldset class="firmware-picker-control firmware-picker-radio-control" data-radio-field="feature" disabled>
|
||||
<legend>Feature profile</legend>
|
||||
<div class="firmware-picker-radio-options" data-field="feature">
|
||||
Loading profiles...
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label>
|
||||
8. Installation method
|
||||
<select data-field="install" disabled>
|
||||
<option value="">Choose firmware options first</option>
|
||||
<div class="firmware-picker-control firmware-picker-select-control">
|
||||
<label for="firmware-picker-variant">Hardware/profile variant</label>
|
||||
<select id="firmware-picker-variant" data-field="variant" disabled>
|
||||
<option value="">Loading variants...</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="firmware-picker-form-actions firmware-picker-wide">
|
||||
<button type="reset" data-action="clear" disabled>Clear all choices</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="firmware-picker-status" data-role="status" aria-live="polite">
|
||||
@@ -145,11 +153,11 @@ and inactive partitions.
|
||||
|
||||
| File | Use |
|
||||
| --- | --- |
|
||||
| <code>-merged.bin</code> | ESP32 first install, recovery, role migration, or partition-profile change over USB |
|
||||
| Non-merged <code>.bin</code> | Same-board, same-role, same-partition application update |
|
||||
| <code>.zip</code> | Native nRF52 Serial DFU package; it is not an extra archive |
|
||||
| <code>.uf2</code> | UF2 bootloader drag-and-drop installation |
|
||||
| <code>.hex</code> | Full wired programmer or recovery flash |
|
||||
| <code>-merged.bin</code> | Erase/fresh install, recovery, role migration, or partition-profile change on ESP32 over USB |
|
||||
| Non-merged <code>.bin</code> | Update an existing same-board, same-role, same-partition installation |
|
||||
| <code>.zip</code> | Native nRF52 Serial DFU update package; it is not an extra archive |
|
||||
| <code>.uf2</code> | UF2 bootloader drag-and-drop install or update |
|
||||
| <code>.hex</code> | Erase/recovery flash with a supported wired programmer |
|
||||
|
||||
Never send a merged ESP32 image through browser OTA or LoRa OTA. Back up the
|
||||
node configuration and verify every filename suffix before flashing.
|
||||
|
||||
@@ -176,6 +176,55 @@ const matches = catalog.profiles.filter(function (item) {
|
||||
assert.strictEqual(matches.length, 1);
|
||||
assert.strictEqual(matches[0].target, "Station_G2_repeater-full-logging");
|
||||
|
||||
assert.deepStrictEqual(
|
||||
picker.FACET_FIELDS,
|
||||
[
|
||||
"hardware",
|
||||
"role",
|
||||
"logging",
|
||||
"ota",
|
||||
"mode",
|
||||
"feature",
|
||||
"variant",
|
||||
"install",
|
||||
]
|
||||
);
|
||||
assert.strictEqual(
|
||||
picker.INSTALL_LABELS["merged-bin"],
|
||||
"Erase & fresh install (merged .bin)"
|
||||
);
|
||||
assert.strictEqual(
|
||||
picker.INSTALL_LABELS.bin,
|
||||
"Update existing install (.bin)"
|
||||
);
|
||||
assert(picker.profileMatchesFacets(wio, { install: "hex" }));
|
||||
assert(!picker.profileMatchesFacets(wio, { install: "merged-bin" }));
|
||||
assert.deepStrictEqual(
|
||||
picker.facetValues(catalog.profiles, { install: "hex" }, "hardware"),
|
||||
["wio-e5"]
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
picker.facetValues(catalog.profiles, { hardware: "wio-e5" }, "install"),
|
||||
["hex"]
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
picker.facetValues(catalog.profiles, { logging: "wifi" }, "hardware"),
|
||||
["Station_G2"]
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
picker.facetValues(
|
||||
catalog.profiles,
|
||||
{
|
||||
install: "merged-bin",
|
||||
feature: "full",
|
||||
logging: "usb",
|
||||
role: "repeater",
|
||||
},
|
||||
"hardware"
|
||||
),
|
||||
["Station_G2"]
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
picker.uniqueValues(catalog.profiles, "hardware").sort(),
|
||||
["Heltec_t096", "ProMicro", "RAK_4631", "Station_G2", "wio-e5"].sort()
|
||||
|
||||
Reference in new issue
Block a user