Group firmware picker hardware variants

This commit is contained in:
mikecarper committed 2026-08-23 02:03:01 -07:00
1 parent 63ab4a1690
commit b75441c819
3 files changed
+187 -21

No files matched your search

+107 -13
View File
@@ -11,6 +11,7 @@
]);
const FILTER_FIELDS = Object.freeze([
"hardwareFamily",
"hardware",
"role",
"logging",
@@ -308,6 +309,57 @@
};
}
function hardwareFamilyFor(hardware, hardwareNames) {
const value = String(hardware || "");
const lowerValue = value.toLowerCase();
const parents = (hardwareNames || []).filter(function (candidate) {
const parent = String(candidate || "");
if (!parent || parent === value) return false;
const lowerParent = parent.toLowerCase();
return lowerValue.startsWith(lowerParent + "_") ||
lowerValue.startsWith(lowerParent + "-");
}).sort(function (a, b) {
return a.length - b.length || a.localeCompare(b);
});
return parents[0] || value;
}
function humanizeHardwareVariant(hardware, family) {
const value = String(hardware || "");
const parent = String(family || "");
if (!parent || value === parent) return "Base / standard";
const knownLabels = {
"heltec_v4_2_v4_3": "V4.2 / V4.3",
"heltec_v4_3": "V4.3",
"heltec_v4_3_expansionkit_tft": "V4.3 + expansion kit + TFT",
"heltec_v4_3_tft": "V4.3 + TFT",
"heltec_v4_expansionkit": "Expansion kit",
"heltec_v4_expansionkit_tft": "Expansion kit + TFT",
"heltec_v4_r8": "R8",
"heltec_v4_r8_tft": "R8 + TFT",
"heltec_v4_tft": "TFT",
"Station_G2_logging": "RX-boosted radio profile",
"Station_G3_ESP32_logging": "Alternate radio profile",
"wio-e5-mini": "Mini",
};
if (knownLabels[value]) return knownLabels[value];
let suffix = value.slice(parent.length).replace(/^[_-]+/, "");
const suffixLabels = {
logging: "Logging hardware profile",
without_display: "Without display",
sdcard: "SD card",
"096": "0.96-inch display",
"096_rotated": "0.96-inch rotated display",
Lite_non_shell: "Lite (no shell)",
};
if (suffixLabels[suffix]) return suffixLabels[suffix];
suffix = humanizeHardware(suffix).replace(/\bTft\b/gi, "TFT")
.replace(/\bWio\b/gi, "WIO");
return suffix || humanizeHardware(value);
}
function canonicalAsset(files, installKind) {
return (Array.isArray(files) ? files : []).filter(function (file) {
return file.installKind === installKind;
@@ -358,6 +410,16 @@
});
});
const hardwareNames = Array.from(new Set(profiles.map(function (profile) {
return profile.hardware;
})));
profiles.forEach(function (profile) {
profile.hardwareFamily = hardwareFamilyFor(
profile.hardware,
hardwareNames
);
});
return {
releaseSet: releaseSet,
rows: firmwareRows,
@@ -374,8 +436,11 @@
}
function profileMatchesFacets(profile, filters, ignoredField) {
const ignoredFields = new Set(
Array.isArray(ignoredField) ? ignoredField : [ignoredField]
);
return FACET_FIELDS.every(function (field) {
if (field === ignoredField) return true;
if (ignoredFields.has(field)) return true;
const value = filters && filters[field];
if (!value) return true;
if (field === "install") {
@@ -385,9 +450,10 @@
});
}
function facetValues(profiles, filters, field) {
function facetValues(profiles, filters, field, additionallyIgnoredFields) {
const ignoredFields = [field].concat(additionallyIgnoredFields || []);
const compatible = (profiles || []).filter(function (profile) {
return profileMatchesFacets(profile, filters, field);
return profileMatchesFacets(profile, filters, ignoredFields);
});
const values = field === "install"
? compatible.flatMap(function (profile) {
@@ -428,6 +494,7 @@
}
function labelFor(field, value) {
if (field === "hardwareFamily") return humanizeHardware(value);
if (field === "hardware") return humanizeHardware(value);
if (field === "role") return ROLE_LABELS[value] || value;
if (field === "logging") return LOGGING_LABELS[value] || value;
@@ -604,7 +671,8 @@
function setSelectOptions(select, field, values, selected) {
select.replaceChildren();
const placeholders = {
hardware: "Any hardware",
hardwareFamily: "Any hardware",
hardware: "Choose a hardware variant",
mode: "Any connection / mode",
variant: "Any hardware/profile variant",
};
@@ -614,7 +682,10 @@
values.slice().sort(function (a, b) {
return optionSort(field, a, b);
}).forEach(function (value) {
const option = createElement("option", labelFor(field, value));
const label = field === "hardware"
? humanizeHardwareVariant(value, filters.hardwareFamily)
: labelFor(field, value);
const option = createElement("option", label);
option.value = value;
select.appendChild(option);
});
@@ -658,18 +729,35 @@
}
}
function stabilizeSingleChoices() {
function valuesForField(field) {
return facetValues(
catalog.profiles,
filters,
field,
field === "hardwareFamily" ? ["hardware"] : []
);
}
function stabilizeFilters() {
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 (field === "hardware" && !filters.hardwareFamily) {
if (filters.hardware) {
filters.hardware = "";
changed = true;
}
return;
}
const values = valuesForField(field);
if (filters[field] && !values.includes(filters[field])) {
filters[field] = "";
changed = true;
} else if (!filters[field] && values.length === 1) {
} else if (field === "hardware" && filters.hardwareFamily &&
!filters[field] && values.length === 1) {
filters[field] = values[0];
changed = true;
}
@@ -678,13 +766,16 @@
}
function refreshFacets() {
stabilizeSingleChoices();
stabilizeFilters();
FACET_FIELDS.forEach(function (field) {
setControlOptions(
field,
facetValues(catalog.profiles, filters, field)
);
setControlOptions(field, valuesForField(field));
});
const hardwareVariants = valuesForField("hardware");
const hardwareVariantControl = root.querySelector(
'[data-role="hardware-variant-control"]'
);
hardwareVariantControl.hidden = !filters.hardwareFamily ||
hardwareVariants.length <= 1;
render();
}
@@ -778,6 +869,7 @@
const target = event.target;
const field = target.dataset.choiceField || target.dataset.field;
if (!FACET_FIELDS.includes(field)) return;
if (field === "hardwareFamily") filters.hardware = "";
filters[field] = target.value;
refreshFacets();
});
@@ -851,6 +943,8 @@
flattenReleaseAssets: flattenReleaseAssets,
parseFirmwareAsset: parseFirmwareAsset,
parseTargetProfile: parseTargetProfile,
hardwareFamilyFor: hardwareFamilyFor,
humanizeHardwareVariant: humanizeHardwareVariant,
buildCatalog: buildCatalog,
profileMatches: profileMatches,
profileMatchesFacets: profileMatchesFacets,
+16 -8
View File
@@ -26,12 +26,19 @@ from the published firmware assets.
<form class="firmware-picker-form" data-role="form">
<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>
<label for="firmware-picker-hardware-family">Hardware</label>
<select id="firmware-picker-hardware-family" data-field="hardwareFamily" disabled>
<option value="">Loading hardware...</option>
</select>
</div>
<div class="firmware-picker-control firmware-picker-select-control" data-role="hardware-variant-control" hidden>
<label for="firmware-picker-hardware">Hardware variant</label>
<select id="firmware-picker-hardware" data-field="hardware" disabled>
<option value="">Choose hardware first</option>
</select>
</div>
<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">
@@ -75,7 +82,7 @@ from the published firmware assets.
</fieldset>
<div class="firmware-picker-control firmware-picker-select-control">
<label for="firmware-picker-variant">Hardware/profile variant</label>
<label for="firmware-picker-variant">Firmware variant</label>
<select id="firmware-picker-variant" data-field="variant" disabled>
<option value="">Loading variants...</option>
</select>
@@ -175,8 +182,9 @@ and select the hardware-matched HEX, Serial DFU ZIP, or bootloader-update UF2.
## Hardware and variant names
The hardware menu preserves meaningful board suffixes such as display type,
radio chip, PA/FEM layout, pin map, and external-flash variant. Later menus
expose build variants such as FEM on/off, power saving, serial port, or
no-external-sensors when those choices exist. Do not substitute a similarly
named target.
Hardware families with multiple released targets get a second hardware-variant
menu. It separates revisions, display type, expansion kit, radio/PA layout,
pin map, and other physical differences without crowding the first menu. The
firmware-variant menu separately exposes build choices such as FEM on/off,
power saving, serial port, or no-external-sensors. Do not substitute a
similarly named target.
+64
View File
@@ -134,6 +134,7 @@ assert.strictEqual(fullUsbLogging.mode, "usb");
assert.strictEqual(fullUsbLogging.variant, "default");
const standardRepeater = profile("Station_G2_repeater");
assert.strictEqual(standardRepeater.hardwareFamily, "Station_G2");
assert.strictEqual(standardRepeater.role, "repeater");
assert.strictEqual(standardRepeater.logging, "none");
assert.strictEqual(standardRepeater.ota, "none");
@@ -179,6 +180,7 @@ assert.strictEqual(matches[0].target, "Station_G2_repeater-full-logging");
assert.deepStrictEqual(
picker.FACET_FIELDS,
[
"hardwareFamily",
"hardware",
"role",
"logging",
@@ -189,6 +191,68 @@ assert.deepStrictEqual(
"install",
]
);
const hardwareNames = [
"heltec_v4",
"heltec_v4_3",
"heltec_v4_3_tft",
"heltec_v4_r8",
"wio-e5",
"wio-e5-mini",
];
assert.strictEqual(
picker.hardwareFamilyFor("heltec_v4_3_tft", hardwareNames),
"heltec_v4"
);
assert.strictEqual(
picker.hardwareFamilyFor("wio-e5-mini", hardwareNames),
"wio-e5"
);
assert.strictEqual(
picker.humanizeHardwareVariant("heltec_v4", "heltec_v4"),
"Base / standard"
);
assert.strictEqual(
picker.humanizeHardwareVariant("heltec_v4_3_tft", "heltec_v4"),
"V4.3 + TFT"
);
assert.strictEqual(
picker.humanizeHardwareVariant("Station_G2_logging", "Station_G2"),
"RX-boosted radio profile"
);
const groupedHardwareProfiles = [
{
hardwareFamily: "heltec_v4",
hardware: "heltec_v4",
installKinds: ["bin"],
},
{
hardwareFamily: "heltec_v4",
hardware: "heltec_v4_3_tft",
installKinds: ["bin"],
},
{
hardwareFamily: "wio-e5",
hardware: "wio-e5",
installKinds: ["hex"],
},
];
assert.deepStrictEqual(
picker.facetValues(
groupedHardwareProfiles,
{ hardwareFamily: "heltec_v4", hardware: "heltec_v4_3_tft" },
"hardwareFamily",
["hardware"]
).sort(),
["heltec_v4", "wio-e5"]
);
assert.deepStrictEqual(
picker.facetValues(
groupedHardwareProfiles,
{ hardwareFamily: "heltec_v4" },
"hardware"
).sort(),
["heltec_v4", "heltec_v4_3_tft"]
);
assert.strictEqual(
picker.INSTALL_LABELS["merged-bin"],
"Erase & fresh install (merged .bin)"